From c5e84c9079d682ca5e1505814ebb88c1bfdabfa5 Mon Sep 17 00:00:00 2001 From: Adolanium <94890352+Adolanium@users.noreply.github.com> Date: Thu, 27 Aug 2026 20:46:58 +0300 Subject: [PATCH] fix(server): keep ready checkpoints when a later placeholder arrives The in-memory projector already refuses a missing placeholder over a ready capture. SQL always wrote the later status. Thread detail and diffs read SQL, so a concurrent placeholder could hide a finished checkpoint. Skip the SQL write when the stored checkpoint is not missing and the new event is. --- .../Layers/ProjectionPipeline.test.ts | 124 ++++++++++++++++++ .../Layers/ProjectionPipeline.ts | 12 ++ 2 files changed, 136 insertions(+) diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts index d27caed4a8e2..9a77ca49e645 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts @@ -2626,6 +2626,130 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => { ]); }), ); + + it.effect("does not let a later missing placeholder clobber a ready checkpoint", () => + Effect.gen(function* () { + const projectionPipeline = yield* OrchestrationProjectionPipeline; + const eventStore = yield* OrchestrationEventStore; + const sql = yield* SqlClient.SqlClient; + const appendAndProject = (event: Parameters[0]) => + eventStore + .append(event) + .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent))); + + yield* appendAndProject({ + type: "project.created", + eventId: EventId.make("evt-checkpoint-guard-1"), + aggregateKind: "project", + aggregateId: ProjectId.make("project-checkpoint-guard"), + occurredAt: "2026-02-26T14:00:00.000Z", + commandId: CommandId.make("cmd-checkpoint-guard-1"), + causationEventId: null, + correlationId: CorrelationId.make("cmd-checkpoint-guard-1"), + metadata: {}, + payload: { + projectId: ProjectId.make("project-checkpoint-guard"), + title: "Project Checkpoint Guard", + workspaceRoot: "/tmp/project-checkpoint-guard", + defaultModelSelection: null, + scripts: [], + createdAt: "2026-02-26T14:00:00.000Z", + updatedAt: "2026-02-26T14:00:00.000Z", + }, + }); + + yield* appendAndProject({ + type: "thread.created", + eventId: EventId.make("evt-checkpoint-guard-2"), + aggregateKind: "thread", + aggregateId: ThreadId.make("thread-checkpoint-guard"), + occurredAt: "2026-02-26T14:00:01.000Z", + commandId: CommandId.make("cmd-checkpoint-guard-2"), + causationEventId: null, + correlationId: CorrelationId.make("cmd-checkpoint-guard-2"), + metadata: {}, + payload: { + threadId: ThreadId.make("thread-checkpoint-guard"), + projectId: ProjectId.make("project-checkpoint-guard"), + title: "Thread Checkpoint Guard", + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5-codex", + }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + createdAt: "2026-02-26T14:00:01.000Z", + updatedAt: "2026-02-26T14:00:01.000Z", + }, + }); + + yield* appendAndProject({ + type: "thread.turn-diff-completed", + eventId: EventId.make("evt-checkpoint-guard-3"), + aggregateKind: "thread", + aggregateId: ThreadId.make("thread-checkpoint-guard"), + occurredAt: "2026-02-26T14:00:02.000Z", + commandId: CommandId.make("cmd-checkpoint-guard-3"), + causationEventId: null, + correlationId: CorrelationId.make("cmd-checkpoint-guard-3"), + metadata: {}, + payload: { + threadId: ThreadId.make("thread-checkpoint-guard"), + turnId: TurnId.make("turn-ready"), + checkpointTurnCount: 1, + checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread-checkpoint-guard/turn/1"), + status: "ready", + files: [], + assistantMessageId: MessageId.make("assistant-ready"), + completedAt: "2026-02-26T14:00:02.000Z", + }, + }); + + yield* appendAndProject({ + type: "thread.turn-diff-completed", + eventId: EventId.make("evt-checkpoint-guard-4"), + aggregateKind: "thread", + aggregateId: ThreadId.make("thread-checkpoint-guard"), + occurredAt: "2026-02-26T14:00:03.000Z", + commandId: CommandId.make("cmd-checkpoint-guard-4"), + causationEventId: null, + correlationId: CorrelationId.make("cmd-checkpoint-guard-4"), + metadata: {}, + payload: { + threadId: ThreadId.make("thread-checkpoint-guard"), + turnId: TurnId.make("turn-ready"), + checkpointTurnCount: 1, + checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread-checkpoint-guard/turn/1"), + status: "missing", + files: [], + assistantMessageId: MessageId.make("assistant-ready"), + completedAt: "2026-02-26T14:00:03.000Z", + }, + }); + + const turnRows = yield* sql<{ + readonly turnId: string; + readonly checkpointStatus: string | null; + readonly checkpointRef: string | null; + }>` + SELECT + turn_id AS "turnId", + checkpoint_status AS "checkpointStatus", + checkpoint_ref AS "checkpointRef" + FROM projection_turns + WHERE thread_id = 'thread-checkpoint-guard' + `; + assert.deepEqual(turnRows, [ + { + turnId: "turn-ready", + checkpointStatus: "ready", + checkpointRef: "refs/t3/checkpoints/thread-checkpoint-guard/turn/1", + }, + ]); + }), + ); }); it.layer(makeProjectionPipelinePrefixedTestLayer("t3-pending-turn-terminal-test-"))( diff --git a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts index 348615b1033b..1e9ede1ac4bf 100644 --- a/apps/server/src/orchestration/Layers/ProjectionPipeline.ts +++ b/apps/server/src/orchestration/Layers/ProjectionPipeline.ts @@ -1445,6 +1445,18 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti threadId: event.payload.threadId, turnId: event.payload.turnId, }); + // Do not let a placeholder (status "missing") overwrite a checkpoint + // that has already been captured with a real git ref. In-memory + // projectEvent already refuses this. SQL did not, so thread detail + // and diffs could show missing after a ready capture. + if ( + Option.isSome(existingTurn) && + existingTurn.value.checkpointStatus !== null && + existingTurn.value.checkpointStatus !== "missing" && + event.payload.status === "missing" + ) { + return; + } const nextState = event.payload.status === "error" ? "error" : "completed"; yield* projectionTurnRepository.clearCheckpointTurnConflict({ threadId: event.payload.threadId,