@staix/agent-hub 0.12.20 → 0.12.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +17 -9
- package/docs/agent-notes/adapters.md +13 -3
- package/docs/agent-notes/benchmarks.md +1 -1
- package/docs/agent-notes/bus.md +7 -3
- package/docs/agent-notes/daemon.md +12 -2
- package/docs/agent-notes/local-worker.md +5 -1
- package/docs/agent-notes/tasks.md +4 -2
- package/docs/agent-notes/tests.md +5 -2
- package/docs/bench.md +143 -0
- package/docs/events.md +15 -5
- package/docs/operations.md +510 -54
- package/docs/quickstart.md +16 -11
- package/docs/research.md +92 -0
- package/docs/security.md +49 -11
- package/docs/smoke.md +138 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +227 -20
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +152 -18
- package/docs/verified.json +34 -17
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +3 -2
- package/src/adapters/acp.ts +77 -10
- package/src/adapters/codex-appserver.ts +72 -2
- package/src/adapters/local-worker.ts +46 -11
- package/src/adapters/pi.ts +215 -22
- package/src/cli/bench.ts +287 -0
- package/src/cli/console-state.ts +226 -28
- package/src/cli/console.ts +16 -4
- package/src/cli/facts-hook.ts +50 -8
- package/src/cli/help.ts +16 -5
- package/src/cli/identity.ts +10 -4
- package/src/cli/launch.ts +272 -18
- package/src/cli/main.ts +290 -30
- package/src/cli/preview.ts +13 -3
- package/src/cli/recovery-package.ts +17 -0
- package/src/cli/status-lines.ts +2 -1
- package/src/cli/terminal-recovery.ts +83 -31
- package/src/cli/upgrade-interactive.ts +297 -0
- package/src/cli/upgrade-runtime.ts +123 -20
- package/src/cli/upgrade.ts +108 -19
- package/src/hub/bench.ts +211 -0
- package/src/hub/bus.ts +24 -7
- package/src/hub/conductor.ts +22 -21
- package/src/hub/config-trust.ts +1 -0
- package/src/hub/control-client.ts +4 -1
- package/src/hub/daemon.ts +477 -86
- package/src/hub/delivery-journal.ts +6 -6
- package/src/hub/events.ts +4 -2
- package/src/hub/lifecycle.ts +8 -13
- package/src/hub/manager.ts +16 -14
- package/src/hub/peers.ts +2 -2
- package/src/hub/permission-mode.ts +44 -0
- package/src/hub/recovery-store.ts +59 -9
- package/src/hub/registry.ts +36 -22
- package/src/hub/research.ts +255 -0
- package/src/hub/tasks.ts +99 -55
- package/src/hub/tool-refusal.ts +7 -0
- package/src/hub/ui.ts +4 -1
- package/src/local/deny.ts +48 -1
- package/src/local/sandbox.ts +6 -3
- package/src/local/tools.ts +34 -11
- package/src/pi/extension.ts +43 -20
- package/src/pi/process-signature.ts +23 -0
- package/src/pi/tool-receipts.ts +24 -2
- package/src/ui/index.html +98 -17
- package/src/ui/task-progress.ts +33 -0
|
@@ -89,7 +89,8 @@ Facts below are tagged **verified** (measured on 2026-09-19 on the owner's Mac)
|
|
|
89
89
|
7. Quota relay hands open tasks to the `local` peer first, then waits for the window
|
|
90
90
|
reset for judgment-heavy classes. Task-level escalation on verification failure.
|
|
91
91
|
8. Safety defaults: cross-peer text is framed as untrusted; permission prompts stay on
|
|
92
|
-
unless `--unattended
|
|
92
|
+
unless `--unattended` or an operator-selected permission mode (issues #240,
|
|
93
|
+
#242); ports bind loopback only; Kimi ACP permission requests are
|
|
93
94
|
relayed to the hub console. Loopback is not enough on its own (any web page can open a
|
|
94
95
|
WebSocket to 127.0.0.1), so the control WS requires a per-run token and both hub
|
|
95
96
|
servers refuse requests that carry an `Origin` header (amended in M1).
|
|
@@ -97,8 +98,9 @@ Facts below are tagged **verified** (measured on 2026-09-19 on the owner's Mac)
|
|
|
97
98
|
10. Shared memory reuses claude-mem as the single store; the hub never runs its own
|
|
98
99
|
memory database. Native capture stays where it exists (Claude hooks, Codex plugin,
|
|
99
100
|
Kimi transcript bridge); the hub captures only the local worker and adds
|
|
100
|
-
cross-platform recall, task-scoped briefs and explicit shared notes.
|
|
101
|
-
|
|
101
|
+
cross-platform recall, task-scoped briefs and explicit shared notes. Hub tool calls
|
|
102
|
+
are not captured; Tasks alone decides what screened tool text reaches memory.
|
|
103
|
+
The bus still passes messages, never transcripts.
|
|
102
104
|
|
|
103
105
|
## Design
|
|
104
106
|
|
|
@@ -279,9 +281,10 @@ peer. Peer ids claimed over the control WS must not be `user` or a hub-managed a
|
|
|
279
281
|
injection is dropped for the same reason as `dynamicTools`.
|
|
280
282
|
- PII (amended in M4): a task matching `signals.pii_patterns` is owned by `local` or by
|
|
281
283
|
nobody; its envelopes are `private` (console tail and `hub.log` print a stub), lists show
|
|
282
|
-
`[pii]` to everyone but `local
|
|
283
|
-
reviewer is the console user; `local` answers such a turn
|
|
284
|
-
of its history, refuses it when the only gateway
|
|
284
|
+
`[pii]` to everyone but `local` inside a PII turn; `ahub task show` is the one place
|
|
285
|
+
the console reads it. The reviewer is the console user; `local` answers such a turn
|
|
286
|
+
to the console only, keeps it out of its history, refuses it when the only gateway
|
|
287
|
+
is off campus (Cloudflare Access), and
|
|
285
288
|
nothing reaches claude-mem (no brief, no note, no capture), because claude-mem's observer is
|
|
286
289
|
a cloud model (verified: `/api/health` reports `ai.provider: claude`).
|
|
287
290
|
|
|
@@ -486,7 +489,8 @@ inside a peer.
|
|
|
486
489
|
### Safety
|
|
487
490
|
|
|
488
491
|
- Untrusted framing on all cross-peer text; standing instruction once per session.
|
|
489
|
-
- `ahub claude` and `ahub codex` keep
|
|
492
|
+
- `ahub claude` and `ahub codex` keep native permission defaults unless the person
|
|
493
|
+
selects a permission mode (issues #240, #242). `--unattended` opts into
|
|
490
494
|
`--dangerously-skip-permissions` (Claude) and `--dangerously-bypass-approvals-and-sandbox`
|
|
491
495
|
(Codex 0.154.0 documents this flag, not `--yolo`) and prints a warning.
|
|
492
496
|
- Kimi and local tools: cwd-scoped, secrets denylist (a secrets directory, `.env*`, keys).
|
|
@@ -496,7 +500,7 @@ inside a peer.
|
|
|
496
500
|
### Configuration and state
|
|
497
501
|
|
|
498
502
|
- `.agenthub/config.json` (roles, ports, filter tiers, watchdog), `.agenthub/routing.toml`.
|
|
499
|
-
- `.agenthub/state/` gitignored: `
|
|
503
|
+
- `.agenthub/state/` gitignored: `status.json` (with the daemon's pid and process signature, #226; daemons up to 0.12.20 also wrote `hub.pid`), `control-token` (0600, per run),
|
|
500
504
|
`hub.db` (tasks, messages, budget; from M4), `hub.log`, `switchyard.toml`, `checkpoint.md`.
|
|
501
505
|
- Env: `AGENTHUB_STATE_DIR`, `AGENTHUB_OMNIROUTE_URL`, `OMNIROUTE_API_KEY`,
|
|
502
506
|
`AGENTHUB_SWITCHYARD_BIN`, `AGENTHUB_UNATTENDED`.
|
|
@@ -763,7 +767,8 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
763
767
|
`hub_send` or a task tool, are answered with their `allow_once` option by the
|
|
764
768
|
hub, without a console prompt, and logged by tool name only. This matches the
|
|
765
769
|
`approval_mode = "approve"` Codex already gets for the same tools. Every other
|
|
766
|
-
request keeps the console path
|
|
770
|
+
request keeps the console path unless the person opts into the permission modes
|
|
771
|
+
below (issues #240, #242); `allow_always` is never chosen automatically.
|
|
767
772
|
- For the other tools, the console payload falls back to the streamed argument
|
|
768
773
|
text once it is a complete JSON object. A partial stream stays unresolved and
|
|
769
774
|
keeps `allow_always` withheld.
|
|
@@ -776,6 +781,84 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
776
781
|
configured server of the same name, so only the hub's `agent-hub` server can
|
|
777
782
|
produce the approved names.
|
|
778
783
|
|
|
784
|
+
## Amendment: operator permission modes (issues #240, #242)
|
|
785
|
+
|
|
786
|
+
- Modes: ask leaves native decisions unchanged; ask-when-needed uses native
|
|
787
|
+
Kimi/Codex policies or scoped Claude/Pi/local grants; never-ask suppresses
|
|
788
|
+
prompts within each peer's existing execution boundaries.
|
|
789
|
+
- Runtime CLI permission changes are refused from ordinary agent shells.
|
|
790
|
+
Never-ask needs --yes; console mode selection needs a separate y. These
|
|
791
|
+
policy/role checks do not contain a hostile same-user shell: Kimi can clear
|
|
792
|
+
markers, use --yes or read the control token and impersonate the console.
|
|
793
|
+
The security and operations guides name that boundary explicitly.
|
|
794
|
+
- permission_modes defaults merge per peer across config files. Automatic
|
|
795
|
+
effects stop at ask-when-needed. Never-ask from an applied untracked config remains ask until
|
|
796
|
+
a person's source-labelled console y at hub start; no boot --yes applies it.
|
|
797
|
+
Git-tracked mode defaults are ignored and never offered for confirmation.
|
|
798
|
+
Every non-ask start logs mode and source (config file or runtime choice). Successful
|
|
799
|
+
runtime changes clear pending config confirmations; startup choices are reconciled
|
|
800
|
+
under the peer fence before a new adapter is exposed to bus delivery. Runtime changes and
|
|
801
|
+
confirmations remain daemon-local and expire on stop.
|
|
802
|
+
- Newly added native peers replay their non-offline state to drain queued work,
|
|
803
|
+
publish state telemetry and persist sessions. Offline/absent ask clears only
|
|
804
|
+
the hub choice. Unattended metadata is saved after successful startup, and
|
|
805
|
+
concurrent joined starts with different flags are refused. Refused default
|
|
806
|
+
confirmation remains handled until state changes; Esc defers without declining.
|
|
807
|
+
- Kimi mapping requires the actual Kimi Code CLI identity: yolo for
|
|
808
|
+
ask-when-needed, auto for never-ask, default for ask. Another ACP agent starts fresh or resumed as unmanaged: its reported mode is
|
|
809
|
+
untouched, and runtime changes are refused until its table exists. Set-mode replies precede prompts;
|
|
810
|
+
missing ids/refused startup changes fail. A timeout is unknown/offline and
|
|
811
|
+
the owned process group stops, so a late reply cannot restore a live peer.
|
|
812
|
+
Loaded non-default Kimi modes are reset for project ask.
|
|
813
|
+
- Codex overlays on-request/never on every outgoing turn/start without changing
|
|
814
|
+
sandbox fields. Ask restores a known native policy once after an accepted
|
|
815
|
+
turn; rejected restoration remains pending. Restoration debt and captured
|
|
816
|
+
policies are per thread and survive proxy detach and thread/start or thread/resume.
|
|
817
|
+
A known restoring turn retains its baseline even if the TUI echoes the resumed
|
|
818
|
+
response's sticky override, in any hub permission mode. After restoration is
|
|
819
|
+
admitted, a later repeated TUI policy is indistinguishable from an explicit
|
|
820
|
+
native choice and passes through; only the restoring turn is protected.
|
|
821
|
+
Unknown native baseline refuses restoration while attached, with instructions
|
|
822
|
+
to close the TUI and run `ahub permission codex ask`; detached ask succeeds and
|
|
823
|
+
returns a CLI-visible note about the old thread's unchanged native policy. Actual native unattended flags, not broker-wide flags, govern
|
|
824
|
+
the refusal to change a bypassed session.
|
|
825
|
+
- Claude ask-when-needed allows only Read/Edit/Write/MultiEdit/NotebookEdit/LS
|
|
826
|
+
and bounded Glob/Grep targets resolved with realPath beneath project root,
|
|
827
|
+
outside .agenthub/.git/.claude/.codex/.qwen/.kimi/.pi/.mcp.json. Missing/nonresolving or escaping targets,
|
|
828
|
+
protected case aliases, symlinks and uncertain wildcard/filter targets yield
|
|
829
|
+
no decision. Never-ask answers allow; ask and transport errors decide nothing.
|
|
830
|
+
Merged/generated settings are 0600 state files passed by path, never inline
|
|
831
|
+
in argv. Preview writes no settings file. The owning launcher removes its settings file
|
|
832
|
+
after native exit; earlier active or unknown launchers retain theirs. Verified
|
|
833
|
+
dead wrapper and native identities permit crash fallback cleanup. Direct binary
|
|
834
|
+
launches publish the native child PID/signature while running, bound to the exact
|
|
835
|
+
launch record; crash cleanup additionally requires its PID absent from a verified
|
|
836
|
+
process table. Script/interpreter native ownership remains unknown. Without
|
|
837
|
+
separate native identity proof, prior files remain for manual inspection. Every
|
|
838
|
+
launch atomically publishes its managed hook/session record without depending
|
|
839
|
+
on the native-proof lock. Exact-binding native proof is stored separately, so
|
|
840
|
+
an old callback cannot replace a later launch record. The populated signed
|
|
841
|
+
native-proof lock waits up to 250 ms for a live owner; an unknown/busy lock
|
|
842
|
+
disables only native identity/crash cleanup with exact-path guidance. Managed
|
|
843
|
+
hooks keep working for the current launch.
|
|
844
|
+
- Permission-only Claude PreToolUse never drives busy state. Only the former
|
|
845
|
+
observation purposes retain native turn bookkeeping. Hook identity/purpose
|
|
846
|
+
are launch/session bound; unverified hook status reads unverified.
|
|
847
|
+
- Pi/local read never asks. Ask-when-needed grants write/edit inside the project, outside the shared native
|
|
848
|
+
configuration segments (.claude/.codex/.qwen/.kimi/.pi/.mcp.json, compared folded and by the path within the
|
|
849
|
+
project) and never through a hard link; bash and mutating
|
|
850
|
+
git retain console approval. Never-ask grants once, retaining sandbox and
|
|
851
|
+
path guards; no mode gives allow-always. ACP tools have no hub sandbox and
|
|
852
|
+
the never-ask confirmation names that difference.
|
|
853
|
+
- Console-role permission and permission_default requests are additive;
|
|
854
|
+
ordinary tools/conductor/messages cannot change or confirm modes. Status
|
|
855
|
+
includes non-ask/unverified/unknown mode state and pending defaults; facts
|
|
856
|
+
replies include permission and authenticated projectRoot. Unknown requests
|
|
857
|
+
are answered, so PROTOCOL remains unchanged. permission_mode events retain
|
|
858
|
+
schema 1 and include only peer/from/to, never tool arguments.
|
|
859
|
+
- Live benign Kimi/Claude results and unverified Codex/Pi limits are recorded
|
|
860
|
+
in docs/smoke.md. No extra account prompts accompany these review corrections.
|
|
861
|
+
|
|
779
862
|
## Amendment: approval notifications and timeout (issue #5)
|
|
780
863
|
|
|
781
864
|
- A waiting permission request raises one macOS notification (`osascript`) with
|
|
@@ -994,7 +1077,11 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
994
1077
|
awaited an approval. An approval is saved before its dependents are assigned,
|
|
995
1078
|
so the daemon's 60 s release timer sweeps for ownerless tasks whose last event
|
|
996
1079
|
is `blocked` or `ready` and that wait for nothing: each is offered once per hub
|
|
997
|
-
run, once an attached peer can take it, outside recovery operations.
|
|
1080
|
+
run, once an attached peer can take it, outside recovery operations. An offer
|
|
1081
|
+
is used up only by an assignment, after an approval and on the timer alike;
|
|
1082
|
+
failed dependent reads, ready writes or assignments do not escape a saved
|
|
1083
|
+
approval. Their notices are best effort because notification can read the
|
|
1084
|
+
same unavailable board. An ownerless task stays eligible once a peer can take it.
|
|
998
1085
|
- A recovery commit also waits for task operations in flight and for completion
|
|
999
1086
|
checks queued or running: both can write the board after its integrity digest
|
|
1000
1087
|
(a check the commit's stop kills records `check interrupted`).
|
|
@@ -1360,14 +1447,59 @@ approval. Deny is direct. Pending requests include expiry and are withdrawn by
|
|
|
1360
1447
|
Panels expose Peers, Approvals, Tasks, Queue and Events with bounded polling and
|
|
1361
1448
|
Unicode cell widths, fall back below 80x24, and restore terminal state on exit.
|
|
1362
1449
|
|
|
1450
|
+
Whole-board progress (#246) uses one pure `taskProgress` function shared by the
|
|
1451
|
+
console and hashed dashboard script. It reads only public id/state/deps fields;
|
|
1452
|
+
PII stubs count like other tasks. Waiting partitions proposed tasks whose public
|
|
1453
|
+
dependencies are not approved, including unknown dependencies. Approved/all gives
|
|
1454
|
+
the done fraction (zero on an empty board). Four stages are proposed, in progress,
|
|
1455
|
+
in review and approved; changes requested returns to stage two and is marked.
|
|
1456
|
+
The console Tasks panel has a 20-cell ASCII summary bar and fixed four-cell row
|
|
1457
|
+
meters, preserving width and color-off geometry. Cells and percentage round down;
|
|
1458
|
+
count labels use importance order changes requested, review, waiting, in progress,
|
|
1459
|
+
proposed, then unknown when nonzero, and drop only from the end. The stream count is in the fixed footer,
|
|
1460
|
+
from existing all-state status counts; it makes no board fetch and retains a
|
|
1461
|
+
row-one scroll region for terminal scrollback. Loading/failed reads never imply
|
|
1462
|
+
confirmed empty progress. Dashboard filters affect rows only, not totals. Its
|
|
1463
|
+
full-width stacked SVG and stages have text equivalents; the existing signature
|
|
1464
|
+
guard prevents unchanged snapshots from mutating the progress live region.
|
|
1465
|
+
System/Light/Dark choice is applied as `data-theme` before first paint, persists
|
|
1466
|
+
in a one-year host-scoped `SameSite=Strict` cookie through guarded access and
|
|
1467
|
+
follows OS CSS preference on System. Cookie port independence shares the choice
|
|
1468
|
+
between hub starts and projects. The shared model is injected before the page's
|
|
1469
|
+
own inline script/style CSP hashes, with no new control message or CLI JSON change.
|
|
1470
|
+
UI public rows expose optional public-view deps for waiting. This is additive
|
|
1471
|
+
metadata enrichment of the existing ui_snapshot payload, not a changed control
|
|
1472
|
+
message envelope/operation, so it does not bump PROTOCOL. Unknown task states
|
|
1473
|
+
count in an explicit unknown bucket with stage zero; no NaN or prototype-key
|
|
1474
|
+
updates are permitted. Approval fraction is derived from counts/total at display
|
|
1475
|
+
time, avoiding a redundant floating-ratio field.
|
|
1476
|
+
|
|
1363
1477
|
Console semantic colors (issue #201) use spans with a fixed terminal-native
|
|
1364
1478
|
palette: cyan information, bold cyan active/selected labels, green availability
|
|
1365
1479
|
and success, yellow attention/waiting, red failure/intervention, and restrained
|
|
1366
1480
|
bright-black metadata. Offline peers, ordinary titles, action details and body
|
|
1367
1481
|
lines use the default foreground. Existing labels, selection markers and
|
|
1368
|
-
confirmation prompts remain sufficient without color.
|
|
1369
|
-
|
|
1370
|
-
|
|
1482
|
+
confirmation prompts remain sufficient without color. Console stream headers
|
|
1483
|
+
color meaningful tokens only (#239; dated issue Decisions): Claude bright blue
|
|
1484
|
+
(94), Codex bright cyan (96), all other peers default; `task` magenta (35, never
|
|
1485
|
+
bold), numbers bold default foreground (1), task references magenta underlined
|
|
1486
|
+
(4;35), and other issue references underlined default foreground (4). No token
|
|
1487
|
+
SGR equals a state or peer SGR. Green/yellow/red remain states/approvals/failures,
|
|
1488
|
+
cyan remains navigation. Case-insensitive `task #N`, `Task #N`, `[task #N]` and
|
|
1489
|
+
`[review #N]` identify task references; unbracketed prose `review #N` remains an
|
|
1490
|
+
issue reference. Ordinary words keep default foreground;
|
|
1491
|
+
there is no hashing or user theme.
|
|
1492
|
+
Only hub-written peer slots, state words and structural markers receive their
|
|
1493
|
+
semantic tones; title words and markers cannot choose them. Numeric tokens have
|
|
1494
|
+
whole-token boundaries, so UUIDs and ISO identifiers stay plain. Legacy notices
|
|
1495
|
+
lack typed reference ranges: the task/review prefix heuristic is bounded by a
|
|
1496
|
+
`ponytail:` comment; other `#N` tokens use the issue tone until structured notice
|
|
1497
|
+
spans classify assign/which references. Header text crosses `terminalText` before tokenization. Wrapped headers normalize
|
|
1498
|
+
tabs to spaces first; plain headers retain their original tabs in both color modes. Wrapped spans retain the source classification. After a projection
|
|
1499
|
+
miss on the first physical header, only structural tones remain. A missed
|
|
1500
|
+
continuation and all later continuations are fully plain. Projection advances
|
|
1501
|
+
a span index through the source once, keeping long permission titles bounded. Bodies and every
|
|
1502
|
+
command-output line stay plain. Denial requests and observed expiry/cancellation
|
|
1371
1503
|
are red; an answered remote closure lacks option-kind metadata and is not
|
|
1372
1504
|
guessed to be a denial.
|
|
1373
1505
|
|
|
@@ -1654,7 +1786,10 @@ push `accepted` and show none of them.
|
|
|
1654
1786
|
to the newest end of the cache) for `reply_to` like a push, and a read clears
|
|
1655
1787
|
the peer's failure streak as a completed push does. If that journal write fails, only this peer's queue
|
|
1656
1788
|
and preface are put back (every pause stays) and the bus stops with
|
|
1657
|
-
`delivery journal unavailable`. A
|
|
1789
|
+
`delivery journal unavailable`. A refused or failed `resolveDelivery` likewise
|
|
1790
|
+
puts back only that peer's queue and preface and keeps every pause. Queue
|
|
1791
|
+
contents change in place, preserving arrays held by a suspended drain; only
|
|
1792
|
+
the constructor and recovery restore reload a bus snapshot. A pull is not a native turn and is not
|
|
1658
1793
|
counted as supervision. The row is completed at hand-out, before anything
|
|
1659
1794
|
shows the model saw it: a reply lost on the way, a tool call cancelled with
|
|
1660
1795
|
Esc or a plugin that dies loses the batch (a `ponytail:` ceiling in
|
|
@@ -1789,9 +1924,9 @@ acts on the stopped state directory.
|
|
|
1789
1924
|
Checked, then renamed by path: a symlink swapped in between still wins
|
|
1790
1925
|
(marked `ponytail:`; `renameat` on a held descriptor would close it).
|
|
1791
1926
|
- Between stop and act: after `stopProject` the CLI takes the project's
|
|
1792
|
-
registry claim (`Registry.claim` with its own pid), the claim a daemon must
|
|
1927
|
+
registry claim (`Registry.claim` with its own pid and process signature), the claim a daemon must
|
|
1793
1928
|
hold to run, and checks that no manifest names a daemon whose pid is alive
|
|
1794
|
-
or uncertain (`
|
|
1929
|
+
or uncertain (`processLiveness`, the rule `inspectProject` uses, #226); either failing
|
|
1795
1930
|
means a hub started in between and nothing is reset. A reset's claim carries
|
|
1796
1931
|
the instance prefix `reset-`, so losing the claim to a concurrent reset says
|
|
1797
1932
|
so. A crashed daemon's manifest (`status.json`, `control-token`, `hub.pid`
|
|
@@ -1800,7 +1935,7 @@ acts on the stopped state directory.
|
|
|
1800
1935
|
removes it and `--all` archives it. With the claim held, the CLI checks the
|
|
1801
1936
|
machine's recovery lock again, so an upgrade or recovery that took it in
|
|
1802
1937
|
between does not run beside the reset. The claim is released when the reset
|
|
1803
|
-
ends; a reset that dies leaves a dead pid, which the next claim takes over.
|
|
1938
|
+
ends; a reset that dies leaves a dead or reused pid with its signature, which the next claim takes over.
|
|
1804
1939
|
- Failures: the plan is read only for the dry run, so `--all --yes` archives a
|
|
1805
1940
|
state directory it cannot read as it is (its dry run says what it could not
|
|
1806
1941
|
read). A failure after the stop says how far it got. A runtime reset that
|
|
@@ -1849,9 +1984,8 @@ acts on the stopped state directory.
|
|
|
1849
1984
|
text right after the pattern check and before triage. "Held as private" is met
|
|
1850
1985
|
by holding the task back entirely: until the verdict it is not on the board, so
|
|
1851
1986
|
no envelope, notice, triage call, brief or note of the hub's can read it. The
|
|
1852
|
-
local worker
|
|
1853
|
-
|
|
1854
|
-
gap for every hub tool, not closed here). Writing it as
|
|
1987
|
+
local worker never captures hub tool calls (#230), so a proposal's arguments
|
|
1988
|
+
cannot bypass the verdict through capture. Writing it as
|
|
1855
1989
|
a PII task first was rejected: a PII task opening lifts every silent turn-free
|
|
1856
1990
|
cohort for good and switches facts off (#107, #108), so every proposal would do
|
|
1857
1991
|
that. The proposal's tool call waits for the verdict, as it already waits for
|
|
@@ -1930,3 +2064,76 @@ acts on the stopped state directory.
|
|
|
1930
2064
|
before any measurement: the model itself labels at least 90% of the positives
|
|
1931
2065
|
`pii` (an unknown verdict counts as a miss). The report goes to
|
|
1932
2066
|
`docs/verification/`; the script exits 1 under the bound.
|
|
2067
|
+
|
|
2068
|
+
## Amendment: research records (issue #247)
|
|
2069
|
+
|
|
2070
|
+
- Opt-in per project (`research.enabled`, off by default). When a task is approved, the daemon builds its record from
|
|
2071
|
+
the project's `events.jsonl` with the same function `ahub research backfill` uses (`taskRecords` in
|
|
2072
|
+
`src/hub/research.ts`), so live and backfilled records agree, and appends it to the machine-wide store
|
|
2073
|
+
`~/.agenthub/research/<sha256(project id)[0:16]>.jsonl`. Records hold ids, states, counts, tokens and times only,
|
|
2074
|
+
because the events do; schema and fields: [research](../research.md).
|
|
2075
|
+
- The board has no terminal state but `approved`, and it is final, so each approved task gets one record; a person's
|
|
2076
|
+
later verdict (`ahub task label`, including `abandoned`) is a separate label record bound to the task's proposal
|
|
2077
|
+
time. A task is keyed by project, id and proposal time, because `ahub reset --all` restarts the ids and the store
|
|
2078
|
+
keeps the older records. Measures (success, first pass, rework, check failures over checks run, tokens and wall time
|
|
2079
|
+
per task) are derived when reported, never stored.
|
|
2080
|
+
- The record counts everything attributed to the task, the turn that approved it included: that turn ends after the
|
|
2081
|
+
approval and its usage arrives later, so the writer waits until no open turn is attributed to the task (polled every
|
|
2082
|
+
10 s, at most 30 minutes; a stopping hub writes once its peers have stopped). The first record written for a
|
|
2083
|
+
task is final; a label binds to the current board's task, else to the store's only record with that id, or with
|
|
2084
|
+
`--created` to a named one from before a reset. Task events carry the board's move `reason` (a closed
|
|
2085
|
+
list) so reassignments are counted by reason.
|
|
2086
|
+
- Recording never blocks or fails the task flow: the writer runs after the board event, and a failure is one
|
|
2087
|
+
hub.log line per run. ponytail: it reads the whole events file for each record; an incremental per-task index
|
|
2088
|
+
if that file grows large.
|
|
2089
|
+
- Agents may read the measures and export; backfill and labels are a person's (identity gate).
|
|
2090
|
+
|
|
2091
|
+
## Amendment: proven Pi tool refusals and idle exit recovery (issues #254, #255)
|
|
2092
|
+
|
|
2093
|
+
- 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.
|
|
2094
|
+
- 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.
|
|
2095
|
+
- 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.
|
|
2096
|
+
## Amendment: benchmarks (issue #251)
|
|
2097
|
+
|
|
2098
|
+
- `ahub bench` runs a person's suite file (tasks with a starting commit, an optional setup and a verify command)
|
|
2099
|
+
against the peers attached to a hub, in a project opted in with `bench.enabled`, because every attempt resets the work
|
|
2100
|
+
tree (`git checkout --force --detach <commit>` with HEAD checked against the pin, `git clean -ffdx -e /.agenthub`, then a
|
|
2101
|
+
clean-tree check). Before
|
|
2102
|
+
anything changes the runner requires the project to be the root of its repository, nothing tracked under `.agenthub/`
|
|
2103
|
+
(in any letter case) in the tree or in any suite ref, every ref to be a commit, which it pins for the run, and a
|
|
2104
|
+
clean tree. The runner is a console client: it proposes each
|
|
2105
|
+
task as the console does and polls the board, so the task flow is unchanged and no new control request exists.
|
|
2106
|
+
- `setup` and `verify` run in their own process group, bounded; the group is stopped at the bound, on an interrupt and
|
|
2107
|
+
when the command exits. An approved attempt's measures come from #247's `taskRecords` for the task proposed last with
|
|
2108
|
+
that id, read once the turns started since its proposal have ended (waiting only while their peers are busy or
|
|
2109
|
+
paused, at most 10 minutes, outside the attempt's duration); a timeout, an error, an interrupted wait, a wait that
|
|
2110
|
+
runs out with the turn still open, or a hub status that cannot be read while it is open leaves none, never partial
|
|
2111
|
+
ones. The fingerprint lists only the peers attached at the start.
|
|
2112
|
+
- Results go to `~/.agenthub/bench/<run>.jsonl` (ids, outcomes, exit codes, counts, times; never suite text or command
|
|
2113
|
+
output). A run's state comes from its end record or its runner's liveness (`processLiveness`). Comparisons are
|
|
2114
|
+
derived when asked: an arm is every run of it that is over (a stopped run included), runs of different suites or
|
|
2115
|
+
suite versions are not mixed unless asked, and each difference has a seeded 95% bootstrap interval, inconclusive
|
|
2116
|
+
below five attempts counted for that measure per arm (errors are not counted for pass rate or wall time).
|
|
2117
|
+
- A task that times out stops the run, because an agent may still work in the tree the next attempt would reset.
|
|
2118
|
+
ponytail: a board state to withdraw an open task would let the run continue. The fingerprint holds what the hub's
|
|
2119
|
+
status reports (a requested model for Pi, permission modes once #242 lands); ponytail: read each peer's launch
|
|
2120
|
+
settings for the rest.
|
|
2121
|
+
- The dashboard snapshot carries run summaries and per-arm measures only. `bench run` is a person's (identity gate,
|
|
2122
|
+
wherever `--json` stands); reading results is not. Schema and commands: [bench](../bench.md).
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
## Amendment: Pi/local approval expiry and cancellation (issue #253)
|
|
2126
|
+
|
|
2127
|
+
- 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.
|
|
2128
|
+
- 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.
|
|
2129
|
+
- 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.
|
|
2130
|
+
- 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.
|
|
2131
|
+
|
|
2132
|
+
## Amendment: requested headless peer stop (issue #278)
|
|
2133
|
+
|
|
2134
|
+
- 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.
|
|
2135
|
+
- 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.
|
|
2136
|
+
- 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.
|
|
2137
|
+
- `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.
|
|
2138
|
+
|
|
2139
|
+
- 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.
|
|
@@ -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.
|
|
@@ -50,6 +58,9 @@ parents/hops, retries, dedupe state, prefaces, manual pauses and peer descriptor
|
|
|
50
58
|
The existing task and budget databases remain authoritative. A mismatched or
|
|
51
59
|
corrupt snapshot blocks startup. Snapshot restoration precedes peer attachment;
|
|
52
60
|
release is idempotent and retires replayable snapshot state before delivery.
|
|
61
|
+
Journal-backed restoration retains missing empty queue entries from the committed
|
|
62
|
+
bus snapshot, including peers that remain offline. The journal stays authoritative
|
|
63
|
+
for queued work and receipts; a non-empty snapshot queue is never replayed over it.
|
|
53
64
|
Release atomically moves that snapshot to a private per-operation archive before
|
|
54
65
|
lifting the hold. Archives retain evidence for an uncertain release and are never
|
|
55
66
|
automatically replayed. Preparation freezes the peer roster; a new unplanned peer
|
|
@@ -322,7 +333,12 @@ nor accepted when the target cannot read recovery waivers; `status` then says
|
|
|
322
333
|
why in its own `freshSession` field, since `next` holds commands only. A runner
|
|
323
334
|
record that cannot be read shows as `unknown` in `status`, never as no runner;
|
|
324
335
|
while it reads so, resume, abort and dispose are all refused (the runner claim
|
|
325
|
-
cannot tell its owner), so `next` names only `status` again.
|
|
336
|
+
cannot tell its owner), so `next` names only `status` again. The runner row
|
|
337
|
+
carries its runner's process signature (#226): a row whose pid now belongs to
|
|
338
|
+
another process (after a reboot, for example) is no runner and the next claim
|
|
339
|
+
replaces it; one whose process exists but cannot be identified is `unknown`, and
|
|
340
|
+
the claim refuses it with that reason, never a raw `kill` error. A row an older
|
|
341
|
+
runner wrote has no signature and is judged by its pid alone, as before. Otherwise `status` and every error that lists choices build them with one function
|
|
326
342
|
(`nextActions`), in this order: resume (which also launches a failed peer
|
|
327
343
|
again), unless it can never get past what is live (`resumeBlocked`): a
|
|
328
344
|
recorded final refusal (staging refused the fixed target or preserved source), a
|
|
@@ -330,11 +346,11 @@ missing project directory, a hub that speaks another control protocol, a
|
|
|
330
346
|
runtime other than the one the receipt expects (a replaced source; a target not
|
|
331
347
|
fenced to this operation, another instance or another version), a stopped
|
|
332
348
|
pending source, a prepared source of a #215 coordinator that stopped with no
|
|
333
|
-
commit request, a target that stopped after it started
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
349
|
+
commit request, a target that stopped after it started when the operation's
|
|
350
|
+
coordinator predates #225, or any other stopped runtime without this operation's
|
|
351
|
+
unreleased restart snapshot to start from (unavailable, starting or stopping is
|
|
352
|
+
waited for). A target that stopped after it started, with that snapshot, is
|
|
353
|
+
started again by resume (#225, below). Then abort where `abortRefusal` allows it; a fresh session for each failed
|
|
338
354
|
Codex or Claude restoration, only where resume is offered (resume launches the
|
|
339
355
|
new session, and dispose refuses `--fresh-session` on the same predicate); and
|
|
340
356
|
last stop-and-archive where `disposeRefusal` allows it, or else `wait until
|
|
@@ -366,7 +382,15 @@ target instance. It is live when its process signature matches, gone when the
|
|
|
366
382
|
pid no longer exists (ESRCH) or now belongs to another process, and unknown when
|
|
367
383
|
the pid exists but its identity cannot be read. Unknown is never treated as gone. At planning, an
|
|
368
384
|
unknown Claude launcher blocks: whether the attached session is managed cannot
|
|
369
|
-
be told.
|
|
385
|
+
be told. A row that fails validation is never dropped and never read as "no
|
|
386
|
+
launcher" (#228): unless its readable `peer`, `projectRoot` or `instanceId` names
|
|
387
|
+
another launcher, it is unknown for that peer (it blocks binding, creation and the
|
|
388
|
+
plan's managed check), and `ahub <peer>` writes every such row back unchanged,
|
|
389
|
+
replacing only its own valid row. A launcher recorded by 0.12.12 or older hashed
|
|
390
|
+
`ps` in its own TZ and locale (the pin came with #177): when the pinned signature
|
|
391
|
+
differs, the signature computed the unpinned way in the reader's environment is
|
|
392
|
+
compared too, and a match is live; a reader in another environment still reads
|
|
393
|
+
such a launcher as gone.
|
|
370
394
|
Restore reads this evidence in one place: the target's report, which counts only
|
|
371
395
|
when the target runs as the expected instance, then the recorded launcher. A
|
|
372
396
|
target that does not, a launcher that cannot be read, or a launcher record file
|
|
@@ -382,7 +406,18 @@ operation, and the runner keeps the disposition's own error. Abort and `next` de
|
|
|
382
406
|
(`abortRefusal`). `status`, abort and the runner's errors read the same live
|
|
383
407
|
state: every project not yet verified, its source or its target. A manifest
|
|
384
408
|
whose pid no longer exists reads as stopped, whatever protocol it names; probing
|
|
385
|
-
it would read unavailable forever.
|
|
409
|
+
it would read unavailable forever. So does a manifest whose pid now belongs to
|
|
410
|
+
another process (#226): from 0.12.21 `status.json` carries the daemon's process
|
|
411
|
+
signature (`pidSignature`, in the file only; the status reply is unchanged, so no
|
|
412
|
+
protocol bump), the registry claim carries it bound to the claiming instance, and
|
|
413
|
+
every ownership check (manifest, claim, runner, manager owner) goes through one
|
|
414
|
+
helper that reads live, gone or unknown. Recovery reads the claim from the
|
|
415
|
+
registry as it is now, never from the plan's frozen copy. Daemons no longer write
|
|
416
|
+
`hub.pid`; one an older daemon left is an unsigned legacy record. Records without
|
|
417
|
+
a signature give the answers they always did, so an unsigned claim whose pid was
|
|
418
|
+
reused still reads as starting: no command clears it (a hub started by 0.12.20 or
|
|
419
|
+
older that crashed before a reboot), and the operations guide gives the manual
|
|
420
|
+
clear after the process at that pid is checked. A stopped runtime also reports whose
|
|
386
421
|
unreleased restart snapshot its state directory holds.
|
|
387
422
|
An attached session is the target's report of that peer online with a thread
|
|
388
423
|
(Codex) or session (Claude, Pi) id.
|
|
@@ -404,7 +439,9 @@ An attached session is the target's report of that peer online with a thread
|
|
|
404
439
|
| project `prepared`, no `commitSent`, operation of a #215 coordinator | source stopped (crashed before any commit request) | blocks; the phase stays `prepared` (nothing was committed, so there is nothing to start from); not offered | abort if no effects, dispose | `<c> recovery abort <id>` without effects, else stop-and-archive (the lock refuses starting that hub by hand) |
|
|
405
440
|
| project `prepared` | source unavailable, starting, stopping or not inspected | as above when it reads again | none: abort (its hold may still stand) and stop-and-archive are refused until it reads | `<c> recovery status <id>` once it answers |
|
|
406
441
|
| project `prepared`, operation of an older coordinator (no `commitSent` written) | source not running, or not inspected | as its own runner does | dispose once it reads running, stopped or missing; abort neither offered nor accepted (it may have committed) | `<c> recovery status <id>` |
|
|
407
|
-
| project `started` or `peers-restored` | target stopped (a crash or reboot after it started),
|
|
442
|
+
| project `started` or `peers-restored` | target stopped (a crash or reboot after it started), this operation's snapshot kept | records the restart (dead instance, time), retires the dead instance's done `restored:<peer>` receipts, moves the project back to `stopped` and starts the target from the snapshot; once per resume | dispose | `the target stopped after it started; resume restarts it from this operation's snapshot` (`stopped again after restart <n> in this resume` when it stops again) |
|
|
443
|
+
| project `started` or `peers-restored` | target stopped, snapshot gone | blocks; not offered | dispose | `this operation's restart snapshot is gone, so nothing can start it` |
|
|
444
|
+
| `retired:<peer>` (a done restoration of a dead target instance) | Orca lists its terminal, or cannot be read | closed under the close receipt rules (`closedRetired:<peer>` `pending` until Orca no longer lists it); an unreadable inventory changes nothing | dispose | `close Orca terminal <handle> by hand` or `check it in Orca`, `then <c> recovery resume <id>` |
|
|
408
445
|
| project `stopped` | target stopped without this operation's snapshot (never committed) | blocks; not offered | dispose | `the target is stopped and this operation's restart snapshot is gone` |
|
|
409
446
|
| project `stopped`, `started` or `peers-restored` | target running unfenced, as another instance or another version | blocks; not offered | dispose | `daemon is not owned by this recovery operation` (or `daemon instance changed`, `target daemon version mismatch`) |
|
|
410
447
|
| any, at staging | preserved source or staged target changed, or (first staging of these bytes only) target protocol or recovery waivers refused | blocks; recorded as `final`, so resume is never offered again | abort if no effects, dispose | the refusal |
|
|
@@ -437,10 +474,39 @@ and its `next` entry says what that runner cannot do. Stop-and-archive stops a
|
|
|
437
474
|
target that speaks an older control protocol at that protocol (a `kill` fenced
|
|
438
475
|
by its instance), then waits for its manifest and registry claim as the
|
|
439
476
|
lifecycle stop does. The disposition and its audit are written before the first
|
|
440
|
-
act.
|
|
477
|
+
act. When the operation's coordinator predates #215, that same write sets the
|
|
478
|
+
receipt's `schema` to 2 (#227): an older runner never reads `disposition`, and an
|
|
479
|
+
older global `ahub recovery resume` still runs it, but every older runner and
|
|
480
|
+
abort refuses a schema other than 1 before it takes the lock or claims the
|
|
481
|
+
runner, so it exits having written nothing (an older `resume` still prints
|
|
482
|
+
`scheduled`; an older `status` still prints the receipt). A #215 coordinator's
|
|
483
|
+
operation keeps schema 1: its runner refuses a disposition itself, and a 0.12.20
|
|
484
|
+
CLI can still rerun its dispose. This release accepts schema 2 only with a
|
|
485
|
+
disposition, and the next abort or dispose that holds the runner claim moves a
|
|
486
|
+
schema-1 disposition 0.12.20 recorded on an older coordinator's operation to 2. A recovery launch of `ahub codex` records its launcher before the hub's
|
|
441
487
|
`start` round trip; an ordinary launch records it after, so a refused start never
|
|
442
488
|
replaces the record of a Codex already running.
|
|
443
489
|
|
|
490
|
+
#225: a target that stopped after it started is restarted only while its state
|
|
491
|
+
directory holds this operation's unreleased snapshot. The runner records the restart
|
|
492
|
+
in the project's receipt (`restarts`: the dead instance and the time) before acting,
|
|
493
|
+
moves each done `restored:<peer>` binding to `retired:<peer>`, clears the instance
|
|
494
|
+
and moves the project back to `stopped`, whose path starts the target from the
|
|
495
|
+
snapshot; the journal imports the snapshot only at revision 0, so the second start
|
|
496
|
+
continues from what the first wrote. `closed:<peer>`, `commitSent`, `fresh`, waivers
|
|
497
|
+
and the install flags stay. Restore closes each retired terminal (or finds it
|
|
498
|
+
absent) before relaunching that peer, and a peer whose retired session was an
|
|
499
|
+
accepted new one (fresh choice, fresh start, zero-turn Claude) is relaunched by that
|
|
500
|
+
id, which the operation-fenced waiver already covers, unless it has nothing to resume
|
|
501
|
+
(a Codex thread with no rollout, a Claude session with no transcript): that one starts
|
|
502
|
+
new under the waiver. Evidence reads the launch
|
|
503
|
+
records of every instance the operation started for the project, the dead ones
|
|
504
|
+
included, and a peer attached to the new instance without a session id is a session
|
|
505
|
+
of unknown identity, never "nothing attached". One restart per project per resume:
|
|
506
|
+
a target that stops again blocks with the restart count, so a crash loop never
|
|
507
|
+
spins unattended. Operations whose coordinator predates #225 keep refusing (their
|
|
508
|
+
own runner does), with stop-and-archive as the way out.
|
|
509
|
+
|
|
444
510
|
A Codex thread with no rollout is not blocked at the plan only when the hub saw
|
|
445
511
|
Codex start it (`native_thread` with `fresh`, logged by the adapter when it
|
|
446
512
|
adopts a thread) and logged no Codex `turn_start` while it was the adopted
|
|
@@ -450,3 +516,71 @@ lose. Detaching forgets nothing; a thread whose start the log does not show
|
|
|
450
516
|
(resumed, an older hub, a pruned log) is unsure and stays a plan blocker, as
|
|
451
517
|
does one with turns. A planned fresh start still needs the store to show the
|
|
452
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.
|