@aragon-agent/cli 0.6.1 → 0.6.3

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 (127) hide show
  1. package/CHANGELOG.md +216 -0
  2. package/README.md +241 -18
  3. package/dist/agent/agent-mode.d.ts +39 -0
  4. package/dist/agent/agent-mode.js +36 -0
  5. package/dist/agent/controller.d.ts +74 -1
  6. package/dist/agent/controller.js +200 -1
  7. package/dist/agent/headless.d.ts +8 -0
  8. package/dist/agent/headless.js +47 -0
  9. package/dist/agent/reducer.d.ts +120 -0
  10. package/dist/agent/reducer.js +201 -2
  11. package/dist/cli.js +297 -6
  12. package/dist/commands/builtins.js +73 -23
  13. package/dist/commands/registry.d.ts +27 -0
  14. package/dist/compaction/archive.d.ts +175 -0
  15. package/dist/compaction/archive.js +267 -0
  16. package/dist/compaction/child.d.ts +97 -0
  17. package/dist/compaction/child.js +113 -0
  18. package/dist/compaction/command.d.ts +41 -0
  19. package/dist/compaction/command.js +342 -0
  20. package/dist/compaction/compactor.d.ts +268 -0
  21. package/dist/compaction/compactor.js +689 -0
  22. package/dist/compaction/digest.d.ts +62 -0
  23. package/dist/compaction/digest.js +164 -0
  24. package/dist/compaction/limits.d.ts +165 -0
  25. package/dist/compaction/limits.js +172 -0
  26. package/dist/compaction/pressure.d.ts +135 -0
  27. package/dist/compaction/pressure.js +167 -0
  28. package/dist/compaction/summarize-call.d.ts +70 -0
  29. package/dist/compaction/summarize-call.js +86 -0
  30. package/dist/compaction/summary-prompt.d.ts +85 -0
  31. package/dist/compaction/summary-prompt.js +197 -0
  32. package/dist/compaction/tail-budget.d.ts +52 -0
  33. package/dist/compaction/tail-budget.js +67 -0
  34. package/dist/compaction/types.d.ts +179 -0
  35. package/dist/compaction/types.js +22 -0
  36. package/dist/compaction/wiring.d.ts +255 -0
  37. package/dist/compaction/wiring.js +556 -0
  38. package/dist/config/app-paths.d.ts +14 -0
  39. package/dist/config/app-paths.js +16 -0
  40. package/dist/config/cli-commands.d.ts +41 -0
  41. package/dist/config/cli-commands.js +77 -1
  42. package/dist/config/env.d.ts +4 -1
  43. package/dist/config/env.js +92 -2
  44. package/dist/config/load.d.ts +31 -0
  45. package/dist/config/load.js +63 -1
  46. package/dist/config/schema.d.ts +159 -0
  47. package/dist/config/schema.js +137 -0
  48. package/dist/config/store.js +17 -1
  49. package/dist/config/ui-state.d.ts +44 -6
  50. package/dist/config/ui-state.js +21 -12
  51. package/dist/diagnostics/doctor.js +41 -1
  52. package/dist/exec/events.d.ts +50 -1
  53. package/dist/exec/index.js +8 -4
  54. package/dist/exec/runner.js +44 -2
  55. package/dist/input/mouse-events.d.ts +31 -5
  56. package/dist/input/mouse-events.js +86 -27
  57. package/dist/input/stdin-mouse-filter.d.ts +8 -3
  58. package/dist/logging/install.d.ts +51 -1
  59. package/dist/logging/install.js +28 -0
  60. package/dist/logging/logger.d.ts +1 -1
  61. package/dist/session/cli-commands.js +23 -1
  62. package/dist/session/persist.js +19 -0
  63. package/dist/session/store.d.ts +18 -0
  64. package/dist/session/store.js +59 -2
  65. package/dist/team/report.js +7 -1
  66. package/dist/team/runtime.d.ts +10 -0
  67. package/dist/team/runtime.js +7 -0
  68. package/dist/team/subagent.d.ts +31 -1
  69. package/dist/team/subagent.js +42 -15
  70. package/dist/team/types.d.ts +8 -0
  71. package/dist/ui/ActivityLine.d.ts +19 -1
  72. package/dist/ui/ActivityLine.js +10 -2
  73. package/dist/ui/App.d.ts +48 -1
  74. package/dist/ui/App.js +317 -17
  75. package/dist/ui/Composer.d.ts +26 -1
  76. package/dist/ui/Composer.js +6 -6
  77. package/dist/ui/PromptInput.d.ts +34 -1
  78. package/dist/ui/PromptInput.js +54 -3
  79. package/dist/ui/StatusBar.d.ts +40 -0
  80. package/dist/ui/StatusBar.js +13 -4
  81. package/dist/ui/TeamPanel.js +7 -1
  82. package/dist/ui/Transcript.d.ts +21 -1
  83. package/dist/ui/Transcript.js +32 -2
  84. package/dist/ui/clipboard.d.ts +52 -0
  85. package/dist/ui/clipboard.js +91 -0
  86. package/dist/ui/entries/CompactionCard.d.ts +75 -0
  87. package/dist/ui/entries/CompactionCard.js +158 -0
  88. package/dist/ui/frame-differ.d.ts +46 -0
  89. package/dist/ui/frame-differ.js +94 -10
  90. package/dist/ui/gauge.d.ts +16 -1
  91. package/dist/ui/gauge.js +25 -6
  92. package/dist/ui/glyphs.d.ts +11 -0
  93. package/dist/ui/glyphs.js +2 -0
  94. package/dist/ui/layout/ScrollViewport.d.ts +43 -6
  95. package/dist/ui/layout/ScrollViewport.js +128 -21
  96. package/dist/ui/layout/budget.d.ts +10 -3
  97. package/dist/ui/layout/budget.js +10 -3
  98. package/dist/ui/layout/follow-state.d.ts +137 -0
  99. package/dist/ui/layout/follow-state.js +150 -0
  100. package/dist/ui/layout/virtual-window.d.ts +11 -0
  101. package/dist/ui/layout/virtual-window.js +34 -0
  102. package/dist/ui/overlays/HelpOverlay.js +18 -2
  103. package/dist/ui/overlays/SettingsScreen.d.ts +47 -1
  104. package/dist/ui/overlays/SettingsScreen.js +60 -1
  105. package/dist/ui/palettes.d.ts +16 -0
  106. package/dist/ui/palettes.js +6 -0
  107. package/dist/ui/screen.d.ts +22 -0
  108. package/dist/ui/screen.js +70 -8
  109. package/dist/ui/selection/highlight.d.ts +39 -0
  110. package/dist/ui/selection/highlight.js +64 -0
  111. package/dist/ui/selection/screen-mirror.d.ts +24 -0
  112. package/dist/ui/selection/screen-mirror.js +35 -0
  113. package/dist/ui/selection/selection-controller.d.ts +88 -0
  114. package/dist/ui/selection/selection-controller.js +282 -0
  115. package/dist/ui/selection/selection.d.ts +90 -0
  116. package/dist/ui/selection/selection.js +223 -0
  117. package/dist/ui/stdout-frame-writer.d.ts +33 -0
  118. package/dist/ui/stdout-frame-writer.js +15 -0
  119. package/dist/ui/theme.d.ts +11 -0
  120. package/dist/ui/theme.js +2 -0
  121. package/dist/ui/transcript-text.js +23 -0
  122. package/dist/ui/use-startup-notices.d.ts +62 -5
  123. package/dist/ui/use-startup-notices.js +81 -17
  124. package/dist/ui/use-wheel-routing.js +8 -0
  125. package/dist/ui/win-vt-force.d.ts +136 -0
  126. package/dist/ui/win-vt-force.js +237 -0
  127. package/package.json +4 -2
