@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
package/CHANGELOG.md CHANGED
@@ -4,6 +4,35 @@ Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.12.23
8
+
9
+ - Console panels: Left and Right move between the five tabs with the digit keys' guards and resets, wrapping at both ends (a read holding only arrow sequences is that many key presses, never a paste), and the Events tab colours each row's header through the stream's own tokenizer on the same tab-flattened header, so an event reads the same in both places (body lines and command output stay plain; with colour off the panel is byte-identical); the tail no longer repeats an unchanged context reading: a context line prints when the printed percentage, freshness, source or known-ness changes for that peer, not at every report (#309).
10
+ - A restart blocked by SQLite contention now records retryable `database_busy` with its offered next action and bounded throw site, settles the first busy inspection without a second probe, keeps a committed source inside its shutdown wait, clears stale failure on disposition, and preserves bounded redacted CI logs and receipt fields for the next occurrence (#303).
11
+ - Pause Kimi on its observed ACP 5-hour quota refusal, requeue untouched work only after a confirmed pause and automatically relay open tasks through the existing budget policy; hard refusals end reading overrides and supersede pending soft checkpoints. (#306)
12
+ - Bound SQLite contention on every shared database connection with a per-file busy timeout and a source guard. (#303)
13
+ - Make status, board, budget, doctor, projects, queue, turns, models and reports readable with shared tables, labelled fields, color policy, complete wrapped text and JSON output; size and sanitize console child output to its available width. `queue show`, `models status` and `budget execution status` now default to text; scripts that parsed their JSON add `--json`; a file name split across lines keeps whole characters, combining marks and joined emoji included (#284).
14
+ - Let unified dashboard peer starts wait 35 seconds for the terminal provider cap plus a response margin; slower headless starts are reported as unconfirmed. Block duplicate in-flight starts, unlock after confirmed answers, and bound an unconfirmed manager start lock by the hub's shared 30-second retry guard or earlier attachment; keep project-local confirmed-start behavior (#298).
15
+ - Keep durable Codex steer replies pending through their native turn and bound unanswered replies from its end so late definitive refusals requeue safely; make coordination and recovery test waits assert their milestones and await ACP cancellation transport before the next prompt (#296).
16
+ - Wait for an ACP cancelled prompt to settle before dispatching the next message on a bus without a journal; bound the wait and retain queued work when the provider cannot confirm cancellation, discard and log cancelled prompt errors, and preserve in-flight work across a legacy peer stop/restart (#304)
17
+ - Kimi's ACP `usage_update` (`{used, size}`, context occupancy) becomes the peer's native context reading with the new source `acp_usage_update`, so `ahub status`, the console and the dashboard show `context N%` for Kimi like Claude's and Codex's; the adapter accepts the update whenever it names the current session (2.1.1's source text emits it after the turn settles, so the peer may already be idle; that order is read from source, not yet observed live), an invalid update reports unknown instead of keeping the previous reading, occupancy still writes no `tokens` event and no budget window, and a `context.gate` crossing records the event and notice for an ACP peer without sending it a checkpoint request yet (#285).
18
+ - The seatbelt profile now also refuses the `config` and `hooks` of submodule git directories below the discovered modules root of the project's own `.git`, at the depth submodules nest and immune to ancestor directories named `refs`, `config` or `hooks` (refs named `config` or `hooks` stay writable through the `/refs/` and `/logs/` exclusion, matched on whole components only), and the discovered git directories, the modules root and the components in between are refused as directory-entry targets, closing the rename, replace and symlink routes; a nested bare-layout repository is recorded as a tested exclusion in `docs/security.md` (no hub git call descends into it, whatever the project root is); and every git call the hub makes outside the sandbox goes through the one helper in `src/hub/git.ts`, which disables the programs a repository's configuration can name (file-system monitor, hooks, external diff, pager, credential and askpass helpers), with a grep guard that fails on a new direct `spawn("git"` under `src/` outside the helper and the sandboxed tool (#281).
19
+ - A repeated `changes_requested` whose escalation saved its move returns the saved task when the assignment's delivery fails, with a console notice that says the delivery is not confirmed and to look for a queued or needs_review delivery (`ahub queue list --peer`, then `ahub queue show <id>`; if the journal is down, `ahub task show <id>` shows whether the owner accepted) before re-sending with `ahub task assign <id> <owner>`, instead of failing the reviewer's tool call; every escalation, a manual one included, now also runs its guarded tail on a saved move (the old owner is told to stop, the decision note is saved) before it throws, and a failure before the escalation's write still throws (#276).
20
+ - The saved-but-undelivered rule now holds on every path through `assignOwner`: a failure after the board write prints exactly one console notice where it is thrown (the move is saved, the delivery is not confirmed, how to check with `ahub queue list --peer` / `ahub queue show` / `ahub task show`, and the `ahub task assign` resend command) and throws `AssignmentUndeliveredError` carrying the saved task; callers decide control flow by the error type and print nothing more (the daemon's `delivery_failed` and `inference_failed` escalations no longer contradict the escalation's own tail, the ready offer's consumed decision reads the error instead of the owner readback, the budget relay records the move and later ticks skip it by its owner, the offline relay records the move and still tells the gone owner to stop, and the idle sweep and rescreen go on). A claim's own accepted write and line stay out of the delivery rule (its overlap and routing announcements to earlier owners share it), a move superseded while the assignment was prepared rethrows the original error with no notice (a later entry that moves or settles it; quiet bookkeeping, reviewer changes and check results do not supersede it), and the pre-write, passed-over and reserved-owner notices go through the same guard, so those notices cannot fail their caller. No automatic resend anywhere (#297).
21
+ - Start modes: a peer with a TUI starts in it by default and headless is opt-in. `ahub pi` opens Pi's TUI (`--headless` or `--mode headless` for the background peer), and a start the hub makes itself (the dashboard's new Start control, a console command, `hub_peer_start`, `pi.auto_start`) follows `peers.<id>.start_mode` (`tui` by default, settable for pi and codex in Settings and `ahub settings`): for `tui` it opens a terminal running the fixed `ahub <peer>` command through `terminal.open` (a machine-local command template) or Orca, and with no provider it is refused with the command to run by hand, never downgraded. A conductor cannot choose the mode. **Migration:** Pi's default changes from headless to TUI; a project with `pi.auto_start` keeps its headless Pi with `ahub settings set peers.pi.start_mode headless`, and scripts add `--headless`. After an unplanned stop a Pi that ran in a terminal is no longer replaced by a headless one unless headless is its start mode. Control protocol stays 16 (`peer_start` is a new console-role request; `ui_snapshot` gains `starts`) (#269).
22
+ - Settings in the dashboard and `ahub settings list|get|set|undo`, from one registry: permission modes now and at hub start, `pi.auto_start`, per-class routing (`peers`, `escalate_to`, `route`, `pi_backend`), `stay_switch` and the safe switches. Writes go only to `.agenthub/config.local.json` and the new machine-local `.agenthub/routing.local.toml` (merged over `routing.toml` key by key and named in `ahub route explain`), read by the hub's own loaders first, with one-step undo, a preview (`route explain` for the open tasks before and after) and a `settings` event. A dashboard session reads, lowers a mode to ask and changes safe switches; `ahub ui --settings` opens a 15-minute settings session for the rest, and never-ask needs the peer id typed. Commands, credentials and sandbox reach are never editable there. `ahub init` ignores `routing.local.toml` in git. Control protocol stays 16 (`settings_get|set|undo|preview` are new console-role requests) and events schema stays 1 (#269).
23
+ - Fix macOS timing-sensitive test fixtures by waiting for channel welcomes and releasing PII responses at the fallback decision and separating ACP receipt checks from watchdog timing; add opt-in repeated macOS verification (#287).
24
+ - Control protocol stays 16: `settings_get`, `settings_set`, `settings_undo`, `settings_preview` and `peer_start` are new console-role requests that an older hub answers as unknown, the `ui` request takes an optional `settings` field, `ui_snapshot` gains `settings` and `starts`, and a refused recovery or control request can carry `code: "database_busy"` with `retryable`. Events schema stays 1: the `settings` event and the `acp_usage_update` context source are additions. A live upgrade of a running 0.12.22 hub to this release has not been run; `docs/smoke.md` has no record of one.
25
+
26
+ ## 0.12.22
27
+
28
+ - Restore the captured Codex approval policy once on a resumed thread after detached ask, show unknown-policy guidance in the CLI, and record direct native Claude child identity so verified crash cleanup can retire its settings file (#270).
29
+ - `ahub upgrade` and `ahub restart` are interactive on a terminal when neither `--dry-run` nor `--yes` is given: a readable plan (each peer's fate and whether it runs as a TUI or headless, what is in progress, every blocker), a choice to end agents first (a TUI agent's terminal is closed; a headless agent is stopped by a hub that knows `peer_stop`, #278), one key to apply, the steps and readiness waits as they happen, and for an open operation the choices `status` lists under `next`, run on the spot: resume, cancel, a fresh session, end (stop-and-archive) and a reset with its dry run first. A runner that only waits for its source can be cancelled when its claim's process signature still matches. Enter opens the operation's menu during the progress view; Ctrl+C leaves and never stops the runner. `--to` defaults to the latest release for a terminal run and a dry run (`--yes` still needs it), and a target newer than the CLI is handed to that release's own coordinator. `ahub recovery status|resume|abort|dispose` take the operation that holds the lock when the id is left out, and bare `ahub recovery` on a terminal opens its screen. With a flag or through a pipe, output and exit codes are unchanged, except that the dry-run plan gains `source.recovery.waiting` while the hub reports something in progress (#272).
30
+ - Add human-only `ahub stop <peer>` and console `peer_stop` for hub-owned headless ACP, Pi and local runtimes; preserve the hub and other peers, hold interrupted deliveries for review, refuse native terminals and recovery-held mutations, and suppress automatic restart after a requested stop (#278).
31
+ - `review()` and `done()` no longer throw after the approval or done board write when a following publish or notify fails: every such call goes through the one `releaseNotice` guard, which turns a failed publish into a console notice, drops a failed notify and returns the board write's result (#243).
32
+ - Distinguish expired Pi/local approvals, end a turn after two unanswered requests through the budget abort path with Pi staying attached (an expiry counts only while no person answers, and an always-cache grant is not an answer), withdraw aborted requests before late answers, show private approval counts, run a person's managed Pi shell without another approval using its actual exit status, and never let a tool past the stop grace block Pi teardown (#253).
33
+ - The seatbelt profile refuses writes that plant `.git` or `.agenthub` through the code points HFS+ ignores, in the name and in `config`/`hooks`/`commondir` alike, at any depth, and refuses the `.git` name itself, closing the rename, symlink, gitfile and commondir routes to a planted hook or config; the cost is that `git init`, `git clone` and `git worktree add` no longer run inside the sandbox (add, commit, stash, checkout and gc still do), and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` with a nested repository, or `rm -rf` of a directory that holds a `.git`. `.gitignore` and `.github/` stay writable, `docs/security.md` says the ignored-code-point fold is HFS+-only, and the dead `hasSegment` export is gone (#270).
34
+ - Control protocol stays 16: `peer_stop` is a new console-role request that an older hub answers as unknown, the `recovery` `inspect` reply lists more readiness blockers, and the `permission` reply can carry a `note`. Events schema stays 1. A running 0.12.21 hub is upgraded with the 0.12.22 coordinator; that live upgrade is still pending (`docs/smoke.md`).
35
+
7
36
  ## 0.12.21
8
37
 
9
38
  - Console and dashboard show shared whole-board task progress, waiting dependencies and four-stage tracks; the dashboard adds a host-scoped System/Light/Dark choice with pre-paint initialization and hashed inline CSP (#246).
package/README.md CHANGED
@@ -4,7 +4,7 @@ Native multi-agent hub for one developer's machine: Claude Code, Codex, Kimi Cod
4
4
  hub-owned local-LLM worker collaborate as peers in independent project directories, with
5
5
  task-aware model routing (Switchyard) in front of a self-hosted gateway (OmniRoute).
6
6
 
7
- Status: 0.12.21, control protocol 16. Durable delivery records distinguish queued
7
+ Status: 0.12.23, control protocol 16. Durable delivery records distinguish queued
8
8
  work from uncertain execution. The [smoke checklist](docs/smoke.md) records
9
9
  verified paths and remaining prerequisites.
10
10
 
@@ -28,14 +28,20 @@ cd <your project> && ahub init && ahub up
28
28
  Or install the same version from GitHub:
29
29
 
30
30
  ```bash
31
- bun add -g github:STAIxBWLB/agent-hub#v0.12.21 && ahub setup
31
+ bun add -g github:STAIxBWLB/agent-hub#v0.12.23 && ahub setup
32
32
  ```
33
33
 
34
34
  The installed commands remain `ahub` and `agent-hub`.
35
35
 
36
- When upgrading an existing project, run `ahub init` to refresh its managed agent
37
- instructions and `ahub setup` to refresh the Claude plugin. Restart the daemon
38
- and agent sessions so both load the update.
36
+ To upgrade while hubs are running, run `ahub upgrade` in a terminal. It takes
37
+ the latest release, shows the plan for every running project, applies it on
38
+ one key and follows it: the operation starts the new hub, installs the Claude
39
+ plugin and the global CLI, and restores or restarts the agent sessions as the
40
+ plan says. The [operations guide](docs/operations.md#upgrade-and-crash-recovery)
41
+ has the details, and the one-time start from an older CLI. With no hub running
42
+ there is nothing to carry over: install the new version as above and run
43
+ `ahub setup`. Afterwards run `ahub init` in a project to refresh its managed
44
+ agent instructions.
39
45
 
40
46
  - [Quickstart](docs/quickstart.md): install, first session, the local worker, the commands of a working day
41
47
  - [Operations guide](docs/operations.md): managed launchers, task/review flow, receipts, shutdown and recovery
@@ -77,17 +83,19 @@ an N-peer message bus and attaches each agent through its native control surface
77
83
  ## Pi with DGX and MLX
78
84
 
79
85
  Pi is optional. Install Pi separately and set `pi.enabled` to `true` in
80
- `.agenthub/config.json`. Set `pi.auto_start` to `true` to start its background
81
- peer with the hub. The project uses its existing OmniRoute connection for DGX;
86
+ `.agenthub/config.json`. Set `pi.auto_start` to `true` to start it with the hub.
87
+ Pi starts in its TUI by default; `ahub pi --headless`, or the start mode
88
+ setting (`ahub settings set peers.pi.start_mode headless`), asks for the
89
+ background peer instead. The project uses its existing OmniRoute connection for DGX;
82
90
  `pi.dgx_coding` and `pi.dgx_fast` select the gateway's configured model IDs.
83
91
 
84
92
  ```bash
85
93
  ahub models setup # prepare bounded Qwen3.5 4B MLX in an existing Ollama service
86
94
  ahub models start # verify loopback Ollama/model readiness (no daemon spawn)
87
95
  ahub models status
88
- ahub pi --mode headless --backend auto
96
+ ahub pi --headless --backend auto
89
97
  ahub say @pi "Read the project and summarize the next small implementation task"
90
- ahub pi --mode tui # switch the idle session to its native terminal
98
+ ahub pi # its TUI in this terminal; an idle headless session moves into it
91
99
  ```
92
100
 
93
101
  The default routing template assigns implementation, bulk edits and tests to
@@ -100,7 +108,8 @@ only. `--backend dgx` or `--backend mlx` explicitly pins a Pi session's backend.
100
108
  Pi connects to an authenticated relay with model aliases `dgx/coding`,
101
109
  `dgx/fast`, and `mlx/fast`; upstream gateway credentials stay in the hub.
102
110
  Its managed tools use the existing path guards, shell sandbox and terminal
103
- approval flow. Keep `ahub console` open to approve write/edit/shell operations.
111
+ approval flow. Keep `ahub console` open to approve write/edit/shell operations:
112
+ a request nobody answers expires, and two in a row end Pi's turn.
104
113
  Tool-call receipts survive daemon restart; an interrupted operation with an
105
114
  unknown outcome must be reconciled before repeating it. Cancelling a Pi turn
106
115
  does not automatically hand its task to a cloud peer.
@@ -144,7 +153,12 @@ browser cannot be opened.
144
153
  Open the link within 60 seconds; the browser session lasts one hour.
145
154
 
146
155
  The dashboard can send console messages, propose and assign tasks, pause or resume
147
- peers, and answer approvals. Budget pauses still require the terminal override.
156
+ peers, answer approvals, start a peer in its start mode and show the settings.
157
+ An ordinary session changes the safe switches and lowers a mode to `ask`; the
158
+ rest needs `ahub ui --settings` or `ahub settings` in a terminal. See
159
+ [Settings](docs/operations.md#settings-from-the-dashboard-and-the-terminal) and
160
+ [Start modes](docs/operations.md#start-modes-tui-by-default-headless-as-opt-in).
161
+ Budget pauses still require the terminal override.
148
162
  Private envelopes and PII tasks stay redacted. Local-worker and Pi tool details and
149
163
  allow decisions stay in `ahub tail` / `ahub permit`; the page can deny them.
150
164
  No dashboard port is opened until requested. See [the session security model](docs/security.md#local-dashboard-sessions-issue-6).
@@ -169,7 +183,8 @@ ahub ui --all
169
183
 
170
184
  The unified dashboard lists registered roots and opens their boards, streams,
171
185
  messages, approvals and budgets. It can start hubs and stop a selected hub after
172
- confirmation. Launch Claude/Codex TUIs from a terminal. The manager stays available
186
+ confirmation. Its Start control opens a peer's TUI only through a terminal provider
187
+ (`terminal.open` or Orca); without one, launch Claude/Codex TUIs from a terminal. The manager stays available
173
188
  when a project stops; `ahub ui --all --stop` stops only the manager. Existing
174
189
  `ahub ui` still opens the current project's dashboard.
175
190
 
@@ -1,28 +1,36 @@
1
1
  # Native adapters and spawned agents
2
2
 
3
- Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/memory/capture.ts` (session end) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
3
+ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` and `activity` events), peer turn state, approvals, and the agent processes the hub spawns and stops. Read before editing `src/adapters/`, `src/pi/`, `src/hub/peers.ts`, `src/hub/permission-mode.ts`, `src/hub/tool-refusal.ts`, `src/hub/child-process.ts`, `src/hub/lifecycle.ts` (shutdown deadline), `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/memory/capture.ts` (session end) or the process-tree stop in `scripts/benchmarks/teardown.ts`, or before adding a native peer.
4
4
 
5
5
  - Codex: the adapter never sends its own `initialize`. It is a proxy; hub requests use negative ids and their responses must not reach the TUI.
6
+ - A durable Codex steer stays pending while its native turn runs. Its RPC may answer after `turn/completed`: bound unanswered replies from that turn's end, requeue a definitive late refusal, and hold a late acceptance as uncertain without attaching it to a later turn's audience (#296).
6
7
  - Codex reports each thread it adopts, started or resumed (`onThread`, the `native_thread` event), before any turn on it starts: the upgrade planner (`codexZeroTurn` in `src/cli/upgrade-runtime.ts`, #215) gives each codex `turn_start` to the thread adopted last before it and plans a thread it saw start with no turn (and no rollout) as a fresh start.
7
8
  - Codex 0.154.0 `agentMessage` items carry `text` and `phase` (not `content[]`); only the last non-`commentary` message of a turn is shared.
8
9
  - Approval titles are agent-written text shown to a person: the daemon escapes control characters, and write/edit/bash show what will be written or run.
9
10
  - An ACP permission request may carry no `rawInput` (Kimi 2.0.1): the arguments were on the earlier `tool_call` update, which `acp.ts` remembers by `toolCallId`. Kimi 2.1.1 sends no `rawInput` before the answer at all: the argument JSON streams as `content` text on `tool_call_update`, and only complete JSON counts. A payload that still cannot be resolved, or is too long to show whole (marked `[cut, N chars]`), withholds its `allow_always` option - never render a bare tool name as if it described the call.
10
11
  - Tag a Pi hub-tool refusal only at a proven pre-effect guard with `PreEffectToolRefusal`; return its exact error and settle its receipt. Never classify a generic/post-effect/storage failure or a legacy pending receipt from error text or current board state.
11
- - Report each Pi owner exit once, with code/signal (unknown for an unobserved TUI exit), turn/tool activity and only the last tool name. Auto-restart only a current, started, unexpectedly idle headless owner on its verified persisted session, once per 60 s; preserve shutdown/recovery/startup/active-tool fences and never fall back fresh.
12
+ - Report each Pi owner exit once, with code/signal (unknown for an unobserved TUI exit), turn/tool activity and only the last tool name. Auto-restart only a current, started, unexpectedly idle headless owner on its verified persisted session, once per 60 s; preserve shutdown/requested-stop/recovery/startup/active-tool fences and never fall back fresh.
12
13
  - Pi's `always` grant (#209) is keyed by `executeTool`'s tool name, never parsed from the approval title, and lives only in that Pi start's closure. Never persist it, share it with the local worker, or let the dashboard give it.
13
14
  - Auto-approval covers the hub's own tools by exact `mcp__agent-hub__<name>` title and only ever picks `allow_once`. Identity comes from the permission request title or, when an agent titles the request with the argument JSON (Qwen), from the announced `tool_call` title bound to the call id and resolved against the session's configured MCP servers, never from payload text, a prefix or an unrelated display title. Never widen it to a prefix or replace it with an agent-selected session mode. Set Kimi `yolo`/`auto` only for its verified native identity and an operator mode; config never-ask waits for a console y, and other ACP agents stay unmanaged on fresh/resumed starts without setting/resetting modes or failing over their reported mode; refuse runtime changes until their map exists; await the advertised mode change after session new/load and before a prompt, and fail startup on refusal. Use the shared `KIMI_MODE_IDS` mapping in `src/hub/permission-mode.ts`.
14
- - Permission modes (#242) are a person's and live in the daemon's memory only: `permission` and `permission_default` are console-role control messages, `ahub permission` is `console` in `classifyPeerCommand`, and no hub tool, conductor tool or dashboard action sets one. Never write a runtime choice to a restart snapshot.
15
+ - Permission modes (#242) are a person's and live in the daemon's memory only: `permission` and `permission_default` are console-role control messages, `ahub permission` is `console` in `classifyPeerCommand`, and no hub tool or conductor tool sets one. The dashboard and `ahub settings` set one only through `settingsSet` (`src/hub/daemon.ts`, #269), which calls `changePermission` inside `permissionChange` as the console does: never add a second path that writes `permissionModes`. Never write a runtime choice to a restart snapshot.
15
16
  - A peer whose mode lives in the native agent (Kimi, Codex) joins the bus only through `reconcileNativeStart` (`src/hub/daemon.ts`): after `start()` the daemon's current mode is applied, then `bus.add` (it can drain at once), then the state `start()` reached is replayed. Every mode change runs inside `permissionChange`, one per peer at a time.
16
- - Codex takes a mode as an `approvalPolicy` override on `turn/start`, and the override persists on the thread: pass the hub's and the TUI's `turn/start` alike through `applyPermissionPolicy`, and return to `ask` only by sending the captured native policy once. With none captured, refuse while the TUI is attached; never just stop sending the override. A detached proxy's `clearPermissionMode` is never refused and logs that the thread keeps its last policy.
17
+ - Codex takes a mode as an `approvalPolicy` override on `turn/start`, and the override persists on the thread: pass the hub's and the TUI's `turn/start` alike through `applyPermissionPolicy`, and return to `ask` only by sending the captured native policy once. With none captured, refuse while the TUI is attached; never just stop sending the override. Keep the captured native policy and the restoration debt per thread (`nativeApprovalPolicies`, `overriddenApprovals`) across detach and `thread/start` or `thread/resume`; clear a debt only on a successful response to its restoring `turn/start` while the mode is `ask` (`settlePermissionRestore`), so a rejected turn keeps it. While a thread owes a restoration with a known baseline, in any mode, never replace that baseline: a `thread/resume` response and the TUI's own `turn/start` can both echo the sticky override, and the restoring turn sends the kept baseline whatever the TUI supplied. That protection is one turn: after the restoration succeeded, a policy the TUI sends is taken as the person's choice, captured and passed through, because the proxy cannot tell a repeated echo from one. A detached proxy's `clearPermissionMode` is never refused; if the native policy is unknown, return a CLI-visible note that the thread keeps its last policy. Attached refusal directs the person to close the TUI, then run `ahub permission codex ask`.
17
18
  - An unanswered `session/set_mode` leaves the native mode unknown, not unchanged: `AcpPeer` invalidates the turn, goes offline, stops its process group and refuses further changes until restarted. Never report the old or the requested mode after a timeout.
18
19
  - A mode grant in `piPermit` or the local worker's `permit` (`src/hub/daemon.ts`) answers as the prompt path does: allowed only while that owner is still the attached one and the call was not aborted. `ask-when-needed` covers `PI_EDIT_TOOLS` on a `grantablePath` only (path rule: `local-worker.md`).
19
20
  - Every agent the hub spawns (the Codex app-server, ACP agents such as Kimi, Pi) runs in its own process group and is stopped as one (`stopOwnedProcess(proc, { group: true })`, `trackGroup` after spawn): `codex` is a node launcher, and mid-turn its native app-server does not exit on SIGTERM within the grace period, so a SIGKILL to the launcher alone left the app-server running under init and holding the daemon alive (#113, #115). A new adapter that spawns an agent does the same.
20
21
  - To stop a process tree, freeze it (SIGSTOP) before reading what it contains, then SIGKILL, and call it done only when the table shows none of it (with no table at all, the stop can only ask the group itself, which cannot see groups below it). A process a read after the freeze shows for the first time is frozen and read again before anything is killed. A snapshot taken while the tree runs misses what it starts next: three review rounds of #113 found such a gap (groups of its own, an unreadable first read, a child started during the grace period).
21
22
  - Work that must survive `ahub kill` (claude-mem `session-end`) is awaited in `stop()`; fire-and-forget dies with the process.
22
- - A peer must set `busy` synchronously inside `deliver()`, otherwise the bus drains the next envelope into a running turn (Kimi answers `turn.agent_busy`).
23
- - A watchdog-cancelled turn still reports later. Anything a turn does on completion must check it is still the current turn (`turn` generation in `acp.ts`, `activeTurns` in `codex-appserver.ts`). A rejected prompt or abnormal prompt end reports through the adapter's `onTurnFailure` (same shape in `acp.ts` and `pi.ts`), even when ACP streamed partial text; a stale cancelled turn never reports it.
23
+ - A peer must set `busy` synchronously inside `deliver()`, otherwise the bus drains the next envelope into a running turn (Kimi answers `turn.agent_busy`). Outside a turn the adapter accepts an occupancy diagnostic (a `context-used` `usage_update` naming its current session) and nothing else: no token total, no chunk, nothing unnamed or for an offline peer (#285).
24
+ - A watchdog-cancelled turn still reports later. Anything a turn does on completion must check it is still the current turn (`turn` generation in `acp.ts`, `activeTurns` in `codex-appserver.ts`). A rejected prompt other than a recognised quota refusal, or an abnormal prompt end, reports through the adapter's `onTurnFailure` (same shape in `acp.ts` and `pi.ts`), even when ACP streamed partial text; a stale cancelled turn never reports it; a rejected watchdog-cancelled ACP prompt during its journal-less wait is discarded with one log line, not requeued as on the earlier adapter.
24
25
  - Native owner teardown must handle a lost shutdown acknowledgement using verified process identity. Pending launches must be revocable, and active tools/streaming output must count as watchdog activity.
25
26
  - Pi owner identity is one shared contract, `processSignature` in `src/pi/process-signature.ts`; the startup claim check, the extension's claim, the TUI owner monitor, `recoveryReady` and the verified teardown all go through it. Its `ps` read pins LC_ALL=C and TZ=UTC (the `processTable` contract) because `lstart` follows the reader's timezone and locale, and a hub on UTC with a Pi child on system time otherwise hashes different strings for the same process and refuses the valid owner (#174). Strict pid + start + command identity stays; never accept PID-only or a fuzzy command match.
26
- - Return the shared `toolResultFailed` verdict as `/tool`'s `failed`; map only `failed === true` to Pi's `isError`. Preserve text and the older bridge's absent-flag behavior.
27
+ - Return the shared `toolResultFailed` verdict as `/tool`'s `failed`; map only `failed === true` to Pi's `isError`. Preserve text and the older bridge's absent-flag behavior. The one exception is a person's `!command` whose process exit was observed: its `failed` is `exitCode !== 0` and the reply carries `exitCode`, from `onProcessExit` through `packPiShellResult`, never parsed from the output (#253).
28
+ - Pi's bridge executes `/tool` only for the verified owner (`verifiedOwner()`: the claimed pid with its `processSignature`; a headless owner is pinned from the spawned process once `get_state` proved its session) with the current `sessionId` and turn `generation`, and admits `/budget` only for that owner and generation, checked again after the awaited admission. Refuse anything else before execution. `tool_abort` aborts that pending call, or fences its id when it arrives before the call (64 ids a turn, then every further call of that turn). A person's `!command` (`purpose: "idle_user_bash"`) is taken only in TUI mode from an idle owner on its one-use reservation, and it alone skips `piPermit`: never widen that to a model's bash call (#253).
29
+ - Pi and local approvals (#253): `permit` answers a `ToolApproval` (contract: `local-worker.md`) and `requestPermission` (`src/hub/daemon.ts`) tells the adapter's `ApprovalObserver` what settled it. An expiry counts toward the stop only as `source: "expired"` with `eligibleExpiry` not false (no person answered a request of that peer while it was pending); `person` resets the count, `automatic` (a mode grant, Pi's `always`) never does, and a new turn starts it over. The second one ends the turn, never the peer: local throws `ApprovalWaitStop`, Pi sends `abort_budget` with `cause: "approval"`, and both settle the delivery `completed` with the reason; a Pi whose extension did not announce `approvalTurnAbort` goes offline with the delivery held. `requestPermission` deletes a request from `permissions` before it runs an observer, resolves or reports, and an aborted signal closes it as `cancelled`: a late answer must find nothing to grant, and a failed event, notice or console push never strands the caller.
30
+ - `stop(reason?)` (`src/hub/peers.ts`): a reason marks a person's requested stop (`peer_stop`, rule: `daemon.md`). Fence before the first await (the adapter's stopping flag, or `offline`) so `deliver()` and buffered native callbacks do nothing more, hold the in-flight delivery as `needs_review` with that reason, and end `offline`. For ACP without a journal, return ordinary in-flight envelopes once through `onFailed` before a requested stop invalidates the turn; never return the watchdog-cancelled original prompt. A requested stop never completes, escalates or reports the turn it cut (`AcpPeer` bumps `turn`; `PiPeer.fail` skips the failure message and `onTurnFailure` while `requestedStopReason` is set). `PiPeer.stop()` waits for aborted tools only up to `stopGraceMs`: a tool past it never blocks the teardown, and the stop throws `PiToolSettlementFailure` once the owner is down.
31
+ - A terminal the hub opens for a person (`src/hub/terminal-open.ts`, #269) gets `personEnv`, which strips every agent marker; never `peerChildEnv`, which sets one. Adding a native peer with a TUI means adding it to `STARTABLE_PEERS` and, if it also has a headless form, to `START_MODE_PEERS` (`src/hub/start-mode.ts`), with its fixed words in `startWords`.
27
32
  - Spawn a native peer with `peerChildEnv(peer, source)` so its own `AGENTHUB_PEER_ID` replaces inherited agent markers. Sandbox tool environments carry their executing peer too. Preserve the native shell environment policy; Codex's `CODEX_THREAD_ID` supplies the fallback after filtering. Before changing marker detection or launcher environments, read `docs/verification/2026-10-09-agent-shell-t0.md` for pinned source and live-probe limits.
28
33
  - Emit Pi's ceiling event at the pre-effect rejection boundary (`src/pi/ceiling.ts`), before `agent_end`. Accept only a valid current-session/current-generation signal during a running turn; retain the first signal and attach it to that turn's failure. Never classify a ceiling from free text.
34
+
35
+ - After an ACP watchdog cancels a prompt on a bus without a journal, keep the peer busy with its inactivity watchdog cleared until that prompt settles; suppress its turn-scoped updates and permissions while still accepting session-bound occupancy diagnostics. Bound the wait by the existing 30 s ACP handshake deadline, go offline, never idle, with queued work retained if it expires, and revoke the wait on stop or transport loss; a durable delivery keeps its existing offline/needs_review path.
36
+ - ACP quota refusals use the pinned agent contract in `src/adapters/acp-quota.ts`, fenced by native identity and current turn generation. Report hard quota and require the hub's explicit pause confirmation before settling a pre-acceptance refusal as `failed_safe`; retain `needs_review` when work already started, and keep unknown refusals on the ordinary failure path. Sanitize agent refusal text before hub-written output.
@@ -8,5 +8,8 @@ Scope: the CooperBench runners, their fixtures, teardown and recovery. Read befo
8
8
  - A latched active-window terminal failure carries the #175 bounded diagnosis: the terminal class comes only from the pinned Qwen build's exact loop-protection message contract (anything else stays `unknown`), the active-window tool trace is count-only and phase/generation-fenced like the latch (setup probes and teardown never count), tool categories come from the protocol kinds and the #138 canonical binding (`canonicalMcpToolName` in `src/adapters/acp.ts`), and the native guard's threshold, predicate and no-op signals are declared unavailable, never derived from event totals. The diagnosis explains the latched failure and never changes it: no retry, no continuation, no guard relaxation.
9
9
  - Classify Pi's ceiling only from the adapter's validated producer signal bound to the latched peer/active generation. Keep shared budgets and Qwen's guard distinct; diagnostics never change the frozen end cause.
10
10
  - Read terminal coverage only through bounded `terminal_coverage` metadata reads, rebinding the audit, ledger and manifest to the seal index. Require complete sealed state, all audit checks, plan/cell/count reconciliation and zero recorded live owners. Report recorded verification separately from fresh audit; missing or inconsistent evidence is unavailable.
11
- - Publish Pi benchmark descriptors from `benchPiToolProblem`; keep production schemas unchanged and omit solo peer messaging. Attest every bootstrap tool publication against declared native/schema/source pins before scored generation. Fail/defer missing, mixed, malformed, truncated or changed publications; retain only bounded names/counts/hashes. Declare a new study condition before any changed collection; never rewrite historical artifacts or select retries by score.
11
+ - Publish Pi benchmark descriptors from `benchPiToolProblem`; keep production schemas unchanged and omit solo peer messaging. Attest every bootstrap tool publication against declared native/schema/source pins before scored generation. Fail/defer missing, mixed, malformed, truncated or changed publications; retain only bounded names/counts/hashes. Declare a new study condition before any changed collection; never rewrite historical artifacts or select retries by score. The driver's Qwen profile and Pi tool sandbox are `profile()` from `src/local/sandbox.ts`: a change to that profile (#270's refusal of the `.git` name is one) is such a changed condition.
12
12
  - The benchmark runner's Claude trust entry: a write still `pending` in the runner's own process never landed, so it is `not_written` on every path (contained or not, its temp file removed or left), and neither the runner nor the recovery touches an entry then. Only a dead runner's `pending` leaves that question to the recovery, which takes back only an entry exactly as the runner would have written it, and none while the runner's temp file is still there (the rename never happened). `changed_concurrently` is the user's entry and is never taken back. The runner's settlement is `settleTrust` in `scripts/benchmarks/teardown.ts`, tested as a table: change it there, never inline.
13
+
14
+ - Treat an earlier journal-less ACP attempt as possibly affected by cancellation overlap (#304) when its recorded peer `state` goes idle without an answer near `watchdogMs` after the last recorded activity, followed by rapid `failure` entries and `qwen_log` prompt-failed lines, then `active_end` as `peer-failure` with that reason. The driver's tap does not record `undeliverable`, main logged no watchdog cancel, and Qwen's busy-refusal wording is still unmeasured: these are candidate signs, not proof, and absence does not clear an attempt. Keep historical results unchanged and the native measurement owed in `docs/smoke.md`.
15
+ - After #304, an unreported cancelled prompt leaves a cancellation-deadline line in `qwen_log`, a recorded offline state about 30 s after the watchdog, and `active_end` as `native-failure` on the next driver tick. The driver starts each peer once and cannot restart it within the attempt; its in-memory queue ends with the process. The end cause of affected attempts is not comparable across this fix (formerly `peer-failure` or the wall limit); keep that distinction in later collections, per issue #304's Decision, without changing historical scores or results.
@@ -12,3 +12,7 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
12
12
  - Keep native context occupancy in `src/hub/context-window.ts` separate from quota accounting; never use Codex's accumulated `total` as occupancy or estimate Pi counters.
13
13
  - Context checkpoint completion requires its request id and the current attached peer/native session. Invalidate its transport generation synchronously at a new peer claim, before asynchronous recall or attachment. Never pause, reassign or replace a session on context pressure. Refuse private-turn, PII-pattern and all non-approved associated PII-task summaries (owner or reviewer, including in_review, with the current routing policy) before local persistence or cloud memory; never broadcast checkpoint text.
14
14
  - Invalid/stale context readings expose unknown and do not rearm a crossing. Latch a crossing only after its event is accepted; reconsider paused/recovery-held crossings after release with fresh current-session readings. Tee context fields contain finite numeric values or null only, bound to daemon instance, launcher and native session.
15
+
16
+ - A provider hard refusal ends a reading override and supersedes a soft evaluation still awaiting its checkpoint; never let the obsolete checkpoint overwrite that pause. An ACP provider quota refusal reports a hard budget reading and confirms the live pause before the adapter requeues work, so the pause fences retries and the existing relay handles assignments. Use the observed window length for an unknown reset; repeated quota refusals while paused do not consume poison-envelope retries.
17
+ - Kimi's ACP `{used, size}` occupancy is a context reading (`acpContext`, source `acp_usage_update`) bound to the adapter's own session id: never a `tokens` event or a budget window, and an invalid shape reports unknown, not the previous value. A gate crossing sends a checkpoint request to Claude and Codex only; an ACP peer gets the event and the notice (#285).
18
+ - A quota resume notice replaces the peer's unread resume notices: carry their moves into the new one (`refs.budgetResumeMoves`, or the whole text of a notice that has none), publish it, and only then withdraw the ones it replaces, so a failed publish keeps them (#306).
@@ -4,6 +4,7 @@
4
4
 
5
5
  Scope: delivery, digests and condensation, reply addressing, priority and limits. Read before editing `src/hub/bus.ts`, `src/hub/envelope.ts`, `src/hub/limits.ts`, `src/hub/delivery-journal.ts`, `src/hub/inference.ts` (digest condensation), or how an adapter addresses a reply or sets its priority.
6
6
 
7
+ - Lifecycle `deliveryHeld` fences both drain/pull and important steering before owner teardown awaits; retain newly queued work for an explicit restart without altering manual, budget or conductor pauses.
7
8
  - A failed digest is retried one envelope at a time, so a poison envelope cannot take its neighbours down with it.
8
9
  - An `important` envelope being steered is not in the queue while the steer is in flight; queue it first and an idle transition delivers it twice.
9
10
  - `replyParent()` decides what a reply answers (highest hop, never the `hub` preface). Use it for deliveries and steers alike, or the hop cap can be reset.
@@ -12,7 +13,7 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
12
13
  - Limits admit what is sent: the envelope `newEnvelope` builds (a reply inherits its parent's sender, `digest` resolves to the originals, `capPriority` applies), never the raw `to` and priority. Admit first and build later, and implicit replies all count as broadcasts.
13
14
  - A reply is addressed, never broadcast. An adapter that answers a delivery passes `to: replyAudience(envs)`; anything else with an `inReplyTo` inherits that envelope's sender. Leaving `to` empty fans the message out to every peer.
14
15
  - `digest` is not a peer: addressing a reply at the envelopes the peer was handed sends it nowhere once a delivery was condensed. The bus resolves `digest` back through `lastDelivery.originals`; anything else that reads a reply's `to` has to do the same.
15
- - A pull-only peer (a Claude session without channel pushes, #205) is never drained; `Bus.pull` hands it what one push would take. Drain and pull share `held()` (recovery, an uncertain delivery, any pause) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
16
+ - A pull-only peer (a Claude session without channel pushes, #205) is never drained; `Bus.pull` hands it what one push would take. Drain and pull share `held()` (recovery, an uncertain delivery, any pause, the lifecycle `deliveryHeld` fence) and `remember()` (so `reply_to` resolves), and a drain rechecks `deliverable()` after condensing because such a session can take the peer meanwhile. A pull is journaled `completed` in the same transaction as the bus snapshot, never `accepted`; a failed one puts back only that peer's queue and preface and never reloads the snapshot.
16
17
  - `resolveDelivery` mutates queues in place; a refused or failed resolution puts back only that peer's queue and preface and keeps every pause. Never reload a snapshot on a live bus.
17
18
  - `restore` takes queued work from the journal, never from the committed snapshot once the journal has state (an empty journal, revision 0, is seeded from the commit): from the commit only an empty queue key is carried over, for an offline peer whose key no delivery ever persisted (#234). Never resurrect a non-empty queue from it.
18
19
  - The journal's owner check (`rejectLiveOwner` in `delivery-journal.ts`) judges the manifest pid with `processLiveness` and its `pidSignature`, never a bare probe (rule: `daemon.md`).
@@ -1,8 +1,11 @@
1
1
  # Daemon, control protocol and recovery
2
2
 
3
- Scope: the control WS and its protocol, state files, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/hub/daemon.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, `src/hub/crash.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts` or `src/cli/facts-hook.ts`, or before adding a native peer.
3
+ Scope: the control WS and its protocol, state files, the dashboard listener and settings, the Claude channel's reconnect, `ahub setup`, git snapshots, upgrade and recovery. Read before editing `src/hub/daemon.ts`, `src/hub/ui.ts`, `src/hub/settings.ts`, `src/hub/control-client.ts`, `src/adapters/claude-channel.ts`, `src/hub/restart.ts`, `src/hub/recovery-store.ts`, `src/hub/snapshots.ts`, `src/hub/manager.ts`, `src/hub/lifecycle.ts`, `src/hub/crash.ts`, `src/hub/start-mode.ts`, `src/hub/terminal-open.ts`, `src/hub/recovery-error.ts`, `src/hub/db-timeout.ts`, `src/cli/setup.ts`, `src/cli/upgrade*.ts`, `src/cli/terminal-recovery.ts`, `src/cli/recovery-package.ts` or `src/cli/facts-hook.ts`, or before adding a native peer.
4
4
 
5
5
  - The plugin bundle is installed apart from the daemon. Breaking a control WS message contract bumps `PROTOCOL` in `control-client.ts`; a new request answered as unknown or an optional field preserves it, as does additive optional public task metadata in `ui_snapshot` that older clients ignore (such as `deps` in #246).
6
+ - `peer_stop` is console-only and stops only a hub-owned headless ACP, Pi or local runtime through its adapter. Refuse native-terminal peers and recovery-held mutations; serialize with starts and permission changes, and refuse a recovery `prepare` while a stop runs. An admitted stop holds delivery through `BusOptions.deliveryHeld` before any teardown await (never a console pause that resume can undo, or a global recovery hold that blocks unrelated peers) and withdraws the peer's pending approvals before the adapter stops. A requested stop holds an in-flight delivery as `needs_review` with an ids-only stop reason, suppresses restart/escalation, and never resolves that delivery automatically.
7
+ - Settings (#269): the dashboard's `setting`, `setting_undo` and `setting_preview` actions and the console-role `settings_get|set|undo|preview` requests go through `settingsSet`, `settingsUndo` and `settingsPreview` (`src/hub/daemon.ts`) and the registry in `src/hub/settings.ts`. A new editable setting is a registry entry, never a new write path; a key that chooses what the hub runs or where secrets or task text go joins `NEVER_EDITABLE` instead. Writes go only to `.agenthub/config.local.json` and `.agenthub/routing.local.toml` through `store()`: the loader's own validation first, temp file and rename, never a file git tracks. Authority comes from the dashboard session (`DashboardSession`, `src/hub/ui.ts`) or the console role, never from the request body; `ui_action` from the manager is an ordinary session. Never return a setting's value in an event or notice except through `recordSetting`.
8
+ - Start modes (#269): every start the hub makes itself goes through `hubStart` (`src/hub/daemon.ts`): the dashboard's `start_peer`, the console-role `peer_start`, the conductor's `start` hook and `pi.auto_start`. It takes the mode from `startMode(peer)` and nothing else; never add a caller-chosen mode, flag or command to it, and never fall back to headless when a terminal cannot be opened (refuse with the manual command). The words a terminal runs are assembled there from `process.execPath`, the CLI entry, the project root and `startWords(peer)`. Recovery and `startPeer` callers that pass a recorded mode keep it. The real terminal providers are wired only in `src/hub/lifecycle.ts` (`DaemonOptions.terminal`): a daemon built without them, as in every test, opens nothing; a test injects a recording provider and never the real one.
6
9
  - `ahub setup` reads Claude Code's state from `--json` listings and takes one step at a time, re-reading after each; never match paths or names by substring.
7
10
  - A copy of the git index keeps the original's mtime (`snapshot()`): git's racy-entry check compares entries with the index file's time in whole seconds, and a fresh copy makes a same-size edit look clean.
8
11
  - State files that clients read (`status.json`, `control-token`) are written after the port is bound, and `status.json` via temp file + rename.
@@ -17,5 +20,13 @@ Scope: the control WS and its protocol, state files, the Claude channel's reconn
17
20
  - A target that stopped after it started is restarted only through `recordRestart` (`src/cli/upgrade.ts`), recorded before the new one starts and at most once per project per resume: each done `restored:<peer>` becomes `retired:<peer>`, whose terminal is closed (`closedRetired:<peer>`) or found absent before that peer is relaunched, and is never revalidated against the new instance. Launcher evidence is read for every instance in `restarts`, and a session attached without an id is unknown, never gone (#225).
18
21
  - An operation receipt stays `schema: 1`. Only `abandoned()` writes 2, on an operation with a recorded `disposition` whose coordinator predates #215: in the same write as the disposition, or, for a disposition 0.12.20 recorded at 1, by the next command that holds the runner claim. `validateReceipt` accepts 2 only with a disposition (#227). Never write 2 on an operation that can still be resumed.
19
22
  - Three checks read another release's source text for an exact line: `coordinatorCurrent` (`export async function disposeRecovery` in `src/cli/upgrade.ts`), `targetReadsWaivers` (`export function readRecoveryWaivers` in `src/hub/restart.ts`) and `coordinatorRestarts` (`export function recordRestart` in `src/cli/upgrade.ts`). Renaming, moving or un-exporting one makes this release read as an older one: keep those lines until a capability list replaces the checks.
23
+ - The interactive screens (`src/cli/upgrade-interactive.ts`, #272) change an operation only by running an existing command (`recovery resume|abort|dispose`, `reset`) through `io.run`, and build their menu from `nextChoices`, whose text is `nextActions`: never call `abortRecovery`, `disposeRecovery` or a driver from a screen, and never offer a choice `next` does not have. The one addition is `cancellableWait` (a runner still at `prepare` with no effect): read the receipt again at the key press, stop only a runner `signedRunner` names (a claim without a matching process signature is never signalled), then run `recovery abort`, which decides again. Agents are ended (`endPlannedPeer`) only before an operation holds the lock, and only after the lock, the hub instance, the attached session and the terminal's handle and incarnation were read again: never on the plan as it was drawn. Ctrl+C leaves the screens and the terminal is not read after it; a prompt answers an interrupt with null, never text. What a hub says it waits for (`recovery.waiting`, the daemon's `blockers`) is shown, never decided on.
24
+ - `upgrade()` (`src/cli/main.ts`, #272) never coordinates a target newer than its own `VERSION`: it hands over to that release's coordinator (`bun x --package @staix/agent-hub@<to> ahub upgrade --to <to>` with the same flags), and `AGENTHUB_UPGRADE_HANDOVER=1` keeps the process it started from handing over again. `--to` passes `exactVersion` before it reaches a package spec or the registry. `latestRelease` supplies it only for a terminal run or a dry run: `--yes`, and an apply through a pipe, still name the release.
20
25
  - Recovery judges the registry claim as it is now (`inspectRecovery` re-reads `registeredProjects()`), never the plan's frozen copy; a registry that cannot be read is not inspected, never stopped (#226).
21
26
  - The managed Claude hook (`src/cli/facts-hook.ts`) may only add `permissionDecision: "allow"` on `PreToolUse`, and for `ask-when-needed` only what `projectFileTool` accepts: a hub that is down, slow or refuses decides nothing. The daemon puts `permission` in a `facts` reply only for the hook of this instance's recorded `ahub claude` launch (the same `launchId`, not `--unattended`), and a permission-only hook (`hookPurpose`) never drives busy state (#242).
27
+ - `ahub claude` (`src/cli/launch.ts`, #270) publishes `claude-launch.json` for every launch against a running hub, atomically and without a lock: hooks, permission status, session observation and recovery read its `launchId`, so never make it wait for native-proof state. The native child's pid and signature go only into the private `claude-native-<binding hash>.json`, keyed by the exact launch record, written once under `claude-launch.lock` and removed at that child's own exit; never put them in `claude-launch.json`, where a late callback of an earlier launch could replace a newer record. Crash cleanup of a previous launch's settings file (`cleanupStaleClaudeSettings`) needs the launcher gone by `processLiveness` and that native proof, and for a `direct-child` proof the native pid absent from an available `processTable`: a direct child may exec in place, so its signature certifies nothing. A script or interpreter gets no proof and keeps its settings file.
28
+
29
+ - Keep SQLite recovery contention in the existing blocked receipt as retryable `database_busy`, retaining bounded throw-site frames and the ordinary `ok: false` rid reply. Use a neutral control error; the receipt's next actions name resume or the partway disposition. Clear a superseded failure on abort, disposal, fresh-session choice or a refused resume of a disposition. After a commit request, treat busy inspection as unconfirmed and keep polling only inside the existing shutdown bound.
30
+ - Recovery inspection uses its existing 30 s control cap for the first status read and consumes a structured busy reply without a second status probe; ordinary lifecycle probes keep their default. Treat `busy_timeout` as a ceiling for eligible waits, not a guarantee that every write waits: a WAL snapshot conflict may fail immediately. Account for synchronous 3 s hub-database waits and serial timer callbacks when choosing a timeout; several waits may consume the 30 s control margin. Never multiply the busy wait through retries or infer stopped from a shorter probe's timeout.
31
+ - Every `new Database(` under `src/` is followed at once by its own `PRAGMA busy_timeout` from a named `*_BUSY_TIMEOUT_MS` constant (`HUB_DB_BUSY_TIMEOUT_MS` in `src/hub/db-timeout.ts` for `hub.db`), and nothing sets `busy_timeout` again later: `test/db-busy-timeout.test.ts` reads the source and fails on any other opening idiom, so a new one extends that guard first (#303).
32
+ - The manager forwards a dashboard `start_peer` with the terminal provider cap (`MAX_COMMAND_MS`) plus 5 s and reports a missed deadline as `unconfirmed`, never as a failure; it recognises the deadline by the exact text `ControlClient.request` resolves with, so change the two together. The hub's terminal reopen guard and the dashboard's retry hold share `PEER_START_RETRY_MS` (`src/hub/start-mode.ts`): never give either its own number (#298).
@@ -1,14 +1,17 @@
1
1
  # Local worker and sandbox
2
2
 
3
- Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt sandbox. Read before editing `src/adapters/local-worker.ts`, `src/local/`, `src/memory/capture.ts` or `src/hub/facts.ts` (both read the denylist), or the grant filter in `src/hub/permission-mode.ts`.
3
+ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt sandbox. Read before editing `src/adapters/local-worker.ts`, `src/local/`, `src/memory/capture.ts` or `src/hub/facts.ts` (both read the denylist), `src/hub/git.ts`, or the grant filter in `src/hub/permission-mode.ts`.
4
4
 
5
5
  - Nothing the local worker executes may run outside `sandboxedExec`; a new tool that spawns a process goes through it, and a tool that touches a path goes through `guardPath`.
6
6
  - The sandbox denies home reads by default (toolchain dirs, the project and its real git dir excepted) and all network, loopback included: claude-mem and the Codex app-server listen on loopback without auth. Tests that bind a local port therefore fail when the worker runs them, also with `local.bash_network: true` (the egress proxy keeps loopback closed, and the proxy variables send a test's plain-HTTP `fetch` to the proxy, which refuses it); that is the intended trade-off, and `"direct"` is the switch until 0.13.0 removes it.
7
+ - The seatbelt profile refuses the `.git` name itself at any depth (creation, rename, symlink, gitfile), `config` and `hooks` directly under it, `commondir` at any depth below it or below an external git dir, and `.agenthub` anywhere in the project, every name folded against the 16 code points HFS+ ignores (#270). A submodule's git dir under the common dir's `modules/` tree gets the same `config`/`hooks` refusal at the depth submodules nest, every term anchored below the discovered modules root so ancestor directories named `refs`/`config`/`hooks` neither disable nor trip the rule, and a `/refs/` or `/logs/` segment below it excludes the path so refs named `config` or `hooks` stay writable (#281); the discovered git dirs, the modules root and the components in between are also refused as directory-entry targets (rename, replace, symlink, recreate). Known residuals: a submodule whose own path holds a whole `refs` or `logs` segment (a substring, `catalogs` included, keeps the refusal — the exclusion matches whole components only), a nested repository in bare layout (the hub never runs git there; docs/security.md), folded spellings of the components between `.git` and a submodule's git dir on a volume that folds names (the 1024-byte SBPL literal cannot hold a folded path; external git dirs carry the same residual since #270), and the submodules of a repository nested inside the project (only the project's own common dir is discovered). An external git dir's `config`/`hooks` rules cover only its own children plus the hooks directory's contents: at any depth they would deny refs named `config` or `hooks` in a worktree or submodule project. The fold is a group alternation, never a character class (the seatbelt regex engine is byte-oriented and a quantifier after a multi-byte class member binds to its last byte), and every pattern keeps its `^` anchor (an unanchored starred alternation does not match). Folded literals never contain the project root: an SBPL string literal dies at 1024 bytes. The cost: `git init`, `git clone` and `git worktree add` do not run inside the sandbox, and neither do `git worktree remove`, `git worktree prune` of a stale record, `git clean -ffd` with a nested repository, `git submodule update --init` (its git dir's `config` write is refused), or `rm -rf` of a directory holding a `.git`.
8
+ - Every git call the hub makes outside the sandbox goes through `src/hub/git.ts` (`hubGitArgv`/`hubGitEnv`/`hubGitSync`), which keeps the repository's configuration from naming a program the hub runs (#281); a test walks all of `src/` (src/local/ included) for a direct `spawn("git"` and fails on one outside `src/hub/git.ts` and the sandboxed tool's `src/local/tools.ts`. The sandboxed `git` tool keeps its own behavior: it runs inside the sandbox. The submodule `config`/`hooks` refusal costs a submodule whose path has `hooks` as a whole component all git-dir writes (docs/security.md).
7
9
  - SBPL strings go through `sbplString` (the plain `"..."` form). In the raw `#"..."` form a backslash escapes nothing, so a `"` in a path ends the literal and the whole profile fails to parse.
8
10
  - One denylist, `src/local/deny.ts`: the path guard, the seatbelt profile and the memory filter all read it. Seatbelt sees absolute paths, so `local.deny` entries are anchored under the project root (a bare `private/` once denied all of `/private/var`).
9
11
  - `guardPath` walks with `lstat`: `existsSync` follows symlinks, so a dangling link looked like a new file and the write landed at its target.
10
12
  - git arguments never pass through `guardPath`; `gitArgsProblem` refuses absolute paths, `..`, `--no-index` and denylisted `rev:path` forms.
11
13
  - A worker turn builds its messages in a local array and joins the history only as a whole. Never push to `history` mid-turn: one tool call without its result poisons every later request.
12
14
  - After a tool with side effects ran, a failed turn is reported, never redelivered.
15
+ - `ctx.permit` answers a `ToolApproval` (`true`, `false`, `"expired"`, `"aborted"`, #253): only `true` permits an effect, so never test it for truthiness. A tool returns `approvalError`'s text for the rest: a denial, the expiry (which tells the model not to retry) or the withdrawal. A turn that two unanswered approvals ended (`ApprovalWaitStop`; counting rule: `adapters.md`) completes its missing tool results, settles its delivery `completed` with the reason and is never handed back for replay, also when no tool had a side effect.
13
16
  - Hub tool calls (`hub_send`, `TASK_TOOL_NAMES`, `CONDUCTOR_TOOL_NAMES`) are never passed to `capture.observe`; `Tasks` alone decides what hub tool text reaches memory (#230).
14
17
  - A tool that asks for approval passes its name to `ctx.permit`, and `write`/`edit` also the `guardPath` result, never the model's path string: `ask-when-needed` grants on that real path through `grantablePath` (`src/hub/permission-mode.ts`). `bash` and mutating `git` pass no path. After the awaited approval `write`/`edit` run `guardPath` again and refuse a target that changed (#242).
@@ -2,9 +2,10 @@
2
2
 
3
3
  Scope: the task flow, assignment, the state machine and the hub's MCP tools. Read before editing `src/hub/tasks.ts`, `src/hub/board.ts`, `src/hub/routing.ts`, `src/hub/task-sweep.ts`, `src/hub/conductor.ts`, `src/hub/supervision.ts`, `src/hub/tool-refusal.ts` or `src/hub/hub-tools.ts`.
4
4
 
5
- - Tool callers are models: MCP `inputSchema` is not enforced on the way in. Normalize at the boundary (`cleanRefs`) before anything reaches the board, and never throw after a board write.
5
+ - Tool callers are models: MCP `inputSchema` is not enforced on the way in. Normalize at the boundary (`cleanRefs`) before anything reaches the board, and never throw after a board write: a publish or notify that follows a `review()` or `done()` board write goes through the one `releaseNotice` guard, which turns a failed publish into a console notice, drops a failed notify and never retries (#231, #243). An assignment's delivery once its board write is saved (#276, #297): the move is fact, the failed publish is not retried in place (the journal's own recovery covers a storage error, and a retry may duplicate a publish that actually left), and a person looks for a queued or needs_review delivery with `ahub queue list --peer <owner>` (then `ahub queue show <id>`, or the accepted entry in `ahub task show <id>` if the journal is down) before re-sending with `ahub task assign <id> <owner>`. The distinction is made once, in `assignOwner`: a failure after the write prints the one console notice from `undeliveredNotice` right there, once, and throws `AssignmentUndeliveredError` carrying the saved task; callers (`review()`'s repeated rejection, `escalate` and its callers, the ready offer, both relays, the idle sweep, rescreen) decide control flow by the type and print nothing more, and `propose`, `assignTo` and `decline` additionally let the error reach the caller in its turn, which is a person or peer that can act. Two exceptions get no notice, by construction: a row superseded meanwhile, meaning a later history entry that moves or settles the task (another assignment, an unassign, the owner's accept, done, approval, close), while entries that do neither (the hub's quiet bookkeeping: answers, reviewer changes, idle sweeps, check results) leave the assignment undelivered and the notice stands, so the original error goes to the caller as before only on supersession; and a claim's own accepted write and "claimed by" line stay outside the delivery rule (a claim is its own hand-over; its overlap and routing announcements to earlier owners are deliveries and share the wrapper, so a failure there does read as one). A failure before the write stays a plain `Error` (#254), a failed write leaves the ready offer unconsumed (#231), and the notices before the write (waits, no-owner, the reserved-owner line) go through `releaseNotice` too, so those pre-write notices cannot fail their caller.
6
6
  - `assign()` never defaults to the task's current owner, or a decline can only come back to the decliner. A reservation (#207) is the same risk: the reserved owner goes first only through `blocked()`, and a decline or an escalation for reason `rejections` or `manual` records the owner it left in the history entry's `from`, which `excluded()` reads, so no later reroute hands the task back through its reservation.
7
7
  - The state machine allows `in_progress -> approved` only for classes without a reviewer; `review()` checks `in_review` itself.
8
+ - `.agenthub/routing.local.toml` (#269) may hold only `stay_switch` and per-class `peers`, `escalate_to`, `route` and `pi_backend` (`checkOverlay` in `src/hub/routing.ts`): never widen it to targets, routes, signals or constraints, which stay in the hand-written `routing.toml`. `parseRouting` is the one merge, and `assign()` reads `routing.sources` only for its trace line.
8
9
  - Conductor authority requires the explicit unique role before capability checks. The only exceptions run before that check in `Conductor.execute` and stay task-scoped: a task's current owner and reviewer read it with `hub_task_show` (#208), and its proposer redirects it with `hub_task_assign` while nobody ever accepted it and no person moved it (#207). Removing them strands reviewers that missed a review request. Assignments preserve the acting peer. A peer releases only its own conductor hold; operator release and manual, quota and recovery holds remain separate paths.
9
10
  - The PII screen (#198) runs in `propose` before anything else reads the task (no board row, envelope, triage or brief before its verdict), and `unknown` is PII. A call that screens free text (`done`, `review`, `remember`) screens only after its own checks pass, and `done` and `review` run them again after the awaited screen, because the task can change meanwhile. A history entry marked `withheld` stays withheld in public views, completed notices and review requests, whatever `nameable()` says; `ahub ask` shows it only on campus, like a note that matches a PII pattern.
10
11
  - `taskOp` (`src/hub/daemon.ts`) lists full PII rows only when `onPrem` holds: the in-process local worker inside a PII turn (#230). Every other caller, the local worker's ordinary turns included, gets stubs.
@@ -7,4 +7,8 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
7
7
  - The Switchyard sidecar, sandboxed commands and the MLX server get a fixed environment (`PATH`, `HOME` and their own keys), so test knobs for their fakes travel in a wrapper script or argv, not in `process.env`; native peer children inherit the hub's environment minus the agent markers (`peerChildEnv`).
8
8
  - Bind agent identity markers explicitly in native/CLI fixture children. The full gate starts the test runner without inherited agent markers and without an `ahub claude` session's hub variables (`AGENTHUB_PROJECT_DIR`, `AGENTHUB_STATE_DIR`, `AGENTHUB_INSTANCE_ID`, `AGENTHUB_LAUNCH_ID`); a new variable a native session exports and a fixture child reads joins that `env -u` list in `scripts/check.sh`. Never weaken production marker detection to make them pass.
9
9
  - A free-port probe binds `hostname: "127.0.0.1"`, matching the loopback server that takes the port (#232).
10
+ - `ahub upgrade` without `--to` asks the registry (`npm view`) and hands a target newer than the CLI to `bun x` (#272): a test that reaches either puts a fake `npm` first on `PATH` and a recording stand-in for `process.execPath`, as `test/upgrade-cli.test.ts` does. The interactive screens are tested through a scripted `ScreenIO`; a CLI test that needs them marks `process.stdin` and `process.stdout` as terminals in a wrapper script.
11
+ - Cross-process fixtures wait for the consuming client to process its handshake (the channel connected diagnostic), not merely the daemon's attached/idle state. Deadline-routing fixtures hold and release responses at the fallback decision; do not place a fixed sleep between deadline fractions and assume scheduler headroom (#287). Receipt-order fixtures use the normal watchdog for ordinary I/O, trigger expiry after observed native acceptance, and await a consumed transport barrier before asserting on late results.
12
+ - A bounded polling wait must assert its condition after the last attempt, or document why giving up is intentional. Append-only event histories wait for a correlated closing event, not removal of the opening event. Before changing a slow wait, identify the unreached state and repair its cause (#296).
13
+ - A request that a test expects a stop or shutdown to cut gets its rejection handler where the request is created, never after the awaited teardown: a late `catch` races the reset and the run fails on an unhandled rejection.
10
14
  - Run `bun scripts/seeded-check.ts` before reporting the seeded-guard gate complete. Keep the six `scripts/seeds.json` cases sequential; require the same named test green before its seeded assertion fails. Seed rot, survivors, setup/compiler failures, timeout/watchdog termination and process leaks fail the gate. Never run the full seed runner from a unit test or mutate the source checkout; inspect preserved failed fixtures before removing them. CI runs this in the required Linux `seeded guards` job after the ordinary check, separately from `scripts/check.sh`.
package/docs/events.md CHANGED
@@ -25,7 +25,10 @@ marked `private: true`, and PII tasks `pii: true`.
25
25
  | `progress` | `peer`, `task` (task id), `severity`, `spinning`, `exploring`, `production`: normalized tool activity dimensions for an open task; coverage differs by peer and no commands or task text are recorded |
26
26
  | `stuck` | `peer`, `task` (task id), `category` (`repetition`, `false_progress`, `drift`, `desperation`, `capability_gap`), `streak`, `latched`: an escalation verdict; it recommends considering reassignment and never reassigns automatically |
27
27
  | `capability` | `peer`, `state` (`verified` or `lost`), `via`: a peer's context path for facts |
28
- | `native_turn_end` | `peer`: Claude's Stop hook, the end of its turn (Codex's is its `turn_end`); quiescence evidence for an integration |
28
+ | `native_turn_end` | `peer`, `id` (an opaque hash that names the completed turn): the end of a Claude turn, recorded once the hub finds the turn its Stop hook reported completed in the native transcript (Codex's is its `turn_end`); quiescence evidence for an integration |
29
+ | `supervision_turn` | `peer`, `turn` (the hub turn a supervision notice was delivered into, or `supervision-<uuid>` when none was open), `tokens` (when the adapter reported any for it), `ms` (from that turn's start, or from the delivery when none was open, to its native end): a completed native turn that received supervision, which `ahub report` counts; a failed delivery or turn, a peer that went offline or a changed native session records none |
30
+ | `conduct` | `peer` (the conductor, or `user` when the console releases a conductor hold), `action`, `task` (when the action names or creates one), `target` (the peer an assignment, start, hold or release names): one action of the conductor that ran, the structured twin of the console's `conductor` notice. `action` is a tool name without its `hub_` prefix: a conductor tool (`status`, `task_show`, `task_assign`, `task_escalate`, `peer_start`, `peer_hold`, `peer_release`), or one of the conductor peer's own task tools, which are not conductor tools (`task_propose`, `task_accept`, `task_decline`, `task_done`, `review`, `checkpoint`, `remember`). The role-free reads and redirects (a task's owner or reviewer with `hub_task_show`, its proposer with `hub_task_assign`) record none, and neither does `hub_task_list` |
31
+ | `agent_cli` | `peer` (`unknown` when the agent markers were invalid), `command` (the command and, where it has one, its subcommand, from a closed list; `unknown` otherwise; never an argument), `refused` (the CLI refused it before connecting: a human-only command, or an invalid identity; a later refusal by the hub is not recorded here): an `ahub` command run from an agent session. The hub reads these from the `cli-audit/` outbox, so `at` is when it read the record |
29
32
  | `hook_stats` | `peer`, `n` (facts hook calls in the turn, its Stop included), `startupMs` and `maxStartupMs` (the hook processes' start-up and connect time, summed and the largest), `hubMs` (the hub's own time for them): at Claude's Stop (issue #108) |
30
33
  | `cohort` | `id`, `event` (`formed`, `joined`, `lifted`), `silent`, `tasks`, `owners`: owners of overlapping tasks formed a cohort, it changed membership, or it stopped being silent (issue #107). Recorded in every regime; only a turn-free project's cohorts can be silent |
31
34
  | `split` | `task`, `where` (`routing`: routing chose the first owner of a task overlapping another owner's task not started yet, not an escalation, relay or reassignment, the record calibration reads; `cohort`: an overlap formed or changed a cohort), `verdict` (`split`, `single`, `unknown`), `single` (the peer that would finish both units alone soonest), `splitS`, `singleS`, `reason` (for `unknown`), `trace` (the inputs and steps: peer names, their profiles of versions and coordination, and numbers only): a shadow split prediction; it never changes the assignment (issue #109) |
@@ -34,21 +37,31 @@ marked `private: true`, and PII tasks `pii: true`.
34
37
  | `turn_start` | `peer`, `turn` (`<peer>#<hub run>.<n>`, unique across restarts). A turn follows the adapter: pausing a busy peer does not end it |
35
38
  | `turn_end` | `peer`, `turn`, `ms`, `tokens` (when the adapter reported any during the turn), `files` and `snapshotMs` (when snapshots are on: how many files the turn changed, and the time both snapshots took); optional numeric `task`, `attribution` (`delivery`, `single_open`, `unattributed`) frozen at turn start, `pii: true` for an attributed PII task |
36
39
  | `tokens` | `peer`, `n` (tokens added since the previous report), optional numeric `task`, `attribution` (`delivery`, `single_open`, `unattributed`), `pii: true` for an attributed PII task |
37
- | `usage` | `peer`, `source`, opaque `id`, optional `measuredAt` (provider/source time), requested/served model and provider labels, and any provider-reported input/output/cache/total counters; optional numeric `task`, `attribution` (`delivery`, `single_open`, `unattributed`), `pii: true` for an attributed PII task. Missing counters stay unknown. |
40
+ | `usage` | `peer`, `source` (`omniroute`, `claude_transcript`), opaque `id`, optional `measuredAt` (provider/source time), the `requestedModel`, `servedModel` and `provider` labels, and any provider-reported counters (`inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `totalTokens`); optional numeric `task`, `attribution` (`delivery`, `single_open`, `unattributed`), `pii: true` for an attributed PII task. Missing counters stay unknown. |
38
41
  | `task` | `id`, `event` (the board history event, e.g. `proposed`, `assigned`, `done`, `check failed`, `blocked`, `ready`), `by`, `state`, `owner`, `reviewer`, `class`, `pii`, and `reason` when the board recorded a move reason (`manual`, `budget`, `offline`, `idle`, `rejections`, `declined`, `inference_failed`, `delivery_failed`) |
39
42
  | `overlap` | `task`, `owner`, `others` (`task`, `owner`, `paths`, and `symbols` when plans name the same symbol; a name that matches a PII pattern is left out, so either list can be empty), the structured twin of the console notice |
40
43
  | `permission_mode` | `peer`, `from`, `to` (`ask`, `ask-when-needed`, `never-ask`): a person set a peer's permission mode (`from` equals `to` when it was already in force) or confirmed a never-ask default (issue #242) |
44
+ | `settings` | `key` (a registry key such as `permission.kimi` or `routing.classes.implement.peers`), `from`, `to` (the value before and after: the one in force for a setting that applies at once, the stored one for a setting read at a start; a closed word, a boolean, a list of peer ids, a route id, or null for none), `source` (`dashboard`, `terminal`), and `undo: true` for an undo: a person changed a setting from the dashboard or `ahub settings` (issue #269). A mode change also writes its `permission_mode` event |
45
+ | `permission` | `id`, `peer`, `event` (`requested`, then one of `answered`, `expired`, `cancelled`) and, on the closing event, `latencyMs`, `surface` (`console`, `dashboard`, `terminal`: where a person answered; absent when the hub closed the request) and `option` (the kind of the option chosen: `allow_once`, `allow_always`, `reject_once`, `reject_always`): one approval request relayed to the console and how it ended. `expired` is `approvals.timeout_s` without an answer. `cancelled` with a `surface` is a person's deny in the console or by `ahub permit <id> deny` (a deny on the dashboard is `answered` with a reject option); without one, the hub withdrew the request (its call or turn ended, the peer was stopped, or the hub is stopping). Never the title or the tool |
41
46
  | `pii_screen` | `task` (absent for a budget hand-off or a note not about a task), `item` (`task`, `summary`, `review note`, `handoff`, `note`), `label` (`pii`, `clear`), `source` (`regex`: a pattern matched and no call was made; `screen`; `unknown`: no verdict, handled as PII), `category` (`name`, `student_id`, `phone`, `address`, `grade`, `health`, `other`), `miss` (why there was no verdict: `too long`, `off campus`, `timeout`, `unreadable`, `failed`), `ms` (absent for `regex`): one PII screen verdict while `signals.pii_screen = "local"` (issue #198); never the text |
47
+ | `context_pressure` | `peer`, `source` (`claude_statusline`, `codex_token_usage`, `acp_usage_update`), `measuredAt` (epoch ms of the reading), `used` (the fraction of the context window in use, 0 to 1), `window` (its size in tokens, or null): a fresh reading of a peer's native context reached `context.gate`. Recorded once per crossing, until a reading below the gate or a new native session rearms it, and not while the peer is paused or offline or the hub is in recovery or stopping |
42
48
  | `quota` | `peer`, `windows` (`id`, `used`, `resetsAt`), `hard`, `measuredAt` (when the reading was taken, if not when it arrived: Claude's numbers come through a file) |
43
- | `conflict` | `peer`, `task` (its task in progress, when it had one), `other` (the other owner's open task), `owner`, `paths` (names that match a PII pattern left out, so it can be empty), `concurrent` (another peer worked during the turn) |
49
+ | `conflict` | `peer`, `task` (its task in progress, when it had one), `other` (the other owner's open task), `owner`, `paths` (names that match a PII pattern left out, so it can be empty), `concurrent` (another peer worked during the turn), `turns` (the two turn ids, when two overlapping turns' snapshots both hold the change) |
44
50
 
45
51
  Token usage by adapter:
46
52
 
47
- - Kimi (ACP `usage_update`): recorded.
53
+ - Kimi (ACP `usage_update`, and the usage a `session/prompt` result carries): recorded when it holds a checked
54
+ total or an input/output pair. The `{used, size}` shape Kimi sends is context occupancy: it is shown as the peer's
55
+ native context reading (source `acp_usage_update`, #285 phase 1) and no `tokens` event is written from it; Kimi's
56
+ consumption is not recorded yet (phase 2). The adapter accepts the update whenever it names the current session;
57
+ Kimi 2.1.1's source text emits it after the prompt resolves, when the peer may already be idle, and that order is
58
+ not yet observed live through the hub (docs/smoke.md).
48
59
  - Codex (app-server `thread/tokenUsage/updated`): recorded as the growth of the thread's running total, so
49
60
  compaction estimates, usage-limit refreshes and the replay to a reattaching connection add nothing. A thread
50
61
  started under the hub counts from zero; a resumed thread's first update is its history and only sets the
51
62
  baseline. The model call of a compaction itself is real usage and counts.
63
+ - Pi: the assistant usage its extension forwards at each `message_end`, tool-loop messages included, is recorded as
64
+ increments, each usage id once; a message without a usable count adds nothing.
52
65
  - Claude native transcript usage is optional and keyed by an opaque hash of session and message identity; streamed records with the same message id count once. The status line tee still carries quota percentages only.
53
66
  - The local worker records optional counters returned by OmniRoute. Requested route/model and gateway-reported served model/provider are separate fields; an alias is never treated as a served model.
54
67
  - `ahub report` deduplicates usage records by peer, source and id. Coverage counts distinguish calls with provider usage from calls where usage was absent. Token counters are provider-reported values; the report never derives a price or treats missing spend as zero. Estimated price and measured provider spend remain unknown unless a future source reports them.