@codemem/opencode-plugin 0.45.0-rc.2 → 0.46.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
- import { appendFile, mkdir } from "node:fs/promises";
1
+ import { execSync, spawn as nodeSpawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
- import { basename, dirname, join, posix, resolve, win32 } from "node:path";
4
+ import { appendFile, mkdir } from "node:fs/promises";
4
5
  import { homedir } from "node:os";
5
- import { createHash } from "node:crypto";
6
- import { spawn as nodeSpawn, execSync } from "node:child_process";
6
+ import { basename, dirname, join, posix, resolve, win32 } from "node:path";
7
7
  import {
8
8
  isVersionAtLeast,
9
9
  parseBackendUpdatePolicy,
@@ -11,20 +11,26 @@ import {
11
11
  resolveAutoUpdatePlan,
12
12
  resolveUpgradeGuidance,
13
13
  } from "./compat.js";
14
+ import { V2_ADAPTER_DIAGNOSTICS } from "./host-contract.js";
15
+ import {
16
+ classifyRawEventTransportCause,
17
+ createRawEventDelivery,
18
+ describeRawEventViewerFailure,
19
+ } from "./raw-event-delivery.js";
14
20
  import {
15
21
  DEFAULT_DRAIN_LIMIT,
16
22
  DEFAULT_MAX_ENTRIES,
17
- RAW_EVENT_SPOOL_FULL_CODE,
18
- loadRawEventSpoolEntries,
19
- removeRawEventSpoolEntry,
20
- resolveSpoolDirectory,
21
- writeRawEventSpoolEntry,
22
23
  } from "./raw-event-spool.js";
23
- import { V2_ADAPTER_DIAGNOSTICS } from "./host-contract.js";
24
+ import {
25
+ createRecallLifecycle,
26
+ getSessionMessageInjectionCache,
27
+ MAX_MESSAGE_INJECTION_CACHE_ENTRIES,
28
+ setSessionMessageInjectionCacheEntry,
29
+ } from "./recall-lifecycle.js";
24
30
 
25
31
  const TRUTHY_VALUES = ["1", "true", "yes"];
26
32
  const DISABLED_VALUES = ["0", "false", "off"];
27
- const PINNED_BACKEND_VERSION = "0.45.0-rc.2";
33
+ const PINNED_BACKEND_VERSION = "0.46.0-alpha.1";
28
34
  const COMPAT_CHECK_DELAY_MS = 1500;
29
35
  const COMPAT_CHECK_CACHE_TTL_MS = 5 * 60 * 1000;
30
36
  const MAX_UPDATE_STATUS_BYTES = 16 * 1024;
@@ -33,8 +39,6 @@ const MAX_UPDATE_VERSION_CHARS = 128;
33
39
  const RELEASE_VERSION =
34
40
  /^(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-]+)*)?$/;
35
41
  const CODEMEM_CONTEXT_PART_ID_PREFIX = "codemem-context-";
36
- const MAX_MESSAGE_INJECTION_CACHE_SESSIONS = 20;
37
- const MAX_MESSAGE_INJECTION_CACHE_ENTRIES = 100;
38
42
  const COMPACTION_INJECTION_SKIP_TTL_MS = 30 * 1000;
39
43
  const MAX_WORKING_SET_PATH_CHARS = 400;
40
44
  const VIEWER_HEALTH_CHECK_INTERVAL_MS = 60_000;
@@ -69,8 +73,6 @@ const claimPluginRegistration = (cwd) => {
69
73
  }
70
74
  };
71
75
  };
72
- const rawEventSpoolDrainsInFlight = new Map();
73
-
74
76
  // Release an unread response body without surfacing cancellation failures.