package/CHANGELOG.md CHANGED
@@ -5,6 +5,154 @@ All notable changes to `@aragon-agent/cli` are documented here. Entries at
5
5
  — those releases really were published under that name, so they are left as they
6
6
  were written.
7
7
 
8
+ ## Unreleased
9
+
10
+ ### Added
11
+
12
+ - **Context compaction, hardened — the trigger now counts the whole history, the
13
+ recent turns can no longer wedge a run, sub-agents get the same protection, and
14
+ a compaction is no longer irreversible.**
15
+
16
+ **The trigger was systematically late.** It fired on the provider's reported
17
+ usage for the PREVIOUS request, read at a checkpoint that runs AFTER the current
18
+ turn's tool results were appended — and tool results are the largest single
19
+ increment in this loop (100 KB per call, several per turn, ~25 k tokens each).
20
+ A session reading 88 % could be sending 110 %. Occupancy is now the measured
21
+ base PLUS an estimate of everything appended since that measurement, and the
22
+ status gauge reads the same number the trigger does, on every turn — including
23
+ the turns where a guard declines to compact, which is exactly when the number
24
+ matters most.
25
+
26
+ **The recent turns can no longer wedge a run.** Every previous rung of the
27
+ ladder operated on the head of the conversation. When the RETAINED tail was what
28
+ did not fit, each rung reported success or "nothing to drop" while the history
29
+ stayed un-sendable and the run died. Oversized `tool_result` bodies inside the
30
+ retained turns are now clipped, oldest first, only when the alternative is a
31
+ request the provider cannot accept. Nothing is removed — no message, no
32
+ tool-call id, no role — so a valid history stays valid. Every clip is announced
33
+ in the text the model reads, at the exact place the data went, and on the card,
34
+ in `/compact status`, on the JSON stream and in the archive.
35
+
36
+ **Sub-agents compact their own history** (`compaction.subagents`, default
37
+ `true`). A `task` child is bounded by 24 turns and fifteen minutes, and when one
38
+ overflowed its entire history was discarded and the dispatch reached the lead as
39
+ one partial sentence. Children run under tighter bounds than the lead: two
40
+ retained turns, at most two compactions, the fast tier preferred, and failure
41
+ forced to truncate because nobody is watching a background worker. No transcript
42
+ card — the lead's transcript describes the lead's context. Set
43
+ `compaction.subagents: false` to restore the previous behaviour exactly.
44
+
45
+ **A compaction is no longer irreversible** (`compaction.archive`, default
46
+ `true`). The dropped messages are written verbatim to `~/.aragon/compaction/`
47
+ before the splice is adopted. `/compact history` lists this run's archives,
48
+ `/compact show <n>` prints one's metadata and summary — never message bodies.
49
+ The directory is shared by every `aragon` on the machine, so files carry a
50
+ per-process run id and retention is two-tier: 20 files per run by count, plus a
51
+ seven-day sweep across all runs, so one session can never prune a live
52
+ neighbour's work. Writing is best-effort: a failure is logged and never affects
53
+ the compaction. This is deliberately **not** undo — restoring an over-full
54
+ history restores the condition that triggered the compaction.
55
+
56
+ **The long moment is legible.** The live card counts elapsed seconds and says
57
+ `esc to cancel`, which `AgentController.abort()` has always made true and no
58
+ surface has ever stated. A compaction that could only clip says so and names no
59
+ model, because none was called.
60
+
61
+ Two new config keys (`compaction.subagents`, `compaction.archive`), two new
62
+ environment variables (`ARAGON_COMPACTION_SUBAGENTS`,
63
+ `ARAGON_COMPACTION_ARCHIVE`), two new `/compact` verbs (`history`, `show <n>`),
64
+ and two new settings-screen rows. No flags were added.
65
+
66
+ - **Context compaction — a long run no longer dies when the window fills, and it
67
+ is ON by default.** A session's conversation only grows: nothing anywhere
68
+ removed a message, so a 40-turn tool-heavy run had exactly one ending available
69
+ to it once the context window filled — an HTTP 400 and a banner reading
70
+ *"Context length exceeded — start a new conversation with /reset."* The
71
+ product's answer to running out of context was "throw away everything you were
72
+ doing."
73
+
74
+ Now, at **90 %** occupancy, the run pauses at a turn boundary, hands the older
75
+ part of the history to a summarizer, and swaps it for a structured record. Your
76
+ original task stays **verbatim**, the most recent turns stay **verbatim**, and
77
+ the run continues in the same loop with the same tools, the same todo list and
78
+ the same working directory.
79
+
80
+ **This changes behaviour for existing users, in the direction of not dying.**
81
+ `--no-compaction`, or `compaction.enabled: false`, restores the old behaviour
82
+ exactly: no manager is constructed, nothing is subscribed, no registry is
83
+ allocated, and a full window ends the run as it did before. Both flag forms are
84
+ declared, so a stored `false` survives a run that passes no flags.
85
+
86
+ Three ways it fires. **Proactively** on measured occupancy — the case that
87
+ happens almost every time. **Reactively** when the provider returns
88
+ `context_overflow` anyway, because the model is not in the static window table
89
+ or you are pointed at a proxy in front of a smaller one: one compaction, one
90
+ re-send, run continues. And **manually**, via `/compact`, which runs
91
+ immediately when idle and queues to the next turn boundary when running.
92
+
93
+ The trigger has two terms. 90 % of a 200 k window leaves 20 k, which is plenty;
94
+ 90 % of a 32 k window leaves 3.2 k, which is less than a single `max_tokens` of
95
+ 8192 — the request is already impossible. So compaction also fires whenever the
96
+ remaining headroom drops below one full response.
97
+
98
+ **The default summarizer is your own model, and that costs real money.**
99
+ `compaction.useFastTier` is `true`, but it is a *preference*: the fast tier is
100
+ off unless you configured it, so out of the box the first compaction is a
101
+ ~30 k-token call on the session's own model. It happens at most 5 times per
102
+ run, the alternative is losing the run entirely, and the spend is visible — in
103
+ the session total, priced with the summarizer's own table, and reported as
104
+ `pricing unknown` rather than `$0.00` when that table has never seen the model.
105
+ Configure the fast tier to make it cheap.
106
+
107
+ Summarization fails sometimes. The ladder is one retry (on your **main** model
108
+ if the first attempt used the fast tier), then a **truncation** that says so on
109
+ the card, on the JSON event stream, and inside the block the model itself
110
+ reads. `compaction.onFailure: "stop"` makes the other trade.
111
+
112
+ Four guards stop it looping on your money: never two on consecutive turns,
113
+ never more than 5 per run, a progress requirement, and a self-disable after two
114
+ no-progress attempts with a notice naming the cause and the fix. The guards are
115
+ **trigger-aware** — the reactive path stays armed even after a self-disable,
116
+ because a compaction answering a request the provider has already refused is
117
+ the difference between a live run and a dead one.
118
+
119
+ Three UI surfaces: `Compacting context…` on the activity row while it runs, a
120
+ `compacting` chip plus threshold-aligned gauge colours as state, and a
121
+ transcript card with the before/after counts, the summarizer, the duration and
122
+ an expandable summary as the record. The gauge falls **immediately** rather
123
+ than waiting for the next turn. `/compact status` is the guaranteed surface at
124
+ any width.
125
+
126
+ New: `compaction` config section (6 keys), `--compaction` / `--no-compaction` /
127
+ `--compaction-threshold`, `ARAGON_COMPACTION*` (3 vars), `/compact` (6 forms),
128
+ three settings-screen rows, a `compaction` event on the `aragon exec` JSON
129
+ stream (`EXEC_SCHEMA_VERSION` **unchanged** — adding an event type never bumps
130
+ it), and two stderr lines under `-p`.
131
+
132
+ ### Changed
133
+
134
+ - **The context gauge now counts cache tokens**, through the same
135
+ `occupiedTokens` the compaction trigger reads. Anthropic reports `input_tokens`
136
+ *excluding* cached tokens and puts them in two separate fields, so the old
137
+ `inputTokens + outputTokens` silently under-reported occupancy for anyone
138
+ behind a caching gateway. That was already a defect in the number on the status
139
+ bar; it becomes a much worse one the moment a trigger fires on a different
140
+ number from the one you are watching.
141
+
142
+ - **`context_overflow` now names `/compact` alongside `/reset`.** That remedy
143
+ list was complete until this release, because throwing the conversation away
144
+ really was the only exit.
145
+
146
+ ### Known limitation
147
+
148
+ - **A summarizer that is 90 % faithful will, eventually, drop something the run
149
+ needed,** and no amount of prompt engineering closes that. The design bounds it
150
+ — verbatim task, verbatim recent turns, a block that tells the model the record
151
+ is partial, and three surfaces that make it visible so a human can step in —
152
+ but it does not eliminate it. That is inherent to compaction as a technique, it
153
+ is the same trade Claude Code and Codex make, and it is preferable by a wide
154
+ margin to the previous behaviour, which was to lose the entire run.
155
+
8
156
  ## 0.6.0
