@staix/agent-hub 0.11.0 → 0.12.1
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 +17 -0
- package/README.md +3 -3
- package/docs/operations.md +66 -21
- package/docs/quickstart.md +1 -1
- package/docs/security.md +1 -1
- package/docs/smoke.md +114 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +16 -2
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +8 -0
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +3 -3
- package/src/adapters/local-worker.ts +2 -2
- package/src/adapters/pi.ts +9 -1
- package/src/cli/main.ts +12 -0
- package/src/cli/status-lines.ts +2 -1
- package/src/cli/upgrade-runtime.ts +2 -2
- package/src/hub/bus.ts +26 -9
- package/src/hub/config-trust.ts +0 -1
- package/src/hub/conflicts.ts +1 -1
- package/src/hub/control-client.ts +3 -3
- package/src/hub/daemon.ts +145 -21
- package/src/hub/events.ts +1 -1
- package/src/hub/manager.ts +3 -3
- package/src/hub/report.ts +1 -1
- package/src/hub/routing.ts +14 -1
- package/src/hub/snapshots.ts +11 -0
- package/src/hub/tasks.ts +44 -4
- package/src/local/proxy.ts +30 -3
- package/src/local/sandbox.ts +22 -33
- package/src/omniroute/client.ts +23 -0
- package/src/pi/extension.ts +9 -0
- package/src/pi/usage.ts +14 -0
- package/templates/config.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
Issue and pull request numbers in the entries for 0.7.7 and earlier refer to the previous repository, archived on 2026-09-30 when this repository's history was rewritten; the one exception is the open smoke-check issue, formerly #12, which moved here as #1. Numbers in newer entries refer to this repository.
|
|
4
4
|
|
|
5
|
+
## 0.12.1
|
|
6
|
+
|
|
7
|
+
- Exhausted task deliveries escalate with the last error and notify the console. Three consecutive exhausted deliveries exclude a peer from routing until a delivery completes. A local worker validates its gateway, model inventory and a minimal availability call before attaching; doctor flags an unserved fixed model (#89).
|
|
8
|
+
- A `needs_review` delivery announces the queue hold once with inspection and resolution commands. Status, task assignment and route explanations identify the held delivery; the operator still decides its outcome (#90).
|
|
9
|
+
- Conflict detection includes tasks in review and reports shared files in overlapping completed turns to both owners, once per task pair and file, with their turn ids and without attributing the changes (#91).
|
|
10
|
+
- Reviewer candidates include attached peers with the `reviewer` role after the review class's preference list. A task assigned without a reviewer explicitly says that completion approves directly and lists the skipped candidates (#92).
|
|
11
|
+
- An idle local worker can be restarted on an explicit model or route; a busy worker refuses the change with a reason, and a failed model validation keeps the existing worker (#93).
|
|
12
|
+
- Pi's reported assistant usage reaches turn records and `ahub report`, including tool-loop messages, before the turn settles (#94).
|
|
13
|
+
- An idle peer with no open owner or reviewer work and no queued or active delivery is paused without spending a checkpoint turn (#95).
|
|
14
|
+
- Control protocol 11 adds queue hold diagnostics. Its recovery coordinator authenticates protocol 9, 10 and 11 sources; upgrade using the target release's coordinator.
|
|
15
|
+
|
|
16
|
+
## 0.12.0
|
|
17
|
+
|
|
18
|
+
- The egress proxy logs every refusal: an oversized request header and an unreachable listed host now leave a `network: refused` line too, the latter with its error code, never the error text; a failure after the tunnel opened closes the connection instead of writing an HTTP answer into it (#81).
|
|
19
|
+
- Behind NAT64 with the well-known prefix `64:ff9b::/96`, the egress proxy judges an answer by the IPv4 address it carries, so a listed name that resolves to an internal IPv4 host through DNS64 is refused; the local-use prefix `64:ff9b:1::/48` is refused outright, and a scoped IPv6 address (`fe80::1%en0`) counts as internal. A network-specific NAT64 prefix is not recognised (#82).
|
|
20
|
+
- `local.sandbox: "allow-default"` is removed: the local worker's and Pi's commands always run under the deny-default sandbox, and a project that still sets it starts normally with a note in `hub.log` and `ahub doctor` (paths a toolchain needs go in `local.read_allow`). `local.bash_network: "direct"` still works until 0.13.0, with the same note naming that release (#83).
|
|
21
|
+
|
|
5
22
|
## 0.11.0
|
|
6
23
|
|
|
7
24
|
- Task operations settle model-written arguments before anything reaches the board: an `owner` or `peer` that is not a peer id, or a task id that is not a whole number, is refused with a clear message instead of leaving an ownerless task behind after a database error; `null` and an empty owner mean no owner (#70).
|
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@ Native multi-agent hub for one developer's machine: Claude Code, Codex, Kimi Cod
|
|
|
4
4
|
hub-owned local-LLM worker collaborate as peers in independent project directories, with
|
|
5
5
|
task-aware model routing (Switchyard) in front of a self-hosted gateway (OmniRoute).
|
|
6
6
|
|
|
7
|
-
Status: 0.
|
|
7
|
+
Status: 0.12.1, control protocol 11. Durable delivery records distinguish queued
|
|
8
8
|
work from uncertain execution. The [smoke checklist](docs/smoke.md) records
|
|
9
9
|
verified paths and remaining prerequisites.
|
|
10
10
|
|
|
11
11
|
Read the [operations guide](docs/operations.md) for the daily workflow, queue
|
|
12
|
-
reconciliation, and the staged protocol-9 to protocol-
|
|
12
|
+
reconciliation, and the staged protocol-9/10 to protocol-11 upgrade command.
|
|
13
13
|
Controlled recovery preserves work; uncertain effects are never automatically
|
|
14
14
|
replayed and may require operator review.
|
|
15
15
|
|
|
@@ -28,7 +28,7 @@ cd <your project> && ahub init && ahub up && ahub tail
|
|
|
28
28
|
Or install the same version from GitHub:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
bun add -g github:STAIxBWLB/agent-hub#v0.
|
|
31
|
+
bun add -g github:STAIxBWLB/agent-hub#v0.12.0 && ahub setup
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
The installed commands remain `ahub` and `agent-hub`.
|
package/docs/operations.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Operations guide
|
|
2
2
|
|
|
3
|
-
This guide describes ahub 0.
|
|
3
|
+
This guide describes ahub 0.12.1 and control protocol 11. Live verification
|
|
4
4
|
results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
|
|
5
5
|
|
|
6
6
|
## Install and start
|
|
@@ -48,16 +48,23 @@ port 443 unless an entry names one) to the hosts in `local.network_allow`, and
|
|
|
48
48
|
the profile denies every other connection, direct egress and other loopback
|
|
49
49
|
ports (claude-mem's, the Codex app-server's) included. A listed name also
|
|
50
50
|
covers its subdomains; a name that resolves to a loopback or private address is
|
|
51
|
-
refused, and so is plain HTTP.
|
|
52
|
-
|
|
51
|
+
refused, and so is plain HTTP. Behind NAT64 with the well-known prefix
|
|
52
|
+
`64:ff9b::/96`, the IPv4 address inside the answer is what counts; the local-use
|
|
53
|
+
prefix `64:ff9b:1::/48` is refused outright, so on a network whose DNS64 uses it
|
|
54
|
+
an IPv4-only host such as github.com is refused too; use `"direct"` there (it is
|
|
55
|
+
removed in 0.13.0). A network-specific NAT64 prefix is not recognised. Every
|
|
56
|
+
refusal is a `network: refused` line in `hub.log`, by host where one is known
|
|
57
|
+
(with the method for plain HTTP, and the error code when a listed host is
|
|
58
|
+
unreachable), never a path, query or header. The default list holds the npm, PyPI, crates.io and Go module
|
|
53
59
|
registries and GitHub's code hosts; set `local.network_allow` in
|
|
54
60
|
`config.local.json` to replace it. `"direct"` keeps the open network of 0.10 and
|
|
55
|
-
earlier
|
|
61
|
+
earlier until 0.13.0, which removes it; while it is set, `hub.log` and `ahub
|
|
62
|
+
doctor` say so. With network on, commands may also read the public CA
|
|
56
63
|
bundles and Python's `certifi/cacert.pem`, which the `*.pem` key deny would
|
|
57
64
|
otherwise hide.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
65
|
+
The allow-default profile of 0.9 and earlier was removed in 0.12.0: a
|
|
66
|
+
`local.sandbox` setting is ignored (`"allow-default"` with a note in `hub.log`
|
|
67
|
+
and `ahub doctor`), and a toolchain the profile lacks goes in `local.read_allow`. Newly closed outside home: `/Applications`
|
|
61
68
|
(an app's bundled CLI), `/nix`, `/Volumes` and `/Users/Shared`. A toolchain
|
|
62
69
|
there, or elsewhere in your home (a CI tool cache, a version manager the profile
|
|
63
70
|
does not list), needs its directory in `local.read_allow`, for example
|
|
@@ -87,6 +94,7 @@ ahub codex
|
|
|
87
94
|
ahub kimi
|
|
88
95
|
ahub pi --mode headless --backend auto
|
|
89
96
|
ahub local
|
|
97
|
+
ahub local --model <served-model-id>
|
|
90
98
|
```
|
|
91
99
|
|
|
92
100
|
Start only the peers configured for the project. A launcher records and checks
|
|
@@ -94,6 +102,36 @@ its project and session identity; do not start a second terminal by guessing
|
|
|
94
102
|
from a process name. For multiple projects, use the explicit selector before
|
|
95
103
|
the command, for example `ahub --project /path/to/project codex`.
|
|
96
104
|
|
|
105
|
+
An attached local worker switches to an explicit `--model` or `--route` when idle.
|
|
106
|
+
A busy worker refuses with `local is busy; retry when it is idle`. With no selection
|
|
107
|
+
flag the command reports that it is already attached. Before attaching or replacing
|
|
108
|
+
it, the hub reads the authenticated model inventory and makes a one-token availability
|
|
109
|
+
call to the selected models (route targets and its fallback included). A failed check
|
|
110
|
+
keeps the existing worker; `ahub doctor` flags a fixed model absent from the inventory.
|
|
111
|
+
Controlled recovery may restore a manually paused local worker without a reachable
|
|
112
|
+
gateway, preserving its queued work. Its manual resume validates the restored model
|
|
113
|
+
choice before releasing that pause.
|
|
114
|
+
|
|
115
|
+
Three consecutive deliveries that exhaust retries mark a peer as failing. Automatic
|
|
116
|
+
assignment skips it until a delivery completes; inspect `ahub route explain` for the
|
|
117
|
+
reason. A task owned by a peer whose task delivery exhausts retries is escalated with
|
|
118
|
+
that delivery's error, and the console is notified.
|
|
119
|
+
|
|
120
|
+
A delivery in `needs_review` holds later deliveries to that peer. The console announces
|
|
121
|
+
the hold once, and status, task assignment and route explanations name its delivery id.
|
|
122
|
+
Inspect it with `ahub queue show <id>` and explicitly choose `completed`, `retry` or
|
|
123
|
+
`discard` through `ahub queue resolve`. Reassignment or approval of its task never
|
|
124
|
+
resolves an uncertain delivery automatically.
|
|
125
|
+
|
|
126
|
+
Reviewer candidates follow `[classes.review].peers`, then attached peers with a
|
|
127
|
+
`reviewer` role in `roles`. The owner cannot review its own task. If none is available,
|
|
128
|
+
the assignment output and console notice say that `done` will approve without review
|
|
129
|
+
and give the skipped-candidate reasons.
|
|
130
|
+
|
|
131
|
+
Pi 0.86 assistant usage is forwarded at `message_end`, including tool-loop messages,
|
|
132
|
+
and recorded before settlement. If a model supplies no usable count, `ahub report`
|
|
133
|
+
says `tokens not reported`; the hub does not estimate usage from text length.
|
|
134
|
+
|
|
97
135
|
## A two-agent task and review
|
|
98
136
|
|
|
99
137
|
Choose an owner explicitly, then inspect the reviewer selected by the project
|
|
@@ -435,21 +473,22 @@ Rows without a live process are stale registrations; forget them with
|
|
|
435
473
|
## Upgrade and crash recovery
|
|
436
474
|
|
|
437
475
|
Upgrade running projects with the target release's own coordinator. It accepts
|
|
438
|
-
a running source on control protocol 9 (0.6.x)
|
|
476
|
+
a running source on control protocol 9 (0.6.x), 10 (0.7.0 through 0.12.0),
|
|
477
|
+
or 11 (0.12.1) and only
|
|
439
478
|
a target on its own protocol, so the target's coordinator fits every supported
|
|
440
479
|
source and carries every recovery fix released up to it. Protocol 8 and older
|
|
441
480
|
(0.5.x and earlier) are refused as `manual-bootstrap-required`. Run from the
|
|
442
481
|
project directory, without replacing the global CLI first:
|
|
443
482
|
|
|
444
483
|
```bash
|
|
445
|
-
bunx --package @staix/agent-hub@0.
|
|
446
|
-
bunx --package @staix/agent-hub@0.
|
|
484
|
+
bunx --package @staix/agent-hub@0.12.1 ahub upgrade --to 0.12.1 --dry-run
|
|
485
|
+
bunx --package @staix/agent-hub@0.12.1 ahub upgrade --to 0.12.1 --yes
|
|
447
486
|
```
|
|
448
487
|
|
|
449
488
|
| Running now | Coordinator to use |
|
|
450
489
|
| --- | --- |
|
|
451
490
|
| 0.6.x (protocol 9) | the target's, through `bunx` as above |
|
|
452
|
-
| 0.7.0
|
|
491
|
+
| 0.7.0 through 0.12.0 (protocol 10) | the target's, through `bunx` as above |
|
|
453
492
|
| any supported source, with the installed CLI already at the target | `ahub upgrade` below, which is the same coordinator |
|
|
454
493
|
| 0.5.x or earlier (protocol 8 and older) | not supported: bootstrap by hand with the matching CLI |
|
|
455
494
|
|
|
@@ -466,28 +505,29 @@ no blocker, and `--yes` stops at staging ("target protocol requires a newer
|
|
|
466
505
|
coordinator") with an operation left to clear by `ahub recovery abort <id>`. An
|
|
467
506
|
older 0.7.x CLI may lack recovery fixes released after it. The
|
|
468
507
|
[smoke ledger](smoke.md) records dry-runs from real 0.6.4 and 0.7.5 hubs (issue
|
|
469
|
-
#75) and these applied upgrades: one with the 0.7.0 coordinator
|
|
470
|
-
0.9.0 coordinator from a running 0.8.1 hub with tasks and a budget pause
|
|
471
|
-
|
|
472
|
-
|
|
508
|
+
#75) and these applied upgrades: one with the 0.7.0 coordinator; one with the
|
|
509
|
+
0.9.0 coordinator from a running 0.8.1 hub with tasks and a budget pause; one
|
|
510
|
+
with the 0.10.0 coordinator from a 0.9.0 hub (one completion check running and
|
|
511
|
+
one queued); and one with the 0.11.0 coordinator from a 0.10.0 hub with
|
|
512
|
+
`local.bash_network` on.
|
|
473
513
|
|
|
474
514
|
The coordinator verifies and retains the exact target package, preserves its
|
|
475
515
|
own source, and promotes the global CLI only after restored projects pass
|
|
476
516
|
readback.
|
|
477
517
|
|
|
478
|
-
Once the installed CLI is 0.
|
|
518
|
+
Once the installed CLI is 0.12.0, review the current project or all registered
|
|
479
519
|
projects first:
|
|
480
520
|
|
|
481
521
|
```bash
|
|
482
522
|
ahub restart --dry-run
|
|
483
|
-
ahub upgrade --to 0.
|
|
523
|
+
ahub upgrade --to 0.12.1 --dry-run
|
|
484
524
|
```
|
|
485
525
|
|
|
486
526
|
Apply only after reviewing the plan:
|
|
487
527
|
|
|
488
528
|
```bash
|
|
489
529
|
ahub restart --yes
|
|
490
|
-
ahub upgrade --to 0.
|
|
530
|
+
ahub upgrade --to 0.12.1 --yes
|
|
491
531
|
ahub recovery status <operation-id>
|
|
492
532
|
ahub recovery resume <operation-id>
|
|
493
533
|
ahub recovery abort <operation-id>
|
|
@@ -513,7 +553,7 @@ to 0 to opt out.
|
|
|
513
553
|
0.10.0 changes every project's local worker: its commands run under the
|
|
514
554
|
deny-default sandbox described above, and a toolchain outside the listed
|
|
515
555
|
directories needs `local.read_allow`; `"local": { "sandbox": "allow-default" }`
|
|
516
|
-
in `config.local.json`
|
|
556
|
+
in `config.local.json` restored the old profile until 0.12.0. Off unless set:
|
|
517
557
|
`review.adaptive`, `recovery.auto_resume_after_crash` and `capabilities`. A hub
|
|
518
558
|
before 0.10.0 keeps no session record, so a crash of one is not reported as such
|
|
519
559
|
by the next start.
|
|
@@ -523,10 +563,15 @@ Pi's commands reach the network only through the hub's egress proxy, to the
|
|
|
523
563
|
hosts in `local.network_allow` (package registries and GitHub's code hosts by
|
|
524
564
|
default). `local.network_allow` is machine-local and replaces the default list:
|
|
525
565
|
a project that needs another host sets it in `config.local.json` with the
|
|
526
|
-
defaults it still needs. `"direct"` keeps the open network of 0.10.0
|
|
527
|
-
|
|
566
|
+
defaults it still needs. `"direct"` keeps the open network of 0.10.0 until
|
|
567
|
+
0.13.0. Each command gets a temp dir of its own, and under deny-default the
|
|
528
568
|
shared temp dirs are closed.
|
|
529
569
|
|
|
570
|
+
0.12.0 removes `local.sandbox: "allow-default"`: a project that still sets it
|
|
571
|
+
runs under the deny-default sandbox, and `hub.log` and `ahub doctor` say so.
|
|
572
|
+
`local.bash_network: "direct"` still works until 0.13.0, and both name it while
|
|
573
|
+
it is set.
|
|
574
|
+
|
|
530
575
|
The 0.7.0 transition stages the verified package and runs a retained
|
|
531
576
|
coordinator from the source tree. It accepts a verified protocol-9 source and
|
|
532
577
|
moves to a protocol-10 target. The source journal, queued envelopes, tasks,
|
package/docs/quickstart.md
CHANGED
package/docs/security.md
CHANGED
|
@@ -12,7 +12,7 @@ agent-hub connects agents that can each run commands. This page says what the hu
|
|
|
12
12
|
- **Snapshots stay in your repository.** Per-turn snapshots (issue #33) are git objects in the project's own object store, written through a temporary index; nothing is referenced, pushed or copied elsewhere, and `git gc` prunes them. They hold what the work tree held, including untracked files that are not ignored, so keep secrets in ignored files. They carry the repository's own permissions, and nothing caps their disk use but `git gc`. A turn of a peer holding an open PII task is not snapshotted; a PII file left in the project is snapshotted by later turns like any other file. `ahub undo` restores only files whose current content is exactly what the turn left.
|
|
13
13
|
- **The edit hook reads, never decides.** `ahub check-path --hook` (issue #32) reads hub.db and returns context for Claude and a line for you; it sets no permission decision, so your permission rules stay in charge. It names other owners' task ids, titles and states, which then reach Claude's model; PII tasks are left out.
|
|
14
14
|
- **The session record holds identities only.** `.agenthub/state/sessions.json` (issue #37, mode 600) keeps each attached peer's recovery metadata: launch options, session and thread ids, Pi's session file path. No message or task text; loss notices name deliveries by id, sender and public task title.
|
|
15
|
-
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval, and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git/hooks` or `.git/config` writes; no network, loopback included, unless `local.bash_network`; with it, only through the hub's egress proxy to the hosts in `local.network_allow` (issue #65), which refuses names that resolve to internal addresses, so claude-mem and the Codex app-server stay out of reach (`"direct"` opens everything,
|
|
15
|
+
- **The local worker is boxed in.** Paths are resolved through symlinks and must stay inside the project; a secrets denylist (`.env*`, keys, credential files, the hub's own state) applies to its file tools, its git arguments and its memory capture alike; `.git` and `.agenthub` are not writable. Writes, edits, shell commands and mutating git wait for approval, and the approver sees what will be written or run, with control characters escaped. Everything it executes runs under the macOS sandbox, attended or not. Since 0.10 the profile starts from deny default (issue #39): commands run and read only the system, toolchain and project directories (and the project's git dir and the selected developer dir; with network on, the public CA bundles); no writes outside the project, its git dir and a temp dir of its own (`TMPDIR`, made for each command and removed when it ends; issue #63); no `.git/hooks` or `.git/config` writes; no network, loopback included, unless `local.bash_network`; with it, only through the hub's egress proxy to the hosts in `local.network_allow` (issue #65), which refuses names that resolve to internal addresses (behind NAT64 with the well-known prefix, by the IPv4 address the answer carries; a network-specific prefix is not recognised), so claude-mem and the Codex app-server stay out of reach (`"direct"` opens everything, until 0.13.0). One channel stays open: `trustd`, which TLS clients need, can fetch a certificate's AIA or OCSP URL on a command's behalf, outside the proxy. The allow-default profile of 0.9 and earlier was removed in 0.12.0 (issue #83); a `local.sandbox` setting is ignored with a note. The shared temp dirs (the user's and `/private/tmp`) are closed. Without the sandbox there is no `bash` tool.
|
|
16
16
|
- **Capabilities are enforced, not suggested.** `capabilities` (issue #39) is checked by the daemon where task operations and messages arrive, so a peer cannot get round it by phrasing. A peer can never answer a permission request: the control link takes `permit` from the console role only, and a message that quotes a permit command is just text. Both bind the hub's own tool paths: a vendor agent with its own shell in the project (Codex, Claude, Kimi) can read `.agenthub/state/control-token` and connect as the console, which only the local worker's and Pi's sandbox prevents.
|
|
17
17
|
- **PII has an enforced path.** A task matching `signals.pii_patterns` goes to `local` or to nobody; its text is absent from other peers' envelopes, the console stream, the log and the board listing; the console user reviews it; `local` answers such a turn to the console only, keeps it out of its history, refuses it when the only gateway is off campus, and may not save notes or spin off tasks during it. Nothing about it is sent to claude-mem, whose observer is a cloud model.
|
|
18
18
|
- **Free text is screened too.** On an ordinary task, a done summary (with its check output: a match withholds the whole note), a review note, an unmet item or a budget handoff that matches a PII pattern (issue #69) is not saved to claude-mem, and every peer, `local` included, gets a stub naming `ahub task show <id>`; the board keeps the text, `hub.log` notes the withholding by task id only, and `ahub ask` shows such a note only when its model is reached on campus. `hub_remember` refuses a match; what a vendor agent writes to its own memory is not screened.
|
package/docs/smoke.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
`scripts/check.sh` covers everything against fakes. The legs below need real accounts and an interactive terminal, so they are run by hand and recorded here.
|
|
4
4
|
|
|
5
|
+
## Reliability and self-development run (#89-#95, 0.12.1)
|
|
6
|
+
|
|
7
|
+
Measured on 2026-10-01 in an isolated worktree of this repository:
|
|
8
|
+
|
|
9
|
+
- Started the installed 0.12.0 hub, with real Kimi 2.1.1 and Pi 0.86.0 peers. The console created tasks, agents accepted them with plans, and the hub recorded completion. Kimi implemented reviewer routing and delivery-health inputs; Pi located the installed usage contracts. The console completed Pi integration after stopping its prolonged exploration and inspected/discarded the superseded uncertain deliveries explicitly.
|
|
10
|
+
- The initial fake-based gate passed 560 tests with `check: OK`; the final review also adds coverage for self-claims made during a running turn. Coverage includes task escalation, failing-peer routing and recovery, queue-hold notices/status/assignment/explanation, reviewer roles, idle local model replacement and busy refusal, concurrent edits and in-review overwrites, Pi bridge usage dedupe, and owner/reviewer checkpoint behavior.
|
|
11
|
+
- `bun scripts/smoke-recovery-09-10.ts` ran the real published 0.6.4 package (protocol 9) into the working tree (protocol 11), preserving queued envelope ids, task identity/state digest and manual pauses. Operation `7acefb22-2eef-4f28-a241-73990ffbee88` completed with queue readback.
|
|
12
|
+
- The target coordinator then restarted the actual development hub from 0.12.0/protocol 10 into 0.12.1/protocol 11. Operation `d42beca5-856b-46f8-8cb9-7694213eb799` completed with its project verified and the board/queues retained.
|
|
13
|
+
- A real Pi DGX probe answered as FYI and reported 42,967 tokens in `ahub report`. Its usage was forwarded from native `message_end` events; the earlier 0.12.0 Pi turn had no recorded token count. This proves accounting, not a comparison of model efficiency.
|
|
14
|
+
- Manually paused local recovery can retain its queue while the gateway is unavailable. Manual resume validates its model choice before lifting the pause. Native Claude/Codex account smoke legs remain separate from this Kimi/Pi run.
|
|
15
|
+
|
|
5
16
|
## Hub collaboration measurement (issues #29-#32, 0.6.2)
|
|
6
17
|
|
|
7
18
|
Measured on 2026-09-20 against a live 0.6.1 hub and a disposable project, before the fixes:
|
|
@@ -964,3 +975,106 @@ was attached. Times are UTC.
|
|
|
964
975
|
check.
|
|
965
976
|
- The coordinator promoted the global CLI to 0.10.0, and `ahub setup --yes`
|
|
966
977
|
installed the 0.10.0 plugin.
|
|
978
|
+
|
|
979
|
+
## 0.10.0 to 0.11.0 attended upgrade (2026-10-01)
|
|
980
|
+
|
|
981
|
+
A scratch project (a git work tree initialized by the 0.10.0 `ahub init`) ran a
|
|
982
|
+
0.10.0 hub with `"local": { "bash_network": true }` in
|
|
983
|
+
`.agenthub/config.local.json`, two tasks (#1 with the path spelled `./a.txt`),
|
|
984
|
+
and an open budget pause: a peer had attached once and gone offline, and `ahub
|
|
985
|
+
budget set` fed it a 95% reading. No peer was attached at the upgrade. Times are
|
|
986
|
+
UTC, from `hub.log` and the operation record.
|
|
987
|
+
|
|
988
|
+
- 0.11.0 was published at 08:43:45 (the registry's `time` field); a
|
|
989
|
+
cache-busting registry read, observed at the console at 08:43:58, showed it
|
|
990
|
+
as `latest`. `bunx --package @staix/agent-hub@0.11.0 ahub
|
|
991
|
+
upgrade --to 0.11.0 --dry-run` then listed one project, source 0.10.0, protocol
|
|
992
|
+
10, no blockers.
|
|
993
|
+
- `--yes` created the operation at 08:44:19.4. The source committed at 08:44:19.5
|
|
994
|
+
and stopped; the 0.11.0 hub started its egress proxy (13 allowed hosts) at
|
|
995
|
+
08:44:19.7, found the pause still open with its reset time, and was up at
|
|
996
|
+
08:44:19.7. The operation, global install and plugin install included,
|
|
997
|
+
completed at 08:44:27.7.
|
|
998
|
+
- The tasks' refs and history and the pause row's columns, dumped with `sqlite3`
|
|
999
|
+
before and after (the latter once the 0.11.0 hub was up), were byte-identical: #1 still reads `./a.txt` (0.11.0 normalizes paths only on new
|
|
1000
|
+
writes).
|
|
1001
|
+
- Through the proxy, `curl` reached registry.npmjs.org (200, observed at the
|
|
1002
|
+
console: the proxy logs no successes) and was refused example.com, logged as `network: refused example.com:443 (example.com:443 is
|
|
1003
|
+
not in local.network_allow)`.
|
|
1004
|
+
- After `ahub setup --yes`, `claude plugin list` showed the 0.11.0 plugin.
|
|
1005
|
+
|
|
1006
|
+
## 0.11.0 to 0.12.0 attended upgrade (2026-10-01)
|
|
1007
|
+
|
|
1008
|
+
A scratch git project ran a 0.11.0 hub whose `.agenthub/config.local.json` set
|
|
1009
|
+
both settings 0.12.0 retires, `"local": { "sandbox": "allow-default",
|
|
1010
|
+
"bash_network": "direct" }`, with two tasks and an open budget pause (a peer had attached once and gone offline, and
|
|
1011
|
+
`ahub budget set` fed it a 95% reading). No peer was attached at the upgrade.
|
|
1012
|
+
Times are UTC, from `hub.log`, the operation record and saved command output.
|
|
1013
|
+
|
|
1014
|
+
- 0.12.0 was published at 10:35:02 (the registry's `time` field); a saved
|
|
1015
|
+
cache-busting registry read at 10:35:17 showed it as `latest` with SLSA
|
|
1016
|
+
provenance.
|
|
1017
|
+
- `bunx --package @staix/agent-hub@0.12.0 ahub upgrade --to 0.12.0 --dry-run`
|
|
1018
|
+
listed one project, source 0.11.0, protocol 10, no blockers.
|
|
1019
|
+
- `--yes` (10:35:35) created the operation at 10:35:36.0; the source committed at
|
|
1020
|
+
10:35:36.1 and stopped. The 0.12.0 hub logged both notes at
|
|
1021
|
+
10:35:36.3 (`local.sandbox "allow-default" was removed in 0.12.0 ...` and
|
|
1022
|
+
`local.bash_network "direct" ... goes in 0.13.0 ...`), found the pause still
|
|
1023
|
+
open with its reset time, and was up at 10:35:36.4. It started no egress
|
|
1024
|
+
proxy, as `"direct"` asks. The operation, global and plugin install included,
|
|
1025
|
+
completed at 10:35:44.2.
|
|
1026
|
+
- The tasks' refs and history and the pause row's columns, dumped with `sqlite3`
|
|
1027
|
+
before and after, were byte-identical.
|
|
1028
|
+
- `ahub doctor` in the project showed both notes as `-- retired setting` rows,
|
|
1029
|
+
and after `ahub setup --yes`, `claude plugin list` showed the 0.12.0 plugin.
|
|
1030
|
+
|
|
1031
|
+
## 0.12.0 live use with real agents (2026-10-01)
|
|
1032
|
+
|
|
1033
|
+
A disposable git project (`textkit`: `slugify` with three failing tests, `mean` without `median`) ran the released
|
|
1034
|
+
0.12.0 hub with real Kimi 2.1.1 under ACP, Pi 0.86.0 headless on the DGX backend, the local worker and real
|
|
1035
|
+
codex-cli 0.156.1 behind the hub's app-server proxy. A script stood in for the Codex TUI, and another for the console
|
|
1036
|
+
approver: it answered each relayed permission with allow once. No Claude peer was attached. The console proposed the
|
|
1037
|
+
work. Times are `hub.log` UTC. Issues #89-#95 come from this run.
|
|
1038
|
+
|
|
1039
|
+
- **Codex's own quota reading paused it.** Its `rateLimits` read the week window at 95%. The hub sent the checkpoint request at
|
|
1040
|
+
12:47:42.1, paused Codex at 12:48:00.3 with "checkpoint received", and recorded the hand-off (reset in 3208 min,
|
|
1041
|
+
beyond `wait_max_min` 30). That one checkpoint turn cost 172,176 tokens (#95).
|
|
1042
|
+
- **Without a gateway the local worker attached, failed and kept its task.** No model gateway was configured for
|
|
1043
|
+
the project, yet `ahub local` printed "attached" (observed at the console). Its first task envelope (#2) failed
|
|
1044
|
+
three times and was given up at 12:48:09.2 (`hub.log` and the console tail say so), and the task stayed with
|
|
1045
|
+
`local`, not escalated (#89).
|
|
1046
|
+
- **Pi's failed turn was escalated.** It failed for the same reason, and #1 moved to Kimi at 12:48:23.2.
|
|
1047
|
+
- **Unanswered approvals were cancelled.** Kimi's two `bun test` approvals went unanswered: the approver script had not
|
|
1048
|
+
subscribed to permission pushes yet, a harness error. They were cancelled after 120 s, and Kimi reported that it
|
|
1049
|
+
could not run the tests. #1's completion check (`bun test`) passed, and #1 was approved with no reviewer at
|
|
1050
|
+
12:53:34.6: Claude was absent, Codex paused, and Kimi owned it. Kimi's `reviewer` role in `config.json` counted
|
|
1051
|
+
only once `routing.toml` listed it in the review class, which was edited mid-run (12:49); that is why Pi's #2, #3
|
|
1052
|
+
and #5 got Kimi as reviewer (#92).
|
|
1053
|
+
- **A controlled restart carried the queues.** After the gateway settings were added, `ahub restart --yes` with
|
|
1054
|
+
Kimi, Pi and local attached took 13:02:26 to 13:02:30, project verified. Pi's queued and needs_review deliveries
|
|
1055
|
+
carried over.
|
|
1056
|
+
- **The pinned model was not served.** With the gateway reachable, the template's `[local] fixed_model` was answered
|
|
1057
|
+
with HTTP 401 ("No active credentials for provider"). #2 was given up again at 13:03:05.8 and 13:04:31.6.
|
|
1058
|
+
`ahub local --model coding` answered "already attached" (observed at the console; #93).
|
|
1059
|
+
- **A needs_review delivery held Pi's queue.** #2, reassigned to Pi, waited behind it until the console discarded
|
|
1060
|
+
it (#90). Pi then accepted #2 at 13:07:13, its check passed, and Kimi approved it at 13:07:43.9 with a review
|
|
1061
|
+
note against the plan.
|
|
1062
|
+
- **The dependent task became ready, and went to a failing peer.** #3, after #1 and #2, became ready at 13:07:44.0 and
|
|
1063
|
+
was assigned to `local`, whose delivery was given up at 13:07:45.3 (#89). Reassigned to Pi, it was done and
|
|
1064
|
+
approved by Kimi at 13:11:39.6.
|
|
1065
|
+
- **The overlap was settled by the agents.** #5 (Pi) was proposed while #4 (Kimi) claimed the same two files, and
|
|
1066
|
+
the claim warned at 13:09:22.3. Pi sent Kimi its plan before editing. Kimi proposed a shared options object and
|
|
1067
|
+
Pi agreed. Both landed, and the suite passed with 18 tests. The hop cap stopped their acknowledgements at hop 4.
|
|
1068
|
+
- **The concurrent edit went undetected.** The two turns overlapped on those files, and no conflict was logged:
|
|
1069
|
+
`ahub report` shows `edit conflicts: 0` (#91).
|
|
1070
|
+
- **The egress proxy held.** In a Pi turn, `curl` reached registry.npmjs.org (200, in Pi's answer). example.com got
|
|
1071
|
+
"CONNECT tunnel failed, response 403", logged at 13:12:01.0 as `network: refused example.com:443`.
|
|
1072
|
+
- **Undo worked both ways** (observed at the console; `ahub undo` writes nothing to `hub.log`). It refused a turn
|
|
1073
|
+
whose files changed later, and listed, then restored, a safe one.
|
|
1074
|
+
- **Crash recovery brought the peers back.** `kill -9` hit the daemon while Kimi was in a turn (its turn started
|
|
1075
|
+
at 13:17:56.7; the kill at 13:17:59 was observed at the console). At 13:18:11.6 the next start reported one in-flight delivery in needs_review. Kimi resumed by ACP `session/load` at
|
|
1076
|
+
13:18:12.5, Pi from its session file at 13:18:12.8, and local started fresh; a process check at the console found
|
|
1077
|
+
no orphan from the killed run.
|
|
1078
|
+
Retried from the console, the question reached Kimi led by the loss notice ("The hub stopped unexpectedly ...",
|
|
1079
|
+
with the delivery id), and Kimi answered at 13:19:26.6.
|
|
1080
|
+
- **Pi's tokens were missing from the report** (#94).
|
|
@@ -1099,7 +1099,9 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
1099
1099
|
the flag on, network is allowed as in 0.9 and earlier. Issue #65 builds it
|
|
1100
1100
|
(amendment below).
|
|
1101
1101
|
- `local.sandbox` ("deny-default" | "allow-default") is machine-local, since
|
|
1102
|
-
"allow-default" widens the sandbox.
|
|
1102
|
+
"allow-default" widens the sandbox. Removed in 0.12.0 (issue #83): the field is
|
|
1103
|
+
ignored with a note in `hub.log` and `ahub doctor`, and is no longer
|
|
1104
|
+
machine-local because nothing reads it.
|
|
1103
1105
|
- Capabilities: `propose`, `assign` (a proposal naming another peer as owner),
|
|
1104
1106
|
`remember`, `important`. A peer not listed in `capabilities` has all of them,
|
|
1105
1107
|
which keeps today's behaviour, and a listed peer whose value is not a list has
|
|
@@ -1125,5 +1127,17 @@ session modes (`default`, `plan`, `auto`, `yolo`) but nothing per server or tool
|
|
|
1125
1127
|
- Decisions recorded in the issue: the default list holds the npm, Yarn, PyPI,
|
|
1126
1128
|
crates.io and Go module registries and GitHub's code hosts, so a project that
|
|
1127
1129
|
already had network on keeps installing packages; `"direct"` keeps the open
|
|
1128
|
-
network of 0.10 and earlier
|
|
1130
|
+
network of 0.10 and earlier until 0.13.0 (issue #83).
|
|
1129
1131
|
|
|
1132
|
+
|
|
1133
|
+
|
|
1134
|
+
## Reliability follow-ups, 0.12.1 (#89-#95)
|
|
1135
|
+
|
|
1136
|
+
- Delivery retries exhausted for an owned open task trigger escalation through `Tasks`, with a redacted reason and a console notice. Delivery health skips peers after three consecutive exhausted deliveries until a completed delivery clears the streak.
|
|
1137
|
+
- `needs_review` continues to hold a peer's queue. Console notices, status and assignment explanations identify the hold and operator commands; moved or approved task references are context for the operator, never automatic discard authorization.
|
|
1138
|
+
- Local attach and idle model/route replacement require a reachable authenticated gateway, served models and successful minimal availability calls. Busy workers refuse replacement; validation failure leaves the current worker intact. Controlled recovery can reconstruct a manually paused local worker without gateway availability, but manual resume validates its restored choice before lifting the pause. Doctor checks the fixed model against the inventory.
|
|
1139
|
+
- Reviewer routing appends attached peers holding `roles.reviewer` after the review class's candidates. Without a reviewer, task results and console notices explain direct approval and the skipped candidates.
|
|
1140
|
+
- Conflict detection includes `in_review` work. Completed overlapping snapshots that share files generate one concurrent-edit notice per task pair/file, containing both turn ids and attributing the changes to neither peer. Private tasks and sensitive file names remain redacted.
|
|
1141
|
+
- Pi assistant usage is forwarded through the authenticated bridge and recorded before settlement, without counting the same message both at `message_end` and `agent_end`.
|
|
1142
|
+
- An idle peer with no open owner/reviewer task and no queued or active delivery skips the quota checkpoint turn; the pause proceeds immediately and the log records the skip.
|
|
1143
|
+
- Control protocol 11 adds queue hold metadata; the current coordinator supports authenticated protocol 9, 10 and 11 recovery sources.
|
|
@@ -155,3 +155,11 @@ successful 0.7.0 cutover from tests, synthetic approvals, or package
|
|
|
155
155
|
installation alone. Issue [#1](https://github.com/STAIxBWLB/agent-hub/issues/1)
|
|
156
156
|
remains open for off-campus Access credentials and a natural near-limit budget
|
|
157
157
|
pause.
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
### Protocol 11 follow-up (0.12.1)
|
|
161
|
+
|
|
162
|
+
The target coordinator accepts authenticated protocol 9, 10 and 11 sources, then
|
|
163
|
+
uses protocol 11 after startup. Older supported managers are refreshed onto the
|
|
164
|
+
current implementation. The real 0.6.4/protocol-9 transition and the 0.12.0/protocol-10
|
|
165
|
+
development-hub transition are recorded in `docs/smoke.md`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Channel between Claude Code and the agent-hub daemon: peer messages from Codex, Kimi and the local worker arrive as channel events; hub_send replies.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Young Joon Lee",
|
|
@@ -15615,8 +15615,8 @@ function projectContext(cwd, env = process.env) {
|
|
|
15615
15615
|
function stateDirFor(cwd) {
|
|
15616
15616
|
return projectContext(cwd).stateDir;
|
|
15617
15617
|
}
|
|
15618
|
-
var PROTOCOL =
|
|
15619
|
-
var RECOVERY_SOURCE_PROTOCOLS = [9, PROTOCOL];
|
|
15618
|
+
var PROTOCOL = 11;
|
|
15619
|
+
var RECOVERY_SOURCE_PROTOCOLS = [9, 10, PROTOCOL];
|
|
15620
15620
|
function readControl(stateDir) {
|
|
15621
15621
|
try {
|
|
15622
15622
|
const status = JSON.parse(readFileSync2(join2(stateDir, "status.json"), "utf8"));
|
|
@@ -15727,7 +15727,7 @@ class ControlClient {
|
|
|
15727
15727
|
// package.json
|
|
15728
15728
|
var package_default = {
|
|
15729
15729
|
name: "@staix/agent-hub",
|
|
15730
|
-
version: "0.
|
|
15730
|
+
version: "0.12.1",
|
|
15731
15731
|
description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
15732
15732
|
license: "MIT",
|
|
15733
15733
|
type: "module",
|
|
@@ -17,7 +17,7 @@ export interface LocalOptions {
|
|
|
17
17
|
route?: string;
|
|
18
18
|
/** Model id sent straight to OmniRoute when the sidecar is absent, unhealthy or fails a call. */
|
|
19
19
|
fixedModel: string;
|
|
20
|
-
tools: { deny: string[]; permit: ToolContext["permit"]; bashNetwork?: SandboxNetwork; readAllow?: string[]
|
|
20
|
+
tools: { deny: string[]; permit: ToolContext["permit"]; bashNetwork?: SandboxNetwork; readAllow?: string[] };
|
|
21
21
|
capture?: Capture;
|
|
22
22
|
/** Runs a hub task tool (hub_task_*, hub_review, hub_remember) as this peer. Absent = the tools are not offered. */
|
|
23
23
|
taskTool?: (name: string, args: Record<string, unknown>, turn: { pii: boolean }) => Promise<string>;
|
|
@@ -67,7 +67,7 @@ export class LocalPeer extends BasePeer {
|
|
|
67
67
|
private readonly opts: LocalOptions,
|
|
68
68
|
) {
|
|
69
69
|
super(id, opts.watchdogMs);
|
|
70
|
-
this.sandboxProfile = profile(opts.cwd, opts.tools.bashNetwork ?? false, opts.tools.readAllow, opts.tools.deny
|
|
70
|
+
this.sandboxProfile = profile(opts.cwd, opts.tools.bashNetwork ?? false, opts.tools.readAllow, opts.tools.deny);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
recoveryMetadata(): Record<string, unknown> {
|
package/src/adapters/pi.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface PiOptions {
|
|
|
16
16
|
relay: PiRelay; executeTool: (name: string, args: unknown, toolCallId: string, sessionId?: string) => Promise<string>; tools: PiToolSchema[];
|
|
17
17
|
preamble?: string;
|
|
18
18
|
selectModel?: (envs: Envelope[]) => Promise<string | undefined>; maxSteps?: number;
|
|
19
|
+
/** Reported message usage as increments, before agent_settled releases the turn. */
|
|
20
|
+
onTokens?: (added: number) => void;
|
|
19
21
|
onTurnFailure?: (envs: Envelope[], reason: string) => Promise<void>;
|
|
20
22
|
watchdogMs?: number; log?: (line: string) => void;
|
|
21
23
|
/** How long stop() waits for a graceful TUI owner exit before verified teardown. Tests shrink this. */
|
|
@@ -42,6 +44,7 @@ export class PiPeer extends BasePeer {
|
|
|
42
44
|
private buffer = "";
|
|
43
45
|
private seq = 0;
|
|
44
46
|
private readonly pending = new Map<string, { resolve: (m: RpcMessage) => void; reject: (e: Error) => void }>();
|
|
47
|
+
private readonly usageSeen = new Set<string>();
|
|
45
48
|
private settledText = "";
|
|
46
49
|
private settledError = "";
|
|
47
50
|
private settledCancelled = false;
|
|
@@ -337,8 +340,13 @@ export class PiPeer extends BasePeer {
|
|
|
337
340
|
this.startOwnerMonitor(); if (this.opts.mode === "tui") this.setState("idle");
|
|
338
341
|
}
|
|
339
342
|
if (event.type === "session_shutdown") { this.stopping = true; this.ownerClaimed = false; this.clearOwnerMonitor(); this.resolveTuiExit?.(); this.resolveTuiExit = undefined; this.fail(new Error("Pi session shut down before settlement")); }
|
|
340
|
-
if (event.type === "agent_start") { this.noteActivity(); this.agentRunning = true; this.setState("busy"); }
|
|
343
|
+
if (event.type === "agent_start") { this.usageSeen.clear(); this.noteActivity(); this.agentRunning = true; this.setState("busy"); }
|
|
341
344
|
if (event.type === "activity" && this.state === "busy") this.touch();
|
|
345
|
+
if (event.type === "tokens" && this.state === "busy" && typeof event.id === "string" && event.id.length <= 100 && Number.isSafeInteger(event.tokens) && event.tokens >= 0 && !this.usageSeen.has(event.id)) {
|
|
346
|
+
this.usageSeen.add(event.id);
|
|
347
|
+
if (this.usageSeen.size > 1000) this.usageSeen.delete(this.usageSeen.values().next().value!);
|
|
348
|
+
if (event.tokens > 0) this.opts.onTokens?.(event.tokens);
|
|
349
|
+
}
|
|
342
350
|
if (event.type === "agent_end") {
|
|
343
351
|
this.settledText = typeof event.text === "string" ? event.text : "";
|
|
344
352
|
this.settledCancelled = event.cancelled === true;
|
package/src/cli/main.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { currentRouting } from "../hub/routing.ts";
|
|
2
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
3
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
4
5
|
import { homedir } from "node:os";
|
|
@@ -550,6 +551,9 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
550
551
|
|
|
551
552
|
local: async () => {
|
|
552
553
|
const opt = (flag: string) => (args.includes(flag) ? args[args.indexOf(flag) + 1] ?? fail(`${flag} needs a value`) : undefined);
|
|
554
|
+
for (const flag of ["--route", "--model"]) if (args.includes(flag) && (!opt(flag) || opt(flag)!.startsWith("--"))) fail(`${flag} needs a value`);
|
|
555
|
+
if (args.includes("--route") && args.includes("--model")) fail("use --route or --model, not both");
|
|
556
|
+
if (args.some((a, i) => i % 2 === 0 && !["--route", "--model"].includes(a))) fail("usage: ahub local [--route <id> | --model <id>]");
|
|
553
557
|
const hub = await connect();
|
|
554
558
|
const res = await hub.request({ t: "start", peer: "local", args: { route: opt("--route"), model: opt("--model") }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION });
|
|
555
559
|
hub.close();
|
|
@@ -892,10 +896,18 @@ const commands: Record<string, () => Promise<void> | void> = {
|
|
|
892
896
|
|
|
893
897
|
const config = loadConfig(cwd);
|
|
894
898
|
row(!config.ignored, "config", config.ignored ? `${config.ignored.join("; ")} (move them to .agenthub/config.local.json)` : "no machine-local field ignored");
|
|
899
|
+
for (const line of config.retired ?? []) row(false, "retired setting", line); // issue #83
|
|
895
900
|
const omni = new OmniRoute(config.omniroute);
|
|
896
901
|
const gateway = await omni.base();
|
|
897
902
|
row(!!gateway, "omniroute", gateway ? `${new URL(gateway).host} healthy` : config.omniroute.urls.length || process.env.AGENTHUB_OMNIROUTE_URL ? "no candidate reachable (VPN off?); ahub local cannot run" : "not configured: set omniroute.urls in .agenthub/config.local.json (any OpenAI-compatible gateway); ahub local cannot run");
|
|
898
903
|
row(!!omni.apiKey(), "omniroute key", omni.apiKey() ? "present" : "missing: set OMNIROUTE_API_KEY or omniroute.api_key_file in .agenthub/config.local.json");
|
|
904
|
+
if (gateway && omni.apiKey()) {
|
|
905
|
+
try {
|
|
906
|
+
const fixed = currentRouting(cwd).local.fixed_model;
|
|
907
|
+
const served = (await omni.models()).includes(fixed);
|
|
908
|
+
row(served, "local fixed_model", served ? `${fixed} served` : `${fixed} is not served by the gateway`);
|
|
909
|
+
} catch { row(false, "local fixed_model", "could not read the gateway model inventory"); }
|
|
910
|
+
}
|
|
899
911
|
const sy = spawnSync(process.env.AGENTHUB_SWITCHYARD_BIN ?? "switchyard-server", ["--version"], { encoding: "utf8" });
|
|
900
912
|
row(sy.status === 0 ? true : undefined, "switchyard", sy.status === 0 ? sy.stdout.trim() : "not installed: ahub local uses fixed_model on OmniRoute (cargo install --locked switchyard-server)");
|
|
901
913
|
const mlxConfig = config.mlx;
|
package/src/cli/status-lines.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// What `ahub status` prints for a peer and for a model backend, kept pure so it can be checked.
|
|
2
2
|
|
|
3
|
-
export interface PeerRow { state?: string; queued?: number; queuedImportant?: number; needsReview?: number; oldestQueuedAt?: number; attached?: boolean; paused?: string; servedBy?: string; requestedModel?: string }
|
|
3
|
+
export interface PeerRow { state?: string; queued?: number; queuedImportant?: number; needsReview?: number; heldBy?: string; holdNote?: string; oldestQueuedAt?: number; attached?: boolean; paused?: string; servedBy?: string; requestedModel?: string }
|
|
4
4
|
export interface BackendRow { kind?: string; alias?: string; state?: string; active?: number; requestedModel?: string; actualModel?: string; provider?: string }
|
|
5
5
|
|
|
6
6
|
/** Aliases are already namespaced ("dgx/coding"); only an alias that is not gets its kind in front of it. */
|
|
@@ -15,6 +15,7 @@ export function peerLine(id: string, p: PeerRow): string {
|
|
|
15
15
|
// Which queued messages the peer will not wait out the batch window for (issue #41).
|
|
16
16
|
(p.queuedImportant ? ` (${p.queuedImportant} important)` : "") +
|
|
17
17
|
(p.needsReview ? ` needs review ${p.needsReview}` : "") +
|
|
18
|
+
(p.heldBy ? ` ${p.holdNote ?? `held by needs_review ${p.heldBy}; ahub queue resolve ${p.heldBy} --action completed|retry|discard --reason <text>`}` : "") +
|
|
18
19
|
(p.oldestQueuedAt !== undefined ? ` oldest ${Math.max(0, Math.floor((Date.now() - p.oldestQueuedAt) / 1000))}s` : "") +
|
|
19
20
|
(p.attached === false ? " disconnected" : "") +
|
|
20
21
|
(p.paused ? ` (${p.paused})` : "") +
|
|
@@ -104,7 +104,7 @@ export async function makeUpgradePlan(kind: "restart" | "upgrade", version: stri
|
|
|
104
104
|
if (existsSync(managerFile)) {
|
|
105
105
|
try {
|
|
106
106
|
const manager = JSON.parse(readFileSync(managerFile, "utf8"));
|
|
107
|
-
if (!RECOVERY_SOURCE_PROTOCOLS.includes(manager.protocol as (typeof RECOVERY_SOURCE_PROTOCOLS)[number])) body.blockers.push("manager requires manual bootstrap with its matching CLI before protocol
|
|
107
|
+
if (!RECOVERY_SOURCE_PROTOCOLS.includes(manager.protocol as (typeof RECOVERY_SOURCE_PROTOCOLS)[number])) body.blockers.push("manager requires manual bootstrap with its matching CLI before current-protocol recovery");
|
|
108
108
|
} catch { body.blockers.push("manager ownership manifest is unreadable"); }
|
|
109
109
|
}
|
|
110
110
|
for (const project of projects) {
|
|
@@ -114,7 +114,7 @@ export async function makeUpgradePlan(kind: "restart" | "upgrade", version: stri
|
|
|
114
114
|
catch { source = { state: "unavailable", peers: [], blockers: ["source recovery metadata could not be authenticated"] }; }
|
|
115
115
|
if (source.state === "stopped" || source.state === "missing") continue;
|
|
116
116
|
const blockers = [...source.blockers];
|
|
117
|
-
if (!RECOVERY_SOURCE_PROTOCOLS.includes(source.protocol as (typeof RECOVERY_SOURCE_PROTOCOLS)[number]) || source.state !== "running") blockers.push("manual-bootstrap-required: an authenticated protocol-9 or protocol-
|
|
117
|
+
if (!RECOVERY_SOURCE_PROTOCOLS.includes(source.protocol as (typeof RECOVERY_SOURCE_PROTOCOLS)[number]) || source.state !== "running") blockers.push("manual-bootstrap-required: an authenticated protocol-9, protocol-10 or protocol-11 source is required");
|
|
118
118
|
if (source.recovery?.operationId && source.recovery.phase !== "released") blockers.push(`existing recovery operation ${source.recovery.operationId} must be resolved first`);
|
|
119
119
|
const sessions: { codex?: string; claude?: string; pi?: SessionRef } = {};
|
|
120
120
|
for (const peer of source.peers) {
|