@nguyenthdat/opencode-memory 0.2.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/LICENSE +21 -0
- package/README.md +170 -0
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/dist/contracts.d.ts +96 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +46 -0
- package/dist/contracts.js.map +1 -0
- package/dist/generated/memory_pb.d.ts +227 -0
- package/dist/generated/memory_pb.d.ts.map +1 -0
- package/dist/generated/memory_pb.js +112 -0
- package/dist/generated/memory_pb.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +665 -0
- package/dist/plugin.js.map +1 -0
- package/dist/policy.d.ts +19 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +125 -0
- package/dist/policy.js.map +1 -0
- package/dist/protocol.d.ts +31 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +220 -0
- package/dist/protocol.js.map +1 -0
- package/dist/session-context.d.ts +40 -0
- package/dist/session-context.d.ts.map +1 -0
- package/dist/session-context.js +92 -0
- package/dist/session-context.js.map +1 -0
- package/dist/shared-markdown.d.ts +11 -0
- package/dist/shared-markdown.d.ts.map +1 -0
- package/dist/shared-markdown.js +178 -0
- package/dist/shared-markdown.js.map +1 -0
- package/dist/sidecar-client.d.ts +39 -0
- package/dist/sidecar-client.d.ts.map +1 -0
- package/dist/sidecar-client.js +397 -0
- package/dist/sidecar-client.js.map +1 -0
- package/dist/validation.d.ts +2 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +29 -0
- package/dist/validation.js.map +1 -0
- package/notices/ZVEC_NOTICE +95 -0
- package/package.json +90 -0
- package/rules/flow.md +16 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { MEMORY_KINDS, MEMORY_SCOPES, WRITABLE_MEMORY_SCOPES, FEEDBACK_EVENTS, LOCK_ACTIONS, MEMORY_TAXONOMIES, } from "./contracts.js";
|
|
3
|
+
import { NativeMemoryClient } from "./sidecar-client.js";
|
|
4
|
+
import { MEMORY_POLICY, MEMORY_POLICY_MARKER, COMPACTION_CONTEXT, formatRecalledMemories, truncateText, contextBudgetChars, parseCuratedCandidates, } from "./policy.js";
|
|
5
|
+
import { SHARED_MEMORY_RELATIVE_DIR, loadSharedMemories, writeSharedMemory, } from "./shared-markdown.js";
|
|
6
|
+
import { SessionContext } from "./session-context.js";
|
|
7
|
+
import { validateUpdateArgs } from "./validation.js";
|
|
8
|
+
export function createMemoryPlugin(options) {
|
|
9
|
+
return async ({ client: opencode, directory, worktree }) => {
|
|
10
|
+
const native = new NativeMemoryClient(options.root, worktree);
|
|
11
|
+
const session = new SessionContext(native, (path, query) => opencode.session.get({ path, query }), directory);
|
|
12
|
+
let sharedSignature;
|
|
13
|
+
let sharedSync;
|
|
14
|
+
const syncSharedMemories = async (force = false) => {
|
|
15
|
+
if (sharedSync)
|
|
16
|
+
return await sharedSync;
|
|
17
|
+
sharedSync = (async () => {
|
|
18
|
+
const loaded = await loadSharedMemories(worktree);
|
|
19
|
+
if (!force && loaded.signature === sharedSignature)
|
|
20
|
+
return;
|
|
21
|
+
const response = await native.request("sync_shared", {
|
|
22
|
+
records: loaded.records,
|
|
23
|
+
});
|
|
24
|
+
if (response.rejected > 0) {
|
|
25
|
+
throw new Error(`Rejected shared memories: ${response.rejected_sources.join(", ")}`);
|
|
26
|
+
}
|
|
27
|
+
sharedSignature = loaded.signature;
|
|
28
|
+
session.recallCache.clear();
|
|
29
|
+
})().finally(() => {
|
|
30
|
+
sharedSync = undefined;
|
|
31
|
+
});
|
|
32
|
+
await sharedSync;
|
|
33
|
+
};
|
|
34
|
+
if (options.warmup !== false) {
|
|
35
|
+
void Promise.all([native.request("status"), syncSharedMemories()]).catch(session.warnOnce);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
dispose: async () => {
|
|
39
|
+
await Promise.all([...session.pendingRecall.keys()].map((sessID) => session.closePendingRecall(sessID, "ignored")));
|
|
40
|
+
session.latestQuery.clear();
|
|
41
|
+
session.recallCache.clear();
|
|
42
|
+
session.pendingRecall.clear();
|
|
43
|
+
session.sessionParents.clear();
|
|
44
|
+
session.sessionRoots.clear();
|
|
45
|
+
session.sessionAgents.clear();
|
|
46
|
+
await native.dispose();
|
|
47
|
+
},
|
|
48
|
+
config: async (config) => {
|
|
49
|
+
config.command ??= {};
|
|
50
|
+
config.command.memory ??= {
|
|
51
|
+
description: "Inspect and manage project memory",
|
|
52
|
+
template: `Manage memory for the current project. User request: $ARGUMENTS
|
|
53
|
+
|
|
54
|
+
When no arguments are supplied, call memory_status and memory_list, then summarize active scopes, stale/expired records, and suggested cleanup.
|
|
55
|
+
Use memory_search for semantic lookup, memory_get for full records, memory_update for corrections, memory_delete for removal, memory_promote for reviewed Git-shareable Markdown, and memory_doctor for diagnostics.
|
|
56
|
+
Never modify repository-scoped memory through memory_update; edit its .opencode/memory Markdown source instead. Ask through the tool permission flow before destructive or sharing operations.`,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
event: async ({ event }) => {
|
|
60
|
+
if (event.type === "session.created" || event.type === "session.updated") {
|
|
61
|
+
session.sessionParents.set(event.properties.info.id, event.properties.info.parentID);
|
|
62
|
+
session.sessionRoots.clear();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (event.type === "session.deleted") {
|
|
66
|
+
const sessID = event.properties.info.id;
|
|
67
|
+
await session.closePendingRecall(sessID, "ignored");
|
|
68
|
+
session.latestQuery.delete(sessID);
|
|
69
|
+
session.recallCache.delete(sessID);
|
|
70
|
+
session.sessionParents.delete(sessID);
|
|
71
|
+
session.sessionRoots.delete(sessID);
|
|
72
|
+
session.sessionAgents.delete(sessID);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (event.type === "session.idle") {
|
|
76
|
+
await session.closePendingRecall(event.properties.sessionID, "ignored");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (event.type === "session.error" && event.properties.sessionID) {
|
|
80
|
+
await session.closePendingRecall(event.properties.sessionID, "error");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (event.type === "file.edited" || event.type === "file.watcher.updated") {
|
|
84
|
+
session.recallCache.clear();
|
|
85
|
+
const file = event.properties.file.replaceAll("\\", "/");
|
|
86
|
+
if (file.includes(`/${SHARED_MEMORY_RELATIVE_DIR}/`)) {
|
|
87
|
+
sharedSignature = undefined;
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (event.type !== "session.compacted")
|
|
92
|
+
return;
|
|
93
|
+
try {
|
|
94
|
+
const response = await opencode.session.messages({
|
|
95
|
+
path: { id: event.properties.sessionID },
|
|
96
|
+
query: { directory, limit: 50 },
|
|
97
|
+
});
|
|
98
|
+
const summary = response.data
|
|
99
|
+
?.toReversed()
|
|
100
|
+
.find((message) => message.info.role === "assistant" && message.info.summary === true);
|
|
101
|
+
if (!summary)
|
|
102
|
+
return;
|
|
103
|
+
const content = summary.parts
|
|
104
|
+
.flatMap((part) => (part.type === "text" && !part.ignored ? [part.text] : []))
|
|
105
|
+
.join("\n")
|
|
106
|
+
.trim();
|
|
107
|
+
if (!content)
|
|
108
|
+
return;
|
|
109
|
+
const candidates = parseCuratedCandidates(content);
|
|
110
|
+
for (const candidate of candidates) {
|
|
111
|
+
try {
|
|
112
|
+
await native.request("store", {
|
|
113
|
+
...candidate,
|
|
114
|
+
source: `session:${event.properties.sessionID}:compaction`,
|
|
115
|
+
scope: "project",
|
|
116
|
+
origin: "auto_compaction",
|
|
117
|
+
revive: false,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
session.warnOnce(error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (candidates.length > 0)
|
|
125
|
+
session.recallCache.clear();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
session.warnOnce(error);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"chat.message": async (input, output) => {
|
|
132
|
+
await session.closePendingRecall(input.sessionID, "ignored");
|
|
133
|
+
const query = output.parts
|
|
134
|
+
.flatMap((part) => part.type === "text" && !part.synthetic && !part.ignored ? [part.text] : [])
|
|
135
|
+
.join("\n")
|
|
136
|
+
.trim();
|
|
137
|
+
if (!query)
|
|
138
|
+
return;
|
|
139
|
+
if (input.agent)
|
|
140
|
+
session.sessionAgents.set(input.sessionID, input.agent);
|
|
141
|
+
session.latestQuery.set(input.sessionID, {
|
|
142
|
+
query: truncateText(query, 2_000),
|
|
143
|
+
agent: input.agent,
|
|
144
|
+
});
|
|
145
|
+
session.recallCache.delete(input.sessionID);
|
|
146
|
+
},
|
|
147
|
+
"experimental.chat.system.transform": async (input, output) => {
|
|
148
|
+
if (!output.system.some((entry) => entry.includes(MEMORY_POLICY_MARKER))) {
|
|
149
|
+
output.system.push(MEMORY_POLICY);
|
|
150
|
+
}
|
|
151
|
+
if (!input.sessionID)
|
|
152
|
+
return;
|
|
153
|
+
const latest = session.latestQuery.get(input.sessionID);
|
|
154
|
+
if (!latest)
|
|
155
|
+
return;
|
|
156
|
+
try {
|
|
157
|
+
await syncSharedMemories();
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
session.warnOnce(error);
|
|
161
|
+
}
|
|
162
|
+
const rootSessionID = await session.resolveSessionRoot(input.sessionID);
|
|
163
|
+
const agent = latest.agent ?? session.sessionAgents.get(input.sessionID) ?? "unknown";
|
|
164
|
+
const budgetChars = contextBudgetChars(input.model);
|
|
165
|
+
const cacheKey = [
|
|
166
|
+
latest.query,
|
|
167
|
+
rootSessionID,
|
|
168
|
+
agent,
|
|
169
|
+
budgetChars,
|
|
170
|
+
sharedSignature ?? "none",
|
|
171
|
+
].join("\0");
|
|
172
|
+
let cached = session.recallCache.get(input.sessionID);
|
|
173
|
+
if (!cached || cached.key !== cacheKey) {
|
|
174
|
+
try {
|
|
175
|
+
const response = await native.request("search", {
|
|
176
|
+
query: latest.query,
|
|
177
|
+
max_results: 20,
|
|
178
|
+
budget_chars: budgetChars,
|
|
179
|
+
kinds: [],
|
|
180
|
+
scopes: [],
|
|
181
|
+
taxonomies: [],
|
|
182
|
+
session_scope_key: rootSessionID,
|
|
183
|
+
agent_scope_key: agent,
|
|
184
|
+
min_score: 0.42,
|
|
185
|
+
include_stale: false,
|
|
186
|
+
include_superseded: false,
|
|
187
|
+
track_feedback: true,
|
|
188
|
+
});
|
|
189
|
+
cached = { key: cacheKey, response };
|
|
190
|
+
session.recallCache.set(input.sessionID, cached);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
session.warnOnce(error);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const formatted = formatRecalledMemories(cached.response, budgetChars);
|
|
198
|
+
if (!formatted || !cached.response.retrieval_id)
|
|
199
|
+
return;
|
|
200
|
+
output.system.push(formatted.text);
|
|
201
|
+
const pending = {
|
|
202
|
+
retrievalID: cached.response.retrieval_id,
|
|
203
|
+
memoryIDs: formatted.memoryIDs,
|
|
204
|
+
};
|
|
205
|
+
await session.recordFeedback(pending, "injected");
|
|
206
|
+
session.pendingRecall.set(input.sessionID, pending);
|
|
207
|
+
},
|
|
208
|
+
"experimental.session.compacting": async (_input, output) => {
|
|
209
|
+
output.context.push(COMPACTION_CONTEXT);
|
|
210
|
+
},
|
|
211
|
+
tool: {
|
|
212
|
+
memory_search: tool({
|
|
213
|
+
description: "Semantically search durable memory for the current project. Use before substantial work when prior decisions, preferences, facts, patterns, or gotchas may matter.",
|
|
214
|
+
args: {
|
|
215
|
+
query: tool.schema
|
|
216
|
+
.string()
|
|
217
|
+
.min(1)
|
|
218
|
+
.max(2_000)
|
|
219
|
+
.describe("Concise task-specific retrieval query."),
|
|
220
|
+
limit: tool.schema
|
|
221
|
+
.number()
|
|
222
|
+
.int()
|
|
223
|
+
.min(1)
|
|
224
|
+
.max(20)
|
|
225
|
+
.default(20)
|
|
226
|
+
.describe("Safety ceiling; context budget normally decides the count."),
|
|
227
|
+
budget_chars: tool.schema
|
|
228
|
+
.number()
|
|
229
|
+
.int()
|
|
230
|
+
.min(512)
|
|
231
|
+
.max(24_000)
|
|
232
|
+
.default(6_000)
|
|
233
|
+
.describe("Maximum serialized memory context in characters."),
|
|
234
|
+
kinds: tool.schema
|
|
235
|
+
.array(tool.schema.enum(MEMORY_KINDS))
|
|
236
|
+
.max(MEMORY_KINDS.length)
|
|
237
|
+
.default([])
|
|
238
|
+
.describe("Optional memory kinds to include."),
|
|
239
|
+
scopes: tool.schema
|
|
240
|
+
.array(tool.schema.enum(MEMORY_SCOPES))
|
|
241
|
+
.max(MEMORY_SCOPES.length)
|
|
242
|
+
.default([])
|
|
243
|
+
.describe("Optional scopes to include."),
|
|
244
|
+
taxonomies: tool.schema
|
|
245
|
+
.array(tool.schema.enum(MEMORY_TAXONOMIES))
|
|
246
|
+
.max(MEMORY_TAXONOMIES.length)
|
|
247
|
+
.default([])
|
|
248
|
+
.describe("Optional CoALA-derived taxonomies to include."),
|
|
249
|
+
min_score: tool.schema
|
|
250
|
+
.number()
|
|
251
|
+
.min(0)
|
|
252
|
+
.max(1)
|
|
253
|
+
.default(0.42)
|
|
254
|
+
.describe("Minimum calibrated relevance score."),
|
|
255
|
+
include_stale: tool.schema
|
|
256
|
+
.boolean()
|
|
257
|
+
.default(false)
|
|
258
|
+
.describe("Include memories whose code anchors changed."),
|
|
259
|
+
include_superseded: tool.schema
|
|
260
|
+
.boolean()
|
|
261
|
+
.default(false)
|
|
262
|
+
.describe("Include historical memories replaced by a successor."),
|
|
263
|
+
},
|
|
264
|
+
async execute(args, context) {
|
|
265
|
+
await session.closePendingRecall(context.sessionID, "ignored");
|
|
266
|
+
await syncSharedMemories();
|
|
267
|
+
const rootSessionID = await session.resolveSessionRoot(context.sessionID);
|
|
268
|
+
const response = await native.request("search", {
|
|
269
|
+
query: args.query,
|
|
270
|
+
max_results: args.limit,
|
|
271
|
+
budget_chars: args.budget_chars,
|
|
272
|
+
kinds: args.kinds,
|
|
273
|
+
scopes: args.scopes,
|
|
274
|
+
taxonomies: args.taxonomies,
|
|
275
|
+
session_scope_key: rootSessionID,
|
|
276
|
+
agent_scope_key: context.agent,
|
|
277
|
+
min_score: args.min_score,
|
|
278
|
+
include_stale: args.include_stale,
|
|
279
|
+
include_superseded: args.include_superseded,
|
|
280
|
+
track_feedback: true,
|
|
281
|
+
}, context.abort);
|
|
282
|
+
if (response.retrieval_id && response.memories.length > 0) {
|
|
283
|
+
const pending = {
|
|
284
|
+
retrievalID: response.retrieval_id,
|
|
285
|
+
memoryIDs: response.memories.map((memory) => memory.id),
|
|
286
|
+
};
|
|
287
|
+
await session.recordFeedback(pending, "injected");
|
|
288
|
+
session.pendingRecall.set(context.sessionID, pending);
|
|
289
|
+
}
|
|
290
|
+
return result("Memory search", response, {
|
|
291
|
+
count: response.count,
|
|
292
|
+
retrieval_id: response.retrieval_id,
|
|
293
|
+
abstained: response.abstained,
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
}),
|
|
297
|
+
memory_store: tool({
|
|
298
|
+
description: "Store one distilled, durable project memory. Never store secrets, raw conversations, temporary logs, or unverified guesses.",
|
|
299
|
+
args: {
|
|
300
|
+
content: tool.schema
|
|
301
|
+
.string()
|
|
302
|
+
.min(1)
|
|
303
|
+
.max(6_000)
|
|
304
|
+
.describe("Self-contained durable fact or concise summary."),
|
|
305
|
+
title: tool.schema
|
|
306
|
+
.string()
|
|
307
|
+
.min(1)
|
|
308
|
+
.max(160)
|
|
309
|
+
.optional()
|
|
310
|
+
.describe("Short descriptive title; inferred when omitted."),
|
|
311
|
+
kind: tool.schema
|
|
312
|
+
.enum(MEMORY_KINDS)
|
|
313
|
+
.default("fact")
|
|
314
|
+
.describe("Durable memory category."),
|
|
315
|
+
importance: tool.schema
|
|
316
|
+
.number()
|
|
317
|
+
.min(0)
|
|
318
|
+
.max(1)
|
|
319
|
+
.default(0.7)
|
|
320
|
+
.describe("Long-term importance from 0 to 1."),
|
|
321
|
+
tags: tool.schema
|
|
322
|
+
.array(tool.schema.string().min(1).max(64))
|
|
323
|
+
.max(12)
|
|
324
|
+
.default([])
|
|
325
|
+
.describe("Short retrieval tags."),
|
|
326
|
+
scope: tool.schema
|
|
327
|
+
.enum(WRITABLE_MEMORY_SCOPES)
|
|
328
|
+
.default("project")
|
|
329
|
+
.describe("session shares with the parent/subagent family; agent is role-specific; project is durable and private."),
|
|
330
|
+
expires_in_days: tool.schema
|
|
331
|
+
.number()
|
|
332
|
+
.int()
|
|
333
|
+
.min(1)
|
|
334
|
+
.max(3_650)
|
|
335
|
+
.optional()
|
|
336
|
+
.describe("Optional hard expiry override."),
|
|
337
|
+
code_paths: tool.schema
|
|
338
|
+
.array(tool.schema.string().min(1).max(512))
|
|
339
|
+
.max(12)
|
|
340
|
+
.default([])
|
|
341
|
+
.describe("Relative files that validate this memory."),
|
|
342
|
+
revive: tool.schema
|
|
343
|
+
.boolean()
|
|
344
|
+
.default(false)
|
|
345
|
+
.describe("Revive a tombstoned memory after user approval."),
|
|
346
|
+
taxonomy: tool.schema
|
|
347
|
+
.enum(MEMORY_TAXONOMIES)
|
|
348
|
+
.optional()
|
|
349
|
+
.describe("Explicit memory taxonomy; inferred when omitted."),
|
|
350
|
+
confidence: tool.schema
|
|
351
|
+
.number()
|
|
352
|
+
.min(0)
|
|
353
|
+
.max(1)
|
|
354
|
+
.optional()
|
|
355
|
+
.describe("Confidence in this memory; defaults from importance."),
|
|
356
|
+
},
|
|
357
|
+
async execute(args, context) {
|
|
358
|
+
if (args.revive) {
|
|
359
|
+
await context.ask({
|
|
360
|
+
permission: "memory_revive",
|
|
361
|
+
patterns: [args.title ?? truncateText(args.content, 80)],
|
|
362
|
+
always: [],
|
|
363
|
+
metadata: { operation: "revive", scope: args.scope },
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
const key = await session.scopeKey(args.scope, context.sessionID, context.agent);
|
|
367
|
+
const response = await native.request("store", {
|
|
368
|
+
...args,
|
|
369
|
+
scope_key: key,
|
|
370
|
+
origin: "manual",
|
|
371
|
+
source: `session:${context.sessionID}`,
|
|
372
|
+
}, context.abort);
|
|
373
|
+
session.recallCache.clear();
|
|
374
|
+
return result("Stored memory", response, {
|
|
375
|
+
id: response.id,
|
|
376
|
+
inserted: response.inserted,
|
|
377
|
+
});
|
|
378
|
+
},
|
|
379
|
+
}),
|
|
380
|
+
memory_get: tool({
|
|
381
|
+
description: "Fetch complete durable memories by IDs returned from memory_search.",
|
|
382
|
+
args: {
|
|
383
|
+
ids: tool.schema
|
|
384
|
+
.array(tool.schema.string().regex(/^mem_[0-9a-f]{32}$/))
|
|
385
|
+
.min(1)
|
|
386
|
+
.max(100)
|
|
387
|
+
.describe("Memory IDs to fetch."),
|
|
388
|
+
},
|
|
389
|
+
async execute(args, context) {
|
|
390
|
+
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
391
|
+
const response = await native.request("get", { ...args, ...keys }, context.abort);
|
|
392
|
+
return result("Memories", response, {
|
|
393
|
+
count: response.length,
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
}),
|
|
397
|
+
memory_list: tool({
|
|
398
|
+
description: "List lifecycle-indexed memories for review, cleanup, and /memory management.",
|
|
399
|
+
args: {
|
|
400
|
+
kinds: tool.schema
|
|
401
|
+
.array(tool.schema.enum(MEMORY_KINDS))
|
|
402
|
+
.max(MEMORY_KINDS.length)
|
|
403
|
+
.default([]),
|
|
404
|
+
scopes: tool.schema
|
|
405
|
+
.array(tool.schema.enum(MEMORY_SCOPES))
|
|
406
|
+
.max(MEMORY_SCOPES.length)
|
|
407
|
+
.default([]),
|
|
408
|
+
taxonomies: tool.schema
|
|
409
|
+
.array(tool.schema.enum(MEMORY_TAXONOMIES))
|
|
410
|
+
.max(MEMORY_TAXONOMIES.length)
|
|
411
|
+
.default([]),
|
|
412
|
+
include_expired: tool.schema.boolean().default(false),
|
|
413
|
+
include_stale: tool.schema.boolean().default(false),
|
|
414
|
+
include_superseded: tool.schema.boolean().default(false),
|
|
415
|
+
offset: tool.schema.number().int().min(0).default(0),
|
|
416
|
+
limit: tool.schema.number().int().min(1).max(100).default(50),
|
|
417
|
+
},
|
|
418
|
+
async execute(args, context) {
|
|
419
|
+
await syncSharedMemories();
|
|
420
|
+
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
421
|
+
const response = await native.request("list", { ...args, ...keys }, context.abort);
|
|
422
|
+
return result("Memory list", response, {
|
|
423
|
+
total: response.total,
|
|
424
|
+
count: response.count,
|
|
425
|
+
});
|
|
426
|
+
},
|
|
427
|
+
}),
|
|
428
|
+
memory_update: tool({
|
|
429
|
+
description: "Correct or reclassify one local memory by ID with optional optimistic concurrency.",
|
|
430
|
+
args: {
|
|
431
|
+
id: tool.schema.string().regex(/^mem_[0-9a-f]{32}$/),
|
|
432
|
+
expected_updated_at_ms: tool.schema.number().int().optional(),
|
|
433
|
+
content: tool.schema.string().min(1).max(6_000).optional(),
|
|
434
|
+
title: tool.schema.string().min(1).max(160).optional(),
|
|
435
|
+
kind: tool.schema.enum(MEMORY_KINDS).optional(),
|
|
436
|
+
importance: tool.schema.number().min(0).max(1).optional(),
|
|
437
|
+
tags: tool.schema.array(tool.schema.string().min(1).max(64)).max(12).optional(),
|
|
438
|
+
scope: tool.schema.enum(WRITABLE_MEMORY_SCOPES).optional(),
|
|
439
|
+
expires_in_days: tool.schema.number().int().min(1).max(3_650).optional(),
|
|
440
|
+
clear_expiry: tool.schema.boolean().default(false),
|
|
441
|
+
code_paths: tool.schema.array(tool.schema.string().min(1).max(512)).max(12).optional(),
|
|
442
|
+
pinned: tool.schema
|
|
443
|
+
.boolean()
|
|
444
|
+
.optional()
|
|
445
|
+
.describe("Pin the memory so it bypasses expiry and retention decay."),
|
|
446
|
+
lock_action: tool.schema
|
|
447
|
+
.enum(LOCK_ACTIONS)
|
|
448
|
+
.optional()
|
|
449
|
+
.describe("Lock or unlock the memory. Locked records block updates and deletes."),
|
|
450
|
+
lock_reason: tool.schema
|
|
451
|
+
.string()
|
|
452
|
+
.min(1)
|
|
453
|
+
.max(240)
|
|
454
|
+
.optional()
|
|
455
|
+
.describe("Reason for locking the memory. Only valid with lock_action='lock'."),
|
|
456
|
+
taxonomy: tool.schema.enum(MEMORY_TAXONOMIES).optional(),
|
|
457
|
+
confidence: tool.schema.number().min(0).max(1).optional(),
|
|
458
|
+
conflict_with: tool.schema
|
|
459
|
+
.array(tool.schema.string().regex(/^mem_[0-9a-f]{32}$/))
|
|
460
|
+
.max(100)
|
|
461
|
+
.optional()
|
|
462
|
+
.describe("Symmetric conflict links; pass [] to clear links."),
|
|
463
|
+
},
|
|
464
|
+
async execute(args, context) {
|
|
465
|
+
validateUpdateArgs(args);
|
|
466
|
+
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
467
|
+
const existing = await native.request("get", { ids: [args.id], ...keys }, context.abort);
|
|
468
|
+
const record = existing[0];
|
|
469
|
+
if (!record)
|
|
470
|
+
throw new Error(`Memory not found: ${args.id}`);
|
|
471
|
+
if (record.scope === "repository") {
|
|
472
|
+
throw new Error("Repository memory is canonical Markdown; edit its .opencode/memory file instead.");
|
|
473
|
+
}
|
|
474
|
+
const key = args.scope
|
|
475
|
+
? await session.scopeKey(args.scope, context.sessionID, context.agent)
|
|
476
|
+
: undefined;
|
|
477
|
+
const response = await native.request("update", { ...args, scope_key: key, ...keys }, context.abort);
|
|
478
|
+
session.recallCache.clear();
|
|
479
|
+
return result("Updated memory", response, response);
|
|
480
|
+
},
|
|
481
|
+
}),
|
|
482
|
+
memory_pin: tool({
|
|
483
|
+
description: "Pin or unpin one local memory without re-embedding or refreshing semantic recency.",
|
|
484
|
+
args: {
|
|
485
|
+
id: tool.schema.string().regex(/^mem_[0-9a-f]{32}$/),
|
|
486
|
+
pinned: tool.schema.boolean(),
|
|
487
|
+
expected_updated_at_ms: tool.schema.number().int().optional(),
|
|
488
|
+
},
|
|
489
|
+
async execute(args, context) {
|
|
490
|
+
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
491
|
+
const response = await native.request("pin", { ...args, ...keys }, context.abort);
|
|
492
|
+
session.recallCache.clear();
|
|
493
|
+
return result(args.pinned ? "Pinned memory" : "Unpinned memory", response, response);
|
|
494
|
+
},
|
|
495
|
+
}),
|
|
496
|
+
memory_lock: tool({
|
|
497
|
+
description: "Lock or unlock one local memory. Unlock is lifecycle-only; locked records reject semantic changes and deletion.",
|
|
498
|
+
args: {
|
|
499
|
+
id: tool.schema.string().regex(/^mem_[0-9a-f]{32}$/),
|
|
500
|
+
lock_action: tool.schema.enum(LOCK_ACTIONS),
|
|
501
|
+
lock_reason: tool.schema.string().min(1).max(240).optional(),
|
|
502
|
+
expected_updated_at_ms: tool.schema.number().int().optional(),
|
|
503
|
+
},
|
|
504
|
+
async execute(args, context) {
|
|
505
|
+
if (args.lock_action === "unlock" && args.lock_reason !== undefined) {
|
|
506
|
+
throw new Error("lock_reason is valid only when locking");
|
|
507
|
+
}
|
|
508
|
+
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
509
|
+
const response = await native.request("lock", { ...args, ...keys }, context.abort);
|
|
510
|
+
session.recallCache.clear();
|
|
511
|
+
return result(args.lock_action === "lock" ? "Locked memory" : "Unlocked memory", response, response);
|
|
512
|
+
},
|
|
513
|
+
}),
|
|
514
|
+
memory_delete: tool({
|
|
515
|
+
description: "Batch-delete obsolete or incorrect memories and leave tombstones by default.",
|
|
516
|
+
args: {
|
|
517
|
+
ids: tool.schema
|
|
518
|
+
.array(tool.schema.string().regex(/^mem_[0-9a-f]{32}$/))
|
|
519
|
+
.min(1)
|
|
520
|
+
.max(100),
|
|
521
|
+
tombstone: tool.schema.boolean().default(true),
|
|
522
|
+
reason: tool.schema
|
|
523
|
+
.enum(["obsolete", "incorrect", "user_deleted"])
|
|
524
|
+
.default("user_deleted"),
|
|
525
|
+
},
|
|
526
|
+
async execute(args, context) {
|
|
527
|
+
await context.ask({
|
|
528
|
+
permission: "memory_delete",
|
|
529
|
+
patterns: args.ids,
|
|
530
|
+
always: [],
|
|
531
|
+
metadata: { operation: "delete", ...args },
|
|
532
|
+
});
|
|
533
|
+
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
534
|
+
const response = await native.request("delete", { ...args, ...keys }, context.abort);
|
|
535
|
+
session.recallCache.clear();
|
|
536
|
+
return result("Deleted memories", response, response);
|
|
537
|
+
},
|
|
538
|
+
}),
|
|
539
|
+
memory_feedback: tool({
|
|
540
|
+
description: "Record whether recalled memory was used, ignored, or caused an error. Used feedback must be explicit.",
|
|
541
|
+
args: {
|
|
542
|
+
retrieval_id: tool.schema
|
|
543
|
+
.string()
|
|
544
|
+
.regex(/^ret_[0-9a-f]{24}$/)
|
|
545
|
+
.optional()
|
|
546
|
+
.describe("Defaults to the latest pending retrieval in this session."),
|
|
547
|
+
event: tool.schema.enum(FEEDBACK_EVENTS),
|
|
548
|
+
memory_ids: tool.schema
|
|
549
|
+
.array(tool.schema.string().regex(/^mem_[0-9a-f]{32}$/))
|
|
550
|
+
.max(100)
|
|
551
|
+
.default([]),
|
|
552
|
+
},
|
|
553
|
+
async execute(args, context) {
|
|
554
|
+
const pending = session.pendingRecall.get(context.sessionID);
|
|
555
|
+
const retrievalID = args.retrieval_id ?? pending?.retrievalID;
|
|
556
|
+
if (!retrievalID) {
|
|
557
|
+
throw new Error("No pending retrieval is available for this session");
|
|
558
|
+
}
|
|
559
|
+
const response = await native.request("feedback", {
|
|
560
|
+
retrieval_id: retrievalID,
|
|
561
|
+
event: args.event,
|
|
562
|
+
memory_ids: args.memory_ids,
|
|
563
|
+
}, context.abort);
|
|
564
|
+
if (pending?.retrievalID === retrievalID) {
|
|
565
|
+
session.pendingRecall.delete(context.sessionID);
|
|
566
|
+
}
|
|
567
|
+
return result("Recorded memory feedback", response, response);
|
|
568
|
+
},
|
|
569
|
+
}),
|
|
570
|
+
memory_promote: tool({
|
|
571
|
+
description: "Promote one reviewed local memory to Git-shareable .opencode/memory Markdown.",
|
|
572
|
+
args: {
|
|
573
|
+
id: tool.schema.string().regex(/^mem_[0-9a-f]{32}$/),
|
|
574
|
+
},
|
|
575
|
+
async execute(args, context) {
|
|
576
|
+
const keys = await session.managementScopeKeys(context.sessionID, context.agent);
|
|
577
|
+
const memories = await native.request("get", { ids: [args.id], ...keys }, context.abort);
|
|
578
|
+
const memory = memories[0];
|
|
579
|
+
if (!memory)
|
|
580
|
+
throw new Error(`Memory not found: ${args.id}`);
|
|
581
|
+
if (memory.scope === "repository") {
|
|
582
|
+
return result("Memory already shared", { id: memory.id, source: memory.source }, { id: memory.id });
|
|
583
|
+
}
|
|
584
|
+
const destination = `${SHARED_MEMORY_RELATIVE_DIR}/${memory.id}.md`;
|
|
585
|
+
await context.ask({
|
|
586
|
+
permission: "memory_promote",
|
|
587
|
+
patterns: [destination],
|
|
588
|
+
always: [],
|
|
589
|
+
metadata: {
|
|
590
|
+
operation: "promote",
|
|
591
|
+
id: memory.id,
|
|
592
|
+
title: memory.title,
|
|
593
|
+
destination,
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
const path = await writeSharedMemory(worktree, memory);
|
|
597
|
+
await syncSharedMemories(true);
|
|
598
|
+
return result("Promoted memory", { id: memory.id, path }, { id: memory.id, path });
|
|
599
|
+
},
|
|
600
|
+
}),
|
|
601
|
+
memory_purge: tool({
|
|
602
|
+
description: "Delete all local indexed memories for the current project. Shared Markdown files are preserved.",
|
|
603
|
+
args: {
|
|
604
|
+
project_id: tool.schema
|
|
605
|
+
.string()
|
|
606
|
+
.regex(/^[0-9a-f]{64}$/)
|
|
607
|
+
.describe("Exact project ID from memory_status."),
|
|
608
|
+
keep_tombstones: tool.schema.boolean().default(true),
|
|
609
|
+
},
|
|
610
|
+
async execute(args, context) {
|
|
611
|
+
await context.ask({
|
|
612
|
+
permission: "memory_purge",
|
|
613
|
+
patterns: [args.project_id],
|
|
614
|
+
always: [],
|
|
615
|
+
metadata: { operation: "purge", ...args },
|
|
616
|
+
});
|
|
617
|
+
const response = await native.request("purge", args, context.abort);
|
|
618
|
+
session.recallCache.clear();
|
|
619
|
+
session.pendingRecall.clear();
|
|
620
|
+
sharedSignature = undefined;
|
|
621
|
+
return result("Purged memory", response, response);
|
|
622
|
+
},
|
|
623
|
+
}),
|
|
624
|
+
memory_optimize: tool({
|
|
625
|
+
description: "Prune expired memories and retrieval logs, compact zvec, and rebuild indexes.",
|
|
626
|
+
args: {},
|
|
627
|
+
async execute(_args, context) {
|
|
628
|
+
const response = await native.request("optimize", {}, context.abort);
|
|
629
|
+
session.recallCache.clear();
|
|
630
|
+
return result("Optimized memory", response, response);
|
|
631
|
+
},
|
|
632
|
+
}),
|
|
633
|
+
memory_doctor: tool({
|
|
634
|
+
description: "Diagnose state compatibility, index health, retention, code anchors, and model cache.",
|
|
635
|
+
args: {
|
|
636
|
+
deep: tool.schema
|
|
637
|
+
.boolean()
|
|
638
|
+
.default(false)
|
|
639
|
+
.describe("Hash all code anchors to detect staleness."),
|
|
640
|
+
},
|
|
641
|
+
async execute(args, context) {
|
|
642
|
+
const response = await native.request("doctor", args, context.abort);
|
|
643
|
+
return result("Memory doctor", response, response);
|
|
644
|
+
},
|
|
645
|
+
}),
|
|
646
|
+
memory_status: tool({
|
|
647
|
+
description: "Inspect the current project's native memory backend, collection, embedding model, indexes, and document count.",
|
|
648
|
+
args: {},
|
|
649
|
+
async execute(_args, context) {
|
|
650
|
+
const response = await native.request("status", {}, context.abort);
|
|
651
|
+
return result("Memory status", response, response);
|
|
652
|
+
},
|
|
653
|
+
}),
|
|
654
|
+
},
|
|
655
|
+
};
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function result(title, value, metadata) {
|
|
659
|
+
return {
|
|
660
|
+
title,
|
|
661
|
+
output: JSON.stringify(value, null, 2),
|
|
662
|
+
metadata,
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
//# sourceMappingURL=plugin.js.map
|