Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
73a26b1
Add Copilot SDK support
zortos293 Apr 18, 2026
e370c08
Include copilot in provider registry test
zortos293 Apr 18, 2026
5ee42cd
Guard Copilot session bootstrap callbacks
zortos293 Apr 18, 2026
4207463
Refactor Copilot adapter to use Effect deferreds
zortos293 Apr 20, 2026
6f60855
Rebase copilot picker onto new modal
zortos293 Apr 20, 2026
8986442
Use copilot icon in model picker
zortos293 Apr 20, 2026
e599c62
Remove duplicate copilot picker entry
zortos293 Apr 20, 2026
6d0d9db
Reuse Copilot git text clients
zortos293 Apr 20, 2026
6c45ae1
Delete apps/web/src/components/chat/providerIconUtils.test.ts
zortos293 Apr 20, 2026
a49f528
Render Copilot task completion as assistant text
zortos293 Apr 22, 2026
84734b1
Complete Copilot send failure lifecycle
zortos293 May 7, 2026
fe984c4
Fix Copilot rebase fallout
zortos293 May 7, 2026
6eb88d0
Remove unused Copilot provider service
zortos293 May 8, 2026
4fa112f
Handle empty Copilot tool progress messages
zortos293 May 11, 2026
3ba69c1
Emit Copilot fallback message for tool-only turns
zortos293 May 16, 2026
8ad829f
Fix Copilot integration after main rebase
huxcrux May 31, 2026
e09e9a6
Handle invalid Copilot CLI paths safely
huxcrux May 31, 2026
4480463
Refine Copilot provider auth labels
huxcrux May 31, 2026
0f48c08
Preserve Copilot auth status labels
huxcrux Jun 1, 2026
a631865
Adapt Copilot changes to latest main
huxcrux Jun 8, 2026
d7c3254
Fix Copilot CLI launch in desktop
huxcrux Jun 8, 2026
ab3a5d5
Update Copilot SDK runtime integration
huxcrux Jun 8, 2026
09e19ae
Add Copilot context window selection
huxcrux Jun 8, 2026
33869f1
Label Copilot none reasoning effort
huxcrux Jun 8, 2026
db0befb
Refresh Copilot provider timestamps
huxcrux Jun 8, 2026
0bd41bf
Clear Copilot permission bindings on reply
huxcrux Jun 8, 2026
20556e2
Drain Copilot events before teardown
huxcrux Jun 8, 2026
fecd5ac
Clean up Copilot regression tests
huxcrux Jun 8, 2026
512d30a
Add Copilot max reasoning effort
huxcrux Jun 8, 2026
c136041
Align Copilot command tool rendering
huxcrux Jun 8, 2026
1aee45d
fix(web): stabilize context window snapshots
huxcrux Jun 8, 2026
0151f69
Handle stale Copilot resume cursors
huxcrux Jun 8, 2026
6d2dae6
Emit Copilot file-change turn diffs
huxcrux Jun 8, 2026
7f361fa
Skip Copilot first-turn text generation
huxcrux Jun 8, 2026
0ed1146
Propagate thread metadata shell updates
huxcrux Jun 8, 2026
ab594d8
Warn on Copilot mode sync failures
huxcrux Jun 8, 2026
66ba94f
Fix Copilot approval resolution
huxcrux Jun 9, 2026
75cf275
Ensure Copilot task completions snapshot diffs
huxcrux Jun 9, 2026
5a3e495
Fix first-turn title generation draining
huxcrux Jun 10, 2026
85e39b5
Align Copilot provider format
huxcrux Jun 11, 2026
a54c107
Wire Copilot tasks into task list
huxcrux Jun 11, 2026
423dd1e
Handle missing Copilot background task list
huxcrux Jun 11, 2026
e139c09
Fix Copilot diff markers
huxcrux Jun 11, 2026
22c00ea
Filter Copilot command-only completion fallback
huxcrux Jun 11, 2026
3976056
Filter Copilot task-agent completion fallback
huxcrux Jun 11, 2026
e52acd8
Filter Copilot task-completed generic fallback
huxcrux Jun 11, 2026
ee65c49
Stop emitting empty Copilot diffs
huxcrux Jun 12, 2026
e83d7a9
Normalize Copilot user input answers
huxcrux Jun 12, 2026
68e4c64
Stream Copilot fallback assistant text
huxcrux Jun 12, 2026
e20ef2a
Add Copilot provider refs
huxcrux Jun 12, 2026
b942dd3
Share provider tool classification
huxcrux Jun 12, 2026
8a8f530
Normalize provider plan updates
huxcrux Jun 12, 2026
4063ba6
Project provider reasoning deltas
huxcrux Jun 12, 2026
af9b778
Project provider tool output deltas
huxcrux Jun 12, 2026
244c015
Guard duplicate Copilot completions
huxcrux Jun 12, 2026
04b6dfd
Fix queued Copilot turn idle handling
huxcrux Jun 13, 2026
3e8aa2d
Restore sidebar metadata shell ownership
huxcrux Jun 14, 2026
6f8123f
Suppress Copilot generic completion fallbacks
huxcrux Jun 14, 2026
be7f3ae
Trigger Copilot diff checkpoints for tool completions
huxcrux Jun 14, 2026
396927a
Make checkpoint revert rollback-safe
huxcrux Jun 14, 2026
e7b7001
Restore Copilot first-turn branch naming
huxcrux Jun 14, 2026
da5f663
Filter empty Copilot diff turns
huxcrux Jun 14, 2026
ab3c54a
Ignore Copilot shell completion lines in diffs
huxcrux Jun 14, 2026
ee4fbef
Fix Copilot session error turn completion
huxcrux Jun 14, 2026
1b50874
Fix Copilot adapter formatting
huxcrux Jun 14, 2026
5a9cff3
Fix web search approval classification
huxcrux Jun 14, 2026
438de02
Fix checkpoint revert rollback ordering
huxcrux Jun 14, 2026
b3d4ebb
Fix file-read tool name classification
huxcrux Jun 14, 2026
823941b
Invalidate workspace cache on rollback recovery failure
huxcrux Jun 14, 2026
63559b7
Fix Copilot provider permission handling
huxcrux Jun 15, 2026
0fbdf7a
Fix Copilot runtime after rebase
huxcrux Jun 15, 2026
388e213
Fix Copilot review issues
huxcrux Jun 15, 2026
8eda163
Fix Copilot test vitest imports
huxcrux Jun 15, 2026
4db5cc5
Fix Copilot mock test imports
huxcrux Jun 15, 2026
558bcf7
fix copilot settings patch trimming
huxcrux Jun 16, 2026
8d1daec
Use active turn IDs for Copilot diffs
huxcrux Jun 16, 2026
481138c
Tighten composer prompt effort descriptors
huxcrux Jun 16, 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
2 changes: 2 additions & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
"@effect/platform-node-shared": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@ff-labs/fff-node": "^0.9.4",
"@github/copilot": "1.0.60",
"@github/copilot-sdk": "1.0.0",
"@opencode-ai/sdk": "^1.3.15",
"@pierre/diffs": "catalog:",
"effect": "catalog:",
Expand Down
286 changes: 277 additions & 9 deletions apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
ProviderService,
type ProviderServiceShape,
} from "../../provider/Services/ProviderService.ts";
import { ProviderAdapterRequestError } from "../../provider/Errors.ts";
import { checkpointRefForThreadTurn } from "../../checkpointing/Utils.ts";
import { ServerConfig } from "../../config.ts";
import * as WorkspaceEntries from "../../workspace/WorkspaceEntries.ts";
Expand All @@ -80,11 +81,12 @@ function createProviderServiceHarness(
hasSession = true,
sessionCwd = cwd,
providerName: ProviderSession["provider"] = ProviderDriverKind.make("codex"),
rollbackConversationImpl?: ProviderServiceShape["rollbackConversation"],
) {
const now = "2026-01-01T00:00:00.000Z";
const runtimeEventPubSub = Effect.runSync(PubSub.unbounded<ProviderRuntimeEvent>());
const rollbackConversation = vi.fn(
(_input: { readonly threadId: ThreadId; readonly numTurns: number }) => Effect.void,
const rollbackConversation = vi.fn<ProviderServiceShape["rollbackConversation"]>(
rollbackConversationImpl ?? (() => Effect.void),
);

const unsupported = <A>() =>
Expand Down Expand Up @@ -247,7 +249,11 @@ async function waitForGitRefExists(cwd: string, ref: string, timeoutMs = 15_000)

describe("CheckpointReactor", () => {
let runtime: ManagedRuntime.ManagedRuntime<
OrchestrationEngineService | CheckpointReactor | CheckpointStore | ProjectionSnapshotQuery,
| OrchestrationEngineService
| CheckpointReactor
| CheckpointStore
| ProjectionSnapshotQuery
| WorkspaceEntries.WorkspaceEntries,
unknown
> | null = null;
let scope: Scope.Closeable | null = null;
Expand Down Expand Up @@ -277,6 +283,7 @@ describe("CheckpointReactor", () => {
readonly threadWorktreePath?: string | null;
readonly providerSessionCwd?: string;
readonly providerName?: ProviderDriverKind;
readonly rollbackConversation?: ProviderServiceShape["rollbackConversation"];
readonly gitStatusRefreshCalls?: Array<string>;
}) {
const cwd = createGitRepository();
Expand All @@ -286,6 +293,7 @@ describe("CheckpointReactor", () => {
options?.hasSession ?? true,
options?.providerSessionCwd ?? cwd,
options?.providerName ?? ProviderDriverKind.make("codex"),
options?.rollbackConversation,
);
const orchestrationLayer = OrchestrationEngineLive.pipe(
Layer.provide(OrchestrationProjectionSnapshotQueryLive),
Expand Down Expand Up @@ -346,6 +354,9 @@ describe("CheckpointReactor", () => {
const snapshotQuery = await runtime.runPromise(Effect.service(ProjectionSnapshotQuery));
const reactor = await runtime.runPromise(Effect.service(CheckpointReactor));
const checkpointStore = await runtime.runPromise(Effect.service(CheckpointStore));
const workspaceEntries = await runtime.runPromise(
Effect.service(WorkspaceEntries.WorkspaceEntries),
);
scope = await Effect.runPromise(Scope.make("sequential"));
await Effect.runPromise(reactor.start().pipe(Scope.provide(scope)));
const drain = () => Effect.runPromise(reactor.drain);
Expand Down Expand Up @@ -411,6 +422,8 @@ describe("CheckpointReactor", () => {
engine,
readModel: () => Effect.runPromise(snapshotQuery.getSnapshot()),
provider,
checkpointStore,
workspaceEntries,
cwd,
drain,
};
Expand Down Expand Up @@ -965,6 +978,81 @@ describe("CheckpointReactor", () => {
).toBe(false);
});

it("does not roll back provider conversation when filesystem restore fails", async () => {
const harness = await createHarness();
const createdAt = "2026-01-01T00:00:00.000Z";
vi.spyOn(harness.checkpointStore, "restoreCheckpoint").mockImplementationOnce(() =>
Effect.succeed(false),
);

await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.session.set",
commandId: CommandId.make("cmd-session-set-restore-fails"),
threadId: ThreadId.make("thread-1"),
session: {
threadId: ThreadId.make("thread-1"),
status: "ready",
providerName: "codex",
runtimeMode: "approval-required",
activeTurnId: null,
lastError: null,
updatedAt: createdAt,
},
createdAt,
}),
);

await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.turn.diff.complete",
commandId: CommandId.make("cmd-restore-fails-diff-1"),
threadId: ThreadId.make("thread-1"),
turnId: asTurnId("turn-restore-fails-1"),
completedAt: createdAt,
checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1),
status: "ready",
files: [],
checkpointTurnCount: 1,
createdAt,
}),
);
await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.turn.diff.complete",
commandId: CommandId.make("cmd-restore-fails-diff-2"),
threadId: ThreadId.make("thread-1"),
turnId: asTurnId("turn-restore-fails-2"),
completedAt: createdAt,
checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 2),
status: "ready",
files: [],
checkpointTurnCount: 2,
createdAt,
}),
);

