@codemem/opencode-plugin 0.44.0-alpha.1 → 0.44.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/lib/compat.js +11 -6
- package/.opencode/lib/raw-event-spool.js +197 -0
- package/.opencode/plugins/codemem.js +871 -316
- package/package.json +1 -1
|
@@ -13,20 +13,28 @@ import {
|
|
|
13
13
|
resolveAutoUpdatePlan,
|
|
14
14
|
resolveUpgradeGuidance,
|
|
15
15
|
} from "../lib/compat.js";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_DRAIN_LIMIT,
|
|
18
|
+
DEFAULT_MAX_ENTRIES,
|
|
19
|
+
RAW_EVENT_SPOOL_FULL_CODE,
|
|
20
|
+
loadRawEventSpoolEntries,
|
|
21
|
+
removeRawEventSpoolEntry,
|
|
22
|
+
resolveSpoolDirectory,
|
|
23
|
+
writeRawEventSpoolEntry,
|
|
24
|
+
} from "../lib/raw-event-spool.js";
|
|
16
25
|
|
|
17
26
|
const TRUTHY_VALUES = ["1", "true", "yes"];
|
|
18
27
|
const DISABLED_VALUES = ["0", "false", "off"];
|
|
19
|
-
const PINNED_BACKEND_VERSION = "0.44.0
|
|
28
|
+
const PINNED_BACKEND_VERSION = "0.44.0";
|
|
20
29
|
const COMPAT_CHECK_DELAY_MS = 1500;
|
|
21
30
|
const COMPAT_CHECK_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
22
31
|
const MAX_UPDATE_STATUS_BYTES = 16 * 1024;
|
|
23
32
|
const MAX_UPDATE_ACTION_CHARS = 1000;
|
|
24
33
|
const MAX_UPDATE_VERSION_CHARS = 128;
|
|
25
|
-
const
|
|
26
|
-
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
34
|
+
const RELEASE_VERSION =
|
|
35
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
27
36
|
const CODEMEM_CONTEXT_PART_ID_PREFIX = "codemem-context-";
|
|
28
37
|
const MAX_MESSAGE_INJECTION_CACHE_SESSIONS = 20;
|
|
29
|
-
const MAX_MESSAGE_INJECTION_CACHE_MESSAGES = 100;
|
|
30
38
|
const COMPACTION_INJECTION_SKIP_TTL_MS = 30 * 1000;
|
|
31
39
|
const MAX_WORKING_SET_PATH_CHARS = 400;
|
|
32
40
|
const VIEWER_HEALTH_CHECK_INTERVAL_MS = 60_000;
|
|
@@ -34,12 +42,34 @@ const VIEWER_HEALTH_TIMEOUT_MS = 5_000;
|
|
|
34
42
|
const VIEWER_HEALTH_RESTART_THRESHOLD = 3;
|
|
35
43
|
const VIEWER_HEALTH_RESTART_COOLDOWN_MS = 5 * 60_000;
|
|
36
44
|
const RAW_EVENTS_STATUS_TIMEOUT_MS = 5_000;
|
|
45
|
+
const DEFAULT_INJECT_TOKEN_BUDGET = 800;
|
|
46
|
+
const DEFAULT_RETAINED_TOKEN_BUDGET = Infinity;
|
|
47
|
+
const CODEMEM_CONTEXT_PREFIX = "[codemem context]\n";
|
|
37
48
|
const DEFAULT_EMBEDDING_MODEL = "Xenova/bge-small-en-v1.5";
|
|
38
49
|
const DEFAULT_EMBEDDING_REVISION = "ea104dacec62c0de699686887e3f920caeb4f3e3";
|
|
50
|
+
const PLUGIN_REGISTRATIONS_KEY = Symbol.for("codemem.opencode-plugin.registrations");
|
|
39
51
|
|
|
40
52
|
let compatCheckCache = null;
|
|
41
53
|
const notifiedReleaseVersions = new Set();
|
|
42
54
|
|
|
55
|
+
const claimPluginRegistration = (cwd) => {
|
|
56
|
+
let registrations = globalThis[PLUGIN_REGISTRATIONS_KEY];
|
|
57
|
+
if (!(registrations instanceof Map)) {
|
|
58
|
+
registrations = new Map();
|
|
59
|
+
globalThis[PLUGIN_REGISTRATIONS_KEY] = registrations;
|
|
60
|
+
}
|
|
61
|
+
const projectPath = resolve(cwd);
|
|
62
|
+
if (registrations.has(projectPath)) return null;
|
|
63
|
+
const registration = Symbol(projectPath);
|
|
64
|
+
registrations.set(projectPath, registration);
|
|
65
|
+
return () => {
|
|
66
|
+
if (registrations.get(projectPath) === registration) {
|
|
67
|
+
registrations.delete(projectPath);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
const rawEventSpoolDrainsInFlight = new Map();
|
|
72
|
+
|
|
43
73
|
// Release an unread response body without surfacing cancellation failures.
|
|
44
74
|
const discardResponseBody = (response) => {
|
|
45
75
|
try {
|
|
@@ -354,10 +384,74 @@ const clearCompatCheckCache = () => {
|
|
|
354
384
|
compatCheckCache = null;
|
|
355
385
|
};
|
|
356
386
|
|
|
357
|
-
const
|
|
358
|
-
if (typeof value !== "string" || value.length > MAX_UPDATE_VERSION_CHARS) return
|
|
359
|
-
const match =
|
|
360
|
-
|
|
387
|
+
const parseReleaseVersion = (value) => {
|
|
388
|
+
if (typeof value !== "string" || value.length > MAX_UPDATE_VERSION_CHARS) return null;
|
|
389
|
+
const match = RELEASE_VERSION.exec(value);
|
|
390
|
+
if (!match || !match.slice(1, 4).map(Number).every(Number.isSafeInteger)) return null;
|
|
391
|
+
const prerelease = match[4]?.split(".") || [];
|
|
392
|
+
if (prerelease.some((identifier) => /^0\d+$/.test(identifier))) return null;
|
|
393
|
+
return { core: match.slice(1, 4).map(Number), prerelease };
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
const releaseChannelForVersion = (value) => {
|
|
397
|
+
const parsed = parseReleaseVersion(value);
|
|
398
|
+
if (!parsed) return null;
|
|
399
|
+
if (parsed.prerelease.length === 0) return "latest";
|
|
400
|
+
const channel = parsed.prerelease[0];
|
|
401
|
+
return channel === "alpha" || channel === "beta" || channel === "rc" ? channel : null;
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
const comparePrereleaseIdentifier = (left, right) => {
|
|
405
|
+
if (left === undefined) return right === undefined ? 0 : -1;
|
|
406
|
+
if (right === undefined) return 1;
|
|
407
|
+
if (left === right) return 0;
|
|
408
|
+
const leftNumeric = /^\d+$/.test(left);
|
|
409
|
+
const rightNumeric = /^\d+$/.test(right);
|
|
410
|
+
if (leftNumeric && rightNumeric && left.length !== right.length) {
|
|
411
|
+
return Math.sign(left.length - right.length);
|
|
412
|
+
}
|
|
413
|
+
if (leftNumeric !== rightNumeric) return leftNumeric ? -1 : 1;
|
|
414
|
+
return left < right ? -1 : 1;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const compareReleaseVersions = (left, right) => {
|
|
418
|
+
const leftVersion = parseReleaseVersion(left);
|
|
419
|
+
const rightVersion = parseReleaseVersion(right);
|
|
420
|
+
if (!leftVersion || !rightVersion) return null;
|
|
421
|
+
for (let index = 0; index < leftVersion.core.length; index += 1) {
|
|
422
|
+
const difference = leftVersion.core[index] - rightVersion.core[index];
|
|
423
|
+
if (difference !== 0) return Math.sign(difference);
|
|
424
|
+
}
|
|
425
|
+
if (leftVersion.prerelease.length === 0) {
|
|
426
|
+
return rightVersion.prerelease.length === 0 ? 0 : 1;
|
|
427
|
+
}
|
|
428
|
+
if (rightVersion.prerelease.length === 0) return -1;
|
|
429
|
+
const length = Math.max(leftVersion.prerelease.length, rightVersion.prerelease.length);
|
|
430
|
+
for (let index = 0; index < length; index += 1) {
|
|
431
|
+
const comparison = comparePrereleaseIdentifier(
|
|
432
|
+
leftVersion.prerelease[index],
|
|
433
|
+
rightVersion.prerelease[index],
|
|
434
|
+
);
|
|
435
|
+
if (comparison !== 0) return comparison;
|
|
436
|
+
}
|
|
437
|
+
return 0;
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const isSameChannelVersionAtLeast = (currentVersion, minimumVersion) => {
|
|
441
|
+
const currentChannel = releaseChannelForVersion(currentVersion);
|
|
442
|
+
if (!currentChannel || currentChannel !== releaseChannelForVersion(minimumVersion)) return false;
|
|
443
|
+
const comparison = compareReleaseVersions(currentVersion, minimumVersion);
|
|
444
|
+
return comparison !== null && comparison >= 0;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const PINNED_RELEASE_CHANNEL = releaseChannelForVersion(PINNED_BACKEND_VERSION);
|
|
448
|
+
|
|
449
|
+
const guidanceMatchesRelease = (guidance, latestVersion) => {
|
|
450
|
+
const versions =
|
|
451
|
+
String(guidance || "").match(
|
|
452
|
+
/\b\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?/g,
|
|
453
|
+
) || [];
|
|
454
|
+
return versions.length === 0 || versions.every((version) => version === latestVersion);
|
|
361
455
|
};
|
|
362
456
|
|
|
363
457
|
const parseReleaseNotification = (result) => {
|
|
@@ -368,18 +462,24 @@ const parseReleaseNotification = (result) => {
|
|
|
368
462
|
const status = JSON.parse(stdout);
|
|
369
463
|
if (!status || typeof status !== "object" || Array.isArray(status)) return null;
|
|
370
464
|
if (status.update_available !== true) return null;
|
|
371
|
-
if (
|
|
465
|
+
if (
|
|
466
|
+
!PINNED_RELEASE_CHANNEL
|
|
467
|
+
|| status.channel !== PINNED_RELEASE_CHANNEL
|
|
468
|
+
|| releaseChannelForVersion(status.latest_version) !== PINNED_RELEASE_CHANNEL
|
|
469
|
+
) {
|
|
372
470
|
return null;
|
|
373
471
|
}
|
|
374
472
|
if (
|
|
375
473
|
typeof status.recommended_action !== "string"
|
|
376
474
|
|| !status.recommended_action.trim()
|
|
377
475
|
|| status.recommended_action.length > MAX_UPDATE_ACTION_CHARS
|
|
476
|
+
|| !guidanceMatchesRelease(status.recommended_action, status.latest_version)
|
|
378
477
|
) {
|
|
379
478
|
return null;
|
|
380
479
|
}
|
|
381
480
|
return {
|
|
382
481
|
latestVersion: status.latest_version,
|
|
482
|
+
channel: PINNED_RELEASE_CHANNEL,
|
|
383
483
|
recommendedAction: status.recommended_action.trim(),
|
|
384
484
|
autoUpdateEligible: status.auto_update_eligible === true,
|
|
385
485
|
};
|
|
@@ -691,7 +791,7 @@ const parsePackOutput = (result) => {
|
|
|
691
791
|
? Number(metrics.total_items)
|
|
692
792
|
: null;
|
|
693
793
|
const candidatePackText = succeeded && itemCount !== 0
|
|
694
|
-
? String(payload?.pack_text || "")
|
|
794
|
+
? String(payload?.pack_text || "")
|
|
695
795
|
: "";
|
|
696
796
|
return {
|
|
697
797
|
conflictPackText: ledgerConflict ? candidatePackText : "",
|
|
@@ -738,6 +838,7 @@ const isValidLedgerFailureHttpPayload = (payload) =>
|
|
|
738
838
|
&& (
|
|
739
839
|
payload.errorCode === "retrieval_ledger_write_failed"
|
|
740
840
|
|| payload.errorCode === "retrieval_ledger_delivery_write_failed"
|
|
841
|
+
|| payload.errorCode === "automatic_recall_write_failed"
|
|
741
842
|
)
|
|
742
843
|
&& typeof payload.reason === "string";
|
|
743
844
|
|
|
@@ -761,6 +862,20 @@ const isViewerInvalidRequestPayload = (payload) =>
|
|
|
761
862
|
&& isRecord(payload.error)
|
|
762
863
|
&& payload.error.code === "invalid_request";
|
|
763
864
|
|
|
865
|
+
const classifyRawEventViewerFailure = (payload) => {
|
|
866
|
+
if (isViewerDbMismatchPayload(payload)) return "database";
|
|
867
|
+
if (isViewerIdentityMismatchPayload(payload)) return "identity";
|
|
868
|
+
if (isViewerContractUnsupportedPayload(payload)) return "contract";
|
|
869
|
+
return "connection";
|
|
870
|
+
};
|
|
871
|
+
|
|
872
|
+
const RAW_EVENT_FAILURE_ACTIONS = Object.freeze({
|
|
873
|
+
database: "restart the viewer from the same workspace/config",
|
|
874
|
+
identity: "restart Codemem and OpenCode with the same environment",
|
|
875
|
+
contract: "update Codemem on the installed channel, then restart OpenCode",
|
|
876
|
+
connection: "check or restart the viewer",
|
|
877
|
+
});
|
|
878
|
+
|
|
764
879
|
// Dependency-free port of @codemem/core prompt-transport semantics. Keep the
|
|
765
880
|
// range and classifier parity pinned by the plugin injection tests.
|
|
766
881
|
const PROMPT_TRANSPORT_PROTOCOL_RANGE = Object.freeze({
|
|
@@ -815,9 +930,10 @@ const isViewerPolicyOrAuthFailurePayload = (payload) => {
|
|
|
815
930
|
].includes(code);
|
|
816
931
|
};
|
|
817
932
|
|
|
818
|
-
const viewerFailureClassification = (cause, disposition) => ({
|
|
933
|
+
const viewerFailureClassification = (cause, disposition, kind = null) => ({
|
|
819
934
|
cause,
|
|
820
935
|
disposition,
|
|
936
|
+
kind,
|
|
821
937
|
retryable: disposition !== "terminal",
|
|
822
938
|
});
|
|
823
939
|
|
|
@@ -854,6 +970,7 @@ const classifyViewerHttpFailure = ({
|
|
|
854
970
|
kind: "viewer_contract_unsupported",
|
|
855
971
|
compatibleProfile,
|
|
856
972
|
}),
|
|
973
|
+
"viewer_contract_unsupported",
|
|
857
974
|
);
|
|
858
975
|
}
|
|
859
976
|
if (operation === "prompt-pack-ledger" && isValidLedgerFailureHttpPayload(body)) {
|
|
@@ -906,6 +1023,7 @@ const buildPackHttpBody = ({
|
|
|
906
1023
|
dbPath,
|
|
907
1024
|
identityTarget,
|
|
908
1025
|
attempt,
|
|
1026
|
+
automaticContext,
|
|
909
1027
|
}) => ({
|
|
910
1028
|
context: query,
|
|
911
1029
|
limit: injectLimit !== null && Number.isFinite(injectLimit) && injectLimit > 0
|
|
@@ -926,6 +1044,7 @@ const buildPackHttpBody = ({
|
|
|
926
1044
|
.map((value) => String(value || "").trim())
|
|
927
1045
|
.filter((value) => value && value.length <= MAX_WORKING_SET_PATH_CHARS),
|
|
928
1046
|
attempt,
|
|
1047
|
+
...(automaticContext !== undefined ? { automatic_context: automaticContext } : {}),
|
|
929
1048
|
});
|
|
930
1049
|
|
|
931
1050
|
const canonicalJson = (value) => {
|
|
@@ -1114,16 +1233,11 @@ const getSessionMessageInjectionCache = (messageInjectionCache, sessionID) => {
|
|
|
1114
1233
|
return sessionCache;
|
|
1115
1234
|
};
|
|
1116
1235
|
|
|
1117
|
-
const
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
if (!oldestKey) break;
|
|
1122
|
-
sessionCache.delete(oldestKey);
|
|
1236
|
+
const normalizeInjectedMessageParts = (messages, sessionCache, { pruneAbsent = true } = {}) => {
|
|
1237
|
+
const presentIds = new Set(messages.map(resolveEntryMessageId).filter(Boolean));
|
|
1238
|
+
for (const id of sessionCache?.keys() || []) {
|
|
1239
|
+
if (pruneAbsent && !presentIds.has(id)) sessionCache.delete(id);
|
|
1123
1240
|
}
|
|
1124
|
-
};
|
|
1125
|
-
|
|
1126
|
-
const normalizeInjectedMessageParts = (messages, sessionCache) => {
|
|
1127
1241
|
for (let index = 0; index < messages.length; index += 1) {
|
|
1128
1242
|
const entry = messages[index];
|
|
1129
1243
|
if (!isUserMessageEntry(entry) || !Array.isArray(entry.parts)) {
|
|
@@ -1131,24 +1245,19 @@ const normalizeInjectedMessageParts = (messages, sessionCache) => {
|
|
|
1131
1245
|
}
|
|
1132
1246
|
|
|
1133
1247
|
const messageId = resolveEntryMessageId(entry);
|
|
1134
|
-
const
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
sessionCache.set(messageId, { text: existingText, attemptId: null, reconstructed: true });
|
|
1148
|
-
trimSessionMessageInjectionCache(sessionCache);
|
|
1149
|
-
}
|
|
1150
|
-
if (retainedParts.length !== entry.parts.length) {
|
|
1151
|
-
entry.parts.splice(0, entry.parts.length, ...retainedParts);
|
|
1248
|
+
const parts = entry.parts.filter(isCodememContextPart);
|
|
1249
|
+
if (parts.length && sessionCache && messageId) {
|
|
1250
|
+
const cached = sessionCache.get(messageId);
|
|
1251
|
+
sessionCache.set(messageId, {
|
|
1252
|
+
...cached,
|
|
1253
|
+
text: parts[0].text,
|
|
1254
|
+
parts: parts.map((part) => {
|
|
1255
|
+
const recall = retainedPartRecall(part, cached?.parts);
|
|
1256
|
+
return recall
|
|
1257
|
+
? { ...part, metadata: { ...part.metadata, codemem: recall } }
|
|
1258
|
+
: { ...part };
|
|
1259
|
+
}),
|
|
1260
|
+
});
|
|
1152
1261
|
}
|
|
1153
1262
|
}
|
|
1154
1263
|
};
|
|
@@ -1163,6 +1272,7 @@ const buildInjectedContextPart = (entry, index, text, options = {}) => {
|
|
|
1163
1272
|
type: "text",
|
|
1164
1273
|
text,
|
|
1165
1274
|
synthetic: true,
|
|
1275
|
+
...(options.recall ? { metadata: { codemem: options.recall } } : {}),
|
|
1166
1276
|
};
|
|
1167
1277
|
};
|
|
1168
1278
|
|
|
@@ -1203,7 +1313,10 @@ const appendCachedInjectedContextParts = async (
|
|
|
1203
1313
|
cacheReuseReady = typeof reuse === "object" ? reuse?.ready : null;
|
|
1204
1314
|
}
|
|
1205
1315
|
try {
|
|
1206
|
-
entry.parts.
|
|
1316
|
+
if (!entry.parts.some(isCodememContextPart)) {
|
|
1317
|
+
const parts = cached?.parts || [buildInjectedContextPart(entry, index, text, { messageId, sessionID })];
|
|
1318
|
+
entry.parts.push(...parts.map((part) => ({ ...part })));
|
|
1319
|
+
}
|
|
1207
1320
|
} catch (error) {
|
|
1208
1321
|
if (messageId === options.latestMessageId && attemptId && options.confirmDelivery) {
|
|
1209
1322
|
if (cacheReuseReady) {
|
|
@@ -1230,6 +1343,11 @@ const appendCachedInjectedContextParts = async (
|
|
|
1230
1343
|
return appended;
|
|
1231
1344
|
};
|
|
1232
1345
|
|
|
1346
|
+
const scopeMessagesToSession = (messages, sessionID) => messages.filter((entry) => {
|
|
1347
|
+
const entrySession = resolveEntrySessionID(entry);
|
|
1348
|
+
return !entrySession || !sessionID || entrySession === sessionID;
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1233
1351
|
const applyInjectedContextToMessages = async ({
|
|
1234
1352
|
injectEnabled,
|
|
1235
1353
|
input,
|
|
@@ -1243,40 +1361,82 @@ const applyInjectedContextToMessages = async ({
|
|
|
1243
1361
|
confirmDelivery,
|
|
1244
1362
|
recordCacheReuse,
|
|
1245
1363
|
recordSkipped,
|
|
1364
|
+
injectTokenBudget = DEFAULT_INJECT_TOKEN_BUDGET,
|
|
1365
|
+
retainedTokenBudget = DEFAULT_RETAINED_TOKEN_BUDGET,
|
|
1366
|
+
workingSet = [],
|
|
1367
|
+
recordMeasurement,
|
|
1246
1368
|
}) => {
|
|
1247
1369
|
const hasMessages = Array.isArray(output?.messages);
|
|
1248
|
-
const
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1251
|
-
|
|
1370
|
+
const hookSessionID = input?.sessionID || null;
|
|
1371
|
+
const hookMessages = hasMessages ? scopeMessagesToSession(output.messages, hookSessionID) : [];
|
|
1372
|
+
const latestUser = findLatestUserMessage(hookMessages);
|
|
1373
|
+
const sessionID = hookSessionID || (latestUser ? resolveEntrySessionID(latestUser.entry) : null);
|
|
1374
|
+
// Both transforms use the hook session when an entry omits session identity.
|
|
1375
|
+
const messages = scopeMessagesToSession(hookMessages, sessionID);
|
|
1376
|
+
const report = (reason, newTokens = 0, duplicates = 0) => {
|
|
1377
|
+
const measurement = {
|
|
1378
|
+
new_tokens: newTokens,
|
|
1379
|
+
retained_tokens: countRetainedInjectionTokens(messages),
|
|
1380
|
+
duplicates_omitted: duplicates,
|
|
1381
|
+
reason,
|
|
1382
|
+
};
|
|
1383
|
+
try { void Promise.resolve(recordMeasurement?.(measurement)).catch(() => {}); } catch { /* best effort */ }
|
|
1384
|
+
};
|
|
1252
1385
|
if (!injectEnabled) {
|
|
1253
1386
|
recordSkipped?.("injection_disabled", sessionID);
|
|
1387
|
+
report("injection_disabled");
|
|
1254
1388
|
return false;
|
|
1255
1389
|
}
|
|
1256
1390
|
if (!hasMessages) {
|
|
1391
|
+
report("missing_history");
|
|
1257
1392
|
return false;
|
|
1258
1393
|
}
|
|
1259
|
-
|
|
1260
1394
|
if (consumeCompactionInjectionSkip(compactionInjectionSkips, sessionID)) {
|
|
1261
|
-
|
|
1395
|
+
const cache = getSessionMessageInjectionCache(messageInjectionCache, sessionID);
|
|
1396
|
+
normalizeInjectedMessageParts(messages, cache, { pruneAbsent: false });
|
|
1262
1397
|
recordSkipped?.("compaction_skipped", sessionID);
|
|
1398
|
+
report("compaction_skipped");
|
|
1263
1399
|
return false;
|
|
1264
1400
|
}
|
|
1265
1401
|
|
|
1266
1402
|
if (!latestUser) {
|
|
1403
|
+
report("missing_user");
|
|
1267
1404
|
return false;
|
|
1268
1405
|
}
|
|
1269
1406
|
|
|
1270
1407
|
const sessionCache = getSessionMessageInjectionCache(messageInjectionCache, sessionID);
|
|
1271
|
-
normalizeInjectedMessageParts(
|
|
1408
|
+
normalizeInjectedMessageParts(messages, sessionCache);
|
|
1272
1409
|
|
|
1273
1410
|
const latestMessageId = resolveEntryMessageId(latestUser.entry);
|
|
1274
1411
|
const canReplay = Boolean(sessionCache && latestMessageId);
|
|
1275
1412
|
const latestCached = canReplay ? sessionCache.get(latestMessageId) : null;
|
|
1276
|
-
const latestWasCached = Boolean(latestCached)
|
|
1413
|
+
const latestWasCached = Boolean(latestCached) || latestUser.entry.parts?.some(isCodememContextPart);
|
|
1414
|
+
if (sessionCache) {
|
|
1415
|
+
try {
|
|
1416
|
+
await appendCachedInjectedContextParts(messages, sessionCache, sessionID, {
|
|
1417
|
+
latestMessageId, confirmDelivery, recordCacheReuse,
|
|
1418
|
+
});
|
|
1419
|
+
} catch (error) {
|
|
1420
|
+
report("delivery_failed");
|
|
1421
|
+
throw error;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const retainedTokens = countRetainedInjectionTokens(messages);
|
|
1425
|
+
const fullBudget = Math.min(injectTokenBudget, Math.max(0, retainedTokenBudget - retainedTokens));
|
|
1277
1426
|
let latestWasBuilt = false;
|
|
1427
|
+
let newlyDelivered = 0;
|
|
1428
|
+
let duplicates = 0;
|
|
1429
|
+
let reason = latestWasCached ? "replay" : "no_context";
|
|
1278
1430
|
if (!latestWasCached) {
|
|
1279
|
-
|
|
1431
|
+
if (reserveContextPrefixBudget(fullBudget) === null) {
|
|
1432
|
+
if (reserveContextPrefixBudget(injectTokenBudget) !== null) {
|
|
1433
|
+
recordSkipped?.("allowance_exhausted", sessionID);
|
|
1434
|
+
}
|
|
1435
|
+
report("allowance_exhausted");
|
|
1436
|
+
return retainedTokens > 0;
|
|
1437
|
+
}
|
|
1438
|
+
const firstUser = findFirstUserMessage(messages);
|
|
1439
|
+
const workingContext = workingContextDigest(messages, workingSet);
|
|
1280
1440
|
const query = resolveInjectQuery({
|
|
1281
1441
|
firstPrompt: firstUser ? extractMessageText(firstUser.entry) : null,
|
|
1282
1442
|
lastPromptText: extractMessageText(latestUser.entry),
|
|
@@ -1284,36 +1444,62 @@ const applyInjectedContextToMessages = async ({
|
|
|
1284
1444
|
const injected = await buildInjectedContext(query, {
|
|
1285
1445
|
sessionID,
|
|
1286
1446
|
requestKey: latestMessageId || fallbackEntryMessageId(latestUser.entry, latestUser.index),
|
|
1447
|
+
fallbackTurn: sessionID && latestMessageId ? null : latestUser.index,
|
|
1287
1448
|
surface: "message",
|
|
1449
|
+
tokenBudget: fullBudget,
|
|
1450
|
+
retainedItems: retainedMemoryFingerprints(messages, sessionCache),
|
|
1451
|
+
retainedMetadata: retainedMetadataGaps(messages, sessionCache),
|
|
1452
|
+
workingContext,
|
|
1453
|
+
continuationOnly: isContinuationOnly(extractMessageText(latestUser.entry), messages, workingContext, sessionCache),
|
|
1288
1454
|
});
|
|
1289
|
-
|
|
1455
|
+
// Another transform may have delivered while retrieval was awaiting I/O.
|
|
1456
|
+
if (sessionCache) {
|
|
1457
|
+
await appendCachedInjectedContextParts(messages, sessionCache, sessionID, {
|
|
1458
|
+
latestMessageId, confirmDelivery, recordCacheReuse,
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
if (latestUser.entry.parts?.some(isCodememContextPart)) {
|
|
1462
|
+
duplicates = injected?.duplicates || 0;
|
|
1463
|
+
if (injected?.attemptId) {
|
|
1464
|
+
confirmDelivery?.(injected.attemptId, "unknown", injected.evaluation);
|
|
1465
|
+
}
|
|
1466
|
+
report("replay", 0, duplicates);
|
|
1467
|
+
return true;
|
|
1468
|
+
}
|
|
1469
|
+
const deliveryBudget = Math.min(fullBudget, Math.max(
|
|
1470
|
+
0, retainedTokenBudget - countRetainedInjectionTokens(messages),
|
|
1471
|
+
));
|
|
1472
|
+
duplicates = injected?.duplicates || 0;
|
|
1473
|
+
reason = injected?.skipReason || "no_context";
|
|
1474
|
+
if (injected?.text && estimateTokens(injected.text) <= deliveryBudget) {
|
|
1290
1475
|
latestWasBuilt = true;
|
|
1291
|
-
if (
|
|
1292
|
-
sessionCache.set(latestMessageId, {
|
|
1293
|
-
text: injected.text,
|
|
1294
|
-
attemptId: injected.attemptId || null,
|
|
1295
|
-
requestId: injected.requestId || null,
|
|
1296
|
-
queryHash: injected.queryHash || null,
|
|
1297
|
-
promptNumber: injected.promptNumber || 0,
|
|
1298
|
-
reuseCount: 0,
|
|
1299
|
-
});
|
|
1300
|
-
trimSessionMessageInjectionCache(sessionCache);
|
|
1301
|
-
} else if (Array.isArray(latestUser.entry.parts)) {
|
|
1476
|
+
if (Array.isArray(latestUser.entry.parts)) {
|
|
1302
1477
|
try {
|
|
1303
1478
|
latestUser.entry.parts.push(
|
|
1304
1479
|
buildInjectedContextPart(latestUser.entry, latestUser.index, injected.text, {
|
|
1305
1480
|
messageId: latestMessageId || undefined,
|
|
1306
1481
|
sessionID: sessionID || undefined,
|
|
1482
|
+
recall: injected.recall,
|
|
1307
1483
|
})
|
|
1308
1484
|
);
|
|
1309
1485
|
} catch (error) {
|
|
1310
1486
|
if (injected.attemptId) {
|
|
1311
|
-
confirmDelivery?.(injected.attemptId, "failed");
|
|
1487
|
+
confirmDelivery?.(injected.attemptId, "failed", injected.evaluation);
|
|
1312
1488
|
}
|
|
1489
|
+
report("delivery_failed", 0, duplicates);
|
|
1313
1490
|
throw error;
|
|
1314
1491
|
}
|
|
1492
|
+
newlyDelivered = estimateTokens(injected.text);
|
|
1493
|
+
reason = "delivered";
|
|
1315
1494
|
if (injected.attemptId) {
|
|
1316
|
-
confirmDelivery?.(injected.attemptId);
|
|
1495
|
+
confirmDelivery?.(injected.attemptId, "handed_off", injected.evaluation);
|
|
1496
|
+
}
|
|
1497
|
+
if (canReplay) {
|
|
1498
|
+
sessionCache.set(latestMessageId, {
|
|
1499
|
+
...injected,
|
|
1500
|
+
parts: latestUser.entry.parts.filter(isCodememContextPart).map((part) => ({ ...part })),
|
|
1501
|
+
reuseCount: 0,
|
|
1502
|
+
});
|
|
1317
1503
|
}
|
|
1318
1504
|
}
|
|
1319
1505
|
|
|
@@ -1326,10 +1512,15 @@ const applyInjectedContextToMessages = async ({
|
|
|
1326
1512
|
// best-effort only
|
|
1327
1513
|
}
|
|
1328
1514
|
}
|
|
1329
|
-
} else if (
|
|
1330
|
-
|
|
1515
|
+
} else if (injected?.skipReason) {
|
|
1516
|
+
// The retrieval already has an attempt. Withheld output is not a second retrieval.
|
|
1517
|
+
if (injected.attemptId) confirmDelivery?.(injected.attemptId, "unknown", injected.evaluation);
|
|
1518
|
+
} else if (injected?.text) {
|
|
1519
|
+
reason = "budget_rejected";
|
|
1520
|
+
if (injected.attemptId) confirmDelivery?.(injected.attemptId, "failed", injected.evaluation);
|
|
1331
1521
|
}
|
|
1332
1522
|
}
|
|
1523
|
+
report(reason, newlyDelivered, duplicates);
|
|
1333
1524
|
|
|
1334
1525
|
if (!canReplay) {
|
|
1335
1526
|
return Boolean(
|
|
@@ -1338,13 +1529,136 @@ const applyInjectedContextToMessages = async ({
|
|
|
1338
1529
|
);
|
|
1339
1530
|
}
|
|
1340
1531
|
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1532
|
+
return latestWasBuilt || retainedTokens > 0;
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
const countRetainedInjectionTokens = (messages) => messages.reduce(
|
|
1536
|
+
(sum, entry) => sum + (entry.parts || []).filter(isCodememContextPart)
|
|
1537
|
+
.reduce((tokens, part) => tokens + estimateTokens(part.text), 0),
|
|
1538
|
+
0,
|
|
1539
|
+
);
|
|
1540
|
+
|
|
1541
|
+
const injectionDigest = (text) => createHash("sha256").update(text).digest("hex");
|
|
1542
|
+
|
|
1543
|
+
const retainedPartRecall = (part, cachedParts = []) => {
|
|
1544
|
+
const valid = (candidate) => candidate?.metadata?.codemem?.v === 1
|
|
1545
|
+
&& candidate.metadata.codemem.digest === injectionDigest(String(part.text || ""));
|
|
1546
|
+
if (valid(part)) return part.metadata.codemem;
|
|
1547
|
+
const cached = cachedParts.find((candidate) => candidate.id === part.id && candidate.text === part.text && valid(candidate));
|
|
1548
|
+
return cached?.metadata?.codemem;
|
|
1549
|
+
};
|
|
1550
|
+
|
|
1551
|
+
const workingContextDigest = (messages, workingSet) => {
|
|
1552
|
+
const scalar = (value) => typeof value === "string" ? value.slice(0, 256) : null;
|
|
1553
|
+
const hash = createHash("sha256");
|
|
1554
|
+
for (const path of [...workingSet].filter((value) => typeof value === "string").sort()) {
|
|
1555
|
+
hash.update(JSON.stringify(["file", path.slice(0, MAX_WORKING_SET_PATH_CHARS)]));
|
|
1556
|
+
}
|
|
1557
|
+
for (const entry of messages) {
|
|
1558
|
+
for (const part of entry.parts || []) {
|
|
1559
|
+
if (part.type === "text") continue;
|
|
1560
|
+
// Never traverse args, output, attachments, or arbitrary tool-state payloads.
|
|
1561
|
+
hash.update(JSON.stringify([
|
|
1562
|
+
scalar(entry.info?.id), scalar(part.type), scalar(part.id),
|
|
1563
|
+
scalar(part.callID), scalar(part.state?.status),
|
|
1564
|
+
]));
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
return hash.digest("hex");
|
|
1568
|
+
};
|
|
1569
|
+
|
|
1570
|
+
const isContinuationOnly = (prompt, messages, workingContext, sessionCache) => {
|
|
1571
|
+
if (!/^(continue|proceed|go on|keep going)[.!]?$/i.test(prompt.trim())) return false;
|
|
1572
|
+
const latest = findLatestUserMessage(messages);
|
|
1573
|
+
for (let index = (latest?.index ?? 0) - 1; index >= 0; index -= 1) {
|
|
1574
|
+
const entry = messages[index];
|
|
1575
|
+
if (!isUserMessageEntry(entry)) continue;
|
|
1576
|
+
return (entry.parts || []).some((part) => {
|
|
1577
|
+
const recall = retainedPartRecall(part, sessionCache?.get(resolveEntryMessageId(entry))?.parts);
|
|
1578
|
+
return isCodememContextPart(part) && recall?.v === 1
|
|
1579
|
+
&& recall.digest === injectionDigest(String(part.text || ""))
|
|
1580
|
+
&& recall.workingContext === workingContext;
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
return false;
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
const retainedMemoryFingerprints = (messages, sessionCache) => {
|
|
1587
|
+
const retained = new Set();
|
|
1588
|
+
for (const entry of messages) {
|
|
1589
|
+
for (const part of (entry.parts || []).filter(isCodememContextPart)) {
|
|
1590
|
+
const recall = retainedPartRecall(part, sessionCache?.get(resolveEntryMessageId(entry))?.parts);
|
|
1591
|
+
if (recall?.v !== 1 || recall.digest !== injectionDigest(String(part.text || ""))) continue;
|
|
1592
|
+
for (const item of Array.isArray(recall.items) ? recall.items : []) {
|
|
1593
|
+
if (isRecord(item) && Number.isSafeInteger(item.id) && item.id > 0 && /^[a-f0-9]{64}$/.test(item.fingerprint)) {
|
|
1594
|
+
retained.add(`${item.id}:${item.fingerprint}`);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
return retained;
|
|
1600
|
+
};
|
|
1601
|
+
|
|
1602
|
+
const filterRetainedPack = (stdout, packText, retained = new Set()) => {
|
|
1603
|
+
const fallback = { text: packText, items: [], duplicates: 0, packMetadata: "missing" };
|
|
1604
|
+
let payload;
|
|
1605
|
+
try { payload = JSON.parse(stdout); } catch { return fallback; }
|
|
1606
|
+
const items = payload?.rendered_items;
|
|
1607
|
+
if (items != null) fallback.packMetadata = "invalid";
|
|
1608
|
+
// Old transports and malformed metadata remain eligible rather than guessing from prose.
|
|
1609
|
+
if (payload?.pack_text !== packText || !Array.isArray(items) || !items.length) return fallback;
|
|
1610
|
+
if (payload.metrics?.total_items !== items.length) return fallback;
|
|
1611
|
+
const spans = [];
|
|
1612
|
+
const seen = new Set();
|
|
1613
|
+
for (const item of items) {
|
|
1614
|
+
if (!isRecord(item) || !Number.isSafeInteger(item.id) || item.id <= 0 || seen.has(item.id)
|
|
1615
|
+
|| !/^[a-f0-9]{64}$/.test(item.fingerprint) || !Array.isArray(item.spans) || !item.spans.length) return fallback;
|
|
1616
|
+
seen.add(item.id);
|
|
1617
|
+
for (const span of item.spans) {
|
|
1618
|
+
if (!isRecord(span) || !Number.isSafeInteger(span.start) || !Number.isSafeInteger(span.end)
|
|
1619
|
+
|| span.start < 0 || span.end <= span.start || span.end > packText.length) return fallback;
|
|
1620
|
+
spans.push({ ...span, omit: retained.has(`${item.id}:${item.fingerprint}`) });
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
if (payload.items != null) {
|
|
1624
|
+
if (!Array.isArray(payload.items) || payload.items.length !== seen.size) return fallback;
|
|
1625
|
+
const selectedIds = new Set(payload.items.map((item) => item?.id));
|
|
1626
|
+
if (selectedIds.size !== seen.size || [...selectedIds].some((id) => !seen.has(id))) return fallback;
|
|
1627
|
+
}
|
|
1628
|
+
for (const ids of [payload.item_ids, payload.metrics?.pack_item_ids]) {
|
|
1629
|
+
if (ids == null) continue;
|
|
1630
|
+
if (!Array.isArray(ids) || new Set(ids).size !== ids.length
|
|
1631
|
+
|| ids.some((id) => !Number.isSafeInteger(id) || id <= 0)
|
|
1632
|
+
|| [...seen].some((id) => !ids.includes(id))) return fallback;
|
|
1633
|
+
}
|
|
1634
|
+
spans.sort((a, b) => a.start - b.start);
|
|
1635
|
+
if (spans.some((span, index) => index > 0 && span.start < spans[index - 1].end)) return fallback;
|
|
1636
|
+
const kept = items.filter((item) => !retained.has(`${item.id}:${item.fingerprint}`));
|
|
1637
|
+
let text = packText;
|
|
1638
|
+
for (const span of spans.reverse()) {
|
|
1639
|
+
if (span.omit) text = text.slice(0, span.start) + text.slice(span.end);
|
|
1640
|
+
}
|
|
1641
|
+
return {
|
|
1642
|
+
text: kept.length ? text : "",
|
|
1643
|
+
items: kept.map(({ id, fingerprint }) => ({ id, fingerprint })),
|
|
1644
|
+
duplicates: items.length - kept.length,
|
|
1645
|
+
packMetadata: "valid",
|
|
1646
|
+
};
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1649
|
+
const retainedMetadataGaps = (messages, sessionCache) => {
|
|
1650
|
+
let missingRetainedMetadata = false;
|
|
1651
|
+
let invalidRetainedMetadata = false;
|
|
1652
|
+
for (const entry of messages) {
|
|
1653
|
+
for (const part of (entry.parts || []).filter(isCodememContextPart)) {
|
|
1654
|
+
const recall = retainedPartRecall(part, sessionCache?.get(resolveEntryMessageId(entry))?.parts);
|
|
1655
|
+
if (recall?.v === 1 && Array.isArray(recall.items) && recall.items.length > 0
|
|
1656
|
+
&& recall.items.every((item) => isRecord(item) && Number.isSafeInteger(item.id) && item.id > 0 && /^[a-f0-9]{64}$/.test(item.fingerprint))) continue;
|
|
1657
|
+
if (part.metadata?.codemem != null || recall != null) invalidRetainedMetadata = true;
|
|
1658
|
+
else missingRetainedMetadata = true;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
return { missingRetainedMetadata, invalidRetainedMetadata };
|
|
1348
1662
|
};
|
|
1349
1663
|
|
|
1350
1664
|
const mapOpencodeEventTypeToAdapterType = (eventType) => {
|
|
@@ -1594,7 +1908,11 @@ const trimEventQueue = ({ events, maxEvents, hardMaxEvents, onUnsentPressure, on
|
|
|
1594
1908
|
hardMaxEvents > 0 &&
|
|
1595
1909
|
events.length > hardMaxEvents
|
|
1596
1910
|
) {
|
|
1597
|
-
const
|
|
1911
|
+
const durableIndex = events.findIndex(
|
|
1912
|
+
(queued) => queued && typeof queued === "object" && queued._raw_spooled
|
|
1913
|
+
);
|
|
1914
|
+
const dropIndex = durableIndex >= 0 ? durableIndex : 0;
|
|
1915
|
+
const [dropped] = events.splice(dropIndex, 1);
|
|
1598
1916
|
if (typeof onForcedDrop === "function") {
|
|
1599
1917
|
onForcedDrop(dropped, events.length, hardMaxEvents);
|
|
1600
1918
|
}
|
|
@@ -1651,6 +1969,25 @@ const parsePositiveInt = (value, fallback) => {
|
|
|
1651
1969
|
return parsed;
|
|
1652
1970
|
};
|
|
1653
1971
|
|
|
1972
|
+
const estimateTokens = (text) => Math.ceil(String(text || "").length / 4);
|
|
1973
|
+
|
|
1974
|
+
const resolveInjectTokenBudget = (value) =>
|
|
1975
|
+
parsePositiveInt(value, DEFAULT_INJECT_TOKEN_BUDGET);
|
|
1976
|
+
|
|
1977
|
+
const resolveRetainedTokenBudget = (value) => {
|
|
1978
|
+
const text = String(value ?? "").trim();
|
|
1979
|
+
const parsed = Number(text);
|
|
1980
|
+
return /^\d+$/.test(text) && Number.isSafeInteger(parsed) && parsed > 0
|
|
1981
|
+
? parsed : DEFAULT_RETAINED_TOKEN_BUDGET;
|
|
1982
|
+
};
|
|
1983
|
+
|
|
1984
|
+
const reserveContextPrefixBudget = (tokenBudget) => {
|
|
1985
|
+
const packBudget = tokenBudget - estimateTokens(CODEMEM_CONTEXT_PREFIX);
|
|
1986
|
+
return packBudget > 0 ? packBudget : null;
|
|
1987
|
+
};
|
|
1988
|
+
|
|
1989
|
+
const wrapInjectedContext = (packText) => `${CODEMEM_CONTEXT_PREFIX}${packText}`;
|
|
1990
|
+
|
|
1654
1991
|
export const buildInjectionToastMessage = (metrics) => {
|
|
1655
1992
|
const items = asFiniteNonNegativeInt(metrics?.items) ?? asFiniteNonNegativeInt(metrics?.total_items);
|
|
1656
1993
|
const packTokens = asFiniteNonNegativeInt(metrics?.pack_tokens);
|
|
@@ -1693,22 +2030,10 @@ const detectRunner = ({ cwd, envRunner }) => {
|
|
|
1693
2030
|
// terminal on every OpenCode startup for npx-only installs.
|
|
1694
2031
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1695
2032
|
}).trim();
|
|
1696
|
-
//
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
|
|
1700
|
-
// Accept a global codemem only when it is the EXACT pinned version, or a
|
|
1701
|
-
// clean (non-prerelease) release at least as new as the pin. isVersionAtLeast
|
|
1702
|
-
// ignores prerelease suffixes, so without this a prerelease like
|
|
1703
|
-
// "0.36.1-alpha.0" would wrongly compare >= the "0.36.1" release even though
|
|
1704
|
-
// SemVer orders it below. Requiring an exact match for prereleases also
|
|
1705
|
-
// handles a prerelease PIN correctly (the release script can set one): only
|
|
1706
|
-
// the identical prerelease is trusted, never an older/different one.
|
|
1707
|
-
const isPrerelease = versionOutput.includes("-");
|
|
1708
|
-
const matchesOrCleanNewer =
|
|
1709
|
-
versionOutput === PINNED_BACKEND_VERSION ||
|
|
1710
|
-
(!isPrerelease && isVersionAtLeast(versionOutput, PINNED_BACKEND_VERSION));
|
|
1711
|
-
if (matchesOrCleanNewer) {
|
|
2033
|
+
// Use a newer global CLI only when it remains on the plugin's release
|
|
2034
|
+
// channel. This prevents both channel crossing and fallback to an older pin
|
|
2035
|
+
// immediately after a successful prerelease auto-update.
|
|
2036
|
+
if (isSameChannelVersionAtLeast(versionOutput, PINNED_BACKEND_VERSION)) {
|
|
1712
2037
|
return "codemem";
|
|
1713
2038
|
}
|
|
1714
2039
|
} catch {
|
|
@@ -1822,6 +2147,13 @@ export const CodememPlugin = async ({
|
|
|
1822
2147
|
if (pluginIgnored) {
|
|
1823
2148
|
return {};
|
|
1824
2149
|
}
|
|
2150
|
+
const releasePluginRegistration = claimPluginRegistration(cwd);
|
|
2151
|
+
if (!releasePluginRegistration) {
|
|
2152
|
+
await log("warn", "codemem duplicate plugin registration skipped", {
|
|
2153
|
+
next_action: "remove either the configured npm plugin or the project-local Codemem plugin",
|
|
2154
|
+
});
|
|
2155
|
+
return {};
|
|
2156
|
+
}
|
|
1825
2157
|
|
|
1826
2158
|
const runner = detectRunner({
|
|
1827
2159
|
cwd,
|
|
@@ -1871,15 +2203,30 @@ export const CodememPlugin = async ({
|
|
|
1871
2203
|
process.env.CODEMEM_INJECT_CONTEXT || "1"
|
|
1872
2204
|
);
|
|
1873
2205
|
const injectSurface = resolveInjectSurface(process.env.CODEMEM_INJECT_SURFACE);
|
|
1874
|
-
//
|
|
2206
|
+
// The item limit remains optional so the CLI can use its configured default.
|
|
1875
2207
|
const injectLimitEnv = process.env.CODEMEM_INJECT_LIMIT;
|
|
1876
2208
|
const injectLimit = injectLimitEnv ? parseNumber(injectLimitEnv, null) : null;
|
|
1877
|
-
const
|
|
1878
|
-
|
|
2209
|
+
const injectTokenBudget = resolveInjectTokenBudget(
|
|
2210
|
+
process.env.CODEMEM_INJECT_TOKEN_BUDGET
|
|
2211
|
+
);
|
|
2212
|
+
const injectPackTokenBudget = reserveContextPrefixBudget(injectTokenBudget);
|
|
2213
|
+
const retainedTokenBudget = resolveRetainedTokenBudget(process.env.CODEMEM_INJECT_RETAINED_TOKEN_BUDGET);
|
|
2214
|
+
if (injectEnabled && injectPackTokenBudget === null) {
|
|
2215
|
+
await log(
|
|
2216
|
+
"warn",
|
|
2217
|
+
"codemem context injection skipped: token budget is too small",
|
|
2218
|
+
{
|
|
2219
|
+
inject_token_budget: injectTokenBudget,
|
|
2220
|
+
required_prefix_tokens: estimateTokens(CODEMEM_CONTEXT_PREFIX),
|
|
2221
|
+
next_action: "increase CODEMEM_INJECT_TOKEN_BUDGET or unset it to use the default",
|
|
2222
|
+
}
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
1879
2225
|
const injectionToastShown = new Set();
|
|
1880
2226
|
const messageInjectionCache = new Map();
|
|
1881
2227
|
const compactionInjectionSkips = new Map();
|
|
1882
2228
|
const disabledInjectionRecorded = new Set();
|
|
2229
|
+
const latestPolicySkips = new Map();
|
|
1883
2230
|
const attemptStartedAt = new Map();
|
|
1884
2231
|
const promptPackRetryCounts = new Map();
|
|
1885
2232
|
const successfulPromptPackArtifacts = new Map();
|
|
@@ -1923,9 +2270,22 @@ export const CodememPlugin = async ({
|
|
|
1923
2270
|
process.env.CODEMEM_RAW_EVENTS_HARD_MAX || "2000",
|
|
1924
2271
|
2000
|
|
1925
2272
|
);
|
|
2273
|
+
const rawEventSpoolDrainLimit = parsePositiveInt(
|
|
2274
|
+
process.env.CODEMEM_RAW_EVENT_SPOOL_DRAIN_LIMIT,
|
|
2275
|
+
DEFAULT_DRAIN_LIMIT,
|
|
2276
|
+
);
|
|
2277
|
+
const rawEventSpoolMaxEntries = parsePositiveInt(
|
|
2278
|
+
process.env.CODEMEM_RAW_EVENT_SPOOL_MAX_ENTRIES,
|
|
2279
|
+
DEFAULT_MAX_ENTRIES,
|
|
2280
|
+
);
|
|
2281
|
+
const rawEventSpoolHome = process.env.HOME?.trim() || homedir();
|
|
2282
|
+
const rawEventSpoolDirectory = resolveSpoolDirectory(rawEventSpoolHome);
|
|
2283
|
+
// Memoize the exact serialized envelope by queued-object identity so retries
|
|
2284
|
+
// cannot drift in timestamp, ID, property order, or bytes.
|
|
2285
|
+
const rawEventEnvelopes = new WeakMap();
|
|
1926
2286
|
let streamUnavailableUntil = 0;
|
|
1927
|
-
let
|
|
1928
|
-
let
|
|
2287
|
+
let spoolPersistenceFailureNoted = null;
|
|
2288
|
+
let spoolLoadFailureNoted = false;
|
|
1929
2289
|
let lastStatusCheckAt = 0;
|
|
1930
2290
|
let lastStatusAvailable = true;
|
|
1931
2291
|
let promptPackTransportUnavailableUntil = 0;
|
|
@@ -1937,9 +2297,9 @@ export const CodememPlugin = async ({
|
|
|
1937
2297
|
return `${Date.now()}-${Math.random()}`;
|
|
1938
2298
|
};
|
|
1939
2299
|
|
|
1940
|
-
const queueRawEventViaCli = async (body) => {
|
|
2300
|
+
const queueRawEventViaCli = async (body, serialized = JSON.stringify(body)) => {
|
|
1941
2301
|
const runFallback = () => runCli(["enqueue-raw-event"], {
|
|
1942
|
-
stdinText:
|
|
2302
|
+
stdinText: serialized,
|
|
1943
2303
|
});
|
|
1944
2304
|
let result = await runFallback();
|
|
1945
2305
|
let classification = classifyFallbackCommandResult(result);
|
|
@@ -1954,23 +2314,191 @@ export const CodememPlugin = async ({
|
|
|
1954
2314
|
const error = new Error(
|
|
1955
2315
|
retryExhausted ? `${classification.cause} after retry` : classification.cause
|
|
1956
2316
|
);
|
|
1957
|
-
error.retryable = classification.retryable
|
|
2317
|
+
error.retryable = classification.retryable;
|
|
1958
2318
|
throw error;
|
|
1959
2319
|
}
|
|
1960
2320
|
return true;
|
|
1961
2321
|
};
|
|
1962
2322
|
|
|
1963
2323
|
const lastToastAtBySession = new Map();
|
|
1964
|
-
const shouldToast = (sessionID) => {
|
|
2324
|
+
const shouldToast = (sessionID, category = "general") => {
|
|
1965
2325
|
const now = Date.now();
|
|
1966
|
-
const
|
|
2326
|
+
const key = `${sessionID || "unknown"}:${category}`;
|
|
2327
|
+
const last = lastToastAtBySession.get(key) || 0;
|
|
1967
2328
|
if (now - last < 60000) {
|
|
1968
2329
|
return false;
|
|
1969
2330
|
}
|
|
1970
|
-
lastToastAtBySession.set(
|
|
2331
|
+
lastToastAtBySession.set(key, now);
|
|
1971
2332
|
return true;
|
|
1972
2333
|
};
|
|
1973
2334
|
|
|
2335
|
+
const warnSpoolPersistenceFailure = async (sessionID, reason) => {
|
|
2336
|
+
const message = reason === "spool_full"
|
|
2337
|
+
? "codemem raw-event retry spool is full; repair Codemem, then archive retained entries"
|
|
2338
|
+
: "codemem could not save a raw event for retry; it remains queued in memory";
|
|
2339
|
+
try {
|
|
2340
|
+
await client.app.log({
|
|
2341
|
+
service: "codemem",
|
|
2342
|
+
level: "error",
|
|
2343
|
+
message,
|
|
2344
|
+
extra: { category: "persistence", reason },
|
|
2345
|
+
});
|
|
2346
|
+
} catch {
|
|
2347
|
+
// Best-effort app logging only.
|
|
2348
|
+
}
|
|
2349
|
+
if (spoolPersistenceFailureNoted === reason) return;
|
|
2350
|
+
spoolPersistenceFailureNoted = reason;
|
|
2351
|
+
if (!client.tui?.showToast || !shouldToast(sessionID, `persistence:${reason}`)) return;
|
|
2352
|
+
try {
|
|
2353
|
+
await client.tui.showToast({
|
|
2354
|
+
body: { message: `codemem: ${message}`, variant: "error" },
|
|
2355
|
+
});
|
|
2356
|
+
} catch {
|
|
2357
|
+
// Best-effort toast only.
|
|
2358
|
+
}
|
|
2359
|
+
};
|
|
2360
|
+
|
|
2361
|
+
const persistRawEventForRetry = async ({ body, serialized, payload, sessionID }) => {
|
|
2362
|
+
try {
|
|
2363
|
+
await writeRawEventSpoolEntry({
|
|
2364
|
+
envelope: body,
|
|
2365
|
+
serialized,
|
|
2366
|
+
homeDir: rawEventSpoolHome,
|
|
2367
|
+
maxEntries: rawEventSpoolMaxEntries,
|
|
2368
|
+
});
|
|
2369
|
+
spoolPersistenceFailureNoted = null;
|
|
2370
|
+
if (payload && typeof payload === "object") {
|
|
2371
|
+
payload._raw_spooled = true;
|
|
2372
|
+
}
|
|
2373
|
+
return true;
|
|
2374
|
+
} catch (error) {
|
|
2375
|
+
const reason = error?.code === RAW_EVENT_SPOOL_FULL_CODE ? "spool_full" : "write_failed";
|
|
2376
|
+
await logLine(`raw_events.spool.${reason} category=persistence`);
|
|
2377
|
+
await warnSpoolPersistenceFailure(sessionID, reason);
|
|
2378
|
+
return false;
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
|
|
2382
|
+
const removeRawEventFromSpool = async ({ eventId, payload }) => {
|
|
2383
|
+
try {
|
|
2384
|
+
await removeRawEventSpoolEntry({ eventId, homeDir: rawEventSpoolHome });
|
|
2385
|
+
if (payload && typeof payload === "object") {
|
|
2386
|
+
payload._raw_spooled = false;
|
|
2387
|
+
}
|
|
2388
|
+
} catch {
|
|
2389
|
+
await logLine("raw_events.spool.cleanup_failed category=persistence");
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
|
|
2393
|
+
const notifyRawEventDelivery = async ({ category, delivered, durable, sessionID }) => {
|
|
2394
|
+
const action = RAW_EVENT_FAILURE_ACTIONS[category] || RAW_EVENT_FAILURE_ACTIONS.connection;
|
|
2395
|
+
let outcome = "was saved for retry";
|
|
2396
|
+
if (delivered) {
|
|
2397
|
+
outcome = "was queued via CLI";
|
|
2398
|
+
} else if (!durable) {
|
|
2399
|
+
outcome = "was not saved for retry; still queued in memory";
|
|
2400
|
+
}
|
|
2401
|
+
const message = `codemem raw event ${outcome}; ${action}`;
|
|
2402
|
+
let delivery = "memory";
|
|
2403
|
+
if (delivered) {
|
|
2404
|
+
delivery = "cli";
|
|
2405
|
+
} else if (durable) {
|
|
2406
|
+
delivery = "spool";
|
|
2407
|
+
}
|
|
2408
|
+
try {
|
|
2409
|
+
await client.app.log({
|
|
2410
|
+
service: "codemem",
|
|
2411
|
+
level: delivered ? "warn" : "error",
|
|
2412
|
+
message,
|
|
2413
|
+
extra: {
|
|
2414
|
+
category,
|
|
2415
|
+
delivery,
|
|
2416
|
+
},
|
|
2417
|
+
});
|
|
2418
|
+
} catch {
|
|
2419
|
+
// Best-effort app logging only.
|
|
2420
|
+
}
|
|
2421
|
+
if (!client.tui?.showToast || !shouldToast(sessionID, `${category}:${delivery}`)) return;
|
|
2422
|
+
try {
|
|
2423
|
+
await client.tui.showToast({
|
|
2424
|
+
body: { message: `codemem: ${message}`, variant: delivered ? "warning" : "error" },
|
|
2425
|
+
});
|
|
2426
|
+
} catch {
|
|
2427
|
+
// Best-effort toast only.
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
|
|
2431
|
+
const drainRawEventSpool = () => {
|
|
2432
|
+
if (!rawEventsEnabled) {
|
|
2433
|
+
return Promise.resolve();
|
|
2434
|
+
}
|
|
2435
|
+
const existingDrain = rawEventSpoolDrainsInFlight.get(rawEventSpoolDirectory);
|
|
2436
|
+
if (existingDrain) {
|
|
2437
|
+
return existingDrain;
|
|
2438
|
+
}
|
|
2439
|
+
const drainPromise = (async () => {
|
|
2440
|
+
let loaded;
|
|
2441
|
+
try {
|
|
2442
|
+
loaded = await loadRawEventSpoolEntries({
|
|
2443
|
+
homeDir: rawEventSpoolHome,
|
|
2444
|
+
limit: rawEventSpoolDrainLimit,
|
|
2445
|
+
});
|
|
2446
|
+
} catch {
|
|
2447
|
+
await logLine("raw_events.spool.load_failed category=persistence");
|
|
2448
|
+
const message = "codemem could not read saved raw events; spool entries were left untouched";
|
|
2449
|
+
try {
|
|
2450
|
+
await client.app.log({
|
|
2451
|
+
service: "codemem",
|
|
2452
|
+
level: "error",
|
|
2453
|
+
message,
|
|
2454
|
+
extra: { category: "persistence", delivery: "load" },
|
|
2455
|
+
});
|
|
2456
|
+
} catch {
|
|
2457
|
+
// Best-effort app logging only.
|
|
2458
|
+
}
|
|
2459
|
+
if (!spoolLoadFailureNoted) {
|
|
2460
|
+
spoolLoadFailureNoted = true;
|
|
2461
|
+
if (client.tui?.showToast && shouldToast(null, "persistence:load")) {
|
|
2462
|
+
try {
|
|
2463
|
+
await client.tui.showToast({
|
|
2464
|
+
body: { message: `codemem: ${message}`, variant: "error" },
|
|
2465
|
+
});
|
|
2466
|
+
} catch {
|
|
2467
|
+
// Best-effort toast only.
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
spoolLoadFailureNoted = false;
|
|
2474
|
+
if (loaded.corruptCount > 0) {
|
|
2475
|
+
await logLine(`raw_events.spool.corrupt_retained count=${loaded.corruptCount}`);
|
|
2476
|
+
}
|
|
2477
|
+
for (const entry of loaded.entries) {
|
|
2478
|
+
try {
|
|
2479
|
+
await queueRawEventViaCli(entry.envelope, entry.serialized);
|
|
2480
|
+
await removeRawEventFromSpool({ eventId: entry.eventId });
|
|
2481
|
+
} catch (error) {
|
|
2482
|
+
await logLine("raw_events.spool.drain_deferred category=fallback");
|
|
2483
|
+
if (error?.retryable === true) {
|
|
2484
|
+
break;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
})();
|
|
2489
|
+
const trackedDrain = drainPromise
|
|
2490
|
+
.catch(async () => {
|
|
2491
|
+
await logLine("raw_events.spool.drain_failed category=persistence");
|
|
2492
|
+
})
|
|
2493
|
+
.finally(() => {
|
|
2494
|
+
if (rawEventSpoolDrainsInFlight.get(rawEventSpoolDirectory) === trackedDrain) {
|
|
2495
|
+
rawEventSpoolDrainsInFlight.delete(rawEventSpoolDirectory);
|
|
2496
|
+
}
|
|
2497
|
+
});
|
|
2498
|
+
rawEventSpoolDrainsInFlight.set(rawEventSpoolDirectory, trackedDrain);
|
|
2499
|
+
return trackedDrain;
|
|
2500
|
+
};
|
|
2501
|
+
|
|
1974
2502
|
const buildViewerCliArgs = (action) => {
|
|
1975
2503
|
const args = ["serve", action, "--host", viewerHost, "--port", viewerPort];
|
|
1976
2504
|
if (String(viewerDbPath || "").trim()) {
|
|
@@ -1990,66 +2518,54 @@ export const CodememPlugin = async ({
|
|
|
1990
2518
|
return false;
|
|
1991
2519
|
}
|
|
1992
2520
|
const now = Date.now();
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2521
|
+
let cachedEnvelope = payload && typeof payload === "object"
|
|
2522
|
+
? rawEventEnvelopes.get(payload)
|
|
2523
|
+
: null;
|
|
2524
|
+
if (!cachedEnvelope) {
|
|
2525
|
+
const builtEnvelope = buildRawEventEnvelope({
|
|
2526
|
+
sessionID,
|
|
2527
|
+
type,
|
|
2528
|
+
payload,
|
|
2529
|
+
cwd,
|
|
2530
|
+
project: resolveProjectName(project, cwd),
|
|
2531
|
+
startedAt: sessionStartedAt,
|
|
2532
|
+
nowMs: now,
|
|
2533
|
+
nowMono:
|
|
2534
|
+
typeof performance !== "undefined" && performance.now
|
|
2535
|
+
? performance.now()
|
|
2536
|
+
: null,
|
|
2537
|
+
nextEventId,
|
|
2538
|
+
});
|
|
2539
|
+
const serialized = JSON.stringify(builtEnvelope);
|
|
2540
|
+
cachedEnvelope = { body: JSON.parse(serialized), serialized };
|
|
2541
|
+
if (payload && typeof payload === "object") {
|
|
2542
|
+
rawEventEnvelopes.set(payload, cachedEnvelope);
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
const { body, serialized } = cachedEnvelope;
|
|
2008
2546
|
if (now < streamUnavailableUntil) {
|
|
2547
|
+
const durable = await persistRawEventForRetry({ body, serialized, payload, sessionID });
|
|
2009
2548
|
try {
|
|
2010
|
-
await queueRawEventViaCli(body);
|
|
2011
|
-
|
|
2549
|
+
await queueRawEventViaCli(body, serialized);
|
|
2550
|
+
await removeRawEventFromSpool({ eventId: body.event_id, payload });
|
|
2012
2551
|
if (payload && typeof payload === "object") {
|
|
2013
2552
|
payload._raw_enqueued = true;
|
|
2014
2553
|
}
|
|
2554
|
+
await notifyRawEventDelivery({
|
|
2555
|
+
category: "connection",
|
|
2556
|
+
delivered: true,
|
|
2557
|
+
durable,
|
|
2558
|
+
sessionID,
|
|
2559
|
+
});
|
|
2015
2560
|
return true;
|
|
2016
|
-
} catch
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
);
|
|
2025
|
-
if (!fallbackFailureNoted) {
|
|
2026
|
-
fallbackFailureNoted = true;
|
|
2027
|
-
try {
|
|
2028
|
-
await client.app.log({
|
|
2029
|
-
service: "codemem",
|
|
2030
|
-
level: "error",
|
|
2031
|
-
message: "codemem fallback enqueue failed during stream backoff",
|
|
2032
|
-
extra: {
|
|
2033
|
-
sessionID,
|
|
2034
|
-
backoffMs: rawEventsBackoffMs,
|
|
2035
|
-
},
|
|
2036
|
-
});
|
|
2037
|
-
} catch (logErr) {
|
|
2038
|
-
// best-effort logging only
|
|
2039
|
-
}
|
|
2040
|
-
if (client.tui?.showToast && shouldToast(sessionID)) {
|
|
2041
|
-
try {
|
|
2042
|
-
await client.tui.showToast({
|
|
2043
|
-
body: {
|
|
2044
|
-
message: "codemem: fallback enqueue failed while stream is down",
|
|
2045
|
-
variant: "error",
|
|
2046
|
-
},
|
|
2047
|
-
});
|
|
2048
|
-
} catch (toastErr) {
|
|
2049
|
-
// best-effort only
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
}
|
|
2561
|
+
} catch {
|
|
2562
|
+
await logLine("raw_events.fallback.error category=connection");
|
|
2563
|
+
await notifyRawEventDelivery({
|
|
2564
|
+
category: "connection",
|
|
2565
|
+
delivered: false,
|
|
2566
|
+
durable,
|
|
2567
|
+
sessionID,
|
|
2568
|
+
});
|
|
2053
2569
|
return false;
|
|
2054
2570
|
}
|
|
2055
2571
|
}
|
|
@@ -2080,130 +2596,46 @@ export const CodememPlugin = async ({
|
|
|
2080
2596
|
});
|
|
2081
2597
|
if (!postResp.ok) {
|
|
2082
2598
|
let responseBody = null;
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
// Generic failure handling below remains the safe fallback.
|
|
2088
|
-
}
|
|
2599
|
+
try {
|
|
2600
|
+
responseBody = await postResp.json();
|
|
2601
|
+
} catch {
|
|
2602
|
+
// Generic connection guidance below remains the safe fallback.
|
|
2089
2603
|
}
|
|
2090
|
-
const targetMismatch = isViewerDbMismatchPayload(responseBody)
|
|
2091
|
-
|| isViewerIdentityMismatchPayload(responseBody)
|
|
2092
|
-
|| isViewerContractUnsupportedPayload(responseBody);
|
|
2093
2604
|
const postError = new Error(`raw-events post failed (${postResp.status})`);
|
|
2094
|
-
postError.
|
|
2605
|
+
postError.rawEventFailureCategory = classifyRawEventViewerFailure(responseBody);
|
|
2095
2606
|
throw postError;
|
|
2096
2607
|
}
|
|
2097
2608
|
streamUnavailableUntil = 0;
|
|
2098
|
-
streamErrorNoted = false;
|
|
2099
|
-
fallbackFailureNoted = false;
|
|
2100
2609
|
lastStatusAvailable = true;
|
|
2610
|
+
await removeRawEventFromSpool({ eventId: body.event_id, payload });
|
|
2101
2611
|
if (payload && typeof payload === "object") {
|
|
2102
2612
|
payload._raw_enqueued = true;
|
|
2103
2613
|
}
|
|
2104
2614
|
return true;
|
|
2105
2615
|
} catch (err) {
|
|
2616
|
+
const category = err?.rawEventFailureCategory || "connection";
|
|
2106
2617
|
streamUnavailableUntil = Date.now() + Math.max(1000, rawEventsBackoffMs);
|
|
2107
|
-
await logLine(`raw_events.error
|
|
2108
|
-
|
|
2109
|
-
await client.app.log({
|
|
2110
|
-
service: "codemem",
|
|
2111
|
-
level: "error",
|
|
2112
|
-
message: "Failed to stream raw events to codemem viewer",
|
|
2113
|
-
extra: {
|
|
2114
|
-
sessionID,
|
|
2115
|
-
type,
|
|
2116
|
-
viewerHost,
|
|
2117
|
-
viewerPort,
|
|
2118
|
-
viewerTargetMismatch: err?.viewerTargetMismatch === true,
|
|
2119
|
-
error: String(err),
|
|
2120
|
-
},
|
|
2121
|
-
});
|
|
2122
|
-
} catch (logErr) {
|
|
2123
|
-
// best-effort logging only
|
|
2124
|
-
}
|
|
2618
|
+
await logLine(`raw_events.error category=${category}`);
|
|
2619
|
+
const durable = await persistRawEventForRetry({ body, serialized, payload, sessionID });
|
|
2125
2620
|
|
|
2126
2621
|
let fallbackOk = false;
|
|
2127
2622
|
try {
|
|
2128
|
-
await queueRawEventViaCli(body);
|
|
2623
|
+
await queueRawEventViaCli(body, serialized);
|
|
2624
|
+
await removeRawEventFromSpool({ eventId: body.event_id, payload });
|
|
2129
2625
|
fallbackOk = true;
|
|
2130
|
-
} catch
|
|
2131
|
-
|
|
2132
|
-
payload._raw_fallback_terminal = fallbackErr?.retryable !== true;
|
|
2133
|
-
}
|
|
2134
|
-
await logLine(
|
|
2135
|
-
`raw_events.fallback.error sessionID=${sessionID} type=${type} err=${String(
|
|
2136
|
-
fallbackErr
|
|
2137
|
-
).slice(0, 200)}`
|
|
2138
|
-
);
|
|
2626
|
+
} catch {
|
|
2627
|
+
await logLine(`raw_events.fallback.error category=${category}`);
|
|
2139
2628
|
}
|
|
2140
2629
|
|
|
2141
2630
|
if (fallbackOk) {
|
|
2142
|
-
fallbackFailureNoted = false;
|
|
2143
2631
|
if (payload && typeof payload === "object") {
|
|
2144
2632
|
payload._raw_enqueued = true;
|
|
2145
2633
|
}
|
|
2146
|
-
|
|
2147
|
-
streamErrorNoted = true;
|
|
2148
|
-
try {
|
|
2149
|
-
await client.app.log({
|
|
2150
|
-
service: "codemem",
|
|
2151
|
-
level: "warn",
|
|
2152
|
-
message: "codemem stream unavailable; queued raw event via CLI fallback",
|
|
2153
|
-
extra: {
|
|
2154
|
-
sessionID,
|
|
2155
|
-
backoffMs: rawEventsBackoffMs,
|
|
2156
|
-
},
|
|
2157
|
-
});
|
|
2158
|
-
} catch (logErr) {
|
|
2159
|
-
// best-effort logging only
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
if (client.tui?.showToast && shouldToast(sessionID)) {
|
|
2163
|
-
try {
|
|
2164
|
-
await client.tui.showToast({
|
|
2165
|
-
body: {
|
|
2166
|
-
message: "codemem: viewer stream unavailable; queue fallback active",
|
|
2167
|
-
variant: "warning",
|
|
2168
|
-
},
|
|
2169
|
-
});
|
|
2170
|
-
} catch (toastErr) {
|
|
2171
|
-
// best-effort only
|
|
2172
|
-
}
|
|
2173
|
-
}
|
|
2634
|
+
await notifyRawEventDelivery({ category, delivered: true, durable, sessionID });
|
|
2174
2635
|
return true;
|
|
2175
2636
|
}
|
|
2176
2637
|
|
|
2177
|
-
|
|
2178
|
-
streamErrorNoted = true;
|
|
2179
|
-
fallbackFailureNoted = true;
|
|
2180
|
-
try {
|
|
2181
|
-
await client.app.log({
|
|
2182
|
-
service: "codemem",
|
|
2183
|
-
level: "error",
|
|
2184
|
-
message: "codemem stream unavailable; fallback enqueue failed",
|
|
2185
|
-
extra: {
|
|
2186
|
-
sessionID,
|
|
2187
|
-
backoffMs: rawEventsBackoffMs,
|
|
2188
|
-
},
|
|
2189
|
-
});
|
|
2190
|
-
} catch (logErr) {
|
|
2191
|
-
// best-effort logging only
|
|
2192
|
-
}
|
|
2193
|
-
}
|
|
2194
|
-
|
|
2195
|
-
if (client.tui?.showToast && shouldToast(sessionID)) {
|
|
2196
|
-
try {
|
|
2197
|
-
await client.tui.showToast({
|
|
2198
|
-
body: {
|
|
2199
|
-
message: `codemem: stream unavailable (${viewerHost}:${viewerPort}); fallback failed`,
|
|
2200
|
-
variant: "error",
|
|
2201
|
-
},
|
|
2202
|
-
});
|
|
2203
|
-
} catch (toastErr) {
|
|
2204
|
-
// best-effort only
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2638
|
+
await notifyRawEventDelivery({ category, delivered: false, durable, sessionID });
|
|
2207
2639
|
return false;
|
|
2208
2640
|
}
|
|
2209
2641
|
};
|
|
@@ -2709,7 +3141,7 @@ export const CodememPlugin = async ({
|
|
|
2709
3141
|
request_id: identity.requestId,
|
|
2710
3142
|
});
|
|
2711
3143
|
|
|
2712
|
-
const runPromptPackLedger = async (payload) => {
|
|
3144
|
+
const runPromptPackLedger = async (payload, { viewerOnly = false } = {}) => {
|
|
2713
3145
|
const viewerPayload = {
|
|
2714
3146
|
...payload,
|
|
2715
3147
|
db_path: promptPackDbPath,
|
|
@@ -2730,12 +3162,12 @@ export const CodememPlugin = async ({
|
|
|
2730
3162
|
};
|
|
2731
3163
|
}
|
|
2732
3164
|
|
|
2733
|
-
const { cause, retryable } = httpResult.classification;
|
|
3165
|
+
const { cause, kind, retryable } = httpResult.classification;
|
|
2734
3166
|
await logLine(
|
|
2735
3167
|
`inject.ledger.http_error cause=${JSON.stringify(redactLog(cause, 200))} retryable=${retryable}`
|
|
2736
3168
|
);
|
|
2737
|
-
if (!retryable) {
|
|
2738
|
-
return { exitCode: 1, stdout: "", stderr: cause, transport: "viewer" };
|
|
3169
|
+
if (!retryable || viewerOnly) {
|
|
3170
|
+
return { exitCode: 1, stdout: "", stderr: cause, transport: "viewer", failureKind: kind };
|
|
2739
3171
|
}
|
|
2740
3172
|
|
|
2741
3173
|
try {
|
|
@@ -2757,9 +3189,18 @@ export const CodememPlugin = async ({
|
|
|
2757
3189
|
queryHash: hashPromptPackQuery(""),
|
|
2758
3190
|
});
|
|
2759
3191
|
|
|
2760
|
-
const recordSkippedPromptPack = (failureCode, sessionID = null, surface = injectSurface) => {
|
|
3192
|
+
const recordSkippedPromptPack = (failureCode, sessionID = null, surface = injectSurface, requestKey = null) => {
|
|
2761
3193
|
const sessionKey = String(sessionID || "unknown");
|
|
2762
3194
|
const memoKey = `${surface}:${sessionKey}`;
|
|
3195
|
+
if (requestKey) {
|
|
3196
|
+
const signature = `${requestKey}:${failureCode}`;
|
|
3197
|
+
if (latestPolicySkips.get(memoKey) === signature) return null;
|
|
3198
|
+
latestPolicySkips.delete(memoKey);
|
|
3199
|
+
latestPolicySkips.set(memoKey, signature);
|
|
3200
|
+
while (latestPolicySkips.size > MAX_MESSAGE_INJECTION_CACHE_SESSIONS) {
|
|
3201
|
+
latestPolicySkips.delete(latestPolicySkips.keys().next().value);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
2763
3204
|
if (failureCode === "injection_disabled" && disabledInjectionRecorded.has(memoKey)) {
|
|
2764
3205
|
return null;
|
|
2765
3206
|
}
|
|
@@ -2768,7 +3209,7 @@ export const CodememPlugin = async ({
|
|
|
2768
3209
|
failureCode,
|
|
2769
3210
|
sessionID,
|
|
2770
3211
|
surface,
|
|
2771
|
-
failureCode === "injection_disabled" ? "once" : `event-${++skippedAttemptCounter}
|
|
3212
|
+
requestKey || (failureCode === "injection_disabled" ? "once" : `event-${++skippedAttemptCounter}`),
|
|
2772
3213
|
);
|
|
2773
3214
|
void runPromptPackLedger({
|
|
2774
3215
|
action: "record",
|
|
@@ -2797,12 +3238,28 @@ export const CodememPlugin = async ({
|
|
|
2797
3238
|
return { attemptId: identity.attemptId, ready };
|
|
2798
3239
|
};
|
|
2799
3240
|
|
|
2800
|
-
const confirmPromptPackDelivery = (attemptId, deliveryStatus = "handed_off") => {
|
|
3241
|
+
const confirmPromptPackDelivery = (attemptId, deliveryStatus = "handed_off", evaluation) => {
|
|
3242
|
+
const delivery = {
|
|
3243
|
+
action: "delivery", attempt_id: attemptId, delivery_status: deliveryStatus,
|
|
3244
|
+
};
|
|
2801
3245
|
void runPromptPackLedger({
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
3246
|
+
...delivery,
|
|
3247
|
+
...evaluation,
|
|
3248
|
+
}).then((result) => {
|
|
3249
|
+
// Retry additive fields only after the transport identifies that exact compatibility case.
|
|
3250
|
+
if (
|
|
3251
|
+
evaluation
|
|
3252
|
+
&& result?.transport === "viewer"
|
|
3253
|
+
&& result.failureKind === "viewer_contract_unsupported"
|
|
3254
|
+
) {
|
|
3255
|
+
return runPromptPackLedger(delivery, { viewerOnly: true });
|
|
3256
|
+
}
|
|
3257
|
+
// Older CLI fallbacks can reject optional fields. Preserve the original receipt;
|
|
3258
|
+
// never retry a Viewer policy/auth rejection through a different transport.
|
|
3259
|
+
if (evaluation && result?.transport === "cli" && result.exitCode !== 0) {
|
|
3260
|
+
return runPromptPackLedger(delivery);
|
|
3261
|
+
}
|
|
3262
|
+
}).catch(() => {});
|
|
2806
3263
|
};
|
|
2807
3264
|
|
|
2808
3265
|
const showToast = async (message, variant = "warning") => {
|
|
@@ -3126,11 +3583,44 @@ export const CodememPlugin = async ({
|
|
|
3126
3583
|
}
|
|
3127
3584
|
};
|
|
3128
3585
|
|
|
3586
|
+
let fallbackEvaluationSessionId = nextEventId();
|
|
3129
3587
|
const buildInjectedContext = async (query, context = {}) => {
|
|
3588
|
+
const requestPackBudget = reserveContextPrefixBudget(context.tokenBudget ?? injectTokenBudget);
|
|
3130
3589
|
const queryHash = hashPromptPackQuery(query);
|
|
3131
|
-
|
|
3590
|
+
// Requester eligibility comes only from the transform's own session identity.
|
|
3591
|
+
// activeSessionID tracks the latest event stream, which can belong to another
|
|
3592
|
+
// overlapping session; inferring from it would authorize the wrong summaries.
|
|
3593
|
+
const requesterHostSessionID = context.sessionID || null;
|
|
3594
|
+
const sessionID = requesterHostSessionID || "unknown";
|
|
3132
3595
|
const surface = context.surface || injectSurface;
|
|
3133
|
-
|
|
3596
|
+
let requestKey = context.requestKey || "unknown";
|
|
3597
|
+
if (context.fallbackTurn != null) {
|
|
3598
|
+
requestKey = JSON.stringify([requestKey, fallbackEvaluationSessionId, promptCounter, context.fallbackTurn]);
|
|
3599
|
+
}
|
|
3600
|
+
const evaluationFields = (
|
|
3601
|
+
beforeText,
|
|
3602
|
+
afterText,
|
|
3603
|
+
candidateItems,
|
|
3604
|
+
duplicatesOmitted,
|
|
3605
|
+
packMetadata,
|
|
3606
|
+
artifactFingerprint,
|
|
3607
|
+
) => ({
|
|
3608
|
+
evaluation_key: injectionDigest(JSON.stringify([
|
|
3609
|
+
sessionID,
|
|
3610
|
+
requestKey,
|
|
3611
|
+
surface,
|
|
3612
|
+
queryHash,
|
|
3613
|
+
artifactFingerprint,
|
|
3614
|
+
])),
|
|
3615
|
+
automatic_recall: {
|
|
3616
|
+
v: 1, candidateItems, duplicatesOmitted,
|
|
3617
|
+
beforeTokens: beforeText ? estimateTokens(wrapInjectedContext(beforeText)) : 0,
|
|
3618
|
+
afterTokens: afterText ? estimateTokens(wrapInjectedContext(afterText)) : 0,
|
|
3619
|
+
missingRetainedMetadata: context.retainedMetadata?.missingRetainedMetadata === true,
|
|
3620
|
+
invalidRetainedMetadata: context.retainedMetadata?.invalidRetainedMetadata === true,
|
|
3621
|
+
packMetadata,
|
|
3622
|
+
},
|
|
3623
|
+
});
|
|
3134
3624
|
const attemptKey = JSON.stringify([
|
|
3135
3625
|
sessionID,
|
|
3136
3626
|
requestKey,
|
|
@@ -3151,14 +3641,23 @@ export const CodememPlugin = async ({
|
|
|
3151
3641
|
let identity = resolveIdentity();
|
|
3152
3642
|
let metadata = attemptMetadata(
|
|
3153
3643
|
identity,
|
|
3154
|
-
|
|
3644
|
+
requesterHostSessionID,
|
|
3155
3645
|
);
|
|
3646
|
+
if (requestPackBudget === null) {
|
|
3647
|
+
return {
|
|
3648
|
+
text: "",
|
|
3649
|
+
attemptId: identity.attemptId,
|
|
3650
|
+
requestId: identity.requestId,
|
|
3651
|
+
queryHash,
|
|
3652
|
+
promptNumber: promptCounter,
|
|
3653
|
+
};
|
|
3654
|
+
}
|
|
3156
3655
|
const runPack = async () => {
|
|
3157
3656
|
let packArgs = buildPackArgs({
|
|
3158
3657
|
query,
|
|
3159
3658
|
filesModified: sessionContext.filesModified,
|
|
3160
3659
|
injectLimit,
|
|
3161
|
-
injectTokenBudget,
|
|
3660
|
+
injectTokenBudget: requestPackBudget,
|
|
3162
3661
|
internalLedger: true,
|
|
3163
3662
|
});
|
|
3164
3663
|
const httpResult = await postViewerJson({
|
|
@@ -3168,12 +3667,15 @@ export const CodememPlugin = async ({
|
|
|
3168
3667
|
query,
|
|
3169
3668
|
filesModified: sessionContext.filesModified,
|
|
3170
3669
|
injectLimit,
|
|
3171
|
-
injectTokenBudget,
|
|
3670
|
+
injectTokenBudget: requestPackBudget,
|
|
3172
3671
|
projectName: normalizeProjectLabel(process.env.CODEMEM_PROJECT),
|
|
3173
3672
|
cwd,
|
|
3174
3673
|
dbPath: promptPackDbPath,
|
|
3175
3674
|
identityTarget: promptPackIdentityTarget,
|
|
3176
3675
|
attempt: metadata,
|
|
3676
|
+
automaticContext: requesterHostSessionID
|
|
3677
|
+
? { source: "opencode", host_session_id: requesterHostSessionID }
|
|
3678
|
+
: null,
|
|
3177
3679
|
}),
|
|
3178
3680
|
validate: isValidPackHttpPayload,
|
|
3179
3681
|
});
|
|
@@ -3200,16 +3702,17 @@ export const CodememPlugin = async ({
|
|
|
3200
3702
|
};
|
|
3201
3703
|
}
|
|
3202
3704
|
|
|
3203
|
-
|
|
3705
|
+
const result = await runCli(packArgs, { stdinText: JSON.stringify(metadata) });
|
|
3204
3706
|
if (rejectsInternalLedgerFlag(result)) {
|
|
3205
|
-
|
|
3206
|
-
|
|
3707
|
+
// An older CLI cannot enforce requester-session continuity. Do not retry
|
|
3708
|
+
// through its generic pack path, which may inject an unrelated summary.
|
|
3709
|
+
await logLine("inject.pack.cli_legacy_unsafe_fallback_blocked");
|
|
3207
3710
|
}
|
|
3208
3711
|
return { packArgs, result: { ...result, transport: "cli" } };
|
|
3209
3712
|
};
|
|
3210
3713
|
let { packArgs, result } = await runPack();
|
|
3211
3714
|
let { packText, conflictPackText, metrics, itemCount, ledgerConflict } = parsePackOutput(result);
|
|
3212
|
-
let artifactFingerprint = packText
|
|
3715
|
+
let artifactFingerprint = packText || itemCount === 0
|
|
3213
3716
|
? promptPackArtifactFingerprint(result.stdout, packText)
|
|
3214
3717
|
: "";
|
|
3215
3718
|
let injectedIdentity = identity;
|
|
@@ -3236,14 +3739,11 @@ export const CodememPlugin = async ({
|
|
|
3236
3739
|
advancePromptPackRetryIdentity(attemptKey);
|
|
3237
3740
|
retryCount = promptPackRetryCounts.get(attemptKey) || 0;
|
|
3238
3741
|
identity = resolveIdentity();
|
|
3239
|
-
metadata = attemptMetadata(
|
|
3240
|
-
identity,
|
|
3241
|
-
context.sessionID || activeSessionID || null,
|
|
3242
|
-
);
|
|
3742
|
+
metadata = attemptMetadata(identity, requesterHostSessionID);
|
|
3243
3743
|
({ packArgs, result } = await runPack());
|
|
3244
3744
|
// Repair-conflict bytes are never preferred over the original preserved pack.
|
|
3245
3745
|
({ packText, metrics, itemCount, ledgerConflict } = parsePackOutput(result));
|
|
3246
|
-
artifactFingerprint = packText
|
|
3746
|
+
artifactFingerprint = packText || itemCount === 0
|
|
3247
3747
|
? promptPackArtifactFingerprint(result.stdout, packText)
|
|
3248
3748
|
: "";
|
|
3249
3749
|
const repairFailed = ledgerConflict
|
|
@@ -3322,13 +3822,10 @@ export const CodememPlugin = async ({
|
|
|
3322
3822
|
advancePromptPackRetryIdentity(attemptKey);
|
|
3323
3823
|
retryCount = promptPackRetryCounts.get(attemptKey) || 0;
|
|
3324
3824
|
identity = resolveIdentity();
|
|
3325
|
-
metadata = attemptMetadata(
|
|
3326
|
-
identity,
|
|
3327
|
-
context.sessionID || activeSessionID || null,
|
|
3328
|
-
);
|
|
3825
|
+
metadata = attemptMetadata(identity, requesterHostSessionID);
|
|
3329
3826
|
({ packArgs, result } = await runPack());
|
|
3330
3827
|
({ packText, metrics, itemCount, ledgerConflict } = parsePackOutput(result));
|
|
3331
|
-
artifactFingerprint = packText
|
|
3828
|
+
artifactFingerprint = packText || itemCount === 0
|
|
3332
3829
|
? promptPackArtifactFingerprint(result.stdout, packText)
|
|
3333
3830
|
: "";
|
|
3334
3831
|
|
|
@@ -3428,6 +3925,10 @@ export const CodememPlugin = async ({
|
|
|
3428
3925
|
}
|
|
3429
3926
|
if (!packText) {
|
|
3430
3927
|
if (itemCount === 0) {
|
|
3928
|
+
if (surface === "message") {
|
|
3929
|
+
void runPromptPackLedger({ action: "recall", attempt_id: identity.attemptId,
|
|
3930
|
+
...evaluationFields("", "", 0, 0, "valid", artifactFingerprint) }).catch(() => {});
|
|
3931
|
+
}
|
|
3431
3932
|
advancePromptPackRetryIdentity(attemptKey);
|
|
3432
3933
|
}
|
|
3433
3934
|
if (debug) {
|
|
@@ -3460,9 +3961,34 @@ export const CodememPlugin = async ({
|
|
|
3460
3961
|
artifactFingerprint || promptPackArtifactFingerprint(result.stdout, packText)
|
|
3461
3962
|
);
|
|
3462
3963
|
}
|
|
3964
|
+
const filtered = context.surface === "message"
|
|
3965
|
+
? filterRetainedPack(result.stdout, packText, context.retainedItems)
|
|
3966
|
+
: { text: packText, items: [], duplicates: 0 };
|
|
3967
|
+
const text = filtered.text ? wrapInjectedContext(filtered.text) : "";
|
|
3968
|
+
const evaluation = surface === "message"
|
|
3969
|
+
? evaluationFields(
|
|
3970
|
+
packText,
|
|
3971
|
+
filtered.text,
|
|
3972
|
+
itemCount,
|
|
3973
|
+
filtered.duplicates,
|
|
3974
|
+
filtered.packMetadata,
|
|
3975
|
+
artifactFingerprint,
|
|
3976
|
+
)
|
|
3977
|
+
: undefined;
|
|
3978
|
+
const recall = filtered.items.length
|
|
3979
|
+
? { v: 1, digest: injectionDigest(text), items: filtered.items, workingContext: context.workingContext }
|
|
3980
|
+
: undefined;
|
|
3981
|
+
let skipReason = null;
|
|
3982
|
+
if (!text && filtered.duplicates > 0) {
|
|
3983
|
+
skipReason = context.continuationOnly ? "continuation_only" : "unchanged_memories";
|
|
3984
|
+
}
|
|
3463
3985
|
if (metrics) {
|
|
3464
3986
|
return {
|
|
3465
|
-
text
|
|
3987
|
+
text,
|
|
3988
|
+
recall,
|
|
3989
|
+
duplicates: filtered.duplicates,
|
|
3990
|
+
evaluation,
|
|
3991
|
+
skipReason,
|
|
3466
3992
|
metrics,
|
|
3467
3993
|
attemptId: injectedIdentity?.attemptId || null,
|
|
3468
3994
|
requestId: injectedIdentity?.requestId || null,
|
|
@@ -3471,7 +3997,11 @@ export const CodememPlugin = async ({
|
|
|
3471
3997
|
};
|
|
3472
3998
|
}
|
|
3473
3999
|
return {
|
|
3474
|
-
text
|
|
4000
|
+
text,
|
|
4001
|
+
recall,
|
|
4002
|
+
duplicates: filtered.duplicates,
|
|
4003
|
+
evaluation,
|
|
4004
|
+
skipReason,
|
|
3475
4005
|
attemptId: injectedIdentity?.attemptId || null,
|
|
3476
4006
|
requestId: injectedIdentity?.requestId || null,
|
|
3477
4007
|
queryHash,
|
|
@@ -3575,9 +4105,17 @@ export const CodememPlugin = async ({
|
|
|
3575
4105
|
void logLine(`queue.pressure unsent_preserved queued=${queuedCount} max_events=${cap}`);
|
|
3576
4106
|
},
|
|
3577
4107
|
onForcedDrop: (dropped, queuedCount, hardCap) => {
|
|
4108
|
+
const durableSpoolCoverage = dropped?._raw_spooled === true;
|
|
3578
4109
|
void logLine(
|
|
3579
|
-
`queue.drop hard_cap
|
|
4110
|
+
`queue.drop hard_cap durable_spool_coverage=${durableSpoolCoverage} queued=${queuedCount} hard_max=${hardCap}`
|
|
3580
4111
|
);
|
|
4112
|
+
void log("error", "codemem raw event dropped at in-memory hard cap", {
|
|
4113
|
+
category: "queue_capacity",
|
|
4114
|
+
delivery: durableSpoolCoverage ? "spool" : "memory",
|
|
4115
|
+
durable_spool_coverage: durableSpoolCoverage,
|
|
4116
|
+
queued_count: queuedCount,
|
|
4117
|
+
hard_max_events: hardCap,
|
|
4118
|
+
});
|
|
3581
4119
|
},
|
|
3582
4120
|
});
|
|
3583
4121
|
};
|
|
@@ -3607,7 +4145,6 @@ export const CodememPlugin = async ({
|
|
|
3607
4145
|
...adapterAnnotatedEvent,
|
|
3608
4146
|
_raw_event_id: rawEventId,
|
|
3609
4147
|
_raw_session_id: resolvedSessionID,
|
|
3610
|
-
_raw_retry_count: 0,
|
|
3611
4148
|
};
|
|
3612
4149
|
recordEvent(queuedEvent);
|
|
3613
4150
|
void emitRawEvent({
|
|
@@ -3619,6 +4156,7 @@ export const CodememPlugin = async ({
|
|
|
3619
4156
|
|
|
3620
4157
|
const flushEvents = async () => {
|
|
3621
4158
|
if (!events.length) {
|
|
4159
|
+
await drainRawEventSpool();
|
|
3622
4160
|
await logLine("flush.skip empty");
|
|
3623
4161
|
return;
|
|
3624
4162
|
}
|
|
@@ -3630,18 +4168,10 @@ export const CodememPlugin = async ({
|
|
|
3630
4168
|
}
|
|
3631
4169
|
|
|
3632
4170
|
const failed = [];
|
|
3633
|
-
let droppedCount = 0;
|
|
3634
4171
|
for (const queuedEvent of batch) {
|
|
3635
4172
|
if (queuedEvent && typeof queuedEvent === "object" && queuedEvent._raw_enqueued) {
|
|
3636
4173
|
continue;
|
|
3637
4174
|
}
|
|
3638
|
-
if (queuedEvent && typeof queuedEvent === "object" && queuedEvent._raw_fallback_terminal) {
|
|
3639
|
-
droppedCount += 1;
|
|
3640
|
-
await logLine(
|
|
3641
|
-
`flush.drop terminal_fallback event_id=${queuedEvent?._raw_event_id || "unknown"}`
|
|
3642
|
-
);
|
|
3643
|
-
continue;
|
|
3644
|
-
}
|
|
3645
4175
|
const queuedSessionID =
|
|
3646
4176
|
queuedEvent?._raw_session_id ||
|
|
3647
4177
|
queuedEvent?.properties?.sessionID ||
|
|
@@ -3652,27 +4182,13 @@ export const CodememPlugin = async ({
|
|
|
3652
4182
|
payload: queuedEvent,
|
|
3653
4183
|
});
|
|
3654
4184
|
if (!ok) {
|
|
3655
|
-
|
|
3656
|
-
droppedCount += 1;
|
|
3657
|
-
await logLine(
|
|
3658
|
-
`flush.drop terminal_fallback event_id=${queuedEvent?._raw_event_id || "unknown"}`
|
|
3659
|
-
);
|
|
3660
|
-
continue;
|
|
3661
|
-
}
|
|
3662
|
-
const currentRetry =
|
|
3663
|
-
typeof queuedEvent?._raw_retry_count === "number" && Number.isFinite(queuedEvent._raw_retry_count)
|
|
3664
|
-
? queuedEvent._raw_retry_count
|
|
3665
|
-
: 0;
|
|
3666
|
-
const nextRetry = currentRetry + 1;
|
|
3667
|
-
failed.push({
|
|
3668
|
-
...queuedEvent,
|
|
3669
|
-
_raw_retry_count: nextRetry,
|
|
3670
|
-
});
|
|
4185
|
+
failed.push(queuedEvent);
|
|
3671
4186
|
}
|
|
3672
4187
|
}
|
|
3673
4188
|
if (failed.length) {
|
|
3674
4189
|
events.unshift(...failed);
|
|
3675
4190
|
await logLine(`flush.retry_deferred count=${failed.length}`);
|
|
4191
|
+
await drainRawEventSpool();
|
|
3676
4192
|
return;
|
|
3677
4193
|
}
|
|
3678
4194
|
|
|
@@ -3683,12 +4199,20 @@ export const CodememPlugin = async ({
|
|
|
3683
4199
|
await logLine(
|
|
3684
4200
|
`flush.stream_only finalize count=${batch.length} tools=${sessionContext.toolCount} prompts=${sessionContext.promptCount} duration=${Math.round(durationMs / 1000)}s`
|
|
3685
4201
|
);
|
|
3686
|
-
await logLine(`flush.ok count=${batch.length
|
|
4202
|
+
await logLine(`flush.ok count=${batch.length} dropped=0`);
|
|
3687
4203
|
sessionStartedAt = null;
|
|
3688
4204
|
resetSessionContext();
|
|
4205
|
+
await drainRawEventSpool();
|
|
3689
4206
|
};
|
|
3690
4207
|
|
|
4208
|
+
void drainRawEventSpool();
|
|
4209
|
+
|
|
3691
4210
|
return {
|
|
4211
|
+
dispose: () => {
|
|
4212
|
+
clearTimeout(updateCheckTimer);
|
|
4213
|
+
stopHealthCheck();
|
|
4214
|
+
releasePluginRegistration();
|
|
4215
|
+
},
|
|
3692
4216
|
"experimental.session.compacting": async (input) => {
|
|
3693
4217
|
const sessionID = extractHookSessionID(input) || activeSessionID;
|
|
3694
4218
|
markCompactionInjectionSkip(compactionInjectionSkips, sessionID);
|
|
@@ -3702,10 +4226,12 @@ export const CodememPlugin = async ({
|
|
|
3702
4226
|
if (injectSurface === "system") {
|
|
3703
4227
|
return;
|
|
3704
4228
|
}
|
|
3705
|
-
const
|
|
3706
|
-
?
|
|
3707
|
-
:
|
|
3708
|
-
const
|
|
4229
|
+
const hookMessages = Array.isArray(output?.messages)
|
|
4230
|
+
? scopeMessagesToSession(output.messages, input?.sessionID || null)
|
|
4231
|
+
: [];
|
|
4232
|
+
const latestUser = findLatestUserMessage(hookMessages);
|
|
4233
|
+
const sessionID = input?.sessionID
|
|
4234
|
+
|| (latestUser ? resolveEntrySessionID(latestUser.entry) : null);
|
|
3709
4235
|
const latestPromptText = latestUser ? extractMessageText(latestUser.entry) : "";
|
|
3710
4236
|
if (debug) {
|
|
3711
4237
|
const query = resolveInjectQuery({ lastPromptText: latestPromptText });
|
|
@@ -3738,10 +4264,16 @@ export const CodememPlugin = async ({
|
|
|
3738
4264
|
resolveInjectQuery,
|
|
3739
4265
|
buildInjectedContext,
|
|
3740
4266
|
messageInjectionCache,
|
|
4267
|
+
injectTokenBudget,
|
|
4268
|
+
retainedTokenBudget,
|
|
4269
|
+
workingSet: [...sessionContext.filesModified, ...sessionContext.filesRead],
|
|
4270
|
+
recordMeasurement: (measurement) => logLine(`inject.recall ${JSON.stringify(measurement)}`),
|
|
3741
4271
|
compactionInjectionSkips,
|
|
3742
4272
|
confirmDelivery: confirmPromptPackDelivery,
|
|
3743
4273
|
recordCacheReuse: recordCachedPromptPack,
|
|
3744
|
-
recordSkipped: recordSkippedPromptPack
|
|
4274
|
+
recordSkipped: (reason, sessionID) => recordSkippedPromptPack(
|
|
4275
|
+
reason, sessionID, "message", latestUser ? resolveEntryMessageId(latestUser.entry) : null,
|
|
4276
|
+
),
|
|
3745
4277
|
});
|
|
3746
4278
|
} catch (err) {
|
|
3747
4279
|
await logLine(
|
|
@@ -3993,6 +4525,7 @@ export const CodememPlugin = async ({
|
|
|
3993
4525
|
activeSessionID = sessionID || null;
|
|
3994
4526
|
sessionStartedAt = new Date().toISOString();
|
|
3995
4527
|
promptCounter = 0;
|
|
4528
|
+
fallbackEvaluationSessionId = nextEventId();
|
|
3996
4529
|
skippedAttemptCounter = 0;
|
|
3997
4530
|
disabledInjectionRecorded.delete("message:unknown");
|
|
3998
4531
|
disabledInjectionRecorded.delete("system:unknown");
|
|
@@ -4004,11 +4537,18 @@ export const CodememPlugin = async ({
|
|
|
4004
4537
|
if (eventType === "session.deleted") {
|
|
4005
4538
|
activeSessionID = null;
|
|
4006
4539
|
if (sessionID) {
|
|
4540
|
+
for (const key of lastToastAtBySession.keys()) {
|
|
4541
|
+
if (key.startsWith(`${sessionID}:`)) {
|
|
4542
|
+
lastToastAtBySession.delete(key);
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4007
4545
|
injectionToastShown.delete(sessionID);
|
|
4008
4546
|
messageInjectionCache.delete(sessionID);
|
|
4009
4547
|
compactionInjectionSkips.delete(sessionID);
|
|
4010
4548
|
disabledInjectionRecorded.delete(`message:${sessionID}`);
|
|
4011
4549
|
disabledInjectionRecorded.delete(`system:${sessionID}`);
|
|
4550
|
+
latestPolicySkips.delete(`message:${sessionID}`);
|
|
4551
|
+
latestPolicySkips.delete(`system:${sessionID}`);
|
|
4012
4552
|
}
|
|
4013
4553
|
await stopViewer();
|
|
4014
4554
|
}
|
|
@@ -4130,6 +4670,8 @@ export const __testUtils = {
|
|
|
4130
4670
|
redactPackCommand,
|
|
4131
4671
|
rejectsInternalLedgerFlag,
|
|
4132
4672
|
classifyFallbackCommandResult,
|
|
4673
|
+
classifyRawEventViewerFailure,
|
|
4674
|
+
detectRunner,
|
|
4133
4675
|
PROMPT_TRANSPORT_PROTOCOL_RANGE,
|
|
4134
4676
|
normalizePromptTransportProtocolRange,
|
|
4135
4677
|
arePromptTransportProtocolRangesCompatible,
|
|
@@ -4159,5 +4701,18 @@ export const __testUtils = {
|
|
|
4159
4701
|
buildRawEventEnvelope,
|
|
4160
4702
|
trimEventQueue,
|
|
4161
4703
|
parsePositiveInt,
|
|
4704
|
+
resolveInjectTokenBudget,
|
|
4705
|
+
reserveContextPrefixBudget,
|
|
4706
|
+
wrapInjectedContext,
|
|
4707
|
+
estimateTokens,
|
|
4708
|
+
countRetainedInjectionTokens,
|
|
4709
|
+
retainedMemoryFingerprints,
|
|
4710
|
+
retainedMetadataGaps,
|
|
4711
|
+
filterRetainedPack,
|
|
4712
|
+
isContinuationOnly,
|
|
4713
|
+
resolveRetainedTokenBudget,
|
|
4714
|
+
workingContextDigest,
|
|
4715
|
+
DEFAULT_INJECT_TOKEN_BUDGET,
|
|
4716
|
+
CODEMEM_CONTEXT_PREFIX,
|
|
4162
4717
|
createViewerHealthMonitor,
|
|
4163
4718
|
};
|