fix: force a fresh task session when historical adapter evidence is absent
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
345b84063a
commit
1cc814e30e
|
|
@ -991,3 +991,5 @@ pending approvals, task completion, reassignment, and later executions still gat
|
|||
admission. Native runners keep their supported reattachment protocol.
|
||||
|
||||
Historical released reusable leases without termination receipts are not reclaimed by an old run. A concurrent resume can own the provider resource before its new lease row exists. Those runs remain gated until termination is confirmed. New cleanup attempts retain their durable pending-cleanup ownership.
|
||||
|
||||
For historical task-bound runs with no adapter evidence, termination authorizes a fresh task conversation with the currently assigned conversational agent. It does not establish which adapter ran previously. Recovery records that identity as unknown and forces a fresh session while supplying the task history and latest request. This is an intentional exception to historical adapter classification, not permission to replay old process, webhook, or tool commands. Known non-conversational dispatch evidence remains ineligible for this automatic path.
|
||||
|
|
|
|||
|
|
@ -137,6 +137,20 @@ const support = await getEmbeddedPostgresTestSupport();
|
|||
expect(old).toMatchObject({ status: "failed", errorCode: "process_lost" });
|
||||
expect(old.resultJson?.automaticSandboxRecovery).toMatchObject({ actionOutcomes: "unknown" });
|
||||
});
|
||||
it("starts fresh after termination when historical adapter identity is unavailable", async () => {
|
||||
const f = await seed();
|
||||
// Old installs did not record the adapter before provisioning. Changing
|
||||
// current settings cannot reveal it; the recovery audit must keep it unknown.
|
||||
await db.update(agents).set({ adapterType: "process" }).where(eq(agents.id, f.agentId));
|
||||
await db.update(agents).set({ adapterType: "claude_local" }).where(eq(agents.id, f.agentId));
|
||||
await heartbeatService(db).resumeInterruptedSandboxRuns();
|
||||
const [next] = await successors(f.run.id);
|
||||
expect(next.contextSnapshot?.forceFreshSession).toBe(true);
|
||||
const [old] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.run.id));
|
||||
expect(old.resultJson?.automaticSandboxRecovery).toMatchObject({
|
||||
priorAdapter: "unknown", actionOutcomes: "unknown", continuation: "fresh_task_conversation",
|
||||
});
|
||||
});
|
||||
it("queues missing termination proof for cleanup, then resumes after a restart", async () => {
|
||||
const f = await seed(false);
|
||||
expect(await prepareAutomaticSandboxContinuation(db, f.run)).toBeNull();
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ export async function prepareAutomaticSandboxContinuation(db: Db, source: typeof
|
|||
if (!agent || !isConversationAdapter(agent.adapterType)) return null;
|
||||
const recorded = await recordedRunAdapter(tx as unknown as Db, run);
|
||||
if (recorded && !isConversationAdapter(recorded)) return null;
|
||||
// Missing historical evidence does not establish the old adapter type.
|
||||
// After exact termination, it permits only a fresh task conversation with
|
||||
// the currently assigned conversational agent, never old session replay.
|
||||
// A later execution owns current task work. Never revive an older request.
|
||||
if (await hasLaterSandboxExecution(tx as unknown as Db, run, issue.id)) return null;
|
||||
const leases = await tx.select().from(environmentLeases).where(and(
|
||||
|
|
@ -80,9 +83,11 @@ export async function prepareAutomaticSandboxContinuation(db: Db, source: typeof
|
|||
// Remote process numbers belong to the remote namespace, not this host.
|
||||
const [ready] = await tx.update(heartbeatRuns).set({
|
||||
processPid: null, processGroupId: null, processStartedAt: null,
|
||||
...(!recorded ? { contextSnapshot: { ...run.contextSnapshot, forceFreshSession: true } } : {}),
|
||||
resultJson: sql`coalesce(${heartbeatRuns.resultJson}, '{}'::jsonb) || ${JSON.stringify({
|
||||
conversationContinuation: CONVERSATION_CONTINUATION_POLICY,
|
||||
automaticSandboxRecovery: { state: "provider_terminated", actionOutcomes: "unknown" },
|
||||
automaticSandboxRecovery: { state: "provider_terminated", actionOutcomes: "unknown",
|
||||
priorAdapter: recorded ?? "unknown", continuation: recorded ? "conversation_turn" : "fresh_task_conversation" },
|
||||
})}::jsonb`,
|
||||
}).where(and(eq(heartbeatRuns.id, run.id), eq(heartbeatRuns.companyId, run.companyId))).returning();
|
||||
const retired = await tx.update(issueRecoveryActions).set({
|
||||
|
|
|
|||
Loading…
Reference in New Issue