mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(native): continue on a replacement when a retained Runner retires before claim (#19897)
This commit is contained in:
1 parent
91b044858c
commit
94718ce168
5 files changed
+123
-47
No files matched your search
@@ -16086,7 +16086,7 @@ describe("runnerd provider runtime wiring", () => {
|
||||
state.toolAuthorityDefinitions.mockImplementation(async binding => binding.readRemoteTaskFile ? [{ name: "register_deliverable" }] as never : []);
|
||||
const readWorkspaceFile = vi.fn(async () => Buffer.from("proof"));
|
||||
const release = vi.fn(async () => {}), resume = vi.fn(async () => "resumed" as const);
|
||||
const owner = vi.spyOn(pluginTaskRuntime, "createPluginRunnerTaskOwner").mockReturnValue({ launcher, registration, driver, readWorkspaceFile, release, resume, isRetired: () => false, rebind:vi.fn() });
|
||||
const owner = vi.spyOn(pluginTaskRuntime, "createPluginRunnerTaskOwner").mockReturnValue({ launcher, registration, driver, readWorkspaceFile, release, resume, isRetired: () => false, retiredBeforeClaim: () => false, rebind:vi.fn() });
|
||||
const task = vi.fn(async (operation: unknown) => {
|
||||
environmentTaskOperationSchema.parse(operation);
|
||||
return { kind: "description", launchPreparationVersion: 1, projectAuthCheckVersion: 1, releaseResumeVersion: 1, ...(kind === "codex" ? { workspaceFileReadVersion: 1 } : {}), taskId: "task", runner: { version: "fixture", digest: `sha256:${"a".repeat(64)}` } };
|
||||
|
||||
@@ -10811,40 +10811,38 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
const resolveWake = () => timeController("wake_binding", () => resolveCurrentWakeCommentsBinding(input.db, input.execution.binding));
|
||||
const expectedCurrentWakeComments = await (executionContext ? executionContext.wait(resolveWake) : resolveWake());
|
||||
let remoteWarmRecoveryPending = remoteWarmRecovery !== undefined;
|
||||
const createExecutionBackend = () => createRunnerdBackend({
|
||||
...input,
|
||||
onDiagnosticLease: ownDiagnosticLease,
|
||||
providerPackAdmissionSource,
|
||||
recoveryPurpose: claimedStopCleanup ? "stop_cleanup" : "execution",
|
||||
executionContext,
|
||||
onAccountingEvent: producerUsage?.capture,
|
||||
controllerPersistence: input.runnerExecutionTarget?.kind === "remote" ? {
|
||||
bootId: controller.bootId,
|
||||
runnerArtifact: restoredController ? { version: restoredController.runnerVersion, digest: restoredController.runnerDigest } : undefined,
|
||||
} : undefined,
|
||||
...(overlapGitHubPreparation ? { prepareRunnerEnvironment } : {}),
|
||||
// Durable scope and prior-run verification use the controller's
|
||||
// canonical workspace identity. createRunnerdBackend separately
|
||||
// projects remoteCwd into the provider execution boundary.
|
||||
execution: input.execution,
|
||||
runnerInstanceId: effectiveRunnerInstanceId,
|
||||
pluginTaskDescription,
|
||||
retainedPluginTaskOwner: existingWarmSession ? pluginTaskSessionOwners.get(existingWarmSession) : undefined,
|
||||
durableEnvironmentLeaseId: durableRunnerBinding?.environmentLeaseId,
|
||||
remoteWarmRecovery: remoteWarmRecovery ? {
|
||||
...remoteWarmRecovery,
|
||||
revalidate: async () => {
|
||||
if (remoteWarmRecoveryPending) await remoteWarmRecovery.revalidate(leaseOwner);
|
||||
},
|
||||
} : undefined,
|
||||
trace,
|
||||
toolTrace,
|
||||
});
|
||||
let runnerdBackend =
|
||||
input.useRunnerd && input.backend === undefined
|
||||
? await timeController("backend_setup", () => {
|
||||
const create = () => createRunnerdBackend({
|
||||
...input,
|
||||
onDiagnosticLease: ownDiagnosticLease,
|
||||
providerPackAdmissionSource,
|
||||
recoveryPurpose: claimedStopCleanup ? "stop_cleanup" : "execution",
|
||||
executionContext,
|
||||
onAccountingEvent: producerUsage?.capture,
|
||||
controllerPersistence: input.runnerExecutionTarget?.kind === "remote" ? {
|
||||
bootId: controller.bootId,
|
||||
runnerArtifact: restoredController ? { version: restoredController.runnerVersion, digest: restoredController.runnerDigest } : undefined,
|
||||
} : undefined,
|
||||
...(overlapGitHubPreparation ? { prepareRunnerEnvironment } : {}),
|
||||
// Durable scope and prior-run verification use the controller's
|
||||
// canonical workspace identity. createRunnerdBackend separately
|
||||
// projects remoteCwd into the provider execution boundary.
|
||||
execution: input.execution,
|
||||
runnerInstanceId: effectiveRunnerInstanceId,
|
||||
pluginTaskDescription,
|
||||
retainedPluginTaskOwner: existingWarmSession ? pluginTaskSessionOwners.get(existingWarmSession) : undefined,
|
||||
durableEnvironmentLeaseId: durableRunnerBinding?.environmentLeaseId,
|
||||
remoteWarmRecovery: remoteWarmRecovery ? {
|
||||
...remoteWarmRecovery,
|
||||
revalidate: async () => {
|
||||
if (remoteWarmRecoveryPending) await remoteWarmRecovery.revalidate(leaseOwner);
|
||||
},
|
||||
} : undefined,
|
||||
trace,
|
||||
toolTrace,
|
||||
});
|
||||
return executionContext ? executionContext.wait(create) : create();
|
||||
})
|
||||
? await timeController("backend_setup", () => executionContext ? executionContext.wait(createExecutionBackend) : createExecutionBackend())
|
||||
: null;
|
||||
if (remoteWarmRecovery) {
|
||||
if (executionContext) await executionContext.wait(() => remoteWarmRecovery.revalidate(leaseOwner));
|
||||
@@ -10877,7 +10875,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
durableEnvironmentLeaseId: durableRunnerBinding?.environmentLeaseId, trace, toolTrace });
|
||||
}
|
||||
}
|
||||
const remoteCleanupLease = input.runnerExecutionTarget?.kind === "remote" &&
|
||||
const readRemoteCleanupLease = async () => input.runnerExecutionTarget?.kind === "remote" &&
|
||||
input.runnerExecutionTarget.transport === "sandbox" && input.runnerExecutionTarget.leaseId
|
||||
? await input.db.select({ provider: environmentLeases.provider, providerLeaseId: environmentLeases.providerLeaseId })
|
||||
.from(environmentLeases).where(and(
|
||||
@@ -10885,6 +10883,26 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
eq(environmentLeases.id, input.runnerExecutionTarget.leaseId),
|
||||
)).then(rows => rows[0])
|
||||
: null;
|
||||
let remoteCleanupLease = await readRemoteCleanupLease();
|
||||
const continueAfterUnavailableReuse = async (error: unknown) => {
|
||||
const target = input.runnerExecutionTarget;
|
||||
if (!(error instanceof Error) || error.message !== "native_runner_reuse_unavailable" || !existingWarmSession
|
||||
|| pluginTaskSessionOwners.get(existingWarmSession)?.retiredBeforeClaim() !== true
|
||||
|| remoteWarmRecovery || warmConfigDigest === null || target?.kind !== "remote" || target.transport !== "sandbox"
|
||||
|| !target.runnerTask || runnerdBackend === null) return false;
|
||||
const retainedTaskId = target.sandboxLeaseAcquisition?.providerLeaseId;
|
||||
await existingWarmSession.close({ reason: "retained Runner was unavailable for reuse" });
|
||||
const inspect = () => target.runnerTask!({ kind: "describe" }, executionContext ? { executionContext } : undefined);
|
||||
const description = environmentTaskResultSchema.parse(await (executionContext ? executionContext.wait(inspect) : inspect()));
|
||||
if (description.kind !== "description" || description.taskId === retainedTaskId) return false;
|
||||
existingWarmSession = undefined;
|
||||
pluginTaskDescription = undefined;
|
||||
await finishStartupContextForFallback();
|
||||
persistedWarmSession = loadWarmNativeCheckpoint(input.execution, warmConfigDigest, target.kind);
|
||||
runnerdBackend = await createExecutionBackend();
|
||||
remoteCleanupLease = await readRemoteCleanupLease();
|
||||
return true;
|
||||
};
|
||||
nativeSessionExecuteStartedAtMs = Date.now();
|
||||
native = await trace.measure(
|
||||
"native.session.execute",
|
||||
@@ -10894,7 +10912,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
startedAtMs: nativeSessionExecuteStartedAtMs,
|
||||
});
|
||||
trace.activate(runnerSessionStartupScope);
|
||||
const result = await trace.run(runnerSessionStartupScope, () =>
|
||||
const runExecution = () =>
|
||||
executeNativeSession({
|
||||
resumeInterruptedTurn: input.restartRecovery?.kind === "resume_dead_runner",
|
||||
getFreshSessionHandoff: input.getFreshSessionHandoff,
|
||||
@@ -11274,8 +11292,14 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
let result: Awaited<ReturnType<typeof runExecution>>;
|
||||
try {
|
||||
result = await trace.run(runnerSessionStartupScope, runExecution);
|
||||
} catch (error) {
|
||||
if (!await continueAfterUnavailableReuse(error)) throw error;
|
||||
result = await trace.run(runnerSessionStartupScope, runExecution);
|
||||
}
|
||||
await trace.end(runnerSessionStartupScope, {
|
||||
outcome:
|
||||
result.terminal.runTerminalState === "succeeded" ? "ok" : "failed",
|
||||
|
||||
@@ -45,10 +45,12 @@ const parkId = (source: DurableRecoveryIdentity) => `park_${sha(JSON.stringify(O
|
||||
describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execution entry", () => {
|
||||
const environment = nativeRestartFixture();
|
||||
const nativeIt = process.platform === "win32" ? it.skip : realProcessIt;
|
||||
nativeIt.for([...(["codex", "claude"] as const).flatMap(harness => (["cached", "detached", "reused lease", "prepared copy", "missing stop", "missing state", "foreign state", "working state"] as const).map(mode => [harness, mode] as const))])("continues %s after graceful eviction with a %s Core owner", { timeout: 60_000 }, async ([harness, mode], context) => {
|
||||
nativeIt.for([...(["codex", "claude"] as const).flatMap(harness => (["cached", "detached", "reused lease", "after inspection", "bare unavailable", "prepared copy", "missing stop", "missing state", "foreign state", "working state"] as const).map(mode => [harness, mode] as const))])("continues %s after graceful eviction with a %s Core owner", { timeout: 60_000 }, async ([harness, mode], context) => {
|
||||
if (harness === "claude" && !existsSync(fakeAcpxSidecar)) context.skip();
|
||||
const detached = mode === "detached" || mode === "missing state" || mode === "foreign state";
|
||||
const denied = mode.startsWith("missing") || mode.endsWith("state");
|
||||
const unavailableResume = mode === "after inspection" || mode === "bare unavailable";
|
||||
const reusesLease = mode === "reused lease" || unavailableResume;
|
||||
const instructionCopy = (id: string) => ({ runId: id, root: workspace, preparationKey: "unchanged-fixture-instructions", hasChanges: async () => false, collectStopped: async () => {} });
|
||||
const first = await environment.seedRun(`RELEASE-ENTRY-${harness}-${mode}`);
|
||||
const { db, runId, issueId, native } = first;
|
||||
@@ -132,6 +134,7 @@ describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execu
|
||||
const releases: DurableRecoveryIdentity[] = [], claims: DurableRecoveryIdentity[] = [];
|
||||
let handle: RunnerProcessHandle | undefined, submissions = 0, cleaning = false, sourceReadRequests = 0, sourceReadResponses = 0;
|
||||
let pendingFollowup: Promise<unknown> | undefined;
|
||||
let inspectedLive = false, resumeAttempts = 0, acquisitions = 0, evictRetained: (() => Promise<void>) | undefined;
|
||||
const providerStarts = async () => (await readFile(calls, "utf8")).trim().split(/\r?\n/)
|
||||
.filter(line => harness === "codex" ? line === "process-start" : JSON.parse(line).processStart).length;
|
||||
const state = async (taskId: string = taskA) => JSON.parse(await readFile(join(runnerRoot(taskId), "runner-state.json"), "utf8"));
|
||||
@@ -139,8 +142,14 @@ describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execu
|
||||
const task = async (raw: unknown, authorityRunId: string, taskId: string): Promise<unknown> => {
|
||||
const host = hosts.get(taskId)!;
|
||||
const operation = environmentTaskOperationSchema.parse(raw);
|
||||
if (operation.kind === "describe") return { kind: "description", taskId, launchPreparationVersion: 1,
|
||||
projectAuthCheckVersion: 1, warmTaskVersion: 1, releaseResumeVersion: 1, runner: artifact };
|
||||
if (operation.kind === "describe") {
|
||||
if (unavailableResume && authorityRunId !== runId && !inspectedLive) {
|
||||
expect(handles.get(taskA)?.isExitConfirmed?.()).toBe(false);
|
||||
inspectedLive = true;
|
||||
}
|
||||
return { kind: "description", taskId, launchPreparationVersion: 1,
|
||||
projectAuthCheckVersion: 1, warmTaskVersion: 1, releaseResumeVersion: 1, runner: artifact };
|
||||
}
|
||||
if (operation.kind === "prepare") {
|
||||
expect(operation.requirements.identity.runId).toBe(authorityRunId);
|
||||
expect(operation.accountIdentity).toBe("fixture-account");
|
||||
@@ -205,6 +214,13 @@ describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execu
|
||||
if (reply.status !== "parked") throw new Error("Fixture native release refused");
|
||||
releases.push(operation.identity); return { kind: "released", taskId };
|
||||
}
|
||||
if (operation.kind === "resume" && unavailableResume) {
|
||||
resumeAttempts++;
|
||||
expect(inspectedLive).toBe(true);
|
||||
if (mode === "after inspection") await evictRetained!();
|
||||
expect(handles.get(taskA)?.isExitConfirmed?.()).toBe(mode === "after inspection");
|
||||
return { kind: "resume", taskId, outcome: "unavailable" };
|
||||
}
|
||||
if (operation.kind === "resume") {
|
||||
const retainedProviderCount = await providerStarts();
|
||||
const reply = await host.request({ op: "claim", id: `claim_${sha(operation.bootstrapTicket)}`, parkId: parkId(operation.identity), bootstrapTicket: operation.bootstrapTicket });
|
||||
@@ -213,7 +229,10 @@ describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execu
|
||||
claims.push(operation.identity);
|
||||
return { kind: "resume", taskId, outcome: "resumed" };
|
||||
}
|
||||
if (operation.kind === "stop") { await host.close(); }
|
||||
if (operation.kind === "stop") {
|
||||
if (mode === "after inspection" && taskId === taskA) await new Promise(done => setTimeout(done, 300));
|
||||
await host.close();
|
||||
}
|
||||
if (operation.kind === "complete") { expect(handles.get(taskId)?.isExitConfirmed?.()).toBe(true); }
|
||||
return { kind: "accepted", taskId };
|
||||
};
|
||||
@@ -227,6 +246,7 @@ describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execu
|
||||
};
|
||||
const proof = async () => (await supervisor()).releasedRunner;
|
||||
const workers = { getWorker: () => ({ supportedMethods: ["environmentTask"] }), call: async (_id: string, method: string, params: any, _timeout?: number, _unused?: unknown, settled?: () => void) => {
|
||||
if (method === "environmentAcquireLease") acquisitions++;
|
||||
if (method === "environmentAcquireLease") return { providerLeaseId: taskB, metadata: { creationOperationId: params.creationOperationId } };
|
||||
try { return await task(params.operation, params.runId, params.taskId); } finally { settled?.(); }
|
||||
} } as unknown as PluginWorkerManager;
|
||||
@@ -237,7 +257,7 @@ describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execu
|
||||
const execute = async (current: typeof execution, leaseId: string) => {
|
||||
await db.update(heartbeatRuns).set({ runnerProfileJson: { nativeExecutionInput: current }, runnerInstanceId: native.runnerInstanceId, completionContractId: contract!.id, completionContractSha256: contract!.canonicalSha256, nativeSessionId: current.session.normalizedSessionId })
|
||||
.where(eq(heartbeatRuns.id, current.binding.runId));
|
||||
const target = mode === "reused lease" ? await resolveEnvironmentExecutionTarget({ db, companyId, adapterType: "paperclip_runner",
|
||||
const target = reusesLease ? await resolveEnvironmentExecutionTarget({ db, companyId, adapterType: "paperclip_runner",
|
||||
environment: { id: environmentId, driver: "plugin", config: { pluginKey: pluginId, driverKey: "tasks" } }, leaseId,
|
||||
lease: await environmentService(db).getLeaseById(leaseId), leaseMetadata: metadata,
|
||||
environmentRuntime: environmentRuntimeService(db, { pluginWorkerManager: workers }) }) : {
|
||||
@@ -274,7 +294,8 @@ describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execu
|
||||
await db.update(environmentLeases).set({ status: "released", releasedAt: new Date(), cleanupStatus: "success" }).where(eq(environmentLeases.id, leaseA));
|
||||
}
|
||||
};
|
||||
if (mode !== "reused lease") await evict();
|
||||
evictRetained = evict;
|
||||
if (!reusesLease) await evict();
|
||||
const admitNext = async () => {
|
||||
const runB = randomUUID(), leaseB = randomUUID();
|
||||
await db.insert(heartbeatRuns).values({ id: runB, companyId, agentId, nativeIssueId: issueId, status: "running", runtimeMode: "native" });
|
||||
@@ -282,8 +303,8 @@ describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execu
|
||||
await db.update(issues).set({ executionRunId: runB, status: "in_progress" }).where(eq(issues.id, issueId));
|
||||
const [oldLease] = await db.select().from(environmentLeases).where(eq(environmentLeases.id, leaseA));
|
||||
await db.insert(environmentLeases).values({ id: leaseB, companyId, environmentId, heartbeatRunId: runB, issueId, executionWorkspaceId: workspaceId,
|
||||
provider: providerKey, providerLeaseId: mode === "reused lease" ? taskA : taskB, status: "active", leasePolicy: "reuse_by_environment",
|
||||
metadata: mode === "reused lease" ? { ...oldLease!.metadata, runnerTaskReused: true, runnerTaskPreviousLeaseId: leaseA,
|
||||
provider: providerKey, providerLeaseId: reusesLease ? taskA : taskB, status: "active", leasePolicy: "reuse_by_environment",
|
||||
metadata: reusesLease ? { ...oldLease!.metadata, runnerTaskReused: true, runnerTaskPreviousLeaseId: leaseA,
|
||||
runnerTaskExecutionStopped: false } : { ...metadata, remoteCwd: workspace } });
|
||||
if (mode === "reused lease") {
|
||||
expect(handles.get(taskA)!.isExitConfirmed?.()).toBe(false);
|
||||
@@ -295,6 +316,12 @@ describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execu
|
||||
preparationKey: "unchanged-fixture-instructions", adopt: async () => instructionCopy(runB) }) : null;
|
||||
if (mode === "prepared copy") expect(releasePreparation).toBeTypeOf("function");
|
||||
pendingFollowup = execute(next, leaseB);
|
||||
if (mode === "bare unavailable") {
|
||||
await expect(pendingFollowup).rejects.toThrow("native_runner_reuse_unavailable");
|
||||
expect({ resumeAttempts, acquisitions, submissions, claims }).toEqual({ resumeAttempts: 1, acquisitions: 0, submissions: 1, claims: [] });
|
||||
expect(await db.select().from(nativeRunResults).where(eq(nativeRunResults.runId, runB))).toHaveLength(0);
|
||||
return runB;
|
||||
}
|
||||
if (denied) {
|
||||
const refusal = mode === "working state" ? "checkpoint_not_settled"
|
||||
: mode === "foreign state" ? "Plugin Runner checkpoint identity mismatch"
|
||||
@@ -316,12 +343,13 @@ describeEmbeddedPostgres("plugin Runner graceful eviction through the Core execu
|
||||
return runB;
|
||||
};
|
||||
const runB = await admitNext();
|
||||
if (denied) return;
|
||||
if (denied || mode === "bare unavailable") return;
|
||||
await waitForCondition("automatic release B", () => releases.length === 2);
|
||||
expect(await proof()).toMatchObject({ source: releases[1], taskId: taskB });
|
||||
expect((await supervisor()).snapshot.providerSessionId).toBe(providerSessionId);
|
||||
const methods = (await readFile(calls, "utf8")).trim().split(/\r?\n/);
|
||||
expect(submissions).toBe(2);
|
||||
if (mode === "after inspection") expect({ resumeAttempts, acquisitions }).toEqual({ resumeAttempts: 1, acquisitions: 1 });
|
||||
expect(releases[1]!.runId).toBe(runB);
|
||||
expect(claims).toEqual([]);
|
||||
if (harness === "codex") {
|
||||
|
||||
@@ -653,7 +653,25 @@ describe("plugin Runner release/resume facade", () => {
|
||||
const owner = createPluginRunnerTaskOwner({ ...input("codex"), task });
|
||||
expect(await owner.driver.adoptExistingRunner!.isAlive()).toBe(false);
|
||||
await expect(owner.resume(requirements.identity, "transient-ticket")).resolves.toBe("unavailable");
|
||||
expect(task.mock.calls.map(([operation]) => operation.kind)).toEqual(["status", "resume"]);
|
||||
expect(task.mock.calls.map(([operation]) => operation.kind)).toEqual(["status", "resume", "status"]);
|
||||
expect(owner.retiredBeforeClaim()).toBe(false); // stopped, but no saved session state
|
||||
});
|
||||
|
||||
it("records a refused resume as retired only when the exact task stopped with saved session state", async () => {
|
||||
for (const [status, retired] of [
|
||||
[{ kind: "status", taskId: "task", phase: "cancelled", executionStopped: true, sessionStateAvailable: true }, true],
|
||||
[{ kind: "status", taskId: "task", phase: "running", executionStopped: false, sessionStateAvailable: false }, false],
|
||||
[new Error("fixture status reply lost"), false],
|
||||
] as const) {
|
||||
const task = vi.fn(async (operation: any): Promise<unknown> => {
|
||||
if (operation.kind === "resume") return { kind: "resume", taskId: "task", outcome: "unavailable" };
|
||||
if (status instanceof Error) throw status;
|
||||
return status;
|
||||
});
|
||||
const owner = createPluginRunnerTaskOwner({ ...input("codex"), task });
|
||||
await expect(owner.resume(requirements.identity, "transient-ticket")).resolves.toBe("unavailable");
|
||||
expect(owner.retiredBeforeClaim()).toBe(retired);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(["release", "resume"] as const)("validates exact task and closed %s result", async operation => {
|
||||
|
||||
@@ -73,6 +73,7 @@ export function createPluginRunnerTaskOwner(input: {
|
||||
let confirmStopped: (code: number) => void = () => {};
|
||||
let stoppedState: Extract<PluginEnvironmentTaskResult, { kind: "status" }> | undefined;
|
||||
let completed = false;
|
||||
let retiredBeforeClaim = false;
|
||||
let rejectFailure!: (error: unknown) => void;
|
||||
const failure = new Promise<never>((_resolve, reject) => { rejectFailure = reject; });
|
||||
void failure.catch(() => undefined);
|
||||
@@ -413,12 +414,17 @@ export function createPluginRunnerTaskOwner(input: {
|
||||
const result = await call(environmentTaskOperationSchema.parse({ kind: "resume", identity, bootstrapTicket }));
|
||||
requireReleaseResume();
|
||||
if (result.kind !== "resume") throw new Error("Plugin Runner resume result unavailable");
|
||||
if (result.outcome === "unavailable") {
|
||||
const state = await call({ kind: "status" }).catch(() => undefined);
|
||||
retiredBeforeClaim = state?.kind === "status" && state.executionStopped === true && state.sessionStateAvailable === true;
|
||||
}
|
||||
return result.outcome;
|
||||
};
|
||||
return { launcher, registration, driver, readWorkspaceFile, release, resume,
|
||||
// This is a lease-bound Launcher receipt, not a terminal phase or a sent
|
||||
// stop request. Completion also proves credential/writeback obligations ran.
|
||||
isRetired: () => stoppedState?.executionStopped === true && completed && !detached,
|
||||
retiredBeforeClaim: () => retiredBeforeClaim,
|
||||
rebind(next: typeof input) {
|
||||
if (!launched || stopRequested || stoppedState || detached || next.taskId !== taskId || next.harness !== input.harness ||
|
||||
input.accountIdentity !== next.accountIdentity || input.credentialIdentity !== next.credentialIdentity ||
|
||||
|
||||
Reference in new issue
Block a user