@codemem/opencode-plugin 0.40.0-alpha.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 +726 -48
- 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.40.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
|
|
|
@@ -1036,10 +1313,15 @@ export const OpencodeMemPlugin = async ({
|
|
|
1036
1313
|
const injectionToastShown = new Set();
|
|
1037
1314
|
const messageInjectionCache = new Map();
|
|
1038
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();
|
|
1039
1320
|
let sessionStartedAt = null;
|
|
1040
1321
|
let activeSessionID = null;
|
|
1041
1322
|
let viewerStarted = false;
|
|
1042
1323
|
let promptCounter = 0;
|
|
1324
|
+
let skippedAttemptCounter = 0;
|
|
1043
1325
|
let lastPromptText = null;
|
|
1044
1326
|
let lastAssistantText = null;
|
|
1045
1327
|
const assistantUsageCaptured = new Set();
|
|
@@ -1628,6 +1910,93 @@ export const OpencodeMemPlugin = async ({
|
|
|
1628
1910
|
const runCli = async (args, options = {}) =>
|
|
1629
1911
|
runCommand([runner, ...runnerArgs, ...args], options);
|
|
1630
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
|
+
|
|
1631
2000
|
const showToast = async (message, variant = "warning") => {
|
|
1632
2001
|
if (backendUpdatePolicy === "off") {
|
|
1633
2002
|
return;
|
|
@@ -1827,28 +2196,300 @@ export const OpencodeMemPlugin = async ({
|
|
|
1827
2196
|
return masked.length > limit ? `${masked.slice(0, limit)}…` : masked;
|
|
1828
2197
|
};
|
|
1829
2198
|
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
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,
|
|
2220
|
+
});
|
|
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,
|
|
1836
2249
|
});
|
|
1837
|
-
|
|
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
|
+
}
|
|
1838
2449
|
if (!result || result.exitCode !== 0) {
|
|
1839
2450
|
const exitCode = result?.exitCode ?? "unknown";
|
|
1840
2451
|
const stderr = redactLog(result?.stderr ? result.stderr.trim() : "");
|
|
1841
2452
|
const stdout = redactLog(result?.stdout ? result.stdout.trim() : "");
|
|
1842
|
-
const cmd =
|
|
2453
|
+
const cmd = redactPackCommand(runner, runnerArgs, packArgs);
|
|
1843
2454
|
await logLine(
|
|
1844
2455
|
`inject.pack.error ${exitCode} cmd=${cmd}` +
|
|
1845
2456
|
`${stderr ? ` stderr=${stderr}` : ""}` +
|
|
1846
2457
|
`${stdout ? ` stdout=${stdout}` : ""}`
|
|
1847
2458
|
);
|
|
1848
|
-
|
|
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
|
+
};
|
|
1849
2488
|
}
|
|
1850
|
-
const packText = parsePackText(result.stdout);
|
|
1851
2489
|
if (!packText) {
|
|
2490
|
+
if (itemCount === 0) {
|
|
2491
|
+
advancePromptPackRetryIdentity(attemptKey);
|
|
2492
|
+
}
|
|
1852
2493
|
if (debug) {
|
|
1853
2494
|
const { safeQuery, firstPromptLen, lastPromptLen, projectName, filesModifiedCount } =
|
|
1854
2495
|
describeInjectQuery(query);
|
|
@@ -1856,27 +2497,46 @@ export const OpencodeMemPlugin = async ({
|
|
|
1856
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))}`
|
|
1857
2498
|
);
|
|
1858
2499
|
}
|
|
1859
|
-
return
|
|
2500
|
+
return {
|
|
2501
|
+
text: "",
|
|
2502
|
+
attemptId: identity.attemptId,
|
|
2503
|
+
requestId: identity.requestId,
|
|
2504
|
+
queryHash,
|
|
2505
|
+
promptNumber: promptCounter,
|
|
2506
|
+
};
|
|
1860
2507
|
}
|
|
1861
|
-
const metrics = parsePackMetrics(result.stdout);
|
|
1862
2508
|
// The pack JSON exposes the item count as `total_items`; `metrics.items`
|
|
1863
2509
|
// does not exist on that payload, so reading it would always log 0.
|
|
1864
|
-
const itemCount = Number.isFinite(Number(metrics?.total_items))
|
|
1865
|
-
? Number(metrics.total_items)
|
|
1866
|
-
: 0;
|
|
1867
2510
|
const packTokens = Number.isFinite(Number(metrics?.pack_tokens))
|
|
1868
2511
|
? Number(metrics.pack_tokens)
|
|
1869
2512
|
: 0;
|
|
1870
2513
|
await logLine(
|
|
1871
|
-
`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}`
|
|
1872
2515
|
);
|
|
2516
|
+
if (!repairFallbackUsed) {
|
|
2517
|
+
rememberSuccessfulPromptPackArtifact(
|
|
2518
|
+
attemptKey,
|
|
2519
|
+
retryCount,
|
|
2520
|
+
artifactFingerprint || promptPackArtifactFingerprint(result.stdout, packText)
|
|
2521
|
+
);
|
|
2522
|
+
}
|
|
1873
2523
|
if (metrics) {
|
|
1874
2524
|
return {
|
|
1875
2525
|
text: `[codemem context]\n${packText}`,
|
|
1876
2526
|
metrics,
|
|
2527
|
+
attemptId: injectedIdentity?.attemptId || null,
|
|
2528
|
+
requestId: injectedIdentity?.requestId || null,
|
|
2529
|
+
queryHash,
|
|
2530
|
+
promptNumber: promptCounter,
|
|
1877
2531
|
};
|
|
1878
2532
|
}
|
|
1879
|
-
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
|
+
};
|
|
1880
2540
|
};
|
|
1881
2541
|
|
|
1882
2542
|
const stopViewer = async () => {
|
|
@@ -2160,6 +2820,9 @@ export const OpencodeMemPlugin = async ({
|
|
|
2160
2820
|
buildInjectedContext,
|
|
2161
2821
|
messageInjectionCache,
|
|
2162
2822
|
compactionInjectionSkips,
|
|
2823
|
+
confirmDelivery: confirmPromptPackDelivery,
|
|
2824
|
+
recordCacheReuse: recordCachedPromptPack,
|
|
2825
|
+
recordSkipped: recordSkippedPromptPack,
|
|
2163
2826
|
});
|
|
2164
2827
|
} catch (err) {
|
|
2165
2828
|
await logLine(
|
|
@@ -2184,6 +2847,7 @@ export const OpencodeMemPlugin = async ({
|
|
|
2184
2847
|
}
|
|
2185
2848
|
const hookSessionID = input?.sessionID || activeSessionID;
|
|
2186
2849
|
if (consumeCompactionInjectionSkip(compactionInjectionSkips, hookSessionID)) {
|
|
2850
|
+
recordSkippedPromptPack("compaction_skipped", hookSessionID, "system");
|
|
2187
2851
|
if (debug) {
|
|
2188
2852
|
await logLine(
|
|
2189
2853
|
`inject.transform.skip_compaction sessionID=${hookSessionID || "unknown"}`
|
|
@@ -2224,6 +2888,8 @@ export const OpencodeMemPlugin = async ({
|
|
|
2224
2888
|
: null,
|
|
2225
2889
|
resolveInjectQuery,
|
|
2226
2890
|
buildInjectedContext,
|
|
2891
|
+
confirmDelivery: confirmPromptPackDelivery,
|
|
2892
|
+
recordSkipped: recordSkippedPromptPack,
|
|
2227
2893
|
});
|
|
2228
2894
|
} catch (err) {
|
|
2229
2895
|
await logLine(
|
|
@@ -2408,6 +3074,9 @@ export const OpencodeMemPlugin = async ({
|
|
|
2408
3074
|
activeSessionID = sessionID || null;
|
|
2409
3075
|
sessionStartedAt = new Date().toISOString();
|
|
2410
3076
|
promptCounter = 0;
|
|
3077
|
+
skippedAttemptCounter = 0;
|
|
3078
|
+
disabledInjectionRecorded.delete("message:unknown");
|
|
3079
|
+
disabledInjectionRecorded.delete("system:unknown");
|
|
2411
3080
|
lastPromptText = null;
|
|
2412
3081
|
lastAssistantText = null;
|
|
2413
3082
|
resetSessionContext();
|
|
@@ -2419,6 +3088,8 @@ export const OpencodeMemPlugin = async ({
|
|
|
2419
3088
|
injectionToastShown.delete(sessionID);
|
|
2420
3089
|
messageInjectionCache.delete(sessionID);
|
|
2421
3090
|
compactionInjectionSkips.delete(sessionID);
|
|
3091
|
+
disabledInjectionRecorded.delete(`message:${sessionID}`);
|
|
3092
|
+
disabledInjectionRecorded.delete(`system:${sessionID}`);
|
|
2422
3093
|
}
|
|
2423
3094
|
await stopViewer();
|
|
2424
3095
|
}
|
|
@@ -2438,15 +3109,15 @@ export const OpencodeMemPlugin = async ({
|
|
|
2438
3109
|
if (filePath) {
|
|
2439
3110
|
const lowerTool = toolName.toLowerCase();
|
|
2440
3111
|
if (lowerTool === "edit" || lowerTool === "write") {
|
|
2441
|
-
sessionContext.filesModified
|
|
3112
|
+
addWorkingSetPath(sessionContext.filesModified, filePath, cwd);
|
|
2442
3113
|
} else if (lowerTool === "read") {
|
|
2443
|
-
sessionContext.filesRead
|
|
3114
|
+
addWorkingSetPath(sessionContext.filesRead, filePath, cwd);
|
|
2444
3115
|
}
|
|
2445
3116
|
}
|
|
2446
3117
|
if (toolName.toLowerCase() === "apply_patch") {
|
|
2447
3118
|
const patchPaths = extractApplyPatchPaths(args.patchText);
|
|
2448
3119
|
for (const path of patchPaths) {
|
|
2449
|
-
sessionContext.filesModified
|
|
3120
|
+
addWorkingSetPath(sessionContext.filesModified, path, cwd);
|
|
2450
3121
|
}
|
|
2451
3122
|
}
|
|
2452
3123
|
|
|
@@ -2527,6 +3198,11 @@ export const __testUtils = {
|
|
|
2527
3198
|
resolveProjectName,
|
|
2528
3199
|
buildInjectQuery,
|
|
2529
3200
|
buildPackArgs,
|
|
3201
|
+
deterministicUuid,
|
|
3202
|
+
promptPackIdentity,
|
|
3203
|
+
hashPromptPackQuery,
|
|
3204
|
+
redactPackCommand,
|
|
3205
|
+
rejectsInternalLedgerFlag,
|
|
2530
3206
|
parsePackText,
|
|
2531
3207
|
parsePackMetrics,
|
|
2532
3208
|
resolveInjectSurface,
|
|
@@ -2537,6 +3213,8 @@ export const __testUtils = {
|
|
|
2537
3213
|
buildRunnerArgs,
|
|
2538
3214
|
appendWorkingSetFileArgs,
|
|
2539
3215
|
extractApplyPatchPaths,
|
|
3216
|
+
normalizeWorkingSetPath,
|
|
3217
|
+
addWorkingSetPath,
|
|
2540
3218
|
mapOpencodeEventTypeToAdapterType,
|
|
2541
3219
|
buildOpencodeAdapterPayload,
|
|
2542
3220
|
buildOpencodeAdapterEvent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemem/opencode-plugin",
|
|
3
|
-
"version": "0.40.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.18.
|
|
21
|
+
"@opencode-ai/plugin": "1.18.12"
|
|
22
22
|
},
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|