Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
781ee0e
fix(claude): preserve the session selected by clear across process re…
luke2x Oct 23, 2026
63401a5
fix(claude): persist a session adopted while idle
luke2x Oct 23, 2026
aa5ccb6
fix(claude): start a fresh task list after /clear
luke2x Oct 23, 2026
3461eec
fix(claude): follow an idle /clear when the next turn is stale
luke2x Oct 23, 2026
b8dc558
test(claude): reproduce /clear races that resume the pre-clear sessio…
juliusmarminge Oct 24, 2026
dcd5379
test(claude): reproduce a restart after /clear starting the cleared s…
juliusmarminge Oct 24, 2026
cdacbe0
fix(claude): keep the session /clear selects across run close and res…
juliusmarminge Oct 24, 2026
0728a86
Merge origin/main into fix/issue-24153
juliusmarminge Oct 24, 2026
b09c2b9
test(claude): reproduce a model change during /clear reopening the pr…
juliusmarminge Oct 24, 2026
775b101
test(server): reproduce fork and rollback across Claude /clear using …
juliusmarminge Oct 24, 2026
89d0e9a
fix(claude): a /clear adopted while a turn replaces the process is no…
juliusmarminge Oct 24, 2026
2a93ea0
fix(server): fork and rollback across Claude /clear use the session t…
juliusmarminge Oct 24, 2026
21e40c6
test(claude): reproduce rollback across /clear leaving the cleared se…
juliusmarminge Oct 24, 2026
f7828e7
fix(claude): rollback across /clear stops the cleared session, resume…
juliusmarminge Oct 24, 2026
65d8f2a
Merge remote-tracking branch 'origin/main' into fix/issue-24153
juliusmarminge Oct 24, 2026
652d046
test(server): reproduce forking a Claude run that adopted a session m…
juliusmarminge Oct 24, 2026
91b52c8
fix(server): fork and rollback follow a session Claude adopted mid-ru…
juliusmarminge Oct 24, 2026
a10d7c5
fix(claude): preserve adopted sessions across unread updates and release
luke2x Oct 24, 2026
59b4d8c
Merge branch 'main' into fix/claude-clear-session-24152
luke2x Oct 24, 2026
b10bab6
fix(server): keep stale session updates from undoing selection restarts
luke2x Oct 24, 2026
ea32574
Merge branch 'main' into fix/claude-clear-session-24152
luke2x Oct 25, 2026
1c5b8fb
fix(server): address review findings for routing and unread updates
luke2x Oct 25, 2026
5e1c4d9
fix(server): keep finalization on the persisted provider thread
luke2x Oct 25, 2026
ab3fea5
fix(server): only adopt a snapshot that actually persisted
luke2x Oct 25, 2026
419daae
Merge main while preserving cleared-session recovery and tool search …
luke2x Oct 25, 2026
b30eaef
fix(claude): retain awaited task marks when adopting a cleared session
luke2x Oct 25, 2026
f039af2
Merge main while retaining cleared-session recovery and attachment re…
luke2x Oct 26, 2026
3f3ed17
Merge main while preserving session adoption and background process g…
luke2x Oct 27, 2026
7033d66
fix(claude): preserve launch settings during session preparation
luke2x Oct 27, 2026
acd08b8
Merge main while preserving session recovery and shared-runtime relea…
luke2x Oct 31, 2026
d2d114f
fix(claude): preserve launch features and release frame lock during m…
luke2x Oct 31, 2026
40a4447
Merge current main while preserving launcher model selection and fork…
luke2x Oct 31, 2026
a9dc444
merge(main): resolve Claude recovery fork test imports
luke2x Nov 1, 2026
52ca184
merge(main): preserve Claude session adoption with usage accounting
luke2x Nov 2, 2026
5278cdd
merge(main): preserve session titles in Claude adoption flow
luke2x Nov 2, 2026
1c91650
merge(main): preserve replaced-query subagent cleanup in Claude adopt…
luke2x Nov 3, 2026
cda6fb6
fix(server): avoid persisting repeated runless thread snapshots
luke2x Nov 3, 2026
69d9b27
fix(server): preserve native sessions across no-op rollback and faile…
luke2x Nov 3, 2026
cacb482
fix(server): report pending-save failures on explicit release
luke2x Nov 3, 2026
df27c58
fix(server): retry idle saves and reject stopped event pumps
luke2x Nov 3, 2026
612cb72
fix(server): retry busy runtime cleanup and close live rollback queries
luke2x Nov 3, 2026
1abd50a
merge(main): preserve Claude context-window reports in session adopti…
luke2x Nov 4, 2026
dd674fa
merge(main): preserve MCP capabilities in Claude session adoption flow
luke2x Nov 5, 2026
0a9c831
merge(main): preserve model catalog updates in Claude session adoption
luke2x Nov 6, 2026
e16c983
merge(main): preserve session persistence with current event ingestion
luke2x Nov 6, 2026
4e8cdac
merge(main): retain pending session saves with linked-environment cle…
luke2x Nov 8, 2026
dd37103
merge(main): integrate session persistence with streaming and query l…
luke2x Nov 11, 2026
a401997
merge(main): retain session persistence with recovered command output
luke2x Nov 11, 2026
00f617f
merge(main): preserve session adoption with live Claude model updates
luke2x Nov 11, 2026
4759c0e
merge(main): preserve session adoption with turn ownership and provid…
luke2x Nov 12, 2026
ca0f352
Merge main and preserve cleared-session adoption and release cleanup
luke2x Nov 13, 2026
7f352a8
merge(main): keep cleared-session adoption with parallel launch reads
juliusmarminge Nov 14, 2026
eb2b362
fix(server): drop unrelated plan routing and a stale duplicate test
juliusmarminge Nov 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
556 changes: 555 additions & 1 deletion apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts

