refactor: isolate tmux runner implementation

This commit is contained in:
pi
2026-04-10 23:55:19 +01:00
parent 29a77c6839
commit b4ed6886c4
4 changed files with 80 additions and 44 deletions

View File

@@ -1,44 +1,9 @@
export function createTmuxSingleRunner(deps: {
assertInsideTmux(): void;
getCurrentWindowId: () => Promise<string>;
createArtifacts: (cwd: string, meta: Record<string, unknown>) => Promise<any>;
buildWrapperCommand: (metaPath: string) => string;
createPane: (input: { windowId: string; cwd: string; command: string }) => Promise<string>;
monitorRun: (input: { eventsPath: string; resultPath: string; onEvent?: (event: any) => void }) => Promise<any>;
killPane: (paneId: string) => Promise<void>;
}) {
return async function runSingleTask(input: {
cwd: string;
meta: Record<string, unknown>;
onEvent?: (event: any) => void;
}) {
deps.assertInsideTmux();
import type { SubagentRunResult } from "./schema.ts";
const artifacts = await deps.createArtifacts(input.cwd, input.meta);
const windowId = await deps.getCurrentWindowId();
const command = deps.buildWrapperCommand(artifacts.metaPath);
const paneId = await deps.createPane({ windowId, cwd: input.cwd, command });
try {
const result = await deps.monitorRun({
eventsPath: artifacts.eventsPath,
resultPath: artifacts.resultPath,
onEvent: input.onEvent,
});
return {
...result,
runId: result.runId ?? artifacts.runId,
paneId,
windowId,
sessionPath: result.sessionPath ?? artifacts.sessionPath,
stdoutPath: result.stdoutPath ?? artifacts.stdoutPath,
stderrPath: result.stderrPath ?? artifacts.stderrPath,
resultPath: artifacts.resultPath,
eventsPath: artifacts.eventsPath,
};
} finally {
await deps.killPane(paneId);
}
};
export interface RunSingleTaskInput {
cwd: string;
meta: Record<string, unknown>;
onEvent?: (event: any) => void;
}
export type RunSingleTask = (input: RunSingleTaskInput) => Promise<SubagentRunResult>;

View File

@@ -1,6 +1,6 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createTmuxSingleRunner } from "./runner.ts";
import { createTmuxSingleRunner } from "./tmux-runner.ts";
test("createTmuxSingleRunner always kills the pane after monitor completion", async () => {
const killed: string[] = [];
@@ -31,3 +31,30 @@ test("createTmuxSingleRunner always kills the pane after monitor completion", as
assert.equal(result.finalText, "done");
assert.deepEqual(killed, ["%9"]);
});
test("createTmuxSingleRunner surfaces explicit tmux precondition errors", async () => {
const runSingleTask = createTmuxSingleRunner({
assertInsideTmux() {
throw new Error("tmux-backed subagents require pi to be running inside tmux.");
},
getCurrentWindowId: async () => "@1",
createArtifacts: async () => ({
metaPath: "/tmp/meta.json",
runId: "run-1",
eventsPath: "/tmp/events.jsonl",
resultPath: "/tmp/result.json",
sessionPath: "/tmp/child-session.jsonl",
stdoutPath: "/tmp/stdout.log",
stderrPath: "/tmp/stderr.log",
}),
buildWrapperCommand: () => "'node' '/wrapper.mjs' '/tmp/meta.json'",
createPane: async () => "%9",
monitorRun: async () => ({ finalText: "done", exitCode: 0 }),
killPane: async () => {},
});
await assert.rejects(
() => runSingleTask({ cwd: "/repo", meta: { task: "inspect auth" } as any }),
/tmux-backed subagents require pi to be running inside tmux/,
);
});

44
src/tmux-runner.ts Normal file
View File

@@ -0,0 +1,44 @@
export function createTmuxSingleRunner(deps: {
assertInsideTmux(): void;
getCurrentWindowId: () => Promise<string>;
createArtifacts: (cwd: string, meta: Record<string, unknown>) => Promise<any>;
buildWrapperCommand: (metaPath: string) => string;
createPane: (input: { windowId: string; cwd: string; command: string }) => Promise<string>;
monitorRun: (input: { eventsPath: string; resultPath: string; onEvent?: (event: any) => void }) => Promise<any>;
killPane: (paneId: string) => Promise<void>;
}) {
return async function runSingleTask(input: {
cwd: string;
meta: Record<string, unknown>;
onEvent?: (event: any) => void;
}) {
deps.assertInsideTmux();
const artifacts = await deps.createArtifacts(input.cwd, input.meta);
const windowId = await deps.getCurrentWindowId();
const command = deps.buildWrapperCommand(artifacts.metaPath);
const paneId = await deps.createPane({ windowId, cwd: input.cwd, command });
try {
const result = await deps.monitorRun({
eventsPath: artifacts.eventsPath,
resultPath: artifacts.resultPath,
onEvent: input.onEvent,
});
return {
...result,
runId: result.runId ?? artifacts.runId,
paneId,
windowId,
sessionPath: result.sessionPath ?? artifacts.sessionPath,
stdoutPath: result.stdoutPath ?? artifacts.stdoutPath,
stderrPath: result.stderrPath ?? artifacts.stderrPath,
resultPath: artifacts.resultPath,
eventsPath: artifacts.eventsPath,
};
} finally {
await deps.killPane(paneId);
}
};
}