await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.checkpoint.revert",
commandId: CommandId.make("cmd-revert-restore-fails"),
threadId: ThreadId.make("thread-1"),
turnCount: 1,
createdAt,
}),
);

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some((activity) => activity.kind === "checkpoint.revert.failed"),
);

expect(thread.activities.some((activity) => activity.kind === "checkpoint.revert.failed")).toBe(
true,
);
expect(harness.provider.rollbackConversation).not.toHaveBeenCalled();
expect(fs.readFileSync(path.join(harness.cwd, "README.md"), "utf8")).toBe("v3\n");
});

it("executes provider revert and emits thread.reverted for claude sessions", async () => {
const harness = await createHarness({ providerName: ProviderDriverKind.make("claudeAgent") });
const createdAt = "2026-01-01T00:00:00.000Z";
Expand Down Expand Up @@ -1034,11 +1122,191 @@ describe("CheckpointReactor", () => {
});
});

it("restores current checkpoint files when provider rollback is unsupported", async () => {
const harness = await createHarness({
providerName: ProviderDriverKind.make("copilot"),
rollbackConversation: () =>
Effect.fail(
new ProviderAdapterRequestError({
provider: "copilot",
method: "thread.rollback",
detail: "Copilot SDK does not expose thread rollback.",
}),
),
});
const createdAt = "2026-01-01T00:00:00.000Z";

await Effect.runPromise(
harness.engine.dispatch({
type: "thread.session.set",
commandId: CommandId.make("cmd-session-set-copilot"),
threadId: ThreadId.make("thread-1"),
session: {
threadId: ThreadId.make("thread-1"),
status: "ready",
providerName: "copilot",
runtimeMode: "approval-required",
activeTurnId: null,
lastError: null,
updatedAt: createdAt,
},
createdAt,
}),
);

await Effect.runPromise(
harness.engine.dispatch({
type: "thread.turn.diff.complete",
commandId: CommandId.make("cmd-diff-copilot-1"),
threadId: ThreadId.make("thread-1"),
turnId: asTurnId("turn-copilot-1"),
completedAt: createdAt,
checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1),
status: "ready",
files: [],
checkpointTurnCount: 1,
createdAt,
}),
);
await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.turn.diff.complete",
commandId: CommandId.make("cmd-diff-copilot-2"),
threadId: ThreadId.make("thread-1"),
turnId: asTurnId("turn-copilot-2"),
completedAt: createdAt,
checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 2),
status: "ready",
files: [],
checkpointTurnCount: 2,
createdAt,
}),
);