9
157
 
10
158
  ### Added
@@ -84,6 +232,74 @@ were written.
84
232
 
85
233
  ## Unreleased
86
234
 
235
+ ### Added
236
+
237
+ - **Drag the mouse to select text, and release to copy it.** Full-screen mode
238
+ takes the alternate screen, turns on mouse reporting so the wheel can scroll a
239
+ transcript the terminal no longer knows how to scroll, and draws every row
240
+ itself. Each of those is right on its own; together they took your terminal's
241
+ own drag-to-select away and put nothing in its place. The only thing shipped
242
+ in its place was a one-line notice naming a bypass key that is Shift on some
243
+ emulators, Fn on others and a preference elsewhere — a workaround, told once,
244
+ in a line that scrolls away.
245
+
246
+ Press the left button and drag: the cells you drag across are highlighted as
247
+ you go, and releasing copies exactly them. The copy goes out over **OSC 52**
248
+ first, so it reaches the clipboard of the machine you are *sitting at* rather
249
+ than the one you are ssh'd into, with the platform clipboard binary attempted
250
+ alongside it for terminals that ignore the sequence. Neither mechanism is
251
+ detectable, so the toast names what was sent rather than claiming a success it
252
+ cannot verify. `/copy` now goes through the same path and gained ssh support on
253
+ the way.
254
+
255
+ The highlight is painted in the frame pipeline rather than in React, so a drag
256
+ costs one to three re-addressed rows instead of a re-render of the whole tree,
257
+ and the text that is copied is read from the same mirrored rows the highlight
258
+ was painted over — what you see is what you get by construction, not by a
259
+ staleness check that can be wrong.
260
+
261
+ Two switches, both reversible without a restart: `/mouse off` hands the mouse
262
+ back to your terminal for the session (and says plainly that the wheel goes
263
+ inert while it is off), `/mouse on` takes it back, and
264
+ `aragon config set mouseSelect false` keeps wheel scrolling while leaving
265
+ drag-select off — which also stops the CLI asking the terminal for motion
266
+ reports at all, so that session puts exactly the bytes on the wire that it did
267
+ before this release. `--no-mouse` remains the switch that captures nothing.
268
+
269
+ ### Changed
270
+
271
+ - **Scrolling up now actually stops the transcript.** The viewport followed the
272
+ newest output by holding a *fixed distance from the bottom*, so scrolling up
273
+ during a run did not pause anything: twelve new rows of output moved the
274
+ window twelve rows down, and whatever you had scrolled up to read slid off the
275
+ top while you were reading it. The faster the agent talked, the faster your
276
+ reading position ran away.
277
+
278
+ While you are scrolled up, the transcript is now anchored to the **content**:
279
+ new output accumulates below the window instead of dragging the window along
280
+ with it, and the rows on screen hold still. After five seconds with no
281
+ scrolling the viewport returns to the newest line by itself — but only if
282
+ something actually arrived while you were paused, so reading a *finished*
283
+ transcript is never interrupted by a jump to a bottom that has not moved.
284
+ Every gesture restarts the countdown, a selection drag suspends it, and
285
+ `aragon config set scrollResumeMs 0` turns the return off entirely
286
+ (`ARAGON_SCROLL_RESUME_MS` sets it too).
287
+
288
+ The number the hint shows means something sharper as a result: it is now
289
+ literally "rows of newer output below your reading position", which is what
290
+ `PgDn` traverses and what the status bar's `↑N` has always shown.
291
+
292
+ - **The "N new lines · PgDn" hint moved inside the input box.** It used to be
293
+ rendered as the viewport's own last row, which spent a line of your transcript
294
+ on chrome at exactly the moment the transcript was longest — and, because the
295
+ hint's presence shrank the viewport, it fed back into the very number it was
296
+ displaying. It is now a right-aligned chip on the composer's input row, inside
297
+ the border, where it costs no rows at all. The chip sits in a fixed-width cell
298
+ so the number gaining a digit cannot re-wrap a draft, it never truncates (the
299
+ draft wraps first), and unlike the teaching row below the box it neither fades
300
+ with experience nor disappears on a short terminal: how far behind you are is
301
+ state, not a tutorial.
302
+
87
303
  ### Fixed
