@codemem/opencode-plugin 0.40.0-alpha.1 → 0.40.1
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 +803 -52
- 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.
|
|
18
|
+
const PINNED_BACKEND_VERSION = "0.40.1";
|
|
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,94 @@ 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
|
+
|
|
94
|
+
const parseFallbackStructuredError = (stdout) => {
|
|
95
|
+
const lines = String(stdout || "").trim().split(/\r?\n/).reverse();
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(line);
|
|
99
|
+
if (parsed && typeof parsed === "object") {
|
|
100
|
+
return {
|
|
101
|
+
code: typeof parsed.error === "string" ? parsed.error : "",
|
|
102
|
+
message: typeof parsed.message === "string" ? parsed.message : "",
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// Continue past non-JSON CLI output.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { code: "", message: "" };
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const classifyFallbackCommandResult = (result) => {
|
|
113
|
+
const structured = parseFallbackStructuredError(result?.stdout);
|
|
114
|
+
const diagnostic = [structured.code, structured.message, result?.stderr, result?.stdout]
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.join("\n");
|
|
117
|
+
if (/SQLITE_(?:BUSY|LOCKED)|database(?: table)? (?:is )?(?:busy|locked)/i.test(diagnostic)) {
|
|
118
|
+
return { retryable: true, cause: "SQLite database is locked" };
|
|
119
|
+
}
|
|
120
|
+
if (result?.exitCode == null && /(?:timeout|timed out|ETIMEDOUT)/i.test(diagnostic)) {
|
|
121
|
+
return { retryable: true, cause: "enqueue-raw-event command timeout" };
|
|
122
|
+
}
|
|
123
|
+
if (structured.code === "validation_error" || /invalid raw event|session id required/i.test(diagnostic)) {
|
|
124
|
+
return { retryable: false, cause: "enqueue-raw-event validation failed" };
|
|
125
|
+
}
|
|
126
|
+
if (/unknown command ['\"]?enqueue-raw-event|command not found|\bENOENT\b/i.test(diagnostic)) {
|
|
127
|
+
return { retryable: false, cause: "enqueue-raw-event command unavailable" };
|
|
128
|
+
}
|
|
129
|
+
const exitCode = result?.exitCode ?? "unknown";
|
|
130
|
+
return { retryable: false, cause: `enqueue-raw-event failed (${exitCode})` };
|
|
131
|
+
};
|
|
132
|
+
|
|
44
133
|
const DEFAULT_LOG_PATH = (homeDir, cwd) => `${homeDir || cwd}/.codemem/plugin.log`;
|
|
45
134
|
|
|
46
135
|
const resolveLogPath = (logPathEnvRaw, cwd, homeDir) => {
|
|
@@ -161,16 +250,82 @@ const extractApplyPatchPaths = (patchText) => {
|
|
|
161
250
|
return paths;
|
|
162
251
|
};
|
|
163
252
|
|
|
253
|
+
const windowsPathFlavor = (value, doubleSlashIsUnc = true) =>
|
|
254
|
+
/^[A-Za-z]:[\\/]/.test(value)
|
|
255
|
+
|| /^\\\\/.test(value)
|
|
256
|
+
|| (doubleSlashIsUnc && /^\/\/[^/]+[\\/][^/]+/.test(value));
|
|
257
|
+
|
|
258
|
+
const hasTraversalSegment = (value) =>
|
|
259
|
+
value.replaceAll("\\", "/").split("/").includes("..");
|
|
260
|
+
|
|
261
|
+
const normalizeWorkingSetPath = (value, repositoryRoot) => {
|
|
262
|
+
if (typeof value !== "string" || typeof repositoryRoot !== "string") return null;
|
|
263
|
+
const candidate = value.trim();
|
|
264
|
+
if (
|
|
265
|
+
!candidate
|
|
266
|
+
|| candidate.length > MAX_WORKING_SET_PATH_CHARS
|
|
267
|
+
|| hasTraversalSegment(candidate)
|
|
268
|
+
) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const root = repositoryRoot.trim();
|
|
273
|
+
const candidateIsAbsolute = posix.isAbsolute(candidate) || win32.isAbsolute(candidate);
|
|
274
|
+
if (candidateIsAbsolute) {
|
|
275
|
+
if (!root || hasTraversalSegment(root)) return null;
|
|
276
|
+
const rootUsesWindows = windowsPathFlavor(root);
|
|
277
|
+
const candidateUsesWindows = windowsPathFlavor(candidate, rootUsesWindows);
|
|
278
|
+
if (candidateUsesWindows !== rootUsesWindows) return null;
|
|
279
|
+
const pathApi = candidateUsesWindows ? win32 : posix;
|
|
280
|
+
if (!pathApi.isAbsolute(root) || !pathApi.isAbsolute(candidate)) return null;
|
|
281
|
+
const relative = pathApi.relative(pathApi.normalize(root), pathApi.normalize(candidate));
|
|
282
|
+
if (
|
|
283
|
+
!relative
|
|
284
|
+
|| relative === ".."
|
|
285
|
+
|| relative.startsWith(`..${pathApi.sep}`)
|
|
286
|
+
|| pathApi.isAbsolute(relative)
|
|
287
|
+
) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
const normalized = relative.replaceAll("\\", "/");
|
|
291
|
+
return normalized.length <= MAX_WORKING_SET_PATH_CHARS ? normalized : null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (/^[A-Za-z]:/.test(candidate)) return null;
|
|
295
|
+
const normalized = posix.normalize(candidate.replaceAll("\\", "/")).replace(/^\.\//, "");
|
|
296
|
+
if (
|
|
297
|
+
!normalized
|
|
298
|
+
|| normalized === "."
|
|
299
|
+
|| normalized === ".."
|
|
300
|
+
|| normalized.startsWith("../")
|
|
301
|
+
|| posix.isAbsolute(normalized)
|
|
302
|
+
|| normalized.length > MAX_WORKING_SET_PATH_CHARS
|
|
303
|
+
) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return normalized;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const addWorkingSetPath = (paths, value, repositoryRoot) => {
|
|
310
|
+
const normalized = normalizeWorkingSetPath(value, repositoryRoot);
|
|
311
|
+
if (!normalized) return null;
|
|
312
|
+
const duplicate = windowsPathFlavor(repositoryRoot)
|
|
313
|
+
? Array.from(paths).some((existing) => existing.toLowerCase() === normalized.toLowerCase())
|
|
314
|
+
: paths.has(normalized);
|
|
315
|
+
if (!duplicate) paths.add(normalized);
|
|
316
|
+
return normalized;
|
|
317
|
+
};
|
|
318
|
+
|
|
164
319
|
const appendWorkingSetFileArgs = (args, workingSetFiles) => {
|
|
165
320
|
if (!Array.isArray(workingSetFiles) || workingSetFiles.length === 0) {
|
|
166
321
|
return args;
|
|
167
322
|
}
|
|
168
323
|
for (const file of workingSetFiles) {
|
|
169
324
|
const normalized = String(file || "").trim();
|
|
170
|
-
if (!normalized) {
|
|
325
|
+
if (!normalized || normalized.length > MAX_WORKING_SET_PATH_CHARS) {
|
|
171
326
|
continue;
|
|
172
327
|
}
|
|
173
|
-
args.push("--working-set-file", normalized
|
|
328
|
+
args.push("--working-set-file", normalized);
|
|
174
329
|
}
|
|
175
330
|
return args;
|
|
176
331
|
};
|
|
@@ -212,12 +367,15 @@ const buildInjectQuery = ({ firstPrompt, lastPromptText, projectName, filesModif
|
|
|
212
367
|
return query.length > 500 ? query.slice(0, 500) : query;
|
|
213
368
|
};
|
|
214
369
|
|
|
215
|
-
const buildPackArgs = ({ query, filesModified, injectLimit, injectTokenBudget }) => {
|
|
370
|
+
const buildPackArgs = ({ query, filesModified, injectLimit, injectTokenBudget, internalLedger = false }) => {
|
|
216
371
|
const workingSetFiles = Array.from(filesModified || [])
|
|
217
372
|
.slice(-8)
|
|
218
373
|
.map((value) => String(value || "").trim())
|
|
219
374
|
.filter(Boolean);
|
|
220
375
|
const args = ["pack", query, "--json"];
|
|
376
|
+
if (internalLedger) {
|
|
377
|
+
args.push("--internal-ledger");
|
|
378
|
+
}
|
|
221
379
|
if (injectLimit !== null && Number.isFinite(injectLimit) && injectLimit > 0) {
|
|
222
380
|
args.push("--limit", String(injectLimit));
|
|
223
381
|
}
|
|
@@ -255,6 +413,63 @@ const parsePackMetrics = (stdout) => {
|
|
|
255
413
|
}
|
|
256
414
|
};
|
|
257
415
|
|
|
416
|
+
const parsePackOutput = (result) => {
|
|
417
|
+
const succeeded = result?.exitCode === 0;
|
|
418
|
+
let payload = null;
|
|
419
|
+
if (succeeded) {
|
|
420
|
+
try {
|
|
421
|
+
payload = JSON.parse(result.stdout);
|
|
422
|
+
} catch {
|
|
423
|
+
payload = null;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const ledgerConflict = payload?.ledger_outcome?.ok === false
|
|
427
|
+
&& payload.ledger_outcome.errorCode === "retrieval_ledger_write_failed"
|
|
428
|
+
&& payload.ledger_outcome.reason === "idempotency_conflict";
|
|
429
|
+
const metrics = succeeded ? payload?.metrics || null : null;
|
|
430
|
+
const itemCount = Number.isFinite(Number(metrics?.total_items))
|
|
431
|
+
? Number(metrics.total_items)
|
|
432
|
+
: null;
|
|
433
|
+
const candidatePackText = succeeded && itemCount !== 0
|
|
434
|
+
? String(payload?.pack_text || "").trim()
|
|
435
|
+
: "";
|
|
436
|
+
return {
|
|
437
|
+
conflictPackText: ledgerConflict ? candidatePackText : "",
|
|
438
|
+
ledgerConflict,
|
|
439
|
+
metrics,
|
|
440
|
+
itemCount,
|
|
441
|
+
// The real builder renders section headings even when it selected no
|
|
442
|
+
// memories. Treat the structured count as authoritative so headings alone
|
|
443
|
+
// are never handed to the model as retrieved context.
|
|
444
|
+
packText: !ledgerConflict ? candidatePackText : "",
|
|
445
|
+
};
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
const canonicalJson = (value) => {
|
|
449
|
+
if (Array.isArray(value)) {
|
|
450
|
+
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
451
|
+
}
|
|
452
|
+
if (value && typeof value === "object") {
|
|
453
|
+
return `{${Object.keys(value)
|
|
454
|
+
.sort()
|
|
455
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
|
|
456
|
+
.join(",")}}`;
|
|
457
|
+
}
|
|
458
|
+
return JSON.stringify(value);
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const promptPackArtifactFingerprint = (stdout, packText) => {
|
|
462
|
+
try {
|
|
463
|
+
const payload = JSON.parse(stdout);
|
|
464
|
+
if (/^[0-9a-f]{64}$/i.test(payload?.ledger_artifact_fingerprint || "")) {
|
|
465
|
+
return payload.ledger_artifact_fingerprint.toLowerCase();
|
|
466
|
+
}
|
|
467
|
+
return createHash("sha256").update(canonicalJson(payload)).digest("hex");
|
|
468
|
+
} catch {
|
|
469
|
+
return createHash("sha256").update(packText).digest("hex");
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
258
473
|
const applyInjectedContextToOutput = async ({
|
|
259
474
|
injectEnabled,
|
|
260
475
|
input,
|
|
@@ -263,8 +478,11 @@ const applyInjectedContextToOutput = async ({
|
|
|
263
478
|
showToast,
|
|
264
479
|
resolveInjectQuery,
|
|
265
480
|
buildInjectedContext,
|
|
481
|
+
confirmDelivery,
|
|
482
|
+
recordSkipped,
|
|
266
483
|
}) => {
|
|
267
484
|
if (!injectEnabled) {
|
|
485
|
+
recordSkipped?.("injection_disabled", input?.sessionID || null);
|
|
268
486
|
return false;
|
|
269
487
|
}
|
|
270
488
|
|
|
@@ -277,7 +495,12 @@ const applyInjectedContextToOutput = async ({
|
|
|
277
495
|
// O(1) hit when the cache key isn't tied to the prompt that produced
|
|
278
496
|
// the pack.
|
|
279
497
|
const query = resolveInjectQuery();
|
|
280
|
-
const
|
|
498
|
+
const sessionID = input?.sessionID || null;
|
|
499
|
+
const injected = await buildInjectedContext(query, {
|
|
500
|
+
sessionID,
|
|
501
|
+
requestKey: `system:${sessionID || "unknown"}`,
|
|
502
|
+
surface: "system",
|
|
503
|
+
});
|
|
281
504
|
if (!injected?.text) {
|
|
282
505
|
return false;
|
|
283
506
|
}
|
|
@@ -291,10 +514,20 @@ const applyInjectedContextToOutput = async ({
|
|
|
291
514
|
}
|
|
292
515
|
}
|
|
293
516
|
|
|
294
|
-
|
|
295
|
-
output.system
|
|
517
|
+
try {
|
|
518
|
+
if (!Array.isArray(output.system)) {
|
|
519
|
+
output.system = [];
|
|
520
|
+
}
|
|
521
|
+
output.system.push(injected.text);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (injected.attemptId) {
|
|
524
|
+
confirmDelivery?.(injected.attemptId, "failed");
|
|
525
|
+
}
|
|
526
|
+
throw error;
|
|
527
|
+
}
|
|
528
|
+
if (injected.attemptId) {
|
|
529
|
+
confirmDelivery?.(injected.attemptId);
|
|
296
530
|
}
|
|
297
|
-
output.system.push(injected.text);
|
|
298
531
|
return true;
|
|
299
532
|
};
|
|
300
533
|
|
|
@@ -428,7 +661,7 @@ const normalizeInjectedMessageParts = (messages, sessionCache) => {
|
|
|
428
661
|
}
|
|
429
662
|
}
|
|
430
663
|
if (existingText && sessionCache && messageId && !sessionCache.has(messageId)) {
|
|
431
|
-
sessionCache.set(messageId, existingText);
|
|
664
|
+
sessionCache.set(messageId, { text: existingText, attemptId: null, reconstructed: true });
|
|
432
665
|
trimSessionMessageInjectionCache(sessionCache);
|
|
433
666
|
}
|
|
434
667
|
if (retainedParts.length !== entry.parts.length) {
|
|
@@ -450,7 +683,12 @@ const buildInjectedContextPart = (entry, index, text, options = {}) => {
|
|
|
450
683
|
};
|
|
451
684
|
};
|
|
452
685
|
|
|
453
|
-
const appendCachedInjectedContextParts = (
|
|
686
|
+
const appendCachedInjectedContextParts = async (
|
|
687
|
+
messages,
|
|
688
|
+
sessionCache,
|
|
689
|
+
sessionID = null,
|
|
690
|
+
options = {},
|
|
691
|
+
) => {
|
|
454
692
|
let appended = 0;
|
|
455
693
|
for (let index = 0; index < messages.length; index += 1) {
|
|
456
694
|
const entry = messages[index];
|
|
@@ -461,11 +699,49 @@ const appendCachedInjectedContextParts = (messages, sessionCache, sessionID = nu
|
|
|
461
699
|
if (!messageId) {
|
|
462
700
|
continue;
|
|
463
701
|
}
|
|
464
|
-
const
|
|
702
|
+
const cached = sessionCache.get(messageId);
|
|
703
|
+
const text = typeof cached === "string" ? cached : cached?.text;
|
|
465
704
|
if (!text) {
|
|
466
705
|
continue;
|
|
467
706
|
}
|
|
468
|
-
|
|
707
|
+
let attemptId = cached?.attemptId || null;
|
|
708
|
+
let cacheReuseReady = null;
|
|
709
|
+
if (
|
|
710
|
+
messageId === options.latestMessageId
|
|
711
|
+
&& !options.latestWasBuilt
|
|
712
|
+
&& attemptId
|
|
713
|
+
&& options.recordCacheReuse
|
|
714
|
+
) {
|
|
715
|
+
const reuse = options.recordCacheReuse(cached, {
|
|
716
|
+
messageId,
|
|
717
|
+
sessionID,
|
|
718
|
+
});
|
|
719
|
+
attemptId = typeof reuse === "string" ? reuse : reuse?.attemptId || null;
|
|
720
|
+
cacheReuseReady = typeof reuse === "object" ? reuse?.ready : null;
|
|
721
|
+
}
|
|
722
|
+
try {
|
|
723
|
+
entry.parts.push(buildInjectedContextPart(entry, index, text, { messageId, sessionID }));
|
|
724
|
+
} catch (error) {
|
|
725
|
+
if (messageId === options.latestMessageId && attemptId && options.confirmDelivery) {
|
|
726
|
+
if (cacheReuseReady) {
|
|
727
|
+
void Promise.resolve(cacheReuseReady)
|
|
728
|
+
.then(() => options.confirmDelivery(attemptId, "failed"))
|
|
729
|
+
.catch(() => {});
|
|
730
|
+
} else {
|
|
731
|
+
options.confirmDelivery(attemptId, "failed");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
if (messageId === options.latestMessageId && attemptId && options.confirmDelivery) {
|
|
737
|
+
if (cacheReuseReady) {
|
|
738
|
+
void Promise.resolve(cacheReuseReady)
|
|
739
|
+
.then(() => options.confirmDelivery(attemptId))
|
|
740
|
+
.catch(() => {});
|
|
741
|
+
} else {
|
|
742
|
+
options.confirmDelivery(attemptId);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
469
745
|
appended += 1;
|
|
470
746
|
}
|
|
471
747
|
return appended;
|
|
@@ -481,18 +757,26 @@ const applyInjectedContextToMessages = async ({
|
|
|
481
757
|
buildInjectedContext,
|
|
482
758
|
messageInjectionCache,
|
|
483
759
|
compactionInjectionSkips,
|
|
760
|
+
confirmDelivery,
|
|
761
|
+
recordCacheReuse,
|
|
762
|
+
recordSkipped,
|
|
484
763
|
}) => {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
const latestUser = findLatestUserMessage(output.messages);
|
|
764
|
+
const hasMessages = Array.isArray(output?.messages);
|
|
765
|
+
const latestUser = hasMessages ? findLatestUserMessage(output.messages) : null;
|
|
490
766
|
const sessionID = latestUser
|
|
491
767
|
? resolveEntrySessionID(latestUser.entry) || input?.sessionID || null
|
|
492
768
|
: input?.sessionID || null;
|
|
769
|
+
if (!injectEnabled) {
|
|
770
|
+
recordSkipped?.("injection_disabled", sessionID);
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
if (!hasMessages) {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
493
776
|
|
|
494
777
|
if (consumeCompactionInjectionSkip(compactionInjectionSkips, sessionID)) {
|
|
495
778
|
normalizeInjectedMessageParts(output.messages, null);
|
|
779
|
+
recordSkipped?.("compaction_skipped", sessionID);
|
|
496
780
|
return false;
|
|
497
781
|
}
|
|
498
782
|
|
|
@@ -505,24 +789,49 @@ const applyInjectedContextToMessages = async ({
|
|
|
505
789
|
|
|
506
790
|
const latestMessageId = resolveEntryMessageId(latestUser.entry);
|
|
507
791
|
const canReplay = Boolean(sessionCache && latestMessageId);
|
|
508
|
-
|
|
792
|
+
const latestCached = canReplay ? sessionCache.get(latestMessageId) : null;
|
|
793
|
+
const latestWasCached = Boolean(latestCached) && latestCached?.reconstructed !== true;
|
|
794
|
+
let latestWasBuilt = false;
|
|
795
|
+
if (!latestWasCached) {
|
|
509
796
|
const firstUser = findFirstUserMessage(output.messages);
|
|
510
797
|
const query = resolveInjectQuery({
|
|
511
798
|
firstPrompt: firstUser ? extractMessageText(firstUser.entry) : null,
|
|
512
799
|
lastPromptText: extractMessageText(latestUser.entry),
|
|
513
800
|
});
|
|
514
|
-
const injected = await buildInjectedContext(query
|
|
801
|
+
const injected = await buildInjectedContext(query, {
|
|
802
|
+
sessionID,
|
|
803
|
+
requestKey: latestMessageId || fallbackEntryMessageId(latestUser.entry, latestUser.index),
|
|
804
|
+
surface: "message",
|
|
805
|
+
});
|
|
515
806
|
if (injected?.text) {
|
|
807
|
+
latestWasBuilt = true;
|
|
516
808
|
if (canReplay) {
|
|
517
|
-
sessionCache.set(latestMessageId,
|
|
809
|
+
sessionCache.set(latestMessageId, {
|
|
810
|
+
text: injected.text,
|
|
811
|
+
attemptId: injected.attemptId || null,
|
|
812
|
+
requestId: injected.requestId || null,
|
|
813
|
+
queryHash: injected.queryHash || null,
|
|
814
|
+
promptNumber: injected.promptNumber || 0,
|
|
815
|
+
reuseCount: 0,
|
|
816
|
+
});
|
|
518
817
|
trimSessionMessageInjectionCache(sessionCache);
|
|
519
818
|
} else if (Array.isArray(latestUser.entry.parts)) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
819
|
+
try {
|
|
820
|
+
latestUser.entry.parts.push(
|
|
821
|
+
buildInjectedContextPart(latestUser.entry, latestUser.index, injected.text, {
|
|
822
|
+
messageId: latestMessageId || undefined,
|
|
823
|
+
sessionID: sessionID || undefined,
|
|
824
|
+
})
|
|
825
|
+
);
|
|
826
|
+
} catch (error) {
|
|
827
|
+
if (injected.attemptId) {
|
|
828
|
+
confirmDelivery?.(injected.attemptId, "failed");
|
|
829
|
+
}
|
|
830
|
+
throw error;
|
|
831
|
+
}
|
|
832
|
+
if (injected.attemptId) {
|
|
833
|
+
confirmDelivery?.(injected.attemptId);
|
|
834
|
+
}
|
|
526
835
|
}
|
|
527
836
|
|
|
528
837
|
const toastKey = sessionID || latestMessageId || "unknown";
|
|
@@ -534,6 +843,8 @@ const applyInjectedContextToMessages = async ({
|
|
|
534
843
|
// best-effort only
|
|
535
844
|
}
|
|
536
845
|
}
|
|
846
|
+
} else if (canReplay && latestCached?.reconstructed === true) {
|
|
847
|
+
sessionCache.delete(latestMessageId);
|
|
537
848
|
}
|
|
538
849
|
}
|
|
539
850
|
|
|
@@ -544,7 +855,12 @@ const applyInjectedContextToMessages = async ({
|
|
|
544
855
|
);
|
|
545
856
|
}
|
|
546
857
|
|
|
547
|
-
const appended = appendCachedInjectedContextParts(output.messages, sessionCache, sessionID
|
|
858
|
+
const appended = await appendCachedInjectedContextParts(output.messages, sessionCache, sessionID, {
|
|
859
|
+
latestMessageId,
|
|
860
|
+
latestWasBuilt,
|
|
861
|
+
confirmDelivery,
|
|
862
|
+
recordCacheReuse,
|
|
863
|
+
});
|
|
548
864
|
return appended > 0;
|
|
549
865
|
};
|
|
550
866
|
|
|
@@ -1036,10 +1352,15 @@ export const OpencodeMemPlugin = async ({
|
|
|
1036
1352
|
const injectionToastShown = new Set();
|
|
1037
1353
|
const messageInjectionCache = new Map();
|
|
1038
1354
|
const compactionInjectionSkips = new Map();
|
|
1355
|
+
const disabledInjectionRecorded = new Set();
|
|
1356
|
+
const attemptStartedAt = new Map();
|
|
1357
|
+
const promptPackRetryCounts = new Map();
|
|
1358
|
+
const successfulPromptPackArtifacts = new Map();
|
|
1039
1359
|
let sessionStartedAt = null;
|
|
1040
1360
|
let activeSessionID = null;
|
|
1041
1361
|
let viewerStarted = false;
|
|
1042
1362
|
let promptCounter = 0;
|
|
1363
|
+
let skippedAttemptCounter = 0;
|
|
1043
1364
|
let lastPromptText = null;
|
|
1044
1365
|
let lastAssistantText = null;
|
|
1045
1366
|
const assistantUsageCaptured = new Set();
|
|
@@ -1087,13 +1408,24 @@ export const OpencodeMemPlugin = async ({
|
|
|
1087
1408
|
};
|
|
1088
1409
|
|
|
1089
1410
|
const queueRawEventViaCli = async (body) => {
|
|
1090
|
-
const
|
|
1411
|
+
const runFallback = () => runCli(["enqueue-raw-event"], {
|
|
1091
1412
|
stdinText: JSON.stringify(body),
|
|
1092
1413
|
});
|
|
1414
|
+
let result = await runFallback();
|
|
1415
|
+
let classification = classifyFallbackCommandResult(result);
|
|
1416
|
+
let attemptedRetry = false;
|
|
1417
|
+
if (result?.exitCode !== 0 && classification.retryable) {
|
|
1418
|
+
attemptedRetry = true;
|
|
1419
|
+
result = await runFallback();
|
|
1420
|
+
classification = classifyFallbackCommandResult(result);
|
|
1421
|
+
}
|
|
1093
1422
|
if (result?.exitCode !== 0) {
|
|
1094
|
-
|
|
1095
|
-
|
|
1423
|
+
const retryExhausted = attemptedRetry && classification.retryable;
|
|
1424
|
+
const error = new Error(
|
|
1425
|
+
retryExhausted ? `${classification.cause} after retry` : classification.cause
|
|
1096
1426
|
);
|
|
1427
|
+
error.retryable = classification.retryable && !attemptedRetry;
|
|
1428
|
+
throw error;
|
|
1097
1429
|
}
|
|
1098
1430
|
return true;
|
|
1099
1431
|
};
|
|
@@ -1152,6 +1484,9 @@ export const OpencodeMemPlugin = async ({
|
|
|
1152
1484
|
}
|
|
1153
1485
|
return true;
|
|
1154
1486
|
} catch (fallbackErr) {
|
|
1487
|
+
if (payload && typeof payload === "object") {
|
|
1488
|
+
payload._raw_fallback_terminal = fallbackErr?.retryable !== true;
|
|
1489
|
+
}
|
|
1155
1490
|
await logLine(
|
|
1156
1491
|
`raw_events.fallback.error sessionID=${sessionID} type=${type} err=${String(
|
|
1157
1492
|
fallbackErr
|
|
@@ -1243,6 +1578,9 @@ export const OpencodeMemPlugin = async ({
|
|
|
1243
1578
|
await queueRawEventViaCli(body);
|
|
1244
1579
|
fallbackOk = true;
|
|
1245
1580
|
} catch (fallbackErr) {
|
|
1581
|
+
if (payload && typeof payload === "object") {
|
|
1582
|
+
payload._raw_fallback_terminal = fallbackErr?.retryable !== true;
|
|
1583
|
+
}
|
|
1246
1584
|
await logLine(
|
|
1247
1585
|
`raw_events.fallback.error sessionID=${sessionID} type=${type} err=${String(
|
|
1248
1586
|
fallbackErr
|
|
@@ -1288,6 +1626,7 @@ export const OpencodeMemPlugin = async ({
|
|
|
1288
1626
|
|
|
1289
1627
|
if (!streamErrorNoted) {
|
|
1290
1628
|
streamErrorNoted = true;
|
|
1629
|
+
fallbackFailureNoted = true;
|
|
1291
1630
|
try {
|
|
1292
1631
|
await client.app.log({
|
|
1293
1632
|
service: "codemem",
|
|
@@ -1628,6 +1967,93 @@ export const OpencodeMemPlugin = async ({
|
|
|
1628
1967
|
const runCli = async (args, options = {}) =>
|
|
1629
1968
|
runCommand([runner, ...runnerArgs, ...args], options);
|
|
1630
1969
|
|
|
1970
|
+
const attemptMetadata = (identity, sessionID = null, promptNumber = promptCounter) => ({
|
|
1971
|
+
attempt_id: identity.attemptId,
|
|
1972
|
+
started_at: (() => {
|
|
1973
|
+
const existing = attemptStartedAt.get(identity.attemptId);
|
|
1974
|
+
if (existing) return existing;
|
|
1975
|
+
const created = new Date().toISOString();
|
|
1976
|
+
attemptStartedAt.set(identity.attemptId, created);
|
|
1977
|
+
while (attemptStartedAt.size > 2000) {
|
|
1978
|
+
const oldest = attemptStartedAt.keys().next().value;
|
|
1979
|
+
if (!oldest) break;
|
|
1980
|
+
attemptStartedAt.delete(oldest);
|
|
1981
|
+
}
|
|
1982
|
+
return created;
|
|
1983
|
+
})(),
|
|
1984
|
+
source: "opencode",
|
|
1985
|
+
...(sessionID ? { stream_id: String(sessionID), source_session_id: String(sessionID) } : {}),
|
|
1986
|
+
...(promptNumber > 0 ? { prompt_number: promptNumber } : {}),
|
|
1987
|
+
request_id: identity.requestId,
|
|
1988
|
+
});
|
|
1989
|
+
|
|
1990
|
+
const runPromptPackLedger = async (payload) => {
|
|
1991
|
+
try {
|
|
1992
|
+
return await runCli(["prompt-pack-ledger"], {
|
|
1993
|
+
stdinText: JSON.stringify(payload),
|
|
1994
|
+
});
|
|
1995
|
+
} catch {
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
|
|
2000
|
+
const skippedIdentity = (failureCode, sessionID, surface, eventKey) =>
|
|
2001
|
+
promptPackIdentity({
|
|
2002
|
+
sessionID: sessionID || "unknown",
|
|
2003
|
+
requestKey: `${failureCode}:${eventKey}`,
|
|
2004
|
+
surface,
|
|
2005
|
+
promptNumber: promptCounter,
|
|
2006
|
+
queryHash: hashPromptPackQuery(""),
|
|
2007
|
+
});
|
|
2008
|
+
|
|
2009
|
+
const recordSkippedPromptPack = (failureCode, sessionID = null, surface = injectSurface) => {
|
|
2010
|
+
const sessionKey = String(sessionID || "unknown");
|
|
2011
|
+
const memoKey = `${surface}:${sessionKey}`;
|
|
2012
|
+
if (failureCode === "injection_disabled" && disabledInjectionRecorded.has(memoKey)) {
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
2015
|
+
if (failureCode === "injection_disabled") disabledInjectionRecorded.add(memoKey);
|
|
2016
|
+
const identity = skippedIdentity(
|
|
2017
|
+
failureCode,
|
|
2018
|
+
sessionID,
|
|
2019
|
+
surface,
|
|
2020
|
+
failureCode === "injection_disabled" ? "once" : `event-${++skippedAttemptCounter}`,
|
|
2021
|
+
);
|
|
2022
|
+
void runPromptPackLedger({
|
|
2023
|
+
action: "record",
|
|
2024
|
+
...attemptMetadata(identity, sessionID),
|
|
2025
|
+
retrieval_status: "skipped",
|
|
2026
|
+
failure_code: failureCode,
|
|
2027
|
+
failure_stage: "policy",
|
|
2028
|
+
});
|
|
2029
|
+
return identity.attemptId;
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
const recordCachedPromptPack = (cached, { messageId, sessionID } = {}) => {
|
|
2033
|
+
cached.reuseCount = (cached.reuseCount || 0) + 1;
|
|
2034
|
+
const identity = promptPackIdentity({
|
|
2035
|
+
sessionID: sessionID || "unknown",
|
|
2036
|
+
requestKey: `${messageId || "unknown"}:cache:${cached.reuseCount}`,
|
|
2037
|
+
surface: "message",
|
|
2038
|
+
promptNumber: cached.promptNumber || promptCounter,
|
|
2039
|
+
queryHash: cached.queryHash || hashPromptPackQuery(""),
|
|
2040
|
+
});
|
|
2041
|
+
const ready = runPromptPackLedger({
|
|
2042
|
+
action: "cache_reuse",
|
|
2043
|
+
...attemptMetadata(identity, sessionID, cached.promptNumber || promptCounter),
|
|
2044
|
+
original_attempt_id: cached.attemptId,
|
|
2045
|
+
});
|
|
2046
|
+
return { attemptId: identity.attemptId, ready };
|
|
2047
|
+
};
|
|
2048
|
+
|
|
2049
|
+
const confirmPromptPackDelivery = (attemptId, deliveryStatus = "handed_off") => {
|
|
2050
|
+
void runPromptPackLedger({
|
|
2051
|
+
action: "delivery",
|
|
2052
|
+
attempt_id: attemptId,
|
|
2053
|
+
delivery_status: deliveryStatus,
|
|
2054
|
+
});
|
|
2055
|
+
};
|
|
2056
|
+
|
|
1631
2057
|
const showToast = async (message, variant = "warning") => {
|
|
1632
2058
|
if (backendUpdatePolicy === "off") {
|
|
1633
2059
|
return;
|
|
@@ -1827,28 +2253,300 @@ export const OpencodeMemPlugin = async ({
|
|
|
1827
2253
|
return masked.length > limit ? `${masked.slice(0, limit)}…` : masked;
|
|
1828
2254
|
};
|
|
1829
2255
|
|
|
1830
|
-
const
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2256
|
+
const advancePromptPackRetryIdentity = (attemptKey) => {
|
|
2257
|
+
promptPackRetryCounts.set(
|
|
2258
|
+
attemptKey,
|
|
2259
|
+
(promptPackRetryCounts.get(attemptKey) || 0) + 1
|
|
2260
|
+
);
|
|
2261
|
+
while (promptPackRetryCounts.size > 2000) {
|
|
2262
|
+
const oldest = promptPackRetryCounts.keys().next().value;
|
|
2263
|
+
if (!oldest) break;
|
|
2264
|
+
promptPackRetryCounts.delete(oldest);
|
|
2265
|
+
}
|
|
2266
|
+
};
|
|
2267
|
+
|
|
2268
|
+
const rememberSuccessfulPromptPackArtifact = (
|
|
2269
|
+
attemptKey,
|
|
2270
|
+
retryCount,
|
|
2271
|
+
fingerprint
|
|
2272
|
+
) => {
|
|
2273
|
+
successfulPromptPackArtifacts.delete(attemptKey);
|
|
2274
|
+
successfulPromptPackArtifacts.set(attemptKey, {
|
|
2275
|
+
retryCount,
|
|
2276
|
+
fingerprint,
|
|
2277
|
+
});
|
|
2278
|
+
while (successfulPromptPackArtifacts.size > 2000) {
|
|
2279
|
+
const oldest = successfulPromptPackArtifacts.keys().next().value;
|
|
2280
|
+
if (!oldest) break;
|
|
2281
|
+
successfulPromptPackArtifacts.delete(oldest);
|
|
2282
|
+
}
|
|
2283
|
+
};
|
|
2284
|
+
|
|
2285
|
+
const buildInjectedContext = async (query, context = {}) => {
|
|
2286
|
+
const queryHash = hashPromptPackQuery(query);
|
|
2287
|
+
const sessionID = context.sessionID || activeSessionID || "unknown";
|
|
2288
|
+
const surface = context.surface || injectSurface;
|
|
2289
|
+
const requestKey = context.requestKey || "unknown";
|
|
2290
|
+
const attemptKey = JSON.stringify([
|
|
2291
|
+
sessionID,
|
|
2292
|
+
requestKey,
|
|
2293
|
+
surface,
|
|
2294
|
+
promptCounter,
|
|
2295
|
+
queryHash,
|
|
2296
|
+
]);
|
|
2297
|
+
let retryCount = promptPackRetryCounts.get(attemptKey) || 0;
|
|
2298
|
+
const resolveIdentity = () => promptPackIdentity({
|
|
2299
|
+
sessionID,
|
|
2300
|
+
requestKey: retryCount > 0
|
|
2301
|
+
? `${requestKey}:empty-retry:${retryCount}`
|
|
2302
|
+
: requestKey,
|
|
2303
|
+
surface,
|
|
2304
|
+
promptNumber: promptCounter,
|
|
2305
|
+
queryHash,
|
|
1836
2306
|
});
|
|
1837
|
-
|
|
2307
|
+
let identity = resolveIdentity();
|
|
2308
|
+
let metadata = attemptMetadata(
|
|
2309
|
+
identity,
|
|
2310
|
+
context.sessionID || activeSessionID || null,
|
|
2311
|
+
);
|
|
2312
|
+
const runPack = async () => {
|
|
2313
|
+
let packArgs = buildPackArgs({
|
|
2314
|
+
query,
|
|
2315
|
+
filesModified: sessionContext.filesModified,
|
|
2316
|
+
injectLimit,
|
|
2317
|
+
injectTokenBudget,
|
|
2318
|
+
internalLedger: true,
|
|
2319
|
+
});
|
|
2320
|
+
let result = await runCli(packArgs, { stdinText: JSON.stringify(metadata) });
|
|
2321
|
+
if (rejectsInternalLedgerFlag(result)) {
|
|
2322
|
+
packArgs = packArgs.filter((arg) => arg !== "--internal-ledger");
|
|
2323
|
+
result = await runCli(packArgs);
|
|
2324
|
+
}
|
|
2325
|
+
return { packArgs, result };
|
|
2326
|
+
};
|
|
2327
|
+
let { packArgs, result } = await runPack();
|
|
2328
|
+
let { packText, conflictPackText, metrics, itemCount, ledgerConflict } = parsePackOutput(result);
|
|
2329
|
+
let artifactFingerprint = packText
|
|
2330
|
+
? promptPackArtifactFingerprint(result.stdout, packText)
|
|
2331
|
+
: "";
|
|
2332
|
+
let injectedIdentity = identity;
|
|
2333
|
+
let repairFallbackUsed = false;
|
|
2334
|
+
if (ledgerConflict) {
|
|
2335
|
+
// A restarted plugin has no artifact cache to predict this conflict. The
|
|
2336
|
+
// CLI marker is authoritative: never attribute delivery to the stale
|
|
2337
|
+
// identity, and retry once with a fresh deterministic identity.
|
|
2338
|
+
await log("warn", "codemem prompt-pack ledger conflict; retrying with fresh identity", {
|
|
2339
|
+
sessionID,
|
|
2340
|
+
surface,
|
|
2341
|
+
});
|
|
2342
|
+
const fallback = conflictPackText
|
|
2343
|
+
? {
|
|
2344
|
+
packArgs,
|
|
2345
|
+
result,
|
|
2346
|
+
packText: conflictPackText,
|
|
2347
|
+
metrics,
|
|
2348
|
+
itemCount,
|
|
2349
|
+
artifactFingerprint: promptPackArtifactFingerprint(result.stdout, conflictPackText),
|
|
2350
|
+
ledgerConflict,
|
|
2351
|
+
}
|
|
2352
|
+
: null;
|
|
2353
|
+
advancePromptPackRetryIdentity(attemptKey);
|
|
2354
|
+
retryCount = promptPackRetryCounts.get(attemptKey) || 0;
|
|
2355
|
+
identity = resolveIdentity();
|
|
2356
|
+
metadata = attemptMetadata(
|
|
2357
|
+
identity,
|
|
2358
|
+
context.sessionID || activeSessionID || null,
|
|
2359
|
+
);
|
|
2360
|
+
({ packArgs, result } = await runPack());
|
|
2361
|
+
// Repair-conflict bytes are never preferred over the original preserved pack.
|
|
2362
|
+
({ packText, metrics, itemCount, ledgerConflict } = parsePackOutput(result));
|
|
2363
|
+
artifactFingerprint = packText
|
|
2364
|
+
? promptPackArtifactFingerprint(result.stdout, packText)
|
|
2365
|
+
: "";
|
|
2366
|
+
const repairFailed = ledgerConflict
|
|
2367
|
+
|| !result
|
|
2368
|
+
|| result.exitCode !== 0
|
|
2369
|
+
|| (!packText && itemCount !== 0);
|
|
2370
|
+
if (fallback && repairFailed) {
|
|
2371
|
+
if (ledgerConflict) {
|
|
2372
|
+
await log("warn", "codemem prompt-pack fresh identity also conflicted", {
|
|
2373
|
+
sessionID,
|
|
2374
|
+
surface,
|
|
2375
|
+
});
|
|
2376
|
+
} else {
|
|
2377
|
+
const malformedSuccess = result?.exitCode === 0;
|
|
2378
|
+
const exitCode = result?.exitCode ?? "unknown";
|
|
2379
|
+
const stderr = redactLog(result?.stderr ? result.stderr.trim() : "");
|
|
2380
|
+
const cmd = redactPackCommand(runner, runnerArgs, packArgs);
|
|
2381
|
+
await logLine(
|
|
2382
|
+
`inject.pack.identity_repair_failed reason=${malformedSuccess ? "malformed_success" : "command_failed"} exit=${exitCode} cmd=${cmd}` +
|
|
2383
|
+
`${stderr ? ` stderr=${stderr}` : ""}`
|
|
2384
|
+
);
|
|
2385
|
+
await log("warn", "codemem prompt-pack identity repair failed", {
|
|
2386
|
+
reason: malformedSuccess ? "malformed_success" : "command_failed",
|
|
2387
|
+
exitCode,
|
|
2388
|
+
});
|
|
2389
|
+
void runPromptPackLedger({
|
|
2390
|
+
action: "record",
|
|
2391
|
+
...metadata,
|
|
2392
|
+
retrieval_status: "failed",
|
|
2393
|
+
failure_code: malformedSuccess
|
|
2394
|
+
? "pack_identity_repair_failed"
|
|
2395
|
+
: "pack_command_failed",
|
|
2396
|
+
failure_stage: malformedSuccess ? "decode" : "transport",
|
|
2397
|
+
});
|
|
2398
|
+
}
|
|
2399
|
+
advancePromptPackRetryIdentity(attemptKey);
|
|
2400
|
+
retryCount = promptPackRetryCounts.get(attemptKey) || 0;
|
|
2401
|
+
({
|
|
2402
|
+
packArgs,
|
|
2403
|
+
result,
|
|
2404
|
+
packText,
|
|
2405
|
+
metrics,
|
|
2406
|
+
itemCount,
|
|
2407
|
+
artifactFingerprint,
|
|
2408
|
+
ledgerConflict,
|
|
2409
|
+
} = fallback);
|
|
2410
|
+
// Neither persisted identity represents fallback delivery: the first
|
|
2411
|
+
// conflicted and the fresh repair failed. Keep the bytes fail-open but
|
|
2412
|
+
// leave delivery and replay attribution empty.
|
|
2413
|
+
injectedIdentity = null;
|
|
2414
|
+
repairFallbackUsed = true;
|
|
2415
|
+
} else {
|
|
2416
|
+
injectedIdentity = identity;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
if (packText) {
|
|
2420
|
+
const previous = successfulPromptPackArtifacts.get(attemptKey);
|
|
2421
|
+
if (
|
|
2422
|
+
previous?.retryCount === retryCount
|
|
2423
|
+
&& previous.fingerprint !== artifactFingerprint
|
|
2424
|
+
) {
|
|
2425
|
+
// The in-memory fingerprint detected a changed artifact before handoff.
|
|
2426
|
+
// Defensively rebuild once with a fresh identity; older CLIs do not
|
|
2427
|
+
// expose the persisted conflict marker used by the restart path above.
|
|
2428
|
+
// Keep the usable changed bytes as a fail-open fallback: diagnostics
|
|
2429
|
+
// identity repair must never suppress context injection.
|
|
2430
|
+
const fallback = {
|
|
2431
|
+
packArgs,
|
|
2432
|
+
result,
|
|
2433
|
+
packText,
|
|
2434
|
+
metrics,
|
|
2435
|
+
itemCount,
|
|
2436
|
+
artifactFingerprint,
|
|
2437
|
+
ledgerConflict,
|
|
2438
|
+
};
|
|
2439
|
+
advancePromptPackRetryIdentity(attemptKey);
|
|
2440
|
+
retryCount = promptPackRetryCounts.get(attemptKey) || 0;
|
|
2441
|
+
identity = resolveIdentity();
|
|
2442
|
+
metadata = attemptMetadata(
|
|
2443
|
+
identity,
|
|
2444
|
+
context.sessionID || activeSessionID || null,
|
|
2445
|
+
);
|
|
2446
|
+
({ packArgs, result } = await runPack());
|
|
2447
|
+
({ packText, metrics, itemCount, ledgerConflict } = parsePackOutput(result));
|
|
2448
|
+
artifactFingerprint = packText
|
|
2449
|
+
? promptPackArtifactFingerprint(result.stdout, packText)
|
|
2450
|
+
: "";
|
|
2451
|
+
|
|
2452
|
+
if (ledgerConflict) {
|
|
2453
|
+
// The repair identity is also persisted with different artifacts.
|
|
2454
|
+
// Preserve the marker for the fail-closed return below; never use the
|
|
2455
|
+
// stale changed bytes as the transport-failure fallback.
|
|
2456
|
+
injectedIdentity = identity;
|
|
2457
|
+
} else if (result?.exitCode === 0 && itemCount === 0) {
|
|
2458
|
+
// A fresh identity can legitimately resolve to no results if the
|
|
2459
|
+
// underlying memory set changed between rebuilds. The CLI already
|
|
2460
|
+
// recorded that terminal outcome; do not misclassify it as a decode
|
|
2461
|
+
// failure or fall back to stale non-empty context.
|
|
2462
|
+
injectedIdentity = identity;
|
|
2463
|
+
} else if (!result || result.exitCode !== 0 || !packText) {
|
|
2464
|
+
const malformedSuccess = result?.exitCode === 0;
|
|
2465
|
+
const exitCode = result?.exitCode ?? "unknown";
|
|
2466
|
+
const stderr = redactLog(result?.stderr ? result.stderr.trim() : "");
|
|
2467
|
+
const cmd = redactPackCommand(runner, runnerArgs, packArgs);
|
|
2468
|
+
await logLine(
|
|
2469
|
+
`inject.pack.identity_repair_failed reason=${malformedSuccess ? "malformed_success" : "command_failed"} exit=${exitCode} cmd=${cmd}` +
|
|
2470
|
+
`${stderr ? ` stderr=${stderr}` : ""}`
|
|
2471
|
+
);
|
|
2472
|
+
await log("warn", "codemem prompt-pack identity repair failed", {
|
|
2473
|
+
reason: malformedSuccess ? "malformed_success" : "command_failed",
|
|
2474
|
+
exitCode,
|
|
2475
|
+
});
|
|
2476
|
+
void runPromptPackLedger({
|
|
2477
|
+
action: "record",
|
|
2478
|
+
...metadata,
|
|
2479
|
+
retrieval_status: "failed",
|
|
2480
|
+
failure_code: malformedSuccess
|
|
2481
|
+
? "pack_identity_repair_failed"
|
|
2482
|
+
: "pack_command_failed",
|
|
2483
|
+
failure_stage: malformedSuccess ? "decode" : "transport",
|
|
2484
|
+
});
|
|
2485
|
+
advancePromptPackRetryIdentity(attemptKey);
|
|
2486
|
+
|
|
2487
|
+
({
|
|
2488
|
+
packArgs,
|
|
2489
|
+
result,
|
|
2490
|
+
packText,
|
|
2491
|
+
metrics,
|
|
2492
|
+
itemCount,
|
|
2493
|
+
artifactFingerprint,
|
|
2494
|
+
ledgerConflict,
|
|
2495
|
+
} = fallback);
|
|
2496
|
+
// The stale attempt does not represent these changed bytes, and the
|
|
2497
|
+
// fresh repair attempt failed. Inject without delivery attribution
|
|
2498
|
+
// rather than falsely marking either ledger attempt handed off.
|
|
2499
|
+
injectedIdentity = null;
|
|
2500
|
+
repairFallbackUsed = true;
|
|
2501
|
+
} else {
|
|
2502
|
+
injectedIdentity = identity;
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
1838
2506
|
if (!result || result.exitCode !== 0) {
|
|
1839
2507
|
const exitCode = result?.exitCode ?? "unknown";
|
|
1840
2508
|
const stderr = redactLog(result?.stderr ? result.stderr.trim() : "");
|
|
1841
2509
|
const stdout = redactLog(result?.stdout ? result.stdout.trim() : "");
|
|
1842
|
-
const cmd =
|
|
2510
|
+
const cmd = redactPackCommand(runner, runnerArgs, packArgs);
|
|
1843
2511
|
await logLine(
|
|
1844
2512
|
`inject.pack.error ${exitCode} cmd=${cmd}` +
|
|
1845
2513
|
`${stderr ? ` stderr=${stderr}` : ""}` +
|
|
1846
2514
|
`${stdout ? ` stdout=${stdout}` : ""}`
|
|
1847
2515
|
);
|
|
1848
|
-
|
|
2516
|
+
void runPromptPackLedger({
|
|
2517
|
+
action: "record",
|
|
2518
|
+
...metadata,
|
|
2519
|
+
retrieval_status: "failed",
|
|
2520
|
+
failure_code: "pack_command_failed",
|
|
2521
|
+
failure_stage: "transport",
|
|
2522
|
+
});
|
|
2523
|
+
advancePromptPackRetryIdentity(attemptKey);
|
|
2524
|
+
return {
|
|
2525
|
+
text: "",
|
|
2526
|
+
attemptId: identity.attemptId,
|
|
2527
|
+
requestId: identity.requestId,
|
|
2528
|
+
queryHash,
|
|
2529
|
+
promptNumber: promptCounter,
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
if (ledgerConflict && !repairFallbackUsed) {
|
|
2533
|
+
await log("warn", "codemem prompt-pack fresh identity also conflicted", {
|
|
2534
|
+
sessionID,
|
|
2535
|
+
surface,
|
|
2536
|
+
});
|
|
2537
|
+
advancePromptPackRetryIdentity(attemptKey);
|
|
2538
|
+
return {
|
|
2539
|
+
text: "",
|
|
2540
|
+
attemptId: identity.attemptId,
|
|
2541
|
+
requestId: identity.requestId,
|
|
2542
|
+
queryHash,
|
|
2543
|
+
promptNumber: promptCounter,
|
|
2544
|
+
};
|
|
1849
2545
|
}
|
|
1850
|
-
const packText = parsePackText(result.stdout);
|
|
1851
2546
|
if (!packText) {
|
|
2547
|
+
if (itemCount === 0) {
|
|
2548
|
+
advancePromptPackRetryIdentity(attemptKey);
|
|
2549
|
+
}
|
|
1852
2550
|
if (debug) {
|
|
1853
2551
|
const { safeQuery, firstPromptLen, lastPromptLen, projectName, filesModifiedCount } =
|
|
1854
2552
|
describeInjectQuery(query);
|
|
@@ -1856,27 +2554,46 @@ export const OpencodeMemPlugin = async ({
|
|
|
1856
2554
|
`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
2555
|
);
|
|
1858
2556
|
}
|
|
1859
|
-
return
|
|
2557
|
+
return {
|
|
2558
|
+
text: "",
|
|
2559
|
+
attemptId: identity.attemptId,
|
|
2560
|
+
requestId: identity.requestId,
|
|
2561
|
+
queryHash,
|
|
2562
|
+
promptNumber: promptCounter,
|
|
2563
|
+
};
|
|
1860
2564
|
}
|
|
1861
|
-
const metrics = parsePackMetrics(result.stdout);
|
|
1862
2565
|
// The pack JSON exposes the item count as `total_items`; `metrics.items`
|
|
1863
2566
|
// 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
2567
|
const packTokens = Number.isFinite(Number(metrics?.pack_tokens))
|
|
1868
2568
|
? Number(metrics.pack_tokens)
|
|
1869
2569
|
: 0;
|
|
1870
2570
|
await logLine(
|
|
1871
|
-
`inject.pack.ok source=opencode items=${itemCount} pack_tokens=${packTokens} query_len=${query ? query.length : 0}`
|
|
2571
|
+
`inject.pack.ok source=opencode items=${itemCount ?? 0} pack_tokens=${packTokens} query_len=${query ? query.length : 0}`
|
|
1872
2572
|
);
|
|
2573
|
+
if (!repairFallbackUsed) {
|
|
2574
|
+
rememberSuccessfulPromptPackArtifact(
|
|
2575
|
+
attemptKey,
|
|
2576
|
+
retryCount,
|
|
2577
|
+
artifactFingerprint || promptPackArtifactFingerprint(result.stdout, packText)
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
1873
2580
|
if (metrics) {
|
|
1874
2581
|
return {
|
|
1875
2582
|
text: `[codemem context]\n${packText}`,
|
|
1876
2583
|
metrics,
|
|
2584
|
+
attemptId: injectedIdentity?.attemptId || null,
|
|
2585
|
+
requestId: injectedIdentity?.requestId || null,
|
|
2586
|
+
queryHash,
|
|
2587
|
+
promptNumber: promptCounter,
|
|
1877
2588
|
};
|
|
1878
2589
|
}
|
|
1879
|
-
return {
|
|
2590
|
+
return {
|
|
2591
|
+
text: `[codemem context]\n${packText}`,
|
|
2592
|
+
attemptId: injectedIdentity?.attemptId || null,
|
|
2593
|
+
requestId: injectedIdentity?.requestId || null,
|
|
2594
|
+
queryHash,
|
|
2595
|
+
promptNumber: promptCounter,
|
|
2596
|
+
};
|
|
1880
2597
|
};
|
|
1881
2598
|
|
|
1882
2599
|
const stopViewer = async () => {
|
|
@@ -2066,10 +2783,18 @@ export const OpencodeMemPlugin = async ({
|
|
|
2066
2783
|
}
|
|
2067
2784
|
|
|
2068
2785
|
const failed = [];
|
|
2786
|
+
let droppedCount = 0;
|
|
2069
2787
|
for (const queuedEvent of batch) {
|
|
2070
2788
|
if (queuedEvent && typeof queuedEvent === "object" && queuedEvent._raw_enqueued) {
|
|
2071
2789
|
continue;
|
|
2072
2790
|
}
|
|
2791
|
+
if (queuedEvent && typeof queuedEvent === "object" && queuedEvent._raw_fallback_terminal) {
|
|
2792
|
+
droppedCount += 1;
|
|
2793
|
+
await logLine(
|
|
2794
|
+
`flush.drop terminal_fallback event_id=${queuedEvent?._raw_event_id || "unknown"}`
|
|
2795
|
+
);
|
|
2796
|
+
continue;
|
|
2797
|
+
}
|
|
2073
2798
|
const queuedSessionID =
|
|
2074
2799
|
queuedEvent?._raw_session_id ||
|
|
2075
2800
|
queuedEvent?.properties?.sessionID ||
|
|
@@ -2080,6 +2805,13 @@ export const OpencodeMemPlugin = async ({
|
|
|
2080
2805
|
payload: queuedEvent,
|
|
2081
2806
|
});
|
|
2082
2807
|
if (!ok) {
|
|
2808
|
+
if (queuedEvent?._raw_fallback_terminal) {
|
|
2809
|
+
droppedCount += 1;
|
|
2810
|
+
await logLine(
|
|
2811
|
+
`flush.drop terminal_fallback event_id=${queuedEvent?._raw_event_id || "unknown"}`
|
|
2812
|
+
);
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2083
2815
|
const currentRetry =
|
|
2084
2816
|
typeof queuedEvent?._raw_retry_count === "number" && Number.isFinite(queuedEvent._raw_retry_count)
|
|
2085
2817
|
? queuedEvent._raw_retry_count
|
|
@@ -2104,7 +2836,7 @@ export const OpencodeMemPlugin = async ({
|
|
|
2104
2836
|
await logLine(
|
|
2105
2837
|
`flush.stream_only finalize count=${batch.length} tools=${sessionContext.toolCount} prompts=${sessionContext.promptCount} duration=${Math.round(durationMs / 1000)}s`
|
|
2106
2838
|
);
|
|
2107
|
-
await logLine(`flush.ok count=${batch.length}`);
|
|
2839
|
+
await logLine(`flush.ok count=${batch.length - droppedCount} dropped=${droppedCount}`);
|
|
2108
2840
|
sessionStartedAt = null;
|
|
2109
2841
|
resetSessionContext();
|
|
2110
2842
|
};
|
|
@@ -2160,6 +2892,9 @@ export const OpencodeMemPlugin = async ({
|
|
|
2160
2892
|
buildInjectedContext,
|
|
2161
2893
|
messageInjectionCache,
|
|
2162
2894
|
compactionInjectionSkips,
|
|
2895
|
+
confirmDelivery: confirmPromptPackDelivery,
|
|
2896
|
+
recordCacheReuse: recordCachedPromptPack,
|
|
2897
|
+
recordSkipped: recordSkippedPromptPack,
|
|
2163
2898
|
});
|
|
2164
2899
|
} catch (err) {
|
|
2165
2900
|
await logLine(
|
|
@@ -2184,6 +2919,7 @@ export const OpencodeMemPlugin = async ({
|
|
|
2184
2919
|
}
|
|
2185
2920
|
const hookSessionID = input?.sessionID || activeSessionID;
|
|
2186
2921
|
if (consumeCompactionInjectionSkip(compactionInjectionSkips, hookSessionID)) {
|
|
2922
|
+
recordSkippedPromptPack("compaction_skipped", hookSessionID, "system");
|
|
2187
2923
|
if (debug) {
|
|
2188
2924
|
await logLine(
|
|
2189
2925
|
`inject.transform.skip_compaction sessionID=${hookSessionID || "unknown"}`
|
|
@@ -2224,6 +2960,8 @@ export const OpencodeMemPlugin = async ({
|
|
|
2224
2960
|
: null,
|
|
2225
2961
|
resolveInjectQuery,
|
|
2226
2962
|
buildInjectedContext,
|
|
2963
|
+
confirmDelivery: confirmPromptPackDelivery,
|
|
2964
|
+
recordSkipped: recordSkippedPromptPack,
|
|
2227
2965
|
});
|
|
2228
2966
|
} catch (err) {
|
|
2229
2967
|
await logLine(
|
|
@@ -2408,6 +3146,9 @@ export const OpencodeMemPlugin = async ({
|
|
|
2408
3146
|
activeSessionID = sessionID || null;
|
|
2409
3147
|
sessionStartedAt = new Date().toISOString();
|
|
2410
3148
|
promptCounter = 0;
|
|
3149
|
+
skippedAttemptCounter = 0;
|
|
3150
|
+
disabledInjectionRecorded.delete("message:unknown");
|
|
3151
|
+
disabledInjectionRecorded.delete("system:unknown");
|
|
2411
3152
|
lastPromptText = null;
|
|
2412
3153
|
lastAssistantText = null;
|
|
2413
3154
|
resetSessionContext();
|
|
@@ -2419,6 +3160,8 @@ export const OpencodeMemPlugin = async ({
|
|
|
2419
3160
|
injectionToastShown.delete(sessionID);
|
|
2420
3161
|
messageInjectionCache.delete(sessionID);
|
|
2421
3162
|
compactionInjectionSkips.delete(sessionID);
|
|
3163
|
+
disabledInjectionRecorded.delete(`message:${sessionID}`);
|
|
3164
|
+
disabledInjectionRecorded.delete(`system:${sessionID}`);
|
|
2422
3165
|
}
|
|
2423
3166
|
await stopViewer();
|
|
2424
3167
|
}
|
|
@@ -2438,15 +3181,15 @@ export const OpencodeMemPlugin = async ({
|
|
|
2438
3181
|
if (filePath) {
|
|
2439
3182
|
const lowerTool = toolName.toLowerCase();
|
|
2440
3183
|
if (lowerTool === "edit" || lowerTool === "write") {
|
|
2441
|
-
sessionContext.filesModified
|
|
3184
|
+
addWorkingSetPath(sessionContext.filesModified, filePath, cwd);
|
|
2442
3185
|
} else if (lowerTool === "read") {
|
|
2443
|
-
sessionContext.filesRead
|
|
3186
|
+
addWorkingSetPath(sessionContext.filesRead, filePath, cwd);
|
|
2444
3187
|
}
|
|
2445
3188
|
}
|
|
2446
3189
|
if (toolName.toLowerCase() === "apply_patch") {
|
|
2447
3190
|
const patchPaths = extractApplyPatchPaths(args.patchText);
|
|
2448
3191
|
for (const path of patchPaths) {
|
|
2449
|
-
sessionContext.filesModified
|
|
3192
|
+
addWorkingSetPath(sessionContext.filesModified, path, cwd);
|
|
2450
3193
|
}
|
|
2451
3194
|
}
|
|
2452
3195
|
|
|
@@ -2527,6 +3270,12 @@ export const __testUtils = {
|
|
|
2527
3270
|
resolveProjectName,
|
|
2528
3271
|
buildInjectQuery,
|
|
2529
3272
|
buildPackArgs,
|
|
3273
|
+
deterministicUuid,
|
|
3274
|
+
promptPackIdentity,
|
|
3275
|
+
hashPromptPackQuery,
|
|
3276
|
+
redactPackCommand,
|
|
3277
|
+
rejectsInternalLedgerFlag,
|
|
3278
|
+
classifyFallbackCommandResult,
|
|
2530
3279
|
parsePackText,
|
|
2531
3280
|
parsePackMetrics,
|
|
2532
3281
|
resolveInjectSurface,
|
|
@@ -2537,6 +3286,8 @@ export const __testUtils = {
|
|
|
2537
3286
|
buildRunnerArgs,
|
|
2538
3287
|
appendWorkingSetFileArgs,
|
|
2539
3288
|
extractApplyPatchPaths,
|
|
3289
|
+
normalizeWorkingSetPath,
|
|
3290
|
+
addWorkingSetPath,
|
|
2540
3291
|
mapOpencodeEventTypeToAdapterType,
|
|
2541
3292
|
buildOpencodeAdapterPayload,
|
|
2542
3293
|
buildOpencodeAdapterEvent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codemem/opencode-plugin",
|
|
3
|
-
"version": "0.40.
|
|
3
|
+
"version": "0.40.1",
|
|
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",
|