sf snapshot: pre-dispatch, uncommitted changes after 97m inactivity

This commit is contained in:
Mikael Hugo 2026-04-30 15:11:45 +02:00
parent b26dca40ec
commit 8677e73046
24 changed files with 322 additions and 80 deletions

View file

@ -166,7 +166,8 @@
"@singularity-forge/engine-linux-x64-gnu": ">=2.10.2",
"@singularity-forge/engine-win32-x64-msvc": ">=2.10.2",
"fsevents": "~2.3.3",
"koffi": "^2.9.0"
"koffi": "^2.9.0",
"vectordrive": "^0.1.35"
},
"overrides": {
"gaxios": "^6.7.1"

View file

@ -100,6 +100,110 @@ describe("agent-loop — pauseTurn handling (#2869)", () => {
});
});
describe("agent-loop — steering during tool batches", () => {
it("defers queued steering until after the current tool batch when configured", async () => {
const calls: string[] = [];
const tool = {
name: "record",
label: "Record",
description: "Record a value",
parameters: Type.Object({ value: Type.String() }),
execute: async (_id: string, args: { value: string }) => {
calls.push(args.value);
return {
content: [{ type: "text" as const, text: `recorded ${args.value}` }],
details: {},
};
},
} satisfies AgentTool<{ value: string }>;
const first = makeAssistantMessage({
content: [
{
type: "toolCall",
id: "tc-1",
name: "record",
arguments: { value: "one" },
},
{
type: "toolCall",
id: "tc-2",
name: "record",
arguments: { value: "two" },
},
],
stopReason: "toolUse",
});
const second = makeAssistantMessage({
content: [{ type: "text", text: "saw steering" }],
stopReason: "stop",
});
const mockStream = createMockStreamFn([first, second]);
let steeringPolls = 0;
const steering: AgentMessage = {
role: "user",
content: [{ type: "text", text: "do not interrupt" }],
timestamp: Date.now(),
};
const context: AgentContext = {
systemPrompt: "You are a test agent.",
messages: [
{
role: "user",
content: [{ type: "text", text: "record values" }],
timestamp: Date.now(),
},
],
tools: [tool],
};
const config: AgentLoopConfig = {
model: TEST_MODEL,
convertToLlm: (msgs) => msgs.filter((m): m is any => m.role !== "custom"),
toolExecution: "sequential",
interruptToolExecutionOnSteering: false,
getSteeringMessages: async () => {
steeringPolls += 1;
return steeringPolls === 1 ? [steering] : [];
},
};
const stream = agentLoop(
[
{
role: "user",
content: [{ type: "text", text: "record values" }],
timestamp: Date.now(),
},
],
context,
config,
undefined,
mockStream as any,
);
const events = await collectEvents(stream);
const skipped = events.filter(
(event) =>
event.type === "tool_execution_end" &&
JSON.stringify(event.result.content).includes(
"Skipped due to queued user message",
),
);
assert.deepEqual(calls, ["one", "two"]);
assert.equal(skipped.length, 0);
assert.ok(
events.some(
(event) =>
event.type === "message_start" && event.message === steering,
),
"queued steering should still be delivered after the tool batch",
);
});
});
/**
* Regression tests for #2783: Stuck-loop on execute-task tool-call schema
* overload causes unbounded retry + budget burn.

View file

@ -517,6 +517,7 @@ async function executeToolCallsSequential(
const results: ToolResultMessage[] = [];
let steeringMessages: AgentMessage[] | undefined;
let preparationErrorCount = 0;
const interruptOnSteering = config.interruptToolExecutionOnSteering !== false;
for (let index = 0; index < toolCalls.length; index++) {
const toolCall = toolCalls[index];
@ -551,12 +552,14 @@ async function executeToolCallsSequential(
if (config.getSteeringMessages) {
const steering = await config.getSteeringMessages();
if (steering.length > 0) {
steeringMessages = steering;
const remainingCalls = toolCalls.slice(index + 1);
for (const skipped of remainingCalls) {
results.push(skipToolCall(skipped, stream));
steeringMessages = [...(steeringMessages ?? []), ...steering];
if (interruptOnSteering) {
const remainingCalls = toolCalls.slice(index + 1);
for (const skipped of remainingCalls) {
results.push(skipToolCall(skipped, stream));
}
break;
}
break;
}
}
}
@ -576,6 +579,7 @@ async function executeToolCallsParallel(
const runnableCalls: PreparedToolCall[] = [];
let steeringMessages: AgentMessage[] | undefined;
let preparationErrorCount = 0;
const interruptOnSteering = config.interruptToolExecutionOnSteering !== false;
for (let index = 0; index < toolCalls.length; index++) {
const toolCall = toolCalls[index];
@ -599,15 +603,17 @@ async function executeToolCallsParallel(
if (config.getSteeringMessages) {
const steering = await config.getSteeringMessages();
if (steering.length > 0) {
steeringMessages = steering;
for (const runnable of runnableCalls) {
results.push(skipToolCall(runnable.toolCall, stream, { emitStart: false }));
steeringMessages = [...(steeringMessages ?? []), ...steering];
if (interruptOnSteering) {
for (const runnable of runnableCalls) {
results.push(skipToolCall(runnable.toolCall, stream, { emitStart: false }));
}
const remainingCalls = toolCalls.slice(index + 1);
for (const skipped of remainingCalls) {
results.push(skipToolCall(skipped, stream));
}
return { toolResults: results, steeringMessages, preparationErrorCount };
}
const remainingCalls = toolCalls.slice(index + 1);
for (const skipped of remainingCalls) {
results.push(skipToolCall(skipped, stream));
}
return { toolResults: results, steeringMessages, preparationErrorCount };
}
}
}

View file

@ -57,6 +57,12 @@ export interface AgentOptions {
*/
steeringMode?: "all" | "one-at-a-time";
/**
* Whether steering messages interrupt the current assistant tool batch.
* Defaults to true for interactive sessions.
*/
interruptToolExecutionOnSteering?: boolean;
/**
* Follow-up mode: "all" = send all follow-up messages at once, "one-at-a-time" = one per turn
*/
@ -161,6 +167,7 @@ export class Agent {
private _afterToolCall?: AgentLoopConfig["afterToolCall"];
private _externalToolExecution?: (model: Model<any>) => boolean;
private _getProviderOptions?: AgentOptions["getProviderOptions"];
private _interruptToolExecutionOnSteering: boolean;
constructor(opts: AgentOptions = {}) {
this._state = { ...this._state, ...opts.initialState };
@ -177,6 +184,8 @@ export class Agent {
this._maxRetryDelayMs = opts.maxRetryDelayMs;
this._externalToolExecution = opts.externalToolExecution;
this._getProviderOptions = opts.getProviderOptions;
this._interruptToolExecutionOnSteering =
opts.interruptToolExecutionOnSteering ?? true;
}
/**
@ -520,6 +529,7 @@ export class Agent {
getFollowUpMessages: async () => this.dequeueFollowUpMessages(),
beforeToolCall: this._beforeToolCall,
afterToolCall: this._afterToolCall,
interruptToolExecutionOnSteering: this._interruptToolExecutionOnSteering,
externalToolExecution: this._externalToolExecution?.(model) ?? false,
};

View file

@ -153,6 +153,16 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
*/
getSteeringMessages?: () => Promise<AgentMessage[]>;
/**
* Whether steering messages interrupt the current assistant tool batch.
*
* Default true preserves interactive behavior: a user message typed while
* tools are running skips remaining tool calls and lets the model react
* immediately. Headless/auto runners can set this false so incidental
* queued messages are deferred until the current tool batch finishes.
*/
interruptToolExecutionOnSteering?: boolean;
/**
* Returns follow-up messages to process after the agent would otherwise stop.
*

View file

@ -605,18 +605,21 @@ async function loadModelsDevData(): Promise<Model<any>[]> {
}
}
// Process Kimi For Coding models
// Process Kimi For Coding models. The kimi-coding endpoint is
// permissive on model slug, so we surface a single canonical id
// (kimi-k2.6) instead of the upstream "kimi-for-coding" alias to
// match opencode-go's slug for the same Moonshot K2.6 model.
if (data["kimi-for-coding"]?.models) {
for (const [modelId, model] of Object.entries(data["kimi-for-coding"].models)) {
const m = model as ModelsDevModel;
if (m.tool_call !== true) continue;
const canonicalId = modelId === "kimi-for-coding" ? "kimi-k2.6" : modelId;
models.push({
id: modelId,
name: m.name || modelId,
id: canonicalId,
name: m.name || canonicalId,
api: "anthropic-messages",
provider: "kimi-coding",
// Kimi For Coding's Anthropic-compatible API - SDK appends /v1/messages
baseUrl: "https://api.kimi.com/coding",
reasoning: m.reasoning === true,
input: m.modalities?.input?.includes("image") ? ["text", "image"] : ["text"],
@ -1249,7 +1252,7 @@ async function generateModels() {
const KIMI_CODING_BASE_URL = "https://api.kimi.com/coding";
const kimiCodingModels: Model<"anthropic-messages">[] = [
{
id: "kimi-for-coding",
id: "kimi-k2.6",
name: "Kimi K2.6",
api: "anthropic-messages",
provider: "kimi-coding",

View file

@ -4616,8 +4616,8 @@ export const MODELS = {
} satisfies Model<"openai-completions">,
},
"kimi-coding": {
"kimi-for-coding": {
id: "kimi-for-coding",
"kimi-k2.6": {
id: "kimi-k2.6",
name: "Kimi K2.6",
api: "anthropic-messages",
provider: "kimi-coding",

View file

@ -436,12 +436,7 @@ const adapters: Record<string, ProviderDiscoveryAdapter> = {
executionBaseUrl: "https://api.minimax.io/anthropic",
api: "anthropic-messages",
}),
"kimi-coding": new ProviderModelListAdapter({
provider: "kimi-coding",
discoveryBaseUrl: "https://api.kimi.com/coding/v1",
executionBaseUrl: "https://api.kimi.com/coding",
api: "anthropic-messages",
}),
"kimi-coding": new StaticDiscoveryAdapter("kimi-coding"),
mistral: new ProviderModelListAdapter({
provider: "mistral",
discoveryBaseUrl: "https://api.mistral.ai/v1",

View file

@ -383,6 +383,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
},
steeringMode: settingsManager.getSteeringMode(),
followUpMode: settingsManager.getFollowUpMode(),
interruptToolExecutionOnSteering: process.env.SF_HEADLESS !== "1",
transport: settingsManager.getTransport(),
thinkingBudgets: settingsManager.getThinkingBudgets(),
maxRetryDelayMs: settingsManager.getRetrySettings().maxDelayMs,

View file

@ -22,7 +22,7 @@ const { getModel, streamSimpleOpenAICompletions } = await import("../packages/pi
const modelIds = modelsArg
? modelsArg.split(",").map((s) => s.trim()).filter(Boolean)
: [
"kimi-coding/kimi-for-coding",
"kimi-coding/kimi-k2.6",
"minimax/MiniMax-M2.7-highspeed",
"zai/glm-4.5",
"mistral/devstral-latest",

View file

@ -15,6 +15,7 @@ export const EXIT_SUCCESS = 0;
export const EXIT_ERROR = 1;
export const EXIT_BLOCKED = 10;
export const EXIT_CANCELLED = 11;
export const EXIT_RELOAD = 12;
/**
* Map a headless session status string to its standardized exit code.

View file

@ -10,6 +10,7 @@
* 1 error or timeout
* 10 blocked (command reported a blocker)
* 11 cancelled (SIGINT/SIGTERM received)
* 12 reload (agent requested restart-with-resume, same session)
*/
import type { ChildProcess } from "node:child_process";
@ -32,6 +33,7 @@ import {
EXIT_BLOCKED,
EXIT_CANCELLED,
EXIT_ERROR,
EXIT_RELOAD,
EXIT_SUCCESS,
FIRE_AND_FORGET_METHODS,
IDLE_TIMEOUT_MS,

View file

@ -76,6 +76,7 @@ function readConfigs(): McpServerConfig[] {
join(process.cwd(), ".sf", "mcp.json"),
join(sfHome, "mcp.json"), // global: ~/.sf/mcp.json
join(sfHome, "agent", "mcp.json"), // global: ~/.sf/agent/mcp.json (legacy alt)
join(homedir(), ".mcp.json"), // user-global: ~/.mcp.json (Claude Code, npx, etc.)
];
for (const configPath of configPaths) {

View file

@ -7,6 +7,29 @@ interface ScaffoldFile {
}
const SCAFFOLD_FILES: ScaffoldFile[] = [
{
path: ".siftignore",
content: `.git/**
.sf/**
.bg-shell/**
.pytest_cache/**
.venv/**
venv/**
node_modules/**
**/node_modules/**
**/__pycache__/**
*.pyc
*.egg-info/**
build/**
dist/**
target/**
vendor/**
coverage/**
.cache/**
tmp/**
*.log
`,
},
{
path: "AGENTS.md",
content: `# Agent Map

View file

@ -36,7 +36,9 @@ export async function closeoutUnit(
startedAt: number,
opts?: CloseoutOptions,
): Promise<string | undefined> {
const modelId = ctx.model?.id ?? "unknown";
const provider = ctx.model?.provider;
const id = ctx.model?.id;
const modelId = provider && id ? `${provider}/${id}` : (id ?? "unknown");
snapshotUnitMetrics(ctx, unitType, unitId, startedAt, modelId, opts);
const activityFile = saveActivityLog(ctx, basePath, unitType, unitId);

View file

@ -32,7 +32,12 @@ export function registerExecTools(pi: ExtensionAPI): void {
],
parameters: Type.Object({
runtime: Type.Union(
[Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")],
[
Type.Literal("bash"),
Type.Literal("node"),
Type.Literal("python"),
Type.Literal("python3"),
],
{ description: "Interpreter: bash (-c), node (-e), or python3 (-c)." },
),
script: Type.String({
@ -92,7 +97,12 @@ export function registerExecTools(pi: ExtensionAPI): void {
),
runtime: Type.Optional(
Type.Union(
[Type.Literal("bash"), Type.Literal("node"), Type.Literal("python")],
[
Type.Literal("bash"),
Type.Literal("node"),
Type.Literal("python"),
Type.Literal("python3"),
],
{
description: "Restrict to one runtime.",
},
@ -139,4 +149,37 @@ export function registerExecTools(pi: ExtensionAPI): void {
});
},
});
/**
* Kill the current pi-agent subprocess. In headless mode: the supervisor
* (sf run-headless) will detect the non-zero exit and restart the agent (up to
* max-restarts=3, default). In interactive TUI mode: the process exits and
* the user restarts manually.
*
* Use this to force-reload extension code (e.g. after a `~/.mcp.json` change)
* or to escape a stuck state that cannot be resolved via normal tool calls.
*/
pi.registerTool({
name: "kill_agent",
label: "Kill Agent",
description:
"Kill the current pi-agent subprocess so the supervisor restarts it with fresh code. " +
"Use after updating extension config files (e.g. ~/.mcp.json) that require a process restart " +
"to take effect. The supervisor will restart the agent automatically (headless mode) or " +
"you restart manually (interactive TUI).",
promptSnippet:
"Kill and restart the pi-agent to reload fresh extension code",
promptGuidelines: [
"Use this only when normal tool calls cannot achieve the desired effect and a clean restart is needed.",
"In headless mode: the supervisor will restart the agent automatically.",
"In interactive TUI: you will need to restart sf manually.",
],
parameters: Type.Object({}),
async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
// Exit with 1 so headless supervisor treats it as a crash and restarts.
// Exit 0 would cause headless to exit normally without restarting.
process.exit(1);
// unreachable
},
});
}

View file

@ -343,7 +343,17 @@ function isFreshMarker(
): boolean {
try {
const stat = statSync(markerPath);
return now - stat.mtimeMs < ttlMs;
if (now - stat.mtimeMs >= ttlMs) return false;
const parsed = JSON.parse(readFileSync(markerPath, "utf-8")) as {
schemaVersion?: number;
cwd?: string;
args?: unknown;
};
return (
parsed.schemaVersion === 2 &&
Array.isArray(parsed.args) &&
parsed.args.at(-2) === "."
);
} catch {
return false;
}
@ -403,7 +413,7 @@ export function ensureSiftIndexWarmup(
options.retrieverTimeoutMs ??
DEFAULT_SIFT_WARMUP_RETRIEVER_TIMEOUT_MS,
),
projectRoot,
".",
options.query ?? DEFAULT_SIFT_WARMUP_QUERY,
];
@ -413,8 +423,10 @@ export function ensureSiftIndexWarmup(
markerPath,
`${JSON.stringify(
{
schemaVersion: 2,
startedAt: new Date(now).toISOString(),
command: detection.binaryPath,
cwd: projectRoot,
args,
},
null,

View file

@ -12,7 +12,7 @@ import { resolve } from "node:path";
export interface ExecSandboxRequest {
/** Interpreter to use. */
runtime: "bash" | "node" | "python";
runtime: "bash" | "node" | "python" | "python3";
/** Script body. Executed via the runtime's -c equivalent. */
script: string;
/** Optional purpose/label recorded in meta.json. */
@ -122,6 +122,7 @@ function resolveCommand(runtime: ExecSandboxRequest["runtime"]): {
case "node":
return { cmd: process.execPath, args: ["-e"] };
case "python":
case "python3":
return { cmd: "python3", args: ["-c"] };
}
}

View file

@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"entries": [
{ "provider": "kimi-coding", "model": "kimi-for-coding", "priority": 0 }
{ "provider": "kimi-coding", "model": "kimi-k2.6", "priority": 0 }
]
}

View file

@ -35,34 +35,6 @@ export { formatTokenCount } from "../shared/format-utils.js";
// ─── Learning Integration ─────────────────────────────────────────────────────
/**
* Infer provider from a model ID when not explicitly prefixed with "provider/".
*/
function inferProviderFromBareModelId(modelId: string): string {
const lower = modelId.toLowerCase();
if (
lower === "kimi-for-coding" ||
lower === "kimi-k2-thinking"
)
return "kimi-coding";
if (lower.startsWith("minimax-m")) return "ollama-cloud";
if (lower.startsWith("minimax-") || modelId.startsWith("MiniMax-"))
return "minimax";
if (lower.startsWith("glm-")) return "zai";
if (lower.startsWith("mimo-")) return "xiaomi";
if (lower.startsWith("gemini-")) return "google-gemini-cli";
if (
lower.startsWith("magistral-") ||
lower.startsWith("mistral-") ||
lower.startsWith("devstral-") ||
lower.startsWith("codestral-") ||
lower.startsWith("ministral-") ||
lower.startsWith("pixtral-")
)
return "mistral";
return "unknown";
}
function formatAggregateModelIdentity(modelId: string): string {
const slashIdx = modelId.indexOf("/");
if (slashIdx === -1) return formatModelIdentity({ id: modelId });
@ -81,13 +53,12 @@ async function recordUnitOutcome(unit: UnitMetrics): Promise<void> {
try {
const { recordOutcome } = await import("./learning/outcome-recorder.mjs");
let provider: string;
const modelId = unit.model;
if (modelId.includes("/")) {
[provider] = modelId.split("/");
} else {
provider = inferProviderFromBareModelId(modelId);
}
const slashIdx = modelId.indexOf("/");
// Unit metrics always carry full "provider/model" slugs upstream;
// drop bare-id entries rather than guessing the provider.
if (slashIdx === -1) return;
const provider = modelId.slice(0, slashIdx);
recordOutcome(db, {
modelId,

View file

@ -46,6 +46,14 @@ test("ensureAgenticDocsScaffold creates repo docs and nested AGENTS files withou
readFileSync(join(dir, "docs", "QUALITY_SCORE.md"), "utf-8"),
/Make code legible to agents/,
);
assert.match(
readFileSync(join(dir, ".siftignore"), "utf-8"),
/\*\*\/node_modules\/\*\*/,
);
assert.match(
readFileSync(join(dir, ".siftignore"), "utf-8"),
/\.venv\/\*\*/,
);
} finally {
rmSync(dir, { recursive: true, force: true });
}

View file

@ -354,9 +354,9 @@ test("ensureSiftIndexWarmup starts page-index-hybrid warmup and writes marker",
const projectRoot = makeProject();
try {
const fakeSift = writeFakeSiftBinary(projectRoot);
const calls: Array<{ command: string; args: string[] }> = [];
const fakeSpawn = ((command: string, args: string[]) => {
calls.push({ command, args });
const calls: Array<{ command: string; args: string[]; cwd?: string }> = [];
const fakeSpawn = ((command: string, args: string[], options?: { cwd?: string }) => {
calls.push({ command, args, cwd: options?.cwd });
return { unref() {} };
}) as unknown as typeof import("node:child_process").spawn;
@ -378,7 +378,8 @@ test("ensureSiftIndexWarmup starts page-index-hybrid warmup and writes marker",
"1",
"--retriever-timeout-ms",
]);
assert.equal(calls[0].args.at(-2), projectRoot);
assert.equal(calls[0].args.at(-2), ".");
assert.equal(calls[0].cwd, projectRoot);
assert.match(calls[0].args.at(-1) ?? "", /repo architecture/);
assert.match(
readFileSync(
@ -446,6 +447,46 @@ test("ensureSiftIndexWarmup skips recent marker and explicit non-sift backends",
}
});
test("ensureSiftIndexWarmup ignores stale absolute-path warmup markers", () => {
const projectRoot = makeProject();
try {
writeFakeSiftBinary(projectRoot);
mkdirSync(join(projectRoot, ".sf", "runtime"), { recursive: true });
writeFileSync(
join(projectRoot, ".sf", "runtime", "sift-index-warmup.json"),
`${JSON.stringify({
startedAt: "2026-04-30T12:00:00.000Z",
command: join(projectRoot, "bin", "sift"),
args: ["search", projectRoot, "repo architecture"],
})}\n`,
"utf-8",
);
let spawnCount = 0;
const fakeSpawn = (() => {
spawnCount += 1;
return { unref() {} };
}) as unknown as typeof import("node:child_process").spawn;
const result = ensureSiftIndexWarmup(projectRoot, undefined, {
env: { PATH: join(projectRoot, "bin") },
spawnFn: fakeSpawn,
now: Date.parse("2026-04-30T12:01:00.000Z"),
});
assert.equal(result.status, "started");
assert.equal(spawnCount, 1);
assert.match(
readFileSync(
join(projectRoot, ".sf", "runtime", "sift-index-warmup.json"),
"utf-8",
),
/"schemaVersion": 2/,
);
} finally {
cleanup(projectRoot);
}
});
test("resolveSiftBinary honors SIFT_PATH before PATH lookup", () => {
const projectRoot = makeProject();
try {

View file

@ -121,9 +121,9 @@ export async function executeSfExec(
if (!isContextModeEnabled(deps.preferences)) return disabledResult();
const runtime = params.runtime;
if (runtime !== "bash" && runtime !== "node" && runtime !== "python") {
if (runtime !== "bash" && runtime !== "node" && runtime !== "python" && runtime !== "python3") {
return paramError(
`invalid runtime "${String(runtime)}" — must be bash | node | python`,
`invalid runtime "${String(runtime)}" — must be bash | node | python | python3`,
);
}
const script = typeof params.script === "string" ? params.script : "";

View file

@ -1070,9 +1070,12 @@ async function runSingleAgent(
.map((s) => s.trim())
.filter(Boolean);
const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
const proc = spawn(
process.execPath,
[process.env.SF_BIN_PATH!, ...extensionArgs, ...args],
// Execute SF_BIN_PATH directly — it is an executable shell script (sf-from-source)
// with a proper shebang. Do NOT pass it to process.execPath as a node script arg,
// otherwise Node parses the bash file as JavaScript and fails with a syntax error.
const proc = spawn(
process.env.SF_BIN_PATH!,
[...extensionArgs, ...args],
{
cwd: cwd ?? defaultCwd,
shell: false,
@ -1247,8 +1250,12 @@ async function runSingleAgentInCmuxSplit(
.map((s) => s.trim())
.filter(Boolean);
const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
// SF_BIN_PATH is an executable shell script with a shebang.
// Execute it directly — do NOT pass it to node as a module arg (node would
// try to parse the shell script as JavaScript and fail with a syntax error).
// The OS honors the shebang when the file is exec'd directly.
const sfBinPath = process.env.SF_BIN_PATH!;
const processArgs = [
process.env.SF_BIN_PATH!,
...extensionArgs,
...buildSubagentProcessArgs(agent, task, tmpPromptPath, modelOverride),
];
@ -1259,7 +1266,7 @@ async function runSingleAgentInCmuxSplit(
const innerScript = [
`cd ${bashPath(cwd ?? defaultCwd)}`,
"set -o pipefail",
`${bashPath(process.execPath)} ${processArgs.map((a) => bashPath(a)).join(" ")} 2> >(tee ${bashPath(stderrPath)} >&2) | tee ${bashPath(stdoutPath)}`,
`${bashPath(sfBinPath)} ${processArgs.map((a) => bashPath(a)).join(" ")} 2> >(tee ${bashPath(stderrPath)} >&2) | tee ${bashPath(stdoutPath)}`,
// biome-ignore lint/suspicious/noTemplateCurlyInString: intentional literal — bash variable syntax
"status=${PIPESTATUS[0]}",
`printf '%s' "$status" > ${bashPath(exitPath)}`,