@tekmidian/pai 0.36.0 → 0.36.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.
@@ -7,7 +7,7 @@ import { i as indexProject, n as indexAll, t as embedChunks } from "./sync-BWbe8
7
7
  import { t as STOP_WORDS } from "./stop-words-Hfu8u22w.mjs";
8
8
  import { a as searchMemory, i as populateSlugs } from "./search-Rpk1cSBC.mjs";
9
9
  import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "./detect-CdaA48EI.mjs";
10
- import { _ as transcriptFiles, a as readBodyFile, c as loadScanConfig, d as saveScanConfig, f as upsertProject, g as scanTranscriptFolders, h as findMovedProjects, i as findNotesDir$1, m as claudeProjectsDir, n as applyContinue, o as extractAndStoreTriples, p as upsertSession, r as findLatestNote, s as cmdScan, t as appendCheckpointToNote, u as resolveHome } from "./checkpoint-block-DKYxCkBL.mjs";
10
+ import { _ as findMovedProjects, a as findLatestNote, c as extractAndStoreTriples, f as resolveHome, g as claudeProjectsDir, h as upsertSession, i as applyContinue, l as cmdScan, m as upsertProject, o as findNotesDir$1, p as saveScanConfig, r as appendCheckpointToNote, s as readBodyFile, t as readContextHandoverCache, u as loadScanConfig, v as scanTranscriptFolders, y as transcriptFiles } from "./context-handover-cache-PtNvj_8D.mjs";
11
11
  import { a as schedulerLogPath, i as paiSocketPath, n as daemonLogPath, r as daemonPidPath } from "./runtime-paths-rni52zHX.mjs";
12
12
  import { t as PaiClient } from "./ipc-client-BmypMNYk.mjs";
13
13
  import { a as expandHome, c as UNROUTED, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, s as OWNER_LABEL_PREFIX, t as CONFIG_DIR } from "./config-B64vFg14.mjs";
@@ -3649,7 +3649,7 @@ function cmdLogs(opts) {
3649
3649
  }
