@staix/agent-hub 0.12.21 → 0.12.23

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 (87) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +27 -12
  3. package/docs/agent-notes/adapters.md +15 -7
  4. package/docs/agent-notes/benchmarks.md +4 -1
  5. package/docs/agent-notes/budget.md +4 -0
  6. package/docs/agent-notes/bus.md +2 -1
  7. package/docs/agent-notes/daemon.md +12 -1
  8. package/docs/agent-notes/local-worker.md +4 -1
  9. package/docs/agent-notes/tasks.md +2 -1
  10. package/docs/agent-notes/tests.md +4 -0
  11. package/docs/events.md +17 -4
  12. package/docs/operations.md +478 -41
  13. package/docs/quickstart.md +14 -6
  14. package/docs/security.md +23 -11
  15. package/docs/smoke.md +133 -7
  16. package/docs/specs/2026-09-19-agent-hub-design.md +115 -15
  17. package/docs/specs/2026-09-20-upgrade-recovery-design.md +84 -8
  18. package/docs/verification/2026-10-10-cli-output-phase1.md +309 -0
  19. package/docs/verification/2026-10-10-cli-output-phase2.md +672 -0
  20. package/docs/verification/2026-10-11-0.12.23.md +90 -0
  21. package/docs/verified.json +32 -17
  22. package/package.json +1 -1
  23. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  24. package/plugins/agent-hub/server.js +14 -3
  25. package/src/adapters/acp-quota.ts +13 -0
  26. package/src/adapters/acp.ts +105 -17
  27. package/src/adapters/codex-appserver.ts +70 -35
  28. package/src/adapters/local-worker.ts +44 -10
  29. package/src/adapters/pi.ts +158 -19
  30. package/src/cli/bench.ts +2 -1
  31. package/src/cli/console-state.ts +28 -22
  32. package/src/cli/console.ts +20 -10
  33. package/src/cli/help.ts +25 -19
  34. package/src/cli/identity.ts +3 -3
  35. package/src/cli/init.ts +1 -1
  36. package/src/cli/launch.ts +182 -7
  37. package/src/cli/main.ts +409 -177
  38. package/src/cli/output.ts +363 -0
  39. package/src/cli/preview.ts +2 -1
  40. package/src/cli/recovery-package.ts +17 -0
  41. package/src/cli/reset.ts +3 -0
  42. package/src/cli/status-lines.ts +0 -28
  43. package/src/cli/terminal-recovery.ts +27 -1
  44. package/src/cli/upgrade-interactive.ts +297 -0
  45. package/src/cli/upgrade-runtime.ts +80 -9
  46. package/src/cli/upgrade.ts +50 -15
  47. package/src/hub/board.ts +2 -0
  48. package/src/hub/budget.ts +39 -7
  49. package/src/hub/bus.ts +15 -14
  50. package/src/hub/child-process.ts +11 -0
  51. package/src/hub/conductor.ts +23 -11
  52. package/src/hub/config-trust.ts +16 -5
  53. package/src/hub/conflicts.ts +2 -0
  54. package/src/hub/context-window.ts +8 -1
  55. package/src/hub/crash.ts +2 -1
  56. package/src/hub/daemon.ts +483 -83
  57. package/src/hub/db-timeout.ts +3 -0
  58. package/src/hub/delivery-journal.ts +2 -1
  59. package/src/hub/envelope.ts +1 -1
  60. package/src/hub/events.ts +4 -1
  61. package/src/hub/execution-budget.ts +2 -0
  62. package/src/hub/facts.ts +5 -4
  63. package/src/hub/git.ts +32 -0
  64. package/src/hub/hub-tools.ts +1 -1
  65. package/src/hub/lifecycle.ts +10 -4
  66. package/src/hub/manager.ts +18 -5
  67. package/src/hub/peers.ts +7 -5
  68. package/src/hub/permission-mode.ts +14 -0
  69. package/src/hub/project.ts +2 -1
  70. package/src/hub/recovery-error.ts +31 -0
  71. package/src/hub/recovery-store.ts +43 -4
  72. package/src/hub/registry.ts +2 -1
  73. package/src/hub/routing.ts +114 -13
  74. package/src/hub/settings.ts +392 -0
  75. package/src/hub/snapshots.ts +4 -2
  76. package/src/hub/start-mode.ts +48 -0
  77. package/src/hub/tasks.ts +158 -43
  78. package/src/hub/terminal-open.ts +64 -0
  79. package/src/hub/ui.ts +25 -13
  80. package/src/local/deny.ts +71 -2
  81. package/src/local/sandbox.ts +47 -6
  82. package/src/local/tools.ts +25 -6
  83. package/src/memory/recall.ts +2 -2
  84. package/src/models/mlx.ts +2 -1
  85. package/src/pi/extension.ts +43 -20
  86. package/src/pi/tool-receipts.ts +19 -2
  87. package/src/ui/index.html +172 -21
@@ -1,8 +1,25 @@
1
1
  # Operations guide
2
2
 
3
- This guide describes ahub 0.12.21 and control protocol 16. Live verification
3
+ This guide describes ahub 0.12.23 and control protocol 16. Live verification
4
4
  results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
5
5
 
6
+ ## Command output
7
+
8
+ `ahub status`, `board`, `budget`, `doctor`, `projects`, `queue list`, `turns` and `report` print shared console tables and labelled details.
9
+ `status --all` uses the projects table. `queue show`, `models status` and `budget execution status` show labelled fields; add `--json` for the raw fetched document.
10
+ `report --by task` keeps the attribution and coverage sentences with section headings and aligned counters.
11
+ Use `--json` for scripts; it returns the fetched data without color, and `status --json` keeps its existing document.
12
+ In `projects --json` and `status --all --json`, an unavailable hub that explicitly refuses an inspection for SQLite contention also carries `failure: { "code": "database_busy" }`; it is additive metadata alongside the neutral `error` sentence.
13
+ Text columns are a human interface, not a scripting contract. An identifier accepted as a command argument is whole wherever its available column can hold its own length:
14
+ turn ids, queue ids, project ids, execution budget ids and ids in labelled views. Only status's informational settlement and hold ids are shortened;
15
+ `status --full` keeps those whole too. Identifiers in suggested commands are always whole.
16
+
17
+ `--color=auto|always|never` uses the console palette. Auto colors only a TTY with `TERM` other than `dumb`
18
+ and empty `NO_COLOR`; pipes and redirects stay plain. Color never replaces a state or level word.
19
+ TTY width is its columns (at least 80). A pipe uses a positive integer `COLUMNS` when supplied, otherwise rows
20
+ are complete and unwrapped. Long fields wrap within their column; when whole metadata words and the readable flexible-column minimum do not fit, table bands retain every field. Each band carries the row id; when it cannot share a line with the metadata, a per-row id banner links them. Labelled two-column views keep each label beside its value, wrapping the value instead of separating labels and values into bands. In `turns`, the time and state cells wrap at word boundaries only when the file list needs the room and the listing gets shorter for it. When `projects` bands, the task list stays with the state columns in the first band and the second band is always PROJECT and ROOT. Console children receive the console's width
21
+ minus its four-column indent and remain uncolored.
22
+
6
23
  ## Command help
7
24
 
8
25
  `ahub help`, `ahub --help` and `ahub -h` print the commands grouped by section;
@@ -46,6 +63,15 @@ a three-line footer, separated by ASCII `-` rules; the stream has one rule above
46
63
  its footer. The active tab is bracketed, as in `[2 Approvals]`, so it reads
