@staix/agent-hub 0.7.3 → 0.7.5
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 +8 -0
- package/README.md +2 -2
- package/docs/operations.md +9 -6
- package/docs/quickstart.md +1 -1
- package/docs/smoke.md +64 -0
- 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/cli/statusline-tee.ts +4 -1
- package/src/cli/upgrade-runtime.ts +32 -4
- package/src/hub/daemon.ts +27 -7
- package/src/hub/restart.ts +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.7.5
|
|
6
|
+
|
|
7
|
+
- A zero-turn Claude session no longer wedges an upgrade after restore either: the commit snapshot records whether the session ever persisted a transcript, coordinator verification re-derives persistence from the terminal binding exactly like the restore gate did, and daemon readiness tolerates a fresh session while the original transcript is absent (snapshots from older sources are re-derived from disk). A session with a transcript keeps the strict identity check in all three places. The status line tee now records the transcript path Claude reports (#64).
|
|
8
|
+
|
|
9
|
+
## 0.7.4
|
|
10
|
+
|
|
11
|
+
- 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).
|
|
12
|
+
|
|
5
13
|
## 0.7.3
|
|
6
14
|
|
|
7
15
|
- 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).
|
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.5, 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.5 && ahub setup
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
The installed commands remain `ahub` and `agent-hub`.
|
package/docs/operations.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Operations guide
|
|
2
2
|
|
|
3
|
-
This guide describes ahub 0.7.
|
|
3
|
+
This guide describes ahub 0.7.5 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.
|
|
186
|
-
bunx --package @staix/agent-hub@0.7.
|
|
185
|
+
bunx --package @staix/agent-hub@0.7.5 ahub upgrade --to 0.7.5 --dry-run
|
|
186
|
+
bunx --package @staix/agent-hub@0.7.5 ahub upgrade --to 0.7.5 --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.
|
|
193
|
+
Once the installed CLI is 0.7.5, 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.
|
|
198
|
+
ahub upgrade --to 0.7.5 --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.
|
|
205
|
+
ahub upgrade --to 0.7.5 --yes
|
|
206
206
|
ahub recovery status <operation-id>
|
|
207
207
|
ahub recovery resume <operation-id>
|
|
208
208
|
ahub recovery abort <operation-id>
|
|
@@ -219,6 +219,9 @@ package digest, terminal binding, and phase receipt. An uncertain terminal
|
|
|
219
219
|
creation is never repeated blindly. If Claude shows its development-channel confirmation screen, confirm it in
|
|
220
220
|
the captured terminal, then run `ahub recovery resume <operation-id>`; resume must not open a duplicate terminal.
|
|
221
221
|
Kimi and local sessions may start fresh with preserved routing and task context.
|
|
222
|
+
A Claude session that never persisted a transcript (zero turns) also starts fresh:
|
|
223
|
+
there is nothing to resume, so the upgrade accepts the new session id; a session
|
|
224
|
+
with a transcript must come back with its original id.
|
|
222
225
|
Previously stopped projects stay stopped; only the reviewed running projects
|
|
223
226
|
are upgraded.
|
|
224
227
|
|
package/docs/quickstart.md
CHANGED
package/docs/smoke.md
CHANGED
|
@@ -629,3 +629,67 @@ A source-build smoke on Apple silicon/macOS 27.0 with Ollama 0.34.2 verified:
|
|
|
629
629
|
The live harness is opt-in and is not called by the hermetic repository
|
|
630
630
|
check. `AHUB_SMOKE_PROJECT` selects a project configuration without starting
|
|
631
631
|
its daemon or replaying its queue. It deliberately forbids remote inference.
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
## 0.7.2-0.7.4 attended upgrade smoke (issue #21, 2026-09-27)
|
|
635
|
+
|
|
636
|
+
Attended registry package/plugin upgrade on a scratch project
|
|
637
|
+
(`~/ahub-smoke`, Orca-managed worktree) with claude, codex and kimi attached
|
|
638
|
+
to a 0.7.1 hub installed from npm. Pre-upgrade state: task #1 in_progress
|
|
639
|
+
(owner codex, reviewer claude), kimi manually paused with one important
|
|
640
|
+
envelope queued, live Claude session and live Codex thread captured by the
|
|
641
|
+
dry-run plan. This run closes the attended leg left open by the 0.7.1
|
|
642
|
+
cutover; it also exposed three recovery defects that became 0.7.3 and 0.7.4.
|
|
643
|
+
|
|
644
|
+
### What the runs proved
|
|
645
|
+
|
|
646
|
+
- The coordinator blocked safely, twice, when the Codex TUI detached
|
|
647
|
+
mid-prepare: "no terminal was closed", the 0.7.1 source kept running, and
|
|
648
|
+
the board, pause and queue were verified unchanged afterwards. Blocked
|
|
649
|
+
operations were resumable or cleanly resolvable once their cause was
|
|
650
|
+
removed.
|
|
651
|
+
- The fifth attempt (0.7.3 coordinator, codex offline at plan time) drove
|
|
652
|
+
prepare, commit, source stop, staged-target start and native peer restore
|
|
653
|
+
without operator intervention; the new daemon reported version 0.7.3 with
|
|
654
|
+
a new instanceId, kimi still paused with its queued envelope intact, and
|
|
655
|
+
the task board unchanged. Claude restoration paused at the documented
|
|
656
|
+
attended step (development-channel confirmation in the captured terminal),
|
|
657
|
+
then resumed.
|
|
658
|
+
- Final state after completion: daemon, CLI and plugin all 0.7.4
|
|
659
|
+
(pid 12483, new instanceId, protocol 10), claude idle, kimi idle with
|
|
660
|
+
queued 0. The important envelope queued at 03:16 on 0.7.1 was delivered
|
|
661
|
+
after resume and answered by kimi at 04:53 ("Received the upgrade
|
|
662
|
+
preservation check envelope"). Task #1 remains in_progress on the board.
|
|
663
|
+
|
|
664
|
+
### Defects found and fixed by this smoke
|
|
665
|
+
|
|
666
|
+
- A peer detaching between prepare and commit wedged the operation:
|
|
667
|
+
readiness never recovered, resume refused, abort refused. Fixed in 0.7.3
|
|
668
|
+
(PR #60): an offline peer no longer fails the identity comparison, and an
|
|
669
|
+
already-exited terminal closes as a no-op.
|
|
670
|
+
- A zero-turn Claude session (no transcript on disk) made restore fail with
|
|
671
|
+
"No conversation found" and wedged the pending-restore identity gate.
|
|
672
|
+
Fixed in 0.7.4 (PR #62): with the original transcript absent, a fresh
|
|
673
|
+
operator-attached session is accepted.
|
|
674
|
+
- The same zero-turn case still cannot pass the coordinator's verify step
|
|
675
|
+
or the daemon's release gate; tracked as issue #64. This operation was
|
|
676
|
+
completed manually (restart snapshot removed, daemon restarted), which is
|
|
677
|
+
an operator action, not a designed path.
|
|
678
|
+
|
|
679
|
+
### Environmental limits recorded
|
|
680
|
+
|
|
681
|
+
- An active native Codex TUI could not be kept alive on this machine: the
|
|
682
|
+
Orca runtime closed three `ahub codex` terminals in-process
|
|
683
|
+
(`origin: in-process`, `ptyKilled: true`) 35-90 seconds after each attach,
|
|
684
|
+
while the Claude terminal survived. The Codex leg therefore ran with
|
|
685
|
+
codex offline at plan time, as in the 0.7.1 cutover; this run does not
|
|
686
|
+
claim an active native Codex TUI restoration either.
|
|
687
|
+
- The recovery runner for an existing operation always executes the
|
|
688
|
+
operation's preserved source, so a coordinator fix released after an
|
|
689
|
+
operation was created does not reach that operation's resume. The
|
|
690
|
+
sourceRoot digest guard correctly refused a hand-patched receipt.
|
|
691
|
+
- `ahub setup` is interactive (one confirmation gates all steps); piping an
|
|
692
|
+
answer works, running it detached does not.
|
|
693
|
+
|
|
694
|
+
Issue [#12](https://github.com/STAIxBWLB/agent-hub/issues/12) remains open
|
|
695
|
+
for off-campus Access credentials and a natural near-limit budget pause.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-hub",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
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.5",
|
|
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",
|
|
@@ -25,7 +25,10 @@ try {
|
|
|
25
25
|
if (sessionId && dir) {
|
|
26
26
|
mkdirSync(dir, { recursive: true });
|
|
27
27
|
const file = join(dir, "claude-session.json");
|
|
28
|
-
|
|
28
|
+
// The transcript path lets the hub tell a zero-turn session (file not yet created, so
|
|
29
|
+
// `claude --resume` can never work) from one whose conversation must be preserved (#64).
|
|
30
|
+
const transcriptPath = typeof parsed.transcript_path === "string" && parsed.transcript_path ? parsed.transcript_path : undefined;
|
|
31
|
+
writeFileSync(`${file}.tmp`, JSON.stringify({ at: Date.now(), sessionId, ...(transcriptPath ? { transcriptPath } : {}), ...(process.env.AGENTHUB_INSTANCE_ID ? { instanceId: process.env.AGENTHUB_INSTANCE_ID } : {}), ...(process.env.AGENTHUB_LAUNCH_ID ? { launchId: process.env.AGENTHUB_LAUNCH_ID } : {}) }), { mode: 0o600 });
|
|
29
32
|
chmodSync(`${file}.tmp`, 0o600);
|
|
30
33
|
renameSync(`${file}.tmp`, file);
|
|
31
34
|
}
|
|
@@ -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 });
|
|
@@ -286,10 +298,20 @@ export function makeRecoveryDriver(run: RunCommand = runCommand): RecoveryDriver
|
|
|
286
298
|
if (progress.terminals[key] === "pending") {
|
|
287
299
|
const observed = await inspectRecovery(planned.project);
|
|
288
300
|
const peer = observed.peers.find((p) => p.id === original.peer);
|
|
289
|
-
|
|
290
|
-
|
|
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 };
|
|
291
313
|
}
|
|
292
|
-
const existing = await revalidateTerminal(planned, progress,
|
|
314
|
+
const existing = await revalidateTerminal(planned, progress, expected, false);
|
|
293
315
|
progress.terminals[key] = existing; save(); continue;
|
|
294
316
|
}
|
|
295
317
|
const entrypoint = join(op.targetRoot!, "src/cli/main.js");
|
|
@@ -330,7 +352,13 @@ export function makeRecoveryDriver(run: RunCommand = runCommand): RecoveryDriver
|
|
|
330
352
|
const peer = live.peers.find((p) => p.id === old.id);
|
|
331
353
|
if (!peer || !["idle", "busy", "paused"].includes(peer.state)) throw new Error(`${old.id}: peer reattachment not verified`);
|
|
332
354
|
if (old.id === "codex" && peer.threadId !== old.threadId) throw new Error("Codex resumed a different conversation");
|
|
333
|
-
if (old.id === "claude" && peer.sessionId !== old.sessionId)
|
|
355
|
+
if (old.id === "claude" && peer.sessionId !== old.sessionId) {
|
|
356
|
+
// #64: the restore gate already accepted a fresh session when the original never
|
|
357
|
+
// persisted a transcript. Re-derive persistence from the binding exactly like that
|
|
358
|
+
// gate did, so a first turn landing after the plan was made keeps the check strict.
|
|
359
|
+
const binding = (planned.terminals as TerminalBinding[]).find((t) => t.peer === "claude");
|
|
360
|
+
if (!binding || binding.sessionId !== old.sessionId || claudeTranscriptExists(binding)) throw new Error("Claude resumed a different conversation");
|
|
361
|
+
}
|
|
334
362
|
if (old.id === "pi" && (peer.sessionId !== old.sessionId || (old.sessionFile && peer.sessionFile !== old.sessionFile))) throw new Error("Pi resumed a different conversation");
|
|
335
363
|
}
|
|
336
364
|
for (const original of planned.terminals as TerminalBinding[]) {
|
package/src/hub/daemon.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
4
5
|
import type { ServerWebSocket } from "bun";
|
|
5
6
|
import { AcpPeer, type PermissionRequest } from "../adapters/acp.ts";
|
|
6
7
|
import { CodexPeer } from "../adapters/codex-appserver.ts";
|
|
@@ -480,27 +481,44 @@ export async function startDaemon(opts: DaemonOptions) {
|
|
|
480
481
|
// Kimi/local rebuild a fresh worker with task context on the target; native
|
|
481
482
|
// Claude and Pi sessions must keep their exact identities across restoration.
|
|
482
483
|
const freshWorker = recoveryPhase === "restored" && (saved.id === "kimi" || saved.id === "local");
|
|
483
|
-
if (!freshWorker && saved.sessionId && saved.sessionId !== now.sessionId)
|
|
484
|
+
if (!freshWorker && saved.sessionId && saved.sessionId !== now.sessionId) {
|
|
485
|
+
// #64: a Claude session that never persisted a transcript cannot be resumed, so the
|
|
486
|
+
// restore gate accepted a fresh session and nothing it stood for is lost. Snapshots
|
|
487
|
+
// written before this flag existed are re-derived from disk.
|
|
488
|
+
const unpersistedClaude = saved.id === "claude" && (saved.sessionPersisted === false || (saved.sessionPersisted === undefined && !claudeTranscriptPersisted(saved.sessionId)));
|
|
489
|
+
if (!unpersistedClaude) return false;
|
|
490
|
+
}
|
|
484
491
|
return true;
|
|
485
492
|
});
|
|
486
493
|
};
|
|
487
|
-
const
|
|
494
|
+
const claudeSession = (): { sessionId?: string; transcriptPath?: string } => {
|
|
488
495
|
try {
|
|
489
496
|
const value = JSON.parse(readFileSync(join(opts.stateDir, "claude-session.json"), "utf8"));
|
|
490
|
-
if (value.instanceId !== instanceId) return
|
|
497
|
+
if (value.instanceId !== instanceId) return {};
|
|
491
498
|
try {
|
|
492
499
|
const records = JSON.parse(readFileSync(join(opts.stateDir, "terminal-recovery.json"), "utf8"));
|
|
493
500
|
const current = Array.isArray(records) ? records.find((row) => row?.peer === "claude" && row?.projectRoot === opts.cwd && row?.instanceId === instanceId) : undefined;
|
|
494
|
-
if (current?.launchId && value.launchId !== current.launchId) return
|
|
501
|
+
if (current?.launchId && value.launchId !== current.launchId) return {};
|
|
495
502
|
} catch { /* no managed terminal record: the instance fence is still enforced */ }
|
|
496
|
-
return
|
|
497
|
-
|
|
503
|
+
return {
|
|
504
|
+
...(typeof value.sessionId === "string" && value.sessionId ? { sessionId: value.sessionId } : {}),
|
|
505
|
+
...(typeof value.transcriptPath === "string" && value.transcriptPath ? { transcriptPath: value.transcriptPath } : {}),
|
|
506
|
+
};
|
|
507
|
+
} catch { return {}; }
|
|
508
|
+
};
|
|
509
|
+
/** Claude persists projects/<slug>/<sessionId>.jsonl only with the first turn. Prefer the
|
|
510
|
+
* path Claude itself reported through the status line; fall back to the slug computation. */
|
|
511
|
+
const claudeTranscriptPersisted = (sessionId: string): boolean => {
|
|
512
|
+
const reported = claudeSession();
|
|
513
|
+
if (reported.sessionId === sessionId && reported.transcriptPath) return existsSync(reported.transcriptPath);
|
|
514
|
+
const config = process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
515
|
+
return existsSync(join(config, "projects", opts.cwd.replace(/[^a-zA-Z0-9]/g, "-"), `${sessionId}.jsonl`));
|
|
498
516
|
};
|
|
499
517
|
const recoveryPeers = (): Record<string, RestartPeerSnapshot> => Object.fromEntries([...bus.peers].map(([id, peer]) => {
|
|
500
518
|
const metadata = peer.recoveryMetadata?.() ?? {};
|
|
501
519
|
const row: RestartPeerSnapshot = { id, state: peer.state, queueIds: bus.queueIds(id), ...(metadata.launch ? { launch: metadata.launch as Record<string, unknown> } : {}) };
|
|
502
520
|
if (typeof metadata.threadId === "string") row.threadId = metadata.threadId;
|
|
503
|
-
const sessionId = id === "claude" ?
|
|
521
|
+
const sessionId = id === "claude" ? claudeSession().sessionId : metadata.sessionId;
|
|
504
522
|
if (typeof sessionId === "string" && sessionId) row.sessionId = sessionId;
|
|
505
523
|
return [id, row];
|
|
506
524
|
}));
|
|
@@ -991,6 +1009,8 @@ export async function startDaemon(opts: DaemonOptions) {
|
|
|
991
1009
|
const peers = (recoveryPeerSnapshot ?? Object.values(currentPeers)).map((saved) => ({
|
|
992
1010
|
...saved,
|
|
993
1011
|
queueIds: bus.queueIds(saved.id),
|
|
1012
|
+
// Recorded at commit, not prepare: a first turn landing in between flips it (#64).
|
|
1013
|
+
...(saved.id === "claude" && saved.sessionId ? { sessionPersisted: claudeTranscriptPersisted(saved.sessionId) } : {}),
|
|
994
1014
|
}));
|
|
995
1015
|
const snapshot: RestartSnapshot = {
|
|
996
1016
|
schemaVersion: 1,
|
package/src/hub/restart.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface RestartPeerSnapshot {
|
|
|
18
18
|
/** Safe native correlation metadata. Never include prompt/message text here. */
|
|
19
19
|
threadId?: string;
|
|
20
20
|
sessionId?: string;
|
|
21
|
+
/** Claude only: false when the session never persisted a transcript (zero turns), so it
|
|
22
|
+
* cannot be resumed and a fresh session is accepted instead. Absent in snapshots written
|
|
23
|
+
* before this flag; those are re-derived from disk (#64). */
|
|
24
|
+
sessionPersisted?: boolean;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export interface RestartSnapshot {
|