@aragon-agent/cli 0.6.4 → 0.6.7

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +108 -11
  3. package/dist/agent/controller.d.ts +39 -2
  4. package/dist/agent/controller.js +125 -0
  5. package/dist/agent/reducer.d.ts +33 -19
  6. package/dist/agent/reducer.js +62 -71
  7. package/dist/agent/usage.d.ts +34 -0
  8. package/dist/agent/usage.js +43 -0
  9. package/dist/cli.js +48 -1
  10. package/dist/commands/builtins.js +9 -0
  11. package/dist/compaction/compactor.d.ts +60 -24
  12. package/dist/compaction/compactor.js +89 -61
  13. package/dist/compaction/context-command.d.ts +33 -0
  14. package/dist/compaction/context-command.js +166 -0
  15. package/dist/compaction/meter.d.ts +251 -0
  16. package/dist/compaction/meter.js +428 -0
  17. package/dist/compaction/pressure.d.ts +6 -0
  18. package/dist/compaction/pressure.js +4 -0
  19. package/dist/compaction/types.d.ts +41 -0
  20. package/dist/compaction/wiring.d.ts +47 -0
  21. package/dist/compaction/wiring.js +88 -27
  22. package/dist/config/cli-commands.js +5 -0
  23. package/dist/config/env.js +15 -1
  24. package/dist/config/load.d.ts +17 -0
  25. package/dist/config/load.js +45 -1
  26. package/dist/config/schema.d.ts +44 -0
  27. package/dist/config/schema.js +36 -0
  28. package/dist/config/store.js +27 -1
  29. package/dist/exec/events.d.ts +138 -1
  30. package/dist/exec/events.js +42 -0
  31. package/dist/exec/index.js +43 -6
  32. package/dist/exec/progress-reporter.d.ts +23 -0
  33. package/dist/exec/progress-reporter.js +88 -0
  34. package/dist/exec/runner.d.ts +34 -0
  35. package/dist/exec/runner.js +137 -25
  36. package/dist/fast/reviewer.d.ts +19 -1
  37. package/dist/fast/reviewer.js +22 -0
  38. package/dist/fast/types.d.ts +15 -0
  39. package/dist/fast/wiring.d.ts +10 -1
  40. package/dist/fast/wiring.js +18 -1
  41. package/dist/todo/limits.d.ts +4 -17
  42. package/dist/todo/limits.js +4 -17
  43. package/dist/todo/panel-rows.d.ts +8 -6
  44. package/dist/todo/panel-rows.js +19 -6
  45. package/dist/ui/App.js +75 -63
  46. package/dist/ui/AutocompletePopup.d.ts +3 -1
  47. package/dist/ui/AutocompletePopup.js +9 -11
  48. package/dist/ui/BottomStatusRow.d.ts +1 -1
  49. package/dist/ui/Composer.d.ts +3 -1
  50. package/dist/ui/Composer.js +2 -2
  51. package/dist/ui/PromptInput.d.ts +4 -1
  52. package/dist/ui/PromptInput.js +19 -5
  53. package/dist/ui/StatusBar.d.ts +21 -15
  54. package/dist/ui/StatusBar.js +48 -9
  55. package/dist/ui/TeamPanel.d.ts +3 -1
  56. package/dist/ui/TeamPanel.js +13 -9
  57. package/dist/ui/TodoPanel.d.ts +5 -5
  58. package/dist/ui/TodoPanel.js +56 -24
  59. package/dist/ui/UpdateLine.d.ts +1 -1
  60. package/dist/ui/capabilities.js +5 -5
  61. package/dist/ui/exit-snapshot.d.ts +9 -0
  62. package/dist/ui/layout/ScrollViewport.d.ts +1 -1
  63. package/dist/ui/layout/autocomplete.d.ts +18 -0
  64. package/dist/ui/layout/autocomplete.js +26 -0
  65. package/dist/ui/layout/rail.d.ts +4 -53
  66. package/dist/ui/layout/rail.js +10 -62
  67. package/dist/ui/layout/team-panel.d.ts +18 -0
  68. package/dist/ui/layout/team-panel.js +22 -0
  69. package/dist/ui/layout/todo-layout.d.ts +24 -0
  70. package/dist/ui/layout/todo-layout.js +27 -0
  71. package/dist/ui/transcript-text.js +6 -2
  72. package/package.json +2 -2
