@nguyenthdat/opencode-memory 0.2.0 → 0.3.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/README.md +32 -10
  2. package/dist/contracts.d.ts +27 -1
  3. package/dist/contracts.d.ts.map +1 -1
  4. package/dist/generated/{memory_pb.d.ts → opencode/memory/v1/memory_pb.d.ts} +15 -3
  5. package/dist/generated/opencode/memory/v1/memory_pb.d.ts.map +1 -0
  6. package/dist/generated/{memory_pb.js → opencode/memory/v1/memory_pb.js} +21 -9
  7. package/dist/generated/opencode/memory/v1/memory_pb.js.map +1 -0
  8. package/dist/index.d.ts +3 -2
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -2
  11. package/dist/index.js.map +1 -1
  12. package/dist/instructions.d.ts +12 -0
  13. package/dist/instructions.d.ts.map +1 -0
  14. package/dist/instructions.js +41 -0
  15. package/dist/instructions.js.map +1 -0
  16. package/dist/plugin.d.ts +16 -0
  17. package/dist/plugin.d.ts.map +1 -1
  18. package/dist/plugin.js +202 -63
  19. package/dist/plugin.js.map +1 -1
  20. package/dist/policy.d.ts +13 -3
  21. package/dist/policy.d.ts.map +1 -1
  22. package/dist/policy.js +78 -42
  23. package/dist/policy.js.map +1 -1
  24. package/dist/protocol.d.ts +4 -1
  25. package/dist/protocol.d.ts.map +1 -1
  26. package/dist/protocol.js +4 -1
  27. package/dist/protocol.js.map +1 -1
  28. package/dist/session-context.d.ts +8 -0
  29. package/dist/session-context.d.ts.map +1 -1
  30. package/dist/session-context.js +47 -0
  31. package/dist/session-context.js.map +1 -1
  32. package/dist/shared-markdown.d.ts +2 -5
  33. package/dist/shared-markdown.d.ts.map +1 -1
  34. package/dist/shared-markdown.js +70 -27
  35. package/dist/shared-markdown.js.map +1 -1
  36. package/dist/sidecar-client.d.ts.map +1 -1
  37. package/dist/sidecar-client.js +16 -17
  38. package/dist/sidecar-client.js.map +1 -1
  39. package/dist/validation.d.ts +2 -0
  40. package/dist/validation.d.ts.map +1 -1
  41. package/dist/validation.js +8 -0
  42. package/dist/validation.js.map +1 -1
  43. package/package.json +8 -8
  44. package/rules/flow.md +12 -12
  45. package/dist/generated/memory_pb.d.ts.map +0 -1
  46. 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 { MEMORY_POLICY, MEMORY_POLICY_MARKER, COMPACTION_CONTEXT, formatRecalledMemories, truncateText, contextBudgetChars, parseCuratedCandidates, } from "./policy.js";
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 native = new NativeMemoryClient(options.root, worktree);
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(worktree);
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.rejected_sources.join(", ")}`);
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.recallCache.clear();
39
+ session.invalidateRecall();
29
40
  })().finally(() => {
30
41
  sharedSync = undefined;
31
42
  });
32
43
  await sharedSync;
33
44
  };
34
- if (options.warmup !== false) {
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
- await Promise.all([...session.pendingRecall.keys()].map((sessID) => session.closePendingRecall(sessID, "ignored")));
50
+ for (const sessID of session.pendingRecall.keys())
51
+ session.discardPendingRecall(sessID);
40
52
  session.latestQuery.clear();
41
- session.recallCache.clear();
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
- await session.closePendingRecall(sessID, "ignored");
80
+ session.discardPendingRecall(sessID);
68
81
  session.latestQuery.delete(sessID);
69
- session.recallCache.delete(sessID);
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
- await session.closePendingRecall(event.properties.sessionID, "ignored");
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.recallCache.clear();
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("store", {
113
- ...candidate,
114
- source: `session:${event.properties.sessionID}:compaction`,
115
- scope: "project",
116
- origin: "auto_compaction",
117
- revive: false,
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 (candidates.length > 0)
125
- session.recallCache.clear();
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
- 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();
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(MEMORY_POLICY_MARKER))) {
149
- output.system.push(MEMORY_POLICY);
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
- const latest = session.latestQuery.get(input.sessionID);
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 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,
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 || !cached.response.retrieval_id)
243
+ if (!formatted)
244
+ return;
245
+ if (!settings.feedbackTracking || !cached.response.retrieval_id) {
246
+ output.system.push(formatted.text);
199
247
  return;
200
- output.system.push(formatted.text);
248
+ }
201
249
  const pending = {
202
250
  retrievalID: cached.response.retrieval_id,
203
251
  memoryIDs: formatted.memoryIDs,
204
252
  };
205
- await session.recordFeedback(pending, "injected");
206
- session.pendingRecall.set(input.sessionID, pending);
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(0.42)
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
- await session.closePendingRecall(context.sessionID, "ignored");
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: true,
332
+ track_feedback: settings.feedbackTracking,
281
333
  }, context.abort);
282
- if (response.retrieval_id && response.memories.length > 0) {
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.recordFeedback(pending, "injected");
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.recallCache.clear();
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.recallCache.clear();
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.recallCache.clear();
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.recallCache.clear();
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.recallCache.clear();
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
- .default([]),
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(worktree, memory);
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.recallCache.clear();
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.recallCache.clear();
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,