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
8 changes: 8 additions & 0 deletions UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,14 @@ Because a server waits while a provider update is queued, a marked seam in
`apps/server/src/provider/providerMaintenanceRunner.ts` records an interrupted update, including
one still waiting for the installer lock, as failed instead of leaving it queued. Upstream main has
the same gap, so this fix is worth upstreaming.
Server tests stay out of the source checkout. A marked seam in `ProviderReplayHarness.ts` makes
`orchestration-v2/scient-fork/ReplayWorkspacePolicy.testkit.ts` the replay default: a local thread resolves to
its project root, as in production, and a project-less test thread to its disposable replay profile.
Upstream leaves that cwd null, and `Orchestrator.ts` then checkpoints `process.cwd()`, the checkout.
A marked line in `apps/server/vite.config.ts` adds `src/testUtils/checkoutIsolation.setup.ts`, which
sets `GIT_CEILING_DIRECTORIES` to the checkout root, so Git started below the root cannot find the
repository.
Upstream's harness has the same fallback, so this is worth upstreaming.
`DesktopUpdates.ts` forwards every real download progress event to the UI instead
of filtering at 10% boundaries; only logging retains the 10% milestones. Preserve
this small marked exception without changing the downloader's event cadence.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,7 @@ if(r.id)emit({type:'response',id:r.id,command:r.type,success:true,data});
}).pipe(Effect.provide(adapterServices)),
);
const runtime = makeOrchestratorV2ReplayLayerWithRegistry(
// The replay policy ignores project roots; without this cwd the run (and its
// checkpoint capture) falls back to process.cwd(), the host repository.
{ name: `pi-observed-${name}`, runtimePolicyOverride: { cwd } },
{ name: `pi-observed-${name}` },
registry,
{ databaseLayer: database, layerServerConfig: configLayer, runContinuationWorker: true },
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { assert, describe, it } from "@effect/vitest";
import {
type ModelSelection,
type OrchestrationV2AppThread,
ProjectId,
ProviderInstanceId,
ThreadId,
} from "@t3tools/contracts";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as NodeServices from "@effect/platform-node/NodeServices";

import * as ServerConfig from "../../config.ts";
import * as ProjectStore from "../ProjectStore.ts";
import * as RuntimePolicy from "../RuntimePolicy.ts";
import * as ReplayWorkspacePolicy from "./ReplayWorkspacePolicy.testkit.ts";

const projectId = ProjectId.make("project:replay-workspace");
const modelSelection = {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5.5",
} satisfies ModelSelection;

const thread = (now: DateTime.Utc, worktreePath: string | null): OrchestrationV2AppThread => {
const id = ThreadId.make("thread:replay-workspace");
return {
createdBy: "user",
creationSource: "web",
id,
projectId,
title: "Replay workspace",
providerInstanceId: modelSelection.instanceId,
modelSelection,
runtimeMode: "approval-required",
interactionMode: "plan",
branch: null,
worktreePath,
activeProviderThreadId: null,
lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: id },
forkedFrom: null,
createdAt: now,
updatedAt: now,
archivedAt: null,
settledOverride: null,
settledAt: null,
lastVisitedAt: null,
deletedAt: null,
};
};

const project = (workspaceRoot: string) => ({
projectId,
title: "Project",
workspaceRoot,
defaultModelSelection: null,
defaultThreadEnvMode: null,
autoPull: false,
faviconPath: null,
projectIcon: null,
scripts: [],
createdAt: "2026-10-19T00:00:00.000Z",
updatedAt: "2026-10-19T00:00:00.000Z",
deletedAt: null,
});

const replayProfile = ServerConfig.layerTest(process.cwd(), {
prefix: "replay-workspace-policy-",
}).pipe(Layer.provide(NodeServices.layer));

