@aragon-agent/cli 0.6.3 → 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 (138) hide show
  1. package/CHANGELOG.md +142 -0
  2. package/README.md +203 -13
  3. package/dist/agent/controller.d.ts +158 -2
  4. package/dist/agent/controller.js +400 -1
  5. package/dist/agent/reducer.d.ts +102 -19
  6. package/dist/agent/reducer.js +162 -71
  7. package/dist/agent/system-prompt.d.ts +14 -0
  8. package/dist/agent/system-prompt.js +2 -0
  9. package/dist/agent/usage.d.ts +34 -0
  10. package/dist/agent/usage.js +43 -0
  11. package/dist/cli.js +132 -23
  12. package/dist/commands/builtins.js +101 -1
  13. package/dist/compaction/child.js +5 -0
  14. package/dist/compaction/command.js +14 -1
  15. package/dist/compaction/compactor.d.ts +94 -24
  16. package/dist/compaction/compactor.js +247 -98
  17. package/dist/compaction/context-command.d.ts +33 -0
  18. package/dist/compaction/context-command.js +166 -0
  19. package/dist/compaction/meter.d.ts +251 -0
  20. package/dist/compaction/meter.js +428 -0
  21. package/dist/compaction/pressure.d.ts +6 -0
  22. package/dist/compaction/pressure.js +4 -0
  23. package/dist/compaction/types.d.ts +77 -0
  24. package/dist/compaction/wiring.d.ts +56 -0
  25. package/dist/compaction/wiring.js +107 -25
  26. package/dist/config/cli-commands.js +5 -0
  27. package/dist/config/env.d.ts +1 -1
  28. package/dist/config/env.js +63 -2
  29. package/dist/config/load.d.ts +28 -0
  30. package/dist/config/load.js +83 -1
  31. package/dist/config/schema.d.ts +129 -0
  32. package/dist/config/schema.js +69 -0
  33. package/dist/config/store.js +47 -1
  34. package/dist/exec/events.d.ts +138 -1
  35. package/dist/exec/events.js +42 -0
  36. package/dist/exec/index.js +43 -6
  37. package/dist/exec/progress-reporter.d.ts +23 -0
  38. package/dist/exec/progress-reporter.js +88 -0
  39. package/dist/exec/runner.d.ts +34 -0
  40. package/dist/exec/runner.js +137 -25
  41. package/dist/fast/reviewer.d.ts +19 -1
  42. package/dist/fast/reviewer.js +22 -0
  43. package/dist/fast/types.d.ts +15 -0
  44. package/dist/fast/wiring.d.ts +10 -1
  45. package/dist/fast/wiring.js +18 -1
  46. package/dist/input/limits.d.ts +72 -0
  47. package/dist/input/limits.js +63 -0
  48. package/dist/input/paste-parse.d.ts +71 -0
  49. package/dist/input/paste-parse.js +95 -0
  50. package/dist/input/stdin-filter.d.ts +81 -0
  51. package/dist/input/stdin-filter.js +379 -0
  52. package/dist/logging/install.d.ts +2 -0
  53. package/dist/logging/install.js +53 -1
  54. package/dist/proc/classify.d.ts +52 -0
  55. package/dist/proc/classify.js +143 -0
  56. package/dist/proc/kill-tree.d.ts +42 -0
  57. package/dist/proc/kill-tree.js +119 -0
  58. package/dist/proc/limits.d.ts +84 -0
  59. package/dist/proc/limits.js +84 -0
  60. package/dist/proc/log-ring.d.ts +68 -0
  61. package/dist/proc/log-ring.js +102 -0
  62. package/dist/proc/prompt.d.ts +24 -0
  63. package/dist/proc/prompt.js +49 -0
  64. package/dist/proc/readiness.d.ts +102 -0
  65. package/dist/proc/readiness.js +219 -0
  66. package/dist/proc/supervisor.d.ts +103 -0
  67. package/dist/proc/supervisor.js +465 -0
  68. package/dist/proc/types.d.ts +175 -0
  69. package/dist/proc/types.js +15 -0
  70. package/dist/session/persist.js +36 -1
  71. package/dist/todo/limits.d.ts +4 -17
  72. package/dist/todo/limits.js +4 -17
  73. package/dist/todo/panel-rows.d.ts +8 -6
  74. package/dist/todo/panel-rows.js +19 -6
  75. package/dist/tools/bash-tool.d.ts +51 -2
  76. package/dist/tools/bash-tool.js +321 -51
  77. package/dist/tools/fs-tools.d.ts +17 -0
  78. package/dist/tools/index.d.ts +55 -2
  79. package/dist/tools/index.js +39 -1
  80. package/dist/tools/proc-tools.d.ts +25 -0
  81. package/dist/tools/proc-tools.js +102 -0
  82. package/dist/ui/App.d.ts +15 -2
  83. package/dist/ui/App.js +330 -69
  84. package/dist/ui/AutocompletePopup.d.ts +3 -1
  85. package/dist/ui/AutocompletePopup.js +9 -11
  86. package/dist/ui/BottomStatusRow.d.ts +1 -1
  87. package/dist/ui/Composer.d.ts +56 -1
  88. package/dist/ui/Composer.js +50 -6
  89. package/dist/ui/PromptInput.d.ts +52 -3
  90. package/dist/ui/PromptInput.js +201 -55
  91. package/dist/ui/StatusBar.d.ts +40 -15
  92. package/dist/ui/StatusBar.js +51 -9
  93. package/dist/ui/TeamPanel.d.ts +3 -1
  94. package/dist/ui/TeamPanel.js +13 -9
  95. package/dist/ui/TodoPanel.d.ts +5 -5
  96. package/dist/ui/TodoPanel.js +56 -24
  97. package/dist/ui/Transcript.js +29 -1
  98. package/dist/ui/UpdateLine.d.ts +1 -1
  99. package/dist/ui/capabilities.js +5 -5
  100. package/dist/ui/composer-limits.d.ts +31 -0
  101. package/dist/ui/composer-limits.js +50 -0
  102. package/dist/ui/composer-rows.d.ts +73 -0
  103. package/dist/ui/composer-rows.js +201 -0
  104. package/dist/ui/editor-reducer.d.ts +37 -0
  105. package/dist/ui/editor-reducer.js +88 -12
  106. package/dist/ui/entries/ServiceCard.d.ts +66 -0
  107. package/dist/ui/entries/ServiceCard.js +116 -0
  108. package/dist/ui/entries/UserEntry.d.ts +17 -3
  109. package/dist/ui/entries/UserEntry.js +34 -5
  110. package/dist/ui/exit-snapshot.d.ts +9 -0
  111. package/dist/ui/layout/ScrollViewport.d.ts +1 -1
  112. package/dist/ui/layout/autocomplete.d.ts +18 -0
  113. package/dist/ui/layout/autocomplete.js +26 -0
  114. package/dist/ui/layout/budget.d.ts +34 -6
  115. package/dist/ui/layout/budget.js +32 -7
  116. package/dist/ui/layout/rail.d.ts +4 -53
  117. package/dist/ui/layout/rail.js +10 -62
  118. package/dist/ui/layout/team-panel.d.ts +18 -0
  119. package/dist/ui/layout/team-panel.js +22 -0
  120. package/dist/ui/layout/todo-layout.d.ts +24 -0
  121. package/dist/ui/layout/todo-layout.js +27 -0
  122. package/dist/ui/layout/virtual-window.js +51 -1
  123. package/dist/ui/overlays/HelpOverlay.js +3 -0
  124. package/dist/ui/overlays/PlanReviewOverlay.js +10 -2
  125. package/dist/ui/overlays/QuestionOverlay.js +5 -1
  126. package/dist/ui/overlays/SettingsScreen.js +14 -1
  127. package/dist/ui/paste-frames.d.ts +87 -0
  128. package/dist/ui/paste-frames.js +112 -0
  129. package/dist/ui/paste-tokens.d.ts +90 -0
  130. package/dist/ui/paste-tokens.js +147 -0
  131. package/dist/ui/screen.d.ts +16 -0
  132. package/dist/ui/screen.js +45 -2
  133. package/dist/ui/selection/selection-controller.d.ts +1 -1
  134. package/dist/ui/transcript-text.js +55 -2
  135. package/dist/ui/use-wheel-routing.d.ts +1 -1
  136. package/package.json +2 -2
  137. package/dist/input/stdin-mouse-filter.d.ts +0 -47
  138. package/dist/input/stdin-mouse-filter.js +0 -153
