From d1c7d9af800dec8445381c8ac70779e40ccf8342 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 5 Apr 2026 20:58:08 +0100 Subject: [PATCH] feat(memory-sdk): add memory event journal bridge --- docs/plugins/sdk-migration.md | 2 + docs/plugins/sdk-overview.md | 2 + .../memory-core/src/dreaming-markdown.ts | 19 ++++ .../memory-core/src/memory-events.test.ts | 105 ++++++++++++++++++ .../memory-core/src/short-term-promotion.ts | 27 +++++ extensions/memory-wiki/src/bridge.test.ts | 55 +++++++++ extensions/memory-wiki/src/bridge.ts | 37 +++++- package.json | 8 ++ scripts/lib/plugin-sdk-entrypoints.json | 2 + src/memory-host-sdk/events.ts | 93 ++++++++++++++++ src/plugin-sdk/memory-core-host-events.ts | 1 + src/plugin-sdk/memory-core.ts | 6 + src/plugin-sdk/memory-host-events.test.ts | 60 ++++++++++ src/plugin-sdk/memory-host-events.ts | 1 + 14 files changed, 413 insertions(+), 5 deletions(-) create mode 100644 extensions/memory-core/src/memory-events.test.ts create mode 100644 src/memory-host-sdk/events.ts create mode 100644 src/plugin-sdk/memory-core-host-events.ts create mode 100644 src/plugin-sdk/memory-host-events.test.ts create mode 100644 src/plugin-sdk/memory-host-events.ts diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md index 544941ea8a0..93c52013dcc 100644 --- a/docs/plugins/sdk-migration.md +++ b/docs/plugins/sdk-migration.md @@ -287,11 +287,13 @@ Current bundled provider examples: | `plugin-sdk/memory-core-host-multimodal` | Memory host multimodal helpers | Memory host multimodal helpers | | `plugin-sdk/memory-core-host-query` | Memory host query helpers | Memory host query helpers | | `plugin-sdk/memory-core-host-secret` | Memory host secret helpers | Memory host secret helpers | + | `plugin-sdk/memory-core-host-events` | Memory host event journal helpers | Memory host event journal helpers | | `plugin-sdk/memory-core-host-status` | Memory host status helpers | Memory host status helpers | | `plugin-sdk/memory-core-host-runtime-cli` | Memory host CLI runtime | Memory host CLI runtime helpers | | `plugin-sdk/memory-core-host-runtime-core` | Memory host core runtime | Memory host core runtime helpers | | `plugin-sdk/memory-core-host-runtime-files` | Memory host file/runtime helpers | Memory host file/runtime helpers | | `plugin-sdk/memory-host-core` | Memory host core runtime alias | Vendor-neutral alias for memory host core runtime helpers | + | `plugin-sdk/memory-host-events` | Memory host event journal alias | Vendor-neutral alias for memory host event journal helpers | | `plugin-sdk/memory-host-files` | Memory host file/runtime alias | Vendor-neutral alias for memory host file/runtime helpers | | `plugin-sdk/memory-host-markdown` | Managed markdown helpers | Shared managed-markdown helpers for memory-adjacent plugins | | `plugin-sdk/memory-host-status` | Memory host status alias | Vendor-neutral alias for memory host status helpers | diff --git a/docs/plugins/sdk-overview.md b/docs/plugins/sdk-overview.md index 8c551ec1133..7dbd4f00f04 100644 --- a/docs/plugins/sdk-overview.md +++ b/docs/plugins/sdk-overview.md @@ -253,11 +253,13 @@ explicitly promotes one as public. | `plugin-sdk/memory-core-host-multimodal` | Memory host multimodal helpers | | `plugin-sdk/memory-core-host-query` | Memory host query helpers | | `plugin-sdk/memory-core-host-secret` | Memory host secret helpers | + | `plugin-sdk/memory-core-host-events` | Memory host event journal helpers | | `plugin-sdk/memory-core-host-status` | Memory host status helpers | | `plugin-sdk/memory-core-host-runtime-cli` | Memory host CLI runtime helpers | | `plugin-sdk/memory-core-host-runtime-core` | Memory host core runtime helpers | | `plugin-sdk/memory-core-host-runtime-files` | Memory host file/runtime helpers | | `plugin-sdk/memory-host-core` | Vendor-neutral alias for memory host core runtime helpers | + | `plugin-sdk/memory-host-events` | Vendor-neutral alias for memory host event journal helpers | | `plugin-sdk/memory-host-files` | Vendor-neutral alias for memory host file/runtime helpers | | `plugin-sdk/memory-host-markdown` | Shared managed-markdown helpers for memory-adjacent plugins | | `plugin-sdk/memory-host-status` | Vendor-neutral alias for memory host status helpers | diff --git a/extensions/memory-core/src/dreaming-markdown.ts b/extensions/memory-core/src/dreaming-markdown.ts index 788a6dbbfa7..7b0b3d13059 100644 --- a/extensions/memory-core/src/dreaming-markdown.ts +++ b/extensions/memory-core/src/dreaming-markdown.ts @@ -5,6 +5,7 @@ import { type MemoryDreamingPhaseName, type MemoryDreamingStorageConfig, } from "openclaw/plugin-sdk/memory-core-host-status"; +import { appendMemoryHostEvent } from "openclaw/plugin-sdk/memory-host-events"; import { replaceManagedMarkdownBlock, withTrailingNewline, @@ -104,6 +105,16 @@ export async function writeDailyDreamingPhaseBlock(params: { await fs.writeFile(reportPath, report, "utf-8"); } + await appendMemoryHostEvent(params.workspaceDir, { + type: "memory.dream.completed", + timestamp: new Date(nowMs).toISOString(), + phase: params.phase, + ...(inlinePath ? { inlinePath } : {}), + ...(reportPath ? { reportPath } : {}), + lineCount: params.bodyLines.length, + storageMode: params.storage.mode, + }); + return { ...(inlinePath ? { inlinePath } : {}), ...(reportPath ? { reportPath } : {}), @@ -125,5 +136,13 @@ export async function writeDeepDreamingReport(params: { await fs.mkdir(path.dirname(reportPath), { recursive: true }); const body = params.bodyLines.length > 0 ? params.bodyLines.join("\n") : "- No durable changes."; await fs.writeFile(reportPath, `# Deep Sleep\n\n${body}\n`, "utf-8"); + await appendMemoryHostEvent(params.workspaceDir, { + type: "memory.dream.completed", + timestamp: new Date(nowMs).toISOString(), + phase: "deep", + reportPath, + lineCount: params.bodyLines.length, + storageMode: params.storage.mode, + }); return reportPath; } diff --git a/extensions/memory-core/src/memory-events.test.ts b/extensions/memory-core/src/memory-events.test.ts new file mode 100644 index 00000000000..df2946c70aa --- /dev/null +++ b/extensions/memory-core/src/memory-events.test.ts @@ -0,0 +1,105 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { readMemoryHostEvents } from "openclaw/plugin-sdk/memory-host-events"; +import { afterEach, describe, expect, it } from "vitest"; +import { writeDailyDreamingPhaseBlock } from "./dreaming-markdown.js"; +import { + applyShortTermPromotions, + rankShortTermPromotionCandidates, + recordShortTermRecalls, +} from "./short-term-promotion.js"; + +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); +}); + +describe("memory host event journal integration", () => { + it("records recall and promotion events from short-term promotion flows", async () => { + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-core-events-")); + tempDirs.push(workspaceDir); + await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true }); + await fs.writeFile( + path.join(workspaceDir, "memory", "2026-04-05.md"), + "# Daily\n\nalpha\nbeta\ngamma\n", + "utf8", + ); + + await recordShortTermRecalls({ + workspaceDir, + query: "alpha memory", + results: [ + { + path: "memory/2026-04-05.md", + startLine: 3, + endLine: 4, + score: 0.92, + snippet: "alpha beta", + source: "memory", + }, + ], + nowMs: Date.UTC(2026, 3, 5, 12, 0, 0), + }); + + const candidates = await rankShortTermPromotionCandidates({ + workspaceDir, + minScore: 0, + minRecallCount: 0, + minUniqueQueries: 0, + nowMs: Date.UTC(2026, 3, 5, 12, 5, 0), + }); + const applied = await applyShortTermPromotions({ + workspaceDir, + candidates, + minScore: 0, + minRecallCount: 0, + minUniqueQueries: 0, + nowMs: Date.UTC(2026, 3, 5, 12, 10, 0), + }); + + expect(applied.applied).toBe(1); + + const events = await readMemoryHostEvents({ workspaceDir }); + + expect(events.map((event) => event.type)).toEqual([ + "memory.recall.recorded", + "memory.promotion.applied", + ]); + expect(events[0]).toMatchObject({ + type: "memory.recall.recorded", + resultCount: 1, + query: "alpha memory", + }); + expect(events[1]).toMatchObject({ + type: "memory.promotion.applied", + applied: 1, + }); + }); + + it("records dreaming completion events when phase artifacts are written", async () => { + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-core-dream-events-")); + tempDirs.push(workspaceDir); + + const written = await writeDailyDreamingPhaseBlock({ + workspaceDir, + phase: "light", + bodyLines: ["- staged note", "- second note"], + nowMs: Date.UTC(2026, 3, 5, 13, 0, 0), + storage: { mode: "both", separateReports: true }, + }); + + const events = await readMemoryHostEvents({ workspaceDir }); + + expect(written.inlinePath).toBeTruthy(); + expect(written.reportPath).toBeTruthy(); + expect(events).toHaveLength(1); + expect(events[0]).toMatchObject({ + type: "memory.dream.completed", + phase: "light", + lineCount: 2, + storageMode: "both", + }); + }); +}); diff --git a/extensions/memory-core/src/short-term-promotion.ts b/extensions/memory-core/src/short-term-promotion.ts index 27a09ffbeab..e772eaaabc8 100644 --- a/extensions/memory-core/src/short-term-promotion.ts +++ b/extensions/memory-core/src/short-term-promotion.ts @@ -3,6 +3,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import type { MemorySearchResult } from "openclaw/plugin-sdk/memory-core-host-runtime-files"; import { formatMemoryDreamingDay } from "openclaw/plugin-sdk/memory-core-host-status"; +import { appendMemoryHostEvent } from "openclaw/plugin-sdk/memory-host-events"; import { deriveConceptTags, MAX_CONCEPT_TAGS, @@ -631,6 +632,18 @@ export async function recordShortTermRecalls(params: { store.updatedAt = nowIso; await writeStore(workspaceDir, store); + await appendMemoryHostEvent(workspaceDir, { + type: "memory.recall.recorded", + timestamp: nowIso, + query, + resultCount: relevant.length, + results: relevant.map((result) => ({ + path: normalizeMemoryPath(result.path), + startLine: Math.max(1, Math.floor(result.startLine)), + endLine: Math.max(1, Math.floor(result.endLine)), + score: clampScore(result.score), + })), + }); }); } @@ -1042,6 +1055,20 @@ export async function applyShortTermPromotions( } store.updatedAt = nowIso; await writeStore(workspaceDir, store); + await appendMemoryHostEvent(workspaceDir, { + type: "memory.promotion.applied", + timestamp: nowIso, + memoryPath, + applied: rehydratedSelected.length, + candidates: rehydratedSelected.map((candidate) => ({ + key: candidate.key, + path: candidate.path, + startLine: candidate.startLine, + endLine: candidate.endLine, + score: candidate.score, + recallCount: candidate.recallCount, + })), + }); return { memoryPath, diff --git a/extensions/memory-wiki/src/bridge.test.ts b/extensions/memory-wiki/src/bridge.test.ts index 98ddd5c34ee..1d4295945ff 100644 --- a/extensions/memory-wiki/src/bridge.test.ts +++ b/extensions/memory-wiki/src/bridge.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; +import { appendMemoryHostEvent } from "openclaw/plugin-sdk/memory-host-events"; import { afterEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../api.js"; import { syncMemoryWikiBridgeSources } from "./bridge.js"; @@ -106,4 +107,58 @@ describe("syncMemoryWikiBridgeSources", () => { pagePaths: [], }); }); + + it("imports the public memory event journal when followMemoryEvents is enabled", async () => { + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-bridge-events-ws-")); + const vaultDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-wiki-bridge-events-vault-")); + tempDirs.push(workspaceDir, vaultDir); + + await appendMemoryHostEvent(workspaceDir, { + type: "memory.recall.recorded", + timestamp: "2026-04-05T12:00:00.000Z", + query: "bridge events", + resultCount: 1, + results: [ + { + path: "memory/2026-04-05.md", + startLine: 1, + endLine: 2, + score: 0.8, + }, + ], + }); + + const config = resolveMemoryWikiConfig( + { + vaultMode: "bridge", + vault: { path: vaultDir }, + bridge: { + enabled: true, + followMemoryEvents: true, + }, + }, + { homedir: "/Users/tester" }, + ); + const appConfig: OpenClawConfig = { + plugins: { + entries: { + "memory-core": { + enabled: true, + config: {}, + }, + }, + }, + agents: { + list: [{ id: "main", default: true, workspace: workspaceDir }], + }, + }; + + const result = await syncMemoryWikiBridgeSources({ config, appConfig }); + + expect(result.artifactCount).toBe(1); + expect(result.importedCount).toBe(1); + const page = await fs.readFile(path.join(vaultDir, result.pagePaths[0] ?? ""), "utf8"); + expect(page).toContain("sourceType: memory-bridge-events"); + expect(page).toContain('"type":"memory.recall.recorded"'); + }); }); diff --git a/extensions/memory-wiki/src/bridge.ts b/extensions/memory-wiki/src/bridge.ts index bc2c173d3fb..435f953dc05 100644 --- a/extensions/memory-wiki/src/bridge.ts +++ b/extensions/memory-wiki/src/bridge.ts @@ -1,16 +1,19 @@ import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; +import { resolveMemoryHostEventLogPath } from "openclaw/plugin-sdk/memory-host-events"; import { resolveMemoryCorePluginConfig, resolveMemoryDreamingWorkspaces, } from "openclaw/plugin-sdk/memory-host-status"; +import type { OpenClawConfig } from "../api.js"; import type { ResolvedMemoryWikiConfig } from "./config.js"; import { appendMemoryWikiLog } from "./log.js"; import { renderMarkdownFence, renderWikiMarkdown, slugifyWikiSegment } from "./markdown.js"; import { initializeMemoryWikiVault } from "./vault.js"; type BridgeArtifact = { + artifactType: "markdown" | "memory-events"; workspaceDir: string; relativePath: string; absolutePath: string; @@ -65,6 +68,7 @@ async function collectWorkspaceArtifacts( const absolutePath = path.join(workspaceDir, relPath); if (await pathExists(absolutePath)) { artifacts.push({ + artifactType: "markdown", workspaceDir, relativePath: relPath, absolutePath, @@ -80,6 +84,7 @@ async function collectWorkspaceArtifacts( const relativePath = path.relative(workspaceDir, absolutePath).replace(/\\/g, "/"); if (!relativePath.startsWith("memory/dreaming/")) { artifacts.push({ + artifactType: "markdown", workspaceDir, relativePath, absolutePath, @@ -94,6 +99,7 @@ async function collectWorkspaceArtifacts( for (const absolutePath of files) { const relativePath = path.relative(workspaceDir, absolutePath).replace(/\\/g, "/"); artifacts.push({ + artifactType: "markdown", workspaceDir, relativePath, absolutePath, @@ -101,6 +107,18 @@ async function collectWorkspaceArtifacts( } } + if (bridgeConfig.followMemoryEvents) { + const eventLogPath = resolveMemoryHostEventLogPath(workspaceDir); + if (await pathExists(eventLogPath)) { + artifacts.push({ + artifactType: "memory-events", + workspaceDir, + relativePath: path.relative(workspaceDir, eventLogPath).replace(/\\/g, "/"), + absolutePath: eventLogPath, + }); + } + } + const deduped = new Map(); for (const artifact of artifacts) { deduped.set(await resolveArtifactKey(artifact.absolutePath), artifact); @@ -108,8 +126,14 @@ async function collectWorkspaceArtifacts( return [...deduped.values()]; } -function resolveBridgeTitle(relativePath: string, agentIds: string[]): string { - const base = relativePath +function resolveBridgeTitle(artifact: BridgeArtifact, agentIds: string[]): string { + if (artifact.artifactType === "memory-events") { + if (agentIds.length === 0) { + return "Memory Bridge: event journal"; + } + return `Memory Bridge (${agentIds.join(", ")}): event journal`; + } + const base = artifact.relativePath .replace(/\.md$/i, "") .replace(/^memory\//, "") .replace(/\//g, " / "); @@ -152,18 +176,20 @@ async function writeBridgeSourcePage(params: { workspaceDir: params.artifact.workspaceDir, relativePath: params.artifact.relativePath, }); - const title = resolveBridgeTitle(params.artifact.relativePath, params.agentIds); + const title = resolveBridgeTitle(params.artifact, params.agentIds); const pageAbsPath = path.join(params.config.vault.path, pagePath); const created = !(await pathExists(pageAbsPath)); const raw = await fs.readFile(params.artifact.absolutePath, "utf8"); const stats = await fs.stat(params.artifact.absolutePath); const sourceUpdatedAt = stats.mtime.toISOString(); + const contentLanguage = params.artifact.artifactType === "memory-events" ? "json" : "markdown"; const rendered = renderWikiMarkdown({ frontmatter: { pageType: "source", id: pageId, title, - sourceType: "memory-bridge", + sourceType: + params.artifact.artifactType === "memory-events" ? "memory-bridge-events" : "memory-bridge", sourcePath: params.artifact.absolutePath, bridgeRelativePath: params.artifact.relativePath, bridgeWorkspaceDir: params.artifact.workspaceDir, @@ -177,11 +203,12 @@ async function writeBridgeSourcePage(params: { "## Bridge Source", `- Workspace: \`${params.artifact.workspaceDir}\``, `- Relative path: \`${params.artifact.relativePath}\``, + `- Kind: \`${params.artifact.artifactType}\``, `- Agents: ${params.agentIds.length > 0 ? params.agentIds.join(", ") : "unknown"}`, `- Updated: ${sourceUpdatedAt}`, "", "## Content", - renderMarkdownFence(raw, "markdown"), + renderMarkdownFence(raw, contentLanguage), "", "## Notes", "", diff --git a/package.json b/package.json index d8955cad490..f7858117711 100644 --- a/package.json +++ b/package.json @@ -679,6 +679,10 @@ "types": "./dist/plugin-sdk/memory-core-host-secret.d.ts", "default": "./dist/plugin-sdk/memory-core-host-secret.js" }, + "./plugin-sdk/memory-core-host-events": { + "types": "./dist/plugin-sdk/memory-core-host-events.d.ts", + "default": "./dist/plugin-sdk/memory-core-host-events.js" + }, "./plugin-sdk/memory-core-host-status": { "types": "./dist/plugin-sdk/memory-core-host-status.d.ts", "default": "./dist/plugin-sdk/memory-core-host-status.js" @@ -699,6 +703,10 @@ "types": "./dist/plugin-sdk/memory-host-core.d.ts", "default": "./dist/plugin-sdk/memory-host-core.js" }, + "./plugin-sdk/memory-host-events": { + "types": "./dist/plugin-sdk/memory-host-events.d.ts", + "default": "./dist/plugin-sdk/memory-host-events.js" + }, "./plugin-sdk/memory-host-files": { "types": "./dist/plugin-sdk/memory-host-files.d.ts", "default": "./dist/plugin-sdk/memory-host-files.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 59b64130f4f..f10414347a8 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -159,11 +159,13 @@ "memory-core-host-multimodal", "memory-core-host-query", "memory-core-host-secret", + "memory-core-host-events", "memory-core-host-status", "memory-core-host-runtime-cli", "memory-core-host-runtime-core", "memory-core-host-runtime-files", "memory-host-core", + "memory-host-events", "memory-host-files", "memory-host-markdown", "memory-host-status", diff --git a/src/memory-host-sdk/events.ts b/src/memory-host-sdk/events.ts new file mode 100644 index 00000000000..0b961ad057d --- /dev/null +++ b/src/memory-host-sdk/events.ts @@ -0,0 +1,93 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import type { MemoryDreamingPhaseName } from "./dreaming.js"; + +export const MEMORY_HOST_EVENT_LOG_RELATIVE_PATH = path.join("memory", ".dreams", "events.jsonl"); + +export type MemoryHostRecallRecordedEvent = { + type: "memory.recall.recorded"; + timestamp: string; + query: string; + resultCount: number; + results: Array<{ + path: string; + startLine: number; + endLine: number; + score: number; + }>; +}; + +export type MemoryHostPromotionAppliedEvent = { + type: "memory.promotion.applied"; + timestamp: string; + memoryPath: string; + applied: number; + candidates: Array<{ + key: string; + path: string; + startLine: number; + endLine: number; + score: number; + recallCount: number; + }>; +}; + +export type MemoryHostDreamCompletedEvent = { + type: "memory.dream.completed"; + timestamp: string; + phase: MemoryDreamingPhaseName; + inlinePath?: string; + reportPath?: string; + lineCount: number; + storageMode: "inline" | "separate" | "both"; +}; + +export type MemoryHostEvent = + | MemoryHostRecallRecordedEvent + | MemoryHostPromotionAppliedEvent + | MemoryHostDreamCompletedEvent; + +export function resolveMemoryHostEventLogPath(workspaceDir: string): string { + return path.join(workspaceDir, MEMORY_HOST_EVENT_LOG_RELATIVE_PATH); +} + +export async function appendMemoryHostEvent( + workspaceDir: string, + event: MemoryHostEvent, +): Promise { + const eventLogPath = resolveMemoryHostEventLogPath(workspaceDir); + await fs.mkdir(path.dirname(eventLogPath), { recursive: true }); + await fs.appendFile(eventLogPath, `${JSON.stringify(event)}\n`, "utf8"); +} + +export async function readMemoryHostEvents(params: { + workspaceDir: string; + limit?: number; +}): Promise { + const eventLogPath = resolveMemoryHostEventLogPath(params.workspaceDir); + const raw = await fs.readFile(eventLogPath, "utf8").catch((err: unknown) => { + if ((err as NodeJS.ErrnoException)?.code === "ENOENT") { + return ""; + } + throw err; + }); + if (!raw.trim()) { + return []; + } + const events = raw + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .flatMap((line) => { + try { + return [JSON.parse(line) as MemoryHostEvent]; + } catch { + return []; + } + }); + if (!Number.isFinite(params.limit)) { + return events; + } + const limit = Math.max(0, Math.floor(params.limit as number)); + return limit === 0 ? [] : events.slice(-limit); +} diff --git a/src/plugin-sdk/memory-core-host-events.ts b/src/plugin-sdk/memory-core-host-events.ts new file mode 100644 index 00000000000..8f68c7d1e6c --- /dev/null +++ b/src/plugin-sdk/memory-core-host-events.ts @@ -0,0 +1 @@ +export * from "../memory-host-sdk/events.js"; diff --git a/src/plugin-sdk/memory-core.ts b/src/plugin-sdk/memory-core.ts index 4b6bde5f930..1d9537f9904 100644 --- a/src/plugin-sdk/memory-core.ts +++ b/src/plugin-sdk/memory-core.ts @@ -46,6 +46,12 @@ export { withProgress, withProgressTotals, } from "./memory-core-host-runtime-cli.js"; +export { + appendMemoryHostEvent, + readMemoryHostEvents, + resolveMemoryHostEventLogPath, +} from "./memory-core-host-events.js"; +export type { MemoryHostEvent } from "./memory-core-host-events.js"; export { resolveMemoryCorePluginConfig, formatMemoryDreamingDay, diff --git a/src/plugin-sdk/memory-host-events.test.ts b/src/plugin-sdk/memory-host-events.test.ts new file mode 100644 index 00000000000..6d50735606d --- /dev/null +++ b/src/plugin-sdk/memory-host-events.test.ts @@ -0,0 +1,60 @@ +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + appendMemoryHostEvent, + readMemoryHostEvents, + resolveMemoryHostEventLogPath, +} from "./memory-host-events.js"; + +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); +}); + +describe("memory host event journal helpers", () => { + it("appends and reads typed workspace events", async () => { + const workspaceDir = await fs.mkdtemp(path.join(os.tmpdir(), "memory-host-events-")); + tempDirs.push(workspaceDir); + + await appendMemoryHostEvent(workspaceDir, { + type: "memory.recall.recorded", + timestamp: "2026-04-05T12:00:00.000Z", + query: "glacier backup", + resultCount: 1, + results: [ + { + path: "memory/2026-04-05.md", + startLine: 1, + endLine: 3, + score: 0.9, + }, + ], + }); + await appendMemoryHostEvent(workspaceDir, { + type: "memory.dream.completed", + timestamp: "2026-04-05T13:00:00.000Z", + phase: "light", + lineCount: 4, + storageMode: "both", + inlinePath: path.join(workspaceDir, "memory", "2026-04-05.md"), + reportPath: path.join(workspaceDir, "memory", "dreaming", "light", "2026-04-05.md"), + }); + + const eventLogPath = resolveMemoryHostEventLogPath(workspaceDir); + await expect(fs.readFile(eventLogPath, "utf8")).resolves.toContain( + '"type":"memory.recall.recorded"', + ); + + const events = await readMemoryHostEvents({ workspaceDir }); + const tail = await readMemoryHostEvents({ workspaceDir, limit: 1 }); + + expect(events).toHaveLength(2); + expect(events[0]?.type).toBe("memory.recall.recorded"); + expect(events[1]?.type).toBe("memory.dream.completed"); + expect(tail).toHaveLength(1); + expect(tail[0]?.type).toBe("memory.dream.completed"); + }); +}); diff --git a/src/plugin-sdk/memory-host-events.ts b/src/plugin-sdk/memory-host-events.ts new file mode 100644 index 00000000000..8f68c7d1e6c --- /dev/null +++ b/src/plugin-sdk/memory-host-events.ts @@ -0,0 +1 @@ +export * from "../memory-host-sdk/events.js";