@staix/agent-hub 0.12.21 → 0.12.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +27 -12
- package/docs/agent-notes/adapters.md +15 -7
- package/docs/agent-notes/benchmarks.md +4 -1
- package/docs/agent-notes/budget.md +4 -0
- package/docs/agent-notes/bus.md +2 -1
- package/docs/agent-notes/daemon.md +12 -1
- package/docs/agent-notes/local-worker.md +4 -1
- package/docs/agent-notes/tasks.md +2 -1
- package/docs/agent-notes/tests.md +4 -0
- package/docs/events.md +17 -4
- package/docs/operations.md +478 -41
- package/docs/quickstart.md +14 -6
- package/docs/security.md +23 -11
- package/docs/smoke.md +133 -7
- package/docs/specs/2026-09-19-agent-hub-design.md +115 -15
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +84 -8
- package/docs/verification/2026-10-10-cli-output-phase1.md +309 -0
- package/docs/verification/2026-10-10-cli-output-phase2.md +672 -0
- package/docs/verification/2026-10-11-0.12.23.md +90 -0
- package/docs/verified.json +32 -17
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +14 -3
- package/src/adapters/acp-quota.ts +13 -0
- package/src/adapters/acp.ts +105 -17
- package/src/adapters/codex-appserver.ts +70 -35
- package/src/adapters/local-worker.ts +44 -10
- package/src/adapters/pi.ts +158 -19
- package/src/cli/bench.ts +2 -1
- package/src/cli/console-state.ts +28 -22
- package/src/cli/console.ts +20 -10
- package/src/cli/help.ts +25 -19
- package/src/cli/identity.ts +3 -3
- package/src/cli/init.ts +1 -1
- package/src/cli/launch.ts +182 -7
- package/src/cli/main.ts +409 -177
- package/src/cli/output.ts +363 -0
- package/src/cli/preview.ts +2 -1
- package/src/cli/recovery-package.ts +17 -0
- package/src/cli/reset.ts +3 -0
- package/src/cli/status-lines.ts +0 -28
- package/src/cli/terminal-recovery.ts +27 -1
- package/src/cli/upgrade-interactive.ts +297 -0
- package/src/cli/upgrade-runtime.ts +80 -9
- package/src/cli/upgrade.ts +50 -15
- package/src/hub/board.ts +2 -0
- package/src/hub/budget.ts +39 -7
- package/src/hub/bus.ts +15 -14
- package/src/hub/child-process.ts +11 -0
- package/src/hub/conductor.ts +23 -11
- package/src/hub/config-trust.ts +16 -5
- package/src/hub/conflicts.ts +2 -0
- package/src/hub/context-window.ts +8 -1
- package/src/hub/crash.ts +2 -1
- package/src/hub/daemon.ts +483 -83
- package/src/hub/db-timeout.ts +3 -0
- package/src/hub/delivery-journal.ts +2 -1
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +4 -1
- package/src/hub/execution-budget.ts +2 -0
- package/src/hub/facts.ts +5 -4
- package/src/hub/git.ts +32 -0
- package/src/hub/hub-tools.ts +1 -1
- package/src/hub/lifecycle.ts +10 -4
- package/src/hub/manager.ts +18 -5
- package/src/hub/peers.ts +7 -5
- package/src/hub/permission-mode.ts +14 -0
- package/src/hub/project.ts +2 -1
- package/src/hub/recovery-error.ts +31 -0
- package/src/hub/recovery-store.ts +43 -4
- package/src/hub/registry.ts +2 -1
- package/src/hub/routing.ts +114 -13
- package/src/hub/settings.ts +392 -0
- package/src/hub/snapshots.ts +4 -2
- package/src/hub/start-mode.ts +48 -0
- package/src/hub/tasks.ts +158 -43
- package/src/hub/terminal-open.ts +64 -0
- package/src/hub/ui.ts +25 -13
- package/src/local/deny.ts +71 -2
- package/src/local/sandbox.ts +47 -6
- package/src/local/tools.ts +25 -6
- package/src/memory/recall.ts +2 -2
- package/src/models/mlx.ts +2 -1
- package/src/pi/extension.ts +43 -20
- package/src/pi/tool-receipts.ts +19 -2
- package/src/ui/index.html +172 -21
|
@@ -151,7 +151,9 @@ hub CLI / console ── control WS 127.0.0.1:<ctl> ─────────
|
|
|
151
151
|
when it has one, otherwise what the `tool_call` update announced for the same `toolCallId`.
|
|
152
152
|
A payload that cannot be resolved either way is titled as unresolved and its `allow_always`
|
|
153
153
|
option is withheld, so a blind click cannot grant every later call of the session. The
|
|
154
|
-
watchdog sends `session/cancel
|
|
154
|
+
watchdog sends `session/cancel`. A durable delivery goes offline with `needs_review`;
|
|
155
|
+
without a journal, the peer stays busy until its cancelled prompt reports, bounded by
|
|
156
|
+
30 s, then goes offline with queued work retained if no report arrives. `--model` maps to `kimi --model <alias> acp`
|
|
155
157
|
(verified flag). Reusable for `opencode acp`.
|
|
156
158
|
- Local worker (amended in M3): a hub-native tool-calling loop over non-streaming
|
|
157
159
|
`POST /chat/completions` with plain `fetch`, no SDK (the repo keeps zero runtime
|
|
@@ -243,9 +245,13 @@ interface Envelope {
|
|
|
243
245
|
Every queue is bounded (`queue_cap`, default 200): on overflow the oldest non-`important`
|
|
244
246
|
envelope is dropped and reported on the console.
|
|
245
247
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
248
|
+
The inactivity watchdog per busy turn defaults to 300 s. ACP sends `session/cancel`: a
|
|
249
|
+
durable delivery goes offline with `needs_review`; on a bus without a journal, the peer
|
|
250
|
+
stays busy until the cancelled prompt reports, then drains on idle. After the 30 s bound
|
|
251
|
+
without a report it goes offline with queued work retained, never idle. Cancelled results
|
|
252
|
+
and errors are discarded; an error during that wait leaves one log line. Other adapters
|
|
253
|
+
keep their cancellation rules (`turn/interrupt` for Codex). Queues drain in order only
|
|
254
|
+
when the adapter is idle. Delivery is at-least-once with
|
|
249
255
|
idempotency by `id`: a failed delivery returns to the queue head and is retried after 1 s, and
|
|
250
256
|
after 3 failures it is reported as undeliverable on the console so one envelope cannot block a
|
|
251
257
|
peer. Peer ids claimed over the control WS must not be `user` or a hub-managed adapter id.
|
|
@@ -382,12 +388,23 @@ inside a peer.
|
|
|
382
388
|
and runs the original command unchanged. `~/.claude/settings.json` is never edited; a
|
|
383
389
|
user-supplied `--settings` wins and turns the tee off. The `--settings` injection is
|
|
384
390
|
verified live (2026-09-19: `claude-usage.json` appears within seconds, the wrapped HUD
|
|
385
|
-
renders unchanged, `ahub budget` shows both windows). Kimi
|
|
386
|
-
rolling 5 h
|
|
391
|
+
renders unchanged, `ahub budget` shows both windows). Kimi has an optional soft
|
|
392
|
+
rolling 5 h ceiling for validated consumed-token readings, `budget.kimi_tokens_5h`
|
|
393
|
+
(off by default), and the observed Kimi Code CLI 2.1.1 five-hour provider refusal
|
|
394
|
+
is a hard source (#306). Only the pinned rejected-prompt text is recognised,
|
|
395
|
+
bound to verified native identity and the current turn. It reports `5h`, used 1,
|
|
396
|
+
source `kimi ACP usage-limit refusal`, and a conservative 300-minute reset
|
|
397
|
+
estimate; weekly/monthly limits and other ACP providers remain unverified. Verified live on kimi
|
|
387
398
|
2.0.1: one update per turn, payload `{"sessionUpdate":"usage_update","used":<tokens>,
|
|
388
399
|
"size":<context window>}`; `used` is the session's context occupancy against `size`
|
|
389
400
|
(1M), not billed quota. ACP records this shape as context occupancy diagnostics,
|
|
390
|
-
never as cumulative consumed tokens.
|
|
401
|
+
never as cumulative consumed tokens. Since #285 phase 1 it is also the peer's
|
|
402
|
+
native context reading (source `acp_usage_update`), shown in `ahub status`, the
|
|
403
|
+
console and the dashboard like Claude's and Codex's; the adapter accepts the
|
|
404
|
+
update whenever it names the current session, since 2.1.1's source text emits it
|
|
405
|
+
after the prompt resolves (an order read from the source, not yet observed live
|
|
406
|
+
through the hub; docs/smoke.md). A gate crossing records the
|
|
407
|
+
event and notice for an ACP peer but sends it no checkpoint request yet. Only validated cumulative totals or input/output
|
|
391
408
|
pairs feed native token readings; unsupported shapes remain unknown. `ahub budget set`
|
|
392
409
|
feeds a reading by hand. `local` has no quota and is never paused.
|
|
393
410
|
- Gate at `budget.gate` (default 0.9) on any fresh window; readings older than
|
|
@@ -405,7 +422,17 @@ inside a peer.
|
|
|
405
422
|
released, so it leads the first delivery. Moved tasks stay with their new owners. A manual
|
|
406
423
|
`ahub pause` is never lifted by the coordinator; `ahub resume` does not override a budget
|
|
407
424
|
pause, `ahub budget resume <peer>` does, and the coordinator then leaves that peer alone
|
|
408
|
-
until the window that paused it has reset
|
|
425
|
+
until the window that paused it has reset, unless a provider refuses another turn:
|
|
426
|
+
that hard refusal ends the override and supersedes a pending soft checkpoint. An
|
|
427
|
+
untouched ACP refusal is requeued only after the hub confirms its budget pause;
|
|
428
|
+
otherwise it stays held for review. After activity, the delivery remains held
|
|
429
|
+
even though open tasks are relayed, and the queue stays held after the budget
|
|
430
|
+
reset until a person inspects and resolves it. Keep one pending resume notice carrying
|
|
431
|
+
every public move since the previous notice was delivered; retain an older unstructured
|
|
432
|
+
notice as public text. Within one hub run, retain a still-future reset bound for the same
|
|
433
|
+
window across an override, while an explicit provider reset remains authoritative. A
|
|
434
|
+
restored pause without per-window reading identity starts a fresh estimate at the next refusal.
|
|
435
|
+
A handoff waits until another peer is attached
|
|
409
436
|
(right after a restart nobody is), readings keep their own timestamp, and a window whose
|
|
410
437
|
reset time has passed no longer counts. A peer whose window reset while the hub was down is
|
|
411
438
|
not paused again, and still gets the resume envelope once it attaches. With no status line
|
|
@@ -667,7 +694,7 @@ M6 internal inference, packaging
|
|
|
667
694
|
| Anthropic-to-chat translation drops `tool_use` | Claude never runs on local models in this design |
|
|
668
695
|
| Local worker executes untrusted room text | Untrusted framing, cwd scope, denylist, no `--unattended` by default |
|
|
669
696
|
| Steering a Codex turn changes its plan mid-flight | Only `important` steers; default tier is `status` |
|
|
670
|
-
| Kimi ACP has no quota API |
|
|
697
|
+
| Kimi ACP has no quota API | Optional token soft ceiling plus the observed five-hour provider refusal as a hard source; conservative reset, other limits unverified |
|
|
671
698
|
| claude-mem worker API is internal and unversioned for third-party clients | Client isolated in `src/memory/`, pinned to a plugin version range, fail-open everywhere, contract test against a fake worker plus one live smoke |
|
|
672
699
|
|
|
673
700
|
|
|
@@ -812,8 +839,15 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
812
839
|
Loaded non-default Kimi modes are reset for project ask.
|
|
813
840
|
- Codex overlays on-request/never on every outgoing turn/start without changing
|
|
814
841
|
sandbox fields. Ask restores a known native policy once after an accepted
|
|
815
|
-
turn; rejected restoration remains pending.
|
|
816
|
-
|
|
842
|
+
turn; rejected restoration remains pending. Restoration debt and captured
|
|
843
|
+
policies are per thread and survive proxy detach and thread/start or thread/resume.
|
|
844
|
+
A known restoring turn retains its baseline even if the TUI echoes the resumed
|
|
845
|
+
response's sticky override, in any hub permission mode. After restoration is
|
|
846
|
+
admitted, a later repeated TUI policy is indistinguishable from an explicit
|
|
847
|
+
native choice and passes through; only the restoring turn is protected.
|
|
848
|
+
Unknown native baseline refuses restoration while attached, with instructions
|
|
849
|
+
to close the TUI and run `ahub permission codex ask`; detached ask succeeds and
|
|
850
|
+
returns a CLI-visible note about the old thread's unchanged native policy. Actual native unattended flags, not broker-wide flags, govern
|
|
817
851
|
the refusal to change a bypassed session.
|
|
818
852
|
- Claude ask-when-needed allows only Read/Edit/Write/MultiEdit/NotebookEdit/LS
|
|
819
853
|
and bounded Glob/Grep targets resolved with realPath beneath project root,
|
|
@@ -823,8 +857,17 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
823
857
|
Merged/generated settings are 0600 state files passed by path, never inline
|
|
824
858
|
in argv. Preview writes no settings file. The owning launcher removes its settings file
|
|
825
859
|
after native exit; earlier active or unknown launchers retain theirs. Verified
|
|
826
|
-
dead wrapper and native identities permit crash fallback cleanup.
|
|
827
|
-
|
|
860
|
+
dead wrapper and native identities permit crash fallback cleanup. Direct binary
|
|
861
|
+
launches publish the native child PID/signature while running, bound to the exact
|
|
862
|
+
launch record; crash cleanup additionally requires its PID absent from a verified
|
|
863
|
+
process table. Script/interpreter native ownership remains unknown. Without
|
|
864
|
+
separate native identity proof, prior files remain for manual inspection. Every
|
|
865
|
+
launch atomically publishes its managed hook/session record without depending
|
|
866
|
+
on the native-proof lock. Exact-binding native proof is stored separately, so
|
|
867
|
+
an old callback cannot replace a later launch record. The populated signed
|
|
868
|
+
native-proof lock waits up to 250 ms for a live owner; an unknown/busy lock
|
|
869
|
+
disables only native identity/crash cleanup with exact-path guidance. Managed
|
|
870
|
+
hooks keep working for the current launch.
|
|
828
871
|
- Permission-only Claude PreToolUse never drives busy state. Only the former
|
|
829
872
|
observation purposes retain native turn bookkeeping. Hook identity/purpose
|
|
830
873
|
are launch/session bound; unverified hook status reads unverified.
|
|
@@ -1386,12 +1429,19 @@ Codex reports `tokenUsage.last.totalTokens / tokenUsage.modelContextWindow` in
|
|
|
1386
1429
|
and [native TUI](https://github.com/openai/codex/blob/main/codex-rs/tui/src/token_usage.rs)
|
|
1387
1430
|
distinguish the last active context from the accumulated `total`; the displayed
|
|
1388
1431
|
raw occupancy does not apply the TUI's baseline-adjusted remaining percentage.
|
|
1432
|
+
The Kimi peer reports `used / size` in `usage_update` (source
|
|
1433
|
+
`acp_usage_update`, #285 phase 1; only Kimi is wired): the session's context
|
|
1434
|
+
occupancy, accepted whenever it names the adapter's current session, including
|
|
1435
|
+
outside a turn; 2.1.1's source text emits it after the prompt resolves (read
|
|
1436
|
+
from the source, not observed live), when the peer may already be idle.
|
|
1389
1437
|
Pi's RPC state exposes no measured native counter. Its extension context API
|
|
1390
1438
|
returns an estimate, so Pi and unsupported peers remain unknown here.
|
|
1391
1439
|
|
|
1392
1440
|
Each reading records source and measurement time. Claude's status-line file is
|
|
1393
1441
|
bound to the daemon instance, managed launcher and native session; Codex's
|
|
1394
|
-
notification is fenced by its owning link and native thread
|
|
1442
|
+
notification is fenced by its owning link and native thread; an ACP reading is
|
|
1443
|
+
bound to the adapter's own session id and dropped when that is not the daemon's
|
|
1444
|
+
current session. Stale, invalid,
|
|
1395
1445
|
disconnected or replaced-session readings expose unknown occupancy, never zero.
|
|
1396
1446
|
`context.gate` defaults to 0 (off); `context.stale_min` defaults to 30.
|
|
1397
1447
|
A valid above-gate reading emits one metadata-only `context_pressure` event
|
|
@@ -1430,6 +1480,15 @@ approval. Deny is direct. Pending requests include expiry and are withdrawn by
|
|
|
1430
1480
|
`permission_closed` on any answer or cancellation. Answer audits have no title.
|
|
1431
1481
|
Panels expose Peers, Approvals, Tasks, Queue and Events with bounded polling and
|
|
1432
1482
|
Unicode cell widths, fall back below 80x24, and restore terminal state on exit.
|
|
1483
|
+
Tabs change with the digits 1 to 5 or with Left and Right, wrapping at both
|
|
1484
|
+
ends, with the digit's guards and resets (an arrow under a confirmation dismisses
|
|
1485
|
+
it like any key but y, without moving a tab); a read holding nothing but arrow
|
|
1486
|
+
sequences is that many key presses, never a paste. An Events row's header is
|
|
1487
|
+
tokenized by the stream's own tokenizer on the same tab-flattened text, with the
|
|
1488
|
+
event's tone and kind; body lines stay plain, and selection changes only the
|
|
1489
|
+
`>` marker. The tail prints a peer's context reading once and repeats it only
|
|
1490
|
+
when the printed line changes apart from its time (the printed percentage, the
|
|
1491
|
+
freshness, the source, or known against unknown).
|
|
1433
1492
|
|
|
1434
1493
|
Whole-board progress (#246) uses one pure `taskProgress` function shared by the
|
|
1435
1494
|
console and hashed dashboard script. It reads only public id/state/deps fields;
|
|
@@ -2075,7 +2134,7 @@ acts on the stopped state directory.
|
|
|
2075
2134
|
## Amendment: proven Pi tool refusals and idle exit recovery (issues #254, #255)
|
|
2076
2135
|
|
|
2077
2136
|
- A trusted guard uses `PreEffectToolRefusal` only when that operation has performed no task or board effect; PII screening and its telemetry record may precede the refusal. Pi receives the same `error: <message>` refusal as the control client, and its session/call receipt is `done`. Initial task ownership/dependency/state checks and conductor entry peer/mode/assignment/escalation validation opt into the type; shared helpers default to ordinary errors, and conductor checks after an awaited release validation stay ordinary. Generic errors, board readback failures after a write, and receipt settlement write failures remain uncertain/pending and are never repeated. A legacy pending row has no pre-effect proof in its stored fingerprint/state/result, so startup preserves it; neither present board state nor a matching error message establishes historical refusal.
|
|
2078
|
-
- Each Pi owner reports one exit with cause, exit code or signal, whether startup completed, whether a turn/tool was active, whether teardown was expected, and the last validated tool name only. TUI shutdown/owner-loss and verified stop teardown lack observed OS status and record unknown. A successfully stopped claimed TUI owner reports once; a never-owned launch reports no invented exit. Internal teardown notices direct inspection and `ahub pi`, without claiming a person requested them. Console notices name the next action or automatic-start state; headless code 0 is still an exit and settles active deliveries as uncertain.
|
|
2137
|
+
- Each Pi owner reports one exit with cause, exit code or signal, whether startup completed, whether a turn/tool was active, whether teardown was expected, and the last validated tool name only. TUI shutdown/owner-loss and verified stop teardown lack observed OS status and record unknown. A successfully stopped claimed TUI owner reports once; a never-owned launch reports no invented exit. Replacement teardown says the owner stopped for a new Pi owner even if replacement startup is refused. Internal teardown notices direct inspection and `ahub pi`, without claiming a person requested them. Replacement, failed-start and active-turn guidance takes precedence over the automatic-start-disabled notice. Console notices name the next action or automatic-start state; headless code 0 is still an exit and settles active deliveries as uncertain.
|
|
2079
2138
|
- With `pi.auto_start`, an unexpected idle headless exit attempts its verified persisted session once in a 60 s window. The daemon owns the attempt/window across adapter replacements, blocks throughout an in-flight attempt, and rearms the 60 s window when that attempt settles. It checks current-owner identity, completed startup, turn/tool inactivity, start serialization, shutdown and recovery before and after awaited receipt drain/session capture. Missing or invalid persisted state leaves Pi offline with `ahub pi` guidance, with no fresh-session fallback. A second exit inside the window stays offline. Requested stops, stale owners, startup failures, active turns/tools and native terminal exits never trigger this automatic path; crash recovery's existing #66 behavior remains separate.
|
|
2080
2139
|
## Amendment: benchmarks (issue #251)
|
|
2081
2140
|
|
|
@@ -2104,3 +2163,44 @@ acts on the stopped state directory.
|
|
|
2104
2163
|
settings for the rest.
|
|
2105
2164
|
- The dashboard snapshot carries run summaries and per-arm measures only. `bench run` is a person's (identity gate,
|
|
2106
2165
|
wherever `--json` stands); reading results is not. Schema and commands: [bench](../bench.md).
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
## Amendment: Pi/local approval expiry and cancellation (issue #253)
|
|
2169
|
+
|
|
2170
|
+
- An unanswered Pi/local approval returns an explicit expiry result: no person answered, do not retry that call, hand off or stop. It is not an answered denial. Two consecutive expiry results in the same native Pi turn or local worker turn end that turn with a console notice. An expiry counts toward the streak only when no person answered a request of that peer while it was pending (a per-peer answer epoch makes a parallel batch safe for an answering person); a grant served from the always cache or a permission mode is not an answer and does not reset the streak, and an actual new turn resets it. The turn ends through the same abort path an execution-budget stop uses, so Pi stays attached and the sender is told the approval reason. Supported Pi and local complete that correlated delivery after the reason reply, allowing the next queued message without an operator release. Pi clears the turn-only approval state at settlement and failure, so idle human shells remain usable and later failures cannot inherit its reason; a delayed approval abort rechecks that the same turn is still running. An extension that cannot abort a turn (one that does not advertise `approvalTurnAbort`) leaves Pi offline instead, with `ahub pi` restart and held-delivery settlement guidance. The second result remains an expiry tool failure before the abort. Approval stops do not invoke automatic safe replay or inference handoff.
|
|
2171
|
+
- Tool cancellation reaches the central approval registry. Settlement atomically removes the request and timer/abort listener before publishing closure or resolving. Late answers fail, cannot populate Pi's exact-tool always cache, and do not hold recovery waiting for approvals. Tool and budget continuations retain their captured session/generation; an admission response rechecks the owner and controller before changing a timer or recording a stop, and an old extension callback never aborts a later turn. Normal tool requests carry the session and generation captured before budget admission; the bridge refuses missing/stale lineage before registry, receipt or effect work, with restart guidance for an older extension. Spawned headless owners are pinned by the actual child process signature and its proven RPC session; TUI owners require the native claim. A Pi native abort is scoped to the verified owner, session, turn generation and call id; it withdraws only that call. Early native aborts fence a bounded set of call ids, with overflow failing closed for the remainder of that running turn. A managed tool that does not settle within the stop grace does not block the teardown: the stop goes on (shutdown, owner teardown, process stop) and reports the failure at the end.
|
|
2172
|
+
- A person's TUI shell command uses the existing authenticated idle reservation, session/owner/generation validation, sandbox and run-budget admission. It bypasses hub approval only in that trusted context and returns the sandbox's structured exit code through the receipt and native extension. Model tool arguments or printed output cannot establish human origin or exit status.
|
|
2173
|
+
- The dashboard counts waiting Pi/local approvals and directs the person to the console or `ahub tail` / `ahub permit`. It keeps those rows deny-only and never reveals private titles.
|
|
2174
|
+
|
|
2175
|
+
## Amendment: requested headless peer stop (issue #278)
|
|
2176
|
+
|
|
2177
|
+
- A console-role `{ t: "peer_stop", peer }` request stops a hub-owned headless ACP, Pi or local runtime through its adapter's owned-runtime teardown. Success is `{ ok: true, state: "offline" }`; refusal or incomplete stop is `{ ok: false, error }` naming the reason and next action. New requests already receive an unknown-request reply on older hubs, so `PROTOCOL` stays unchanged.
|
|
2178
|
+
- Refuse agent/tools-role callers, unknown or offline peers, Claude/Codex and Pi TUI owners, and recovery/shutdown-held mutations. Starts, requested stops and permission changes are fenced per peer; recovery preparation waits until requested stops finish. A TUI is ended in its terminal, or by the upgrade plan's verified terminal binding.
|
|
2179
|
+
- A requested stop marks each in-flight delivery `needs_review` with a fixed ids-only stop reason, cancels pending approvals, and fences stale native completion/failure callbacks. It never completes or implicitly redelivers interrupted work, invokes failure escalation, or triggers Pi's idle auto-restart. The operator inspects effects and explicitly resolves the delivery before replay. Queued unattempted work remains for the next manual start under existing holds/pauses.
|
|
2180
|
+
- `ahub stop <peer>` is human-only in `classifyPeerCommand`, prints the returned state, and reports an older hub's unknown request as requiring an upgrade, with a nonzero exit. The upgrade plan's headless `endPlannedPeer` uses the same console request; the daemon does not close a native terminal.
|
|
2181
|
+
|
|
2182
|
+
- Requested stop admission also holds per-peer drain, pull and steering before any teardown await, without changing operator/budget pauses. Arriving work remains queued for explicit restart. Idle Pi captures live empty-session proof before stopping; a first accepted prompt with unpersisted history is not silently recreated as empty.
|
|
2183
|
+
|
|
2184
|
+
## Amendment: settings from the dashboard and the terminal (issue #269, part 1)
|
|
2185
|
+
|
|
2186
|
+
- The dashboard stops being read-mostly for one closed list: a settings registry in `src/hub/settings.ts`, one entry per editable setting (key, type and allowed values, the store, when it applies, and whether it can raise authority). The dashboard's Settings section and `ahub settings list|get|set|undo` render from it and write through it; a key outside it cannot be written from either. Keys that choose what the hub runs or where secrets and task text go (`kimi_cmd`, `codex_bin`, `pi.cmd`, `checks`, `mlx`, `omniroute`, `memory.worker_url`, `local.read_allow`, `local.bash_network`, `local.network_allow`, `bench.enabled`, `terminal`) are refused by name from every surface.
|
|
2187
|
+
- First list: `permission.<peer>` (the running mode, the console's `permission` request), `permission_modes.<peer>` (the default at hub start), `pi.auto_start`, `routing.stay_switch`, per class `routing.classes.<class>.peers`, `.escalate_to`, `.route` and `.pi_backend`, and the switches `research.enabled`, `approvals.notify`, `snapshots.enabled` and `coordination`. Start modes (`peers.<id>.start_mode`, the terminal provider, Pi's TUI default) are part 2 of the issue.
|
|
2188
|
+
- Stores: config values go to `.agenthub/config.local.json`; routing values go to a new machine-local overlay, `.agenthub/routing.local.toml`, which may hold only those routing keys and is merged over `routing.toml` key by key by the one loader (`parseRouting`). `assign()` stays pure; its trace, and so `ahub route explain`, names the keys the overlay set. The tracked `config.json` and the hand-written `routing.toml` are never written, a machine-local file that git tracks is refused, and `ahub init` adds the overlay to `.gitignore`. A write is validated by the real loader first (a scratch copy for the config, the parser for the overlay), replaces the file through a temp file and rename, and keeps one previous version: undo restores it, refuses when the file changed since, and validates what it restores. The undo record is written after the file, so a write that failed leaves the earlier undo in place. A write that would change nothing (the same value again, or removing a value that is not set) writes no file, keeps the undo record and records no event. A config setting is refused in a project that has no config file at all (neither `config.json` nor `config.local.json`): its first config file would switch the project defaults on (snapshots, limits, the budget relay), and that is `ahub init`'s to do. In a repository where git cannot say whether the file is tracked, nothing is written; a project outside git has nothing tracked. Removing a machine-local value (`inherit`) is a write like any other. When config trust (#17) drops the written field at load (`permission_modes` in a file git cannot vouch for), the reply and the setting's row say that the hub does not read the value.
|
|
2189
|
+
- Authority. An ordinary dashboard session reads settings, lowers a mode to `ask`, turns `pi.auto_start` off and changes the safe switches. A settings session, opened by `ahub ui --settings` (a console command behind the identity gate, a console-role `ui` request with `settings: true`), may also raise: a non-ask mode or default, routing, `pi.auto_start`, removing a machine-local value, and undo. Its ticket marks the session for 15 minutes; afterwards the session continues as an ordinary one until its hour ends. `never-ask`, running or stored, needs the peer id as the confirmation (typed in the dashboard, `--yes` in the terminal); a stored never-ask default still waits for the console's `y` at each hub start (#242). No other path puts never-ask on disk: an undo that would restore it is refused (set it again with its confirmation), and `inherit` that would uncover a never-ask from the shared file needs the same confirmation. Tool approvals stay deny-only for Pi and local in every session. The unified dashboard forwards settings actions with ordinary authority.
|
|
2190
|
+
- A mode set from the dashboard or `ahub settings` is `changePermission` inside `permissionChange`, the console's path, with its refusals (not attached, unverified Claude hook, unattended Codex or Claude, a stop in progress) and its `permission_mode` event.
|
|
2191
|
+
- Preview before save: a routing change returns `route explain` for up to 20 open tasks under the policy in force and under the candidate (PII tasks are counted, not shown); a mode change returns the peer's bounds and what the mode grants.
|
|
2192
|
+
- Every change is announced in `hub.log` and open consoles with its source and recorded as a `settings` event: key, value before and after, source (`dashboard` or `terminal`), `undo`. Values are closed words, booleans, peer ids or route ids, never free text.
|
|
2193
|
+
- Control protocol: `settings_get`, `settings_set`, `settings_undo` and `settings_preview` are new console-role requests, and `ui` takes an optional `settings` flag and echoes it; an older hub answers the requests as unknown, and the CLI refuses a `ui` reply without the echo, so `PROTOCOL` stays 16. The `ui_snapshot` reply gains an optional `settings` object. Events schema stays 1.
|
|
2194
|
+
- `ahub settings` talks to the running hub (ponytail: a file-only path if setting a start-time value on a stopped hub is ever wanted).
|
|
2195
|
+
|
|
2196
|
+
## Amendment: TUI by default, headless as opt-in (issue #269, part 2)
|
|
2197
|
+
|
|
2198
|
+
- `peers.<id>.start_mode` (`tui` or `headless`, for pi and codex; `tui` when unset) is the mode a start takes when nothing more specific was asked. Claude has only a TUI; Kimi (ACP) and the local worker have no hub-attached TUI and stay headless. The loader refuses a malformed `peers` block.
|
|
2199
|
+
- A person typing `ahub pi` gets that mode: the TUI in their terminal by default. `--mode headless` or `--headless` asks for headless. Without a terminal (the console's command line, a script) and with the default asking for the TUI, the CLI sends the console-role `peer_start` request and the hub opens a terminal, or refuses with the command; an option given then is refused, because the fixed command carries none. An explicit `--mode tui` is taken at its word. `ahub codex` and `ahub claude` typed by a person are unchanged.
|
|
2200
|
+
- A start the hub makes itself is `hubStart(peer)`: the dashboard's `start_peer` action, `peer_start`, the conductor's `hub_peer_start` and `pi.auto_start` at hub start. With `headless` it starts the peer as before (for Codex: the app-server only). With `tui` it opens a terminal running `<runtime> <CLI entry> --project <root> <peer>` (`--mode tui` for Pi), assembled from the hub's own words and quoted word by word; no caller text is part of it. The peer is reserved before the provider is asked, so starts that arrive together open one terminal, and a second start within 30 seconds of an opened terminal is refused instead of opening another (a failed open frees it at once). A start mode of headless never takes over a Pi that runs in a terminal.
|
|
2201
|
+
- Terminal providers, in order: `terminal.open`, a machine-local argv template (`{command}` in exactly one element, plus `{title}` and `{cwd}`; a `MACHINE_LOCAL` field under config trust and on the never-editable list), then Orca (`orca terminal create --worktree id:<id> --command ...`) when the hub runs in an Orca worktree or a recorded launch of the project names one. Each template placeholder is substituted once; only `{command}` is shell-quoted. The opened terminal's environment carries no agent marker and no Orca terminal identity of the hub's own. A provider's error text is logged, not returned. With no provider the start is refused with the manual command; it never falls back to headless. Only the running product wires the real providers; a daemon constructed without them opens nothing.
|
|
2202
|
+
- The conductor's `hub_peer_start` no longer takes a mode: it takes the peer's start mode, and a `mode` argument that differs is refused. It may now start claude and codex through the provider, and codex headless when a person set that.
|
|
2203
|
+
- Recovery restores each peer in the mode its recorded session had. After an unplanned stop, `pi.auto_start` no longer starts a fresh headless Pi in place of one that ran in a terminal (with or without a recorded session file), unless headless is Pi's start mode. `peers.<id>.start_mode` may be set in the shared config file too: it is not a machine-local field.
|
|
2204
|
+
- Settings: `peers.pi.start_mode` and `peers.codex.start_mode` join the registry (group Start, stored in `config.local.json`, raising except to `tui`). They are the one stored setting a running hub takes up: after a settings write the hub re-reads the block, so the mode applies at that peer's next start. The dashboard gains a Start control per peer that is not attached, saying the mode and the provider (or why there is none); it offers no button when the start would be refused. An ordinary session may press it: it runs only the fixed start. The page blocks repeat starts while the request is in flight and unlocks after a confirmed answer, leaving the hub's terminal reopen guard authoritative. The manager waits 35 seconds (the terminal provider cap plus its margin); a slower headless start can end unconfirmed. After an unconfirmed manager result the page holds that project/instance/peer control for `PEER_START_RETRY_MS` (the same 30-second bound as the hub's reopen guard), or until the `starts` snapshot confirms attachment, and names the retry bound. A confirmed project-local dashboard start behaves as before; no manager or settings authority changes.
|
|
2205
|
+
- Control protocol stays 16: `peer_start` is a new console-role request an older hub answers as unknown, and `ui_snapshot` gains the optional `starts` list.
|
|
2206
|
+
- Migration: Pi's default changes from headless to its TUI. `ahub settings set peers.pi.start_mode headless` (or the same key in `config.local.json`) keeps a headless auto-started Pi; scripts add `--headless`.
|
|
@@ -17,16 +17,24 @@ their matching CLI; the new coordinator never guesses a PID to terminate.
|
|
|
17
17
|
|
|
18
18
|
## Commands
|
|
19
19
|
|
|
20
|
-
- `ahub upgrade --to <exact-version> --dry-run`: inspect the running registered
|
|
21
|
-
projects, release identity, native session mappings and blockers.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
- `ahub upgrade [--to <exact-version>] --dry-run`: inspect the running registered
|
|
21
|
+
projects, release identity, native session mappings and blockers. Without
|
|
22
|
+
`--to` the target is the registry's latest release (#272).
|
|
23
|
+
- `ahub upgrade [--to <exact-version>] [--yes]`: review the plan, revalidate it and
|
|
24
|
+
schedule an independent recovery process. `--yes` accepts the displayed scope
|
|
25
|
+
and needs `--to`; it never overrides blockers. On a terminal without either
|
|
26
|
+
flag the review, the apply and the progress are one interactive session (see
|
|
27
|
+
"Interactive screens").
|
|
28
|
+
- A target newer than the running CLI is handed to that release's own
|
|
29
|
+
coordinator (`bun x --package @staix/agent-hub@<version> ahub upgrade --to
|
|
30
|
+
<version>` with the same flags), named on stderr first and confirmed on a
|
|
31
|
+
terminal (#272).
|
|
25
32
|
- `ahub --project <path|id> restart [--dry-run] [--yes]`: recover one project using
|
|
26
33
|
the current package, without changing the global installation or plugin.
|
|
27
|
-
- `ahub recovery status|resume <operation-id
|
|
28
|
-
the recorded operation after checking live ownership.
|
|
29
|
-
|
|
34
|
+
- `ahub recovery status|resume [<operation-id>]`: read redacted progress or resume
|
|
35
|
+
the recorded operation after checking live ownership. Without an id every
|
|
36
|
+
`recovery` command takes the operation that holds the machine's lock (#272).
|
|
37
|
+
- `ahub recovery abort [<operation-id>]`: cancel a preflight and release its lock
|
|
30
38
|
only before a runtime has stopped or a terminal mutation has been attempted.
|
|
31
39
|
|
|
32
40
|
Upgrade uses all affected running registrations. Stopped projects stay stopped.
|
|
@@ -508,3 +516,71 @@ lose. Detaching forgets nothing; a thread whose start the log does not show
|
|
|
508
516
|
(resumed, an older hub, a pruned log) is unsure and stays a plan blocker, as
|
|
509
517
|
does one with turns. A planned fresh start still needs the store to show the
|
|
510
518
|
rollout missing: an unreadable store blocks the close and the create.
|
|
519
|
+
|
|
520
|
+
### Interactive screens (#272)
|
|
521
|
+
|
|
522
|
+
`ahub upgrade`, `ahub restart` and bare `ahub recovery` on a terminal are a
|
|
523
|
+
front end to the commands above, never a second coordinator.
|
|
524
|
+
|
|
525
|
+
- The screens (`src/cli/upgrade-interactive.ts`) read the plan, the receipt, the
|
|
526
|
+
runner claim and the live inspections, and change an operation only by running
|
|
527
|
+
`recovery resume|abort|dispose` or `reset` as a child process, with the
|
|
528
|
+
operation's own coordinator (`recoveryArgv`, the argv form of
|
|
529
|
+
`recoveryCommand`). Each command decides its own refusal.
|
|
530
|
+
- The operation screen's choices are `nextChoices`, the structured form of the
|
|
531
|
+
`next` list: `nextActions` is its text, and the invariant test that holds
|
|
532
|
+
`next` to what the commands accept also holds the two together.
|
|
533
|
+
- One choice is not in `next`: cancelling while a runner only waits for its
|
|
534
|
+
source (`cancellableWait`: phase `running`, step `prepare:<project>`, no
|
|
535
|
+
effect receipt, no commit request). The screen stops only a runner that
|
|
536
|
+
`signedRunner` names: a claim whose recorded process signature still matches.
|
|
537
|
+
`recoveryRunner` answers an unsigned claim (a coordinator of 0.12.20 or older)
|
|
538
|
+
from its bare pid, which is enough to wait for and never enough to signal, so
|
|
539
|
+
cancel is not offered for it. The receipt is read again at the key press, and
|
|
540
|
+
`recovery abort` then reads what is live once more. A runner killed after it
|
|
541
|
+
recorded an effect leaves an operation that abort refuses and resume
|
|
542
|
+
continues, as after any runner crash.
|
|
543
|
+
- The plan screen can end attached agents before the operation exists (user
|
|
544
|
+
request, 2026-10-10: choose to end headless or TUI agents during an upgrade).
|
|
545
|
+
A TUI agent is ended by closing the terminal the plan bound to its session; a
|
|
546
|
+
headless agent by the console-role request `peer_stop` to its hub (#278),
|
|
547
|
+
which an older hub answers as unknown. The plan is not acted on as it was
|
|
548
|
+
drawn: `endPlannedPeer` reads again the lock, the hub instance and the peer's
|
|
549
|
+
attachment, and for a TUI agent the attached session and the terminal's
|
|
550
|
+
handle and incarnation (`inspectTerminals` for that one session); any
|
|
551
|
+
difference ends nothing. `closeTerminal` itself compares only the terminal's
|
|
552
|
+
handle, incarnation, worktree and root. An ended agent is offline in the next
|
|
553
|
+
plan, so it is neither waited for nor restored. Nothing is ended once an
|
|
554
|
+
operation holds the lock: the coordinator's roster check reads a planned peer
|
|
555
|
+
that left as a changed source. With several projects in the plan an agent is
|
|
556
|
+
named `<project id>/<peer id>`, and a bare name attached in more than one
|
|
557
|
+
selects none. A Ctrl+C between two agents leaves the rest alone.
|
|
558
|
+
- A Ctrl+C while `apply` checks the plan again creates no operation.
|
|
559
|
+
- Ctrl+C leaves the screens and stops nothing; the terminal is not read again
|
|
560
|
+
after it, because a parent process that died on the same signal (`bun x`, a
|
|
561
|
+
hand-over) may have returned the terminal to the shell. While a progress view
|
|
562
|
+
runs, a line (Enter) opens the operation screen instead. A prompt answers an
|
|
563
|
+
interrupt or the end of input with no value, never with text.
|
|
564
|
+
- A follow that starts right after a command scheduled a runner (resume, a
|
|
565
|
+
fresh session) waits for that runner's first write, up to 5 seconds: until
|
|
566
|
+
then the receipt still shows the blocked state it was scheduled from. It
|
|
567
|
+
starts from the receipt as the command left it (a fresh-session choice writes
|
|
568
|
+
the receipt itself before it schedules the runner).
|
|
569
|
+
- The daemon's `recovery inspect` lists every condition `recoveryReady()` waits
|
|
570
|
+
for in `blockers` (task commands, completion checks, Pi calls and a release
|
|
571
|
+
in flight, an unsettled Pi, besides busy peers, approvals, startup and a
|
|
572
|
+
budget transition). The roster identity comparison is not listed. A hub that
|
|
573
|
+
is not ready and names no cause is shown as such.
|
|
574
|
+
- The progress view prints the receipt's steps and, during `prepare`, the
|
|
575
|
+
source's own readiness blockers. `inspectRecovery` copies them into
|
|
576
|
+
`Inspection.recovery.waiting` for display only; `planFingerprint` leaves
|
|
577
|
+
`recovery` out, so they never change a plan's identity.
|
|
578
|
+
- Without `--to` the target is `npm view @staix/agent-hub version`. `--yes`
|
|
579
|
+
keeps requiring `--to`, so an unattended apply names its release. `--to` is
|
|
580
|
+
checked as an exact version before it reaches the registry or a package spec.
|
|
581
|
+
- The dry-run plan gains `projects[].source.recovery.waiting` when the hub
|
|
582
|
+
reports something in progress; nothing else in the non-interactive output
|
|
583
|
+
changes.
|
|
584
|
+
- Out of scope: a hub-side interrupt of a running turn (the running source may
|
|
585
|
+
be older and would not know the request), and answering approvals from the
|
|
586
|
+
screens.
|