Skip to content
This repository was archived by the owner on Jul 24, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ describe("native launch command builders (cold-start boot-prompt)", () => {
it("dingCommand: st ding <claude-session-id> --identity <bus-id>", () => {
expect(dingCommand("convoy-claude", "silber.convoy")).toBe("st ding silber.convoy --identity convoy-claude");
});

it("dingCommand: bakes --root <net> 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)", () => {
Expand Down Expand Up @@ -69,6 +77,22 @@ describe("writePtyToml (pinned hostname-prefixed ids, cold start)", () => {
}
});

it("networkRoot bakes --root <net> 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 {
Expand Down
11 changes: 7 additions & 4 deletions src/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <prefix.agentShort> --identity <bus-id>`). `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 <net>` (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
Expand Down Expand Up @@ -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,
},
Expand Down