@staix/agent-hub 0.30.0 → 0.31.0
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 +39 -0
- package/README.md +13 -7
- package/docs/agent-notes/README.md +1 -0
- package/docs/agent-notes/adapters.md +5 -2
- package/docs/agent-notes/budget.md +1 -0
- package/docs/agent-notes/bus.md +9 -4
- package/docs/agent-notes/daemon.md +15 -8
- package/docs/agent-notes/local-worker.md +1 -0
- package/docs/agent-notes/tasks.md +7 -3
- package/docs/agent-notes/tests.md +6 -4
- package/docs/design/adapters.md +52 -0
- package/docs/design/agent-hub-architecture.md +46 -0
- package/docs/design/bus.md +139 -1
- package/docs/design/task-board.md +82 -0
- package/docs/dogfood.md +22 -4
- package/docs/events.md +3 -0
- package/docs/memory.md +17 -1
- package/docs/operations.md +307 -78
- package/docs/profiles.md +35 -0
- package/docs/quickstart.md +18 -5
- package/docs/security.md +154 -60
- package/docs/specs/2026-09-19-agent-hub-design.md +84 -14
- package/docs/specs/2026-09-19-multi-project-design.md +7 -2
- package/docs/verification/2026-11-04-1171-channel-client-evidence.md +31 -0
- package/docs/verification/2026-11-05-0.31.0.md +182 -0
- package/docs/verification/2026-11-05-claude-implementer-2.1.318.md +70 -0
- package/docs/verification/2026-11-05-hardcoded-values-classification-rules.md +257 -0
- package/docs/verification/2026-11-05-hardcoded-values-ratchet.md +55 -0
- package/docs/verification/2026-11-06-rooms-presence-cost.md +152 -0
- package/docs/verification/claude-shell-wrapper-native.py +1 -1
- package/docs/verified.json +14 -14
- package/package.json +3 -4
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +4393 -3609
- package/src/adapters/acp.ts +5 -4
- package/src/adapters/channel-peer.ts +72 -3
- package/src/adapters/claude-channel-evidence.ts +52 -0
- package/src/adapters/claude-channel.ts +61 -18
- package/src/adapters/claude-implementer.ts +29 -7
- package/src/adapters/claude-print-child.ts +12 -2
- package/src/adapters/codex-appserver.ts +13 -12
- package/src/adapters/compat.ts +3 -3
- package/src/adapters/definitions/builtin-metadata.ts +13 -13
- package/src/adapters/definitions/qwen.ts +1 -0
- package/src/adapters/local-worker.ts +61 -18
- package/src/adapters/pi.ts +48 -6
- package/src/adapters/registry-types.ts +12 -0
- package/src/adapters/registry.ts +2 -0
- package/src/adapters/reviewer-print.ts +50 -11
- package/src/broker/server.ts +44 -13
- package/src/broker/store.ts +38 -8
- package/src/cli/answer-args.ts +50 -0
- package/src/cli/attention.ts +17 -2
- package/src/cli/backlog-return-args.ts +37 -0
- package/src/cli/console-question.ts +110 -0
- package/src/cli/console-state.ts +77 -16
- package/src/cli/console.ts +54 -1
- package/src/cli/handoff-args.ts +15 -5
- package/src/cli/handoff-output.ts +15 -2
- package/src/cli/help.ts +8 -5
- package/src/cli/identity.ts +13 -3
- package/src/cli/main.ts +128 -16
- package/src/cli/memory.ts +107 -3
- package/src/cli/output.ts +29 -6
- package/src/cli/pet.ts +4 -4
- package/src/cli/results-args.ts +30 -0
- package/src/cli/runtime-version-warning.ts +50 -0
- package/src/cli/status-lines.ts +41 -1
- package/src/cli/tail-render.ts +1 -0
- package/src/cli/upgrade-interactive.ts +14 -2
- package/src/hub/action-class.ts +9 -1
- package/src/hub/board.ts +369 -18
- package/src/hub/bus.ts +493 -53
- package/src/hub/completion-detail.ts +50 -0
- package/src/hub/completions-store.ts +168 -0
- package/src/hub/completions.ts +120 -0
- package/src/hub/config-apply.ts +7 -5
- package/src/hub/config-reload.ts +10 -1
- package/src/hub/control-client.ts +40 -4
- package/src/hub/daemon.ts +1569 -134
- package/src/hub/delivery-failure.ts +14 -0
- package/src/hub/delivery-hints.ts +14 -4
- package/src/hub/delivery-journal.ts +751 -42
- package/src/hub/delivery-superseded.ts +4 -1
- package/src/hub/envelope.ts +3 -3
- package/src/hub/events.ts +6 -0
- package/src/hub/handoff.ts +145 -6
- package/src/hub/hub-tools.ts +56 -7
- package/src/hub/manager.ts +59 -3
- package/src/hub/memory-policy.ts +44 -1
- package/src/hub/memory-sync-policy.ts +109 -0
- package/src/hub/memory-sync-state.ts +145 -0
- package/src/hub/no-writes.ts +1 -1
- package/src/hub/operational-settings.ts +11 -2
- package/src/hub/operational-view.ts +12 -1
- package/src/hub/peers.ts +25 -2
- package/src/hub/planning.ts +6 -1
- package/src/hub/question-desk.ts +201 -0
- package/src/hub/question-detach.ts +6 -0
- package/src/hub/questions.ts +260 -0
- package/src/hub/research-evidence.ts +106 -0
- package/src/hub/restart.ts +14 -0
- package/src/hub/review-anchor.ts +181 -0
- package/src/hub/review-brief.ts +13 -0
- package/src/hub/review-comments.ts +115 -0
- package/src/hub/review-findings.ts +7 -2
- package/src/hub/review-material.ts +76 -0
- package/src/hub/routing.ts +3 -1
- package/src/hub/task-sweep.ts +2 -0
- package/src/hub/tasks.ts +577 -63
- package/src/memory/capture.ts +5 -2
- package/src/pet/config.ts +25 -13
- package/src/rooms/link.ts +209 -21
- package/src/rooms/remote-peer.ts +186 -19
- package/src/rooms/service.ts +338 -22
- package/src/rooms/store.ts +437 -65
- package/src/rooms/wire.ts +24 -6
- package/src/ui/index.html +138 -7
- package/src/ui/needs.ts +10 -4
- package/src/ui/plugin-questions.ts +56 -0
- package/src/ui/plugin-results.ts +81 -0
- package/src/ui/plugin.ts +207 -27
- package/src/pet/window.html +0 -114
- package/src/pet/window.jxa.js +0 -122
- package/src/pet/window.ts +0 -154
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@ 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.31.0
|
|
8
|
+
|
|
9
|
+
- Delivery failures carry one bounded diagnostic cause: peer offline, adapter refused, push error, turn aborted or timeout (#1178, PR #1224, task 714). `ahub queue show` and JSON listings expose it without message text; legacy rows say when no cause was recorded. Status shows each peer's unresolved failed count and the discard next step, excluding explicitly resolved rows; inspect the causes with `ahub queue list` or `show`. Causes do not authorize replay or alter settlement.
|
|
10
|
+
- Record one convention for hub length bounds and truncations and relabel 44 existing entries by name (PR #1213, task 739, Refs #1180). Peer/model/cross-project text bounds are safety floors, wire/config shape bounds are contracts, and own-list/batch/display caps are operational. This records the calibrated rule without classifying a new baseline batch; #1180 remains open.
|
|
11
|
+
- Cross-project handoff failures are named and repairable (#1203 T3, task 734, PR #1215). `ahub task handoff --list` shows each handoff's age and whether its target is reachable, unreachable (with the cause and the command to run) or moved (the project id is no longer registered), and `--sync` asks every target now, for at most 20 seconds: with slow or down targets it prints the list so far and "catch-up still running for N targets" instead of timing out. A target on another wire version is refused as `older_target` with the upgrade step, an instance mismatch as the new code `mismatch`, wire mismatch names the upgrade step, while instance mismatch names `ahub projects` and `ahub up` only if the intended target is not running. A failed notify or catch-up writes one `hub.log` line per target and cause every 15 minutes instead of nothing. A catch-up request the target does not answer now counts as a failure (the target reads unreachable) instead of clearing it, and `--sync` says `synced: false` when its pass fails. Completes #1203: T1 made the request idempotent, T2 made the follow-back converge, T3 makes failures visible.
|
|
12
|
+
|
|
13
|
+
- A cross-project handoff's follow-back now converges (#1203 T2, #733). Each state event carries the target task's flow revision, and the origin ignores one that is not newer, so an `in_review` that arrives after `approved` cannot reopen the row. The origin's catch-up runs every 5 minutes and when a target restarts, over every open handoff with no 200-row cap, so a state change that missed the first notify arrives within one period. A reviewer's `changes requested` and a reopen are now reported to the requesting peer. Additive feature `task_handoff_seq`: an older origin is sent no sequence and not the two new events.
|
|
14
|
+
- Classify the first 65 baseline keys of the hard-coded values inventory by mechanical rules (#738, refs #1180): 43 `*_MS` constants, 8 timer-call literals, 4 reduce and loop starts and 3 config-shaped defaults as operational, and 3 benchmark error-text truncations and 4 `*_MS` constants that bound replay, revocation or rate as safety floor. The rules and their calibration are in `docs/verification/2026-11-05-hardcoded-values-classification-rules.md`; that batch reduced the baseline to 1157 keys. The remaining baseline is handled by later calibrated batches; PR #1213 records the bound convention and existing-label corrections.
|
|
15
|
+
- Gate the hard-coded values inventory (#719, refs #1180). Nothing ran `scripts/inventory-hardcoded-values.ts` after #764, so unclassified keys grew from 22 to 1438 unnoticed. `bun scripts/inventory-hardcoded-values.ts --check` now runs in `scripts/check.sh` and fails on any unclassified key outside `scripts/inventory-hardcoded-values.baseline.json`, which only shrinks. 36 stale keys were removed and 216 keys were classified from precedent in the file; the other 1222 stay unclassified in the baseline, and #1180 stays open for them (cause, calibration and the batch rules: `docs/verification/2026-11-05-hardcoded-values-ratchet.md`).
|
|
16
|
+
- A cross-project handoff that times out no longer reads as refused, and a retry no longer files a second task (#1203 T1, #731). The origin sends each request with a `handoffKey`; the target stores it with the task in one board transaction and answers a replayed key with the task it already made. The origin sends the same key once more, asks the target by key, and otherwise reports `outcome_unknown` with the key to retry with (`ahub task handoff --key <uuid>`, `handoffKey` in `hub_task_handoff`). The follow-back message now names the target's task (`refs.handoff`) instead of a local task number, so a cancelled or approved local task of the same number cannot drop it. Additive feature `task_handoff_key`: an older target is sent no key and keeps today's behavior. The periodic catch-up, sequence numbers and `--list` reachability are the next two tasks of #1203.
|
|
17
|
+
- A redirect to `/dev/null` or `/dev/tty` is no longer a destroy (#725, #1193). `2>/dev/null` on an otherwise read-only command raised an "always-ask: destroy" prompt whenever `action_policy.floor` was on. Every other target keeps its class, including `/dev/stdout`, `/dev/stderr` and `/dev/fd/<n>` (on Linux they reopen the file fd 1 or 2 already points at and a `>` truncates it), `/dev/sda`, a lookalike such as `/dev//null`, and any real target beside a device one. The redirect still closes the read-only git fast path. A release is needed before the installed plugin picks it up.
|
|
18
|
+
- Tell the person to update the global CLI and the plugin after `ahub upgrade` (#718, #1179). `ahub upgrade` upgrades one project's daemon only, so a stale CLI later failed with a message that did not say why. A completed or scheduled upgrade now ends with `bun add -g @staix/agent-hub@<version>` and `ahub setup` when the CLI or the plugin is older than the daemon it installed (the comparison `ahub doctor` already prints), the 0.30.0 upgrade note carries the step, `release-prepare` writes it into every new `Upgrading to <v>` section, and `release-tag` refuses a release whose section lacks it.
|
|
19
|
+
- Claude Code 2.1.318 is a receipt-verified headless implementer version (#888): the model-free permission-transport and host shell-wrapper producers pass on it with 2.1.316 as the control (`docs/verification/2026-11-05-claude-implementer-2.1.318.md`). 2.1.317 (installed, not run here) stays unverified.
|
|
20
|
+
- Move the floating pet to acon Mini (#1012). `ahub pet --window [--close]` and the retired `--palette <value>` now print the `acon ui` pointer and exit 0 without writing configuration; the standalone JXA host, assets, automatic startup and console `o` shortcut are removed. Existing `pet.window` is ignored with one notice, and `pet.palette` remains validated compatibility input with no rendering effect. Terminal line/mini/full pets remain, and the existing acon plugin **Console message** view supplies the composer.
|
|
21
|
+
- Questions for the person (#1053 T2-T6: #1170, #1183). The new `hub_ask` tool lets a peer ask the person structured questions. They are stored in a `questions` table with one compare-and-set terminal transition, and the call waits a bounded time per adapter (`questions.wait_ms`) before it answers `pending`. A later answer arrives as an envelope of kind `answer`, and an unanswered question expires after `questions.ttl_s`. Only the person can answer: with `ahub answer`, with the console keys, or through the acon inbox form path. An open question is a need in `ahub status` and the console. Question and answer text stay in hub.db and in the envelope sent to the asker; hub.log, events.jsonl, the console tail, memory capture and the progress observer stub them. AC1 live receipts and the T1 timeout measurement stay open in #1053.
|
|
22
|
+
- Settled runs and completion receipts (#1054: #1185, #1186, #1190, #1195). A run the person started settles into an id-only completion record with outcome `done`, `failed`, `limit` or `interrupted`. That happens when the adapter's settle signal fires and no continuation, permission, question, child, check or held delivery remains. A result stays unread until the person marks it read with `ahub results [--unread] [--read <id> | --read-all | --unread-last]` or in the dashboard's Results section. An unread result is a `watch`-level need. The acon inbox shows it only to a console that advertises `inbox.result` and `action.meta`. Receipts are accepted only from the console role with the person's verified meta, and a snapshot never writes one. Canary tests keep result text out of hub.db rows, hub.log and events.jsonl. A recovery target without the `completions` feature is refused while a run is open or a result is unread.
|
|
23
|
+
- Anchored review findings and person comment packages (#1056: #1181, #1192, #1205). Agent findings and console comments carry immutable acon anchors. A confirmed Send delivers one structured package to the task owner, and drafts start no agent turn. A retry reuses the saved package identity and never replays a published effect. Review-derived text reaches only its named task or current input, and that state survives a restart through session-bound receipts. Sessionless peers now receive review-scoped input without capture, progress, titles or memory writes. The devDependency `@staix/agent-console` is pinned at 0.1.3, and the hub imports only its type contract. #1056 stays open.
|
|
24
|
+
- Mark person-run backlog legs (#1176, #1188). `ahub backlog person <id> <revision> true|false <reason...>` (or `needsPerson` in `plan`) sets or clears the mark, and only the person may do either. Auto-pull, offers, assignment, the sweep and planner WIP never give a marked card to an agent. `ahub backlog list` shows `<peer> (person)` and `ahub status` shows `WAITING ON YOU`. An unmarked reserved card behaves as before.
|
|
25
|
+
- `ahub backlog return <task> <revision> <reason...> [--reserved <peer> | --unreserve]` passes `fields.reserved` to `hub_backlog_return_untouched` (#1177, #1191). With neither flag it sends what it always did.
|
|
26
|
+
- Cross-project handoff: a handoff whose spec issue already has an open task on the receiving board is refused, with the existing ids, unless the sender passes `duplicate_ok` with a reason (`ahub task handoff --duplicate-ok <reason>`). The receiving conductor may cancel an unaccepted handoff task with a reason, which the follow-back reports (#1175, #1182).
|
|
27
|
+
- Research owners record claim-bound evidence with `hub_evidence_record` (#931 T1, #1169). `hub_task_show` and `ahub task show --json` now expose text-free `claims: [{slot, claimId, claimRevision}]`, and recovery carries the new `research_evidence` feature. T2 and T3 of #931 stay open.
|
|
28
|
+
- Memory exact-pins `@staix/agent-mem` 0.21.0 (PR #1228, task 743, Refs #1173). The person-only `ahub memory declined [--json]` uses bounded physical-row inspection to list the complete declined IDs and safe rule categories, including forgotten originals; incomplete input remains explicit. Complete-set fingerprints distinguish changed failures. Existing standalone-policy screening of long writes and promotion, refusal before fallback and log-once behavior remain. Status and the dashboard project verified withheld/exported counts from a successful SDK placement result, but automatic v3 sync is unsupported until agent-mem#281 supplies supported activation and fresh host attestation; no activation proof is fabricated. #1173 remains open.
|
|
29
|
+
- Rooms delivery (#1204 T1-T2: #1206, #1214, #1216). Ephemeral presence is coalesced for supporting brokers, and a broker `queue_full` is retried with backoff from 250 ms to 30 s instead of waiting for timeout. Room messages resend after a reconnect, a restart or an acknowledgement timeout, under the same application identity and the original TTL. Admission receipts are committed to the durable outbox before the chat transport ACK. Transient pause and resume no longer persist an unchanged bus snapshot, which cut the 1,000-presence-flip test from 1.8 s to 0.5 s. T3 and T4 of #1204 stay open.
|
|
30
|
+
- A Claude session that inherits `AGENTHUB_CHANNEL=1` without the parent's native launch evidence attaches tools-only and reads its messages through `hub_inbox`, instead of dropping every push while reporting it accepted (#1171, #1184). Status reports the attachment mode and a closed evidence label. Evidence: `docs/verification/2026-11-04-1171-channel-client-evidence.md`.
|
|
31
|
+
- An accepted push-channel delivery that is never confirmed moves to `needs_review` after `bus.accepted_settle_ms` (default 10 minutes, 1 second to 24 hours) (#1172, #1187). The configured conductor gets one ids-only notice, and `hub_inbox` shows the session's own accepted and needs-review metadata without bodies. The expiry never replays the work or stops native execution.
|
|
32
|
+
- The acon inbox snapshot asks the running projects concurrently, each within a 2 s budget. It serves a fresh or briefly stale cached result and returns at most 50 items as `{ items, more }`, decisions first (#1196: #1200, #1209). #1196 stays open until the live console is measured.
|
|
33
|
+
- `Tasks.splitBacklog` validates every part's plan sources before the board write, so an unregistered `src:` id is refused and nothing is written (#1161, #1165).
|
|
34
|
+
- A second channel session no longer replaces the attached one (#1174, #1217). It stays connected as a non-owning standby, and `ahub status` lists each waiting connection's id, reported PID and age. A person hands the peer over with `ahub peer takeover <peer> [standby-id]` or `o` in the console's Peers panel; the old session's hub connection is detached, its native process keeps running, and its next hub tool call says the person detached it. When the owner leaves, the oldest standby attaches. Standby connections run no peer operation, and their ids and PIDs are display only. A plugin that does not opt in (an older plugin, or tools mode) keeps the newest-hello behavior.
|
|
35
|
+
- Rooms presence: an ordinary presence flip reuses a pending coalesced republish instead of sealing a new frame per flip, a chat reserves its FIFO slot before awaiting earlier presence work, and a closed bus publishes nothing (#1204, #1222). This cut a macOS CI test from about 18 s to under 1 s. Evidence: `docs/verification/2026-11-06-rooms-presence-cost.md`.
|
|
36
|
+
- Tests: `untilSeen` in `test/control-client.test.ts` waits on time, not on 200 event-loop turns (#1163, #1166). A failing fake-ripwire call now reports where its time went (#1197).
|
|
37
|
+
- 0.31.0 N-1: 0.30.0, the latest published 0.30.x (`@staix/agent-mem` 0.20.1, protocol 17). `test/fixtures/release-n-1` now pins it.
|
|
38
|
+
- Wire/protocol: unchanged at 16 through 17, with descriptor floor 16. The new daemon features (`questions`, `completions`, `review_comments`, `review_observer_input_state`, `accepted_delivery_settlement`, `research_evidence`, `task_handoff_key`, `task_handoff_seq`, `peer_takeover`) are additive, and a client sends what needs them only to a daemon that advertises them.
|
|
39
|
+
- Files: the hub.db schema version stays at 2 and format floors remain enforced, but this release adds the nullable `deliveries.failure_cause` column. The N-1 recovery reader ignores the extra diagnostic snapshot key; legacy snapshots may omit it. Other new tables and columns remain additive. A recovery target without `completions` is refused while a run is open or a result is unread.
|
|
40
|
+
- Memory: this release pins agent-mem 0.21.0 instead of 0.20.1. [Agent-mem's 0.21.0 N-1 notes](https://github.com/STAIxBWLB/agent-mem/blob/v0.21.0/CHANGELOG.md) compare against 0.20.3, not the 0.20.1 bundled by hub 0.30.0. They retain curated/capture/hook-spool/candidate/work-list/setup receipt v1/v2 formats, shared-v1/generation/v1-v2 shard sync, index version and index filename. Their activated-v3 refusal proof applies to 0.20.3; this record does not extend that test claim to 0.20.1. V3 activation has no command/writer in that package release, and the hub's automatic v3 sync also awaits supported fresh attestation in agent-mem#281.
|
|
41
|
+
- CLI: the 0.30.0 control clients interoperate. `ahub answer`, `ahub results`, `ahub backlog person`, `ahub memory declined`, `backlog return --reserved | --unreserve` `task handoff --duplicate-ok | --key` and `ahub peer takeover` need a 0.31.0 runtime.
|
|
42
|
+
- MCP tools: the 0.30.0 channel plugin interoperates but has no `hub_ask` or `hub_evidence_record`. Ceilings: a handoff that a 0.30.0 origin sends to a 0.31.0 target is refused when its spec issue already has an open task there, because a 0.30.0 sender cannot pass `duplicate_ok`. A split whose part plans name unregistered `src:` ids, which 0.30.0 stored, is refused. A 0.30.0 plugin does not opt in to standby, so its second session still replaces the attached one; a 0.31.0 plugin against a 0.30.0 daemon keeps the prior standby reconnect behavior.
|
|
43
|
+
- 0.30.0 N-1 (recorded after release on 2026-11-04, #1164): 0.29.0, the latest published 0.29.x. Wire/protocol: 16 through 17, including descriptor floor 16. Files: existing database and recovery format floors remain enforced; incompatible newer formats refuse. CLI: previous-minor control clients interoperate; new commands and flags require their runtime capabilities. MCP tools: the previous-minor channel plugin interoperates, but 0.30.0 (#1151) requires registered `src:` ids with 16 hexadecimal digits in `plan.sources` for `hub_task_propose` and `hub_task_accept`. Free-form citation/DOI/URL ids accepted by 0.29.0 are refused. Accept also revalidates the stored plan when no new plan is sent, so an existing task with such ids must replace or remove unsupported source ids before acceptance. This is a post-release record, not a statement carried by the v0.30.0 tag or npm tarball.
|
|
44
|
+
- Add frozen previous-minor runtime interoperability tests alongside the retained protocol-floor fixture, and document fixture maintenance and release compatibility statements (#1164).
|
|
45
|
+
|
|
7
46
|
## 0.30.0
|
|
8
47
|
|
|
9
48
|
- Worktree shelf (#1057: #1156, #1158, #1159, #1160). The hub derives at read time which worktrees are live and which are settled (its PR merged or closed with no newer work, or quiet for `worktrees.quiet_days`, default 7), and the person can Park or Pin a row. `ahub worktrees [--json]` lists both; `ahub worktrees tidy` previews what is safe to remove, with every refused row's reasons, and `ahub worktrees tidy --confirm <fingerprint>` removes only those, in the person's own CLI process, with `git worktree remove` and `git branch -d` and never a force flag. Dirty, locked, unpushed, unmerged, bound-to-open-task, main and instance worktrees are refused. The dashboard's Agents panel gets a Worktrees section with Park, Pin and an explicit "Refresh PR states" (at most 8 PRs); no read makes a GitHub call, and an inbox alert fires above `worktrees.alert_over` (default 10). The shelf adds no removal to the daemon: it still removes only an empty hub-made task worktree on decline or cancel (0.29.0).
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Code, Codex, Kimi Code, Pi and a hub-owned local worker (plus Qwen Code, Jcode a
|
|
|
5
5
|
Antigravity if you want them) share a message bus, a task board and a memory, each
|
|
6
6
|
connected through its own native interface.
|
|
7
7
|
|
|
8
|
-
Status: 0.
|
|
8
|
+
Status: 0.31.0, control protocol 17 (accepts 16). Bun + TypeScript, one loopback daemon per
|
|
9
9
|
project. Site: [agent-hub.staix.net](https://agent-hub.staix.net).
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
@@ -30,9 +30,11 @@ ahub ui # the web dashboard
|
|
|
30
30
|
|
|
31
31
|
`ahub setup` also registers agent-hub as a plugin of the family console
|
|
32
32
|
([agent-console](https://github.com/STAIxBWLB/agent-console), `acon ui`): the same dashboard
|
|
33
|
-
appears as a panel for every registered project, and pending approvals
|
|
34
|
-
to you
|
|
35
|
-
|
|
33
|
+
appears as a panel for every registered project, and pending approvals, reviews assigned
|
|
34
|
+
to you, questions agents ask you and, where acon supports them, unread results reach the
|
|
35
|
+
console inbox. The plugin is `@staix/agent-hub/ui-plugin`; it uses the dashboard manager's
|
|
36
|
+
own listing and forwarding, and an answer, a result receipt or a review comment sent from
|
|
37
|
+
acon goes through a manager port of its own that needs acon's verified person.
|
|
36
38
|
The plugin also feeds the console's Space view (Flat, Room and 3D): agents become actors
|
|
37
39
|
at their desks, the task board's columns become zones, and task links and recent messages
|
|
38
40
|
become links. The scene carries labels and states only, never message bodies or details.
|
|
@@ -102,7 +104,10 @@ defaults. The
|
|
|
102
104
|
(`ahub schedule list`), a one-confirmation autonomy
|
|
103
105
|
preset (`ahub autonomy on|off|status`), a worktree shelf that lists live and
|
|
104
106
|
settled worktrees and removes the safe ones only on your confirmation
|
|
105
|
-
(`ahub worktrees`),
|
|
107
|
+
(`ahub worktrees`), questions an agent asks you with `hub_ask` (answered with
|
|
108
|
+
`ahub answer`, the console's `A` or the acon inbox), the results of runs you
|
|
109
|
+
started (`ahub results`), a person-requested handover of a peer to a waiting
|
|
110
|
+
standby session (`ahub peer takeover`), structural config
|
|
106
111
|
(instances, review, lifecycle) edited with a validated diff and a backup from the
|
|
107
112
|
console or a dashboard settings session, token usage and quota
|
|
108
113
|
history (`ahub usage` and the dashboard's Usage view), and `ahub upgrade`, which
|
|
@@ -113,7 +118,8 @@ defaults. The
|
|
|
113
118
|
Run `ahub upgrade` in a project while its hub is running (`--project` selects
|
|
114
119
|
another); other projects keep their version. Update the global CLI
|
|
115
120
|
(`bun add -g @staix/agent-hub@<version>`) and run `ahub setup` for the plugin
|
|
116
|
-
yourself
|
|
121
|
+
yourself; `ahub upgrade` names that step when either is older than the daemon it
|
|
122
|
+
installed. Read the notes for your version in
|
|
117
123
|
the [Quickstart](docs/quickstart.md) ("Upgrading to ...") and the
|
|
118
124
|
[Changelog](CHANGELOG.md) first: some releases need a one-time step, such as
|
|
119
125
|
refreshing an agent's MCP entry.
|
|
@@ -137,7 +143,7 @@ uses the home must run a version that understands the new layout.
|
|
|
137
143
|
[#258](https://github.com/STAIxBWLB/agent-hub/issues/258)).
|
|
138
144
|
|
|
139
145
|
Live checks that still need a real account or a person are listed in the
|
|
140
|
-
[release verification report](docs/verification/2026-11-
|
|
146
|
+
[release verification report](docs/verification/2026-11-05-0.31.0.md) and the
|
|
141
147
|
[smoke checklist](docs/smoke.md).
|
|
142
148
|
|
|
143
149
|
## Documentation
|
|
@@ -64,6 +64,7 @@ Background behind the 0.18.0 rules:
|
|
|
64
64
|
Background behind the 0.19.0 rules:
|
|
65
65
|
|
|
66
66
|
- Code-scope verdicts (#852, tasks.md): two review rounds showed that any classifier reading a `code` reviewer's unmet text files real defects, such as "the CI workflow is missing the trigger", as verification-only, so the evidence-only rule reads only the structured findings.
|
|
67
|
+
- VM closure dependencies (#1056, tests.md): review refresh and durable observer checks added cache, snapshot and scope dependencies to existing functions. Searches for the new dependency names missed fixtures that extracted their callers, causing ReferenceError failures or fail-closed paths that pre-empted the behavior under test.
|
|
67
68
|
- Policy-table fixtures (#843, tests.md): a fixture with a fixed list of keys or versions, which a planned `COMPAT` entry was not in, was reported twice in #843.
|
|
68
69
|
- Suite watchdog (#516, tests.md): a total-time kill cut suites that were still passing tests line by line (hosted ubuntu runs at 1080 s, the macOS leg of PR #857 at 1440 s), so a lost-progress bound became the hang detector and the total bound only its backstop.
|
|
69
70
|
|
|
@@ -29,7 +29,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
29
29
|
- Agent shell authority (#812, `classifyPeerCommand` in `src/cli/identity.ts`, `src/hub/operational-authority.ts`): the Allowed read forms live in `peerReadForms` and are admitted only when the daemon also admits the same form for an ordinary tools connection; the conductor family is the closed `conductorOperation` list, each admitted by `admitOperational` (role recheck, closed audit label, no argument text; `queue show`, a settings preview and the authorize handshake recheck through `operationalCurrent` without an audit record). Throw `OperationalAuthorityError` only from an authority fence: only it is audited as refused and gets the conductor or person guidance, and an ordinary operation failure keeps its own error (#839). A new CLI form is `console` until it is added to one of those tables with a test in `test/identity.test.ts`. #933: `console` is only trust, money, attribution and interactive terminals; a conductor form joins `CONDUCTOR_DAEMON_OPERATIONS` (its own daemon request calls `admitOperational`) or `CONDUCTOR_LOCAL_OPERATIONS` (its effect runs in the CLI, so the `admit` handshake is its one audit row, sent only after its `LOCAL_OPERATION_ARGS` check in `src/cli/main.ts` passed), never both; a form whose write records the person as actor (amem's `user`) or whose runner inherits the agent markers stays `console`, except the recovery-bearing daemon forms (`restart --yes`, `upgrade --yes --to <version>`, `recovery resume|abort`, #900), which also require their source-issued continuation proof (rule: `daemon.md`), and every reclassified form changes its daemon check in the same PR.
|
|
30
30
|
- A headless ACP Kimi or Codex whose mode lives in the native agent 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.
|
|
31
31
|
- Generic permission and generation dispatch in the daemon reads the optional `PeerAdapter` ports (`src/hub/peers.ts`, #667): `permissionModeState`, `setPermissionMode` (resolves only after native acknowledgement; changes approval frequency, never the sandbox), `sessionGeneration` and `checkpointUsesSessionGeneration`; never add a native-class predicate (`isAcpPeer`, `isCodexPeer`) for it. An absent `sessionGeneration` keeps the recovery-session/owner-token fallback; a present but undefined one is unknown and refuses. `BasePeer` installs no default generation, and only an explicit `checkpointUsesSessionGeneration = false` (Pi) skips the checkpoint generation fence. A `permissionModeState` of `unknown` or `unmanaged` refuses a change.
|
|
32
|
-
- Delivery, lifecycle and session dispatch in the daemon reads the same optional ports (#625 slices 2b-2c): `deliveryOnly` (absent or `false` permits `hub_delivery_retry`; `true` or a present `undefined` refuses), `hubOwned` (only `true` lets `peer_stop` stop the runtime), `pendingOwnedProcess`, `form` (a present `undefined` is unknown: `peer_stop` refuses, and a definition with `permission.unmanagedForms` treats it as a TUI), `captureResume`, `detachForRoster`/`resumeFromRoster`, `pullOnly`, `recoveryReady` and `nativeSession` (rules: `daemon.md`). An adapter the hub owns and may stop sets `hubOwned = true`, a delivery-only transport sets `deliveryOnly = true
|
|
32
|
+
- Delivery, lifecycle and session dispatch in the daemon reads the same optional ports (#625 slices 2b-2c): `deliveryOnly` (absent or `false` permits `hub_delivery_retry`; `true` or a present `undefined` refuses), `hubOwned` (only `true` lets `peer_stop` stop the runtime), `pendingOwnedProcess`, `form` (a present `undefined` is unknown: `peer_stop` refuses, and a definition with `permission.unmanagedForms` treats it as a TUI), `captureResume`, `detachForRoster`/`resumeFromRoster`, `pullOnly`, `recoveryReady` and `nativeSession` (rules: `daemon.md`). An adapter the hub owns and may stop sets `hubOwned = true`, a delivery-only transport sets `deliveryOnly = true`, and only a live push channel (`WsPeer`) sets `acceptedSettlement = "explicit"`, the one port that lets a peer settle or discard its own `accepted` row (`Bus`, #1172: `accepted` is transport acceptance only, and an unconfirmed row moves to `needs_review` after `bus.accepted_settle_ms`, never replayed); never add an `is*Peer` call for any of them (ratchet above).
|
|
33
33
|
- 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`.
|
|
34
34
|
- Codex planner threads (#525-6, `threadSandbox` in `codex-appserver.ts`, live from `isPlannerOnly` for a declared instance): every `thread/start`/`thread/resume` goes up with `sandbox: "read-only"` and is adopted only when its response positively confirms the `readOnly` policy (`confirmsReadOnlySandbox`). An unconfirmed or malformed reply, a reused RPC id, an `unavailable` policy (a planner with a declared worktree) or a throwing reader refuses that RPC and closes the TUI link before adoption or delivery, and `deliver`, `steer` and `steerText` recheck `threadSandboxReady` right before they send. Never count missing evidence as read-only, and never let a role change promote an unconfirmed thread.
|
|
35
35
|
- 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.
|
|
@@ -41,6 +41,7 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
41
41
|
|
|
42
42
|
## Turns, watchdogs and hub tools
|
|
43
43
|
|
|
44
|
+
- Delivery failure diagnostics (#1178): carry the optional typed `failureCause` from `src/hub/delivery-failure.ts` through `DeliveryReceipt` and legacy `onFailed`, including pending RPC, steer and watchdog paths. Classify from structured host facts, never error, reason or body text. Channel receipt causes are assigned by the host, never trusted from the caller. A known cause never downgrades an uncertain delivery to `failed_safe` or changes settlement.
|
|
44
45
|
- A peer must set `busy` synchronously inside `deliver()`, otherwise the bus drains the next envelope into a running turn (Kimi answers `turn.agent_busy`). Outside a turn the adapter accepts an occupancy diagnostic (a `context-used` `usage_update` naming its current session) and nothing else: no token total, no chunk, nothing unnamed or for an offline peer (#285).
|
|
45
46
|
- A watchdog-cancelled turn still reports later. Anything a turn does on completion must check it is still the current turn (`turn` generation in `acp.ts`, `activeTurns` in `codex-appserver.ts`). A rejected prompt other than a recognised quota refusal, or an abnormal prompt end, reports through the adapter's `onTurnFailure` (same shape in `acp.ts` and `pi.ts`), even when ACP streamed partial text; a stale cancelled turn never reports it; a rejected watchdog-cancelled ACP prompt during its journal-less wait is discarded with one log line, not requeued as on the earlier adapter.
|
|
46
47
|
- After an ACP watchdog cancels a prompt on a bus without a journal, keep the peer busy with its inactivity watchdog cleared until that prompt settles; suppress its turn-scoped updates and permissions while still accepting session-bound occupancy diagnostics. Bound the wait by the existing 30 s ACP handshake deadline, go offline, never idle, with queued work retained if it expires, and revoke the wait on stop or transport loss; a durable delivery keeps its existing offline/needs_review path.
|
|
@@ -121,6 +122,8 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
121
122
|
- Bind every running print review to its immutable envelope head and review revision. Recheck `Tasks.reviewBindingCurrent` for task-MCP operations, legacy print reads and the verdict authority after awaits; an old child never gains a replacement request. A committed verdict acknowledgement does not authorize subsequent reads or verdicts.
|
|
122
123
|
- `ReviewerPrintPeer` (`src/adapters/reviewer-print.ts`, #223, #519; `ClaudePrintPeer` is its Claude wrapper) runs one fresh child per review and clears `reviewing` at the leader's exit and first in `kill()`. A stop or watchdog settles `needs_review` at once, before the kill; `completed`, `idle` and `offline` come only after the whole process group is confirmed gone, and a failed cleanup keeps the child handle and stays `busy`. The daemon's `reviewAuthority` compares `reviewToken`: never widen it.
|
|
123
124
|
- Every print child (reviewer and Claude implementer) is spawned, bounded and stopped only through `OwnedPrintChild` (`src/adapters/claude-print-child.ts`): its `done` settles only after the whole group is confirmed gone, and a failed stop rejects with `PrintChildCleanupError` while the owning peer keeps the handle. Never add a second spawn path to an adapter.
|
|
125
|
+
- Owned input observers (#1056): `beforeInput`/`inputReleased` (`OwnedInputObserver` in `src/adapters/claude-print-child.ts`) carry review-observer provenance only. `observerSessionId` (Claude implementer, reviewer print, local) is never `nativeSession` and never grants budget, resume, permission or task authority. `beforeInput` runs with the final target session before any prompt reaches the child or model, and a throw releases no prompt; `onInputReleased` fires only after stdin flushed for the current generation, and an observer throw never changes settlement. A fresh reviewer calls `inputFinished` only after its group is confirmed gone, its task MCP is closed and launch cleanup succeeded.
|
|
126
|
+
- Retained standby (#1174, `WsPeer` in `src/adapters/channel-peer.ts`): an opt-in (`retainStandby`) hello while another session holds the peer is kept as one of at most `MAX_STANDBY_SESSIONS` (8) standbys; `standbySessions` ids and PIDs are display only, never native-session provenance or authority. Ownership changes only through `takeover` (the person's `peer_takeover`, or the oldest standby when the owner leaves), guarded by the host's `channelTakeoverAdmission` and `claim.isCurrent()` across the async preface; the old socket closes 4405 (opt-in) or 4403 "detached by the person" and its native process is not stopped. A standby socket runs no peer operation until `ownsSocket` confirms it. A hello without the opt-in keeps newest-hello replacement.
|
|
124
127
|
- A reviewer backend is a fixed builtin `ReviewerBackend` in a definition's `reviewer`, never config. Its `prepare` must return a complete isolation receipt (matching backend id, binary, version, profile, evidence and `readOnly` true, plus `strictMcp` and `ephemeral` true, or for a `nativePolicy` backend `enforcement: "native-policy"`, `freshSession` and `hubToolsOnly` true) before every child. A `taskMcp` backend refuses without the host's `taskMcp` port and gets only that task's MCP config, never the hub-wide one. `admit(env)` and `launch.current()` are rechecked right before spawn, and a `watchCurrent` launch is polled while the review runs: a change aborts it as `review policy changed`; a missing or failed proof refuses, never a weaker launch or a continuing native session. The daemon puts the reviewer on the bus only after `start()` proved isolation; a failed start or a `start_failed` report marks it in `reviewerUnavailable`, which routing reads as `failing`. An unavailable reviewer variant has no reviewer backend or launch code; `startPeerBody` refuses it before preflight, independently of any native TUI launcher.
|
|
125
128
|
- Report reviewer failures (`onUnavailable`: `start_failed`, `quota`, `no_verdict`) only after the child's group is confirmed gone and only while its generation is current: a person's stop owns its hold and never reroutes. Complete a normal no-verdict delivery only after the host confirms a current re-offer; an uncertain or failed fallback stays `needs_review`, with its `REVIEWER_FALLBACK_UNCONFIRMED` reason excluded from automatic moved-review settlement; ordinary superseded holds keep their existing rule. Log one bounded no-verdict line from exit code and allowlisted hub-tool metadata only, with full refusal privacy/secret screening before sanitization and truncation; retain the first full host refusal across native echoes, and withhold differing later host refusals rather than replacing or accumulating them; stale/private context is withheld. Record only allowlisted tool names and refusal text; never retain argument objects, task/diff fields or model-message events as trace data. `quota` comes only from the backend's structured `failure(event)`, never from vendor text. An NDJSON frame over 256 KiB or more than 4 MiB of output aborts the review as `needs_review`.
|
|
126
129
|
- Codex reviewer isolation (`src/adapters/codex-review-isolation.ts`): accept only a `codex-cli` version that compat `codex-review` admits (verified or the person's trust, #691), and still run every proof below after trust; pass `-c notify=[]` (legacy notify off) on every launch; prove model-free that Apps, plugins and hooks are disabled (`features list`), ambient MCP servers are replaced by disabled stubs, and the hub server alone is enabled with exactly `hub_review` and `hub_task_show`; approve exactly those two MCP tools per tool, never with a server-wide approval default; before marking a reviewer backend/version verified, record a successful model-backed hub tool call as well as the model-free inventory (inventory alone is not verdict-delivery proof); run the native sandbox fixture probe before every launch; hash the binary once and fence binary and config files by metadata. Credential denies come only from the bounded `HOST_CREDENTIAL_PATHS` and `HOST_CREDENTIAL_ENV` lists, denied at the lexical and canonical path; refuse glob syntax or a root inside the checkout. A root that is dangling or unreadable refuses too; the reason names only its path class (`~/.docker`, `$DOCKER_CONFIG`), never a path or contents. Never pass the control token in a reviewer child's argv, env or MCP config: keep the hub state dir in its `privatePaths`. A refused `prepare` carries a `causeClass` from the closed `REVIEWER_CAUSE_CLASSES` (`version`, `binary`, `mcp_inventory`, `plugins_hooks_apps`, `sandbox_proof`, `credential_root`, `timeout`, `unknown`; `reviewer-print.ts`, #722): `prepare` tracks the proof leg it is on, and `timeout` means a metadata command ran out of its 2 s or the 12 s budget, never an abort. The host builds the reason only through `reviewerUnavailableReason(cause, detail)`: a fixed sentence per class, plus a credential-root path class (`~/.docker: a dangling link`) as the only detail. The one exception is a failed `start()` whose error is a version refusal (`VERSION_REFUSAL`) or comes from a `nativePolicy` backend: the daemon shows that sanitized message, capped at 1000 characters (#691). `ReviewerPrintPeer` classes a missing or invalid receipt as `unknown`, a `launch.current()` change before spawn as `binary`, and any other start error as `unknown`. Keep admission refusals separate from start failures: skip only a stale-bound request with one task-id log and no reroute; reroute a still-current request whose policy changed as `no_verdict`, without marking the reviewer unavailable. The daemon writes that reason to the `reviewerUnavailable` map (read by routing and shown as the `reviewer` line of `ahub status`), and a `hub.log` line names the class. Never add native CLI text, a path, a version output or a token to the class, the reason or the log.
|
|
@@ -142,5 +145,5 @@ Scope: the Codex, ACP and Pi adapters and the Pi extension (turn `generation` an
|
|
|
142
145
|
- The class-budget choice is the hub's own option (`ACTION_BUDGET_OPTION`), answered to the agent as its own `allow_once`; only that choice calls `ActionBudgets.approve`, and a refusal answers cancelled. Never let an ordinary allow seed the ledger. Claude never consults the ledger. `ActionBudgets.approve` consumes a slot when another approval landed since its challenge was issued (requests are concurrent; a reset would let N+1 calls through), and the daemon revokes a peer's entry on its `offline` transition and before creating a new owned adapter, so a resumed session id never revives an allowance.
|
|
143
146
|
- Do not claim enforcement a native mode bypasses: Kimi's `yolo` and `auto` send no request for a call they allow, so `ahub status` lists that peer as not enforceable until it is back in `ask`.
|
|
144
147
|
- The floor (#999 T3, `action_policy.floor`) is the only place the classifier is told a push or pull request is ordinary: `classifyAction(..., floor?: FloorFacts)` runs `floorPush` and `floorPullRequest` only for a bare command (an `unwrapped` wrapper, `-c`, `--git-dir`, `--work-tree`, an expansion or a `cd` it cannot place keeps the class), and without a `FloorFacts` argument it is byte-identical to before. The flag is read only through `ActionBudgets.floorOn()`; the daemon's `floorFacts(peer)` supplies the facts per execution root (the default branch from the same authoritative remote read as the task shell, cached and refreshed at every `action_policy` reload; `inOwnRepository` only when git proves the top level is that root and its origin is the project's repository). A fact that is missing or unproven is the strict case; never add an allowed form to `floorRefspec` or `floorPullRequest` without a test that pushes the matching unforced and forced, default-branch, tag and opaque variants. A new allowed class change belongs there and in `test/action-floor.test.ts`, never in the shared `classify` branches.
|
|
145
|
-
- Floor facts hold only until the first earlier segment on the line that could change them. In `floorInert` (`src/hub/action-class.ts`), `FLOOR_INERT_PROGRAMS` and `FLOOR_INERT_GIT` are allowlists: any other program or git subcommand, a `FIND_WRITERS` primary, `rg --pre` or a glob argument ends the facts for the rest of the line. Before adding a program or git subcommand to either allowlist, show that no form of it can write a file, git config or a ref, or run another command, and add a test in `test/action-floor.test.ts` where its writing form still asks. Adding to `FIND_WRITERS` only makes the floor stricter. Revisit if the facts are ever re-read per segment.
|
|
148
|
+
- Floor facts hold only until the first earlier segment on the line that could change them. In `floorInert` (`src/hub/action-class.ts`), `FLOOR_INERT_PROGRAMS` and `FLOOR_INERT_GIT` are allowlists: any other program or git subcommand, a `FIND_WRITERS` primary, `rg --pre` or a glob argument ends the facts for the rest of the line. Before adding a program or git subcommand to either allowlist, show that no form of it can write a file, git config or a ref, or run another command, and add a test in `test/action-floor.test.ts` where its writing form still asks. Adding to `FIND_WRITERS` only makes the floor stricter. Revisit if the facts are ever re-read per segment. A redirect to a device file is not an output for the destroy rule (#725, #1193): `DEVICE_OUTPUTS` in `src/hub/action-class.ts` lists exactly `/dev/null` and `/dev/tty`, matched as written and never resolved, prefix-matched or normalized. `/dev/stdout`, `/dev/stderr` and every `/dev/fd/<n>` (1 and 2 included) stay destroys, because on Linux they reopen the file fd 1 or 2 already points at and a `>` truncates it (`sh -c 'echo x >/dev/stdout' >>/tmp/log`; the person's ruling after review of PR #1202); so do `/dev/sda`, `/dev//null` and `/dev/null/x`, and a device target never hides a real one beside it. The redirect is still recorded (`redirected`), so the read-only fast path in `floorInert` stays closed. Add a device to the list only with proof that opening it for writing cannot change a file and that no descriptor stands behind it, and a test in `test/action-class.test.ts` beside the lookalike rows.
|
|
146
149
|
- 2026-11-02 (#1089): a terminal the hub opens for a start through Orca (`src/hub/terminal-open.ts`, #269) is opened in the worktree `resolveStartTerminalWorktree` finds by asking `orca worktree list` for the one whose path equals this project's root, never in `ORCA_WORKTREE_ID`: that variable names the terminal this process was started from, which is another project's when `ahub --project`, the dashboard manager or an upgrade brought the project up. `startTerminalCandidate` (the environment or a recorded launch) only answers the synchronous "could Orca be used" question in `available()`. The match is exact because `ahub <peer>` compares the terminal's worktree root with the project root by equality; a parent, a child and an archived entry are not matches, two matches and a cut-off list that lacks the root are refusals, and each refusal names the cause and the next step and carries no path. Verified against Orca 1.4.245: `worktree list --json` rows carry `id` and `path`, with `truncated` and `totalCount` beside them. A change to that shape is found by the fake-Orca test in `test/start-modes.test.ts` only if its fixture is updated with it, so recheck the real output when Orca moves (the read-only probe is `orca worktree list --json`; never create or write to a live pane).
|
|
@@ -7,6 +7,7 @@ Scope: quota and native context readings, checkpoints, pause, handoff and resume
|
|
|
7
7
|
- A reading has its own timestamp. Numbers that arrive through a file (`claude-usage.json`) carry the file's `at`; a window whose `resetsAt` has passed says nothing any more.
|
|
8
8
|
- On resume the notice is published before the peer is released, so it leads the first delivery.
|
|
9
9
|
- The coordinator only lifts its own pauses: `manualPaused` in the daemon keeps a `ahub pause` in place, and `ahub resume` refuses while a budget record is open.
|
|
10
|
+
- `Bus.pause` and `Bus.resume` are transient and write no bus snapshot (#1204): the bus's `paused` set is not in `BusSnapshot`. Each owner persists and re-applies its own pause (`budget_pauses` for a budget record, `setManualPaused` for `ahub pause`, `conductorHolds` for a conductor hold); never restore a pause from the bus snapshot or add a `persist()` back for one.
|
|
10
11
|
- The status line tee must never fail or slow the render: no throw, original command run with the same stdin, 5 s cap.
|
|
11
12
|
|
|
12
13
|
- Keep native context occupancy (`src/hub/context-window.ts`, readers in `src/adapters/definitions/native-context.ts`) separate from quota accounting; never use Codex's accumulated `total` as occupancy or estimate Pi counters. Host code reads quota and context only through `readNativeQuota` (`budget.ts`) and `readNativeContext` on the resolved definition's declared `budget.reader` / `budget.context`; no declared reader is unknown, never an empty reading (rule: `adapters.md`).
|
package/docs/agent-notes/bus.md
CHANGED
|
@@ -6,12 +6,13 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
6
6
|
- 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
7
|
- Apply reloaded `batch_max`, `batch_ms` and `queue_cap` only through `Bus.configureScheduling` (#439): it validates the values and changes later batching and admission only. Never re-arm an existing batch deadline, trim or reorder queued work, or touch prefaces, pauses, recovery holds or in-flight delivery from it.
|
|
8
8
|
- A failed digest is retried one envelope at a time, so a poison envelope cannot take its neighbours down with it.
|
|
9
|
+
- Preserve typed `failureCause` through journal transitions, direct holds, recovery and explicit retry; an absent cause on an older row stays unknown, never inferred from its reason or body. Read unresolved failed counts only through `failedDeliveryCounts()` / `unresolvedFailedCounts()`, without hydrating payload history; exclude explicitly resolved rows, including failed retry parents, and keep unavailable counts distinct from zero. These diagnostics never authorize replay or change delivery disposition (#1178).
|
|
9
10
|
- 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.
|
|
10
11
|
- `WsPeer.steer` exists only for a push channel session: set at attach, cleared on detach, roster detach and a pull-only attach, and it refuses anything but important work in the current busy turn (#493).
|
|
11
12
|
- `replyParent()` decides what a reply answers (highest hop, never the `hub` preface). Use it for deliveries and steers alike, or the hop cap can be reset.
|
|
12
13
|
- Report a send's outcome from `Bus.publishResult`, never from `publish()`'s bare target list: a hop-capped send answers `ok: true, dropped: "hop"` with the hop-cap notice, and the channel tool and `ahub say` print that notice, never "no peers attached" (#546). A native peer's `onMessage` returns that `PublishResult` on a hop drop, its send tool (Pi, the local worker) reports it as not delivered, and no off-roster retention note follows a hop drop (#550).
|
|
13
14
|
- 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, an invocation kind or a remote sender is never condensed; a remote recipient (`remoteId`, rooms #941) gets one envelope per delivery with its queue deadline, never a preface, batching delay, condensation or steer.
|
|
14
|
-
- Invocations (#445 C2, #986): an `invocation` or `invocation_result` (`isInvocationKind`) goes alone in `take` and `takeBudget`, is never condensed, and is never steered into: both steer sites and `steerQueued` ask `invocationTurnOpen` first. In `onMessage`, a peer's answer goes to `onAnswer` before `parseMarker` and before any publish, when it names the request (`inReplyTo` in the
|
|
15
|
+
- Invocations (#445 C2, #986): an `invocation` or `invocation_result` (`isInvocationKind`) goes alone in `take` and `takeBudget`, is never condensed, and is never steered into: both steer sites and `steerQueued` ask `invocationTurnOpen` first. In `onMessage`, a peer's answer goes to `onAnswer` before `parseMarker` and before any publish, when it names the request (`inReplyTo` in the pushed `lastDelivery` or the pulled `lastPulled`) or names nothing while the invocation turn is open. A pull is kept in `lastPulled`, never in `lastDelivery`, so it cannot replace a pushed invocation's answer parent (#1192). A taken answer, or a throwing hook, publishes, stores and emits nothing.
|
|
15
16
|
- An invocation is never replayed through ordinary retry (#991). `retryDelivery` refuses a row that holds one. An uncertain or thrown delivery holds the peer (`recoveryHeldPeers`) and calls `onInvocationUncertain` instead of `failed()`. `failed()` allows at most one automatic retry, and only when `canRetryInvocation` returns true; unknown or a throw refuses.
|
|
16
17
|
- Add an envelope kind only to `KINDS` (`envelope.ts`); the journal, restart reader, deferred-hub check and held hints read that one list. Test room provenance with `remoteEnvelope(env)`, never `from.includes("@")`: a relayed room invocation arrives `from: hub` with `refs.invocationRemote` (#992).
|
|
17
18
|
- `Envelope.session` (agent-hub#1036) marks a sender that is a distinct session, `from` being its minted `<peer>~<origin>` id (rules: `daemon.md`). It grants nothing; keep it on queued and journaled originals, because the `session_identity` recovery guard reads it from unsettled deliveries.
|
|
@@ -23,9 +24,9 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
23
24
|
- Record exact task-offer reads only through `DeliveryJournal.completeDelivered` on an active-fenced adapter completion, correlated reply or actual pull, atomically with completion. Capture original offers before terminal pruning and record their ids, never condensed ids; accepted receipts, generic transitions, operator/hub dispositions and restored completed rows are not read evidence.
|
|
24
25
|
- The `onImportantQueued` nudge for a pull-only peer (#398) is advisory: it never dequeues, steers, journals or settles, and a throw from it never stops an enqueue.
|
|
25
26
|
- Merge recall and restored prefaces through `mergePrefaces`/`uniqueMemoryLines`; preserve distinct context and new notes, deduplicate memory identities, and keep colliding short prefixes distinct. Use `recallDisplayText`/`recallMemoryLines` before display caps: suppress only matching automatic approval outcomes, retain nonempty rejection details, preserve id headers and complete-word excerpts, and keep token trimming at line boundaries. Keep these changes display-only.
|
|
26
|
-
- Observe memory rendering from final `out`, not `originals`, and suppress telemetry when either contains private envelopes. A pull emits rendered/delivered observations only after its journal transaction commits; observer exceptions must never change delivery settlement.
|
|
27
|
+
- Observe memory rendering from final `out`, not `originals`, and suppress telemetry when either contains private or `isReviewObserverExcluded` envelopes or `reviewObserverInputExcluded` holds for the peer (#1056). A pull emits rendered/delivered observations only after its journal transaction commits; observer exceptions must never change delivery settlement.
|
|
27
28
|
- Read `Bus.onDelivered`'s optional persisted delivery ID only at its positive-ACK observation site; isolate fallible observers so they cannot skip invocation, pending-delivery or supervision bookkeeping.
|
|
28
|
-
- Classify automatic held-row completion only through `supersededDeliveryReason`: every original must be a hub presence or a provably moot hub task/review notice. An owner offer (`refs.taskOffer`) is moot only while that recipient owns the task in `in_progress`, `in_review` or `changes_requested` and its own `accepted` entry follows the latest ownership event within the 4096-entry bound; a person's acceptance, an unmarked reminder or missing proof keeps the hold (#434). Unknown/live/mixed rows stay held; keep `deliveryTaskContext` display-only. Tag only current-owner notices as owner audience; leave former-owner withdrawals untagged until the task closes. For cancelled tasks, settle earlier workflow notices but retain the stop notice whose `refs.taskCancelled` matches the cancellation revision. Preserve the requested-stop exclusion in `daemon.md`. Record the hub actor through `resolveDelivery`, restoring presence originals through `restorePreface` only for hub completion (person completion stays consumed); run on hold entry, restore, recovery release and board changes, never replay uncertain work. List every hold once in status, console notices and route explanations (#434).
|
|
29
|
+
- Classify automatic held-row completion only through `supersededDeliveryReason`: every original must be a hub presence or a provably moot hub task/review notice. An owner offer (`refs.taskOffer`) is moot only while that recipient owns the task in `in_progress`, `in_review` or `changes_requested` and its own `accepted` entry follows the latest ownership event within the 4096-entry bound; a person's acceptance, an unmarked reminder or missing proof keeps the hold (#434). Unknown/live/mixed rows stay held; keep `deliveryTaskContext` display-only. An owner-audience `review` (`refs.taskRecipient: "owner"`: a review package or verdict) is never moot (#1181). Tag only current-owner notices as owner audience; leave former-owner withdrawals untagged until the task closes. For cancelled tasks, settle earlier workflow notices but retain the stop notice whose `refs.taskCancelled` matches the cancellation revision. Preserve the requested-stop exclusion in `daemon.md`. Record the hub actor through `resolveDelivery`, restoring presence originals through `restorePreface` only for hub completion (person completion stays consumed); run on hold entry, restore, recovery release and board changes, never replay uncertain work. List every hold once in status, console notices and route explanations (#434).
|
|
29
30
|
- `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. Keep the selected delivery id and journal revision fixed in console row actions; retry/discard/completed never change a task role or cancel a native turn.
|
|
30
31
|
- Recovery-deferred hub notices (`deferredHub`, #478) are accepted work kept outside the queues and the carried-queue integrity projection: never put them in `queues` or the integrity view while held. Every read of pending work includes them (`pendingEnvelopes`, `withdraw`, `revokeSupervision`, `queuedBacklogIds`, `get`, reset). Past `MAX_DEFERRED_HUB` refuse the publish; never drop a kept notice.
|
|
31
32
|
- Release moves each deferred notice through `publishNow` and checkpoints it before delivery, events or nudges run. A failed checkpoint restores only publication state (the affected queue arrays in place, deferred, seen and withdrawn ids, pending outcomes) and keeps the hold. Never reload a snapshot or touch prefaces, pauses or the recovery mode from it. A successful release clears the deferral mode (`deferHub`) after `flushDeferredHub()` and before dropping `recoveryHeld`, so a later bare hold or `fenceRecovery` queues hub notices; a failed flush keeps both the hold and the deferral mode (#541).
|
|
@@ -44,4 +45,8 @@ Scope: delivery, digests and condensation, reply addressing, priority and limits
|
|
|
44
45
|
- Concurrent-edit fan-out is linear in affected tasks: retain each notice's own-task stale/owner fence, but never persist the same change-set notice once for every task pair. Preserve per-task-pair research evidence separately (#576).
|
|
45
46
|
|
|
46
47
|
- Completed-change notices (#713): `Tasks.tellCompleted` publishes one notice per recipient per done through `whileOpenFor`, naming all of that recipient's overlapping tasks, and only for tasks the recipient accepted (`ownerAcceptance` plus an in-work state): never one per overlapping task, never for an offered or proposed task, never again for a repeat done without superseding. The envelope carries `refs.completedFor` (the done task); `Bus.enqueue` replaces an earlier queued, never-attempted notice for the same `completedFor` and recipient through `discardQueued` (a `superseded completed-change notice for task #N` journal outcome), and never touches a user message or another hub notice. `relevant` keeps a grouped notice while any of its tasks is still open for the recipient. Do not widen `completedFor` to anything but the done task id.
|
|
47
|
-
- `resolveDelivery` (#713): a `queued` delivery was never handed to the peer, and a `failed` one was refused before it ran (#1103), so a busy peer blocks discarding neither; `dispatching`
|
|
48
|
+
- `resolveDelivery` (#713): a `queued` delivery was never handed to the peer, and a `failed` one was refused before it ran (#1103), so a busy peer blocks discarding neither; `dispatching` stays refused as `active delivery`, and so does `accepted` except for its own explicit channel's completed/discard (#1172, below); unrelated resolutions keep the busy fence. Only the recipient may explicitly complete/discard its own `needs_review` row, or for a peer with `acceptedSettlement: "explicit"` its own `accepted` row, through `resolveOwnDelivery` with an observed revision and reason (#1079), recording the peer actor through the existing journal audit. Another peer, remote-room disposition and active dispatching/accepted row refuse; this never replays the originals. Every other operator resolution stays fenced while busy. `resolveOwnDelivery` refuses with its own `PreEffectToolRefusal`, which names the cause and the next step, never through `resolutionRefusal`. A row that carries an invocation stays person-only (#1112). The daemon names the refusal from a closed list (`resolutionRefusal`), never from free error text. `queue discard --peer` goes through `resolveDelivery` row by row, reading each row's current revision just before its discard, never the revision listed at the start. Without `--state` it takes the peer's `queued` rows and its unsettled `failed` rows (wire `states`, sent only to a hub whose `features` list `queue_discard_states`; `needs_review` still needs an explicit `--state`). A row a resolution already settled (`settledIds()`) is history: `queue list` hides it unless `all`, and resolving it again is refused as `already resolved by <action>`, never `stale revision` (#1098, #1103).
|
|
49
|
+
- Accepted channel deliveries (#1172): an `accepted` receipt means the channel transport took the notification, never that the session handled it. `expireAcceptedDeliveries`, run from the daemon's 5 s invocation sweep with the host-only `DaemonOptions.acceptedSettlementNow` clock, moves an explicit channel's `accepted` row older than `bus.accepted_settle_ms` (`acceptedSettleMs`, default 10 min) to `needs_review` with `ACCEPTED_SETTLE_TIMEOUT_REASON` under a revision fence. It holds the peer, routes an invocation through `onInvocationUncertain` and never replays. A repeated `accepted` receipt never renews the deadline. `settleSupersededDeliveries` and supervision skip timeout rows; an important, task or review timeout gets one ids-only conductor status notice unless `conductor.feed` is `off`. The explicit channel's `inbox` reply, a refused one included, carries `held` metadata for its own accepted and `needs_review` rows (`heldDeliveryHints(peer, true)`), or `{ unavailable: true }`, never an empty claim.
|
|
50
|
+
- Review provenance (#1056): `reviewResponse` runs on every publish (`publishResult` and the daemon's channel `send`). It strips a caller's `refs.reviewPackage`/`refs.reviewResponse` and stamps a host-derived `reviewResponse` only on a chat or status that answers a review-scoped parent (`lastDelivery`, `lastPulled`, the bounded `reviewParents`, or `reviewObserverScope`, whose failure counts as scoped); never trust a peer's marker or match content. `beforeReviewInputHandoff` runs before every native handoff (steer, drain, pull); a throw leaves the work queued as `ReviewInputHandoffError` and is never a storage error. `publishReviewEnvelope` republishes a saved package only when `reviewEnvelopeStatus` is `absent`, refuses `failed`, `discarded` and `unknown`, and never replays a published one. Terminal pruning keeps hub review-package rows (`refs.reviewPackage` `review-*`). The bus constructor restores from `transportSnapshot()`; only recovery uses the strict `snapshot()`, which refuses malformed review provenance.
|
|
51
|
+
- Room delivery rows (#1204): a remote (`remoteId`) `needs_review` row holds its peer only when it carries an invocation; an ordinary room chat uncertainty never blocks later queued work. Settle, requeue, hold or resume a remote row only through the room adapter's host ports (`settleRemoteReceipt`, `requeueRemoteNotSent`, `holdRemoteMissingIntent`, `holdInvalidatedRemoteOutcome`, `resumeRemotePending`). Each is bound to the current adapter, room and member, revision-fenced and refused after a person disposition. `deliveryReceipt` never calls `completeDelivered` or `deliveryFailed` for a remote peer, and an uncertain invocation stays person-only. At journal open, `recoverInterrupted` keeps an active single-envelope, non-invocation remote row active only while the bus snapshot still holds its room, member and an unexpired queue deadline; every other `dispatching`/`accepted` row becomes `needs_review`.
|
|
52
|
+
- Closed publication (#1222): once the `Bus` is closed, `publish` returns `{ targets: [] }` before touching the journal or observers, so a late room or presence publication after shutdown writes nothing; a live storage error still propagates. Keep the closed check first when you touch the publish path.
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
- Session/recovery dispatch (#625 slice 2c) reads `PeerAdapter.nativeSession`, never `definition.session.currentId`: local and reviewer context/memory provenance stays unbound. Keep Claude's session/claim proof in the daemon and `lastCodexThreads` as the resume-command fallback. An absent `recoveryReady` is settled; a present false or undefined value refuses. Call an idle owner's `captureResume` before recovery inspect/prepare/commit; preserve snapshot and protocol shapes.
|
|
8
8
|
|
|
9
9
|
- For explicitly user-authorized live verification, use the per-invocation CLI delegation in `docs/security.md` §Agent CLI identity and conductor authority; keep caller markers and record native evidence separately from fixtures.
|
|
10
|
+
- Every new person-only CLI command or subcommand that mutates state (a receipt, an answer, a verdict, a stamp, a confirm) is added to `liveTestRefusal` in `src/cli/identity.ts` in the same PR, with a delegated-form test: a delegated `--live-test` run from an agent shell is promoted to console identity unless it is refused there, so a missing entry lets an agent act as the person. Test the delegated forms (`--live-test <command> ...`, with `=` and separate-operand spellings) and the ordinary agent-shell forms, and assert the person-attributed-write refusal text, because a missing audit outbox also exits 1 and hides the gap. A plain listing may stay allowed. Seen twice on 2026-11-04: `ahub results --read|--read-all|--unread-last` (#1054, PR #1186) and `ahub answer` (#1053, PR #1183).
|
|
10
11
|
- Keep `export const LIVE_TEST_SUPPORTED = true;` in `src/cli/identity.ts`: delegated recovery staging reads this declaration before any source effect.
|
|
11
12
|
- A delegated non-recovery command runs only after `recordDelegatedCliAudit` committed `delegated` against a hub whose `features` list `cli_audit_delegated`, that instance's console `cli_audit_ack` confirmed retention, and `ControlClient.pinInstance` pinned the invocation to it; a refused feature, failed ack or changed consumer executes nothing (#410). Upgrade, restart and recovery record delegation only in the operation receipt (`delegatedAudits` through `createOperation` or `recordRecoveryDelegation`). The daemon counts an audit retained only after `event()` returned true.
|
|
12
13
|
- For conductor recovery (#900), keep source-issued private continuity separate from public actor/audit metadata; recheck the same initiator and every affected project at effect boundaries. Compare candidate config and routing with the running accepted policy. Verify and use the same captured startup inputs before any adapter import; never publish a later raw-file reread. Keep captured TUI restoration and unknown source/target evidence person-only. Derive target feature floors from verified target bytes, never a request or receipt claim (`docs/operations.md`, "Conductor-bound restart and recovery continuation").
|
|
13
14
|
- Conductor operational refusals (#812, #839): throw `OperationalAuthorityError` (`src/hub/operational-authority.ts`) for an authority refusal, never a plain `Error`. Only it sets `authorityRefused: true` and its scope on the reply, which the control `reply` wrapper audits once as a refused `agent_cli` event; any other failure of a labelled request keeps its own error with `authorityRefused: false` and is never audited as refused, and the CLI's `fail` audits only when `authorityRefused` is not `false`. An unreachable or dead hub is "authorization unavailable", never a missing conductor role. `publicOperationalView` is cached on the board status revision, config revision, routing and the private-path policy: a new input to that projection joins the cache key.
|
|
14
15
|
|
|
15
|
-
- Peer DOING (#878): `observeDoing` is the only writer and `doingRows` the only reader (`src/hub/daemon.ts`, helpers in `src/hub/peer-doing.ts`). Decide privacy at observation (`contextPii` or a refusing or throwing `tasks.publicMetadataText` latches a stub) and again at display (an open PII task owned or reviewed,
|
|
16
|
+
- Peer DOING (#878): `observeDoing` is the only writer and `doingRows` the only reader (`src/hub/daemon.ts`, helpers in `src/hub/peer-doing.ts`). Decide privacy at observation (`contextPii`, held review input (`reviewObserverHeld`) or a last delivery holding an `isReviewObserverExcluded` envelope, or a refusing or throwing `tasks.publicMetadataText` latches a stub) and again at display (an open PII task owned or reviewed, a private turn, or the same review-input screens); reuse those screens, never add one. `status()` adds it only with `includeDoing` (the `status` reply and the dashboard snapshot), so `writeStatus` never writes it to `status.json`; the conductor's `hub_status` gets the same rows, each non-stub value framed by `publicConductorStatus` as untrusted (`sanitize`d text, `untrusted: true`, and `from` with `doingNote`, or `note: INCOMING_LABEL_NOTE` for a `label` row); never give an agent DOING text without that frame. A `label` row (`unnamedSessionLabel`: an unnamed `title: "hub"` session's first acknowledged incoming delivery, #1055) reaches only the console, the dashboard and the conductor (`includeIncomingLabels`); every other status reader gets `unnamed`. The console `title` op and `hub_set_title` both go through `setSessionTitle` (writer rule: `adapters.md`), and `session-title-inputs.json` keeps only session, delivery and envelope ids (`sessionTitleBindings`, `titlePersistence: "native"` peers only), never a name, body or label. Claude's title comes from the transcript path the facts hook reported and `claudeTranscript` validated, read through `readAiTitle` (1 MiB per read, lines over 64 KiB skipped); its current item only from PreToolUse input, never the transcript; every cached TaskUpdate subject, current or not, is screened when it is learned (`contextPii` or `tasks.publicMetadataText` refusing or throwing, via `ClaudeTodoSource`), keeps that verdict and passes it to `observeDoing` as `learnedPrivate`. A doing failure never changes a facts reply.
|
|
16
17
|
- Crash-recovery lines (#1111): `recoverAfterCrash` adds a line only through its `report(line, notice)`. Tag a line that asks something of a peer's owner with `peer`; it clears when that peer attaches. Tag the headline with `inFlight`; its count is read live from `crashLostIds`. A line about what recovery found or did gets neither and stays. Status shows only `crashView()` (`openCrashNotices`), never the raw list. `crashReported`, which the Jcode TUI hint reads, never clears.
|
|
17
18
|
|
|
18
19
|
- Conductor console ops (#933): a tools caller's `CONDUCTOR_TASK_OPS` request (`github_import`, `intake_approve`, `schedule_run|pause|resume`, `task_unassign`) and `goal_link` are admitted by `admitOperational` in `onMessage` before Tasks sees them, and the task op runs inside `operationalContext`, which `TasksDeps.operational` reads; never let `taskOpBody` admit a non-person caller to a console op any other way. `conductor_operational` `admit` audits a `CONDUCTOR_LOCAL_OPERATIONS` form once; `authorize` only rechecks the role. `task_catalog_role` from a tools caller passes the peer to `Tasks.setRole` (never `USER`), and is refused for a child, external, read-only or PII-context caller.
|
|
@@ -31,7 +32,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
31
32
|
- Console Queue `D` (#713) only prefills `queue discard --peer <selected row's peer> --reason `; Enter refuses a missing peer or reason and arms a `y` confirm that names the count, then runs the person's `ahub queue discard ... --yes`. Without `--state` the confirm counts the peer's queued and unsettled failed rows; the console sends that form only when the hub's `features` list `queue_discard_states`, otherwise it sends `--state queued` (#1103). Beyond `list`, the `queue` op is the console's or an admitted conductor's (`admitOperational`, `show`/`resolve`/`discard` only); `discard` without `confirmed` only counts, and a confirmed one needs a reason, is refused during recovery or shutdown and discards row by row through `resolveDelivery` (rule: `bus.md`).
|
|
32
33
|
- Console Usage panel (#683 U3): tab `7`, text from `src/cli/console-usage.ts` (pure strings, no terminal or state import), rendered in `console-state.ts`, fetched in `console.ts` `refresh()` only while panel 7 is open. It reads the `usage` op and nothing else: no event-log read, no board field beyond the task id selected in Tasks. A refusal shows `usage unavailable`, never empty usage; an unknown counter is `unknown`, never 0; a private task is `#id [pii]` from the op's `taskPii`, never a title. The `usage` op folds with `catchUp(250)` and answers `behind: true` when it is not done: never answer from one pass of a big log. Keep Left/Right from 7 wrapping like Settings so the `1`..`5` cycle (#309) stays pinned, and keep PAUSE and 7D out of the width drop list.
|
|
33
34
|
- Dashboard snapshot `view` (#683 U4): the daemon's `ui_snapshot` and the manager's forward pass a request's `view` only through `dashboardView` (`DASHBOARD_VIEWS` in `src/hub/ui.ts`); keep that list equal to the page's `DASHBOARD_VIEWS` in `src/ui/index.html`. Any other value is dropped, never forwarded.
|
|
34
|
-
- acon plugin (#935): `src/ui/plugin.ts` never bypasses `uiAction`'s whitelist or `DASHBOARD_VIEWS`. It reaches project hubs only through `projectConsole` (`src/hub/manager.ts`), the same listing and forwarding `ahub ui --all` uses; widen neither for the plugin. Its inbox verbs map to the hub's own `permit` with an option the request offers. Its panel is built from `renderDashboard()` by exact rewrites that throw when a target line moves: when you edit a rewritten line in `src/ui/index.html` (post, the cookie helper, `$`, the theme `apply`, the view key listener, the title update or the landmark tags), update `panelAssets()` in the same PR. `lifecycle.ts` is loaded lazily there, because it reaches the daemon and CLI files acon's import scan reads. `ahub setup` registers it only through `registerAconPlugin` (`src/cli/acon-register.ts`): `console/plugins/ahub.json` under the hub home names the module and its sha256 (directories 0700, written to a temp file and renamed); the package export `./ui-plugin` is pinned by `scripts/check-package.mjs`. The Space panel (#1012, `src/ui/office-space.ts`) maps only the #264-allowlisted `officeModel` scene, pinned by the #264 marker fixture in `test/acon-plugin.test.ts`; never feed it an envelope body, task detail, path or symbol. A Space snapshot answers within acon's 5 s: the listing gets half of `SPACE_BUDGET_MS` and falls back to the last listing (a late one still replaces it), projects are read concurrently and a late one is left out of that poll, the cached office model is keyed by the daemon's `instanceId` (a restarted daemon restarts its ui sequence at zero), and `pollMs` stays under acon's 3000 ms link gap.
|
|
35
|
+
- acon plugin (#935): `src/ui/plugin.ts` never bypasses `uiAction`'s whitelist or `DASHBOARD_VIEWS`. It reaches project hubs only through `projectConsole` (`src/hub/manager.ts`), the same listing and forwarding `ahub ui --all` uses; widen neither for the plugin. Its inbox verbs map to the hub's own `permit` with an option the request offers. A question's `answer` and a result's receipts (`result_seen`, `results_read_all`, `results_unread_last`, #1054 T6) are the exceptions, and none goes through `projectConsole(...).act` or the `FORWARDED` list from the plugin (the receipts use the `receipt` port beside `answer`, with `receiptEvidence` in `src/ui/plugin-results.ts` checking the same console person meta; the page of `ahub ui` reaches the same three writes as dashboard actions, refused for a live-test session by both `ui.ts` and the daemon). The answer's route is (#1053 T5): `action()` checks the console's person `meta` first, then calls the dedicated `projectConsole(...).answer` port, which sends `answer { surface: "acon", person }` as a console-role control message. `answer` is advertised in `capabilities.actions` (`QUESTION_ACTIONS`) so acon lets the page send it, and is never in `FORWARDED`; widening the dashboard whitelist for it would let a dashboard page answer without person evidence. Acon types are type-only imports from the exact-pinned `@staix/agent-console/contract` entry, never hand-copied. Its panel is built from `renderDashboard()` by exact rewrites that throw when a target line moves: when you edit a rewritten line in `src/ui/index.html` (post, the cookie helper, `$`, the theme `apply`, the view key listener, the title update or the landmark tags), update `panelAssets()` in the same PR. `lifecycle.ts` is loaded lazily there, because it reaches the daemon and CLI files acon's import scan reads. `ahub setup` registers it only through `registerAconPlugin` (`src/cli/acon-register.ts`): `console/plugins/ahub.json` under the hub home names the module and its sha256 (directories 0700, written to a temp file and renamed); the package export `./ui-plugin` is pinned by `scripts/check-package.mjs`. The Space panel (#1012, `src/ui/office-space.ts`) maps only the #264-allowlisted `officeModel` scene, pinned by the #264 marker fixture in `test/acon-plugin.test.ts`; never feed it an envelope body, task detail, path or symbol. A Space snapshot answers within acon's 5 s: the listing gets half of `SPACE_BUDGET_MS` and falls back to the last listing (a late one still replaces it), projects are read concurrently and a late one is left out of that poll, the cached office model is keyed by the daemon's `instanceId` (a restarted daemon restarts its ui sequence at zero), and `pollMs` stays under acon's 3000 ms link gap. The inbox snapshot (#727, #1196) answers within 1 s at p95: the project listing is the one in-flight listing Space also joins (`runningWithin`, half of `inboxProjectBudgetMs`, falling back to the last listing), the running projects' `agents` snapshots are asked at once, each within `inboxProjectBudgetMs` (a project that is late keeps its last rows and their `at`) and joined while pending in the same action generation and instance (`projectFlights`), so a slow project is asked once, not once per poll; concurrent polls join one computation, a result under `inboxFreshMs` is served as it is, one under `inboxStaleMs` is served while one refresh runs (nothing runs while nobody polls), and `action()` bumps a generation before and after it so a computation that started earlier never becomes the cache. The reply holds at most `INBOX_MAX_ITEMS` (50) items, decisions first, with `more`; the `seen` bookkeeping runs before the cut, so a cut item keeps its `at`. Never put a per-request hub call back into `inbox()` outside that single flight.
|
|
35
36
|
- Dashboard Usage view (#683 U4): `uiSnapshot` builds `usage` only when the request's view is `agents`, never on a hidden view's poll. It folds with `catchUp(100)` and sets `behind: true` when that is not done, so a partial figure is never shown as the whole. A failed read answers `{ unavailable: true }`, never `undefined`, which the page reads as a hidden view and keeps the old figures. The snapshot carries task ids, a `pii` flag and numbers only, never a title; the page shows a private task as `#id [pii]`. Daily figures come only from `UsageRollup.daily` (rule: `budget.md`).
|
|
36
37
|
- Dashboard Code group (#684 2c): `codeGroup` (`src/hub/task-code-group.ts`) is the only dashboard classifier of changed symbols against `plan.symbols`, called in `uiSnapshot` with the public `codeSymbols`, `tasks.publicMetadataText` and `tasks.isPii`; the page (`codeGroupSection`) draws it and never matches names. A private task gets no `codeGroup`. A stored receipt (one with `repositoryKey`), a missing or an unknown one is `state: "unknown"`. A malformed or screened-out plan name is counted in `planWithheld` and marks the group incomplete, never dropped. Lists stop at `CODE_GROUP_MAX` (40) with the rest in `more`, and `counts` are totals. The memory view reads a `node` of up to 512 characters only when it starts with `symbol:` (200 otherwise).
|
|
37
38
|
- Worktree shelf (#1057, `worktreeShelf` and `worktreesView` in `src/hub/daemon.ts`; spec `docs/design/task-board.md`): never add a worktree or branch removal to the daemon (its only one stays the empty hub-made task worktree in `src/hub/task-worktree.ts`); shelf removal exists only in the person's `ahub worktrees tidy --confirm` (`src/cli/worktree-tidy.ts`), which `daemon.ts`, `worktree-shelf.ts`, `board.ts`, `ui.ts` and `plugin.ts` never import. GitHub is read only by `worktrees_refresh_prs` (`refreshPrStates`); a snapshot, `ahub worktrees` and the inbox alert never reach it, and `worktreesView` answers from its 30 s cache without awaiting git. The `worktrees` read is open to a tools caller outside a private turn; override set/clear and the refresh are console-only and refused for a live-test dashboard session. After the awaited git reads, ask the recovery/stopping fence again, then read and write the board with nothing awaited between. The board keeps only the person's Park/Pin (`worktree_override`), never a derived settled state, and accepts one only for a path the shelf just listed, never the main checkout or an instance worktree.
|
|
@@ -45,7 +46,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
45
46
|
- Planner setting (#755): `planner` is one id or a list of one to eight distinct eligible ids, validated only by the `planner` row of `projectSettings` (eligible declared instances join its `values`) and `checkSettingValue`, in `loadConductorPolicy` and `validateRawConfig` alike. Every `resolveSettledRoles` call (`settlePlanner`, `roleHolders`) passes the same `plannerEligible` predicate built from that row, so reload guards compare holders the way the running map settles them.
|
|
46
47
|
- Retained configuration (#544): a key the reload refused, or a file edit outside a scoped write that still differs from the accepted value, is listed in `retainedConfig` (key and sanitized reason only, never a value) in status, `status.json`, the settings view and the console snapshot until a reload accepts or reverts it (`retainedConfigAfterReload`). A config leaf that selects a peer's native launch belongs in that definition's `cli.launchConfigKeys`: the `start` and `peer_start` replies then carry `retainedLaunchConfig` (the running values), the launcher starts with them and prints `retainedLaunchWarning` before its start request; when a retained value cannot launch (Kimi TUI `kimi_cmd` with no TUI form), the error names the running hub as its source and never echoes the words (#654).
|
|
47
48
|
- `action_policy` (#636) is machine-local only: `stripUntrusted` drops it from any file but `config.local.json`, and `authorizeConfigLeaf` refuses an `action_policy` leaf whose `sourceFile` is not an untracked `config.local.json` before any other check. It applies live (`actionBudgets.policyChanged`); Claude's PreToolUse hook, hub-owned ACP peers in `ask`, the local worker and Pi (`classedToolPermission`, #750) enforce it, and a headless Claude implementer only while a current child generation is busy. Codex's approval RPCs are gated but Codex stays not enforceable, because its native policy can run a command without a request. Keep `actionEnforcement` (`src/hub/daemon.ts`) honest: a peer is enforceable only where the hub sees the call before it runs, in the mode it runs in now, and every refusal names its reason (rules: `adapters.md`, Always-ask).
|
|
48
|
-
- `pet` is console-local presentation (#483): validate it only through `validatePetConfig`, which both `loadConfig` and the reload schema call, and keep it applied live with no authority (`authorizeConfigLeaf`).
|
|
49
|
+
- `pet` is console-local presentation (#483): validate it only through `validatePetConfig`, which both `loadConfig` and the reload schema call, and keep it applied live with no authority (`authorizeConfigLeaf`). The floating window is retired (#1012): strip a legacy `pet.window` only through `stripLegacyPetWindow` (in `loadConfig`, `parseConfigSources` and `validatePetConfig`, keeping the raw value for the trust layer). It is reported once in `retired`, refused as a config leaf and never published, and `ahub pet --window|--palette` only print `PET_WINDOW_RETIRED_NOTICE`.
|
|
49
50
|
- Start modes (#269): every start the hub makes itself goes through `hubStart` (`src/hub/daemon.ts`): the dashboard's `start_peer`, the console-role `peer_start`, the conductor's `start` hook and `pi.auto_start`. It takes the mode from `startMode(peer)` and nothing else; never add a caller-chosen mode, flag or command to it, and never fall back to headless when a terminal cannot be opened (refuse with the manual command). The words a terminal runs are assembled there from `process.execPath`, the CLI entry, the project root and `startWords(peer)`. The only continuity exception is a validated Pi bookmark on `pi.auto_start`: append its recorded session file (or its session id when no file was recorded), backend and model; never pass both session selectors or accept caller-written replacements. Recovery and `startPeer` callers that pass a recorded mode keep it. A launcher with no `--mode` takes the daemon's accepted start mode from `status.json` `startModes` (`readControl`) before the file's value. A TUI start of an `owned-acp` peer (Jcode) from the console's `peer_start` opens its terminal even while the ACP owner is online: that terminal is a frontend on the owner. The conductor and the dashboard answer `already` for an attached one (#559). The real terminal providers are wired only in `src/hub/lifecycle.ts` (`DaemonOptions.terminal`): a daemon built without them, as in every test, opens nothing; a test injects a recording provider and never the real one.
|
|
50
51
|
- A headless start that `hubStart` made for a peer with `capabilities.watch` (Kimi, Qwen) also opens `ahub watch <peer>` through `openWatchPane` (#286 AC10): the same provider, fixed words from `process.execPath`, `CLI_ENTRY`, the project root and the peer id, one per peer per `PEER_START_RETRY_MS`. It runs only after the start succeeded and never fails it: no provider or a failed open reports `watch.command` in the result and a notice, with the provider's own text in the log only. Never open a pane for an `already` result or a declared instance.
|
|
51
52
|
- Default agent set (#608): `start.agents` is read only by the CLI at a person's `ahub up` and by the dashboard manager's start (`afterStart` in `src/hub/manager.ts`), through the console `peer_start` and so through `hubStart` (`src/cli/start-agents-run.ts`). The daemon never reads it: do not add a start from it to daemon startup, restart, recovery, `reloadConfig` or upgrade, and keep the `AGENTHUB_RECOVERY_OPERATION`, `--live-test` and console-caller guards on the `up` body together (each overlaps another, and removing one is caught only because the test removes them all). It is in `ALL_MACHINE_LOCAL` with an empty list kept as a value (`EMPTY_IS_VALUE` in `config-trust.ts`): an empty list means start none, never the default. `src/hub/start-agents.ts` and `config-apply.ts` stay free of file-system imports because the schema reaches the Claude channel bundle. `defaults.json` gained `start.agents`; `writeAutonomyDefaults` and `writeMachineStartAgents` each keep the other half, and any other key still makes the whole file untrusted.
|
|
@@ -58,10 +59,12 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
58
59
|
- 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.
|
|
59
60
|
- The channel advertises `tools.listChanged`; build its tool list only through `toolListFor` and record `listedTools` from each `ListTools` reply, so a reconnect sends `sendToolListChanged` when the whole definitions differ or the lifecycle stays unreadable (#490).
|
|
60
61
|
- 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. `status.json` is advisory: a reconnecting standby sends `standby: true`, and the daemon refuses it with 4000 while `standbyHeld` holds (a claim, or an open socket; only a channel-capable standby may replace a pull-only holder). A hello whose `launchId` differs from this instance's current `claude-launch` record closes with 4430, which is in `TERMINAL_CLOSES`; no launch id or no record keeps the old admission (#551). `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.
|
|
62
|
+
- Peer takeover (#1174): `peer_takeover` is console-role only, with closed fields (`peer`, optional `standbyId` of at most 64 characters, no control or format characters), and `ahub peer takeover` is in `liveTestRefusal`. It refuses without detaching the owner when no eligible standby remains, and answers `outcome: "requested"`; attachment is confirmed only by status. A connection detached by the person (`detachedByPerson`) refuses every peer request with the fixed detached text, and the plugin stops reconnecting on 4405. Never treat a standby id or reported PID as session, launch or permission evidence.
|
|
63
|
+
- Channel push evidence (#1171): the plugin pushes only when `currentClaudeChannelEvidence()` (`src/adapters/claude-channel-evidence.ts`) reads `development-channel-launch` from the direct parent's bounded process row. The inherited `AGENTHUB_CHANNEL` marker alone, or unknown, malformed or changed-parent evidence, keeps it tools-only. The hello's `channelEvidence` is a closed, display-only label: an unlisted value becomes `unknown`, a missing one `unreported`. Never admit, deliver or authorize from it, and never walk ancestors.
|
|
61
64
|
- 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.
|
|
62
65
|
- 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.
|
|
63
66
|
- Preserve `task_cancel` target-feature preflight before source holds, capture or commit. Reuse stored target features only after verifying the same retained root and digest; legacy receipts still capable of commit must obtain feature proof. Do not apply pre-commit compatibility checks to sources already past commit. Every prepare, including the lease renewal before a terminal close and after a resume, sends the operation's stored `targetFeatures` (#478).
|
|
64
|
-
- A recovery target-feature floor (`task_cancel`, `invocation` while the invocation ledger has an open (`queued` or `delivered`) row, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded, `external_adapter_instances` with `external_adapter_modules` and `peer_instances` while an external module-kind instance is declared, `conductor_recovery` while `Board.hasConductorManualState()` finds open conductor manual-assignment intent, `project_autonomy` while the daemon holds retained autonomy consent or a raw `config.json` or `config.local.json` has a top-level `_autonomy` key (an unreadable file refuses as unknown), `calendar_schedules` while a raw `schedules` entry has a `cron` or `tz` key (#1017: an older target refuses the key at load), `research_records` while `Board.hasResearchState()` or a raw `config.json` or `config.local.json` `research` object names `runners` (unreadable refuses as unknown), `research_dispatch`, `research_dropped_offers`, `research_experiments` and `
|
|
67
|
+
- A recovery target-feature floor (`task_cancel`, `invocation` while the invocation ledger has an open (`queued` or `delivered`) row, `peer_instances`, `role_stages` while a declared Claude-kind instance holds `implementer`, `task_planners` while a list/instance planner is configured or `Board.hasPlannerState()` finds a task/release binding or plan-review authorship marker, `release_trains` while a configured train is active or frozen, `reviewer_pool` while a fresh reviewer or `review.pool` is configured, `configured_acp` while the project declares `adapters`, `external_adapter_modules` while external module definitions are loaded, `external_adapter_instances` with `external_adapter_modules` and `peer_instances` while an external module-kind instance is declared, `conductor_recovery` while `Board.hasConductorManualState()` finds open conductor manual-assignment intent, `project_autonomy` while the daemon holds retained autonomy consent or a raw `config.json` or `config.local.json` has a top-level `_autonomy` key (an unreadable file refuses as unknown), `calendar_schedules` while a raw `schedules` entry has a `cron` or `tz` key (#1017: an older target refuses the key at load), `research_records` while `Board.hasResearchState()` or a raw `config.json` or `config.local.json` `research` object names `runners` (unreadable refuses as unknown), `research_dispatch`, `research_dropped_offers`, `research_experiments`, `research_comparisons` and `research_evidence` while `Board.hasResearchDispatchState()`, `hasResearchDroppedOffers()`, `hasResearchExperimentState()`, `hasResearchComparisonState()` and `hasResearchEvidenceState()` find rows (each research digest joins `integrity()` only when its rows exist), `research_runners` under the runner registration rule below, `review_comments` while `Board.hasReviewCommentsState()` finds stored comments or packages (its `reviewComments` digest joins `integrity()` only then), `review_observer_input_state` while the journal (`hasReviewInputState`) or a live or recovery-snapshot peer holds review input (unreadable refuses as unknown), `accepted_delivery_settlement` while `Bus.hasAcceptedTimeoutHolds()` finds a timeout hold (unreadable refuses as unknown), `action_policy_floor` while the raw `config.json` or `config.local.json` names `action_policy.floor`, true or false, even where the key is ignored or normalized away: an older target parses the raw key and rejects it before any trust rule, so check raw file presence, never the effective config (#582, seen twice)) lives in `incompatibleRecoveryTarget`, which every existing check site calls, and its name in `DAEMON_FEATURES`; never check a floor at one site only.
|
|
65
68
|
- Project autonomy (`autonomyCommand` in `src/hub/daemon.ts`, `writeAutonomyPreset` in `src/hub/settings.ts`): the `autonomy` control message is console-only; its operations come only from `planAutonomy` and `autonomyRemovalKeys`, never from the request; `off` removes the fixed key list and clears consent whatever `_autonomy` says. `_autonomy` is inert metadata, never approval provenance. Retained consent is created only by `createAutonomyConsent(...).confirm` inside the daemon (console `on`, or startup under a trusted `readAutonomyDefaults` opt-in with an exact plan match), never by a CLI or offline writer; a config `never-ask` takes effect only for a peer in `autonomyConfirmed`.
|
|
66
69
|
- Defer hub notices only for an authenticated recovery `prepare` or `commit` whose target features include `recovery_deferred_hub`, or for a restored daemon. Otherwise `setRecoveryHold(true)` moves the deferred notices into queues before commit records expected integrity, and a bare `fenceRecovery` defers nothing. Abort and lease expiry release the bus hold before clearing the operation; if that release fails, keep the operation and let expiry retry on its 1 s timer (#478).
|
|
67
70
|
- Resume a prepared `commitSent` source that is unavailable or database-busy inside the existing bounded shutdown poll; never re-send commit, re-prepare or close terminals again on that path. Require confirmed `stopped` before target startup, refuse a replacement instance, and retain the prepared receipt on timeout.
|
|
@@ -78,7 +81,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
78
81
|
- `reattachRecovery` (`src/cli/upgrade.ts`, #463) relaunches only the peer a blocked receipt's `blockedReattachment` names, once per its `reattachments` marker, after the lock, the runner claim and a re-read, and only against the same held target instance and version. The terminal proof is `inspectSavedTerminal` (`src/cli/terminal-recovery.ts`): only an `orphaned` pane without positive connected evidence or `missing` (a typed stale handle and a complete, untruncated inventory) counts as gone; `unknown`, `changed`, an orphaned-but-connected pane or a disconnected-only reading never authorizes a terminal. It counts as reattached only on a `done` restore receipt with the exact recorded session id, session file and project root, then leaves the operation blocked for `recovery resume`. It is person-only: `classifyPeerCommand` and `liveTestRefusal` refuse it from an agent shell.
|
|
79
82
|
- The interactive screens (`src/cli/upgrade-interactive.ts`, #272) change an operation only by running an existing command (`recovery resume|abort|dispose|reattach`, `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.
|
|
80
83
|
- Recovery presentation (#466, `src/cli/upgrade-presentation*.ts`) only renders, and only when `humanRecovery` holds (a TTY and no `--json`); `--json` stays the raw document. Recognise `--json`, `--verbose` and `--color=` only through `recoveryPresentationFlag` (`src/cli/recovery-presentation-options.ts`), which `liveTestRefusal` and `classifyPeerCommand` share, so a presentation flag cannot hide reattach or a repaired dispose from authorization. A screen's Enter default (`preferred`) is read-only (refresh, follow the runner, read the receipt); apply, resume, reattach and destructive choices need their own key, and an item with a `disabledReason` never runs. The receipt's `waits` and `presentationCause` describe the current attempt only: written best-effort (a failed timing save warns, never fails the operation or replaces the wait's own error) and cleared at each resume. Without them show `timing unknown` or an unknown cause; never substitute `updatedAt` or guess a cause from error text. With no typed cause, `renderBlocked` may show only the first recorded error line through `terminalText`, controls stripped, capped at 512 code points and labelled `typed cause unknown`, with the uniform action; never classify it or choose an option from its text (#478). `endInspection` (the completion and stop-and-archive summary) only inspects, and a failed reading is `runtime unknown`.
|
|
81
|
-
- `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.
|
|
84
|
+
- `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. `ahub upgrade` never updates the global CLI or the shared Claude plugin (#1039), so after a completed or scheduled upgrade it prints the step (#718, #1179): `globalUpdateAdvice` (`src/cli/runtime-version-warning.ts`) reuses `runtimeVersionWarnings`, the comparison `ahub doctor` prints, with the plugin read from the same `claude plugin list --json` probe, and speaks only when the CLI or the plugin is older than the daemon the upgrade installs. A handover sets `AGENTHUB_UPGRADE_HANDOVER=1`, and the target's coordinator then speaks for the older CLI that started it, which it cannot see. An unknown plugin (no `claude`, a failing or unparsable probe) is never a claim, a failure of the advice never fails an upgrade, a restart prints nothing, and `--json` stays the raw document. The sentence for the release notes is `globalUpdateStep` in `scripts/lib/release-plan.ts`: `release-prepare` writes it into every new `## Upgrading to <v>` section of `docs/quickstart.md` and `tagPreflight` refuses a tag whose section lacks it or still has a `TODO(agent)`.
|
|
82
85
|
- 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).
|
|
83
86
|
- `inspectRecovery` catches only `RecoveryDeferredHubError` from `readRestartSnapshot`: keep the stopped state and return the static `snapshotError` blocker without a `snapshot` binding. Never echo rejected content, repair the snapshot or offer resume from it (#478).
|
|
84
87
|
- The managed Claude hook (`src/cli/facts-hook.ts`) may add `permissionDecision: "allow"` on `PreToolUse` (for `ask-when-needed` only what `projectFileTool` accepts) and, since #636, `permissionDecision: "ask"` for a call the hub classed (`actionClasses` on its `facts` reply, in every permission mode, and an `ask` replaces any `allow`); it never adds `deny`, and a value that is present but unreadable asks. A hub that is down, slow or refuses decides nothing, so Claude's own policy stays in force. 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`) or of a headless Claude implementer's current child (rule below). For a declared instance with a `worktree` it answers `ok: false` without a current verified owner root (`ownerWriteRoot`), never a canonical-tree fallback, and that root is the reply's `projectRoot`. A permission-only hook (`hookPurpose`) never drives busy state (#242). With `AGENTHUB_HOOK_EVENT` set, the hook serves only the peer definition's `hooks.factsHook` port: input over 1 MiB or refused by its `decode` decides nothing, it sends `native_facts`, and it outputs only the port's `encode` (agy: an ephemeral message on SessionStart/PreInvocation, never a permission decision). The daemon refuses plain `facts` from a peer whose definition has `factsHook`.
|
|
@@ -110,6 +113,8 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
110
113
|
|
|
111
114
|
- Observe memory counters only through the machine-local candidate sidecar. Bind delivered counters to a positive journal acknowledgement and the original render context; skip private envelopes and current PII tasks/turns. Leave missing telemetry unknown, and never pass candidates or counters to evidence consumers. Require memory_correct for correction proposals, refuse PII turns, and keep project/identity authority server-side.
|
|
112
115
|
|
|
116
|
+
- Review observer input (#1056; contract in `docs/design/task-board.md` "Anchored review packages (#1056)"): while a peer holds review-scoped input (`reviewObserverHeld`, true on any throw) or its last delivery holds an `isReviewObserverExcluded` envelope, keep its capture, progress, DOING, session title and incoming label, memory counters and shared-memory writes held. Decide it only through those two predicates and the bus ports wired in `startDaemon` (`beforeReviewInputHandoff`, `reviewObserverScope`, `reviewObserverInputExcluded`, `onPulled`), never from message text. Durable state is the journal's (`reviewInputState`, `markReviewInputIncomplete`, `clearReviewInputIncomplete`), keyed by peer and stable native session hash. A sessionless peer still gets the delivery, held as incomplete. Only a confirmed ordinary next-turn input or a confirmed native session change releases a hold; presence-only traffic or more input in the same turn never does. Unknown provenance is held.
|
|
117
|
+
|
|
113
118
|
- Keep full memory home, generation ID, selector revision and malformed-row totals in authenticated person status/CLI receipts, never disk status.json or agent CLI output; low-privilege receipts use a generation prefix and omit home, selector revision and malformed-row totals. Emit typed unbound disabled/unavailable receipts with no profile when no memory handle opened; profile-check may treat an authenticated same-home generation-capable unbound daemon as memory-quiesced, never a stale bound handle. Scope profile-check readiness to its named registry inventory; old, unknown or unreadable receipts refuse readiness, and other custom registries need separate checks.
|
|
114
119
|
|
|
115
120
|
- The memory client (#752) is the in-process amem `MemoryClient` built once in `startDaemon` from the alias-resolved `memoryProject`, the bound store and candidates, `screenMemory` and the capture binding; its `check` rechecks that binding and the project before each call. `DaemonOptions.memoryClient` is a host-only seam, never read from configuration or a tool; `memory.worker_url` is ignored legacy configuration, never a destination. Local capture takes `memoryProject`, never the project chain. Shutdown aborts `syncStop`, awaits `syncRun`, then closes `memory`, the candidates and the store, in that order.
|
|
@@ -134,8 +139,8 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
134
139
|
|
|
135
140
|
- Share one monotonic 250 ms snapshot Git budget until the next event-loop I/O turn. Repository/HEAD probes, capture and end diffs consume the same remaining deadline; exhaustion or unreadable diff evidence records unknown trees and no attribution/undo, with one diagnostic per burst. Never substitute a stale baseline or apply a late result (#576). `DaemonOptions.snapshotClock` is host-only (tests hold it, rule: `tests.md`): never read it from configuration or a tool, production leaves it unset (`performance.now`), and the share stays 250 ms with `snapshotBurstMs` still clamped to it. Every deadline comparison goes through `budgetNow(budget)`, never `performance.now()` against `budget.deadline`.
|
|
136
141
|
|
|
137
|
-
- Channel tool-result hints (#493, #221): `withPendingHint` (`src/adapters/claude-channel.ts`) appends fresh `delivery_holds` counts, and drops the "call hub_inbox" advice whenever the same reply carries a held-delivery hint, because `bus.pull` refuses under that hold and the advice would send the session into a refused read. A `hub_inbox` result that already says how many messages remain unread adds only the important count, never a second queued count. Do not add a Claude client-version gate on steer without a probe of the older client and an optional hello field that carries the version (background: README).
|
|
138
|
-
- Memory sync (#438): stop an in-flight `syncMemory` only through `syncStop` (`src/hub/daemon.ts`), and await `syncRun` before the memory store closes, as `stopOnce` does. agent-mem runs the sync's git in its own process group, so a process that ends without `daemon.stop()` leaves that git and its transport child holding the memory repository. A signal the hub exits on goes through lifecycle stop (`STOP_SIGNALS` in `src/hub/lifecycle.ts`: SIGINT, SIGTERM, SIGHUP, SIGQUIT), never the default action; SIGQUIT in particular must not reach its default core dump (Bun honours it: agent-mem #435 measured a 5.9 GB core file and a delayed exit). The forced exit after the shutdown deadline stays `process.exit`. A sync result is logged as a failure only when `quietSyncError` is false: a missing remote in either agent-mem wording (`no sync remote`, `no generation sync remote configured`) and a stop are not failures (#401); a new agent-mem wording for them extends that one predicate.
|
|
142
|
+
- Channel tool-result hints (#493, #221): `withPendingHint` (`src/adapters/claude-channel.ts`) appends fresh `delivery_holds` counts, and drops the "call hub_inbox" advice whenever the same reply carries a held-delivery hint, because `bus.pull` refuses under that hold and the advice would send the session into a refused read. A `hub_inbox` result that already says how many messages remain unread adds only the important count, never a second queued count. A `hub_inbox` result that renders its own held or accepted metadata (`HELD_HINT_INCLUDED`) gets no second held hint and no polling advice, and unavailable metadata adds the inspect step instead of "Nothing is waiting" (#1172). Do not add a Claude client-version gate on steer without a probe of the older client and an optional hello field that carries the version (background: README).
|
|
143
|
+
- Memory sync (#438): stop an in-flight `syncMemory` only through `syncStop` (`src/hub/daemon.ts`), and await `syncRun` before the memory store closes, as `stopOnce` does. agent-mem runs the sync's git in its own process group, so a process that ends without `daemon.stop()` leaves that git and its transport child holding the memory repository. A signal the hub exits on goes through lifecycle stop (`STOP_SIGNALS` in `src/hub/lifecycle.ts`: SIGINT, SIGTERM, SIGHUP, SIGQUIT), never the default action; SIGQUIT in particular must not reach its default core dump (Bun honours it: agent-mem #435 measured a 5.9 GB core file and a delayed exit). The forced exit after the shutdown deadline stays `process.exit`. A sync result is logged as a failure only when `quietSyncError` is false: a missing remote in either agent-mem wording (`no sync remote`, `no generation sync remote configured`) and a stop are not failures (#401); a new agent-mem wording for them extends that one predicate. Every pass gets the host screen and policy version (`currentMemorySyncPolicy`, `memorySyncPolicyVersion`, #1173); a version change during the pass records `binding`, never success. Record outcomes only through `reduceMemorySync` and publish them only as `memorySyncView` (`sync` in memory status: closed cause, counts, next step). The log line names the closed cause (`classifyMemorySyncFailure`), never the raw error text. `DaemonOptions.memorySyncTrigger` is a host-only fixture seam.
|
|
139
144
|
- Schedules timer (#512a): `schedulesTimer` in `startDaemon` is its own interval, apart from the task sweep's, armed whenever `config.schedules` or `config.folder_triggers` is non-empty and re-armed in `reloadConfig` on a change to either; never gate it on `task_sweep.enabled`. `schedules` is machine-local (`MACHINE_LOCAL`) and never editable from settings (`NEVER_EDITABLE`): a committed file or a settings write must not be able to make the hub create tasks. The daemon status carries `schedules` (`scheduleViews`) only when some are configured.
|
|
140
145
|
- Anything the daemon starts on a timer and registers only with `startupCleanup` is not stopped by a normal `stop()`: `startupCleanup` runs on a failed startup only. Clear the timer and close the store in `stopOnce`, and wait for a pass in flight before the board closes (`schedulesPass`, #512a). A console op that runs the same writer on demand (`schedule_run`, #335) joins that wait through `manualSchedulePasses`; add any new on-demand entry to it, or `stopOnce` closes the board and the store under it.
|
|
141
146
|
- Corpus (#510a, `src/hub/corpus.ts`, `corpus-config.ts`): `corpus.paths` is machine-local and `corpus` is never editable from settings. Keep `corpus-config.ts` free of file-system imports, because the config schema (`config-apply.ts`) reaches the Claude channel bundle. A refusal of a root is a reason string, never a thrown error and never a partial read: evidence the check cannot read refuses, and only a completed read certifies a folder. The vault check is `.obsidian` at the real root, in every ancestor and anywhere below (any entry of that name), always on the `realPath`. A folder the pii, deny or local.deny rules protect is never listed, so it cannot be shown vault-free: the root that holds it is refused, never certified with that subtree skipped (a protected file is only skipped).
|
|
@@ -150,5 +155,7 @@ Scope: the operator console, control WS and its protocol, state files, the dashb
|
|
|
150
155
|
|
|
151
156
|
- Experiment comparisons (#599): preserve the separate conditional `research_comparisons` floor and integrity for plans, exact protocol/evaluator approvals and causal protocol-context rows. Never backfill legacy order from timestamps or carry approval to a changed hash. Keep the approval op under the console-only `study_` dispatch guard.
|
|
152
157
|
- Doctor memory capture (#1008, #1025): the `memory capture: <tool>` rows come only from `amem setup <tool> --json`, a preview that writes nothing (`readMemorySetup` in `src/cli/main.ts`: no shell, 10 s, 256 KiB), judged by the pure `nativeMemoryCaptureRow` (`src/adapters/definitions/native-cli-doctor.ts`). A plan not in the documented shape, a non-zero exit, a timeout or unparsable output is unknown, never ready; never read dotfiles text or a client's config, and do not scrub the caller's identity before the call.
|
|
153
|
-
- Handoff transport (#604): `handoffOut` connects to the target as a `console` role through `ControlClient` (same token file and machine user as every control message; the hello checks the target's project root and instance), and the target answers every handoff message, an unknown one included. `handoffNotifyOrigin` hangs off `board.onChange` inside its own try, so a task move never waits for, or fails because of, another project's daemon. A target is judged by its own `status.json` features (`checkTarget`), never by a guess from its version. The inbound `task_handoff` is refused while recovery is active, counts in `taskOpsInFlight` so a recovery commit waits for it, and hands `handoffIn` a fence (recovery or `stopping`) that it asks again right before the board write, after the PII screen's await. Keep `task_handoff` in `DAEMON_FEATURES` and its recovery guard beside `research_experiments`; no `PROTOCOL` bump, because a new message type is answered as unknown by an older hub and an optional field is ignored.
|
|
158
|
+
- Handoff transport (#604): `handoffOut` connects to the target as a `console` role through `ControlClient` (same token file and machine user as every control message; the hello checks the target's project root and instance), and the target answers every handoff message, an unknown one included. `handoffNotifyOrigin` hangs off `board.onChange` inside its own try, so a task move never waits for, or fails because of, another project's daemon. A target is judged by its own `status.json` features (`checkTarget`), never by a guess from its version. The inbound `task_handoff` is refused while recovery is active, counts in `taskOpsInFlight` so a recovery commit waits for it, and hands `handoffIn` a fence (recovery or `stopping`) that it asks again right before the board write, after the PII screen's await. Keep `task_handoff` in `DAEMON_FEATURES` and its recovery guard beside `research_experiments`; no `PROTOCOL` bump, because a new message type is answered as unknown by an older hub and an optional field is ignored. Handoff diagnostics (#1203 T3): a connection failure to a target is classified by `handoffConnectFailure` from the error's `code` (4426, 4404, `AHUB_CONNECT_TIMEOUT`, anything else), never from its message, and every handoff failure log goes through `handoffLog`, which keeps the last cause per target (`handoffFailures`, read by `handoffListView`) and rate-limits the line with `handoffLogGate`; a new silent `catch` in the handoff block is a defect. `handoffListView` opens no connection (registry and descriptor files only), so `--list` never waits on a target. `hub_task_handoff_sync` is a control op and not in `hub-tools.ts`: an agent never sees it, and `Tasks.handoffSyncAuthority` refuses anyone but the person or the conductor. `handoffCatchUp(only, wait)` with `wait` waits for the running pass and then runs its own, so a sync answers after a pass that began when it was asked, but never for longer than `handoffSyncBoundMs` (20 s, below the CLI's 30 s control timeout): a slower pass makes it answer `syncing: true` with the list, and a sync must never end in the generic "no answer from the hub". Handoff sequence (#1203 T2): `recordHandoffOutbound` applies a write with a `seq` only when it is newer, inside the one statement; `handoffEvent` returns `recorded: false` for a stale one before it publishes, so never publish a follow-back before the write says `applied`. `handoffCatchUp` reads `handoffOutboundOpen` (no cap; `handoffOutbound` is the capped display list, never the catch-up's input), runs one pass at a time and queues a request that arrives meanwhile (the targets it names, or all) to be drained when the pass ends, so the guard never drops a watcher's request; `handoffWatch` asks once for the whole changed set; it asks in chunks of `HANDOFF_STATES_CHUNK`, and checks `stopping` between requests; its timers go in `intervals`. A new handoff event goes only to an origin that advertises `task_handoff_seq`. Handoff key (#1203 T1): `handoffOut` sends a key only to a target whose status descriptor lists `task_handoff_key`; a timeout or closed connection (`handoffOutcomeUnknown`, which reads the client's fixed wording and anchors it) is resent once with the same key, then asked by key (`task_handoff_by_key`), then `outcome_unknown`, never `invalid`. A refusal of the resend never settles an unknown first attempt (its PII screen may still file the task): it is kept as a note and the by-key lookup still runs. The caller's deadline covers the whole recovery: `controlRequestTimeout` gives `hub_task_handoff` `HANDOFF_CONTROL_TIMEOUT_MS`, built from the same `HANDOFF_*` budgets `handoffOut` spends, so the CLI and the MCP path (both ask for 30 s) receive `outcome_unknown` with the key instead of a generic timeout; change a budget there and the deadline moves with it. Never write the outbound row before an answer, and never reset an existing one (`handoffSent`). The follow-back envelope carries `refs.handoff` and no `refs.task`: a new envelope field that names a task in another project must not reuse `refs.task`, which the bus judges against this board.
|
|
154
159
|
- Staged upgrades (#1039): retain `status.protocol` at the compatibility floor and publish `protocolMin`/`protocolMax`; negotiate the highest common control version and preserve each attached channel's actual negotiated protocol in recovery metadata. New scoped recovery receipts use schema 3 and per-project holds; keep legacy unscoped receipt behavior. Start ordinary project daemons from verified versioned release captures. Check `hub.db` with `db-schema.ts` before startup effects and every writable opener; advance its marker only with an incompatible format migration, never merely on opening with a newer binary. Before source replacement, controlled targets require `schema_guard`; preserve the retained target evidence of fully committed legacy receipts. Unguarded published binaries remain outside the bootstrap guarantee. Keep project runtime selection in bounded project-bound `runtime.json`; refuse Git-tracked or indeterminate repository state before using or replacing that authority. Updated 0.26+ up/manager/dashboard paths use its verified capture or a named version refusal. Preserve the explicit pre-0.26 published-CLI ceiling with a `ponytail:` note; never raise a DB/bootstrap floor for this selection. Compute path-scoped Git trust once per startup outside the writer lock; reuse its project-bound access for runtime reads and writes. Record selection and synchronous launch under the same registry lock used for capture publication and prune quarantine; confirm the unchanged selection and owner at readiness, and require the internal daemon to execute from that exact root. Scoped recovery must not refresh the shared manager. Prune only verified unreferenced versioned source captures, keep newest three/version, and defer on active recovery, unknown reference inventory or exhausted budgets. Schedule rate-limited maintenance after startup returns; keep only bounded metadata validation and atomic quarantine inside the registry writer lock, with candidate hashing and recursive deletion outside it. Refuse managed execution from reserved `.runtime-prune-*` paths; leave changed quarantine contents undeleted. Keep the 1 ms ledger wait on startup admission only; worker statements use the normal bounded wait. Record PID plus process signature and replace only a proven-gone running owner atomically under the admission transaction, with id/phase-fenced completion. Doctor names uncertain ledger state and leftover quarantine paths without repairing or deleting them. Reject empty recovery plans before source preservation.
|
|
160
|
+
- Questions (#1053, `src/hub/questions.ts`, `src/hub/question-desk.ts`): the store owns the row and its ONE terminal transition (`UPDATE ... WHERE state = 'open'`; of concurrent answer, stop and expiry exactly one sees `changes === 1`), the desk owns the waiting call and the hub-built envelope, and the daemon only supplies ports. `answer` and `questions` are console-role control messages that always reply (rid); a tools or peer connection never reaches `questionDesk.answer`, and nothing a peer sends (a `hub_send` body, a tool result, an envelope) is parsed as an answer. A late answer is `kind: "answer"`, `from: user`, id `answer-<set>` (so a republish after a crash is dropped by the bus's own dedupe) with `refs.question`; an expiry notice is `kind: "status"` from the hub, never an answer. Supersede keys on `from === USER && kind !== "answer"` in the bus tap, never on the body, and never on the hub's own answer envelope. Question and answer text reach nothing but `hub.db` and the envelope to the asking peer: the tap's log line, `redactBusEvent` and the `question` event all stub them, `QUESTION_TOOLS` skips capture and the progress observer skips `hub_ask`. `hub_ask` holds its control request open (`controlRequestTimeout`, `request_cancel` through the shared `invocationRequests` map, so a closed socket or an aborted turn cancels the row) and Pi's bridge lifts its idle timeout for that one call. A new peer kind gets `questionWaitMs` on its definition (fixed builtins only, `assertNativeCapabilityScope`), never a wait chosen by the caller. The `questions` feature label is a recovery label: an older target ignores the table, so keep it in `DAEMON_FEATURES`. Start repair runs once after `ready`'s locals exist (`questionDesk.repair()` just before `ready = true`), and the sweep timer is pushed only after the desk exists.
|
|
161
|
+
- Completions (#1054 T2/T3, `src/hub/completions.ts`, `src/hub/completions-store.ts`): a run opens at the `turn_start` that consumes a `from: user` delivery (never the hub's own `answer`), only for a peer whose definition declares `settle` and that is not pull-only. `settleCheck(peer)` is the ONE settle check and never throws; call it from every place a pending-work fact can change (turn end, `bus.onQueues`, a permission or question closing, an isolated check ending, `Tasks.checkEnded`). The bus takes a delivery out of its queue before the peer's idle reaches the state tap, so `queuedContinuations` also counts `dispatching` rows and `deliveredAwaitingTurn` (bounded to 10 s, `ponytail:` in the code); drop either and a continuation reads as nothing pending. Claude settles only on the verified native Stop (a lost channel or watchdog carries no id and never settles). A `limit` or budget pause ends the run with no completion (`budget.record`), an explicit stop calls `quietStop`, and `completions` is a recovery feature label (`hasPending`: an open run or an unread completion). The store keys a completion by the run's first turn id (`UNIQUE`), so repeated restarts yield one `interrupted` completion; never reconstruct from `events.jsonl`, which cannot say who started a turn. Receipts (#1054 T4/T5): `completion_receipts` has a UNIQUE index on `completion_id`, so `markSeen` is one compare-and-set and a mark-unread has exactly one row to delete; `result_seen` and `results` are console-role control cases (rid always) placed before `case "kill"`, and nothing else writes a receipt: never mark read from a snapshot, a status read, a listing or an envelope. The `result` need is level `watch` and is not in the `waiting`/`blocked` lists in `needs()`; keep it that way so a result never reads as something to act on. Reconstruction drops an open run whose peer has a budget pause record before it would settle it `interrupted` (persist before remove, per run), and `CompletionStore.prune` bounds the table to the newest 500 read completions at start; never prune an unread completion or an open run. The title of a result is computed at display (`resultView`), a stub when private, and no prompt or reply text is ever stored.
|
|
@@ -17,6 +17,7 @@ Scope: the hub-native local worker, its tools, path guard, denylist and seatbelt
|
|
|
17
17
|
- 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.
|
|
18
18
|
- After a tool with side effects ran, a failed turn is reported, never redelivered.
|
|
19
19
|
- A turn with any remote envelope (`remoteEnvelope(env)`: `from` containing `@`, or a hub `invocation`/`invocation_result` carrying `refs.invocationRemote`; rooms #941, #992; never test `from` alone) never joins `history` and never reaches `capture.observe`, like a PII turn, and every failure it reports (answer, delivery reason, log line) says only `remote turn failed`. Keep all three when you touch the turn's settlement or the tool loop.
|
|
20
|
+
- A review turn (any envelope `isReviewObserverExcluded` accepts, or an anchored `hub_review` call, which latches the exclusion for the rest of the turn; both in `src/hub/memory-policy.ts`) is handled like a remote turn: it never joins `history`, never reaches `capture.observe`, `capture.summarize` or `observeTool`, and every failure it reports (answer, delivery reason, log line) says only `review turn failed`. While latched, `hub_remember` and `hub_task_propose` are refused before they reach `taskTool`. A throwing `beforeInput` settles the delivery `failed_safe` (`review input provenance unavailable`) before the turn starts. Keep all of these when you touch the turn's settlement or the tool loop.
|
|
20
21
|
- `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.
|
|
21
22
|
- `Facts` (`src/hub/facts.ts`, #428) names a large file whose ctime is under `RACY_MS` by its git blob id: in `load()` and `observe()` only through `racyBlob()` (per-file `RACY_HASH_MAX_BYTES`, per-store `RACY_BURST_BYTES` per event-loop turn, memo cleared at `boundary()`), past which it keeps the stat-only stamp and makes no claim; `atHead()` keeps its own `HASHED_BYTES` bound, `tree()` reads large files by stat only, and `largeBlob()` caches only settled stamps.
|
|
22
23
|
- 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). Every live `capture.observe` passes a host `sourceId` (`<session>.<turn>.<step>.<n>`, #752); a new host caller supplies one too. Capture stores `paths` only for `read`/`write`/`edit` and only project-relative ones: command argv is never path metadata.
|