Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @effect-diagnostics nodeBuiltinImport:off
/* oxlint-disable t3code/no-manual-effect-runtime-in-tests */
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
Expand Down Expand Up @@ -1195,6 +1196,74 @@ describe("ProviderCommandReactor", () => {
});
});

it("discards grok resume cursors when the thread workspace changes", async () => {
const harness = await createHarness({
threadModelSelection: {
instanceId: ProviderInstanceId.make("grok"),
model: "grok-build",
},
});
const now = "2026-01-01T00:00:00.000Z";

await Effect.runPromise(
harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.make("cmd-turn-start-grok-workspace-1"),
threadId: ThreadId.make("thread-1"),
message: {
messageId: asMessageId("user-message-grok-workspace-1"),
role: "user",
text: "first in project root",
attachments: [],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt: now,
}),
);

await waitFor(() => harness.startSession.mock.calls.length === 1);
await waitFor(() => harness.sendTurn.mock.calls.length === 1);

await Effect.runPromise(
harness.engine.dispatch({
type: "thread.meta.update",
commandId: CommandId.make("cmd-thread-grok-worktree-change"),
threadId: ThreadId.make("thread-1"),
worktreePath: "/tmp/provider-project-grok-worktree",
}),
);

await Effect.runPromise(
harness.engine.dispatch({
type: "thread.turn.start",
commandId: CommandId.make("cmd-turn-start-grok-workspace-2"),
threadId: ThreadId.make("thread-1"),
message: {
messageId: asMessageId("user-message-grok-workspace-2"),
role: "user",
text: "second in worktree",
attachments: [],
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt: now,
}),
);

await waitFor(() => harness.startSession.mock.calls.length === 2);
expect(harness.startSession.mock.calls[1]?.[1]).toMatchObject({
threadId: ThreadId.make("thread-1"),
cwd: "/tmp/provider-project-grok-worktree",
resumeCursor: null,
modelSelection: {
instanceId: ProviderInstanceId.make("grok"),
model: "grok-build",
},
runtimeMode: "approval-required",
});
});

it("restarts claude sessions when claude effort changes", async () => {
const harness = await createHarness({
threadModelSelection: {
Expand Down
11 changes: 7 additions & 4 deletions apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ const make = Effect.gen(function* () {
});

const startProviderSession = (input?: {
readonly resumeCursor?: unknown;
readonly resumeCursor?: unknown | null;
readonly provider?: ProviderDriverKind;
}) =>
providerService.startSession(threadId, {
Expand Down Expand Up @@ -541,9 +541,11 @@ const make = Effect.gen(function* () {
return existingSessionThreadId;
}

const resumeCursor = shouldRestartForModelChange
? undefined
: (activeSession?.resumeCursor ?? undefined);
const shouldDiscardResumeCursorForCwdChange = cwdChanged && preferredProvider === "grok";
const resumeCursor =
shouldRestartForModelChange || shouldDiscardResumeCursorForCwdChange
? null
: (activeSession?.resumeCursor ?? undefined);
yield* Effect.logInfo("provider command reactor restarting provider session", {
threadId,
existingSessionThreadId,
Expand All @@ -561,6 +563,7 @@ const make = Effect.gen(function* () {
instanceChanged,
shouldRestartForModelChange,
shouldRestartForModelSelectionChange,
shouldDiscardResumeCursorForCwdChange,
hasResumeCursor: resumeCursor !== undefined,
});
const restartedSession = yield* startProviderSession(
Expand Down
37 changes: 37 additions & 0 deletions apps/server/src/provider/Layers/ProviderService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,43 @@ routing.layer("ProviderServiceLive routing", (it) => {
}),
);

it.effect("does not reuse a persisted resume cursor when an explicit cwd changes", () =>
Effect.gen(function* () {
const provider = yield* ProviderService.ProviderService;

const initial = yield* provider.startSession(asThreadId("thread-cwd-changed"), {
provider: ProviderDriverKind.make("codex"),
providerInstanceId: codexInstanceId,
threadId: asThreadId("thread-cwd-changed"),
cwd: "/tmp/project-before-cwd-change",
runtimeMode: "full-access",
});

yield* provider.stopSession({ threadId: initial.threadId });
routing.codex.startSession.mockClear();

yield* provider.startSession(initial.threadId, {
provider: ProviderDriverKind.make("codex"),
providerInstanceId: codexInstanceId,
threadId: initial.threadId,
cwd: "/tmp/project-after-cwd-change",
runtimeMode: "full-access",
});

assert.equal(routing.codex.startSession.mock.calls.length, 1);
const startInput = routing.codex.startSession.mock.calls[0]?.[0];
assert.equal(typeof startInput === "object" && startInput !== null, true);
if (startInput && typeof startInput === "object") {
const startPayload = startInput as {
cwd?: string;
resumeCursor?: unknown;
};
assert.equal(startPayload.cwd, "/tmp/project-after-cwd-change");
assert.equal("resumeCursor" in startPayload, false);
}
}),
);

it.effect("routes explicit claudeAgent provider session starts to the claude adapter", () =>
Effect.gen(function* () {
const provider = yield* ProviderService.ProviderService;
Expand Down
21 changes: 12 additions & 9 deletions apps/server/src/provider/Layers/ProviderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,16 +560,19 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
);
}
const persistedBinding = Option.getOrUndefined(yield* directory.getBinding(threadId));
const effectiveResumeCursor =
input.resumeCursor ??
(persistedBinding?.providerInstanceId === resolvedInstanceId
? persistedBinding.resumeCursor
: undefined);
const effectiveCwd =
input.cwd ??
(persistedBinding?.providerInstanceId === resolvedInstanceId
const persistedCwd =
persistedBinding?.providerInstanceId === resolvedInstanceId
? readPersistedCwd(persistedBinding.runtimePayload)
: undefined);
: undefined;
const canUsePersistedResumeCursor =
persistedBinding?.providerInstanceId === resolvedInstanceId &&
(input.cwd === undefined || persistedCwd === undefined || input.cwd === persistedCwd);
const effectiveResumeCursor =
input.resumeCursor === null
? undefined
: (input.resumeCursor ??
(canUsePersistedResumeCursor ? persistedBinding.resumeCursor : undefined));
const effectiveCwd = input.cwd ?? persistedCwd;
yield* Effect.annotateCurrentSpan({
"provider.kind": resolvedProvider,
"provider.resume_cursor.source":
Expand Down
Loading