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
83 changes: 66 additions & 17 deletions apps/server/integration/OrchestrationEngineHarness.integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
ApprovalRequestId,
CodexSettings,
ProviderDriverKind,
ThreadId,
type OrchestrationEvent,
type OrchestrationThread,
} from "@t3tools/contracts";
Expand All @@ -16,6 +17,7 @@ import * as Layer from "effect/Layer";
import * as ManagedRuntime from "effect/ManagedRuntime";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as PubSub from "effect/PubSub";
import * as Ref from "effect/Ref";
import * as Schedule from "effect/Schedule";
import * as Schema from "effect/Schema";
Expand Down Expand Up @@ -46,11 +48,10 @@ import {
import { ProviderService } from "../src/provider/Services/ProviderService.ts";
import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts";
import { RepositoryIdentityResolverLive } from "../src/project/Layers/RepositoryIdentityResolver.ts";
import { RepositoryIdentityResolver } from "../src/project/Services/RepositoryIdentityResolver.ts";
import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "../src/orchestration/Layers/ProjectionPipeline.ts";
import { OrchestrationProjectionSnapshotQueryLive } from "../src/orchestration/Layers/ProjectionSnapshotQuery.ts";
import { RuntimeReceiptBusTest } from "../src/orchestration/Layers/RuntimeReceiptBus.ts";
import { OrchestrationReactorLive } from "../src/orchestration/Layers/OrchestrationReactor.ts";
import { ProviderCommandReactorLive } from "../src/orchestration/Layers/ProviderCommandReactor.ts";
import { ProviderRuntimeIngestionLive } from "../src/orchestration/Layers/ProviderRuntimeIngestion.ts";
Expand Down Expand Up @@ -244,6 +245,13 @@ export const makeOrchestrationIntegrationHarness = (
makeAdapterRegistryMock({ [adapterHarness.provider]: adapterHarness.adapter }),
)
: null;
const receiptPubSub = yield* PubSub.unbounded<OrchestrationRuntimeReceipt>();
const runtimeReceiptBusLayer = Layer.succeed(RuntimeReceiptBus, {
publish: (receipt) => PubSub.publish(receiptPubSub, receipt).pipe(Effect.asVoid),
get streamEventsForTest() {
return Stream.fromPubSub(receiptPubSub);
},
});
const rootDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-orchestration-integration-",
});
Expand Down Expand Up @@ -302,7 +310,7 @@ export const makeOrchestrationIntegrationHarness = (
ProjectionPendingApprovalRepositoryLive,
checkpointStoreLayer,
providerLayer,
RuntimeReceiptBusTest,
runtimeReceiptBusLayer,
);
const serverSettingsLayer = ServerSettingsService.layerTest();
const runtimeIngestionLayer = ProviderRuntimeIngestionLive.pipe(
Expand Down Expand Up @@ -369,7 +377,11 @@ export const makeOrchestrationIntegrationHarness = (
Layer.provideMerge(runtimeServicesLayer),
Layer.provideMerge(orchestrationReactorLayer),
Layer.provide(persistenceLayer),
Layer.provideMerge(RepositoryIdentityResolverLive),
Layer.provideMerge(
Layer.succeed(RepositoryIdentityResolver, {
resolve: () => Effect.succeed(null),
}),
),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, rootDir)),
Layer.provideMerge(NodeServices.layer),
Expand Down Expand Up @@ -404,13 +416,13 @@ export const makeOrchestrationIntegrationHarness = (
).pipe(Effect.orDie);

const scope = yield* Scope.make("sequential");
yield* tryRuntimePromise("start OrchestrationReactor", () =>
runtime.runPromise(reactor.start().pipe(Scope.provide(scope))),
).pipe(Effect.orDie);
const receiptHistory = yield* Ref.make<ReadonlyArray<OrchestrationRuntimeReceipt>>([]);
yield* Stream.runForEach(runtimeReceiptBus.streamEventsForTest, (receipt) =>
Ref.update(receiptHistory, (history) => [...history, receipt]).pipe(Effect.asVoid),
).pipe(Effect.forkIn(scope));
yield* tryRuntimePromise("start OrchestrationReactor", () =>
runtime.runPromise(reactor.start().pipe(Scope.provide(scope))),
).pipe(Effect.orDie);
yield* Effect.sleep(10);

