@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/CHANGELOG.md +104 -1
- package/README.md +36 -5
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +25 -2
- package/dist/lib/db.js.map +1 -1
- package/dist/lib/memory-ops.d.ts +58 -0
- package/dist/lib/memory-ops.d.ts.map +1 -0
- package/dist/lib/memory-ops.js +138 -0
- package/dist/lib/memory-ops.js.map +1 -0
- package/dist/lib/reflection.d.ts +27 -0
- package/dist/lib/reflection.d.ts.map +1 -0
- package/dist/lib/reflection.js +130 -0
- package/dist/lib/reflection.js.map +1 -0
- package/dist/lib/sqlite.d.ts.map +1 -1
- package/dist/lib/sqlite.js +5 -3
- package/dist/lib/sqlite.js.map +1 -1
- package/dist/lib/store.d.ts +49 -2
- package/dist/lib/store.d.ts.map +1 -1
- package/dist/lib/store.js +175 -24
- package/dist/lib/store.js.map +1 -1
- package/dist/plugin-v2.d.ts.map +1 -1
- package/dist/plugin-v2.js +135 -75
- package/dist/plugin-v2.js.map +1 -1
- package/dist/plugin.d.ts +8 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +143 -73
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -2
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 {
|
|
4
|
-
import {
|
|
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 (
|
|
13
|
-
|
|
41
|
+
if (disposed) {
|
|
42
|
+
throw new Error("memory store is closed");
|
|
14
43
|
}
|
|
44
|
+
db ??= openMemoryDb(dbPath);
|
|
15
45
|
return db;
|
|
16
46
|
};
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
const
|
|
20
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
package/dist/plugin-v2.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin-v2.js","sourceRoot":"","sources":["../src/plugin-v2.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,OAAO,EAAE,YAAY,EAAiB,MAAM,aAAa,CAAC;AAC1D,OAAO,
|
|
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
|
|
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
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,MAAM,
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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 (
|
|
21
|
-
|
|
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:
|
|
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) =>
|
|
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:
|
|
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) =>
|
|
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:
|
|
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) =>
|
|
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:
|
|
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
|
|
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,
|
|
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 (
|
|
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
|