@staix/agent-hub 0.12.19 → 0.12.20

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 (66) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +3 -3
  3. package/docs/agent-notes/adapters.md +1 -0
  4. package/docs/agent-notes/bus.md +1 -0
  5. package/docs/agent-notes/daemon.md +2 -1
  6. package/docs/agent-notes/models.md +3 -0
  7. package/docs/agent-notes/tasks.md +3 -2
  8. package/docs/events.md +4 -1
  9. package/docs/operations.md +413 -17
  10. package/docs/quickstart.md +1 -1
  11. package/docs/security.md +17 -10
  12. package/docs/smoke.md +58 -0
  13. package/docs/specs/2026-09-19-agent-hub-design.md +399 -3
  14. package/docs/specs/2026-09-20-pi-local-models-design.md +2 -0
  15. package/docs/specs/2026-09-20-upgrade-recovery-design.md +287 -0
  16. package/docs/specs/2026-10-04-switchyard-source-port-design.md +10 -0
  17. package/docs/verified.json +13 -13
  18. package/package.json +1 -1
  19. package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
  20. package/plugins/agent-hub/server.js +63 -25
  21. package/src/adapters/claude-channel.ts +61 -20
  22. package/src/adapters/codex-appserver.ts +3 -0
  23. package/src/adapters/local-worker.ts +4 -0
  24. package/src/cli/console-state.ts +325 -102
  25. package/src/cli/console.ts +44 -23
  26. package/src/cli/help.ts +120 -0
  27. package/src/cli/identity.ts +3 -3
  28. package/src/cli/launch.ts +2 -0
  29. package/src/cli/main.ts +171 -75
  30. package/src/cli/preview.ts +1 -0
  31. package/src/cli/reset.ts +194 -0
  32. package/src/cli/status-lines.ts +9 -5
  33. package/src/cli/terminal-recovery.ts +140 -36
  34. package/src/cli/upgrade-runtime.ts +274 -56
  35. package/src/cli/upgrade.ts +366 -41
  36. package/src/hub/ask.ts +1 -1
  37. package/src/hub/board.ts +15 -7
  38. package/src/hub/bus.ts +60 -4
  39. package/src/hub/child-process.ts +1 -0
  40. package/src/hub/conductor.ts +38 -6
  41. package/src/hub/control-client.ts +5 -3
  42. package/src/hub/daemon.ts +56 -9
  43. package/src/hub/events.ts +7 -1
  44. package/src/hub/execution-budget.ts +15 -6
  45. package/src/hub/hub-tools.ts +4 -4
  46. package/src/hub/inference.ts +136 -1
  47. package/src/hub/lifecycle.ts +7 -1
  48. package/src/hub/peers.ts +2 -0
  49. package/src/hub/recovery-store.ts +51 -3
  50. package/src/hub/report.ts +43 -1
  51. package/src/hub/restart.ts +25 -0
  52. package/src/hub/routing.ts +48 -16
  53. package/src/hub/snapshots.ts +3 -2
  54. package/src/hub/tasks.ts +236 -83
  55. package/src/local/deny.ts +2 -1
  56. package/src/models/mlx.ts +11 -8
  57. package/src/models/ollama.ts +2 -2
  58. package/src/models/relay.ts +190 -42
  59. package/src/models/route/relay-selector.ts +43 -31
  60. package/src/models/route/runtime.ts +17 -7
  61. package/src/models/route/signals.ts +12 -1
  62. package/src/models/route/stage.ts +57 -3
  63. package/src/omniroute/client.ts +1 -1
  64. package/src/ui/index.html +1 -0
  65. package/templates/AGENTS.block.md +1 -1
  66. package/templates/routing.toml +8 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ 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.20
