@staix/agent-hub 0.7.1 → 0.7.3

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.7.3
6
+
7
+ - A peer that detaches mid-upgrade no longer wedges the recovery: readiness ignores a now-offline peer's stale identity (a reattach with a different thread still blocks), an already-exited terminal is closed as a no-op instead of being waited on, and resume can drive the operation to release once the peer returns (#21).
8
+ - The check.sh leak guard matches tmp test roots only, so a real hub in a directory starting with `ahub-` no longer trips it (#56).
9
+
10
+ ## 0.7.2
11
+
12
+ - Local inference defaults to a bounded Ollama MLX runtime: `ahub models setup/start/stop` manages it, the configured context is advertised to Pi, and the legacy standalone MLX runtime needs an explicit legacy provider (#51, #52).
13
+ - `ahub init` writes the managed block to `AGENTS.md` only and never creates `CLAUDE.md`: any `CLAUDE.md` stops Claude Code from loading `AGENTS.md`. A block an older `init` left in `CLAUDE.md` is removed, and a `CLAUDE.md` that held nothing else is deleted; a symlinked or hard-linked `CLAUDE.md` is left alone. The one template now tells every agent how it receives and answers hub messages (#54).
14
+ - Stop orphaned hub daemons from leaking and spinning: a rejected peer stop no longer blocks shutdown, a 15 s hard deadline ends a hung stop, the recovery fence waits with backoff and a timeout instead of spinning, and a daemon or manager whose project root or state dir vanished stops itself. A surviving Pi TUI owner is torn down by verified process identity before the hub reports stopped (#56).
15
+ - `ahub doctor --orphans [--kill]` lists registrations whose project root is gone and kills only processes whose argv names that project's hub daemon exactly (#56).
16
+ - A recovery commit no longer blocks when its stop-poll lands in the moment a stopping hub already refuses connections but has not removed its manifest yet: the inspection reports "unavailable" and the poll retries instead of failing the operation (#21).
17
+ - The test suite records and sweeps every hub and manager pid it starts, and `scripts/check.sh` fails the gate on any leaked `ahub-*` test daemon (#56).
18
+
3
19
  ## 0.7.1
4
20
 
5
21
  - Restore native sessions while unrelated agents remain in the same worktree; the coordinator continues to fence uncertain terminal creations (#48).
package/README.md CHANGED
@@ -4,7 +4,7 @@ Native multi-agent hub for one developer's machine: Claude Code, Codex, Kimi Cod
4
4
  hub-owned local-LLM worker collaborate as peers in independent project directories, with
5
5
  task-aware model routing (Switchyard) in front of a self-hosted gateway (OmniRoute).
6
6
 
7
- Status: 0.7.1, control protocol 10. Durable delivery records distinguish queued
7
+ Status: 0.7.3, control protocol 10. Durable delivery records distinguish queued
8
8
  work from uncertain execution. The [smoke checklist](docs/smoke.md) records
9
9
  verified paths and remaining prerequisites.
10
10
 
@@ -28,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.7.1 && ahub setup
31
+ bun add -g github:STAIxBWLB/agent-hub#v0.7.3 && ahub setup
32
32
  ```
33
33
 
34
34
  The installed commands remain `ahub` and `agent-hub`.
@@ -69,8 +69,8 @@ peer with the hub. The project uses its existing OmniRoute connection for DGX;
69
69
  `pi.dgx_coding` and `pi.dgx_fast` select the gateway's configured model IDs.
70
70
 
71
71
  ```bash
72
- ahub models setup # dedicated Python 3.12 environment and pinned Qwen3 8B MLX weights
73
- ahub models start # loopback-only Apple Silicon runtime
72
+ ahub models setup # prepare bounded Qwen3.5 4B MLX in an existing Ollama service
73
+ ahub models start # verify loopback Ollama/model readiness (no daemon spawn)
74
74
  ahub models status
75
75
  ahub pi --mode headless --backend auto
76
76
  ahub say @pi "Read the project and summarize the next small implementation task"
@@ -92,11 +92,22 @@ Tool-call receipts survive daemon restart; an interrupted operation with an
92
92
  unknown outcome must be reconciled before repeating it. Cancelling a Pi turn
93
93
  does not automatically hand its task to a cloud peer.
94
94
 
95
- MLX uses a pinned Qwen3 8B 4-bit model, a 16K input budget and one generation
96
- at a time. `ahub models stop` stops only the runtime whose process identity
97
- matches its ownership record. Project hubs share the runtime; stopping a hub
98
- does not stop MLX. The relay can fall back from MLX to DGX before streaming
99
- starts. Inspect `ahub status` for requested routes and reported backend models.
95
+ The `mlx/fast` alias now defaults to an externally managed Ollama MLX backend.
96
+ Install and start Ollama separately, with `OLLAMA_NO_CLOUD=1`,
97
+ `OLLAMA_NUM_PARALLEL=1`, `OLLAMA_MAX_LOADED_MODELS=1`, and a finite
98
+ `OLLAMA_KEEP_ALIVE` (five minutes is the tested setting). `models setup`
99
+ prepares `agenthub-fast-mlx:4b-8k` from `qwen3.5:4b-mlx`, with an 8K total
100
+ context and a 2K output cap. The relay budgets input separately at 6,000
101
+ estimated tokens and serializes local requests across project hubs.
102
+
103
+ `models status` distinguishes model availability from memory residency.
104
+ `models stop` refuses to unload an externally managed Ollama model: other
105
+ clients may be using it. Ollama owns idle eviction; stopping a hub never
106
+ kills Ollama. The OpenAI compatibility endpoint does not enforce a request
107
+ `keep_alive` field, so configure the server and verify its expiry readback.
108
+ The relay can fall back to DGX before streaming starts as before. See the
109
+ [Ollama migration procedure](docs/operations.md#ollama-mlx-migration-issue-51)
110
+ for existing Python runtime/configuration cleanup and rollback.
100
111
 
101
112
  A project has one managed Pi session owner. Mode changes require the agent to
102
113
  settle and preserve the session file. Before the first generation, a live source
@@ -1,6 +1,6 @@
1
1
  # Operations guide
2
2
 
3
- This guide describes ahub 0.7.1 and control protocol 10. Live verification
3
+ This guide describes ahub 0.7.3 and control protocol 10. Live verification
4
4
  results and remaining prerequisites are recorded separately in [the smoke ledger](smoke.md).
5
5
 
6
6
  ## Install and start
@@ -144,33 +144,65 @@ peers. A stopped daemon does not delete task records or the durable journal.
144
144
  Do not kill a native terminal by PID or start a replacement while ownership is
145
145
  uncertain.
146
146
 
147
+ Shutdown is bounded: once it begins, a daemon that cannot finish within
148
+ 15 seconds exits anyway, and a peer that refuses to stop is logged rather than
149
+ allowed to block state cleanup. A native Pi TUI owner that does not exit
150
+ gracefully is terminated by its verified process identity (never a bare PID),
151
+ so a restarted hub never launches beside a survivor. A hub whose project root
152
+ or state directory was deleted stops itself within about 10 seconds; the
153
+ dashboard manager does the same when its home directory vanishes.
154
+
155
+ ## Orphaned daemons
156
+
157
+ Daemons from before those guards, or daemons whose project directory was
158
+ deleted while they were stopped mid-shutdown, are found with:
159
+
160
+ ```bash
161
+ ahub doctor --orphans
162
+ ```
163
+
164
+ It lists registrations whose project root is gone, with any live process
165
+ candidates (the registry claim, the state manifest, the legacy pid file).
166
+ To stop them:
167
+
168
+ ```bash
169
+ ahub doctor --orphans --kill
170
+ ```
171
+
172
+ `--kill` sends SIGTERM, then SIGKILL, but only while the process argv names
173
+ that project's hub daemon exactly (the argument after `--project` must equal
174
+ the registered root); a bare, reused, or merely prefix-matching PID is never
175
+ killed.
176
+ Rows without a live process are stale registrations; forget them with
177
+ `ahub projects remove <id>`.
178
+
147
179
  ## Upgrade and crash recovery
148
180
 
149
181
  For an upgrade from 0.6.4 (protocol 9) or 0.7.0, use the patched coordinator without
150
182
  replacing the global CLI prematurely. Run from the project directory:
151
183
 
152
184
  ```bash
153
- bunx --package @staix/agent-hub@0.7.1 ahub upgrade --to 0.7.1 --dry-run
154
- bunx --package @staix/agent-hub@0.7.1 ahub upgrade --to 0.7.1 --yes
185
+ bunx --package @staix/agent-hub@0.7.3 ahub upgrade --to 0.7.3 --dry-run
186
+ bunx --package @staix/agent-hub@0.7.3 ahub upgrade --to 0.7.3 --yes
155
187
  ```
156
188
 
157
189
  The coordinator verifies and retains the exact target package, preserves its
158
190
  own source, and promotes the global CLI only after restored projects pass
159
191
  readback. Do not use the old protocol-9 coordinator to target protocol 10.
160
192
 
161
- Once the installed CLI is 0.7.1, review the current project or all registered
193
+ Once the installed CLI is 0.7.3, review the current project or all registered
162
194
  projects first:
163
195
 
164
196
  ```bash
165
197
  ahub restart --dry-run
166
- ahub upgrade --to 0.7.1 --dry-run
198
+ ahub upgrade --to 0.7.3 --dry-run
167
199
  ```
168
200
 
169
201
  Apply only after reviewing the plan:
170
202
 
171
203
  ```bash
172
204
  ahub restart --yes
173
- ahub upgrade --to 0.7.1 --yes
205
+ ahub upgrade --to 0.7.3 --yes
174
206
  ahub recovery status <operation-id>
175
207
  ahub recovery resume <operation-id>
176
208
  ahub recovery abort <operation-id>
@@ -207,7 +239,8 @@ not a latency or reliability benchmark.
207
239
  The 0.6.4 production readback retained two envelopes for a disconnected Codex
208
240
  peer with identical IDs before and after recovery; general status lists attached
209
241
  peers and therefore did not show that backlog. No messages were deleted. The
210
- 0.7.0 queue readback and receipt semantics remain pending live verification.
242
+ 0.7.1 attended cutover and production readback verified queue visibility and
243
+ completion receipts; see the final section of the smoke checklist.
211
244
 
212
245
  Issue [#12](https://github.com/STAIxBWLB/agent-hub/issues/12) remains open for
213
246
  off-campus Access credentials and a natural near-limit budget pause. Tests,
@@ -215,3 +248,62 @@ synthetic approvals, and an authenticated internal network do not close those
215
248
  prerequisites. Adapter acceptance is not task completion, and package
216
249
  installation or a passing test suite is not proof of a successful production
217
250
  cutover.
251
+
252
+
253
+ ## Ollama MLX migration (issue #51)
254
+
255
+ The routing names `mlx/fast` and `--backend mlx` are retained. The normal
256
+ runtime provider is now Ollama; the standalone Python server is legacy.
257
+ Use an Apple Silicon Ollama build with MLX support and keep it bound to
258
+ loopback. Configure its finite keep-alive and one-model/one-generation
259
+ limits before use. The model is prepared only by explicit `models setup`;
260
+ an incoming generation does not download models or start a service.
261
+
262
+ Project `.agenthub/config.json` example:
263
+
264
+ ```json
265
+ {
266
+ "mlx": {
267
+ "provider": "ollama",
268
+ "host": "127.0.0.1",
269
+ "port": 11434,
270
+ "model": "agenthub-fast-mlx:4b-8k",
271
+ "sourceModel": "qwen3.5:4b-mlx",
272
+ "contextWindow": 8192,
273
+ "maxInputTokens": 6000,
274
+ "maxTokens": 2048,
275
+ "maxConcurrency": 1
276
+ }
277
+ }
278
+ ```
279
+
280
+ 1. Record the current project/hub state. Do not restart stopped hubs or
281
+ replay queued tasks as part of model migration.
282
+ 2. If the old Python server is still running, use the old matching CLI's
283
+ `models stop` before changing configuration, or explicitly select
284
+ `provider: "legacy"` temporarily. Its PID, start time and command must
285
+ match the ownership record. Never kill by process name. Preserve model
286
+ files and the old configuration for rollback.
287
+ 3. Apply the Ollama configuration above, removing legacy `modelPath`,
288
+ `runtimeDir` and Python binary settings. Existing custom legacy paths
289
+ without an explicit provider fail with a migration error. Old 16K input
290
+ overrides must also be reduced; the new total context is 8K.
291
+ 4. Run `ahub models setup`, `ahub models start`, and `ahub models status`.
292
+ Setup creates a dedicated derived model; an existing model is inspected
293
+ rather than silently overwritten. A wrong context recipe must be fixed
294
+ deliberately under a new model name or after an explicit model change.
295
+ 5. Verify a streamed completion and tool call through the authenticated
296
+ relay. Check Ollama logs for the MLX runner, `/api/ps` for context and
297
+ finite expiry, then confirm idle eviction. Catalog availability does
298
+ not mean the model is resident. Unit tests are not this live proof.
299
+
300
+ Ollama is shared and external. `models stop` refuses in Ollama mode rather
301
+ than interrupting another client. Hub shutdown releases local handles and
302
+ requests, not the server. Normal idle eviction is Ollama's responsibility;
303
+ no AgentHub timer unloads a potentially shared active model.
304
+
305
+ The relay's input budget is an estimate, not exact model tokenization. Keep
306
+ headroom for templates/tool metadata and choose a larger dedicated recipe
307
+ only after measuring memory. This path does not change local-worker PII
308
+ routing or the DGX backend. Rollback requires explicitly restoring the old
309
+ config with `provider: "legacy"`; Ollama errors never launch Python.
@@ -17,7 +17,7 @@ ahub setup # installs the Claude Code channel plu
17
17
  Or use the matching GitHub release:
18
18
 
19
19
  ```bash
20
- bun add -g github:STAIxBWLB/agent-hub#v0.7.1
20
+ bun add -g github:STAIxBWLB/agent-hub#v0.7.3
21
21
  ahub setup
22
22
  ```
23
23
 
@@ -33,7 +33,7 @@ and upgrade or crash recovery, see the [operations guide](operations.md).
33
33
  In your project directory, one terminal each:
34
34
 
35
35
  ```bash
36
- ahub init # .agenthub/config.json, .agenthub/routing.toml, marker blocks in CLAUDE.md and AGENTS.md
36
+ ahub init # .agenthub/config.json, .agenthub/routing.toml, the marker block in AGENTS.md
37
37
  ahub up # the daemon for this directory (loopback only)
38
38
  ahub tail # keep open: the conversation, state changes, permission requests
39
39
  ahub kimi # Kimi, headless
package/docs/smoke.md CHANGED
@@ -511,7 +511,7 @@ pause.
511
511
  npm integrity against the new coordinator. Protocol 9 to 10 migration reached
512
512
  `released`; exact queued envelope IDs, manual pauses and task identity/state
513
513
  digest were preserved. The disposable daemon was stopped afterward.
514
- - Real Kimi 2.0.1 and Pi ran in a disposable project using the 0.7.0 code.
514
+ - Real Kimi and Pi ran in a disposable project using the 0.7.0 code.
515
515
  Kimi requested a read-only task-list approval before answering the no-action
516
516
  note as FYI; 85.361 seconds includes operator waiting and is not model latency.
517
517
  No follow-on recipient delivery occurred.
@@ -543,3 +543,89 @@ reached `completed` with its project `verified`.
543
543
  coordinator's pending-create fence and returned terminal/session verification.
544
544
  Regression tests include unrelated terminals and delayed close inventory.
545
545
  The 0.7.1 attended repeat is recorded separately after application.
546
+
547
+
548
+ ## 0.7.1 verified production application (2026-09-20)
549
+
550
+ - Local full gate: 328 pass, 0 fail, 1732 assertions, `check: OK`. Ubuntu and
551
+ macOS CI passed for the merged patch. npm publication with provenance and
552
+ GitHub Release completed; the staged package matched registry integrity.
553
+ - The patched coordinator recreated the captured Claude session while the
554
+ unrelated Codex and Qwen terminals remained open. It did not require manual
555
+ terminal creation. The native development-channel confirmation still required
556
+ an attended acknowledgement; resume adopted that same replacement terminal.
557
+ - Recovery completed with the project verified and the live daemon released.
558
+ Global CLI, daemon and Claude plugin report 0.7.1, control protocol 10.
559
+ The installed runtime digest matches the verified staged artifact.
560
+ - Claude and Pi session identities match the pre-0.7.0 plan. Kimi reattached
561
+ with a fresh session by design. Queue/manual-pause/task/budget integrity
562
+ matched the recovery snapshot; the board still has three approved tasks.
563
+ - The disconnected Codex recipient is visible with the same two queued
564
+ messages. They were neither dispatched nor discarded during validation.
565
+ - A production Pi read returned an exact random marker through MLX in 6.067
566
+ seconds. Its journal receipt reached `completed`; its FYI answer triggered
567
+ zero other-peer turns. The daemon instance and Pi session stayed unchanged,
568
+ and the temporary file was removed.
569
+ - `ahub doctor` reported all configured checks healthy, including the delivery
570
+ journal and Claude recovery identity. The hwp-cli hub remained stopped.
571
+ - These are supervised live checks. Native confirmation screens, uncertain
572
+ delivery review, and the infrastructure prerequisites in #12 remain explicit
573
+ operator responsibilities; this does not certify unattended operation.
574
+
575
+
576
+ ## 0.7.1 live status re-measurement (2026-09-21)
577
+
578
+ Read-only checks against the running production daemon one day after the
579
+ verified application; no daemon, plugin, terminal, or journal mutation.
580
+
581
+ - `ahub --version` returned 0.7.1 and `ahub status` showed the daemon
582
+ (pid 48154, control 127.0.0.1:4600) with claude offline (queued 0), kimi
583
+ idle, pi idle on `mlx/fast`, and the disconnected Codex recipient still
584
+ carrying two queued messages (`oldest 68615s`). The board still shows
585
+ three approved tasks.
586
+ - `ahub doctor` reported all 14 configured checks ok, including delivery
587
+ recovery (`journal healthy; no deliveries need review`), the Claude plugin
588
+ at 0.7.1, omniroute and switchyard 0.2.0, Pi MLX, and claude-mem 13.25.2.
589
+ - `ahub queue list --json` (protocol 10) read back the two Codex receipts as
590
+ `queued` at revision 16 with their envelope IDs unchanged from the 0.7.1
591
+ validation, and the Pi read receipt as `completed` with `important: true`.
592
+ Neither Codex message was dispatched or discarded.
593
+ - `bun run check` on the current HEAD passed with 328 tests, 0 failures and
594
+ 1734 expect() calls, `check: OK`. The count is two above the 1732 recorded
595
+ for the 0.7.1 application; no source change is present on HEAD.
596
+ - Kimi reports 2.0.2 in doctor; earlier live legs on this page used 2.0.1.
597
+
598
+ Issue [#12](https://github.com/STAIxBWLB/agent-hub/issues/12) remains open
599
+ for off-campus Access credentials and a natural near-limit budget pause.
600
+
601
+
602
+ ## Ollama MLX migration validation (issue #51, 2026-09-22)
603
+
604
+ The repository gate passed with 343 tests, zero failures, and `check: OK`.
605
+ A source-build smoke on Apple silicon/macOS 27.0 with Ollama 0.34.2 verified:
606
+
607
+ - Official `qwen3.5:4b-mlx` was prepared as the dedicated model
608
+ `agenthub-fast-mlx:4b-8k` (digest prefix `975ef418e0df`). Model metadata and
609
+ resident status both reported context length 8192; output is capped at 2048.
610
+ - `bun scripts/smoke-ollama.ts` sent a completion, streamed a `get_status`
611
+ tool call, and sent back a synthetic tool result through the authenticated
612
+ AgentHub relay. The final response was nonempty and the relay reported
613
+ actual model `agenthub-fast-mlx:4b-8k`. No real tool or DGX fallback ran.
614
+ - Ollama logged `starting mlx runner subprocess`. A short direct completion
615
+ recorded approximately 3.87 GiB peak; after tool calls the resident model
616
+ was approximately 4.6 GB by Ollama's accounting. These are short smoke
617
+ samples, not a long-context peak-memory guarantee or a controlled
618
+ like-for-like comparison with the former Qwen3-8B Python process.
619
+ - A separate native-API 15-second keep-alive probe expired and `/api/ps`
620
+ became empty. The OpenAI relay requests independently reported a five-minute
621
+ expiry from the external service configuration. After the last relay
622
+ request at 08:19:21 KST, `/api/ps` was empty at 08:24:27 without an explicit
623
+ unload. This does not mean the relay sets a per-request keep-alive on the
624
+ OpenAI endpoint.
625
+ - The Homebrew Ollama build logged a missing optional xgrammar library.
626
+ Ordinary text and tool calls passed; grammar-constrained structured JSON
627
+ output is not certified by this smoke.
628
+
629
+ The live harness is opt-in and is not called by the hermetic repository
630
+ check. `AHUB_SMOKE_PROJECT` selects a project configuration without starting
631
+ its daemon or replaying its queue. It deliberately forbids remote inference.
@@ -274,7 +274,8 @@ peer. Peer ids claimed over the control WS must not be `user` or a hub-managed a
274
274
  natively. This replaces Codex `dynamicTools`: no rewriting of proxied TUI traffic.
275
275
  - Role contract: Claude in the plugin `instructions`, Codex, Kimi and `local` with the
276
276
  standing instruction of their first delivery, all of them in the `AGENT_HUB` marker
277
- blocks; `roles` in `.agenthub/config.json` is the source. Codex `developerInstructions`
277
+ block of `AGENTS.md` (`ahub init` writes no `CLAUDE.md`: one hides `AGENTS.md` from
278
+ Claude Code); `roles` in `.agenthub/config.json` is the source. Codex `developerInstructions`
278
279
  injection is dropped for the same reason as `dynamicTools`.
279
280
  - PII (amended in M4): a task matching `signals.pii_patterns` is owned by `local` or by
280
281
  nobody; its envelopes are `private` (console tail and `hub.log` print a stub), lists show
@@ -502,10 +503,10 @@ src/memory/ claude-mem worker client, capture for the local worker, reca
502
503
  src/cli/ up, claude, codex, kimi, local, say, tail, board, route, budget, status, logs, kill, init
503
504
  plugins/agent-hub/ .claude-plugin/plugin.json, .mcp.json, server bundle, hooks (SessionStart health, Stop announce)
504
505
  .claude-plugin/marketplace.json
505
- templates/ CLAUDE.md and AGENTS.md marker blocks, routing.toml default, config.json default
506
+ templates/ AGENTS.md marker block, routing.toml default, config.json default
506
507
  scripts/ check.sh, build.mjs, smoke-*.ts
507
508
  docs/specs/ this file
508
- CLAUDE.md, REVIEW.md
509
+ AGENTS.md, REVIEW.md
509
510
  ```
510
511
 
511
512
  ## Acceptance criteria
@@ -673,3 +674,22 @@ ownership and a separate optional dashboard manager. Runtime data and routing
673
674
  remain per project. Native memory aliases remain compatible and shared.
674
675
  Control protocol 7 adds project/instance identity and console-only dashboard
675
676
  snapshot/action forwarding. Existing project-local UI security rules still apply.
677
+
678
+
679
+ ## Amendment: externally managed Ollama MLX (issue #51)
680
+
681
+ The `mlx/fast` route retains its name but defaults to `mlx.provider=ollama`.
682
+ The hub connects to a loopback Ollama service and a dedicated bounded model,
683
+ without spawning Python or owning the Ollama PID. Setup explicitly prepares
684
+ Qwen3.5 4B MLX with an 8K context; input and output budgets and shared
685
+ cross-process admission bound each request. Status separates catalog
686
+ availability, residency, expiry and provider readiness. Shared-service
687
+ shutdown/unload is not a hub operation; finite idle eviction is configured
688
+ and verified in Ollama. Legacy Python lifecycle functions remain available
689
+ only through explicit project configuration for migration/rollback.
690
+
691
+ The authenticated relay, cancellation, tool streaming, Pi backend aliases,
692
+ pre-stream DGX fallback and PII policy remain intact. Ollama model/context
693
+ mismatch fails explicitly, and errors do not resurrect a Python runtime.
694
+ See issue #51 for the acceptance contract and docs/operations.md for the
695
+ non-destructive migration procedure.
@@ -1,6 +1,6 @@
1
1
  # Durable delivery and supervised operations
2
2
 
3
- Status: Implemented on `feat/durable-delivery-operations`; release application pending
3
+ Status: Implemented in #47; applied and verified with the #48/#49 recovery patch in 0.7.1
4
4
  Date: 2026-09-20
5
5
  Target: 0.7.0, protocol 10
6
6
 
@@ -58,7 +58,7 @@ Update README and quickstart, add an English operations guide covering installat
58
58
  - [x] Queue operator controls and status/dashboard visibility.
59
59
  - [x] Cross-version recovery compatibility and actionable diagnostics.
60
60
  - [x] English guides and recorded live evidence.
61
- - [ ] Fault tests, independent review, CI, release and verified application.
61
+ - [x] Fault tests, independent review, CI, release and verified application.
62
62
 
63
63
  ## Integration decision
64
64
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-hub",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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",
@@ -15727,7 +15727,7 @@ class ControlClient {
15727
15727
  // package.json
15728
15728
  var package_default = {
15729
15729
  name: "@staix/agent-hub",
15730
- version: "0.7.1",
15730
+ version: "0.7.3",
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,6 +17,8 @@ export interface PiOptions {
17
17
  selectModel?: (envs: Envelope[]) => Promise<string | undefined>; maxSteps?: number;
18
18
  onTurnFailure?: (envs: Envelope[], reason: string) => Promise<void>;
19
19
  watchdogMs?: number; log?: (line: string) => void;
20
+ /** How long stop() waits for a graceful TUI owner exit before verified teardown. Tests shrink this. */
21
+ stopGraceMs?: number;
20
22
  }
21
23
  export interface PiTuiLaunch { cmd: string; args: string[]; env: NodeJS.ProcessEnv; }
22
24
  type RpcMessage = { type?: string; id?: string | number; command?: string; success?: boolean; data?: any; [key: string]: any };
@@ -212,22 +214,25 @@ export class PiPeer extends BasePeer {
212
214
  this.stopping = true;
213
215
  this.clearOwnerMonitor();
214
216
  if (this.opts.mode === "tui") {
217
+ const grace = this.opts.stopGraceMs ?? 5_000;
215
218
  const alive = () => !!this.ownerPid && ownerStillAlive(this.ownerPid, this.ownerSignature);
216
219
  if (this.ownerClaimed && alive()) {
217
220
  try {
218
221
  await Promise.race([this.sendTui({ type: "shutdown" }), (async () => {
219
- const deadline = Date.now() + 5_000;
222
+ const deadline = Date.now() + grace;
220
223
  while (alive() && Date.now() < deadline) await Bun.sleep(50);
221
224
  if (alive()) throw new Error("Pi TUI shutdown was not acknowledged");
222
225
  })()]);
223
226
  }
224
227
  catch (error) {
225
- if (alive()) { this.setState("busy"); this.startOwnerMonitor(); throw error; }
228
+ // A lost shutdown acknowledgement is not a reason to leave a native owner running
229
+ // next to its replacement: tear it down by verified identity, or fail stop (#56).
230
+ if (alive() && !(await this.stopSurvivingOwner())) { this.setState("busy"); this.startOwnerMonitor(); throw error; }
226
231
  }
227
232
  }
228
- const deadline = Date.now() + 5_000;
233
+ const deadline = Date.now() + grace;
229
234
  while (alive() && Date.now() < deadline) await Bun.sleep(50);
230
- if (alive()) { this.setState("busy"); this.startOwnerMonitor(); throw new Error("Pi TUI owner process did not exit"); }
235
+ if (alive() && !(await this.stopSurvivingOwner()) && alive()) { this.setState("busy"); this.startOwnerMonitor(); throw new Error("Pi TUI owner process did not exit"); }
231
236
  this.ownerClaimed = false;
232
237
  this.resolveTuiExit?.(); this.resolveTuiExit = undefined;
233
238
  }
@@ -243,6 +248,26 @@ export class PiPeer extends BasePeer {
243
248
  this.server?.stop(true); this.server = undefined; this.setState("offline");
244
249
  }
245
250
 
251
+ /**
252
+ * Terminate a verified TUI owner that outlived its graceful shutdown. Every signal is
253
+ * guarded by ownerStillAlive, which re-reads the process signature on each poll, so a
254
+ * reused PID is never signaled. True once the owner is confirmed gone.
255
+ */
256
+ private async stopSurvivingOwner(): Promise<boolean> {
257
+ const pid = this.ownerPid;
258
+ const signature = this.ownerSignature;
259
+ const gone = () => !pid || !ownerStillAlive(pid, signature);
260
+ if (!pid || !signature || gone()) return true;
261
+ try { process.kill(pid, "SIGTERM"); } catch { return gone(); }
262
+ let deadline = Date.now() + 2_000;
263
+ while (!gone() && Date.now() < deadline) await Bun.sleep(50);
264
+ if (gone()) return true;
265
+ try { process.kill(pid, "SIGKILL"); } catch { return gone(); }
266
+ deadline = Date.now() + 2_000;
267
+ while (!gone() && Date.now() < deadline) await Bun.sleep(50);
268
+ return gone();
269
+ }
270
+
246
271
  async deliver(envs: Envelope[], deliveryId?: string): Promise<void> {
247
272
  if (this.state !== "idle" || this.stopping) {
248
273
  if (deliveryId) this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} is not ready` });
package/src/cli/init.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
4
  const TEMPLATES = join(import.meta.dir, "..", "..", "templates");
@@ -14,6 +14,15 @@ export function upsertBlock(existing: string, block: string): string {
14
14
  return `${existing.trimEnd()}${existing.trim() ? "\n\n" : ""}${managed}\n`;
15
15
  }
16
16
 
17
+ /** Drop the managed block and the blank lines around it. Text without a complete block is returned as is. */
18
+ export function removeBlock(existing: string): string {
19
+ const start = existing.indexOf("<!-- AGENT_HUB:BEGIN");
20
+ const end = existing.indexOf(END, Math.max(start, 0));
21
+ if (start === -1 || end <= start) return existing;
22
+ const rest = [existing.slice(0, start).trimEnd(), existing.slice(end + END.length).trim()].filter(Boolean).join("\n\n");
23
+ return rest && `${rest}\n`;
24
+ }
25
+
17
26
  /** Returns the paths it changed. */
18
27
  export function init(cwd: string): string[] {
19
28
  const changed: string[] = [];
@@ -30,10 +39,23 @@ export function init(cwd: string): string[] {
30
39
  const routing = join(cwd, ".agenthub", "routing.toml");
31
40
  if (!existsSync(routing)) write(routing, readFileSync(join(TEMPLATES, "routing.toml"), "utf8"));
32
41
 
33
- for (const [file, template] of [["CLAUDE.md", "CLAUDE.block.md"], ["AGENTS.md", "AGENTS.block.md"]] as const) {
34
- const path = join(cwd, file);
35
- const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
36
- write(path, upsertBlock(existing, readFileSync(join(TEMPLATES, template), "utf8")));
42
+ const agents = join(cwd, "AGENTS.md");
43
+ write(agents, upsertBlock(existsSync(agents) ? readFileSync(agents, "utf8") : "", readFileSync(join(TEMPLATES, "AGENTS.block.md"), "utf8")));
44
+
45
+ // Any CLAUDE.md, even an empty one, stops Claude Code from loading AGENTS.md: take back the block older versions wrote there.
46
+ // Only a regular file of its own: a symlink may lead out of the project, and a link to AGENTS.md
47
+ // (hard, or AGENTS.md -> CLAUDE.md) holds the block just written.
48
+ const claude = join(cwd, "CLAUDE.md");
49
+ const own = lstatSync(claude, { throwIfNoEntry: false });
50
+ const target = statSync(agents);
51
+ const legacy = own?.isFile() && !(own.dev === target.dev && own.ino === target.ino) ? readFileSync(claude, "utf8") : "";
52
+ const rest = removeBlock(legacy);
53
+ if (rest !== legacy) {
54
+ if (rest) write(claude, rest);
55
+ else {
56
+ unlinkSync(claude);
57
+ changed.push(claude);
58
+ }
37
59
  }
38
60
 
39
61
  const ignore = join(cwd, ".gitignore");