@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 +16 -0
- package/README.md +20 -9
- package/docs/operations.md +99 -7
- package/docs/quickstart.md +2 -2
- package/docs/smoke.md +87 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +23 -3
- package/docs/specs/2026-09-20-durable-delivery-design.md +2 -2
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1 -1
- package/src/adapters/pi.ts +29 -4
- package/src/cli/init.ts +27 -5
- package/src/cli/main.ts +95 -5
- package/src/cli/models-setup.ts +39 -0
- package/src/cli/upgrade-runtime.ts +25 -4
- package/src/hub/bus.ts +18 -3
- package/src/hub/daemon.ts +64 -27
- package/src/hub/lifecycle.ts +15 -2
- package/src/hub/manager.ts +15 -3
- package/src/models/mlx.ts +24 -2
- package/src/models/ollama.ts +167 -0
- package/src/models/relay.ts +19 -4
- package/templates/AGENTS.block.md +8 -7
- package/templates/CLAUDE.block.md +0 -11
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.
|
|
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.
|
|
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 #
|
|
73
|
-
ahub models start # loopback
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
package/docs/operations.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Operations guide
|
|
2
2
|
|
|
3
|
-
This guide describes ahub 0.7.
|
|
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.
|
|
154
|
-
bunx --package @staix/agent-hub@0.7.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
package/docs/quickstart.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
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
|
-
- [
|
|
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": "agent-hub",
|
|
3
|
-
"version": "0.7.
|
|
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.
|
|
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",
|
package/src/adapters/pi.ts
CHANGED
|
@@ -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() +
|
|
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
|
-
|
|
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() +
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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");
|