@log0u7/opencode-memory 0.2.0 → 0.4.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.
package/dist/plugin-v2.js CHANGED
@@ -1,48 +1,63 @@
1
1
  import { Plugin } from "@opencode/plugin";
2
2
  import { openMemoryDb } from "./lib/db.js";
3
- import { forget, list, save, search, SecretDetectedError } from "./lib/store.js";
4
- import { SYSTEM_GUIDANCE, COMPACTION_CONTEXT, defaultDbPath, } from "./plugin.js";
3
+ import { TOOL_DESCRIPTIONS, createMemoryOps, memoryInventory, oncePerSession, } from "./lib/memory-ops.js";
4
+ import { createReflection, parseReflectionConfig } from "./lib/reflection.js";
5
+ import { SYSTEM_GUIDANCE, COMPACTION_CONTEXT, defaultScope, defaultDbPath, } from "./plugin.js";
5
6
  const textPart = (text) => ({ type: "text", text });
7
+ /** Reconnect backoff for the v2 event stream: short first retry, capped growth. */
8
+ const RECONNECT_MIN_MS = 250;
9
+ const RECONNECT_MAX_MS = 5000;
10
+ /** Session tail as plain text: user messages verbatim, assistant text parts joined. */
11
+ async function transcriptV2(ctx, sessionID) {
12
+ const messages = await ctx.session.context({ sessionID });
13
+ const lines = [];
14
+ for (const message of messages) {
15
+ if (message.type === "user") {
16
+ lines.push(`user: ${message.text}`);
17
+ }
18
+ else if (message.type === "assistant") {
19
+ const text = message.content
20
+ .filter((part) => part.type === "text")
21
+ .map((part) => part.text)
22
+ .join(" ")
23
+ .trim();
24
+ if (text) {
25
+ lines.push(`assistant: ${text}`);
26
+ }
27
+ }
28
+ }
29
+ return lines.join("\n");
30
+ }
6
31
  export const MemoryV2 = Plugin.define({
7
32
  id: "@log0u7/opencode-memory",
8
33
  async setup(ctx) {
9
34
  const options = ctx.options;
35
+ const dbPath = defaultDbPath(options);
10
36
  let db = null;
37
+ let disposed = false;
38
+ // After cleanup the store is closed for good: a late path (tool call, in-flight
39
+ // reflection) must not silently reopen a connection that nobody closes again.
11
40
  const getDb = () => {
12
- if (!db) {
13
- db = openMemoryDb(defaultDbPath(options));
41
+ if (disposed) {
42
+ throw new Error("memory store is closed");
14
43
  }
44
+ db ??= openMemoryDb(dbPath);
15
45
  return db;
16
46
  };
17
- // Tool results are shown to the model: caught errors become readable text, so
18
- // a rejected save is guidance instead of a crashed tool call.
19
- const run = async (fn) => {
20
- try {
21
- return fn();
22
- }
23
- catch (error) {
24
- if (error instanceof SecretDetectedError) {
25
- return "refused: the content looks like a credential; secrets must never be stored in memory";
26
- }
27
- if (error instanceof Error) {
28
- return `error: ${error.message}`;
29
- }
30
- throw error;
31
- }
32
- };
33
- // v1 scoped entries per session worktree; the v2 tool context carries no
34
- // worktree, so entries default to the plugin location's directory.
35
- const defaultScope = ctx.location.directory;
47
+ const ops = createMemoryOps(getDb);
48
+ // Request and compaction hooks carry no directory: the instance scope is the project.
49
+ const projectScope = defaultScope(undefined, ctx.location.directory);
50
+ const injected = oncePerSession();
36
51
  await ctx.tool.transform((editor) => {
37
52
  editor.add({
38
53
  name: "memory_save",
39
- description: "Save a durable entry to shared working memory (decisions, facts, preferences, gotchas). Never store credentials.",
54
+ description: TOOL_DESCRIPTIONS.save,
40
55
  input: {
41
56
  type: "object",
42
57
  properties: {
43
58
  kind: {
44
59
  type: "string",
45
- description: "Entry type, e.g. decision, fact, preference, gotcha",
60
+ description: "Entry type, e.g. decision, fact, preference, gotcha, lesson",
46
61
  },
47
62
  title: { type: "string", description: "Short title (max 120 chars)" },
48
63
  body: { type: "string", description: "Entry content (max 8000 chars)" },
@@ -56,23 +71,13 @@ export const MemoryV2 = Plugin.define({
56
71
  async execute(args) {
57
72
  const a = args;
58
73
  return {
59
- content: await run(() => {
60
- const entry = save(getDb(), {
61
- scope: a.scope ?? defaultScope,
62
- kind: a.kind,
63
- title: a.title,
64
- body: a.body,
65
- tags: a.tags ?? [],
66
- expires_at: a.expires_at ?? null,
67
- });
68
- return `saved ${entry.id} (${entry.scope})`;
69
- }),
74
+ content: ops.save(a, defaultScope(undefined, a.scope ?? ctx.location.directory)),
70
75
  };
71
76
  },
72
77
  });
73
78
  editor.add({
74
79
  name: "memory_search",
75
- description: "Search shared working memory (all sessions on this machine). Results include this project's scope and global entries.",
80
+ description: TOOL_DESCRIPTIONS.search,
76
81
  input: {
77
82
  type: "object",
78
83
  properties: {
@@ -86,23 +91,13 @@ export const MemoryV2 = Plugin.define({
86
91
  async execute(args) {
87
92
  const a = args;
88
93
  return {
89
- content: await run(() => {
90
- const hits = search(getDb(), {
91
- query: a.query,
92
- scope: a.scope ?? defaultScope,
93
- ...(a.limit !== undefined ? { limit: a.limit } : {}),
94
- });
95
- if (hits.length === 0) {
96
- return "no matching memory entries";
97
- }
98
- return JSON.stringify(hits, null, 2);
99
- }),
94
+ content: ops.search({ query: a.query, ...(a.limit !== undefined ? { limit: a.limit } : {}) }, defaultScope(undefined, a.scope ?? ctx.location.directory)),
100
95
  };
101
96
  },
102
97
  });
103
98
  editor.add({
104
99
  name: "memory_list",
105
- description: "List recent memory entries for a scope (default: the project directory), newest first.",
100
+ description: TOOL_DESCRIPTIONS.list,
106
101
  input: {
107
102
  type: "object",
108
103
  properties: {
@@ -114,22 +109,13 @@ export const MemoryV2 = Plugin.define({
114
109
  async execute(args) {
115
110
  const a = args;
116
111
  return {
117
- content: await run(() => {
118
- const entries = list(getDb(), {
119
- scope: a.scope ?? defaultScope,
120
- ...(a.limit !== undefined ? { limit: a.limit } : {}),
121
- });
122
- if (entries.length === 0) {
123
- return "no memory entries";
124
- }
125
- return JSON.stringify(entries, null, 2);
126
- }),
112
+ content: ops.list(a.limit !== undefined ? { limit: a.limit } : {}, defaultScope(undefined, a.scope ?? ctx.location.directory)),
127
113
  };
128
114
  },
129
115
  });
130
116
  editor.add({
131
117
  name: "memory_forget",
132
- description: "Delete memory entries by id, or all entries matching a query.",
118
+ description: TOOL_DESCRIPTIONS.forget,
133
119
  input: {
134
120
  type: "object",
135
121
  properties: {
@@ -140,35 +126,109 @@ export const MemoryV2 = Plugin.define({
140
126
  },
141
127
  async execute(args) {
142
128
  const a = args;
143
- return {
144
- content: await run(() => {
145
- if (a.id !== undefined) {
146
- const deleted = forget(getDb(), { id: a.id });
147
- return `deleted ${deleted} entr${deleted === 1 ? "y" : "ies"}`;
148
- }
149
- if (a.query !== undefined) {
150
- const deleted = forget(getDb(), { query: a.query });
151
- return `deleted ${deleted} entr${deleted === 1 ? "y" : "ies"}`;
152
- }
153
- return "provide id or query";
154
- }),
155
- };
129
+ return { content: ops.forget(a, defaultScope(undefined, ctx.location.directory)) };
130
+ },
131
+ });
132
+ editor.add({
133
+ name: "memory_stats",
134
+ description: TOOL_DESCRIPTIONS.stats,
135
+ input: {
136
+ type: "object",
137
+ properties: {
138
+ scope: { type: "string", description: "Scope key; defaults to the project directory" },
139
+ },
140
+ additionalProperties: false,
141
+ },
142
+ async execute(args) {
143
+ const a = args;
144
+ return { content: ops.stats(defaultScope(undefined, a.scope ?? ctx.location.directory)) };
156
145
  },
157
146
  });
158
147
  });
159
- // experimental.chat.system.transform -> context hook (edit event.system)
148
+ // The v1 system transform maps to the v2 context hook: guidance first, then the
149
+ // once-per-session inventory.
160
150
  await ctx.session.hook("context", (event) => {
161
151
  event.system.push(textPart(SYSTEM_GUIDANCE));
152
+ if (disposed) {
153
+ return;
154
+ }
155
+ const inventory = injected.inject(event.sessionID, () => memoryInventory(getDb, dbPath, projectScope));
156
+ if (inventory !== null) {
157
+ event.system.push(textPart(inventory));
158
+ }
162
159
  });
163
160
  // experimental.session.compacting -> compaction hook
164
161
  await ctx.session.hook("compaction", (event) => {
165
162
  event.system.push(textPart(COMPACTION_CONTEXT));
163
+ if (disposed) {
164
+ return;
165
+ }
166
+ const inventory = memoryInventory(getDb, dbPath, projectScope);
167
+ if (inventory !== null) {
168
+ event.system.push(textPart(inventory));
169
+ }
166
170
  });
167
171
  const cleanup = async () => {
172
+ disposed = true;
173
+ injected.reset();
168
174
  db?.close();
169
175
  db = null;
170
176
  };
171
- return cleanup;
177
+ const reflection = parseReflectionConfig(options.reflection);
178
+ if (!reflection) {
179
+ return cleanup;
180
+ }
181
+ const onIdle = createReflection(reflection, {
182
+ isSubagent: async (sessionID) => {
183
+ const session = await ctx.session.get({ sessionID });
184
+ return !!session.parentID;
185
+ },
186
+ transcript: (sessionID) => transcriptV2(ctx, sessionID),
187
+ generate: async (prompt, _sessionID, signal) => {
188
+ const model = modelRefV2(reflection.model);
189
+ const result = await ctx.generate.text({ prompt, ...(model ? { model } : {}) }, { signal });
190
+ return result.text;
191
+ },
192
+ saveLesson: (title, body) => {
193
+ ops.save({ kind: "lesson", title, body }, projectScope);
194
+ },
195
+ });
196
+ // The host keeps the event stream alive through this controller; aborting it stops
197
+ // the idle loop without touching the tool or session hooks. A stream that errors is
198
+ // retried with a capped backoff instead of ending the loop for good.
199
+ const controller = new AbortController();
200
+ void (async () => {
201
+ let delay = RECONNECT_MIN_MS;
202
+ while (!controller.signal.aborted) {
203
+ let failed = false;
204
+ try {
205
+ for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
206
+ if (event.type === "session.idle") {
207
+ onIdle(event.data.sessionID);
208
+ }
209
+ }
210
+ }
211
+ catch {
212
+ failed = true;
213
+ }
214
+ if (!failed || controller.signal.aborted) {
215
+ break;
216
+ }
217
+ await new Promise((resolve) => setTimeout(resolve, delay));
218
+ delay = Math.min(delay * 2, RECONNECT_MAX_MS);
219
+ }
220
+ })();
221
+ return async () => {
222
+ controller.abort();
223
+ await cleanup();
224
+ };
172
225
  },
173
226
  });
227
+ function modelRefV2(model) {
228
+ if (!model) {
229
+ return undefined;
230
+ }
231
+ const [providerID, id] = model.split("/", 2);
232
+ return providerID && id ? { providerID, id } : undefined;
233
+ }
174
234
  //# sourceMappingURL=plugin-v2.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin-v2.js","sourceRoot":"","sources":["../src/plugin-v2.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plugin-v2.js","sourceRoot":"","sources":["../src/plugin-v2.ts"],"names":[],"mappings":"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"}
package/dist/plugin.d.ts CHANGED
@@ -1,9 +1,16 @@
1
1
  import type { Plugin } from "@opencode-ai/plugin";
2
2
  export type PluginOptions = {
3
3
  dbPath?: string;
4
+ reflection?: unknown;
4
5
  };
5
- export declare const SYSTEM_GUIDANCE = "You have persistent working memory shared with all OpenCode sessions on this machine.\nAt the start of a non-trivial task, run memory_search for relevant decisions and facts about this project.\nWhen you make a durable decision or learn a lasting fact, call memory_save (kinds: decision, fact, preference, gotcha).\nNever store credentials, tokens, or API keys in memory; the store refuses them.";
6
+ export declare const REFLECTION_SESSION_TITLE = "[memory-reflect] lessons";
7
+ export declare const SYSTEM_GUIDANCE = "You have persistent working memory shared with all OpenCode sessions on this machine.\nRecent entries are listed above when present; run memory_search for anything older or more specific.\nWhen you make a durable decision or learn a lasting fact, call memory_save (kinds: decision, fact, preference, gotcha).\nWhen a non-obvious approach clearly fails or succeeds, save the lesson (kind=lesson) as soon as it is confirmed, and check stored lessons before retrying a failed approach.\nNever store credentials, tokens, or API keys in memory; the store refuses them.";
6
8
  export declare const COMPACTION_CONTEXT: string;
7
9
  export declare function defaultDbPath(options?: PluginOptions): string;
10
+ /**
11
+ * Scope of a session: the v1 tool context carries a worktree, so that is the canonical
12
+ * key. `directory` is the fallback so the same project does not get two scopes.
13
+ */
14
+ export declare function defaultScope(worktree?: string, directory?: string): string;
8
15
  export declare const MemoryPlugin: Plugin;
9
16
  //# sourceMappingURL=plugin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAMzD,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,eAAe,gZAGoD,CAAC;AAEjF,eAAO,MAAM,kBAAkB,QAEoE,CAAC;AAEpG,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAM7D;AAED,eAAO,MAAM,YAAY,EAAE,MA6H1B,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAiBtE,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,wBAAwB,6BAA6B,CAAC;AAqBnE,eAAO,MAAM,eAAe,wjBAIoD,CAAC;AAEjF,eAAO,MAAM,kBAAkB,QAG0B,CAAC;AAE1D,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,aAAa,GAAG,MAAM,CAM7D;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1E;AA8DD,eAAO,MAAM,YAAY,EAAE,MA4I1B,CAAC"}
package/dist/plugin.js CHANGED
@@ -1,12 +1,32 @@
1
- import { openMemoryDb } from "./lib/db.js";
2
- import { forget, list, save, search, SecretDetectedError } from "./lib/store.js";
3
1
  import { tool } from "@opencode-ai/plugin";
2
+ import { openMemoryDb } from "./lib/db.js";
3
+ import { TOOL_DESCRIPTIONS, createMemoryOps, memoryInventory, oncePerSession, } from "./lib/memory-ops.js";
4
+ import { parseReflectionConfig, createReflection, } from "./lib/reflection.js";
5
+ import { canonicalScope } from "./lib/store.js";
6
+ export const REFLECTION_SESSION_TITLE = "[memory-reflect] lessons";
7
+ const REFLECTION_SYSTEM = "You extract durable lessons for a memory store. Reply with the JSON array only; do not call any tool.";
8
+ function transcriptText(messages, chars) {
9
+ const lines = [];
10
+ for (const { info, parts } of messages) {
11
+ const text = parts
12
+ .filter((part) => part.type === "text")
13
+ .map((part) => part.text ?? "")
14
+ .join(" ")
15
+ .trim();
16
+ if (text) {
17
+ lines.push(`${info.role}: ${text}`);
18
+ }
19
+ }
20
+ return lines.join("\n").slice(-chars);
21
+ }
4
22
  export const SYSTEM_GUIDANCE = `You have persistent working memory shared with all OpenCode sessions on this machine.
5
- At the start of a non-trivial task, run memory_search for relevant decisions and facts about this project.
23
+ Recent entries are listed above when present; run memory_search for anything older or more specific.
6
24
  When you make a durable decision or learn a lasting fact, call memory_save (kinds: decision, fact, preference, gotcha).
25
+ When a non-obvious approach clearly fails or succeeds, save the lesson (kind=lesson) as soon as it is confirmed, and check stored lessons before retrying a failed approach.
7
26
  Never store credentials, tokens, or API keys in memory; the store refuses them.`;
8
27
  export const COMPACTION_CONTEXT = "The session is being compacted. After compaction, call memory_save for every durable decision still relevant " +
9
- "(kind=decision) and any newly learned facts (kind=fact), so other sessions keep this knowledge.";
28
+ "(kind=decision), any newly learned facts (kind=fact), and every reusable lesson confirmed this session " +
29
+ "(kind=lesson), so other sessions keep this knowledge.";
10
30
  export function defaultDbPath(options) {
11
31
  if (typeof options?.dbPath === "string" && options.dbPath.length > 0) {
12
32
  return options.dbPath;
@@ -14,22 +34,93 @@ export function defaultDbPath(options) {
14
34
  const dataHome = process.env.XDG_DATA_HOME ?? `${process.env.HOME ?? ""}/.local/share`;
15
35
  return `${dataHome}/opencode-memory/memory.db`;
16
36
  }
17
- export const MemoryPlugin = async (_input, options) => {
37
+ /**
38
+ * Scope of a session: the v1 tool context carries a worktree, so that is the canonical
39
+ * key. `directory` is the fallback so the same project does not get two scopes.
40
+ */
41
+ export function defaultScope(worktree, directory) {
42
+ return canonicalScope(worktree, directory);
43
+ }
44
+ /** One ephemeral child session per reflection: the transcript tail is distilled with a
45
+ * synchronous prompt on that child (system pins the model to JSON-only, no tools), the
46
+ * reply is parsed here, and the child is deleted. Fire-and-forget: the host event loop
47
+ * never waits on the LLM. */
48
+ function reflectionDeps(client, config, ops, projectScope) {
49
+ const model = modelRef(config.model);
50
+ return {
51
+ isSubagent: async (sessionID) => {
52
+ const session = await client.session.get({ path: { id: sessionID } });
53
+ return !!session.data?.parentID;
54
+ },
55
+ transcript: async (sessionID) => {
56
+ const result = await client.session.messages({ path: { id: sessionID } });
57
+ return transcriptText(result.data ?? [], config.transcriptChars);
58
+ },
59
+ generate: async (prompt, sessionID, signal) => {
60
+ const child = await client.session.create({
61
+ body: { parentID: sessionID, title: REFLECTION_SESSION_TITLE },
62
+ });
63
+ const id = child.data?.id;
64
+ if (!id) {
65
+ throw new Error("reflection session could not be created");
66
+ }
67
+ try {
68
+ const reply = await client.session.prompt({
69
+ path: { id },
70
+ body: {
71
+ parts: [{ type: "text", text: prompt }],
72
+ system: REFLECTION_SYSTEM,
73
+ ...(model ? { model } : {}),
74
+ },
75
+ signal,
76
+ });
77
+ return (reply.data?.parts ?? [])
78
+ .filter((part) => part.type === "text")
79
+ .map((part) => part.text ?? "")
80
+ .join("\n");
81
+ }
82
+ finally {
83
+ await client.session.delete({ path: { id } }).catch(() => { });
84
+ }
85
+ },
86
+ saveLesson: (title, body) => {
87
+ ops.save({ kind: "lesson", title, body }, projectScope);
88
+ },
89
+ };
90
+ }
91
+ function modelRef(model) {
92
+ if (!model) {
93
+ return undefined;
94
+ }
95
+ const [providerID, modelID] = model.split("/", 2);
96
+ return providerID && modelID ? { providerID, modelID } : undefined;
97
+ }
98
+ export const MemoryPlugin = async (input, options) => {
99
+ const dbPath = defaultDbPath(options);
18
100
  let db = null;
101
+ let disposed = false;
102
+ // After dispose the store is closed for good: a late path (tool call, in-flight
103
+ // reflection) must not silently reopen a connection that nobody closes again.
19
104
  const getDb = () => {
20
- if (!db) {
21
- db = openMemoryDb(defaultDbPath(options));
105
+ if (disposed) {
106
+ throw new Error("memory store is closed");
22
107
  }
108
+ db ??= openMemoryDb(dbPath);
23
109
  return db;
24
110
  };
111
+ const ops = createMemoryOps(getDb);
112
+ // Request and compaction hooks carry no directory: the instance scope is the project.
113
+ const projectScope = defaultScope(input.worktree, input.directory);
114
+ const injected = oncePerSession();
115
+ const reflection = parseReflectionConfig(options?.reflection);
25
116
  const hooks = {
26
117
  tool: {
27
118
  memory_save: tool({
28
- description: "Save a durable entry to shared working memory (decisions, facts, preferences, gotchas). Never store credentials.",
119
+ description: TOOL_DESCRIPTIONS.save,
29
120
  args: {
30
121
  kind: tool.schema
31
122
  .string()
32
- .describe("Entry type, e.g. decision, fact, preference, gotcha"),
123
+ .describe("Entry type, e.g. decision, fact, preference, gotcha, lesson"),
33
124
  title: tool.schema.string().describe("Short title (max 120 chars)"),
34
125
  body: tool.schema.string().describe("Entry content (max 8000 chars)"),
35
126
  scope: tool.schema
@@ -39,20 +130,10 @@ export const MemoryPlugin = async (_input, options) => {
39
130
  tags: tool.schema.array(tool.schema.string()).optional().describe("Optional tags"),
40
131
  expires_at: tool.schema.number().optional().describe("Optional epoch ms expiry"),
41
132
  },
42
- execute: (args, context) => run(() => {
43
- const entry = save(getDb(), {
44
- scope: args.scope ?? context.worktree,
45
- kind: args.kind,
46
- title: args.title,
47
- body: args.body,
48
- tags: args.tags ?? [],
49
- expires_at: args.expires_at ?? null,
50
- });
51
- return `saved ${entry.id} (${entry.scope})`;
52
- }),
133
+ execute: async (args, context) => ops.save(args, defaultScope(args.scope ?? context.worktree ?? context.directory)),
53
134
  }),
54
135
  memory_search: tool({
55
- description: "Search shared working memory (all sessions on this machine). Results include this project's scope and global entries.",
136
+ description: TOOL_DESCRIPTIONS.search,
56
137
  args: {
57
138
  query: tool.schema.string().describe("Free-text query"),
58
139
  scope: tool.schema
@@ -61,20 +142,10 @@ export const MemoryPlugin = async (_input, options) => {
61
142
  .describe("Scope key; defaults to this session's worktree"),
62
143
  limit: tool.schema.number().optional().describe("Max results (default 10)"),
63
144
  },
64
- execute: (args, context) => run(() => {
65
- const hits = search(getDb(), {
66
- query: args.query,
67
- scope: args.scope ?? context.worktree,
68
- ...(args.limit !== undefined ? { limit: args.limit } : {}),
69
- });
70
- if (hits.length === 0) {
71
- return "no matching memory entries";
72
- }
73
- return JSON.stringify(hits, null, 2);
74
- }),
145
+ execute: async (args, context) => ops.search({ query: args.query, ...(args.limit !== undefined ? { limit: args.limit } : {}) }, defaultScope(args.scope ?? context.worktree ?? context.directory)),
75
146
  }),
76
147
  memory_list: tool({
77
- description: "List recent memory entries for a scope (default: this session's worktree), newest first.",
148
+ description: TOOL_DESCRIPTIONS.list,
78
149
  args: {
79
150
  scope: tool.schema
80
151
  .string()
@@ -82,65 +153,64 @@ export const MemoryPlugin = async (_input, options) => {
82
153
  .describe("Scope key; defaults to this session's worktree"),
83
154
  limit: tool.schema.number().optional().describe("Max results (default 20)"),
84
155
  },
85
- execute: (args, context) => run(() => {
86
- const entries = list(getDb(), {
87
- scope: args.scope ?? context.worktree,
88
- ...(args.limit !== undefined ? { limit: args.limit } : {}),
89
- });
90
- if (entries.length === 0) {
91
- return "no memory entries";
92
- }
93
- return JSON.stringify(entries, null, 2);
94
- }),
156
+ execute: async (args, context) => ops.list(args.limit !== undefined ? { limit: args.limit } : {}, defaultScope(args.scope ?? context.worktree ?? context.directory)),
95
157
  }),
96
158
  memory_forget: tool({
97
- description: "Delete memory entries by id, or all entries matching a query.",
159
+ description: TOOL_DESCRIPTIONS.forget,
98
160
  args: {
99
161
  id: tool.schema.string().optional().describe("Entry id to delete"),
100
- query: tool.schema.string().optional().describe("Delete every entry matching this query"),
162
+ query: tool.schema.string().optional().describe("Delete every entry matching a query"),
163
+ },
164
+ execute: async (args, context) => ops.forget(args, defaultScope(context.worktree, context.directory)),
165
+ }),
166
+ memory_stats: tool({
167
+ description: TOOL_DESCRIPTIONS.stats,
168
+ args: {
169
+ scope: tool.schema
170
+ .string()
171
+ .optional()
172
+ .describe("Scope key; defaults to this session's worktree"),
101
173
  },
102
- execute: (args, _context) => run(() => {
103
- if (args.id !== undefined) {
104
- const deleted = forget(getDb(), { id: args.id });
105
- return `deleted ${deleted} entr${deleted === 1 ? "y" : "ies"}`;
106
- }
107
- if (args.query !== undefined) {
108
- const deleted = forget(getDb(), { query: args.query });
109
- return `deleted ${deleted} entr${deleted === 1 ? "y" : "ies"}`;
110
- }
111
- return "provide id or query";
112
- }),
174
+ execute: async (args, context) => ops.stats(defaultScope(args.scope ?? context.worktree ?? context.directory)),
113
175
  }),
114
176
  },
115
177
  };
178
+ if (reflection) {
179
+ const onIdle = createReflection(reflection, reflectionDeps(input.client, reflection, ops, projectScope));
180
+ hooks.event = async ({ event }) => {
181
+ if (event.type === "session.idle") {
182
+ onIdle(event.properties.sessionID);
183
+ }
184
+ };
185
+ }
116
186
  return {
117
187
  ...hooks,
118
- "experimental.chat.system.transform": async (_input, output) => {
188
+ "experimental.chat.system.transform": async (input, output) => {
119
189
  output.system.push(SYSTEM_GUIDANCE);
190
+ if (disposed) {
191
+ return;
192
+ }
193
+ const inventory = injected.inject(input.sessionID, () => memoryInventory(getDb, dbPath, projectScope));
194
+ if (inventory !== null) {
195
+ output.system.push(inventory);
196
+ }
120
197
  },
121
198
  "experimental.session.compacting": async (_input, output) => {
122
199
  output.context.push(COMPACTION_CONTEXT);
200
+ if (disposed) {
201
+ return;
202
+ }
203
+ const inventory = memoryInventory(getDb, dbPath, projectScope);
204
+ if (inventory !== null) {
205
+ output.context.push(inventory);
206
+ }
123
207
  },
124
208
  dispose: async () => {
209
+ disposed = true;
210
+ injected.reset();
125
211
  db?.close();
126
212
  db = null;
127
213
  },
128
214
  };
129
215
  };
130
- // Tool results are shown to the model: caught errors become readable text, so
131
- // a rejected save is guidance instead of a crashed tool call.
132
- async function run(fn) {
133
- try {
134
- return fn();
135
- }
136
- catch (error) {
137
- if (error instanceof SecretDetectedError) {
138
- return "refused: the content looks like a credential; secrets must never be stored in memory";
139
- }
140
- if (error instanceof Error) {
141
- return `error: ${error.message}`;
142
- }
143
- throw error;
144
- }
145
- }
146
216
  //# sourceMappingURL=plugin.js.map