@tekmidian/pai 0.50.0 → 0.51.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 +1 -1
- package/dist/cli/program.mjs +1 -1
- package/dist/daemon/index.mjs +1 -1
- package/dist/{daemon-CBTQYtHF.mjs → daemon-Bhs7kk0a.mjs} +9 -5
- package/dist/daemon-Bhs7kk0a.mjs.map +1 -0
- package/dist/{daemon-Vf7pHnjR.mjs → daemon-pX0-MiAH.mjs} +1 -1
- package/dist/hooks/context-compression-hook.mjs.map +2 -2
- package/dist/hooks/post-compact-inject.mjs.map +2 -2
- package/dist/{program-BX67-4I2.mjs → program-BQaDYGNC.mjs} +5569 -5334
- package/dist/program-BQaDYGNC.mjs.map +1 -0
- package/docs/commands/audit.md +1 -1
- package/package.json +1 -1
- package/src/hooks/ts/lib/context-fill.test.ts +124 -46
- package/src/hooks/ts/lib/context-fill.ts +57 -14
- package/statusline-command.sh +22 -10
- package/dist/daemon-CBTQYtHF.mjs.map +0 -1
- package/dist/program-BX67-4I2.mjs.map +0 -1
package/docs/commands/audit.md
CHANGED
|
@@ -58,7 +58,7 @@ Cache/input/output token split for a session transcript (default: newest)
|
|
|
58
58
|
|
|
59
59
|
| Option | Description | Default |
|
|
60
60
|
|--------|-------------|---------|
|
|
61
|
-
| `--ctx-threshold <n>` |
|
|
61
|
+
| `--ctx-threshold <n>` | Override the derived compaction trigger (default: derived from the session's own window and measured/configured trigger — see 'window'/'trigger' in the report header) | |
|
|
62
62
|
| `--history [n]` | List first-turn context of the newest n sessions in this project's transcript directory (default 20) | |
|
|
63
63
|
| `--turn <n>` | Attribute API call n's context growth to the transcript lines injected since the previous call | |
|
|
64
64
|
|
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
formatContextFill,
|
|
12
12
|
statuslineStateFilePath,
|
|
13
13
|
contextFillThresholds,
|
|
14
|
+
configuredTrigger,
|
|
14
15
|
resolveAutocompactPct,
|
|
15
16
|
measureCompactionTrigger,
|
|
16
17
|
modelFamily,
|
|
@@ -269,40 +270,33 @@ function readingAt(windowSize: number, source: ContextFillReading["source"] = "s
|
|
|
269
270
|
}
|
|
270
271
|
|
|
271
272
|
describe("contextFillThresholds", () => {
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
// than an ABSENT override that defaults to the same 80, which the next
|
|
278
|
-
// fixture below requires to be 700,000. Asserting 684,000 here would only
|
|
279
|
-
// be possible by special-casing "override was explicitly set" versus
|
|
280
|
-
// "override defaulted", which the formula gives no basis for. Testing the
|
|
281
|
-
// formula as specified instead: both explicit-80 and absent-defaults-to-80
|
|
282
|
-
// must agree, and they do.
|
|
283
|
-
it("derives warmup from windowSize * (override/100) - 100k, for an explicit override", () => {
|
|
273
|
+
// effectiveTrigger = configuredTrigger(windowSize, pct) = pct/100 *
|
|
274
|
+
// (windowSize - 20,000) — NOT windowSize * pct/100. An EXPLICIT override
|
|
275
|
+
// of 80 and an ABSENT override that defaults to the same 80 must agree,
|
|
276
|
+
// and they do: both give 784,000 on a 1M window, warmUp 684,000.
|
|
277
|
+
it("derives warmup from configuredTrigger(windowSize, override) - 100k, for an explicit override", () => {
|
|
284
278
|
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "80" });
|
|
285
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
286
|
-
expect(t.warmupTokens).toBe(
|
|
279
|
+
expect(t.effectiveTriggerTokens).toBe(784_000);
|
|
280
|
+
expect(t.warmupTokens).toBe(684_000);
|
|
287
281
|
});
|
|
288
282
|
|
|
289
283
|
it("derives the same effective trigger when the override is absent (defaults to 80)", () => {
|
|
290
284
|
const t = contextFillThresholds(readingAt(1_000_000), {});
|
|
291
285
|
expect(t.autocompactPct).toBe(80);
|
|
292
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
293
|
-
expect(t.warmupTokens).toBe(
|
|
286
|
+
expect(t.effectiveTriggerTokens).toBe(784_000);
|
|
287
|
+
expect(t.warmupTokens).toBe(684_000);
|
|
294
288
|
});
|
|
295
289
|
|
|
296
290
|
it("scales down correctly for a 200k window with the override absent", () => {
|
|
297
291
|
const t = contextFillThresholds(readingAt(200_000), {});
|
|
298
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
299
|
-
expect(t.warmupTokens).toBe(
|
|
292
|
+
expect(t.effectiveTriggerTokens).toBe(144_000);
|
|
293
|
+
expect(t.warmupTokens).toBe(44_000);
|
|
300
294
|
});
|
|
301
295
|
|
|
302
296
|
it("computes refresh 40k and immediate 15k below the effective trigger", () => {
|
|
303
297
|
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "80" });
|
|
304
|
-
expect(t.refreshTokens).toBe(
|
|
305
|
-
expect(t.immediateTokens).toBe(
|
|
298
|
+
expect(t.refreshTokens).toBe(744_000);
|
|
299
|
+
expect(t.immediateTokens).toBe(769_000);
|
|
306
300
|
});
|
|
307
301
|
|
|
308
302
|
it("marks the window confirmed only for a statusline-sourced reading", () => {
|
|
@@ -313,8 +307,15 @@ describe("contextFillThresholds", () => {
|
|
|
313
307
|
it("clamps a threshold that would go negative instead of returning it", () => {
|
|
314
308
|
// A tiny window where even the trigger itself is below the warmup margin.
|
|
315
309
|
const t = contextFillThresholds(readingAt(50_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "80" });
|
|
316
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
317
|
-
expect(t.warmupTokens).toBe(0); //
|
|
310
|
+
expect(t.effectiveTriggerTokens).toBe(24_000); // 0.8 * (50,000 - 20,000)
|
|
311
|
+
expect(t.warmupTokens).toBe(0); // 24,000 - 100,000 would be negative
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
describe("configuredTrigger", () => {
|
|
316
|
+
it("is pct/100 * (windowSize - 20,000), not windowSize * pct/100", () => {
|
|
317
|
+
expect(configuredTrigger(1_000_000, 80)).toBe(784_000);
|
|
318
|
+
expect(configuredTrigger(200_000, 80)).toBe(144_000);
|
|
318
319
|
});
|
|
319
320
|
});
|
|
320
321
|
|
|
@@ -570,8 +571,8 @@ describe("contextFillThresholds — trigger source", () => {
|
|
|
570
571
|
it("reports triggerSource 'configured' and falls back to the env chain when there is no measured value", () => {
|
|
571
572
|
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "80" }, { measuredTrigger: null });
|
|
572
573
|
expect(t.triggerSource).toBe("configured");
|
|
573
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
574
|
-
expect(t.warmupTokens).toBe(
|
|
574
|
+
expect(t.effectiveTriggerTokens).toBe(784_000);
|
|
575
|
+
expect(t.warmupTokens).toBe(684_000);
|
|
575
576
|
});
|
|
576
577
|
|
|
577
578
|
it("falls back to configured when cwd is given but the project has no compaction history (real scan, empty fixture)", () => {
|
|
@@ -583,7 +584,7 @@ describe("contextFillThresholds — trigger source", () => {
|
|
|
583
584
|
measuredTrigger: measureCompactionTrigger(cwd, projectsDir),
|
|
584
585
|
});
|
|
585
586
|
expect(t.triggerSource).toBe("configured");
|
|
586
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
587
|
+
expect(t.effectiveTriggerTokens).toBe(784_000);
|
|
587
588
|
} finally {
|
|
588
589
|
rmSync(projectsDir, { recursive: true, force: true });
|
|
589
590
|
}
|
|
@@ -600,27 +601,27 @@ describe("contextFillThresholds — trigger source", () => {
|
|
|
600
601
|
|
|
601
602
|
it("CLAMPS a stale-HIGH measured value down to the configured one — the real CaseLeaf case", () => {
|
|
602
603
|
// measured=998,267 (this project's real, but stale, most-recent trigger)
|
|
603
|
-
// configured=
|
|
604
|
+
// configured=784,000 (80% of a 1,000,000 window, platform formula)
|
|
604
605
|
const t = contextFillThresholds(readingAt(1_000_000), {}, { measuredTrigger: 998_267 });
|
|
605
606
|
expect(t.measuredTriggerTokens).toBe(998_267);
|
|
606
|
-
expect(t.configuredTriggerTokens).toBe(
|
|
607
|
+
expect(t.configuredTriggerTokens).toBe(784_000);
|
|
607
608
|
expect(t.triggerSource).toBe("measured-clamped");
|
|
608
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
609
|
-
expect(t.warmupTokens).toBe(
|
|
609
|
+
expect(t.effectiveTriggerTokens).toBe(784_000); // min(998267, 784000)
|
|
610
|
+
expect(t.warmupTokens).toBe(684_000);
|
|
610
611
|
});
|
|
611
612
|
|
|
612
613
|
it("uses the measured value directly when it is LOWER than configured — no clamp needed", () => {
|
|
613
|
-
// measured=
|
|
614
|
-
const t = contextFillThresholds(readingAt(1_000_000), {}, { measuredTrigger:
|
|
614
|
+
// measured=700,000, configured=784,000 -> measured wins, tighter warm-up.
|
|
615
|
+
const t = contextFillThresholds(readingAt(1_000_000), {}, { measuredTrigger: 700_000 });
|
|
615
616
|
expect(t.triggerSource).toBe("measured");
|
|
616
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
617
|
-
expect(t.warmupTokens).toBe(
|
|
617
|
+
expect(t.effectiveTriggerTokens).toBe(700_000);
|
|
618
|
+
expect(t.warmupTokens).toBe(600_000);
|
|
618
619
|
});
|
|
619
620
|
|
|
620
621
|
it("uses measured directly when it exactly equals configured (boundary case, not clamped)", () => {
|
|
621
|
-
const t = contextFillThresholds(readingAt(1_000_000), {}, { measuredTrigger:
|
|
622
|
+
const t = contextFillThresholds(readingAt(1_000_000), {}, { measuredTrigger: 784_000 });
|
|
622
623
|
expect(t.triggerSource).toBe("measured");
|
|
623
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
624
|
+
expect(t.effectiveTriggerTokens).toBe(784_000);
|
|
624
625
|
});
|
|
625
626
|
|
|
626
627
|
it("reports both raw values on the result even when one of them wasn't used, so the clamp is visible", () => {
|
|
@@ -628,7 +629,7 @@ describe("contextFillThresholds — trigger source", () => {
|
|
|
628
629
|
// Both numbers are on the object — a caller can see what was measured
|
|
629
630
|
// AND what was configured, not just the winner.
|
|
630
631
|
expect(t.measuredTriggerTokens).toBe(998_267);
|
|
631
|
-
expect(t.configuredTriggerTokens).toBe(
|
|
632
|
+
expect(t.configuredTriggerTokens).toBe(784_000);
|
|
632
633
|
});
|
|
633
634
|
});
|
|
634
635
|
|
|
@@ -642,36 +643,113 @@ describe("contextFillThresholds — trigger source", () => {
|
|
|
642
643
|
// ---------------------------------------------------------------------------
|
|
643
644
|
|
|
644
645
|
describe("contextFillThresholds — 2026-09-20 regime change (override 80 -> 20)", () => {
|
|
645
|
-
it("measured 784k + configured
|
|
646
|
+
it("measured 784k + configured 196k (override=20, 1M window) -> clamps to 196k", () => {
|
|
646
647
|
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "20" }, { measuredTrigger: 784_000 });
|
|
647
|
-
expect(t.configuredTriggerTokens).toBe(
|
|
648
|
+
expect(t.configuredTriggerTokens).toBe(196_000); // 0.2 * (1,000,000 - 20,000)
|
|
648
649
|
expect(t.triggerSource).toBe("measured-clamped");
|
|
649
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
650
|
-
expect(t.warmupTokens).toBe(
|
|
650
|
+
expect(t.effectiveTriggerTokens).toBe(196_000);
|
|
651
|
+
expect(t.warmupTokens).toBe(96_000); // 196,000 - 100,000
|
|
651
652
|
});
|
|
652
653
|
|
|
653
|
-
it("measured 784k + configured
|
|
654
|
+
it("measured 784k + configured 784k (override=80, 1M window) -> measured wins at 784k", () => {
|
|
654
655
|
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "80" }, { measuredTrigger: 784_000 });
|
|
655
|
-
expect(t.configuredTriggerTokens).toBe(
|
|
656
|
+
expect(t.configuredTriggerTokens).toBe(784_000);
|
|
656
657
|
expect(t.triggerSource).toBe("measured");
|
|
657
658
|
expect(t.effectiveTriggerTokens).toBe(784_000);
|
|
658
659
|
});
|
|
659
660
|
|
|
660
|
-
it("measured null + configured
|
|
661
|
+
it("measured null + configured 196k (override=20, no compaction history yet) -> 196k", () => {
|
|
661
662
|
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "20" }, { measuredTrigger: null });
|
|
662
663
|
expect(t.triggerSource).toBe("configured");
|
|
663
|
-
expect(t.effectiveTriggerTokens).toBe(
|
|
664
|
+
expect(t.effectiveTriggerTokens).toBe(196_000);
|
|
664
665
|
});
|
|
665
666
|
|
|
666
|
-
it("measured 784k + override unset (defaults to 80, configured
|
|
667
|
+
it("measured 784k + override unset (defaults to 80, configured 784k) -> measured wins at 784k", () => {
|
|
667
668
|
const t = contextFillThresholds(readingAt(1_000_000), {}, { measuredTrigger: 784_000 });
|
|
668
669
|
expect(t.autocompactPct).toBe(80);
|
|
669
|
-
expect(t.configuredTriggerTokens).toBe(
|
|
670
|
+
expect(t.configuredTriggerTokens).toBe(784_000);
|
|
670
671
|
expect(t.triggerSource).toBe("measured");
|
|
671
672
|
expect(t.effectiveTriggerTokens).toBe(784_000);
|
|
672
673
|
});
|
|
673
674
|
});
|
|
674
675
|
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
// measureCompactionTrigger — admissibility filter (Fault 1, 2026-09-20):
|
|
678
|
+
// samples from a different override regime must not win just because they
|
|
679
|
+
// are numerically <= the CURRENT configured trigger. A compaction that
|
|
680
|
+
// fired far below the current configured trigger came from a lower
|
|
681
|
+
// override or a smaller window; it is discarded, not averaged in.
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
|
|
684
|
+
describe("measureCompactionTrigger — other-regime samples are discarded", () => {
|
|
685
|
+
it("discards all samples below half of configured and falls back to configured (measured=null, source configured)", () => {
|
|
686
|
+
const projectsDir = mkdtempSync(join(tmpdir(), "pai-measured-trigger-test-"));
|
|
687
|
+
const cwd = "/fake/project/other-regime-all-discarded";
|
|
688
|
+
const projectDir = join(projectsDir, encodeForFixture(cwd));
|
|
689
|
+
mkdirSync(projectDir, { recursive: true });
|
|
690
|
+
writeFileSync(
|
|
691
|
+
join(projectDir, "a.jsonl"),
|
|
692
|
+
[
|
|
693
|
+
compactBoundaryLine(196_765, "2026-09-20T13:10:00.000Z", "u1"),
|
|
694
|
+
compactBoundaryLine(195_075, "2026-09-20T15:00:00.000Z", "u2"),
|
|
695
|
+
compactBoundaryLine(197_902, "2026-09-20T19:00:00.000Z", "u3"),
|
|
696
|
+
].join("\n") + "\n"
|
|
697
|
+
);
|
|
698
|
+
try {
|
|
699
|
+
// configured = configuredTrigger(1,000,000, 80) = 784,000; floor = 392,000.
|
|
700
|
+
const measured = measureCompactionTrigger(cwd, projectsDir, undefined, 784_000);
|
|
701
|
+
expect(measured).toBeNull();
|
|
702
|
+
|
|
703
|
+
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "80" }, { measuredTrigger: measured });
|
|
704
|
+
expect(t.triggerSource).toBe("configured");
|
|
705
|
+
expect(t.effectiveTriggerTokens).toBe(784_000);
|
|
706
|
+
} finally {
|
|
707
|
+
rmSync(projectsDir, { recursive: true, force: true });
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it("keeps admissible samples and discards only the other-regime one (measured=780,000, source measured)", () => {
|
|
712
|
+
const projectsDir = mkdtempSync(join(tmpdir(), "pai-measured-trigger-test-"));
|
|
713
|
+
const cwd = "/fake/project/other-regime-partial";
|
|
714
|
+
const projectDir = join(projectsDir, encodeForFixture(cwd));
|
|
715
|
+
mkdirSync(projectDir, { recursive: true });
|
|
716
|
+
writeFileSync(
|
|
717
|
+
join(projectDir, "a.jsonl"),
|
|
718
|
+
[
|
|
719
|
+
compactBoundaryLine(780_000, "2026-09-19T09:00:00.000Z", "u1"),
|
|
720
|
+
compactBoundaryLine(790_000, "2026-09-19T10:00:00.000Z", "u2"),
|
|
721
|
+
compactBoundaryLine(195_000, "2026-09-20T15:00:00.000Z", "u3"), // other-regime, discarded
|
|
722
|
+
].join("\n") + "\n"
|
|
723
|
+
);
|
|
724
|
+
try {
|
|
725
|
+
const measured = measureCompactionTrigger(cwd, projectsDir, undefined, 784_000);
|
|
726
|
+
expect(measured).toBe(780_000);
|
|
727
|
+
|
|
728
|
+
const t = contextFillThresholds(readingAt(1_000_000), { CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "80" }, { measuredTrigger: measured });
|
|
729
|
+
expect(t.triggerSource).toBe("measured");
|
|
730
|
+
expect(t.effectiveTriggerTokens).toBe(780_000);
|
|
731
|
+
} finally {
|
|
732
|
+
rmSync(projectsDir, { recursive: true, force: true });
|
|
733
|
+
}
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
it("applies no filter at all when configuredTriggerTokens is omitted (back-compat with the raw scan)", () => {
|
|
737
|
+
const projectsDir = mkdtempSync(join(tmpdir(), "pai-measured-trigger-test-"));
|
|
738
|
+
const cwd = "/fake/project/no-filter";
|
|
739
|
+
const projectDir = join(projectsDir, encodeForFixture(cwd));
|
|
740
|
+
mkdirSync(projectDir, { recursive: true });
|
|
741
|
+
writeFileSync(
|
|
742
|
+
join(projectDir, "a.jsonl"),
|
|
743
|
+
compactBoundaryLine(195_000, "2026-09-20T15:00:00.000Z", "u1") + "\n"
|
|
744
|
+
);
|
|
745
|
+
try {
|
|
746
|
+
expect(measureCompactionTrigger(cwd, projectsDir)).toBe(195_000);
|
|
747
|
+
} finally {
|
|
748
|
+
rmSync(projectsDir, { recursive: true, force: true });
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
|
|
675
753
|
// ---------------------------------------------------------------------------
|
|
676
754
|
// Foreign-model transcripts — a headless worker on another provider (a
|
|
677
755
|
// different context window) shares the project folder; its compactions must
|
|
@@ -294,7 +294,8 @@ export function formatContextFill(reading: ContextFillReading): FillDisplay {
|
|
|
294
294
|
// The configured chain — env override, then a default — is the fallback,
|
|
295
295
|
// used ONLY when a project has no compaction history yet:
|
|
296
296
|
//
|
|
297
|
-
// effectiveTrigger = measured ?? (windowSize
|
|
297
|
+
// effectiveTrigger = measured ?? configuredTrigger(windowSize, override ?? 80)
|
|
298
|
+
// configuredTrigger(windowSize, pct) = pct/100 * (windowSize - 20,000)
|
|
298
299
|
//
|
|
299
300
|
// DEFAULT IS 80, NOT 100, WHEN NOTHING IS KNOWN. 100 was considered — it
|
|
300
301
|
// matches the FIRST regime above — and rejected: the costs are asymmetric.
|
|
@@ -313,6 +314,17 @@ export function formatContextFill(reading: ContextFillReading): FillDisplay {
|
|
|
313
314
|
// waiting for one.
|
|
314
315
|
export const DEFAULT_AUTOCOMPACT_PCT = 80;
|
|
315
316
|
|
|
317
|
+
/**
|
|
318
|
+
* The platform's actual compaction trigger for a window/pct pair. NOT
|
|
319
|
+
* windowSize * pct/100 — that formula gives 800,000 on 1M/80 and 160,000 on
|
|
320
|
+
* 200k/80, but the platform fires at pct/100 * (windowSize - 20,000):
|
|
321
|
+
* 784,000 and 144,000, measured on every compact_boundary on this machine,
|
|
322
|
+
* 2026-09-20 (see the module comment above).
|
|
323
|
+
*/
|
|
324
|
+
export function configuredTrigger(windowSize: number, pct: number): number {
|
|
325
|
+
return Math.round((pct / 100) * (windowSize - 20_000));
|
|
326
|
+
}
|
|
327
|
+
|
|
316
328
|
/** How many of the most recent compact_boundary events to consider, and to
|
|
317
329
|
* take the minimum of. */
|
|
318
330
|
const MEASURED_TRIGGER_SAMPLE_SIZE = 3;
|
|
@@ -575,26 +587,57 @@ export function selectedCompactionSamples(
|
|
|
575
587
|
|
|
576
588
|
/**
|
|
577
589
|
* The measured compaction trigger for a project, or null when it has no
|
|
578
|
-
* compaction history yet (a brand-new project,
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
590
|
+
* compaction history yet (a brand-new project, one whose transcripts this
|
|
591
|
+
* process cannot read, or one whose only recent samples belong to a
|
|
592
|
+
* different override regime — see the admissibility filter below).
|
|
593
|
+
* Minimum of the most recent MEASURED_TRIGGER_SAMPLE_SIZE DISTINCT
|
|
594
|
+
* compact_boundary events, ordered by the events' own timestamps across
|
|
595
|
+
* every transcript the project has (live and archived) — see the module
|
|
596
|
+
* comment above for why minimum, not mean, and readCompactBoundarySamples
|
|
597
|
+
* for why "distinct" and "own timestamp" both matter (a file-mtime-ordered,
|
|
598
|
+
* non-deduplicated version of this returned a stale pre-regime-change
|
|
599
|
+
* trigger on real project data).
|
|
600
|
+
*
|
|
601
|
+
* When `configuredTriggerTokens` is given, a sample is admissible only when
|
|
602
|
+
* preTokens >= 0.5 * configuredTriggerTokens — a compaction that fired far
|
|
603
|
+
* below the currently configured trigger came from a lower override or a
|
|
604
|
+
* smaller window, a different regime than the one in force now, and must
|
|
605
|
+
* not drag the trigger down with it. Real case, 2026-09-20:
|
|
606
|
+
* CLAUDE_AUTOCOMPACT_PCT_OVERRIDE was 20 for seven hours, this project
|
|
607
|
+
* compacted three times at ~195k-198k under it, then the override went
|
|
608
|
+
* back to 80 (configured ~784k) — those three samples are inadmissible and
|
|
609
|
+
* must be discarded, not averaged into the new regime's trigger, or the
|
|
610
|
+
* handover would fire at ~155k for days until three fresh ~784k
|
|
611
|
+
* compactions pushed them out on their own.
|
|
586
612
|
*/
|
|
587
613
|
export function measureCompactionTrigger(
|
|
588
614
|
cwd: string,
|
|
589
615
|
projectsDir: string = CLAUDE_PROJECTS_DIR,
|
|
590
|
-
configPath?: string
|
|
616
|
+
configPath?: string,
|
|
617
|
+
configuredTriggerTokens?: number
|
|
591
618
|
): number | null {
|
|
592
619
|
const samples = selectedCompactionSamples(cwd, projectsDir, configPath);
|
|
593
620
|
if (samples.length === 0) return null;
|
|
594
|
-
|
|
621
|
+
|
|
622
|
+
let admissible = samples;
|
|
623
|
+
if (configuredTriggerTokens !== undefined) {
|
|
624
|
+
const floor = 0.5 * configuredTriggerTokens;
|
|
625
|
+
admissible = samples.filter((s) => s.preTokens >= floor);
|
|
626
|
+
const discarded = samples.length - admissible.length;
|
|
627
|
+
if (discarded > 0) {
|
|
628
|
+
console.error(
|
|
629
|
+
`[context-fill] discarded ${discarded} compaction sample(s) as other-regime for ${cwd} ` +
|
|
630
|
+
`(preTokens below ${Math.round(floor)}, half of configured=${configuredTriggerTokens}): ` +
|
|
631
|
+
samples.filter((s) => s.preTokens < floor).map((s) => `${s.preTokens}@${s.timestamp}`).join(", ")
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (admissible.length === 0) return null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const trigger = Math.min(...admissible.map((s) => s.preTokens));
|
|
595
638
|
console.error(
|
|
596
639
|
`[context-fill] measured trigger for ${cwd}: ${trigger} (minimum of ` +
|
|
597
|
-
|
|
640
|
+
admissible.map((s) => `${s.preTokens}@${s.timestamp}`).join(", ") + ")"
|
|
598
641
|
);
|
|
599
642
|
return trigger;
|
|
600
643
|
}
|
|
@@ -717,12 +760,12 @@ export function contextFillThresholds(
|
|
|
717
760
|
): ContextFillThresholds {
|
|
718
761
|
const windowConfirmed = reading.source === "statusline";
|
|
719
762
|
const autocompactPct = resolveAutocompactPct(env);
|
|
720
|
-
const configuredTriggerTokens =
|
|
763
|
+
const configuredTriggerTokens = configuredTrigger(reading.windowSize, autocompactPct);
|
|
721
764
|
|
|
722
765
|
const measuredTriggerTokens = opts.measuredTrigger !== undefined
|
|
723
766
|
? opts.measuredTrigger
|
|
724
767
|
: opts.cwd
|
|
725
|
-
? measureCompactionTrigger(opts.cwd)
|
|
768
|
+
? measureCompactionTrigger(opts.cwd, CLAUDE_PROJECTS_DIR, undefined, configuredTriggerTokens)
|
|
726
769
|
: null;
|
|
727
770
|
|
|
728
771
|
let effectiveTriggerTokens: number;
|
package/statusline-command.sh
CHANGED
|
@@ -699,8 +699,12 @@ if [ "$session_provider" = "anthropic" ]; then
|
|
|
699
699
|
remaining_secs=$((seven_reset_epoch - now_epoch))
|
|
700
700
|
[ "$remaining_secs" -lt 0 ] && remaining_secs=0
|
|
701
701
|
elapsed_secs=$((window_secs - remaining_secs))
|
|
702
|
-
# Daily pace: actual spend/day vs
|
|
703
|
-
# Budget = remaining capacity / remaining days
|
|
702
|
+
# Daily pace: actual spend/day vs the flat weekly share.
|
|
703
|
+
# Budget = static 100/7 (not remaining capacity / remaining days): the
|
|
704
|
+
# user reads "1d" as "what I may spend today", a fixed share of the
|
|
705
|
+
# week, not a figure that shrinks as the week is used — dividing the
|
|
706
|
+
# shrinking remainder by the shrinking remaining days double-counted
|
|
707
|
+
# overspend and drove the pace red for no new reason.
|
|
704
708
|
elapsed_days_x10=$((elapsed_secs * 10 / 86400))
|
|
705
709
|
# A per-day rate divided by a fraction of a day is not a rate, it is an
|
|
706
710
|
# extrapolation from noise: an hour into a fresh window the old clamp to
|
|
@@ -708,10 +712,7 @@ if [ "$session_provider" = "anthropic" ]; then
|
|
|
708
712
|
# Below half a day there is nothing to pace against yet — say nothing.
|
|
709
713
|
if [ "$elapsed_days_x10" -ge 5 ]; then
|
|
710
714
|
spend_per_day=$((seven_day_int * 10 / elapsed_days_x10))
|
|
711
|
-
|
|
712
|
-
[ "$remaining_days_x10" -lt 1 ] && remaining_days_x10=1
|
|
713
|
-
remaining_budget=$((100 - seven_day_int))
|
|
714
|
-
budget_per_day=$((remaining_budget * 10 / remaining_days_x10))
|
|
715
|
+
budget_per_day=$((100 / 7))
|
|
715
716
|
# Color: green = under budget, orange = near budget, red = over budget
|
|
716
717
|
overspend=$((spend_per_day - budget_per_day))
|
|
717
718
|
if [ "$overspend" -le -3 ] 2>/dev/null; then
|
|
@@ -903,10 +904,21 @@ if [ "$session_provider" != "anthropic" ]; then
|
|
|
903
904
|
fi
|
|
904
905
|
|
|
905
906
|
# LINE 3 - Context meter + usage limits
|
|
906
|
-
# Auto-compact remaining: how much context left until compaction triggers
|
|
907
|
+
# Auto-compact remaining: how much context left until compaction triggers.
|
|
908
|
+
# Claude Code fires the auto-compact hook at pct/100 * (window - 20000)
|
|
909
|
+
# tokens, not at pct% of the raw window — the first 20k tokens of any window
|
|
910
|
+
# are reserved and never count toward the trigger. Computing "% left" against
|
|
911
|
+
# the raw window (as a plain threshold-context_pct subtraction did) reads as
|
|
912
|
+
# "the window is full" on a 1M-token session that still has 80% of its real
|
|
913
|
+
# capacity free. Work in tokens, then re-express as a percent of the real
|
|
914
|
+
# window so the figure is comparable across window sizes.
|
|
907
915
|
ac_threshold="${CLAUDE_AUTOCOMPACT_PCT_OVERRIDE:-80}"
|
|
908
|
-
|
|
909
|
-
|
|
916
|
+
ac_trigger_tokens=$((ac_threshold * (context_size - 20000) / 100))
|
|
917
|
+
ac_used_tokens=$((context_pct * context_size / 100))
|
|
918
|
+
ac_left_tokens=$((ac_trigger_tokens - ac_used_tokens))
|
|
919
|
+
[ "$ac_left_tokens" -lt 0 ] && ac_left_tokens=0
|
|
920
|
+
ac_remaining=$((ac_left_tokens * 100 / context_size))
|
|
921
|
+
ac_left_k=$((ac_left_tokens / 1000))
|
|
910
922
|
# Color the remaining %: red ≤5, yellow ≤15, green otherwise
|
|
911
923
|
if [ "$ac_remaining" -le 5 ] 2>/dev/null; then
|
|
912
924
|
ac_color="$BRIGHT_RED"
|
|
@@ -915,7 +927,7 @@ elif [ "$ac_remaining" -le 15 ] 2>/dev/null; then
|
|
|
915
927
|
else
|
|
916
928
|
ac_color="$BRIGHT_GREEN"
|
|
917
929
|
fi
|
|
918
|
-
ac_suffix=" ${ac_color}(${ac_remaining}%% left)${RESET}"
|
|
930
|
+
ac_suffix=" ${ac_color}(${ac_remaining}%% left, ${ac_left_k}K)${RESET}"
|
|
919
931
|
|
|
920
932
|
if [ "$context_pct" -gt 0 ] 2>/dev/null; then
|
|
921
933
|
# Color based on usage: green < 50%, yellow 50-75%, red > 75%
|