@staix/agent-hub 0.7.2 → 0.7.4

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
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.7.4
6
+
7
+ - A Claude session that never persisted a transcript (zero turns) no longer wedges an upgrade at restore: when the original transcript is absent, the coordinator accepts the fresh session the operator attached, because nothing was preserved and nothing is lost. Sessions with a transcript keep the strict identity check; Codex stays strict in all cases (#21).
8
+
9
+ ## 0.7.3
10
+
11
+ - 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).
12
+ - 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).
13
+
5
14
  ## 0.7.2
6
15
 
7
16
  - 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).
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.2, control protocol 10. Durable delivery records distinguish queued
7
+ Status: 0.7.4, 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.2 && ahub setup
31
+ bun add -g github:STAIxBWLB/agent-hub#v0.7.4 && ahub setup
32
32
  ```
33
33
 
34
34
  The installed commands remain `ahub` and `agent-hub`.
@@ -1,6 +1,6 @@
1
1
  # Operations guide
2
2
 
3
- This guide describes ahub 0.7.2 and control protocol 10. Live verification
3
+ This guide describes ahub 0.7.4 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
@@ -182,27 +182,27 @@ For an upgrade from 0.6.4 (protocol 9) or 0.7.0, use the patched coordinator wit
182
182
  replacing the global CLI prematurely. Run from the project directory:
183
183
 
184
184
  ```bash
185
- bunx --package @staix/agent-hub@0.7.2 ahub upgrade --to 0.7.2 --dry-run
186
- bunx --package @staix/agent-hub@0.7.2 ahub upgrade --to 0.7.2 --yes
185
+ bunx --package @staix/agent-hub@0.7.4 ahub upgrade --to 0.7.4 --dry-run
186
+ bunx --package @staix/agent-hub@0.7.4 ahub upgrade --to 0.7.4 --yes
187
187
  ```
188
188
 
189
189
  The coordinator verifies and retains the exact target package, preserves its
190
190
  own source, and promotes the global CLI only after restored projects pass
191
191
  readback. Do not use the old protocol-9 coordinator to target protocol 10.
192
192
 
193
- Once the installed CLI is 0.7.2, review the current project or all registered
193
+ Once the installed CLI is 0.7.4, review the current project or all registered
194
194
  projects first:
195
195
 
196
196
  ```bash
197
197
  ahub restart --dry-run
198
- ahub upgrade --to 0.7.2 --dry-run
198
+ ahub upgrade --to 0.7.4 --dry-run
199
199
  ```
200
200
 
201
201
  Apply only after reviewing the plan:
202
202
 
203
203
  ```bash
204
204
  ahub restart --yes
205
- ahub upgrade --to 0.7.2 --yes
205
+ ahub upgrade --to 0.7.4 --yes
206
206
  ahub recovery status <operation-id>
207
207
  ahub recovery resume <operation-id>
208
208
  ahub recovery abort <operation-id>
@@ -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.2
20
+ bun add -g github:STAIxBWLB/agent-hub#v0.7.4
21
21
  ahub setup
