@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.
- package/dist/cli/index.mjs +2 -2
- package/dist/cli/program.mjs +2 -2
- package/dist/{checkpoint-block-DKYxCkBL.mjs → context-handover-cache-PtNvj_8D.mjs} +34 -3
- package/dist/context-handover-cache-PtNvj_8D.mjs.map +1 -0
- package/dist/daemon/index.mjs +3 -3
- package/dist/{daemon--N2JFnUs.mjs → daemon-BZ93KRBo.mjs} +4 -4
- package/dist/{daemon--N2JFnUs.mjs.map → daemon-BZ93KRBo.mjs.map} +1 -1
- package/dist/{daemon-DJEFqV84.mjs → daemon-DRdoA489.mjs} +3 -3
- package/dist/hooks/context-compression-hook.mjs +18 -10
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/{program-BnMNFb4O.mjs → program-C-fUghPv.mjs} +200 -73
- package/dist/{program-BnMNFb4O.mjs.map → program-C-fUghPv.mjs.map} +1 -1
- package/dist/{work-queue-worker-228XjABm.mjs → work-queue-worker-B8W8_3Rn.mjs} +23 -55
- package/dist/work-queue-worker-B8W8_3Rn.mjs.map +1 -0
- package/dist/{work-queue-worker-LRA9Fj9z.mjs → work-queue-worker-HN2Ufg-L.mjs} +2 -2
- package/package.json +1 -1
- package/src/hooks/ts/lib/context-fill.test.ts +117 -4
- package/src/hooks/ts/lib/context-fill.ts +136 -53
- package/src/hooks/ts/pre-compact/context-compression-hook.ts +37 -18
- package/dist/checkpoint-block-DKYxCkBL.mjs.map +0 -1
- package/dist/work-queue-worker-228XjABm.mjs.map +0 -1
|
@@ -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
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
12456
|
-
|
|
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
|
|
12463
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
12491
|
-
*
|
|
12492
|
-
*
|
|
12493
|
-
*
|
|
12494
|
-
*
|
|
12495
|
-
*
|
|
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
|
|
12502
|
-
|
|
12503
|
-
|
|
12504
|
-
if (
|
|
12505
|
-
|
|
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
|
-
/**
|
|
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
|
|
12640
|
+
function triggerStatePath(sessionId) {
|
|
12575
12641
|
return join(tmpdir(), `pai-context-handover-fired-${sessionId}.json`);
|
|
12576
12642
|
}
|
|
12577
|
-
function
|
|
12578
|
-
const path =
|
|
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
|
|
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
|
|
12662
|
+
function saveTriggerState(sessionId, state) {
|
|
12588
12663
|
try {
|
|
12589
|
-
writeFileSync(
|
|
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
|
-
|
|
12611
|
-
|
|
12612
|
-
|
|
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
|
|
12617
|
-
*
|
|
12618
|
-
*
|
|
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
|
|
12623
|
-
* model-written handover already covers what
|
|
12703
|
+
* tagged with the more urgent of the two, and — once confirmed — marks 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
|
-
|
|
12627
|
-
if (ALL_THRESHOLDS.every((t) =>
|
|
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,
|
|
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
|
|
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
|
-
|
|
12649
|
-
|
|
12650
|
-
|
|
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-
|
|
15283
|
+
//# sourceMappingURL=program-C-fUghPv.mjs.map
|