Large diffs are not rendered by default.

896 changes: 565 additions & 331 deletions apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions apps/server/src/orchestration-v2/AttemptNativeSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { OrchestrationV2ProviderThread } from "@t3tools/contracts";

// Historical attempts can belong to a session from before Claude's /clear.
export function withAttemptNativeSession(
providerThread: OrchestrationV2ProviderThread,
nativeThreadId: string | undefined,
): OrchestrationV2ProviderThread {
if (
providerThread.nativeThreadRef === null ||
nativeThreadId === undefined ||
providerThread.nativeThreadRef.nativeId === nativeThreadId
)
return providerThread;
return {
...providerThread,
nativeThreadRef: { ...providerThread.nativeThreadRef, nativeId: nativeThreadId },
nativeConversationHeadRef: null,
};
}
286 changes: 286 additions & 0 deletions apps/server/src/orchestration-v2/CheckpointRollbackService.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it, vi } from "@effect/vitest";
import {
Expand All @@ -12,12 +13,14 @@ import {
RunAttemptId,
RunId,
CheckpointScopeId,
ClaudeSettings,
ProjectId,
ProviderInstanceId,
ProviderSessionId,
ProviderThreadId,
ThreadId,
} from "@t3tools/contracts";
import * as SecureRandom from "effect/crypto/SecureRandom";
import * as Effect from "effect/Effect";
import * as DateTime from "effect/DateTime";
import * as SqlClient from "effect/sql/SqlClient";
Expand All @@ -27,13 +30,17 @@ import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Option from "effect/Option";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
import * as Stream from "effect/Stream";
import * as McpProviderSessions from "@t3tools/provider-core/server/McpProviderSessions";

import * as SqlitePersistence from "../persistence/Sqlite.ts";
import * as ServerConfig from "../config.ts";
import * as ServerSettings from "../serverSettings.ts";
import * as GitWorkflow from "../git/GitWorkflowService.ts";
import { withTurnStartReservation, withWorkspaceLease } from "../workspace/workspaceLease.ts";
import { ProviderWorkspaceMissingError } from "../provider/Errors.ts";
import * as ClaudeAdapterV2 from "./Adapters/ClaudeAdapterV2.ts";
import { resolveCodexRollbackTurnCount } from "./Adapters/CodexAdapterV2.ts";
import * as CheckpointService from "./CheckpointService.ts";
import * as CheckpointRollbackService from "./CheckpointRollbackService.ts";
Expand All @@ -46,6 +53,7 @@ import type * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAda
import * as ProviderSessionManager from "./ProviderSessionManager.ts";
import * as RuntimePolicy from "./RuntimePolicy.ts";

const DEFAULT_CLAUDE_SETTINGS = Schema.decodeSync(ClaudeSettings)({});
const unrelatedProject = Option.some({
workspaceRoot: "/nonexistent/t3-rollback-project",
} as never);
Expand Down Expand Up @@ -874,6 +882,284 @@ it.layer(ProjectionStore.layer.pipe(Layer.provideMerge(SqlitePersistence.layerMe
}),
);

// regression: #24153 a rollback across Claude's /clear paired the adopted session with a
// pre-clear message cursor, so the next resume asked the new session for a missing message.
it.effect.each([1, 2] as const)("keeps Claude session at turn %s", (targetOrdinal) =>
Effect.gen(function* () {
const store = yield* ProjectionStore.ProjectionStoreV2;
const now = yield* DateTime.now;
const threadId = ThreadId.make("rollback-clear");
const providerThreadId = ProviderThreadId.make("rollback-clear-provider");
const providerSessionId = ProviderSessionId.make("rollback-clear-session");
const providerInstanceId = ClaudeAdapterV2.CLAUDE_DEFAULT_INSTANCE_ID;
const driver = ClaudeAdapterV2.CLAUDE_PROVIDER;
const modelSelection = { instanceId: providerInstanceId, model: "claude-sonnet-4-6" };
const cwd = "/nonexistent/t3-rollback-clear";
const scopeId = CheckpointScopeId.make("rollback-clear-scope");
const runId = (ordinal: number) => RunId.make(`rollback-clear-run-${ordinal}`);
const nodeId = (ordinal: number) => NodeId.make(`rollback-clear-node-${ordinal}`);
const checkpointId = (ordinal: number) =>
CheckpointId.make(`rollback-clear-checkpoint-${ordinal}`);
let sequence = 0;
const apply = (event: Record<string, unknown>) =>
store.apply({
...event,
id: EventId.make(`rollback-clear-event-${sequence++}`),
threadId,
occurredAt: now,
} as OrchestrationV2DomainEvent);
yield* apply({
type: "thread.created",
payload: {
id: threadId,
createdBy: "user",
creationSource: "web",
projectId: ProjectId.make("rollback-clear-project"),
title: "Rollback across /clear",
providerInstanceId,
modelSelection,
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: cwd,
activeProviderThreadId: providerThreadId,
lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId },
forkedFrom: null,
createdAt: now,
updatedAt: now,
archivedAt: null,
settledOverride: null,
settledAt: null,
lastVisitedAt: null,
deletedAt: null,
},
});
// Run 2 started in the original session, and its `/clear` adopted a new one.
yield* apply({
type: "provider-thread.updated",
payload: {
id: providerThreadId,
driver,
providerInstanceId,
providerSessionId,
appThreadId: threadId,
ownerNodeId: null,
nativeThreadRef: { driver, nativeId: "native-after-clear", strength: "strong" },
nativeConversationHeadRef: { driver, nativeId: "current-head", strength: "weak" },
status: "idle",
firstRunOrdinal: 1,
lastRunOrdinal: 2,
handoffIds: [],
forkedFrom: null,
createdAt: now,
updatedAt: now,
},
});
yield* apply({
type: "checkpoint-scope.created",
payload: {
id: scopeId,
threadId,
runId: runId(1),
nodeId: nodeId(1),
parentScopeId: null,
providerThreadId,
kind: "root_run",
ordinalWithinParent: 0,
advancesAppRunCount: true,
cwd,
createdAt: now,
},
});
for (const ordinal of [1, 2]) {
const attemptId = RunAttemptId.make(`rollback-clear-attempt-${ordinal}`);
const turnId = ProviderTurnId.make(`rollback-clear-turn-${ordinal}`);
yield* apply({
type: "run.created",
payload: {
id: runId(ordinal),
threadId,
ordinal,
providerInstanceId,
modelSelection,
providerThreadId,
userMessageId: MessageId.make(`rollback-clear-message-${ordinal}`),
rootNodeId: nodeId(ordinal),
activeAttemptId: attemptId,
status: "completed",
requestedAt: now,
startedAt: now,
completedAt: now,
checkpointId: checkpointId(ordinal),
contextHandoffId: null,
},
});
yield* apply({
type: "node.updated",
payload: {
id: nodeId(ordinal),
threadId,
runId: runId(ordinal),
parentNodeId: null,
rootNodeId: nodeId(ordinal),
kind: "root_turn",
status: "completed",
countsForRun: true,
providerThreadId,
providerTurnId: turnId,
nativeItemRef: null,
runtimeRequestId: null,
checkpointScopeId: scopeId,
startedAt: now,
completedAt: now,
},
});
yield* apply({
type: "run-attempt.created",
runId: runId(ordinal),
payload: {
id: attemptId,
nativeThreadId: "native-before-clear",
runId: runId(ordinal),
attemptOrdinal: 1,
rootNodeId: nodeId(ordinal),
providerInstanceId,
providerThreadId,
providerTurnId: turnId,
reason: "initial",
status: "completed",
startedAt: now,
completedAt: now,
},
});
yield* apply({
type: "provider-turn.updated",
payload: {
id: turnId,
providerThreadId,
nodeId: nodeId(ordinal),
runAttemptId: attemptId,
nativeTurnRef: { driver, nativeId: `assistant-${ordinal}`, strength: "weak" },
ordinal,
status: "completed",
startedAt: now,
completedAt: now,
},
});
}
for (const ordinal of [0, 1, 2]) {
yield* apply({
type: "checkpoint.captured",
payload: {
id: checkpointId(ordinal),
threadId,
scopeId,
runId: ordinal === 0 ? null : runId(ordinal),
nodeId: nodeId(Math.max(1, ordinal)),
parentCheckpointId: null,
ordinalWithinScope: ordinal,
appRunOrdinal: ordinal === 0 ? null : ordinal,
ref: CheckpointRef.make(`refs/t3/rollback-clear/${ordinal}`),
status: "ready",
files: [],
capturedAt: now,
},
});
}

const fileSystem = yield* FileSystem.FileSystem;
const adapter = yield* ClaudeAdapterV2.makeClaudeAdapterV2({
instanceId: providerInstanceId,
settings: DEFAULT_CLAUDE_SETTINGS,
environment: {},
attachmentsDir: yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-rollback-clear-",
}),
fileSystem,
path: yield* Path.Path,
random: yield* SecureRandom.SecureRandom,
idAllocator: yield* IdAllocator.IdAllocatorV2,
queryRunner: {
allocateSessionId: Effect.die("unused allocateSessionId"),
open: () => Effect.die("unused open"),
forkSession: () => Effect.die("unused forkSession"),
subagentLaunchToolUseId: () => Effect.succeed(null),
subagentModel: () => Effect.succeed(null),
assertComplete: Effect.void,
},
});
const runtime = yield* adapter.openSession({
threadId,
providerSessionId,
modelSelection,
runtimePolicy: { runtimeMode: "full-access", interactionMode: "default", cwd: null },
});
yield* runtime.events.pipe(Stream.runDrain, Effect.forkScoped);
const committed: OrchestrationV2DomainEvent[] = [];
yield* Effect.gen(function* () {
const service = yield* CheckpointRollbackService.CheckpointRollbackServiceV2;
yield* service.execute({
threadId,
providerThreadId,
scopeId,
checkpointId: checkpointId(targetOrdinal),
restoreFiles: false,
});
}).pipe(
Effect.provide(
layerCheckpointRollbackService.pipe(
Layer.provide(
Layer.mergeAll(
ThreadCommandExecutor.layer,
IdAllocator.layer,
Layer.mock(CheckpointService.CheckpointServiceV2)({
restore: () => Effect.void,
deleteStaleRefs: () => Effect.void,
}),
Layer.mock(EventSink.EventSinkV2)({
write: ({ events }) =>
Effect.sync(() => {
committed.push(...events);
return [];
}),
}),
Layer.mock(ProviderSessionManager.ProviderSessionManagerV2)({
open: () => Effect.succeed(runtime as never),
}),
Layer.mock(RuntimePolicy.RuntimePolicyV2)({
resolve: () => Effect.succeed({} as never),
}),
),
),
),
),
);

const providerThread = committed.find(
(event) => event.type === "provider-thread.updated",
)?.payload;
assert.deepEqual(
{
session: providerThread?.nativeThreadRef?.nativeId,
resumeAt: providerThread?.nativeConversationHeadRef?.nativeId,
},
targetOrdinal === 1
? { session: "native-before-clear", resumeAt: "assistant-1" }
: { session: "native-after-clear", resumeAt: "current-head" },
);
}).pipe(
Effect.scoped,
Effect.provide(
Layer.mergeAll(
IdAllocator.layer,
McpProviderSessions.layer,
NodeServices.layer,
NodeCrypto.layer,
),
),
),
);

it.effect(
"rewinds stopped runs repeatedly without counting discarded provider turns again",
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
isCheckpointRestoreIsolated,
SHARED_WORKSPACE_RESTORE_MESSAGE,
} from "./CheckpointRestoreSafety.ts";
import { withAttemptNativeSession } from "./AttemptNativeSession.ts";
import { CheckpointRestoreError, CheckpointServiceV2 } from "./CheckpointService.ts";
import { EventSinkV2 } from "./EventSink.ts";
import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
Expand Down Expand Up @@ -336,7 +337,10 @@ export const layer: Layer.Layer<
runsToRollback.length === 0
? { providerThread }
: yield* session.rollbackThread({
providerThread,
providerThread: withAttemptNativeSession(
providerThread,
targetAttempt?.nativeThreadId,
),
target: rollbackTarget,
providerThreadTurns,
});
Expand Down
Loading
Loading