22
22
  ```
23
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staix/agent-hub",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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.2",
3
+ "version": "0.7.4",
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.2",
15730
+ version: "0.7.4",
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",
@@ -1,4 +1,5 @@
1
1
  import { cpSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync } from "node:fs";
2
+ import { homedir } from "node:os";
2
3
  import { join, resolve } from "node:path";
3
4
  import { randomUUID } from "node:crypto";
4
5
  import { ControlClient, PROTOCOL, RECOVERY_SOURCE_PROTOCOLS, readControl } from "../hub/control-client.ts";
@@ -13,6 +14,17 @@ import { refreshManager } from "../hub/manager.ts";
13
14
  export const PACKAGE_ROOT = resolve(import.meta.dir, "../..");
14
15
  const orcaExecutable = () => process.env.ORCA_CLI_COMMAND || (process.env.ORCA_DEV_REPO_ROOT ? "orca-dev" : process.platform === "linux" && !process.env.ORCA_TERMINAL_HANDLE ? "orca-ide" : "orca");
15
16
 
17
+ /**
18
+ * Claude Code persists a transcript at projects/<slug>/<sessionId>.jsonl under its config
19
+ * dir, creating the project directory only with the first persisted turn; the slug replaces
20
+ * every non-alphanumeric character of the project root with '-'.
21
+ */
22
+ function claudeTranscriptExists(binding: TerminalBinding): boolean {
23
+ const config = binding.launch.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
24
+ const slug = binding.projectRoot.replace(/[^a-zA-Z0-9]/g, "-");
25
+ return existsSync(join(config, "projects", slug, `${binding.sessionId}.jsonl`));
26
+ }
27
+
16
28
  function terminalOptions(run: RunCommand = runCommand): TerminalRecoveryOptions {
17
29
  return { orcaExecutable: orcaExecutable(), runner: async (args) => {
18
30
  const result = await run([orcaExecutable(), ...args], { timeoutMs: 610_000 });
@@ -224,13 +236,26 @@ export function makeRecoveryDriver(run: RunCommand = runCommand): RecoveryDriver
224
236
  }
225
237
  progress.terminals[key] = true; save(); continue;
226
238
  }
239
+ // A peer whose TUI already exited left nothing to close: the source reports it
240
+ // detached, and inventory silence is the same proof the pending-reconcile path
241
+ // accepts. No mutation is issued for a terminal that no longer exists (#21).
242
+ const attached = (await inspectRecovery(planned.project)).peers.find((item) => item.id === binding.peer);
243
+ if (attached?.state === "offline") {
244
+ const result = await run([orcaExecutable(), "terminal", "list", "--json"]);
245
+ const inventory = JSON.parse(result.stdout);
246
+ const rows = inventory.result?.terminals;
247
+ if (result.code !== 0 || inventory.ok !== true || !Array.isArray(rows) || inventory.result?.truncated ||
248
+ rows.some((t: any) => t.handle === binding.handle || t.incarnationId === binding.incarnationId)) {
249
+ throw new Error(`${binding.peer}: terminal close outcome needs manual reconciliation`);
250
+ }
251
+ progress.terminals[key] = true; save(); continue;
252
+ }
227
253
  const idle = await waitForIdle(binding, 600_000, terminalOptions(run));
228
254
  if (!idle.satisfied) throw new Error(`${binding.peer}: terminal is not verified idle; source retained`);
229
255
  const source = await inspectRecovery(planned.project);
230
256
  if (source.instanceId !== planned.source.instanceId || source.recovery?.operationId !== op.id || !source.recovery.ready) {
231
257
  throw new Error("source preparation expired or changed while waiting for the terminal; no terminal was closed");
232
- }
233
- // Refresh the same prepared lease immediately before the terminal effect. Preserve
258
+ } // Refresh the same prepared lease immediately before the terminal effect. Preserve
234
259
  // the original peer roster, including any terminal already closed in this operation.
235
260
  await control(planned.project, "prepare", op.id, planned.source.instanceId!);
236
261
  progress.terminals[key] = "pending"; save();
@@ -273,10 +298,20 @@ export function makeRecoveryDriver(run: RunCommand = runCommand): RecoveryDriver
273
298
  if (progress.terminals[key] === "pending") {
274
299
  const observed = await inspectRecovery(planned.project);
275
300
  const peer = observed.peers.find((p) => p.id === original.peer);
276
- if ((original.peer === "codex" ? peer?.threadId : peer?.sessionId) !== original.sessionId) {
277
- throw new Error(`${original.peer}: terminal creation outcome is uncertain; attach the original session manually, then resume`);
301
+ const attached = original.peer === "codex" ? peer?.threadId : peer?.sessionId;
302
+ let expected = original;
303
+ if (attached !== original.sessionId) {
304
+ // #21: a Claude session that never got a first turn has no transcript on disk,
305
+ // so `claude --resume S` can never succeed and nothing it stood for is lost by
306
+ // accepting the fresh attach. A session WITH a transcript keeps the strict
307
+ // identity check. Codex stays strict too: its rollout jsonl is written when the
308
+ // thread starts, so an unrestorable thread should not exist.
309
+ if (original.peer !== "claude" || !peer || peer.state === "offline" || !attached || claudeTranscriptExists(original)) {
310
+ throw new Error(`${original.peer}: terminal creation outcome is uncertain; attach the original session manually, then resume`);
311
+ }
312
+ expected = { ...original, sessionId: attached };
278
313
  }
279
- const existing = await revalidateTerminal(planned, progress, original, false);
314
+ const existing = await revalidateTerminal(planned, progress, expected, false);
280
315
  progress.terminals[key] = existing; save(); continue;
281
316
  }
282
317
  const entrypoint = join(op.targetRoot!, "src/cli/main.js");
package/src/hub/daemon.ts CHANGED
@@ -471,6 +471,11 @@ export async function startDaemon(opts: DaemonOptions) {
471
471
  return recoveryPeerSnapshot.every((saved) => {
472
472
  const now = current[saved.id];
473
473
  if (!now) return true; // a detached peer is checked by the coordinator before terminal close
474
+ // A peer that went offline detached; it is not a changed conversation (#21). It is
475
+ // compared again the moment it reattaches, and release refuses while a peer that was
476
+ // online at the snapshot is still offline, so skipping the comparison cannot hide an
477
+ // identity change - it only keeps a detach from wedging readiness forever.
478
+ if (now.state === "offline") return true;
474
479
  if (saved.threadId && saved.threadId !== now.threadId) return false;
475
480
  // Kimi/local rebuild a fresh worker with task context on the target; native
476
481
  // Claude and Pi sessions must keep their exact identities across restoration.