@aragon-agent/cli 0.6.15 → 0.6.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +108 -71
  2. package/dist/agent/controller.d.ts +10 -0
  3. package/dist/agent/controller.js +52 -47
  4. package/dist/agent/reducer.js +0 -4
  5. package/dist/cli.js +49 -15
  6. package/dist/commands/builtins.js +27 -43
  7. package/dist/compaction/child.d.ts +10 -8
  8. package/dist/compaction/child.js +23 -25
  9. package/dist/compaction/context-command.js +14 -3
  10. package/dist/compaction/limits.d.ts +0 -2
  11. package/dist/compaction/limits.js +0 -2
  12. package/dist/compaction/types.d.ts +6 -1
  13. package/dist/config/app-paths.d.ts +10 -0
  14. package/dist/config/app-paths.js +12 -0
  15. package/dist/config/cli-commands.d.ts +1 -1
  16. package/dist/config/cli-commands.js +16 -15
  17. package/dist/config/env.d.ts +2 -1
  18. package/dist/config/env.js +13 -1
  19. package/dist/config/load.d.ts +9 -17
  20. package/dist/config/load.js +14 -6
  21. package/dist/config/model-windows.d.ts +91 -0
  22. package/dist/config/model-windows.js +181 -0
  23. package/dist/config/schema.d.ts +34 -5
  24. package/dist/config/schema.js +15 -11
  25. package/dist/exec/events.d.ts +12 -8
  26. package/dist/exec/events.js +2 -2
  27. package/dist/exec/runner.js +4 -1
  28. package/dist/fast/limits.d.ts +2 -2
  29. package/dist/fast/limits.js +2 -2
  30. package/dist/fast/prompt.d.ts +10 -11
  31. package/dist/fast/prompt.js +19 -20
  32. package/dist/fast/reviewer.d.ts +1 -1
  33. package/dist/fast/reviewer.js +1 -1
  34. package/dist/fast/types.d.ts +0 -2
  35. package/dist/fast/wiring.d.ts +18 -34
  36. package/dist/fast/wiring.js +20 -51
  37. package/dist/input/csiu-keys.d.ts +66 -0
  38. package/dist/input/csiu-keys.js +170 -0
  39. package/dist/input/keyboard-enhancement.d.ts +42 -0
  40. package/dist/input/keyboard-enhancement.js +65 -0
  41. package/dist/input/stdin-filter.d.ts +9 -0
  42. package/dist/input/stdin-filter.js +45 -9
  43. package/dist/input/win32-input-mode.d.ts +61 -0
  44. package/dist/input/win32-input-mode.js +251 -0
  45. package/dist/session/validate-session.js +3 -1
  46. package/dist/team/child-reviewer.d.ts +105 -0
  47. package/dist/team/child-reviewer.js +117 -0
  48. package/dist/team/normalize.d.ts +7 -26
  49. package/dist/team/normalize.js +9 -22
  50. package/dist/team/prompt.d.ts +0 -11
  51. package/dist/team/prompt.js +3 -12
  52. package/dist/team/report.d.ts +4 -19
  53. package/dist/team/report.js +1 -27
  54. package/dist/team/runtime.d.ts +5 -10
  55. package/dist/team/runtime.js +35 -39
  56. package/dist/team/subagent.d.ts +18 -18
  57. package/dist/team/subagent.js +80 -30
  58. package/dist/team/task-tool.d.ts +6 -26
  59. package/dist/team/task-tool.js +13 -47
  60. package/dist/team/types.d.ts +3 -42
  61. package/dist/ui/App.js +3 -4
  62. package/dist/ui/BottomStatusRow.js +2 -1
  63. package/dist/ui/StatusBar.js +2 -1
  64. package/dist/ui/TeamPanel.js +1 -1
  65. package/dist/ui/entries/TeamCard.js +1 -6
  66. package/dist/ui/layout/status-detail-layout.js +12 -2
  67. package/dist/ui/layout/status-layout.d.ts +4 -2
  68. package/dist/ui/layout/status-layout.js +24 -1
  69. package/dist/ui/overlays/HelpOverlay.js +1 -2
  70. package/package.json +2 -2
  71. package/dist/ui/ModeChip.d.ts +0 -18
  72. package/dist/ui/ModeChip.js +0 -24
package/README.md CHANGED
@@ -409,7 +409,7 @@ signal ends the run (`130` SIGINT, `143` SIGTERM, `129` SIGHUP).
409
409
  | `/team [on\|off\|max <n>]` | Team subagents: status and the live roster, the on/off switch, the fan-out width |
410
410
  | `/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 |
411
411
  | `/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 |
412
- | `/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 |
412
+ | `/fast [on\|off\|model <id>\|provider <id>\|same\|review <n\|off>]` | Fast model tier: status and this session's totals, the on/off switch, which model, the review cadence |
413
413
  | `/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 |
414
414
  | `/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 |
415
415
  | `/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 |
@@ -433,7 +433,7 @@ completes, `Up` / `Down` moves the selection, `Esc` closes it.
433
433
  | Key | Action |
434
434
  | --- | --- |
435
435
  | `Enter` | 空闲时发送,运行时加入队列;收到 Core 已纳入会话的接收凭据后才移除 Queue 提示 |
