Compare commits

...
Author SHA1 Message Date
dev60-review 9aaa523cf4 fix(daemon): the provider switch reads a custom-path Claude's running effort by its launched executable
The switch's effort read now runs today's observer first, and only when that finds no Claude process, observes the
pane again with the exact executable this daemon launched there. The Claude adapter exposes that frozen executable
read-only. Every shape the observer already accepts is unchanged. The launched executable is in memory only, so after
a daemon restart, or for a launch this daemon didn't make, the effort stays unreadable with today's disclosed fallback.
2026-11-02 05:37:40 +00:00
dev60-review 6e606fb1fe test(daemon): a switch reads a custom-path Claude seat's running effort
Red at main: a managed launch runs a custom Claude's frozen realpath (argv[0] and the OS name are its version file,
not claude), so the provider switch's effort read finds no Claude process and the relaunch falls back to the saved
effort. The control cell, with no known launched executable, keeps today's fallback and passes on main.
2026-11-02 05:37:40 +00:00
3 changed files with 50 additions and 1 deletions

No files matched your search

@@ -92,6 +92,13 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
private listProcesses?: NativeProcessLister;
private readinessTimeoutMs?: number;
private autoLaunches = new Map<string, { binding: NodeBinding; token: string; freshInPlace?: boolean; executable?: string; scriptLaunch?: boolean; fingerprint?: string; process?: ClaudeLaunchedProcess }>();
/** The frozen executable of this daemon's managed launch now in `pane`, for read-only process observation. It's in
* memory only: after a daemon restart, or for a launch this daemon didn't make, there's none. */
launchedExecutable(nodeId: string, pane: string): { executable: string; scriptLaunch?: boolean } | null {
const launch = this.autoLaunches.get(nodeId);
if (!launch?.executable || (launch.binding.tmuxPane && launch.binding.tmuxPane !== pane)) return null;
return { executable: launch.executable, ...(launch.scriptLaunch ? { scriptLaunch: true } : {}) };
}
readonly claudeManagedLaunch?: ClaudeManagedLaunch;
private activityRelayPath: string | null;
private claudeHooksManifestPath: string | null;
+7 -1
View File
@@ -106,7 +106,13 @@ function claudeSeatOf(c: Context): ClaudeSeatRelaunch {
const saved = readSeatEffort(rigRepo.db, nodeId);
if (!tmux || !saved) return null;
const listProcesses = c.get("listProcesses" as never) as NativeProcessLister | undefined;
const running = await verifyClaudePaneRuntime({ target: pane, tmux, ...(listProcesses ? { listProcesses } : {}) });
const observe = (frozen?: { executable: string; scriptLaunch?: boolean }) => verifyClaudePaneRuntime({ target: pane, tmux,
...(listProcesses ? { listProcesses } : {}), ...(frozen ? { selectedExecutable: frozen.executable, scriptLaunch: frozen.scriptLaunch } : {}) });
// Today's recognised shapes first; then the exact executable this daemon launched in the pane (a custom path).
const claude = ((c.get("runtimeAdapters" as never) as Record<string, unknown> | undefined) ?? {})["claude-code"] as
{ launchedExecutable?: (nodeId: string, pane: string) => { executable: string; scriptLaunch?: boolean } | null } | undefined;
const frozen = claude?.launchedExecutable?.(nodeId, pane) ?? null;
const running = await observe() ?? (frozen ? await observe(frozen) : null);
if (!running) return null;
const emitted = emittedEffortFromArgv("claude-code", processArgv(running.process.command));
// A switch's relaunch is restore's resume from a snapshot taken now: its effort is the seat's explicit choice, else
@@ -159,6 +159,42 @@ describe("provider routes", () => {
expect(bad.status).toBe(400);
});
// A managed launch runs the frozen realpath of a custom-path Claude, so the process isn't named claude.
it.each([[true, "high"], [false, null]] as const)("a switch reads a custom-path Claude seat's running effort through its launched executable (known: %s)", async (known, expected) => {
const db = createFullTestDb();
try {
const repo = new RigRepository(db), registry = new SessionRegistry(db);
const rig = repo.createRig("fixture-rig");
const node = repo.addNode(rig.id, "dev.a", { runtime: "claude-code" });
db.prepare("UPDATE nodes SET effort = 'medium' WHERE id = ?").run(node.id);
const session = registry.registerSession(node.id, "dev-a@fixture-rig");
registry.updateStatus(session.id, "running");
registry.updateBinding(node.id, { attachmentType: "tmux", tmuxSession: "dev-a@fixture-rig", tmuxPane: "%101" });
const executable = "/opt/openrig-host-programs/078-host-native/2.1.316", startedAt = "Mon Nov 2 05:00:00 2026";
const listProcesses = async () => [
{ pid: 700, ppid: 1, pgid: 700, tpgid: 702, executableName: "bash", command: "-bash", startedAt },
{ pid: 702, ppid: 700, pgid: 702, tpgid: 702, executableName: "2.1.316", executablePath: executable,
command: `${executable} --effort high --session-id 9bab03df-f6e6-4ed3-8f3d-9a9efa3a79cc`, startedAt },
];
const claude = { launchedExecutable: (nodeId: string, pane: string) => known && nodeId === node.id && pane === "%101" ? { executable } : null };
let lent!: ClaudeSeatRelaunch;
const app = new Hono();
app.use("*", async (c, next) => {
for (const [key, value] of Object.entries({ rigRepo: repo, tmuxAdapter: { getPanePid: async () => 700 }, listProcesses,
runtimeAdapters: { "claude-code": claude },
providerService: { ...stubService, precheck: async (_i: unknown, _r: unknown, claudeSeat: ClaudeSeatRelaunch) => { lent = claudeSeat; return { safe: true }; } } })) {
c.set(key as never, value as never);
}
await next();
});
app.route("/api/provider", providerRoutes());
await app.request("/api/provider/precheck?seat=dev-a@fixture-rig&toAccount=work&tokenFile=%2Ftokens%2Fwork");
const effort = await lent.effortChange!(node.id, "%101");
if (expected) expect(effort).toEqual({ running: expected, relaunch: "medium", relaunchIsSeatChoice: false });
else expect(effort).toBeNull(); // today's disclosed fallback: the running effort couldn't be read
} finally { db.close(); }
});
it("a Claude seat's relaunch keeps its pane: restore launches only that seat from the snapshot in it, under the seat's lease, and its warnings come back", async () => {
const db = createFullTestDb();
try {