@codemem/opencode-plugin 0.44.0-beta.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.
@@ -25,7 +25,7 @@ import {
25
25
 
26
26
  const TRUTHY_VALUES = ["1", "true", "yes"];
27
27
  const DISABLED_VALUES = ["0", "false", "off"];
28
- const PINNED_BACKEND_VERSION = "0.44.0-beta.1";
28
+ const PINNED_BACKEND_VERSION = "0.44.0";
29
29
  const COMPAT_CHECK_DELAY_MS = 1500;
30
30
  const COMPAT_CHECK_CACHE_TTL_MS = 5 * 60 * 1000;
31
31
  const MAX_UPDATE_STATUS_BYTES = 16 * 1024;
@@ -35,7 +35,6 @@ const RELEASE_VERSION =
35
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-]+)*)?$/;
36
36
  const CODEMEM_CONTEXT_PART_ID_PREFIX = "codemem-context-";
37
37
  const MAX_MESSAGE_INJECTION_CACHE_SESSIONS = 20;
38
- const MAX_MESSAGE_INJECTION_CACHE_MESSAGES = 100;
39
38
  const COMPACTION_INJECTION_SKIP_TTL_MS = 30 * 1000;
40
39
  const MAX_WORKING_SET_PATH_CHARS = 400;
41
40
  const VIEWER_HEALTH_CHECK_INTERVAL_MS = 60_000;
@@ -43,6 +42,9 @@ const VIEWER_HEALTH_TIMEOUT_MS = 5_000;
43
42
  const VIEWER_HEALTH_RESTART_THRESHOLD = 3;
44
43
  const VIEWER_HEALTH_RESTART_COOLDOWN_MS = 5 * 60_000;
45
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";
46
48
  const DEFAULT_EMBEDDING_MODEL = "Xenova/bge-small-en-v1.5";
47
49
  const DEFAULT_EMBEDDING_REVISION = "ea104dacec62c0de699686887e3f920caeb4f3e3";
48
50
  const PLUGIN_REGISTRATIONS_KEY = Symbol.for("codemem.opencode-plugin.registrations");