3650
3650
  function registerDaemonCommands(daemonCmd) {
3651
3651
  daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
3652
- const { serve } = await import("./daemon-DJEFqV84.mjs");
3652
+ const { serve } = await import("./daemon-DRdoA489.mjs");
3653
3653
  const { loadConfig: lc, ensureConfigDir } = await import("./config-C_ErGddD.mjs");
3654
3654
  ensureConfigDir();
3655
3655
  await serve(lc());
@@ -12375,12 +12375,6 @@ const DEFAULT_AUTOCOMPACT_PCT = 80;
12375
12375
  /** How many of the most recent compact_boundary events to consider, and to
12376
12376
  * take the minimum of. */
12377
12377
  const MEASURED_TRIGGER_SAMPLE_SIZE = 3;
12378
- /** How many of a project's most-recently-modified transcripts to scan for
12379
- * compact_boundary events. compact_boundary events cluster in whichever
12380
- * files were touched most recently — a full-history scan would cost a lot
12381
- * for a long-lived project and buy nothing this doesn't already get from
12382
- * the last handful of files. */
12383
- const MEASURED_TRIGGER_MAX_FILES = 8;
12384
12378
  const THRESHOLD_MARGIN_TOKENS = {
12385
12379
  warmup: 1e5,
12386
12380
  refresh: 4e4,
@@ -12392,10 +12386,18 @@ const CLAUDE_PROJECTS_DIR = join(homedir(), ".claude", "projects");
12392
12386
  function encodeProjectPath(cwd) {
12393
12387
  return cwd.replace(/[/\s.-]/g, "-");
12394
12388
  }
12389
+ /** Every `.jsonl` transcript belonging to a project — top-level (the live
12390
+ * file) and `sessions/` (Claude Code's archive, which mirrors it). No file
12391
+ * is excluded and no ordering is applied here: ordering by EVENT
12392
+ * timestamp, not by file mtime, is the whole point (see
12393
+ * readCompactBoundarySamples) — a file's mtime does not reliably track
12394
+ * which events inside it are recent, and pre-filtering by mtime is exactly
12395
+ * what caused this function to return a stale, pre-regime-change trigger
12396
+ * on real data. */
12395
12397
  function listProjectTranscripts(cwd, projectsDir) {
12396
12398
  const projectDir = join(projectsDir, encodeProjectPath(cwd));
12397
12399
  if (!existsSync(projectDir)) return [];
12398
- const candidates = [];
12400
+ const paths = [];
12399
12401
  const collect = (dir) => {
12400
12402
  if (!existsSync(dir)) return;
12401
12403
  let entries;
@@ -12404,28 +12406,21 @@ function listProjectTranscripts(cwd, projectsDir) {
12404
12406
  } catch {
12405
12407
  return;
12406
12408
  }
12407
- for (const entry of entries) {
12408
- if (!entry.endsWith(".jsonl")) continue;
12409
- const full = join(dir, entry);
12410
- try {
12411
- candidates.push({
12412
- path: full,
12413
- mtimeMs: statSync(full).mtimeMs
12414
- });
12415
- } catch {}
12416
- }
12409
+ for (const entry of entries) if (entry.endsWith(".jsonl")) paths.push(join(dir, entry));
12417
12410
  };
12418
12411
  collect(projectDir);
12419
12412
  collect(join(projectDir, "sessions"));
12420
- candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
12421
- return candidates.slice(0, MEASURED_TRIGGER_MAX_FILES).map((c) => c.path);
12413
+ return paths;
12422
12414
  }
12423
12415
  /**
12424
- * Every compact_boundary sample found in a project's most-recently-modified
12425
- * transcripts, newest first.
12416
+ * Every DISTINCT compact_boundary sample found across ALL of a project's
12417
+ * transcripts (live + archived), newest first by the event's OWN timestamp
12418
+ * — never by which file it came from or that file's mtime. Deduplicated by
12419
+ * the event's uuid (falling back to a timestamp+preTokens key for the rare
12420
+ * line with no uuid) so an event mirrored into `sessions/` is counted once.
12426
12421
  */
12427
12422
  function readCompactBoundarySamples(cwd, projectsDir) {
12428
- const samples = [];
12423
+ const byKey = /* @__PURE__ */ new Map();
12429
12424
  for (const path of listProjectTranscripts(cwd, projectsDir)) {
12430
12425
  let raw;
12431
12426
  try {
@@ -12446,27 +12441,45 @@ function readCompactBoundarySamples(cwd, projectsDir) {
12446
12441
  if (typeof preTokens !== "number" || !Number.isFinite(preTokens)) continue;
12447
12442
  const timestampMs = entry.timestamp ? Date.parse(entry.timestamp) : NaN;
12448
12443
  if (!Number.isFinite(timestampMs)) continue;
12449
- samples.push({
12444
+ const key = entry.uuid ?? `${timestampMs}:${preTokens}`;
12445
+ if (!byKey.has(key)) byKey.set(key, {
12450
12446
  preTokens,
12451
- timestampMs
12447
+ timestampMs,
12448
+ timestamp: entry.timestamp,
12449
+ uuid: entry.uuid
12452
12450
  });
12453
12451
  }
12454
12452
  }
12455
- samples.sort((a, b) => b.timestampMs - a.timestampMs);
12456
- return samples;
12453
+ return [...byKey.values()].sort((a, b) => b.timestampMs - a.timestampMs);
12454
+ }
12455
+ /**
12456
+ * The most recent MEASURED_TRIGGER_SAMPLE_SIZE distinct compact_boundary
12457
+ * events for a project, newest first — exposed on its own (not just the
12458
+ * derived minimum) so the number `measureCompactionTrigger` returns is
12459
+ * checkable: print these and the timestamps prove which three events
12460
+ * produced it, rather than asking for trust.
12461
+ */
12462
+ function selectedCompactionSamples(cwd, projectsDir = CLAUDE_PROJECTS_DIR) {
12463
+ if (!cwd) return [];
12464
+ return readCompactBoundarySamples(cwd, projectsDir).slice(0, MEASURED_TRIGGER_SAMPLE_SIZE);
12457
12465
  }
12458
12466
  /**
12459
12467
  * The measured compaction trigger for a project, or null when it has no
12460
12468
  * compaction history yet (a brand-new project, or one whose transcripts
12461
12469
  * this process cannot read). Minimum of the most recent
12462
- * MEASURED_TRIGGER_SAMPLE_SIZE compact_boundary events see the module
12463
- * comment above for why minimum, not mean.
12470
+ * MEASURED_TRIGGER_SAMPLE_SIZE DISTINCT compact_boundary events, ordered by
12471
+ * the events' own timestamps across every transcript the project has
12472
+ * (live and archived) — see the module comment above for why minimum, not
12473
+ * mean, and readCompactBoundarySamples for why "distinct" and "own
12474
+ * timestamp" both matter (a file-mtime-ordered, non-deduplicated version of
12475
+ * this returned a stale pre-regime-change trigger on real project data).
12464
12476
  */
12465
12477
  function measureCompactionTrigger(cwd, projectsDir = CLAUDE_PROJECTS_DIR) {
12466
- if (!cwd) return null;
12467
- const samples = readCompactBoundarySamples(cwd, projectsDir).slice(0, MEASURED_TRIGGER_SAMPLE_SIZE);
12478
+ const samples = selectedCompactionSamples(cwd, projectsDir);
12468
12479
  if (samples.length === 0) return null;
12469
- return Math.min(...samples.map((s) => s.preTokens));
12480
+ const trigger = Math.min(...samples.map((s) => s.preTokens));
12481
+ console.error(`[context-fill] measured trigger for ${cwd}: ${trigger} (minimum of ` + samples.map((s) => `${s.preTokens}@${s.timestamp}`).join(", ") + ")");
12482
+ return trigger;
12470
12483
  }
12471
12484
  /**
12472
12485
  * Read CLAUDE_AUTOCOMPACT_PCT_OVERRIDE from the environment. Absent →
@@ -12487,22 +12500,53 @@ function resolveAutocompactPct(env = process.env) {
12487
12500
  return parsed;
12488
12501
  }
12489
12502
  /**
12490
- * Derive warmup/refresh/immediate thresholds from a fill reading, preferring
12491
- * this project's own measured compaction history over the configured
12492
- * override chain see the module comment above for why. Margins are
12493
- * clamped at 0 (and logged) for a window small enough that a margin would
12494
- * otherwise go negative a pathological input should degrade to "fire
12495
- * immediately", never to a threshold below zero.
12503
+ * Derive warmup/refresh/immediate thresholds from a fill reading.
12504
+ *
12505
+ * effectiveTrigger = min(measured, configuredChainValue) when a measured
12506
+ * value exists NOT the measured value outright. A project whose newest
12507
+ * compaction predates a regime change measures a stale-HIGH trigger: a real
12508
+ * case on this machine measured 998,267 for a project whose ACTUAL current
12509
+ * boundary (from a different project's fresher history, cross-checked
12510
+ * independently) is ~784,000 — using 998,267 directly would compute a
12511
+ * warm-up of 898,267, above the real boundary, so the handover would never
12512
+ * fire there. The measurement is honest; it is just old.
12513
+ *
12514
+ * The minimum is correct in both directions: the measured value is the
12515
+ * better estimate when it is LOWER than configured (it reflects reality the
12516
+ * configured percentage cannot know — see the module comment above, the
12517
+ * 100%-to-78% regime change this project itself lived through); it is
12518
+ * unsafe when it is HIGHER (it reflects a regime that no longer applies).
12519
+ * Taking the minimum costs nothing in the safe direction — one wasted
12520
+ * summary if the project's regime actually did move up — and prevents the
12521
+ * unsafe direction, where a stale-high measurement suppresses the handover
12522
+ * past the real boundary. That asymmetry is the same one the 80-not-100
12523
+ * default was chosen for.
12524
+ *
12525
+ * Margins below effectiveTrigger are clamped at 0 (and logged) for a window
12526
+ * small enough that a margin would otherwise go negative — a pathological
12527
+ * input should degrade to "fire immediately", never to a threshold below
12528
+ * zero.
12496
12529
  */
12497
12530
  function contextFillThresholds(reading, env = process.env, opts = {}) {
12498
12531
  const windowConfirmed = reading.source === "statusline";
12499
12532
  const autocompactPct = resolveAutocompactPct(env);
12500
12533
  const configuredTriggerTokens = Math.round(reading.windowSize * (autocompactPct / 100));
12501
- const measured = opts.measuredTrigger !== void 0 ? opts.measuredTrigger : opts.cwd ? measureCompactionTrigger(opts.cwd) : null;
12502
- const triggerSource = measured !== null ? "measured" : "configured";
12503
- const effectiveTriggerTokens = measured !== null ? measured : configuredTriggerTokens;
12504
- if (triggerSource === "measured") console.error(`[context-fill] trigger source: MEASURED — ${effectiveTriggerTokens} tokens (minimum of the most recent ${MEASURED_TRIGGER_SAMPLE_SIZE} compact_boundary events for this project; the configured chain would have given ${configuredTriggerTokens}).`);
12505
- else console.error(`[context-fill] trigger source: CONFIGURED — no compaction history for this project yet, using ${effectiveTriggerTokens} tokens (${autocompactPct}% of a ${reading.windowSize}-token window).`);
12534
+ const measuredTriggerTokens = opts.measuredTrigger !== void 0 ? opts.measuredTrigger : opts.cwd ? measureCompactionTrigger(opts.cwd) : null;
12535
+ let effectiveTriggerTokens;
12536
+ let triggerSource;
12537
+ if (measuredTriggerTokens === null) {
12538
+ effectiveTriggerTokens = configuredTriggerTokens;
12539
+ triggerSource = "configured";
12540
+ console.error(`[context-fill] trigger source: CONFIGURED — no compaction history for this project yet. measured=none, configured=${configuredTriggerTokens} (${autocompactPct}% of a ${reading.windowSize}-token window) -> using ${effectiveTriggerTokens}.`);
12541
+ } else if (measuredTriggerTokens <= configuredTriggerTokens) {
12542
+ effectiveTriggerTokens = measuredTriggerTokens;
12543
+ triggerSource = "measured";
12544
+ console.error(`[context-fill] trigger source: MEASURED — measured=${measuredTriggerTokens} (minimum of the most recent ${MEASURED_TRIGGER_SAMPLE_SIZE} compact_boundary events), configured=${configuredTriggerTokens} -> using ${effectiveTriggerTokens} (measured, ≤ configured).`);
12545
+ } else {
12546
+ effectiveTriggerTokens = configuredTriggerTokens;
12547
+ triggerSource = "measured-clamped";
12548
+ console.error(`[context-fill] trigger source: MEASURED-CLAMPED — measured=${measuredTriggerTokens} is HIGHER than configured=${configuredTriggerTokens} (a stale regime this project's history predates) -> using ${effectiveTriggerTokens} (configured, the safer bound).`);
12549
+ }
12506
12550
  const clamp = (name, value) => {
12507
12551
  if (value >= 0) return value;
12508
12552
  console.error(`[context-fill] ${name} threshold went negative (${value}) for a ${reading.windowSize}-token window — clamping to 0.`);
@@ -12513,6 +12557,8 @@ function contextFillThresholds(reading, env = process.env, opts = {}) {
12513
12557
  refreshTokens: clamp("refresh", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.refresh),
12514
12558
  immediateTokens: clamp("immediate", effectiveTriggerTokens - THRESHOLD_MARGIN_TOKENS.immediate),
12515
12559
  effectiveTriggerTokens,
12560
+ measuredTriggerTokens,
12561
+ configuredTriggerTokens,
12516
12562
  autocompactPct,
12517
12563
  triggerSource,
12518
12564
  windowConfirmed
@@ -12559,6 +12605,16 @@ function isImmediate(usedTokens, thresholds) {
12559
12605
  * derived thresholds from hooks/ts/lib/context-fill.ts; this module only
12560
12606
  * adds the "have we already fired this one" bookkeeping and the enqueue.
12561
12607
  *
12608
+ * BUG FIXED (found in live use, session 77084e72-...): a threshold used to
12609
+ * be marked fired the moment the ENQUEUE call succeeded — recording intent,
12610
+ * not outcome. When the daemon worker then failed or was restarted mid-spawn
12611
+ * (observed: a daemon restart 90s after the enqueue), no handover cache was
12612
+ * ever written, but the marker already said "done" — so the session
12613
+ * compacted with the mechanical scrape only, and would never have retried,
12614
+ * ever, for that session. A threshold is now marked CONFIRMED only after a
12615
+ * handover cache file actually appears; an enqueue with no cache to show for
12616
+ * it within PENDING_TIMEOUT_MS is treated as failed and retried.
12617
+ *
12562
12618
  * Never throws. A daemon that isn't running, a stale reading, an unknown
12563
12619
  * fill — every one of those is a reason to do nothing this check and try
12564
12620
  * again next time, not a reason to interrupt the hook that called this.
@@ -12569,24 +12625,43 @@ function isImmediate(usedTokens, thresholds) {
12569
12625
  * making the calling hook wait anywhere near as long as a normal IPC call
12570
12626
  * is allowed to. */
12571
12627
  const ENQUEUE_TIMEOUT_MS = 2e3;
12572
- /** Both thresholds fired — nothing left to check for the rest of the session. */
12628
+ /**
12629
+ * How long to wait for a handover cache to appear after an enqueue before
12630
+ * treating it as failed and retrying. Generous over the worker's own sonnet
12631
+ * timeout (120s, see session-summary-worker.ts's CLAUDE_TIMEOUT_MS) plus
12632
+ * queue latency, so a slow-but-working run isn't retried out from under
12633
+ * itself — and short enough that a genuinely dead attempt (a daemon restart
12634
+ * mid-spawn, exactly what happened in the live failure this fixes) is
12635
+ * retried well before the next compaction, not "never".
12636
+ */
12637
+ const PENDING_TIMEOUT_MS = 300 * 1e3;
12638
+ /** Both thresholds confirmed — nothing left to check for the rest of the session. */
12573
12639
  const ALL_THRESHOLDS = ["warmup", "refresh"];
12574
- function firedThresholdsPath(sessionId) {
12640
+ function triggerStatePath(sessionId) {
12575
12641
  return join(tmpdir(), `pai-context-handover-fired-${sessionId}.json`);
12576
12642
  }
12577
- function loadFiredThresholds(sessionId) {
12578
- const path = firedThresholdsPath(sessionId);
12579
- if (!existsSync(path)) return [];
12643
+ function loadTriggerState(sessionId) {
12644
+ const path = triggerStatePath(sessionId);
12645
+ if (!existsSync(path)) return {
12646
+ confirmed: [],
12647
+ pending: null
12648
+ };
12580
12649
  try {
12581
12650
  const raw = JSON.parse(readFileSync(path, "utf-8"));
12582
- return Array.isArray(raw.fired) ? raw.fired : [];
12651
+ return {
12652
+ confirmed: Array.isArray(raw.confirmed) ? raw.confirmed : [],
12653
+ pending: raw.pending && Array.isArray(raw.pending.thresholds) && typeof raw.pending.enqueuedAt === "string" ? raw.pending : null
12654
+ };
12583
12655
  } catch {
12584
- return [];
12656
+ return {
12657
+ confirmed: [],
12658
+ pending: null
12659
+ };
12585
12660
  }
12586
12661
  }
12587
- function saveFiredThresholds(sessionId, fired) {
12662
+ function saveTriggerState(sessionId, state) {
12588
12663
  try {
12589
- writeFileSync(firedThresholdsPath(sessionId), JSON.stringify({ fired }), "utf-8");
12664
+ writeFileSync(triggerStatePath(sessionId), JSON.stringify(state), "utf-8");
12590
12665
  } catch {}
12591
12666
  }
12592
12667
  async function enqueueContextHandover(payload) {
@@ -12607,30 +12682,69 @@ const defaultDeps = {
12607
12682
  sessionId: input.sessionId,
12608
12683
  transcriptPath: input.transcriptPath
12609
12684
  }),
12610
- loadFired: loadFiredThresholds,
12611
- saveFired: saveFiredThresholds,
12612
- enqueue: enqueueContextHandover
12685
+ loadState: loadTriggerState,
12686
+ saveState: saveTriggerState,
12687
+ readCache: readContextHandoverCache,
12688
+ enqueue: enqueueContextHandover,
12689
+ now: () => Date.now()
12690
+ };
12691
+ const NOTHING = {
12692
+ attempted: [],
12693
+ confirmed: []
12613
12694
  };
12614
12695
  /**
12615
- * Check this session's current context fill against the derived thresholds
12616
- * and enqueue a context-handover job for any newly-crossed one. Returns the
12617
- * threshold name(s) newly fired this call empty when nothing crossed, the
12618
- * reading is unknown, or every threshold has already fired.
12696
+ * Check this session's current context fill against the derived thresholds,
12697
+ * confirm or retry any in-flight attempt, and enqueue a context-handover job
12698
+ * for any newly-crossed threshold. Never marks a threshold done on the
12699
+ * strength of the enqueue call alone only a verified cache file does that.
12619
12700
  *
12620
12701
  * A single check that crosses both warmup and refresh in one jump (a
12621
12702
  * session first observed already close to compaction) enqueues ONE job,
12622
- * tagged with the more urgent of the two, and marks both firedone
12623
- * model-written handover already covers what either alone would have.
12703
+ * tagged with the more urgent of the two, and once confirmedmarks both
12704
+ * done together, since one model-written handover already covers what
12705
+ * either alone would have.
12624
12706
  */
12625
12707
  async function checkAndEnqueueContextHandover(input, deps = defaultDeps) {
12626
- const alreadyFired = deps.loadFired(input.sessionId);
12627
- if (ALL_THRESHOLDS.every((t) => alreadyFired.includes(t))) return [];
12708
+ let state = deps.loadState(input.sessionId);
12709
+ if (ALL_THRESHOLDS.every((t) => state.confirmed.includes(t))) return NOTHING;
12710
+ let confirmedThisCheck = [];
12711
+ if (state.pending) {
12712
+ const cache = deps.readCache(input.sessionId);
12713
+ if (cache !== null && Date.parse(cache.generatedAt) > Date.parse(state.pending.enqueuedAt)) {
12714
+ const newlyConfirmed = state.pending.thresholds.filter((t) => !state.confirmed.includes(t));
12715
+ state = {
12716
+ confirmed: [...state.confirmed, ...newlyConfirmed],
12717
+ pending: null
12718
+ };
12719
+ deps.saveState(input.sessionId, state);
12720
+ confirmedThisCheck = newlyConfirmed;
12721
+ console.error(`[context-handover-trigger] session ${input.sessionId}: confirmed [${newlyConfirmed.join(", ")}] — handover cache verified (generated ${cache.generatedAt}).`);
12722
+ if (ALL_THRESHOLDS.every((t) => state.confirmed.includes(t))) return {
12723
+ attempted: [],
12724
+ confirmed: confirmedThisCheck
12725
+ };
12726
+ } else if (deps.now() - Date.parse(state.pending.enqueuedAt) < PENDING_TIMEOUT_MS) return NOTHING;
12727
+ else {
12728
+ console.error(`[context-handover-trigger] session ${input.sessionId}: pending [${state.pending.thresholds.join(", ")}] timed out after ${PENDING_TIMEOUT_MS}ms with no handover cache written — treating as FAILED and retrying (daemon worker likely died or was restarted mid-run).`);
12729
+ state = {
12730
+ ...state,
12731
+ pending: null
12732
+ };
12733
+ deps.saveState(input.sessionId, state);
12734
+ }
12735
+ }
12628
12736
  const reading = deps.getReading(input);
12629
- if (reading.status !== "ok" || reading.usedTokens === null) return [];
12737
+ if (reading.status !== "ok" || reading.usedTokens === null) return {
12738
+ attempted: [],
12739
+ confirmed: confirmedThisCheck
12740
+ };
12630
12741
  const thresholds = contextFillThresholds(reading, process.env, { cwd: input.cwd });
12631
12742
  if (!thresholds.windowConfirmed) console.error(`[context-handover-trigger] session ${input.sessionId}: window size not confirmed — using the assumed default (${reading.windowSize} tokens) rather than a reading Claude Code reported.`);
12632
- const newlyCrossed = crossedThresholds(reading.usedTokens, thresholds, alreadyFired);
12633
- if (newlyCrossed.length === 0) return [];
12743
+ const newlyCrossed = crossedThresholds(reading.usedTokens, thresholds, state.confirmed);
12744
+ if (newlyCrossed.length === 0) return {
12745
+ attempted: [],
12746
+ confirmed: confirmedThisCheck
12747
+ };
12634
12748
  const urgent = isImmediate(reading.usedTokens, thresholds);
12635
12749
  const mostUrgent = newlyCrossed[newlyCrossed.length - 1];
12636
12750
  try {
@@ -12642,12 +12756,25 @@ async function checkAndEnqueueContextHandover(input, deps = defaultDeps) {
12642
12756
  urgent
12643
12757
  });
12644
12758
  } catch (err) {
12645
- console.error(`[context-handover-trigger] session ${input.sessionId}: enqueue failed, will retry next check: ${err}`);
12646
- return [];
12759
+ console.error(`[context-handover-trigger] session ${input.sessionId}: enqueue FAILED will retry next check: ${err}`);
12760
+ return {
12761
+ attempted: [],
12762
+ confirmed: confirmedThisCheck
12763
+ };
12647
12764
  }
12648
- deps.saveFired(input.sessionId, [...alreadyFired, ...newlyCrossed]);
12649
- console.error(`[context-handover-trigger] session ${input.sessionId}: crossed [${newlyCrossed.join(", ")}] at ${reading.usedTokens} tokens (trigger=${thresholds.effectiveTriggerTokens}, triggerSource=${thresholds.triggerSource}, autocompactPct=${thresholds.autocompactPct}, urgent=${urgent}).`);
12650
- return newlyCrossed;
12765
+ state = {
12766
+ ...state,
12767
+ pending: {
12768
+ thresholds: newlyCrossed,
12769
+ enqueuedAt: new Date(deps.now()).toISOString()
12770
+ }
12771
+ };
12772
+ deps.saveState(input.sessionId, state);
12773
+ console.error(`[context-handover-trigger] session ${input.sessionId}: enqueued [${newlyCrossed.join(", ")}] at ${reading.usedTokens} tokens (trigger=${thresholds.effectiveTriggerTokens}, triggerSource=${thresholds.triggerSource}, autocompactPct=${thresholds.autocompactPct}, urgent=${urgent}) — awaiting outcome confirmation before marking done.`);
12774
+ return {
12775
+ attempted: newlyCrossed,
12776
+ confirmed: confirmedThisCheck
12777
+ };
12651
12778
  }
12652
12779
 
12653
12780
  //#endregion
@@ -15153,4 +15280,4 @@ claude() {
15153
15280
 
15154
15281
  //#endregion
15155
15282
  export { drainStdio as n, buildProgram as t };
15156
- //# sourceMappingURL=program-BnMNFb4O.mjs.map
15283
+ //# sourceMappingURL=program-C-fUghPv.mjs.map