await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.checkpoint.revert",
commandId: CommandId.make("cmd-revert-copilot-request"),
threadId: ThreadId.make("thread-1"),
turnCount: 1,
createdAt,
}),
);

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some((activity) => activity.kind === "checkpoint.revert.failed"),
);

expect(thread.activities.some((activity) => activity.kind === "checkpoint.revert.failed")).toBe(
true,
);
expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1);
expect(fs.readFileSync(path.join(harness.cwd, "README.md"), "utf8")).toBe("v3\n");
expect(
gitRefExists(harness.cwd, checkpointRefForThreadTurn(ThreadId.make("thread-1"), 2)),
).toBe(true);
});

it("refreshes workspace entries when provider rollback and recovery restore fail", async () => {
const harness = await createHarness({
providerName: ProviderDriverKind.make("copilot"),
rollbackConversation: () =>
Effect.fail(
new ProviderAdapterRequestError({
provider: "copilot",
method: "thread.rollback",
detail: "Copilot SDK does not expose thread rollback.",
}),
),
});
const createdAt = "2026-01-01T00:00:00.000Z";
const refresh = vi
.spyOn(harness.workspaceEntries, "refresh")
.mockImplementation(() => Effect.void);
const restoreCheckpoint = harness.checkpointStore.restoreCheckpoint;
let restoreCalls = 0;
vi.spyOn(harness.checkpointStore, "restoreCheckpoint").mockImplementation((input) => {
restoreCalls += 1;
if (restoreCalls === 2) {
return Effect.succeed(false);
}
return restoreCheckpoint(input);
});

await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.session.set",
commandId: CommandId.make("cmd-session-set-recovery-restore-fails"),
threadId: ThreadId.make("thread-1"),
session: {
threadId: ThreadId.make("thread-1"),
status: "ready",
providerName: "copilot",
runtimeMode: "approval-required",
activeTurnId: null,
lastError: null,
updatedAt: createdAt,
},
createdAt,
}),
);

