Skip to content
Closed
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
Expand Up @@ -78,6 +78,7 @@ import { VcsStatusBroadcaster } from "../src/vcs/VcsStatusBroadcaster.ts";
import { GitWorkflowService } from "../src/git/GitWorkflowService.ts";
import * as VcsProcess from "../src/vcs/VcsProcess.ts";
import * as AgentAwarenessRelay from "../src/relay/AgentAwarenessRelay.ts";
import * as VcsMaintenanceReactor from "../src/vcs/VcsMaintenanceReactor.ts";

const decodeCodexSettings = Schema.decodeEffect(CodexSettings);

Expand Down Expand Up @@ -372,6 +373,12 @@ export const makeOrchestrationIntegrationHarness = (
start: () => Effect.void,
}),
),
Layer.provideMerge(
Layer.succeed(VcsMaintenanceReactor.VcsMaintenanceReactor, {
start: () => Effect.void,
sweep: () => Effect.void,
}),
),
);
const layer = Layer.empty.pipe(
Layer.provideMerge(runtimeServicesLayer),
Expand Down
10 changes: 10 additions & 0 deletions apps/server/src/checkpointing/CheckpointDiffQuery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ describe("CheckpointDiffQuery.layer", () => {
return "full thread diff patch";
}),
deleteCheckpointRefs: () => Effect.void,
pruneCheckpointRefs: () =>
Effect.succeed({ scannedCount: 0, keptCount: 0, deletedCount: 0, threadCount: 0 }),
};

const layer = CheckpointDiffQuery.layer.pipe(
Expand Down Expand Up @@ -176,6 +178,8 @@ describe("CheckpointDiffQuery.layer", () => {
return "diff patch";
}),
deleteCheckpointRefs: () => Effect.void,
pruneCheckpointRefs: () =>
Effect.succeed({ scannedCount: 0, keptCount: 0, deletedCount: 0, threadCount: 0 }),
};

const layer = CheckpointDiffQuery.layer.pipe(
Expand Down Expand Up @@ -258,6 +262,8 @@ describe("CheckpointDiffQuery.layer", () => {
return "diff patch";
}),
deleteCheckpointRefs: () => Effect.void,
pruneCheckpointRefs: () =>
Effect.succeed({ scannedCount: 0, keptCount: 0, deletedCount: 0, threadCount: 0 }),
};