88
304
 
89
305
  - **The working spinner no longer disappears when an ack takes its row.** Steer a
package/README.md CHANGED
@@ -14,10 +14,14 @@ steer / configure the session — all without leaving the terminal.
14
14
 
15
15
  Node **≥ 18**, any platform.
16
16
 
17
- **On Windows, `Shift+Tab` and the mouse wheel additionally need Node ≥ 22.17.0
18
- (or ≥ 24.2.0).** Everything else — the agent loop, every tool, skills, teams,
19
- `aragon -p` — works exactly the same on Node 18 and 20, which is why `engines`
20
- stays at `>=18` rather than locking those users out.
17
+ **On Windows with Node < 22.17.0 (or 24.0/24.1), aragon turns the missing
18
+ console flag on itself at startup**, which restores `Shift+Tab` on most
19
+ machines. Where that is blocked — a locked-down PowerShell, an old console
20
+ host — **`Ctrl+P` toggles the mode instead**; it is bound on every platform and
21
+ does not depend on the console mode, the Node version, or anything else.
22
+ Everything else — the agent loop, every tool, skills, teams, `aragon -p` — works
23
+ exactly the same on Node 18 and 20, which is why `engines` stays at `>=18`
24
+ rather than locking those users out.
21
25
 
22
26
  The reason is not the terminal emulator and not a setting: on Windows both
23
27
  `Shift+Tab` (`CSI Z`) and mouse reporting exist only while the console input
@@ -27,9 +31,14 @@ itself, and that translation has no entry for `Shift+Tab` (it arrives as a
27
31
  plain `Tab`) and discards mouse records outright. Node 20 is the current
28
32
  Windows default LTS, so this is a common configuration rather than an exotic
29
33
  one — it is why the same build behaves differently on two machines. Below that
