@codemem/opencode-plugin 0.39.1 → 0.40.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.
@@ -1,6 +1,6 @@
1
1
  import { appendFile, mkdir } from "node:fs/promises";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
- import { basename, dirname, join, resolve } from "node:path";
3
+ import { basename, dirname, join, posix, resolve, win32 } from "node:path";
4
4
  import { createHash } from "node:crypto";
5
5
  import { spawn as nodeSpawn, execSync } from "node:child_process";
6
6
  import { tool } from "@opencode-ai/plugin";
@@ -15,13 +15,14 @@ import {
15
15
 
16
16
  const TRUTHY_VALUES = ["1", "true", "yes"];
17
17
  const DISABLED_VALUES = ["0", "false", "off"];
18
- const PINNED_BACKEND_VERSION = "0.39.1";
18
+ const PINNED_BACKEND_VERSION = "0.40.0";
19
19
  const COMPAT_CHECK_DELAY_MS = 1500;
20
20
  const COMPAT_CHECK_CACHE_TTL_MS = 5 * 60 * 1000;
21
21
  const CODEMEM_CONTEXT_PART_ID_PREFIX = "codemem-context-";
22
22
  const MAX_MESSAGE_INJECTION_CACHE_SESSIONS = 20;
23
23
  const MAX_MESSAGE_INJECTION_CACHE_MESSAGES = 100;
24
24
  const COMPACTION_INJECTION_SKIP_TTL_MS = 30 * 1000;
25
+ const MAX_WORKING_SET_PATH_CHARS = 400;
25
26
 
26
27
  let compatCheckCache = null;
27
28
 
@@ -41,6 +42,55 @@ const resolveInjectSurface = (value) => {
41
42
 
42
43
  const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value || {}, key);
43
44
 
45
+ const deterministicUuid = (namespace, components) => {
46
+ const hex = createHash("sha256")
47
+ .update(JSON.stringify([namespace, ...components.map((value) => String(value ?? ""))]))
48
+ .digest("hex");
49
+ const bytes = Buffer.from(hex.slice(0, 32), "hex");
50
+ bytes[6] = (bytes[6] & 0x0f) | 0x50;
51
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
52
+ const value = bytes.toString("hex");
53
+ return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`;
54
+ };
55
+
56
+ const promptPackIdentity = ({
57
+ source = "opencode",
58
+ sessionID = "unknown",
59
+ requestKey,
60
+ surface,
61
+ promptNumber = 0,
62
+ queryHash,
63
+ }) => {
64
+ const components = [source, sessionID, requestKey, surface, promptNumber, queryHash];
65
+ return {
66
+ attemptId: deterministicUuid("codemem.prompt-pack.attempt.v1", components),
67
+ requestId: deterministicUuid("codemem.prompt-pack.request.v1", components),
68
+ };
69
+ };
70
+
71
+ const hashPromptPackQuery = (query) =>
72
+ createHash("sha256").update(String(query || "")).digest("hex");
73
+
74
+ const redactPackCommand = (runner, runnerArgs, packArgs) => {
75
+ const safePackArgs = [...packArgs];
76
+ if (safePackArgs[0] === "pack" && safePackArgs.length > 1) {
77
+ safePackArgs[1] = "[query-redacted]";
78
+ }
79
+ for (let index = 0; index < safePackArgs.length - 1; index += 1) {
80
+ if (safePackArgs[index] === "--working-set-file") {
81
+ safePackArgs[index + 1] = "[path-redacted]";
82
+ }
83
+ }
84
+ return [runner, ...runnerArgs, ...safePackArgs].join(" ");
85
+ };
86
+
87
+ const rejectsInternalLedgerFlag = (result) => {
88
+ if (!result || result.exitCode === 0) return false;
89
+ const diagnostics = `${result.stderr || ""}\n${result.stdout || ""}`;
90
+ return diagnostics.includes("--internal-ledger")
91
+ && /(?:unknown|unsupported|unrecognized|invalid)\s+(?:option|argument)/i.test(diagnostics);
92
+ };
93
+
44
94
  const DEFAULT_LOG_PATH = (homeDir, cwd) => `${homeDir || cwd}/.codemem/plugin.log`;
45
95
 
46
96
  const resolveLogPath = (logPathEnvRaw, cwd, homeDir) => {
@@ -161,16 +211,82 @@ const extractApplyPatchPaths = (patchText) => {
161
211
  return paths;
162
212
  };
163
213
 
214
+ const windowsPathFlavor = (value, doubleSlashIsUnc = true) =>
215
+ /^[A-Za-z]:[\\/]/.test(value)
216
+ || /^\\\\/.test(value)
217
+ || (doubleSlashIsUnc && /^\/\/[^/]+[\\/][^/]+/.test(value));
218
+
219
+ const hasTraversalSegment = (value) =>
220
+ value.replaceAll("\\", "/").split("/").includes("..");
221
+
222
+ const normalizeWorkingSetPath = (value, repositoryRoot) => {
223
+ if (typeof value !== "string" || typeof repositoryRoot !== "string") return null;
224
+ const candidate = value.trim();
225
+ if (
226
+ !candidate
227
+ || candidate.length > MAX_WORKING_SET_PATH_CHARS
228
+ || hasTraversalSegment(candidate)
229
+ ) {
230
+ return null;
231
+ }
232
+
233
+ const root = repositoryRoot.trim();
234
+ const candidateIsAbsolute = posix.isAbsolute(candidate) || win32.isAbsolute(candidate);
235
+ if (candidateIsAbsolute) {
236
+ if (!root || hasTraversalSegment(root)) return null;
237
+ const rootUsesWindows = windowsPathFlavor(root);
238
+ const candidateUsesWindows = windowsPathFlavor(candidate, rootUsesWindows);
239
+ if (candidateUsesWindows !== rootUsesWindows) return null;
240
+ const pathApi = candidateUsesWindows ? win32 : posix;
241
+ if (!pathApi.isAbsolute(root) || !pathApi.isAbsolute(candidate)) return null;
242
+ const relative = pathApi.relative(pathApi.normalize(root), pathApi.normalize(candidate));
243
+ if (
244
+ !relative
245
+ || relative === ".."
246
+ || relative.startsWith(`..${pathApi.sep}`)
247
+ || pathApi.isAbsolute(relative)
248
+ ) {
249
+ return null;
250
+ }
251
+ const normalized = relative.replaceAll("\\", "/");
252
+ return normalized.length <= MAX_WORKING_SET_PATH_CHARS ? normalized : null;
253
+ }
254
+
255
+ if (/^[A-Za-z]:/.test(candidate)) return null;
256
+ const normalized = posix.normalize(candidate.replaceAll("\\", "/")).replace(/^\.\//, "");
257
+ if (
258
+ !normalized
259
+ || normalized === "."
260
+ || normalized === ".."
261
+ || normalized.startsWith("../")
262
+ || posix.isAbsolute(normalized)
263
+ || normalized.length > MAX_WORKING_SET_PATH_CHARS
264
+ ) {
265
+ return null;
266
+ }
267
+ return normalized;
268
+ };
269
+
270
+ const addWorkingSetPath = (paths, value, repositoryRoot) => {
271
+ const normalized = normalizeWorkingSetPath(value, repositoryRoot);
272
+ if (!normalized) return null;
273
+ const duplicate = windowsPathFlavor(repositoryRoot)
274
+ ? Array.from(paths).some((existing) => existing.toLowerCase() === normalized.toLowerCase())
275
+ : paths.has(normalized);
276
+ if (!duplicate) paths.add(normalized);
277
+ return normalized;
278
+ };
279
+
164
280
  const appendWorkingSetFileArgs = (args, workingSetFiles) => {
165
281
  if (!Array.isArray(workingSetFiles) || workingSetFiles.length === 0) {
166
282
  return args;
167
283
  }
168
284
  for (const file of workingSetFiles) {
169
285
  const normalized = String(file || "").trim();
170
- if (!normalized) {
286
+ if (!normalized || normalized.length > MAX_WORKING_SET_PATH_CHARS) {
171
287
  continue;
172
288
  }
173
- args.push("--working-set-file", normalized.slice(0, 400));
289
+ args.push("--working-set-file", normalized);
174
290
  }
175
291
  return args;
176
292
  };
@@ -212,12 +328,15 @@ const buildInjectQuery = ({ firstPrompt, lastPromptText, projectName, filesModif
212
328
  return query.length > 500 ? query.slice(0, 500) : query;
213
329
  };
214
330
 
215
- const buildPackArgs = ({ query, filesModified, injectLimit, injectTokenBudget }) => {
331
+ const buildPackArgs = ({ query, filesModified, injectLimit, injectTokenBudget, internalLedger = false }) => {
216
332
  const workingSetFiles = Array.from(filesModified || [])
217
333
  .slice(-8)
218
334
  .map((value) => String(value || "").trim())
219
335
  .filter(Boolean);
220
336
  const args = ["pack", query, "--json"];
337
+ if (internalLedger) {
338
+ args.push("--internal-ledger");
339
+ }
221
340
  if (injectLimit !== null && Number.isFinite(injectLimit) && injectLimit > 0) {
222
341
  args.push("--limit", String(injectLimit));
223
342
  }
@@ -255,6 +374,63 @@ const parsePackMetrics = (stdout) => {
255
374
  }
256
375
  };
257
376
 
377
+ const parsePackOutput = (result) => {
378
+ const succeeded = result?.exitCode === 0;
379
+ let payload = null;
380
+ if (succeeded) {
381
+ try {
382
+ payload = JSON.parse(result.stdout);
383
+ } catch {
384
+ payload = null;
385
+ }
386
+ }
387
+ const ledgerConflict = payload?.ledger_outcome?.ok === false
388
+ && payload.ledger_outcome.errorCode === "retrieval_ledger_write_failed"
389
+ && payload.ledger_outcome.reason === "idempotency_conflict";
390
+ const metrics = succeeded ? payload?.metrics || null : null;
391
+ const itemCount = Number.isFinite(Number(metrics?.total_items))
392
+ ? Number(metrics.total_items)
393
+ : null;
394
+ const candidatePackText = succeeded && itemCount !== 0
395
+ ? String(payload?.pack_text || "").trim()
396
+ : "";
397
+ return {
398
+ conflictPackText: ledgerConflict ? candidatePackText : "",
399
+ ledgerConflict,
400
+ metrics,
401
+ itemCount,
402
+ // The real builder renders section headings even when it selected no
403
+ // memories. Treat the structured count as authoritative so headings alone
404
+ // are never handed to the model as retrieved context.
405
+ packText: !ledgerConflict ? candidatePackText : "",
406
+ };
407
+ };
408
+
409
+ const canonicalJson = (value) => {
410
+ if (Array.isArray(value)) {
411
+ return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
412
+ }
413
+ if (value && typeof value === "object") {
414
+ return `{${Object.keys(value)
415
+ .sort()
416
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
417
+ .join(",")}}`;
418
+ }
419
+ return JSON.stringify(value);
420
+ };
421
+
422
+ const promptPackArtifactFingerprint = (stdout, packText) => {
423
+ try {
424
+ const payload = JSON.parse(stdout);
425
+ if (/^[0-9a-f]{64}$/i.test(payload?.ledger_artifact_fingerprint || "")) {
426
+ return payload.ledger_artifact_fingerprint.toLowerCase();
427
+ }
428
+ return createHash("sha256").update(canonicalJson(payload)).digest("hex");
429
+ } catch {
430
+ return createHash("sha256").update(packText).digest("hex");
431
+ }
432
+ };
433
+
258
434
  const applyInjectedContextToOutput = async ({
259
435
  injectEnabled,
260
436
  input,
@@ -263,8 +439,11 @@ const applyInjectedContextToOutput = async ({
263
439
  showToast,
264
440
  resolveInjectQuery,
265
441
  buildInjectedContext,
442
+ confirmDelivery,
443
+ recordSkipped,
266
444
  }) => {
267
445
  if (!injectEnabled) {
446
+ recordSkipped?.("injection_disabled", input?.sessionID || null);
268
447
  return false;
269
448
  }
270
449
 
@@ -277,7 +456,12 @@ const applyInjectedContextToOutput = async ({
277
456
  // O(1) hit when the cache key isn't tied to the prompt that produced
278
457
  // the pack.
279
458
  const query = resolveInjectQuery();
280
- const injected = await buildInjectedContext(query);
459
+ const sessionID = input?.sessionID || null;
460
+ const injected = await buildInjectedContext(query, {
461
+ sessionID,
462
+ requestKey: `system:${sessionID || "unknown"}`,
463
+ surface: "system",
464
+ });
281
465
  if (!injected?.text) {
282
466
  return false;
283
467
  }
@@ -291,10 +475,20 @@ const applyInjectedContextToOutput = async ({
291
475
  }
292
476
  }
293
477
 
294
- if (!Array.isArray(output.system)) {
295
- output.system = [];
478
+ try {
479
+ if (!Array.isArray(output.system)) {
480
+ output.system = [];
481
+ }
482
+ output.system.push(injected.text);
483
+ } catch (error) {
484
+ if (injected.attemptId) {
485
+ confirmDelivery?.(injected.attemptId, "failed");
486
+ }
487
+ throw error;
488
+ }
489
+ if (injected.attemptId) {
490
+ confirmDelivery?.(injected.attemptId);
296
491
  }
297
- output.system.push(injected.text);
298
492
  return true;
299
493
  };
300
494
 
@@ -428,7 +622,7 @@ const normalizeInjectedMessageParts = (messages, sessionCache) => {
428
622
  }
429
623
  }
430
624
  if (existingText && sessionCache && messageId && !sessionCache.has(messageId)) {
431
- sessionCache.set(messageId, existingText);
625
+ sessionCache.set(messageId, { text: existingText, attemptId: null, reconstructed: true });
432
626
  trimSessionMessageInjectionCache(sessionCache);
433
627
  }
434
628
  if (retainedParts.length !== entry.parts.length) {
@@ -450,7 +644,12 @@ const buildInjectedContextPart = (entry, index, text, options = {}) => {
450
644
  };
451
645
  };
452
646
 
453
- const appendCachedInjectedContextParts = (messages, sessionCache, sessionID = null) => {
647
+ const appendCachedInjectedContextParts = async (
648
+ messages,
649
+ sessionCache,
650
+ sessionID = null,
651
+ options = {},
652
+ ) => {
454
653
  let appended = 0;
455
654
  for (let index = 0; index < messages.length; index += 1) {
456
655
  const entry = messages[index];
@@ -461,11 +660,49 @@ const appendCachedInjectedContextParts = (messages, sessionCache, sessionID = nu
461
660
  if (!messageId) {
462
661
  continue;
463
662
  }
464
- const text = sessionCache.get(messageId);
663
+ const cached = sessionCache.get(messageId);
664
+ const text = typeof cached === "string" ? cached : cached?.text;
465
665
  if (!text) {
466
666
  continue;
467
667
  }
468
- entry.parts.push(buildInjectedContextPart(entry, index, text, { messageId, sessionID }));
668
+ let attemptId = cached?.attemptId || null;
669
+ let cacheReuseReady = null;
670
+ if (
671
+ messageId === options.latestMessageId
672
+ && !options.latestWasBuilt
673
+ && attemptId
674
+ && options.recordCacheReuse
675
+ ) {
676
+ const reuse = options.recordCacheReuse(cached, {
677
+ messageId,
678
+ sessionID,
679
+ });
680
+ attemptId = typeof reuse === "string" ? reuse : reuse?.attemptId || null;
681
+ cacheReuseReady = typeof reuse === "object" ? reuse?.ready : null;
682
+ }
683
+ try {
684
+ entry.parts.push(buildInjectedContextPart(entry, index, text, { messageId, sessionID }));
685
+ } catch (error) {
686
+ if (messageId === options.latestMessageId && attemptId && options.confirmDelivery) {
687
+ if (cacheReuseReady) {
688
+ void Promise.resolve(cacheReuseReady)
689
+ .then(() => options.confirmDelivery(attemptId, "failed"))
690
+ .catch(() => {});
691
+ } else {
692
+ options.confirmDelivery(attemptId, "failed");
693
+ }
694
+ }
695
+ throw error;
696
+ }
697
+ if (messageId === options.latestMessageId && attemptId && options.confirmDelivery) {
698
+ if (cacheReuseReady) {
699
+ void Promise.resolve(cacheReuseReady)
700
+ .then(() => options.confirmDelivery(attemptId))
701
+ .catch(() => {});
702
+ } else {
703
+ options.confirmDelivery(attemptId);
704
+ }
705
+ }
469
706
  appended += 1;
470
707
  }
471
708
  return appended;
@@ -481,18 +718,26 @@ const applyInjectedContextToMessages = async ({
481
718
  buildInjectedContext,
482
719
  messageInjectionCache,
483
720
  compactionInjectionSkips,
721
+ confirmDelivery,
722
+ recordCacheReuse,
723
+ recordSkipped,
484
724
  }) => {
485
- if (!injectEnabled || !Array.isArray(output?.messages)) {
486
- return false;
487
- }
488
-
489
- const latestUser = findLatestUserMessage(output.messages);
725
+ const hasMessages = Array.isArray(output?.messages);
726
+ const latestUser = hasMessages ? findLatestUserMessage(output.messages) : null;
490
727
  const sessionID = latestUser
491
728
  ? resolveEntrySessionID(latestUser.entry) || input?.sessionID || null
492
729
  : input?.sessionID || null;
730
+ if (!injectEnabled) {
731
+ recordSkipped?.("injection_disabled", sessionID);
732
+ return false;
733
+ }
734
+ if (!hasMessages) {
735
+ return false;
736
+ }
493
737
 
494
738
  if (consumeCompactionInjectionSkip(compactionInjectionSkips, sessionID)) {
495
739
  normalizeInjectedMessageParts(output.messages, null);
740
+ recordSkipped?.("compaction_skipped", sessionID);
496
741
  return false;
497
742
  }
498
743
 
@@ -505,24 +750,49 @@ const applyInjectedContextToMessages = async ({
505
750
 
506
751
  const latestMessageId = resolveEntryMessageId(latestUser.entry);
507
752
  const canReplay = Boolean(sessionCache && latestMessageId);
508
- if (!canReplay || !sessionCache.has(latestMessageId)) {
753
+ const latestCached = canReplay ? sessionCache.get(latestMessageId) : null;
754
+ const latestWasCached = Boolean(latestCached) && latestCached?.reconstructed !== true;
755
+ let latestWasBuilt = false;
756
+ if (!latestWasCached) {
509
757
  const firstUser = findFirstUserMessage(output.messages);
510
758
  const query = resolveInjectQuery({
511
759
  firstPrompt: firstUser ? extractMessageText(firstUser.entry) : null,
512
760
  lastPromptText: extractMessageText(latestUser.entry),
513
761
  });
514
- const injected = await buildInjectedContext(query);
762
+ const injected = await buildInjectedContext(query, {
763
+ sessionID,
764
+ requestKey: latestMessageId || fallbackEntryMessageId(latestUser.entry, latestUser.index),
765
+ surface: "message",
766
+ });
515
767
  if (injected?.text) {
768
+ latestWasBuilt = true;
516
769
  if (canReplay) {
517
- sessionCache.set(latestMessageId, injected.text);
770
+ sessionCache.set(latestMessageId, {
771
+ text: injected.text,
772
+ attemptId: injected.attemptId || null,
773
+ requestId: injected.requestId || null,
774
+ queryHash: injected.queryHash || null,
775
+ promptNumber: injected.promptNumber || 0,
776
+ reuseCount: 0,
777
+ });
518
778
  trimSessionMessageInjectionCache(sessionCache);
519
779
  } else if (Array.isArray(latestUser.entry.parts)) {
520
- latestUser.entry.parts.push(
521
- buildInjectedContextPart(latestUser.entry, latestUser.index, injected.text, {
522
- messageId: latestMessageId || undefined,
523
- sessionID: sessionID || undefined,
524
- })
525
- );
780
+ try {
781
+ latestUser.entry.parts.push(
782
+ buildInjectedContextPart(latestUser.entry, latestUser.index, injected.text, {
783
+ messageId: latestMessageId || undefined,
784
+ sessionID: sessionID || undefined,
785
+ })
786
+ );
787
+ } catch (error) {
788
+ if (injected.attemptId) {
789
+ confirmDelivery?.(injected.attemptId, "failed");
790
+ }
791
+ throw error;
792
+ }
793
+ if (injected.attemptId) {
794
+ confirmDelivery?.(injected.attemptId);
795
+ }
526
796
  }
527
797
 
528
798
  const toastKey = sessionID || latestMessageId || "unknown";
@@ -534,6 +804,8 @@ const applyInjectedContextToMessages = async ({
534
804
  // best-effort only
535
805
  }
536
806
  }
807
+ } else if (canReplay && latestCached?.reconstructed === true) {
808
+ sessionCache.delete(latestMessageId);
537
809
  }
538
810
  }
539
811
 
@@ -544,7 +816,12 @@ const applyInjectedContextToMessages = async ({
544
816
  );
545
817
  }
546
818
 
547
- const appended = appendCachedInjectedContextParts(output.messages, sessionCache, sessionID);
819
+ const appended = await appendCachedInjectedContextParts(output.messages, sessionCache, sessionID, {
820
+ latestMessageId,
821
+ latestWasBuilt,
822
+ confirmDelivery,
823
+ recordCacheReuse,
824
+ });
548
825
  return appended > 0;
549
826
  };
550
827
 
@@ -886,7 +1163,14 @@ const detectRunner = ({ cwd, envRunner }) => {
886
1163
  }
887
1164
  // Prefer the TS codemem if installed globally, fall back to npx
888
1165
  try {
889
- const versionOutput = execSync("codemem --version", { encoding: "utf-8", timeout: 3000 }).trim();
1166
+ const versionOutput = execSync("codemem --version", {
1167
+ encoding: "utf-8",
1168
+ timeout: 3000,
1169
+ // Suppress shell "not found" noise when codemem is not on PATH; the
1170
+ // catch below falls back to npx. Without this, stderr leaks to the
1171
+ // terminal on every OpenCode startup for npx-only installs.
1172
+ stdio: ["ignore", "pipe", "ignore"],
1173
+ }).trim();
890
1174
  // Accept any global codemem at or above the pinned backend version, so a
891
1175
  // newer global install is used instead of silently downgrading to
892
1176
  // `npx codemem@<pin>` (the old check only matched the exact pin or the
@@ -1029,10 +1313,15 @@ export const OpencodeMemPlugin = async ({
1029
1313
  const injectionToastShown = new Set();
1030
1314
  const messageInjectionCache = new Map();
1031
1315
  const compactionInjectionSkips = new Map();
1316
+ const disabledInjectionRecorded = new Set();
1317
+ const attemptStartedAt = new Map();
1318
+ const promptPackRetryCounts = new Map();
1319
+ const successfulPromptPackArtifacts = new Map();
1032
1320
  let sessionStartedAt = null;
1033
1321
  let activeSessionID = null;
1034
1322
  let viewerStarted = false;
1035
1323
  let promptCounter = 0;
1324
+ let skippedAttemptCounter = 0;
1036
1325
  let lastPromptText = null;
1037
1326
  let lastAssistantText = null;
1038
1327
  const assistantUsageCaptured = new Set();
@@ -1621,6 +1910,93 @@ export const OpencodeMemPlugin = async ({
1621
1910
  const runCli = async (args, options = {}) =>
1622
1911
  runCommand([runner, ...runnerArgs, ...args], options);
1623
1912
 
1913
+ const attemptMetadata = (identity, sessionID = null, promptNumber = promptCounter) => ({
1914
+ attempt_id: identity.attemptId,
1915
+ started_at: (() => {
1916
+ const existing = attemptStartedAt.get(identity.attemptId);
1917
+ if (existing) return existing;
1918
+ const created = new Date().toISOString();
1919
+ attemptStartedAt.set(identity.attemptId, created);
1920
+ while (attemptStartedAt.size > 2000) {
1921
+ const oldest = attemptStartedAt.keys().next().value;
1922
+ if (!oldest) break;
1923
+ attemptStartedAt.delete(oldest);
1924
+ }
1925
+ return created;
1926
+ })(),
1927
+ source: "opencode",
1928
+ ...(sessionID ? { stream_id: String(sessionID), source_session_id: String(sessionID) } : {}),
1929
+ ...(promptNumber > 0 ? { prompt_number: promptNumber } : {}),
1930
+ request_id: identity.requestId,
1931
+ });
1932
+
1933
+ const runPromptPackLedger = async (payload) => {
1934
+ try {
1935
+ return await runCli(["prompt-pack-ledger"], {
1936
+ stdinText: JSON.stringify(payload),
1937
+ });
1938
+ } catch {
1939
+ return null;
1940
+ }
1941
+ };
1942
+
1943
+ const skippedIdentity = (failureCode, sessionID, surface, eventKey) =>
1944
+ promptPackIdentity({
1945
+ sessionID: sessionID || "unknown",
1946
+ requestKey: `${failureCode}:${eventKey}`,
1947
+ surface,
1948
+ promptNumber: promptCounter,
1949
+ queryHash: hashPromptPackQuery(""),
1950
+ });
1951
+
1952
+ const recordSkippedPromptPack = (failureCode, sessionID = null, surface = injectSurface) => {
1953
+ const sessionKey = String(sessionID || "unknown");
1954
+ const memoKey = `${surface}:${sessionKey}`;
1955
+ if (failureCode === "injection_disabled" && disabledInjectionRecorded.has(memoKey)) {
1956
+ return null;
1957
+ }
1958
+ if (failureCode === "injection_disabled") disabledInjectionRecorded.add(memoKey);
1959
+ const identity = skippedIdentity(
1960
+ failureCode,
1961
+ sessionID,
1962
+ surface,
1963
+ failureCode === "injection_disabled" ? "once" : `event-${++skippedAttemptCounter}`,
1964
+ );
1965
+ void runPromptPackLedger({
1966
+ action: "record",
1967
+ ...attemptMetadata(identity, sessionID),
1968
+ retrieval_status: "skipped",
1969
+ failure_code: failureCode,
1970
+ failure_stage: "policy",
1971
+ });
1972
+ return identity.attemptId;
1973
+ };
1974
+
1975
+ const recordCachedPromptPack = (cached, { messageId, sessionID } = {}) => {
1976
+ cached.reuseCount = (cached.reuseCount || 0) + 1;
1977
+ const identity = promptPackIdentity({
1978
+ sessionID: sessionID || "unknown",
1979
+ requestKey: `${messageId || "unknown"}:cache:${cached.reuseCount}`,
1980
+ surface: "message",
1981
+ promptNumber: cached.promptNumber || promptCounter,
1982
+ queryHash: cached.queryHash || hashPromptPackQuery(""),
1983
+ });
1984
+ const ready = runPromptPackLedger({
1985
+ action: "cache_reuse",
1986
+ ...attemptMetadata(identity, sessionID, cached.promptNumber || promptCounter),
1987
+ original_attempt_id: cached.attemptId,
1988
+ });
1989
+ return { attemptId: identity.attemptId, ready };
1990
+ };
1991
+
1992
+ const confirmPromptPackDelivery = (attemptId, deliveryStatus = "handed_off") => {
1993
+ void runPromptPackLedger({
1994
+ action: "delivery",
1995
+ attempt_id: attemptId,
1996
+ delivery_status: deliveryStatus,
1997
+ });
1998
+ };
1999
+
1624
2000
  const showToast = async (message, variant = "warning") => {
1625
2001
  if (backendUpdatePolicy === "off") {
1626
2002
  return;
@@ -1820,28 +2196,300 @@ export const OpencodeMemPlugin = async ({
1820
2196
  return masked.length > limit ? `${masked.slice(0, limit)}…` : masked;
1821
2197
  };
1822
2198
 
1823
- const buildInjectedContext = async (query) => {
1824
- const packArgs = buildPackArgs({
1825
- query,
1826
- filesModified: sessionContext.filesModified,
1827
- injectLimit,
1828
- injectTokenBudget,
2199
+ const advancePromptPackRetryIdentity = (attemptKey) => {
2200
+ promptPackRetryCounts.set(
2201
+ attemptKey,
2202
+ (promptPackRetryCounts.get(attemptKey) || 0) + 1
2203
+ );
2204
+ while (promptPackRetryCounts.size > 2000) {
2205
+ const oldest = promptPackRetryCounts.keys().next().value;
2206
+ if (!oldest) break;
2207
+ promptPackRetryCounts.delete(oldest);
2208
+ }
2209
+ };
2210
+
2211
+ const rememberSuccessfulPromptPackArtifact = (
2212
+ attemptKey,
2213
+ retryCount,
2214
+ fingerprint
2215
+ ) => {
2216
+ successfulPromptPackArtifacts.delete(attemptKey);
2217
+ successfulPromptPackArtifacts.set(attemptKey, {
2218
+ retryCount,
2219
+ fingerprint,
1829
2220
  });
1830
- const result = await runCli(packArgs);
2221
+ while (successfulPromptPackArtifacts.size > 2000) {
2222
+ const oldest = successfulPromptPackArtifacts.keys().next().value;
2223
+ if (!oldest) break;
2224
+ successfulPromptPackArtifacts.delete(oldest);
2225
+ }
2226
+ };
2227
+
2228
+ const buildInjectedContext = async (query, context = {}) => {
2229
+ const queryHash = hashPromptPackQuery(query);
2230
+ const sessionID = context.sessionID || activeSessionID || "unknown";
2231
+ const surface = context.surface || injectSurface;
2232
+ const requestKey = context.requestKey || "unknown";
2233
+ const attemptKey = JSON.stringify([
2234
+ sessionID,
2235
+ requestKey,
2236
+ surface,
2237
+ promptCounter,
2238
+ queryHash,
2239
+ ]);
2240
+ let retryCount = promptPackRetryCounts.get(attemptKey) || 0;
2241
+ const resolveIdentity = () => promptPackIdentity({
2242
+ sessionID,
2243
+ requestKey: retryCount > 0
2244
+ ? `${requestKey}:empty-retry:${retryCount}`
2245
+ : requestKey,
2246
+ surface,
2247
+ promptNumber: promptCounter,
2248
+ queryHash,
2249
+ });
2250
+ let identity = resolveIdentity();
2251
+ let metadata = attemptMetadata(
2252
+ identity,
2253
+ context.sessionID || activeSessionID || null,
2254
+ );
2255
+ const runPack = async () => {
2256
+ let packArgs = buildPackArgs({
2257
+ query,
2258
+ filesModified: sessionContext.filesModified,
2259
+ injectLimit,
2260
+ injectTokenBudget,
2261
+ internalLedger: true,
2262
+ });
2263
+ let result = await runCli(packArgs, { stdinText: JSON.stringify(metadata) });
2264
+ if (rejectsInternalLedgerFlag(result)) {
2265
+ packArgs = packArgs.filter((arg) => arg !== "--internal-ledger");
2266
+ result = await runCli(packArgs);
2267
+ }
2268
+ return { packArgs, result };
2269
+ };
2270
+ let { packArgs, result } = await runPack();
2271
+ let { packText, conflictPackText, metrics, itemCount, ledgerConflict } = parsePackOutput(result);
2272
+ let artifactFingerprint = packText
2273
+ ? promptPackArtifactFingerprint(result.stdout, packText)
2274
+ : "";
2275
+ let injectedIdentity = identity;
2276
+ let repairFallbackUsed = false;
2277
+ if (ledgerConflict) {
2278
+ // A restarted plugin has no artifact cache to predict this conflict. The
2279
+ // CLI marker is authoritative: never attribute delivery to the stale
2280
+ // identity, and retry once with a fresh deterministic identity.
2281
+ await log("warn", "codemem prompt-pack ledger conflict; retrying with fresh identity", {
2282
+ sessionID,
2283
+ surface,
2284
+ });
2285
+ const fallback = conflictPackText
2286
+ ? {
2287
+ packArgs,
2288
+ result,
2289
+ packText: conflictPackText,
2290
+ metrics,
2291
+ itemCount,
2292
+ artifactFingerprint: promptPackArtifactFingerprint(result.stdout, conflictPackText),
2293
+ ledgerConflict,
2294
+ }
2295
+ : null;
2296
+ advancePromptPackRetryIdentity(attemptKey);
2297
+ retryCount = promptPackRetryCounts.get(attemptKey) || 0;
2298
+ identity = resolveIdentity();
2299
+ metadata = attemptMetadata(
2300
+ identity,
2301
+ context.sessionID || activeSessionID || null,
2302
+ );
2303
+ ({ packArgs, result } = await runPack());
2304
+ // Repair-conflict bytes are never preferred over the original preserved pack.
2305
+ ({ packText, metrics, itemCount, ledgerConflict } = parsePackOutput(result));
2306
+ artifactFingerprint = packText
2307
+ ? promptPackArtifactFingerprint(result.stdout, packText)
2308
+ : "";
2309
+ const repairFailed = ledgerConflict
2310
+ || !result
2311
+ || result.exitCode !== 0
2312
+ || (!packText && itemCount !== 0);
2313
+ if (fallback && repairFailed) {
2314
+ if (ledgerConflict) {
2315
+ await log("warn", "codemem prompt-pack fresh identity also conflicted", {
2316
+ sessionID,
2317
+ surface,
2318
+ });
2319
+ } else {
2320
+ const malformedSuccess = result?.exitCode === 0;
2321
+ const exitCode = result?.exitCode ?? "unknown";
2322
+ const stderr = redactLog(result?.stderr ? result.stderr.trim() : "");
2323
+ const cmd = redactPackCommand(runner, runnerArgs, packArgs);
2324
+ await logLine(
2325
+ `inject.pack.identity_repair_failed reason=${malformedSuccess ? "malformed_success" : "command_failed"} exit=${exitCode} cmd=${cmd}` +
2326
+ `${stderr ? ` stderr=${stderr}` : ""}`
2327
+ );
2328
+ await log("warn", "codemem prompt-pack identity repair failed", {
2329
+ reason: malformedSuccess ? "malformed_success" : "command_failed",
2330
+ exitCode,
2331
+ });
2332
+ void runPromptPackLedger({
2333
+ action: "record",
2334
+ ...metadata,
2335
+ retrieval_status: "failed",
2336
+ failure_code: malformedSuccess
2337
+ ? "pack_identity_repair_failed"
2338
+ : "pack_command_failed",
2339
+ failure_stage: malformedSuccess ? "decode" : "transport",
2340
+ });
2341
+ }
2342
+ advancePromptPackRetryIdentity(attemptKey);
2343
+ retryCount = promptPackRetryCounts.get(attemptKey) || 0;
2344
+ ({
2345
+ packArgs,
2346
+ result,
2347
+ packText,
2348
+ metrics,
2349
+ itemCount,
2350
+ artifactFingerprint,
2351
+ ledgerConflict,
2352
+ } = fallback);
2353
+ // Neither persisted identity represents fallback delivery: the first
2354
+ // conflicted and the fresh repair failed. Keep the bytes fail-open but
2355
+ // leave delivery and replay attribution empty.
2356
+ injectedIdentity = null;
2357
+ repairFallbackUsed = true;
2358
+ } else {
2359
+ injectedIdentity = identity;
2360
+ }
2361
+ }
2362
+ if (packText) {
2363
+ const previous = successfulPromptPackArtifacts.get(attemptKey);
2364
+ if (
2365
+ previous?.retryCount === retryCount
2366
+ && previous.fingerprint !== artifactFingerprint
2367
+ ) {
2368
+ // The in-memory fingerprint detected a changed artifact before handoff.
2369
+ // Defensively rebuild once with a fresh identity; older CLIs do not
2370
+ // expose the persisted conflict marker used by the restart path above.
2371
+ // Keep the usable changed bytes as a fail-open fallback: diagnostics
2372
+ // identity repair must never suppress context injection.
2373
+ const fallback = {
2374
+ packArgs,
2375
+ result,
2376
+ packText,
2377
+ metrics,
2378
+ itemCount,
2379
+ artifactFingerprint,
2380
+ ledgerConflict,
2381
+ };
2382
+ advancePromptPackRetryIdentity(attemptKey);
2383
+ retryCount = promptPackRetryCounts.get(attemptKey) || 0;
2384
+ identity = resolveIdentity();
2385
+ metadata = attemptMetadata(
2386
+ identity,
2387
+ context.sessionID || activeSessionID || null,
2388
+ );
2389
+ ({ packArgs, result } = await runPack());
2390
+ ({ packText, metrics, itemCount, ledgerConflict } = parsePackOutput(result));
2391
+ artifactFingerprint = packText
2392
+ ? promptPackArtifactFingerprint(result.stdout, packText)
2393
+ : "";
2394
+
2395
+ if (ledgerConflict) {
2396
+ // The repair identity is also persisted with different artifacts.
2397
+ // Preserve the marker for the fail-closed return below; never use the
2398
+ // stale changed bytes as the transport-failure fallback.
2399
+ injectedIdentity = identity;
2400
+ } else if (result?.exitCode === 0 && itemCount === 0) {
2401
+ // A fresh identity can legitimately resolve to no results if the
2402
+ // underlying memory set changed between rebuilds. The CLI already
2403
+ // recorded that terminal outcome; do not misclassify it as a decode
2404
+ // failure or fall back to stale non-empty context.
2405
+ injectedIdentity = identity;
2406
+ } else if (!result || result.exitCode !== 0 || !packText) {
2407
+ const malformedSuccess = result?.exitCode === 0;
2408
+ const exitCode = result?.exitCode ?? "unknown";
2409
+ const stderr = redactLog(result?.stderr ? result.stderr.trim() : "");
2410
+ const cmd = redactPackCommand(runner, runnerArgs, packArgs);
2411
+ await logLine(
2412
+ `inject.pack.identity_repair_failed reason=${malformedSuccess ? "malformed_success" : "command_failed"} exit=${exitCode} cmd=${cmd}` +
2413
+ `${stderr ? ` stderr=${stderr}` : ""}`
2414
+ );
2415
+ await log("warn", "codemem prompt-pack identity repair failed", {
2416
+ reason: malformedSuccess ? "malformed_success" : "command_failed",
2417
+ exitCode,
2418
+ });
2419
+ void runPromptPackLedger({
2420
+ action: "record",
2421
+ ...metadata,
2422
+ retrieval_status: "failed",
2423
+ failure_code: malformedSuccess
2424
+ ? "pack_identity_repair_failed"
2425
+ : "pack_command_failed",
2426
+ failure_stage: malformedSuccess ? "decode" : "transport",
2427
+ });
2428
+ advancePromptPackRetryIdentity(attemptKey);
2429
+
2430
+ ({
2431
+ packArgs,
2432
+ result,
2433
+ packText,
2434
+ metrics,
2435
+ itemCount,
2436
+ artifactFingerprint,
2437
+ ledgerConflict,
2438
+ } = fallback);
2439
+ // The stale attempt does not represent these changed bytes, and the
2440
+ // fresh repair attempt failed. Inject without delivery attribution
2441
+ // rather than falsely marking either ledger attempt handed off.
2442
+ injectedIdentity = null;
2443
+ repairFallbackUsed = true;
2444
+ } else {
2445
+ injectedIdentity = identity;
2446
+ }
2447
+ }
2448
+ }
1831
2449
  if (!result || result.exitCode !== 0) {
1832
2450
  const exitCode = result?.exitCode ?? "unknown";
1833
2451
  const stderr = redactLog(result?.stderr ? result.stderr.trim() : "");
1834
2452
  const stdout = redactLog(result?.stdout ? result.stdout.trim() : "");
1835
- const cmd = [runner, ...runnerArgs, ...packArgs].join(" ");
2453
+ const cmd = redactPackCommand(runner, runnerArgs, packArgs);
1836
2454
  await logLine(
1837
2455
  `inject.pack.error ${exitCode} cmd=${cmd}` +
1838
2456
  `${stderr ? ` stderr=${stderr}` : ""}` +
1839
2457
  `${stdout ? ` stdout=${stdout}` : ""}`
1840
2458
  );
1841
- return "";
2459
+ void runPromptPackLedger({
2460
+ action: "record",
2461
+ ...metadata,
2462
+ retrieval_status: "failed",
2463
+ failure_code: "pack_command_failed",
2464
+ failure_stage: "transport",
2465
+ });
2466
+ advancePromptPackRetryIdentity(attemptKey);
2467
+ return {
2468
+ text: "",
2469
+ attemptId: identity.attemptId,
2470
+ requestId: identity.requestId,
2471
+ queryHash,
2472
+ promptNumber: promptCounter,
2473
+ };
2474
+ }
2475
+ if (ledgerConflict && !repairFallbackUsed) {
2476
+ await log("warn", "codemem prompt-pack fresh identity also conflicted", {
2477
+ sessionID,
2478
+ surface,
2479
+ });
2480
+ advancePromptPackRetryIdentity(attemptKey);
2481
+ return {
2482
+ text: "",
2483
+ attemptId: identity.attemptId,
2484
+ requestId: identity.requestId,
2485
+ queryHash,
2486
+ promptNumber: promptCounter,
2487
+ };
1842
2488
  }
1843
- const packText = parsePackText(result.stdout);
1844
2489
  if (!packText) {
2490
+ if (itemCount === 0) {
2491
+ advancePromptPackRetryIdentity(attemptKey);
2492
+ }
1845
2493
  if (debug) {
1846
2494
  const { safeQuery, firstPromptLen, lastPromptLen, projectName, filesModifiedCount } =
1847
2495
  describeInjectQuery(query);
@@ -1849,27 +2497,46 @@ export const OpencodeMemPlugin = async ({
1849
2497
  `inject.pack.empty query_len=${query ? query.length : 0} query=${JSON.stringify(safeQuery)} first_prompt_len=${firstPromptLen} last_prompt_len=${lastPromptLen} project=${JSON.stringify(projectName)} files_modified=${filesModifiedCount} stdout=${JSON.stringify(redactLog((result.stdout || "").trim(), 240))}`
1850
2498
  );
1851
2499
  }
1852
- return "";
2500
+ return {
2501
+ text: "",
2502
+ attemptId: identity.attemptId,
2503
+ requestId: identity.requestId,
2504
+ queryHash,
2505
+ promptNumber: promptCounter,
2506
+ };
1853
2507
  }
1854
- const metrics = parsePackMetrics(result.stdout);
1855
2508
  // The pack JSON exposes the item count as `total_items`; `metrics.items`
1856
2509
  // does not exist on that payload, so reading it would always log 0.
1857
- const itemCount = Number.isFinite(Number(metrics?.total_items))
1858
- ? Number(metrics.total_items)
1859
- : 0;
1860
2510
  const packTokens = Number.isFinite(Number(metrics?.pack_tokens))
1861
2511
  ? Number(metrics.pack_tokens)
1862
2512
  : 0;
1863
2513
  await logLine(
1864
- `inject.pack.ok source=opencode items=${itemCount} pack_tokens=${packTokens} query_len=${query ? query.length : 0}`
2514
+ `inject.pack.ok source=opencode items=${itemCount ?? 0} pack_tokens=${packTokens} query_len=${query ? query.length : 0}`
1865
2515
  );
2516
+ if (!repairFallbackUsed) {
2517
+ rememberSuccessfulPromptPackArtifact(
2518
+ attemptKey,
2519
+ retryCount,
2520
+ artifactFingerprint || promptPackArtifactFingerprint(result.stdout, packText)
2521
+ );
2522
+ }
1866
2523
  if (metrics) {
1867
2524
  return {
1868
2525
  text: `[codemem context]\n${packText}`,
1869
2526
  metrics,
2527
+ attemptId: injectedIdentity?.attemptId || null,
2528
+ requestId: injectedIdentity?.requestId || null,
2529
+ queryHash,
2530
+ promptNumber: promptCounter,
1870
2531
  };
1871
2532
  }
1872
- return { text: `[codemem context]\n${packText}` };
2533
+ return {
2534
+ text: `[codemem context]\n${packText}`,
2535
+ attemptId: injectedIdentity?.attemptId || null,
2536
+ requestId: injectedIdentity?.requestId || null,
2537
+ queryHash,
2538
+ promptNumber: promptCounter,
2539
+ };
1873
2540
  };
1874
2541
 
1875
2542
  const stopViewer = async () => {
@@ -1946,6 +2613,9 @@ export const OpencodeMemPlugin = async ({
1946
2613
  cwd: runnerFrom,
1947
2614
  timeout: 500,
1948
2615
  encoding: "utf-8",
2616
+ // Suppress "fatal: not a git repository" when the working directory is
2617
+ // not a git repo. The catch below leaves version as "unknown".
2618
+ stdio: ["ignore", "pipe", "ignore"],
1949
2619
  }).trim();
1950
2620
  } catch (err) {
1951
2621
  // Ignore - version will remain 'unknown'
@@ -2150,6 +2820,9 @@ export const OpencodeMemPlugin = async ({
2150
2820
  buildInjectedContext,
2151
2821
  messageInjectionCache,
2152
2822
  compactionInjectionSkips,
2823
+ confirmDelivery: confirmPromptPackDelivery,
2824
+ recordCacheReuse: recordCachedPromptPack,
2825
+ recordSkipped: recordSkippedPromptPack,
2153
2826
  });
2154
2827
  } catch (err) {
2155
2828
  await logLine(
@@ -2174,6 +2847,7 @@ export const OpencodeMemPlugin = async ({
2174
2847
  }
2175
2848
  const hookSessionID = input?.sessionID || activeSessionID;
2176
2849
  if (consumeCompactionInjectionSkip(compactionInjectionSkips, hookSessionID)) {
2850
+ recordSkippedPromptPack("compaction_skipped", hookSessionID, "system");
2177
2851
  if (debug) {
2178
2852
  await logLine(
2179
2853
  `inject.transform.skip_compaction sessionID=${hookSessionID || "unknown"}`
@@ -2214,6 +2888,8 @@ export const OpencodeMemPlugin = async ({
2214
2888
  : null,
2215
2889
  resolveInjectQuery,
2216
2890
  buildInjectedContext,
2891
+ confirmDelivery: confirmPromptPackDelivery,
2892
+ recordSkipped: recordSkippedPromptPack,
2217
2893
  });
2218
2894
  } catch (err) {
2219
2895
  await logLine(
@@ -2398,6 +3074,9 @@ export const OpencodeMemPlugin = async ({
2398
3074
  activeSessionID = sessionID || null;
2399
3075
  sessionStartedAt = new Date().toISOString();
2400
3076
  promptCounter = 0;
3077
+ skippedAttemptCounter = 0;
3078
+ disabledInjectionRecorded.delete("message:unknown");
3079
+ disabledInjectionRecorded.delete("system:unknown");
2401
3080
  lastPromptText = null;
2402
3081
  lastAssistantText = null;
2403
3082
  resetSessionContext();
@@ -2409,6 +3088,8 @@ export const OpencodeMemPlugin = async ({
2409
3088
  injectionToastShown.delete(sessionID);
2410
3089
  messageInjectionCache.delete(sessionID);
2411
3090
  compactionInjectionSkips.delete(sessionID);
3091
+ disabledInjectionRecorded.delete(`message:${sessionID}`);
3092
+ disabledInjectionRecorded.delete(`system:${sessionID}`);
2412
3093
  }
2413
3094
  await stopViewer();
2414
3095
  }
@@ -2428,15 +3109,15 @@ export const OpencodeMemPlugin = async ({
2428
3109
  if (filePath) {
2429
3110
  const lowerTool = toolName.toLowerCase();
2430
3111
  if (lowerTool === "edit" || lowerTool === "write") {
2431
- sessionContext.filesModified.add(filePath);
3112
+ addWorkingSetPath(sessionContext.filesModified, filePath, cwd);
2432
3113
  } else if (lowerTool === "read") {
2433
- sessionContext.filesRead.add(filePath);
3114
+ addWorkingSetPath(sessionContext.filesRead, filePath, cwd);
2434
3115
  }
2435
3116
  }
2436
3117
  if (toolName.toLowerCase() === "apply_patch") {
2437
3118
  const patchPaths = extractApplyPatchPaths(args.patchText);
2438
3119
  for (const path of patchPaths) {
2439
- sessionContext.filesModified.add(path);
3120
+ addWorkingSetPath(sessionContext.filesModified, path, cwd);
2440
3121
  }
2441
3122
  }
2442
3123
 
@@ -2517,6 +3198,11 @@ export const __testUtils = {
2517
3198
  resolveProjectName,
2518
3199
  buildInjectQuery,
2519
3200
  buildPackArgs,
3201
+ deterministicUuid,
3202
+ promptPackIdentity,
3203
+ hashPromptPackQuery,
3204
+ redactPackCommand,
3205
+ rejectsInternalLedgerFlag,
2520
3206
  parsePackText,
2521
3207
  parsePackMetrics,
2522
3208
  resolveInjectSurface,
@@ -2527,6 +3213,8 @@ export const __testUtils = {
2527
3213
  buildRunnerArgs,
2528
3214
  appendWorkingSetFileArgs,
2529
3215
  extractApplyPatchPaths,
3216
+ normalizeWorkingSetPath,
3217
+ addWorkingSetPath,
2530
3218
  mapOpencodeEventTypeToAdapterType,
2531
3219
  buildOpencodeAdapterPayload,
2532
3220
  buildOpencodeAdapterEvent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemem/opencode-plugin",
3
- "version": "0.39.1",
3
+ "version": "0.40.0",
4
4
  "description": "CodeMem plugin for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -18,7 +18,7 @@
18
18
  ".opencode/lib/"
19
19
  ],
20
20
  "dependencies": {
21
- "@opencode-ai/plugin": "1.17.13"
21
+ "@opencode-ai/plugin": "1.18.12"
22
22
  },
23
23
  "repository": {
24
24
  "type": "git",