47
64
  without color. The Approvals tab counts pending requests and the Queue tab
48
65
  counts `needs_review` deliveries, in the attention and failure tones.
66
+ Tabs change with the digits 1 to 5 or with Left and Right (wrapping at both
67
+ ends), never while a command is being edited or a choice is open; under a
68
+ confirmation an arrow dismisses it like any key but y, without moving a tab. A pasted
69
+ chunk can only edit the command line, but a read that holds nothing but arrow
70
+ sequences is that many key presses, so a held key repeats its tab move. In the Events
71
+ tab a row's header line is tokenized and coloured exactly as the same event in
72
+ the stream; only the header is tab-flattened before tokenizing. Body lines retain
73
+ the previous rendering and stay plain, as does command output; colour never changes
74
+ what the panel's text says.
49
75
 
50
76
  Peers, Approvals, Tasks and Queue are tables with a header row. A column is as
51
77
  wide as its widest cell, up to a third of the terminal; the last column takes
@@ -78,7 +104,7 @@ prompts, are quoted unless they are a single word starting with a letter, so
78
104
  punctuation, a newline or a bare number in them cannot imitate the console's own
79
105
  structure. Under the `?` key table only `?`, Escape and `q` act.
80
106
 
81
- Arrow keys or j/k move, Enter opens a detail of labeled fields (relative times,
107
+ Up/Down arrows or j/k move, Enter opens a detail of labeled fields (relative times,
82
108
  never JSON), Escape returns, and `?` shows the keys grouped by panel; in the
83
109
  stream, `?` prints that table and `v` prints the selected request as its push
84
110
  showed it. The footer's last line lists only the keys that act in the current
@@ -199,10 +225,12 @@ Launch that peer with `ahub claude` for channel pushes, or select `codex` in
199
225
  `roles` and launch `ahub codex`. The conductor splits work into owned tasks,
200
226
  observes `hub_status`, moves stalled work and obtains review before reporting
201
227
  results and open decisions. It does not implement the tasks it handed out.
202
- `hub_peer_start` starts local, Kimi or headless Pi; requests for native TUIs
203
- return the `ahub` command for the person to run, after validating it through
204
- the shared launcher planner. The wrapper plans again at launch when native
205
- endpoints are available. `hub_peer_hold` and `hub_peer_release`
228
+ `hub_peer_start` starts a peer in the start mode a person set for it (see
229
+ "Start modes"): local, Kimi and a headless Pi or Codex start at once; for a
230
+ TUI peer the hub opens a terminal running the fixed `ahub <peer>` command,
231
+ after validating it through the shared launcher planner, and when it cannot
232
+ open one the call fails with that command for the person to run. The
233
+ conductor cannot choose the mode. `hub_peer_hold` and `hub_peer_release`
206
234
  manage only holds placed by that conductor. Assignment also requires `assign`
207
235
  when the conductor has an explicit capabilities list.
208
236
 
@@ -213,7 +241,8 @@ final response in the native TUI. Broadcasting implementation instructions can
213
241
  cause an otherwise unassigned owner to claim duplicate work.
214
242
 
215
243
  The person answers approvals in the console, resolves `needs_review` deliveries
216
- with `ahub queue resolve`, and handles budget overrides and hub lifecycle.
244
+ with `ahub queue resolve`, stops a headless peer with `ahub stop`, and handles
245
+ budget overrides and hub lifecycle.
217
246
  Running these commands from an agent shell is refused; the CLI retains the
218
247
  agent's identity even when invoked through a shell tool.
219
248
 
@@ -286,6 +315,19 @@ project and a temp dir of their own (`TMPDIR`, made for each command and
286
315
  removed when it ends; one left by a hub crash, named `ahub-cmd-*`, goes with
287
316
  the OS temp cleanup), and nothing else; the shared temp dirs are closed.
288
317
 
318
+ Some names stay closed to those commands, and to Pi's, wherever they may
319
+ otherwise write: anything named `.git` at any depth, the `config` and `hooks`
320
+ directly under a `.git`, `commondir` below it, and `.agenthub` anywhere in the
321
+ project. A submodule's git directory under `.git/modules/` is not covered yet
322
+ (#281). A command can therefore not create, rename or remove a `.git`:
323
+ `git init`, `git clone` and `git worktree add` do not run inside the sandbox,
324
+ and neither do `git worktree remove`, `git worktree prune` of a stale record,
325
+ `git clean -ffd` over a nested repository or `rm -rf` of a directory that
326
+ holds a `.git`. `git add`, `commit`, `stash`, `checkout` and `gc` work, and
327
+ `.gitignore` and `.github/` stay writable; a commit that your git configuration
328
+ signs with a key under `~/.ssh` or `~/.gnupg` fails, because the sandbox cannot
329
+ read those directories. Run a refused command yourself, in a terminal.
330
+
289
331
  Network is off unless `local.bash_network` says otherwise. With `true` it goes
290
332
  only through the hub's egress proxy on a loopback port (0.11): commands get
291
333
  `HTTPS_PROXY` and the other proxy variables, the proxy opens HTTPS (`CONNECT`,
@@ -337,7 +379,7 @@ Managed launchers attach native peers to the project daemon:
337
379
  ahub claude
338
380
  ahub codex
339
381
  ahub kimi
340
- ahub pi --mode headless --backend auto
382
+ ahub pi # its TUI; --headless for the background peer
341
383
  ahub local
342
384
  ahub local --model <served-model-id>
343
385
  ```
@@ -366,7 +408,19 @@ A delivery in `needs_review` holds later deliveries to that peer. The console an
366
408
  the hold once, and status, task assignment and route explanations name its delivery id.
367
409
  Inspect it with `ahub queue show <id>` and explicitly choose `completed`, `retry` or
368
410
  `discard` through `ahub queue resolve`. Reassignment or approval of its task never
369
- resolves an uncertain delivery automatically.
411
+ resolves an uncertain delivery automatically. The same rule holds on every path that
412
+ assigns a task (#297): once the move is saved on the board, a failed delivery prints
413
+ exactly one console notice ("the move is saved... delivery is not confirmed"), printed
414
+ where the error is thrown, and the caller (a peer in its turn, or the CLI) still gets
415
+ its error. Two exceptions, by construction: a move superseded while the assignment was being prepared, meaning a later history entry that moves or settles the task (another assignment, an unassign, the owner's accept, done, approval, close; the hub's quiet bookkeeping such as a late check result or a reviewer change does not supersede), is
416
+ the caller's error as before, with no notice naming either owner; and a claim is its own hand-over, so its
417
+ accepted write and line stay outside the delivery rule (its overlap and routing announcements to earlier
418
+ owners are deliveries and share it). The repair is always
419
+ to look for a queued or needs_review delivery with
420
+ `ahub queue list --peer <owner>` (then `ahub queue show <id>`, or the accepted entry in
421
+ `ahub task show <id>` if the journal itself is down) and then send the assignment again
422
+ with `ahub task assign <id> <owner>`. Nothing resends automatically: a blind resend can
423
+ duplicate a delivery that actually left.
370
424
 
371
425
  Reviewer candidates follow `[classes.review].peers`, then attached peers with a
372
426
  `reviewer` role in `roles`. The owner cannot review its own task. If none is available,
@@ -630,7 +684,7 @@ ahub undo <turn> --yes --context # Codex's latest turn: also drop it from Codex
630
684
  has not removed yet included, and a PII file left in the project is
631
685
  snapshotted by later turns like any other file. Keep PII files in an ignored
632
686
  directory.
633
- - A turn the hub stopped in the middle of shows as `(no end snapshot)` and
687
+ - A turn the hub stopped in the middle of shows the state `no end snapshot` and
634
688
  cannot be undone.
635
689
  - Claude's turns are not recorded: its channel shows the hub no turn boundary.
636
690
  Claude Code's own checkpoints cover its edits, though not its shell commands.
@@ -881,12 +935,36 @@ Other tools retain native decisions, which may already allow a harmless shell.
881
935
  Permission-only PreToolUse never marks Claude busy; former observation hooks
882
936
  retain their native-turn role. Settings are passed as a 0600 state file retained
883
937
  until that native launch exits. Live/unknown previous launchers keep their files;
884
- verified dead wrapper and native identities permit crash cleanup. Without
938
+ verified dead wrapper and native identities permit crash cleanup. Managed direct
939
+ binary launches record their native PID while running; crash cleanup also requires
940
+ that PID to be absent from the process table. Scripts/interpreters and unavailable
941
+ process evidence retain the settings file. Every launch atomically replaces
942
+ `claude-launch.json` with its current managed hook/session identity. Native PID
943
+ proof is separate in a private `claude-native-<binding-hash>.json` file, so an old
944
+ callback cannot overwrite a newer launch record. Only that native proof uses
945
+ `claude-launch.lock`, published with its signed owner already written. It waits
946
+ up to 250 ms for a verified live owner. An empty, unsigned or unverifiable lock
947
+ is retained and disables native identity/crash cleanup for this launch; the
948
+ warning names the path and what is off. Managed hooks and session observations
949
+ still use the newly published launch record. Inspect the named lock and verify
950
+ its owner has ended before removing it; a signed dead lock is recovered. Without
885
951
  separate native identity proof, old settings remain for manual inspection. Settings carry
886
952
  no inline caller values in argv. Status reports an unverified Claude hook as
887
953
  unverified. Unattended native sessions cannot take a runtime mode; restart
888
954
  without that native flag. Codex must be behind the hub proxy, and returning to
889
- ask requires a known native approval baseline. An offline or absent peer accepts
955
+ ask while attached requires a known native approval baseline. If unavailable,
956
+ close the TUI, then run `ahub permission codex ask` before starting `ahub codex`.
957
+ For a known baseline, a detached proxy keeps restoration owed per thread and sends
958
+ that policy once on the resumed thread's next accepted turn. Visiting another thread
959
+ does not clear the debt. The restoring turn uses that kept baseline even if the
960
+ TUI echoes the resume response's sticky override, whether ask was selected before
961
+ or after that resume. Rejected turns retain it. The protection covers that one
962
+ restoring turn. A later TUI turn that repeats the policy looks exactly like a
963
+ person's explicit native policy choice and is passed through; the proxy cannot
964
+ resolve that ambiguity. An explicit choice on the restoring turn is delayed
965
+ until the following turn. A baseline that was never reported
966
+ is explained in the CLI reply, and that old thread keeps its last policy.
967
+ An offline or absent peer accepts
890
968
  `ahub permission <peer> ask` to clear its hub choice. Codex --unattended launch
891
969
  is refused under a non-ask choice; clear it with `ahub permission codex ask`
892
970
  first. A pending never-ask default cannot be confirmed for an unattended launch.
@@ -918,6 +996,130 @@ permission_mode (peer/from/to) with no tool arguments under events schema 1.
918
996
  The optional control requests preserve PROTOCOL; an older hub answers unknown
919
997
  and the CLI advises upgrading.
920
998
 
999
+ ## Settings from the dashboard and the terminal
1000
+
1001
+ The dashboard's Settings section and `ahub settings` show and change the same
1002
+ closed list of settings (issue #269). Run them from a plain terminal:
1003
+
1004
+ ```bash
1005
+ ahub settings # every setting: value, source file, when it applies
1006
+ ahub settings get permission.kimi
1007
+ ahub settings set routing.classes.implement.peers codex,kimi,pi --preview
1008
+ ahub settings set routing.classes.implement.peers codex,kimi,pi
1009
+ ahub settings set permission_modes.kimi ask-when-needed
1010
+ ahub settings set research.enabled inherit # remove the machine-local value
1011
+ ahub settings undo # put back the file the last write changed
1012
+ ahub ui --settings # dashboard with a 15-minute settings session
1013
+ ```
1014
+
1015
+ | Group | Keys | Stored in | Applies |
1016
+ | --- | --- | --- | --- |
1017
+ | Permissions | `permission.<peer>` (the mode now) | the running hub only | at once |
1018
+ | Permissions | `permission_modes.<peer>` (the mode at hub start) | `config.local.json` | next hub start |
1019
+ | Start | `peers.pi.start_mode`, `peers.codex.start_mode` (`tui` or `headless`) | `config.local.json` | that peer's next start |
1020
+ | Start | `pi.auto_start` | `config.local.json` | next hub start |
1021
+ | Routing | `routing.stay_switch`, `routing.classes.<class>.peers`, `.escalate_to`, `.route`, `.pi_backend` | `routing.local.toml` | at once |
1022
+ | Switches | `research.enabled`, `approvals.notify`, `snapshots.enabled`, `coordination` | `config.local.json` | next hub start |
1023
+
1024
+ - Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml`,
1025
+ the machine-local files (`ahub init` ignores both in git). The shared
1026
+ `config.json` and the hand-written `routing.toml` are never rewritten, and a
1027
+ machine-local file that is committed to git is refused. Each write is read by
1028
+ the hub's own loader first and replaces the file atomically.
1029
+ - `routing.local.toml` overrides `routing.toml` key by key and may hold only the
1030
+ routing keys in the table. `ahub route explain` and the Settings rows name the
1031
+ file each value comes from. `--preview`, and the dashboard's Preview button,
1032
+ show `ahub route explain` for the open tasks before and after, or what a
1033
+ permission mode grants, and save nothing.
1034
+ - A row shows the value in force, its source file, when it applies, and for a
1035
+ value read at hub start what the next start will read (`Pending restart`).
1036
+ - `ahub settings undo` (the dashboard's "Undo last write") puts back the one
1037
+ previous version of the file the last write changed. It refuses when that
1038
+ file was edited since, and when it would put a stored never-ask back (set it
1039
+ again with its confirmation instead). A mode changed for the running hub is
1040
+ not a file: set it again instead. Setting a value it already has, or
1041
+ removing one that is not set, writes nothing. A project with no
1042
+ `.agenthub/config.json` gets no first config file from a settings write
1043
+ (any config file turns the project defaults on): run `ahub init` first.
1044
+ - A project initialised before 0.12.23 has no `.gitignore` line for
1045
+ `.agenthub/routing.local.toml`: run `ahub init` again, or add it. A
1046
+ `permission_modes` value is read only from a file git confirms nobody
1047
+ committed (config trust); where the hub will not read what was written, the
1048
+ reply and the Settings row say so.
1049
+ - Authority: a dashboard opened with `ahub ui` reads settings, lowers a mode to
1050
+ `ask`, turns `pi.auto_start` off, sets a start mode back to `tui` and changes
1051
+ the Switches group. Everything
1052
+ else needs `ahub ui --settings`, whose session may raise for 15 minutes, or
1053
+ `ahub settings` in a terminal. `never-ask` needs the peer id typed in the
1054
+ dashboard, or `--yes` in the terminal, and a stored never-ask default still
1055
+ waits for `y` in `ahub console` at each hub start. Tool approvals stay
1056
+ deny-only for Pi and local in every session.
1057
+ - Commands and binaries, credential files, gateway URLs and the local worker's
1058
+ reach are never editable here: edit `.agenthub/config.local.json` yourself.
1059
+ - Every change is announced in `hub.log` and open consoles with its source and
1060
+ recorded as a `settings` event. `ahub settings` needs a running hub; an older
1061
+ hub answers the request as unknown and the CLI advises upgrading. The unified
1062
+ dashboard (`ahub ui --all`) acts as an ordinary session.
1063
+
1064
+ ## Start modes: TUI by default, headless as opt-in
1065
+
1066
+ A peer that has a TUI the hub attaches to starts in it by default (issue #269).
1067
+
1068
+ | Peer | Default | Headless |
1069
+ | --- | --- | --- |
1070
+ | pi | `tui` | `ahub pi --headless` (or `--mode headless`), or `peers.pi.start_mode: headless` |
1071
+ | codex | `tui` (`ahub codex`) | `peers.codex.start_mode: headless`: a hub-made start runs the app-server only |
1072
+ | claude | `tui` (`ahub claude`) | none |
1073
+ | kimi | headless (ACP) | it has no TUI the hub attaches to |
1074
+ | local | headless (hub-native) | not applicable |
1075
+
1076
+ - **You type the command:** `ahub pi` opens Pi's TUI in that terminal.
1077
+ `--headless` and the start mode setting are the two ways to ask for the
1078
+ background peer. `ahub settings set peers.pi.start_mode headless` writes the
1079
+ setting; it applies at Pi's next start.
1080
+ - **The hub starts the peer** (the dashboard's Start control, `pi` typed in
1081
+ `ahub console`, the conductor's `hub_peer_start`, `pi.auto_start`): it
1082
+ follows the peer's start mode. For `tui` it opens a terminal running the
1083
+ fixed command `ahub <peer>`, through a terminal provider:
1084
+ 1. `terminal.open` in `.agenthub/config.local.json`, an argv in which one
1085
+ element holds `{command}` (the quoted command line to run); `{title}` and
1086
+ `{cwd}` are replaced too. Examples:
1087
+ `["tmux", "new-window", "-n", "{title}", "{command}"]`,
1088
+ `["wezterm", "start", "--cwd", "{cwd}", "--", "sh", "-c", "{command}"]`.
1089
+ Only a config file nobody committed may set it, and it is not a setting
1090
+ the dashboard or `ahub settings` can write. It is read at hub start.
1091
+ `{command}` is quoted for a POSIX shell; `{title}` and `{cwd}` are raw,
1092
+ so give each an argv element of its own.
1093
+ 2. Orca, when the hub runs in an Orca worktree or an earlier `ahub <peer>`
1094
+ launch of this project recorded one (the path recovery uses).
1095
+ - **No provider:** the start is refused with the command to run by hand. It is
1096
+ never downgraded to headless. `ahub settings` lists each start mode, and the
1097
+ dashboard's Start control says which mode a start will use and where its
1098
+ terminal comes from.
1099
+ - **Dashboard Start:** the control is locked while its request is in flight. A confirmed answer unlocks it immediately, so a retry reaches the hub's own 30-second terminal reopen guard and shows any known refusal. The unified dashboard waits 35 seconds, covering a terminal provider's 30-second cap plus a 5-second response margin; a slower headless start can still end unconfirmed while the hub continues. Check the Peers panel before retrying. An unconfirmed unified-dashboard start holds that peer's control for the shared 30-second reopen bound, or until its `starts` snapshot confirms attachment, across redraws and project switches within the same hub instance; the row states the retry bound. The project-local dashboard keeps its usual confirmed-answer behavior. Other forwarded actions keep their 10-second wait; the manager opens no settings session.
1100
+ - The conductor cannot choose the mode: `hub_peer_start` takes the peer's own.
1101
+ - Recovery restores each peer in the mode its recorded session had. After an
1102
+ unplanned stop, a Pi that ran in a terminal is reported with its command and
1103
+ is not replaced by a headless one unless headless is its start mode.
1104
+
1105
+ ### Migration from 0.12.22 and earlier
1106
+
1107
+ Pi's default changes from headless to its TUI.
1108
+
1109
+ - `ahub pi` in a terminal now opens the TUI. For the background peer run
1110
+ `ahub pi --headless`.
1111
+ - A project with `"pi": { "auto_start": true }` used to get a headless Pi at
1112
+ every hub start. To keep that, set the start mode once:
1113
+ `ahub settings set peers.pi.start_mode headless` (or add
1114
+ `"peers": { "pi": { "start_mode": "headless" } }` to
1115
+ `.agenthub/config.local.json`). Without it the hub opens Pi's TUI in a
1116
+ terminal at start, or, with no terminal provider, says
1117
+ `pi.auto_start did not start Pi` with the command to run.
1118
+ - Scripts and the console's `pi` command: without a terminal, `ahub pi` asks
1119
+ the hub to open one. Add `--headless` where a headless Pi is meant.
1120
+ - A conductor's `hub_peer_start` no longer takes a mode. A call that passes one
1121
+ is refused unless it repeats the peer's start mode.
1122
+
921
1123
  ## Approvals and pauses
922
1124
 
923
1125
  Inspect permission requests in the terminal:
@@ -936,9 +1138,11 @@ ahub permit <request-id> allow
936
1138
 
937
1139
  Use the exact option shown by `ahub tail`; do not approve an unresolved or
938
1140
  unexpected request. On macOS a waiting request also raises a desktop
939
- notification that names the peer and, for Kimi and the local worker, the tool,
1141
+ notification that names the peer and, for Kimi, Pi and the local worker, the tool,
940
1142
  never what it would run. An unanswered request is cancelled after `approvals.timeout_s` (default
941
1143
  120, 30 to 3600) in `.agenthub/config.json`, and the console and log say so.
1144
+ Pi and the local worker are told that nobody answered, and two such requests in
1145
+ a row end their turn ([Pi/local unanswered approvals](#pilocal-unanswered-approvals)).
942
1146
  Set `approvals.notify` to `false` to turn notifications off; a hub started
943
1147
  without a project config file raises none. While Kimi waits for an answer its
944
1148
  turn is kept alive, so a timeout longer than the inactivity watchdog does not
@@ -955,6 +1159,13 @@ A budget pause remains authoritative until the budget command explicitly
955
1159
  overrides it or the window resets. Check `ahub status` and `ahub board` after
956
1160
  a pause or handoff.
957
1161
 
1162
+ Kimi Code CLI 2.1.1's observed 5-hour usage-limit refusal creates a hard budget reading (`kimi ACP usage-limit refusal`). `ahub status` shows a budget pause and `ahub budget` shows its source and reset estimate. The refusal gives no reset timestamp, so the hub estimates the reset at five hours after it was observed and resumes on the next budget tick after the existing one-minute grace. With `budget.wait_max_min` below that estimate, the existing budget relay automatically hands open tasks to an attached eligible peer; routing skips Kimi while paused. No checkpoint is requested from an exhausted agent.
1163
+ `ahub budget resume kimi` overrides the reading so the provider can be tried early. A new provider refusal ends that override and pauses it again; a pending soft checkpoint cannot postpone this hard pause.
1164
+ During one hub run, an early override followed by the same window's refusal keeps its earlier future reset bound; a provider's explicit reset remains authoritative. A restored pause without a per-window reading cannot identify that earlier window, so its next refusal starts a new estimate.
1165
+ Unread quota resume notices are coalesced without losing their public task moves; the next notice lists moves accumulated across those pauses. An older notice without structured move metadata is kept as public text inside that notice.
1166
+
1167
+ A refused prompt with no acceptance or activity is requeued without a review hold. At the estimated reset it is tried again; another quota refusal pauses it again without consuming the poison-envelope retry count. Work that already started remains held for review even while its open tasks move to other peers. After the quota reset its queue stays held: inspect `ahub queue show <delivery-id>` and the associated task, then choose `ahub queue resolve <delivery-id> --action completed|retry|discard --reason "<text>"`. Do not retry work that another owner already took over without inspecting it. If the hub cannot confirm the pause, the refusal is also held instead of spending retries. Other errors keep their ordinary review behavior. Only the observed Kimi 5-hour text is recognised; weekly/monthly limits and other ACP vendors, including Qwen, remain unverified. The recognised text and its provenance live in `src/adapters/acp-quota.ts`.
1168
+
958
1169
  Quota also shapes routing and handoffs:
959
1170
 
960
1171
  - Among peers a task could go to, those with quota readings are ordered by
@@ -1012,8 +1223,8 @@ Only `ahub claude` starts Claude Code with the development-channel flag. A
1012
1223
  Claude session started another way (plain `claude`, an IDE) keeps the hub tools
1013
1224
  but cannot show pushes, so it attaches tools-only (protocol 16, issue #205):
1014
1225
 
1015
- - Its messages stay queued. `ahub status` shows `queued N` and `tools-only:
1016
- messages wait for hub_inbox; for pushes restart Claude with ahub claude`; the
1226
+ - Its messages stay queued. `ahub status` shows the queue count in `Q` and a `tools-only` detail:
1227
+ `messages wait for hub_inbox; for pushes restart Claude with ahub claude`; the
1017
1228
  console footer shows `tools-only: ahub claude` and the dashboard shows the
1018
1229
  status line.
1019
1230
  - The session reads them with `hub_inbox`, at most ten messages at a time plus
@@ -1044,11 +1255,15 @@ The daemon waits for shutdown work that must survive termination. Afterward,
1044
1255
  run `ahub status` or `ahub up` and read the project state before restarting
1045
1256
  peers. A stopped daemon does not delete task records or the durable journal.
1046
1257
  Do not kill a native terminal by PID or start a replacement while ownership is
1047
- uncertain.
1258
+ uncertain. To stop one headless peer and keep the hub running, use
1259
+ `ahub stop <peer>` ([Stop one headless peer](#stop-one-headless-peer)).
1048
1260
 
1049
1261
  Shutdown is bounded: once it begins, a daemon that cannot finish within
1050
1262
  15 seconds exits anyway, and a peer that refuses to stop is logged rather than
1051
- allowed to block state cleanup. A native Pi TUI owner that does not exit
1263
+ allowed to block state cleanup. A managed Pi tool that does not settle within
1264
+ the stop grace does not block the teardown either: the stop goes on
1265
+ (shutdown, owner teardown, process stop) and reports the failure at the end.
1266
+ A native Pi TUI owner that does not exit
1052
1267
  gracefully is terminated by its verified process identity (never a bare PID),
1053
1268
  so a restarted hub never launches beside a survivor. A hub whose project root
1054
1269
  or state directory was deleted stops itself within about 10 seconds; the
@@ -1169,23 +1384,38 @@ Rows without a live process are stale registrations; forget them with
1169
1384
 
1170
1385
  ## Upgrade and crash recovery
1171
1386
 
1387
+ On a terminal, one command reviews, applies and follows an upgrade:
1388
+
1389
+ ```bash
1390
+ ahub upgrade
1391
+ ```
1392
+
1393
+ Without `--to` the target is the registry's latest release. When that release is
1394
+ newer than the installed CLI, the CLI names the release's own coordinator
1395
+ (`bun x --package @staix/agent-hub@<version> ahub upgrade --to <version>`), asks,
1396
+ and hands over to it, so the version is never typed twice and an older CLI never
1397
+ coordinates. From an installed CLI older than 0.12.22, start it once by hand:
1398
+ `bunx --package @staix/agent-hub@<version> ahub upgrade`. The screens are
1399
+ described under [Interactive upgrade](#interactive-upgrade); the rest of this
1400
+ section is what they run.
1401
+
1172
1402
  Upgrade running projects with the target release's own coordinator. It accepts
1173
1403
  a running source on control protocol 9 (0.6.x), 10 (0.7.0 through 0.12.0),
1174
- 11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 and 0.12.21), and only
1404
+ 11 (0.12.1 and 0.12.2), 12 (0.12.3), 13 (0.12.4 through 0.12.15), 14 (0.12.16), 15 (0.12.17 through 0.12.19) or 16 (0.12.20 through 0.12.23), and only
1175
1405
  a target on its own protocol, so the target's coordinator fits every supported
1176
1406
  source and carries every recovery fix released up to it. Protocol 8 and older
1177
1407
  (0.5.x and earlier) are refused as `manual-bootstrap-required`. Run from the
1178
1408
  project directory, without replacing the global CLI first:
1179
1409
 
1180
1410
  ```bash
1181
- bunx --package @staix/agent-hub@0.12.21 ahub upgrade --to 0.12.21 --dry-run
1182
- bunx --package @staix/agent-hub@0.12.21 ahub upgrade --to 0.12.21 --yes
1411
+ bunx --package @staix/agent-hub@0.12.23 ahub upgrade --to 0.12.23 --dry-run
1412
+ bunx --package @staix/agent-hub@0.12.23 ahub upgrade --to 0.12.23 --yes
1183
1413
  ```
1184
1414
 
1185
1415
  | Running now | Coordinator to use |
1186
1416
  | --- | --- |
1187
1417
  | 0.6.x (protocol 9) | the target's, through `bunx` as above |
1188
- | 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 and 0.12.21 (protocol 16) | the target's, through `bunx` as above |
1418
+ | 0.7.0 through 0.12.0 (protocol 10), 0.12.1 and 0.12.2 (protocol 11), 0.12.3 (protocol 12), 0.12.4 through 0.12.15 (protocol 13), 0.12.16 (protocol 14), 0.12.17 through 0.12.19 (protocol 15), 0.12.20 through 0.12.23 (protocol 16) | the target's, through `bunx` as above |
1189
1419
  | any supported source, with the installed CLI already at the target | `ahub upgrade` below, which is the same coordinator |
1190
1420
  | 0.5.x or earlier (protocol 8 and older) | not supported: bootstrap by hand with the matching CLI |
1191
1421
 
@@ -1217,21 +1447,27 @@ projects first:
1217
1447
 
1218
1448
  ```bash
1219
1449
  ahub restart --dry-run
1220
- ahub upgrade --to 0.12.21 --dry-run
1450
+ ahub upgrade --to 0.12.23 --dry-run
1221
1451
  ```
1222
1452
 
1223
1453
  Apply only after reviewing the plan:
1224
1454
 
1225
1455
  ```bash
1226
1456
  ahub restart --yes
1227
- ahub upgrade --to 0.12.21 --yes
1228
- ahub recovery status <operation-id>
1229
- ahub recovery resume <operation-id>
1230
- ahub recovery abort <operation-id>
1231
- ahub recovery dispose <operation-id> --fresh-session <peer> --reason <text>
1232
- ahub recovery dispose <operation-id> --stop-and-archive --reason <text>
1457
+ ahub upgrade --to 0.12.23 --yes
1458
+ ahub recovery status [<operation-id>]
1459
+ ahub recovery resume [<operation-id>]
1460
+ ahub recovery abort [<operation-id>]
1461
+ ahub recovery dispose [<operation-id>] --fresh-session <peer> --reason <text>
1462
+ ahub recovery dispose [<operation-id>] --stop-and-archive --reason <text>
1233
1463
  ```
1234
1464
 
1465
+ `--yes` needs `--to`; a dry run without `--to` plans the latest release. A target
1466
+ newer than the CLI is handed to that release's coordinator with the same flags,
1467
+ and the command is printed on stderr first. The `recovery` commands take the
1468
+ operation that holds the machine's lock when the id is left out, and say `no
1469
+ recovery operation holds the lock` when none does.
1470
+
1235
1471
  The coordinator commits only once the source is quiet: no turn running, no
1236
1472
  approval pending, no completion check queued or running. It waits up to 10
1237
1473
  minutes and then aborts, leaving the source running; upgrade between long
@@ -1271,6 +1507,10 @@ runs under the deny-default sandbox, and `hub.log` and `ahub doctor` say so.
1271
1507
  `local.bash_network: "direct"` still works until 0.13.0, and both name it while
1272
1508
  it is set.
1273
1509
 
1510
+ 0.12.22 closes the name `.git` to the local worker's and Pi's commands at any
1511
+ depth: `git init`, `git clone` and `git worktree add` no longer run inside the
1512
+ sandbox (the full list is under Install and start).
1513
+
1274
1514
  The 0.7.0 transition stages the verified package and runs a retained
1275
1515
  coordinator from the source tree. It accepts a verified protocol-9 source and
1276
1516
  moves to a protocol-10 target. The source journal, queued envelopes, tasks,
@@ -1306,6 +1546,93 @@ Do not run an upgrade with an incompatible active protocol, an unverified
1306
1546
  terminal binding, or an unresolved operation lock. Dry-run performs no package,
1307
1547
  plugin, daemon, or terminal mutation.
1308
1548
 
1549
+ ### Interactive upgrade
1550
+
1551
+ `ahub upgrade` and `ahub restart` are interactive when standard input and output
1552
+ are a terminal and neither `--dry-run` nor `--yes` is given. With either flag, or
1553
+ through a pipe, the JSON plan, the `ahub: blocker:` lines and the exit codes are
1554
+ unchanged. The screens add no way to change an operation: every choice runs one
1555
+ of the commands in this section for you, so that command's refusals decide.
1556
+
1557
+ The plan screen lists each project with its hub version and, per attached peer,
1558
+ its state, how it runs (`TUI` in a terminal the plan bound to its session,
1559
+ `headless` as the hub's own process, `own` for a session the hub did not
1560
+ launch or whose terminal the plan could not bind) and what happens to it: resumed in a new terminal (the old one is
1561
+ named), reconnecting by itself, restarted as a new session, restarted headless,
1562
+ or left offline. `in progress:` repeats what the running hub says keeps it from
1563
+ being quiet (busy peers, pending approvals, a peer starting, a budget
1564
+ transition; a hub of 0.12.22 or newer also names a task command, completion
1565
+ checks or Pi tool calls in flight, ready tasks being released and a Pi that has
1566
+ not settled); apply waits up to 10 minutes for it. Blockers follow, each with its
1567
+ next action. The choices are `[a] apply` (absent while a blocker stands), `[r]
1568
+ refresh`, `[k] end agents` (shown when an attached agent can be ended), `[j]`
1569
+ the plan as JSON, `[x] reset a project's hub` (shown when the plan has a running
1570
+ project) and `[q] quit`.
1571
+
1572
+ `[k] end agents` ends attached agents before the upgrade, so they are neither
1573
+ waited for nor restored: `t` for the TUI agents, `h` for the headless ones
1574
+ (Kimi, Pi in headless mode, the local worker), or their names, written
1575
+ `<project id>/<name>` when the plan covers more than one project. After a
1576
+ confirmation a TUI agent's terminal is closed, which cuts a turn in progress,
1577
+ and a headless agent is stopped by its hub through the `peer_stop` request
1578
+ (#278). A delivery that a busy agent had not settled is then held as
1579
+ `needs_review`, and its queue stays held until `ahub queue resolve` settles it. Each agent is ended only
1580
+ if the lock is free and its hub, its session and its terminal are still the
1581
+ ones the plan read; otherwise the screen says why it was left. A hub that does
1582
+ not know `peer_stop` (0.12.21 and older) cannot end a headless agent: the
1583
+ screen says so, the agent stops with the old hub at the upgrade, and the new
1584
+ hub starts it again. A session marked `own` is ended where it runs. The next
1585
+ plan shows the ended agents as offline.
1586
+
1587
+ After apply the command stays attached and prints each step as the receipt
1588
+ records it, and while a source is being prepared, what it still waits for.
1589
+ Enter opens the operation's screen while the runner works on. Ctrl+C leaves the
1590
+ command, at any prompt and while following, and never stops the runner: the
1591
+ `left:` line names the command that comes back to the screen, with the
1592
+ operation's own coordinator, because the installed `ahub` is the older release
1593
+ until the upgrade ends. A Ctrl+C between `a` and the start of the operation
1594
+ starts nothing. A prompt for a reason that is answered with
1595
+ Ctrl+C or the end of input ends nothing.
1596
+
1597
+ The operation's screen also opens first whenever an operation holds the lock,
1598
+ and for bare `ahub recovery`. It shows the phase and step, each project's phase
1599
+ and effect receipts and the error, then the choices that `status` lists under
1600
+ `next` for the same receipt and live state:
1601
+
1602
+ | Key | Runs | Offered when |
1603
+ | --- | --- | --- |
1604
+ | `r` | `recovery resume`, then follows | resume can get past what is live |
1605
+ | `c` | `recovery abort` | no runtime was stopped and no terminal effect or commit request is recorded |
1606
+ | `f` | `recovery dispose --fresh-session <peer>`, after a reason | a Codex or Claude restoration failed |
1607
+ | `e` | `recovery dispose --stop-and-archive`, after a confirmation and a reason | every unverified project reads as running, stopped or missing |
1608
+ | `x` | `e`, then the reset below | as `e` |
1609
+ | `w` | follows the runner | a runner holds the operation |
1610
+
1611
+ When more than one restoration failed, the second and later peers get
1612
+ `f-<peer>`. `s` refreshes the screen, `j` prints the receipt as JSON and `q`
1613
+ leaves the operation as it is.
1614
+
1615
+ While a runner is only waiting for its source to get quiet (step `prepare`, no
1616
+ effect recorded), `c` stops that runner and then aborts. It is offered only for
1617
+ a runner whose claim carries a process signature that still matches; a claim
1618
+ from a coordinator of 0.12.20 or older has none, so its pid is never signalled,
1619
+ and such a runner gives up its wait after 10 minutes. The receipt is read again
1620
+ when the key is pressed, and a runner that has moved on is left alone. If it
1621
+ records an effect in the instant between that read and the signal, the abort is
1622
+ refused and resume is offered instead. Past that step a running operation
1623
+ offers only `w`.
1624
+
1625
+ A reset is offered from the plan screen for a project whose hub is running, and
1626
+ with one question right after an operation was cancelled or ended from its
1627
+ screen: it asks for the project (when there are several) and the scope, prints
1628
+ the dry run of `ahub reset` or `ahub reset --all`, and applies it with `--yes`
1629
+ only after a second confirmation. While an operation holds the lock it refuses,
1630
+ as `ahub reset` does.
1631
+
1632
+ Ending a peer's running turn is not part of these screens: finish or interrupt
1633
+ it in that peer's terminal, answer approvals with `ahub permit` or the console,
1634
+ or cancel and upgrade later.
1635
+
1309
1636
  ### A partial operation
1310
1637
 
1311
1638
  `ahub recovery status <operation-id>` reads the receipt without changing it:
@@ -1354,6 +1681,28 @@ next actions").
1354
1681
  restart snapshot.
1355
1682
  A second `resume` while its own hold still stands checks the peers again
1356
1683
  before it closes anything.
1684
+ - If a restart stops with `database_busy`, the receipt stays `blocked` and
1685
+ carries `failure: { code: "database_busy", retryable: true, site: [...] }`.
1686
+ Let the other connection settle, inspect `ahub recovery status <id>` and
1687
+ use its offered `ahub recovery resume <id>`. Resume rechecks the source and
1688
+ reconciles a commit that may already have been requested. A busy read while
1689
+ that committed source is stopping stays inside the existing shutdown wait.
1690
+ A stop-and-archive that meets contention instead says to retry that
1691
+ disposition; resume remains refused while abandonment is partway. Successful
1692
+ abort, fresh-session choice and disposal clear the earlier failure annotation.
1693
+ Do not delete database files or start a second operation to bypass the lock.
1694
+ A failed installed-layout CI restart retains bounded, redacted fixture logs,
1695
+ receipt fields (including `commitSent` and failure frames) and file-mode
1696
+ metadata as a per-OS artifact.
1697
+
1698
+ The historical CI receipt (38061057253, attempt 1) kept no throw site or
1699
+ hub log. At its main, status/inspect RPC failures were swallowed and the
1700
+ synchronous catch sent a fixed sentence, so its raw SQLite text could only
1701
+ have come from the commit rejection or the runner's own `registry.db` read.
1702
+ "Resume | wait until the hub settles", without stop-and-archive, means its
1703
+ next inspection saw neither running nor stopped/missing. Which of the two
1704
+ paths, which statement and which holder remain unknown. The controlled
1705
+ rollback-copy reproduction is separate evidence, not that historical cause.
1357
1706
  - When a target hub dies after it started (a crash or a reboot), `resume`
1358
1707
  starts it again from the operation's snapshot while the project's state
1359
1708
  directory still holds it: it records the restart (status shows `restarts`),
@@ -1492,8 +1841,9 @@ crash of one is not reported this way.
1492
1841
  spends no cloud quota), and on a fresh session if that fails, keeping the
1493
1842
  recorded backend and model; the report says which. Malformed records in
1494
1843
  `sessions.json` are skipped. A Pi that ran in a terminal (`--mode tui`) is never started on its
1495
- recorded session by the hub; the report gives the command, and with
1496
- `pi.auto_start` a fresh headless Pi starts instead.
1844
+ recorded session by the hub; the report gives the command. With
1845
+ `pi.auto_start` a fresh headless Pi starts instead only when Pi's start mode
1846
+ is headless; with the default (tui) nothing is started in its place.
1497
1847
  - Codex's app-server died with the hub; run `ahub codex` again. Claude Code's
1498
1848
  plugin reconnects by itself while that session is open.
1499
1849
 
@@ -1620,7 +1970,7 @@ whose MLX slot is still busy after that wait goes to `dgx/fast` first (`source:
1620
1970
  so does an enforced tool loop pinned to it (`source: "pin"`); without it nothing moves for load. `dgx/fast`
1621
1971
  never goes first while it is cooling down, or while its own last dispatch failed in any way,
1622
1972
  an error status or a failed stream included, until it succeeds or 30 s pass (`ahub status`:
1623
- `last dispatch failed, no load moves until ...`). No load move or pin happens while the request
1973
+ a `dispatch held` detail with a relative time and `no load moves until then`). No load move or pin happens while the request
1624
1974
  runs under an execution budget, and enforced, a load move happens only at a user turn. A moved
1625
1975
  attempt gets 15 s to its response headers after the gateway lookup (OmniRoute's own probe,
1626
1976
  up to two 4 s rounds when no gateway is cached) and is then abandoned for MLX, which serves
@@ -1634,7 +1984,7 @@ consecutive transport or startup failures outside a cooldown (a failure more tha
1634
1984
  the last counted one starts the count over), a relay alias cools down for 30 s, doubling up to
1635
1985
  5 min. A busy MLX slot and timeouts cut short by an execution budget never count; any HTTP
1636
1986
  answer, a success or an error status, proves the transport works and ends the streak and the
1637
- cooldown. `ahub status` shows `cooling down until ...` on the backend line and `events.jsonl`
1987
+ cooldown. `ahub status` shows a `cooling` detail with a relative time and failure count and `events.jsonl`
1638
1988
  records `cooldown` events.
1639
1989
  Progress judgements suggest reassignment; they never change task ownership.
1640
1990
 
@@ -1774,7 +2124,8 @@ sequential seeded-guard job. Main and release jobs reuse only a successful full
1774
2124
  PR or push check with the identical Git tree and all three successful jobs. An absent or
1775
2125
  unreadable result runs the main gate again and refuses release. A manual
1776
2126
  `prepare_bundle` dispatch builds reviewable plugin assets without publishing;
1777
- it is never accepted as full-gate evidence.
2127
+ it is never accepted as full-gate evidence. A `timing_runs=20` dispatch runs only the daemon, PII screen and ACP
2128
+ test files twenty times on macOS, in its own concurrency group; it does not replace the full gate.
1778
2129
 
1779
2130
  ## Preview initialization and native launch
1780
2131
 
@@ -1803,9 +2154,19 @@ ownership, bind servers or start agents, sidecars or models.
1803
2154
  `ahub status`, `ahub tail` and the dashboard show native context occupancy,
1804
2155
  source and measurement freshness. Claude readings come from the status-line
1805
2156
  tee installed by `ahub claude`; Codex readings come from its current thread's
1806
- native token-usage updates. Pi and other unsupported surfaces show unknown.
2157
+ native token-usage updates. Kimi readings come from its ACP `usage_update`
2158
+ (`{used, size}`, source `acp_usage_update`): context occupancy, not
2159
+ consumption, so no token total is counted from it; the adapter accepts the
2160
+ update whenever it names the current session, because 2.1.1's source text
2161
+ emits it after the turn settles (read from the source, not yet observed live
2162
+ through the hub; docs/smoke.md), so the peer may already be idle.
2163
+ Pi and other unsupported surfaces show unknown.
1807
2164
  A stale or disconnected reading is unknown, not 0%. Codex's accumulated session
1808
- usage is never used as context occupancy.
2165
+ usage is never used as context occupancy. In the tail (`ahub tail` and the
2166
+ console's stream) a reading prints once and repeats only when the line changes
2167
+ apart from its time: the printed percentage, the freshness, the source, or known
2168
+ against unknown. Status, the Peers panel and the dashboard always read the
2169
+ latest reading with its time.
1809
2170
 
1810
2171
  Context-triggered checkpoints are disabled by default. To enable them, add
1811
2172
  this to `.agenthub/config.json` and restart the daemon deliberately:
@@ -1817,8 +2178,9 @@ this to `.agenthub/config.json` and restart the daemon deliberately:
1817
2178
  `gate` is a fraction between 0 and 1; 0 disables checkpoint requests.
1818
2179
  `stale_min` must be positive. A reading at or above the threshold records a metadata-only
1819
2180
  event and console notice, then asks an attached Claude/Codex with active work
1820
- for a checkpoint when no checkpoint request is already outstanding. The request
1821
- supplies a `request_id`; include that id with
2181
+ for a checkpoint when no checkpoint request is already outstanding (an ACP peer
2182
+ such as Kimi gets the event and notice only; no checkpoint request is sent to it
2183
+ yet). The request supplies a `request_id`; include that id with
1822
2184
  `hub_checkpoint {summary, request_id}`. Repeated high readings do not repeat
1823
2185
  it until a fresh below-threshold reading or new session rearms the crossing.
1824
2186
 
@@ -1849,13 +2211,88 @@ Pi exit records show code or signal, turn/tool activity, whether teardown was
1849
2211
  expected, startup status, and the last tool's name without arguments. An
1850
2212
  unobserved native-terminal exit records unknown OS status. The console notice
1851
2213
  gives the next action, usually `ahub pi`, or says why automatic start is held.
1852
- A requested replacement says the new owner is starting; internal failure
1853
- teardown directs session inspection rather than claiming a person requested it.
2214
+ A replacement says the owner stopped for a new Pi owner, including when the
2215
+ replacement is refused. Internal failure teardown directs session inspection
2216
+ rather than claiming a person requested it. Replacement, failed-start and
2217
+ active-turn guidance appears before automatic-start-disabled guidance.
1854
2218
 
1855
2219
  With `pi.auto_start`, an unexpectedly idle headless Pi is resumed on its
1856
2220
  verified persisted session once. The 60-second retry window rearms when the
1857
2221
  attempt settles; a second exit inside that window stays offline. Missing or
1858
2222
  invalid session history has no fresh fallback; inspect it before running
1859
2223
  `ahub pi`. Active turns/tools, failed startup, a superseded owner, native
1860
- terminal exits, requested shutdown and recovery operations suppress this
1861
- restart. Crash recovery's #66 recorded-session/fresh-start choices are unchanged.
2224
+ terminal exits, a requested stop (`ahub stop pi`), requested shutdown and
2225
+ recovery operations suppress this restart. Crash recovery's #66
2226
+ recorded-session/fresh-start choices are unchanged.
2227
+
2228
+ ### Pi/local unanswered approvals
2229
+
2230
+ An expired Pi/local request says "approval expired: no person answered". The
2231
+ worker must not retry that call; hand the task off or stop. Two consecutive
2232
+ unanswered requests end the current turn and produce a console notice. An
2233
+ expiry counts only when no person answered a request of that peer while it
2234
+ was pending, so a batch of parallel requests does not stop a worker whose
2235
+ person is answering; a grant served from the always-allow cache or a permission
2236
+ mode is not an answer and does not reset the count. A new turn also resets it. The turn ends
2237
+ through the same abort path an execution-budget stop uses: Pi stays attached
2238
+ and the sender is told the approval reason. Supported Pi and local settle that
2239
+ delivery after reporting the reason, so the next queued message runs without
2240
+ an operator release. Pi clears turn-only approval state at settlement; the
2241
+ person's idle `!command` remains usable and a later failure keeps its own reason.
2242
+ A Pi whose extension cannot abort
2243
+ a turn (an older hub extension or a runtime without native abort support) goes
2244
+ offline instead. Inspect prior work, restart with `ahub pi`, find the held
2245
+ delivery with `ahub queue list --peer pi`, inspect it with `ahub queue show <id>`,
2246
+ then explicitly settle it with `ahub queue resolve <id> --action completed|retry|discard --reason "<text>"`.
2247
+ A cancelled tool withdraws
2248
+ its request immediately; a later `ahub permit` answer is refused and cannot
2249
+ create an always-allow grant. Tool requests from a previous turn or an older
2250
+ extension missing session/turn identity are refused before execution; restart
2251
+ Pi with the current hub extension rather than retrying that old call.
2252
+
2253
+ The dashboard shows the number of waiting Pi/local requests and where to
2254
+ answer them. It remains deny-only for those private rows. Use the console or
2255
+ `ahub tail` to inspect the request, then `ahub permit` to answer it. Recovery
2256
+ stops waiting for an approval when that request is withdrawn.
2257
+
2258
+ A person's `!command` in the managed Pi TUI runs through the managed sandbox
2259
+ without asking for another hub approval. Its reported exit code comes from
2260
+ the process, including a nonzero exit; text printed by the command does not
2261
+ set its status. This exception requires the current idle TUI owner's reserved
2262
+ request and does not apply to model-origin bash calls.
2263
+
2264
+ ### Stop one headless peer
2265
+
2266
+ `ahub stop kimi`, `ahub stop pi` (headless mode), or `ahub stop local` stops
2267
+ that hub-owned runtime through its adapter and prints `<peer>: offline`.
2268
+ The hub and its other peers keep running. The command is human-only;
2269
+ agent shells and tools-role connections cannot use the `peer_stop` request.
2270
+ An older hub that does not know the request needs an upgrade before this
2271
+ command is available.
2272
+
2273
+ Claude, Codex and Pi in TUI mode belong to their native terminals: end them
2274
+ in those terminals. The upgrade plan's end-agents action closes a TUI through
2275
+ its verified terminal binding and uses `peer_stop` for a headless runtime.
2276
+ Unknown and already-offline peers are refused with a next action. A recovery
2277
+ operation or shutdown holds stop mutations; finish that operation first.
2278
+ A peer's concurrent start, stop or permission-mode change must also finish
2279
+ before another lifecycle change is admitted, and an upgrade or restart does not
2280
+ prepare the hub while a stop is in progress.
2281
+
2282
+ A requested stop does not automatically restart a peer. A person or an authorized
2283
+ conductor can explicitly start it again; conductor starts remain permitted. Start it with
2284
+ `ahub kimi`, `ahub pi` or `ahub local`. Pending approvals are withdrawn before
2285
+ teardown. An in-flight delivery becomes `needs_review`, with a reason naming
2286
+ the requested stop; it is neither completed nor automatically replayed.
2287
+ Inspect partial effects with `ahub queue list --peer <peer>` and
2288
+ `ahub queue show <delivery-id>`, then explicitly choose `completed`, `retry`
2289
+ or `discard` with `ahub queue resolve`. Restarting alone does not release
2290
+ that held delivery. Work queued behind an idle stopped peer is retained and
2291
+ runs when it is started again, subject to its existing pauses and holds. New work
2292
+ arriving during teardown stays queued too, even if a console resume runs meanwhile.
2293
+ An idle Pi with no message/file yet records its live empty-session proof before
2294
+ stopping, so its next start resumes that identity. A Pi stopped after its first
2295
+ prompt was accepted but before history was persisted is not treated as empty:
2296
+ its delivery is held, and another start refuses until resume history is verified.
2297
+ Inspect and settle the held delivery before deliberately replacing that unpersisted
2298
+ session by restarting the hub; the stop never silently chooses a fresh session.