@staix/agent-hub 0.12.20 → 0.12.22
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.
- package/CHANGELOG.md +32 -0
- package/README.md +17 -9
- package/docs/agent-notes/adapters.md +13 -3
- package/docs/agent-notes/benchmarks.md +1 -1
- package/docs/agent-notes/bus.md +7 -3
- package/docs/agent-notes/daemon.md +12 -2
- package/docs/agent-notes/local-worker.md +5 -1
- package/docs/agent-notes/tasks.md +4 -2
- package/docs/agent-notes/tests.md +5 -2
- package/docs/bench.md +143 -0
- package/docs/events.md +15 -5
- package/docs/operations.md +510 -54
- package/docs/quickstart.md +16 -11
- package/docs/research.md +92 -0
- package/docs/security.md +49 -11
- package/docs/smoke.md +138 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +227 -20
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +152 -18
- package/docs/verified.json +34 -17
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +3 -2
- package/src/adapters/acp.ts +77 -10
- package/src/adapters/codex-appserver.ts +72 -2
- package/src/adapters/local-worker.ts +46 -11
- package/src/adapters/pi.ts +215 -22
- package/src/cli/bench.ts +287 -0
- package/src/cli/console-state.ts +226 -28
- package/src/cli/console.ts +16 -4
- package/src/cli/facts-hook.ts +50 -8
- package/src/cli/help.ts +16 -5
- package/src/cli/identity.ts +10 -4
- package/src/cli/launch.ts +272 -18
- package/src/cli/main.ts +290 -30
- package/src/cli/preview.ts +13 -3
- package/src/cli/recovery-package.ts +17 -0
- package/src/cli/status-lines.ts +2 -1
- package/src/cli/terminal-recovery.ts +83 -31
- package/src/cli/upgrade-interactive.ts +297 -0
- package/src/cli/upgrade-runtime.ts +123 -20
- package/src/cli/upgrade.ts +108 -19
- package/src/hub/bench.ts +211 -0
- package/src/hub/bus.ts +24 -7
- package/src/hub/conductor.ts +22 -21
- package/src/hub/config-trust.ts +1 -0
- package/src/hub/control-client.ts +4 -1
- package/src/hub/daemon.ts +477 -86
- package/src/hub/delivery-journal.ts +6 -6
- package/src/hub/events.ts +4 -2
- package/src/hub/lifecycle.ts +8 -13
- package/src/hub/manager.ts +16 -14
- package/src/hub/peers.ts +2 -2
- package/src/hub/permission-mode.ts +44 -0
- package/src/hub/recovery-store.ts +59 -9
- package/src/hub/registry.ts +36 -22
- package/src/hub/research.ts +255 -0
- package/src/hub/tasks.ts +99 -55
- package/src/hub/tool-refusal.ts +7 -0
- package/src/hub/ui.ts +4 -1
- package/src/local/deny.ts +48 -1
- package/src/local/sandbox.ts +6 -3
- package/src/local/tools.ts +34 -11
- package/src/pi/extension.ts +43 -20
- package/src/pi/process-signature.ts +23 -0
- package/src/pi/tool-receipts.ts +24 -2
- package/src/ui/index.html +98 -17
- package/src/ui/task-progress.ts +33 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,38 @@ 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.22
|
|
8
|
+
|
|
9
|
+
- 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).
|
|
10
|
+
- `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).
|
|
11
|
+
- 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).
|
|
12
|
+
- `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).
|
|
13
|
+
- 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).
|
|
14
|
+
- 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).
|
|
15
|
+
- 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`).
|
|
16
|
+
|
|
17
|
+
## 0.12.21
|
|
18
|
+
|
|
19
|
+
- 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).
|
|
20
|
+
- `ahub bench` follow-ups: a task id such as `constructor` no longer breaks reports, an attempt whose measure wait ran out or was interrupted in its last poll records no measures, the fingerprint lists only attached peers, a run returns to a branch that shares its name with a tag, and a suite ref deleted during the run is reported (#265).
|
|
21
|
+
- `ahub bench`: run a suite of tasks against the attached peers in a project kept for benchmarks, record pass, fail, timeout and the #247 measures per attempt in `~/.agenthub/bench/`, and compare arms with bootstrap intervals (`bench run|list|status|report|compare|export`); the dashboard gains a Benchmarks section (#251).
|
|
22
|
+
- Kimi can start or resume with an operator-selected `permission_modes.kimi` default, setting ACP `yolo` for `ask-when-needed` or `auto` for `never-ask` before its first prompt; unavailable or refused modes fail startup with a reason. Non-Kimi ACP sessions retain their native mode as unmanaged. Kimi 2.1.1 benign live checks accepted both ids and completed file edits and harmless shells with no ACP approval requests; risky-action behavior remains unverified (#240).
|
|
23
|
+
- Add human-only `ahub permission` and a console Peers `m` chooser for running Claude, Codex, Kimi, Pi and local, with explicit confirmation for `never-ask`, status/event visibility and project defaults capped at ask-when-needed until a source-labelled console confirmation enables never-ask at each hub start. Native sandboxes remain unchanged; `ask` restores native approval handling (#242).
|
|
24
|
+
- Return proven pre-effect Pi task/conductor refusals as their exact errors and completed receipts; preserve uncertain effects, storage failures and unproven legacy pending receipts (#254).
|
|
25
|
+
- Log each Pi exit cause and last tool name without arguments, show the next action, and resume an unexpectedly idle headless Pi on its verified session at most once in 60 seconds under `pi.auto_start`, with no fresh fallback (#255).
|
|
26
|
+
- Opt-in research records (`"research": { "enabled": true }`): one record per approved task in `~/.agenthub/research/`, across projects, with outcome, review rounds, rework, failed checks, tokens and time per peer and model, built only from `events.jsonl` (never task or message text). `ahub research` reports success, first-pass, rework and check-failure rates and tokens and wall time per task; `ahub research export` writes JSONL or CSV; `ahub research backfill` builds records from existing events; `ahub task label` adds a person's later verdict (#247).
|
|
27
|
+
- Preserve empty committed queue entries for offline peers during journal-backed recovery, so preservation verification does not require them to reconnect. Durable queues and delivery receipts remain authoritative; stale non-empty snapshot queues are never replayed (#234).
|
|
28
|
+
- Preserve every pause and queue array when a delivery resolution is refused or its journal write fails; roll back only the recipient queue and preface (#229).
|
|
29
|
+
- Retry a dependent task released while no peer can take it or its ready/assignment write fails; consume its per-run offer only after assignment (#231).
|
|
30
|
+
- Test port probes bind 127.0.0.1 to match the Codex servers, and failed daemon starts show their error in assertion diffs (#232).
|
|
31
|
+
- Reuse Git fixture templates and restore sealed-study mutations between subtests to reduce export test process spawns without changing its 20 s timeout (#233).
|
|
32
|
+
- Judge a recorded hub manifest, registry claim, recovery runner and manager owner by its process signature as well as its pid, through one helper, so a pid that a reboot handed to another process (one we may not even signal) no longer wedges recovery, `ahub up` or `ahub reset`. `status.json` (file only), the registry claim, the runner record and the manager owner now carry the signature, daemons stop writing `hub.pid`, and recovery reads the registry's live claim instead of the plan's frozen copy. Records written by 0.12.20 and older keep the bare-pid answer; the operations guide gives the manual clear for such a claim (#226).
|
|
33
|
+
- A stop-and-archive recorded on an operation whose coordinator predates #215 marks its receipt `schema: 2`, which every older runner and abort refuse before they take the lock, so an older global `ahub recovery resume` can no longer advance an operation being abandoned; checked against the real v0.12.19 source (#227).
|
|
34
|
+
- A `terminal-recovery.json` row that fails validation is unknown for the launcher it may describe, never "no launcher", and the next `ahub codex`, `ahub claude` or `ahub pi` writes it back unchanged; launchers recorded by 0.12.12 or older are also read by their unpinned signature (#228).
|
|
35
|
+
- `ahub recovery resume` restarts a target hub that stopped after it started (a crash, an OOM kill, a reboot) from the operation's snapshot, once per resume: it records the restart (status shows `restarts`), closes the terminals the dead hub's peers were restored into, relaunches those peers against the new hub (a session the operator had accepted by its own id), and treats a launcher of the dead hub or a session attached without an id as live, never as nothing attached. Without the snapshot, or for an operation of an older coordinator, stop-and-archive stays the way out (#225).
|
|
36
|
+
- Keep local-worker hub tool arguments and results out of claude-mem capture; ordinary turns see PII task stubs, and only PII turns may read full local board rows (#230).
|
|
37
|
+
- Console stream lines color peer names, the `task` keyword, numeric values and issue/task references separately, keeping ordinary words and message bodies at the default foreground and preserving plain output and terminal geometry (#239).
|
|
38
|
+
|
|
7
39
|
## 0.12.20
|
|
8
40
|
|
|
9
41
|
- `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).
|
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.
|
|
7
|
+
Status: 0.12.22, 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.
|
|
31
|
+
bun add -g github:STAIxBWLB/agent-hub#v0.12.22 && ahub setup
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
The installed commands remain `ahub` and `agent-hub`.
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
and
|
|
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
|
|
@@ -100,7 +106,8 @@ only. `--backend dgx` or `--backend mlx` explicitly pins a Pi session's backend.
|
|
|
100
106
|
Pi connects to an authenticated relay with model aliases `dgx/coding`,
|
|
101
107
|
`dgx/fast`, and `mlx/fast`; upstream gateway credentials stay in the hub.
|
|
102
108
|
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
|
|
109
|
+
approval flow. Keep `ahub console` open to approve write/edit/shell operations:
|
|
110
|
+
a request nobody answers expires, and two in a row end Pi's turn.
|
|
104
111
|
Tool-call receipts survive daemon restart; an interrupted operation with an
|
|
105
112
|
unknown outcome must be reconciled before repeating it. Cancelling a Pi turn
|
|
106
113
|
does not automatically hand its task to a cloud peer.
|
|
@@ -138,13 +145,14 @@ Bun + TypeScript. Single daemon per project directory, loopback only.
|
|
|
138
145
|
## Local dashboard
|
|
139
146
|
|
|
140
147
|
With a running daemon, `ahub ui` opens a local dashboard for messages, peer states
|
|
141
|
-
and queues, the task board, budget windows
|
|
142
|
-
`ahub ui --no-open` to print a single-use link when the
|
|
148
|
+
and queues, the task board, budget windows, pending approvals and this machine's
|
|
149
|
+
benchmark runs. Use `ahub ui --no-open` to print a single-use link when the
|
|
150
|
+
browser cannot be opened.
|
|
143
151
|
Open the link within 60 seconds; the browser session lasts one hour.
|
|
144
152
|
|
|
145
153
|
The dashboard can send console messages, propose and assign tasks, pause or resume
|
|
146
154
|
peers, and answer approvals. Budget pauses still require the terminal override.
|
|
147
|
-
Private envelopes and PII tasks stay redacted. Local-worker tool details and
|
|
155
|
+
Private envelopes and PII tasks stay redacted. Local-worker and Pi tool details and
|
|
148
156
|
allow decisions stay in `ahub tail` / `ahub permit`; the page can deny them.
|
|
149
157
|
No dashboard port is opened until requested. See [the session security model](docs/security.md#local-dashboard-sessions-issue-6).
|
|
150
158
|
|
|
@@ -1,14 +1,21 @@
|
|
|
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/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/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
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.
|
|
7
7
|
- Codex 0.154.0 `agentMessage` items carry `text` and `phase` (not `content[]`); only the last non-`commentary` message of a turn is shared.
|
|
8
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.
|
|
9
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.
|
|
10
|
+
- 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/requested-stop/recovery/startup/active-tool fences and never fall back fresh.
|
|
10
12
|
- 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.
|
|
11
|
-
- 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
|
|
13
|
+
- 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
|
+
- 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. 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
|
+
- 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
|
+
- 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`).
|
|
12
19
|
- 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.
|
|
13
20
|
- 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).
|
|
14
21
|
- Work that must survive `ahub kill` (claude-mem `session-end`) is awaited in `stop()`; fire-and-forget dies with the process.
|
|
@@ -16,6 +23,9 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
16
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.
|
|
17
24
|
- 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.
|
|
18
25
|
- 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.
|
|
19
|
-
- 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.
|
|
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. 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).
|
|
27
|
+
- 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).
|
|
28
|
+
- 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.
|
|
29
|
+
- `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`. 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.
|
|
20
30
|
- 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.
|
|
21
31
|
- 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.
|
|
@@ -8,5 +8,5 @@ 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.
|
package/docs/agent-notes/bus.md
CHANGED
|
@@ -4,13 +4,17 @@
|
|
|
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.
|
|
10
|
-
- What a peer is handed (`out`) and what it stands for (`originals`) differ once a delivery is condensed: the bus keeps both, registers `out` so `reply_to` resolves, puts the originals back on any failure (a thrown `deliver
|
|
11
|
-
- A condensed digest is sent by `digest`, not `hub`: `replyParent()` skips
|
|
11
|
+
- What a peer is handed (`out`) and what it stands for (`originals`) differ once a delivery is condensed: the bus keeps both, registers `out` so `reply_to` resolves, puts the originals back on a `failed_safe` receipt and holds any other failure (a thrown `deliver`, a later `onFailed`, a `needs_review` receipt) as `needs_review` with its originals, for `ahub queue resolve`, and counts no attempt when the peer merely got busy while the delivery was prepared. A delivery with an `important` envelope is never condensed.
|
|
12
|
+
- A condensed digest is sent by `digest`, not `hub`: `replyParent()` skips only the hub's `presence` item, and a reply to a digest must keep the highest hop of what it replaced. On a `failed_safe` delivery or a `retry` resolution the bus puts back the originals, never the digest.
|
|
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.
|
|
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.
|
|
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.
|
|
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`).
|
|
16
20
|
- 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.
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
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.
|
|
4
4
|
|
|
5
|
-
- The plugin bundle is installed apart from the daemon.
|
|
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.
|
|
6
7
|
- `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
8
|
- 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
9
|
- State files that clients read (`status.json`, `control-token`) are written after the port is bound, and `status.json` via temp file + rename.
|
|
@@ -11,5 +12,14 @@ Scope: the control WS and its protocol, state files, the Claude channel's reconn
|
|
|
11
12
|
- 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
13
|
- 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
14
|
- 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
|
-
- 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
|
+
- 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. A row written by 0.12.12 or older carries the unpinned signature, which `launcherState` also compares (#228). A launch-record row that fails validation is unknown for the peer it may belong to and is written back unchanged, never filtered out (#228).
|
|
16
|
+
- Judge every recorded owner pid (hub manifest, registry claim, recovery runner, manager owner, launcher record) with `processLiveness` in `src/pi/process-signature.ts`, never a bare `process.kill(pid, 0)`: after a reboot a recorded pid can belong to another process, even one we may not signal, and the bare probe wedged recovery and `ahub up` (#226). A writer records `processSignature(process.pid)` beside its pid; a registry claim stores it as `<instance id> <signature>` because an older daemon's claim rewrites the pid but not that column. Records without a signature keep the bare-pid answer; unknown is never gone.
|
|
15
17
|
- 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".
|
|
18
|
+
- 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).
|
|
19
|
+
- 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.
|
|
20
|
+
- 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.
|
|
21
|
+
- 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.
|
|
22
|
+
- `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.
|
|
23
|
+
- 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).
|
|
24
|
+
- 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).
|
|
25
|
+
- `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.
|
|
@@ -1,12 +1,16 @@
|
|
|
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).
|
|
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`.
|
|
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). 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, or `rm -rf` of a directory holding a `.git`.
|
|
7
8
|
- 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
9
|
- 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
10
|
- `guardPath` walks with `lstat`: `existsSync` follows symlinks, so a dangling link looked like a new file and the write landed at its target.
|
|
10
11
|
- git arguments never pass through `guardPath`; `gitArgsProblem` refuses absolute paths, `..`, `--no-index` and denylisted `rev:path` forms.
|
|
11
12
|
- 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
13
|
- After a tool with side effects ran, a failed turn is reported, never redelivered.
|
|
14
|
+
- `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.
|
|
15
|
+
- 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).
|
|
16
|
+
- 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).
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Task board and hub tools
|
|
2
2
|
|
|
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`.
|
|
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). Of the escalation a repeated `changes_requested` triggers, only `escalate()`'s own notice and "moved" message are guarded: the assignment inside it (`assignOwner`) still throws after its board write, and must not be wrapped before #276 decides, because the #231 ready-offer retry and the #254 uncertain-receipt test depend on that throw.
|
|
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
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
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.
|
|
10
|
+
- `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.
|
|
11
|
+
- A guard in `tasks.ts` or `conductor.ts` passes `preEffect` (throws `PreEffectToolRefusal`, `src/hub/tool-refusal.ts`) only before the operation's first board or hold write; a throw after a write, a readback failure or a wrapped error stays a plain `Error` (#254). The Pi side of the rule is in `adapters.md`.
|
|
10
12
|
- Supervision reasons use structured enums, never history notes, check output or approval titles. Round signatures survive restart; later summaries count only newly joined tasks.
|
|
@@ -4,6 +4,9 @@ Scope: tests, fakes and the test gate. Read before adding or changing a test or
|
|
|
4
4
|
|
|
5
5
|
- Tests that are not about batching build the bus with `batchMs: 0`; with the default 15 s window a lone status envelope looks like a lost message.
|
|
6
6
|
- In Bun 1.3.14 a test timeout that fires while `Bun.spawnSync` runs can start the next test inside spawnSync's own event loop, where another spawnSync then spins at full CPU for good (trigger not pinned down; see README). Bun 1.4.2 still runs the next test inside the outer spawnSync's event loop. `scripts/check.sh` runs tests with a 20 s timeout and under `scripts/hang-watch.sh`; a test that spawns and reads the process table gets a timeout of its own.
|
|
7
|
-
-
|
|
8
|
-
- Bind agent identity markers explicitly in native/CLI fixture children. The full gate starts the test runner without inherited agent markers
|
|
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
|
+
- 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
|
+
- 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
|
+
- 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.
|
|
9
12
|
- 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/bench.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
`ahub bench` runs a fixed set of tasks against the agents attached to a hub and compares configurations (#251). A run
|
|
4
|
+
records each attempt's outcome and the #247 measures of the task (tokens, time, review rounds, rework), never the
|
|
5
|
+
suite's text, the verify command's output or the agents' words. CooperBench (`docs/cooperbench.md`) stays the
|
|
6
|
+
rigorous native study of this repository; `ahub bench` is for any project.
|
|
7
|
+
|
|
8
|
+
## A bench project
|
|
9
|
+
|
|
10
|
+
Each attempt resets the work tree, so a run refuses every project but one kept for benchmarks: a git repository whose
|
|
11
|
+
`.agenthub/config.json` has
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
{ "bench": { "enabled": true } }
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
and that passes these checks before anything changes:
|
|
18
|
+
|
|
19
|
+
- the project is the root of its repository (a reset acts on the whole repository);
|
|
20
|
+
- nothing under `.agenthub/` is tracked, in the tree or in any ref the suite names (a checkout would replace the hub's own
|
|
21
|
+
files): `git rm -r --cached .agenthub`, ignore it, commit;
|
|
22
|
+
- every ref the suite names is a commit (it is pinned to that commit for the whole run, so a branch an agent moves
|
|
23
|
+
later changes nothing);
|
|
24
|
+
- the tree is clean (`.agenthub/` aside).
|
|
25
|
+
|
|
26
|
+
The reset also deletes ignored files (`git clean -x`): keep the suite file, hidden verify scripts and anything else the
|
|
27
|
+
run needs outside the tree, and let `setup` recreate what an attempt needs (dependencies, local settings).
|
|
28
|
+
|
|
29
|
+
Between attempts the runner checks out the task's pinned commit (`git checkout --force --detach <commit>`), runs
|
|
30
|
+
`git clean -ffdx -e /.agenthub` and confirms the tree is clean; a step that fails says why. A run that finishes returns
|
|
31
|
+
to the branch (or commit) it started on; a run that stops leaves the tree as its last attempt left it. Either way the
|
|
32
|
+
runner says so if a branch or tag the suite names moved or was deleted during the run (a ref given as a hash, relative
|
|
33
|
+
to another such as `main~1`, or by a name that is both a branch and a tag is not tracked).
|
|
34
|
+
|
|
35
|
+
## Suite file
|
|
36
|
+
|
|
37
|
+
JSON, kept outside the bench project's tree (a relative path is read from the directory you run `ahub` in). Unknown
|
|
38
|
+
fields, a missing `verify` and duplicate ids are refused with the task and the field named.
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"name": "parser-fixes",
|
|
43
|
+
"tasks": [
|
|
44
|
+
{ "id": "csv-quote", "title": "Fix quoted commas in src/csv.ts", "detail": "parse('\"a,b\",c') must give two fields",
|
|
45
|
+
"class": "implement", "owner": "codex", "ref": "3f2c1aa", "setup": "bun install --frozen-lockfile",
|
|
46
|
+
"verify": "bun test test/csv.test.ts", "timeout_s": 1800 }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
| Field | Meaning |
|
|
52
|
+
| --- | --- |
|
|
53
|
+
| `name` | the suite's label, kept in the records |
|
|
54
|
+
| `id` | the task's label, kept in the records |
|
|
55
|
+
| `title`, `detail` | what the hub proposes (never recorded) |
|
|
56
|
+
| `class` | the task class; `implement` when absent, since a hub without a model cannot triage |
|
|
57
|
+
| `owner` | the peer to propose it to; absent lets routing choose |
|
|
58
|
+
| `ref` | the commit the attempt starts from |
|
|
59
|
+
| `setup` | a shell command before the task is proposed (bounded, 5 min) |
|
|
60
|
+
| `verify` | a shell command run in the tree the agents left; exit 0 is a pass (bounded, 10 min) |
|
|
61
|
+
| `timeout_s` | 10 to 86400 seconds from the start of the attempt to approval |
|
|
62
|
+
|
|
63
|
+
`setup` and `verify` run in their own process group: at the bound, on Ctrl-C and when the command exits, everything in
|
|
64
|
+
that group is stopped, so nothing keeps writing into the next attempt's tree (a process that leaves the group, as a
|
|
65
|
+
daemon does, is not followed).
|
|
66
|
+
|
|
67
|
+
`name`, `id` and the arm label are stored; keep them free of anything private.
|
|
68
|
+
|
|
69
|
+
## Running
|
|
70
|
+
|
|
71
|
+
Start the peers of the configuration under test (`ahub claude`, `ahub codex`, `ahub pi`, their models, permission
|
|
72
|
+
modes and `routing.toml`), then:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
ahub bench run ../suites/suite.json --arm baseline --repeat 5
|
|
76
|
+
ahub bench run ../suites/suite.json --arm ripwire-on --repeat 5 --tasks b,a,c # --tasks also sets the order
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Only a person can run a suite (agent shells are refused). Each attempt resets the tree to `ref`, runs `setup`, proposes
|
|
80
|
+
the task as the console does, waits until it is approved or `timeout_s` passes, then runs `verify`. Outcomes:
|
|
81
|
+
|
|
82
|
+
- `pass`: approved and `verify` exited 0;
|
|
83
|
+
- `fail`: approved and `verify` exited non-zero or ran out of time (`verify timeout`);
|
|
84
|
+
- `timeout`: not approved in time. The run stops there, because an agent may still be working in the tree the next
|
|
85
|
+
attempt would reset; settle the open task on the board before the next run;
|
|
86
|
+
- `error`: the reset, `setup` or the proposal failed, the hub stopped, or a person interrupted the run (the reason is
|
|
87
|
+
one of a closed list).
|
|
88
|
+
|
|
89
|
+
Ctrl-C stops the run at its next check, also inside `setup` or `verify`: the attempt in progress is recorded as
|
|
90
|
+
`error` (`interrupted`), a task still open on the board is named, and the run reads `stopped`. An interrupt after
|
|
91
|
+
`verify` has finished keeps that attempt's outcome and drops its measures. A run whose runner died without its end
|
|
92
|
+
record reads `interrupted`.
|
|
93
|
+
|
|
94
|
+
## Store
|
|
95
|
+
|
|
96
|
+
`~/.agenthub/bench/<run id>.jsonl` (directory 0700, files 0600), one file per run, shared by every bench project on the
|
|
97
|
+
machine. Schema `agent-hub.bench/v1`:
|
|
98
|
+
|
|
99
|
+
- `run` header: suite name and SHA-256 of the suite file, arm, fingerprint (the peers attached at the start, with the requested model and
|
|
100
|
+
permission mode where the hub's status reports them: today the model for Pi only; the SHA-256 of `routing.toml`; the
|
|
101
|
+
hub version), task order, repeats, the runner's process.
|
|
102
|
+
- `attempt`: task id, repeat, outcome, error reason, verify exit code, hub task id, start, end, duration, and, for an
|
|
103
|
+
approved task, its #247 measures (tokens, wall and active time, review rounds, changes requested, failed checks, first
|
|
104
|
+
pass, turns, files changed, models). They are read once the turns started since the task's proposal have ended (the
|
|
105
|
+
runner waits only while their peers are still busy or paused, at most 10 minutes), so the turn that approved the task is
|
|
106
|
+
counted; that wait is not part of the attempt's duration. A timeout or an error has none, nor has an attempt whose
|
|
107
|
+
wait was interrupted, ran out with the turn still open, or could not read the hub's status while it was open.
|
|
108
|
+
- `end`: end time, and `stopped` (`timeout`, `interrupted`, `error`) when the run did not finish.
|
|
109
|
+
|
|
110
|
+
## Reading results
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
ahub bench list # runs on this machine
|
|
114
|
+
ahub bench status # the run in progress
|
|
115
|
+
ahub bench report <run> [--json] # per task and overall
|
|
116
|
+
ahub bench compare baseline ripwire-on [--suite parser-fixes] [--json]
|
|
117
|
+
ahub bench export --format csv
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Measures: pass rate and median wall time (over attempts that did not end in `error`), first-pass rate, median tokens
|
|
121
|
+
and mean rework (over approved attempts). `compare` takes run ids or arm labels (an arm means all its runs that are
|
|
122
|
+
over, a run a timeout stopped included, since each attempt carries its own outcome), sets the first as the baseline and
|
|
123
|
+
gives each other arm's difference with a 95% bootstrap interval (1000 resamples, fixed seed, so the same records give
|
|
124
|
+
the same interval). Below 5 attempts counted for a measure in either arm, its difference is marked inconclusive. Runs of
|
|
125
|
+
different suites, or of different versions of one suite file, are not compared unless you pass `--mixed`; name runs by
|
|
126
|
+
id to compare within one version. Agents may
|
|
127
|
+
read all of this. CSV columns: run, suite, arm, task, repeat, outcome, error, verifyExit, hubTask, startedAt, endedAt,
|
|
128
|
+
ms, tokens, wallMs, activeMs, reviewRounds, changesRequested, checkFailed, firstPass, turns, filesChanged, models.
|
|
129
|
+
|
|
130
|
+
The dashboard's Benchmarks section shows the latest runs with their progress (the attempt in progress, elapsed time and
|
|
131
|
+
attempts left while one runs) and, per suite with two or more arms, a bar per arm and measure with its number as text.
|
|
132
|
+
|
|
133
|
+
## A fair comparison
|
|
134
|
+
|
|
135
|
+
- Change one thing per arm (a model, a permission mode, an add-on, a routing rule) and keep the suite, the refs and the
|
|
136
|
+
peers otherwise the same; the fingerprint records the routing file, the hub version and what the status reports of
|
|
137
|
+
each peer, so note the rest (models, permission modes, add-ons) in the arm label.
|
|
138
|
+
- Repeat: agents are not deterministic. Five attempts per task and arm is the floor for a comparison; more narrows the
|
|
139
|
+
interval.
|
|
140
|
+
- Randomize the order per run with `--tasks` (for example `--tasks "$(jq -r '.tasks[].id' ../suites/suite.json | shuf | paste -sd, -)"`)
|
|
141
|
+
and interleave the arms' runs, so quota, time of day and provider load do not line up with one arm.
|
|
142
|
+
- Keep `verify` independent of the agents' own tests where you can: a hidden test the agents never see measures the
|
|
143
|
+
outcome, not their opinion of it.
|
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
|
|
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,20 +37,27 @@ 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),
|
|
38
|
-
| `task` | `id`, `event` (the board history event, e.g. `proposed`, `assigned`, `done`, `check failed`, `blocked`, `ready`), `by`, `state`, `owner`, `reviewer`, `class`, `pii` |
|
|
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. |
|
|
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 |
|
|
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
|
+
| `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 |
|
|
40
45
|
| `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 |
|
|
46
|
+
| `context_pressure` | `peer`, `source` (`claude_statusline`, `codex_token_usage`), `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 |
|
|
41
47
|
| `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) |
|
|
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) |
|
|
48
|
+
| `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) |
|
|
43
49
|
|
|
44
50
|
Token usage by adapter:
|
|
45
51
|
|
|
46
|
-
- Kimi (ACP `usage_update`): recorded
|
|
52
|
+
- Kimi (ACP `usage_update`, and the usage a `session/prompt` result carries): recorded when it holds a checked
|
|
53
|
+
total or an input/output pair. The `{used, size}` shape Kimi sends is context occupancy and is not recorded
|
|
54
|
+
(issue #285 tracks showing Kimi's usage).
|
|
47
55
|
- Codex (app-server `thread/tokenUsage/updated`): recorded as the growth of the thread's running total, so
|
|
48
56
|
compaction estimates, usage-limit refreshes and the replay to a reattaching connection add nothing. A thread
|
|
49
57
|
started under the hub counts from zero; a resumed thread's first update is its history and only sets the
|
|
50
58
|
baseline. The model call of a compaction itself is real usage and counts.
|
|
59
|
+
- Pi: the assistant usage its extension forwards at each `message_end`, tool-loop messages included, is recorded as
|
|
60
|
+
increments, each usage id once; a message without a usable count adds nothing.
|
|
51
61
|
- 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.
|
|
52
62
|
- 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.
|
|
53
63
|
- `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.
|