mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 23:56:07 +00:00
Dreaming: surface grounded scene lane (#63395)
Merged via squash.
Prepared head SHA: 0c7f586f32
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
@@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
||||
- QA/lab: add character-vibes evaluation reports with model selection and parallel runs so live QA can compare candidate behavior faster.
|
||||
- Plugins/provider-auth: let provider manifests declare `providerAuthAliases` so provider variants can share env vars, auth profiles, config-backed auth, and API-key onboarding choices without core-specific wiring.
|
||||
- iOS: pin release versioning to an explicit CalVer in `apps/ios/version.json`, keep TestFlight iteration on the same short version until maintainers intentionally promote the next gateway version, and add the documented `pnpm ios:version:pin -- --from-gateway` workflow for release trains. (#63001) Thanks @ngutman.
|
||||
- Control UI/dreaming: surface grounded historical replay as its own Scene lane with grounded-led promotion hints and a safe clear-grounded action for staged backfill-only signals. (#63395) Thanks @mbelinky.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -88,7 +88,12 @@ There is also a grounded historical backfill lane for review and recovery work:
|
||||
- `memory rem-backfill --path ... --stage-short-term` stages grounded durable candidates into the same short-term evidence store the normal deep phase already uses.
|
||||
- `memory rem-backfill --rollback` and `--rollback-short-term` remove those staged backfill artifacts without touching ordinary diary entries or live short-term recall.
|
||||
|
||||
The Control UI exposes the same diary backfill/reset flow so you can inspect results in the Dreams scene before deciding whether the grounded candidates deserve promotion.
|
||||
The Control UI exposes the same diary backfill/reset flow so you can inspect
|
||||
results in the Dreams scene before deciding whether the grounded candidates
|
||||
deserve promotion. The Scene also shows a distinct grounded lane so you can see
|
||||
which staged short-term entries came from historical replay, which promoted
|
||||
items were grounded-led, and clear only grounded-only staged entries without
|
||||
touching ordinary live short-term state.
|
||||
|
||||
## Deep ranking signals
|
||||
|
||||
@@ -216,8 +221,9 @@ When enabled, the Gateway **Dreams** tab shows:
|
||||
|
||||
- current dreaming enabled state
|
||||
- phase-level status and managed-sweep presence
|
||||
- short-term, long-term, and promoted-today counts
|
||||
- short-term, grounded, signal, and promoted-today counts
|
||||
- next scheduled run timing
|
||||
- a distinct grounded Scene lane for staged historical replay entries
|
||||
- an expandable Dream Diary reader backed by `doctor.memory.dreamDiary`
|
||||
|
||||
## Related
|
||||
|
||||
@@ -95,9 +95,10 @@ cat ~/.openclaw/openclaw.json
|
||||
|
||||
## Dreams UI backfill and reset
|
||||
|
||||
The Control UI Dreams scene includes **Backfill** and **Reset** actions for the
|
||||
grounded diary workflow. These actions use gateway doctor-style RPC methods, but
|
||||
they are **not** part of `openclaw doctor` CLI repair/migration.
|
||||
The Control UI Dreams scene includes **Backfill**, **Reset**, and **Clear Grounded**
|
||||
actions for the grounded dreaming workflow. These actions use gateway
|
||||
doctor-style RPC methods, but they are **not** part of `openclaw doctor` CLI
|
||||
repair/migration.
|
||||
|
||||
What they do:
|
||||
|
||||
@@ -105,13 +106,16 @@ What they do:
|
||||
workspace, runs the grounded REM diary pass, and writes reversible backfill
|
||||
entries into `DREAMS.md`.
|
||||
- **Reset** removes only those marked backfill diary entries from `DREAMS.md`.
|
||||
- **Clear Grounded** removes only staged grounded-only short-term entries that
|
||||
came from historical replay and have not accumulated live recall or daily
|
||||
support yet.
|
||||
|
||||
What they do **not** do by themselves:
|
||||
|
||||
- they do not edit `MEMORY.md`
|
||||
- they do not run full doctor migrations
|
||||
- they do not automatically stage grounded candidates into the live short-term
|
||||
promotion store
|
||||
promotion store unless you explicitly run the staged CLI path first
|
||||
|
||||
If you want grounded historical replay to influence the normal deep promotion
|
||||
lane, use the CLI flow instead:
|
||||
|
||||
@@ -17,6 +17,7 @@ export { checkQmdBinaryAvailability } from "openclaw/plugin-sdk/memory-core-host
|
||||
export { hasConfiguredMemorySecretInput } from "openclaw/plugin-sdk/memory-core-host-secret";
|
||||
export {
|
||||
auditShortTermPromotionArtifacts,
|
||||
removeGroundedShortTermCandidates,
|
||||
repairShortTermPromotionArtifacts,
|
||||
} from "./src/short-term-promotion.js";
|
||||
export type { BuiltinMemoryEmbeddingProviderDoctorMetadata } from "./src/memory/provider-adapters.js";
|
||||
|
||||
@@ -138,6 +138,7 @@ const METHOD_SCOPE_GROUPS: Record<OperatorScope, readonly string[]> = {
|
||||
"node.pending.enqueue",
|
||||
"doctor.memory.backfillDreamDiary",
|
||||
"doctor.memory.resetDreamDiary",
|
||||
"doctor.memory.resetGroundedShortTerm",
|
||||
],
|
||||
[ADMIN_SCOPE]: [
|
||||
"channels.logout",
|
||||
|
||||
@@ -7,6 +7,7 @@ const BASE_METHODS = [
|
||||
"doctor.memory.dreamDiary",
|
||||
"doctor.memory.backfillDreamDiary",
|
||||
"doctor.memory.resetDreamDiary",
|
||||
"doctor.memory.resetGroundedShortTerm",
|
||||
"logs.tail",
|
||||
"channels.status",
|
||||
"channels.logout",
|
||||
|
||||
@@ -3,3 +3,4 @@ export {
|
||||
previewGroundedRemMarkdown,
|
||||
writeBackfillDiaryEntries,
|
||||
} from "../../../extensions/memory-core/api.js";
|
||||
export { removeGroundedShortTermCandidates } from "../../../extensions/memory-core/runtime-api.js";
|
||||
|
||||
@@ -18,6 +18,7 @@ const getMemorySearchManager = vi.hoisted(() => vi.fn());
|
||||
const previewGroundedRemMarkdown = vi.hoisted(() => vi.fn());
|
||||
const writeBackfillDiaryEntries = vi.hoisted(() => vi.fn());
|
||||
const removeBackfillDiaryEntries = vi.hoisted(() => vi.fn());
|
||||
const removeGroundedShortTermCandidates = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../../config/config.js", () => ({
|
||||
loadConfig,
|
||||
@@ -40,6 +41,7 @@ vi.mock("./doctor.memory-core-runtime.js", () => ({
|
||||
previewGroundedRemMarkdown,
|
||||
writeBackfillDiaryEntries,
|
||||
removeBackfillDiaryEntries,
|
||||
removeGroundedShortTermCandidates,
|
||||
}));
|
||||
|
||||
import { doctorHandlers } from "./doctor.js";
|
||||
@@ -100,6 +102,17 @@ const invokeDoctorMemoryResetDreamDiary = async (respond: ReturnType<typeof vi.f
|
||||
});
|
||||
};
|
||||
|
||||
const invokeDoctorMemoryResetGroundedShortTerm = async (respond: ReturnType<typeof vi.fn>) => {
|
||||
await doctorHandlers["doctor.memory.resetGroundedShortTerm"]({
|
||||
req: {} as never,
|
||||
params: {} as never,
|
||||
respond: respond as never,
|
||||
context: {} as never,
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
});
|
||||
};
|
||||
|
||||
const expectEmbeddingErrorResponse = (respond: ReturnType<typeof vi.fn>, error: string) => {
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
@@ -121,6 +134,10 @@ describe("doctor.memory.status", () => {
|
||||
resolveAgentWorkspaceDir.mockReset().mockReturnValue("/tmp/openclaw");
|
||||
resolveMemorySearchConfig.mockReset().mockReturnValue({ enabled: true });
|
||||
getMemorySearchManager.mockReset();
|
||||
previewGroundedRemMarkdown.mockReset();
|
||||
writeBackfillDiaryEntries.mockReset();
|
||||
removeBackfillDiaryEntries.mockReset();
|
||||
removeGroundedShortTermCandidates.mockReset();
|
||||
});
|
||||
|
||||
it("returns gateway embedding probe status for the default agent", async () => {
|
||||
@@ -701,6 +718,32 @@ describe("doctor.memory.status", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("doctor.memory dream actions", () => {
|
||||
it("clears grounded-only staged short-term entries without touching the diary", async () => {
|
||||
resolveAgentWorkspaceDir.mockReturnValue("/tmp/openclaw");
|
||||
removeGroundedShortTermCandidates.mockResolvedValue({
|
||||
removed: 3,
|
||||
storePath: "/tmp/openclaw/memory/.dreams/short-term-recall.json",
|
||||
});
|
||||
const respond = vi.fn();
|
||||
|
||||
await invokeDoctorMemoryResetGroundedShortTerm(respond);
|
||||
|
||||
expect(removeGroundedShortTermCandidates).toHaveBeenCalledWith({
|
||||
workspaceDir: "/tmp/openclaw",
|
||||
});
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
{
|
||||
agentId: "main",
|
||||
action: "resetGroundedShortTerm",
|
||||
removedShortTermEntries: 3,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("doctor.memory.dreamDiary", () => {
|
||||
beforeEach(() => {
|
||||
loadConfig.mockClear();
|
||||
|
||||
@@ -16,6 +16,7 @@ import { getActiveMemorySearchManager } from "../../plugins/memory-runtime.js";
|
||||
import { formatError } from "../server-utils.js";
|
||||
import {
|
||||
removeBackfillDiaryEntries,
|
||||
removeGroundedShortTermCandidates,
|
||||
previewGroundedRemMarkdown,
|
||||
writeBackfillDiaryEntries,
|
||||
} from "./doctor.memory-core-runtime.js";
|
||||
@@ -122,15 +123,16 @@ export type DoctorMemoryDreamDiaryPayload = {
|
||||
updatedAtMs?: number;
|
||||
};
|
||||
|
||||
export type DoctorMemoryDreamDiaryActionPayload = {
|
||||
export type DoctorMemoryDreamActionPayload = {
|
||||
agentId: string;
|
||||
path: string;
|
||||
action: "backfill" | "reset";
|
||||
found: boolean;
|
||||
action: "backfill" | "reset" | "resetGroundedShortTerm";
|
||||
path?: string;
|
||||
found?: boolean;
|
||||
scannedFiles?: number;
|
||||
written?: number;
|
||||
replaced?: number;
|
||||
removedEntries?: number;
|
||||
removedShortTermEntries?: number;
|
||||
};
|
||||
|
||||
function extractIsoDayFromPath(filePath: string): string | null {
|
||||
@@ -880,7 +882,7 @@ export const doctorHandlers: GatewayRequestHandlers = {
|
||||
const sourceFiles = await listWorkspaceDailyFiles(memoryDir);
|
||||
if (sourceFiles.length === 0) {
|
||||
const dreamDiary = await readDreamDiary(workspaceDir);
|
||||
const payload: DoctorMemoryDreamDiaryActionPayload = {
|
||||
const payload: DoctorMemoryDreamActionPayload = {
|
||||
agentId,
|
||||
path: dreamDiary.path,
|
||||
action: "backfill",
|
||||
@@ -919,7 +921,7 @@ export const doctorHandlers: GatewayRequestHandlers = {
|
||||
timezone: remConfig.timezone,
|
||||
});
|
||||
const dreamDiary = await readDreamDiary(workspaceDir);
|
||||
const payload: DoctorMemoryDreamDiaryActionPayload = {
|
||||
const payload: DoctorMemoryDreamActionPayload = {
|
||||
agentId,
|
||||
path: dreamDiary.path,
|
||||
action: "backfill",
|
||||
@@ -936,7 +938,7 @@ export const doctorHandlers: GatewayRequestHandlers = {
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const removed = await removeBackfillDiaryEntries({ workspaceDir });
|
||||
const dreamDiary = await readDreamDiary(workspaceDir);
|
||||
const payload: DoctorMemoryDreamDiaryActionPayload = {
|
||||
const payload: DoctorMemoryDreamActionPayload = {
|
||||
agentId,
|
||||
path: dreamDiary.path,
|
||||
action: "reset",
|
||||
@@ -945,4 +947,16 @@ export const doctorHandlers: GatewayRequestHandlers = {
|
||||
};
|
||||
respond(true, payload, undefined);
|
||||
},
|
||||
"doctor.memory.resetGroundedShortTerm": async ({ respond }) => {
|
||||
const cfg = loadConfig();
|
||||
const agentId = resolveDefaultAgentId(cfg);
|
||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
const removed = await removeGroundedShortTermCandidates({ workspaceDir });
|
||||
const payload: DoctorMemoryDreamActionPayload = {
|
||||
agentId,
|
||||
action: "resetGroundedShortTerm",
|
||||
removedShortTermEntries: removed.removed,
|
||||
};
|
||||
respond(true, payload, undefined);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -293,19 +293,24 @@ export const en: TranslationMap = {
|
||||
scene: {
|
||||
backfill: "Backfill",
|
||||
reset: "Reset",
|
||||
clearGrounded: "Clear Grounded",
|
||||
working: "Working…",
|
||||
},
|
||||
stats: {
|
||||
shortTerm: "Short-term",
|
||||
grounded: "Grounded",
|
||||
signals: "Signals",
|
||||
promoted: "Promoted",
|
||||
phaseHits: "Phase Hits",
|
||||
},
|
||||
trace: {
|
||||
shortTerm: "Short-term",
|
||||
grounded: "Grounded",
|
||||
signals: "Signals",
|
||||
promoted: "Promoted",
|
||||
groundedLed: "grounded-led",
|
||||
emptyShortTerm: "No active short-term items.",
|
||||
emptyGrounded: "No staged grounded items.",
|
||||
emptySignals: "No active signals.",
|
||||
emptyPromoted: "Nothing promoted yet today.",
|
||||
},
|
||||
|
||||
@@ -69,6 +69,7 @@ import {
|
||||
backfillDreamDiary,
|
||||
loadDreamDiary,
|
||||
loadDreamingStatus,
|
||||
resetGroundedShortTerm,
|
||||
resetDreamDiary,
|
||||
resolveConfiguredDreaming,
|
||||
updateDreamingEnabled,
|
||||
@@ -2118,6 +2119,7 @@ export function renderApp(state: AppViewState) {
|
||||
? renderDreaming({
|
||||
active: dreamingOn,
|
||||
shortTermCount: state.dreamingStatus?.shortTermCount ?? 0,
|
||||
groundedSignalCount: state.dreamingStatus?.groundedSignalCount ?? 0,
|
||||
totalSignalCount: state.dreamingStatus?.totalSignalCount ?? 0,
|
||||
promotedCount: state.dreamingStatus?.promotedToday ?? 0,
|
||||
phaseSignalCount: state.dreamingStatus?.phaseSignalCount ?? 0,
|
||||
@@ -2139,6 +2141,7 @@ export function renderApp(state: AppViewState) {
|
||||
onRefreshDiary: () => loadDreamDiary(state),
|
||||
onBackfillDiary: () => backfillDreamDiary(state),
|
||||
onResetDiary: () => resetDreamDiary(state),
|
||||
onResetGroundedShortTerm: () => resetGroundedShortTerm(state),
|
||||
onToggleEnabled: applyDreamingEnabled,
|
||||
onRequestUpdate: requestHostUpdate,
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
backfillDreamDiary,
|
||||
loadDreamDiary,
|
||||
loadDreamingStatus,
|
||||
resetGroundedShortTerm,
|
||||
resetDreamDiary,
|
||||
resolveConfiguredDreaming,
|
||||
updateDreamingEnabled,
|
||||
@@ -501,4 +502,27 @@ describe("dreaming controller", () => {
|
||||
expect(state.dreamDiaryContent).toBeNull();
|
||||
expect(state.dreamDiaryActionLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("clears grounded staged entries and reloads only dreaming status", async () => {
|
||||
const { state, request } = createState();
|
||||
state.dreamDiaryContent = "keep existing diary";
|
||||
request.mockImplementation(async (method: string) => {
|
||||
if (method === "doctor.memory.resetGroundedShortTerm") {
|
||||
return { action: "resetGroundedShortTerm", removedShortTermEntries: 2 };
|
||||
}
|
||||
if (method === "doctor.memory.status") {
|
||||
return { dreaming: null };
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const ok = await resetGroundedShortTerm(state);
|
||||
|
||||
expect(ok).toBe(true);
|
||||
expect(request).toHaveBeenCalledWith("doctor.memory.resetGroundedShortTerm", {});
|
||||
expect(request).toHaveBeenCalledWith("doctor.memory.status", {});
|
||||
expect(request).not.toHaveBeenCalledWith("doctor.memory.dreamDiary", {});
|
||||
expect(state.dreamDiaryContent).toBe("keep existing diary");
|
||||
expect(state.dreamDiaryActionLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,11 +89,12 @@ type DoctorMemoryDreamDiaryPayload = {
|
||||
content?: unknown;
|
||||
};
|
||||
|
||||
type DoctorMemoryDreamDiaryActionPayload = {
|
||||
type DoctorMemoryDreamActionPayload = {
|
||||
action?: unknown;
|
||||
removedEntries?: unknown;
|
||||
written?: unknown;
|
||||
replaced?: unknown;
|
||||
removedShortTermEntries?: unknown;
|
||||
};
|
||||
|
||||
export type DreamingState = {
|
||||
@@ -344,7 +345,13 @@ export async function loadDreamDiary(state: DreamingState): Promise<void> {
|
||||
|
||||
async function runDreamDiaryAction(
|
||||
state: DreamingState,
|
||||
method: "doctor.memory.backfillDreamDiary" | "doctor.memory.resetDreamDiary",
|
||||
method:
|
||||
| "doctor.memory.backfillDreamDiary"
|
||||
| "doctor.memory.resetDreamDiary"
|
||||
| "doctor.memory.resetGroundedShortTerm",
|
||||
options?: {
|
||||
reloadDiary?: boolean;
|
||||
},
|
||||
): Promise<boolean> {
|
||||
if (!state.client || !state.connected || state.dreamDiaryActionLoading) {
|
||||
return false;
|
||||
@@ -353,8 +360,10 @@ async function runDreamDiaryAction(
|
||||
state.dreamingStatusError = null;
|
||||
state.dreamDiaryError = null;
|
||||
try {
|
||||
await state.client.request<DoctorMemoryDreamDiaryActionPayload>(method, {});
|
||||
await loadDreamDiary(state);
|
||||
await state.client.request<DoctorMemoryDreamActionPayload>(method, {});
|
||||
if (options?.reloadDiary !== false) {
|
||||
await loadDreamDiary(state);
|
||||
}
|
||||
await loadDreamingStatus(state);
|
||||
return true;
|
||||
} catch (err) {
|
||||
@@ -375,6 +384,12 @@ export async function resetDreamDiary(state: DreamingState): Promise<boolean> {
|
||||
return runDreamDiaryAction(state, "doctor.memory.resetDreamDiary");
|
||||
}
|
||||
|
||||
export async function resetGroundedShortTerm(state: DreamingState): Promise<boolean> {
|
||||
return runDreamDiaryAction(state, "doctor.memory.resetGroundedShortTerm", {
|
||||
reloadDiary: false,
|
||||
});
|
||||
}
|
||||
|
||||
async function writeDreamingPatch(
|
||||
state: DreamingState,
|
||||
patch: Record<string, unknown>,
|
||||
|
||||
@@ -8,6 +8,7 @@ function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
|
||||
return {
|
||||
active: true,
|
||||
shortTermCount: 47,
|
||||
groundedSignalCount: 9,
|
||||
totalSignalCount: 182,
|
||||
promotedCount: 12,
|
||||
phaseSignalCount: 29,
|
||||
@@ -52,8 +53,8 @@ function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
|
||||
snippet: "Use the Happy Together calendar for flights.",
|
||||
recallCount: 3,
|
||||
dailyCount: 2,
|
||||
groundedCount: 0,
|
||||
totalSignalCount: 5,
|
||||
groundedCount: 4,
|
||||
totalSignalCount: 9,
|
||||
lightHits: 0,
|
||||
remHits: 0,
|
||||
phaseHitCount: 0,
|
||||
@@ -76,6 +77,7 @@ function buildProps(overrides?: Partial<DreamingProps>): DreamingProps {
|
||||
onRefreshDiary: () => {},
|
||||
onBackfillDiary: () => {},
|
||||
onResetDiary: () => {},
|
||||
onResetGroundedShortTerm: () => {},
|
||||
onToggleEnabled: () => {},
|
||||
...overrides,
|
||||
};
|
||||
@@ -114,36 +116,44 @@ describe("dreaming view", () => {
|
||||
it("displays memory stats", () => {
|
||||
const container = renderInto(buildProps());
|
||||
const values = container.querySelectorAll(".dreams__stat-value");
|
||||
expect(values.length).toBe(3);
|
||||
expect(values.length).toBe(4);
|
||||
expect(values[0]?.textContent).toBe("47");
|
||||
expect(values[1]?.textContent).toBe("182");
|
||||
expect(values[2]?.textContent).toBe("12");
|
||||
expect(values[1]?.textContent).toBe("9");
|
||||
expect(values[2]?.textContent).toBe("182");
|
||||
expect(values[3]?.textContent).toBe("12");
|
||||
});
|
||||
|
||||
it("renders short-term, signals, and promoted detail sections", () => {
|
||||
it("renders short-term, grounded, signals, and promoted detail sections", () => {
|
||||
const container = renderInto(buildProps());
|
||||
const titles = [...container.querySelectorAll(".dreams__trace-title")].map((node) =>
|
||||
node.textContent?.trim(),
|
||||
);
|
||||
expect(titles).toEqual(["Short-term", "Signals", "Promoted"]);
|
||||
expect(titles).toEqual(["Short-term", "Grounded", "Signals", "Promoted"]);
|
||||
expect(
|
||||
container.querySelector('[data-kind="shortTerm"] .dreams__trace-snippet')?.textContent,
|
||||
).toContain("Emma prefers shorter");
|
||||
expect(
|
||||
container.querySelector('[data-kind="grounded"] .dreams__trace-meta')?.textContent,
|
||||
).toContain("1 grounded");
|
||||
expect(
|
||||
container.querySelector('[data-kind="signals"] .dreams__trace-meta')?.textContent,
|
||||
).toContain("3 signals");
|
||||
expect(
|
||||
container.querySelector('[data-kind="promoted"] .dreams__trace-source')?.textContent,
|
||||
).toContain("memory/2026-04-04.md:4-5");
|
||||
expect(
|
||||
container.querySelector('[data-kind="promoted"] .dreams__trace-meta')?.textContent,
|
||||
).toContain("grounded-led");
|
||||
});
|
||||
|
||||
it("renders scene backfill and reset controls", () => {
|
||||
it("renders scene backfill, reset, and clear grounded controls", () => {
|
||||
const container = renderInto(buildProps());
|
||||
const buttons = [...container.querySelectorAll("button")].map((node) =>
|
||||
node.textContent?.trim(),
|
||||
);
|
||||
expect(buttons).toContain("Backfill");
|
||||
expect(buttons).toContain("Reset");
|
||||
expect(buttons).toContain("Clear Grounded");
|
||||
});
|
||||
|
||||
it("shows dream bubble when active", () => {
|
||||
|
||||
@@ -258,6 +258,7 @@ function renderDiaryNavigator(
|
||||
export type DreamingProps = {
|
||||
active: boolean;
|
||||
shortTermCount: number;
|
||||
groundedSignalCount: number;
|
||||
totalSignalCount: number;
|
||||
promotedCount: number;
|
||||
phaseSignalCount: number;
|
||||
@@ -324,6 +325,7 @@ export type DreamingProps = {
|
||||
onRefreshDiary: () => void;
|
||||
onBackfillDiary: () => void;
|
||||
onResetDiary: () => void;
|
||||
onResetGroundedShortTerm: () => void;
|
||||
onToggleEnabled: (enabled: boolean) => void;
|
||||
onRequestUpdate?: () => void;
|
||||
};
|
||||
@@ -473,6 +475,7 @@ export function renderDreaming(props: DreamingProps) {
|
||||
// ── Scene renderer ────────────────────────────────────────────────────
|
||||
|
||||
function renderScene(props: DreamingProps, idle: boolean, dreamText: string) {
|
||||
const groundedEntries = props.shortTermEntries.filter((entry) => entry.groundedCount > 0);
|
||||
return html`
|
||||
<section class="dreams ${idle ? "dreams--idle" : ""}">
|
||||
${STARS.map(
|
||||
@@ -548,6 +551,13 @@ function renderScene(props: DreamingProps, idle: boolean, dreamText: string) {
|
||||
>
|
||||
${t("dreaming.scene.reset")}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn--subtle btn--sm"
|
||||
?disabled=${props.modeSaving || props.dreamDiaryActionLoading}
|
||||
@click=${() => props.onResetGroundedShortTerm()}
|
||||
>
|
||||
${t("dreaming.scene.clearGrounded")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dreams__stats">
|
||||
@@ -558,6 +568,13 @@ function renderScene(props: DreamingProps, idle: boolean, dreamText: string) {
|
||||
<span class="dreams__stat-label">${t("dreaming.stats.shortTerm")}</span>
|
||||
</div>
|
||||
<div class="dreams__stat-divider"></div>
|
||||
<div class="dreams__stat">
|
||||
<span class="dreams__stat-value" style="color: var(--accent-muted);"
|
||||
>${props.groundedSignalCount}</span
|
||||
>
|
||||
<span class="dreams__stat-label">${t("dreaming.stats.grounded")}</span>
|
||||
</div>
|
||||
<div class="dreams__stat-divider"></div>
|
||||
<div class="dreams__stat">
|
||||
<span class="dreams__stat-value" style="color: var(--accent);"
|
||||
>${props.totalSignalCount}</span
|
||||
@@ -591,6 +608,21 @@ function renderScene(props: DreamingProps, idle: boolean, dreamText: string) {
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
})}
|
||||
${renderTraceSection("grounded", groundedEntries, {
|
||||
count: groundedEntries.length,
|
||||
emptyKey: "dreaming.trace.emptyGrounded",
|
||||
meta: (entry) =>
|
||||
[
|
||||
`${entry.groundedCount} grounded`,
|
||||
entry.recallCount > 0
|
||||
? `${entry.recallCount} recall${entry.recallCount === 1 ? "" : "s"}`
|
||||
: null,
|
||||
entry.dailyCount > 0 ? `${entry.dailyCount} daily` : null,
|
||||
isGroundedLed(entry) ? t("dreaming.trace.groundedLed") : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · "),
|
||||
})}
|
||||
${renderTraceSection("signals", props.signalEntries, {
|
||||
count: props.totalSignalCount,
|
||||
emptyKey: "dreaming.trace.emptySignals",
|
||||
@@ -610,6 +642,8 @@ function renderScene(props: DreamingProps, idle: boolean, dreamText: string) {
|
||||
meta: (entry) =>
|
||||
[
|
||||
entry.promotedAt ? formatCompactDateTime(entry.promotedAt) : null,
|
||||
entry.groundedCount > 0 ? `${entry.groundedCount} grounded` : null,
|
||||
isGroundedLed(entry) ? t("dreaming.trace.groundedLed") : null,
|
||||
entry.totalSignalCount > 0
|
||||
? `${entry.totalSignalCount} signal${entry.totalSignalCount === 1 ? "" : "s"} before promote`
|
||||
: null,
|
||||
@@ -643,8 +677,21 @@ function formatCompactDateTime(value: string): string {
|
||||
});
|
||||
}
|
||||
|
||||
function isGroundedLed(
|
||||
entry: Pick<
|
||||
DreamingProps["shortTermEntries"][number],
|
||||
"groundedCount" | "recallCount" | "dailyCount"
|
||||
>,
|
||||
): boolean {
|
||||
return (
|
||||
entry.groundedCount > 0 &&
|
||||
entry.groundedCount >= entry.recallCount &&
|
||||
entry.groundedCount >= entry.dailyCount
|
||||
);
|
||||
}
|
||||
|
||||
function renderTraceSection(
|
||||
kind: "shortTerm" | "signals" | "promoted",
|
||||
kind: "shortTerm" | "grounded" | "signals" | "promoted",
|
||||
entries: DreamingProps["shortTermEntries"],
|
||||
options: {
|
||||
count: number;
|
||||
|
||||
Reference in New Issue
Block a user