@nguyenthdat/opencode-memory 0.2.0 → 0.3.2
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/README.md +35 -12
- package/dist/contracts.d.ts +27 -1
- package/dist/contracts.d.ts.map +1 -1
- package/dist/generated/{memory_pb.d.ts → opencode/memory/v1/memory_pb.d.ts} +15 -3
- package/dist/generated/opencode/memory/v1/memory_pb.d.ts.map +1 -0
- package/dist/generated/{memory_pb.js → opencode/memory/v1/memory_pb.js} +21 -9
- package/dist/generated/opencode/memory/v1/memory_pb.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +12 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +41 -0
- package/dist/instructions.js.map +1 -0
- package/dist/plugin.d.ts +16 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +202 -63
- package/dist/plugin.js.map +1 -1
- package/dist/policy.d.ts +13 -3
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +78 -42
- package/dist/policy.js.map +1 -1
- package/dist/protocol.d.ts +4 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +4 -1
- package/dist/protocol.js.map +1 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +13 -0
- package/dist/server.js.map +1 -0
- package/dist/session-context.d.ts +8 -0
- package/dist/session-context.d.ts.map +1 -1
- package/dist/session-context.js +47 -0
- package/dist/session-context.js.map +1 -1
- package/dist/shared-markdown.d.ts +2 -5
- package/dist/shared-markdown.d.ts.map +1 -1
- package/dist/shared-markdown.js +70 -27
- package/dist/shared-markdown.js.map +1 -1
- package/dist/sidecar-client.d.ts.map +1 -1
- package/dist/sidecar-client.js +16 -18
- package/dist/sidecar-client.js.map +1 -1
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +8 -0
- package/dist/validation.js.map +1 -1
- package/package.json +12 -8
- package/rules/flow.md +12 -12
- package/dist/generated/memory_pb.d.ts.map +0 -1
- package/dist/generated/memory_pb.js.map +0 -1
package/dist/plugin.js
CHANGED
|
@@ -1,44 +1,56 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin";
|
|
2
2
|
import { MEMORY_KINDS, MEMORY_SCOPES, WRITABLE_MEMORY_SCOPES, FEEDBACK_EVENTS, LOCK_ACTIONS, MEMORY_TAXONOMIES, } from "./contracts.js";
|
|
3
3
|
import { NativeMemoryClient } from "./sidecar-client.js";
|
|
4
|
-
import {
|
|
4
|
+
import { COMPACTION_CONTEXT, formatRecalledMemories, truncateText, contextBudgetChars, parseCuratedCandidates, deriveRecallQuery, } from "./policy.js";
|
|
5
|
+
import { MEMORY_INSTRUCTIONS_MARKER, loadMemoryInstructions, registerMemoryInstructions, } from "./instructions.js";
|
|
5
6
|
import { SHARED_MEMORY_RELATIVE_DIR, loadSharedMemories, writeSharedMemory, } from "./shared-markdown.js";
|
|
6
7
|
import { SessionContext } from "./session-context.js";
|
|
7
|
-
import { validateUpdateArgs } from "./validation.js";
|
|
8
|
+
import { validateDeleteRecords, validateUpdateArgs } from "./validation.js";
|
|
8
9
|
export function createMemoryPlugin(options) {
|
|
9
10
|
return async ({ client: opencode, directory, worktree }) => {
|
|
10
|
-
const
|
|
11
|
+
const settings = resolveMemoryPluginOptions(options);
|
|
12
|
+
const memoryProjectRoot = options.projectRoot ?? worktree;
|
|
13
|
+
const memoryInstructions = await loadMemoryInstructions(options.root);
|
|
14
|
+
const native = new NativeMemoryClient(options.root, memoryProjectRoot);
|
|
11
15
|
const session = new SessionContext(native, (path, query) => opencode.session.get({ path, query }), directory);
|
|
12
16
|
let sharedSignature;
|
|
13
17
|
let sharedSync;
|
|
14
18
|
const syncSharedMemories = async (force = false) => {
|
|
19
|
+
if (!settings.sharedSync)
|
|
20
|
+
return;
|
|
15
21
|
if (sharedSync)
|
|
16
22
|
return await sharedSync;
|
|
17
23
|
sharedSync = (async () => {
|
|
18
|
-
const loaded = await loadSharedMemories(
|
|
24
|
+
const loaded = await loadSharedMemories(memoryProjectRoot);
|
|
25
|
+
for (const error of loaded.errors) {
|
|
26
|
+
session.warnOnce(new Error(`${error.source}: ${error.message}`));
|
|
27
|
+
}
|
|
19
28
|
if (!force && loaded.signature === sharedSignature)
|
|
20
29
|
return;
|
|
21
30
|
const response = await native.request("sync_shared", {
|
|
22
31
|
records: loaded.records,
|
|
23
32
|
});
|
|
24
33
|
if (response.rejected > 0) {
|
|
25
|
-
throw new Error(`Rejected shared memories: ${response.
|
|
34
|
+
throw new Error(`Rejected shared memories: ${response.rejections
|
|
35
|
+
.map((rejection) => `${rejection.source}: ${rejection.message}`)
|
|
36
|
+
.join(", ")}`);
|
|
26
37
|
}
|
|
27
38
|
sharedSignature = loaded.signature;
|
|
28
|
-
session.
|
|
39
|
+
session.invalidateRecall();
|
|
29
40
|
})().finally(() => {
|
|
30
41
|
sharedSync = undefined;
|
|
31
42
|
});
|
|
32
43
|
await sharedSync;
|
|
33
44
|
};
|
|
34
|
-
if (
|
|
45
|
+
if (settings.warmup) {
|
|
35
46
|
void Promise.all([native.request("status"), syncSharedMemories()]).catch(session.warnOnce);
|
|
36
47
|
}
|
|
37
48
|
return {
|
|
38
49
|
dispose: async () => {
|
|
39
|
-
|
|
50
|
+
for (const sessID of session.pendingRecall.keys())
|
|
51
|
+
session.discardPendingRecall(sessID);
|
|
40
52
|
session.latestQuery.clear();
|
|
41
|
-
session.
|
|
53
|
+
session.invalidateRecall();
|
|
42
54
|
session.pendingRecall.clear();
|
|
43
55
|
session.sessionParents.clear();
|
|
44
56
|
session.sessionRoots.clear();
|
|
@@ -46,6 +58,7 @@ export function createMemoryPlugin(options) {
|
|
|
46
58
|
await native.dispose();
|
|
47
59
|
},
|
|
48
60
|
config: async (config) => {
|
|
61
|
+
await registerMemoryInstructions(config, memoryInstructions, directory);
|
|
49
62
|
config.command ??= {};
|
|
50
63
|
config.command.memory ??= {
|
|
51
64
|
description: "Inspect and manage project memory",
|
|
@@ -64,16 +77,16 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
64
77
|
}
|
|
65
78
|
if (event.type === "session.deleted") {
|
|
66
79
|
const sessID = event.properties.info.id;
|
|
67
|
-
|
|
80
|
+
session.discardPendingRecall(sessID);
|
|
68
81
|
session.latestQuery.delete(sessID);
|
|
69
|
-
session.
|
|
82
|
+
session.invalidateRecall(sessID);
|
|
70
83
|
session.sessionParents.delete(sessID);
|
|
71
84
|
session.sessionRoots.delete(sessID);
|
|
72
85
|
session.sessionAgents.delete(sessID);
|
|
73
86
|
return;
|
|
74
87
|
}
|
|
75
88
|
if (event.type === "session.idle") {
|
|
76
|
-
|
|
89
|
+
session.discardPendingRecall(event.properties.sessionID);
|
|
77
90
|
return;
|
|
78
91
|
}
|
|
79
92
|
if (event.type === "session.error" && event.properties.sessionID) {
|
|
@@ -81,7 +94,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
81
94
|
return;
|
|
82
95
|
}
|
|
83
96
|
if (event.type === "file.edited" || event.type === "file.watcher.updated") {
|
|
84
|
-
session.
|
|
97
|
+
session.invalidateRecall();
|
|
85
98
|
const file = event.properties.file.replaceAll("\\", "/");
|
|
86
99
|
if (file.includes(`/${SHARED_MEMORY_RELATIVE_DIR}/`)) {
|
|
87
100
|
sharedSignature = undefined;
|
|
@@ -90,6 +103,8 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
90
103
|
}
|
|
91
104
|
if (event.type !== "session.compacted")
|
|
92
105
|
return;
|
|
106
|
+
if (!settings.automaticCapture)
|
|
107
|
+
return;
|
|
93
108
|
try {
|
|
94
109
|
const response = await opencode.session.messages({
|
|
95
110
|
path: { id: event.properties.sessionID },
|
|
@@ -107,33 +122,43 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
107
122
|
if (!content)
|
|
108
123
|
return;
|
|
109
124
|
const candidates = parseCuratedCandidates(content);
|
|
125
|
+
let storedAny = false;
|
|
110
126
|
for (const candidate of candidates) {
|
|
111
127
|
try {
|
|
112
|
-
await native.request("
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
const response = await native.request("capture", {
|
|
129
|
+
candidate: {
|
|
130
|
+
...candidate,
|
|
131
|
+
source: `session:${event.properties.sessionID}:compaction`,
|
|
132
|
+
scope: "project",
|
|
133
|
+
origin: "auto_compaction",
|
|
134
|
+
revive: false,
|
|
135
|
+
},
|
|
136
|
+
significance: candidate.importance,
|
|
137
|
+
impact: candidate.kind === "decision" || candidate.kind === "gotcha" ? 0.8 : 0.6,
|
|
138
|
+
rarity: candidate.code_paths.length > 0 ? 0.7 : 0.5,
|
|
139
|
+
source_trust: "agent",
|
|
140
|
+
has_valid_evidence: candidate.code_paths.length > 0,
|
|
141
|
+
suggested_supersession_ids: [],
|
|
142
|
+
suggested_conflict_ids: [],
|
|
118
143
|
});
|
|
144
|
+
storedAny ||= response.stored !== undefined;
|
|
119
145
|
}
|
|
120
146
|
catch (error) {
|
|
121
147
|
session.warnOnce(error);
|
|
122
148
|
}
|
|
123
149
|
}
|
|
124
|
-
if (
|
|
125
|
-
session.
|
|
150
|
+
if (storedAny)
|
|
151
|
+
session.invalidateRecall();
|
|
126
152
|
}
|
|
127
153
|
catch (error) {
|
|
128
154
|
session.warnOnce(error);
|
|
129
155
|
}
|
|
130
156
|
},
|
|
131
157
|
"chat.message": async (input, output) => {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
.trim();
|
|
158
|
+
session.latestQuery.delete(input.sessionID);
|
|
159
|
+
session.invalidateRecall(input.sessionID);
|
|
160
|
+
session.discardPendingRecall(input.sessionID);
|
|
161
|
+
const query = deriveRecallQuery(output.parts);
|
|
137
162
|
if (!query)
|
|
138
163
|
return;
|
|
139
164
|
if (input.agent)
|
|
@@ -142,15 +167,17 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
142
167
|
query: truncateText(query, 2_000),
|
|
143
168
|
agent: input.agent,
|
|
144
169
|
});
|
|
145
|
-
session.recallCache.delete(input.sessionID);
|
|
146
170
|
},
|
|
147
171
|
"experimental.chat.system.transform": async (input, output) => {
|
|
148
|
-
if (!output.system.some((entry) => entry.includes(
|
|
149
|
-
output.system.push(
|
|
172
|
+
if (!output.system.some((entry) => entry.includes(MEMORY_INSTRUCTIONS_MARKER))) {
|
|
173
|
+
output.system.push(memoryInstructions.content);
|
|
150
174
|
}
|
|
151
175
|
if (!input.sessionID)
|
|
152
176
|
return;
|
|
153
|
-
|
|
177
|
+
if (!settings.automaticRecall)
|
|
178
|
+
return;
|
|
179
|
+
const sessionID = input.sessionID;
|
|
180
|
+
const latest = session.latestQuery.get(sessionID);
|
|
154
181
|
if (!latest)
|
|
155
182
|
return;
|
|
156
183
|
try {
|
|
@@ -159,33 +186,51 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
159
186
|
catch (error) {
|
|
160
187
|
session.warnOnce(error);
|
|
161
188
|
}
|
|
189
|
+
if (session.latestQuery.get(input.sessionID) !== latest)
|
|
190
|
+
return;
|
|
162
191
|
const rootSessionID = await session.resolveSessionRoot(input.sessionID);
|
|
163
192
|
const agent = latest.agent ?? session.sessionAgents.get(input.sessionID) ?? "unknown";
|
|
164
193
|
const budgetChars = contextBudgetChars(input.model);
|
|
194
|
+
const recallGeneration = session.recallGeneration(input.sessionID);
|
|
165
195
|
const cacheKey = [
|
|
166
196
|
latest.query,
|
|
167
197
|
rootSessionID,
|
|
168
198
|
agent,
|
|
169
199
|
budgetChars,
|
|
170
200
|
sharedSignature ?? "none",
|
|
201
|
+
recallGeneration,
|
|
171
202
|
].join("\0");
|
|
172
203
|
let cached = session.recallCache.get(input.sessionID);
|
|
173
204
|
if (!cached || cached.key !== cacheKey) {
|
|
205
|
+
session.discardPendingRecall(input.sessionID);
|
|
206
|
+
if (session.latestQuery.get(input.sessionID) !== latest ||
|
|
207
|
+
session.recallGeneration(input.sessionID) !== recallGeneration) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
174
210
|
try {
|
|
175
|
-
const response = await
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
211
|
+
const response = await session.searchRecallOnce(input.sessionID, cacheKey, async () => {
|
|
212
|
+
const response = await native.request("search", {
|
|
213
|
+
query: latest.query,
|
|
214
|
+
max_results: 20,
|
|
215
|
+
budget_chars: budgetChars,
|
|
216
|
+
kinds: [],
|
|
217
|
+
scopes: [],
|
|
218
|
+
taxonomies: [],
|
|
219
|
+
session_scope_key: rootSessionID,
|
|
220
|
+
agent_scope_key: agent,
|
|
221
|
+
min_score: settings.minScore,
|
|
222
|
+
include_stale: false,
|
|
223
|
+
include_superseded: false,
|
|
224
|
+
track_feedback: settings.feedbackTracking,
|
|
225
|
+
});
|
|
226
|
+
for (const warning of response.warnings)
|
|
227
|
+
session.warnOnce(new Error(warning));
|
|
228
|
+
return response;
|
|
188
229
|
});
|
|
230
|
+
if (session.latestQuery.get(input.sessionID) !== latest ||
|
|
231
|
+
session.recallGeneration(input.sessionID) !== recallGeneration) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
189
234
|
cached = { key: cacheKey, response };
|
|
190
235
|
session.recallCache.set(input.sessionID, cached);
|
|
191
236
|
}
|
|
@@ -195,15 +240,22 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
195
240
|
}
|
|
196
241
|
}
|
|
197
242
|
const formatted = formatRecalledMemories(cached.response, budgetChars);
|
|
198
|
-
if (!formatted
|
|
243
|
+
if (!formatted)
|
|
244
|
+
return;
|
|
245
|
+
if (!settings.feedbackTracking || !cached.response.retrieval_id) {
|
|
246
|
+
output.system.push(formatted.text);
|
|
199
247
|
return;
|
|
200
|
-
|
|
248
|
+
}
|
|
201
249
|
const pending = {
|
|
202
250
|
retrievalID: cached.response.retrieval_id,
|
|
203
251
|
memoryIDs: formatted.memoryIDs,
|
|
204
252
|
};
|
|
205
|
-
await session.
|
|
206
|
-
|
|
253
|
+
const opened = await session.openPendingRecall(sessionID, pending, () => {
|
|
254
|
+
return (session.latestQuery.get(sessionID) === latest &&
|
|
255
|
+
session.recallGeneration(sessionID) === recallGeneration);
|
|
256
|
+
});
|
|
257
|
+
if (opened)
|
|
258
|
+
output.system.push(formatted.text);
|
|
207
259
|
},
|
|
208
260
|
"experimental.session.compacting": async (_input, output) => {
|
|
209
261
|
output.context.push(COMPACTION_CONTEXT);
|
|
@@ -250,7 +302,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
250
302
|
.number()
|
|
251
303
|
.min(0)
|
|
252
304
|
.max(1)
|
|
253
|
-
.default(
|
|
305
|
+
.default(settings.minScore)
|
|
254
306
|
.describe("Minimum calibrated relevance score."),
|
|
255
307
|
include_stale: tool.schema
|
|
256
308
|
.boolean()
|
|
@@ -262,7 +314,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
262
314
|
.describe("Include historical memories replaced by a successor."),
|
|
263
315
|
},
|
|
264
316
|
async execute(args, context) {
|
|
265
|
-
|
|
317
|
+
session.discardPendingRecall(context.sessionID);
|
|
266
318
|
await syncSharedMemories();
|
|
267
319
|
const rootSessionID = await session.resolveSessionRoot(context.sessionID);
|
|
268
320
|
const response = await native.request("search", {
|
|
@@ -277,15 +329,18 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
277
329
|
min_score: args.min_score,
|
|
278
330
|
include_stale: args.include_stale,
|
|
279
331
|
include_superseded: args.include_superseded,
|
|
280
|
-
track_feedback:
|
|
332
|
+
track_feedback: settings.feedbackTracking,
|
|
281
333
|
}, context.abort);
|
|
282
|
-
|
|
334
|
+
for (const warning of response.warnings)
|
|
335
|
+
session.warnOnce(new Error(warning));
|
|
336
|
+
if (settings.feedbackTracking &&
|
|
337
|
+
response.retrieval_id &&
|
|
338
|
+
response.memories.length > 0) {
|
|
283
339
|
const pending = {
|
|
284
340
|
retrievalID: response.retrieval_id,
|
|
285
341
|
memoryIDs: response.memories.map((memory) => memory.id),
|
|
286
342
|
};
|
|
287
|
-
await session.
|
|
288
|
-
session.pendingRecall.set(context.sessionID, pending);
|
|
343
|
+
await session.openPendingRecall(context.sessionID, pending);
|
|
289
344
|
}
|
|
290
345
|
return result("Memory search", response, {
|
|
291
346
|
count: response.count,
|
|
@@ -370,7 +425,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
370
425
|
origin: "manual",
|
|
371
426
|
source: `session:${context.sessionID}`,
|
|
372
427
|
}, context.abort);
|
|
373
|
-
session.
|
|
428
|
+
session.invalidateRecall();
|
|
374
429
|
return result("Stored memory", response, {
|
|
375
430
|
id: response.id,
|
|
376
431
|
inserted: response.inserted,
|
|
@@ -475,7 +530,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
475
530
|
? await session.scopeKey(args.scope, context.sessionID, context.agent)
|
|
476
531
|
: undefined;
|
|
477
532
|
const response = await native.request("update", { ...args, scope_key: key, ...keys }, context.abort);
|
|
478
|
-
session.
|
|
533
|
+
session.invalidateRecall();
|
|
479
534
|
return result("Updated memory", response, response);
|
|
480
535
|
},
|
|
481
536
|
}),
|
|
@@ -489,7 +544,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
489
544
|
async execute(args, context) {
|
|
490
545
|
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
491
546
|
const response = await native.request("pin", { ...args, ...keys }, context.abort);
|
|
492
|
-
session.
|
|
547
|
+
session.invalidateRecall();
|
|
493
548
|
return result(args.pinned ? "Pinned memory" : "Unpinned memory", response, response);
|
|
494
549
|
},
|
|
495
550
|
}),
|
|
@@ -507,7 +562,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
507
562
|
}
|
|
508
563
|
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
509
564
|
const response = await native.request("lock", { ...args, ...keys }, context.abort);
|
|
510
|
-
session.
|
|
565
|
+
session.invalidateRecall();
|
|
511
566
|
return result(args.lock_action === "lock" ? "Locked memory" : "Unlocked memory", response, response);
|
|
512
567
|
},
|
|
513
568
|
}),
|
|
@@ -524,15 +579,17 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
524
579
|
.default("user_deleted"),
|
|
525
580
|
},
|
|
526
581
|
async execute(args, context) {
|
|
582
|
+
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
583
|
+
const records = await native.request("get", { ids: args.ids, ...keys }, context.abort);
|
|
584
|
+
validateDeleteRecords(records);
|
|
527
585
|
await context.ask({
|
|
528
586
|
permission: "memory_delete",
|
|
529
587
|
patterns: args.ids,
|
|
530
588
|
always: [],
|
|
531
589
|
metadata: { operation: "delete", ...args },
|
|
532
590
|
});
|
|
533
|
-
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
534
591
|
const response = await native.request("delete", { ...args, ...keys }, context.abort);
|
|
535
|
-
session.
|
|
592
|
+
session.invalidateRecall();
|
|
536
593
|
return result("Deleted memories", response, response);
|
|
537
594
|
},
|
|
538
595
|
}),
|
|
@@ -547,8 +604,9 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
547
604
|
event: tool.schema.enum(FEEDBACK_EVENTS),
|
|
548
605
|
memory_ids: tool.schema
|
|
549
606
|
.array(tool.schema.string().regex(/^mem_[0-9a-f]{32}$/))
|
|
607
|
+
.min(1)
|
|
550
608
|
.max(100)
|
|
551
|
-
.
|
|
609
|
+
.describe("Exact recalled memory IDs affected by this feedback."),
|
|
552
610
|
},
|
|
553
611
|
async execute(args, context) {
|
|
554
612
|
const pending = session.pendingRecall.get(context.sessionID);
|
|
@@ -593,11 +651,57 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
593
651
|
destination,
|
|
594
652
|
},
|
|
595
653
|
});
|
|
596
|
-
const path = await writeSharedMemory(
|
|
654
|
+
const path = await writeSharedMemory(memoryProjectRoot, memory);
|
|
597
655
|
await syncSharedMemories(true);
|
|
598
656
|
return result("Promoted memory", { id: memory.id, path }, { id: memory.id, path });
|
|
599
657
|
},
|
|
600
658
|
}),
|
|
659
|
+
memory_export: tool({
|
|
660
|
+
description: "Export visible memories, lifecycle relations, and tombstones as a portable JSON snapshot.",
|
|
661
|
+
args: {
|
|
662
|
+
include_expired: tool.schema.boolean().default(true),
|
|
663
|
+
include_superseded: tool.schema.boolean().default(true),
|
|
664
|
+
},
|
|
665
|
+
async execute(args, context) {
|
|
666
|
+
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
667
|
+
const snapshot = await native.request("export", { ...args, ...keys }, context.abort);
|
|
668
|
+
return result("Memory snapshot", snapshot, {
|
|
669
|
+
format_version: snapshot.format_version,
|
|
670
|
+
source_project_id: snapshot.source_project_id,
|
|
671
|
+
});
|
|
672
|
+
},
|
|
673
|
+
}),
|
|
674
|
+
memory_import: tool({
|
|
675
|
+
description: "Import a native-memory JSON snapshot after validating IDs, relations, lifecycle metadata, and content safety.",
|
|
676
|
+
args: {
|
|
677
|
+
snapshot_json: tool.schema
|
|
678
|
+
.string()
|
|
679
|
+
.min(2)
|
|
680
|
+
.max(4_000_000)
|
|
681
|
+
.describe("Exact JSON returned by memory_export."),
|
|
682
|
+
},
|
|
683
|
+
async execute(args, context) {
|
|
684
|
+
let snapshot;
|
|
685
|
+
try {
|
|
686
|
+
snapshot = JSON.parse(args.snapshot_json);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
throw new Error("snapshot_json is not valid JSON", { cause: error });
|
|
690
|
+
}
|
|
691
|
+
if (typeof snapshot !== "object" || snapshot === null || Array.isArray(snapshot)) {
|
|
692
|
+
throw new Error("snapshot_json must contain a snapshot object");
|
|
693
|
+
}
|
|
694
|
+
await context.ask({
|
|
695
|
+
permission: "memory_import",
|
|
696
|
+
patterns: ["native-memory-snapshot"],
|
|
697
|
+
always: [],
|
|
698
|
+
metadata: { operation: "import" },
|
|
699
|
+
});
|
|
700
|
+
const response = await native.request("import", { snapshot }, context.abort);
|
|
701
|
+
session.invalidateRecall();
|
|
702
|
+
return result("Imported memory snapshot", response, response);
|
|
703
|
+
},
|
|
704
|
+
}),
|
|
601
705
|
memory_purge: tool({
|
|
602
706
|
description: "Delete all local indexed memories for the current project. Shared Markdown files are preserved.",
|
|
603
707
|
args: {
|
|
@@ -615,7 +719,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
615
719
|
metadata: { operation: "purge", ...args },
|
|
616
720
|
});
|
|
617
721
|
const response = await native.request("purge", args, context.abort);
|
|
618
|
-
session.
|
|
722
|
+
session.invalidateRecall();
|
|
619
723
|
session.pendingRecall.clear();
|
|
620
724
|
sharedSignature = undefined;
|
|
621
725
|
return result("Purged memory", response, response);
|
|
@@ -626,7 +730,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
626
730
|
args: {},
|
|
627
731
|
async execute(_args, context) {
|
|
628
732
|
const response = await native.request("optimize", {}, context.abort);
|
|
629
|
-
session.
|
|
733
|
+
session.invalidateRecall();
|
|
630
734
|
return result("Optimized memory", response, response);
|
|
631
735
|
},
|
|
632
736
|
}),
|
|
@@ -639,6 +743,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
639
743
|
.describe("Hash all code anchors to detect staleness."),
|
|
640
744
|
},
|
|
641
745
|
async execute(args, context) {
|
|
746
|
+
await syncSharedMemories();
|
|
642
747
|
const response = await native.request("doctor", args, context.abort);
|
|
643
748
|
return result("Memory doctor", response, response);
|
|
644
749
|
},
|
|
@@ -647,6 +752,7 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
647
752
|
description: "Inspect the current project's native memory backend, collection, embedding model, indexes, and document count.",
|
|
648
753
|
args: {},
|
|
649
754
|
async execute(_args, context) {
|
|
755
|
+
await syncSharedMemories();
|
|
650
756
|
const response = await native.request("status", {}, context.abort);
|
|
651
757
|
return result("Memory status", response, response);
|
|
652
758
|
},
|
|
@@ -655,6 +761,39 @@ Never modify repository-scoped memory through memory_update; edit its .opencode/
|
|
|
655
761
|
};
|
|
656
762
|
};
|
|
657
763
|
}
|
|
764
|
+
export function resolveMemoryPluginOptions(options) {
|
|
765
|
+
const minScore = options.minScore ?? envNumber("OPENCODE_MEMORY_MIN_SCORE", 0.42);
|
|
766
|
+
if (!Number.isFinite(minScore) || minScore < 0 || minScore > 1) {
|
|
767
|
+
throw new Error("memory minScore must be between 0 and 1");
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
warmup: options.warmup ?? envBoolean("OPENCODE_MEMORY_WARMUP", true),
|
|
771
|
+
automaticRecall: options.automaticRecall ?? envBoolean("OPENCODE_MEMORY_AUTO_RECALL", true),
|
|
772
|
+
automaticCapture: options.automaticCapture ?? envBoolean("OPENCODE_MEMORY_AUTO_CAPTURE", true),
|
|
773
|
+
sharedSync: options.sharedSync ?? envBoolean("OPENCODE_MEMORY_SHARED_SYNC", true),
|
|
774
|
+
feedbackTracking: options.feedbackTracking ?? envBoolean("OPENCODE_MEMORY_FEEDBACK_TRACKING", true),
|
|
775
|
+
minScore,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
function envBoolean(name, fallback) {
|
|
779
|
+
const value = process.env[name];
|
|
780
|
+
if (value === undefined || value === "")
|
|
781
|
+
return fallback;
|
|
782
|
+
if (["1", "true", "yes", "on"].includes(value.toLowerCase()))
|
|
783
|
+
return true;
|
|
784
|
+
if (["0", "false", "no", "off"].includes(value.toLowerCase()))
|
|
785
|
+
return false;
|
|
786
|
+
throw new Error(`${name} must be a boolean`);
|
|
787
|
+
}
|
|
788
|
+
function envNumber(name, fallback) {
|
|
789
|
+
const value = process.env[name];
|
|
790
|
+
if (value === undefined || value === "")
|
|
791
|
+
return fallback;
|
|
792
|
+
const parsed = Number(value);
|
|
793
|
+
if (!Number.isFinite(parsed))
|
|
794
|
+
throw new Error(`${name} must be a finite number`);
|
|
795
|
+
return parsed;
|
|
796
|
+
}
|
|
658
797
|
function result(title, value, metadata) {
|
|
659
798
|
return {
|
|
660
799
|
title,
|