30
- version aragon says so at startup and turns mouse reporting off rather than
31
- asking for reports that can never arrive; use `/plan` to switch mode and
32
- `Shift+↑` / `Shift+↓` to scroll until you upgrade.
34
+ version aragon sets `ENABLE_VIRTUAL_TERMINAL_INPUT` itself, from a short
35
+ PowerShell helper, right after it puts the console into raw mode — the flag is a
36
+ property of the console, not of Node, so any process attached to it can turn it
37
+ on. When that works, `Shift+Tab` arrives as `CSI Z` again and mouse reporting
38
+ goes back on with it — the wheel should follow by the same route, though only
39
+ the key has been measured. When it does not work, aragon says so at startup and
40
+ turns mouse reporting off rather than asking for reports that can never arrive;
41
+ `Ctrl+P` and `/plan` switch mode, and `Shift+↑` / `Shift+↓` scroll.
33
42
 
34
43
  ## Install
35
44
 
@@ -368,6 +377,8 @@ below works there too, in either position — `aragon --model m exec "…"` and
368
377
  | `--fast-model <id>` | The fast model id. Implies nothing about `--fast` on its own |
369
378
  | `--fast-provider <id>` | Provider for the fast model. Absent inherits the main provider |
370
379
  | `--fast-review <n\|off>` | Turns between automatic fast reviews, or `off` |
380
+ | `--compaction` / `--no-compaction` | Compact the conversation when the context window fills, or never. **On** by default — see **Context compaction** |
381
+ | `--compaction-threshold <n>` | Occupancy that triggers compaction for this run: `0.9` or `90%`, clamped to `[0.5, 0.95]` |
371
382
  | `--no-skills` | Disable the skill system entirely (no catalog, no skill tools) |
372
383
  | `--skill <name>` | Force-load a skill's full body for this run (repeatable) |
373
384
  | `--skills-yes` | Approve skill installs for this run (CI / headless) |
@@ -401,6 +412,7 @@ signal ends the run (`130` SIGINT, `143` SIGTERM, `129` SIGHUP).
401
412
  | `/todo [on\|off\|panel on\|off\|follow <mode>\|clear\|continue]` | Todo planning: status, the on/off switch, the rail, follow-through, and picking an unfinished plan back up |
402
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 |
403
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
+ | `/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 |
404
416
  | `/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 |
405
417
  | `/theme <auto\|warm\|cool\|light>` | Switch the color theme live (persisted) |
406
418
  | `/expand [n]` | Expand / collapse the n-th-from-last (default last) tool card |
@@ -422,7 +434,8 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
422
434
  | --- | --- |
423
435
  | `Enter` | Submit (idle) / queue a steering message (running) |
424
436
  | `Alt+Enter` / `Shift+Enter` | Insert a newline |
