authorgravatar for eggfriedricew.g.o@gmail.comeggfriedrice <eggfriedricew.g.o@gmail.com> 2026-03-10 23:54:47+00:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-03-10 16:54:47-07:00
loge8b0126371a0d72b2a2ad71a4829e452a109a566
tree5ed91761bbc10b4f99c61c6472b531e0379794b5
parent90d9a2ad1f1810b27d88f0dfc38304ec5b1de566
signature Signed by PGP key B5690EEEBB952194

fix: checkpoint diffs never resolve (shared PubSub subscription) (#595)


5 files changed, 297 insertions(+), 139 deletions(-)

apps/server/src/orchestration/Layers/CheckpointReactor.ts+247-118
...@@ -2,6 +2,7 @@ import {...@@ -2,6 +2,7 @@ import {
2 CommandId,2 CommandId,
3 EventId,3 EventId,
4 MessageId,4 MessageId,
5 type ProjectId,
5 ThreadId,6 ThreadId,
6 TurnId,7 TurnId,
7 type OrchestrationEvent,8 type OrchestrationEvent,
...@@ -147,88 +148,89 @@ const make = Effect.gen(function* () {...@@ -147,88 +148,89 @@ const make = Effect.gen(function* () {
147148
148 const isGitWorkspace = (cwd: string) => isGitRepository(cwd);149 const isGitWorkspace = (cwd: string) => isGitRepository(cwd);
149150
150 const captureCheckpointFromTurnCompletion = Effect.fnUntraced(function* (151 // Resolves the workspace CWD for checkpoint operations, preferring the
151 event: Extract<ProviderRuntimeEvent, { type: "turn.completed" }>,152 // active provider session CWD and falling back to the thread/project config.
152 ) {153 // Returns undefined when no CWD can be determined or the workspace is not
153 const turnId = toTurnId(event.turnId);154 // a git repository.
154 if (!turnId) {155 const resolveCheckpointCwd = Effect.fnUntraced(function* (input: {
155 return;156 readonly threadId: ThreadId;
156 }157 readonly thread: { readonly projectId: ProjectId; readonly worktreePath: string | null };
157158 readonly projects: ReadonlyArray<{ readonly id: ProjectId; readonly workspaceRoot: string }>;
158 const readModel = yield* orchestrationEngine.getReadModel();159 readonly preferSessionRuntime: boolean;
159 const thread = readModel.threads.find((entry) => entry.id === event.threadId);160 }): Effect.fn.Return<string | undefined> {
160 if (!thread) {161 const fromSession = yield* resolveSessionRuntimeForThread(input.threadId);
161 return;162 const fromThread = resolveThreadWorkspaceCwd({
162 }163 thread: input.thread,
163164 projects: input.projects,
164 // When a primary turn is active, only that turn may produce completion checkpoints.165 });
165 if (thread.session?.activeTurnId && !sameId(thread.session.activeTurnId, turnId)) {
166 return;
167 }
168
169 if (thread.checkpoints.some((checkpoint) => checkpoint.turnId === turnId)) {
170 return;
171 }
172166
173 const sessionRuntime = yield* resolveSessionRuntimeForThread(thread.id);167 const cwd = input.preferSessionRuntime
174 const checkpointCwd =168 ? (Option.match(fromSession, {
175 Option.match(sessionRuntime, {169 onNone: () => undefined,
176 onNone: () => undefined,170 onSome: (runtime) => runtime.cwd,
177 onSome: (runtime) => runtime.cwd,171 }) ?? fromThread)
178 }) ??172 : (fromThread ??
179 resolveThreadWorkspaceCwd({173 Option.match(fromSession, {
180 thread,174 onNone: () => undefined,
181 projects: readModel.projects,175 onSome: (runtime) => runtime.cwd,
182 });176 }));
183 if (!checkpointCwd) {177
184 yield* Effect.logWarning("checkpoint capture skipped: no active provider session cwd", {178 if (!cwd) {
185 threadId: thread.id,179 return undefined;
186 turnId,
187 });
188 return;
189 }180 }
190 if (!isGitWorkspace(checkpointCwd)) {181 if (!isGitWorkspace(cwd)) {
191 yield* Effect.logDebug("checkpoint capture skipped for non-git workspace", {182 return undefined;
192 threadId: thread.id,
193 turnId,
194 cwd: checkpointCwd,
195 });
196 return;
197 }183 }
184 return cwd;
185 });
198186
199 const currentTurnCount = thread.checkpoints.reduce(187 // Shared tail for both capture paths: creates the git checkpoint ref, diffs
200 (maxTurnCount, checkpoint) => Math.max(maxTurnCount, checkpoint.checkpointTurnCount),188 // it against the previous turn, then dispatches the domain events to update
201 0,189 // the orchestration read model.
202 );190 const captureAndDispatchCheckpoint = Effect.fnUntraced(function* (input: {
203 const nextTurnCount = currentTurnCount + 1;191 readonly threadId: ThreadId;
204 const fromTurnCount = Math.max(0, nextTurnCount - 1);192 readonly turnId: TurnId;
205 const fromCheckpointRef = checkpointRefForThreadTurn(thread.id, fromTurnCount);193 readonly thread: {
206 const targetCheckpointRef = checkpointRefForThreadTurn(thread.id, nextTurnCount);194 readonly messages: ReadonlyArray<{
195 readonly id: MessageId;
196 readonly role: string;
197 readonly turnId: TurnId | null;
198 }>;
199 };
200 readonly cwd: string;
201 readonly turnCount: number;
202 readonly status: "ready" | "missing" | "error";
203 readonly assistantMessageId: MessageId | undefined;
204 readonly createdAt: string;
205 }) {
206 const fromTurnCount = Math.max(0, input.turnCount - 1);
207 const fromCheckpointRef = checkpointRefForThreadTurn(input.threadId, fromTurnCount);
208 const targetCheckpointRef = checkpointRefForThreadTurn(input.threadId, input.turnCount);
207209
208 const fromCheckpointExists = yield* checkpointStore.hasCheckpointRef({210 const fromCheckpointExists = yield* checkpointStore.hasCheckpointRef({
209 cwd: checkpointCwd,211 cwd: input.cwd,
210 checkpointRef: fromCheckpointRef,212 checkpointRef: fromCheckpointRef,
211 });213 });
212 if (!fromCheckpointExists) {214 if (!fromCheckpointExists) {
213 yield* Effect.logWarning("checkpoint completion missing pre-turn baseline", {215 yield* Effect.logWarning("checkpoint capture missing pre-turn baseline", {
214 threadId: thread.id,216 threadId: input.threadId,
215 turnId,217 turnId: input.turnId,
216 fromTurnCount,218 fromTurnCount,
217 });219 });
218 }220 }
219221
220 yield* checkpointStore.captureCheckpoint({222 yield* checkpointStore.captureCheckpoint({
221 cwd: checkpointCwd,223 cwd: input.cwd,
222 checkpointRef: targetCheckpointRef,224 checkpointRef: targetCheckpointRef,
223 });225 });
224226
225 // Invalidate the workspace entry cache so the @-mention file picker227 // Invalidate the workspace entry cache so the @-mention file picker
226 // reflects files created or deleted during this turn.228 // reflects files created or deleted during this turn.
227 clearWorkspaceIndexCache(checkpointCwd);229 clearWorkspaceIndexCache(input.cwd);
228230
229 const files = yield* checkpointStore231 const files = yield* checkpointStore
230 .diffCheckpoints({232 .diffCheckpoints({
231 cwd: checkpointCwd,233 cwd: input.cwd,
232 fromCheckpointRef,234 fromCheckpointRef,
233 toCheckpointRef: targetCheckpointRef,235 toCheckpointRef: targetCheckpointRef,
234 fallbackFromToHead: false,236 fallbackFromToHead: false,
...@@ -244,81 +246,82 @@ const make = Effect.gen(function* () {...@@ -244,81 +246,82 @@ const make = Effect.gen(function* () {
244 ),246 ),
245 Effect.tapError((error) =>247 Effect.tapError((error) =>
246 appendCaptureFailureActivity({248 appendCaptureFailureActivity({
247 threadId: thread.id,249 threadId: input.threadId,
248 turnId,250 turnId: input.turnId,
249 detail: `Checkpoint captured, but turn diff summary is unavailable: ${error.message}`,251 detail: `Checkpoint captured, but turn diff summary is unavailable: ${error.message}`,
250 createdAt: event.createdAt,252 createdAt: input.createdAt,
251 }),253 }),
252 ),254 ),
253 Effect.catch((error) =>255 Effect.catch((error) =>
254 Effect.logWarning("failed to derive checkpoint file summary", {256 Effect.logWarning("failed to derive checkpoint file summary", {
255 threadId: thread.id,257 threadId: input.threadId,
256 turnId,258 turnId: input.turnId,
257 turnCount: nextTurnCount,259 turnCount: input.turnCount,
258 detail: error.message,260 detail: error.message,
259 }).pipe(Effect.as([])),261 }).pipe(Effect.as([])),
260 ),262 ),
261 );263 );
262264
263 const assistantMessageId =265 const assistantMessageId =
264 thread.messages266 input.assistantMessageId ??
267 input.thread.messages
265 .toReversed()268 .toReversed()
266 .find((entry) => entry.role === "assistant" && entry.turnId === turnId)?.id ??269 .find((entry) => entry.role === "assistant" && entry.turnId === input.turnId)?.id ??
267 MessageId.makeUnsafe(`assistant:${turnId}`);270 MessageId.makeUnsafe(`assistant:${input.turnId}`);
268271
269 const now = event.createdAt;
270 yield* orchestrationEngine.dispatch({272 yield* orchestrationEngine.dispatch({
271 type: "thread.turn.diff.complete",273 type: "thread.turn.diff.complete",
272 commandId: serverCommandId("checkpoint-turn-diff-complete"),274 commandId: serverCommandId("checkpoint-turn-diff-complete"),
273 threadId: thread.id,275 threadId: input.threadId,
274 turnId,276 turnId: input.turnId,
275 completedAt: now,277 completedAt: input.createdAt,
276 checkpointRef: targetCheckpointRef,278 checkpointRef: targetCheckpointRef,
277 status: checkpointStatusFromRuntime(event.payload.state),279 status: input.status,
278 files,280 files,
279 assistantMessageId,281 assistantMessageId,
280 checkpointTurnCount: nextTurnCount,282 checkpointTurnCount: input.turnCount,
281 createdAt: now,283 createdAt: input.createdAt,
282 });284 });
283 yield* receiptBus.publish({285 yield* receiptBus.publish({
284 type: "checkpoint.diff.finalized",286 type: "checkpoint.diff.finalized",
285 threadId: thread.id,287 threadId: input.threadId,
286 turnId,288 turnId: input.turnId,
287 checkpointTurnCount: nextTurnCount,289 checkpointTurnCount: input.turnCount,
288 checkpointRef: targetCheckpointRef,290 checkpointRef: targetCheckpointRef,
289 status: checkpointStatusFromRuntime(event.payload.state),291 status: input.status,
290 createdAt: now,292 createdAt: input.createdAt,
291 });293 });
292 yield* receiptBus.publish({294 yield* receiptBus.publish({
293 type: "turn.processing.quiesced",295 type: "turn.processing.quiesced",
294 threadId: thread.id,296 threadId: input.threadId,
295 turnId,297 turnId: input.turnId,
296 checkpointTurnCount: nextTurnCount,298 checkpointTurnCount: input.turnCount,
297 createdAt: now,299 createdAt: input.createdAt,
298 });300 });
299301
300 yield* orchestrationEngine.dispatch({302 yield* orchestrationEngine.dispatch({
301 type: "thread.activity.append",303 type: "thread.activity.append",
302 commandId: serverCommandId("checkpoint-captured-activity"),304 commandId: serverCommandId("checkpoint-captured-activity"),
303 threadId: thread.id,305 threadId: input.threadId,
304 activity: {306 activity: {
305 id: EventId.makeUnsafe(crypto.randomUUID()),307 id: EventId.makeUnsafe(crypto.randomUUID()),
306 tone: "info",308 tone: "info",
307 kind: "checkpoint.captured",309 kind: "checkpoint.captured",
308 summary: "Checkpoint captured",310 summary: "Checkpoint captured",
309 payload: {311 payload: {
310 turnCount: nextTurnCount,312 turnCount: input.turnCount,
311 status: event.payload.state,313 status: input.status,
312 },314 },
313 turnId,315 turnId: input.turnId,
314 createdAt: now,316 createdAt: input.createdAt,
315 },317 },
316 createdAt: now,318 createdAt: input.createdAt,
317 });319 });
318 });320 });
319321
320 const ensurePreTurnBaselineFromTurnStart = Effect.fnUntraced(function* (322 // Captures a real git checkpoint when a turn completes via a runtime event.
321 event: Extract<ProviderRuntimeEvent, { type: "turn.started" }>,323 const captureCheckpointFromTurnCompletion = Effect.fnUntraced(function* (
324 event: Extract<ProviderRuntimeEvent, { type: "turn.completed" }>,
322 ) {325 ) {
323 const turnId = toTurnId(event.turnId);326 const turnId = toTurnId(event.turnId);
324 if (!turnId) {327 if (!turnId) {
...@@ -331,24 +334,140 @@ const make = Effect.gen(function* () {...@@ -331,24 +334,140 @@ const make = Effect.gen(function* () {
331 return;334 return;
332 }335 }
333336
334 const checkpointCwdFromThreadOrProject = resolveThreadWorkspaceCwd({337 // When a primary turn is active, only that turn may produce completion checkpoints.
338 if (thread.session?.activeTurnId && !sameId(thread.session.activeTurnId, turnId)) {
339 return;
340 }
341
342 // Only skip if a real (non-placeholder) checkpoint already exists for this turn.
343 // ProviderRuntimeIngestion may insert placeholder entries with status "missing"
344 // before this reactor runs; those must not prevent real git capture.
345 if (
346 thread.checkpoints.some(
347 (checkpoint) => checkpoint.turnId === turnId && checkpoint.status !== "missing",
348 )
349 ) {
350 return;
351 }
352
353 const checkpointCwd = yield* resolveCheckpointCwd({
354 threadId: thread.id,
335 thread,355 thread,
336 projects: readModel.projects,356 projects: readModel.projects,
357 preferSessionRuntime: true,
337 });358 });
338 const checkpointCwd =
339 checkpointCwdFromThreadOrProject ??
340 Option.match(yield* resolveSessionRuntimeForThread(thread.id), {
341 onNone: () => undefined,
342 onSome: (runtime) => runtime.cwd,
343 });
344 if (!checkpointCwd) {359 if (!checkpointCwd) {
345 yield* Effect.logWarning("checkpoint pre-turn capture skipped: no workspace cwd", {360 return;
346 threadId: thread.id,361 }
347 turnId,362
363 // If a placeholder checkpoint exists for this turn, reuse its turn count
364 // instead of incrementing past it.
365 const existingPlaceholder = thread.checkpoints.find(
366 (checkpoint) => checkpoint.turnId === turnId && checkpoint.status === "missing",
367 );
368 const currentTurnCount = thread.checkpoints.reduce(
369 (maxTurnCount, checkpoint) => Math.max(maxTurnCount, checkpoint.checkpointTurnCount),
370 0,
371 );
372 const nextTurnCount = existingPlaceholder
373 ? existingPlaceholder.checkpointTurnCount
374 : currentTurnCount + 1;
375
376 yield* captureAndDispatchCheckpoint({
377 threadId: thread.id,
378 turnId,
379 thread,
380 cwd: checkpointCwd,
381 turnCount: nextTurnCount,
382 status: checkpointStatusFromRuntime(event.payload.state),
383 assistantMessageId: undefined,
384 createdAt: event.createdAt,
385 });
386 });
387
388 // Captures a real git checkpoint when a placeholder checkpoint (status "missing")
389 // is detected via a domain event. This replaces the placeholder with a real
390 // git-ref-based checkpoint.
391 //
392 // ProviderRuntimeIngestion creates placeholder checkpoints on turn.diff.updated
393 // events from the Codex runtime. This handler fires when the corresponding
394 // domain event arrives, allowing the reactor to capture the actual filesystem
395 // state into a git ref and dispatch a replacement checkpoint.
396 const captureCheckpointFromPlaceholder = Effect.fnUntraced(function* (
397 event: Extract<OrchestrationEvent, { type: "thread.turn-diff-completed" }>,
398 ) {
399 const { threadId, turnId, checkpointTurnCount, status } = event.payload;
400
401 // Only replace placeholders; skip events from our own real captures.
402 if (status !== "missing") {
403 return;
404 }
405
406 const readModel = yield* orchestrationEngine.getReadModel();
407 const thread = readModel.threads.find((entry) => entry.id === threadId);
408 if (!thread) {
409 yield* Effect.logWarning("checkpoint capture from placeholder skipped: thread not found", {
410 threadId,
348 });411 });
349 return;412 return;
350 }413 }
351 if (!isGitWorkspace(checkpointCwd)) {414
415 // If a real checkpoint already exists for this turn, skip.
416 if (
417 thread.checkpoints.some(
418 (checkpoint) => checkpoint.turnId === turnId && checkpoint.status !== "missing",
419 )
420 ) {
421 yield* Effect.logDebug(
422 "checkpoint capture from placeholder skipped: real checkpoint already exists",
423 { threadId, turnId },
424 );
425 return;
426 }
427
428 const checkpointCwd = yield* resolveCheckpointCwd({
429 threadId,
430 thread,
431 projects: readModel.projects,
432 preferSessionRuntime: true,
433 });
434 if (!checkpointCwd) {
435 return;
436 }
437
438 yield* captureAndDispatchCheckpoint({
439 threadId,
440 turnId,
441 thread,
442 cwd: checkpointCwd,
443 turnCount: checkpointTurnCount,
444 status: "ready",
445 assistantMessageId: event.payload.assistantMessageId ?? undefined,
446 createdAt: event.payload.completedAt,
447 });
448 });
449
450 const ensurePreTurnBaselineFromTurnStart = Effect.fnUntraced(function* (
451 event: Extract<ProviderRuntimeEvent, { type: "turn.started" }>,
452 ) {
453 const turnId = toTurnId(event.turnId);
454 if (!turnId) {
455 return;
456 }
457
458 const readModel = yield* orchestrationEngine.getReadModel();
459 const thread = readModel.threads.find((entry) => entry.id === event.threadId);
460 if (!thread) {
461 return;
462 }
463
464 const checkpointCwd = yield* resolveCheckpointCwd({
465 threadId: thread.id,
466 thread,
467 projects: readModel.projects,
468 preferSessionRuntime: false,
469 });
470 if (!checkpointCwd) {
352 return;471 return;
353 }472 }
354473
...@@ -401,23 +520,13 @@ const make = Effect.gen(function* () {...@@ -401,23 +520,13 @@ const make = Effect.gen(function* () {
401 return;520 return;
402 }521 }
403522
404 const checkpointCwdFromThreadOrProject = resolveThreadWorkspaceCwd({523 const checkpointCwd = yield* resolveCheckpointCwd({
524 threadId,
405 thread,525 thread,
406 projects: readModel.projects,526 projects: readModel.projects,
527 preferSessionRuntime: false,
407 });528 });
408 const checkpointCwd =
409 checkpointCwdFromThreadOrProject ??
410 Option.match(yield* resolveSessionRuntimeForThread(threadId), {
411 onNone: () => undefined,
412 onSome: (runtime) => runtime.cwd,
413 });
414 if (!checkpointCwd) {529 if (!checkpointCwd) {
415 yield* Effect.logWarning("checkpoint pre-turn capture skipped: no workspace cwd", {
416 threadId,
417 });
418 return;
419 }
420 if (!isGitWorkspace(checkpointCwd)) {
421 return;530 return;
422 }531 }
423532
...@@ -592,6 +701,25 @@ const make = Effect.gen(function* () {...@@ -592,6 +701,25 @@ const make = Effect.gen(function* () {
592 }),701 }),
593 ),702 ),
594 );703 );
704 return;
705 }
706
707 // When ProviderRuntimeIngestion creates a placeholder checkpoint (status "missing")
708 // from a turn.diff.updated runtime event, capture the real git checkpoint to
709 // replace it. The providerService.streamEvents PubSub does not reliably deliver
710 // turn.completed runtime events to this reactor (shared subscription), so
711 // reacting to the domain event is the reliable path.
712 if (event.type === "thread.turn-diff-completed") {
713 yield* captureCheckpointFromPlaceholder(event).pipe(
714 Effect.catch((error) =>
715 appendCaptureFailureActivity({
716 threadId: event.payload.threadId,
717 turnId: event.payload.turnId,
718 detail: error.message,
719 createdAt: new Date().toISOString(),
720 }).pipe(Effect.catch(() => Effect.void)),
721 ),
722 );
595 }723 }
596 });724 });
597725
...@@ -644,7 +772,8 @@ const make = Effect.gen(function* () {...@@ -644,7 +772,8 @@ const make = Effect.gen(function* () {
644 if (772 if (
645 event.type !== "thread.turn-start-requested" &&773 event.type !== "thread.turn-start-requested" &&
646 event.type !== "thread.message-sent" &&774 event.type !== "thread.message-sent" &&
647 event.type !== "thread.checkpoint-revert-requested"775 event.type !== "thread.checkpoint-revert-requested" &&
776 event.type !== "thread.turn-diff-completed"
648 ) {777 ) {
649 return Effect.void;778 return Effect.void;
650 }779 }
apps/server/src/orchestration/Layers/OrchestrationEngine.ts+6-4
...@@ -222,14 +222,16 @@ const makeOrchestrationEngine = Effect.gen(function* () {...@@ -222,14 +222,16 @@ const makeOrchestrationEngine = Effect.gen(function* () {
222 return yield* Deferred.await(result);222 return yield* Deferred.await(result);
223 });223 });
224224
225 const streamDomainEvents: OrchestrationEngineShape["streamDomainEvents"] =
226 Stream.fromPubSub(eventPubSub);
227
228 return {225 return {
229 getReadModel,226 getReadModel,
230 readEvents,227 readEvents,
231 dispatch,228 dispatch,
232 streamDomainEvents,229 // Each access creates a fresh PubSub subscription so that multiple
230 // consumers (wsServer, ProviderRuntimeIngestion, CheckpointReactor, etc.)
231 // each independently receive all domain events.
232 get streamDomainEvents(): OrchestrationEngineShape["streamDomainEvents"] {
233 return Stream.fromPubSub(eventPubSub);
234 },
233 } satisfies OrchestrationEngineShape;235 } satisfies OrchestrationEngineShape;
234});236});
235237
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+28-16
...@@ -1064,22 +1064,34 @@ const make = Effect.gen(function* () {...@@ -1064,22 +1064,34 @@ const make = Effect.gen(function* () {
1064 if (event.type === "turn.diff.updated") {1064 if (event.type === "turn.diff.updated") {
1065 const turnId = toTurnId(event.turnId);1065 const turnId = toTurnId(event.turnId);
1066 if (turnId && (yield* isGitRepoForThread(thread.id))) {1066 if (turnId && (yield* isGitRepoForThread(thread.id))) {
1067 const assistantMessageId = MessageId.makeUnsafe(1067 // Skip if a checkpoint already exists for this turn. A real
1068 `assistant:${event.itemId ?? event.turnId ?? event.eventId}`,1068 // (non-placeholder) capture from CheckpointReactor should not
1069 );1069 // be clobbered, and dispatching a duplicate placeholder for the
1070 yield* orchestrationEngine.dispatch({1070 // same turnId would produce an unstable checkpointTurnCount.
1071 type: "thread.turn.diff.complete",1071 if (thread.checkpoints.some((c) => c.turnId === turnId)) {
1072 commandId: providerCommandId(event, "thread-turn-diff-complete"),1072 // Already tracked; no-op.
1073 threadId: thread.id,1073 } else {
1074 turnId,1074 const assistantMessageId = MessageId.makeUnsafe(
1075 completedAt: now,1075 `assistant:${event.itemId ?? event.turnId ?? event.eventId}`,
1076 checkpointRef: CheckpointRef.makeUnsafe(`provider-diff:${event.eventId}`),1076 );
1077 status: "missing",1077 const maxTurnCount = thread.checkpoints.reduce(
1078 files: [],1078 (max, c) => Math.max(max, c.checkpointTurnCount),
1079 assistantMessageId,1079 0,
1080 checkpointTurnCount: thread.checkpoints.length + 1,1080 );
1081 createdAt: now,1081 yield* orchestrationEngine.dispatch({
1082 });1082 type: "thread.turn.diff.complete",
1083 commandId: providerCommandId(event, "thread-turn-diff-complete"),
1084 threadId: thread.id,
1085 turnId,
1086 completedAt: now,
1087 checkpointRef: CheckpointRef.makeUnsafe(`provider-diff:${event.eventId}`),
1088 status: "missing",
1089 files: [],
1090 assistantMessageId,
1091 checkpointTurnCount: maxTurnCount + 1,
1092 createdAt: now,
1093 });
1094 }
1083 }1095 }
1084 }1096 }
10851097
apps/server/src/orchestration/projector.ts+10
...@@ -500,6 +500,16 @@ export function projectEvent(...@@ -500,6 +500,16 @@ export function projectEvent(
500 "checkpoint",500 "checkpoint",
501 );501 );
502502
503 // Do not let a placeholder (status "missing") overwrite a checkpoint
504 // that has already been captured with a real git ref (status "ready").
505 // ProviderRuntimeIngestion may fire multiple turn.diff.updated events
506 // per turn; without this guard later placeholders would clobber the
507 // real capture dispatched by CheckpointReactor.
508 const existing = thread.checkpoints.find((entry) => entry.turnId === checkpoint.turnId);
509 if (existing && existing.status !== "missing" && checkpoint.status === "missing") {
510 return nextBase;
511 }
512
503 const checkpoints = [513 const checkpoints = [
504 ...thread.checkpoints.filter((entry) => entry.turnId !== checkpoint.turnId),514 ...thread.checkpoints.filter((entry) => entry.turnId !== checkpoint.turnId),
505 checkpoint,515 checkpoint,
apps/server/src/provider/Layers/ProviderService.ts+6-1
...@@ -537,7 +537,12 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>...@@ -537,7 +537,12 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
537 listSessions,537 listSessions,
538 getCapabilities,538 getCapabilities,
539 rollbackConversation,539 rollbackConversation,
540 streamEvents: Stream.fromPubSub(runtimeEventPubSub),540 // Each access creates a fresh PubSub subscription so that multiple
541 // consumers (ProviderRuntimeIngestion, CheckpointReactor, etc.) each
542 // independently receive all runtime events.
543 get streamEvents(): ProviderServiceShape["streamEvents"] {
544 return Stream.fromPubSub(runtimeEventPubSub);
545 },
541 } satisfies ProviderServiceShape;546 } satisfies ProviderServiceShape;
542 });547 });
543548