package/CHANGELOG.md CHANGED
@@ -9,6 +9,148 @@ were written.
9
9
 
10
10
  ### Added
11
11
 
12
+ - **`system/init` now lists what this build can do.** A new optional
13
+ `capabilities: string[]` field, carrying `["interrupt"]` for this release.
14
+
15
+ A wrapper cannot tell by observation whether an interrupt will settle
16
+ gracefully or kill the process outright, and guessing wrong costs the user
17
+ their conversation. Rather than have callers compare version numbers — which
18
+ breaks on forks, prereleases, and version shapes nobody anticipated — the
19
+ build now says so itself, in an event that already arrives on every start.
20
+ Absent on older builds, and absence should be read as "no", never as
21
+ "probably".
22
+
23
+ Additive, so `schemaVersion` does not move. Consumers that do not know the
24
+ field ignore it, exactly as the schema contract asks.
25
+
26
+ - **The composer understands pasting.** Until now it did not have the concept at
27
+ all: every byte the terminal delivered on stdin was a keystroke, and a
28
+ clipboard block is thousands of bytes arriving in an unpredictable number of
29
+ chunks. Three separate failures fell out of that one omission, and on Windows
30
+ you hit all three at once.
31
+
32
+ A carriage return that landed alone in a chunk **sent the half-finished
33
+ message** — a fifteen-line paste could arrive as a dozen separate messages,
34
+ interleaved with the agent's replies. A carriage return in the middle of a
35
+ chunk was inserted into the draft as a literal `\r`, which the terminal obeyed
36
+ by **overwriting the row it was drawn on**. And a paste that survived both grew
37
+ the composer to the height of the pasted text, pushing the transcript out of a
38
+ frame whose row accounting still believed the composer was three rows tall.
39
+
40
+ Pasting now works the way it reads:
41
+
42
+ - **A paste never submits.** Line breaks become buffer newlines, exactly one
43
+ per source line break, `CRLF` and lone `CR` alike.
44
+ - **A short paste appears verbatim.** At or below 6 lines and 400 characters it
45
+ is inserted as-is, indentation intact, and renders exactly like typed text.
46
+ - **A large paste collapses to a placeholder** — `[Pasted text #1 +218 lines]`
47
+ — which occupies one line, carries its own size in its label, deletes as a
48
+ single unit (one `Backspace`, and `Ctrl+W` / `Ctrl+U` / `Ctrl+K` cannot leave
49
+ half of one behind), and expands back to the full text the moment you send.
50
+ The model receives every byte; the composer never has to draw them.
51
+ - **No pasted byte is executed as a terminal control.** `\r`, `\x1b`, NUL, DEL
52
+ and the C1 range are stripped from the payload — never from your keystrokes,
53
+ so `Ctrl+A` and Escape are untouched.
54
+ - **The composer has a ceiling.** A draft of any length occupies at most 3 / 6 /
55
+ 10 rows depending on terminal height, scrolls internally to keep the caret
56
+ visible, shows how many rows are hidden above and below, and reports its true
57
+ height to the layout budget. Four hundred presses of `Shift+Enter` were
58
+ already able to destroy the frame; they no longer are.
59
+ - **A submitted message cannot make the transcript unresponsive.** A user entry
60
+ renders at most 40 rows plus a `... +N more lines` tail. The full text is kept
61
+ and the exit transcript is uncapped.
62
+
63
+ On terminals that support it, detection is **exact**: aragon asks for bracketed
64
+ paste (`DECSET 2004`) in full-screen mode and reads the markers the terminal
65
+ wraps your paste in. Where that is unavailable — inline mode, an old Windows
66
+ console — a heuristic on chunk shape catches the same cases, and it needs no
67
+ cooperation from the terminal at all. Both feed one code path.
68
+
69
+ Turn it off with `--no-paste`, `ARAGON_PASTE=0`, or
70
+ `aragon config set paste false`; the input path is then byte-for-byte what
71
+ 0.6.2 did.
72
+
73
+ ### Fixed
74
+
75
+ - **`{"type":"interrupt"}` now really does settle the run — on every platform.**
76
+ The stdin interrupt frame used to raise a SIGINT at our own process. On POSIX
77
+ that reaches the runner's terminator and the run winds down properly: it
78
+ breaks out of the loop, persists the session, and emits `result` with the
79
+ usage and cost. On Windows, Node does not deliver a self-directed SIGINT to
80
+ its own handlers — the process simply terminated with exit code 1, having run
81
+ none of it.
82
+
83
+ For anyone using `aragon exec` interactively that difference was invisible,
84
+ because an interrupt there is usually the last thing that happens. It is not
85
+ invisible to a **wrapper** that interrupts a turn and then respawns on the same
86
+ `--session-id` to keep the conversation going: on Windows it silently got back
87
+ a session missing everything since the previous save. `exec/index.ts` has
88
+ carried a comment about that caller for some time ("PERSIST FIRST, ANNOUNCE
89
+ SECOND … a caller that reads `result` and immediately respawns with the same
90
+ `--session-id` must not race the writer"), so the intent was there; the path
91
+ just could not be reached on Windows.
92
+
93
+ The frame now calls the runner directly instead of going through a signal.
94
+ POSIX behaviour is byte-for-byte unchanged — the same code runs, with the same
95
+ `stopReason`, the same exit code 130, and the same two-second window in which a
96
+ second signal still forces an immediate exit.
97
+
98
+ - **A pasted API key is stored correctly again.** Ink broadcasts each stdin chunk
99
+ to every mounted input handler, so the settings screen, the question overlay
100
+ and the plan-review feedback field all see what the composer sees. Without
101
+ this, a ~100-character key pasted into Settings would have been stored wrapped
102
+ in invisible framing bytes, masked on screen by the password dots, and would
103
+ have failed to authenticate on every request afterwards with nothing anywhere
104
+ explaining why.
105
+
106
+ - **`--no-mouse` still writes no mouse sequence.** Paste support installs a
107
+ stdin filter, and `paste` defaults to on, so the filter now exists in sessions
108
+ that disabled the mouse. Whether the mouse is on is a separate question from
109
+ whether the stream is wrapped, and the four places that used to conflate them
110
+ now ask the right one: `aragon --no-mouse` writes no `?1000h`, no `?1006h` and
111
+ no `?1002h`, leaves your terminal's own click-drag selection alone, and reports
112
+ `/mouse` as off — while pasting still works.
113
+
114
+ ### Changed
115
+
116
+ - **Auto-compaction no longer reports compactions that did not happen.** A long
117
+ unattended run used to print a red `context not compacted #2: nothing_to_drop`
118
+ card into the transcript. The card was accurate; nobody had asked for it.
119
+ Auto-compaction evaluates itself at every turn boundary, and a checkpoint that
120
+ looks at the history and finds nothing worth dropping has changed nothing,
121
+ called nothing and spent nothing — a smoke alarm announcing "no fire" twice an
122
+ hour, in error colours, with a machine reason attached.
123
+
124
+ A compaction now announces itself **if and only if it commits to doing work**.
125
+ That is a position in the control flow, not a list of reason strings: everything
126
+ decided before the first spend is silent, and everything decided after it —
127
+ including every failure, the truncation fallback, an Esc, and a host fault — is
128
+ as loud as it ever was. A declined `pressure` checkpoint is now
129
+ indistinguishable from a turn on which the threshold was never crossed.
130
+
131
+ **`/compact` and overflow recovery are unchanged.** A compaction you asked for
132
+ is owed an answer, and a reactive compaction running after the provider has
133
+ already refused your request is the one moment the diagnosis matters most. Both
134
+ still report on every outcome.
135
+
136
+ **Quiet, not unrecorded.** Every attempt is still written to the diagnostic log
137
+ (its `compaction` line gains an `announced` field, so `announced: false` is one
138
+ grep), still charged to the anti-loop guards, and still counted by
139
+ `/compact status` — which now reads
140
+ `This session: 1 compaction (2 checkpoints declined), 94.2k tokens reclaimed`
141
+ when checkpoints declined, and is byte-identical to before when none did. If
142
+ declines keep happening, the self-disable notice still says so out loud, once,
143
+ naming the cause and the remedies. Card numbers are also contiguous again: `#N`
144
+ counts compactions that ran, not checkpoints that fired.
145
+
146
+ **Behaviour change for wrapper scripts.** A declined `pressure` compaction no
147
+ longer emits a `{ type: 'compaction', subtype: 'start' | 'end' }` pair on the
148
+ `--output-format json` stream, and no longer prints `[compaction] skipped:` on
149
+ `-p` stderr. The pairs stay balanced, so only consumers counting *attempts* are
150
+ affected; the diagnostic log reports those more accurately.
151
+
152
+ ### Added
153
+
12
154
  - **Context compaction, hardened — the trigger now counts the whole history, the
13
155
  recent turns can no longer wedge a run, sub-agents get the same protection, and
14
156
  a compaction is no longer irreversible.**
package/README.md CHANGED
@@ -413,6 +413,8 @@ signal ends the run (`130` SIGINT, `143` SIGTERM, `129` SIGHUP).
413
413
  | `/retry [show\|on\|off\|max <n>]` | API retry: the effective ladder, the on/off switch, the retry count. Takes effect **in the running session**, children included |
414
414
  | `/fast [on\|off\|model <id>\|provider <id>\|same\|review <n\|off>\|delegate on\|off]` | Fast model tier: status and this session's totals, the on/off switch, which model, the review cadence |
415
415
  | `/compact [status\|on\|off\|threshold <n>\|keep <n>\|history\|show <n>\|<instructions>]` | Context compaction: occupancy and this session's totals, the on/off switch, when it fires, how much it keeps. With free text, compacts now and tells the summarizer what to pay attention to |
416
+ | `/context` | What the context gauge is showing: occupancy split into measured and estimated, the window and **where it came from**, how stale the measurement is, compaction's actual state, and what the session-spend readout includes |
417
+ | `/bg [list\|logs <id> [n]\|stop <id\|all>\|status]` | Background services: what is running, its log tail, and how to stop it. `list` is the default |
416
418
  | `/update [status\|now\|skip\|off]` | Auto-update: what is running, what is available, where it came from, when it looks again. `now` forces a check past the throttle; `skip` declines the offered version until a newer one appears |
417
419
  | `/theme <auto\|warm\|cool\|light>` | Switch the color theme live (persisted) |
418
420
  | `/expand [n]` | Expand / collapse the n-th-from-last (default last) tool card |
@@ -437,10 +439,12 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
437
439
  | `Shift+Tab` | Toggle **plan mode** (`BUILD` <-> `PLAN`). Equivalent: `/plan`. On Windows this needs a console flag Node < 22.17 does not set; aragon sets it at startup — see [Requirements](#requirements) |
438
440
  | `Ctrl+P` | The same toggle, over a channel no Windows console can swallow. Use it if `Shift+Tab` does nothing |
439
441
  | `Esc` | Abort the run / close an overlay / close a popup |
440
- | `Ctrl+C` ×2 | Exit (first press warns) |
442
+ | `Esc` ×2 | **Force-stop** a run that did not abort: kills the shell command it is blocked on and returns the view to `idle`. The second press only does anything while the run is *still* running, so in a healthy run it is a no-op |
443
+ | `Ctrl+C` | Stop the background **services** the agent started, when there are any. It does not arm exit |
444
+ | `Ctrl+C` ×2 | Exit (first press warns), once no services are running |
441
445
  | `Ctrl+L` | Redraw the frame (clear the screen in inline mode) |
442
446
  | `Ctrl+T` | Show / hide thinking (**off by default**). In inline mode it applies to the live entry and everything drawn after it — settled entries are already in your terminal's scrollback and cannot be repainted |
443
- | `Ctrl+O` | Expand / collapse the most recent tool card — including its diff |
447
+ | `Ctrl+O` | Expand / collapse the most recent tool, team, compaction or **service** card — including a diff or a service's log tail |
444
448
  | `PgUp` / `PgDn` | Scroll the transcript a page (full-screen mode) |
445
449
  | `Shift+↑` / `Shift+↓` | Scroll the transcript a line (full-screen mode) |
446
450
  | Mouse wheel | Scroll the transcript (or the open overlay). On Windows needs Node ≥ 22.17 — see [Requirements](#requirements) |
@@ -450,6 +454,7 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
450
454
  | `Ctrl+W`, `Alt+Backspace` | Delete the previous word |
451
455
  | `Ctrl+U` / `Ctrl+K` | Kill to line start / end |
452
456
  | `Up` / `Down` | Move between draft lines; recall prompt history at the edges |
457
+ | Paste | Inserted as-is up to 6 lines / 400 chars; larger collapses to `[Pasted text #1 +15 lines]`, which deletes as one unit and expands in full when you send. `--no-paste` restores the pre-0.6.3 behaviour |
453
458
  | `?` | Open help (empty input) |
454
459
 
455
460
  In **full-screen mode** the transcript is scrolled by the app itself: `PgUp` /
@@ -795,7 +800,8 @@ ask — and the agent writes the finished plan as its final message in markdown.
795
800
  For work with three or more distinct steps the agent keeps a **visible plan**:
796
801
  it writes a checklist with the `todo_write` tool, marks exactly one item as in
797
802
  progress, and ticks it off before starting the next. In full-screen mode that
798
- list is rendered as a right-hand rail, about a fifth of the terminal wide.
803
+ list is rendered as a right-hand rail, targeting 15% of the terminal width
804
+ (14–36 columns, including its separator and padding).
799
805
 
800
806
  ```
801
807
  transcript ... | TODO 2/7
@@ -803,8 +809,7 @@ list is rendered as a right-hand rail, about a fifth of the terminal wide.
803
809
  |
804
810
  | v 1 Read the reducer
805
811
  | v 2 Design the store
806
- | > 3 Wiring the rail into
807
- | AppShell
812
+ | > 3 Wiring the rail...
808
813
  | o 4 Add the panel-rows test
809
814
  | o 5 Update the README
810
815
  | +2 more
@@ -828,7 +833,7 @@ not also tell the model may change it — except you, saying so:
828
833
  conversation has just been replaced.
829
834
  - A finished plan is dropped at the START of your next message, so you still get
830
835
  to see `7/7 done`. An unfinished one survives "continue" / "now do the rest",
831
- and is dropped after three unrelated turns rather than holding a fifth of the
836
+ and is dropped after three unrelated turns rather than holding part of the
832
837
  screen for the rest of the session.
833
838
 
834
839
  ### When a run ends with steps left
@@ -884,11 +889,19 @@ planning discipline and hides the column — which is what you want with a scree
884
889
  reader, and what `--no-todo-panel` does for a whole session. Under `-p` the plan
885
890
  is written to stderr as `[todo] 3/7 <step>` lines, suppressed by `--quiet`.
886
891
 
887
- The rail is not mounted below 80 columns, while an overlay is open, in inline
888
- mode, or when the viewport is too short to say anything useful; the transcript
889
- keeps at least 62 columns at every width. In those cases the counter appears in
890
- the status bar instead, and the checklist is still written into the transcript
891
- as one card per turn.
892
+ 右栏从 76 列开始显示,正文至少保留 62 列;100 列终端使用 15 列右栏。
893
+ 已有任务时,团队面板按实际行数占用空间,补全菜单的边框和提示也计入预算。
894
+ 例如 100×20、团队占 8 行时,TODO 仍以 4 行紧凑视图显示。
895
+
896
+ 剩余 3–5 行时保留标题、完成计数、当前任务与合并提示 `-A +B`:
897
+ `-9 +10` 表示上方隐藏 9 项、下方隐藏 10 项。6 行起使用常规视图;
898
+ 所有任务各占一行,全文仍可在正文任务卡片中查看。进度条需要至少 20 列、8 行。
899
+ 输入增长导致空间不足时团队先折叠为摘要;菜单不足 3 行时隐藏,其候选不会拦截输入。
900
+
901
+ 低于 76 列、共享区域不足 3 行、打开 overlay、显式关闭面板、没有任务或
902
+ inline 模式时不显示右栏。低于 12 行使用原有小终端提示;扩大窗口后自动恢复。
903
+ inline 仍使用上文的单行任务条。`/todo status` 中的 `Panel: on` 仅表示配置开启,
904
+ 实际可见性还取决于数据和窗口空间;可以通过该命令区分未生成计划与暂时隐藏。
892
905
 
893
906
  ## Built-in tools
894
907
 
@@ -908,6 +921,94 @@ delegation tool (see below). `--no-team` removes it again.
908
921
  With todo planning on — also the default — one more is added: `todo_write`, the
909
922
  planning tool (see above). `--no-todo` removes it again.
910
923
 
924
+ With background services on — also the default — two more are added:
925
+ `bash_output` and `bash_kill` (see below), and `bash` itself gains a `background`
926
+ parameter. `bash.background: false` removes all three again.
927
+
928
+ ## Background services
929
+
930
+ The agent can write a web app; it also has to be able to **run** one. A command
931
+ that never exits on its own — `npm run dev`, `uvicorn main:app`, `vite`,
932
+ `docker compose up`, `tsc --watch` — is not a hung tool, it is a server, and
933
+ running it in the foreground stalls the turn until the tool timeout kills it
934
+ three minutes later.
935
+
936
+ So those commands are **supervised** instead. `bash` gains one optional
937
+ parameter:
938
+
939
+ ```jsonc
940
+ { "command": "npm run dev", "background": true }
941
+ ```
942
+
943
+ `true` supervises it, `false` never does, and **omitting it lets the runtime
944
+ decide** from the command itself — a closed allowlist over the leading token and
945
+ the script name (`npm|pnpm|yarn|bun run dev|start|serve|watch|preview`,
946
+ `next dev`, `vite`, `nuxt dev`, `ng serve`, `nodemon`, `tsc --watch`, `uvicorn`,
947
+ `gunicorn`, `flask run`, `manage.py runserver`, `rails server`, `php -S`,
948
+ `cargo watch`, `docker compose up` without `-d`, `tail -f`, `watch `). It never
949
+ fires on a chained command, because a chain's tail is what you actually wanted
950
+ the result of, and `npm run build` and `npm test` are deliberately not on it.
951
+
952
+ A supervised launch returns as soon as there is something true to say — the
953
+ **first** of ready, exited, or four seconds — so a server that crashes on boot is
954
+ reported in 400 ms with the stack in the result rather than after a three-minute
955
+ stall:
956
+
957
+ ```
958
+ $ npm run dev
959
+ [background] service s1 started (pid 24188)
960
+ [ready] http://localhost:3000 (detected from output after 1.4s)
961
+ [log tail]
962
+ > next dev
963
+ - Local: http://localhost:3000
964
+ Read more with bash_output({ service: "s1" }); stop it with bash_kill({ service: "s1" }).
965
+ ```
966
+
967
+ Readiness is a URL the server printed, or one TCP connect to a port the command
968
+ named — and if that port was **already answering before the launch**, the probe
969
+ is switched off and the card says so, rather than calling somebody else's server
970
+ yours. A process that stays up without listening on anything (`tsc --watch`) is
971
+ reported as `running`, which is the honest third answer.
972
+
973
+ Each service gets a transcript card and a `svc N` chip in the status bar. Two
974
+ tools read and stop them:
975
+
976
+ - `bash_output({ service, since? })` — the log tail and the current status.
977
+ `since` is the cursor the previous call returned, so a watcher can be read
978
+ incrementally. The tail is a bounded in-memory ring of 200 rows; there are no
979
+ log files on disk.
980
+ - `bash_kill({ service })` — stop one, or `"all"`.
981
+
982
+ **Services do not survive the CLI.** Quitting, `Ctrl+C`, `SIGTERM` and a crash
983
+ all reap the tree on a best-effort basis; there is no daemon, no PID file and no
984
+ reattach across launches. A `SIGKILL` of aragon itself is unrecoverable by
985
+ construction, and if a stop cannot confirm the process is gone it says
986
+ `may have left a detached child` rather than claiming success.
987
+
988
+ From your side there are two keys and one command: `Ctrl+C` stops every live
989
+ service (and does *not* arm exit), `/bg` lists them, and `/bg logs s1` prints a
990
+ tail without going through the model.
991
+
992
+ ### Turning it off
993
+
994
+ ```jsonc
995
+ // ~/.aragon/config.json
996
+ { "bash": { "background": false } }
997
+ ```
998
+
999
+ `ARAGON_BASH_BACKGROUND=0` does the same for one run;
1000
+ `ARAGON_BASH_AUTO_BACKGROUND=0` keeps the tools but stops the classifier firing
1001
+ on a command that did not ask. With `background: false` the build behaves exactly
1002
+ as it did before this feature existed — no `background` parameter, no extra
1003
+ tools, no prompt block, no chip.
1004
+
1005
+ Two things are **not** governed by that switch, because they are fixes rather
1006
+ than features. `bash` now always settles: it returns when the process exits
1007
+ rather than when its output stream closes, so a command that detaches a
1008
+ grandchild can no longer leave the turn pending forever, and it is handed a
1009
+ closed stdin so a command that prompts fails instead of blocking on a read
1010
+ nobody will answer. And `Esc` twice always force-stops, whatever the flag says.
1011
+
911
1012
  ## Team subagents
912
1013
 
913
1014
  The agent can hand parts of a job to short-lived **subagents** that run in
@@ -1119,6 +1220,84 @@ it for a background advisory is how an unrequested call ends up competing with
1119
1220
  you for a rate-limited provider's quota. The review's own wall-clock bound is
1120
1221
  20 s.
1121
1222
 
1223
+ ## The context gauge
1224
+
1225
+ The right end of the status bar answers one question: **how full is the context,
1226
+ out of how much.**
1227
+
1228
+ ```
1229
+ [####----] 43% 86.0k/200.0k total 1.2M^ 48.0k v $3.21 12 tok/s 1m02s
1230
+ ^ ^ ^
1231
+ | | +-- session spend, prompt side / output side
1232
+ | +-- occupied / window
1233
+ +-- the two together, as a percentage
1234
+ ```
1235
+
1236
+ The readouts drop as the terminal narrows, in that order of priority:
1237
+
1238
+ | Columns | Right cluster |
1239
+ |---|---|
1240
+ | >= 96 | `[####----] 43% 86.0k/200.0k total 1.2M^ 48.0k v $3.21` |
1241
+ | 72-95 | `[####----] 43% 86.0k/200.0k $3.21` |
1242
+ | 60-71 | `[####----] 43% $3.21` |
1243
+ | < 60 | `43% $3.21` |
1244
+
1245
+ **Two different markers, because there are two different things to doubt.**
1246
+
1247
+ - `~` sits on the **percentage** and means the *numerator* is partly a guess:
1248
+ either no completed turn has reported usage yet (right after `/resume`, or
1249
+ right after a compaction), or tool results have been appended since the last
1250
+ measurement and their size is estimated.
1251
+ - `?` sits on the **window** and means the *denominator* is a guess: the model is
1252
+ not in the built-in table, so a flat 128 000 was substituted. See
1253
+ `contextWindow` below.
1254
+
1255
+ **The percentage keeps moving during a turn.** Every tool result nudges it,
1256
+ whether or not compaction is enabled — it is not a compaction feature, and a
1257
+ session started with `--no-compaction` gets the same live reading. After
1258
+ `/resume` the gauge shows the restored session's real occupancy immediately,
1259
+ without sending anything.
1260
+
1261
+ ### `/context`
1262
+
1263
+ The gauge is a bar and a rounded percentage; `/context` is the full answer, at
1264
+ any width, in any mode:
1265
+
1266
+ ```
1267
+ Context
1268
+ Occupancy 43% 86.2k of 200.0k [measured 85.1k + 1.1k estimated]
1269
+ Window 200000 from the model table
1270
+ Since measured 1.1k estimated tokens appended since the last provider-reported usage
1271
+ Compaction on - triggers at 90% (amber at 75%), 2 this session, 118.0k reclaimed
1272
+ Session spend 1.2M in (incl. 940.0k cache read, 12.0k cache write) / 48.2k out, $3.21
1273
+ includes subagent, fast-tier and compaction spend,
1274
+ not just this conversation
1275
+ ```
1276
+
1277
+ Two lines are worth reading closely. **`Window`** always names where the number
1278
+ came from — the model table, your own `contextWindow`, or the invented
1279
+ placeholder — because those are indistinguishable everywhere else. And
1280
+ **`Compaction`** reports what is actually true of this session, including
1281
+ `off - not registered for this session (started with --no-compaction)`; it never
1282
+ promises a threshold that nothing will act on.
1283
+
1284
+ ### `contextWindow`
1285
+
1286
+ When the model is not in the built-in table — a custom `baseUrl`, a self-hosted
1287
+ id, something released last week — the denominator is a **fabricated** 128 000
1288
+ and the percentage is meaningless. Correct it:
1289
+
1290
+ ```bash
1291
+ aragon config set contextWindow 1000000 # persisted; prints "Set contextWindow = 1000000"
1292
+ aragon config set contextWindow auto # back to the model table / placeholder
1293
+ ARAGON_CONTEXT_WINDOW=1000000 aragon # one session
1294
+ ```
1295
+
1296
+ Values are clamped to `[8000, 5000000]`; `auto` (the default) is stored as
1297
+ `null`. Setting it removes the `?` marker, because you have asserted the number —
1298
+ and `/context` still shows that it came from you, which is what makes a wrong
1299
+ value findable.
1300
+
1122
1301
  ## Context compaction
1123
1302
 
1124
1303
  A long agentic run fills the model's context window. Before this existed, the
@@ -1215,7 +1394,10 @@ Three surfaces, each answering something the others cannot:
1215
1394
  - **As state** — a `compacting` chip in the status bar, and the context gauge's
1216
1395
  colours aligned to your own thresholds. The gauge **falls immediately** after a
1217
1396
  compaction rather than waiting for the next turn, with a `~` while the value is
1218
- derived rather than measured.
1397
+ derived rather than measured. The colours follow your configured thresholds
1398
+ whenever compaction is **enabled**, even if no summarizer model currently
1399
+ resolves; after `/compact off` they fall back to the generic 60 / 85, because
1400
+ colouring by a rescue that is not coming is worse than not colouring at all.
1219
1401
  - **As a record** — a transcript card with the before/after message and token
1220
1402
  counts, the summarizer, the duration, and the summary itself. `Ctrl+O` expands
1221
1403
  it.
@@ -1228,7 +1410,9 @@ self-disabled, or the ladder ran out.
1228
1410
 
1229
1411
  `/compact status` is the guaranteed surface: the chip drops on a narrow terminal
1230
1412
  and the card scrolls away, but the command answers at any width, in any mode,
1231
- after the fact.
1413
+ after the fact. For the gauge specifically, [`/context`](#the-context-gauge) is
1414
+ the equivalent — and it works in a session that never registered compaction at
1415
+ all.
1232
1416
 
1233
1417
  ### When the RECENT turns are what does not fit
1234
1418
 
@@ -1609,6 +1793,8 @@ env / `.env` → CLI flags**.
1609
1793
  reasoning blocks), `ARAGON_LIVE_TOOL_OUTPUT` (`0` keeps a running tool card to
1610
1794
  one line),
1611
1795
  `ARAGON_MAX_TOKENS` (a number, or `auto` / `0` for the per-model ceiling),
1796
+ `ARAGON_CONTEXT_WINDOW` (a number, or `auto`; the denominator the context
1797
+ gauge measures against),
1612
1798
  `ARAGON_THEME`,
1613
1799
  `ARAGON_FULLSCREEN`, `ARAGON_PLAN` (`1` starts in plan mode),
1614
1800
  `ARAGON_TEAM` (`0` disables team subagents), `ARAGON_TEAM_MAX` (fan-out width),
@@ -1636,6 +1822,7 @@ env / `.env` → CLI flags**.
1636
1822
  | Config key | Default | Meaning |
1637
1823
  | --- | --- | --- |
1638
1824
  | `maxTokens` | `64000` | Output token cap, clamped to `[256, 200000]`. `null` means **auto** — use each model's own ceiling, never above 64000. Absent means the default. See [Output token limits](#output-token-limits). |
1825
+ | `contextWindow` | `null` | The window the context gauge measures against, clamped to `[8000, 5000000]`. `null` means **auto** — the built-in model table, or a fabricated 128000 for a model it has never seen. Set it when the gauge shows `?` on the denominator. See [The context gauge](#the-context-gauge). |
1639
1826
  | `showThinking` | `false` | Draw the reasoning the model returns. `thinkingLevel` is the effort the provider is asked to **spend**; this is whether the terminal **shows** it. Off by default: a settled turn that thought leaves one muted `thought for 12s` row in its place, so nothing is hidden silently. |
1640
1827
  | `liveToolOutput` | `true` | Draw up to eight sanitised rows of a **running** tool's output on its card, plus a `no output for Ns` row when the child goes quiet. On by default, unlike `showThinking`: this ADDS the information a long `bash` call otherwise hides, and its cost is bounded by construction — eight rows per call, sixteen calls, whatever the command emits. Turn it off and the card is a single `running` row again, with no store allocated and no recorder attached. |
1641
1828
  | `fullscreen` | `true` | Use the full-screen TUI (still subject to the automatic downgrades). Setting it to `false` opts out permanently; leaving it `true` is *not* a force — only `--fullscreen` / `ARAGON_FULLSCREEN=1` override the heuristics. |
@@ -1647,6 +1834,7 @@ env / `.env` → CLI flags**.
1647
1834
  | `planModeDefault` | `false` | Start every session in plan mode. `--no-plan` still overrides it for a single run. |
1648
1835
  | `planModeMaxAskRounds` | `4` | `ask_user` rounds allowed per user message, clamped to `[1, 10]`. The counter resets on your next message; steering does **not** reset it. |
1649
1836
  | `planModeHumanTimeoutMs` | `1800000` | Ceiling on a single question / plan wait, clamped to `[60000, 7200000]`. Thirty minutes is far outside "went to get coffee", but a ceiling has to exist so an unattended terminal cannot wedge a CI job forever. |
1837
+ | `paste` | `true` | Recognise pasted text before Ink's key parser sees it: line breaks become buffer newlines instead of fifteen separate messages, control bytes are stripped instead of overwriting the row they land on, and a paste above 6 lines or 400 characters collapses to a one-line placeholder that expands when you send. `false` treats pasted bytes as keystrokes, which is what 0.6.2 did. |
1650
1838
  | `historyEnabled` | `true` | Record submitted prompts to `prompt-history.jsonl` for `↑` recall. Setting it to `false` stops new writes; it does not delete what is already stored (that is `aragon history clear --yes`) and does not stop reading. See [Prompt history](#prompt-history). |
1651
1839
  | `team.enabled` | `true` | Master switch for team subagents. `false` means the `task` tool is never registered — `/team on` in such a session saves the setting for next launch and says so, rather than advertising a tool that is not there. |
1652
1840
  | `team.maxSubagents` | `5` | Subagents per dispatch, clamped to `[1, 10]`. The 10 is the hard ceiling and is enforced twice — on the config value and again on what the model asks for — so hand-editing this file cannot raise it. |
@@ -1712,6 +1900,8 @@ ARAGON_MAX_TOKENS=32000 aragon # env layer, between the flag and the file
1712
1900
  aragon config set maxTokens 32000 # persisted; prints "Set maxTokens = 32000"
1713
1901
  aragon config set maxTokens auto # persisted as null
1714
1902
  aragon config set maxTokens default # back to 64000
1903
+ aragon config set contextWindow 1000000 # the gauge's denominator
1904
+ aragon config set contextWindow auto # back to the model table
1715
1905
  aragon config get maxTokens # "64000" | "auto"
1716
1906
  ```
1717
1907
 
@@ -15,9 +15,12 @@ import type { ToolPermission } from '../exec/permission.js';
15
15
  import { type ToolOutputListener } from '../tools/tool-output-store.js';
16
16
  import type { FilePatch } from '../tools/patch.js';
17
17
  import type { TeamEvent, TeamSnapshot } from '../team/types.js';
18
+ import { ProcSupervisor } from '../proc/supervisor.js';
19
+ import type { ProcEventListener, ServiceSnapshot } from '../proc/types.js';
18
20
  import { type FastStatus } from '../fast/wiring.js';
19
- import type { FastEventListener } from '../fast/types.js';
20
- import type { CompactionEventListener, CompactionSnapshot } from '../compaction/types.js';
21
+ import type { FastEventListener, FastSnapshot } from '../fast/types.js';
22
+ import { ContextMeter, type ContextUsageListener } from '../compaction/meter.js';
23
+ import type { CompactionEventListener, CompactionSnapshot, ContextUsageSnapshot } from '../compaction/types.js';
21
24
  import type { TodoEventListener, TodoSnapshot } from '../todo/types.js';
22
25
  import { type HumanInputGate } from '../tools/human-input.js';
23
26
  import { SkillService, type ApprovalGate } from '../skills/service.js';
@@ -176,6 +179,34 @@ export declare class AgentController {
176
179
  private readonly outputListeners;
177
180
  private readonly fast;
178
181
  private readonly compaction;
182
+ private readonly contextMeter;
183
+ private readonly procs;
184
+ /**
185
+ * The host's notice channel, kept so `prompt()`'s totality has somewhere to
186
+ * report to (I-8). Every other consumer reads `deps.notify` through a closure
187
+ * built in the constructor; this one is called from a method, which is why it
188
+ * needs a field.
189
+ */
190
+ private readonly notifyHostFn;
191
+ /** Whether the SERVICE half is on: tools, prompt block, `background` param. */
192
+ private readonly bashBackgroundRegistered;
193
+ /** Release the exit reaper. Called by `dispose()`. */
194
+ private readonly releaseSignalHook;
195
+ /**
196
+ * Bumped by `forceStop()`; captured by `App` when it subscribes to a run.
197
+ *
198
+ * THE VIEW AND THE ENGINE DELIBERATELY DISAGREE AFTER A FORCE-STOP (D-10 /
199
+ * I-7). Rung two dispatches `runEnd` locally so the composer is usable
200
+ * immediately, but the engine keeps unwinding and keeps EMITTING -
201
+ * `turn_start`, `tool_call_start`, `turn_end`, `agent_end` - and
202
+ * `runStart`/`turnStart` would set `status: 'running'` again, bouncing the
203
+ * view straight back out of the `idle` the user was just given and appending
204
+ * entries to a transcript they believe is finished. `App` drops every event
205
+ * whose generation is stale, which is the same shape as `abortRequested`
206
+ * above and exists for the same reason: the loop legitimately emits after an
207
+ * abort lands.
208
+ */
209
+ private runGen;
179
210
  /**
180
211
  * Set SYNCHRONOUSLY by `abort()` before `agent.abort()` (§3.5.4a guard 1).
181
212
  * Cleared at the next `agent_start`.
@@ -234,7 +265,97 @@ export declare class AgentController {
234
265
  * never rejects, so callers must not treat resolution as success.
235
266
  */
236
267
  prompt(text: string): Promise<void>;
268
+ /**
269
+ * Wait for the engine to unwind, bounded.
270
+ *
271
+ * BOUNDED BECAUSE THE UNBOUNDED CASE IS THE ONE THIS FEATURE IS ABOUT: a tool
272
+ * that never settles is exactly why rung two exists, and `Agent.waitForIdle()`
273
+ * alone would hang here for as long as it hangs there. The budget is the two
274
+ * graces a wedged `bash` can legitimately spend, plus the executor's own.
275
+ */
276
+ private waitForEngineIdle;
277
+ /** The pre-round body of `prompt`, unchanged apart from being private. */
278
+ private startPrompt;
237
279
  abort(): void;
280
+ /**
281
+ * Rung two of the Esc ladder: stop the AGENT, whatever it is blocked on
282
+ * (§3.6 / G3).
283
+ *
284
+ * FOUR STEPS, AND THE FOURTH IS AN ABSENCE.
285
+ *
286
+ * 1. `abort()` - everything it does today, unchanged.
287
+ * 2. `killForeground('force')` - hard-kill every tracked foreground `bash`
288
+ * child. THIS is what actually unblocks a wedged `await tool.execute`;
289
+ * an abort signal a tool ignores is advisory, and rung two exists for
290
+ * precisely the tools that ignore it.
291
+ * 3. `runGen += 1` - so `App` can drop the events the engine will keep
292
+ * emitting while it unwinds (I-7). Without it the view bounces straight
293
+ * back out of the `idle` the user was just given.
294
+ * 4. IT DOES NOT TOUCH BACKGROUND SERVICES. `Esc` interrupts the AGENT;
295
+ * `Ctrl+C` stops the SERVICES. That asymmetry is the user's own two
296
+ * sentences and it is exactly the kind of thing a later reader will
297
+ * "fix" by making this kill everything - which would mean a user who
298
+ * pressed Esc twice to stop a runaway turn also lost the dev server they
299
+ * had been working against for an hour. It is invariant I-4.
300
+ *
301
+ * UNCONDITIONAL ON `bash.background` (P1-6 / D-9). The supervisor is always
302
+ * constructed; with services off its foreground registry is all there is, and
303
+ * that is the half this method uses.
304
+ */
305
+ forceStop(): void;
306
+ /**
307
+ * The current run generation (I-7).
308
+ *
309
+ * `App` captures it when it subscribes to a run and drops every engine event
310
+ * whose generation is stale. Same shape as the `abortRequested` guard the fast
311
+ * reviewer already uses, and for the same reason: the loop legitimately emits
312
+ * after an abort lands.
313
+ */
314
+ get runGeneration(): number;
315
+ /** Whether `bash_output` / `bash_kill` are in `this.tools`. Fixed at construction. */
316
+ isBackgroundRegistered(): boolean;
317
+ /**
318
+ * Subscribe to the CLI-local `ProcEvent` stream.
319
+ *
320
+ * Returns a live unsubscribe even when services are off, because the
321
+ * supervisor exists either way - the contract `subscribeTeam` /
322
+ * `subscribeTodos` / `subscribeFast` / `subscribeToolOutput` already set. With
323
+ * services off nothing is ever emitted, so an unconditional subscription in
324
+ * `App` costs one `Set` entry.
325
+ */
326
+ subscribeProc(listener: ProcEventListener): () => void;
327
+ getServiceSnapshot(id: string): ServiceSnapshot | undefined;
328
+ listServices(): ServiceSnapshot[];
329
+ /** `starting | ready | running`. Drives the status chip and the Ctrl+C rung. */
330
+ liveServiceCount(): number;
331
+ readServiceLog(id: string, since?: number): ReturnType<ProcSupervisor['read']>;
332
+ /**
333
+ * Stop one service, or every service when `id` is `'all'`.
334
+ *
335
+ * The GRACEFUL ladder (`SIGTERM` -> `SIGKILL` after `stopGraceMs`), because
336
+ * this path has a live event loop to run it on. `reapServicesSync` is the
337
+ * other one, and it is the only one an exit path may use.
338
+ */
339
+ stopService(id: string): Promise<ServiceSnapshot[]>;
340
+ /**
341
+ * Rung one of `Ctrl+C`: stop every live service (§3.6 / G4).
342
+ *
343
+ * `force: true` means `reapSync()` SEMANTICS - immediate `SIGKILL`, no
344
+ * `stopGraceMs` - and is what `doExit` uses, because `doExit` awaits nothing
345
+ * and must not start (§3.6 step 3). The graceful ladder belongs to `Ctrl+C`,
346
+ * `bash_kill` and `/bg stop`.
347
+ */
348
+ stopAllServices(opts?: {
349
+ force?: boolean;
350
+ }): Promise<ServiceSnapshot[]>;
351
+ /**
352
+ * The synchronous reaper (I-9).
353
+ *
354
+ * Exposed so `doExit` can call it without awaiting anything. It is also what
355
+ * the signal hook registered in the constructor calls, and it is idempotent -
356
+ * which it has to be, since a normal quit reaches it twice.
357
+ */
358
+ reapServicesSync(): void;
238
359
  steer(text: string): void;
239
360
  followUp(text: string): void;
240
361
  isRunning(): boolean;
@@ -493,6 +614,41 @@ export declare class AgentController {
493
614
  * `subscribeTodos` already set.
494
615
  */
495
616
  subscribeFast(listener: FastEventListener): () => void;
617
+ /**
618
+ * The FIFTH thin forwarder: the current fast-tier snapshot, or `null` when
619
+ * this session never registered a tier at all
620
+ * (web-use-tier-cooperation-and-control-closure §4.2.1).
621
+ *
622
+ * `null` and "a snapshot with `live: false`" are different answers and callers
623
+ * depend on the difference: the first means "there is no tier here", the
624
+ * second means "there is one and it is not usable right now".
625
+ *
626
+ * Deliberately NOT folded into the existing `review_end` event: that shape is
627
+ * consumed by `App` too, and this round promised to be purely additive.
628
+ */
629
+ fastSnapshot(): FastSnapshot | null;
630
+ /**
631
+ * Subscribe to occupancy publications. Returns an unsubscribe function.
632
+ *
633
+ * THE SOLE UPSTREAM OF `ViewState.context` (I-1). The gauge used to have two
634
+ * writers - `turnEnd` and a `contextTokensEstimated` dispatch fired from two
635
+ * places in `App` - which is the entire cause of P0-1: both wrote in the same
636
+ * synchronous fan-out and the second one won, restoring the pre-compaction
637
+ * figure one statement after the bar had correctly fallen. One writer makes
638
+ * that class of bug unrepresentable rather than merely fixed.
639
+ */
640
+ subscribeContextUsage(listener: ContextUsageListener): () => void;
641
+ /**
642
+ * Occupancy right now, re-measuring if the history has moved.
643
+ *
644
+ * `/context` READS THIS AND NOT `getCompactionSnapshot().pressure` (RV-8). The
645
+ * latter returns `offCompactionSnapshot()`'s hardcoded zero pressure when
646
+ * compaction was never registered, so a report built on it would read 0 % for
647
+ * precisely the sessions this feature exists to fix.
648
+ */
649
+ getContextUsage(): ContextUsageSnapshot;
650
+ /** The meter itself, for the wiring's single-instance assertion and tests. */
651
+ getContextMeter(): ContextMeter;
496
652
  /** Whether a `ContextManager` exists at all. Fixed at construction. */
497
653
  isCompactionRegistered(): boolean;
498
654
  /** Whether it will actually fire. Flipped live by `/compact on|off`. */