425
- | `Shift+Tab` | Toggle **plan mode** (`BUILD` <-> `PLAN`). Equivalent: `/plan`. On Windows needs Node ≥ 22.17 — see [Requirements](#requirements) |
437
+ | `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
+ | `Ctrl+P` | The same toggle, over a channel no Windows console can swallow. Use it if `Shift+Tab` does nothing |
426
439
  | `Esc` | Abort the run / close an overlay / close a popup |
427
440
  | `Ctrl+C` ×2 | Exit (first press warns) |
428
441
  | `Ctrl+L` | Redraw the frame (clear the screen in inline mode) |
@@ -747,17 +760,28 @@ it is an informed act rather than a stray keypress.
747
760
 
748
761
  ### If `Shift+Tab` does nothing
749
762
 
750
- **On Windows, check your Node version first: `node -v`.** Below 22.17.0 (or on
751
- 24.0/24.1) the key never reaches aragon as `Shift+Tab` at all — it arrives as an
752
- ordinary `Tab`, so it does not switch mode, and with the `/` palette or an `@`
753
- popup open it completes that entry over your draft instead. The mouse wheel is
754
- dead for the same reason. See [Requirements](#requirements); aragon reports this
755
- once at startup when it detects it.
763
+ **Press `Ctrl+P`.** It is the same toggle and it does not depend on any of the
764
+ machinery below.
756
765
 
757
- Otherwise: some multiplexers and remote-desktop stacks never send `CSI Z`.
758
- Either way `/plan`, `/plan on`, `/plan off` and `/plan status` are first-class
759
- equivalents, and `--plan` / `ARAGON_PLAN=1` / `planModeDefault` cover the
760
- startup case.
766
+ To find out why, run this in the same terminal, press `Shift+Tab` once, then
767
+ `Ctrl+C`:
768
+
769
+ ```powershell
770
+ node -e "process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on('data',b=>{console.log(b.toString('hex'));if(b[0]===3)process.exit(0)})"
771
+ ```
772
+
773
+ | It prints | What it means |
774
+ | --- | --- |
775
+ | `09` | The console is swallowing the modifier — the key is arriving as an ordinary `Tab`. Check `node -v`: below 22.17.0 (or on 24.0/24.1) this is expected, and aragon's own attempt to fix it was blocked. See [Requirements](#requirements) |
776
+ | `1b5b5a` | The key reaches the process fine, so something inside aragon is eating it — most often an overlay is open, or the run is still going and the switch is pending (the status bar shows `BUILD → PLAN`) |
777
+ | nothing | Your terminal, multiplexer or remote-desktop stack never sent it; some never send `CSI Z` at all |
778
+
779
+ On the first row the wheel is dead for the same reason, and with the `/` palette
780
+ or an `@` popup open the stray `Tab` completes that entry over your draft.
781
+
782
+ `/plan`, `/plan on`, `/plan off` and `/plan status` are first-class equivalents
783
+ in every case, and `--plan` / `ARAGON_PLAN=1` / `planModeDefault` cover the
784
+ startup case. `aragon doctor` reports this machine's answer as `vt-input`.
761
785
 
762
786
  ### Headless
763
787
 
@@ -1095,6 +1119,205 @@ it for a background advisory is how an unrequested call ends up competing with
1095
1119
  you for a rate-limited provider's quota. The review's own wall-clock bound is
1096
1120
  20 s.
1097
1121
 
1122
+ ## Context compaction
1123
+
1124
+ A long agentic run fills the model's context window. Before this existed, the
1125
+ only ending available to it at that point was an HTTP 400 and a banner telling
1126
+ you to start over — which threw away everything the run had done.
1127
+
1128
+ Now the CLI **compacts** instead. When the conversation occupies **90 %** of the
1129
+ window, the run pauses at a turn boundary, hands the older part of the history to
1130
+ a summarizer, and swaps it for a structured record. Your original task stays
1131
+ **verbatim**, the most recent turns stay **verbatim**, and the run continues in
1132
+ the same loop with the same tools, the same todo list and the same working
1133
+ directory. Nothing about the session restarts.
1134
+
1135
+ **It is on by default**, unlike every other optional subsystem here, and that is
1136
+ deliberate: the fast tier and team mode *add* behaviour you did not ask for,
1137
+ while this one *removes* a failure you did not ask for. `--no-compaction` or
1138
+ `compaction.enabled: false` restores the old behaviour exactly — no manager is
1139
+ constructed, nothing is subscribed, and a full window ends the run as it did
1140
+ before.
1141
+
1142
+ ### What actually happens
1143
+
1144
+ 1. **Proactively**, when measured occupancy crosses the threshold. This is the
1145
+ case that fires almost every time.
1146
+ 2. **Reactively**, when the provider returns `context_overflow` anyway — because
1147
+ the model's context window is not in the static table, or you are pointed at a
1148
+ proxy in front of a smaller model. One compaction, one re-send, run continues.
1149
+ The engine allows exactly one of these per turn.
1150
+ 3. **Manually**, with `/compact`. While the agent is idle it runs immediately;
1151
+ while it is running it queues and fires at the next turn boundary.
1152
+
1153
+ The trigger has **two** terms, and the second one is not belt-and-braces: 90 % of
1154
+ a 200 k window leaves 20 k, which is plenty, but 90 % of a 32 k window leaves
1155
+ 3.2 k — less than a single `max_tokens` of 8192, so the request is already
1156
+ impossible. Compaction therefore also fires whenever the remaining headroom drops
1157
+ below one full response.
1158
+
1159
+ ### Which model summarizes, and what it costs
1160
+
1161
+ **By default, your own model.** `compaction.useFastTier` is `true`, but it
1162
+ describes a *preference*: the fast tier only resolves when you have configured
1163
+ it, and it is off by default. So out of the box the first compaction is a
1164
+ ~30 k-token call **on the session's own model** — real money on a frontier model,
1165
+ spent by a feature that is on by default.
1166
+
1167
+ Three things make that the right trade rather than a hidden cost. It happens at
1168
+ most 5 times per run. The alternative is losing the entire run. And the spend is
1169
+ visible: it goes into the session total, it is priced with the *summarizer's* own
1170
+ table, and `/compact status` reports `pricing unknown` rather than `$0.00` when
1171
+ the model is not in that table.
1172
+
1173
+ To make it cheap, configure the fast tier (`/fast on`, `/fast model <cheap-id>`).
1174
+ The transcript card then names the cheap model.
1175
+
1176
+ ### When it cannot summarize
1177
+
1178
+ Summarization is a network call and it will sometimes fail. The ladder is: one
1179
+ retry — on your **main** model if the first attempt used the fast tier, because
1180
+ "a fast model that cannot summarize" is the likeliest single failure — and then,
1181
+ by default, a **truncation**: the same cut, the same verbatim task, and a block
1182
+ that says plainly that the dropped messages were not summarized. That is
1183
+ announced on the card, in the transcript, on the JSON event stream and inside the
1184
+ block the model itself reads.
1185
+
1186
+ Truncation loses information, and it is still the default, because an unattended
1187
+ `aragon exec` that wedges on an unrecoverable 400 loses the whole run.
1188
+ `compaction.onFailure: "stop"` makes the other trade: the history is left alone,
1189
+ an error names your exits, and the next request will probably fail — which is
1190
+ what you asked for by setting it.
1191
+
1192
+ ### It will not loop on your money
1193
+
1194
+ Four independent guards: never two compactions on consecutive turns, never more
1195
+ than 5 per run, a splice that does not project a real reclaim does not count as
1196
+ progress, and two consecutive no-progress compactions **switch the proactive
1197
+ trigger off for the session** with a notice naming the cause and the fix.
1198
+
1199
+ The guards are trigger-aware. A *pressure* compaction that reclaims little has
1200
+ spent money to buy two turns and should stop. An *overflow* compaction that
1201
+ reclaims little is the difference between a live run and a dead one, because the
1202
+ provider has already refused the request — so the reactive path stays armed even
1203
+ after a self-disable, and an overflow attempt may halve `keepRecentTurns` for
1204
+ itself, which is the one lever that can make a too-large tail fit.
1205
+
1206
+ ### What you see
1207
+
1208
+ Three surfaces, each answering something the others cannot:
1209
+
1210
+ - **While it runs** — the activity row reads `Compacting context…`. It outranks
1211
+ the tool name and the rotating phrase, because compaction happens between
1212
+ turns and the phrase would otherwise claim the model is thinking. The
1213
+ transcript card counts the seconds and tells you `esc to cancel`, which has
1214
+ always been true and was never said.
1215
+ - **As state** — a `compacting` chip in the status bar, and the context gauge's
1216
+ colours aligned to your own thresholds. The gauge **falls immediately** after a
1217
+ compaction rather than waiting for the next turn, with a `~` while the value is
1218
+ derived rather than measured.
1219
+ - **As a record** — a transcript card with the before/after message and token
1220
+ counts, the summarizer, the duration, and the summary itself. `Ctrl+O` expands
1221
+ it.
1222
+
1223
+ **What a red gauge means:** the trigger is `>= threshold` while the red band is
1224
+ `> threshold`, so any occupancy that would paint red has already triggered
1225
+ compaction and the bar repaints downward in the same frame. Red is therefore only
1226
+ ever visible when compaction did **not** or **could not** run — it is off, it
1227
+ self-disabled, or the ladder ran out.
1228
+
1229
+ `/compact status` is the guaranteed surface: the chip drops on a narrow terminal
1230
+ and the card scrolls away, but the command answers at any width, in any mode,
1231
+ after the fact.
1232
+
1233
+ ### When the RECENT turns are what does not fit
1234
+
1235
+ Every rung above operates on the **head** of the conversation. When the retained
1236
+ tail is the problem — a few turns each carrying several 100 KB tool results —
1237
+ each of those rungs reports success or "nothing to drop" while the history stays
1238
+ un-sendable.
1239
+
1240
+ The last rung clips oversized `tool_result` bodies **inside the retained turns**,
1241
+ oldest first, stopping the moment the request fits. Nothing is removed: no
1242
+ message, no tool-call id, no role — so a history that was structurally valid
1243
+ before is still valid after. Every clip is announced **in the text the model
1244
+ reads**, at the exact place the data went:
1245
+
1246
+ ```
1247
+ [... 214003 characters removed by context compaction ...]
1248
+ ```
1249
+
1250
+ A model that sees that knows the output is partial and can re-run the tool. It is
1251
+ also stated on the transcript card, in `/compact status`, on the JSON event
1252
+ stream and in the archive. When clipping is the **only** thing that happened, the
1253
+ card says so and names no model, because none was called.
1254
+
1255
+ ### Sub-agents get the same protection
1256
+
1257
+ A `task` child is bounded by `team.maxTurnsPerSubagent` (24) and
1258
+ `team.dispatchTimeoutMs` (15 minutes) — the same profile as a lead run that fills
1259
+ a window. When a child overflowed, its history was discarded and the whole
1260
+ dispatch reached you as one partial sentence.
1261
+
1262
+ Children now compact their own history, under tighter bounds: **two** retained
1263
+ turns instead of four, at most **two** compactions per child instead of five, the
1264
+ fast tier preferred whenever it resolves, and `onFailure` forced to `truncate`
1265
+ because nobody is watching a background worker. There is no transcript card for a
1266
+ child — the lead's transcript describes the lead's context. Turn it off with
1267
+ `compaction.subagents: false`.
1268
+
1269
+ ### A compaction is no longer irreversible
1270
+
1271
+ Before a splice is adopted, the dropped messages are written verbatim to
1272
+ `~/.aragon/compaction/` — metadata, the summary, and the messages themselves.
1273
+
1274
+ `/compact history` lists **this run's** archives, newest first, with the path of
1275
+ each. `/compact show <n>` prints one archive's metadata and its stored summary;
1276
+ it never prints message bodies, because a single archive can be megabytes and the
1277
+ transcript is not a pager. The directory is shared by every `aragon` on the
1278
+ machine, so files carry a per-process run id: another terminal's compactions are
1279
+ counted in one trailing line rather than listed as yours.
1280
+
1281
+ Retention is two-tier and bounded: the newest 20 files **of this run**, plus a
1282
+ seven-day sweep across all runs. Writing is best-effort — a failure is logged and
1283
+ never affects the compaction. `compaction.archive: false` turns it off and writes
1284
+ nothing.
1285
+
1286
+ This is **not undo**: restoring an over-full history restores the condition that
1287
+ triggered the compaction, and the next request fails. It is fidelity on disk, so
1288
+ "the summary was wrong and the agent proceeded on a false record" is something
1289
+ you can check rather than something you have to accept.
1290
+
1291
+ ### Configuration
1292
+
1293
+ ```jsonc
1294
+ {
1295
+ "compaction": {
1296
+ "enabled": true, // false restores the pre-feature behaviour exactly
1297
+ "threshold": 0.9, // [0.5, 0.95]
1298
+ "warnThreshold": 0.75, // [0.4, threshold - 0.05]; where the gauge turns amber
1299
+ "keepRecentTurns": 4, // [1, 20] complete turns kept verbatim
1300
+ "useFastTier": true, // summarize with the fast tier WHEN it resolves
1301
+ "onFailure": "truncate", // or "stop"
1302
+ "subagents": true, // give `task` children their own compaction
1303
+ "archive": true // write the dropped messages to ~/.aragon/compaction
1304
+ }
1305
+ }
1306
+ ```
1307
+
1308
+ Environment: `ARAGON_COMPACTION=0|1`, `ARAGON_COMPACTION_THRESHOLD`,
1309
+ `ARAGON_COMPACTION_KEEP_TURNS`, `ARAGON_COMPACTION_SUBAGENTS`,
1310
+ `ARAGON_COMPACTION_ARCHIVE`.
1311
+
1312
+ ### The part that cannot be fixed
1313
+
1314
+ A summarizer that is 90 % faithful will, eventually, drop something the run
1315
+ needed. No amount of prompt engineering closes that. What this design does is
1316
+ bound it: your task is verbatim, the recent turns are verbatim, the block tells
1317
+ the model the record is partial, and three surfaces make it visible so you can
1318
+ step in. That is the same trade Claude Code and Codex make, and it is preferable
1319
+ by a wide margin to the alternative, which is losing the entire run.
1320
+
1098
1321
  ## Skills
1099
1322
 
1100
1323
  A skill is a reusable expert procedure stored on disk: a directory containing a
@@ -28,6 +28,45 @@ export declare const MODE_LABEL: Record<AgentMode, string>;
28
28
  * and nothing else. Two ship today (§1 non-goals).
29
29
  */
30
30
  export declare function nextMode(current: AgentMode): AgentMode;
31
+ /**
32
+ * The keys that toggle the mode, as the user sees them written
33
+ * (shift-tab-mode-toggle-still-dead-on-windows, C2).
34
+ *
35
+ * `fallback` exists because `primary` CANNOT be made to work everywhere. On a
36
+ * Windows console without `ENABLE_VIRTUAL_TERMINAL_INPUT`, libuv takes the
37
+ * character branch for `VK_TAB` and drops the modifier wholesale, so `Shift+Tab`
38
+ * and `Ctrl+Tab` both arrive as a bare `0x09` - byte-identical to `Tab`. C1
39
+ * turns that bit back on where it can, but it has three known ways to fail that
40
+ * are outside this package (an old conhost refusing the bit, a locked-down
41
+ * PowerShell, a foreign process resetting the mode), so a rung that depends on
42
+ * NOTHING has to exist underneath it.
43
+ *
44
+ * `ctrl+p` IS MEASURED, not guessed. On the lossy path it arrives as `0x10`,
45
+ * because Windows generates a real control character for `Ctrl+<letter>` and
46
+ * libuv's character branch forwards it untouched; `Alt+M` produces no bytes at
47
+ * all and `Ctrl+Tab` collapses exactly like `Shift+Tab` (analysis section 3.6).
48
+ * Any idle `Ctrl+<letter>` in the `0x01-0x1A` range would work; this one is free
49
+ * throughout the package - `keymap.ts` claims a/e/w/u/k, `App` claims c/l/t/o,
50
+ * and `PromptInput` returns early on every `key.ctrl` - so binding it takes no
51
+ * behaviour away from anyone. `Ctrl+B` survives the same path but is tmux's
52
+ * default prefix, and this binding is unconditional and cross-platform, so it
53
+ * would be silently dead for a whole class of macOS and Linux users.
54
+ *
55
+ * BOUND UNCONDITIONALLY, on every platform. A binding that only exists where it
56
+ * is needed cannot be documented in one place, cannot be taught by muscle
57
+ * memory, and is missing on precisely the machine whose capability probe was
58
+ * wrong.
59
+ *
60
+ * THESE ARE THE LABELS, NOT THE BINDING. Editing a value here changes only what
61
+ * the user is TOLD to press - the hint row, the startup notice, `doctor`. The
62
+ * key that is actually listened for is `input === 'p'` in `App`'s `useInput`,
63
+ * and the help overlay and README spell both keys out in their own title case.
64
+ * Change one without the others and the product advertises a key it ignores.
65
+ */
66
+ export declare const MODE_TOGGLE_KEYS: {
67
+ readonly primary: "shift+tab";
68
+ readonly fallback: "ctrl+p";
69
+ };
31
70
  /** The two tools that only exist because a human is attached. */
32
71
  export declare const PLAN_TOOL_NAMES: readonly ["ask_user", "submit_plan"];
33
72
  /**
@@ -30,6 +30,42 @@ export function nextMode(current) {
30
30
  const index = AGENT_MODES.indexOf(current);
31
31
  return AGENT_MODES[(index + 1) % AGENT_MODES.length];
32
32
  }
33
+ /**
34
+ * The keys that toggle the mode, as the user sees them written
35
+ * (shift-tab-mode-toggle-still-dead-on-windows, C2).
36
+ *
37
+ * `fallback` exists because `primary` CANNOT be made to work everywhere. On a
38
+ * Windows console without `ENABLE_VIRTUAL_TERMINAL_INPUT`, libuv takes the
39
+ * character branch for `VK_TAB` and drops the modifier wholesale, so `Shift+Tab`
40
+ * and `Ctrl+Tab` both arrive as a bare `0x09` - byte-identical to `Tab`. C1
41
+ * turns that bit back on where it can, but it has three known ways to fail that
42
+ * are outside this package (an old conhost refusing the bit, a locked-down
43
+ * PowerShell, a foreign process resetting the mode), so a rung that depends on
44
+ * NOTHING has to exist underneath it.
45
+ *
46
+ * `ctrl+p` IS MEASURED, not guessed. On the lossy path it arrives as `0x10`,
47
+ * because Windows generates a real control character for `Ctrl+<letter>` and
48
+ * libuv's character branch forwards it untouched; `Alt+M` produces no bytes at
49
+ * all and `Ctrl+Tab` collapses exactly like `Shift+Tab` (analysis section 3.6).
50
+ * Any idle `Ctrl+<letter>` in the `0x01-0x1A` range would work; this one is free
51
+ * throughout the package - `keymap.ts` claims a/e/w/u/k, `App` claims c/l/t/o,
52
+ * and `PromptInput` returns early on every `key.ctrl` - so binding it takes no
53
+ * behaviour away from anyone. `Ctrl+B` survives the same path but is tmux's
54
+ * default prefix, and this binding is unconditional and cross-platform, so it
55
+ * would be silently dead for a whole class of macOS and Linux users.
56
+ *
57
+ * BOUND UNCONDITIONALLY, on every platform. A binding that only exists where it
58
+ * is needed cannot be documented in one place, cannot be taught by muscle
59
+ * memory, and is missing on precisely the machine whose capability probe was
60
+ * wrong.
61
+ *
62
+ * THESE ARE THE LABELS, NOT THE BINDING. Editing a value here changes only what
63
+ * the user is TOLD to press - the hint row, the startup notice, `doctor`. The
64
+ * key that is actually listened for is `input === 'p'` in `App`'s `useInput`,
65
+ * and the help overlay and README spell both keys out in their own title case.
66
+ * Change one without the others and the product advertises a key it ignores.
67
+ */
68
+ export const MODE_TOGGLE_KEYS = { primary: 'shift+tab', fallback: 'ctrl+p' };
33
69
  /** The two tools that only exist because a human is attached. */
34
70
  export const PLAN_TOOL_NAMES = ['ask_user', 'submit_plan'];
35
71
  /**