authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-22 17:56:35-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 13:20:36-08:00
log42b510e8c876d97bf505c3e637e521f7c59ef73c
treef1ea5b8d84977ed2ad671a0b06d9aea5f501c260
parentce841a7ef10543478823d44c73da3820b9990829

fix tests


20 files changed, 840 insertions(+), 640 deletions(-)

apps/server/package.json-1
......@@ -18,7 +18,6 @@
1818 },
1919 "dependencies": {
2020 "@effect/platform-node": "4.0.0-beta.10",
21 "@effect/sql-sqlite-node": "4.0.0-beta.10",
2221 "effect": "4.0.0-beta.10",
2322 "node-pty": "^1.1.0",
2423 "open": "^10.1.0",
apps/server/src/checkpointing/Layers/CheckpointService.test.ts+147-112
......@@ -1,7 +1,3 @@
1import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2import os from "node:os";
3import path from "node:path";
4
51import type {
62 ProviderApprovalDecision,
73 ProviderEvent,
......@@ -13,7 +9,7 @@ import type {
139} from "@t3tools/contracts";
1410import { NodeServices } from "@effect/platform-node";
1511import { it, assert } from "@effect/vitest";
16import { Effect, Layer } from "effect";
12import { Effect, FileSystem, Layer, Path } from "effect";
1713
1814import { runGit as runGitProcess } from "../../git/Process.ts";
1915import { CheckpointServiceLive } from "./CheckpointService.ts";
......@@ -34,14 +30,9 @@ import type {
3430} from "../../provider/Services/ProviderAdapter.ts";
3531import { ProviderSessionDirectory } from "../../provider/Services/ProviderSessionDirectory.ts";
3632import { ProviderSessionDirectoryLive } from "../../provider/Layers/ProviderSessionDirectory.ts";
37import { makeSqliteCheckpointRepositoryLive } from "../../persistence/Layers/Checkpoints.ts";
33import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";
3834import { CheckpointRepository } from "../../persistence/Services/Checkpoints.ts";
39
40const makeTemporaryDirectoryScoped = (prefix: string) =>
41 Effect.acquireRelease(
42 Effect.promise(() => mkdtemp(path.join(os.tmpdir(), prefix))),
43 (directory) => Effect.promise(() => rm(directory, { recursive: true, force: true })),
44 );
35import { CheckpointRepositoryLive } from "../../persistence/Layers/Checkpoints.ts";
4536
4637const runGit = (cwd: string, args: readonly string[]) =>
4738 runGitProcess({
......@@ -53,23 +44,24 @@ const runGit = (cwd: string, args: readonly string[]) =>
5344 Effect.provide(NodeServices.layer),
5445 );
5546
56function createRepo() {
57 return Effect.gen(function* () {
58 const cwd = yield* makeTemporaryDirectoryScoped("t3-checkpoint-service-repo-");
59 yield* runGit(cwd, ["init", "--initial-branch=main"]);
60 yield* runGit(cwd, ["config", "user.email", "test@example.com"]);
61 yield* runGit(cwd, ["config", "user.name", "Test User"]);
62 yield* Effect.promise(() => writeFile(path.join(cwd, "README.md"), "v1\n"));
63 yield* runGit(cwd, ["add", "."]);
64 yield* runGit(cwd, ["commit", "-m", "Initial"]);
65 return cwd;
66 });
67}
47const makeGitRepository = Effect.gen(function* () {
48 const fs = yield* FileSystem.FileSystem;
49 const path = yield* Path.Path;
50 const cwd = yield* fs.makeTempDirectory();
51 yield* runGit(cwd, ["init", "--initial-branch=main"]);
52 yield* runGit(cwd, ["config", "user.email", "test@example.com"]);
53 yield* runGit(cwd, ["config", "user.name", "Test User"]);
54 yield* fs.writeFileString(path.join(cwd, "README.md"), "v1\n");
55 yield* runGit(cwd, ["add", "."]);
56 yield* runGit(cwd, ["commit", "-m", "Initial"]);
57 return cwd;
58});
6859
6960class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterError> {
7061 readonly provider = "codex" as const;
7162
7263 private active = true;
64 private readThreadFailure: ProviderAdapterError | undefined;
7365 private snapshot: ProviderThreadSnapshot;
7466 private readonly session: ProviderSession;
7567
......@@ -105,6 +97,10 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr
10597 return this.snapshot.turns;
10698 }
10799
100 setReadThreadFailure(error: ProviderAdapterError | undefined): void {
101 this.readThreadFailure = error;
102 }
103
108104 readonly startSession = (
109105 _input: ProviderSessionStartInput,
110106 ): Effect.Effect<ProviderSession, ProviderAdapterError> =>
......@@ -157,7 +153,9 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr
157153 this.hasSession(sessionId).pipe(
158154 Effect.flatMap((hasSession) =>
159155 hasSession
160 ? Effect.succeed(this.snapshot)
156 ? this.readThreadFailure
157 ? Effect.fail(this.readThreadFailure)
158 : Effect.succeed(this.snapshot)
161159 : Effect.fail(
162160 new ProviderAdapterSessionNotFoundError({
163161 provider: this.provider,
......@@ -203,9 +201,7 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr
203201
204202function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) {
205203 return Effect.gen(function* () {
206 const cwd = yield* createRepo();
207 const stateDir = yield* makeTemporaryDirectoryScoped("t3-checkpoint-service-state-");
208 const dbPath = path.join(stateDir, "orchestration.sqlite");
204 const cwd = yield* makeGitRepository;
209205
210206 const sessionId = "sess-1";
211207 const adapter = new InMemoryProviderAdapter(sessionId, cwd, turns);
......@@ -220,9 +216,10 @@ function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) {
220216
221217 const dependencies = Layer.mergeAll(
222218 Layer.provide(CheckpointStoreLive, NodeServices.layer),
223 makeSqliteCheckpointRepositoryLive(dbPath),
219 Layer.provide(CheckpointRepositoryLive, SqlitePersistenceMemory),
224220 Layer.succeed(ProviderAdapterRegistry, registry),
225221 ProviderSessionDirectoryLive,
222 NodeServices.layer,
226223 );
227224
228225 const layer = CheckpointServiceLive.pipe(Layer.provideMerge(dependencies));
......@@ -236,87 +233,84 @@ function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) {
236233 });
237234}
238235
239it.effect(
240 "CheckpointServiceLive captures checkpoints, diffs refs, and reverts workspace + turns",
241 () =>
242 Effect.gen(function* () {
243 const firstTurn: ProviderThreadTurnSnapshot = {
244 id: "turn-1",
245 items: [{ type: "userMessage", content: [{ type: "text", text: "first" }] }],
246 };
247 const secondTurn: ProviderThreadTurnSnapshot = {
248 id: "turn-2",
249 items: [{ type: "agentMessage", text: "second" }],
250 };
251
252 const fixture = yield* makeFixture([]);
253
254 yield* Effect.gen(function* () {
255 const service = yield* CheckpointService;
256 const directory = yield* ProviderSessionDirectory;
257
258 yield* directory.upsert({
259 sessionId: fixture.sessionId,
260 provider: "codex",
261 threadId: "thread-1",
262 });
263
264 yield* service.initializeForSession({
265 providerSessionId: fixture.sessionId,
266 cwd: fixture.cwd,
267 });
268
269 yield* Effect.promise(() => writeFile(path.join(fixture.cwd, "README.md"), "v2\n"));
270 fixture.adapter.setTurns([firstTurn]);
271
272 yield* service.captureCurrentTurn({
273 providerSessionId: fixture.sessionId,
274 });
275
276 const listed = yield* service.listCheckpoints({ sessionId: fixture.sessionId });
277 assert.equal(listed.checkpoints.length, 2);
278 assert.equal(listed.checkpoints[1]?.id, "turn-1");
279 assert.equal(listed.checkpoints[1]?.isCurrent, true);
280
281 const diff = yield* service.getCheckpointDiff({
282 sessionId: fixture.sessionId,
283 fromTurnCount: 0,
284 toTurnCount: 1,
285 });
286 assert.equal(diff.threadId, "thread-1");
287 assert.equal(diff.diff.includes("diff --git a/README.md b/README.md"), true);
288 assert.equal(diff.diff.includes("-v1"), true);
289 assert.equal(diff.diff.includes("+v2"), true);
290
291 yield* Effect.promise(() => writeFile(path.join(fixture.cwd, "README.md"), "v3\n"));
292 fixture.adapter.setTurns([firstTurn, secondTurn]);
293
294 yield* service.captureCurrentTurn({
295 providerSessionId: fixture.sessionId,
296 });
297
298 const beforeRevert = yield* Effect.promise(() =>
299 readFile(path.join(fixture.cwd, "README.md"), "utf8"),
300 );
301 assert.equal(beforeRevert, "v3\n");
302
303 const reverted = yield* service.revertToCheckpoint({
304 sessionId: fixture.sessionId,
305 turnCount: 1,
306 });
236it.effect("captures checkpoints, diffs refs, and reverts workspace + turns", () =>
237 Effect.gen(function* () {
238 const firstTurn: ProviderThreadTurnSnapshot = {
239 id: "turn-1",
240 items: [{ type: "userMessage", content: [{ type: "text", text: "first" }] }],
241 };
242 const secondTurn: ProviderThreadTurnSnapshot = {
243 id: "turn-2",
244 items: [{ type: "agentMessage", text: "second" }],
245 };
307246
308 const afterRevert = yield* Effect.promise(() =>
309 readFile(path.join(fixture.cwd, "README.md"), "utf8"),
310 );
311 assert.equal(afterRevert, "v2\n");
312 assert.equal(reverted.turnCount, 1);
313 assert.equal(reverted.rolledBackTurns, 1);
314 assert.equal(reverted.checkpoints.length, 2);
315 assert.equal(reverted.checkpoints[1]?.id, "turn-1");
316 assert.equal(reverted.checkpoints[1]?.isCurrent, true);
317 assert.equal(fixture.adapter.getTurns().length, 1);
318 }).pipe(Effect.provide(fixture.layer));
319 }),
247 const fixture = yield* makeFixture([]);
248
249 yield* Effect.gen(function* () {
250 const fs = yield* FileSystem.FileSystem;
251 const path = yield* Path.Path;
252
253 const service = yield* CheckpointService;
254 const directory = yield* ProviderSessionDirectory;
255
256 yield* directory.upsert({
257 sessionId: fixture.sessionId,
258 provider: "codex",
259 threadId: "thread-1",
260 });
261
262 yield* service.initializeForSession({
263 providerSessionId: fixture.sessionId,
264 cwd: fixture.cwd,
265 });
266
267 yield* fs.writeFileString(path.join(fixture.cwd, "README.md"), "v2\n");
268 fixture.adapter.setTurns([firstTurn]);
269
270 yield* service.captureCurrentTurn({
271 providerSessionId: fixture.sessionId,
272 });
273
274 const listed = yield* service.listCheckpoints({ sessionId: fixture.sessionId });
275 assert.equal(listed.checkpoints.length, 2);
276 assert.equal(listed.checkpoints[1]?.id, "turn-1");
277 assert.equal(listed.checkpoints[1]?.isCurrent, true);
278
279 const diff = yield* service.getCheckpointDiff({
280 sessionId: fixture.sessionId,
281 fromTurnCount: 0,
282 toTurnCount: 1,
283 });
284 assert.equal(diff.threadId, "thread-1");
285 assert.equal(diff.diff.includes("diff --git a/README.md b/README.md"), true);
286 assert.equal(diff.diff.includes("-v1"), true);
287 assert.equal(diff.diff.includes("+v2"), true);
288
289 yield* fs.writeFileString(path.join(fixture.cwd, "README.md"), "v3\n");
290 fixture.adapter.setTurns([firstTurn, secondTurn]);
291
292 yield* service.captureCurrentTurn({
293 providerSessionId: fixture.sessionId,
294 });
295
296 const beforeRevert = yield* fs.readFileString(path.join(fixture.cwd, "README.md"));
297 assert.equal(beforeRevert, "v3\n");
298
299 const reverted = yield* service.revertToCheckpoint({
300 sessionId: fixture.sessionId,
301 turnCount: 1,
302 });
303
304 const afterRevert = yield* fs.readFileString(path.join(fixture.cwd, "README.md"));
305 assert.equal(afterRevert, "v2\n");
306 assert.equal(reverted.turnCount, 1);
307 assert.equal(reverted.rolledBackTurns, 1);
308 assert.equal(reverted.checkpoints.length, 2);
309 assert.equal(reverted.checkpoints[1]?.id, "turn-1");
310 assert.equal(reverted.checkpoints[1]?.isCurrent, true);
311 assert.equal(fixture.adapter.getTurns().length, 1);
312 }).pipe(Effect.provide(fixture.layer));
313 }).pipe(Effect.provide(NodeServices.layer)),
320314);
321315
322316it.effect(
......@@ -338,7 +332,7 @@ it.effect(
338332 assert.equal(result.failure._tag, "CheckpointSessionNotFoundError");
339333 }
340334 }).pipe(Effect.provide(fixture.layer));
341 }),
335 }).pipe(Effect.provide(NodeServices.layer)),
342336);
343337
344338it.effect(
......@@ -353,6 +347,8 @@ it.effect(
353347 const fixture = yield* makeFixture([]);
354348
355349 yield* Effect.gen(function* () {
350 const fs = yield* FileSystem.FileSystem;
351 const path = yield* Path.Path;
356352 const service = yield* CheckpointService;
357353 const store = yield* CheckpointStore;
358354 const checkpointRepository = yield* CheckpointRepository;
......@@ -369,7 +365,7 @@ it.effect(
369365 cwd: fixture.cwd,
370366 });
371367
372 yield* Effect.promise(() => writeFile(path.join(fixture.cwd, "README.md"), "v2\n"));
368 yield* fs.writeFileString(path.join(fixture.cwd, "README.md"), "v2\n");
373369 fixture.adapter.setTurns([firstTurn]);
374370 yield* service.captureCurrentTurn({
375371 providerSessionId: fixture.sessionId,
......@@ -400,5 +396,44 @@ it.effect(
400396 assert.equal(result.failure._tag, "CheckpointUnavailableError");
401397 }
402398 }).pipe(Effect.provide(fixture.layer));
403 }),
399 }).pipe(Effect.provide(NodeServices.layer)),
400);
401
402it.effect("CheckpointServiceLive initializes root checkpoints without requiring thread/read", () =>
403 Effect.gen(function* () {
404 const fixture = yield* makeFixture([]);
405
406 yield* Effect.gen(function* () {
407 const service = yield* CheckpointService;
408 const checkpointRepository = yield* CheckpointRepository;
409 const directory = yield* ProviderSessionDirectory;
410
411 yield* directory.upsert({
412 sessionId: fixture.sessionId,
413 provider: "codex",
414 threadId: "thread-1",
415 });
416 fixture.adapter.setReadThreadFailure(
417 new ProviderAdapterValidationError({
418 provider: "codex",
419 operation: "readThread",
420 issue: "thread/read is temporarily unavailable",
421 }),
422 );
423
424 yield* service.initializeForSession({
425 providerSessionId: fixture.sessionId,
426 cwd: fixture.cwd,
427 });
428
429 const root = yield* checkpointRepository.getCheckpoint({
430 providerSessionId: fixture.sessionId,
431 turnCount: 0,
432 });
433 assert.equal(root._tag, "Some");
434 if (root._tag === "Some") {
435 assert.equal(root.value.threadId, "thread-1");
436 }
437 }).pipe(Effect.provide(fixture.layer));
438 }).pipe(Effect.provide(NodeServices.layer)),
404439);
apps/server/src/checkpointing/Layers/CheckpointService.ts+40-12
......@@ -441,6 +441,43 @@ const makeCheckpointService = Effect.gen(function* () {
441441 ),
442442 );
443443
444 const resolveThreadIdForSession = (
445 sessionId: string,
446 operation: string,
447 ): Effect.Effect<string, CheckpointServiceError> =>
448 directory.getThreadId(sessionId).pipe(
449 Effect.mapError(
450 (cause) =>
451 new CheckpointSessionNotFoundError({
452 sessionId,
453 cause,
454 }),
455 ),
456 Effect.flatMap((threadId) =>
457 Option.match(threadId, {
458 onSome: (value) => {
459 const normalizedThreadId = value.trim();
460 if (normalizedThreadId.length > 0) {
461 return Effect.succeed(normalizedThreadId);
462 }
463 return Effect.fail(
464 invariantError(
465 operation,
466 `Resolved empty thread id for session ${sessionId}.`,
467 ),
468 );
469 },
470 onNone: () =>
471 Effect.fail(
472 invariantError(
473 operation,
474 `Missing thread id in session directory for session ${sessionId}.`,
475 ),
476 ),
477 }),
478 ),
479 );
480
444481 const rollbackThreadSnapshot = (
445482 sessionId: string,
446483 adapter: ProviderAdapterShape<ProviderAdapterError>,
......@@ -633,19 +670,10 @@ const makeCheckpointService = Effect.gen(function* () {
633670 Effect.gen(function* () {
634671 const adapter = yield* resolveAdapterForSession(input.providerSessionId);
635672 yield* ensureSessionIsActive(input.providerSessionId, adapter);
636 const snapshot = yield* readThreadSnapshot(
673 const threadId = yield* resolveThreadIdForSession(
637674 input.providerSessionId,
638 adapter,
639 "CheckpointService.initializeForSession:readThread",
675 "CheckpointService.initializeForSession",
640676 );
641 if (snapshot.turns.length !== 0) {
642 return yield* Effect.fail(
643 invariantError(
644 "CheckpointService.initializeForSession",
645 `Expected empty thread snapshot during initialization, received ${snapshot.turns.length} turn(s).`,
646 ),
647 );
648 }
649677
650678 const supportsGit = yield* store.isGitRepository(input.cwd);
651679 if (!supportsGit) {
......@@ -686,7 +714,7 @@ const makeCheckpointService = Effect.gen(function* () {
686714 yield* captureAndPersistCheckpoint({
687715 providerSessionId: input.providerSessionId,
688716 cwd: input.cwd,
689 threadId: snapshot.threadId,
717 threadId,
690718 checkpoint: rootCheckpoint(),
691719 });
692720 yield* setTrackedCheckpointCwd(input.providerSessionId, input.cwd);
apps/server/src/orchestration/Layer.test.ts+34-90
......@@ -1,40 +1,21 @@
1import fs from "node:fs";
2import os from "node:os";
3import path from "node:path";
4
51import type { OrchestrationEvent } from "@t3tools/contracts";
62import { Effect, Layer, ManagedRuntime } from "effect";
7import { afterEach, describe, expect, it } from "vitest";
3import { describe, expect, it } from "vitest";
84
95import {
106 OrchestrationEventRepository,
117 type OrchestrationEventRepositoryShape,
128} from "../persistence/Services/OrchestrationEvents.ts";
139import { PersistenceSqlError } from "../persistence/Errors.ts";
14import { makeSqlitePersistenceLive } from "../persistence/Layers/Sqlite.ts";
10import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
1511import { OrchestrationEngineLive } from "./Layer.ts";
1612import { OrchestrationEngineService } from "./Service.ts";
1713import { OrchestrationEventRepositoryLive } from "../persistence/Layers/OrchestrationEvents.ts";
1814
19const tempDirs: string[] = [];
20
21function makeTempDir(prefix: string): string {
22 const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
23 tempDirs.push(dir);
24 return dir;
25}
26
27afterEach(() => {
28 for (const dir of tempDirs.splice(0, tempDirs.length)) {
29 fs.rmSync(dir, { recursive: true, force: true });
30 }
31});
32
33export async function createOrchestrationSystem(stateDir: string) {
34 const dbPath = path.join(stateDir, "orchestration.sqlite");
15export async function createOrchestrationSystem() {
3516 const orchestrationLayer = OrchestrationEngineLive.pipe(
3617 Layer.provide(OrchestrationEventRepositoryLive),
37 Layer.provide(makeSqlitePersistenceLive(dbPath)),
18 Layer.provide(SqlitePersistenceMemory),
3819 );
3920 const runtime = ManagedRuntime.make(orchestrationLayer);
4021 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));
......@@ -46,16 +27,15 @@ export async function createOrchestrationSystem(stateDir: string) {
4627}
4728
4829describe("OrchestrationEngine", () => {
49 it("replays to the same deterministic snapshot", async () => {
50 const stateDir = makeTempDir("t3code-orchestration-");
30 it("returns deterministic snapshots for repeated reads", async () => {
5131 const createdAt = new Date().toISOString();
5232 const projectId = "project-1";
5333 const threadId = "thread-1";
5434
55 const firstSystem = await createOrchestrationSystem(stateDir);
56 const engineA = firstSystem.engine;
57 await firstSystem.run(
58 engineA.dispatch({
35 const system = await createOrchestrationSystem();
36 const engine = system.engine;
37 await system.run(
38 engine.dispatch({
5939 type: "thread.create",
6040 commandId: "cmd-1",
6141 threadId,
......@@ -67,8 +47,8 @@ describe("OrchestrationEngine", () => {
6747 createdAt,
6848 }),
6949 );
70 await firstSystem.run(
71 engineA.dispatch({
50 await system.run(
51 engine.dispatch({
7252 type: "message.send",
7353 commandId: "cmd-3",
7454 threadId,
......@@ -79,19 +59,14 @@ describe("OrchestrationEngine", () => {
7959 createdAt,
8060 }),
8161 );
82 const snapshotA = await firstSystem.run(engineA.getSnapshot());
83 await firstSystem.dispose();
84
85 const secondSystem = await createOrchestrationSystem(stateDir);
86 const engineB = secondSystem.engine;
87 const snapshotB = await secondSystem.run(engineB.getSnapshot());
62 const snapshotA = await system.run(engine.getSnapshot());
63 const snapshotB = await system.run(engine.getSnapshot());
8864 expect(snapshotB).toEqual(snapshotA);
89 await secondSystem.dispose();
65 await system.dispose();
9066 });
9167
9268 it("fans out read-model updates to subscribers", async () => {
93 const stateDir = makeTempDir("t3code-orchestration-fanout-");
94 const system = await createOrchestrationSystem(stateDir);
69 const system = await createOrchestrationSystem();
9570 const engine = system.engine;
9671 const updates: number[] = [];
9772 const unsubscribe = await system.run(
......@@ -118,8 +93,7 @@ describe("OrchestrationEngine", () => {
11893 });
11994
12095 it("replays append-only events from sequence", async () => {
121 const stateDir = makeTempDir("t3code-orchestration-replay-");
122 const system = await createOrchestrationSystem(stateDir);
96 const system = await createOrchestrationSystem();
12397 const engine = system.engine;
12498 await system.run(
12599 engine.dispatch({
......@@ -150,11 +124,10 @@ describe("OrchestrationEngine", () => {
150124 });
151125
152126 it("stores completed turn summaries even when no files changed", async () => {
153 const stateDir = makeTempDir("t3code-orchestration-turn-diff-");
154 const firstSystem = await createOrchestrationSystem(stateDir);
155 const engine = firstSystem.engine;
127 const system = await createOrchestrationSystem();
128 const engine = system.engine;
156129 const completedAt = new Date().toISOString();
157 await firstSystem.run(
130 await system.run(
158131 engine.dispatch({
159132 type: "thread.create",
160133 commandId: "cmd-thread-turn-diff",
......@@ -167,7 +140,7 @@ describe("OrchestrationEngine", () => {
167140 createdAt: completedAt,
168141 }),
169142 );
170 await firstSystem.run(
143 await system.run(
171144 engine.dispatch({
172145 type: "thread.turnDiff.complete",
173146 commandId: "cmd-turn-diff-complete",
......@@ -181,7 +154,7 @@ describe("OrchestrationEngine", () => {
181154 }),
182155 );
183156
184 const firstThread = (await firstSystem.run(engine.getSnapshot())).threads.find(
157 const firstThread = (await system.run(engine.getSnapshot())).threads.find(
185158 (thread) => thread.id === "thread-turn-diff",
186159 );
187160 expect(firstThread?.turnDiffSummaries).toEqual([
......@@ -193,31 +166,14 @@ describe("OrchestrationEngine", () => {
193166 checkpointTurnCount: 1,
194167 },
195168 ]);
196 await firstSystem.dispose();
197
198 const secondSystem = await createOrchestrationSystem(stateDir);
199 const restartedEngine = secondSystem.engine;
200 const restartedThread = (await secondSystem.run(restartedEngine.getSnapshot())).threads.find(
201 (thread) => thread.id === "thread-turn-diff",
202 );
203 expect(restartedThread?.turnDiffSummaries).toEqual([
204 {
205 turnId: "turn-1",
206 completedAt,
207 status: "completed",
208 files: [],
209 checkpointTurnCount: 1,
210 },
211 ]);
212 await secondSystem.dispose();
169 await system.dispose();
213170 });
214171
215172 it("reverts thread messages and turn summaries to a checkpoint", async () => {
216 const stateDir = makeTempDir("t3code-orchestration-revert-");
217 const firstSystem = await createOrchestrationSystem(stateDir);
218 const engine = firstSystem.engine;
173 const system = await createOrchestrationSystem();
174 const engine = system.engine;
219175 const createdAt = new Date().toISOString();
220 await firstSystem.run(
176 await system.run(
221177 engine.dispatch({
222178 type: "thread.create",
223179 commandId: "cmd-thread-revert",
......@@ -230,7 +186,7 @@ describe("OrchestrationEngine", () => {
230186 createdAt,
231187 }),
232188 );
233 await firstSystem.run(
189 await system.run(
234190 engine.dispatch({
235191 type: "message.send",
236192 commandId: "cmd-msg-1-user",
......@@ -242,7 +198,7 @@ describe("OrchestrationEngine", () => {
242198 createdAt,
243199 }),
244200 );
245 await firstSystem.run(
201 await system.run(
246202 engine.dispatch({
247203 type: "message.send",
248204 commandId: "cmd-msg-1-assistant",
......@@ -254,7 +210,7 @@ describe("OrchestrationEngine", () => {
254210 createdAt,
255211 }),
256212 );
257 await firstSystem.run(
213 await system.run(
258214 engine.dispatch({
259215 type: "thread.turnDiff.complete",
260216 commandId: "cmd-turn-1-complete",
......@@ -270,7 +226,7 @@ describe("OrchestrationEngine", () => {
270226 );
271227
272228 const createdAtSecond = new Date(Date.now() + 1_000).toISOString();
273 await firstSystem.run(
229 await system.run(
274230 engine.dispatch({
275231 type: "message.send",
276232 commandId: "cmd-msg-2-user",
......@@ -282,7 +238,7 @@ describe("OrchestrationEngine", () => {
282238 createdAt: createdAtSecond,
283239 }),
284240 );
285 await firstSystem.run(
241 await system.run(
286242 engine.dispatch({
287243 type: "message.send",
288244 commandId: "cmd-msg-2-assistant",
......@@ -294,7 +250,7 @@ describe("OrchestrationEngine", () => {
294250 createdAt: createdAtSecond,
295251 }),
296252 );
297 await firstSystem.run(
253 await system.run(
298254 engine.dispatch({
299255 type: "thread.turnDiff.complete",
300256 commandId: "cmd-turn-2-complete",
......@@ -309,7 +265,7 @@ describe("OrchestrationEngine", () => {
309265 }),
310266 );
311267
312 await firstSystem.run(
268 await system.run(
313269 engine.dispatch({
314270 type: "thread.revert",
315271 commandId: "cmd-thread-revert-apply",
......@@ -320,25 +276,13 @@ describe("OrchestrationEngine", () => {
320276 }),
321277 );
322278
323 const thread = (await firstSystem.run(engine.getSnapshot())).threads.find(
279 const thread = (await system.run(engine.getSnapshot())).threads.find(
324280 (entry) => entry.id === "thread-revert",
325281 );
326282 expect(thread?.messages.map((message) => message.id)).toEqual(["user-1", "assistant:turn-1"]);
327283 expect(thread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]);
328284 expect(thread?.latestTurnId).toBe("turn-1");
329 await firstSystem.dispose();
330
331 const secondSystem = await createOrchestrationSystem(stateDir);
332 const restarted = secondSystem.engine;
333 const restartedThread = (await secondSystem.run(restarted.getSnapshot())).threads.find(
334 (entry) => entry.id === "thread-revert",
335 );
336 expect(restartedThread?.messages.map((message) => message.id)).toEqual([
337 "user-1",
338 "assistant:turn-1",
339 ]);
340 expect(restartedThread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]);
341 await secondSystem.dispose();
285 await system.dispose();
342286 });
343287
344288 it("allows stop to be called multiple times", async () => {
apps/server/src/persistence/Layers/Checkpoints.test.ts+4-17
......@@ -1,29 +1,16 @@
1import { mkdtemp, rm } from "node:fs/promises";
2import os from "node:os";
3import path from "node:path";
4
51import { it, assert } from "@effect/vitest";
62import { Effect, Layer, Option } from "effect";
73
84import { CheckpointRepository } from "../Services/Checkpoints.ts";
95import { CheckpointRepositoryLive } from "./Checkpoints.ts";
10import { makeSqlitePersistenceLive } from "./Sqlite.ts";
11
12const makeTemporaryDirectoryScoped = (prefix: string) =>
13 Effect.acquireRelease(
14 Effect.promise(() => mkdtemp(path.join(os.tmpdir(), prefix))),
15 (directory) => Effect.promise(() => rm(directory, { recursive: true, force: true })),
16 );
6import { SqlitePersistenceMemory } from "./Sqlite.ts";
177
18function makeCheckpointRepositoryLayer(dbPath: string) {
19 return CheckpointRepositoryLive.pipe(Layer.provide(makeSqlitePersistenceLive(dbPath)));
8function makeCheckpointRepositoryLayer() {
9 return CheckpointRepositoryLive.pipe(Layer.provide(SqlitePersistenceMemory));
2010}
2111
2212it.effect("CheckpointRepositoryLive upserts, lists, reads, and prunes checkpoint metadata", () =>
2313 Effect.gen(function* () {
24 const stateDir = yield* makeTemporaryDirectoryScoped("t3code-checkpoint-repository-");
25 const dbPath = path.join(stateDir, "orchestration.sqlite");
26
2714 yield* Effect.gen(function* () {
2815 const repository = yield* CheckpointRepository;
2916
......@@ -90,6 +77,6 @@ it.effect("CheckpointRepositoryLive upserts, lists, reads, and prunes checkpoint
9077 yield* repository.deleteAllForSession({ providerSessionId: "sess-1" });
9178 const empty = yield* repository.listCheckpoints({ providerSessionId: "sess-1" });
9279 assert.equal(empty.length, 0);
93 }).pipe(Effect.provide(makeCheckpointRepositoryLayer(dbPath)));
80 }).pipe(Effect.provide(makeCheckpointRepositoryLayer()));
9481 }),
9582);
apps/server/src/persistence/Layers/Checkpoints.ts-5
......@@ -20,7 +20,6 @@ import {
2020 CheckpointRepositoryValidationError,
2121 type CheckpointRepositoryError,
2222} from "../Errors.ts";
23import { makeSqlitePersistenceLive } from "./Sqlite.ts";
2423import {
2524 CheckpointRepository,
2625 type CheckpointRepositoryEntry,
......@@ -357,7 +356,3 @@ export const CheckpointRepositoryLive = Layer.effect(
357356 CheckpointRepository,
358357 makeCheckpointRepository,
359358);
360
361export function makeSqliteCheckpointRepositoryLive(dbPath: string) {
362 return CheckpointRepositoryLive.pipe(Layer.provide(makeSqlitePersistenceLive(dbPath)));
363}
apps/server/src/persistence/Layers/OrchestrationEvents.test.ts+26-86
......@@ -1,39 +1,25 @@
1import fs from "node:fs";
2import os from "node:os";
3import path from "node:path";
4import { DatabaseSync } from "node:sqlite";
5
6import { ManagedRuntime, Effect } from "effect";
7import { afterEach, describe, expect, it } from "vitest";
1import { Effect, Layer } from "effect";
2import { assert, it } from "@effect/vitest";
83
94import { OrchestrationEventRepository } from "../Services/OrchestrationEvents.ts";
10import { makeSqliteOrchestrationEventRepositoryLive } from "./OrchestrationEvents.ts";
11
12const tempDirs: string[] = [];
13
14function makeTempDir(prefix: string): string {
15 const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
16 tempDirs.push(dir);
17 return dir;
18}
19
20afterEach(() => {
21 for (const dir of tempDirs.splice(0, tempDirs.length)) {
22 fs.rmSync(dir, { recursive: true, force: true });
23 }
24});
25
26describe("OrchestrationEventRepository", () => {
27 it("persists and replays events across restarts", async () => {
28 const stateDir = makeTempDir("t3code-event-store-");
29 const dbPath = path.join(stateDir, "orchestration.sqlite");
30
31 const createdAt = new Date().toISOString();
32
33 const firstRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath));
34 const first = await firstRuntime.runPromise(Effect.service(OrchestrationEventRepository));
35 const saved = await firstRuntime.runPromise(
36 first.append({
5import { SqlitePersistenceMemory } from "./Sqlite.ts";
6import { OrchestrationEventRepositoryLive } from "./OrchestrationEvents.ts";
7import { NodeServices } from "@effect/platform-node";
8
9const layer = it.layer(
10 Layer.mergeAll(
11 Layer.provide(OrchestrationEventRepositoryLive, SqlitePersistenceMemory),
12 NodeServices.layer,
13 ),
14);
15
16layer("OrchestrationEventRepository", (it) => {
17 it.effect("stores and replays events", () =>
18 Effect.gen(function* () {
19 const createdAt = new Date().toISOString();
20
21 const repository = yield* OrchestrationEventRepository;
22 const saved = yield* repository.append({
3723 eventId: "event-1",
3824 type: "thread.created",
3925 aggregateType: "thread",
......@@ -41,57 +27,11 @@ describe("OrchestrationEventRepository", () => {
4127 occurredAt: createdAt,
4228 commandId: "cmd-1",
4329 payload: { id: "thread-1", projectId: "project-1", title: "demo" },
44 }),
45 );
46 expect(saved.sequence).toBe(1);
47 await firstRuntime.dispose();
48
49 const secondRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath));
50 const second = await secondRuntime.runPromise(Effect.service(OrchestrationEventRepository));
51 const replayed = await secondRuntime.runPromise(second.readFromSequence(0));
52 expect(replayed).toEqual([saved]);
53 await secondRuntime.dispose();
54 });
55
56 it("creates and reuses the migrator tracking table", async () => {
57 const stateDir = makeTempDir("t3code-event-store-migrations-");
58 const dbPath = path.join(stateDir, "orchestration.sqlite");
59
60 const firstRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath));
61 const first = await firstRuntime.runPromise(Effect.service(OrchestrationEventRepository));
62 await firstRuntime.runPromise(first.readAll());
63 await firstRuntime.dispose();
64
65 const secondRuntime = ManagedRuntime.make(makeSqliteOrchestrationEventRepositoryLive(dbPath));
66 const second = await secondRuntime.runPromise(Effect.service(OrchestrationEventRepository));
67 await secondRuntime.runPromise(second.readAll());
68 await secondRuntime.dispose();
69
70 const db = new DatabaseSync(dbPath, { readOnly: true });
71 const tableRow = db
72 .prepare(
73 `
74 SELECT name
75 FROM sqlite_master
76 WHERE type = 'table' AND name IN ('effect_sql_migrations', 'orchestration_sql_migrations')
77 ORDER BY name ASC
78 `,
79 )
80 .get() as { name: string } | undefined;
81 expect(tableRow?.name).toBeDefined();
82 expect(tableRow).toBeDefined();
83 if (!tableRow) return;
30 });
31 assert.equal(saved.sequence, 1);
8432
85 const migrationRows = db
86 .prepare(
87 `
88 SELECT *
89 FROM ${tableRow.name}
90 `,
91 )
92 .all() as Array<Record<string, unknown>>;
93 expect(migrationRows.length).toBeGreaterThanOrEqual(1);
94 expect(Object.values(migrationRows[0] ?? {}).length).toBeGreaterThan(0);
95 db.close();
96 });
33 const replayed = yield* repository.readFromSequence(0);
34 assert.deepEqual(replayed, [saved]);
35 }),
36 );
9737});
apps/server/src/persistence/Layers/OrchestrationEvents.ts-5
......@@ -11,7 +11,6 @@ import {
1111 toPersistenceSqlError,
1212 type OrchestrationEventRepositoryError,
1313} from "../Errors.ts";
14import { makeSqlitePersistenceLive } from "./Sqlite.ts";
1514import {
1615 OrchestrationEventRepository,
1716 type OrchestrationEventRepositoryShape,
......@@ -177,7 +176,3 @@ export const OrchestrationEventRepositoryLive = Layer.effect(
177176 OrchestrationEventRepository,
178177 makeRepository,
179178);
180
181export function makeSqliteOrchestrationEventRepositoryLive(dbPath: string) {
182 return OrchestrationEventRepositoryLive.pipe(Layer.provide(makeSqlitePersistenceLive(dbPath)));
183}
apps/server/src/persistence/Layers/Projects.test.ts+40-67
......@@ -1,83 +1,56 @@
1import fs from "node:fs";
2import os from "node:os";
3import path from "node:path";
4
5import { Effect, Layer, ManagedRuntime } from "effect";
6import { afterEach, describe, expect, it } from "vitest";
1import { Effect, Layer, FileSystem } from "effect";
2import { assert, it } from "@effect/vitest";
73
84import { ProjectRepository } from "../Services/Projects.ts";
95import { ProjectRepositoryLive } from "./Projects.ts";
10import { makeSqlitePersistenceLive } from "./Sqlite.ts";
11
12function makeProjectRepositoryTest(dbPath: string) {
13 return ProjectRepositoryLive.pipe(Layer.provide(makeSqlitePersistenceLive(dbPath)));
14}
15
16const tempDirs: string[] = [];
17
18function makeTempDir(prefix: string): string {
19 const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
20 tempDirs.push(dir);
21 return dir;
22}
23
24afterEach(() => {
25 for (const dir of tempDirs.splice(0, tempDirs.length)) {
26 fs.rmSync(dir, { recursive: true, force: true });
27 }
28});
29
30describe("ProjectRepository", () => {
31 it("persists projects and deduplicates adds by cwd", async () => {
32 const stateDir = makeTempDir("t3code-project-repo-state-");
33 const projectDir = makeTempDir("t3code-project-repo-project-");
34 const dbPath = path.join(stateDir, "orchestration.sqlite");
35
36 const firstRuntime = ManagedRuntime.make(makeProjectRepositoryTest(dbPath));
37 const first = await firstRuntime.runPromise(Effect.service(ProjectRepository));
38
39 const created = await firstRuntime.runPromise(first.add({ cwd: projectDir }));
40 expect(created.created).toBe(true);
41
42 const duplicate = await firstRuntime.runPromise(first.add({ cwd: projectDir }));
43 expect(duplicate.created).toBe(false);
44 expect(duplicate.project.id).toBe(created.project.id);
6import { SqlitePersistenceMemory } from "./Sqlite.ts";
7import { NodeServices } from "@effect/platform-node";
458
46 await firstRuntime.dispose();
9const layer = it.layer(
10 Layer.mergeAll(Layer.provide(ProjectRepositoryLive, SqlitePersistenceMemory), NodeServices.layer),
11);
4712
48 const secondRuntime = ManagedRuntime.make(makeProjectRepositoryTest(dbPath));
49 const second = await secondRuntime.runPromise(Effect.service(ProjectRepository));
50 const listed = await secondRuntime.runPromise(second.list());
13layer("ProjectRepository", (it) => {
14 it.effect("stores projects and deduplicates adds by cwd", () =>
15 Effect.gen(function* () {
16 const fs = yield* FileSystem.FileSystem;
17 const projectDir = yield* fs.makeTempDirectoryScoped();
5118
52 expect(listed).toHaveLength(1);
53 expect(listed[0]?.cwd).toBe(projectDir);
19 const repository = yield* ProjectRepository;
5420
55 await secondRuntime.dispose();
56 });
21 const created = yield* repository.add({ cwd: projectDir });
22 assert.equal(created.created, true);
5723
58 it("prunes missing project paths", async () => {
59 const stateDir = makeTempDir("t3code-project-repo-prune-state-");
60 const existing = makeTempDir("t3code-project-repo-prune-existing-");
61 const missing = makeTempDir("t3code-project-repo-prune-missing-");
62 const dbPath = path.join(stateDir, "orchestration.sqlite");
24 const duplicate = yield* repository.add({ cwd: projectDir });
25 assert.equal(duplicate.created, false);
26 assert.equal(duplicate.project.id, created.project.id);
6327
64 const runtime = ManagedRuntime.make(makeProjectRepositoryTest(dbPath));
65 const repository = await runtime.runPromise(Effect.service(ProjectRepository));
28 const listed = yield* repository.list();
29 assert.equal(listed.length, 1);
30 assert.equal(listed[0]?.cwd, projectDir);
31 }),
32 );
6633
67 const existingProject = await runtime.runPromise(repository.add({ cwd: existing }));
68 const missingProject = await runtime.runPromise(repository.add({ cwd: missing }));
34 it.effect("prunes missing project paths", () =>
35 Effect.gen(function* () {
36 const fs = yield* FileSystem.FileSystem;
37 const existing = yield* fs.makeTempDirectoryScoped();
38 const missing = yield* fs.makeTempDirectory();
6939
70 expect(existingProject.created).toBe(true);
71 expect(missingProject.created).toBe(true);
40 const repository = yield* ProjectRepository;
7241
73 fs.rmSync(missing, { recursive: true, force: true });
42 const existingProject = yield* repository.add({ cwd: existing });
43 const missingProject = yield* repository.add({ cwd: missing });
7444
75 await runtime.runPromise(repository.pruneMissing());
76 const listed = await runtime.runPromise(repository.list());
45 assert.isTrue(existingProject.created);
46 assert.isTrue(missingProject.created);
7747
78 expect(listed).toHaveLength(1);
79 expect(listed[0]?.cwd).toBe(existing);
48 yield* fs.remove(missing, { recursive: true, force: true });
8049
81 await runtime.dispose();
82 });
50 yield* repository.pruneMissing();
51 const listed = yield* repository.list();
52 assert.equal(listed.length, 1);
53 assert.equal(listed[0]?.cwd, existing);
54 }),
55 );
8356});
apps/server/src/persistence/Layers/Sqlite.ts+18-35
......@@ -1,41 +1,24 @@
1import fs from "node:fs";
2import path from "node:path";
3
4import * as SqlClient from "effect/unstable/sql/SqlClient";
5import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient";
6import { Effect, Layer, ServiceMap } from "effect";
1import { Effect, Layer, FileSystem, Path } from "effect";
72
83import { runMigrations } from "../Migrations.ts";
4import * as SqliteClient from "../SqliteClient.ts";
95
10export interface PersistenceConfigShape {
11 readonly dbPath: string;
12}
13
14export class PersistenceConfig extends ServiceMap.Service<
15 PersistenceConfig,
16 PersistenceConfigShape
17>()("persistence/Config") {}
6const setup = Layer.effectDiscard(
7 Effect.gen(function* () {
8 const sql = yield* SqliteClient.SqliteClient;
9 yield* sql`PRAGMA journal_mode = WAL;`;
10 yield* sql`PRAGMA foreign_keys = ON;`;
11 yield* runMigrations;
12 }),
13);
1814
19export function makeSqlitePersistenceLive(dbPath: string) {
20 const SqliteClientLive = Layer.unwrap(
21 Effect.sync(() => {
22 fs.mkdirSync(path.dirname(dbPath), { recursive: true });
23 return SqliteClient.layer({ filename: dbPath });
24 }),
25 );
15export const makeSqlitePersistenceLive = (dbPath: string) =>
16 Effect.gen(function* () {
17 const fs = yield* FileSystem.FileSystem;
18 const path = yield* Path.Path;
19 yield* fs.makeDirectory(path.dirname(dbPath), { recursive: true });
2620
27 return Layer.effect(
28 SqlClient.SqlClient,
29 Effect.gen(function* () {
30 const sql = yield* SqliteClient.SqliteClient;
31 yield* sql`PRAGMA journal_mode = WAL;`;
32 yield* sql`PRAGMA foreign_keys = ON;`;
33 yield* runMigrations;
34 return sql;
35 }),
36 ).pipe(Layer.provide(SqliteClientLive));
37}
21 return Layer.provideMerge(setup, SqliteClient.layer({ filename: dbPath }));
22 }).pipe(Layer.unwrap);
3823
39export const SqlitePersistenceLive = Layer.unwrap(
40 Effect.map(Effect.service(PersistenceConfig), ({ dbPath }) => makeSqlitePersistenceLive(dbPath)),
41);
24export const SqlitePersistenceMemory = Layer.provideMerge(setup, SqliteClient.layerMemory());
apps/server/src/persistence/Migrations.ts+1-1
......@@ -59,7 +59,7 @@ export const runMigrations = run({ loader });
5959 * @example
6060 * ```typescript
6161 * import { MigrationsLive } from "@acme/db/Migrations"
62 * import { SqliteClient } from "@effect/sql-sqlite-node"
62 * import * as SqliteClient from "@acme/db/SqliteClient"
6363 *
6464 * // Migrations run automatically when SqliteClient is provided
6565 * const AppLayer = MigrationsLive.pipe(
apps/server/src/persistence/SqliteClient.test.ts created+26
......@@ -0,0 +1,26 @@
1import { assert, it } from "@effect/vitest";
2import { Effect } from "effect";
3import * as SqlClient from "effect/unstable/sql/SqlClient";
4
5import * as SqliteClient from "./SqliteClient.ts";
6
7it.effect("runs prepared queries and returns positional values", () =>
8 Effect.gen(function* () {
9 const sql = yield* SqlClient.SqlClient;
10
11 yield* sql`CREATE TABLE entries(id INTEGER PRIMARY KEY, name TEXT NOT NULL)`;
12 yield* sql`INSERT INTO entries(name) VALUES (${"alpha"}), (${"beta"})`;
13
14 const rows = yield* sql<{ readonly id: number; readonly name: string }>`
15 SELECT id, name FROM entries ORDER BY id
16 `;
17 assert.equal(rows.length, 2);
18 assert.equal(rows[0]?.name, "alpha");
19 assert.equal(rows[1]?.name, "beta");
20
21 const values = yield* sql`SELECT id, name FROM entries ORDER BY id`.values;
22 assert.equal(values.length, 2);
23 assert.equal(values[0]?.[1], "alpha");
24 assert.equal(values[1]?.[1], "beta");
25 }).pipe(Effect.provide(SqliteClient.layerMemory())),
26);
apps/server/src/persistence/SqliteClient.ts created+242
......@@ -0,0 +1,242 @@
1/**
2 * Port of `@effect/sql-sqlite-node` that uses the native `node:sqlite`
3 * bindings instead of `better-sqlite3`.
4 */
5import { DatabaseSync, type StatementSync } from "node:sqlite";
6
7import * as Cache from "effect/Cache";
8import * as Config from "effect/Config";
9import * as Duration from "effect/Duration";
10import * as Effect from "effect/Effect";
11import * as Fiber from "effect/Fiber";
12import { identity } from "effect/Function";
13import * as Layer from "effect/Layer";
14import * as Scope from "effect/Scope";
15import * as Semaphore from "effect/Semaphore";
16import * as ServiceMap from "effect/ServiceMap";
17import * as Stream from "effect/Stream";
18import * as Reactivity from "effect/unstable/reactivity/Reactivity";
19import * as Client from "effect/unstable/sql/SqlClient";
20import type { Connection } from "effect/unstable/sql/SqlConnection";
21import { SqlError } from "effect/unstable/sql/SqlError";
22import * as Statement from "effect/unstable/sql/Statement";
23
24const ATTR_DB_SYSTEM_NAME = "db.system.name";
25
26export const TypeId: TypeId = "~local/sqlite-node/SqliteClient";
27
28export type TypeId = "~local/sqlite-node/SqliteClient";
29
30export const SqliteClient = ServiceMap.Service<Client.SqlClient>("@local/sqlite-node/SqliteClient");
31
32export interface SqliteClientConfig {
33 readonly filename: string;
34 readonly readonly?: boolean | undefined;
35 readonly allowExtension?: boolean | undefined;
36 readonly prepareCacheSize?: number | undefined;
37 readonly prepareCacheTTL?: Duration.Input | undefined;
38 readonly disableWAL?: boolean | undefined;
39 readonly spanAttributes?: Record<string, unknown> | undefined;
40 readonly transformResultNames?: ((str: string) => string) | undefined;
41 readonly transformQueryNames?: ((str: string) => string) | undefined;
42}
43
44export interface SqliteMemoryClientConfig extends Omit<
45 SqliteClientConfig,
46 "filename" | "readonly"
47> {}
48
49const makeWithDatabase = (
50 options: SqliteClientConfig,
51 openDatabase: () => DatabaseSync,
52): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> =>
53 Effect.gen(function* () {
54 const compiler = Statement.makeCompilerSqlite(options.transformQueryNames);
55 const transformRows = options.transformResultNames
56 ? Statement.defaultTransforms(options.transformResultNames).array
57 : undefined;
58
59 const makeConnection = Effect.gen(function* () {
60 const scope = yield* Effect.scope;
61 const db = openDatabase();
62 yield* Scope.addFinalizer(
63 scope,
64 Effect.sync(() => db.close()),
65 );
66
67 if (options.disableWAL !== true) {
68 db.exec("PRAGMA journal_mode = WAL");
69 }
70
71 const statementReaderCache = new WeakMap<StatementSync, boolean>();
72 const hasRows = (statement: StatementSync): boolean => {
73 const cached = statementReaderCache.get(statement);
74 if (cached !== undefined) {
75 return cached;
76 }
77 const value = statement.columns().length > 0;
78 statementReaderCache.set(statement, value);
79 return value;
80 };
81
82 const prepareCache = yield* Cache.make({
83 capacity: options.prepareCacheSize ?? 200,
84 timeToLive: options.prepareCacheTTL ?? Duration.minutes(10),
85 lookup: (sql: string) =>
86 Effect.try({
87 try: () => db.prepare(sql),
88 catch: (cause) => new SqlError({ cause, message: "Failed to prepare statement" }),
89 }),
90 });
91
92 const runStatement = (
93 statement: StatementSync,
94 params: ReadonlyArray<unknown>,
95 raw: boolean,
96 ) =>
97 Effect.withFiber<ReadonlyArray<any>, SqlError>((fiber) => {
98 statement.setReadBigInts(Boolean(ServiceMap.get(fiber.services, Client.SafeIntegers)));
99 try {
100 if (hasRows(statement)) {
101 return Effect.succeed(statement.all(...(params as any)));
102 }
103 const result = statement.run(...(params as any));
104 return Effect.succeed(raw ? (result as unknown as ReadonlyArray<any>) : []);
105 } catch (cause) {
106 return Effect.fail(new SqlError({ cause, message: "Failed to execute statement" }));
107 }
108 });
109
110 const run = (sql: string, params: ReadonlyArray<unknown>, raw = false) =>
111 Effect.flatMap(Cache.get(prepareCache, sql), (s) => runStatement(s, params, raw));
112
113 const runValues = (sql: string, params: ReadonlyArray<unknown>) =>
114 Effect.acquireUseRelease(
115 Cache.get(prepareCache, sql),
116 (statement) =>
117 Effect.try({
118 try: () => {
119 if (hasRows(statement)) {
120 statement.setReturnArrays(true);
121 // Safe to cast to array after we've setReturnArrays(true)
122 return statement.all(...(params as any)) as unknown as ReadonlyArray<
123 ReadonlyArray<unknown>
124 >;
125 }
126 statement.run(...(params as any));
127 return [];
128 },
129 catch: (cause) => new SqlError({ cause, message: "Failed to execute statement" }),
130 }),
131 (statement) =>
132 Effect.sync(() => {
133 if (hasRows(statement)) {
134 statement.setReturnArrays(false);
135 }
136 }),
137 );
138
139 return identity<Connection>({
140 execute(sql, params, rowTransform) {
141 return rowTransform ? Effect.map(run(sql, params), rowTransform) : run(sql, params);
142 },
143 executeRaw(sql, params) {
144 return run(sql, params, true);
145 },
146 executeValues(sql, params) {
147 return runValues(sql, params);
148 },
149 executeUnprepared(sql, params, rowTransform) {
150 const effect = runStatement(db.prepare(sql), params ?? [], false);
151 return rowTransform ? Effect.map(effect, rowTransform) : effect;
152 },
153 executeStream(_sql, _params) {
154 return Stream.die("executeStream not implemented");
155 },
156 });
157 });
158
159 const semaphore = yield* Semaphore.make(1);
160 const connection = yield* makeConnection;
161
162 const acquirer = semaphore.withPermits(1)(Effect.succeed(connection));
163 const transactionAcquirer = Effect.uninterruptibleMask((restore) => {
164 const fiber = Fiber.getCurrent()!;
165 const scope = ServiceMap.getUnsafe(fiber.services, Scope.Scope);
166 return Effect.as(
167 Effect.tap(restore(semaphore.take(1)), () =>
168 Scope.addFinalizer(scope, semaphore.release(1)),
169 ),
170 connection,
171 );
172 });
173
174 return yield* Client.make({
175 acquirer,
176 compiler,
177 transactionAcquirer,
178 spanAttributes: [
179 ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []),
180 [ATTR_DB_SYSTEM_NAME, "sqlite"],
181 ],
182 transformRows,
183 });
184 });
185
186const make = (
187 options: SqliteClientConfig,
188): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> =>
189 makeWithDatabase(
190 options,
191 () =>
192 new DatabaseSync(options.filename, {
193 readOnly: options.readonly ?? false,
194 allowExtension: options.allowExtension ?? false,
195 }),
196 );
197
198const makeMemory = (
199 config: SqliteMemoryClientConfig = {},
200): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> =>
201 makeWithDatabase(
202 {
203 ...config,
204 filename: ":memory:",
205 readonly: false,
206 disableWAL: config.disableWAL ?? true,
207 },
208 () => {
209 const database = new DatabaseSync(":memory:", {
210 allowExtension: config.allowExtension ?? false,
211 });
212 return database;
213 },
214 );
215
216export const layerConfig = (
217 config: Config.Wrap<SqliteClientConfig>,
218): Layer.Layer<Client.SqlClient, Config.ConfigError> =>
219 Layer.effectServices(
220 Config.unwrap(config)
221 .asEffect()
222 .pipe(
223 Effect.flatMap(make),
224 Effect.map((client) =>
225 ServiceMap.make(SqliteClient, client).pipe(ServiceMap.add(Client.SqlClient, client)),
226 ),
227 ),
228 ).pipe(Layer.provide(Reactivity.layer));
229
230export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> =>
231 Layer.effectServices(
232 Effect.map(make(config), (client) =>
233 ServiceMap.make(SqliteClient, client).pipe(ServiceMap.add(Client.SqlClient, client)),
234 ),
235 ).pipe(Layer.provide(Reactivity.layer));
236
237export const layerMemory = (config: SqliteMemoryClientConfig = {}): Layer.Layer<Client.SqlClient> =>
238 Layer.effectServices(
239 Effect.map(makeMemory(config), (client) =>
240 ServiceMap.make(SqliteClient, client).pipe(ServiceMap.add(Client.SqlClient, client)),
241 ),
242 ).pipe(Layer.provide(Reactivity.layer));
apps/server/src/provider/Layers/ProviderAdapterRegistry.test.ts+11-7
......@@ -6,8 +6,9 @@ import { Effect, Layer } from "effect";
66
77import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";
88import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
9import { makeProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
9import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
1010import { ProviderUnsupportedError } from "../Errors.ts";
11import { NodeServices } from "@effect/platform-node";
1112
1213const fakeCodexAdapter: CodexAdapterShape = {
1314 provider: "codex",
......@@ -25,13 +26,14 @@ const fakeCodexAdapter: CodexAdapterShape = {
2526};
2627
2728const layer = it.layer(
28 makeProviderAdapterRegistryLive().pipe(
29 Layer.provide(Layer.succeed(CodexAdapter, fakeCodexAdapter)),
29 Layer.mergeAll(
30 Layer.provide(ProviderAdapterRegistryLive, Layer.succeed(CodexAdapter, fakeCodexAdapter)),
31 NodeServices.layer,
3032 ),
3133);
3234
3335layer("ProviderAdapterRegistryLive", (it) => {
34 it("resolves a registered provider adapter", () =>
36 it.effect("resolves a registered provider adapter", () =>
3537 Effect.gen(function* () {
3638 const registry = yield* ProviderAdapterRegistry;
3739 const adapter = yield* registry.getByProvider("codex");
......@@ -39,12 +41,14 @@ layer("ProviderAdapterRegistryLive", (it) => {
3941
4042 const providers = yield* registry.listProviders();
4143 assert.deepEqual(providers, ["codex"]);
42 }));
44 }),
45 );
4346
44 it("fails with ProviderUnsupportedError for unknown providers", () =>
47 it.effect("fails with ProviderUnsupportedError for unknown providers", () =>
4548 Effect.gen(function* () {
4649 const registry = yield* ProviderAdapterRegistry;
4750 const adapter = yield* registry.getByProvider("unknown" as ProviderKind).pipe(Effect.result);
4851 assertFailure(adapter, new ProviderUnsupportedError({ provider: "unknown" }));
49 }));
52 }),
53 );
5054});
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts-4
......@@ -51,7 +51,3 @@ export const ProviderAdapterRegistryLive = Layer.effect(
5151 ProviderAdapterRegistry,
5252 makeProviderAdapterRegistry(),
5353);
54
55export function makeProviderAdapterRegistryLive(options?: ProviderAdapterRegistryLiveOptions) {
56 return Layer.effect(ProviderAdapterRegistry, makeProviderAdapterRegistry(options));
57}
apps/server/src/provider/Layers/ProviderService.test.ts+46-7
......@@ -31,6 +31,7 @@ import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"
3131import { ProviderService } from "../Services/ProviderService.ts";
3232import { ProviderServiceLive } from "./ProviderService.ts";
3333import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts";
34import { NodeServices } from "@effect/platform-node";
3435
3536function makeFakeCodexAdapter() {
3637 const sessions = new Map<string, ProviderSession>();
......@@ -253,15 +254,17 @@ function makeProviderServiceLayer() {
253254 listProviders: () => Effect.succeed(["codex"]),
254255 };
255256
257 const checkpointLayer = Layer.succeed(CheckpointService, checkpoint.service);
258 const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry);
259
256260 const layer = it.layer(
257 ProviderServiceLive.pipe(
258 Layer.provide(
259 Layer.mergeAll(
260 Layer.succeed(ProviderAdapterRegistry, registry),
261 ProviderSessionDirectoryLive,
262 Layer.succeed(CheckpointService, checkpoint.service),
263 ),
261 Layer.mergeAll(
262 ProviderServiceLive.pipe(
263 Layer.provide(providerAdapterLayer),
264 Layer.provide(ProviderSessionDirectoryLive),
265 Layer.provide(checkpointLayer),
264266 ),
267 NodeServices.layer,
265268 ),
266269 );
267270
......@@ -424,4 +427,40 @@ validation.layer("ProviderServiceLive validation", (it) => {
424427 );
425428 }),
426429 );
430
431 it.effect("fails startSession when adapter returns no threadId", () =>
432 Effect.gen(function* () {
433 const provider = yield* ProviderService;
434
435 validation.codex.startSession.mockImplementationOnce((input: ProviderSessionStartInput) =>
436 Effect.sync(() => {
437 const now = new Date().toISOString();
438 return {
439 sessionId: "sess-missing-thread",
440 provider: "codex",
441 status: "ready",
442 cwd: input.cwd ?? process.cwd(),
443 createdAt: now,
444 updatedAt: now,
445 } satisfies ProviderSession;
446 }),
447 );
448
449 const failure = yield* Effect.result(
450 provider.startSession({
451 provider: "codex",
452 cwd: "/tmp/project",
453 }),
454 );
455
456 assertFailure(
457 failure,
458 new ProviderValidationError({
459 operation: "ProviderService.startSession",
460 issue:
461 "Provider 'codex' returned a session without threadId. threadId is required for checkpoint initialization.",
462 }),
463 );
464 }),
465 );
427466});
apps/server/src/provider/Layers/ProviderService.ts+11-1
......@@ -161,10 +161,20 @@ const makeProviderService = Effect.gen(function* () {
161161 );
162162 }
163163
164 const threadId = session.threadId?.trim();
165 if (!threadId) {
166 return yield* Effect.fail(
167 toValidationError(
168 "ProviderService.startSession",
169 `Provider '${adapter.provider}' returned a session without threadId. threadId is required for checkpoint initialization.`,
170 ),
171 );
172 }
173
164174 yield* directory.upsert({
165175 sessionId: session.sessionId,
166176 provider: session.provider,
167 ...(session.threadId !== undefined ? { threadId: session.threadId } : {}),
177 threadId,
168178 });
169179
170180 const checkpointCwd = session.cwd ?? input.cwd ?? process.cwd();
apps/server/src/wsServer.test.ts+13-5
......@@ -30,7 +30,9 @@ import type {
3030} from "@t3tools/contracts";
3131import type { TerminalManager } from "./terminalManager";
3232import { ProjectRepositoryLive } from "./persistence/Layers/Projects";
33import { makeSqlitePersistenceLive } from "./persistence/Layers/Sqlite";
33import { makeSqlitePersistenceLive, SqlitePersistenceMemory } from "./persistence/Layers/Sqlite";
34import { NodeServices } from "@effect/platform-node";
35import { SqlClient } from "effect/unstable/sql";
3436
3537interface PendingMessages {
3638 queue: unknown[];
......@@ -237,6 +239,7 @@ describe("WebSocket Server", () => {
237239
238240 function createTestServer(
239241 options: {
242 persistenceLayer?: Layer.Layer<SqlClient.SqlClient, any>;
240243 cwd?: string;
241244 devUrl?: string;
242245 authToken?: string;
......@@ -253,6 +256,7 @@ describe("WebSocket Server", () => {
253256 port: 0,
254257 cwd: options.cwd ?? "/test/project",
255258 stateDir,
259 persistenceLayer: options.persistenceLayer ?? SqlitePersistenceMemory,
256260 ...(options.devUrl ? { devUrl: options.devUrl } : {}),
257261 ...(options.authToken ? { authToken: options.authToken } : {}),
258262 ...(options.gitManager ? { gitManager: options.gitManager as never } : {}),
......@@ -1033,11 +1037,11 @@ describe("WebSocket Server", () => {
10331037 const stateDir = makeTempDir("t3code-ws-prune-state-");
10341038 const existing = makeTempDir("t3code-ws-existing-project-");
10351039 const missing = makeTempDir("t3code-ws-missing-project-");
1040 const dbPath = path.join(stateDir, "test-prune.sqlite");
1041 const persistenceLayer = Layer.provide(makeSqlitePersistenceLive(dbPath), NodeServices.layer);
10361042
10371043 const projectRuntime = ManagedRuntime.make(
1038 ProjectRepositoryLive.pipe(
1039 Layer.provide(makeSqlitePersistenceLive(path.join(stateDir, "orchestration.sqlite"))),
1040 ),
1044 ProjectRepositoryLive.pipe(Layer.provide(persistenceLayer)),
10411045 );
10421046 const projectRepository = await projectRuntime.runPromise(Effect.service(ProjectRepository));
10431047 await projectRuntime.runPromise(projectRepository.add({ cwd: existing }));
......@@ -1045,7 +1049,11 @@ describe("WebSocket Server", () => {
10451049 await projectRuntime.dispose();
10461050 fs.rmSync(missing, { recursive: true, force: true });
10471051
1048 server = createTestServer({ stateDir, cwd: "/test" });
1052 server = createTestServer({
1053 stateDir,
1054 cwd: "/test",
1055 persistenceLayer,
1056 });
10491057 await server.start();
10501058 const addr = server.httpServer.address();
10511059 const port = typeof addr === "object" && addr !== null ? addr.port : 0;
apps/server/src/wsServer.ts+181-124
......@@ -18,11 +18,11 @@ import {
1818 type WsResponse,
1919 wsRequestSchema,
2020} from "@t3tools/contracts";
21import { NodeServices } from "@effect/platform-node";
2122import { Effect, Layer, ManagedRuntime } from "effect";
2223import { WebSocketServer, type WebSocket } from "ws";
2324
2425import { createLogger } from "./logger";
25import { ProviderManager } from "./providerManager";
2626import { GitManager } from "./gitManager";
2727import {
2828 checkoutGitBranch,
......@@ -43,6 +43,17 @@ import { makeSqlitePersistenceLive } from "./persistence/Layers/Sqlite";
4343import { ProjectRepository, type ProjectRepositoryShape } from "./persistence/Services/Projects";
4444import assert from "node:assert";
4545import { OrchestrationEventRepositoryLive } from "./persistence/Layers/OrchestrationEvents";
46import { CodexAdapterLive } from "./provider/Layers/CodexAdapter";
47import { ProviderAdapterRegistryLive } from "./provider/Layers/ProviderAdapterRegistry";
48import { ProviderServiceLive } from "./provider/Layers/ProviderService";
49import { ProviderSessionDirectoryLive } from "./provider/Layers/ProviderSessionDirectory";
50import { ProviderService, type ProviderServiceShape } from "./provider/Services/ProviderService";
51import { CheckpointStoreLive } from "./checkpointing/Layers/CheckpointStore";
52import { CheckpointServiceLive } from "./checkpointing/Layers/CheckpointService";
53import { CheckpointRepositoryLive } from "./persistence/Layers/Checkpoints";
54import * as SqlClient from "effect/unstable/sql/SqlClient";
55import * as SqlError from "effect/unstable/sql/SqlError";
56import * as Migrator from "effect/unstable/sql/Migrator";
4657
4758const MIME_TYPES: Record<string, string> = {
4859 ".html": "text/html; charset=utf-8",
......@@ -64,6 +75,9 @@ export interface ServerOptions {
6475 host?: string | undefined;
6576 cwd: string;
6677 stateDir?: string | undefined;
78 persistenceLayer?:
79 | Layer.Layer<SqlClient.SqlClient, SqlError.SqlError | Migrator.MigrationError>
80 | undefined;
6781 staticDir?: string | undefined;
6882 devUrl?: string | undefined;
6983 logWebSocketEvents?: boolean | undefined;
......@@ -121,6 +135,7 @@ export function createServer(options: ServerOptions) {
121135 host,
122136 cwd,
123137 stateDir,
138 persistenceLayer: customPersistenceLayer,
124139 staticDir,
125140 devUrl,
126141 logWebSocketEvents: explicitLogWsEvents,
......@@ -128,15 +143,15 @@ export function createServer(options: ServerOptions) {
128143 terminalManager = new TerminalManager(),
129144 authToken,
130145 } = options;
131 const providerManager = new ProviderManager();
132146
133147 const resolvedStateDir = stateDir ?? path.join(os.homedir(), ".t3", "userdata");
134148 let effectRuntime: ManagedRuntime.ManagedRuntime<
135 ProjectRepository | OrchestrationEngineService,
149 ProjectRepository | OrchestrationEngineService | ProviderService,
136150 unknown
137151 > | null = null;
138152 let projectRepository: ProjectRepositoryShape | null = null;
139153 let orchestrationEngine: OrchestrationEngineShape | null = null;
154 let providerService: ProviderServiceShape | null = null;
140155 const clients = new Set<WebSocket>();
141156 const logger = createLogger("ws");
142157 const logWebSocketEvents =
......@@ -144,6 +159,7 @@ export function createServer(options: ServerOptions) {
144159 let keybindingsConfig = loadResolvedKeybindingsConfig(logger);
145160 let unsubscribeReadModel = noop;
146161 let unsubscribeDomainEvents = noop;
162 let unsubscribeProviderEvents = noop;
147163
148164 function logOutgoingPush(push: WsPush, recipients: number) {
149165 if (!logWebSocketEvents) return;
......@@ -169,8 +185,16 @@ export function createServer(options: ServerOptions) {
169185 return effectRuntime;
170186 };
171187
188 const getProviderService = () => {
189 assert(providerService, "Provider service is not started");
190 return providerService;
191 };
192
172193 async function attachOrchestrationSubscriptions(
173 runtime: ManagedRuntime.ManagedRuntime<ProjectRepository | OrchestrationEngineService, unknown>,
194 runtime: ManagedRuntime.ManagedRuntime<
195 ProjectRepository | OrchestrationEngineService | ProviderService,
196 unknown
197 >,
174198 orchestrationEngine: OrchestrationEngineShape,
175199 ) {
176200 unsubscribeReadModel = await runtime.runPromise(
......@@ -215,114 +239,121 @@ export function createServer(options: ServerOptions) {
215239 );
216240 }
217241
218 // Forward provider events to all connected WebSocket clients
219 providerManager.on("event", (event) => {
220 void (async () => {
221 const liveOrchestrationEngine = orchestrationEngine;
222 const runtime = effectRuntime;
223 if (!liveOrchestrationEngine || !runtime) {
224 return;
225 }
226 const snapshot = await runtime.runPromise(liveOrchestrationEngine.getSnapshot());
227 const thread = snapshot.threads.find((entry) => entry.session?.sessionId === event.sessionId);
228 if (!thread) return;
229 const now = event.createdAt;
230 if (event.method === "turn/started" || event.method === "turn/completed") {
231 await runtime.runPromise(
232 liveOrchestrationEngine.dispatch({
233 type: "thread.session",
234 commandId: crypto.randomUUID(),
235 threadId: thread.id,
236 session: {
237 sessionId: event.sessionId,
238 provider: event.provider,
239 status: event.method === "turn/started" ? "running" : "ready",
240 threadId: thread.id,
241 activeTurnId: event.method === "turn/started" ? (event.turnId ?? null) : null,
242 createdAt: thread.session?.createdAt ?? now,
243 updatedAt: now,
244 lastError: null,
245 },
246 createdAt: now,
247 }),
248 );
249 }
250 if (event.textDelta && event.textDelta.length > 0) {
251 const assistantMessageId = `assistant:${event.turnId ?? event.itemId ?? event.sessionId}`;
252 await runtime.runPromise(
253 liveOrchestrationEngine.dispatch({
254 type: "message.send",
255 commandId: crypto.randomUUID(),
256 threadId: thread.id,
257 messageId: assistantMessageId,
258 role: "assistant",
259 text: event.textDelta,
260 streaming: true,
261 createdAt: now,
262 }),
263 );
264 }
265 if (event.method === "turn/completed") {
266 const assistantMessageId = `assistant:${event.turnId ?? event.itemId ?? event.sessionId}`;
267 await runtime.runPromise(
268 liveOrchestrationEngine.dispatch({
269 type: "message.send",
270 commandId: crypto.randomUUID(),
271 threadId: thread.id,
272 messageId: assistantMessageId,
273 role: "assistant",
274 text: "",
275 streaming: false,
276 createdAt: now,
277 }),
278 );
279 }
280 if (event.method === "checkpoint/captured") {
281 const payload = asObject(event.payload);
282 const turnId = event.turnId ?? asString(payload?.turnId);
283 if (turnId) {
284 await runtime.runPromise(
285 liveOrchestrationEngine.dispatch({
286 type: "thread.turnDiff.complete",
287 commandId: crypto.randomUUID(),
288 threadId: thread.id,
289 turnId,
290 completedAt: now,
291 ...(asString(payload?.status) !== null
292 ? { status: asString(payload?.status) ?? undefined }
293 : {}),
294 files: [],
295 assistantMessageId: `assistant:${turnId}`,
296 ...(asNumber(payload?.turnCount) !== null
297 ? { checkpointTurnCount: asNumber(payload?.turnCount) ?? undefined }
298 : {}),
299 createdAt: now,
300 }),
242 async function attachProviderSubscriptions(
243 runtime: ManagedRuntime.ManagedRuntime<
244 ProjectRepository | OrchestrationEngineService | ProviderService,
245 unknown
246 >,
247 providerService: ProviderServiceShape,
248 orchestrationEngine: OrchestrationEngineShape,
249 ) {
250 unsubscribeProviderEvents = await runtime.runPromise(
251 providerService.subscribeToEvents((event) => {
252 void (async () => {
253 const snapshot = await runtime.runPromise(orchestrationEngine.getSnapshot());
254 const thread = snapshot.threads.find(
255 (entry) => entry.session?.sessionId === event.sessionId,
301256 );
302 }
303 }
304 if (event.kind === "error" && event.message) {
305 await runtime.runPromise(
306 liveOrchestrationEngine.dispatch({
307 type: "thread.session",
308 commandId: crypto.randomUUID(),
309 threadId: thread.id,
310 session: {
311 sessionId: event.sessionId,
312 provider: event.provider,
313 status: "error",
314 threadId: thread.id,
315 activeTurnId: event.turnId ?? null,
316 createdAt: thread.session?.createdAt ?? now,
317 updatedAt: now,
318 lastError: event.message,
319 },
320 createdAt: now,
321 }),
322 );
323 }
324 })().catch(() => undefined);
325 });
257 if (!thread) return;
258 const now = event.createdAt;
259 if (event.method === "turn/started" || event.method === "turn/completed") {
260 await runtime.runPromise(
261 orchestrationEngine.dispatch({
262 type: "thread.session",
263 commandId: crypto.randomUUID(),
264 threadId: thread.id,
265 session: {
266 sessionId: event.sessionId,
267 provider: event.provider,
268 status: event.method === "turn/started" ? "running" : "ready",
269 threadId: thread.id,
270 activeTurnId: event.method === "turn/started" ? (event.turnId ?? null) : null,
271 createdAt: thread.session?.createdAt ?? now,
272 updatedAt: now,
273 lastError: null,
274 },
275 createdAt: now,
276 }),
277 );
278 }
279 if (event.textDelta && event.textDelta.length > 0) {
280 const assistantMessageId = `assistant:${event.turnId ?? event.itemId ?? event.sessionId}`;
281 await runtime.runPromise(
282 orchestrationEngine.dispatch({
283 type: "message.send",
284 commandId: crypto.randomUUID(),
285 threadId: thread.id,
286 messageId: assistantMessageId,
287 role: "assistant",
288 text: event.textDelta,
289 streaming: true,
290 createdAt: now,
291 }),
292 );
293 }
294 if (event.method === "turn/completed") {
295 const assistantMessageId = `assistant:${event.turnId ?? event.itemId ?? event.sessionId}`;
296 await runtime.runPromise(
297 orchestrationEngine.dispatch({
298 type: "message.send",
299 commandId: crypto.randomUUID(),
300 threadId: thread.id,
301 messageId: assistantMessageId,
302 role: "assistant",
303 text: "",
304 streaming: false,
305 createdAt: now,
306 }),
307 );
308 }
309 if (event.method === "checkpoint/captured") {
310 const payload = asObject(event.payload);
311 const turnId = event.turnId ?? asString(payload?.turnId);
312 if (turnId) {
313 await runtime.runPromise(
314 orchestrationEngine.dispatch({
315 type: "thread.turnDiff.complete",
316 commandId: crypto.randomUUID(),
317 threadId: thread.id,
318 turnId,
319 completedAt: now,
320 ...(asString(payload?.status) !== null
321 ? { status: asString(payload?.status) ?? undefined }
322 : {}),
323 files: [],
324 assistantMessageId: `assistant:${turnId}`,
325 ...(asNumber(payload?.turnCount) !== null
326 ? { checkpointTurnCount: asNumber(payload?.turnCount) ?? undefined }
327 : {}),
328 createdAt: now,
329 }),
330 );
331 }
332 }
333 if (event.kind === "error" && event.message) {
334 await runtime.runPromise(
335 orchestrationEngine.dispatch({
336 type: "thread.session",
337 commandId: crypto.randomUUID(),
338 threadId: thread.id,
339 session: {
340 sessionId: event.sessionId,
341 provider: event.provider,
342 status: "error",
343 threadId: thread.id,
344 activeTurnId: event.turnId ?? null,
345 createdAt: thread.session?.createdAt ?? now,
346 updatedAt: now,
347 lastError: event.message,
348 },
349 createdAt: now,
350 }),
351 );
352 }
353 })().catch(() => undefined);
354 }),
355 );
356 }
326357
327358 const onTerminalEvent = (event: TerminalEvent) => {
328359 const push: WsPush = {
......@@ -504,33 +535,36 @@ export function createServer(options: ServerOptions) {
504535 const orchestrationEngine = getOrchestrationEngine();
505536 const projectRepository = getProjectRepository();
506537 const effectRuntime = getEffectRuntime();
538 const providerService = getProviderService();
507539
508540 switch (request.method) {
509541 case WS_METHODS.providersStartSession:
510 return providerManager.startSession(request.params as never);
542 return effectRuntime.runPromise(providerService.startSession(request.params as never));
511543
512544 case WS_METHODS.providersSendTurn:
513 return providerManager.sendTurn(request.params as never);
545 return effectRuntime.runPromise(providerService.sendTurn(request.params as never));
514546
515547 case WS_METHODS.providersInterruptTurn:
516 return providerManager.interruptTurn(request.params as never);
548 return effectRuntime.runPromise(providerService.interruptTurn(request.params as never));
517549
518550 case WS_METHODS.providersRespondToRequest:
519 return providerManager.respondToRequest(request.params as never);
551 return effectRuntime.runPromise(providerService.respondToRequest(request.params as never));
520552
521553 case WS_METHODS.providersStopSession: {
522 providerManager.stopSession(request.params as never);
554 await effectRuntime.runPromise(providerService.stopSession(request.params as never));
523555 return undefined;
524556 }
525557
526558 case WS_METHODS.providersListCheckpoints:
527 return providerManager.listCheckpoints(request.params as never);
559 return effectRuntime.runPromise(providerService.listCheckpoints(request.params as never));
528560
529561 case WS_METHODS.providersGetCheckpointDiff:
530 return providerManager.getCheckpointDiff(request.params as never);
562 return effectRuntime.runPromise(providerService.getCheckpointDiff(request.params as never));
531563
532564 case WS_METHODS.providersRevertToCheckpoint: {
533 const result = await providerManager.revertToCheckpoint(request.params as never);
565 const result = await effectRuntime.runPromise(
566 providerService.revertToCheckpoint(request.params as never),
567 );
534568 const params = request.params as { sessionId?: string };
535569 const sessionId = params.sessionId;
536570 if (typeof sessionId === "string") {
......@@ -735,25 +769,42 @@ export function createServer(options: ServerOptions) {
735769
736770 async function createEffectRuntime() {
737771 const dbPath = path.join(resolvedStateDir, "orchestration.sqlite");
738 const persistenceLayer = makeSqlitePersistenceLive(dbPath);
772 const persistenceLayer = customPersistenceLayer ?? makeSqlitePersistenceLive(dbPath);
739773 const orchestrationLayer = Layer.provide(
740774 OrchestrationEngineLive,
741775 OrchestrationEventRepositoryLive,
742776 );
743 const layer = Layer.mergeAll(orchestrationLayer, ProjectRepositoryLive).pipe(
777 const adapterRegistryLayer = ProviderAdapterRegistryLive.pipe(Layer.provide(CodexAdapterLive));
778 const checkpointStoreLayer = CheckpointStoreLive.pipe(Layer.provide(NodeServices.layer));
779 const checkpointDependenciesLayer = Layer.mergeAll(
780 checkpointStoreLayer,
781 CheckpointRepositoryLive,
782 adapterRegistryLayer,
783 ProviderSessionDirectoryLive,
784 );
785 const checkpointServiceLayer = CheckpointServiceLive.pipe(
786 Layer.provideMerge(checkpointDependenciesLayer),
787 );
788 const providerLayer = ProviderServiceLive.pipe(Layer.provideMerge(checkpointServiceLayer));
789
790 const layer = Layer.mergeAll(orchestrationLayer, ProjectRepositoryLive, providerLayer).pipe(
744791 Layer.provide(persistenceLayer),
792 Layer.provideMerge(NodeServices.layer),
745793 );
746794 const runtime = ManagedRuntime.make(layer);
747795
748796 try {
749 const [nextOrchestrationEngine, repository] = await Promise.all([
797 const [nextOrchestrationEngine, repository, nextProviderService] = await Promise.all([
750798 runtime.runPromise(Effect.service(OrchestrationEngineService)),
751799 runtime.runPromise(Effect.service(ProjectRepository)),
800 runtime.runPromise(Effect.service(ProviderService)),
752801 ]);
753802 orchestrationEngine = nextOrchestrationEngine;
754803 projectRepository = repository;
804 providerService = nextProviderService;
755805 await runtime.runPromise(repository.pruneMissing());
756806 await attachOrchestrationSubscriptions(runtime, nextOrchestrationEngine);
807 await attachProviderSubscriptions(runtime, nextProviderService, nextOrchestrationEngine);
757808 } catch (error) {
758809 await runtime.dispose().catch(() => undefined);
759810 throw error;
......@@ -765,18 +816,26 @@ export function createServer(options: ServerOptions) {
765816 async function disposeEffectRuntime() {
766817 unsubscribeReadModel();
767818 unsubscribeDomainEvents();
819 unsubscribeProviderEvents();
768820 unsubscribeReadModel = noop;
769821 unsubscribeDomainEvents = noop;
822 unsubscribeProviderEvents = noop;
770823
771824 const runtime = effectRuntime;
825 const liveProviderService = providerService;
772826 effectRuntime = null;
773827 orchestrationEngine = null;
774828 projectRepository = null;
829 providerService = null;
775830
776831 if (!runtime) {
777832 return;
778833 }
779834
835 if (liveProviderService) {
836 await runtime.runPromise(liveProviderService.stopAll()).catch(() => undefined);
837 }
838
780839 await runtime.dispose();
781840 }
782841
......@@ -804,8 +863,6 @@ export function createServer(options: ServerOptions) {
804863 async function stop(): Promise<void> {
805864 await disposeEffectRuntime();
806865 terminalManager.off("event", onTerminalEvent);
807 providerManager.stopAll();
808 providerManager.dispose();
809866 terminalManager.dispose();
810867
811868 for (const client of clients) {
bun.lock-61
......@@ -35,7 +35,6 @@
3535 },
3636 "dependencies": {
3737 "@effect/platform-node": "4.0.0-beta.10",
38 "@effect/sql-sqlite-node": "4.0.0-beta.10",
3938 "effect": "4.0.0-beta.10",
4039 "node-pty": "^1.1.0",
4140 "open": "^10.1.0",
......@@ -154,8 +153,6 @@
154153
155154 "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.10", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.19.0" }, "peerDependencies": { "effect": "^4.0.0-beta.10" } }, "sha512-gBSeOG2bfchXFdgdi5oipHKynOzSd7BlzTy5QcjFOoCUTtLP9igqohPB9hTkV3a6seWE2JJqsgsQ79VuLb7fAg=="],
156155
157 "@effect/sql-sqlite-node": ["@effect/sql-sqlite-node@4.0.0-beta.10", "", { "dependencies": { "better-sqlite3": "^12.6.2" }, "peerDependencies": { "effect": "^4.0.0-beta.10" } }, "sha512-oQl+gF31HaVM9R8YEpf96CIJypP1Roa5z1VPj8s7ZP+3aCtRVor1K+eMxYNAb8ph7R68Tz0LpmVkefq0mEI3CA=="],
158
159156 "@effect/vitest": ["@effect/vitest@4.0.0-beta.10", "", { "peerDependencies": { "effect": "^4.0.0-beta.10", "vitest": "^3.0.0" } }, "sha512-WmE8vCNw+eDoAq9CwwymrAdIBw1f+8hDP3AUF7jyC7/Cj/kyAYOFwkbtKmDXIFaTlf7G/2fmulEXeCO5W8j0kQ=="],
160157
161158 "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="],
......@@ -610,28 +607,18 @@
610607
611608 "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
612609
613 "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
614
615610 "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],
616611
617 "better-sqlite3": ["better-sqlite3@12.6.2", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA=="],
618
619612 "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
620613
621 "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
622
623614 "birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="],
624615
625 "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
626
627616 "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
628617
629618 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
630619
631620 "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
632621
633 "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
634
635622 "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
636623
637624 "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
......@@ -662,8 +649,6 @@
662649
663650 "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
664651
665 "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
666
667652 "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
668653
669654 "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
......@@ -698,8 +683,6 @@
698683
699684 "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
700685
701 "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
702
703686 "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
704687
705688 "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
......@@ -774,8 +757,6 @@
774757
775758 "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
776759
777 "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
778
779760 "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
780761
781762 "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
......@@ -788,8 +769,6 @@
788769
789770 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
790771
791 "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
792
793772 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
794773
795774 "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
......@@ -798,8 +777,6 @@
798777
799778 "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
800779
801 "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
802
803780 "fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
804781
805782 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
......@@ -818,8 +795,6 @@
818795
819796 "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
820797
821 "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
822
823798 "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
824799
825800 "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="],
......@@ -868,14 +843,10 @@
868843
869844 "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],
870845
871 "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
872
873846 "import-from": ["import-from@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ=="],
874847
875848 "import-without-cache": ["import-without-cache@0.2.5", "", {}, "sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A=="],
876849
877 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
878
879850 "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
880851
881852 "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
......@@ -1082,8 +1053,6 @@
10821053
10831054 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
10841055
1085 "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
1086
10871056 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
10881057
10891058 "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="],
......@@ -1098,10 +1067,6 @@
10981067
10991068 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
11001069
1101 "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
1102
1103 "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
1104
11051070 "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
11061071
11071072 "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
......@@ -1150,8 +1115,6 @@
11501115
11511116 "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
11521117
1153 "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
1154
11551118 "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
11561119
11571120 "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
......@@ -1168,8 +1131,6 @@
11681131
11691132 "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
11701133
1171 "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
1172
11731134 "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
11741135
11751136 "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
......@@ -1178,8 +1139,6 @@
11781139
11791140 "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
11801141
1181 "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
1182
11831142 "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
11841143
11851144 "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
......@@ -1234,8 +1193,6 @@
12341193
12351194 "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],
12361195
1237 "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
1238
12391196 "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
12401197
12411198 "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
......@@ -1254,10 +1211,6 @@
12541211
12551212 "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
12561213
1257 "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
1258
1259 "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
1260
12611214 "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
12621215
12631216 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
......@@ -1278,14 +1231,10 @@
12781231
12791232 "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
12801233
1281 "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
1282
12831234 "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
12841235
12851236 "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
12861237
1287 "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
1288
12891238 "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
12901239
12911240 "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
......@@ -1306,10 +1255,6 @@
13061255
13071256 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
13081257
1309 "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
1310
1311 "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
1312
13131258 "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
13141259
13151260 "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
......@@ -1346,8 +1291,6 @@
13461291
13471292 "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
13481293
1349 "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
1350
13511294 "turbo": ["turbo@2.8.7", "", { "optionalDependencies": { "turbo-darwin-64": "2.8.7", "turbo-darwin-arm64": "2.8.7", "turbo-linux-64": "2.8.7", "turbo-linux-arm64": "2.8.7", "turbo-windows-64": "2.8.7", "turbo-windows-arm64": "2.8.7" }, "bin": { "turbo": "bin/turbo" } }, "sha512-RBLh5caMAu1kFdTK1jgH2gH/z+jFsvX5rGbhgJ9nlIAWXSvxlzwId05uDlBA1+pBd3wO/UaKYzaQZQBXDd7kcA=="],
13521295
13531296 "turbo-darwin-64": ["turbo-darwin-64@2.8.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xr4TO/oDDwoozbDtBvunb66g//WK8uHRygl72vUthuwzmiw48pil4IuoG/QbMHd9RE8aBnVmzC0WZEWk/WWt3A=="],
......@@ -1400,8 +1343,6 @@
14001343
14011344 "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
14021345
1403 "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
1404
14051346 "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
14061347
14071348 "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
......@@ -1514,8 +1455,6 @@
15141455
15151456 "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
15161457
1517 "rc/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
1518
15191458 "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
15201459
15211460 "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],