const resolveWith = (get: ProjectStore.ProjectStoreV2["Service"]["get"], worktree: string | null) =>
Effect.gen(function* () {
const policy = yield* RuntimePolicy.RuntimePolicyV2;
const resolved = yield* policy.resolve({
thread: thread(yield* DateTime.now, worktree),
modelSelection,
});
return { ...resolved, profile: (yield* ServerConfig.ServerConfig).baseDir };
}).pipe(
Effect.provide(
ReplayWorkspacePolicy.layer.pipe(
Layer.provide(Layer.mock(ProjectStore.ProjectStoreV2)({ get })),
Layer.provideMerge(replayProfile),
),
),
);

describe("ReplayWorkspacePolicy", () => {
it.effect("keeps a thread's worktree without reading its project", () =>
Effect.gen(function* () {
const policy = yield* resolveWith(() => Effect.die("project read"), "/worktree");
assert.strictEqual(policy.cwd, "/worktree");
}),
);

it.effect("resolves a local thread to its project root and keeps its modes", () =>
Effect.gen(function* () {
const policy = yield* resolveWith(() => Effect.succeedSome(project("/project-root")), null);
assert.strictEqual(policy.cwd, "/project-root");
assert.strictEqual(policy.runtimeMode, "approval-required");
assert.strictEqual(policy.interactionMode, "plan");
}),
);

it.effect("gives a thread without a project the disposable replay profile", () =>
Effect.gen(function* () {
const policy = yield* resolveWith(() => Effect.succeedNone, null);
assert.strictEqual(policy.cwd, policy.profile);
}),
);

it.effect("yields to a scenario's explicit cwd override", () =>
Effect.gen(function* () {
const policy = yield* RuntimePolicy.RuntimePolicyV2;
const resolved = yield* policy.resolve({
thread: thread(yield* DateTime.now, null),
modelSelection,
});
assert.strictEqual(resolved.cwd, "/override");
}).pipe(
Effect.provide(
RuntimePolicy.layerWithOverride({ cwd: "/override" }).pipe(
Layer.provide(ReplayWorkspacePolicy.layer),
Layer.provide(
Layer.mock(ProjectStore.ProjectStoreV2)({
get: () => Effect.succeedSome(project("/project-root")),
}),
),
Layer.provideMerge(replayProfile),
),
),
),
);

it.effect("reports a project store failure as a policy resolution error", () =>
Effect.gen(function* () {
const error = yield* resolveWith(
() => Effect.fail(new ProjectStore.ProjectStoreV2Error({ operation: "get", cause: "x" })),
null,
).pipe(Effect.flip);
assert.instanceOf(error, RuntimePolicy.RuntimePolicyResolveError);
assert.strictEqual(error.projectId, projectId);
}),
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";

import * as ServerConfig from "../../config.ts";
import * as ProjectStore from "../ProjectStore.ts";
import * as RuntimePolicy from "../RuntimePolicy.ts";

/**
* A replay thread's workspace, resolved as production resolves it: its worktree, else its
* project's root. Upstream's test policy leaves a local thread's cwd null, and the
* orchestrator then checkpoints `process.cwd()`, which in tests is the source checkout. A
* thread with no project, which production refuses, gets the disposable replay profile.
*/
export const layer: Layer.Layer<
RuntimePolicy.RuntimePolicyV2,
never,
ProjectStore.ProjectStoreV2 | ServerConfig.ServerConfig
> = Layer.effect(
RuntimePolicy.RuntimePolicyV2,
Effect.gen(function* () {
const base = yield* RuntimePolicy.RuntimePolicyV2;
const projects = yield* ProjectStore.ProjectStoreV2;
const { baseDir } = yield* ServerConfig.ServerConfig;
return RuntimePolicy.RuntimePolicyV2.of({
resolve: (input) =>
base.resolve(input).pipe(
Effect.filterOrElse(
(policy) => policy.cwd !== null,
(policy) =>
projects.get(input.thread.projectId).pipe(
Effect.mapError(
(cause) =>
new RuntimePolicy.RuntimePolicyResolveError({
projectId: input.thread.projectId,
providerInstanceId: input.modelSelection.instanceId,
cause,
}),
),
Effect.map((project) => ({
...policy,
cwd: Option.match(project, {
onNone: () => baseDir,
onSome: (row) => row.workspaceRoot,
}),
})),
),
),
),
});
}),
).pipe(Layer.provide(RuntimePolicy.layer));
15 changes: 13 additions & 2 deletions apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ import * as RunExecutionService from "../RunExecutionService.ts";
import * as RunFinalizationService from "../RunFinalizationService.ts";
import * as ThreadTitleRegenerationService from "../ThreadTitleRegenerationService.ts";
import * as RuntimePolicy from "../RuntimePolicy.ts";
// SCIENT-FORK:START — replay workspaces resolve like production, never to the checkout.
import * as ScientReplayWorkspacePolicy from "../scient-fork/ReplayWorkspacePolicy.testkit.ts";
// SCIENT-FORK:END
import * as TurnItemPositionStore from "../TurnItemPositionStore.ts";
import * as RuntimeRequestService from "../RuntimeRequestService.ts";
import * as ThreadCommandExecutor from "../ThreadCommandExecutor.ts";
Expand Down Expand Up @@ -419,13 +422,21 @@ export function layerWithRegistry<Error>(
ServerConfig.ServerConfig,
makeReplayServerConfig(scenario.name).pipe(Effect.orDie),
).pipe(Layer.provide(NodeServices.layer));
// SCIENT-FORK:START — a local thread checkpoints its project, not process.cwd() (the checkout).
// Suspended: the stores it reads are built below.
const layerReplayPolicy = Layer.suspend(() =>
ScientReplayWorkspacePolicy.layer.pipe(
Layer.provide(Layer.merge(layerStores, layerServerConfig)),
),
);
const layerRuntime =
options.runtimePolicyLayer ??
(scenario.runtimePolicyOverride === undefined
? RuntimePolicy.layer
? layerReplayPolicy
: RuntimePolicy.layerWithOverride(scenario.runtimePolicyOverride).pipe(
Layer.provide(RuntimePolicy.layer),
Layer.provide(layerReplayPolicy),
));
// SCIENT-FORK:END
const layerDatabase = options.databaseLayer ?? SqlitePersistence.layerMemory;
const layerThreadCommandExecutor = ThreadCommandExecutor.layer;
// One queue shared by the adapters, the orchestrator, and the worker, like
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeChildProcess from "node:child_process";
import * as NodePath from "node:path";

import {
Expand Down Expand Up @@ -65,6 +66,7 @@ import { LegacyV1ThreadImporter } from "../../orchestration-v2/legacy/LegacyV1Th

import { ServerConfig } from "../../config.ts";
import { ProjectionStoreV2 } from "../../orchestration-v2/ProjectionStore.ts";
import { ProjectStoreV2 } from "../../orchestration-v2/ProjectStore.ts";

const retainedMessages = Effect.fn("ImportTest.retainedMessages")(function* (
handoff: OrchestrationV2ContextHandoff,
Expand Down Expand Up @@ -1112,6 +1114,23 @@ describe("native import continuation and forks", () => {
assert.isTrue(
first.contextHandoffs.every((handoff) => (handoff.history?.omittedItems ?? 0) === 0),
);
// The continued run checkpoints the imported project's own repository.
const project = Option.getOrThrow(
yield* Effect.flatMap(ProjectStoreV2, (projects) => projects.get(PROJECT_ID)),
);
const run = first.runs.at(-1);
const scope = first.checkpointScopes.find((candidate) => candidate.runId === run?.id);
assert.strictEqual(scope?.cwd, project.workspaceRoot);
const checkpoint = first.checkpoints.find(
(candidate) => candidate.id === run?.checkpointId,
);
assert.strictEqual(checkpoint?.status, "ready");
assert.strictEqual(
NodeChildProcess.spawnSync("git", ["rev-parse", "--verify", "--quiet", checkpoint!.ref], {
cwd: project.workspaceRoot,
}).status,
0,
);
const firstProvider = first.providerThreads.find(
(thread) => thread.id === first.thread.activeProviderThreadId,
)!;
Expand Down Expand Up @@ -1151,6 +1170,55 @@ describe("native import continuation and forks", () => {
}),
),
);
it.live(
"continues an import whose project folder is not a repository, without a checkpoint",
() =>
withImporter(
Effect.gen(function* () {
const folder = yield* (yield* FileSystem.FileSystem).makeTempDirectoryScoped({
prefix: "scient-native-import-plain-",
});
const now = DateTime.formatIso(yield* DateTime.now);
yield* (yield* EventSinkV2).commitProjectCommand({
commandId: CommandId.make("plain-folder-project"),
projectId: PROJECT_ID,
commandType: "project.meta-updated",
acceptedAt: yield* DateTime.now,
event: {
eventId: EventId.make("plain-folder-project"),
type: "project.meta-updated",
aggregateKind: "project",
aggregateId: PROJECT_ID,
occurredAt: now,
commandId: null,
causationEventId: null,
correlationId: null,
metadata: {},
payload: { projectId: PROJECT_ID, workspaceRoot: folder, updatedAt: now },
},
});
const { lease } = yield* leaseFor(importFixture({ turns: 1 }));
const { result } = yield* importOnce(lease);
yield* continueImport(
result.threadId,
MessageId.make("plain-folder-continuation"),
"Continue without a repository",
);
const projection = yield* (yield* ProjectionStoreV2).getThreadProjection(result.threadId);
const run = projection.runs.at(-1);
assert.strictEqual(run?.status, "completed");
assert.strictEqual(
projection.checkpointScopes.find((scope) => scope.runId === run?.id)?.cwd,
folder,
);
assert.isFalse(
projection.checkpoints.some(
(checkpoint) => checkpoint.runId === run?.id && checkpoint.status === "ready",
),
);
}),
),
);
it.live(
"forks an inherited imported answer after revert without losing history or provenance",
() =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as FileSystem from "effect/FileSystem";
import { checkpointWorkspace } from "@t3tools/provider-testing/replayWorkspace";
import { layerWithRegistry as makeOrchestratorV2ReplayLayerWithRegistry } from "../../orchestration-v2/testkit/ProviderReplayHarness.ts";
import type { ProviderAdapterRegistryV2 } from "../../orchestration-v2/ProviderAdapterRegistry.ts";

Expand Down Expand Up @@ -114,9 +114,7 @@ export function nativeImportTestLayer(controls: NativeImportTestControls = {}) {
export const createNativeProjects = Effect.gen(function* () {
const sink = yield* EventSink.EventSinkV2;
const now = yield* DateTime.now;
const workspaceRoot = yield* (yield* FileSystem.FileSystem).makeTempDirectoryScoped({
prefix: "scient-native-import-workspace-",
});
const workspaceRoot = yield* checkpointWorkspace("scient-native-import-workspace");
for (const projectId of [PROJECT_ID, OTHER_PROJECT_ID]) {
yield* sink.commitProjectCommand({
commandId: CommandId.make(`native-create-${projectId}`),
Expand Down
17 changes: 17 additions & 0 deletions apps/server/src/testUtils/checkoutIsolation.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @effect-diagnostics nodeBuiltinImport:off - vitest setup outside any Effect runtime.
import * as NodePath from "node:path";
import * as NodeURL from "node:url";

// Git started anywhere below the source checkout's root must not find its repository: a
// test that falls back to `process.cwd()` (apps/server) would otherwise capture, publish,
// restore or clean inside the repository under test. Every Git child inherits this,
// whichever process runner starts it. Fixtures live in the OS temp directory, outside it.
const checkoutRoot = NodeURL.fileURLToPath(new URL("../../../../", import.meta.url));

// Setup files run again for every test file in the same worker process.
const ceilings = process.env.GIT_CEILING_DIRECTORIES?.split(NodePath.delimiter) ?? [];
if (!ceilings.includes(checkoutRoot)) {
process.env.GIT_CEILING_DIRECTORIES = [...ceilings, checkoutRoot]
.filter(Boolean)
.join(NodePath.delimiter);
}
Loading
Loading