@@ -789,7 +791,7 @@ const parsePackOutput = (result) => {
789
791
  ? Number(metrics.total_items)
790
792
  : null;
791
793
  const candidatePackText = succeeded && itemCount !== 0
792
- ? String(payload?.pack_text || "").trim()
794
+ ? String(payload?.pack_text || "")
793
795
  : "";
794
796
  return {
795
797
  conflictPackText: ledgerConflict ? candidatePackText : "",
@@ -836,6 +838,7 @@ const isValidLedgerFailureHttpPayload = (payload) =>
836
838
  && (
837
839
  payload.errorCode === "retrieval_ledger_write_failed"
838
840
  || payload.errorCode === "retrieval_ledger_delivery_write_failed"
841
+ || payload.errorCode === "automatic_recall_write_failed"
839
842
  )
840
843
  && typeof payload.reason === "string";
841
844
 
@@ -927,9 +930,10 @@ const isViewerPolicyOrAuthFailurePayload = (payload) => {
927
930
  ].includes(code);
928
931
  };
929
932
 
930
- const viewerFailureClassification = (cause, disposition) => ({
933
+ const viewerFailureClassification = (cause, disposition, kind = null) => ({
931
934
  cause,
932
935
  disposition,
936
+ kind,
933
937
  retryable: disposition !== "terminal",
934
938
  });
935
939
 
@@ -966,6 +970,7 @@ const classifyViewerHttpFailure = ({
966
970
  kind: "viewer_contract_unsupported",
967
971
  compatibleProfile,
968
972
  }),
973
+ "viewer_contract_unsupported",
969
974
  );
970
975
  }
971
976
  if (operation === "prompt-pack-ledger" && isValidLedgerFailureHttpPayload(body)) {
@@ -1018,6 +1023,7 @@ const buildPackHttpBody = ({
1018
1023
  dbPath,
1019
1024
  identityTarget,
1020
1025
  attempt,
1026
+ automaticContext,
1021
1027
  }) => ({
1022
1028
  context: query,
1023
1029
  limit: injectLimit !== null && Number.isFinite(injectLimit) && injectLimit > 0
@@ -1038,6 +1044,7 @@ const buildPackHttpBody = ({
1038
1044
  .map((value) => String(value || "").trim())
1039
1045
  .filter((value) => value && value.length <= MAX_WORKING_SET_PATH_CHARS),
1040
1046
  attempt,
1047
+ ...(automaticContext !== undefined ? { automatic_context: automaticContext } : {}),
1041
1048
  });
1042
1049
 
1043
1050
  const canonicalJson = (value) => {
@@ -1226,16 +1233,11 @@ const getSessionMessageInjectionCache = (messageInjectionCache, sessionID) => {
1226
1233
  return sessionCache;
1227
1234
  };
1228
1235
 
1229
- const trimSessionMessageInjectionCache = (sessionCache) => {
1230
- if (!sessionCache) return;
1231
- while (sessionCache.size > MAX_MESSAGE_INJECTION_CACHE_MESSAGES) {
1232
- const oldestKey = sessionCache.keys().next().value;
1233
- if (!oldestKey) break;
1234
- 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);
1235
1240
  }
1236
- };
1237
-
1238
- const normalizeInjectedMessageParts = (messages, sessionCache) => {
1239
1241
  for (let index = 0; index < messages.length; index += 1) {
1240
1242
  const entry = messages[index];
1241
1243
  if (!isUserMessageEntry(entry) || !Array.isArray(entry.parts)) {
@@ -1243,24 +1245,19 @@ const normalizeInjectedMessageParts = (messages, sessionCache) => {
1243
1245
  }
1244
1246
 
1245
1247
  const messageId = resolveEntryMessageId(entry);
1246
- const retainedParts = [];
1247
- let existingText = null;
1248
- for (const part of entry.parts) {
1249
- if (isCodememContextPart(part)) {
1250
- const text = String(part?.text || "");
1251
- if (text.trim()) {
1252
- existingText = text;
1253
- }
1254
- } else {
1255
- retainedParts.push(part);
1256
- }
1257
- }
1258
- if (existingText && sessionCache && messageId && !sessionCache.has(messageId)) {
1259
- sessionCache.set(messageId, { text: existingText, attemptId: null, reconstructed: true });
1260
- trimSessionMessageInjectionCache(sessionCache);
1261
- }
1262
- if (retainedParts.length !== entry.parts.length) {
1263
- 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
+ });
1264
1261
  }
1265
1262
  }
1266
1263
  };
@@ -1275,6 +1272,7 @@ const buildInjectedContextPart = (entry, index, text, options = {}) => {
1275
1272
  type: "text",
1276
1273
  text,
1277
1274
  synthetic: true,
1275
+ ...(options.recall ? { metadata: { codemem: options.recall } } : {}),
1278
1276
  };
1279
1277
  };
1280
1278
 
@@ -1315,7 +1313,10 @@ const appendCachedInjectedContextParts = async (
1315
1313
  cacheReuseReady = typeof reuse === "object" ? reuse?.ready : null;
1316
1314
  }
1317
1315
  try {
1318
- entry.parts.push(buildInjectedContextPart(entry, index, text, { messageId, sessionID }));
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
+ }
1319
1320
  } catch (error) {
1320
1321
  if (messageId === options.latestMessageId && attemptId && options.confirmDelivery) {
1321
1322
  if (cacheReuseReady) {
@@ -1342,6 +1343,11 @@ const appendCachedInjectedContextParts = async (
1342
1343
  return appended;
1343
1344
  };
1344
1345
 
1346
+ const scopeMessagesToSession = (messages, sessionID) => messages.filter((entry) => {
1347
+ const entrySession = resolveEntrySessionID(entry);
1348
+ return !entrySession || !sessionID || entrySession === sessionID;
1349
+ });
1350
+
1345
1351
  const applyInjectedContextToMessages = async ({
1346
1352
  injectEnabled,
1347
1353
  input,
@@ -1355,40 +1361,82 @@ const applyInjectedContextToMessages = async ({
1355
1361
  confirmDelivery,
1356
1362
  recordCacheReuse,
1357
1363
  recordSkipped,
1364
+ injectTokenBudget = DEFAULT_INJECT_TOKEN_BUDGET,
1365
+ retainedTokenBudget = DEFAULT_RETAINED_TOKEN_BUDGET,
1366
+ workingSet = [],
1367
+ recordMeasurement,
1358
1368
  }) => {
1359
1369
  const hasMessages = Array.isArray(output?.messages);
1360
- const latestUser = hasMessages ? findLatestUserMessage(output.messages) : null;
1361
- const sessionID = latestUser
1362
- ? resolveEntrySessionID(latestUser.entry) || input?.sessionID || null
1363
- : input?.sessionID || null;
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
+ };
1364
1385
  if (!injectEnabled) {
1365
1386
  recordSkipped?.("injection_disabled", sessionID);
1387
+ report("injection_disabled");
1366
1388
  return false;
1367
1389
  }
1368
1390
  if (!hasMessages) {
1391
+ report("missing_history");
1369
1392
  return false;
1370
1393
  }
1371
-
1372
1394
  if (consumeCompactionInjectionSkip(compactionInjectionSkips, sessionID)) {
1373
- normalizeInjectedMessageParts(output.messages, null);
1395
+ const cache = getSessionMessageInjectionCache(messageInjectionCache, sessionID);
1396
+ normalizeInjectedMessageParts(messages, cache, { pruneAbsent: false });
1374
1397
  recordSkipped?.("compaction_skipped", sessionID);
1398
+ report("compaction_skipped");
1375
1399
  return false;
1376
1400
  }
1377
1401
 
1378
1402
  if (!latestUser) {
1403
+ report("missing_user");
1379
1404
  return false;
1380
1405
  }
1381
1406
 
1382
1407
  const sessionCache = getSessionMessageInjectionCache(messageInjectionCache, sessionID);
1383
- normalizeInjectedMessageParts(output.messages, sessionCache);
1408
+ normalizeInjectedMessageParts(messages, sessionCache);
1384
1409
 
1385
1410
  const latestMessageId = resolveEntryMessageId(latestUser.entry);
1386
1411
  const canReplay = Boolean(sessionCache && latestMessageId);
1387
1412
  const latestCached = canReplay ? sessionCache.get(latestMessageId) : null;
1388
- const latestWasCached = Boolean(latestCached) && latestCached?.reconstructed !== true;
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));
1389
1426
  let latestWasBuilt = false;
1427
+ let newlyDelivered = 0;
1428
+ let duplicates = 0;
1429
+ let reason = latestWasCached ? "replay" : "no_context";
1390
1430
  if (!latestWasCached) {
1391
- const firstUser = findFirstUserMessage(output.messages);
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);
1392
1440
  const query = resolveInjectQuery({
1393
1441
  firstPrompt: firstUser ? extractMessageText(firstUser.entry) : null,
1394
1442
  lastPromptText: extractMessageText(latestUser.entry),
@@ -1396,36 +1444,62 @@ const applyInjectedContextToMessages = async ({
1396
1444
  const injected = await buildInjectedContext(query, {
1397
1445
  sessionID,
1398
1446
  requestKey: latestMessageId || fallbackEntryMessageId(latestUser.entry, latestUser.index),
1447
+ fallbackTurn: sessionID && latestMessageId ? null : latestUser.index,
1399
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),
1400
1454
  });
1401
- if (injected?.text) {
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) {
1402
1475
  latestWasBuilt = true;
1403
- if (canReplay) {
1404
- sessionCache.set(latestMessageId, {
1405
- text: injected.text,
1406
- attemptId: injected.attemptId || null,
1407
- requestId: injected.requestId || null,
1408
- queryHash: injected.queryHash || null,
1409
- promptNumber: injected.promptNumber || 0,
1410
- reuseCount: 0,
1411
- });
1412
- trimSessionMessageInjectionCache(sessionCache);
1413
- } else if (Array.isArray(latestUser.entry.parts)) {
1476
+ if (Array.isArray(latestUser.entry.parts)) {
1414
1477
  try {
1415
1478
  latestUser.entry.parts.push(
1416
1479
  buildInjectedContextPart(latestUser.entry, latestUser.index, injected.text, {
1417
1480
  messageId: latestMessageId || undefined,
1418
1481
  sessionID: sessionID || undefined,
1482
+ recall: injected.recall,
1419
1483
  })
1420
1484
  );
1421
1485
  } catch (error) {
1422
1486
  if (injected.attemptId) {
1423
- confirmDelivery?.(injected.attemptId, "failed");
1487
+ confirmDelivery?.(injected.attemptId, "failed", injected.evaluation);
1424
1488
  }
1489
+ report("delivery_failed", 0, duplicates);
1425
1490
  throw error;
1426
1491
  }
1492
+ newlyDelivered = estimateTokens(injected.text);
1493
+ reason = "delivered";
1427
1494
  if (injected.attemptId) {
1428
- 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
+ });
1429
1503
  }
1430
1504
  }
1431
1505
 
@@ -1438,10 +1512,15 @@ const applyInjectedContextToMessages = async ({
1438
1512
  // best-effort only
1439
1513
  }
1440
1514
  }
1441
- } else if (canReplay && latestCached?.reconstructed === true) {
1442
- sessionCache.delete(latestMessageId);
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);
1443
1521
  }
1444
1522
  }
1523
+ report(reason, newlyDelivered, duplicates);
1445
1524
 
1446
1525
  if (!canReplay) {
1447
1526
  return Boolean(
@@ -1450,13 +1529,136 @@ const applyInjectedContextToMessages = async ({
1450
1529
  );
1451
1530
  }
1452
1531
 
1453
- const appended = await appendCachedInjectedContextParts(output.messages, sessionCache, sessionID, {
1454
- latestMessageId,
1455
- latestWasBuilt,
1456
- confirmDelivery,
1457
- recordCacheReuse,
1458
- });
1459
- return appended > 0;
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 };
1460
1662
  };
1461
1663
 
1462
1664
  const mapOpencodeEventTypeToAdapterType = (eventType) => {
@@ -1767,6 +1969,25 @@ const parsePositiveInt = (value, fallback) => {
1767
1969
  return parsed;
1768
1970
  };
1769
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
+
1770
1991
  export const buildInjectionToastMessage = (metrics) => {
1771
1992
  const items = asFiniteNonNegativeInt(metrics?.items) ?? asFiniteNonNegativeInt(metrics?.total_items);
1772
1993
  const packTokens = asFiniteNonNegativeInt(metrics?.pack_tokens);
@@ -1982,15 +2203,30 @@ export const CodememPlugin = async ({
1982
2203
  process.env.CODEMEM_INJECT_CONTEXT || "1"
1983
2204
  );
1984
2205
  const injectSurface = resolveInjectSurface(process.env.CODEMEM_INJECT_SURFACE);
1985
- // Only use env overrides if explicitly set; otherwise CLI uses config defaults
2206
+ // The item limit remains optional so the CLI can use its configured default.
1986
2207
  const injectLimitEnv = process.env.CODEMEM_INJECT_LIMIT;
1987
2208
  const injectLimit = injectLimitEnv ? parseNumber(injectLimitEnv, null) : null;
1988
- const injectTokenBudgetEnv = process.env.CODEMEM_INJECT_TOKEN_BUDGET;
1989
- const injectTokenBudget = injectTokenBudgetEnv ? parseNumber(injectTokenBudgetEnv, null) : null;
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
+ }
1990
2225
  const injectionToastShown = new Set();
1991
2226
  const messageInjectionCache = new Map();
1992
2227
  const compactionInjectionSkips = new Map();
1993
2228
  const disabledInjectionRecorded = new Set();
2229
+ const latestPolicySkips = new Map();
1994
2230
  const attemptStartedAt = new Map();
1995
2231
  const promptPackRetryCounts = new Map();
1996
2232
  const successfulPromptPackArtifacts = new Map();
@@ -2905,7 +3141,7 @@ export const CodememPlugin = async ({
2905
3141
  request_id: identity.requestId,
2906
3142
  });
2907
3143
 
2908
- const runPromptPackLedger = async (payload) => {
3144
+ const runPromptPackLedger = async (payload, { viewerOnly = false } = {}) => {
2909
3145
  const viewerPayload = {
2910
3146
  ...payload,
2911
3147
  db_path: promptPackDbPath,
@@ -2926,12 +3162,12 @@ export const CodememPlugin = async ({
2926
3162
  };
2927
3163
  }
2928
3164
 
2929
- const { cause, retryable } = httpResult.classification;
3165
+ const { cause, kind, retryable } = httpResult.classification;
2930
3166
  await logLine(
2931
3167
  `inject.ledger.http_error cause=${JSON.stringify(redactLog(cause, 200))} retryable=${retryable}`
2932
3168
  );
2933
- if (!retryable) {
2934
- return { exitCode: 1, stdout: "", stderr: cause, transport: "viewer" };
3169
+ if (!retryable || viewerOnly) {
3170
+ return { exitCode: 1, stdout: "", stderr: cause, transport: "viewer", failureKind: kind };
2935
3171
  }
2936
3172
 
2937
3173
  try {
@@ -2953,9 +3189,18 @@ export const CodememPlugin = async ({
2953
3189
  queryHash: hashPromptPackQuery(""),
2954
3190
  });
2955
3191
 
2956
- const recordSkippedPromptPack = (failureCode, sessionID = null, surface = injectSurface) => {
3192
+ const recordSkippedPromptPack = (failureCode, sessionID = null, surface = injectSurface, requestKey = null) => {
2957
3193
  const sessionKey = String(sessionID || "unknown");
2958
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
+ }
2959
3204
  if (failureCode === "injection_disabled" && disabledInjectionRecorded.has(memoKey)) {
2960
3205
  return null;
2961
3206
  }
@@ -2964,7 +3209,7 @@ export const CodememPlugin = async ({
2964
3209
  failureCode,
2965
3210
  sessionID,
2966
3211
  surface,
2967
- failureCode === "injection_disabled" ? "once" : `event-${++skippedAttemptCounter}`,
3212
+ requestKey || (failureCode === "injection_disabled" ? "once" : `event-${++skippedAttemptCounter}`),
2968
3213
  );
2969
3214
  void runPromptPackLedger({
2970
3215
  action: "record",
@@ -2993,12 +3238,28 @@ export const CodememPlugin = async ({
2993
3238
  return { attemptId: identity.attemptId, ready };
2994
3239
  };
2995
3240
 
2996
- 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
+ };
2997
3245
  void runPromptPackLedger({
2998
- action: "delivery",
2999
- attempt_id: attemptId,
3000
- delivery_status: deliveryStatus,
3001
- });
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(() => {});
3002
3263
  };
3003
3264
 
3004
3265
  const showToast = async (message, variant = "warning") => {
@@ -3322,11 +3583,44 @@ export const CodememPlugin = async ({
3322
3583
  }
3323
3584
  };
3324
3585
 
3586
+ let fallbackEvaluationSessionId = nextEventId();
3325
3587
  const buildInjectedContext = async (query, context = {}) => {
3588
+ const requestPackBudget = reserveContextPrefixBudget(context.tokenBudget ?? injectTokenBudget);
3326
3589
  const queryHash = hashPromptPackQuery(query);
3327
- const sessionID = context.sessionID || activeSessionID || "unknown";
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";
3328
3595
  const surface = context.surface || injectSurface;
3329
- const requestKey = context.requestKey || "unknown";
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
+ });
3330
3624
  const attemptKey = JSON.stringify([
3331
3625
  sessionID,
3332
3626
  requestKey,
@@ -3347,14 +3641,23 @@ export const CodememPlugin = async ({
3347
3641
  let identity = resolveIdentity();
3348
3642
  let metadata = attemptMetadata(
3349
3643
  identity,
3350
- context.sessionID || activeSessionID || null,
3644
+ requesterHostSessionID,
3351
3645
  );
3646
+ if (requestPackBudget === null) {
3647
+ return {
3648
+ text: "",
3649
+ attemptId: identity.attemptId,
3650
+ requestId: identity.requestId,
3651
+ queryHash,
3652
+ promptNumber: promptCounter,
3653
+ };
3654
+ }
3352
3655
  const runPack = async () => {
3353
3656
  let packArgs = buildPackArgs({
3354
3657
  query,
3355
3658
  filesModified: sessionContext.filesModified,
3356
3659
  injectLimit,
3357
- injectTokenBudget,
3660
+ injectTokenBudget: requestPackBudget,
3358
3661
  internalLedger: true,
3359
3662
  });
3360
3663
  const httpResult = await postViewerJson({
@@ -3364,12 +3667,15 @@ export const CodememPlugin = async ({
3364
3667
  query,
3365
3668
  filesModified: sessionContext.filesModified,
3366
3669
  injectLimit,
3367
- injectTokenBudget,
3670
+ injectTokenBudget: requestPackBudget,
3368
3671
  projectName: normalizeProjectLabel(process.env.CODEMEM_PROJECT),
3369
3672
  cwd,
3370
3673
  dbPath: promptPackDbPath,
3371
3674
  identityTarget: promptPackIdentityTarget,
3372
3675
  attempt: metadata,
3676
+ automaticContext: requesterHostSessionID
3677
+ ? { source: "opencode", host_session_id: requesterHostSessionID }
3678
+ : null,
3373
3679
  }),
3374
3680
  validate: isValidPackHttpPayload,
3375
3681
  });
@@ -3396,16 +3702,17 @@ export const CodememPlugin = async ({
3396
3702
  };
3397
3703
  }
3398
3704
 
3399
- let result = await runCli(packArgs, { stdinText: JSON.stringify(metadata) });
3705
+ const result = await runCli(packArgs, { stdinText: JSON.stringify(metadata) });
3400
3706
  if (rejectsInternalLedgerFlag(result)) {
3401
- packArgs = packArgs.filter((arg) => arg !== "--internal-ledger");
3402
- result = await runCli(packArgs);
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");
3403
3710
  }
3404
3711
  return { packArgs, result: { ...result, transport: "cli" } };
3405
3712
  };
3406
3713
  let { packArgs, result } = await runPack();
3407
3714
  let { packText, conflictPackText, metrics, itemCount, ledgerConflict } = parsePackOutput(result);
3408
- let artifactFingerprint = packText
3715
+ let artifactFingerprint = packText || itemCount === 0
3409
3716
  ? promptPackArtifactFingerprint(result.stdout, packText)
3410
3717
  : "";
3411
3718
  let injectedIdentity = identity;
@@ -3432,14 +3739,11 @@ export const CodememPlugin = async ({
3432
3739
  advancePromptPackRetryIdentity(attemptKey);
3433
3740
  retryCount = promptPackRetryCounts.get(attemptKey) || 0;
3434
3741
  identity = resolveIdentity();
3435
- metadata = attemptMetadata(
3436
- identity,
3437
- context.sessionID || activeSessionID || null,
3438
- );
3742
+ metadata = attemptMetadata(identity, requesterHostSessionID);
3439
3743
  ({ packArgs, result } = await runPack());
3440
3744
  // Repair-conflict bytes are never preferred over the original preserved pack.
3441
3745
  ({ packText, metrics, itemCount, ledgerConflict } = parsePackOutput(result));
3442
- artifactFingerprint = packText
3746
+ artifactFingerprint = packText || itemCount === 0
3443
3747
  ? promptPackArtifactFingerprint(result.stdout, packText)
3444
3748
  : "";
3445
3749
  const repairFailed = ledgerConflict
@@ -3518,13 +3822,10 @@ export const CodememPlugin = async ({
3518
3822
  advancePromptPackRetryIdentity(attemptKey);
3519
3823
  retryCount = promptPackRetryCounts.get(attemptKey) || 0;
3520
3824
  identity = resolveIdentity();
3521
- metadata = attemptMetadata(
3522
- identity,
3523
- context.sessionID || activeSessionID || null,
3524
- );
3825
+ metadata = attemptMetadata(identity, requesterHostSessionID);
3525
3826
  ({ packArgs, result } = await runPack());
3526
3827
  ({ packText, metrics, itemCount, ledgerConflict } = parsePackOutput(result));
3527
- artifactFingerprint = packText
3828
+ artifactFingerprint = packText || itemCount === 0
3528
3829
  ? promptPackArtifactFingerprint(result.stdout, packText)
3529
3830
  : "";
3530
3831
 
@@ -3624,6 +3925,10 @@ export const CodememPlugin = async ({
3624
3925
  }
3625
3926
  if (!packText) {
3626
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
+ }
3627
3932
  advancePromptPackRetryIdentity(attemptKey);
3628
3933
  }
3629
3934
  if (debug) {
@@ -3656,9 +3961,34 @@ export const CodememPlugin = async ({
3656
3961
  artifactFingerprint || promptPackArtifactFingerprint(result.stdout, packText)
3657
3962
  );
3658
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
+ }
3659
3985
  if (metrics) {
3660
3986
  return {
3661
- text: `[codemem context]\n${packText}`,
3987
+ text,
3988
+ recall,
3989
+ duplicates: filtered.duplicates,
3990
+ evaluation,
3991
+ skipReason,
3662
3992
  metrics,
3663
3993
  attemptId: injectedIdentity?.attemptId || null,
3664
3994
  requestId: injectedIdentity?.requestId || null,
@@ -3667,7 +3997,11 @@ export const CodememPlugin = async ({
3667
3997
  };
3668
3998
  }
3669
3999
  return {
3670
- text: `[codemem context]\n${packText}`,
4000
+ text,
4001
+ recall,
4002
+ duplicates: filtered.duplicates,
4003
+ evaluation,
4004
+ skipReason,
3671
4005
  attemptId: injectedIdentity?.attemptId || null,
3672
4006
  requestId: injectedIdentity?.requestId || null,
3673
4007
  queryHash,
@@ -3892,10 +4226,12 @@ export const CodememPlugin = async ({
3892
4226
  if (injectSurface === "system") {
3893
4227
  return;
3894
4228
  }
3895
- const latestUser = Array.isArray(output?.messages)
3896
- ? findLatestUserMessage(output.messages)
3897
- : null;
3898
- const sessionID = latestUser ? resolveEntrySessionID(latestUser.entry) : input?.sessionID;
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);
3899
4235
  const latestPromptText = latestUser ? extractMessageText(latestUser.entry) : "";
3900
4236
  if (debug) {
3901
4237
  const query = resolveInjectQuery({ lastPromptText: latestPromptText });
@@ -3928,10 +4264,16 @@ export const CodememPlugin = async ({
3928
4264
  resolveInjectQuery,
3929
4265
  buildInjectedContext,
3930
4266
  messageInjectionCache,
4267
+ injectTokenBudget,
4268
+ retainedTokenBudget,
4269
+ workingSet: [...sessionContext.filesModified, ...sessionContext.filesRead],
4270
+ recordMeasurement: (measurement) => logLine(`inject.recall ${JSON.stringify(measurement)}`),
3931
4271
  compactionInjectionSkips,
3932
4272
  confirmDelivery: confirmPromptPackDelivery,
3933
4273
  recordCacheReuse: recordCachedPromptPack,
3934
- recordSkipped: recordSkippedPromptPack,
4274
+ recordSkipped: (reason, sessionID) => recordSkippedPromptPack(
4275
+ reason, sessionID, "message", latestUser ? resolveEntryMessageId(latestUser.entry) : null,
4276
+ ),
3935
4277
  });
3936
4278
  } catch (err) {
3937
4279
  await logLine(
@@ -4183,6 +4525,7 @@ export const CodememPlugin = async ({
4183
4525
  activeSessionID = sessionID || null;
4184
4526
  sessionStartedAt = new Date().toISOString();
4185
4527
  promptCounter = 0;
4528
+ fallbackEvaluationSessionId = nextEventId();
4186
4529
  skippedAttemptCounter = 0;
4187
4530
  disabledInjectionRecorded.delete("message:unknown");
4188
4531
  disabledInjectionRecorded.delete("system:unknown");
@@ -4204,6 +4547,8 @@ export const CodememPlugin = async ({
4204
4547
  compactionInjectionSkips.delete(sessionID);
4205
4548
  disabledInjectionRecorded.delete(`message:${sessionID}`);
4206
4549
  disabledInjectionRecorded.delete(`system:${sessionID}`);
4550
+ latestPolicySkips.delete(`message:${sessionID}`);
4551
+ latestPolicySkips.delete(`system:${sessionID}`);
4207
4552
  }
4208
4553
  await stopViewer();
4209
4554
  }
@@ -4356,5 +4701,18 @@ export const __testUtils = {
4356
4701
  buildRawEventEnvelope,
4357
4702
  trimEventQueue,
4358
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,
4359
4717
  createViewerHealthMonitor,
4360
4718
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codemem/opencode-plugin",
3
- "version": "0.44.0-beta.1",
3
+ "version": "0.44.0",
4
4
  "description": "CodeMem plugin for OpenCode",
5
5
  "type": "module",
6
6
  "main": "./index.js",