const layer = CheckpointDiffQuery.layer.pipe(
Expand Down Expand Up @@ -325,6 +331,8 @@ describe("CheckpointDiffQuery.layer", () => {
restoreCheckpoint: () => Effect.succeed(true),
diffCheckpoints: () => Effect.succeed("diff patch"),
deleteCheckpointRefs: () => Effect.void,
pruneCheckpointRefs: () =>
Effect.succeed({ scannedCount: 0, keptCount: 0, deletedCount: 0, threadCount: 0 }),
};

const layer = CheckpointDiffQuery.layer.pipe(
Expand Down Expand Up @@ -377,6 +385,8 @@ describe("CheckpointDiffQuery.layer", () => {
restoreCheckpoint: () => Effect.succeed(true),
diffCheckpoints: () => Effect.succeed(""),
deleteCheckpointRefs: () => Effect.void,
pruneCheckpointRefs: () =>
Effect.succeed({ scannedCount: 0, keptCount: 0, deletedCount: 0, threadCount: 0 }),
};

const layer = CheckpointDiffQuery.layer.pipe(
Expand Down
92 changes: 92 additions & 0 deletions apps/server/src/checkpointing/CheckpointStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,98 @@ it.layer(TestLayer)("CheckpointStore.layer", (it) => {
);
});

describe("pruneCheckpointRefs", () => {
it.effect("keeps turn zero and the newest tail per thread", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-checkpoint-prune");
const refs = [0, 1, 2, 3, 4].map((turn) => checkpointRefForThreadTurn(threadId, turn));

for (const [index, checkpointRef] of refs.entries()) {
yield* writeTextFile(NodePath.join(tmp, "README.md"), `# turn ${index}\n`);
yield* checkpointStore.captureCheckpoint({
cwd: tmp,
checkpointRef,
});
}

const result = yield* checkpointStore.pruneCheckpointRefs({
cwd: tmp,
keepPerThread: 3,
});

expect(result).toMatchObject({
scannedCount: 5,
keptCount: 3,
deletedCount: 2,
threadCount: 1,
});
expect(yield* checkpointStore.hasCheckpointRef({ cwd: tmp, checkpointRef: refs[0]! })).toBe(
true,
);
expect(yield* checkpointStore.hasCheckpointRef({ cwd: tmp, checkpointRef: refs[1]! })).toBe(
false,
);
expect(yield* checkpointStore.hasCheckpointRef({ cwd: tmp, checkpointRef: refs[2]! })).toBe(
false,
);
expect(yield* checkpointStore.hasCheckpointRef({ cwd: tmp, checkpointRef: refs[3]! })).toBe(
true,
);
expect(yield* checkpointStore.hasCheckpointRef({ cwd: tmp, checkpointRef: refs[4]! })).toBe(
true,
);
}),
);

it.effect("uses the full keep budget when a thread has no turn zero", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const checkpointStore = yield* CheckpointStore.CheckpointStore;
const threadId = ThreadId.make("thread-checkpoint-prune-no-baseline");
const refs = [1, 2, 3, 4, 5].map((turn) => checkpointRefForThreadTurn(threadId, turn));

for (const [index, checkpointRef] of refs.entries()) {
yield* writeTextFile(NodePath.join(tmp, "README.md"), `# turn ${index + 1}\n`);
yield* checkpointStore.captureCheckpoint({
cwd: tmp,
checkpointRef,
});
}

const result = yield* checkpointStore.pruneCheckpointRefs({
cwd: tmp,
keepPerThread: 3,
});

expect(result).toMatchObject({
scannedCount: 5,
keptCount: 3,
deletedCount: 2,
threadCount: 1,
});
expect(yield* checkpointStore.hasCheckpointRef({ cwd: tmp, checkpointRef: refs[0]! })).toBe(
false,
);
expect(yield* checkpointStore.hasCheckpointRef({ cwd: tmp, checkpointRef: refs[1]! })).toBe(
false,
);
expect(yield* checkpointStore.hasCheckpointRef({ cwd: tmp, checkpointRef: refs[2]! })).toBe(
true,
);
expect(yield* checkpointStore.hasCheckpointRef({ cwd: tmp, checkpointRef: refs[3]! })).toBe(
true,
);
expect(yield* checkpointStore.hasCheckpointRef({ cwd: tmp, checkpointRef: refs[4]! })).toBe(
true,
);
}),
);
});

