@basicmemory/openclaw-basic-memory 0.2.3 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +49 -19
  2. package/bm-client.ts +55 -21
  3. package/commands/cli.ts +1 -2
  4. package/commands/skills.ts +71 -10
  5. package/commands/slash.ts +4 -2
  6. package/config.ts +0 -16
  7. package/context-engine/basic-memory-context-engine.ts +22 -90
  8. package/dist/bm-client.d.ts +179 -0
  9. package/dist/bm-client.js +582 -0
  10. package/dist/commands/cli.d.ts +4 -0
  11. package/dist/commands/cli.js +116 -0
  12. package/dist/commands/skills.d.ts +2 -0
  13. package/dist/commands/skills.js +86 -0
  14. package/dist/commands/slash.d.ts +3 -0
  15. package/dist/commands/slash.js +92 -0
  16. package/dist/config.d.ts +17 -0
  17. package/dist/config.js +99 -0
  18. package/dist/context-engine/basic-memory-context-engine.d.ts +34 -0
  19. package/dist/context-engine/basic-memory-context-engine.js +176 -0
  20. package/dist/hooks/capture.d.ts +24 -0
  21. package/dist/hooks/capture.js +88 -0
  22. package/dist/hooks/recall.d.ts +24 -0
  23. package/dist/hooks/recall.js +56 -0
  24. package/dist/index.d.ts +9 -0
  25. package/dist/index.js +135 -0
  26. package/dist/logger.d.ts +13 -0
  27. package/dist/logger.js +34 -0
  28. package/dist/schema/conversation-schema.d.ts +5 -0
  29. package/dist/schema/conversation-schema.js +33 -0
  30. package/dist/schema/task-schema.d.ts +5 -0
  31. package/dist/schema/task-schema.js +34 -0
  32. package/dist/tools/build-context.d.ts +3 -0
  33. package/dist/tools/build-context.js +100 -0
  34. package/dist/tools/delete-note.d.ts +3 -0
  35. package/dist/tools/delete-note.js +52 -0
  36. package/dist/tools/edit-note.d.ts +3 -0
  37. package/dist/tools/edit-note.js +81 -0
  38. package/dist/tools/list-memory-projects.d.ts +3 -0
  39. package/dist/tools/list-memory-projects.js +86 -0
  40. package/dist/tools/list-workspaces.d.ts +3 -0
  41. package/dist/tools/list-workspaces.js +64 -0
  42. package/dist/tools/memory-provider.d.ts +15 -0
  43. package/dist/tools/memory-provider.js +274 -0
  44. package/dist/tools/move-note.d.ts +3 -0
  45. package/dist/tools/move-note.js +56 -0
  46. package/dist/tools/read-note.d.ts +3 -0
  47. package/dist/tools/read-note.js +57 -0
  48. package/dist/tools/schema-diff.d.ts +3 -0
  49. package/dist/tools/schema-diff.js +81 -0
  50. package/dist/tools/schema-infer.d.ts +3 -0
  51. package/dist/tools/schema-infer.js +66 -0
  52. package/dist/tools/schema-validate.d.ts +3 -0
  53. package/dist/tools/schema-validate.js +73 -0
  54. package/dist/tools/search-notes.d.ts +3 -0
  55. package/dist/tools/search-notes.js +97 -0
  56. package/dist/tools/write-note.d.ts +3 -0
  57. package/dist/tools/write-note.js +80 -0
  58. package/index.ts +27 -17
  59. package/openclaw.plugin.json +27 -20
  60. package/package.json +27 -8
  61. package/scripts/fetch-skills.ts +62 -67
  62. package/scripts/setup-bm.sh +27 -10
  63. package/skills/manifest.json +25 -0
  64. package/skills/memory-capture/SKILL.md +275 -0
  65. package/skills/memory-ci-capture/SKILL.md +56 -0
  66. package/skills/memory-continue/SKILL.md +148 -0
  67. package/skills/memory-curate/SKILL.md +243 -0
  68. package/skills/memory-literary-analysis/SKILL.md +15 -12
  69. package/skills/memory-notes/SKILL.md +22 -6
  70. package/skills/memory-onboarding/SKILL.md +142 -0
  71. package/skills/memory-onboarding/evals/evals.json +29 -0
  72. package/skills/memory-onboarding/references/assistant-setup.md +64 -0
  73. package/skills/memory-onboarding/references/conventions.md +118 -0
  74. package/skills/memory-onboarding/references/domain-playbooks.md +159 -0
  75. package/skills/memory-onboarding/references/schema-guide.md +136 -0
  76. package/skills/memory-schema/SKILL.md +7 -5
  77. package/skills/memory-tasks/SKILL.md +1 -1
  78. package/tools/build-context.ts +1 -1
  79. package/tools/delete-note.ts +1 -1
  80. package/tools/edit-note.ts +1 -1
  81. package/tools/list-memory-projects.ts +3 -2
  82. package/tools/list-workspaces.ts +1 -1
  83. package/tools/memory-provider.ts +1 -1
  84. package/tools/move-note.ts +1 -1
  85. package/tools/read-note.ts +1 -1
  86. package/tools/schema-diff.ts +1 -1
  87. package/tools/schema-infer.ts +1 -1
  88. package/tools/schema-validate.ts +1 -1
  89. package/tools/search-notes.ts +1 -1
  90. package/tools/write-note.ts +1 -1
  91. package/context-engine/basic-memory-context-engine.test.ts +0 -398