75
77
  const discardResponseBody = (response) => {
76
78
  try {
@@ -249,7 +251,7 @@ const resolveInjectSurface = (value) => {
249
251
  return "message";
250
252
  };
251
253
 
252
- const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value || {}, key);
254
+ const hasOwn = (value, key) => Object.hasOwn(value || {}, key);
253
255
 
254
256
  const deterministicUuid = (namespace, components) => {
255
257
  const hex = createHash("sha256")
@@ -332,7 +334,7 @@ const classifyFallbackCommandResult = (result) => {
332
334
  if (structured.code === "validation_error" || /invalid raw event|session id required/i.test(diagnostic)) {
333
335
  return { retryable: false, cause: "enqueue-raw-event validation failed" };
334
336
  }
335
- if (/unknown command ['\"]?enqueue-raw-event|command not found|\bENOENT\b/i.test(diagnostic)) {
337
+ if (/unknown command ['"]?enqueue-raw-event|command not found|\bENOENT\b/i.test(diagnostic)) {
336
338
  return { retryable: false, cause: "enqueue-raw-event command unavailable" };
337
339
  }
338
340
  const exitCode = result?.exitCode ?? "unknown";
@@ -388,7 +390,7 @@ const clearCompatCheckCache = () => {
388
390
  const parseReleaseVersion = (value) => {
389
391
  if (typeof value !== "string" || value.length > MAX_UPDATE_VERSION_CHARS) return null;
390
392
  const match = RELEASE_VERSION.exec(value);
391
- if (!match || !match.slice(1, 4).map(Number).every(Number.isSafeInteger)) return null;
393
+ if (!match?.slice(1, 4).map(Number).every(Number.isSafeInteger)) return null;
392
394
  const prerelease = match[4]?.split(".") || [];
393
395
  if (prerelease.some((identifier) => /^0\d+$/.test(identifier))) return null;
394
396
  return { core: match.slice(1, 4).map(Number), prerelease };
@@ -504,7 +506,7 @@ const createLogLine = (logPath) => async (line) => {
504
506
  try {
505
507
  await mkdir(dirname(logPath), { recursive: true });
506
508
  await appendFile(logPath, `${new Date().toISOString()} ${line}\n`);
507
- } catch (err) {
509
+ } catch {
508
510
  // ignore logging failures
509
511
  }
510
512
  };
@@ -544,7 +546,7 @@ const createDebugLogger = ({ debug, host, logTimeoutMs, getLogLine, getErrorLogL
544
546
  if (timedOut) {
545
547
  await getLogLine()("debug log timed out");
546
548
  }
547
- } catch (err) {
549
+ } catch {
548
550
  // ignore debug logging failures
549
551
  }
550
552
  };
@@ -711,7 +713,7 @@ const buildPackArgs = ({ query, filesModified, injectLimit, injectTokenBudget, i
711
713
  };
712
714
 
713
715
  const parsePackText = (stdout) => {
714
- if (!stdout || !stdout.trim()) {
716
+ if (!stdout?.trim()) {
715
717
  return "";
716
718
  }
717
719
  try {
@@ -723,7 +725,7 @@ const parsePackText = (stdout) => {
723
725
  };
724
726
 
725
727
  const parsePackMetrics = (stdout) => {
726
- if (!stdout || !stdout.trim()) {
728
+ if (!stdout?.trim()) {
727
729
  return null;
728
730
  }
729
731
  try {
@@ -867,6 +869,11 @@ const classifyRawEventViewerFailure = (payload) => {
867
869
  if (isViewerDbMismatchPayload(payload)) return "database";
868
870
  if (isViewerIdentityMismatchPayload(payload)) return "identity";
869
871
  if (isViewerContractUnsupportedPayload(payload)) return "contract";
872
+ if (
873
+ isRecord(payload)
874
+ && isRecord(payload.error)
875
+ && ["raw_event_queue_full", "raw_event_queue_write_failed"].includes(payload.error.code)
876
+ ) return "queue";
870
877
  return "connection";
871
878
  };
872
879
 
@@ -874,6 +881,7 @@ const RAW_EVENT_FAILURE_ACTIONS = Object.freeze({
874
881
  database: "restart the viewer from the same workspace/config",
875
882
  identity: "restart Codemem and OpenCode with the same environment",
876
883
  contract: "update Codemem on the installed channel, then restart OpenCode",
884
+ queue: "repair or archive the viewer raw-event queue",
877
885
  connection: "check or restart the viewer",
878
886
  });
879
887
 
@@ -917,11 +925,11 @@ const classifyPromptTransportFailure = ({ kind, compatibleProfile = false }) =>
917
925
  };
918
926
 
919
927
  const isViewerPolicyOrAuthFailurePayload = (payload) => {
920
- const code = isRecord(payload) && isRecord(payload.error)
921
- ? payload.error.code
922
- : isRecord(payload)
923
- ? payload.error
924
- : null;
928
+ let code = null;
929
+ if (isRecord(payload)) {
930
+ if (isRecord(payload.error)) code = payload.error.code;
931
+ else code = payload.error;
932
+ }
925
933
  return typeof code === "string" && [
926
934
  "authorization_failed",
927
935
  "forbidden",
@@ -1213,68 +1221,8 @@ const consumeCompactionInjectionSkip = (compactionInjectionSkips, sessionID, now
1213
1221
  // session ID and a real message ID; unidentified messages still receive current
1214
1222
  // turn context, but are not cached because positional fallbacks can drift across
1215
1223
  // turns or sessions. Do not "simplify" this into recomputing previous turns.
1216
- const getSessionMessageInjectionCache = (messageInjectionCache, sessionID) => {
1217
- if (!sessionID) {
1218
- return null;
1219
- }
1220
- const cacheKey = sessionID;
1221
- let sessionCache = messageInjectionCache.get(cacheKey);
1222
- if (sessionCache) {
1223
- // Refresh recency so the bounded cache behaves like a small LRU.
1224
- messageInjectionCache.delete(cacheKey);
1225
- } else {
1226
- sessionCache = new Map();
1227
- }
1228
- messageInjectionCache.set(cacheKey, sessionCache);
1229
- while (messageInjectionCache.size > MAX_MESSAGE_INJECTION_CACHE_SESSIONS) {
1230
- const oldestKey = messageInjectionCache.keys().next().value;
1231
- if (!oldestKey) break;
1232
- messageInjectionCache.delete(oldestKey);
1233
- }
1234
- return sessionCache;
1235
- };
1236
-
1237
- const setSessionMessageInjectionCacheEntry = (
1238
- sessionCache,
1239
- messageID,
1240
- value,
1241
- { preserveMessageIDs = new Set(), evictionSnapshot = null } = {},
1242
- ) => {
1243
- sessionCache.delete(messageID);
1244
- sessionCache.set(messageID, value);
1245
- while (sessionCache.size > MAX_MESSAGE_INJECTION_CACHE_ENTRIES) {
1246
- // Protect host-visible blocks and entries published after this transform began.
1247
- const oldestMessageID = [...sessionCache.keys()].find((id) =>
1248
- !preserveMessageIDs.has(id)
1249
- && (!evictionSnapshot || (evictionSnapshot.has(id) && evictionSnapshot.get(id) === sessionCache.get(id))),
1250
- );
1251
- if (!oldestMessageID) break;
1252
- sessionCache.delete(oldestMessageID);
1253
- }
1254
- };
1255
-
1256
1224
  const commitImmediateDelivery = (commit) => commit();
1257
1225
 
1258
- const createSessionFinalizationMutex = () => {
1259
- const tails = new Map();
1260
- return async (sessionID) => {
1261
- if (!sessionID) return () => {};
1262
- const key = String(sessionID);
1263
- const previous = tails.get(key) || Promise.resolve();
1264
- let releaseOwner = () => {};
1265
- const owner = new Promise((resolve) => { releaseOwner = resolve; });
1266
- tails.set(key, owner);
1267
- await previous;
1268
- let released = false;
1269
- return () => {
1270
- if (released) return;
1271
- released = true;
1272
- releaseOwner();
1273
- if (tails.get(key) === owner) tails.delete(key);
1274
- };
1275
- };
1276
- };
1277
-
1278
1226
  const normalizeInjectedMessageParts = (
1279
1227
  messages,
1280
1228
  sessionCache,
@@ -2056,7 +2004,7 @@ const attachAdapterEvent = ({ sessionID, event }) => {
2056
2004
  let adapterEvent = null;
2057
2005
  try {
2058
2006
  adapterEvent = buildOpencodeAdapterEvent({ sessionID, event });
2059
- } catch (err) {
2007
+ } catch {
2060
2008
  return event;
2061
2009
  }
2062
2010
  if (!adapterEvent) {
@@ -2169,18 +2117,6 @@ const detectRunner = ({ cwd, envRunner }) => {
2169
2117
  return "npx";
2170
2118
  };
2171
2119
 
2172
- /**
2173
- * Check if the TS CLI is available at the given path.
2174
- * Used by the "node" runner to verify the built CLI exists.
2175
- */
2176
- const tsCliAvailable = (cliPath) => {
2177
- try {
2178
- return require("fs").existsSync(cliPath);
2179
- } catch {
2180
- return false;
2181
- }
2182
- };
2183
-
2184
2120
  // When an explicit npx override targets the codemem package, pair it with the
2185
2121
  // matching @codemem/embeddings spec so the detached Viewer can resolve the
2186
2122
  // optional runtime instead of silently falling back to lexical search. Returns
@@ -2354,16 +2290,7 @@ export const createCodememRuntime = async ({ location, host }) => {
2354
2290
  }
2355
2291
  );
2356
2292
  }
2357
- const injectionToastShown = new Set();
2358
- const messageInjectionCache = new Map();
2359
- const emptyRecallCache = new Map();
2360
- const acquireSessionFinalization = createSessionFinalizationMutex();
2361
2293
  const compactionInjectionSkips = new Map();
2362
- const disabledInjectionRecorded = new Set();
2363
- const latestPolicySkips = new Map();
2364
- const attemptStartedAt = new Map();
2365
- const promptPackRetryCounts = new Map();
2366
- const successfulPromptPackArtifacts = new Map();
2367
2294
  let sessionStartedAt = null;
2368
2295
  let sessionStartSessionID = null;
2369
2296
  let activeSessionID = null;
@@ -2371,7 +2298,6 @@ export const createCodememRuntime = async ({ location, host }) => {
2371
2298
  let viewerStartInFlight = false;
2372
2299
  let compatibilityAutoUpdateAttempted = false;
2373
2300
  let promptCounter = 0;
2374
- let skippedAttemptCounter = 0;
2375
2301
  let lastPromptText = null;
2376
2302
  const capturedPrompts = new Set();
2377
2303
  const pendingPrompts = new Map();
@@ -2383,8 +2309,6 @@ export const createCodememRuntime = async ({ location, host }) => {
2383
2309
  const messageRoles = new Map();
2384
2310
  const messageTexts = new Map();
2385
2311
  const promptPartsByMessage = new Map();
2386
- let debugLogCount = 0;
2387
-
2388
2312
  const rawEventsEnabled = envNotDisabled(
2389
2313
  process.env.CODEMEM_RAW_EVENTS || "1"
2390
2314
  );
@@ -2392,7 +2316,7 @@ export const createCodememRuntime = async ({ location, host }) => {
2392
2316
  ? `[${viewerHost}]`
2393
2317
  : viewerHost;
2394
2318
  const rawEventsUrl = `http://${viewerUrlHost}:${viewerPort}/api/raw-events`;
2395
- const rawEventsStatusUrl = `http://${viewerUrlHost}:${viewerPort}/api/raw-events/status?limit=1`;
2319
+ const rawEventsStatusUrl = `http://${viewerUrlHost}:${viewerPort}/api/raw-events/status?limit=0`;
2396
2320
  const packUrl = `http://${viewerUrlHost}:${viewerPort}/api/pack`;
2397
2321
  const promptPackProfileUrl = `http://${viewerUrlHost}:${viewerPort}/api/prompt-pack-profile`;
2398
2322
  const promptPackLedgerUrl = `http://${viewerUrlHost}:${viewerPort}/api/prompt-pack-ledger`;
@@ -2419,27 +2343,15 @@ export const createCodememRuntime = async ({ location, host }) => {
2419
2343
  DEFAULT_MAX_ENTRIES,
2420
2344
  );
2421
2345
  const rawEventSpoolHome = process.env.HOME?.trim() || homedir();
2422
- const rawEventSpoolDirectory = resolveSpoolDirectory(rawEventSpoolHome);
2423
- // Memoize the exact serialized envelope by queued-object identity so retries
2424
- // cannot drift in timestamp, ID, property order, or bytes.
2425
- const rawEventEnvelopes = new WeakMap();
2426
- const rawEventCaptureContexts = new WeakMap();
2346
+ let runtimeActive = true;
2347
+ let rawEventDelivery = null;
2427
2348
  const rawEventPreparations = new WeakMap();
2428
2349
  const rawEventDeliveries = new WeakMap();
2429
- const rawEventStartedAts = new WeakMap();
2430
2350
  const capacityDeferredEvents = new Set();
2431
- let rawEventDeliveryCount = 0;
2432
2351
  let rawEventDeliveryTail = Promise.resolve();
2352
+ let rawEventDeliveryCount = 0;
2433
2353
  let flushDeliveryTail = Promise.resolve();
2434
- let streamUnavailableUntil = 0;
2435
- let spoolPersistenceFailureNoted = null;
2436
- let spoolLoadFailureNoted = false;
2437
- let lastStatusCheckAt = 0;
2438
- let lastStatusAvailable = true;
2439
2354
  let promptPackTransportUnavailableUntil = 0;
2440
- const rawEventAbortController = new AbortController();
2441
- let runtimeActive = true;
2442
-
2443
2355
  const nextEventId = () => {
2444
2356
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
2445
2357
  return crypto.randomUUID();
@@ -2447,202 +2359,6 @@ export const createCodememRuntime = async ({ location, host }) => {
2447
2359
  return `${Date.now()}-${Math.random()}`;
2448
2360
  };
2449
2361
 
2450
- const queueRawEventViaCli = async (body, serialized = JSON.stringify(body)) => {
2451
- const runFallback = () => runCli(["enqueue-raw-event"], {
2452
- stdinText: serialized,
2453
- });
2454
- let result = await runFallback();
2455
- let classification = classifyFallbackCommandResult(result);
2456
- let attemptedRetry = false;
2457
- if (result?.exitCode !== 0 && classification.retryable) {
2458
- attemptedRetry = true;
2459
- result = await runFallback();
2460
- classification = classifyFallbackCommandResult(result);
2461
- }
2462
- if (result?.exitCode !== 0) {
2463
- const retryExhausted = attemptedRetry && classification.retryable;
2464
- const error = new Error(
2465
- retryExhausted ? `${classification.cause} after retry` : classification.cause
2466
- );
2467
- error.retryable = classification.retryable;
2468
- throw error;
2469
- }
2470
- return true;
2471
- };
2472
-
2473
- const lastToastAtBySession = new Map();
2474
- const shouldToast = (sessionID, category = "general") => {
2475
- const now = Date.now();
2476
- const key = `${sessionID || "unknown"}:${category}`;
2477
- const last = lastToastAtBySession.get(key) || 0;
2478
- if (now - last < 60000) {
2479
- return false;
2480
- }
2481
- lastToastAtBySession.set(key, now);
2482
- return true;
2483
- };
2484
-
2485
- const warnSpoolPersistenceFailure = async (sessionID, reason) => {
2486
- const message = reason === "spool_full"
2487
- ? "codemem raw-event retry spool is full; repair Codemem, then archive retained entries"
2488
- : "codemem could not save a raw event for retry; it remains queued in memory";
2489
- try {
2490
- await hostLog({
2491
- service: "codemem",
2492
- level: "error",
2493
- message,
2494
- extra: { category: "persistence", reason },
2495
- });
2496
- } catch {
2497
- // Best-effort app logging only.
2498
- }
2499
- if (spoolPersistenceFailureNoted === reason) return;
2500
- spoolPersistenceFailureNoted = reason;
2501
- if (!hostNotify || !shouldToast(sessionID, `persistence:${reason}`)) return;
2502
- try {
2503
- await hostNotify({ message: `codemem: ${message}`, variant: "error" });
2504
- } catch {
2505
- // Best-effort toast only.
2506
- }
2507
- };
2508
-
2509
- const persistRawEventForRetry = async ({ body, serialized, payload, sessionID }) => {
2510
- try {
2511
- await writeRawEventSpoolEntry({
2512
- envelope: body,
2513
- serialized,
2514
- homeDir: rawEventSpoolHome,
2515
- maxEntries: rawEventSpoolMaxEntries,
2516
- });
2517
- spoolPersistenceFailureNoted = null;
2518
- if (payload && typeof payload === "object") {
2519
- payload._raw_spooled = true;
2520
- }
2521
- return true;
2522
- } catch (error) {
2523
- const reason = error?.code === RAW_EVENT_SPOOL_FULL_CODE ? "spool_full" : "write_failed";
2524
- await logLine(`raw_events.spool.${reason} category=persistence`);
2525
- await warnSpoolPersistenceFailure(sessionID, reason);
2526
- return false;
2527
- }
2528
- };
2529
-
2530
- const removeRawEventFromSpool = async ({ eventId, payload }) => {
2531
- try {
2532
- await removeRawEventSpoolEntry({ eventId, homeDir: rawEventSpoolHome });
2533
- if (payload && typeof payload === "object") {
2534
- payload._raw_spooled = false;
2535
- }
2536
- } catch {
2537
- await logLine("raw_events.spool.cleanup_failed category=persistence");
2538
- }
2539
- };
2540
-
2541
- const notifyRawEventDelivery = async ({ category, delivered, durable, sessionID }) => {
2542
- const action = RAW_EVENT_FAILURE_ACTIONS[category] || RAW_EVENT_FAILURE_ACTIONS.connection;
2543
- let outcome = "was saved for retry";
2544
- if (delivered) {
2545
- outcome = "was queued via CLI";
2546
- } else if (!durable) {
2547
- outcome = "was not saved for retry; still queued in memory";
2548
- }
2549
- const message = `codemem raw event ${outcome}; ${action}`;
2550
- let delivery = "memory";
2551
- if (delivered) {
2552
- delivery = "cli";
2553
- } else if (durable) {
2554
- delivery = "spool";
2555
- }
2556
- try {
2557
- await hostLog({
2558
- service: "codemem",
2559
- level: delivered ? "warn" : "error",
2560
- message,
2561
- extra: {
2562
- category,
2563
- delivery,
2564
- },
2565
- });
2566
- } catch {
2567
- // Best-effort app logging only.
2568
- }
2569
- if (!hostNotify || !shouldToast(sessionID, `${category}:${delivery}`)) return;
2570
- try {
2571
- await hostNotify({ message: `codemem: ${message}`, variant: delivered ? "warning" : "error" });
2572
- } catch {
2573
- // Best-effort toast only.
2574
- }
2575
- };
2576
-
2577
- const drainRawEventSpool = () => {
2578
- if (!rawEventsEnabled) {
2579
- return Promise.resolve();
2580
- }
2581
- const existingDrain = rawEventSpoolDrainsInFlight.get(rawEventSpoolDirectory);
2582
- if (existingDrain) {
2583
- return existingDrain;
2584
- }
2585
- const drainPromise = (async () => {
2586
- let loaded;
2587
- try {
2588
- loaded = await loadRawEventSpoolEntries({
2589
- homeDir: rawEventSpoolHome,
2590
- limit: rawEventSpoolDrainLimit,
2591
- });
2592
- } catch {
2593
- await logLine("raw_events.spool.load_failed category=persistence");
2594
- const message = "codemem could not read saved raw events; spool entries were left untouched";
2595
- try {
2596
- await hostLog({
2597
- service: "codemem",
2598
- level: "error",
2599
- message,
2600
- extra: { category: "persistence", delivery: "load" },
2601
- });
2602
- } catch {
2603
- // Best-effort app logging only.
2604
- }
2605
- if (!spoolLoadFailureNoted) {
2606
- spoolLoadFailureNoted = true;
2607
- if (hostNotify && shouldToast(null, "persistence:load")) {
2608
- try {
2609
- await hostNotify({ message: `codemem: ${message}`, variant: "error" });
2610
- } catch {
2611
- // Best-effort toast only.
2612
- }
2613
- }
2614
- }
2615
- return;
2616
- }
2617
- spoolLoadFailureNoted = false;
2618
- if (loaded.corruptCount > 0) {
2619
- await logLine(`raw_events.spool.corrupt_retained count=${loaded.corruptCount}`);
2620
- }
2621
- for (const entry of loaded.entries) {
2622
- try {
2623
- await queueRawEventViaCli(entry.envelope, entry.serialized);
2624
- await removeRawEventFromSpool({ eventId: entry.eventId });
2625
- } catch (error) {
2626
- await logLine("raw_events.spool.drain_deferred category=fallback");
2627
- if (error?.retryable === true) {
2628
- break;
2629
- }
2630
- }
2631
- }
2632
- })();
2633
- const trackedDrain = drainPromise
2634
- .catch(async () => {
2635
- await logLine("raw_events.spool.drain_failed category=persistence");
2636
- })
2637
- .finally(() => {
2638
- if (rawEventSpoolDrainsInFlight.get(rawEventSpoolDirectory) === trackedDrain) {
2639
- rawEventSpoolDrainsInFlight.delete(rawEventSpoolDirectory);
2640
- }
2641
- });
2642
- rawEventSpoolDrainsInFlight.set(rawEventSpoolDirectory, trackedDrain);
2643
- return trackedDrain;
2644
- };
2645
-
2646
2362
  const buildViewerCliArgs = (action) => {
2647
2363
  const args = ["serve", action, "--host", viewerHost, "--port", viewerPort];
2648
2364
  if (String(viewerDbPath || "").trim()) {
@@ -2654,150 +2370,6 @@ export const createCodememRuntime = async ({ location, host }) => {
2654
2370
  return args;
2655
2371
  };
2656
2372
 
2657
- const deliverRawEvent = async ({ sessionID, type, payload }) => {
2658
- if (!rawEventsEnabled) {
2659
- return true;
2660
- }
2661
- if (!sessionID || !type) {
2662
- return false;
2663
- }
2664
- if (payload?._raw_enqueued) return true;
2665
- const now = Date.now();
2666
- let cachedEnvelope = payload && typeof payload === "object"
2667
- ? rawEventEnvelopes.get(payload)
2668
- : null;
2669
- if (!cachedEnvelope) {
2670
- const builtEnvelope = buildRawEventEnvelope({
2671
- sessionID,
2672
- type,
2673
- payload,
2674
- cwd,
2675
- project: resolveProjectName(project, cwd),
2676
- startedAt: rawEventStartedAts.has(payload)
2677
- ? rawEventStartedAts.get(payload)
2678
- : sessionStartedAt,
2679
- nowMs: now,
2680
- nowMono:
2681
- typeof performance !== "undefined" && performance.now
2682
- ? performance.now()
2683
- : null,
2684
- nextEventId,
2685
- captureContext: rawEventCaptureContexts.get(payload),
2686
- });
2687
- const serialized = JSON.stringify(builtEnvelope);
2688
- cachedEnvelope = { body: JSON.parse(serialized), serialized };
2689
- if (payload && typeof payload === "object") {
2690
- rawEventEnvelopes.set(payload, cachedEnvelope);
2691
- }
2692
- }
2693
- const { body, serialized } = cachedEnvelope;
2694
- if (!runtimeActive) {
2695
- return persistRawEventForRetry({ body, serialized, payload, sessionID });
2696
- }
2697
- if (now < streamUnavailableUntil) {
2698
- const durable = await persistRawEventForRetry({ body, serialized, payload, sessionID });
2699
- try {
2700
- await queueRawEventViaCli(body, serialized);
2701
- await removeRawEventFromSpool({ eventId: body.event_id, payload });
2702
- if (payload && typeof payload === "object") {
2703
- payload._raw_enqueued = true;
2704
- }
2705
- await notifyRawEventDelivery({
2706
- category: "connection",
2707
- delivered: true,
2708
- durable,
2709
- sessionID,
2710
- });
2711
- return true;
2712
- } catch {
2713
- await logLine("raw_events.fallback.error category=connection");
2714
- await notifyRawEventDelivery({
2715
- category: "connection",
2716
- delivered: false,
2717
- durable,
2718
- sessionID,
2719
- });
2720
- return false;
2721
- }
2722
- }
2723
- try {
2724
- if (now - lastStatusCheckAt >= Math.max(1000, rawEventsStatusCheckMs)) {
2725
- const statusResp = await fetchRawEventsStatus(rawEventsStatusUrl);
2726
- if (!statusResp.ok) {
2727
- // Release the unread body before bailing into the backoff path.
2728
- discardResponseBody(statusResp);
2729
- throw new Error(`raw-events status failed (${statusResp.status})`);
2730
- }
2731
- const statusJson = await statusResp.json();
2732
- lastStatusAvailable = statusJson?.ingest?.available !== false;
2733
- lastStatusCheckAt = now;
2734
- }
2735
- if (!lastStatusAvailable) {
2736
- throw new Error("raw-events ingest unavailable");
2737
- }
2738
-
2739
- const postResp = await fetch(rawEventsUrl, {
2740
- method: "POST",
2741
- headers: { "Content-Type": "application/json" },
2742
- signal: AbortSignal.any([
2743
- rawEventAbortController.signal,
2744
- AbortSignal.timeout(RAW_EVENTS_STATUS_TIMEOUT_MS),
2745
- ]),
2746
- body: JSON.stringify({
2747
- ...body,
2748
- db_path: promptPackDbPath,
2749
- identity_target: promptPackIdentityTarget,
2750
- }),
2751
- });
2752
- if (!postResp.ok) {
2753
- let responseBody = null;
2754
- try {
2755
- responseBody = await postResp.json();
2756
- } catch {
2757
- // Generic connection guidance below remains the safe fallback.
2758
- }
2759
- const postError = new Error(`raw-events post failed (${postResp.status})`);
2760
- postError.rawEventFailureCategory = classifyRawEventViewerFailure(responseBody);
2761
- throw postError;
2762
- }
2763
- streamUnavailableUntil = 0;
2764
- lastStatusAvailable = true;
2765
- await removeRawEventFromSpool({ eventId: body.event_id, payload });
2766
- if (payload && typeof payload === "object") {
2767
- payload._raw_enqueued = true;
2768
- }
2769
- return true;
2770
- } catch (err) {
2771
- if (!runtimeActive) {
2772
- return persistRawEventForRetry({ body, serialized, payload, sessionID });
2773
- }
2774
- const category = err?.rawEventFailureCategory || "connection";
2775
- streamUnavailableUntil = Date.now() + Math.max(1000, rawEventsBackoffMs);
2776
- await logLine(`raw_events.error category=${category}`);
2777
- const durable = await persistRawEventForRetry({ body, serialized, payload, sessionID });
2778
-
2779
- let fallbackOk = false;
2780
- try {
2781
- await queueRawEventViaCli(body, serialized);
2782
- await removeRawEventFromSpool({ eventId: body.event_id, payload });
2783
- fallbackOk = true;
2784
- } catch {
2785
- await logLine(`raw_events.fallback.error category=${category}`);
2786
- }
2787
-
2788
- if (fallbackOk) {
2789
- if (payload && typeof payload === "object") {
2790
- payload._raw_enqueued = true;
2791
- }
2792
- await notifyRawEventDelivery({ category, delivered: true, durable, sessionID });
2793
- return true;
2794
- }
2795
-
2796
- await notifyRawEventDelivery({ category, delivered: false, durable, sessionID });
2797
- return false;
2798
- }
2799
- };
2800
-
2801
2373
  // Session context tracking for comprehensive memories
2802
2374
  const createSessionContext = () => ({
2803
2375
  firstPrompt: null,
@@ -3084,7 +2656,7 @@ export const createCodememRuntime = async ({ location, host }) => {
3084
2656
  };
3085
2657
 
3086
2658
  const extractAssistantUsage = (event) => {
3087
- if (!event || event.type !== "message.updated" || !event.messageInfo) {
2659
+ if (event?.type !== "message.updated" || !event.messageInfo) {
3088
2660
  return null;
3089
2661
  }
3090
2662
  const info = event.messageInfo;
@@ -3229,6 +2801,33 @@ export const createCodememRuntime = async ({ location, host }) => {
3229
2801
  const runCli = async (args, options = {}) =>
3230
2802
  runCommand([runner, ...runnerArgs, ...args], options);
3231
2803
 
2804
+ rawEventDelivery = createRawEventDelivery({
2805
+ backoffMs: rawEventsBackoffMs,
2806
+ buildEnvelope: buildRawEventEnvelope,
2807
+ classifyViewerFailure: classifyRawEventViewerFailure,
2808
+ cwd,
2809
+ discardResponseBody,
2810
+ enabled: rawEventsEnabled,
2811
+ failureActions: RAW_EVENT_FAILURE_ACTIONS,
2812
+ fetchRawEventsStatus,
2813
+ hostLog,
2814
+ hostNotify,
2815
+ identityTarget: promptPackIdentityTarget,
2816
+ isActive: () => runtimeActive,
2817
+ logLine,
2818
+ nextEventId,
2819
+ projectName: resolveProjectName(project, cwd),
2820
+ promptPackDbPath,
2821
+ rawEventsStatusTimeoutMs: RAW_EVENTS_STATUS_TIMEOUT_MS,
2822
+ rawEventsStatusUrl,
2823
+ rawEventsUrl,
2824
+ sessionStartedAt: () => sessionStartedAt,
2825
+ statusCheckMs: rawEventsStatusCheckMs,
2826
+ spoolDrainLimit: rawEventSpoolDrainLimit,
2827
+ spoolHome: rawEventSpoolHome,
2828
+ spoolMaxEntries: rawEventSpoolMaxEntries,
2829
+ });
2830
+
3232
2831
  const postViewerJson = async ({ url, operation, payload, validate }) => {
3233
2832
  if (!viewerEnabled) {
3234
2833
  return {
@@ -3363,26 +2962,6 @@ export const createCodememRuntime = async ({ location, host }) => {
3363
2962
  return { ok: true, body };
3364
2963
  };
3365
2964
 
3366
- const attemptMetadata = (identity, sessionID = null, promptNumber = promptCounter) => ({
3367
- attempt_id: identity.attemptId,
3368
- started_at: (() => {
3369
- const existing = attemptStartedAt.get(identity.attemptId);
3370
- if (existing) return existing;
3371
- const created = new Date().toISOString();
3372
- attemptStartedAt.set(identity.attemptId, created);
3373
- while (attemptStartedAt.size > 2000) {
3374
- const oldest = attemptStartedAt.keys().next().value;
3375
- if (!oldest) break;
3376
- attemptStartedAt.delete(oldest);
3377
- }
3378
- return created;
3379
- })(),
3380
- source: "opencode",
3381
- ...(sessionID ? { stream_id: String(sessionID), source_session_id: String(sessionID) } : {}),
3382
- ...(promptNumber > 0 ? { prompt_number: promptNumber } : {}),
3383
- request_id: identity.requestId,
3384
- });
3385
-
3386
2965
  const runPromptPackLedger = async (payload, { viewerOnly = false } = {}) => {
3387
2966
  const viewerPayload = {
3388
2967
  ...payload,
@@ -3422,152 +3001,28 @@ export const createCodememRuntime = async ({ location, host }) => {
3422
3001
  }
3423
3002
  };
3424
3003
 
3425
- const skippedIdentity = (failureCode, sessionID, surface, eventKey) =>
3426
- promptPackIdentity({
3427
- sessionID: sessionID || "unknown",
3428
- requestKey: `${failureCode}:${eventKey}`,
3429
- surface,
3430
- promptNumber: promptCounter,
3431
- queryHash: hashPromptPackQuery(""),
3432
- });
3433
-
3434
- const recordSkippedPromptPack = (failureCode, sessionID = null, surface = injectSurface, requestKey = null) => {
3435
- const sessionKey = String(sessionID || "unknown");
3436
- const memoKey = `${surface}:${sessionKey}`;
3437
- if (requestKey) {
3438
- const signature = `${requestKey}:${failureCode}`;
3439
- if (latestPolicySkips.get(memoKey) === signature) return null;
3440
- latestPolicySkips.delete(memoKey);
3441
- latestPolicySkips.set(memoKey, signature);
3442
- while (latestPolicySkips.size > MAX_MESSAGE_INJECTION_CACHE_SESSIONS) {
3443
- latestPolicySkips.delete(latestPolicySkips.keys().next().value);
3444
- }
3445
- }
3446
- if (failureCode === "injection_disabled" && disabledInjectionRecorded.has(memoKey)) {
3447
- return null;
3448
- }
3449
- if (failureCode === "injection_disabled") disabledInjectionRecorded.add(memoKey);
3450
- const identity = skippedIdentity(
3451
- failureCode,
3452
- sessionID,
3453
- surface,
3454
- requestKey || (failureCode === "injection_disabled" ? "once" : `event-${++skippedAttemptCounter}`),
3455
- );
3456
- void runPromptPackLedger({
3457
- action: "record",
3458
- ...attemptMetadata(identity, sessionID),
3459
- retrieval_status: "skipped",
3460
- failure_code: failureCode,
3461
- failure_stage: "policy",
3462
- });
3463
- return identity.attemptId;
3464
- };
3465
-
3466
- const recordCachedPromptPack = (cached, { messageId, sessionID, surface = "message" } = {}) => {
3467
- cached.reuseCount = (cached.reuseCount || 0) + 1;
3468
- const identity = promptPackIdentity({
3469
- sessionID: sessionID || "unknown",
3470
- requestKey: `${messageId || "unknown"}:cache:${cached.reuseCount}`,
3471
- surface,
3472
- promptNumber: cached.promptNumber || promptCounter,
3473
- queryHash: cached.queryHash || hashPromptPackQuery(""),
3474
- });
3475
- const ready = runPromptPackLedger({
3476
- action: "cache_reuse",
3477
- ...attemptMetadata(identity, sessionID, cached.promptNumber || promptCounter),
3478
- original_attempt_id: cached.attemptId,
3479
- });
3480
- return { attemptId: identity.attemptId, ready };
3481
- };
3482
-
3483
- const confirmPromptPackDelivery = (attemptId, deliveryStatus = "handed_off", evaluation) => {
3484
- const delivery = {
3485
- action: "delivery", attempt_id: attemptId, delivery_status: deliveryStatus,
3486
- };
3487
- void runPromptPackLedger({
3488
- ...delivery,
3489
- ...evaluation,
3490
- }).then((result) => {
3491
- // Retry additive fields only after the transport identifies that exact compatibility case.
3492
- if (
3493
- evaluation
3494
- && result?.transport === "viewer"
3495
- && result.failureKind === "viewer_contract_unsupported"
3496
- ) {
3497
- return runPromptPackLedger(delivery, { viewerOnly: true });
3498
- }
3499
- // Older CLI fallbacks can reject optional fields. Preserve the original receipt;
3500
- // never retry a Viewer policy/auth rejection through a different transport.
3501
- if (evaluation && result?.transport === "cli" && result.exitCode !== 0) {
3502
- return runPromptPackLedger(delivery);
3503
- }
3504
- }).catch(() => {});
3505
- };
3506
-
3507
- const createTransformDelivery = (deferred) => {
3508
- const recordMeasurement = (measurement) => logLine(`inject.recall ${JSON.stringify(measurement)}`);
3509
- if (!deferred) {
3510
- return {
3511
- complete: undefined,
3512
- finish: async () => {},
3513
- beginFinalization: async () => {},
3514
- confirm: confirmPromptPackDelivery,
3515
- recordMeasurement,
3516
- };
3517
- }
3518
- let completion = null;
3519
- let finalizationReady = null;
3520
- let releaseFinalization = null;
3521
- const pending = [];
3522
- const commits = [];
3523
- const measurements = [];
3524
- const dispatch = ([attemptId, status, evaluation]) => {
3525
- const deliveryStatus = completion === "failed" && (!status || status === "handed_off")
3526
- ? "failed"
3527
- : status;
3528
- confirmPromptPackDelivery(attemptId, deliveryStatus, evaluation);
3529
- };
3530
- const beginFinalization = async (sessionID) => {
3531
- if (!finalizationReady) {
3532
- finalizationReady = acquireSessionFinalization(sessionID).then((release) => {
3533
- releaseFinalization = release;
3534
- });
3535
- }
3536
- await finalizationReady;
3537
- };
3538
- const finish = async (status = "handed_off") => {
3539
- if (completion) return;
3540
- completion = status;
3541
- const tasks = [];
3542
- try {
3543
- for (const commit of commits.splice(0)) {
3544
- if (status === "handed_off") tasks.push(commit());
3545
- }
3546
- for (const confirmation of pending.splice(0)) dispatch(confirmation);
3547
- for (const measurement of measurements.splice(0)) {
3548
- const finalized = status === "handed_off" ? measurement : {
3549
- ...measurement, reason: "delivery_failed", new_tokens: 0, retained_tokens: 0,
3550
- };
3551
- tasks.push(recordMeasurement(finalized));
3552
- }
3553
- await Promise.all(tasks);
3554
- } finally {
3555
- releaseFinalization?.();
3556
- releaseFinalization = null;
3557
- }
3558
- };
3559
- return {
3560
- commit: (commit) => { commits.push(commit); },
3561
- recordMeasurement: (measurement) => { measurements.push(measurement); },
3562
- beginFinalization,
3563
- finish,
3564
- complete: finish,
3565
- confirm: (...confirmation) => {
3566
- if (completion) dispatch(confirmation);
3567
- else pending.push(confirmation);
3568
- },
3569
- };
3570
- };
3004
+ const recallLifecycle = createRecallLifecycle({
3005
+ createIdentity: promptPackIdentity,
3006
+ emptyQueryHash: hashPromptPackQuery(""),
3007
+ runPromptPackLedger,
3008
+ logLine,
3009
+ getPromptCounter: () => promptCounter,
3010
+ defaultSurface: injectSurface,
3011
+ });
3012
+ const {
3013
+ injectionToastShown,
3014
+ messageInjectionCache,
3015
+ emptyRecallCache,
3016
+ attemptMetadata,
3017
+ recordSkippedPromptPack,
3018
+ recordCachedPromptPack,
3019
+ confirmPromptPackDelivery,
3020
+ createTransformDelivery,
3021
+ advancePromptPackRetryIdentity,
3022
+ getPromptPackRetryCount,
3023
+ getSuccessfulPromptPackArtifact,
3024
+ rememberSuccessfulPromptPackArtifact,
3025
+ } = recallLifecycle;
3571
3026
 
3572
3027
  const showToast = async (message, variant = "warning") => {
3573
3028
  if (backendUpdatePolicy === "off") {
@@ -3578,7 +3033,7 @@ export const createCodememRuntime = async ({ location, host }) => {
3578
3033
  }
3579
3034
  try {
3580
3035
  await hostNotify({ message, variant });
3581
- } catch (toastErr) {
3036
+ } catch {
3582
3037
  // best-effort only
3583
3038
  }
3584
3039
  };
@@ -3615,7 +3070,7 @@ export const createCodememRuntime = async ({ location, host }) => {
3615
3070
  }
3616
3071
 
3617
3072
  const versionResult = await runCli(["version"]);
3618
- if (!versionResult || versionResult.exitCode !== 0) {
3073
+ if (versionResult?.exitCode !== 0) {
3619
3074
  await logLine(
3620
3075
  `compat.version_check_failed exit=${versionResult?.exitCode ?? "unknown"} stderr=${
3621
3076
  versionResult?.stderr ? redactLog(versionResult.stderr.trim()) : ""
@@ -3856,35 +3311,6 @@ export const createCodememRuntime = async ({ location, host }) => {
3856
3311
  return masked.length > limit ? `${masked.slice(0, limit)}…` : masked;
3857
3312
  };
3858
3313
 
3859
- const advancePromptPackRetryIdentity = (attemptKey) => {
3860
- promptPackRetryCounts.set(
3861
- attemptKey,
3862
- (promptPackRetryCounts.get(attemptKey) || 0) + 1
3863
- );
3864
- while (promptPackRetryCounts.size > 2000) {
3865
- const oldest = promptPackRetryCounts.keys().next().value;
3866
- if (!oldest) break;
3867
- promptPackRetryCounts.delete(oldest);
3868
- }
3869
- };
3870
-
3871
- const rememberSuccessfulPromptPackArtifact = (
3872
- attemptKey,
3873
- retryCount,
3874
- fingerprint
3875
- ) => {
3876
- successfulPromptPackArtifacts.delete(attemptKey);
3877
- successfulPromptPackArtifacts.set(attemptKey, {
3878
- retryCount,
3879
- fingerprint,
3880
- });
3881
- while (successfulPromptPackArtifacts.size > 2000) {
3882
- const oldest = successfulPromptPackArtifacts.keys().next().value;
3883
- if (!oldest) break;
3884
- successfulPromptPackArtifacts.delete(oldest);
3885
- }
3886
- };
3887
-
3888
3314
  let fallbackEvaluationSessionId = nextEventId();
3889
3315
  const buildInjectedContext = async (query, context = {}) => {
3890
3316
  const requestPackBudget = reserveContextPrefixBudget(context.tokenBudget ?? injectTokenBudget);
@@ -3930,7 +3356,7 @@ export const createCodememRuntime = async ({ location, host }) => {
3930
3356
  promptCounter,
3931
3357
  queryHash,
3932
3358
  ]);
3933
- let retryCount = promptPackRetryCounts.get(attemptKey) || 0;
3359
+ let retryCount = getPromptPackRetryCount(attemptKey);
3934
3360
  const resolveIdentity = () => promptPackIdentity({
3935
3361
  sessionID,
3936
3362
  requestKey: retryCount > 0
@@ -3955,7 +3381,7 @@ export const createCodememRuntime = async ({ location, host }) => {
3955
3381
  };
3956
3382
  }
3957
3383
  const runPack = async () => {
3958
- let packArgs = buildPackArgs({
3384
+ const packArgs = buildPackArgs({
3959
3385
  query,
3960
3386
  filesModified: sessionContext.filesModified,
3961
3387
  injectLimit,
@@ -4039,7 +3465,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4039
3465
  }
4040
3466
  : null;
4041
3467
  advancePromptPackRetryIdentity(attemptKey);
4042
- retryCount = promptPackRetryCounts.get(attemptKey) || 0;
3468
+ retryCount = getPromptPackRetryCount(attemptKey);
4043
3469
  identity = resolveIdentity();
4044
3470
  metadata = attemptMetadata(identity, requesterHostSessionID);
4045
3471
  ({ packArgs, result } = await runPack());
@@ -4082,7 +3508,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4082
3508
  });
4083
3509
  }
4084
3510
  advancePromptPackRetryIdentity(attemptKey);
4085
- retryCount = promptPackRetryCounts.get(attemptKey) || 0;
3511
+ retryCount = getPromptPackRetryCount(attemptKey);
4086
3512
  ({
4087
3513
  packArgs,
4088
3514
  result,
@@ -4102,7 +3528,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4102
3528
  }
4103
3529
  }
4104
3530
  if (packText) {
4105
- const previous = successfulPromptPackArtifacts.get(attemptKey);
3531
+ const previous = getSuccessfulPromptPackArtifact(attemptKey);
4106
3532
  if (
4107
3533
  previous?.retryCount === retryCount
4108
3534
  && previous.fingerprint !== artifactFingerprint
@@ -4122,7 +3548,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4122
3548
  ledgerConflict,
4123
3549
  };
4124
3550
  advancePromptPackRetryIdentity(attemptKey);
4125
- retryCount = promptPackRetryCounts.get(attemptKey) || 0;
3551
+ retryCount = getPromptPackRetryCount(attemptKey);
4126
3552
  identity = resolveIdentity();
4127
3553
  metadata = attemptMetadata(identity, requesterHostSessionID);
4128
3554
  ({ packArgs, result } = await runPack());
@@ -4142,7 +3568,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4142
3568
  // recorded that terminal outcome; do not misclassify it as a decode
4143
3569
  // failure or fall back to stale non-empty context.
4144
3570
  injectedIdentity = identity;
4145
- } else if (!result || result.exitCode !== 0 || !packText) {
3571
+ } else if (result?.exitCode !== 0 || !packText) {
4146
3572
  const malformedSuccess = result?.exitCode === 0;
4147
3573
  const exitCode = result?.exitCode ?? "unknown";
4148
3574
  const stderr = redactLog(result?.stderr ? result.stderr.trim() : "");
@@ -4185,7 +3611,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4185
3611
  }
4186
3612
  }
4187
3613
  }
4188
- if (!result || result.exitCode !== 0) {
3614
+ if (result?.exitCode !== 0) {
4189
3615
  const exitCode = result?.exitCode ?? "unknown";
4190
3616
  const stderr = redactLog(result?.stderr ? result.stderr.trim() : "");
4191
3617
  const stdout = redactLog(result?.stdout ? result.stdout.trim() : "");
@@ -4336,7 +3762,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4336
3762
  // not a git repo. The catch below leaves version as "unknown".
4337
3763
  stdio: ["ignore", "pipe", "ignore"],
4338
3764
  }).trim();
4339
- } catch (err) {
3765
+ } catch {
4340
3766
  // Ignore - version will remain 'unknown'
4341
3767
  }
4342
3768
 
@@ -4386,7 +3812,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4386
3812
  }
4387
3813
  try {
4388
3814
  return JSON.stringify(value);
4389
- } catch (err) {
3815
+ } catch {
4390
3816
  return String(value);
4391
3817
  }
4392
3818
  };
@@ -4472,7 +3898,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4472
3898
  rawEventDeliveryCount++;
4473
3899
  const delivery = rawEventDeliveryTail.then(async () => {
4474
3900
  await rawEventPreparations.get(input.payload);
4475
- return deliverRawEvent(input);
3901
+ return rawEventDelivery.deliver(input);
4476
3902
  });
4477
3903
  rawEventDeliveries.set(input.payload, delivery);
4478
3904
  rawEventDeliveryTail = delivery.catch(() => {}).finally(() => {
@@ -4501,19 +3927,19 @@ export const createCodememRuntime = async ({ location, host }) => {
4501
3927
  });
4502
3928
  const rawEventId =
4503
3929
  adapterAnnotatedEvent?._adapter?.event_id ||
4504
- (adapterAnnotatedEvent && adapterAnnotatedEvent._raw_event_id) ||
3930
+ adapterAnnotatedEvent?._raw_event_id ||
4505
3931
  nextEventId();
4506
3932
  const queuedEvent = {
4507
3933
  ...adapterAnnotatedEvent,
4508
3934
  _raw_event_id: rawEventId,
4509
3935
  _raw_session_id: resolvedSessionID,
4510
3936
  };
4511
- rawEventStartedAts.set(queuedEvent, sessionStartedAt);
3937
+ rawEventDelivery.setStartedAt(queuedEvent, sessionStartedAt);
4512
3938
  recordEvent(queuedEvent);
4513
3939
  if (prompt && rawEventsEnabled && host.resolveCaptureContext) {
4514
3940
  const preparation = Promise.resolve().then(() => host.resolveCaptureContext(prompt))
4515
3941
  .then((context) => {
4516
- if (context) rawEventCaptureContexts.set(queuedEvent, context);
3942
+ if (context) rawEventDelivery.setCaptureContext(queuedEvent, context);
4517
3943
  }).catch(() => {
4518
3944
  // Optional provenance must never discard an otherwise valid prompt.
4519
3945
  });
@@ -4635,7 +4061,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4635
4061
  );
4636
4062
  await logLine(`flush.ok count=${batch.events.length} dropped=0`);
4637
4063
  }
4638
- if (runtimeActive) await drainRawEventSpool();
4064
+ if (runtimeActive) await rawEventDelivery.drainSpool();
4639
4065
  } catch {
4640
4066
  // Flush delivery and diagnostics must not reject into OpenCode hooks.
4641
4067
  }
@@ -4668,7 +4094,7 @@ export const createCodememRuntime = async ({ location, host }) => {
4668
4094
 
4669
4095
  const flushEmptyQueue = async () => {
4670
4096
  try {
4671
- await drainRawEventSpool();
4097
+ await rawEventDelivery.drainSpool();
4672
4098
  await logLine("flush.skip empty");
4673
4099
  } catch {
4674
4100
  // Empty flushes follow the same non-rejecting hook contract.
@@ -4747,14 +4173,14 @@ export const createCodememRuntime = async ({ location, host }) => {
4747
4173
  return scheduleQueuedEvents({ owner: activeSessionID });
4748
4174
  };
4749
4175
 
4750
- void drainRawEventSpool();
4176
+ void rawEventDelivery.drainSpool();
4751
4177
 
4752
4178
  const deactivateRuntime = () => {
4753
4179
  if (!runtimeActive) return;
4754
4180
  runtimeActive = false;
4755
4181
  clearTimeout(updateCheckTimer);
4756
4182
  stopHealthCheck();
4757
- rawEventAbortController.abort();
4183
+ rawEventDelivery.abort();
4758
4184
  releasePluginRegistration();
4759
4185
  };
4760
4186
 
@@ -5003,9 +4429,7 @@ export const createCodememRuntime = async ({ location, host }) => {
5003
4429
  sessionStartedAt = new Date().toISOString();
5004
4430
  promptCounter = 0;
5005
4431
  fallbackEvaluationSessionId = nextEventId();
5006
- skippedAttemptCounter = 0;
5007
- disabledInjectionRecorded.delete("message:unknown");
5008
- disabledInjectionRecorded.delete("system:unknown");
4432
+ recallLifecycle.startSession();
5009
4433
  lastPromptText = null;
5010
4434
  if (sessionID) {
5011
4435
  await capturePendingPrompts({
@@ -5190,19 +4614,9 @@ export const createCodememRuntime = async ({ location, host }) => {
5190
4614
  sessionStartSessionID = null;
5191
4615
  }
5192
4616
  if (sessionID) {
5193
- for (const key of lastToastAtBySession.keys()) {
5194
- if (key.startsWith(`${sessionID}:`)) {
5195
- lastToastAtBySession.delete(key);
5196
- }
5197
- }
5198
- injectionToastShown.delete(sessionID);
5199
- messageInjectionCache.delete(sessionID);
5200
- emptyRecallCache.delete(sessionID);
4617
+ rawEventDelivery.clearSession(sessionID);
4618
+ recallLifecycle.clearSession(sessionID);
5201
4619
  compactionInjectionSkips.delete(sessionID);
5202
- disabledInjectionRecorded.delete(`message:${sessionID}`);
5203
- disabledInjectionRecorded.delete(`system:${sessionID}`);
5204
- latestPolicySkips.delete(`message:${sessionID}`);
5205
- latestPolicySkips.delete(`system:${sessionID}`);
5206
4620
  for (const key of failedToolCaptured) {
5207
4621
  if (key.startsWith(`${sessionID}:`)) {
5208
4622
  failedToolCaptured.delete(key);
@@ -5333,6 +4747,8 @@ export const __testUtils = {
5333
4747
  rejectsInternalLedgerFlag,
5334
4748
  classifyFallbackCommandResult,
5335
4749
  classifyRawEventViewerFailure,
4750
+ classifyRawEventTransportCause,
4751
+ describeRawEventViewerFailure,
5336
4752
  detectRunner,
5337
4753
  PROMPT_TRANSPORT_PROTOCOL_RANGE,
5338
4754
  normalizePromptTransportProtocolRange,