Skip to content

Commit c61ecd1

Browse files
saphidclaude
authored andcommitted
fix(server): native forks after an undo no longer copy later turns
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 61c3794 commit c61ecd1

2 files changed

Lines changed: 65 additions & 6 deletions

File tree

‎apps/server/src/orchestration-v2/ProviderTurnStartService.test.ts‎

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,35 @@ function makeLocalCommandHarness(input: {
488488
strength: "strong" as const,
489489
},
490490
};
491+
const parentProviderTurn = (ordinal: number, attemptRunId: RunId) => ({
492+
attempt: {
493+
...projection.attempts[0]!,
494+
id: RunAttemptId.make(`side-chat-parent-attempt-${ordinal}`),
495+
runId: attemptRunId,
496+
providerThreadId: parentProviderThread.id,
497+
status: "completed" as const,
498+
},
499+
turn: {
500+
id: ProviderTurnId.make(`side-chat-parent-turn-${ordinal}`),
501+
providerThreadId: parentProviderThread.id,
502+
nodeId: rootNodeId,
503+
runAttemptId: RunAttemptId.make(`side-chat-parent-attempt-${ordinal}`),
504+
nativeTurnRef: {
505+
driver: providerThread.driver,
506+
nativeId: `side-chat-parent-native-turn-${ordinal}`,
507+
strength: "strong" as const,
508+
},
509+
ordinal,
510+
status: "completed" as const,
511+
startedAt: now,
512+
completedAt: now,
513+
},
514+
});
515+
const parentRolledBackRunId = RunId.make("side-chat-parent-rolled-back-run");
516+
const parentTurns = [
517+
parentProviderTurn(1, parentRunId),
518+
parentProviderTurn(2, parentRolledBackRunId),
519+
];
491520
const parentRecords = {
492521
...projection,
493522
thread: { ...projection.thread, id: parentThreadId },
@@ -498,26 +527,36 @@ function makeLocalCommandHarness(input: {
498527
threadId: parentThreadId,
499528
ordinal: 1,
500529
providerThreadId: parentProviderThread.id,
501-
activeAttemptId: null,
530+
activeAttemptId: parentTurns[0]!.attempt.id,
502531
status: "completed" as const,
503532
completedAt: now,
504533
},
534+
{
535+
...run,
536+
id: parentRolledBackRunId,
537+
threadId: parentThreadId,
538+
ordinal: 2,
539+
providerThreadId: parentProviderThread.id,
540+
activeAttemptId: parentTurns[1]!.attempt.id,
541+
status: "rolled_back" as const,
542+
completedAt: now,
543+
},
505544
...(input.sideChat?.nativeForkFrom === "busy"
506545
? [
507546
{
508547
...run,
509548
id: RunId.make("side-chat-parent-busy-run"),
510549
threadId: parentThreadId,
511-
ordinal: 2,
550+
ordinal: 3,
512551
providerThreadId: parentProviderThread.id,
513552
status: "running" as const,
514553
},
515554
]
516555
: []),
517556
],
518557
providerThreads: [parentProviderThread],
519-
attempts: [],
520-
providerTurns: [],
558+
attempts: parentTurns.map(({ attempt }) => attempt),
559+
providerTurns: parentTurns.map(({ turn }) => turn),
521560
turnItems: [],
522561
};
523562
if (input.sideChat !== undefined) {
@@ -749,7 +788,9 @@ function makeLocalCommandHarness(input: {
749788
strength: "strong" as const,
750789
},
751790
};
752-
const forkThread = vi.fn(() => Effect.succeed(sideChatThread));
791+
const forkThread = vi.fn((_fork: ProviderAdapter.ProviderAdapterV2ForkThreadInput) =>
792+
Effect.succeed(sideChatThread),
793+
);
753794
const sideChatSession = {
754795
driver: providerThread.driver,
755796
providerSession,
@@ -1273,6 +1314,10 @@ effectIt.effect.each([
12731314
yield* harness.start;
12741315

12751316
expect(harness.forkThread).toHaveBeenCalledTimes(forks ? 1 : 0);
1317+
// The parent's rolled-back turn is gone from its native conversation.
1318+
expect(
1319+
harness.forkThread.mock.calls[0]?.[0].sourceProviderTurns?.map((turn) => turn.id),
1320+
).toEqual(forks ? ["side-chat-parent-turn-1"] : undefined);
12761321
expect(harness.projection().contextTransfers).toMatchObject([
12771322
{ id: "side-chat-fork", status: transferStatus },
12781323
]);

‎apps/server/src/orchestration-v2/ProviderTurnStartService.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1150,10 +1150,24 @@ export const layer: Layer.Layer<
11501150
(candidate) => candidate.ordinal > sourceRun.ordinal && isActiveRun(candidate),
11511151
);
11521152
if (!sourceBusy) {
1153+
// Rolled-back turns are no longer in the source conversation, so they cannot bound a fork.
1154+
const rolledBackAttemptIds = new Set(
1155+
sourceProjection.attempts
1156+
.filter((attempt) =>
1157+
sourceProjection.runs.some(
1158+
(candidate) =>
1159+
candidate.id === attempt.runId && candidate.status === "rolled_back",
1160+
),
1161+
)
1162+
.map((attempt) => attempt.id),
1163+
);
11531164
return yield* loadFromProvider(
11541165
session.forkThread({
11551166
sourceProviderThread,
1156-
sourceProviderTurns: sourceProjection.providerTurns,
1167+
sourceProviderTurns: sourceProjection.providerTurns.filter(
1168+
(turn) =>
1169+
turn.runAttemptId === null || !rolledBackAttemptIds.has(turn.runAttemptId),
1170+
),
11571171
targetThreadId: projection.thread.id,
11581172
modelSelection: run.modelSelection,
11591173
runtimePolicy: resolvedRuntimePolicy,

0 commit comments

Comments
 (0)