@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.
- package/.opencode/plugins/codemem.js +737 -49
- package/package.json +2 -2
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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 = (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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", {
|
|
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
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 {
|
|
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
|
|
3112
|
+
addWorkingSetPath(sessionContext.filesModified, filePath, cwd);
|
|
2432
3113
|
} else if (lowerTool === "read") {
|
|
2433
|
-
sessionContext.filesRead
|
|
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
|
|
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.
|
|
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.
|
|
21
|
+
"@opencode-ai/plugin": "1.18.12"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|