const waitForThread: OrchestrationIntegrationHarness["waitForThread"] = (
Expand All @@ -420,12 +432,8 @@ export const makeOrchestrationIntegrationHarness = (
) =>
waitFor(
snapshotQuery
.getSnapshot()
.pipe(
Effect.map(
(snapshot) => snapshot.threads.find((thread) => thread.id === threadId) ?? null,
),
),
.getThreadDetailById(ThreadId.make(threadId))
.pipe(Effect.map((thread) => Option.getOrNull(thread))),
(thread): thread is OrchestrationThread => thread !== null && predicate(thread),
`projected thread '${threadId}'`,
timeoutMs,
Expand Down Expand Up @@ -494,9 +502,40 @@ export const makeOrchestrationIntegrationHarness = (
predicate: (receipt: OrchestrationRuntimeReceipt) => boolean,
timeoutMs?: number,
) {
const readMatchingReceipt = Ref.get(receiptHistory).pipe(
Effect.map((history) => history.find(predicate)),
);
const readMatchingReceipt = Effect.gen(function* () {
const inMemoryReceipt = (yield* Ref.get(receiptHistory)).find(predicate);
if (inMemoryReceipt) {
return inMemoryReceipt;
}

const events = Array.from(yield* Stream.runCollect(engine.readEvents(0)));
const durableReceipts = events.flatMap(
(event): ReadonlyArray<OrchestrationRuntimeReceipt> => {
if (event.type !== "thread.turn-diff-completed") {
return [];
}
return [
{
type: "checkpoint.diff.finalized",
threadId: event.payload.threadId,
turnId: event.payload.turnId,
checkpointTurnCount: event.payload.checkpointTurnCount,
checkpointRef: event.payload.checkpointRef,
status: event.payload.status,
createdAt: event.payload.completedAt,
},
{
type: "turn.processing.quiesced",
threadId: event.payload.threadId,
turnId: event.payload.turnId,
checkpointTurnCount: event.payload.checkpointTurnCount,
createdAt: event.payload.completedAt,
},
];
},
);
return durableReceipts.find(predicate);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

return waitFor(
readMatchingReceipt,
Expand Down Expand Up @@ -528,7 +567,17 @@ export const makeOrchestrationIntegrationHarness = (
}
});

yield* shutdown;
const shutdownResult = yield* shutdown.pipe(
Effect.timeoutOption("5 seconds"),
Effect.catchCause((cause) =>
Effect.logWarning("orchestration integration harness disposal failed", { cause }).pipe(
Effect.as(Option.some(undefined)),
),
),
);
if (Option.isNone(shutdownResult)) {
yield* Effect.logWarning("orchestration integration harness disposal timed out");
}
});

return {
Expand Down
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@effect/platform-node": "catalog:",
"@effect/platform-node-shared": "catalog:",
"@effect/sql-sqlite-bun": "catalog:",
"@factory/droid-sdk": "^0.2.0",
"@github/copilot": "1.0.2",
"@github/copilot-sdk": "^0.1.32",
"@opencode-ai/sdk": "^1.3.15",
Expand Down
71 changes: 71 additions & 0 deletions apps/server/src/orchestration/Layers/CheckpointReactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1034,6 +1034,77 @@ describe("CheckpointReactor", () => {
});
});

it("rejects Droid checkpoint revert before restoring filesystem checkpoints", async () => {
const harness = await createHarness({ providerName: ProviderDriverKind.make("droid") });
const createdAt = "2026-01-01T00:00:00.000Z";

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

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

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

await waitForThread(harness.readModel, (entry) =>
entry.activities.some((activity) => activity.kind === "checkpoint.revert.failed"),
);
expect(harness.provider.rollbackConversation).not.toHaveBeenCalled();
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("processes consecutive revert requests with deterministic rollback sequencing", async () => {
const harness = await createHarness();
const createdAt = "2026-01-01T00:00:00.000Z";
Expand Down
24 changes: 21 additions & 3 deletions apps/server/src/orchestration/Layers/CheckpointReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
EventId,
MessageId,
type ProjectId,
ProviderDriverKind,
ThreadId,
TurnId,
type OrchestrationEvent,
Expand Down Expand Up @@ -132,11 +133,17 @@ const make = Effect.gen(function* () {

const resolveSessionRuntimeForThread = Effect.fn("resolveSessionRuntimeForThread")(function* (
threadId: ThreadId,
): Effect.fn.Return<Option.Option<{ readonly threadId: ThreadId; readonly cwd: string }>> {
): Effect.fn.Return<
Option.Option<{
readonly threadId: ThreadId;
readonly cwd: string;
readonly provider: ProviderDriverKind;
}>
> {
const sessions = yield* providerService.listSessions();
const session = sessions.find((entry) => entry.threadId === threadId);
return session?.cwd
? Option.some({ threadId: session.threadId, cwd: session.cwd })
? Option.some({ threadId: session.threadId, cwd: session.cwd, provider: session.provider })
: Option.none();
});

Expand Down Expand Up @@ -656,6 +663,18 @@ const make = Effect.gen(function* () {
return;
}

const rolledBackTurns = Math.max(0, currentTurnCount - event.payload.turnCount);
if (rolledBackTurns > 0 && sessionRuntime.value.provider === ProviderDriverKind.make("droid")) {
yield* appendRevertFailureActivity({
threadId: event.payload.threadId,
turnCount: event.payload.turnCount,
detail:
"Droid rollback requires provider-native rewind/fork support and is not yet wired into T3 Code.",
createdAt: now,
}).pipe(Effect.catch(() => Effect.void));
return;
}

const restored = yield* checkpointStore.restoreCheckpoint({
cwd: sessionRuntime.value.cwd,
checkpointRef: targetCheckpointRef,
Expand All @@ -675,7 +694,6 @@ const make = Effect.gen(function* () {
// reflects the reverted filesystem state.
yield* workspaceEntries.invalidate(sessionRuntime.value.cwd);

const rolledBackTurns = Math.max(0, currentTurnCount - event.payload.turnCount);
if (rolledBackTurns > 0) {
yield* providerService.rollbackConversation({
threadId: sessionRuntime.value.threadId,
Expand Down
Loading
Loading