436
- | `Ctrl+J` / `Alt+Enter` / `Shift+Enter` | 插入换行。Ctrl+J 已通过输入链路测试;另外两种组合键需要宿主发送独立序列,Alt+Enter 可能被宿主拦截。参见 [终端设置](#terminal-setup-for-shiftenter) |
436
+ | `Ctrl+J` / `Alt+Enter` / `Shift+Enter` | 插入换行。应用默认请求终端区分修饰键(Windows 用 win32-input-mode,macOS/Linux 用 kitty/modifyOtherKeys),Shift+Enter 在支持的终端开箱即用;不支持的终端用 Ctrl+J。参见 [终端设置](#terminal-setup-for-shiftenter) |
437
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
438
  | `Ctrl+P` | The same toggle, over a channel no Windows console can swallow. Use it if `Shift+Tab` does nothing |
439
439
  | `Esc` | Close an overlay or popup; otherwise ask for interruption confirmation. While idle, cancel pending auto-continuation |
@@ -490,10 +490,13 @@ is what it used to do. Inline mode is unaffected either way.
490
490
 
491
491
  ## Terminal setup for Shift+Enter
492
492
 
493
- 输入支持 `Ctrl+J`、`Alt+Enter` 和可独立编码的 `Shift+Enter` 换行。如果终端把
494
- Shift+Enter 与 Enter 都发送为 CR,应用无法区分;请用 `/terminal-setup` 查看绑定,
495
- 让 Shift+Enter 发送 `\u001b[13;2u`。合并到已有终端配置,不要覆盖其他快捷键;
496
- 终端拦截某个组合键时可使用另外两种方式。参考绑定如下:
493
+ 应用现在会主动请求终端区分修饰键:Windows 上推送 win32-input-mode(conhost 与
494
+ Windows Terminal 均支持),macOS/Linux 上推送 kitty disambiguate 与
495
+ `modifyOtherKeys=2`(iTerm2、kitty、ghostty、WezTerm、foot、Alacritty 实现其一),
496
+ 并在 stdin 过滤器里把新编码翻译回传统字节,Ink 的按键解析不受影响。上述推送不满足时
497
+ (macOS Terminal.app、旧 Node 下的 conhost、或不支持任何协议的终端)Shift+Enter 仍与
498
+ Enter 同为 CR,此时用 `Ctrl+J` 换行,或按 `/terminal-setup` 给终端加一条让
499
+ Shift+Enter 发送 `\u001b[13;2u` 的绑定(合并进已有配置,不要覆盖其他快捷键)。参考绑定如下:
497
500
 
498
501
  - **Windows Terminal** — `settings.json` 的 `actions` 与 `keybindings` 分别定义动作和按键;完整示例见下文“输入、待处理队列与底部状态区”。
499
502
  - **VS Code integrated terminal** — `keybindings.json`:
@@ -507,10 +510,14 @@ Shift+Enter 与 Enter 都发送为 CR,应用无法区分;请用 `/terminal-s
507
510
  - **macOS Terminal.app / legacy conhost** — cannot rebind `Enter`; use
508
511
  `Ctrl+J` or `Alt+Enter`
509
512
 
510
- aragon deliberately does not push kitty keyboard mode or `modifyOtherKeys=2`
511
- onto the terminal itself: mode 2 re-encodes *every* modified key as CSI-u and
512
- would blind this app's (and Ink's) key parsing for the whole session. One
513
- user-side binding reaches the same result for the one key that needs it.
513
+ The push and its translation ship as one unit: `input/win32-input-mode.ts` and
514
+ `input/csiu-keys.ts` translate every enhanced encoding back to the legacy
515
+ bytes before Ink parses them, so a translated stream is byte-identical to a
516
+ pre-enhancement one (Enter family excepted: a modified Enter becomes the
517
+ newline frame). Windows additionally requires Node >= 22.17 / >= 24.2 --
518
+ older versions never turn on virtual-terminal input. Opt out with
519
+ `aragon config set keyboardEnhancement false`, `--no-keyboard-enhancement`,
520
+ or `ARAGON_KEYBOARD_ENHANCEMENT=0`.
514
521
 
515
522
  ## Queued messages while the agent runs
516
523
 
@@ -803,9 +810,11 @@ still exits the session outright.
803
810
 
804
811
  ### Where the mode is shown
805
812
 
806
- The status bar always names a non-default mode (`PLAN`, or `PLAN → BUILD` while
807
- a switch is pending), including on short terminals and with `--no-hints`. The composer additionally shows a `PLAN` chip on its hint row and
808
- tints its border, where there is room for it.
813
+ The status bar's first row always names the current mode (`BUILD` or `PLAN`,
814
+ an accent word right after the run state; `PLAN>BUILD` while a switch is
815
+ pending; `B` / `P` / `P>B` when the terminal is narrow), including on short
816
+ terminals and with `--no-hints`. The Ctrl+G details row repeats it, and the
817
+ composer tints its border in plan mode.
809
818
 
810
819
  ### Switching mid-run
811
820
 
@@ -814,7 +823,7 @@ during a run takes effect immediately — you pressed it *because* the agent is
814
823
  about to do something. Pressing it toward `BUILD` during a run is **deferred to
815
824
  the end of the run**, so a session you launched under a read-only guarantee
816
825
  cannot start writing files because of one stray keypress. The status bar shows
817
- `PLAN → BUILD` while that is pending. Approving a plan is the one exception, and
826
+ `PLAN>BUILD` while that is pending. Approving a plan is the one exception, and
818
827
  it is an informed act rather than a stray keypress.
819
828
 
820
829
  ### If `Shift+Tab` does nothing
@@ -832,7 +841,7 @@ node -e "process.stdin.setRawMode(true);process.stdin.resume();process.stdin.on(
832
841
  | It prints | What it means |
833
842
  | --- | --- |
834
843
  | `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) |
835
- | `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`) |
844
+ | `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 `PLAN>BUILD`) |
836
845
  | nothing | Your terminal, multiplexer or remote-desktop stack never sent it; some never send `CSI Z` at all |
837
846
 
838
847
  On the first row the wheel is dead for the same reason, and with the `/` palette
@@ -1177,25 +1186,24 @@ children's tokens are folded into the `[usage]` footer.
1177
1186
  ## Fast model tier
1178
1187
 
1179
1188
  A second, cheaper model can be made a first-class citizen of a session. It is
1180
- **off by default** — with `fast.enabled: false` the system prompt, the `task`
1181
- tool schema, the transcript and the request payloads are exactly what they were
1182
- before this feature existed — and it buys you two things once you turn it on.
1183
-
1184
- **Delegation (the agent decides).** `task` gains one optional field per
1185
- subagent, `model: "fast"`. A child marked that way runs on the fast model and is
1186
- otherwise identical: same tools, same working directory, same `--confirm` gate,
1187
- same plan-mode gate, same skills ceiling. Delegating to a cheaper model is a
1188
- *model* choice, never a permission boundary. This is where "not complicated, but
1189
- very expensive in context" is actually paid for: a child that reads nine files
1190
- and reports four sentences moves ~200 KB of file bodies out of the lead's window
1191
- and onto a model that costs a fraction as much per token.
1189
+ **off by default** — with `fast.enabled: false` the system prompt, the
1190
+ transcript and the request payloads are exactly what they were before this
1191
+ feature existed — and it buys you two things once you turn it on.
1192
+
1193
+ **Every `task` child runs the lead's own model.** Per-child fast dispatch
1194
+ (`model: "fast"`) was removed for main-agent parity: a delegation is never a
1195
+ downgrade, and all the gates (tools, working directory, `--confirm`, plan mode,
1196
+ skills ceiling, compaction policy) are the lead's own.
1192
1197
 
1193
1198
  **Periodic review (the harness decides).** Every *N* completed turns, a small
1194
1199
  digest of what the lead has been doing goes to the fast model with one question
1195
1200
  — is this still on track? — and if the answer is not "on track" it is injected
1196
1201
  back into the running loop as a `<fast_review>` block. The lead sees a short
1197
1202
  second opinion mid-run, when it can still act on it, for a few hundred tokens
1198
- instead of a second full-size turn.
1203
+ instead of a second full-size turn. **Subagents get the same review**: when the
1204
+ tier is live and `fast.review` is on, each dispatched child runs with its own
1205
+ reviewer — same cadence, same budget per child, same fail-fast transport — so a
1206
+ team is as capable as the agent that dispatched it.
1199
1207
 
1200
1208
  ```
