diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cad2f5b370..68d504c6193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Memory/active-memory+dreaming: keep active-memory recall runs on the strongest resolved channel, consume managed dreaming heartbeat events exactly once, stop dreaming from re-ingesting its own narrative transcripts, and add explicit repair/dedupe recovery flows in CLI, doctor, and the Dreams UI. - Matrix/mentions: keep room mention gating strict while accepting visible `@displayName` Matrix URI labels, so `requireMention` works for non-OpenClaw Matrix clients again. (#64796) Thanks @hclsys. - Doctor: warn when on-disk agent directories still exist under `~/.openclaw/agents//agent` but the matching `agents.list[]` entries are missing from config. (#65113) Thanks @neeravmakwana. - Telegram: route approval button callback queries onto a separate sequentializer lane so plugin approval clicks can resolve immediately instead of deadlocking behind the blocked agent turn. (#64979) Thanks @nk3750. diff --git a/extensions/active-memory/index.ts b/extensions/active-memory/index.ts index 075c4719f4e..36c8b0b0a63 100644 --- a/extensions/active-memory/index.ts +++ b/extensions/active-memory/index.ts @@ -1033,7 +1033,10 @@ function buildPluginDebugLine(params: { if (fallback) { debugParts.push(`fallback=${fallback}`); } - if (typeof params.searchDebug?.searchMs === "number" && Number.isFinite(params.searchDebug.searchMs)) { + if ( + typeof params.searchDebug?.searchMs === "number" && + Number.isFinite(params.searchDebug.searchMs) + ) { debugParts.push(`searchMs=${Math.max(0, Math.round(params.searchDebug.searchMs))}`); } if (typeof params.searchDebug?.hits === "number" && Number.isFinite(params.searchDebug.hits)) { @@ -1220,7 +1223,9 @@ function normalizeSearchDebug(value: unknown): ActiveMemorySearchDebug | undefin : undefined; } -function readActiveMemorySearchDebugFromRunResult(result: unknown): ActiveMemorySearchDebug | undefined { +function readActiveMemorySearchDebugFromRunResult( + result: unknown, +): ActiveMemorySearchDebug | undefined { const record = asRecord(result); const meta = asRecord(record?.meta); return ( diff --git a/extensions/discord/src/monitor/native-command.options.test.ts b/extensions/discord/src/monitor/native-command.options.test.ts index c15e6aeb6d6..5a035d9b67b 100644 --- a/extensions/discord/src/monitor/native-command.options.test.ts +++ b/extensions/discord/src/monitor/native-command.options.test.ts @@ -113,11 +113,11 @@ function requireAutocomplete(option: CommandOption, errorMessage: string) { if (typeof autocomplete !== "function") { throw new Error(errorMessage); } - return autocomplete; + return autocomplete as (interaction: unknown) => Promise; } async function runAutocomplete( - autocomplete: (interaction: never) => Promise, + autocomplete: (interaction: unknown) => Promise, params: { userId: string; username?: string; diff --git a/extensions/memory-core/api.ts b/extensions/memory-core/api.ts index da2b585d0f0..afcd41faff9 100644 --- a/extensions/memory-core/api.ts +++ b/extensions/memory-core/api.ts @@ -4,5 +4,9 @@ export type { MemoryProviderStatus, MemorySyncProgressUpdate, } from "openclaw/plugin-sdk/memory-core-host-engine-storage"; -export { removeBackfillDiaryEntries, writeBackfillDiaryEntries } from "./src/dreaming-narrative.js"; +export { + dedupeDreamDiaryEntries, + removeBackfillDiaryEntries, + writeBackfillDiaryEntries, +} from "./src/dreaming-narrative.js"; export { previewGroundedRemMarkdown } from "./src/rem-evidence.js"; diff --git a/extensions/memory-core/runtime-api.ts b/extensions/memory-core/runtime-api.ts index 59fb9f76895..4d5e4d8b74a 100644 --- a/extensions/memory-core/runtime-api.ts +++ b/extensions/memory-core/runtime-api.ts @@ -15,12 +15,17 @@ export { } from "openclaw/plugin-sdk/memory-core-host-status"; export { checkQmdBinaryAvailability } from "openclaw/plugin-sdk/memory-core-host-engine-qmd"; export { hasConfiguredMemorySecretInput } from "openclaw/plugin-sdk/memory-core-host-secret"; +export { auditDreamingArtifacts, repairDreamingArtifacts } from "./src/dreaming-repair.js"; export { auditShortTermPromotionArtifacts, removeGroundedShortTermCandidates, repairShortTermPromotionArtifacts, } from "./src/short-term-promotion.js"; export type { BuiltinMemoryEmbeddingProviderDoctorMetadata } from "./src/memory/provider-adapters.js"; +export type { + DreamingArtifactsAuditSummary, + RepairDreamingArtifactsResult, +} from "./src/dreaming-repair.js"; export type { RepairShortTermPromotionArtifactsResult, ShortTermAuditSummary, diff --git a/extensions/memory-core/src/cli.runtime.ts b/extensions/memory-core/src/cli.runtime.ts index 0f7e4031e80..ee9947fef5f 100644 --- a/extensions/memory-core/src/cli.runtime.ts +++ b/extensions/memory-core/src/cli.runtime.ts @@ -37,6 +37,12 @@ import type { } from "./cli.types.js"; import { removeBackfillDiaryEntries, writeBackfillDiaryEntries } from "./dreaming-narrative.js"; import { previewRemDreaming, seedHistoricalDailyMemorySignals } from "./dreaming-phases.js"; +import { + auditDreamingArtifacts, + repairDreamingArtifacts, + type DreamingArtifactsAuditSummary, + type RepairDreamingArtifactsResult, +} from "./dreaming-repair.js"; import { asRecord } from "./dreaming-shared.js"; import { resolveShortTermPromotionDreamingConfig } from "./dreaming.js"; import { previewGroundedRemMarkdown } from "./rem-evidence.js"; @@ -249,6 +255,35 @@ function formatRepairSummary(repair: RepairShortTermPromotionArtifactsResult): s return actions.length > 0 ? actions.join(" · ") : "no changes"; } +function formatDreamingAuditSummary(audit: DreamingArtifactsAuditSummary): string { + const bits = [ + audit.dreamsPath ? "diary present" : "diary absent", + `${audit.sessionCorpusFileCount} corpus files`, + audit.sessionIngestionExists ? "ingestion state present" : "ingestion state absent", + audit.suspiciousSessionCorpusLineCount > 0 + ? `${audit.suspiciousSessionCorpusLineCount} suspicious lines` + : null, + ].filter(Boolean); + return bits.join(" · "); +} + +function formatDreamingRepairSummary(repair: RepairDreamingArtifactsResult): string { + const actions: string[] = []; + if (repair.archivedSessionCorpus) { + actions.push("archived session corpus"); + } + if (repair.archivedSessionIngestion) { + actions.push("archived ingestion state"); + } + if (repair.archivedDreamsDiary) { + actions.push("archived diary"); + } + if (repair.warnings.length > 0) { + actions.push(`${repair.warnings.length} warning${repair.warnings.length === 1 ? "" : "s"}`); + } + return actions.length > 0 ? actions.join(" · ") : "no changes"; +} + function formatSourceLabel(source: string, workspaceDir: string, agentId: string): string { if (source === "memory") { return shortenHomeInString( @@ -648,6 +683,8 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) { scan?: MemorySourceScan; audit?: ShortTermAuditSummary; repair?: RepairShortTermPromotionArtifactsResult; + dreamingAudit?: DreamingArtifactsAuditSummary; + dreamingRepair?: RepairDreamingArtifactsResult; }> = []; for (const agentId of agentIds) { @@ -723,7 +760,14 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) { : undefined; let audit: ShortTermAuditSummary | undefined; let repair: RepairShortTermPromotionArtifactsResult | undefined; + let dreamingAudit: DreamingArtifactsAuditSummary | undefined; + let dreamingRepair: RepairDreamingArtifactsResult | undefined; if (workspaceDir) { + dreamingAudit = await auditDreamingArtifacts({ workspaceDir }); + if (opts.fix && dreamingAudit.issues.some((issue) => issue.fixable)) { + dreamingRepair = await repairDreamingArtifacts({ workspaceDir }); + dreamingAudit = await auditDreamingArtifacts({ workspaceDir }); + } if (opts.fix) { repair = await repairShortTermPromotionArtifacts({ workspaceDir }); } @@ -742,7 +786,17 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) { : undefined, }); } - allResults.push({ agentId, status, embeddingProbe, indexError, scan, audit, repair }); + allResults.push({ + agentId, + status, + embeddingProbe, + indexError, + scan, + audit, + repair, + dreamingAudit, + dreamingRepair, + }); }, }); } @@ -762,7 +816,17 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) { const label = (text: string) => muted(`${text}:`); for (const result of allResults) { - const { agentId, status, embeddingProbe, indexError, scan, audit, repair } = result; + const { + agentId, + status, + embeddingProbe, + indexError, + scan, + audit, + repair, + dreamingAudit, + dreamingRepair, + } = result; const filesIndexed = status.files ?? 0; const chunksIndexed = status.chunks ?? 0; const totalFiles = scan?.totalFiles ?? null; @@ -898,9 +962,29 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) { lines.push(`${label("QMD audit")} ${info(qmdBits.join(" · "))}`); } } + if (dreamingAudit) { + lines.push( + `${label("Dreaming artifacts")} ${info(formatDreamingAuditSummary(dreamingAudit))}`, + ); + lines.push( + `${label("Dream corpus")} ${info(shortenHomePath(dreamingAudit.sessionCorpusDir))}`, + ); + lines.push( + `${label("Dream ingestion")} ${info(shortenHomePath(dreamingAudit.sessionIngestionPath))}`, + ); + if (dreamingAudit.dreamsPath) { + lines.push(`${label("Dream diary")} ${info(shortenHomePath(dreamingAudit.dreamsPath))}`); + } + } if (repair) { lines.push(`${label("Repair")} ${info(formatRepairSummary(repair))}`); } + if (dreamingRepair) { + lines.push(`${label("Dream repair")} ${info(formatDreamingRepairSummary(dreamingRepair))}`); + if (dreamingRepair.archiveDir) { + lines.push(`${label("Dream archive")} ${info(shortenHomePath(dreamingRepair.archiveDir))}`); + } + } if (status.fallback?.reason) { lines.push(muted(status.fallback.reason)); } @@ -924,6 +1008,17 @@ export async function runMemoryStatus(opts: MemoryCommandOptions) { lines.push(` ${muted(`Fix: openclaw memory status --fix --agent ${agentId}`)}`); } } + if (dreamingAudit?.issues.length) { + if (!scan?.issues.length && !audit?.issues.length) { + lines.push(label("Issues")); + } + for (const issue of dreamingAudit.issues) { + lines.push(` ${issue.severity === "error" ? warn(issue.message) : muted(issue.message)}`); + } + if (!opts.fix) { + lines.push(` ${muted(`Fix: openclaw memory status --fix --agent ${agentId}`)}`); + } + } defaultRuntime.log(lines.join("\n")); defaultRuntime.log(""); } diff --git a/extensions/memory-core/src/cli.test.ts b/extensions/memory-core/src/cli.test.ts index ad5f1c00832..d463823554c 100644 --- a/extensions/memory-core/src/cli.test.ts +++ b/extensions/memory-core/src/cli.test.ts @@ -474,6 +474,50 @@ describe("memory cli", () => { }); }); + it("repairs contaminated dreaming artifacts during status --fix", async () => { + await withTempWorkspace(async (workspaceDir) => { + const sessionCorpusDir = path.join(workspaceDir, "memory", ".dreams", "session-corpus"); + await fs.mkdir(sessionCorpusDir, { recursive: true }); + await fs.writeFile( + path.join(sessionCorpusDir, "2026-04-11.txt"), + [ + "[main/dreaming-main.jsonl#L3] ordinary session line", + "[main/dreaming-narrative-light.jsonl#L1] Write a dream diary entry from these memory fragments:", + ].join("\n"), + "utf-8", + ); + await fs.writeFile( + path.join(workspaceDir, "memory", ".dreams", "session-ingestion.json"), + JSON.stringify({ version: 3, files: {}, seenMessages: {} }, null, 2), + "utf-8", + ); + await fs.writeFile(path.join(workspaceDir, "DREAMS.md"), "# Dream Diary\n", "utf-8"); + + const close = vi.fn(async () => {}); + mockManager({ + probeVectorAvailability: vi.fn(async () => true), + status: () => makeMemoryStatus({ workspaceDir }), + close, + }); + + const log = spyRuntimeLogs(defaultRuntime); + await runMemoryCli(["status", "--fix"]); + + expect(log).toHaveBeenCalledWith( + expect.stringContaining("Dream repair: archived session corpus"), + ); + expect(log).toHaveBeenCalledWith(expect.stringContaining("Dream archive:")); + await expect(fs.access(sessionCorpusDir)).rejects.toMatchObject({ code: "ENOENT" }); + await expect( + fs.access(path.join(workspaceDir, "memory", ".dreams", "session-ingestion.json")), + ).rejects.toMatchObject({ code: "ENOENT" }); + await expect(fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8")).resolves.toContain( + "# Dream Diary", + ); + expect(close).toHaveBeenCalled(); + }); + }); + it("enables verbose logging with --verbose", async () => { const close = vi.fn(async () => {}); mockManager({ diff --git a/extensions/memory-core/src/dreaming-narrative.test.ts b/extensions/memory-core/src/dreaming-narrative.test.ts index c3f0899ae21..e07e770d140 100644 --- a/extensions/memory-core/src/dreaming-narrative.test.ts +++ b/extensions/memory-core/src/dreaming-narrative.test.ts @@ -5,11 +5,13 @@ import { SUBAGENT_RUNTIME_REQUEST_SCOPE_ERROR_CODE, } from "openclaw/plugin-sdk/error-runtime"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { resolveGlobalMap } from "../../../src/shared/global-singleton.js"; import { appendNarrativeEntry, buildBackfillDiaryEntry, buildDiaryEntry, buildNarrativePrompt, + dedupeDreamDiaryEntries, extractNarrativeText, formatNarrativeDate, formatBackfillDiaryDate, @@ -21,9 +23,11 @@ import { import { createMemoryCoreTestHarness } from "./test-helpers.js"; const { createTempWorkspace } = createMemoryCoreTestHarness(); +const DREAMS_FILE_LOCKS_KEY = Symbol.for("openclaw.memoryCore.dreamingNarrative.fileLocks"); afterEach(() => { vi.restoreAllMocks(); + resolveGlobalMap(DREAMS_FILE_LOCKS_KEY).clear(); }); describe("buildNarrativePrompt", () => { @@ -358,6 +362,145 @@ describe("appendNarrativeEntry", () => { expect(stat.mode & 0o777).toBe(0o600); }); + it("dedupes only exact diary duplicates while keeping distinct timestamps", async () => { + const workspaceDir = await createTempWorkspace("openclaw-dreaming-dedupe-"); + const dreamsPath = path.join(workspaceDir, "DREAMS.md"); + await fs.writeFile( + dreamsPath, + [ + "# Dream Diary", + "", + "", + "---", + "", + "*April 11, 2026, 8:00 AM*", + "", + "The server room smelled like rain.", + "", + "---", + "", + "*April 11, 2026, 8:00 AM*", + "", + "", + "", + "The server room smelled like rain.", + "", + "---", + "", + "*April 11, 2026, 8:30 AM*", + "", + "The server room smelled like rain.", + "", + "", + "", + ].join("\n"), + "utf-8", + ); + + const result = await dedupeDreamDiaryEntries({ workspaceDir }); + + expect(result.removed).toBe(1); + expect(result.kept).toBe(2); + const content = await fs.readFile(dreamsPath, "utf-8"); + expect(content.match(/The server room smelled like rain\./g)?.length).toBe(2); + expect(content).toContain("*April 11, 2026, 8:00 AM*"); + expect(content).toContain("*April 11, 2026, 8:30 AM*"); + }); + + it("serializes append and dedupe so concurrent rewrites keep the new entry", async () => { + const workspaceDir = await createTempWorkspace("openclaw-dreaming-dedupe-"); + const dreamsPath = path.join(workspaceDir, "DREAMS.md"); + await fs.writeFile( + dreamsPath, + [ + "# Dream Diary", + "", + "", + "---", + "", + "*April 11, 2026, 8:00 AM*", + "", + "The server room smelled like rain.", + "", + "---", + "", + "*April 11, 2026, 8:00 AM*", + "", + "The server room smelled like rain.", + "", + "", + "", + ].join("\n"), + "utf-8", + ); + + await Promise.all([ + dedupeDreamDiaryEntries({ workspaceDir }), + appendNarrativeEntry({ + workspaceDir, + narrative: "A fresh signal arrived after the cleanup started.", + nowMs: Date.parse("2026-04-11T14:30:00Z"), + timezone: "UTC", + }), + ]); + + const content = await fs.readFile(dreamsPath, "utf-8"); + expect(content.match(/The server room smelled like rain\./g)?.length).toBe(1); + expect(content).toContain("A fresh signal arrived after the cleanup started."); + }); + + it("keeps dedupe a no-op when no exact duplicates exist", async () => { + const workspaceDir = await createTempWorkspace("openclaw-dreaming-dedupe-"); + await appendNarrativeEntry({ + workspaceDir, + narrative: "Only one entry exists.", + nowMs: Date.parse("2026-04-11T14:00:00Z"), + timezone: "UTC", + }); + + const result = await dedupeDreamDiaryEntries({ workspaceDir }); + + expect(result.removed).toBe(0); + expect(result.kept).toBe(1); + await expect(fs.readFile(path.join(workspaceDir, "DREAMS.md"), "utf-8")).resolves.toContain( + "Only one entry exists.", + ); + }); + + it("does not rewrite the diary file when dedupe finds nothing to remove", async () => { + const workspaceDir = await createTempWorkspace("openclaw-dreaming-dedupe-"); + const dreamsPath = await appendNarrativeEntry({ + workspaceDir, + narrative: "Only one entry exists.", + nowMs: Date.parse("2026-04-11T14:00:00Z"), + timezone: "UTC", + }); + const before = await fs.stat(dreamsPath); + await new Promise((resolve) => setTimeout(resolve, 20)); + + const result = await dedupeDreamDiaryEntries({ workspaceDir }); + const after = await fs.stat(dreamsPath); + + expect(result.removed).toBe(0); + expect(after.mtimeMs).toBe(before.mtimeMs); + }); + + it("cleans up the per-file lock entry after diary updates finish", async () => { + const workspaceDir = await createTempWorkspace("openclaw-dreaming-dedupe-"); + const dreamsLocks = resolveGlobalMap(DREAMS_FILE_LOCKS_KEY); + + expect(dreamsLocks.size).toBe(0); + + await appendNarrativeEntry({ + workspaceDir, + narrative: "Only one entry exists.", + nowMs: Date.parse("2026-04-11T14:00:00Z"), + timezone: "UTC", + }); + + expect(dreamsLocks.size).toBe(0); + }); + it("surfaces temp cleanup failure after atomic replace error", async () => { const workspaceDir = await createTempWorkspace("openclaw-dreaming-narrative-"); const dreamsPath = path.join(workspaceDir, "DREAMS.md"); diff --git a/extensions/memory-core/src/dreaming-narrative.ts b/extensions/memory-core/src/dreaming-narrative.ts index ba6021d5eb1..627a408fb9f 100644 --- a/extensions/memory-core/src/dreaming-narrative.ts +++ b/extensions/memory-core/src/dreaming-narrative.ts @@ -7,6 +7,8 @@ import { readErrorName, SUBAGENT_RUNTIME_REQUEST_SCOPE_ERROR_CODE, } from "openclaw/plugin-sdk/error-runtime"; +import { createAsyncLock } from "../../../src/infra/json-files.js"; +import { resolveGlobalMap } from "../../../src/shared/global-singleton.js"; // ── Types ────────────────────────────────────────────────────────────── @@ -78,6 +80,14 @@ const DREAMS_FILENAMES = ["DREAMS.md", "dreams.md"] as const; const DIARY_START_MARKER = ""; const DIARY_END_MARKER = ""; const BACKFILL_ENTRY_MARKER = "openclaw:dreaming:backfill-entry"; +const DREAMS_FILE_LOCKS_KEY = Symbol.for("openclaw.memoryCore.dreamingNarrative.fileLocks"); + +type DreamsFileLockEntry = { + withLock: ReturnType; + refs: number; +}; + +const dreamsFileLocks = resolveGlobalMap(DREAMS_FILE_LOCKS_KEY); function isRequestScopedSubagentRuntimeError(err: unknown): boolean { return ( @@ -291,6 +301,31 @@ function splitDiaryBlocks(diaryContent: string): string[] { .filter((block) => block.length > 0); } +function normalizeDiaryBlockFingerprint(block: string): string { + const lines = block + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); + let dateLine = ""; + const bodyLines: string[] = []; + for (const line of lines) { + if (!dateLine && line.startsWith("*") && line.endsWith("*") && line.length > 2) { + dateLine = line.slice(1, -1).trim(); + continue; + } + if (line.startsWith("\n\n---\n\n*April 5, 2026, 3:00 AM*\n\nThe repository whispered of forgotten endpoints tonight.\n\n", + memoryWikiEnabled: true, wikiImportInsightsLoading: false, wikiImportInsightsError: null, wikiImportInsights: { @@ -176,10 +179,14 @@ function buildProps(overrides?: Partial): DreamingProps { onRefreshDiary: () => {}, onRefreshImports: () => {}, onRefreshMemoryPalace: () => {}, + onOpenConfig: () => {}, onOpenWikiPage: async () => null, onBackfillDiary: () => {}, + onCopyDreamingArchivePath: () => {}, + onDedupeDreamDiary: () => {}, onResetDiary: () => {}, onResetGroundedShortTerm: () => {}, + onRepairDreamingArtifacts: () => {}, ...overrides, }; } @@ -387,6 +394,27 @@ describe("dreaming view", () => { setDreamSubTab("scene"); }); + it("shows a memory-wiki enablement CTA when wiki subtabs are selected but the plugin is disabled", () => { + setDreamSubTab("diary"); + setDreamDiarySubTab("palace"); + const onOpenConfig = vi.fn(); + const container = renderInto( + buildProps({ + memoryWikiEnabled: false, + onOpenConfig, + }), + ); + expect(container.textContent).toContain("Memory Wiki is not enabled"); + expect(container.textContent).toContain("plugins.entries.memory-wiki.enabled = true"); + + container + .querySelector(".dreams-diary__empty-actions .btn") + ?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + expect(onOpenConfig).toHaveBeenCalledTimes(1); + setDreamDiarySubTab("dreams"); + setDreamSubTab("scene"); + }); + it("renders dream diary with parsed entry on diary tab", () => { setDreamSubTab("diary"); setDreamDiarySubTab("dreams"); diff --git a/ui/src/ui/views/dreaming.ts b/ui/src/ui/views/dreaming.ts index 0c82dd4393f..8bd1f03e979 100644 --- a/ui/src/ui/views/dreaming.ts +++ b/ui/src/ui/views/dreaming.ts @@ -113,9 +113,12 @@ export type DreamingProps = { modeSaving: boolean; dreamDiaryLoading: boolean; dreamDiaryActionLoading: boolean; + dreamDiaryActionMessage: { kind: "success" | "error"; text: string } | null; + dreamDiaryActionArchivePath: string | null; dreamDiaryError: string | null; dreamDiaryPath: string | null; dreamDiaryContent: string | null; + memoryWikiEnabled: boolean; wikiImportInsightsLoading: boolean; wikiImportInsightsError: string | null; wikiImportInsights: WikiImportInsights | null; @@ -126,6 +129,7 @@ export type DreamingProps = { onRefreshDiary: () => void; onRefreshImports: () => void; onRefreshMemoryPalace: () => void; + onOpenConfig: () => void; onOpenWikiPage: (lookup: string) => Promise<{ title: string; path: string; @@ -135,8 +139,11 @@ export type DreamingProps = { updatedAt?: string; } | null>; onBackfillDiary: () => void; + onCopyDreamingArchivePath: () => void; + onDedupeDreamDiary: () => void; onResetDiary: () => void; onResetGroundedShortTerm: () => void; + onRepairDreamingArtifacts: () => void; onRequestUpdate?: () => void; }; @@ -763,6 +770,20 @@ function renderAdvancedSection(props: DreamingProps) {
${summary}
+ +
+ ${props.dreamDiaryActionMessage + ? html` +
+
+ ${props.dreamDiaryActionMessage.text} + ${props.dreamDiaryActionArchivePath + ? html` + + ` + : nothing} +
+
+ ` + : nothing}
${renderAdvancedEntryList({ @@ -1294,13 +1340,15 @@ function renderDreamDiaryEntries(props: DreamingProps) { // ── Diary section renderer ──────────────────────────────────────────── function renderDiarySection(props: DreamingProps) { + const wikiTabSelected = _diarySubTab === "insights" || _diarySubTab === "palace"; + const memoryWikiUnavailable = wikiTabSelected && !props.memoryWikiEnabled; const diaryError = _diarySubTab === "dreams" ? props.dreamDiaryError : _diarySubTab === "insights" ? props.wikiImportInsightsError : props.wikiMemoryPalaceError; - if (diaryError) { + if (diaryError && !memoryWikiUnavailable) { return html`
${diaryError}
@@ -1356,15 +1404,19 @@ function renderDiarySection(props: DreamingProps) {
${renderDiarySubtabExplainer()} - ${_diarySubTab === "dreams" - ? renderDreamDiaryEntries(props) - : _diarySubTab === "insights" - ? renderDiaryImportsSection(props) - : renderMemoryPalaceSection(props)} + ${memoryWikiUnavailable + ? html` +
+
Memory Wiki is not enabled
+
+ Imported Insights and Memory Palace are provided by the bundled + memory-wiki plugin. +
+
+ Enable plugins.entries.memory-wiki.enabled = true, then reload this + tab. +
+
+ +
+
+ ` + : _diarySubTab === "dreams" + ? renderDreamDiaryEntries(props) + : _diarySubTab === "insights" + ? renderDiaryImportsSection(props) + : renderMemoryPalaceSection(props)} ${renderWikiPreviewOverlay(props)} `;