@@ -17,7 +17,8 @@ import { attemptTailRelief, reliefSummary, tailBudgetTokens } from './tail-budge
17
17
  import { buildDigest, isPriorSummaryBlock } from './digest.js';
18
18
  import { buildAnchor, buildCompactedBlock, countProtectedPrefix, readGeneration, truncationBody, } from './summary-prompt.js';
19
19
  import { accumulateUsage, errorText, runSummarizeCall } from './summarize-call.js';
20
- import { computeEstimateOffset, computePressure, shouldCompactAt } from './pressure.js';
20
+ import { computePressure, shouldCompactAt } from './pressure.js';
21
+ import { ContextMeter } from './meter.js';
21
22
  export class Compactor {
22
23
  deps;
23
24
  log = getLogger().child('compaction');
@@ -44,18 +45,15 @@ export class Compactor {
44
45
  selfDisabledReason;
45
46
  pendingManual = null;
46
47
  generation = 0;
47
- /** Calibration for every ESTIMATED figure (D-23). Recomputed on every turn_end. */
48
- estimateOffset;
49
- /** The last pressure computed, so the status bar and `/compact status` agree. */
50
- lastPressure = null;
51
48
  /**
52
- * The history LENGTH the measurement in `lastUsage` covered (hardening W1).
49
+ * The fallback meter, built on first use when no host injected one (R-1).
53
50
  *
54
- * `undefined` DISABLES THE DELTA and reproduces round 1's arithmetic exactly.
55
- * Every host-side mutation that is not an append must clear it - see
56
- * `invalidateMeasurement`.
51
+ * `estimateOffset`, `measuredPrefixLength` and `lastPressure` used to be
52
+ * FIELDS HERE. They moved to `ContextMeter` wholesale
53
+ * (context-usage-gauge-accuracy W1), because keeping them here is what put the
54
+ * only measurement in the process behind `compaction.enabled`.
57
55
  */
58
- measuredPrefixLength;
56
+ ownMeter = null;
59
57
  /**
60
58
  * The messages the last splice dropped, held ONLY until the wiring collects
61
59
  * them for the archive (W4 / RV-3).
@@ -87,28 +85,34 @@ export class Compactor {
87
85
  this.lastCompactionTurn = -Infinity;
88
86
  }
89
87
  /**
90
- * `turn_end` — recompute the calibration offset (D-23).
88
+ * `turn_end` - recompute the calibration offset (D-23).
91
89
  *
92
- * RE-MEASURED EVERY TURN, not once, so it tracks a toolset that changed
93
- * mid-session (`/tools`, a skill that registers one).
90
+ * A THIN FORWARDER since context-usage-gauge-accuracy W1. The meter subscribes
91
+ * to `turn_end` itself, so BOTH PATHS MUST BE IDEMPOTENT: this one recomputes
92
+ * exactly what the other one did from the same inputs, which it is.
93
+ *
94
+ * `messages` / `systemPrompt` stay in the signature and are ignored: the meter
95
+ * reads them from its own lazy deps, and every existing caller and test keeps
96
+ * compiling.
94
97
  */
95
- onTurnEnd(usage, messages, systemPrompt) {
96
- this.estimateOffset = computeEstimateOffset(usage, messages, systemPrompt);
97
- // THE SAME ARRAY `computeEstimateOffset` JUST MEASURED, so the two numbers
98
- // describe the same request by construction (hardening §3.2.3).
99
- this.measuredPrefixLength = messages.length;
98
+ onTurnEnd(usage, _messages, _systemPrompt) {
99
+ this.meter().onTurnEnd(usage);
100
100
  }
101
- /**
102
- * Any host-side history mutation that is NOT an append (hardening §3.2.3).
101
+ /*
102
+ * `invalidateMeasurement()` IS GONE, DELIBERATELY (I-9 / RV-2 round two).
103
+ *
104
+ * It had exactly two production callers and their reset depths are now
105
+ * OPPOSITE: `CompactionWiring.onHistoryReplaced` (a `/clear`, `/reset` or
106
+ * `/resume`) must drop `estimateOffset`, while `settlePending`'s applied
107
+ * branch (a splice) must KEEP it - the toolset and the model are unchanged, so
108
+ * the calibration still holds and dropping it makes the gauge fall too far and
109
+ * climb back (I-5 / I-8). The two used to share a method only because the one
110
+ * field it cleared happened to be their intersection; that intersection no
111
+ * longer exists. Both call sites now name the depth they mean, on the meter.
103
112
  *
104
- * `/clear`, `/resume`, and the splice itself: after any of them a recorded
105
- * index means nothing, and the delta would be measured against the wrong
106
- * array. `estimateAppendedTokens` bounds-checks as well, so a MISSED call here
107
- * costs accuracy - the number degrades to round 1's - and never correctness.
113
+ * Reinstating a forwarder here would hide "which caller gets which depth"
114
+ * behind a name that mentions neither, and BOTH wrong choices are silent.
108
115
  */
109
- invalidateMeasurement() {
110
- this.measuredPrefixLength = undefined;
111
- }
112
116
  /**
113
117
  * The dropped messages, released as they are returned (W4 / RV-3).
114
118
  *
@@ -172,17 +176,13 @@ export class Compactor {
172
176
  const config = this.deps.getConfig().compaction;
173
177
  if (!config.enabled)
174
178
  return false;
175
- // THE HISTORY IS PASSED IN, NEVER OMITTED (§3.4.2). Without it the estimate
176
- // branch of `computePressure` measures an EMPTY array and reports 0 %, so a
177
- // resumed 180 k-token session — where `lastUsage` is `undefined` by
178
- // definition and this branch is the only one available — would never trigger.
179
- // The measured branch reads `messages` only to estimate what was appended
180
- // after the measurement (hardening §3.2.2); the slice is one turn wide.
181
- const pressure = this.measure({
182
- ...(probe.lastUsage ? { lastUsage: probe.lastUsage } : {}),
183
- messages: this.deps.getMessages(),
184
- systemPrompt: this.deps.getSystemPrompt(),
185
- });
179
+ // THE METER READS THE LIVE HISTORY ITSELF (W1), so the estimate branch can no
180
+ // longer be handed an empty array by a caller that forgot - which used to be
181
+ // a 0 % reading on precisely the session that most needs a trigger, the
182
+ // resumed 180 k-token one where `lastUsage` is `undefined` by definition.
183
+ // The probe's own usage is still passed, because it is the authority for
184
+ // THIS checkpoint.
185
+ const pressure = this.meter().measureWith(probe.lastUsage);
186
186
  this.deps.onPressure?.(pressure);
187
187
  const manual = this.pendingManual !== null;
188
188
  // Guard 2 — the per-run cap. BOTH TRIGGERS, HARD. It is the bound that makes
@@ -568,12 +568,17 @@ export class Compactor {
568
568
  checkProgress(ctx, uiTrigger, after) {
569
569
  const config = this.deps.getConfig().compaction;
570
570
  const info = this.modelInfo();
571
+ // THE OFFSET NOW LIVES ON THE METER (W1), and it is read from the last
572
+ // PUBLISHED pressure rather than re-derived: `Pressure.estimateOffset` is
573
+ // carried on every measurement, so this is the same number the field used to
574
+ // hold, without the compactor owning a second copy of it.
575
+ const estimateOffset = this.meter().lastPublished()?.estimateOffset;
571
576
  const projected = computePressure({
572
577
  messages: after,
573
578
  systemPrompt: ctx.systemPrompt,
574
579
  contextWindow: info.contextWindow,
575
580
  windowKnown: true,
576
- ...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
581
+ ...(estimateOffset !== undefined ? { estimateOffset } : {}),
577
582
  });
578
583
  const target = config.threshold - COMPACTION_LIMITS.minReclaimRatio;
579
584
  const madeProgress = projected.ratio < target;
@@ -619,7 +624,9 @@ export class Compactor {
619
624
  finish(args, outcome, applied, keepRecentTurnsUsed, announced) {
620
625
  const before = args.ctx.messages.length;
621
626
  const after = outcome.action === 'replace' ? outcome.messages.length : before;
622
- const tokensBefore = this.lastPressure?.occupied ?? 0;
627
+ // THE OCCUPANCY BEFORE THE SPLICE (I-10). `lastMeasured()` is the pure cache
628
+ // read on purpose; anything that re-measures here reports 0 reclaimed.
629
+ const tokensBefore = this.lastMeasured()?.occupied ?? 0;
623
630
  // `'relieved'` IS A CLI-ONLY WORD (RV-5 / DH-20). The port reports
624
631
  // `'truncated'` for a relief-only compaction, which is accurate for the
625
632
  // engine's narrower question; the card's `summarized N messages with
@@ -690,32 +697,30 @@ export class Compactor {
690
697
  // Reporting helpers
691
698
  // =========================================================================
692
699
  /**
693
- * The pressure the trigger and the gauge both read.
700
+ * The pressure the trigger and the gauge both read - MEASURED NOW.
694
701
  *
695
- * CACHED on every evaluation, so `/compact status` reports the same number the
696
- * last decision used rather than recomputing one that has since moved.
702
+ * A FORWARDER since W1: the decision point supplies its own authoritative
703
+ * `lastUsage` (the probe's), and the meter owns everything else. `messages` /
704
+ * `systemPrompt` stay in the signature and are ignored, because the meter
705
+ * reads the LIVE history through its own deps - which is strictly better than
706
+ * a caller that could pass `[]` and get a 0 % reading (the old default here).
697
707
  */
698
708
  measure(probe) {
699
- const info = this.modelInfo();
700
- const pressure = computePressure({
701
- ...(probe.lastUsage ? { lastUsage: probe.lastUsage } : {}),
702
- messages: probe.messages ?? [],
703
- systemPrompt: probe.systemPrompt ?? '',
704
- contextWindow: info.contextWindow,
705
- windowKnown: this.deps.isPricedModel({
706
- providerId: this.mainRef().providerId,
707
- modelId: this.mainRef().modelId,
708
- }),
709
- ...(this.estimateOffset !== undefined ? { estimateOffset: this.estimateOffset } : {}),
710
- ...(this.measuredPrefixLength !== undefined
711
- ? { measuredPrefixLength: this.measuredPrefixLength }
712
- : {}),
713
- });
714
- this.lastPressure = pressure;
715
- return pressure;
709
+ return this.meter().measureWith(probe.lastUsage);
716
710
  }
711
+ /**
712
+ * The last pressure PUBLISHED. NEVER re-measured - `lastPublished`, not
713
+ * `current` (I-10 / RV-3).
714
+ *
715
+ * `finish()` below reads this for `tokensBefore`, which is the sole upstream of
716
+ * `tokensReclaimed`, the card's "reclaimed N tokens" and `/compact status`'s
717
+ * session total. All three want the occupancy BEFORE the splice. Point this at
718
+ * `meter.current()` and the moment the meter is dirty - which after a splice it
719
+ * always is - `tokensBefore` becomes the POST-compaction figure, `reclaimed`
720
+ * collapses to 0, and the card renders that with nothing logging a fault.
721
+ */
717
722
  lastMeasured() {
718
- return this.lastPressure;
723
+ return this.meter().lastPublished();
719
724
  }
720
725
  /** Written by the wiring once the engine has reported its verdict. */
721
726
  recordApplied(tokensBefore, tokensAfter) {
@@ -768,6 +773,29 @@ export class Compactor {
768
773
  return null;
769
774
  return { ref, fromFastTier: false };
770
775
  }
776
+ /**
777
+ * The meter this compactor reads, injected or private (R-1).
778
+ *
779
+ * MEMOIZED, because a fresh meter on every call would have no `estimateOffset`
780
+ * and no cached pressure - `lastMeasured()` would answer `null` forever and
781
+ * `tokensReclaimed` would sit at 0. The fallback exists so the ten existing
782
+ * `compaction-*.test.ts` construction sites compile unchanged; production
783
+ * injects, and `context-gauge-wiring.test.ts` asserts the single instance.
784
+ */
785
+ meter() {
786
+ if (this.deps.meter)
787
+ return this.deps.meter;
788
+ if (!this.ownMeter) {
789
+ this.ownMeter = new ContextMeter({
790
+ getMessages: () => this.deps.getMessages(),
791
+ getSystemPrompt: () => this.deps.getSystemPrompt(),
792
+ getModelInfo: () => this.modelInfo(),
793
+ isWindowKnown: () => this.deps.isPricedModel(this.mainRef()),
794
+ getWindowOverride: () => this.deps.getConfig().contextWindow,
795
+ });
796
+ }
797
+ return this.ownMeter;
798
+ }
771
799
  mainRef() {
772
800
  const config = this.deps.getConfig();
773
801
  return {
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The `/context` slash command (context-usage-gauge-accuracy §3.7 / W6).
3
+ *
4
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope, and this
5
+ * text lands in a transcript `notice` - produced outside `src/ui/**`, where no
6
+ * terminal capabilities are available, so it must be safe on a legacy `cmd.exe`.
7
+ *
8
+ * IT LIVES HERE RATHER THAN IN `builtins.ts` FOR THE REASON `/compact` DOES
9
+ * (C-16 / P1-12): `CLAUDE.md` caps a source file at 1000 lines and `builtins.ts`
10
+ * is already past it. `builtins.ts` gets the registration and nothing else.
11
+ *
12
+ * THIS IS THE GUARANTEED REPORTING SURFACE FOR OCCUPANCY. The status bar drops
13
+ * readouts on a narrow terminal, the gauge is a bar and a rounded percentage,
14
+ * and neither can say WHERE the denominator came from or WHAT the session total
15
+ * includes. Every question a user asks after distrusting the bar is answered
16
+ * here, at any width, in any mode - which is also why the honesty rules below
17
+ * are not decoration.
18
+ */
19
+ import type { CommandContext } from '../commands/registry.js';
20
+ /**
21
+ * `/context` - occupancy, denominator, freshness, compaction, session spend.
22
+ *
23
+ * THE OCCUPANCY COMES FROM `controller.getContextUsage()` AND NOWHERE ELSE
24
+ * (RV-8). The obvious alternative, `getCompactionSnapshot().pressure`, returns
25
+ * `offCompactionSnapshot()`'s HARDCODED ZERO pressure whenever compaction was
26
+ * never registered - so a report built on it would read `0%` for exactly the
27
+ * sessions this whole feature exists to fix. `/compact status` gets away with
28
+ * reading it only because it returns early on that branch and never prints an
29
+ * occupancy line at all.
30
+ */
31
+ export declare function formatContextReport(ctx: CommandContext): string;
32
+ /** `/context` takes no arguments; anything after it is ignored. */
33
+ export declare function runContextCommand(ctx: CommandContext): void;
@@ -0,0 +1,166 @@
1
+ /**
2
+ * The `/context` slash command (context-usage-gauge-accuracy §3.7 / W6).
3
+ *
4
+ * ASCII ONLY: `src/compaction/**` is inside the glyph scanner's scope, and this
5
+ * text lands in a transcript `notice` - produced outside `src/ui/**`, where no
6
+ * terminal capabilities are available, so it must be safe on a legacy `cmd.exe`.
7
+ *
8
+ * IT LIVES HERE RATHER THAN IN `builtins.ts` FOR THE REASON `/compact` DOES
9
+ * (C-16 / P1-12): `CLAUDE.md` caps a source file at 1000 lines and `builtins.ts`
10
+ * is already past it. `builtins.ts` gets the registration and nothing else.
11
+ *
12
+ * THIS IS THE GUARANTEED REPORTING SURFACE FOR OCCUPANCY. The status bar drops
13
+ * readouts on a narrow terminal, the gauge is a bar and a rounded percentage,
14
+ * and neither can say WHERE the denominator came from or WHAT the session total
15
+ * includes. Every question a user asks after distrusting the bar is answered
16
+ * here, at any width, in any mode - which is also why the honesty rules below
17
+ * are not decoration.
18
+ */
19
+ import { formatCost, formatTokens, promptTokensOf } from '../agent/usage.js';
20
+ /**
21
+ * `/context` - occupancy, denominator, freshness, compaction, session spend.
22
+ *
23
+ * THE OCCUPANCY COMES FROM `controller.getContextUsage()` AND NOWHERE ELSE
24
+ * (RV-8). The obvious alternative, `getCompactionSnapshot().pressure`, returns
25
+ * `offCompactionSnapshot()`'s HARDCODED ZERO pressure whenever compaction was
26
+ * never registered - so a report built on it would read `0%` for exactly the
27
+ * sessions this whole feature exists to fix. `/compact status` gets away with
28
+ * reading it only because it returns early on that branch and never prints an
29
+ * occupancy line at all.
30
+ */
31
+ export function formatContextReport(ctx) {
32
+ const controller = ctx.controller;
33
+ const usage = controller.getContextUsage();
34
+ const lines = ['Context'];
35
+ lines.push(` Occupancy ${occupancyLine(usage)}`);
36
+ lines.push(` Window ${windowLine(usage)}`);
37
+ lines.push(` Since measured ${freshnessLine(usage)}`);
38
+ lines.push(` Compaction ${compactionLine(ctx)}`);
39
+ lines.push(` Session spend ${spendLine(ctx)}`);
40
+ // THE SENTENCE THAT MAKES THE LINE ABOVE HONEST (D-7 / N-3). The session
41
+ // total folds in subagents, the fast tier and compaction's own summarization
42
+ // calls - three other models' tokens in a figure sitting next to the lead's
43
+ // occupancy. Splitting it into four counters would need four units of
44
+ // provenance argument for one line of output; SAYING what it contains costs
45
+ // one line and answers the same question.
46
+ lines.push(' includes subagent, fast-tier and compaction spend,');
47
+ lines.push(' not just this conversation');
48
+ return lines.join('\n');
49
+ }
50
+ /**
51
+ * `43% 86.2k of 200k [measured 85.1k + 1.1k estimated]`
52
+ *
53
+ * THE BREAKDOWN IS THE POINT. "43%" alone cannot be checked by a user who thinks
54
+ * it is wrong; naming how much of it is a provider measurement and how much is
55
+ * our own guess about messages appended since tells them whether to distrust the
56
+ * number or the model.
57
+ */
58
+ function occupancyLine(usage) {
59
+ const approximate = usage.source === 'estimate' || usage.deltaTokens > 0;
60
+ const pct = `${approximate || !usage.windowKnown ? '~' : ''}${usage.pct}%`;
61
+ const measured = Math.max(0, usage.occupied - usage.deltaTokens);
62
+ const breakdown = usage.source === 'estimate'
63
+ ? `[estimated ${formatTokens(usage.occupied)}]`
64
+ : usage.deltaTokens > 0
65
+ ? `[measured ${formatTokens(measured)} + ${formatTokens(usage.deltaTokens)} estimated]`
66
+ : `[measured ${formatTokens(measured)}]`;
67
+ return `${pct} ${formatTokens(usage.occupied)} of ${formatTokens(usage.window)} ${breakdown}`;
68
+ }
69
+ /**
70
+ * Where the denominator came from. THREE SOURCES, ALL NAMED.
71
+ *
72
+ * THE PLACEHOLDER BRANCH IS THE ONE THAT MATTERS. `buildRuntimeModel` returns a
73
+ * flat 128000 for any model the static table has never seen - a custom
74
+ * `baseUrl`, a self-hosted id, a model released last week - and before this
75
+ * command there was nothing that would admit the number was invented. A user
76
+ * reading `128000` next to their 1M-token model concludes the gauge is broken;
77
+ * they are right, and this line tells them which key fixes it.
78
+ */
79
+ function windowLine(usage) {
80
+ if (usage.windowOverridden) {
81
+ return `${usage.window} from contextWindow in config.json (overrides the model table)`;
82
+ }
83
+ if (!usage.windowKnown) {
84
+ return (`${usage.window} PLACEHOLDER - this model is not in the table; ` +
85
+ 'set contextWindow to correct it');
86
+ }
87
+ return `${usage.window} from the model table`;
88
+ }
89
+ /**
90
+ * How stale the measured half is.
91
+ *
92
+ * `deltaTokens` IS A TOKEN COUNT, NOT A MESSAGE COUNT, and this line says so in
93
+ * tokens for that reason: the meter estimates the appended SLICE, and turning
94
+ * that back into "4 messages" would be a second derivation of something nobody
95
+ * recorded.
96
+ */
97
+ function freshnessLine(usage) {
98
+ if (usage.source === 'estimate') {
99
+ return 'no provider-reported usage yet - the whole figure is an estimate';
100
+ }
101
+ if (usage.deltaTokens <= 0) {
102
+ return 'nothing appended since the last provider-reported usage';
103
+ }
104
+ return `${formatTokens(usage.deltaTokens)} estimated tokens appended since the last provider-reported usage`;
105
+ }
106
+ /**
107
+ * The compaction state, IN FIVE FORMS (RV-8).
108
+ *
109
+ * A SINGLE FORM WOULD BE A LIE FOUR TIMES OUT OF FIVE. The obvious version of
110
+ * this line - "on - triggers at 90% (amber at 75%)" - is printed unconditionally
111
+ * only if you forget that compaction has four other observable states, and the
112
+ * first of them is `aragon --no-compaction`, where that sentence promises a
113
+ * rescue that cannot come. A report that exists to be trusted does not get to
114
+ * have one branch.
115
+ *
116
+ * THE PREDICATES ARE THE ONES `formatCompactionStatus` ALREADY USES
117
+ * (`compaction/command.ts`), not a second set: two ladders over the same three
118
+ * booleans will eventually disagree, and the disagreement would be between two
119
+ * commands the user runs precisely because they already distrust something.
120
+ */
121
+ function compactionLine(ctx) {
122
+ const controller = ctx.controller;
123
+ if (!controller.isCompactionRegistered()) {
124
+ return 'off - not registered for this session (started with --no-compaction)';
125
+ }
126
+ const snapshot = controller.getCompactionSnapshot();
127
+ if (snapshot.selfDisabled) {
128
+ const reason = snapshot.selfDisabledReason ?? 'no progress';
129
+ return `self-disabled (${reason}) - no compaction will run until /compact on`;
130
+ }
131
+ if (!controller.isCompactionEnabled()) {
132
+ return 'off for this session - /compact on re-enables it';
133
+ }
134
+ const config = controller.getCompactionConfig();
135
+ const thresholds = `triggers at ${Math.round(config.threshold * 100)}% ` +
136
+ `(amber at ${Math.round(config.warnThreshold * 100)}%)`;
137
+ if (!snapshot.live) {
138
+ return `on, but no summarizer model resolves - ${thresholds}`;
139
+ }
140
+ const count = snapshot.compactions;
141
+ return (`on - ${thresholds}, ${count} this session, ` +
142
+ `${formatTokens(snapshot.tokensReclaimed)} reclaimed`);
143
+ }
144
+ /**
145
+ * `1.24M in (incl. 940k cache read, 12k cache write) / 48.2k out, $3.21`
146
+ *
147
+ * THE CACHE PARENTHETICAL IS OMITTED WHEN BOTH TERMS ARE ZERO, which is every
148
+ * direct connection - this CLI never sends `cache_control`, so the terms are
149
+ * non-zero only behind a gateway that caches on the user's behalf. Those are
150
+ * exactly the users for whom the pre-feature `^` under-reported (P1-3), and the
151
+ * clause is how they can see that it no longer does.
152
+ */
153
+ function spendLine(ctx) {
154
+ const total = ctx.state.usageTotal;
155
+ const cached = total.cacheReadTokens + total.cacheWriteTokens;
156
+ const parenthetical = cached > 0
157
+ ? ` (incl. ${formatTokens(total.cacheReadTokens)} cache read, ` +
158
+ `${formatTokens(total.cacheWriteTokens)} cache write)`
159
+ : '';
160
+ return (`${formatTokens(promptTokensOf(total))} in${parenthetical} / ` +
161
+ `${formatTokens(total.outputTokens)} out, ${formatCost(total.costUsd)}`);
162
+ }
163
+ /** `/context` takes no arguments; anything after it is ignored. */
164
+ export function runContextCommand(ctx) {
165
+ ctx.notify('info', formatContextReport(ctx));
166
+ }