await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.turn.diff.complete",
commandId: CommandId.make("cmd-recovery-restore-fails-diff-1"),
threadId: ThreadId.make("thread-1"),
turnId: asTurnId("turn-recovery-restore-fails-1"),
completedAt: createdAt,
checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 1),
status: "ready",
files: [],
checkpointTurnCount: 1,
createdAt,
}),
);
await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.turn.diff.complete",
commandId: CommandId.make("cmd-recovery-restore-fails-diff-2"),
threadId: ThreadId.make("thread-1"),
turnId: asTurnId("turn-recovery-restore-fails-2"),
completedAt: createdAt,
checkpointRef: checkpointRefForThreadTurn(ThreadId.make("thread-1"), 2),
status: "ready",
files: [],
checkpointTurnCount: 2,
createdAt,
}),
);

await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.checkpoint.revert",
commandId: CommandId.make("cmd-revert-recovery-restore-fails"),
threadId: ThreadId.make("thread-1"),
turnCount: 1,
createdAt,
}),
);

const thread = await waitForThread(harness.readModel, (entry) =>
entry.activities.some((activity) => activity.kind === "checkpoint.revert.failed"),
);

expect(thread.activities.some((activity) => activity.kind === "checkpoint.revert.failed")).toBe(
true,
);
expect(harness.provider.rollbackConversation).toHaveBeenCalledTimes(1);
expect(restoreCalls).toBe(2);
expect(refresh).toHaveBeenCalledWith(harness.cwd);
});