1201
1209
  > refactor the config loader and keep the tests green
@@ -1217,7 +1225,7 @@ separately. If the fast model is not in the built-in price table the number is
1217
1225
  shown as `unknown` rather than as `$0.00` — a feature that looks free while it
1218
1226
  is spending money is worse than one that admits it does not know.
1219
1227
 
1220
- **The review has a session budget, and delegation deliberately does not.** A
1228
+ **The review has a session budget.** A
1221
1229
  session is not one run — it is one run per message you send — so a cadence alone
1222
1230
  bounds nothing over a working afternoon. `fast.reviewMaxPerSession` (default
1223
1231
  **40**) is the total number of reviews a session may *start*; reaching it prints
@@ -1226,9 +1234,7 @@ first review rather than a bare count you can only size afterwards. Raise it liv
1226
1234
  with `/fast budget 80`. The budget is denominated in **reviews, not currency**,
1227
1235
  because the fast tier is precisely where a model the price table has never seen
1228
1236
  lives — a dollar ceiling would silently never fire for the configuration that
1229
- most needs one. Delegation is never budgeted: it is spend the lead chose in
1230
- service of a message you sent, it is already bounded by `team.*`, and it exists
1231
- to *reduce* total cost.
1237
+ most needs one.
1232
1238
 
1233
1239
  ### Turning it on
1234
1240
 
@@ -1248,9 +1254,8 @@ cannot grow a field mid-session.
1248
1254
  | `fast.provider` | `''` | Empty inherits the main provider. |
1249
1255
  | `fast.model` | `''` | The fast model id. Empty means *not configured* — it never falls back to the main model, so "same as main" stays distinguishable from a typo. |
1250
1256
  | `fast.baseUrl` | `''` | Empty inherits the session's base URL **only** when the provider matches. |
1251
- | `fast.thinkingLevel` | `'off'` | Applied to fast children and to reviews. |
1252
- | `fast.delegate` | `true` | Allow `model:"fast"` on `task`. |
1253
- | `fast.review` | `true` | Run the periodic review. |
1257
+ | `fast.thinkingLevel` | `'off'` | Applied to reviews (lead and subagents) and to fast-tier compaction summaries. |
1258
+ | `fast.review` | `true` | Run the periodic review — for the lead and for every `task` child. |
1254
1259
  | `fast.reviewEveryTurns` | `5` | Turns between reviews (1–50). |
1255
1260
  | `fast.reviewContextTurns` | `3` | Turns included in one digest (1–10). |
1256
1261
  | `fast.reviewMaxChars` | `280` | Ceiling on an injected critique (80–600). |
@@ -1274,7 +1279,6 @@ reporting one:
1274
1279
  /fast same run the fast tier on the main model
1275
1280
  /fast review <n> | off
1276
1281
  /fast budget [n] the session review budget; bare form reads it
1277
- /fast delegate on | off
1278
1282
  ```
1279
1283
 
1280
1284
  `/fast budget` with no argument is a **read**, so it is answerable while a `task`
@@ -1283,9 +1287,8 @@ like every other `/fast` setting.
1283
1287
 
1284
1288
  **What you see.** A `fast` chip in the status bar's right cluster while the tier
1285
1289
  is live (`fast*` while a review is in flight, dropped below 100 columns), a
1286
- one-line card per review in the transcript, and `n fast` plus a trailing `~` on
1287
- fast children in the team roster. `/fast status` reports the session totals the
1288
- status bar cannot: reviews run, children delegated, tokens and cost. Under `-p`
1290
+ one-line card per review in the transcript. `/fast status` reports the session totals the
1291
+ status bar cannot: reviews run, tokens and cost. Under `-p`
1289
1292
  nothing extra is written to stdout or stderr — the review spend is folded into
1290
1293
  the `[usage]` footer and the reviews are in the log.
1291
1294
 
@@ -1353,7 +1356,8 @@ Context
1353
1356
  ```
1354
1357
 
1355
1358
  Two lines are worth reading closely. **`Window`** always names where the number
1356
- came from — the model API, the model table, your own `contextWindow`, or the invented
1359
+ came from — the model API, the model table, your own `contextWindow`, a
1360
+ `model-windows.json` entry, or the invented
1357
1361
  placeholder — because those are indistinguishable everywhere else. And
1358
1362
  **`Compaction`** reports what is actually true of this session, including
1359
1363
  `off - not registered for this session (started with --no-compaction)`; it never
@@ -1361,8 +1365,9 @@ promises a threshold that nothing will act on.
1361
1365
 
1362
1366
  ### `contextWindow`
1363
1367
 
