@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +170 -0
  3. package/THIRD_PARTY_NOTICES.md +14 -0
  4. package/dist/contracts.d.ts +96 -0
  5. package/dist/contracts.d.ts.map +1 -0
  6. package/dist/contracts.js +46 -0
  7. package/dist/contracts.js.map +1 -0
  8. package/dist/generated/memory_pb.d.ts +227 -0
  9. package/dist/generated/memory_pb.d.ts.map +1 -0
  10. package/dist/generated/memory_pb.js +112 -0
  11. package/dist/generated/memory_pb.js.map +1 -0
  12. package/dist/index.d.ts +15 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +24 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/plugin.d.ts +7 -0
  17. package/dist/plugin.d.ts.map +1 -0
  18. package/dist/plugin.js +665 -0
  19. package/dist/plugin.js.map +1 -0
  20. package/dist/policy.d.ts +19 -0
  21. package/dist/policy.d.ts.map +1 -0
  22. package/dist/policy.js +125 -0
  23. package/dist/policy.js.map +1 -0
  24. package/dist/protocol.d.ts +31 -0
  25. package/dist/protocol.d.ts.map +1 -0
  26. package/dist/protocol.js +220 -0
  27. package/dist/protocol.js.map +1 -0
  28. package/dist/session-context.d.ts +40 -0
  29. package/dist/session-context.d.ts.map +1 -0
  30. package/dist/session-context.js +92 -0
  31. package/dist/session-context.js.map +1 -0
  32. package/dist/shared-markdown.d.ts +11 -0
  33. package/dist/shared-markdown.d.ts.map +1 -0
  34. package/dist/shared-markdown.js +178 -0
  35. package/dist/shared-markdown.js.map +1 -0
  36. package/dist/sidecar-client.d.ts +39 -0
  37. package/dist/sidecar-client.d.ts.map +1 -0
  38. package/dist/sidecar-client.js +397 -0
  39. package/dist/sidecar-client.js.map +1 -0
  40. package/dist/validation.d.ts +2 -0
  41. package/dist/validation.d.ts.map +1 -0
  42. package/dist/validation.js +29 -0
  43. package/dist/validation.js.map +1 -0
  44. package/notices/ZVEC_NOTICE +95 -0
  45. package/package.json +90 -0
  46. 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