it("processes consecutive revert requests with deterministic rollback sequencing", async () => {
const harness = await createHarness();
const createdAt = "2026-01-01T00:00:00.000Z";

await Effect.runPromise(
await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.session.set",
commandId: CommandId.make("cmd-session-set-inline-revert"),
Expand All @@ -1056,7 +1324,7 @@ describe("CheckpointReactor", () => {
}),
);

await Effect.runPromise(
await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.turn.diff.complete",
commandId: CommandId.make("cmd-inline-revert-diff-1"),
Expand All @@ -1070,7 +1338,7 @@ describe("CheckpointReactor", () => {
createdAt,
}),
);
await Effect.runPromise(
await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.turn.diff.complete",
commandId: CommandId.make("cmd-inline-revert-diff-2"),
Expand All @@ -1085,7 +1353,7 @@ describe("CheckpointReactor", () => {
}),
);

await Effect.runPromise(
await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.checkpoint.revert",
commandId: CommandId.make("cmd-sequenced-revert-request-1"),
Expand All @@ -1094,7 +1362,7 @@ describe("CheckpointReactor", () => {
createdAt,
}),
);
await Effect.runPromise(
await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.checkpoint.revert",
commandId: CommandId.make("cmd-sequenced-revert-request-0"),
Expand All @@ -1121,7 +1389,7 @@ describe("CheckpointReactor", () => {
const harness = await createHarness({ hasSession: false });
const createdAt = "2026-01-01T00:00:00.000Z";

await Effect.runPromise(
await runtime!.runPromise(
harness.engine.dispatch({
type: "thread.checkpoint.revert",
commandId: CommandId.make("cmd-revert-no-session"),
Expand Down
Loading
Loading