describe("diffCheckpoints", () => {
it.effect("returns full oversized checkpoint diffs without truncation", () =>
Effect.gen(function* () {
Expand Down
30 changes: 30 additions & 0 deletions apps/server/src/checkpointing/CheckpointStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ export interface DeleteCheckpointRefsInput {
readonly checkpointRefs: ReadonlyArray<CheckpointRef>;
}

export interface PruneCheckpointRefsInput {
readonly cwd: string;
readonly keepPerThread: number;
}

export interface PruneCheckpointRefsResult {
readonly scannedCount: number;
readonly keptCount: number;
readonly deletedCount: number;
readonly threadCount: number;
}

/** Service tag for checkpoint persistence and restore operations. */
export class CheckpointStore extends Context.Service<
CheckpointStore,
Expand Down Expand Up @@ -93,6 +105,16 @@ export class CheckpointStore extends Context.Service<
readonly deleteCheckpointRefs: (
input: DeleteCheckpointRefsInput,
) => Effect.Effect<void, CheckpointStoreError>;

/**
* Prune hidden checkpoint refs, retaining only the bounded tail per thread.
*
* Turn 0 is preserved as the thread baseline; the remaining budget is spent
* on the newest positive turn refs.
*/
readonly pruneCheckpointRefs: (
input: PruneCheckpointRefsInput,
) => Effect.Effect<PruneCheckpointRefsResult, CheckpointStoreError>;
}
>()("t3/checkpointing/CheckpointStore") {}

Expand Down Expand Up @@ -157,13 +179,21 @@ export const make = Effect.gen(function* () {
return yield* checkpoints.deleteCheckpointRefs(input);
});

const pruneCheckpointRefs: CheckpointStore["Service"]["pruneCheckpointRefs"] = Effect.fn(
"pruneCheckpointRefs",
)(function* (input) {
const checkpoints = yield* resolveCheckpoints("CheckpointStore.pruneCheckpointRefs", input.cwd);
return yield* checkpoints.pruneCheckpointRefs(input);
});

return CheckpointStore.of({
isGitRepository,
captureCheckpoint,
hasCheckpointRef,
restoreCheckpoint,
diffCheckpoints,
deleteCheckpointRefs,
pruneCheckpointRefs,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ThreadDeletionReactor } from "../Services/ThreadDeletionReactor.ts";
import { OrchestrationReactor } from "../Services/OrchestrationReactor.ts";
import { makeOrchestrationReactor } from "./OrchestrationReactor.ts";
import * as AgentAwarenessRelay from "../../relay/AgentAwarenessRelay.ts";
import * as VcsMaintenanceReactor from "../../vcs/VcsMaintenanceReactor.ts";

describe("OrchestrationReactor", () => {
let runtime: ManagedRuntime.ManagedRuntime<OrchestrationReactor, never> | null = null;
Expand All @@ -23,7 +24,7 @@ describe("OrchestrationReactor", () => {
runtime = null;
});

it("starts provider ingestion, provider command, checkpoint, and thread deletion reactors", async () => {
it("starts provider ingestion, provider command, checkpoint, thread deletion, and maintenance reactors", async () => {
const started: string[] = [];

runtime = ManagedRuntime.make(
Expand Down Expand Up @@ -73,6 +74,15 @@ describe("OrchestrationReactor", () => {
},
}),
),
Layer.provideMerge(
Layer.succeed(VcsMaintenanceReactor.VcsMaintenanceReactor, {
start: () => {
started.push("vcs-maintenance-reactor");
return Effect.void;
},
sweep: () => Effect.void,
}),
),
),
);

Expand All @@ -86,6 +96,7 @@ describe("OrchestrationReactor", () => {
"checkpoint-reactor",
"thread-deletion-reactor",
"agent-awareness-relay",
"vcs-maintenance-reactor",
]);

await Effect.runPromise(Scope.close(scope, Exit.void));
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/orchestration/Layers/OrchestrationReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,23 @@ import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts";
import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts";
import { ThreadDeletionReactor } from "../Services/ThreadDeletionReactor.ts";
import * as AgentAwarenessRelay from "../../relay/AgentAwarenessRelay.ts";
import * as VcsMaintenanceReactor from "../../vcs/VcsMaintenanceReactor.ts";

export const makeOrchestrationReactor = Effect.gen(function* () {
const providerRuntimeIngestion = yield* ProviderRuntimeIngestionService;
const providerCommandReactor = yield* ProviderCommandReactor;
const checkpointReactor = yield* CheckpointReactor;
const threadDeletionReactor = yield* ThreadDeletionReactor;
const agentAwarenessRelay = yield* AgentAwarenessRelay.AgentAwarenessRelay;
const vcsMaintenanceReactor = yield* VcsMaintenanceReactor.VcsMaintenanceReactor;

const start: OrchestrationReactorShape["start"] = Effect.fn("start")(function* () {
yield* providerRuntimeIngestion.start();
yield* providerCommandReactor.start();
yield* checkpointReactor.start();
yield* threadDeletionReactor.start();
yield* agentAwarenessRelay.start();
yield* vcsMaintenanceReactor.start();
});

return {
Expand Down
Loading
Loading