@staix/agent-hub 0.6.4 → 0.7.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.
@@ -0,0 +1,95 @@
1
+ # Concurrent projects and a unified local dashboard
2
+
3
+ Status: Approved for implementation on `feat/multi-project-management`
4
+ Date: 2026-09-19
5
+
6
+ ## Problem
7
+
8
+ Verified against 0.3.2: inherited `AGENTHUB_STATE_DIR` routes commands in a second
9
+ project to the first hub. The unlocked JSON port registry also loses updates:
10
+ 32 parallel allocations produced two unique ports and one surviving registration.
11
+ The baseline gate passes 162 tests, but does not cover these scenarios.
12
+
13
+ ## Existing assets
14
+
15
+ Each daemon already owns its bus, task database, adapters, control token and local
16
+ dashboard. Bun provides SQLite without another runtime dependency. Reuse existing
17
+ console authorization, dashboard validation and PII redaction.
18
+
19
+ ## Design
20
+
21
+ ### Project context
22
+
23
+ - Resolve real paths and the nearest initialized ahub directory without crossing
24
+ the nearest Git working-tree root. Otherwise use that root or the non-Git cwd.
25
+ - Each worktree/submodule is independent. A global CLI prefix,
26
+ `ahub --project <path|id> <command>`, selects another registered project or path.
27
+ - Only accept an inherited state directory when its recorded project matches.
28
+ Pass the validated root and state directory to children and MCP servers.
29
+ For initial custom-state setup, an explicit matching `AGENTHUB_PROJECT_DIR` pair
30
+ is accepted only when no contradictory marker exists. Startup records a persistent
31
+ project marker. Native MCP sessions stay pinned to their launcher-supplied state
32
+ directory, while their control handshake verifies the manifest identity.
33
+ - Native memory identity remains separate: preserve native claude-mem aliases,
34
+ including parent and parent/worktree names. Show shared-alias warnings; do not
35
+ change global hooks or historical records.
36
+
37
+ ### Registry and lifecycle
38
+
39
+ - Use `~/.agenthub/registry.db` (override root with `AGENTHUB_HOME` for tests).
40
+ Register on init/up. Store opaque ID, canonical root, state directory, ports and
41
+ runtime ownership. Import legacy ports.json transactionally and conservatively.
42
+ - Serialize allocation and per-project startup claims. Retain base 4600/stride 10;
43
+ retry available ranges on startup port conflicts. Never terminate foreign listeners.
44
+ - Publish readiness only after binding. Authenticated status carries project ID,
45
+ instance ID, version and protocol; bounded clients verify the requested identity.
46
+ - Only the owning instance removes runtime files or releases a claim. Stop waits
47
+ for owned children; uncertain ownership and incomplete shutdown are explicit errors.
48
+ - Expose `projects`, `status --all`, and `projects remove <id>` (stopped metadata
49
+ only). Moved roots are new registrations; never merge by basename.
50
+
51
+ ### Unified dashboard
52
+
53
+ - `ui --all [--no-open]` starts a separate lazy manager, usable outside projects.
54
+ `ui --all --stop` stops only the manager. Existing project-local ui remains.
55
+ - List roots, status, peers, queues and task counts. Selecting a project exposes
56
+ existing stream, board, messages, approvals and budgets.
57
+ - Start/stop registered hubs; stop confirmation names the project. Native TUI
58
+ launch remains CLI-only. Manager starts are attended and use project config and
59
+ credential files, not another project's gateway/state/unattended environment.
60
+ - Manager uses console-only authenticated snapshot/action RPCs. Browser input
61
+ selects registry IDs, never arbitrary filesystem paths, PIDs, ports or tokens.
62
+ Mutations carry expected daemon instance. No automatic mutation retries.
63
+ - Scope cursors/drafts by project, discard stale responses across switches and
64
+ restarts, and maintain bounded redacted event buffers in each daemon.
65
+ - Preserve exact Host/Origin checks, loopback-only listeners, one-time tickets,
66
+ expiring HttpOnly sessions, CSP and terminal-only private approvals.
67
+ - Bump the control protocol and rebuild the plugin. Incompatible hubs get explicit
68
+ upgrade/restart guidance, never automatic restart.
69
+ - One active Codex TUI per hub; reject a second attachment. Quota windows may be
70
+ account-wide and must not be added together across projects.
71
+
72
+ ## Testing and acceptance criteria
73
+
74
+ - Root discovery: nested directory/project, symlink, submodule, worktree, same
75
+ basename, invalid target, and inherited context from another project.
76
+ - 32 simultaneous allocations retain all entries with unique ports. Concurrent
77
+ starts of one project produce one daemon. Migration, occupied ports and startup
78
+ recovery preserve existing data and live processes.
79
+ - Two hubs with fake native/model services isolate boards, messages, approvals,
80
+ quota files, sidecars and shutdown. Test stale tokens, PID reuse, old protocols,
81
+ partial startup, instance changes and owned cleanup.
82
+ - Browser: start with no hub, start two projects, switch/action/stop one and keep
83
+ using the other. Cover late responses, identical task IDs, expired approvals,
84
+ session expiry and PII redaction.
85
+ - Run scripts/check.sh and record real two-project smoke evidence separately.
86
+
87
+ ## Delivery
88
+
89
+ Implementation issue and feature branch, spec/docs updates, independently reviewed
90
+ and verified PR. Preserve pre-existing instruction-file changes.
91
+
92
+ ## Out of scope
93
+
94
+ Cross-project task routing, fully isolated native memory, new runtime dependencies,
95
+ visual redesign, automatic merging and npm publication.
@@ -0,0 +1,67 @@
1
+ # Durable delivery and supervised operations
2
+
3
+ Status: Implemented on `feat/durable-delivery-operations`; release application pending
4
+ Date: 2026-09-20
5
+ Target: 0.7.0, protocol 10
6
+
7
+ ## Intent and acceptance
8
+
9
+ Make ahub suitable for supervised daily development: expose disconnected recipients' pending work, survive daemon termination without silently losing accepted queued messages, and make uncertain execution an explicit operator decision. Publish an English operations guide and verify the release before applying it to the current installation.
10
+
11
+ The user chose conservative recovery and deployment through production application. Never automatically replay uncertain or partially executed work. Exactly-once external effects, automatic rollback, and unattended native approval handling remain outside this release. Infrastructure prerequisites in issue #12 remain pending unless independently available; never manufacture quota exhaustion or disconnect the user's network.
12
+
13
+ ## Delivery model
14
+
15
+ - Journal delivery state in the private project SQLite database with WAL, synchronous FULL, instance fencing, and private file permissions.
16
+ - Commit fan-out recipients before acknowledging submission or dispatching any recipient. Preserve original envelopes and condensed-delivery mappings, IDs, priority/order, trace/hops, attempts, withdrawal state, prefaces and manual pauses.
17
+ - States: queued, dispatching, accepted, completed, needs_review, failed, discarded. Persist handoff before invoking native delivery/steering. Adapter acceptance is not execution completion.
18
+ - Recover queued messages automatically. Unsettled dispatching/accepted records become needs_review after an unplanned stop. Hold subsequent deliveries for that recipient until review is resolved.
19
+ - Automatic retry requires positive evidence that delivery was not accepted or execution caused no effects. Timeouts, disconnects, cancellation and partial effects are uncertain.
20
+ - Claude bridge acknowledgement only confirms bridge receipt. Socket writes, bridge submission and idle state never prove execution completion. Native completion or an explicit correlated reply may settle a receipt.
21
+ - Persist failure/overflow outcomes; retain unresolved records indefinitely and bound resolved history with the existing deduplication window. A persistence fault stops dispatch and rejects successful-submission claims.
22
+
23
+ ## Interfaces
24
+
25
+ - Peer delivery and steering receive an optional delivery ID. Adapters emit correlated accepted/completed/failed_safe/needs_review receipts separately from their state or answer.
26
+ - Queue-only recipients appear offline in status with pending, important, needs-review counts and oldest queued timestamp. Broadcast and automatic assignment remain attached-peer operations; explicit known offline recipients can queue work.
27
+ - Console commands: `ahub queue list [--peer <id>] [--json]`, `queue show <delivery-id>`, `queue resolve <delivery-id> --action completed|retry|discard --reason <text>`.
28
+ - Resolution uses an observed revision, records the operator assertion and reason, and is idempotent. Retry closes the old record and creates exactly one linked attempt. Stale or conflicting resolutions fail.
29
+ - New public status/recovery surfaces expose metadata; detailed console inspection follows existing private-envelope/PII redaction. No private body enters shared memory or public logs.
30
+
31
+ ## Recovery compatibility
32
+
33
+ Protocol 10 carries the receipt contract. The new coordinator supports authenticated protocol 9 sources and protocol 10 targets, plus protocol 10 restarts. Unsupported combinations are rejected before shutdown; legacy protocols use their matching documented bootstrap.
34
+
35
+ A legacy controlled-restart snapshot is imported exactly once into an uninitialized journal. An initialized journal remains authoritative; repeated starts and released snapshots cannot resurrect work. New snapshots carry the journal revision/state needed to validate preservation. Unsupported downgrade is refused before mutation.
36
+
37
+ Stage the verified 0.7.0 artifact and run its retained coordinator for the initial 0.6.4-to-0.7.0 transition. Promote the global CLI only after verification. Preserve existing queued Codex envelopes, tasks, pauses and native session identity. Do not start hwp-cli or rewrite the divergent ai-workspace history.
38
+
39
+ Recovery diagnostics distinguish missing session identity, terminal ownership uncertainty, confirmation pending and delivery reconciliation. Report the affected peer, verified terminal reference and next action; never fabricate receipts, bypass approval dialogs, or blindly create another terminal.
40
+
41
+ ## Documentation
42
+
43
+ Update README and quickstart, add an English operations guide covering installation, managed launchers, a two-agent task/review workflow, targeted messages and priorities, approvals, queue resolution, graceful shutdown, upgrades, crash recovery and partial effects. Record real measurements separately from synthetic tests. Remove stale version pins and outdated compatibility statements.
44
+
45
+ ## Verification
46
+
47
+ - Subprocess fault injection at durable enqueue, condensation, handoff, steering, acknowledgement and completion boundaries; only disposable processes may be killed.
48
+ - Ordering, fan-out independence, overflow, retry exhaustion, withdrawal, dedupe, manual pauses, storage failure, corruption, stale-instance callbacks and migration interruption.
49
+ - Idempotent resolution and snapshot import; no uncertain effect automatically repeated. Verify every adapter's receipt semantics, including silent completion and bridge failure.
50
+ - CLI/dashboard parity for queue-only peers and redaction/access-control checks.
51
+ - Full repository gate and Ubuntu/macOS CI, fresh-install documented workflow, real MLX/DGX/Kimi tests and attended Claude recovery.
52
+ - Actual 0.6.4 package transition to protocol 10, then production application with version, session, queue, task and budget readback and a read-only Pi probe.
53
+
54
+ ## Work items
55
+
56
+ - [x] Durable journal and bus recovery.
57
+ - [x] Adapter delivery receipts and safe failure classification.
58
+ - [x] Queue operator controls and status/dashboard visibility.
59
+ - [x] Cross-version recovery compatibility and actionable diagnostics.
60
+ - [x] English guides and recorded live evidence.
61
+ - [ ] Fault tests, independent review, CI, release and verified application.
62
+
63
+ ## Integration decision
64
+
65
+ The journal, adapter receipt contract and wire change are delivered in one coherent implementation PR, reviewed by subsystem. Shipping intermediate incompatible interfaces would leave a falsely durable runtime. The queue remains a durable snapshot until handoff; receipt rows retain handoff and resolution history. An operator retry atomically links a queued attempt and updates the bus snapshot.
66
+
67
+ An identical operator action/reason may be repeated with its original observed revision or the freshly read terminal revision. Both return the existing result without another audit transition or retry enqueue. Conflicting actions/reasons and unrelated stale revisions are refused.
@@ -0,0 +1,40 @@
1
+ # Pi with DGX and MLX
2
+
3
+ Issue: #25. Pi is a managed project peer for implementation, edits, tests, summaries and triage. Claude/Codex retain planning and final review. Existing local-worker PII ownership does not change.
4
+
5
+ ## Runtime boundary
6
+
7
+ - One managed Pi session owns each project, in headless RPC or native TUI mode.
8
+ - Mode changes preserve the session ID/file and stop the previous owner before starting a replacement.
9
+ - Only `agent_settled` completes a turn. Important chat can steer; task/review and private envelopes queue.
10
+ - Native tools, extension discovery, skills and prompt templates are disabled. The explicit hub extension delegates file, shell and task tools back to the daemon.
11
+ - The daemon retains existing path guards, sandbox and terminal approvals. A session/tool-call ledger records pending effects before execution and cached results afterward. Uncertain effects are never automatically replayed.
12
+ - Native process identities and bridge credentials are separate from model credentials. Pi receives only the relay token, not gateway credentials.
13
+
14
+ ## Inference
15
+
16
+ - `dgx/coding` and `dgx/fast` resolve through the project's existing OmniRoute configuration.
17
+ - `mlx/fast` resolves to the machine-shared loopback MLX server.
18
+ - Apple Silicon setup pins Python 3.12, `mlx-lm==0.31.3`, and `Qwen/Qwen3-8B-MLX-4bit` revision `383413e909f3bc5303ce195ebbdf0339c5a1a2a3`.
19
+ - The MLX input estimate is capped at 16,000 tokens; decode and prompt concurrency are one. DGX's default input estimate cap is 262,144 tokens.
20
+ - The relay authenticates every call, rejects browser origins and unknown model aliases, and preserves streamed bytes. It records the requested route and model reported by the backend.
21
+ - MLX may fall back to DGX before response streaming begins. Failed non-PII tasks can escalate to configured cloud peers with an explicit warning to reconcile partial effects.
22
+ - Runtime ownership includes process start identity. Project shutdown releases its relay, while explicit `ahub models stop` controls the shared MLX process.
23
+
24
+ ## Recovery and rollout
25
+
26
+ Protocol 9 carries Pi session/launch metadata. Supported controlled upgrades read protocol-8 source contracts and restore protocol-9 targets. Legacy protocols 5-7 still require a matching CLI and attended bootstrap. No active session is forcibly interrupted to satisfy a deployment deadline.
27
+
28
+ Public templates keep Pi disabled until the optional executable and model runtime are configured. Existing custom routing files remain authoritative and require explicit Pi class preferences when adopting this feature.
29
+
30
+ ## Verification
31
+
32
+ The release gate is `scripts/check.sh`. Live acceptance additionally requires Pi tool roundtrips through DGX and MLX, native/RPC session handover, and canonical runtime status readback. Unit tests alone do not prove operational cutover.
33
+
34
+ ## Empty session handover (#27)
35
+
36
+ Pi may report a session file before writing it. Before stopping an idle source,
37
+ the hub queries its live session state. Only a verified empty source with no
38
+ observed activity or persisted history may resume by exact session ID alone.
39
+ Persisted conversations retain their exact file; missing nonempty history blocks
40
+ handover. A failed target launch retains the verified resume identity for retry.
@@ -0,0 +1,151 @@
1
+ # Controlled upgrade and session recovery
2
+
3
+ Status: Implementation on `feat/upgrade-recovery`, issue #21
4
+ Date: 2026-09-20
5
+
6
+ ## Problem and decisions
7
+
8
+ Version 0.4.0 added project isolation but upgrading still required a separate
9
+ restart script. Queued messages and manual pauses lived only in memory, and
10
+ native terminal identity was not recorded. A successful package installation
11
+ therefore did not prove that conversations or pending work survived.
12
+
13
+ Protocol 8 adds a controlled-restart contract. Package installation, daemon
14
+ readiness, native conversation recovery and queue release are separately
15
+ verified. Existing protocols 5–7 require a manual maintenance bootstrap with
16
+ their matching CLI; the new coordinator never guesses a PID to terminate.
17
+
18
+ ## Commands
19
+
20
+ - `ahub upgrade --to <exact-version> --dry-run`: inspect the running registered
21
+ projects, release identity, native session mappings and blockers.
22
+ - `ahub upgrade --to <exact-version> [--yes]`: review the plan, revalidate it and
23
+ schedule an independent recovery process. `--yes` accepts the displayed scope;
24
+ it never overrides blockers.
25
+ - `ahub --project <path|id> restart [--dry-run] [--yes]`: recover one project using
26
+ the current package, without changing the global installation or plugin.
27
+ - `ahub recovery status|resume <operation-id>`: read redacted progress or resume
28
+ the recorded operation after checking live ownership.
29
+ - `ahub recovery abort <operation-id>`: cancel a preflight and release its lock
30
+ only before a runtime has stopped or a terminal mutation has been attempted.
31
+
32
+ Upgrade uses all affected running registrations. Stopped projects stay stopped.
33
+ An incompatible or unavailable active registration blocks apply. No global
34
+ installation, plugin changes, runtime shutdown or terminal mutation occurs in
35
+ dry-run. No lifecycle mutation is retried solely because a subprocess exited.
36
+
37
+ ## Runtime and coordinator
38
+
39
+ The daemon exposes console-only `recovery` RPCs: inspect, prepare, commit, abort
40
+ and release. Mutations are pinned to the expected instance and operation. Prepare
41
+ holds deliveries while current turns and approvals finish. A ten-minute source
42
+ preparation timeout aborts the hold without stopping the source. The replacement
43
+ remains held until explicit verified release.
44
+ Budget expiry, pause and handoff transitions are held with message delivery;
45
+ in-flight budget work must settle before commit. Deferred quota readings are
46
+ evaluated after release or abort, preserving hard-limit semantics.
47
+
48
+ Commit writes a private, atomic project snapshot of queued envelopes, IDs,
49
+ parents/hops, retries, dedupe state, prefaces, manual pauses and peer descriptors.
50
+ The existing task and budget databases remain authoritative. A mismatched or
51
+ corrupt snapshot blocks startup. Snapshot restoration precedes peer attachment;
52
+ release is idempotent and retires replayable snapshot state before delivery.
53
+ Release atomically moves that snapshot to a private per-operation archive before
54
+ lifting the hold. Archives retain evidence for an uncertain release and are never
55
+ automatically replayed. Preparation freezes the peer roster; a new unplanned peer
56
+ cannot attach until recovery ends.
57
+
58
+ The coordinator has an exclusive machine operation lock and an exclusive runner
59
+ claim. Lifecycle commands and the manager respect that lock. Package versions
60
+ are staged in a retained directory, with exact registry integrity and local
61
+ runtime digest checks. The coordinator runs from a preserved source tree so
62
+ replacing the global executable cannot kill its own recovery logic.
63
+ Recovery authority is removed from native agent and app-server child environments.
64
+ The reviewed fingerprint retains active-versus-offline peer membership, while
65
+ normalizing transient idle/busy/paused changes.
66
+
67
+ Projects transition sequentially. Source terminal ownership and idle state are
68
+ verified before closing only the captured terminals. New daemons restore with
69
+ delivery held, then Codex and hub-owned workers reconnect. The shared Claude
70
+ plugin is installed in a final common phase, followed by the original Claude
71
+ sessions. Verified projects release queues; the global CLI is promoted last.
72
+ There is no automatic rollback of projects that have resumed work.
73
+
74
+ An interrupted operation re-reads actual daemon identity, package digests,
75
+ terminal mappings and phase receipts. Lost commit/start replies can be reconciled.
76
+ Mutable receipts are reloaded under the exclusive runner claim, and saved terminal
77
+ bindings are checked again before release. An ordinary startup ignores a completed
78
+ operation ID inherited from a restored agent session.
79
+ An uncertain terminal creation is not repeated: the original session must be
80
+ found and verified, or the operation stays blocked for manual recovery.
81
+
82
+ ## Native session boundaries
83
+
84
+ - Codex resumes the exact captured thread, with authenticated adapter readback.
85
+ - Claude resumes the captured session after the shared plugin transition.
86
+ - Kimi/local start new sessions using preserved routing selection and task context.
87
+ - Orca terminal bindings use exact project, worktree, handle and incarnation.
88
+ Newly launched `ahub codex`/`ahub claude` wrappers record private ownership
89
+ metadata when Orca omits session identity. Only account-home references are
90
+ retained from their launch environment; credentials and arbitrary environment
91
+ values are excluded.
92
+ - Other terminals or unverifiable bindings are `manual-required`. No terminal
93
+ title, generic process-name match or shell-input guess authorizes a restart.
94
+
95
+ The initial coordinator supports protocol 8 source and target packages. A target
96
+ with another control protocol requires a compatible coordinator and is rejected
97
+ before shutdown. Fully general crash recovery and exactly-once external tool
98
+ effects are not claimed.
99
+
100
+ ## Verification and release
101
+
102
+ Test two-project ordering, queue and pause preservation, active turns/approvals,
103
+ changed identities, stale terminal handles, PID reuse, interrupted steps,
104
+ uncertain subprocess outcomes, corrupt snapshots and post-release normal use.
105
+ Run `scripts/check.sh` on macOS and Ubuntu. Fake services and isolated detached
106
+ CLI tests are distinct from an attended real Orca/native-account smoke.
107
+
108
+ Protocol 5–7 bootstrap, real account smoke, production deployment and npm
109
+ publication remain explicit release activities; this implementation does not
110
+ silently restart the developer's currently running project sessions.
111
+ The bootstrap includes an incompatible dashboard manager, which must also be
112
+ stopped with its matching CLI before a protocol-8 manager is launched.
113
+
114
+ ## Current 0.7.0 correction and recovery contract
115
+
116
+ The earlier sections record the protocol-8 implementation history. The current
117
+ release is 0.6.4 with protocol 9; the next target is 0.7.0 with protocol 10.
118
+ This correction supersedes the earlier protocol-8 source/target statement for
119
+ the planned transition while retaining that historical evidence.
120
+
121
+ The 0.7.0 coordinator accepts a verified protocol-9 source and starts a
122
+ protocol-10 target. It must stage the exact package, preserve the source
123
+ coordinator, and promote the global CLI only after the target has passed its
124
+ readback. The transition is sequential per project and uses native managed
125
+ launchers. Claude requires manual confirmation of the captured session before
126
+ resume; the coordinator must never create a second Claude terminal when
127
+ ownership is uncertain. Kimi and local-worker sessions may start fresh with
128
+ preserved routing and task context. hwp-cli is outside this transition.
129
+
130
+ Protocol 10 adds durable delivery receipts with the states
131
+ `queued`, `dispatching`, `accepted`, `completed`, `needs_review`, `failed`,
132
+ and `discarded`. An accepted bridge handoff is not task completion. An
133
+ unsettled `dispatching` or `accepted` record after an unplanned stop becomes
134
+ `needs_review`; timeout, disconnect, cancellation, and partial effects are
135
+ uncertain and are never replayed automatically. Queue resolution records the
136
+ observed revision and operator reason. A retry closes the old record and makes
137
+ one linked attempt; stale or conflicting resolutions fail.
138
+
139
+ The target console commands are `ahub queue list [--peer <id>] [--json]`,
140
+ `ahub queue show <delivery-id>`, and `ahub queue resolve <delivery-id>
141
+ --action completed|retry|discard --reason <text>`. They are a 0.7.0 target and
142
+ remain unavailable in the released 0.6.4 CLI until implementation and live
143
+ verification complete.
144
+
145
+ The documented verification sequence is: run a dry-run, verify package and
146
+ session identity, manually confirm Claude recovery, read back version, session,
147
+ queue, task, and budget state, then release the held queues. Do not claim a
148
+ successful 0.7.0 cutover from tests, synthetic approvals, or package
149
+ installation alone. Issue [#12](https://github.com/STAIxBWLB/agent-hub/issues/12)
150
+ remains open for off-campus Access credentials and a natural near-limit budget
151
+ pause.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-hub",
3
- "version": "0.6.4",
3
+ "version": "0.7.1",
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",
@@ -26,6 +26,7 @@
26
26
  "src",
27
27
  "plugins",
28
28
  "templates",
29
+ "docs",
29
30
  "README.md",
30
31
  "LICENSE",
31
32
  "CHANGELOG.md"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hub",
3
- "version": "0.6.4",
3
+ "version": "0.7.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,7 +15615,8 @@ function projectContext(cwd, env = process.env) {
15615
15615
  function stateDirFor(cwd) {
15616
15616
  return projectContext(cwd).stateDir;
15617
15617
  }
15618
- var PROTOCOL = 9;
15618
+ var PROTOCOL = 10;
15619
+ var RECOVERY_SOURCE_PROTOCOLS = [9, PROTOCOL];
15619
15620
  function readControl(stateDir) {
15620
15621
  try {
15621
15622
  const status = JSON.parse(readFileSync2(join2(stateDir, "status.json"), "utf8"));
@@ -15646,7 +15647,7 @@ class ControlClient {
15646
15647
  this.ws = ws;
15647
15648
  }
15648
15649
  static connect(stateDir, hello, timeoutMs = 3000, protocol = PROTOCOL) {
15649
- if (protocol !== 8 && protocol !== PROTOCOL)
15650
+ if (!RECOVERY_SOURCE_PROTOCOLS.includes(protocol))
15650
15651
  return Promise.reject(new Error(`unsupported recovery source protocol ${protocol}`));
15651
15652
  const control = readControl(stateDir);
15652
15653
  if (!control)
@@ -15726,7 +15727,7 @@ class ControlClient {
15726
15727
  // package.json
15727
15728
  var package_default = {
15728
15729
  name: "@staix/agent-hub",
15729
- version: "0.6.4",
15730
+ version: "0.7.1",
15730
15731
  description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
15731
15732
  license: "MIT",
15732
15733
  type: "module",
@@ -15752,6 +15753,7 @@ var package_default = {
15752
15753
  "src",
15753
15754
  "plugins",
15754
15755
  "templates",
15756
+ "docs",
15755
15757
  "README.md",
15756
15758
  "LICENSE",
15757
15759
  "CHANGELOG.md"
@@ -15879,7 +15881,7 @@ var inbox = [];
15879
15881
  var hub;
15880
15882
  var detached;
15881
15883
  var offline = () => detached ?? "hub is not running for this project (start it with: ahub up).";
15882
- async function push(envs) {
15884
+ async function push(envs, deliveryId) {
15883
15885
  const parent = replyParent(envs);
15884
15886
  const single = envs.length === 1;
15885
15887
  const content = single ? parent.body : envs.map((e) => `--- from ${e.from} (id ${e.id}, kind ${e.kind}) ---
@@ -15896,12 +15898,31 @@ ${sanitize(e.body)}`).join(`
15896
15898
  };
15897
15899
  try {
15898
15900
  await server.notification({ method: "notifications/claude/channel", params: { content, meta: meta2 } });
15901
+ if (deliveryId && hub) {
15902
+ try {
15903
+ const receipt = await hub.request({ t: "delivery_receipt", deliveryId, state: "accepted" });
15904
+ if (!receipt.ok)
15905
+ log(`delivery receipt rejected by hub: ${receipt.error}`);
15906
+ } catch (e) {
15907
+ log(`channel delivery accepted but receipt could not be sent: ${e.message}`);
15908
+ }
15909
+ }
15899
15910
  } catch (e) {
15900
- log(`channel push failed, queued for hub_inbox: ${e.message}`);
15901
- for (const env of envs)
15902
- inbox.push(frame(env));
15903
- while (inbox.length > INBOX_CAP)
15904
- inbox.shift();
15911
+ log(`channel push failed${deliveryId ? ", delivery requires review" : ", queued for hub_inbox"}: ${e.message}`);
15912
+ if (deliveryId) {
15913
+ if (hub) {
15914
+ const receipt = await hub.request({ t: "delivery_receipt", deliveryId, state: "needs_review", reason: e.message });
15915
+ if (!receipt.ok)
15916
+ log(`delivery receipt rejected by hub: ${receipt.error}`);
15917
+ } else {
15918
+ log(`channel push failed while hub was unavailable; delivery ${deliveryId} remains unresolved`);
15919
+ }
15920
+ } else {
15921
+ for (const env of envs)
15922
+ inbox.push(frame(env));
15923
+ while (inbox.length > INBOX_CAP)
15924
+ inbox.shift();
15925
+ }
15905
15926
  }
15906
15927
  }
15907
15928
  async function connectLoop() {
@@ -15918,7 +15939,7 @@ async function connectLoop() {
15918
15939
  peer: peerId,
15919
15940
  ...process.env.AGENTHUB_PROJECT_DIR ? { projectRoot: projectRoot2 } : {}
15920
15941
  });
15921
- client.onPush = (msg) => msg.t === "deliver" && void push(msg.envs ?? [msg.env]);
15942
+ client.onPush = (msg) => msg.t === "deliver" && void push(msg.envs ?? [msg.env], msg.deliveryId);
15922
15943
  hub = client;
15923
15944
  attempt = -1;
15924
15945
  standingBy = false;
@@ -52,6 +52,8 @@ export class AcpPeer extends BasePeer {
52
52
  private readonly toolInputs = new Map<string, unknown>();
53
53
  private primed = false;
54
54
  private turn = 0; // generation: a prompt cancelled by the watchdog must not touch the turn that followed it
55
+ private activeDeliveryId: string | undefined;
56
+ private deliveryAccepted = false;
55
57
 
56
58
  constructor(
57
59
  id: PeerId,
@@ -94,6 +96,8 @@ export class AcpPeer extends BasePeer {
94
96
  }
95
97
 
96
98
  async stop(): Promise<void> {
99
+ if (this.activeDeliveryId) this.delivery({ id: this.activeDeliveryId, state: "needs_review", reason: "ACP session stopped before settlement" });
100
+ this.activeDeliveryId = undefined;
97
101
  const proc = this.proc;
98
102
  if (!proc || proc.exitCode !== null) return;
99
103
  await stopOwnedProcess(proc);
@@ -101,38 +105,64 @@ export class AcpPeer extends BasePeer {
101
105
  }
102
106
 
103
107
  /** Resolves once the prompt is in flight; the turn result arrives on its own. */
104
- async deliver(envs: Envelope[]): Promise<void> {
105
- if (this.state !== "idle") throw new Error(`${this.id} is ${this.state}`);
108
+ async deliver(envs: Envelope[], deliveryId?: string): Promise<void> {
109
+ if (this.state !== "idle") {
110
+ if (deliveryId) this.delivery({ id: deliveryId, state: "failed_safe", reason: `${this.id} is ${this.state}` });
111
+ throw new Error(`${this.id} is ${this.state}`);
112
+ }
106
113
  const turn = ++this.turn;
114
+ this.activeDeliveryId = deliveryId;
115
+ this.deliveryAccepted = false;
107
116
  this.chunks = [];
108
117
  this.setState("busy");
109
118
  // session/prompt answers only when the turn ends, so deliver resolves now and failures come back through onFailed.
110
- this.request("session/prompt", { sessionId: this.sessionId, prompt: [{ type: "text", text: this.primed || !this.opts.preamble ? renderDigest(envs, this.primed) : `${this.opts.preamble}\n\n${renderDigest(envs, false)}` }] })
111
- .then(() => {
112
- this.primed = true;
119
+ const prompt = this.request("session/prompt", { sessionId: this.sessionId, prompt: [{ type: "text", text: this.primed || !this.opts.preamble ? renderDigest(envs, this.primed) : `${this.opts.preamble}\n\n${renderDigest(envs, false)}` }] });
120
+ prompt
121
+ .then((result) => {
113
122
  if (turn !== this.turn) return; // superseded: these chunks belong to a later turn
123
+ this.primed = true;
114
124
  const body = this.chunks.join("").trim();
115
125
  if (body) this.onMessage?.(body, { inReplyTo: replyParent(envs), to: replyAudience(envs) });
126
+ if (deliveryId && this.activeDeliveryId === deliveryId) {
127
+ this.acceptDelivery();
128
+ this.delivery({ id: deliveryId, state: result?.stopReason === "end_turn" ? "completed" : "needs_review", ...(result?.stopReason === "end_turn" ? {} : { reason: "ACP prompt ended without normal completion" }) });
129
+ }
116
130
  })
117
131
  .catch((e: Error) => {
118
132
  this.opts.log?.(`[${this.id}] prompt failed: ${e.message}`);
119
- this.onFailed?.(envs);
133
+ if (deliveryId && this.activeDeliveryId === deliveryId) this.delivery({ id: deliveryId, state: "needs_review", reason: e.message });
134
+ else if (!deliveryId) this.onFailed?.(envs);
120
135
  })
121
136
  .finally(() => {
122
137
  if (turn === this.turn && this.state === "busy") this.setState("idle");
138
+ if (turn === this.turn && this.activeDeliveryId === deliveryId) this.activeDeliveryId = undefined;
123
139
  });
124
140
  }
125
141
 
126
142
  protected override onWatchdog(): void {
143
+ const durable = !!this.activeDeliveryId;
144
+ if (this.activeDeliveryId) this.delivery({ id: this.activeDeliveryId, state: "needs_review", reason: "ACP turn watchdog timeout" });
145
+ this.activeDeliveryId = undefined;
127
146
  this.notify("session/cancel", { sessionId: this.sessionId });
128
147
  this.turn++; // whatever the cancelled prompt still reports is stale
129
- super.onWatchdog();
148
+ // ACP updates carry only a session ID, not a turn ID. After a durable turn times out,
149
+ // keep the adapter offline until it is restarted so late chunks cannot contaminate a new turn.
150
+ if (durable) this.setState("offline");
151
+ else super.onWatchdog();
152
+ }
153
+
154
+ private acceptDelivery(): void {
155
+ if (!this.activeDeliveryId || this.deliveryAccepted) return;
156
+ this.deliveryAccepted = true;
157
+ this.delivery({ id: this.activeDeliveryId, state: "accepted" });
130
158
  }
131
159
 
132
160
  private down(reason: string): void {
133
161
  this.opts.log?.(`[${this.id}] ${reason}`);
134
162
  for (const p of this.pending.values()) p.reject(new Error(reason));
135
163
  this.pending.clear();
164
+ if (this.activeDeliveryId) this.delivery({ id: this.activeDeliveryId, state: "needs_review", reason });
165
+ this.activeDeliveryId = undefined;
136
166
  this.setState("offline");
137
167
  }
138
168
 
@@ -146,6 +176,9 @@ export class AcpPeer extends BasePeer {
146
176
  if (this.state === "busy") this.touch();
147
177
 
148
178
  if (msg.method === "session/update") {
179
+ if (msg.params?.sessionId && msg.params.sessionId !== this.sessionId) return;
180
+ if (this.state !== "busy") return;
181
+ this.acceptDelivery();
149
182
  const u = msg.params?.update;
150
183
  if (u?.sessionUpdate === "agent_message_chunk" && u.content?.type === "text") this.chunks.push(u.content.text);
151
184
  // The permission request that follows may carry no `rawInput` (Kimi 2.0.1 does not), and then the
@@ -86,7 +86,7 @@ let detached: string | undefined; // why this server stopped reconnecting; tool
86
86
  const offline = () => detached ?? "hub is not running for this project (start it with: ahub up).";
87
87
 
88
88
  /** One delivery = one notification, because every notification can cost Claude a turn. */
89
- async function push(envs: Envelope[]): Promise<void> {
89
+ async function push(envs: Envelope[], deliveryId?: string): Promise<void> {
90
90
  const parent = replyParent(envs); // reply_to on this id keeps the hop count honest
91
91
  const single = envs.length === 1;
92
92
  const content = single ? parent.body : envs.map((e) => `--- from ${e.from} (id ${e.id}, kind ${e.kind}) ---\n${sanitize(e.body)}`).join("\n\n");
@@ -100,10 +100,27 @@ async function push(envs: Envelope[]): Promise<void> {
100
100
  };
101
101
  try {
102
102
  await server.notification({ method: "notifications/claude/channel", params: { content, meta } });
103
+ if (deliveryId && hub) {
104
+ try {
105
+ const receipt = await hub.request({ t: "delivery_receipt", deliveryId, state: "accepted" });
106
+ if (!receipt.ok) log(`delivery receipt rejected by hub: ${receipt.error}`);
107
+ } catch (e) {
108
+ log(`channel delivery accepted but receipt could not be sent: ${(e as Error).message}`);
109
+ }
110
+ }
103
111
  } catch (e) {
104
- log(`channel push failed, queued for hub_inbox: ${(e as Error).message}`);
105
- for (const env of envs) inbox.push(frame(env)); // same sanitized header as everywhere else
106
- while (inbox.length > INBOX_CAP) inbox.shift();
112
+ log(`channel push failed${deliveryId ? ", delivery requires review" : ", queued for hub_inbox"}: ${(e as Error).message}`);
113
+ if (deliveryId) {
114
+ if (hub) {
115
+ const receipt = await hub.request({ t: "delivery_receipt", deliveryId, state: "needs_review", reason: (e as Error).message });
116
+ if (!receipt.ok) log(`delivery receipt rejected by hub: ${receipt.error}`);
117
+ } else {
118
+ log(`channel push failed while hub was unavailable; delivery ${deliveryId} remains unresolved`);
119
+ }
120
+ } else {
121
+ for (const env of envs) inbox.push(frame(env)); // same sanitized header as everywhere else
122
+ while (inbox.length > INBOX_CAP) inbox.shift();
123
+ }
107
124
  }
108
125
  }
109
126
 
@@ -119,7 +136,7 @@ async function connectLoop(): Promise<void> {
119
136
  try {
120
137
  const client = await ControlClient.connect(stateDir, { role: toolsOnly ? "tools" : "peer", peer: peerId,
121
138
  ...(process.env.AGENTHUB_PROJECT_DIR ? { projectRoot } : {}) });
122
- client.onPush = (msg) => msg.t === "deliver" && void push(msg.envs ?? [msg.env]); // `env`: a daemon older than wire version 2
139
+ client.onPush = (msg) => msg.t === "deliver" && void push(msg.envs ?? [msg.env], msg.deliveryId); // `env`: a daemon older than wire version 2
123
140
  hub = client;
124
141
  attempt = -1;
125
142
  standingBy = false;