From c4bb5ca9236614cb05c4670a85450f6ea793c4d4 Mon Sep 17 00:00:00 2001 From: Nathan Herald Date: Mon, 13 Jul 2026 05:43:27 +0200 Subject: [PATCH] launch: bake --root into the ding command line (durable ST_ROOT fix) dingCommand() now takes an optional network root and, when set, emits st ding --identity --root (smalltalk #85's new flag) instead of relying on ST_ROOT in the session env alone. writePtyToml passes spec.networkRoot through. Why: a ding launched with ST_ROOT unset defaults to st's install root (~/.local/state/smalltalk), not the convoy network root -- it then watches the wrong inbox, causing the fleet phantom-poke loops + non-delivery. The env carried ST_ROOT, but a pty-restart replays the STORED command and could drop the env for pre-fix daemons. Putting --root in the command line makes it restart-proof: the root travels with the command pty replays. ST_ROOT stays in env too (belt-and-suspenders). No-root specs are unchanged (no flag; falls back to env/default). tsc clean, 147 tests green (adds dingCommand --root + writePtyToml networkRoot coverage). HOLD: do NOT merge until smalltalk #85 (st ding --root) is in main AND the box's st binary carries it -- emitting --root against an older st would fail on an unknown flag. Pairs with the fleet ding re-launch (cos's go). --- src/launch.test.ts | 24 ++++++++++++++++++++++++ src/launch.ts | 11 +++++++---- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/launch.test.ts b/src/launch.test.ts index c521d69..02f93a1 100644 --- a/src/launch.test.ts +++ b/src/launch.test.ts @@ -35,6 +35,14 @@ describe("native launch command builders (cold-start boot-prompt)", () => { it("dingCommand: st ding --identity ", () => { expect(dingCommand("convoy-claude", "silber.convoy")).toBe("st ding silber.convoy --identity convoy-claude"); }); + + it("dingCommand: bakes --root into the command line when a network root is given (restart-proof)", () => { + expect(dingCommand("convoy-claude", "silber.convoy", "/Users/x/.local/state/convoy")).toBe( + "st ding silber.convoy --identity convoy-claude --root /Users/x/.local/state/convoy", + ); + // no root → no flag (unchanged behavior; falls back to ST_ROOT env / install default) + expect(dingCommand("convoy-claude", "silber.convoy", null)).toBe("st ding silber.convoy --identity convoy-claude"); + }); }); describe("writePtyToml (pinned hostname-prefixed ids, cold start)", () => { @@ -69,6 +77,22 @@ describe("writePtyToml (pinned hostname-prefixed ids, cold start)", () => { } }); + it("networkRoot bakes --root into the ding command line only (not the harness session)", () => { + const dir = mkdtempSync(join(tmpdir(), "convoy-ptytoml-root-")); + try { + writePtyToml(dir, spec({ networkRoot: "/net/convoy" })); + const toml = readFileSync(join(dir, "pty.toml"), "utf8"); + // ding command carries --root so a pty-restart can't drop the root + expect(toml).toContain("st ding silber.convoy --identity convoy-claude --root /net/convoy"); + // --root is a ding-only concern; the harness (claude) command must not get it + expect(toml).not.toContain("exec claude --permission-mode bypassPermissions --root"); + // env still carries ST_ROOT too (belt-and-suspenders) + expect(toml).toContain('ST_ROOT = "/net/convoy"'); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("--config-dir sets CLAUDE_CONFIG_DIR on the HARNESS session env only, not the ding sidecar", () => { const dir = mkdtempSync(join(tmpdir(), "convoy-ptytoml-cfg-")); try { diff --git a/src/launch.ts b/src/launch.ts index 169cb19..e4d5c6d 100644 --- a/src/launch.ts +++ b/src/launch.ts @@ -109,9 +109,12 @@ export function harnessCommand(harness: Harness, permissionMode: string, prompt: /** The ding sidecar command — pokes the agent's claude session when its bus inbox gets mail. Points at * the stable session id (`st ding --identity `). `st ding` stays a - * smalltalk runtime binary. */ -export function dingCommand(busId: string, claudeSessionId: string): string { - return `st ding ${claudeSessionId} --identity ${busId}`; + * smalltalk runtime binary. When a network `root` is given we bake `--root ` (smalltalk #85) into + * the command line — NOT just the env — so a `pty restart` (which replays the stored command) can never + * drop it and silently fall back to st's install-default root (the fleet phantom-poke/non-delivery bug). */ +export function dingCommand(busId: string, claudeSessionId: string, root?: string | null): string { + const rootFlag = root ? ` --root ${root}` : ""; + return `st ding ${claudeSessionId} --identity ${busId}${rootFlag}`; } /** Serialize the per-agent pty.toml (pty's manifest format — NOT a convoy.toml). Pins the session ids @@ -145,7 +148,7 @@ export function writePtyToml(dir: string, spec: AgentSpec): void { ? { ding: { id: dingId, - command: dingCommand(busId, harnessId), + command: dingCommand(busId, harnessId, root), tags: { role: "ding", ...(permanent ? { strategy: "permanent" } : {}), ...stTag }, env, },