mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
fix(runner): finish durable duplicate resume
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
ff1e497334
commit
b0fff1ec23
4 files changed
+47
-3
No files matched your search
@@ -532,7 +532,30 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
);
|
||||
expect(resumed.mockState().comments).toHaveLength(1);
|
||||
expect(resumed.mockState().idempotency).toHaveLength(1);
|
||||
expect(resumed.snapshot().evidence.filter((entry) => entry.kind === "tool_result")).toHaveLength(2);
|
||||
const replayedSnapshot = resumed.snapshot();
|
||||
expect(replayedSnapshot.terminalTurns).toEqual([
|
||||
expect.objectContaining({ turnId: "turn-1", status: "interrupted" }),
|
||||
expect.objectContaining({
|
||||
turnId: "turn-1:durable-duplicate-replay",
|
||||
status: "completed",
|
||||
reconciledByAttemptId: "attempt-terminal-resumed",
|
||||
}),
|
||||
]);
|
||||
expect(replayedSnapshot.evidence.filter((entry) => entry.kind === "tool_result")).toHaveLength(2);
|
||||
const receipts = replayedSnapshot.evidence.filter((entry) => entry.kind === "tool_result");
|
||||
expect(receipts.map((entry) => (entry.data.result as Record<string, unknown>).ok)).toEqual([true, true]);
|
||||
expect(receipts.map((entry) => (
|
||||
((entry.data.result as Record<string, unknown>).result as Record<string, unknown>).disposition
|
||||
))).toEqual(["applied", "duplicate"]);
|
||||
expect(receipts.every((entry) => (
|
||||
Number(entry.data.beforeRevision) <= Number(entry.data.afterRevision)
|
||||
))).toBe(true);
|
||||
expect(receipts.every((entry) => (
|
||||
Number((entry.data.result as Record<string, unknown>).stateRevision)
|
||||
=== Number(entry.data.afterRevision)
|
||||
))).toBe(true);
|
||||
expect(receipts.at(-1)?.data.afterRevision).toBe(resumed.mockState().revision);
|
||||
expect(state.transports[1]!.requests.some((request) => request.method === "turn/start")).toBe(false);
|
||||
|
||||
const rejected = await state.transports[1]!.invokeServerRequest({
|
||||
id: "request-turn-1:new-key",
|
||||
@@ -552,6 +575,11 @@ describe("Capability live runnerd and Codex session", () => {
|
||||
expect(resumed.mockState().comments).toHaveLength(1);
|
||||
expect(resumed.mockState().idempotency).toHaveLength(1);
|
||||
expect(resumed.snapshot().evidence.filter((entry) => entry.kind === "tool_result")).toHaveLength(2);
|
||||
await resumed.completeAttempt("succeeded");
|
||||
expect((await store.load(resumed.id))?.attempts).toMatchObject([
|
||||
{ attemptId: "attempt-terminal-killed", status: "terminated" },
|
||||
{ attemptId: "attempt-terminal-resumed", status: "succeeded" },
|
||||
]);
|
||||
await resumedService.shutdown(resumed.id, "terminal replay test complete");
|
||||
});
|
||||
|
||||
|
||||
@@ -1381,18 +1381,25 @@ export class CapabilityLiveSession {
|
||||
afterRevision: result.stateRevision,
|
||||
});
|
||||
this.#emit({ turnId, kind: "activity", reason: "tool_result" });
|
||||
await this.#persist();
|
||||
if (terminalReplay && replayDisposition !== "duplicate") {
|
||||
await this.#persist();
|
||||
return {
|
||||
success: false,
|
||||
contentItems: [{ type: "inputText", text: "Terminal-turn replay did not resolve to a durable duplicate." }],
|
||||
};
|
||||
}
|
||||
if (terminalReplay) {
|
||||
this.#recordTerminalFact(this.#durableReplayTurnId(turnId), "completed");
|
||||
}
|
||||
await this.#persist();
|
||||
return this.#codexToolResponse(result);
|
||||
}
|
||||
|
||||
#isDurableTerminalReplay(turnId: string, input: unknown): boolean {
|
||||
if (turnId.length === 0 || this.#activeTurnId !== null) return false;
|
||||
if (this.#terminalTurns.some(
|
||||
(candidate) => candidate.turnId === this.#durableReplayTurnId(turnId),
|
||||
)) return false;
|
||||
const terminal = this.#terminalTurns.find((candidate) => candidate.turnId === turnId);
|
||||
if (
|
||||
terminal?.status !== "interrupted" ||
|
||||
@@ -1406,6 +1413,10 @@ export class CapabilityLiveSession {
|
||||
);
|
||||
}
|
||||
|
||||
#durableReplayTurnId(interruptedTurnId: string): string {
|
||||
return `${interruptedTurnId}:durable-duplicate-replay`;
|
||||
}
|
||||
|
||||
#codexToolResponse(result: CapabilitySemanticToolResult): Record<string, unknown> {
|
||||
return {
|
||||
success: result.ok,
|
||||
|
||||
@@ -214,6 +214,7 @@ describe("CapabilityMockControlPlaneAdapter", () => {
|
||||
});
|
||||
const replayed = await adapter.applyCommand(envelope);
|
||||
expect(replayed.disposition).toBe("duplicate");
|
||||
expect(replayed.stateRevision).toBe(adapter.snapshot().revision);
|
||||
expect(adapter.snapshot().comments.filter((comment) => comment.body === "Exactly once.")).toHaveLength(1);
|
||||
expect(adapter.decisionRecords().map((decision) => decision.outcome)).toContain("faulted");
|
||||
expect(adapter.decisionRecords().map((decision) => decision.outcome)).toContain("duplicate");
|
||||
|
||||
@@ -413,7 +413,11 @@ export class CapabilityMockControlPlaneAdapter implements CapabilityMockControlP
|
||||
this.#recordDecisionOnly(run.id, envelope.command.kind, "duplicate", "command_already_applied", [
|
||||
...existing.result.entityRefs,
|
||||
]);
|
||||
return deepFreeze({ ...clone(existing.result), disposition: "duplicate" });
|
||||
return deepFreeze({
|
||||
...clone(existing.result),
|
||||
disposition: "duplicate",
|
||||
stateRevision: this.#state.revision,
|
||||
});
|
||||
}
|
||||
if (
|
||||
envelope.expectedRevision !== undefined &&
|
||||
|
||||
Reference in new issue
Block a user