authorgravatar for 94890352+Adolanium@users.noreply.github.comAdolanium <94890352+Adolanium@users.noreply.github.com> 2026-09-17 04:52:33+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-16 18:52:33-07:00
logd42a3bd2ae3e698141b26f6426aa28d8886e1754
treec9a7dc51b06012b65a874a423a0e4c46514dbb11
parent88d0c40c8581f8886b8c94496c3e4af105851424
signature Signed by PGP key B5690EEEBB952194

fix(server): keep ready checkpoints when a later placeholder arrives (#8432)

Co-authored-by: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com>

2 files changed, 136 insertions(+), 0 deletions(-)

apps/server/src/orchestration/Layers/ProjectionPipeline.test.ts+124
......@@ -3967,6 +3967,130 @@ it.layer(BaseTestLayer)("OrchestrationProjectionPipeline", (it) => {
39673967 ]);
39683968 }),
39693969 );
3970
3971 it.effect("does not let a later missing placeholder clobber a ready checkpoint", () =>
3972 Effect.gen(function* () {
3973 const projectionPipeline = yield* OrchestrationProjectionPipeline;
3974 const eventStore = yield* OrchestrationEventStore;
3975 const sql = yield* SqlClient.SqlClient;
3976 const appendAndProject = (event: Parameters<typeof eventStore.append>[0]) =>
3977 eventStore
3978 .append(event)
3979 .pipe(Effect.flatMap((savedEvent) => projectionPipeline.projectEvent(savedEvent)));
3980
3981 yield* appendAndProject({
3982 type: "project.created",
3983 eventId: EventId.make("evt-checkpoint-guard-1"),
3984 aggregateKind: "project",
3985 aggregateId: ProjectId.make("project-checkpoint-guard"),
3986 occurredAt: "2026-02-26T14:00:00.000Z",
3987 commandId: CommandId.make("cmd-checkpoint-guard-1"),
3988 causationEventId: null,
3989 correlationId: CorrelationId.make("cmd-checkpoint-guard-1"),
3990 metadata: {},
3991 payload: {
3992 projectId: ProjectId.make("project-checkpoint-guard"),
3993 title: "Project Checkpoint Guard",
3994 workspaceRoot: "/tmp/project-checkpoint-guard",
3995 defaultModelSelection: null,
3996 scripts: [],
3997 createdAt: "2026-02-26T14:00:00.000Z",
3998 updatedAt: "2026-02-26T14:00:00.000Z",
3999 },
4000 });
4001
4002 yield* appendAndProject({
4003 type: "thread.created",
4004 eventId: EventId.make("evt-checkpoint-guard-2"),
4005 aggregateKind: "thread",
4006 aggregateId: ThreadId.make("thread-checkpoint-guard"),
4007 occurredAt: "2026-02-26T14:00:01.000Z",
4008 commandId: CommandId.make("cmd-checkpoint-guard-2"),
4009 causationEventId: null,
4010 correlationId: CorrelationId.make("cmd-checkpoint-guard-2"),
4011 metadata: {},
4012 payload: {
4013 threadId: ThreadId.make("thread-checkpoint-guard"),
4014 projectId: ProjectId.make("project-checkpoint-guard"),
4015 title: "Thread Checkpoint Guard",
4016 modelSelection: {
4017 instanceId: ProviderInstanceId.make("codex"),
4018 model: "gpt-5-codex",
4019 },
4020 runtimeMode: "full-access",
4021 interactionMode: "default",
4022 branch: null,
4023 worktreePath: null,
4024 createdAt: "2026-02-26T14:00:01.000Z",
4025 updatedAt: "2026-02-26T14:00:01.000Z",
4026 },
4027 });
4028
4029 yield* appendAndProject({
4030 type: "thread.turn-diff-completed",
4031 eventId: EventId.make("evt-checkpoint-guard-3"),
4032 aggregateKind: "thread",
4033 aggregateId: ThreadId.make("thread-checkpoint-guard"),
4034 occurredAt: "2026-02-26T14:00:02.000Z",
4035 commandId: CommandId.make("cmd-checkpoint-guard-3"),
4036 causationEventId: null,
4037 correlationId: CorrelationId.make("cmd-checkpoint-guard-3"),
4038 metadata: {},
4039 payload: {
4040 threadId: ThreadId.make("thread-checkpoint-guard"),
4041 turnId: TurnId.make("turn-ready"),
4042 checkpointTurnCount: 1,
4043 checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread-checkpoint-guard/turn/1"),
4044 status: "ready",
4045 files: [],
4046 assistantMessageId: MessageId.make("assistant-ready"),
4047 completedAt: "2026-02-26T14:00:02.000Z",
4048 },
4049 });
4050
4051 yield* appendAndProject({
4052 type: "thread.turn-diff-completed",
4053 eventId: EventId.make("evt-checkpoint-guard-4"),
4054 aggregateKind: "thread",
4055 aggregateId: ThreadId.make("thread-checkpoint-guard"),
4056 occurredAt: "2026-02-26T14:00:03.000Z",
4057 commandId: CommandId.make("cmd-checkpoint-guard-4"),
4058 causationEventId: null,
4059 correlationId: CorrelationId.make("cmd-checkpoint-guard-4"),
4060 metadata: {},
4061 payload: {
4062 threadId: ThreadId.make("thread-checkpoint-guard"),
4063 turnId: TurnId.make("turn-ready"),
4064 checkpointTurnCount: 1,
4065 checkpointRef: CheckpointRef.make("refs/t3/checkpoints/thread-checkpoint-guard/turn/1"),
4066 status: "missing",
4067 files: [],
4068 assistantMessageId: MessageId.make("assistant-ready"),
4069 completedAt: "2026-02-26T14:00:03.000Z",
4070 },
4071 });
4072
4073 const turnRows = yield* sql<{
4074 readonly turnId: string;
4075 readonly checkpointStatus: string | null;
4076 readonly checkpointRef: string | null;
4077 }>`
4078 SELECT
4079 turn_id AS "turnId",
4080 checkpoint_status AS "checkpointStatus",
4081 checkpoint_ref AS "checkpointRef"
4082 FROM projection_turns
4083 WHERE thread_id = 'thread-checkpoint-guard'
4084 `;
4085 assert.deepEqual(turnRows, [
4086 {
4087 turnId: "turn-ready",
4088 checkpointStatus: "ready",
4089 checkpointRef: "refs/t3/checkpoints/thread-checkpoint-guard/turn/1",
4090 },
4091 ]);
4092 }),
4093 );
39704094});
39714095
39724096it.layer(makeProjectionPipelinePrefixedTestLayer("t3-pending-turn-terminal-test-"))(
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+12
......@@ -1675,6 +1675,18 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
16751675 threadId: event.payload.threadId,
16761676 turnId: event.payload.turnId,
16771677 });
1678 // Do not let a placeholder (status "missing") overwrite a checkpoint
1679 // that has already been captured with a real git ref. In-memory
1680 // projectEvent already refuses this. SQL did not, so thread detail
1681 // and diffs could show missing after a ready capture.
1682 if (
1683 Option.isSome(existingTurn) &&
1684 existingTurn.value.checkpointStatus !== null &&
1685 existingTurn.value.checkpointStatus !== "missing" &&
1686 event.payload.status === "missing"
1687 ) {
1688 return;
1689 }
16781690 const nextState = event.payload.status === "error" ? "error" : "completed";
16791691 yield* projectionTurnRepository.clearCheckpointTurnConflict({
16801692 threadId: event.payload.threadId,