8
+
9
+ - `ahub help` groups the commands into sections, starts every description at one column and wraps at word boundaries to the terminal width (80 to 100 columns), colored with the console palette only on a terminal without `TERM=dumb` or `NO_COLOR`. `--help`, `-h` and `help <command>` work, and `ahub <command> -h` (or `--help`) given as the only argument prints that command's help instead of running it, except for claude and codex, which pass it to the agent; an unknown command prints a one-line hint instead of the full list. The console shares the word-aware wrapping, which also covers the wrapping item of #213: the help overlay, approval titles, details and stream lines break at word boundaries. A continuation line starts four columns deeper than its source line (at most half the width), so wrapped peer text in the console stream never starts at column 0 or 2, where headers and event lines start; a wrapped line that starts like a hub header keeps its `> ` marker, now counted in its width (#212).
10
+ - A Claude session started without `ahub claude` (`AGENTHUB_CHANNEL=1`) has no channel evidence and attaches tools-only: it declares no channel and never reports a delivery `accepted`; its messages stay queued and `hub_inbox` reads them, one push delivery's worth at a time, as `completed` deliveries, under the same holds as pushes. It stands by instead of taking the peer from an `ahub claude` session, while `ahub claude` takes the peer from it with pushes. `ahub status`, the console and the dashboard name the state and the next action, `ahub claude` (#205).
11
+ - Control protocol 16 (hello `channel`, `inbox`). The upgrade coordinator accepts protocol-15 sources (0.12.17 through 0.12.19); the live upgrade from a running protocol-15 hub is still pending.
12
+ - Let a task's current owner and reviewer read its public view with history through `hub_task_show`, so a reviewer sees the done summary and check line; PII tasks stay stubs and other peers stay refused (#208).
13
+ - Keep `owner` with `after` as a reserved owner offered the task first once it is ready, falling back with a notice when that peer is offline, paused, declined or excluded by PII; let a proposer redirect its own unaccepted task; `ahub route explain` shows the reservation (#207).
14
+ - Add session-aware stay/switch for Pi's `hub/auto` and hub stage routes: a hard override escalates at once, a tool loop keeps its tier, other changes wait for a user turn, and a de-escalation over `max_switch_prefill_tokens` stays; no route summarizes or trims the history. Top-level `stay_switch` in `routing.toml` is `off`, `shadow` (default: route events record the planner's verdict, routing unchanged) or `enforce`; `ahub report` counts model changes per session and inside tool loops. Enforce stays opt-in until measured (#197).
15
+ - Cool a relay alias down after three consecutive transport or startup failures (30 s doubling to 5 min; a busy slot and budget-cut timeouts never count, and any HTTP answer ends it), and try the DGX fallback first, keeping MLX behind it, while MLX cools down on `hub/auto`. Opt-in with `[pi] efficient_wait_ms`: an efficient `hub/auto` request whose MLX slot stays busy past it tries `dgx/fast` first, for at most 15 s to its response headers before MLX takes over, never under an execution budget. A stream that fails after its headers is not retried. `ahub status` and `cooldown` events show it (#199).
16
+ - Events schema stays 1: route events gain optional fields and sources, and `cooldown` (#199), `pii_screen` (#198) and `native_thread` (#215) are new event types.
17
+ - `ahub console` separates header, body and footer with ASCII rules and brackets the active tab, which counts pending approvals or `needs_review` deliveries. Peers, Approvals, Tasks and Queue are aligned tables with header rows, `-` for zero counters, human durations and relative resets; Enter shows labeled fields instead of JSON. The Approvals panel shows the selected request's title, numbered allow options and `d deny`. The footer lists only the keys that act in the current mode, panel and state, `?` shows a key table grouped by panel, and notices clear at the next key or after about ten seconds. Untrusted text can no longer look like a hub line: `v` in the stream frames an approval title as its push does, titles in the Approvals panel and details sit in their value column, command output starts at column 4, and a cut line ending in `[agent-hub` no longer overflows the width. Peers show `tools-only` when a status carries #205's field, and a context reading in the console shows local time instead of an ISO time; `ahub tail` is unchanged (#213).
18
+ - `ahub reset [--all] [--yes]` returns a project's hub to a clean state. Without `--yes` it lists, by id and count and never with task or message text, what it would settle, clear, archive and keep, and changes nothing. With `--yes` it stops a running hub as `ahub kill` does; the runtime reset then discards every queued and needs_review delivery with reason `reset` through the journal (one `resolution_history` entry each), clears manual, budget and conductor holds and drops the session pointers, keeping the board, logs, audit, recovery records and execution budgets. `--all` moves the state directory to a git-ignored `.agenthub/archive/state-<UTC time>/` (0700) and starts an empty one holding only `project.json`, so the project id and registration stay; the local worker's and Pi's denylist and turn snapshots leave `.agenthub/archive` out, and `--all` archives a state directory it cannot read as it is. A crashed hub's leftover manifest does not block a reset; `--all` refuses a symlinked archive directory or `.gitignore`. Human-only; refused while an upgrade or recovery operation is open or for a hub without a matching registration; claude-mem is never touched (#214).
19
+ - Add an opt-in model PII screen (`signals.pii_screen = "local"` in `routing.toml`, default `off`): a new task is held back until the on-device model, or the gateway only while on campus, labels it `pii` or `clear` with a closed category; no verdict (timeout, off campus, an answer outside the list, text over 6000 UTF-8 bytes) counts as PII, and a task left that way with no owner is screened again on the release timer, at most 10 times per hub run and never while its text is over the limit. A taken, failing or still-loading on-device model hands the screen to the campus gateway. The screen joins the pattern checks on done summaries, review notes, unmet items, budget hand-offs and `hub_remember`; withheld history entries stay withheld. History and the new `pii_screen` event record the source and category, never the text; `scripts/pii-screen-eval.ts` measures it on a synthetic Korean and English fixture set against a 90% recall bound (#198).
20
+ - Plan an attached Claude session that no live `ahub claude` launcher recorded as reconnect-only: its stale `claude-session.json` is never a recovery target or shown, nothing in its terminal is closed or relaunched, and an operation-fenced waiver lets the restored hub accept it once its plugin reconnects. Refused `upgrade --yes`, `restart --yes` and every dry-run print each blocker on stderr (#206).
21
+ - Re-prepare the verified source after an expired preparation lease, keeping effect receipts; refuse a replaced daemon, a source held by another operation and a changed peer with the next action. Check Codex resume viability against the launcher's session store before closing and relaunching. A restoration launcher that recorded itself and whose process is gone, or whose terminal Orca no longer lists, is receipted failed within one 5-second wait (an Orca wait timeout counts as not yet; an unreadable launcher counts as running); resume settles a pending or failed launch by what is live, and no terminal is created beside a launcher that may run. `abort` also cancels a prepared source whose hold lapsed. `ahub recovery status` reports the live runner, stale receipts, effects and next commands; `ahub recovery dispose <id> --fresh-session <codex or claude> --reason <text>` records a lost conversation and resumes, and `ahub recovery dispose <id> --stop-and-archive --reason <text>` abandons the operation, releasing the lock only after its own holds and targets are released or stopped (a target of an older coordinator is stopped at its own control protocol). A Codex thread with no rollout that the hub saw start and saw no turn on (from a new `native_thread` event) restarts as a new session, with the plan saying so. Next actions and errors name the operation's own coordinator, since the global CLI may be the older release mid-upgrade; reconnect-only and new sessions need a target that reads recovery waivers. A daemon that crashed and left its manifest behind reads as stopped, not unavailable, so it no longer wedges abort, resume and stop-and-archive; `next` offers resume and `--fresh-session` only where resume can get past what is live, and stop-and-archive only where dispose accepts it; a target that dies after it started is not restarted in this release (stop-and-archive is the way out) (#215).
22
+
7
23
  ## 0.12.19
8
24
 
9
25
  - Pi's write, edit, bash and git write approvals offer `Always allow <tool> until Pi restarts`: later calls of that tool by the running Pi skip the prompt, still under the path guard and sandbox. The grant is in memory only, logged by tool name, and the dashboard can still only deny (#209).
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.19, control protocol 15. Durable delivery records distinguish queued
7
+ Status: 0.12.20, 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,7 +28,7 @@ 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.19 && ahub setup
31
+ bun add -g github:STAIxBWLB/agent-hub#v0.12.20 && ahub setup
32
32
  ```
33
33
 
34
34
  The installed commands remain `ahub` and `agent-hub`.
@@ -68,7 +68,7 @@ an N-peer message bus and attaches each agent through its native control surface
68
68
 
69
69
  | Peer | Inbound | Outbound | Busy handling |
70
70
  | --- | --- | --- | --- |
71
- | Claude Code | channel plugin push (`notifications/claude/channel`) | MCP tools | queued by Claude Code, grouped on next turn |
71
+ | Claude Code | channel plugin push (`notifications/claude/channel`) under `ahub claude`; otherwise tools-only, read with `hub_inbox` | MCP tools | queued by Claude Code, grouped on next turn; tools-only: queued at the hub |
72
72
  | Codex | app-server `turn/start` (idle) / `turn/steer` (busy) | proxy intercepts `item/agentMessage` | steer or queue |
73
73
  | Kimi Code | ACP `session/prompt` | ACP `session/update` stream | queue (`turn.agent_busy`) |
74
74
  | Pi | RPC or managed native TUI extension | hub-approved tools | queued until `agent_settled` |
@@ -3,6 +3,7 @@
3
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/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.
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
+ - 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.
6
7
  - Codex 0.154.0 `agentMessage` items carry `text` and `phase` (not `content[]`); only the last non-`commentary` message of a turn is shared.
7
8
  - 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.
8
9
  - 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.
@@ -12,4 +12,5 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
12
12
  - 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
13
  - 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
14
  - `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.
15
16
  - A hub-native peer (Pi, the local worker) does not get to call its own message `important`: `capPriority` caps it unless the delivery it answers held an `important` envelope addressed to it. Check the delivery, not `replyParent`, which ties on hop and takes the later item. Do not bypass it by setting `priority` in the adapter.
@@ -7,8 +7,9 @@ Scope: the control WS and its protocol, state files, the Claude channel's reconn
7
7
  - 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
8
  - State files that clients read (`status.json`, `control-token`) are written after the port is bound, and `status.json` via temp file + rename.
9
9
  - The channel's reconnect loop stops on closes a retry cannot fix (`TERMINAL_CLOSES` in `claude-channel.ts`); a new daemon close code that means "do not come back" belongs there, or two clients fight over it forever.
10
- - Close 4000 is the one close that ends on its own, so it is not in `TERMINAL_CLOSES`: the session stands by and reconnects only once `status.json` reports the peer offline and not `claiming`. Reconnecting blind would evict whoever holds the id now, and the two would trade it forever. `claim()` happens at hello and `attach()` only after the preface, so the peer reads as offline in between: the daemon writes the status file at the claim and the flag covers that window.
10
+ - Close 4000 is the one close that ends on its own, so it is not in `TERMINAL_CLOSES`: the session stands by and reconnects only once `status.json` reports the peer offline and not `claiming`. One exception (#205, `peerHeld` in `claude-channel.ts`): a session with channel pushes also reconnects when the holder is reported `toolsOnly` and not `claiming`, and takes the peer; the daemon refuses the reverse with 4000. Reconnecting blind would evict whoever holds the id now, and the two would trade it forever. `claim()` happens at hello and `attach()` only after the preface, so the peer reads as offline in between: the daemon writes the status file at the claim and the flag covers that window.
11
11
  - Adding a native peer requires testing the command emitted by the actual recovery driver, not only its inspection or launch helpers. Never let a generic non-Codex branch treat a new peer as Claude.
12
12
  - Cross-version recovery must use the authenticated source protocol for prepare/commit/abort and the target protocol after startup. Prove the transition against a real prior release before claiming compatibility.
13
13
  - A zero-turn Claude session has no transcript, so it can never be resumed: every identity gate that compares session ids (restore, coordinator verify, daemon readiness) must tolerate a fresh session while the transcript is absent and stay strict the moment one exists. The commit snapshot records `sessionPersisted` for the restored daemon, and snapshots too old to have it are re-derived from disk. Never treat "session id changed" as proof of a lost conversation without checking the transcript first.
14
14
  - The terminal-recovery launcher signature is the shared pinned `processSignature` (`src/pi/process-signature.ts`): a record written by one CLI invocation must compare equal when a later invocation re-reads the live launcher under another TZ/LC_ALL (#177). In-process `process.env` mutation does not reach Bun.spawnSync's default child environment, so cross-environment regressions are tested with real subprocesses.
15
+ - A restored daemon accepts a changed native session id (Kimi and local aside: they restart as fresh workers) in two ways only: the #64 rule for a Claude session that never persisted a transcript, and the operation-fenced `recovery-waivers.json`, which the coordinator writes wherever it accepts a session other than the planned one: a reconnect-only Claude (#206), an operator-chosen fresh session, the plan's `freshStart` of a Codex thread the hub saw start with no turn, or a zero-turn Claude it accepted (#215). Never waive on any other evidence. Re-preparation after an expired lease keeps every effect receipt, and `restored:<peer>` = `failed` never counts as restored. Judge a restoration launcher by its `terminal-recovery.json` record and process signature (and a `terminal_handle_stale` answer), never by the terminal's exit; an unreadable record is unknown, never "none".
@@ -4,7 +4,10 @@ Scope: the model relay and its journal, the hub's own inference and its slots, O
4
4
 
5
5
  - The model relay journals per-request identity (`RelayRequestRecord`): a request's served model comes only from its own gateway header, its own generation SSE event (#137 heartbeat classification), or the locally validated MLX configuration. A backend's mutable last-served label is never a request's evidence, HTTP 200 plus the requested alias identifies nothing, and a request cancelled before identification stays explicitly unidentified (`identified: false`, `outcome: "cancelled"`). Journal records carry no prompts, tools, keys or Access headers.
6
6
  - Inference is optional and fail-open: it returns its input or `undefined` on any failure and backs off, so a delivery never waits on a dead model twice. Its output is only ever capped text or a value checked against a closed list.
7
+ - The PII screen (`screenPii`, #198) is the exception: it fails closed. Every failure (timeout, no or off-campus model, an answer outside `clear` / `pii <category>`, text over `SCREEN_MAX_BYTES` UTF-8 bytes, since a token is at least a byte) is `unknown`, which `Tasks` handles as PII. It never goes through the sidecar or the inference backoff, asks the gateway only after `onCampus()` and with `onCampusOnly`, takes the on-device generation slot under its own deadline (`acquire(abort.signal, 0)` first, one look and `MlxBusyError` when all are taken: a taken slot sends it to the campus gateway, and only off campus does it wait; on campus a failing or still-loading device hands over to the gateway after 60% of the deadline), sends temperature 0 and `reasoning_effort: "none"` on both paths so a reasoning model cannot spend its 32 tokens thinking, and never puts the text in a log line, an error or an event.
7
8
  - Secrets stay inside `OmniRoute`: never put the key or Access values in a log line, an error message, a return value or the generated Switchyard file (the key goes by env var name).
8
9
  - Switchyard's docs drift from the released binary. Any change to `switchyardToml` is checked with the real `switchyard-server --dry-run`, not only the stand-in in `test/fakes/`.
9
10
  - Shared inference slots must recover after a hub process dies, without evicting a live owner. Cancellation has to reach slot acquisition from the relay caller, not just exist in the helper signature.
11
+ - Stay/switch (#197): `hub/auto` and local stage routes apply `stay_switch` only through `stayOrSwitch` (`src/models/route/stage.ts`), never at a call site; `shadow` must not change a routing decision (the recorded-decision golden test in `test/route-relay.test.ts`). No route summarizes, trims or rewrites the forwarded conversation: a tier that cannot fit is filtered out. A PII route's planner never sees the conversation size (no prefill bound) and its event omits `prefillTokens`.
12
+ - Load and cooldowns (#199) only reorder a `hub/auto` request's candidates (the route's own backend, then its fallback, as on main), in one ordering step in the relay's fetch handler; a fixed alias is never reordered. Never remove a candidate. A candidate goes first only while it is neither cooling nor `failing` (any failure, an error status or a failed stream included, for 30 s or until a success; separate from the cooldown streak). Load and pin moves are opt-in (`efficientWaitMs` absent: none) until #199 AC5 measures them. Their attempt is bounded only up to its response headers (`MOVE_FIRST_BYTE_MS`, after `omni.base()`); a cut there marks `failing` without counting toward a cooldown, and a stream that fails after the headers cannot fall back, so do not claim a move never fails a request MLX would have served. None happens while the request runs under an execution budget (`underBudget`, a read that admits nothing; a throw counts as a budget). The selector only names `prefer`; it never picks a moved backend itself. They never consider PII because the relay serves only Pi, which refuses PII work before any model call (`selectModel` in `daemon.ts`); a relay caller that can carry PII must not get a reorder toward DGX without a positive `onCampus()`. Count a cooldown failure only for a transport or startup failure outside a cooldown that was neither cancelled, refused admission, a busy MLX slot (`MlxBusyError`: load, never failure), nor a deadline the relay cut itself (`journalEntry.cut`: `"budget"` for the execution budget's, `"move"` for a moved attempt's first byte, which only marks `failing`); any HTTP answer, whatever its status, ends the streak and the cooldown (`answered`).
10
13
  - MLX owner and generation identity read `ps lstart` with LC_ALL=C/TZ=UTC pinned (#177): unpinned, a record written under one environment reads as a foreign process under another, and the fail-closed paths would refuse the hub's own server.
@@ -3,7 +3,8 @@
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` or `src/hub/hub-tools.ts`.
4
4
 
5
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.
6
- - `assign()` never defaults to the task's current owner, or a decline can only come back to the decliner.
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
- - Conductor authority requires the explicit unique role before capability checks. Assignments preserve the acting peer. A peer releases only its own conductor hold; operator release and manual, quota and recovery holds remain separate paths.
8
+ - 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
+ - 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.
9
10
  - Supervision reasons use structured enums, never history notes, check output or approval titles. Round signatures survive restart; later summaries count only newly joined tasks.
package/docs/events.md CHANGED
@@ -18,7 +18,8 @@ marked `private: true`, and PII tasks `pii: true`.
18
18
  | `quiet` | `id`, `from`, `peers`: an agent message held back from these members of a silent turn-free cohort; its other recipients got it (issue #107) |
19
19
  | `fact` | `peer`, `id` (the offer), `files` (files whose diff it carried), `plans`, `unknown` (files whose change it showed with attribution unknown), `named` (files under a named directory it named without a diff, those counted as "N more" or held back by the PII filter included), `bytes` (the injected text), `via` (`hook` for Claude, `steer` for Codex, `done` with an integration request), `ms` (the hub's time to build it), `hookMs` (the hook process's own start-up and connect time), `accepted` (whether app-server took the steer), `unanswered` (app-server did not answer it within 10 s: it may have gone in), `rttMs` (an accepted steer: from sending it to app-server's answer), `probe` (a context check), `coverage` (it named files earlier changes are not covered for): one fact offer (issue #108) |
20
20
  | `fact_ack` | `peer`, `id`, `via` (`hook` and `steer`: a readback found the offer in the native session; `done`: the next `hub_task_done`), `ms` (from the offer): an acknowledged offer, the only thing that moves a peer's view |
21
- | `route` | `peer`, `route`, `tier`, `source` (`override`, `dimensions`, `hold`, `classifier`, `default`), `score`, `ms`. Local decisions also carry `decision`, `turn`, optional numeric `task`, `pii`, and decision-time `severity`, `spinning`, `exploring`, `production`; no prompt text |
21
+ | `route` | `peer`, `route`, `tier`, `source` (`override`, `dimensions`, `hold`, `classifier`, `default`; for `hub/auto` also `load`: a busy MLX slot moved it to `dgx/fast` (opt-in, `[pi] efficient_wait_ms`), `cooldown`: a cooling MLX went to its fallback first, and `pin`: an enforced tool loop went to the backend it is pinned to, issue #199), `score`, `ms`. Local decisions also carry `decision`, `turn`, optional numeric `task`, `pii`, and decision-time `severity`, `spinning`, `exploring`, `production`. A `hub/auto` decision for a request without a session key carries `stateless: true` (`ahub report` then counts its sessions as unknown). `hub/auto` and hub stage decisions carry `turnType` (`tool_result`, `user`, `compaction`: what the call answers) and `prefillTokens` (estimated input a backend would prefill; absent on a PII route), and, unless `stay_switch` is `off`, `staySwitch` (`shadow`, `enforce`), `plan` (`stay`, `switch`) and `reason` (`new_pin`, `compaction`, `context_fit`, `same_tier`, `override`, `tool_loop`, `prefill_bound`, `user_turn`): the planner's verdict next to the tier taken (issue #197). No prompt text |
22
+ | `cooldown` | `peer`, `alias`, `event` (`start`, `end`), `failures` (consecutive transport or startup failures), `ms` (on `start`, how long the alias cools down): a relay backend alias after repeated failures (issue #199). An `end` is recorded at an HTTP answer (any status) or when the hub next checks the alias after it expired |
22
23
  | `route_outcome` | `peer`, `decision`, `turnId`, `turn` (`completed` or `failed`), optional `task`, `pii`, `latched`, optional `next` (`severity`, `tests`: `pass`/`fail`/`none`, `repeat`) and `advisor` (`approve`/`redo`/`failed`). Exactly once per local decision at turn settlement, including failure/cancellation |
23
24
  | `advisor` | `peer`, `route`, `trigger`, `verdict` (`approve`, `redo`, `failed`), `discardedChars`: an advisor check result; no transcript or feedback text |
24
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 |
@@ -29,12 +30,14 @@ marked `private: true`, and PII tasks `pii: true`.
29
30
  | `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 |
30
31
  | `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) |
31
32
  | `state` | `peer`, `state` |
33
+ | `native_thread` | `peer` (`codex`), `thread` (its native thread id), `fresh` (`true` when the TUI started the thread, `false` when it resumed one): each thread the Codex adapter adopts, before any turn on it. The upgrade planner reads it: a thread with no rollout that the hub saw start, with no `turn_start` since, is planned as a fresh start (issue #215) |
32
34
  | `turn_start` | `peer`, `turn` (`<peer>#<hub run>.<n>`, unique across restarts). A turn follows the adapter: pausing a busy peer does not end it |
33
35
  | `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 |
34
36
  | `tokens` | `peer`, `n` (tokens added since the previous report), optional numeric `task`, `attribution` (`delivery`, `single_open`, `unattributed`), `pii: true` for an attributed PII task |
35
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. |
36
38
  | `task` | `id`, `event` (the board history event, e.g. `proposed`, `assigned`, `done`, `check failed`, `blocked`, `ready`), `by`, `state`, `owner`, `reviewer`, `class`, `pii` |
37
39
  | `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
+ | `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 |
38
41
  | `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) |
39
42
  | `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) |
40
43