1364
- Window resolution uses your explicit override first, then API metadata from the
1365
- configured endpoint, then the built-in catalog (including known aliases and dated
1368
+ Window resolution uses your explicit override first, then per-model declarations
1369
+ in `model-windows.json`, then API metadata from the configured endpoint, then the
1370
+ built-in catalog (including known aliases and dated
1366
1371
  snapshots). Startup, connection changes, and subsequent prompts refresh discovery
1367
1372
  in the background, with a five-minute cache. OpenAI-compatible endpoints may expose
1368
1373
  `context_length` or `context_window`; Anthropic uses `max_input_tokens`; Google uses
@@ -1384,6 +1389,33 @@ Values are clamped to `[8000, 5000000]`; `auto` (the default) is stored as
1384
1389
  and `/context` still shows that it came from you, which is what makes a wrong
1385
1390
  value findable.
1386
1391
 
1392
+ #### `model-windows.json` — one window per model
1393
+
1394
+ `contextWindow` describes ONE model: the active one. To pin several at once — a
1395
+ main model plus a fast tier plus a gateway alias — declare them by id in
1396
+ `~/.aragon-agent/model-windows.json`:
1397
+
1398
+ ```json
1399
+ {
1400
+ "version": 1,
1401
+ "windows": {
1402
+ "kimi-k3": 1048576,
1403
+ "glm-5.3": 1048576,
1404
+ "glm-5.3-flash": 1048576
1405
+ }
1406
+ }
1407
+ ```
1408
+
1409
+ A bare map without the wrapper works too. Keys are matched case-insensitively
1410
+ after stripping a `models/`-style prefix and a dated snapshot suffix, so
1411
+ `Kimi-K3-20260716` still resolves. Entries must be whole tokens between 1024 and
1412
+ 1e9; at most 512 are kept. The file is read live (mtime-cached), never written
1413
+ by the CLI, and outranks API discovery and the catalog while still losing to the
1414
+ active model's `contextWindow` override. `/context` reports these windows as
1415
+ `from model-windows.json`, and a file that cannot be parsed is ignored with one
1416
+ log line rather than trusted. UTF-8 (with or without BOM) and UTF-16 saves are
1417
+ both accepted.
1418
+
1387
1419
  ## Context compaction
1388
1420
 
1389
1421
  A long agentic run fills the model's context window. Before this existed, the
@@ -1522,19 +1554,20 @@ refusal is explained. Automatic tail clipping and failure truncation are disable
1522
1554
 
1523
1555
  ### Sub-agents get the same protection
1524
1556
 
1525
- A `task` child is bounded by `team.maxTurnsPerSubagent` (24) and
1526
- `team.dispatchTimeoutMs` (15 minutes) — the same profile as a lead run that fills
1527
- a window. When a child overflowed, its history was discarded and the whole
1528
- dispatch reached you as one partial sentence.
1529
-
1530
- Children now compact their own history, under tighter bounds: **two** retained
1531
- turns instead of four, at most **two** compactions per child instead of five, the
1532
- fast tier preferred whenever it resolves, and failures preserve history. Each
1557
+ Children compact their own history **under the lead's own policy**: the same
1558
+ `keepRecentTurns`, the same `onFailure`, the same archive setting, the same
1559
+ per-run compaction bound, the fast tier preferred whenever it resolves. Each
1533
1560
  child owns its own context meter and counts compaction only after Core accepts
1534
1561
  the candidate. There is no transcript card for a
1535
1562
  child — the lead's transcript describes the lead's context. Turn it off with
1536
1563
  `compaction.subagents: false`.
1537
1564
 
1565
+ Since main-agent parity, the turn cap, the per-child wall clock and the dispatch
1566
+ wall clock all default to **0 = no limit**: a child runs until it finishes, like
1567
+ the lead, with only its own idle watchdog and your Esc as ceilings. Set any of
1568
+ `team.subagentTimeoutMs` / `team.dispatchTimeoutMs` / `team.maxTurnsPerSubagent`
1569
+ to a positive value to bring a ceiling back.
1570
+
1538
1571
  ### A compaction is no longer irreversible
1539
1572
 
1540
1573
  After a splice is adopted, the dropped messages are written verbatim to
@@ -1923,9 +1956,9 @@ env / `.env` → CLI flags**.
1923
1956
  | `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. |
1924
1957
  | `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. |
1925
1958
  | `team.maxConcurrent` | `3` | Subagents in flight at once, clamped to `[1, 10]` and capped at `maxSubagents`. A separate knob because provider rate limits are a different constraint from context economics. |
1926
- | `team.subagentTimeoutMs` | `300000` | One subagent's wall clock before it is aborted, clamped to `[30000, 1800000]`. A wedged subagent dies on its own ceiling without taking the dispatch with it. |
1927
- | `team.dispatchTimeoutMs` | `900000` | The whole dispatch's wall clock, clamped to `[60000, 3600000]`. |
1928
- | `team.maxTurnsPerSubagent` | `24` | Runaway-loop cap, clamped to `[4, 100]`. A subagent stopped here is reported as `stopped: turn cap reached`, with whatever it had produced. |
1959
+ | `team.subagentTimeoutMs` | `0` | One subagent's wall clock before it is aborted, clamped to `[0, 1800000]`. `0` (the default) = no ceiling — a wedged subagent still dies on its own idle watchdog, exactly like the lead. |
1960
+ | `team.dispatchTimeoutMs` | `0` | The whole dispatch's wall clock (and the `task` tool timeout), clamped to `[0, 3600000]`. `0` (the default) = no ceiling; only Esc ends a dispatch early. |
1961
+ | `team.maxTurnsPerSubagent` | `0` | Runaway-loop cap, clamped to `[0, 100]`. `0` (the default) = no cap, matching the lead. A subagent stopped at a positive cap is reported as `stopped: turn cap reached`, with whatever it had produced. |
1929
1962
  | `todo.enabled` | `true` | Master switch for todo planning. `false` means the `todo_write` tool is never registered — `/todo on` in such a session saves the setting for next launch and says so, rather than advertising a tool that is not there. |
1930
1963
  | `todo.panel` | `true` | Render the right-hand rail. Independent of `todo.enabled` on purpose: a screen-reader user wants the planning discipline without the column, and the system prompt varies one sentence accordingly. |
1931
1964
  | `todo.followThrough` | `"notify"` | What happens when a run ends with steps left. `notify` says so and stops; `auto` continues the plan after a 3-second grace window, bounded by two structural limits (one fruitless attempt, 25 continuations per plan) that are deliberately **not** settings; `off` is silent. An unrecognized value falls back to `notify`. |
@@ -1936,7 +1969,7 @@ env / `.env` → CLI flags**.
1936
1969
  | `retry.multiplier` | `2` | Growth factor, clamped to `[1, 5]`. The one **float** key in this section: `2.5` is a legitimate factor and is stored as typed. |
1937
1970
  | `retry.jitter` | `true` | Equal jitter (`d/2 + rand·d/2`), which de-correlates concurrent subagents hitting the same 429 while keeping the announced countdown within a factor of two of the truth. Full jitter can fire 40 ms after announcing 30 s, which reads as a bug. |
1938
1971
  | `retry.respectRetryAfter` | `true` | Honour `Retry-After` (and the provider rate-limit reset headers) as a **floor**, never a replacement — a `Retry-After: 1` on the ninth retry does not undo eight retries of backoff. A stated wait above 60 s is surfaced immediately instead of waited out. |
1939
- | `retry.maxElapsedMs` | `240000` | Wall-clock budget from the first failure, clamped to `[10000, 1800000]`. Deliberately **below** `team.subagentTimeoutMs`: an equal budget means a subagent is killed at the same instant its ladder ends, and the user is then told "subagent timed out" for what was a provider outage. |
1972
+ | `retry.maxElapsedMs` | `240000` | Wall-clock budget from the first failure, clamped to `[10000, 1800000]`. When `team.subagentTimeoutMs` is positive, keep this below it: an equal budget means a subagent is killed at the same instant its ladder ends, and the user is then told "subagent timed out" for what was a provider outage. |
1940
1973
  | `retry.onPartialStream` | `true` | Allow a restart after content has already streamed to the transcript. The view is rewound and the discarded tool cards removed; engine history is provably clean at that point, because it is only written on a completed turn. `false` refuses the whole path. |
1941
1974
  | `skills.enabled` | `true` | Master switch for the skill system. |
1942
1975
  | `skills.requireApproval` | `true` | Require a human to approve `skill_install` / `skill_create`. |
@@ -2150,6 +2183,8 @@ Everything AragonAgent keeps for you lives under one directory in your home:
2150
2183
  ~/.aragon-agent/ (Windows: C:\Users\<you>\.aragon-agent\)
2151
2184
  ├── config.json settings + API keys (0600 on POSIX)
2152
2185
  ├── config.json.bak written by `aragon config edit` before it opens
2186
+ ├── model-windows.json per-model context windows you declare by hand;
2187
+ │ read live, never written by the CLI
2153
2188
  ├── prompt-history.jsonl prompts you submitted, for ↑ recall (0600 on POSIX)
2154
2189
  ├── state.json UI bookkeeping: submit count, one-shot notices
2155
2190
  ├── update-state.json auto-update: last check, pending restart, skipped version,
@@ -2326,19 +2361,19 @@ node packages/cli/dist/cli.js --version
2326
2361
  - `echo "list files" | node dist/cli.js -p` prints an answer and exits `0`.
2327
2362
  - A missing/invalid key produces a visible, actionable message (never a blank
2328
2363
  screen), both in the TUI and headless.
2329
- - `Shift+Tab` at 40, 80 and 120 columns - the chip, the hint and the status bar
2330
- are all legible and nothing wraps onto a second row.
2364
+ - `Shift+Tab` at 40, 80 and 120 columns - the status-bar mode word (`PLAN` / `P`)
2365
+ stays legible and nothing wraps onto a second row.
2331
2366
  - The question wizard at 24 rows with 5 questions x 4 options - no clipping, and
2332
2367
  the `n/N` counter matches what is on screen.
2333
2368
  - Approve a plan and watch the SAME run start writing files, with no second
2334
2369
  prompt.
2335
- - `--no-color` on a legacy `cmd.exe` - the chip renders `[PLAN]`, radios render
2336
- `*`/`o`, checkboxes render `[x]`/`[ ]`, no mojibake anywhere.
2370
+ - `--no-color` on a legacy `cmd.exe` - the mode word renders as plain `PLAN` text,
2371
+ radios render `*`/`o`, checkboxes render `[x]`/`[ ]`, no mojibake anywhere.
2337
2372
  - A plan with a 600-character summary and a 400-character step detail - the text
2338
2373
  wraps and scrolls, the footer count matches the real number of rows, and
2339
2374
  nothing is truncated to a single line.
2340
2375
  - `--no-hints` on a 20-row terminal in plan mode - the status bar still says
2341
- `PLAN` with no chip on screen.
2376
+ `PLAN`.
2342
2377
  - Dismiss a plan card with `Esc`, let the agent resubmit, then close the card and press `Esc` twice within 1.5 seconds:
2343
2378
  the run aborts and the session returns to an idle prompt. Do this one BY HAND
2344
2379
  as well as in a test - the point of the check is whether the exit is findable,
@@ -2366,7 +2401,9 @@ UTF-16 编辑和 ZWJ/组合字形仍有既有限制,本次修复保证视觉
2366
2401
  ## 输入、待处理队列与底部状态区
2367
2402
 
2368
2403
  Enter 在空闲时发送,运行时加入队列;启动阶段拒绝重复提交并保留草稿。
2369
- Ctrl+J 换行;终端能发送独立序列时,Shift+Enter 与 Alt+Enter 也可换行。
2404
+ Ctrl+J 换行;应用默认推送键盘增强模式(Windows 用 win32-input-mode,其余平台用
2405
+ kitty disambiguate + modifyOtherKeys=2)并将新编码翻译回传统字节,因此 Shift+Enter
2406
+ 与 Alt+Enter 在支持的终端开箱即用,Ctrl+J 始终可用。
2370
2407
  光标按完整字素移动,支持中文、组合字符和 emoji;输入最多占终端高度四分之一且不超过六行。
2371
2408
  括号粘贴保留换行,不触发发送。过滤器或粘贴支持关闭时,不保证未标记多行粘贴安全。
2372
2409
  过滤器不可用时 Delete 保守按 Backspace 处理,避免退格误删后文。
@@ -2375,14 +2412,14 @@ Ctrl+J 换行;终端能发送独立序列时,Shift+Enter 与 Alt+Enter 也
2375
2412
  原生剪贴板工具完成后显示确认,OSC 52 只表示请求已发送,终端可能拒绝。
2376
2413
  Shift+拖动或 `/mouse off` 后的选区由宿主管理,应用无法关闭宿主的自动复制。
2377
2414
 
2378
- Queue 面板逐条显示待处理消息,数量在展开状态详情中按空间显示。只有 Core 已把消息纳入会话并发回接收凭据,
2415
+ Queue 面板逐条显示待处理消息,数量在展开状态详情中按空间显示。只有 Core 已把消息纳入会话并发回接收凭据,
2379
2416
  该条提示才会消失。中断时未接收消息显示暂停;`/queue` 可分页查看全部原文,Esc 关闭。
2380
2417
  `/clear` 不丢待处理消息;`/reset`、切换会话会明确报告取消数量;恢复保存的未发送历史不会自动重发。
2381
2418
 
2382
- 底部依次为固定输入框和一行核心状态;Ctrl+G 展开或收起第二行详情。五个核心字段按显示宽度缩写,始终保留。
2383
- 核心上下文显示当前主 Agent 的百分比;展开详情或 /context 可查看已用/上限 tokens;`~` 表示含估算,`?` 表示上限未知。
2419
+ 底部依次为固定输入框和一行核心状态;Ctrl+G 展开或收起第二行详情。五个核心字段按显示宽度缩写,始终保留。
2420
+ 核心上下文显示当前主 Agent 的百分比;展开详情或 /context 可查看已用/上限 tokens;`~` 表示含估算,`?` 表示上限未知。
2384
2421
  费用是含子代理、快速模型及压缩的会话估算 USD,恢复会话沿用累计,缺价格按零计,零值不保证免费。
2385
- 本轮均速只累计主 Agent 已报告的 output tokens,再除以本轮总耗时(含工具与等待);排除辅助调用,并非瞬时生成速度。结束后保留耗时,速度显示 --。
2422
+ 本轮均速只累计主 Agent 已报告的 output tokens,再除以本轮总耗时(含工具与等待);排除辅助调用,并非瞬时生成速度。结束后保留耗时,速度显示 --。
2386
2423
  `/context` 提供统计范围和完整值。
2387
2424
 
2388
2425
  Windows Terminal:在设置 JSON **根级**添加 `"copyOnSelect": false`,把以下条目合并到已有数组:
@@ -2432,10 +2469,10 @@ VS Code 用户设置添加 `"terminal.integrated.copyOnSelection": false`,快
2432
2469
 
2433
2470
  配置库仍位于 `ARAGON_HOME` 指向目录中的 `config.json`,不写入会话文件。密钥按现有机制明文保存:POSIX 创建权限为 0600,Windows 依赖用户目录 ACL,不提供加密。`config get/list` 对方案密钥使用固定掩码;损坏或未来版本的配置段整体隐藏。既有 `config edit` 备份也包含凭据。关闭通用日志脱敏、使用短密钥或分享第三方 SDK 错误日志时,仍需自行检查日志内容。
2434
2471
 
2435
- ### 固定输入与状态详情人工验收
2436
-
2437
- 在 Windows Terminal/PowerShell、VS Code 集成终端以及 macOS/Linux 终端分别检查
2438
- 40x12、80x24、120x40。保留草稿,连续输出思考、正文与工具日志至少 30 秒:
2439
- 输入上下边框不得移动;拖动消息滚动条不会隐藏输入。切换 Ctrl+G 仅改变一行,
2440
- 复制、队列、通知和更新不自动展开。验证补全、Unicode 草稿和光标在切换后保留,
2441
- 弹层期间 Ctrl+G 无效,小屏恢复后草稿与状态模式保留。自动化测试不能替代这些宿主观察。
2472
+ ### 固定输入与状态详情人工验收
2473
+
2474
+ 在 Windows Terminal/PowerShell、VS Code 集成终端以及 macOS/Linux 终端分别检查
2475
+ 40x12、80x24、120x40。保留草稿,连续输出思考、正文与工具日志至少 30 秒:
2476
+ 输入上下边框不得移动;拖动消息滚动条不会隐藏输入。切换 Ctrl+G 仅改变一行,
2477
+ 复制、队列、通知和更新不自动展开。验证补全、Unicode 草稿和光标在切换后保留,
2478
+ 弹层期间 Ctrl+G 无效,小屏恢复后草稿与状态模式保留。自动化测试不能替代这些宿主观察。
@@ -15,6 +15,7 @@ import type { PromptOptions, PromptOutcome } from './prompt-options.js';
15
15
  import { ModelRegistry, type AgentEvent, type AgentTool, type Message, type ModelInfo, type ModelRef, type ProviderRegistry, type ThinkingLevel } from '@aragon-agent/core';
16
16
  import type { CliConfig, CompactionConfig, FastConfig, RetryConfig, TeamConfig, ThemeName, TodoConfig } from '../config/schema.js';
17
17
  import type { ModelRole } from '../config/model-profiles.js';
18
+ import { ModelWindows } from '../config/model-windows.js';
18
19
  import { type ConfirmRequest } from '../tools/index.js';
19
20
  import type { ToolPermission } from '../exec/permission.js';
20
21
  import { type ToolOutputListener } from '../tools/tool-output-store.js';
@@ -102,6 +103,13 @@ export interface ControllerDeps {
102
103
  * once at construction and lost at the first `rebuildSystemPrompt()`.
103
104
  */
104
105
  appendSystemPrompt?: string;
106
+ /**
107
+ * The per-model window table from ~/.aragon-agent/model-windows.json.
108
+ * Injectable so tests can hand the controller a table without writing into
109
+ * the shared vitest home root (app-paths.ts TEST-ISOLATION CONTRACT);
110
+ * production leaves it absent and the constructor builds the real one.
111
+ */
112
+ modelWindows?: ModelWindows;
105
113
  }
106
114
  /** What `setAgentMode` actually adopted — see `AgentController.setAgentMode`. */
107
115
  export interface AgentModeState {
@@ -124,6 +132,8 @@ export declare class AgentController {
124
132
  private readonly agent;
125
133
  private readonly providerRegistry;
126
134
  private readonly modelRegistry;
135
+ /** Per-model user-declared context windows; see `config/model-windows.ts`. */
136
+ private readonly modelWindows;
127
137
  private modelMetadataEnabled;
128
138
  private modelMetadataGeneration;
129
139
  private modelMetadataAbort;
@@ -7,10 +7,12 @@ import { clampCompactionConfig, clampFastConfig, clampRetryConfig, clampTeamConf
7
7
  import { resolveModelProfileState } from '../config/model-profile-resolution.js';
8
8
  import { makeGetApiKey } from '../config/load.js';
9
9
  import { updatePersistedConfig } from '../config/store.js';
10
+ import { ModelWindows } from '../config/model-windows.js';
10
11
  import { createBuiltinTools } from '../tools/index.js';
11
12
  import { createFileChangeStore } from '../tools/file-change-store.js';
12
13
  import { createToolOutputStore, } from '../tools/tool-output-store.js';
13
14
  import { TeamRuntime } from '../team/runtime.js';
15
+ import { createChildReviewer } from '../team/child-reviewer.js';
14
16
  import { TeamHumanQueue } from '../team/human-queue.js';
15
17
  import { createTaskTool } from '../team/task-tool.js';
16
18
  import { buildTeamBlock } from '../team/prompt.js';
@@ -53,6 +55,8 @@ export class AgentController {
53
55
  agent;
54
56
  providerRegistry;
55
57
  modelRegistry;
58
+ /** Per-model user-declared context windows; see `config/model-windows.ts`. */
59
+ modelWindows;
56
60
  modelMetadataEnabled = false;
57
61
  modelMetadataGeneration = 0;
58
62
  modelMetadataAbort = new AbortController();
@@ -158,14 +162,14 @@ export class AgentController {
158
162
  // Fast model tier (fast-model-tier §3.1 / §3.3)
159
163
  //
160
164
  // ONE FIELD AND FOUR THIN FORWARDERS. The flag pair, the tier cache, the
161
- // reviewer's lifetime and the `resolveTier` closure all live in
165
+ // reviewer's lifetime and the shared fail-fast review transport all live in
162
166
  // `fast/wiring.ts`, which is a BUDGET decision rather than an aesthetic one
163
167
  // (C-12 / RV-5): `CLAUDE.md` caps a source file at 1000 lines and this one had
164
168
  // 40 to spare when the design was written.
165
169
  //
166
170
  // `null` when the tier could not be resolved at construction, which is the
167
- // byte-identity branch: no reviewer is subscribed, `task` carries no `model`
168
- // property, and `<fast_tier>` is never spliced (I-2).
171
+ // byte-identity branch: no reviewer is subscribed and `<fast_tier>` is
172
+ // never spliced (I-2).
169
173
  // -----------------------------------------------------------------------
170
174
  fast;
171
175
  // -----------------------------------------------------------------------
@@ -275,6 +279,7 @@ export class AgentController {
275
279
  // reuses this very registry instance.
276
280
  this.providerRegistry = initProviders({ retryPolicy: toRetryPolicy(config.retry) });
277
281
  this.modelRegistry = new ModelRegistry(this.providerRegistry);
282
+ this.modelWindows = deps.modelWindows ?? new ModelWindows();
278
283
  this.skillsEnabled = config.skills.enabled;
279
284
  this.onSkillsChanged = deps.onSkillsChanged;
280
285
  this.skills = new SkillService({
@@ -307,8 +312,8 @@ export class AgentController {
307
312
  this.todos = this.todoRegistered ? new TodoStore() : null;
308
313
  // --- Fast model tier ---------------------------------------------------
309
314
  //
310
- // RESOLVED BEFORE THE TOOL ARRAY, because `fastRegistered` decides whether
311
- // `task`'s schema carries a `model` property at all and the array is built
315
+ // RESOLVED BEFORE THE AGENT, because `fastRegistered` decides whether the
316
+ // `FastWiring` (reviewer, tier cache) exists at all and the wiring is built
312
317
  // ONCE (C-2). `FastWiring` re-runs the same pure function in its own
313
318
  // constructor; the two agree by construction because they read the same
314
319
  // config through the same resolver.
@@ -320,24 +325,35 @@ export class AgentController {
320
325
  this.teamRuntime = this.teamRegistered
321
326
  ? new TeamRuntime({
322
327
  getConfig: () => this.config,
323
- // Read LIVE through the wiring, exactly as `resolveTier` below is: the
324
- // closure is only ever called from inside a dispatch, and
325
- // `childFactory()` re-reads `compaction.subagents` on every call so a
326
- // settings-screen edit reaches the next dispatch.
328
+ // Read LIVE: the closure is only ever called from inside a dispatch,
329
+ // and `childFactory()` re-reads `compaction.subagents` on every call
330
+ // so a settings-screen edit reaches the next dispatch.
327
331
  contextManagerFor: (req) => this.compaction?.childFactory()?.(req),
328
- // Read LIVE through the wiring, which does not exist yet — the closure
329
- // is only ever called from inside a dispatch, long after construction.
330
- resolveTier: (tier) => this.fast
331
- ? this.fast.resolveTier(tier)
332
- : {
333
- ref: {
334
- providerId: this.config.provider,
335
- modelId: this.config.model,
336
- ...(this.config.baseUrl ? { baseUrl: this.config.baseUrl } : {}),
337
- },
338
- thinkingLevel: this.config.thinkingLevel,
339
- role: 'main',
332
+ // PER-CHILD FAST REVIEW (main-agent parity): one reviewer per
333
+ // child, sharing the lead wiring's tier, policy and fail-fast
334
+ // transport. `this.fast` is null until after the Agent exists; these
335
+ // closures run only from inside a dispatch, long after construction.
336
+ review: {
337
+ // EVERY `this.fast` READ LIVES INSIDE A CLOSURE: the wiring is
338
+ // assigned AFTER this constructor block (it needs the Agent), and
339
+ // a read in the constructor's own flow would be a use-before-assign.
340
+ // The closures run only from inside a dispatch, long after
341
+ // construction, when the field has its final value.
342
+ active: () => this.fast !== null && this.fast.available() && this.config.fast.review,
343
+ modelId: () => {
344
+ const tier = this.fast?.getTier();
345
+ return tier && tier.ok ? tier.ref.modelId : '';
340
346
  },
347
+ create: (request) => this.fast
348
+ ? createChildReviewer(request, {
349
+ getConfig: () => this.config,
350
+ hasKey: (id, role) => this.hasApiKey(id, role),
351
+ getApiKey: (id, role) => this.resolveKey(id, role),
352
+ complete: (providerId, llmRequest) => this.fast.completeViaFastRegistry(providerId, llmRequest),
353
+ available: () => this.fast?.available() === true,
354
+ })
355
+ : undefined,
356
+ },
341
357
  providerRegistry: this.providerRegistry,
342
358
  getCwd: () => this.cwd,
343
359
  getMode: () => this.effectiveMode,
@@ -484,23 +500,12 @@ export class AgentController {
484
500
  runtime: this.teamRuntime,
485
501
  // Read LIVE: `/team off` flips the flag, it cannot unregister.
486
502
  isTeamEnabled: () => this.teamEnabled,
487
- // FROZEN AT CONSTRUCTION (C-2). `/fast on` in a session launched
488
- // without the tier cannot add a schema property, and it says so
489
- // rather than advertising one that is not there.
490
- fastRegistered,
491
- // Read LIVE at dispatch time (RV-3): the tier can stop resolving
492
- // mid-session, and the normalizer and the subagent factory must
493
- // agree about that at the same instant.
494
- fastAvailable: () => this.fast?.delegationAvailable() === true,
495
503
  maxSubagents: () => this.config.team.maxSubagents,
496
504
  hasApiKey: () => this.hasApiKey(),
497
505
  activeProvider: () => this.config.provider,
506
+ // Every child runs the lead's own model, so the lead's cost table
507
+ // is the one honest price for the dispatch total.
498
508
  modelCost: () => this.getModelInfo().cost,
499
- fastModelCost: () => {
500
- const ref = this.fast?.fastRef();
501
- return ref ? this.getModelInfoFor(ref).cost : undefined;
502
- },
503
- fastPricingUnknown: () => this.fast?.snapshot().pricingUnknown === true,
504
509
  withPausedWatchdog: (fn) => this.withPausedWatchdog(fn),
505
510
  }),
506
511
  ]
@@ -599,6 +604,10 @@ export class AgentController {
599
604
  // number only bites BECAUSE `task` races `ctx.signal` — the
600
605
  // executor's timeout is cooperative, so an override is inert for any
601
606
  // tool that does not listen (I-2).
607
+ //
608
+ // `dispatchTimeoutMs: 0` (the default) reaches the executor as an
609
+ // override of `0` = NO TIMER AT ALL: the dispatch lasts as long as its
610
+ // slowest child, and only an Esc ends it early (main-agent parity).
602
611
  ...(this.teamRegistered ? { task: config.team.dispatchTimeoutMs } : {}),
603
612
  },
604
613
  },
@@ -668,13 +677,6 @@ export class AgentController {
668
677
  });
669
678
  if (this.fast) {
670
679
  this.rebuildSystemPrompt();
671
- // Fast-tier children are counted from the outcome rather than from the
672
- // specs, so a downgraded child is not miscounted as a saving (§3.6).
673
- this.teamRuntime?.subscribe((event) => {
674
- if (event.type !== 'dispatch_end')
675
- return;
676
- this.fast?.noteDelegated(event.outcome.runs.filter((r) => r.tier === 'fast').length);
677
- });
678
680
  }
679
681
  // ONE LINE FOR THE TWO CONFIGURATION MISTAKES THAT ARE OTHERWISE INVISIBLE
680
682
  // (§3.3): the tier is on but unnamed, or named against a provider with no
@@ -734,11 +736,6 @@ export class AgentController {
734
736
  ? buildTeamBlock({
735
737
  maxSubagents: config.team.maxSubagents,
736
738
  maxConcurrent: config.team.maxConcurrent,
737
- // One cross-reference sentence, present only when the capability
738
- // is (§3.8). `this.fast` is null during the FIRST compose (the
739
- // wiring needs the Agent), which is why the constructor composes
740
- // again once it exists.
741
- fastDelegation: !!fastAvailable && config.fast.delegate,
742
739
  })
743
740
  : '',
744
741
  // BOTH FLAGS, exactly as team mode uses both, and for the identical
@@ -758,7 +755,6 @@ export class AgentController {
758
755
  fastBlock: fastAvailable
759
756
  ? buildFastBlock({
760
757
  model: tier.ok ? tier.ref.modelId : '',
761
- delegate: config.fast.delegate,
762
758
  review: config.fast.review,
763
759
  })
764
760
  : '',
@@ -1737,9 +1733,18 @@ export class AgentController {
1737
1733
  const found = this.modelRegistry.getModel(ref.providerId, ref.modelId);
1738
1734
  const baseUrl = ref.baseUrl ?? (ref.providerId === this.config.provider && ref.modelId === this.config.model
1739
1735
  ? this.config.baseUrl : undefined);
1736
+ const userWindow = this.modelWindows.lookup(ref.modelId);
1740
1737
  return {
1741
1738
  ...(found ?? this.modelRegistry.buildRuntimeModel(ref.providerId, ref.modelId)),
1742
1739
  ...this.modelRegistry.getContextWindow(ref.providerId, ref.modelId, baseUrl),
1740
+ // A USER-DECLARED WINDOW OUTRANKS EVERY TABLE (model-windows.json): the
1741
+ // user asserted the number for exactly this id, while api/catalog are
1742
+ // what this process managed to look up about it. It still LOSES to
1743
+ // config.json's `contextWindow`, which `ContextMeter.resolveWindow`
1744
+ // consults before it ever reaches here.
1745
+ ...(userWindow !== undefined
1746
+ ? { contextWindow: userWindow, contextWindowSource: 'user' }
1747
+ : {}),
1743
1748
  };
1744
1749
  }
1745
1750
  /** Best-effort metadata lookup; never block typing or a model request. */
@@ -860,10 +860,6 @@ export function viewReducer(state, action) {
860
860
  const runs = action.specs.map((spec) => ({
861
861
  label: spec.label,
862
862
  description: spec.description,
863
- // Already downgraded by the normalizer when the tier was unavailable, so
864
- // the card and the report agree from the first frame (fast-model-tier
865
- // §3.4).
866
- tier: spec.tier,
867
863
  phase: 'queued',
868
864
  turns: 0,
869
865
  toolCalls: 0,