@@ -0,0 +1,2 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
2
+ export declare function registerSkillCommands(api: OpenClawPluginApi): void;
@@ -0,0 +1,86 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ function resolveSkillsDir(api) {
6
+ if (api.resolvePath) {
7
+ return api.resolvePath("skills");
8
+ }
9
+ const sourceRootSkills = resolve(__dirname, "..", "skills");
10
+ if (existsSync(sourceRootSkills)) {
11
+ return sourceRootSkills;
12
+ }
13
+ return resolve(__dirname, "..", "..", "skills");
14
+ }
15
+ function loadManifest(skillsDir) {
16
+ try {
17
+ const raw = readFileSync(resolve(skillsDir, "manifest.json"), "utf-8");
18
+ return JSON.parse(raw);
19
+ }
20
+ catch {
21
+ throw new Error("skills/manifest.json not found. Run `bun scripts/fetch-skills.ts` first.");
22
+ }
23
+ }
24
+ function loadSkill(skillsDir, dir) {
25
+ return readFileSync(resolve(skillsDir, dir, "SKILL.md"), "utf-8");
26
+ }
27
+ /**
28
+ * List a skill's bundled resource files (references/, assets/, ...) so the
29
+ * command can tell the assistant where they live on disk. SKILL.md workflows
30
+ * reference these by relative path; without the mapping, bundled commands
31
+ * would direct the assistant to files it was never given. evals/ is excluded
32
+ * as test fixtures.
33
+ */
34
+ function listResourceFiles(skillsDir, dir) {
35
+ const root = resolve(skillsDir, dir);
36
+ const files = [];
37
+ const walk = (rel) => {
38
+ for (const entry of readdirSync(resolve(root, rel), {
39
+ withFileTypes: true,
40
+ })) {
41
+ const relPath = rel ? `${rel}/${entry.name}` : entry.name;
42
+ if (entry.isDirectory()) {
43
+ if (relPath !== "evals")
44
+ walk(relPath);
45
+ }
46
+ else if (relPath !== "SKILL.md") {
47
+ files.push(relPath);
48
+ }
49
+ }
50
+ };
51
+ walk("");
52
+ return files.sort();
53
+ }
54
+ function resourceSection(skillsDir, dir, files) {
55
+ if (files.length === 0)
56
+ return "";
57
+ const lines = files
58
+ .map((f) => `- \`${f}\` → ${resolve(skillsDir, dir, f)}`)
59
+ .join("\n");
60
+ return ("\n\n## Bundled resource files\n\n" +
61
+ "This skill references companion files by relative path. They are installed at:\n\n" +
62
+ `${lines}\n\n` +
63
+ "When the workflow directs you to read one of these files, read it from the absolute path listed above.");
64
+ }
65
+ export function registerSkillCommands(api) {
66
+ const skillsDir = resolveSkillsDir(api);
67
+ const manifest = loadManifest(skillsDir);
68
+ for (const entry of manifest) {
69
+ const commandName = entry.dir.replace(/^memory-/, "");
70
+ const content = loadSkill(skillsDir, entry.dir) +
71
+ resourceSection(skillsDir, entry.dir, listResourceFiles(skillsDir, entry.dir));
72
+ api.registerCommand({
73
+ name: commandName,
74
+ description: entry.description,
75
+ acceptsArgs: true,
76
+ requireAuth: true,
77
+ handler: async (ctx) => {
78
+ const args = ctx.args?.trim();
79
+ const prefix = args
80
+ ? `User request: ${args}\n\nFollow this workflow:\n\n`
81
+ : "Follow this workflow:\n\n";
82
+ return { text: prefix + content };
83
+ },
84
+ });
85
+ }
86
+ }
@@ -0,0 +1,3 @@
1
+ import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
2
+ import type { BmClient } from "../bm-client.ts";
3
+ export declare function registerCommands(api: OpenClawPluginApi, client: BmClient): void;
@@ -0,0 +1,92 @@
1
+ import { execSync } from "node:child_process";
2
+ import { dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { log } from "../logger.js";
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ export function registerCommands(api, client) {
7
+ api.registerCommand({
8
+ name: "bm-setup",
9
+ description: "Install or update the Basic Memory CLI (requires uv)",
10
+ requireAuth: true,
11
+ handler: async () => {
12
+ const scriptPath = api.resolvePath
13
+ ? api.resolvePath("scripts/setup-bm.sh")
14
+ : resolve(__dirname, "..", "scripts", "setup-bm.sh");
15
+ log.info(`/bm-setup: running ${scriptPath}`);
16
+ try {
17
+ const output = execSync(`bash "${scriptPath}"`, {
18
+ encoding: "utf-8",
19
+ timeout: 180_000,
20
+ stdio: "pipe",
21
+ env: { ...process.env },
22
+ });
23
+ return { text: output.trim() };
24
+ }
25
+ catch (err) {
26
+ const execErr = err;
27
+ const detail = execErr.stderr || execErr.stdout || String(err);
28
+ log.error("/bm-setup failed", err);
29
+ return {
30
+ text: `Setup failed:\n${detail.trim()}`,
31
+ };
32
+ }
33
+ },
34
+ });
35
+ api.registerCommand({
36
+ name: "remember",
37
+ description: "Save something to the Basic Memory knowledge graph",
38
+ acceptsArgs: true,
39
+ requireAuth: true,
40
+ handler: async (ctx) => {
41
+ const text = ctx.args?.trim();
42
+ if (!text) {
43
+ return { text: "Usage: /remember <text to save as a note>" };
44
+ }
45
+ log.debug(`/remember: "${text.slice(0, 50)}"`);
46
+ try {
47
+ const title = text.length > 60 ? text.slice(0, 60) : text;
48
+ await client.writeNote(title, text, "agent/memories");
49
+ const preview = text.length > 60 ? `${text.slice(0, 60)}...` : text;
50
+ return { text: `Remembered: "${preview}"` };
51
+ }
52
+ catch (err) {
53
+ log.error("/remember failed", err);
54
+ return {
55
+ text: "Failed to save memory. Is Basic Memory running?",
56
+ };
57
+ }
58
+ },
59
+ });
60
+ api.registerCommand({
61
+ name: "recall",
62
+ description: "Search the Basic Memory knowledge graph",
63
+ acceptsArgs: true,
64
+ requireAuth: true,
65
+ handler: async (ctx) => {
66
+ const query = ctx.args?.trim();
67
+ if (!query) {
68
+ return { text: "Usage: /recall <search query>" };
69
+ }
70
+ log.debug(`/recall: "${query}"`);
71
+ try {
72
+ const results = await client.search(query, 5);
73
+ if (results.length === 0) {
74
+ return { text: `No notes found for: "${query}"` };
75
+ }
76
+ const lines = results.map((r, i) => {
77
+ const score = r.score ? ` (${(r.score * 100).toFixed(0)}%)` : "";
78
+ return `${i + 1}. ${r.title}${score}`;
79
+ });
80
+ return {
81
+ text: `Found ${results.length} notes:\n\n${lines.join("\n")}`,
82
+ };
83
+ }
84
+ catch (err) {
85
+ log.error("/recall failed", err);
86
+ return {
87
+ text: "Failed to search. Is Basic Memory running?",
88
+ };
89
+ }
90
+ },
91
+ });
92
+ }
@@ -0,0 +1,17 @@
1
+ export type BasicMemoryConfig = {
2
+ project: string;
3
+ bmPath: string;
4
+ memoryDir: string;
5
+ memoryFile: string;
6
+ projectPath: string;
7
+ autoCapture: boolean;
8
+ captureMinChars: number;
9
+ autoRecall: boolean;
10
+ recallPrompt: string;
11
+ debug: boolean;
12
+ };
13
+ export declare function resolveProjectPath(projectPath: string, workspaceDir: string): string;
14
+ export declare function parseConfig(raw: unknown): BasicMemoryConfig;
15
+ export declare const basicMemoryConfigSchema: {
16
+ parse: typeof parseConfig;
17
+ };
package/dist/config.js ADDED
@@ -0,0 +1,99 @@
1
+ import { homedir, hostname } from "node:os";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ const ALLOWED_KEYS = [
4
+ "project",
5
+ "bmPath",
6
+ "memoryDir",
7
+ "memory_dir",
8
+ "memoryFile",
9
+ "memory_file",
10
+ "projectPath",
11
+ "autoCapture",
12
+ "captureMinChars",
13
+ "capture_min_chars",
14
+ "autoRecall",
15
+ "auto_recall",
16
+ "recallPrompt",
17
+ "recall_prompt",
18
+ "debug",
19
+ ];
20
+ function assertAllowedKeys(value, allowed, label) {
21
+ const unknown = Object.keys(value).filter((k) => !allowed.includes(k));
22
+ if (unknown.length > 0) {
23
+ throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
24
+ }
25
+ }
26
+ function defaultProject() {
27
+ return `openclaw-${hostname()
28
+ .replace(/[^a-zA-Z0-9-]/g, "-")
29
+ .toLowerCase()}`;
30
+ }
31
+ function expandUserPath(path) {
32
+ if (path === "~")
33
+ return homedir();
34
+ if (path.startsWith("~/"))
35
+ return `${homedir()}/${path.slice(2)}`;
36
+ return path;
37
+ }
38
+ export function resolveProjectPath(projectPath, workspaceDir) {
39
+ const expanded = expandUserPath(projectPath);
40
+ if (isAbsolute(expanded))
41
+ return expanded;
42
+ return resolve(workspaceDir, expanded);
43
+ }
44
+ export function parseConfig(raw) {
45
+ const cfg = raw && typeof raw === "object" && !Array.isArray(raw)
46
+ ? raw
47
+ : {};
48
+ if (Object.keys(cfg).length > 0) {
49
+ assertAllowedKeys(cfg, ALLOWED_KEYS, "basic-memory config");
50
+ }
51
+ // Support both camelCase and snake_case for memory_dir / memory_file
52
+ const memoryDir = typeof cfg.memoryDir === "string" && cfg.memoryDir.length > 0
53
+ ? cfg.memoryDir
54
+ : typeof cfg.memory_dir === "string" &&
55
+ cfg.memory_dir.length > 0
56
+ ? cfg.memory_dir
57
+ : "memory/";
58
+ const memoryFile = typeof cfg.memoryFile === "string" && cfg.memoryFile.length > 0
59
+ ? cfg.memoryFile
60
+ : typeof cfg.memory_file === "string" &&
61
+ cfg.memory_file.length > 0
62
+ ? cfg.memory_file
63
+ : "MEMORY.md";
64
+ return {
65
+ project: typeof cfg.project === "string" && cfg.project.length > 0
66
+ ? cfg.project
67
+ : defaultProject(),
68
+ projectPath: typeof cfg.projectPath === "string" && cfg.projectPath.length > 0
69
+ ? cfg.projectPath
70
+ : ".",
71
+ bmPath: typeof cfg.bmPath === "string" && cfg.bmPath.length > 0
72
+ ? cfg.bmPath
73
+ : "bm",
74
+ memoryDir,
75
+ memoryFile,
76
+ autoCapture: typeof cfg.autoCapture === "boolean" ? cfg.autoCapture : true,
77
+ captureMinChars: typeof cfg.captureMinChars === "number" && cfg.captureMinChars >= 0
78
+ ? cfg.captureMinChars
79
+ : typeof cfg.capture_min_chars === "number" &&
80
+ cfg.capture_min_chars >= 0
81
+ ? cfg.capture_min_chars
82
+ : 10,
83
+ autoRecall: typeof cfg.autoRecall === "boolean"
84
+ ? cfg.autoRecall
85
+ : typeof cfg.auto_recall === "boolean"
86
+ ? cfg.auto_recall
87
+ : true,
88
+ recallPrompt: typeof cfg.recallPrompt === "string" && cfg.recallPrompt.length > 0
89
+ ? cfg.recallPrompt
90
+ : typeof cfg.recall_prompt === "string" &&
91
+ cfg.recall_prompt.length > 0
92
+ ? cfg.recall_prompt
93
+ : "Check for active tasks and recent activity. Summarize anything relevant to the current session.",
94
+ debug: typeof cfg.debug === "boolean" ? cfg.debug : false,
95
+ };
96
+ }
97
+ export const basicMemoryConfigSchema = {
98
+ parse: parseConfig,
99
+ };
@@ -0,0 +1,34 @@
1
+ import type { AssembleResult, BootstrapResult, CompactResult, ContextEngine, SubagentSpawnPreparation } from "openclaw/plugin-sdk";
2
+ import type { BmClient } from "../bm-client.ts";
3
+ import type { BasicMemoryConfig } from "../config.ts";
4
+ export declare const MAX_ASSEMBLE_RECALL_CHARS = 1200;
5
+ type BootstrapParams = Parameters<NonNullable<ContextEngine["bootstrap"]>>[0];
6
+ type AssembleParams = Parameters<ContextEngine["assemble"]>[0];
7
+ type AfterTurnParams = Parameters<NonNullable<ContextEngine["afterTurn"]>>[0];
8
+ type CompactParams = Parameters<ContextEngine["compact"]>[0];
9
+ type PrepareSubagentSpawnParams = Parameters<NonNullable<ContextEngine["prepareSubagentSpawn"]>>[0];
10
+ type OnSubagentEndedParams = Parameters<NonNullable<ContextEngine["onSubagentEnded"]>>[0];
11
+ export declare class BasicMemoryContextEngine implements ContextEngine {
12
+ private readonly client;
13
+ private readonly cfg;
14
+ readonly info: {
15
+ readonly id: "openclaw-basic-memory";
16
+ readonly name: "Basic Memory Context Engine";
17
+ readonly version: "0.1.5";
18
+ readonly ownsCompaction: false;
19
+ };
20
+ private readonly sessionState;
21
+ private readonly subagentState;
22
+ constructor(client: BmClient, cfg: BasicMemoryConfig);
23
+ bootstrap(params: BootstrapParams): Promise<BootstrapResult>;
24
+ ingest(): Promise<{
25
+ ingested: boolean;
26
+ }>;
27
+ assemble(params: AssembleParams): Promise<AssembleResult>;
28
+ afterTurn(params: AfterTurnParams): Promise<void>;
29
+ compact(params: CompactParams): Promise<CompactResult>;
30
+ prepareSubagentSpawn(params: PrepareSubagentSpawnParams): Promise<SubagentSpawnPreparation | undefined>;
31
+ onSubagentEnded(params: OnSubagentEndedParams): Promise<void>;
32
+ dispose(): Promise<void>;
33
+ }
34
+ export {};
@@ -0,0 +1,176 @@
1
+ import { delegateCompactionToRuntime } from "openclaw/plugin-sdk/core";
2
+ import { selectCaptureTurn } from "../hooks/capture.js";
3
+ import { loadRecallState } from "../hooks/recall.js";
4
+ import { log } from "../logger.js";
5
+ export const MAX_ASSEMBLE_RECALL_CHARS = 1200;
6
+ const TRUNCATED_RECALL_SUFFIX = "\n\n[Basic Memory recall truncated]";
7
+ const SUBAGENT_HANDOFF_FOLDER = "agent/subagents";
8
+ const MAX_SUBAGENT_RECALL_CHARS = 800;
9
+ function boundRecallContext(context) {
10
+ if (context.length <= MAX_ASSEMBLE_RECALL_CHARS) {
11
+ return context;
12
+ }
13
+ const trimmed = context
14
+ .slice(0, Math.max(0, MAX_ASSEMBLE_RECALL_CHARS - TRUNCATED_RECALL_SUFFIX.length))
15
+ .trimEnd();
16
+ return `${trimmed}${TRUNCATED_RECALL_SUFFIX}`;
17
+ }
18
+ function slugifySessionKey(sessionKey) {
19
+ return sessionKey
20
+ .toLowerCase()
21
+ .replace(/[^a-z0-9]+/g, "-")
22
+ .replace(/^-+|-+$/g, "")
23
+ .slice(0, 80);
24
+ }
25
+ function buildSubagentNoteTitle(childSessionKey) {
26
+ return `subagent-handoff-${slugifySessionKey(childSessionKey)}`;
27
+ }
28
+ function buildSubagentHandoffContent(params) {
29
+ const sections = [
30
+ "# Subagent Handoff",
31
+ "",
32
+ "## Sessions",
33
+ `- Parent: ${params.parentSessionKey}`,
34
+ `- Child: ${params.childSessionKey}`,
35
+ "",
36
+ "## Lifecycle",
37
+ `- Spawned: ${new Date().toISOString()}`,
38
+ ];
39
+ if (params.recallContext) {
40
+ sections.push("", "## Parent Basic Memory Context", params.recallContext.slice(0, MAX_SUBAGENT_RECALL_CHARS).trimEnd());
41
+ }
42
+ return sections.join("\n");
43
+ }
44
+ function buildSubagentCompletionUpdate(params) {
45
+ const statusLine = params.reason === "completed"
46
+ ? "Child run completed. Durable conversation capture continues through the normal afterTurn path."
47
+ : `Child run ended with reason: ${params.reason}.`;
48
+ return [
49
+ "",
50
+ "## Completion",
51
+ `- Child: ${params.childSessionKey}`,
52
+ `- Ended: ${new Date().toISOString()}`,
53
+ `- Reason: ${params.reason}`,
54
+ "",
55
+ statusLine,
56
+ ].join("\n");
57
+ }
58
+ export class BasicMemoryContextEngine {
59
+ client;
60
+ cfg;
61
+ info = {
62
+ id: "openclaw-basic-memory",
63
+ name: "Basic Memory Context Engine",
64
+ version: "0.1.5",
65
+ ownsCompaction: false,
66
+ };
67
+ sessionState = new Map();
68
+ subagentState = new Map();
69
+ constructor(client, cfg) {
70
+ this.client = client;
71
+ this.cfg = cfg;
72
+ }
73
+ async bootstrap(params) {
74
+ if (!this.cfg.autoRecall) {
75
+ this.sessionState.delete(params.sessionId);
76
+ return { bootstrapped: false, reason: "autoRecall disabled" };
77
+ }
78
+ try {
79
+ const recall = await loadRecallState(this.client, this.cfg);
80
+ if (!recall) {
81
+ this.sessionState.delete(params.sessionId);
82
+ return { bootstrapped: false, reason: "no recall context found" };
83
+ }
84
+ this.sessionState.set(params.sessionId, {
85
+ recallContext: boundRecallContext(recall.context),
86
+ });
87
+ log.debug(`context-engine bootstrap: session=${params.sessionId} tasks=${recall.tasks.length} recent=${recall.recent.length}`);
88
+ return { bootstrapped: true };
89
+ }
90
+ catch (err) {
91
+ this.sessionState.delete(params.sessionId);
92
+ log.error("context-engine bootstrap failed", err);
93
+ return { bootstrapped: false, reason: "recall failed" };
94
+ }
95
+ }
96
+ async ingest() {
97
+ return { ingested: false };
98
+ }
99
+ async assemble(params) {
100
+ const state = this.sessionState.get(params.sessionId);
101
+ return {
102
+ messages: params.messages,
103
+ estimatedTokens: 0,
104
+ systemPromptAddition: state?.recallContext,
105
+ };
106
+ }
107
+ async afterTurn(params) {
108
+ if (!this.cfg.autoCapture)
109
+ return;
110
+ const newMessages = params.messages.slice(params.prePromptMessageCount);
111
+ const turn = selectCaptureTurn(newMessages, this.cfg.captureMinChars) ??
112
+ selectCaptureTurn(params.messages, this.cfg.captureMinChars);
113
+ if (!turn)
114
+ return;
115
+ log.debug(`context-engine afterTurn: session=${params.sessionId} user=${turn.userText.length} assistant=${turn.assistantText.length}`);
116
+ try {
117
+ await this.client.indexConversation(turn.userText, turn.assistantText);
118
+ }
119
+ catch (err) {
120
+ log.error("context-engine capture failed", err);
121
+ }
122
+ }
123
+ async compact(params) {
124
+ return delegateCompactionToRuntime(params);
125
+ }
126
+ async prepareSubagentSpawn(params) {
127
+ const parentState = this.sessionState.get(params.parentSessionKey);
128
+ const noteTitle = buildSubagentNoteTitle(params.childSessionKey);
129
+ try {
130
+ const note = await this.client.writeNote(noteTitle, buildSubagentHandoffContent({
131
+ parentSessionKey: params.parentSessionKey,
132
+ childSessionKey: params.childSessionKey,
133
+ recallContext: parentState?.recallContext,
134
+ }), SUBAGENT_HANDOFF_FOLDER);
135
+ this.subagentState.set(params.childSessionKey, {
136
+ noteIdentifier: note.permalink,
137
+ noteTitle: note.title,
138
+ parentSessionKey: params.parentSessionKey,
139
+ });
140
+ return {
141
+ rollback: async () => {
142
+ const handoff = this.subagentState.get(params.childSessionKey);
143
+ this.subagentState.delete(params.childSessionKey);
144
+ if (!handoff)
145
+ return;
146
+ try {
147
+ await this.client.deleteNote(handoff.noteIdentifier);
148
+ }
149
+ catch (err) {
150
+ log.error("context-engine subagent rollback failed", err);
151
+ }
152
+ },
153
+ };
154
+ }
155
+ catch (err) {
156
+ log.error("context-engine prepareSubagentSpawn failed", err);
157
+ return undefined;
158
+ }
159
+ }
160
+ async onSubagentEnded(params) {
161
+ const handoff = this.subagentState.get(params.childSessionKey);
162
+ if (!handoff)
163
+ return;
164
+ this.subagentState.delete(params.childSessionKey);
165
+ try {
166
+ await this.client.editNote(handoff.noteIdentifier, "append", buildSubagentCompletionUpdate(params));
167
+ }
168
+ catch (err) {
169
+ log.error("context-engine onSubagentEnded failed", err);
170
+ }
171
+ }
172
+ async dispose() {
173
+ this.sessionState.clear();
174
+ this.subagentState.clear();
175
+ }
176
+ }
@@ -0,0 +1,24 @@
1
+ import type { BmClient } from "../bm-client.ts";
2
+ import type { BasicMemoryConfig } from "../config.ts";
3
+ /**
4
+ * Extract text content from a message object.
5
+ */
6
+ export declare function extractText(msg: Record<string, unknown>): string;
7
+ /**
8
+ * Find the last user+assistant turn from the messages array.
9
+ */
10
+ export declare function getLastTurn(messages: unknown[]): {
11
+ userText: string;
12
+ assistantText: string;
13
+ };
14
+ export declare function selectCaptureTurn(messages: unknown[], minChars: number): {
15
+ userText: string;
16
+ assistantText: string;
17
+ } | null;
18
+ /**
19
+ * Build the post-turn capture handler for Mode B.
20
+ *
21
+ * After each agent turn, extracts the conversation content and indexes it
22
+ * into the Basic Memory knowledge graph as a conversation note.
23
+ */
24
+ export declare function buildCaptureHandler(client: BmClient, cfg: BasicMemoryConfig): (event: Record<string, unknown>) => Promise<void>;
@@ -0,0 +1,88 @@
1
+ import { log } from "../logger.js";
2
+ /**
3
+ * Extract text content from a message object.
4
+ */
5
+ export function extractText(msg) {
6
+ const content = msg.content;
7
+ if (typeof content === "string")
8
+ return content;
9
+ if (Array.isArray(content)) {
10
+ const parts = [];
11
+ for (const block of content) {
12
+ if (!block || typeof block !== "object")
13
+ continue;
14
+ const b = block;
15
+ if (b.type === "text" && typeof b.text === "string") {
16
+ parts.push(b.text);
17
+ }
18
+ }
19
+ return parts.join("\n");
20
+ }
21
+ return "";
22
+ }
23
+ /**
24
+ * Find the last user+assistant turn from the messages array.
25
+ */
26
+ export function getLastTurn(messages) {
27
+ let lastUserIdx = -1;
28
+ for (let i = messages.length - 1; i >= 0; i--) {
29
+ const msg = messages[i];
30
+ if (msg &&
31
+ typeof msg === "object" &&
32
+ msg.role === "user") {
33
+ lastUserIdx = i;
34
+ break;
35
+ }
36
+ }
37
+ if (lastUserIdx < 0)
38
+ return { userText: "", assistantText: "" };
39
+ let userText = "";
40
+ let assistantText = "";
41
+ for (let i = lastUserIdx; i < messages.length; i++) {
42
+ const msg = messages[i];
43
+ if (!msg?.role)
44
+ continue;
45
+ const text = extractText(msg);
46
+ if (msg.role === "user")
47
+ userText = text;
48
+ else if (msg.role === "assistant")
49
+ assistantText = text;
50
+ }
51
+ return { userText, assistantText };
52
+ }
53
+ export function selectCaptureTurn(messages, minChars) {
54
+ const turn = getLastTurn(messages);
55
+ if (!turn.userText && !turn.assistantText)
56
+ return null;
57
+ if (turn.userText.length < minChars && turn.assistantText.length < minChars) {
58
+ return null;
59
+ }
60
+ return turn;
61
+ }
62
+ /**
63
+ * Build the post-turn capture handler for Mode B.
64
+ *
65
+ * After each agent turn, extracts the conversation content and indexes it
66
+ * into the Basic Memory knowledge graph as a conversation note.
67
+ */
68
+ export function buildCaptureHandler(client, cfg) {
69
+ const minChars = cfg.captureMinChars;
70
+ return async (event) => {
71
+ if (!event.success ||
72
+ !Array.isArray(event.messages) ||
73
+ event.messages.length === 0) {
74
+ return;
75
+ }
76
+ const turn = selectCaptureTurn(event.messages, minChars);
77
+ if (!turn)
78
+ return;
79
+ const { userText, assistantText } = turn;
80
+ log.debug(`capturing conversation: user=${userText.length} chars, assistant=${assistantText.length} chars`);
81
+ try {
82
+ await client.indexConversation(userText, assistantText);
83
+ }
84
+ catch (err) {
85
+ log.error("capture failed", err);
86
+ }
87
+ };
88
+ }
@@ -0,0 +1,24 @@
1
+ import type { BmClient, RecentResult, SearchResult } from "../bm-client.ts";
2
+ import type { BasicMemoryConfig } from "../config.ts";
3
+ export interface RecallState {
4
+ tasks: SearchResult[];
5
+ recent: RecentResult[];
6
+ context: string;
7
+ }
8
+ /**
9
+ * Format recalled context from active tasks and recent activity.
10
+ * Returns empty string if nothing was found.
11
+ */
12
+ export declare function formatRecallContext(tasks: SearchResult[], recent: RecentResult[], prompt: string): string;
13
+ export declare function loadRecallState(client: BmClient, cfg: BasicMemoryConfig): Promise<RecallState | null>;
14
+ /**
15
+ * Build the pre-turn recall handler.
16
+ *
17
+ * On agent_start, queries active tasks and recent activity from Basic Memory,
18
+ * then returns formatted context for injection into the conversation.
19
+ */
20
+ export declare function buildRecallHandler(client: BmClient, cfg: BasicMemoryConfig): (_event: Record<string, unknown>) => Promise<{
21
+ context?: undefined;
22
+ } | {
23
+ context: string;
24
+ }>;