feat(adapters): collect instruction edits at verified provider stop boundary

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-09-27 18:33:11 -05:00
1 parent 3943048383
commit a5119d40eb
16 files changed
+194 -19

No files matched your search

@@ -6355,10 +6355,30 @@ describe("ACPX engine run lifecycle corrections (F3: one teardown error policy)"
};
}
it("collects instructions after confirmed close on a thrown provider turn, before workspace restore", async () => {
const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox();
const { paperclipStops, anyStopped } = stubBridges();
const order: string[] = [];
const execute = createAcpxEngineExecutor({
stagingLocks: new Map(), warmHandles: new Map(), stagedRuntimes: new Map(),
createRuntime: () => ({ ensureSession: async () => okHandle, startTurn: () => throwingTurn(),
close: async () => { order.push("close"); } }) as never,
});
const result = await execute({ runId: "instruction-stop-failure",
...remoteArgs(stateDir, localCwd, executionTarget, { onProviderStopped: async () => {
expect(anyStopped(paperclipStops)).toBe(true);
order.push("collect");
} }),
} as never);
expect(result.exitCode).toBe(1);
expect(order).toEqual(["close", "collect"]);
});
it("test_teardown_continues_after_one_teardown_step_fails", async () => {
const { stateDir, localCwd, executionTarget } = await setupRemoteSandbox();
const { paperclipStops, processStops, anyStopped } = stubBridges();
const stagingLocks = new Map<string, Promise<unknown>>();
const collectInstructions = vi.fn(async () => {});
const logs: Array<{ stream: string; text: string }> = [];
const execute = createAcpxEngineExecutor({
stagingLocks,
@@ -6378,6 +6398,7 @@ describe("ACPX engine run lifecycle corrections (F3: one teardown error policy)"
const result = await execute({
runId: "td-continue",
...remoteArgs(stateDir, localCwd, executionTarget, {
onProviderStopped: collectInstructions,
onLog: async (stream: "stdout" | "stderr", text: string) => {
logs.push({ stream, text });
},
@@ -6385,6 +6406,7 @@ describe("ACPX engine run lifecycle corrections (F3: one teardown error policy)"
} as never);
expect(result.exitCode).toBe(1);
expect(collectInstructions).not.toHaveBeenCalled();
// The close failure did not stop the bridge stops or the lease release.
expect(anyStopped(paperclipStops)).toBe(true);
expect(anyStopped(processStops)).toBe(true);
@@ -3600,12 +3600,13 @@ async function closeWarmHandle(input: {
input.handles.delete(input.key);
}
clearWarmHandleTimer(input.entry);
await input.entry.runtime.close({
const stopped = await input.entry.runtime.close({
handle: input.entry.handle,
reason: input.reason,
discardPersistentState: input.discardPersistentState ?? false,
}).catch(() => {});
}).then(() => true, () => false);
flushChildStderr(input.entry.childStderrState);
return stopped;
}
function warmHandleMatches(
@@ -5289,7 +5290,7 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
) {
// A matching warm entry closes through the warm store, which also
// clears its idle timer and flushes its child stderr.
await closeWarmHandle({
runtimeStopConfirmed = await closeWarmHandle({
handles: warmHandles,
key: prepared.sessionKey,
entry: existing,
@@ -5335,6 +5336,10 @@ export function createAcpxEngineExecutor(deps: AcpxEngineExecutorOptions = {}) {
// runs, so the host mints a `traceparent` for the provider spans, and the
// per-task restore spans parent to `sandbox.syncBack`.
syncBack: () => timedPhase("sync_back", async () => {
// A transport error or terminal narrative cannot authorize collecting
// mutable provider files. Require the owned close or local OS handle.
const stopped = runtimeStopConfirmed || (!prepared.processSessionBridge && capturedProcessExited(processIdentitySink?.localProcess));
if (stopped) await ctx.onProviderStopped?.();
await runRuntimeSpan("sandbox.syncBack", async () => {
const restoreOutcome = await syncBackManagedHome(prepared);
if (!restoreOutcome.ok) {
+12 -1
View File
@@ -280,6 +280,9 @@ export interface AdapterExecutionTargetProcessOptions {
onLog: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
onRuntimeProgress?: RuntimeStatusSink;
onSpawn?: (meta: { pid: number; processGroupId: number | null; startedAt: string }) => Promise<void>;
/** Trusted invocation observation, not a turn completion callback. Called only
* after local child close or a remotely observed exit, including nonzero exits. */
onProcessStopped?: () => void;
terminalResultCleanup?: TerminalResultCleanupOptions;
/**
* Sandbox-only: factory from the Paperclip bridge handle that streams the
@@ -894,6 +897,7 @@ export async function runAdapterExecutionTargetProcess(
// after the clean process completion cannot latch a false mid-run loss. A
// control channel that died before this clean completion still fails the
// run closed.
if (!result.timedOut && typeof result.exitCode === "number" && Number.isInteger(result.exitCode) && result.exitCode >= 0) options.onProcessStopped?.();
const settled = applyRunDispositionSeam(result, options.settleRunDisposition);
if (runLogTail) {
await runLogTail.finish({ stdout: result.stdout, stderr: result.stderr });
@@ -912,7 +916,7 @@ export async function runAdapterExecutionTargetProcess(
? sanitizeRemoteExecutionEnv(options.env)
: options.env;
return await runChildProcess(runId, command, args, {
const result = await runChildProcess(runId, command, args, {
cwd: options.cwd,
env,
stdin: options.stdin,
@@ -924,6 +928,13 @@ export async function runAdapterExecutionTargetProcess(
localProcessSandbox: target?.kind === "local" || !target ? options.localProcessSandbox : null,
remoteExecution: adapterExecutionTargetToRemoteSpec(target),
});
// Closing an SSH client on timeout/disconnect does not prove the remote
// provider exited. SSH status 255 is transport failure, never a stop receipt.
if (!target || target.kind === "local" ||
(!result.timedOut && !result.signal && result.exitCode !== null && result.exitCode >= 0 && result.exitCode < 255)) {
options.onProcessStopped?.();
}
return result;
}
export async function runAdapterExecutionTargetShellCommand(
@@ -0,0 +1,75 @@
import { describe, expect, it, vi, afterEach } from "vitest";
import * as serverUtils from "./server-utils.js";
import { runAdapterExecutionTargetProcess } from "./execution-target.js";
import { createProviderStoppedBoundary } from "./provider-stopped-boundary.js";
const result = { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "", pid: 12, startedAt: "now" };
const options = (onProcessStopped: () => void) => ({ cwd: "/tmp", env: {}, timeoutSec: 1, graceSec: 1, onLog: async () => {}, onProcessStopped });
const target = (execute: () => Promise<any>) => ({ kind: "remote", transport: "sandbox", remoteCwd: "/workspace", runner: { execute } }) as any;
afterEach(() => vi.restoreAllMocks());
describe("owned provider process stop boundary", () => {
it.each([0, 1])("notifies after a remote invocation returns an observed exit code %s", async exitCode => {
const stopped = vi.fn();
await runAdapterExecutionTargetProcess("run", target(async () => ({ ...result, exitCode })), "provider", [], options(stopped));
expect(stopped).toHaveBeenCalledOnce();
});
it("notifies after the local child closed on timeout", async () => {
vi.spyOn(serverUtils, "runChildProcess").mockResolvedValue({ ...result, exitCode: null, signal: "SIGTERM", timedOut: true });
const stopped = vi.fn();
await runAdapterExecutionTargetProcess("run", null, "provider", [], options(stopped));
expect(stopped).toHaveBeenCalledOnce();
});
it.each([
{ ...result, exitCode: null, timedOut: true },
{ ...result, exitCode: null, timedOut: false },
])("does not treat an incomplete remote execution as a stop receipt", async incomplete => {
const stopped = vi.fn();
await runAdapterExecutionTargetProcess("run", target(async () => incomplete), "provider", [], options(stopped));
expect(stopped).not.toHaveBeenCalled();
});
it("does not infer remote termination from a transport rejection", async () => {
const stopped = vi.fn();
await expect(runAdapterExecutionTargetProcess("run", target(async () => { throw new Error("transport lost"); }), "provider", [], options(stopped))).rejects.toThrow("transport lost");
expect(stopped).not.toHaveBeenCalled();
});
});
describe("final invocation instruction collection fence", () => {
it("collects once, after the last completed attempt, before restore", async () => {
const order: string[] = [];
const fence = createProviderStoppedBoundary(async () => { order.push("collect"); });
fence.beginInvocation()();
expect(order).toEqual([]);
fence.beginInvocation()();
await fence.collectBeforeRestore();
order.push("restore");
await fence.collectBeforeRestore();
expect(order).toEqual(["collect", "restore"]);
});
it("an uncertain later attempt invalidates an earlier stop, including late callbacks", async () => {
const collect = vi.fn(async () => {});
const fence = createProviderStoppedBoundary(collect);
const old = fence.beginInvocation(); old();
fence.beginInvocation(); old();
await fence.collectBeforeRestore();
expect(collect).not.toHaveBeenCalled();
});
it("does not collect before any provider invocation", async () => {
const collect = vi.fn(async () => {});
await createProviderStoppedBoundary(collect).collectBeforeRestore();
expect(collect).not.toHaveBeenCalled();
});
it("does not infer SSH provider termination from transport exit 255", async () => {
vi.spyOn(serverUtils, "runChildProcess").mockResolvedValue({ ...result, exitCode: 255 });
const stopped = vi.fn();
await runAdapterExecutionTargetProcess("run", { kind: "remote", transport: "ssh", spec: { host: "fixture" }, remoteCwd: "/workspace" } as any, "provider", [], options(stopped));
expect(stopped).not.toHaveBeenCalled();
});
it("retains the stop observation when a subsequent log-tail flush throws", async () => {
const stopped = vi.fn();
await expect(runAdapterExecutionTargetProcess("run", target(async () => result), "provider", [], {
...options(stopped), runLogTail: { create: () => ({ wrapCommand: (command: string, args: string[]) => ({ command, args }), start: () => {}, finish: async () => { throw new Error("log flush failed"); }, abort: async () => {} }) } as any,
})).rejects.toThrow("log flush failed");
expect(stopped).toHaveBeenCalledOnce();
});
});
@@ -0,0 +1,19 @@
/** Per-adapter invocation fence. A failed later attempt invalidates an earlier
* stop receipt; a late callback from that earlier attempt cannot restore it. */
export function createProviderStoppedBoundary(onProviderStopped?: () => Promise<void>) {
let invocation = 0;
let stopped = false;
let collected = false;
return {
beginInvocation(): () => void {
const current = ++invocation;
stopped = false;
return () => { if (invocation === current) stopped = true; };
},
async collectBeforeRestore() {
if (!stopped || collected || !onProviderStopped) return;
collected = true;
await onProviderStopped();
},
};
}
+3
View File
@@ -202,6 +202,9 @@ export interface AdapterExecutionContext {
/** Host-owned stop of this run's sandbox during setup or direct CLI execution. Resolves only after
* provider termination is verified; never accepts an agent-selected lease. */
stopRemoteStartup?: () => Promise<void>;
/** Host-owned collection after the final provider invocation is confirmed stopped,
* before remote workspace restore or disposal. Never call on an unverified timeout. */
onProviderStopped?: () => Promise<void>;
/** Server-owned, actor-attributed snapshot also rendered by legacy wake prompts. */
executionContinuation?: ExecutionContinuationEnvelope | null;
runId: string;
@@ -1,3 +1,4 @@
import { createProviderStoppedBoundary } from "@paperclipai/adapter-utils/provider-stopped-boundary";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -397,6 +398,7 @@ export async function runClaudeLogin(input: {
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const providerStop = createProviderStoppedBoundary(ctx.onProviderStopped);
const engineSelection = await resolveClaudeExecutionEngineForRun(ctx);
if (engineSelection.unavailableReason) {
return {
@@ -961,6 +963,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
}
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
onProcessStopped: providerStop.beginInvocation(),
cwd,
env,
stdin: prompt,
@@ -1355,6 +1358,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
return toAdapterResult(initial, { fallbackSessionId: runtimeSessionId || runtime.sessionId });
} finally {
await providerStop.collectBeforeRestore();
if (paperclipBridge) {
await paperclipBridge.stop();
}
@@ -117,7 +117,7 @@ describe("codex execute — outbound auth copy-back restore contribution", () =>
);
}
async function runTeardown(input: { sandboxAuth: string; hostAuth: string }) {
async function runTeardown(input: { sandboxAuth: string; hostAuth: string; onProviderStopped?: () => Promise<void> }) {
const rootDir = await mkdtemp(
path.join(os.tmpdir(), "paperclip-codex-copyback-e2e-"),
);
@@ -138,6 +138,7 @@ describe("codex execute — outbound auth copy-back restore contribution", () =>
const executionResult = await execute({
runId: "run-copyback-e2e",
onProviderStopped: input.onProviderStopped,
agent: {
id: "agent-1",
companyId: "company-1",
@@ -181,6 +182,24 @@ describe("codex execute — outbound auth copy-back restore contribution", () =>
};
}
it("collects stopped-provider instruction edits before a throwing remote restore", async () => {
const order: string[] = [];
prepareAdapterExecutionTargetRuntime.mockImplementationOnce(async () => ({
target: { kind: "remote", transport: "ssh" }, workspaceRemoteDir: "/remote/workspace",
runtimeRootDir: REMOTE_RUNTIME_ROOT, assetDirs: { home: `${REMOTE_RUNTIME_ROOT}/home` },
restoreWorkspace: async () => { order.push("restore"); throw new Error("restore failed"); },
}));
await expect(runTeardown({ sandboxAuth: "{}", hostAuth: "{}", onProviderStopped: async () => { order.push("collect"); } })).rejects.toThrow("restore failed");
expect(order).toEqual(["collect", "restore"]);
});
it("collects after a failed provider exit before restoring its workspace", async () => {
runChildProcess.mockResolvedValueOnce({ exitCode: 1, signal: null, timedOut: false, stdout: "", stderr: "provider failed", pid: 321, startedAt: new Date().toISOString() });
const collected = vi.fn(async () => {});
await runTeardown({ sandboxAuth: "{}", hostAuth: "{}", onProviderStopped: collected });
expect(collected).toHaveBeenCalledOnce();
});
it("declares a Codex `home` asset carrying both inbound provision and outbound restore contributions", async () => {
await runTeardown({
sandboxAuth: subscriptionAuth({ accountId: "acct", lastRefresh: "2026-07-09T01:00:00Z", marker: "s" }),
@@ -1,3 +1,4 @@
import { createProviderStoppedBoundary } from "@paperclipai/adapter-utils/provider-stopped-boundary";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
@@ -567,6 +568,7 @@ export async function ensureCodexSkillsInjected(
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const providerStop = createProviderStoppedBoundary(ctx.onProviderStopped);
const engineSelection = await resolveCodexExecutionEngineForRun(ctx);
if (engineSelection.unavailableReason) {
return {
@@ -1321,6 +1323,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
try {
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
onProcessStopped: providerStop.beginInvocation(),
cwd,
env,
stdin: prompt,
@@ -1580,6 +1583,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
executionError = error;
throw error;
} finally {
await providerStop.collectBeforeRestore();
if (paperclipBridge) {
await paperclipBridge.stop();
}
@@ -271,21 +271,11 @@ printf '%s\\n' '{"type":"result","subtype":"success","session_id":"cursor-sessio
const runner = {
execute: async (input: { command: string; args?: string[]; env?: Record<string, string> }) => {
runnerState.commands.push(input.command);
if (input.command === "sh") {
return {
exitCode: 0,
signal: null,
timedOut: false,
stdout: "",
stderr: "",
pid: 555,
startedAt: new Date().toISOString(),
};
}
// Exercise actual bounded file reads during managed-home restoration;
// reporting empty success for every shell command hides missing bytes.
return runChildProcess(`cursor-fresh-lease-${runnerState.commands.length}`, input.command, input.args ?? [], {
cwd: remoteWorkspace,
env: input.env ?? {},
env: { ...input.env, PATH: `${input.env?.PATH ?? ""}:/usr/bin:/bin` },
timeoutSec: 30,
graceSec: 5,
onLog: async () => {},
@@ -1,3 +1,4 @@
import { createProviderStoppedBoundary } from "@paperclipai/adapter-utils/provider-stopped-boundary";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -199,6 +200,7 @@ export async function ensureCursorSkillsInjected(
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const providerStop = createProviderStoppedBoundary(ctx.onProviderStopped);
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
const executionTarget = readAdapterExecutionTarget({
executionTarget: ctx.executionTarget,
@@ -655,6 +657,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
};
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
onProcessStopped: providerStop.beginInvocation(),
cwd,
env,
timeoutSec,
@@ -775,6 +778,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
}
return toResult(initial);
} finally {
await providerStop.collectBeforeRestore();
if (paperclipBridge) {
await paperclipBridge.stop();
}
@@ -1,3 +1,4 @@
import { createProviderStoppedBoundary } from "@paperclipai/adapter-utils/provider-stopped-boundary";
import fs from "node:fs/promises";
import type { Dirent } from "node:fs";
import os from "node:os";
@@ -206,6 +207,7 @@ async function buildGeminiSkillsDir(
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const providerStop = createProviderStoppedBoundary(ctx.onProviderStopped);
const engineSelection = await resolveGeminiExecutionEngineForRun(ctx);
if (engineSelection.unavailableReason) {
return {
@@ -638,6 +640,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
}
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
onProcessStopped: providerStop.beginInvocation(),
cwd,
env: invocationEnv,
timeoutSec,
@@ -784,6 +787,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
return toResult(initial);
} finally {
await providerStop.collectBeforeRestore();
await Promise.all([
paperclipBridge?.stop(),
restoreRemoteWorkspace?.(),
@@ -1,3 +1,4 @@
import { createProviderStoppedBoundary } from "@paperclipai/adapter-utils/provider-stopped-boundary";
import { withWorkspaceRestore } from "@paperclipai/adapter-utils/workspace-restore-result";
import { cancellableSandboxStartup } from "@paperclipai/adapter-utils/acpx-engine/startup-cancellation";
import fs from "node:fs/promises";
@@ -246,6 +247,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
}
async function executeTurn(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const providerStop = createProviderStoppedBoundary(ctx.onProviderStopped);
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
const executionTarget = readAdapterExecutionTarget({
executionTarget: ctx.executionTarget,
@@ -616,6 +618,7 @@ async function executeTurn(ctx: AdapterExecutionContext): Promise<AdapterExecuti
}
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
onProcessStopped: providerStop.beginInvocation(),
cwd,
env,
timeoutSec,
@@ -725,7 +728,7 @@ async function executeTurn(ctx: AdapterExecutionContext): Promise<AdapterExecuti
};
try {
return await withWorkspaceRestore(executeTurn, async () => { await restoreRemoteWorkspace?.(); });
return await withWorkspaceRestore(executeTurn, async () => { await providerStop.collectBeforeRestore(); await restoreRemoteWorkspace?.(); });
} finally {
// Cleanup runs after settlement on both success and failure.
if (stagedGrokHomeDir) {
@@ -1,3 +1,4 @@
import { createProviderStoppedBoundary } from "@paperclipai/adapter-utils/provider-stopped-boundary";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -186,6 +187,7 @@ async function buildKimiSkillsDir(
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const providerStop = createProviderStoppedBoundary(ctx.onProviderStopped);
const engineSelection = await resolveKimiExecutionEngineForRun(ctx);
if (engineSelection.unavailableReason) {
return {
@@ -605,6 +607,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const eventForwarder = createKimiEventForwardingLog(onLog, onEvent);
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
onProcessStopped: providerStop.beginInvocation(),
cwd,
env: invocationEnv,
timeoutSec,
@@ -746,6 +749,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
return toResult(initial);
} finally {
await providerStop.collectBeforeRestore();
await Promise.all([
paperclipBridge?.stop(),
restoreRemoteWorkspace?.(),
@@ -1,3 +1,4 @@
import { createProviderStoppedBoundary } from "@paperclipai/adapter-utils/provider-stopped-boundary";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -222,6 +223,7 @@ async function buildOpenCodeSkillsDir(config: Record<string, unknown>): Promise<
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const providerStop = createProviderStoppedBoundary(ctx.onProviderStopped);
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
const executionTarget = readAdapterExecutionTarget({
executionTarget: ctx.executionTarget,
@@ -641,6 +643,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
}
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
onProcessStopped: providerStop.beginInvocation(),
cwd,
env: preparedRuntimeConfig.env,
stdin: prompt,
@@ -755,6 +758,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
return toResult(initial);
} finally {
await providerStop.collectBeforeRestore();
await Promise.all([
paperclipBridge?.stop(),
restoreRemoteWorkspace?.(),
@@ -1,3 +1,4 @@
import { createProviderStoppedBoundary } from "@paperclipai/adapter-utils/provider-stopped-boundary";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -221,6 +222,7 @@ async function readSavedSessionCwd(input: {
}
export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExecutionResult> {
const providerStop = createProviderStoppedBoundary(ctx.onProviderStopped);
const { runId, agent, runtime, config, context, onLog, onMeta, onSpawn, authToken } = ctx;
const executionTarget = readAdapterExecutionTarget({
executionTarget: ctx.executionTarget,
@@ -733,6 +735,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
};
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
onProcessStopped: providerStop.beginInvocation(),
cwd,
env: executionTargetIsRemote ? env : runtimeEnv,
timeoutSec,
@@ -867,6 +870,7 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
return toResult(initial);
} finally {
await providerStop.collectBeforeRestore();
await Promise.all([
paperclipBridge?.stop(),
restoreRemoteWorkspace?.(),