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 @@...@@ -18,7 +18,6 @@
18 },18 },
19 "dependencies": {19 "dependencies": {
20 "@effect/platform-node": "4.0.0-beta.10",20 "@effect/platform-node": "4.0.0-beta.10",
21 "@effect/sql-sqlite-node": "4.0.0-beta.10",
22 "effect": "4.0.0-beta.10",21 "effect": "4.0.0-beta.10",
23 "node-pty": "^1.1.0",22 "node-pty": "^1.1.0",
24 "open": "^10.1.0",23 "open": "^10.1.0",
apps/server/src/checkpointing/Layers/CheckpointService.test.ts+147-112
...@@ -1,7 +1,3 @@...@@ -1,7 +1,3 @@
1import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2import os from "node:os";
3import path from "node:path";
4
5import type {1import type {
6 ProviderApprovalDecision,2 ProviderApprovalDecision,
7 ProviderEvent,3 ProviderEvent,
...@@ -13,7 +9,7 @@ import type {...@@ -13,7 +9,7 @@ import type {
13} from "@t3tools/contracts";9} from "@t3tools/contracts";
14import { NodeServices } from "@effect/platform-node";10import { NodeServices } from "@effect/platform-node";
15import { it, assert } from "@effect/vitest";11import { it, assert } from "@effect/vitest";
16import { Effect, Layer } from "effect";12import { Effect, FileSystem, Layer, Path } from "effect";
1713
18import { runGit as runGitProcess } from "../../git/Process.ts";14import { runGit as runGitProcess } from "../../git/Process.ts";
19import { CheckpointServiceLive } from "./CheckpointService.ts";15import { CheckpointServiceLive } from "./CheckpointService.ts";
...@@ -34,14 +30,9 @@ import type {...@@ -34,14 +30,9 @@ import type {
34} from "../../provider/Services/ProviderAdapter.ts";30} from "../../provider/Services/ProviderAdapter.ts";
35import { ProviderSessionDirectory } from "../../provider/Services/ProviderSessionDirectory.ts";31import { ProviderSessionDirectory } from "../../provider/Services/ProviderSessionDirectory.ts";
36import { ProviderSessionDirectoryLive } from "../../provider/Layers/ProviderSessionDirectory.ts";32import { ProviderSessionDirectoryLive } from "../../provider/Layers/ProviderSessionDirectory.ts";
37import { makeSqliteCheckpointRepositoryLive } from "../../persistence/Layers/Checkpoints.ts";33import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";
38import { CheckpointRepository } from "../../persistence/Services/Checkpoints.ts";34import { CheckpointRepository } from "../../persistence/Services/Checkpoints.ts";
3935import { CheckpointRepositoryLive } from "../../persistence/Layers/Checkpoints.ts";
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 );
4536
46const runGit = (cwd: string, args: readonly string[]) =>37const runGit = (cwd: string, args: readonly string[]) =>
47 runGitProcess({38 runGitProcess({
...@@ -53,23 +44,24 @@ const runGit = (cwd: string, args: readonly string[]) =>...@@ -53,23 +44,24 @@ const runGit = (cwd: string, args: readonly string[]) =>
53 Effect.provide(NodeServices.layer),44 Effect.provide(NodeServices.layer),
54 );45 );
5546
56function createRepo() {47const makeGitRepository = Effect.gen(function* () {
57 return Effect.gen(function* () {48 const fs = yield* FileSystem.FileSystem;
58 const cwd = yield* makeTemporaryDirectoryScoped("t3-checkpoint-service-repo-");49 const path = yield* Path.Path;
59 yield* runGit(cwd, ["init", "--initial-branch=main"]);50 const cwd = yield* fs.makeTempDirectory();
60 yield* runGit(cwd, ["config", "user.email", "test@example.com"]);51 yield* runGit(cwd, ["init", "--initial-branch=main"]);
61 yield* runGit(cwd, ["config", "user.name", "Test User"]);52 yield* runGit(cwd, ["config", "user.email", "test@example.com"]);
62 yield* Effect.promise(() => writeFile(path.join(cwd, "README.md"), "v1\n"));53 yield* runGit(cwd, ["config", "user.name", "Test User"]);
63 yield* runGit(cwd, ["add", "."]);54 yield* fs.writeFileString(path.join(cwd, "README.md"), "v1\n");
64 yield* runGit(cwd, ["commit", "-m", "Initial"]);55 yield* runGit(cwd, ["add", "."]);
65 return cwd;56 yield* runGit(cwd, ["commit", "-m", "Initial"]);
66 });57 return cwd;
67}58});
6859
69class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterError> {60class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterError> {
70 readonly provider = "codex" as const;61 readonly provider = "codex" as const;
7162
72 private active = true;63 private active = true;
64 private readThreadFailure: ProviderAdapterError | undefined;
73 private snapshot: ProviderThreadSnapshot;65 private snapshot: ProviderThreadSnapshot;
74 private readonly session: ProviderSession;66 private readonly session: ProviderSession;
7567
...@@ -105,6 +97,10 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr...@@ -105,6 +97,10 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr
105 return this.snapshot.turns;97 return this.snapshot.turns;
106 }98 }
10799
100 setReadThreadFailure(error: ProviderAdapterError | undefined): void {
101 this.readThreadFailure = error;
102 }
103
108 readonly startSession = (104 readonly startSession = (
109 _input: ProviderSessionStartInput,105 _input: ProviderSessionStartInput,
110 ): Effect.Effect<ProviderSession, ProviderAdapterError> =>106 ): Effect.Effect<ProviderSession, ProviderAdapterError> =>
...@@ -157,7 +153,9 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr...@@ -157,7 +153,9 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr
157 this.hasSession(sessionId).pipe(153 this.hasSession(sessionId).pipe(
158 Effect.flatMap((hasSession) =>154 Effect.flatMap((hasSession) =>
159 hasSession155 hasSession
160 ? Effect.succeed(this.snapshot)156 ? this.readThreadFailure
157 ? Effect.fail(this.readThreadFailure)
158 : Effect.succeed(this.snapshot)
161 : Effect.fail(159 : Effect.fail(
162 new ProviderAdapterSessionNotFoundError({160 new ProviderAdapterSessionNotFoundError({
163 provider: this.provider,161 provider: this.provider,
...@@ -203,9 +201,7 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr...@@ -203,9 +201,7 @@ class InMemoryProviderAdapter implements ProviderAdapterShape<ProviderAdapterErr
203201
204function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) {202function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) {
205 return Effect.gen(function* () {203 return Effect.gen(function* () {
206 const cwd = yield* createRepo();204 const cwd = yield* makeGitRepository;
207 const stateDir = yield* makeTemporaryDirectoryScoped("t3-checkpoint-service-state-");
208 const dbPath = path.join(stateDir, "orchestration.sqlite");
209205
210 const sessionId = "sess-1";206 const sessionId = "sess-1";
211 const adapter = new InMemoryProviderAdapter(sessionId, cwd, turns);207 const adapter = new InMemoryProviderAdapter(sessionId, cwd, turns);
...@@ -220,9 +216,10 @@ function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) {...@@ -220,9 +216,10 @@ function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) {
220216
221 const dependencies = Layer.mergeAll(217 const dependencies = Layer.mergeAll(
222 Layer.provide(CheckpointStoreLive, NodeServices.layer),218 Layer.provide(CheckpointStoreLive, NodeServices.layer),
223 makeSqliteCheckpointRepositoryLive(dbPath),219 Layer.provide(CheckpointRepositoryLive, SqlitePersistenceMemory),
224 Layer.succeed(ProviderAdapterRegistry, registry),220 Layer.succeed(ProviderAdapterRegistry, registry),
225 ProviderSessionDirectoryLive,221 ProviderSessionDirectoryLive,
222 NodeServices.layer,
226 );223 );
227224
228 const layer = CheckpointServiceLive.pipe(Layer.provideMerge(dependencies));225 const layer = CheckpointServiceLive.pipe(Layer.provideMerge(dependencies));
...@@ -236,87 +233,84 @@ function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) {...@@ -236,87 +233,84 @@ function makeFixture(turns: ReadonlyArray<ProviderThreadTurnSnapshot>) {
236 });233 });
237}234}
238235
239it.effect(236it.effect("captures checkpoints, diffs refs, and reverts workspace + turns", () =>
240 "CheckpointServiceLive captures checkpoints, diffs refs, and reverts workspace + turns",237 Effect.gen(function* () {
241 () =>238 const firstTurn: ProviderThreadTurnSnapshot = {
242 Effect.gen(function* () {239 id: "turn-1",
243 const firstTurn: ProviderThreadTurnSnapshot = {240 items: [{ type: "userMessage", content: [{ type: "text", text: "first" }] }],
244 id: "turn-1",241 };
245 items: [{ type: "userMessage", content: [{ type: "text", text: "first" }] }],242 const secondTurn: ProviderThreadTurnSnapshot = {
246 };243 id: "turn-2",
247 const secondTurn: ProviderThreadTurnSnapshot = {244 items: [{ type: "agentMessage", text: "second" }],
248 id: "turn-2",245 };
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 });
307246
308 const afterRevert = yield* Effect.promise(() =>247 const fixture = yield* makeFixture([]);
309 readFile(path.join(fixture.cwd, "README.md"), "utf8"),248
310 );249 yield* Effect.gen(function* () {
311 assert.equal(afterRevert, "v2\n");250 const fs = yield* FileSystem.FileSystem;
312 assert.equal(reverted.turnCount, 1);251 const path = yield* Path.Path;
313 assert.equal(reverted.rolledBackTurns, 1);252
314 assert.equal(reverted.checkpoints.length, 2);253 const service = yield* CheckpointService;
315 assert.equal(reverted.checkpoints[1]?.id, "turn-1");254 const directory = yield* ProviderSessionDirectory;
316 assert.equal(reverted.checkpoints[1]?.isCurrent, true);255
317 assert.equal(fixture.adapter.getTurns().length, 1);256 yield* directory.upsert({
318 }).pipe(Effect.provide(fixture.layer));257 sessionId: fixture.sessionId,
319 }),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)),
320);314);
321315
322it.effect(316it.effect(
...@@ -338,7 +332,7 @@ it.effect(...@@ -338,7 +332,7 @@ it.effect(
338 assert.equal(result.failure._tag, "CheckpointSessionNotFoundError");332 assert.equal(result.failure._tag, "CheckpointSessionNotFoundError");
339 }333 }
340 }).pipe(Effect.provide(fixture.layer));334 }).pipe(Effect.provide(fixture.layer));
341 }),335 }).pipe(Effect.provide(NodeServices.layer)),
342);336);
343337
344it.effect(338it.effect(
...@@ -353,6 +347,8 @@ it.effect(...@@ -353,6 +347,8 @@ it.effect(
353 const fixture = yield* makeFixture([]);347 const fixture = yield* makeFixture([]);
354348
355 yield* Effect.gen(function* () {349 yield* Effect.gen(function* () {
350 const fs = yield* FileSystem.FileSystem;
351 const path = yield* Path.Path;
356 const service = yield* CheckpointService;352 const service = yield* CheckpointService;
357 const store = yield* CheckpointStore;353 const store = yield* CheckpointStore;
358 const checkpointRepository = yield* CheckpointRepository;354 const checkpointRepository = yield* CheckpointRepository;
...@@ -369,7 +365,7 @@ it.effect(...@@ -369,7 +365,7 @@ it.effect(
369 cwd: fixture.cwd,365 cwd: fixture.cwd,
370 });366 });
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");
373 fixture.adapter.setTurns([firstTurn]);369 fixture.adapter.setTurns([firstTurn]);
374 yield* service.captureCurrentTurn({370 yield* service.captureCurrentTurn({
375 providerSessionId: fixture.sessionId,371 providerSessionId: fixture.sessionId,
...@@ -400,5 +396,44 @@ it.effect(...@@ -400,5 +396,44 @@ it.effect(
400 assert.equal(result.failure._tag, "CheckpointUnavailableError");396 assert.equal(result.failure._tag, "CheckpointUnavailableError");
401 }397 }
402 }).pipe(Effect.provide(fixture.layer));398 }).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)),
404);439);
apps/server/src/checkpointing/Layers/CheckpointService.ts+40-12
...@@ -441,6 +441,43 @@ const makeCheckpointService = Effect.gen(function* () {...@@ -441,6 +441,43 @@ const makeCheckpointService = Effect.gen(function* () {
441 ),441 ),
442 );442 );
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
444 const rollbackThreadSnapshot = (481 const rollbackThreadSnapshot = (
445 sessionId: string,482 sessionId: string,
446 adapter: ProviderAdapterShape<ProviderAdapterError>,483 adapter: ProviderAdapterShape<ProviderAdapterError>,
...@@ -633,19 +670,10 @@ const makeCheckpointService = Effect.gen(function* () {...@@ -633,19 +670,10 @@ const makeCheckpointService = Effect.gen(function* () {
633 Effect.gen(function* () {670 Effect.gen(function* () {
634 const adapter = yield* resolveAdapterForSession(input.providerSessionId);671 const adapter = yield* resolveAdapterForSession(input.providerSessionId);
635 yield* ensureSessionIsActive(input.providerSessionId, adapter);672 yield* ensureSessionIsActive(input.providerSessionId, adapter);
636 const snapshot = yield* readThreadSnapshot(673 const threadId = yield* resolveThreadIdForSession(
637 input.providerSessionId,674 input.providerSessionId,
638 adapter,675 "CheckpointService.initializeForSession",
639 "CheckpointService.initializeForSession:readThread",
640 );676 );
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
650 const supportsGit = yield* store.isGitRepository(input.cwd);678 const supportsGit = yield* store.isGitRepository(input.cwd);
651 if (!supportsGit) {679 if (!supportsGit) {
...@@ -686,7 +714,7 @@ const makeCheckpointService = Effect.gen(function* () {...@@ -686,7 +714,7 @@ const makeCheckpointService = Effect.gen(function* () {
686 yield* captureAndPersistCheckpoint({714 yield* captureAndPersistCheckpoint({
687 providerSessionId: input.providerSessionId,715 providerSessionId: input.providerSessionId,
688 cwd: input.cwd,716 cwd: input.cwd,
689 threadId: snapshot.threadId,717 threadId,
690 checkpoint: rootCheckpoint(),718 checkpoint: rootCheckpoint(),
691 });719 });
692 yield* setTrackedCheckpointCwd(input.providerSessionId, input.cwd);720 yield* setTrackedCheckpointCwd(input.providerSessionId, input.cwd);
apps/server/src/orchestration/Layer.test.ts+34-90
...@@ -1,40 +1,21 @@...@@ -1,40 +1,21 @@
1import fs from "node:fs";
2import os from "node:os";
3import path from "node:path";
4
5import type { OrchestrationEvent } from "@t3tools/contracts";1import type { OrchestrationEvent } from "@t3tools/contracts";
6import { Effect, Layer, ManagedRuntime } from "effect";2import { Effect, Layer, ManagedRuntime } from "effect";
7import { afterEach, describe, expect, it } from "vitest";3import { describe, expect, it } from "vitest";
84
9import {5import {
10 OrchestrationEventRepository,6 OrchestrationEventRepository,
11 type OrchestrationEventRepositoryShape,7 type OrchestrationEventRepositoryShape,
12} from "../persistence/Services/OrchestrationEvents.ts";8} from "../persistence/Services/OrchestrationEvents.ts";
13import { PersistenceSqlError } from "../persistence/Errors.ts";9import { PersistenceSqlError } from "../persistence/Errors.ts";
14import { makeSqlitePersistenceLive } from "../persistence/Layers/Sqlite.ts";10import { SqlitePersistenceMemory } from "../persistence/Layers/Sqlite.ts";
15import { OrchestrationEngineLive } from "./Layer.ts";11import { OrchestrationEngineLive } from "./Layer.ts";
16import { OrchestrationEngineService } from "./Service.ts";12import { OrchestrationEngineService } from "./Service.ts";
17import { OrchestrationEventRepositoryLive } from "../persistence/Layers/OrchestrationEvents.ts";13import { OrchestrationEventRepositoryLive } from "../persistence/Layers/OrchestrationEvents.ts";
1814
19const tempDirs: string[] = [];15export async function createOrchestrationSystem() {
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");
35 const orchestrationLayer = OrchestrationEngineLive.pipe(16 const orchestrationLayer = OrchestrationEngineLive.pipe(
36 Layer.provide(OrchestrationEventRepositoryLive),17 Layer.provide(OrchestrationEventRepositoryLive),
37 Layer.provide(makeSqlitePersistenceLive(dbPath)),18 Layer.provide(SqlitePersistenceMemory),
38 );19 );
39 const runtime = ManagedRuntime.make(orchestrationLayer);20 const runtime = ManagedRuntime.make(orchestrationLayer);
40 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));21 const engine = await runtime.runPromise(Effect.service(OrchestrationEngineService));
...@@ -46,16 +27,15 @@ export async function createOrchestrationSystem(stateDir: string) {...@@ -46,16 +27,15 @@ export async function createOrchestrationSystem(stateDir: string) {
46}27}
4728
48describe("OrchestrationEngine", () => {29describe("OrchestrationEngine", () => {
49 it("replays to the same deterministic snapshot", async () => {30 it("returns deterministic snapshots for repeated reads", async () => {
50 const stateDir = makeTempDir("t3code-orchestration-");
51 const createdAt = new Date().toISOString();31 const createdAt = new Date().toISOString();
52 const projectId = "project-1";32 const projectId = "project-1";
53 const threadId = "thread-1";33 const threadId = "thread-1";
5434
55 const firstSystem = await createOrchestrationSystem(stateDir);35 const system = await createOrchestrationSystem();
56 const engineA = firstSystem.engine;36 const engine = system.engine;
57 await firstSystem.run(37 await system.run(
58 engineA.dispatch({38 engine.dispatch({
59 type: "thread.create",39 type: "thread.create",
60 commandId: "cmd-1",40 commandId: "cmd-1",
61 threadId,41 threadId,
...@@ -67,8 +47,8 @@ describe("OrchestrationEngine", () => {...@@ -67,8 +47,8 @@ describe("OrchestrationEngine", () => {
67 createdAt,47 createdAt,
68 }),48 }),
69 );49 );
70 await firstSystem.run(50 await system.run(
71 engineA.dispatch({51 engine.dispatch({
72 type: "message.send",52 type: "message.send",
73 commandId: "cmd-3",53 commandId: "cmd-3",
74 threadId,54 threadId,
...@@ -79,19 +59,14 @@ describe("OrchestrationEngine", () => {...@@ -79,19 +59,14 @@ describe("OrchestrationEngine", () => {
79 createdAt,59 createdAt,
80 }),60 }),
81 );61 );
82 const snapshotA = await firstSystem.run(engineA.getSnapshot());62 const snapshotA = await system.run(engine.getSnapshot());
83 await firstSystem.dispose();63 const snapshotB = await system.run(engine.getSnapshot());
84
85 const secondSystem = await createOrchestrationSystem(stateDir);
86 const engineB = secondSystem.engine;
87 const snapshotB = await secondSystem.run(engineB.getSnapshot());
88 expect(snapshotB).toEqual(snapshotA);64 expect(snapshotB).toEqual(snapshotA);
89 await secondSystem.dispose();65 await system.dispose();
90 });66 });
9167
92 it("fans out read-model updates to subscribers", async () => {68 it("fans out read-model updates to subscribers", async () => {
93 const stateDir = makeTempDir("t3code-orchestration-fanout-");69 const system = await createOrchestrationSystem();
94 const system = await createOrchestrationSystem(stateDir);
95 const engine = system.engine;70 const engine = system.engine;
96 const updates: number[] = [];71 const updates: number[] = [];
97 const unsubscribe = await system.run(72 const unsubscribe = await system.run(
...@@ -118,8 +93,7 @@ describe("OrchestrationEngine", () => {...@@ -118,8 +93,7 @@ describe("OrchestrationEngine", () => {
118 });93 });
11994
120 it("replays append-only events from sequence", async () => {95 it("replays append-only events from sequence", async () => {
121 const stateDir = makeTempDir("t3code-orchestration-replay-");96 const system = await createOrchestrationSystem();
122 const system = await createOrchestrationSystem(stateDir);
123 const engine = system.engine;97 const engine = system.engine;
124 await system.run(98 await system.run(
125 engine.dispatch({99 engine.dispatch({
...@@ -150,11 +124,10 @@ describe("OrchestrationEngine", () => {...@@ -150,11 +124,10 @@ describe("OrchestrationEngine", () => {
150 });124 });
151125
152 it("stores completed turn summaries even when no files changed", async () => {126 it("stores completed turn summaries even when no files changed", async () => {
153 const stateDir = makeTempDir("t3code-orchestration-turn-diff-");127 const system = await createOrchestrationSystem();
154 const firstSystem = await createOrchestrationSystem(stateDir);128 const engine = system.engine;
155 const engine = firstSystem.engine;
156 const completedAt = new Date().toISOString();129 const completedAt = new Date().toISOString();
157 await firstSystem.run(130 await system.run(
158 engine.dispatch({131 engine.dispatch({
159 type: "thread.create",132 type: "thread.create",
160 commandId: "cmd-thread-turn-diff",133 commandId: "cmd-thread-turn-diff",
...@@ -167,7 +140,7 @@ describe("OrchestrationEngine", () => {...@@ -167,7 +140,7 @@ describe("OrchestrationEngine", () => {
167 createdAt: completedAt,140 createdAt: completedAt,
168 }),141 }),
169 );142 );
170 await firstSystem.run(143 await system.run(
171 engine.dispatch({144 engine.dispatch({
172 type: "thread.turnDiff.complete",145 type: "thread.turnDiff.complete",
173 commandId: "cmd-turn-diff-complete",146 commandId: "cmd-turn-diff-complete",
...@@ -181,7 +154,7 @@ describe("OrchestrationEngine", () => {...@@ -181,7 +154,7 @@ describe("OrchestrationEngine", () => {
181 }),154 }),
182 );155 );
183156
184 const firstThread = (await firstSystem.run(engine.getSnapshot())).threads.find(157 const firstThread = (await system.run(engine.getSnapshot())).threads.find(
185 (thread) => thread.id === "thread-turn-diff",158 (thread) => thread.id === "thread-turn-diff",
186 );159 );
187 expect(firstThread?.turnDiffSummaries).toEqual([160 expect(firstThread?.turnDiffSummaries).toEqual([
...@@ -193,31 +166,14 @@ describe("OrchestrationEngine", () => {...@@ -193,31 +166,14 @@ describe("OrchestrationEngine", () => {
193 checkpointTurnCount: 1,166 checkpointTurnCount: 1,
194 },167 },
195 ]);168 ]);
196 await firstSystem.dispose();169 await system.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();
213 });170 });
214171
215 it("reverts thread messages and turn summaries to a checkpoint", async () => {172 it("reverts thread messages and turn summaries to a checkpoint", async () => {
216 const stateDir = makeTempDir("t3code-orchestration-revert-");173 const system = await createOrchestrationSystem();
217 const firstSystem = await createOrchestrationSystem(stateDir);174 const engine = system.engine;
218 const engine = firstSystem.engine;
219 const createdAt = new Date().toISOString();175 const createdAt = new Date().toISOString();
220 await firstSystem.run(176 await system.run(
221 engine.dispatch({177 engine.dispatch({
222 type: "thread.create",178 type: "thread.create",
223 commandId: "cmd-thread-revert",179 commandId: "cmd-thread-revert",
...@@ -230,7 +186,7 @@ describe("OrchestrationEngine", () => {...@@ -230,7 +186,7 @@ describe("OrchestrationEngine", () => {
230 createdAt,186 createdAt,
231 }),187 }),
232 );188 );
233 await firstSystem.run(189 await system.run(
234 engine.dispatch({190 engine.dispatch({
235 type: "message.send",191 type: "message.send",
236 commandId: "cmd-msg-1-user",192 commandId: "cmd-msg-1-user",
...@@ -242,7 +198,7 @@ describe("OrchestrationEngine", () => {...@@ -242,7 +198,7 @@ describe("OrchestrationEngine", () => {
242 createdAt,198 createdAt,
243 }),199 }),
244 );200 );
245 await firstSystem.run(201 await system.run(
246 engine.dispatch({202 engine.dispatch({
247 type: "message.send",203 type: "message.send",
248 commandId: "cmd-msg-1-assistant",204 commandId: "cmd-msg-1-assistant",
...@@ -254,7 +210,7 @@ describe("OrchestrationEngine", () => {...@@ -254,7 +210,7 @@ describe("OrchestrationEngine", () => {
254 createdAt,210 createdAt,
255 }),211 }),
256 );212 );
257 await firstSystem.run(213 await system.run(
258 engine.dispatch({214 engine.dispatch({
259 type: "thread.turnDiff.complete",215 type: "thread.turnDiff.complete",
260 commandId: "cmd-turn-1-complete",216 commandId: "cmd-turn-1-complete",
...@@ -270,7 +226,7 @@ describe("OrchestrationEngine", () => {...@@ -270,7 +226,7 @@ describe("OrchestrationEngine", () => {
270 );226 );
271227
272 const createdAtSecond = new Date(Date.now() + 1_000).toISOString();228 const createdAtSecond = new Date(Date.now() + 1_000).toISOString();
273 await firstSystem.run(229 await system.run(
274 engine.dispatch({230 engine.dispatch({
275 type: "message.send",231 type: "message.send",
276 commandId: "cmd-msg-2-user",232 commandId: "cmd-msg-2-user",
...@@ -282,7 +238,7 @@ describe("OrchestrationEngine", () => {...@@ -282,7 +238,7 @@ describe("OrchestrationEngine", () => {
282 createdAt: createdAtSecond,238 createdAt: createdAtSecond,
283 }),239 }),
284 );240 );
285 await firstSystem.run(241 await system.run(
286 engine.dispatch({242 engine.dispatch({
287 type: "message.send",243 type: "message.send",
288 commandId: "cmd-msg-2-assistant",244 commandId: "cmd-msg-2-assistant",
...@@ -294,7 +250,7 @@ describe("OrchestrationEngine", () => {...@@ -294,7 +250,7 @@ describe("OrchestrationEngine", () => {
294 createdAt: createdAtSecond,250 createdAt: createdAtSecond,
295 }),251 }),
296 );252 );
297 await firstSystem.run(253 await system.run(
298 engine.dispatch({254 engine.dispatch({
299 type: "thread.turnDiff.complete",255 type: "thread.turnDiff.complete",
300 commandId: "cmd-turn-2-complete",256 commandId: "cmd-turn-2-complete",
...@@ -309,7 +265,7 @@ describe("OrchestrationEngine", () => {...@@ -309,7 +265,7 @@ describe("OrchestrationEngine", () => {
309 }),265 }),
310 );266 );
311267
312 await firstSystem.run(268 await system.run(
313 engine.dispatch({269 engine.dispatch({
314 type: "thread.revert",270 type: "thread.revert",
315 commandId: "cmd-thread-revert-apply",271 commandId: "cmd-thread-revert-apply",
...@@ -320,25 +276,13 @@ describe("OrchestrationEngine", () => {...@@ -320,25 +276,13 @@ describe("OrchestrationEngine", () => {
320 }),276 }),
321 );277 );
322278
323 const thread = (await firstSystem.run(engine.getSnapshot())).threads.find(279 const thread = (await system.run(engine.getSnapshot())).threads.find(
324 (entry) => entry.id === "thread-revert",280 (entry) => entry.id === "thread-revert",
325 );281 );
326 expect(thread?.messages.map((message) => message.id)).toEqual(["user-1", "assistant:turn-1"]);282 expect(thread?.messages.map((message) => message.id)).toEqual(["user-1", "assistant:turn-1"]);
327 expect(thread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]);283 expect(thread?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual(["turn-1"]);
328 expect(thread?.latestTurnId).toBe("turn-1");284 expect(thread?.latestTurnId).toBe("turn-1");
329 await firstSystem.dispose();285 await system.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();
342 });286 });
343287
344 it("allows stop to be called multiple times", async () => {288 it("allows stop to be called multiple times", async () => {
apps/server/src/persistence/Layers/Checkpoints.test.ts+4-17
...@@ -1,29 +1,16 @@...@@ -1,29 +1,16 @@
1import { mkdtemp, rm } from "node:fs/promises";
2import os from "node:os";
3import path from "node:path";
4
5import { it, assert } from "@effect/vitest";1import { it, assert } from "@effect/vitest";
6import { Effect, Layer, Option } from "effect";2import { Effect, Layer, Option } from "effect";
73
8import { CheckpointRepository } from "../Services/Checkpoints.ts";4import { CheckpointRepository } from "../Services/Checkpoints.ts";
9import { CheckpointRepositoryLive } from "./Checkpoints.ts";5import { CheckpointRepositoryLive } from "./Checkpoints.ts";
10import { makeSqlitePersistenceLive } from "./Sqlite.ts";6import { SqlitePersistenceMemory } 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 );
177
18function makeCheckpointRepositoryLayer(dbPath: string) {8function makeCheckpointRepositoryLayer() {
19 return CheckpointRepositoryLive.pipe(Layer.provide(makeSqlitePersistenceLive(dbPath)));9 return CheckpointRepositoryLive.pipe(Layer.provide(SqlitePersistenceMemory));
20}10}
2111
22it.effect("CheckpointRepositoryLive upserts, lists, reads, and prunes checkpoint metadata", () =>12it.effect("CheckpointRepositoryLive upserts, lists, reads, and prunes checkpoint metadata", () =>
23 Effect.gen(function* () {13 Effect.gen(function* () {
24 const stateDir = yield* makeTemporaryDirectoryScoped("t3code-checkpoint-repository-");
25 const dbPath = path.join(stateDir, "orchestration.sqlite");
26
27 yield* Effect.gen(function* () {14 yield* Effect.gen(function* () {
28 const repository = yield* CheckpointRepository;15 const repository = yield* CheckpointRepository;
2916
...@@ -90,6 +77,6 @@ it.effect("CheckpointRepositoryLive upserts, lists, reads, and prunes checkpoint...@@ -90,6 +77,6 @@ it.effect("CheckpointRepositoryLive upserts, lists, reads, and prunes checkpoint
90 yield* repository.deleteAllForSession({ providerSessionId: "sess-1" });77 yield* repository.deleteAllForSession({ providerSessionId: "sess-1" });
91 const empty = yield* repository.listCheckpoints({ providerSessionId: "sess-1" });78 const empty = yield* repository.listCheckpoints({ providerSessionId: "sess-1" });
92 assert.equal(empty.length, 0);79 assert.equal(empty.length, 0);
93 }).pipe(Effect.provide(makeCheckpointRepositoryLayer(dbPath)));80 }).pipe(Effect.provide(makeCheckpointRepositoryLayer()));
94 }),81 }),
95);82);
apps/server/src/persistence/Layers/Checkpoints.ts-5
...@@ -20,7 +20,6 @@ import {...@@ -20,7 +20,6 @@ import {
20 CheckpointRepositoryValidationError,20 CheckpointRepositoryValidationError,
21 type CheckpointRepositoryError,21 type CheckpointRepositoryError,
22} from "../Errors.ts";22} from "../Errors.ts";
23import { makeSqlitePersistenceLive } from "./Sqlite.ts";
24import {23import {
25 CheckpointRepository,24 CheckpointRepository,
26 type CheckpointRepositoryEntry,25 type CheckpointRepositoryEntry,
...@@ -357,7 +356,3 @@ export const CheckpointRepositoryLive = Layer.effect(...@@ -357,7 +356,3 @@ export const CheckpointRepositoryLive = Layer.effect(
357 CheckpointRepository,356 CheckpointRepository,
358 makeCheckpointRepository,357 makeCheckpointRepository,
359);358);
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 @@...@@ -1,39 +1,25 @@
1import fs from "node:fs";1import { Effect, Layer } from "effect";
2import os from "node:os";2import { assert, it } from "@effect/vitest";
3import path from "node:path";
4import { DatabaseSync } from "node:sqlite";
5
6import { ManagedRuntime, Effect } from "effect";
7import { afterEach, describe, expect, it } from "vitest";
83
9import { OrchestrationEventRepository } from "../Services/OrchestrationEvents.ts";4import { OrchestrationEventRepository } from "../Services/OrchestrationEvents.ts";
10import { makeSqliteOrchestrationEventRepositoryLive } from "./OrchestrationEvents.ts";5import { SqlitePersistenceMemory } from "./Sqlite.ts";
116import { OrchestrationEventRepositoryLive } from "./OrchestrationEvents.ts";
12const tempDirs: string[] = [];7import { NodeServices } from "@effect/platform-node";
138
14function makeTempDir(prefix: string): string {9const layer = it.layer(
15 const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));10 Layer.mergeAll(
16 tempDirs.push(dir);11 Layer.provide(OrchestrationEventRepositoryLive, SqlitePersistenceMemory),
17 return dir;12 NodeServices.layer,
18}13 ),
1914);
20afterEach(() => {15
21 for (const dir of tempDirs.splice(0, tempDirs.length)) {16layer("OrchestrationEventRepository", (it) => {
22 fs.rmSync(dir, { recursive: true, force: true });17 it.effect("stores and replays events", () =>
23 }18 Effect.gen(function* () {
24});19 const createdAt = new Date().toISOString();
2520
26describe("OrchestrationEventRepository", () => {21 const repository = yield* OrchestrationEventRepository;
27 it("persists and replays events across restarts", async () => {22 const saved = yield* repository.append({
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({
37 eventId: "event-1",23 eventId: "event-1",
38 type: "thread.created",24 type: "thread.created",
39 aggregateType: "thread",25 aggregateType: "thread",
...@@ -41,57 +27,11 @@ describe("OrchestrationEventRepository", () => {...@@ -41,57 +27,11 @@ describe("OrchestrationEventRepository", () => {
41 occurredAt: createdAt,27 occurredAt: createdAt,
42 commandId: "cmd-1",28 commandId: "cmd-1",
43 payload: { id: "thread-1", projectId: "project-1", title: "demo" },29 payload: { id: "thread-1", projectId: "project-1", title: "demo" },
44 }),30 });
45 );31 assert.equal(saved.sequence, 1);
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;
8432
85 const migrationRows = db33 const replayed = yield* repository.readFromSequence(0);
86 .prepare(34 assert.deepEqual(replayed, [saved]);
87 `35 }),
88 SELECT *36 );
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 });
97});37});
apps/server/src/persistence/Layers/OrchestrationEvents.ts-5
...@@ -11,7 +11,6 @@ import {...@@ -11,7 +11,6 @@ import {
11 toPersistenceSqlError,11 toPersistenceSqlError,
12 type OrchestrationEventRepositoryError,12 type OrchestrationEventRepositoryError,
13} from "../Errors.ts";13} from "../Errors.ts";
14import { makeSqlitePersistenceLive } from "./Sqlite.ts";
15import {14import {
16 OrchestrationEventRepository,15 OrchestrationEventRepository,
17 type OrchestrationEventRepositoryShape,16 type OrchestrationEventRepositoryShape,
...@@ -177,7 +176,3 @@ export const OrchestrationEventRepositoryLive = Layer.effect(...@@ -177,7 +176,3 @@ export const OrchestrationEventRepositoryLive = Layer.effect(
177 OrchestrationEventRepository,176 OrchestrationEventRepository,
178 makeRepository,177 makeRepository,
179);178);
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 @@...@@ -1,83 +1,56 @@
1import fs from "node:fs";1import { Effect, Layer, FileSystem } from "effect";
2import os from "node:os";2import { assert, it } from "@effect/vitest";
3import path from "node:path";
4
5import { Effect, Layer, ManagedRuntime } from "effect";
6import { afterEach, describe, expect, it } from "vitest";
73
8import { ProjectRepository } from "../Services/Projects.ts";4import { ProjectRepository } from "../Services/Projects.ts";
9import { ProjectRepositoryLive } from "./Projects.ts";5import { ProjectRepositoryLive } from "./Projects.ts";
10import { makeSqlitePersistenceLive } from "./Sqlite.ts";6import { SqlitePersistenceMemory } from "./Sqlite.ts";
117import { NodeServices } from "@effect/platform-node";
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);
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));13layer("ProjectRepository", (it) => {
49 const second = await secondRuntime.runPromise(Effect.service(ProjectRepository));14 it.effect("stores projects and deduplicates adds by cwd", () =>
50 const listed = await secondRuntime.runPromise(second.list());15 Effect.gen(function* () {
16 const fs = yield* FileSystem.FileSystem;
17 const projectDir = yield* fs.makeTempDirectoryScoped();
5118
52 expect(listed).toHaveLength(1);19 const repository = yield* ProjectRepository;
53 expect(listed[0]?.cwd).toBe(projectDir);
5420
55 await secondRuntime.dispose();21 const created = yield* repository.add({ cwd: projectDir });
56 });22 assert.equal(created.created, true);
5723
58 it("prunes missing project paths", async () => {24 const duplicate = yield* repository.add({ cwd: projectDir });
59 const stateDir = makeTempDir("t3code-project-repo-prune-state-");25 assert.equal(duplicate.created, false);
60 const existing = makeTempDir("t3code-project-repo-prune-existing-");26 assert.equal(duplicate.project.id, created.project.id);
61 const missing = makeTempDir("t3code-project-repo-prune-missing-");
62 const dbPath = path.join(stateDir, "orchestration.sqlite");
6327
64 const runtime = ManagedRuntime.make(makeProjectRepositoryTest(dbPath));28 const listed = yield* repository.list();
65 const repository = await runtime.runPromise(Effect.service(ProjectRepository));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 }));34 it.effect("prunes missing project paths", () =>
68 const missingProject = await runtime.runPromise(repository.add({ cwd: missing }));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);40 const repository = yield* ProjectRepository;
71 expect(missingProject.created).toBe(true);
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());45 assert.isTrue(existingProject.created);
76 const listed = await runtime.runPromise(repository.list());46 assert.isTrue(missingProject.created);
7747
78 expect(listed).toHaveLength(1);48 yield* fs.remove(missing, { recursive: true, force: true });
79 expect(listed[0]?.cwd).toBe(existing);
8049
81 await runtime.dispose();50 yield* repository.pruneMissing();
82 });51 const listed = yield* repository.list();
52 assert.equal(listed.length, 1);
53 assert.equal(listed[0]?.cwd, existing);
54 }),
55 );
83});56});
apps/server/src/persistence/Layers/Sqlite.ts+18-35
...@@ -1,41 +1,24 @@...@@ -1,41 +1,24 @@
1import fs from "node:fs";1import { Effect, Layer, FileSystem, Path } from "effect";
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";
72
8import { runMigrations } from "../Migrations.ts";3import { runMigrations } from "../Migrations.ts";
4import * as SqliteClient from "../SqliteClient.ts";
95
10export interface PersistenceConfigShape {6const setup = Layer.effectDiscard(
11 readonly dbPath: string;7 Effect.gen(function* () {
12}8 const sql = yield* SqliteClient.SqliteClient;
139 yield* sql`PRAGMA journal_mode = WAL;`;
14export class PersistenceConfig extends ServiceMap.Service<10 yield* sql`PRAGMA foreign_keys = ON;`;
15 PersistenceConfig,11 yield* runMigrations;
16 PersistenceConfigShape12 }),
17>()("persistence/Config") {}13);
1814
19export function makeSqlitePersistenceLive(dbPath: string) {15export const makeSqlitePersistenceLive = (dbPath: string) =>
20 const SqliteClientLive = Layer.unwrap(16 Effect.gen(function* () {
21 Effect.sync(() => {17 const fs = yield* FileSystem.FileSystem;
22 fs.mkdirSync(path.dirname(dbPath), { recursive: true });18 const path = yield* Path.Path;
23 return SqliteClient.layer({ filename: dbPath });19 yield* fs.makeDirectory(path.dirname(dbPath), { recursive: true });
24 }),
25 );
2620
27 return Layer.effect(21 return Layer.provideMerge(setup, SqliteClient.layer({ filename: dbPath }));
28 SqlClient.SqlClient,22 }).pipe(Layer.unwrap);
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}
3823
39export const SqlitePersistenceLive = Layer.unwrap(24export const SqlitePersistenceMemory = Layer.provideMerge(setup, SqliteClient.layerMemory());
40 Effect.map(Effect.service(PersistenceConfig), ({ dbPath }) => makeSqlitePersistenceLive(dbPath)),
41);
apps/server/src/persistence/Migrations.ts+1-1
...@@ -59,7 +59,7 @@ export const runMigrations = run({ loader });...@@ -59,7 +59,7 @@ export const runMigrations = run({ loader });
59 * @example59 * @example
60 * ```typescript60 * ```typescript
61 * import { MigrationsLive } from "@acme/db/Migrations"61 * import { MigrationsLive } from "@acme/db/Migrations"
62 * import { SqliteClient } from "@effect/sql-sqlite-node"62 * import * as SqliteClient from "@acme/db/SqliteClient"
63 *63 *
64 * // Migrations run automatically when SqliteClient is provided64 * // Migrations run automatically when SqliteClient is provided
65 * const AppLayer = MigrationsLive.pipe(65 * 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";...@@ -6,8 +6,9 @@ import { Effect, Layer } from "effect";
66
7import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";7import { CodexAdapter, CodexAdapterShape } from "../Services/CodexAdapter.ts";
8import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";8import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts";
9import { makeProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";9import { ProviderAdapterRegistryLive } from "./ProviderAdapterRegistry.ts";
10import { ProviderUnsupportedError } from "../Errors.ts";10import { ProviderUnsupportedError } from "../Errors.ts";
11import { NodeServices } from "@effect/platform-node";
1112
12const fakeCodexAdapter: CodexAdapterShape = {13const fakeCodexAdapter: CodexAdapterShape = {
13 provider: "codex",14 provider: "codex",
...@@ -25,13 +26,14 @@ const fakeCodexAdapter: CodexAdapterShape = {...@@ -25,13 +26,14 @@ const fakeCodexAdapter: CodexAdapterShape = {
25};26};
2627
27const layer = it.layer(28const layer = it.layer(
28 makeProviderAdapterRegistryLive().pipe(29 Layer.mergeAll(
29 Layer.provide(Layer.succeed(CodexAdapter, fakeCodexAdapter)),30 Layer.provide(ProviderAdapterRegistryLive, Layer.succeed(CodexAdapter, fakeCodexAdapter)),
31 NodeServices.layer,
30 ),32 ),
31);33);
3234
33layer("ProviderAdapterRegistryLive", (it) => {35layer("ProviderAdapterRegistryLive", (it) => {
34 it("resolves a registered provider adapter", () =>36 it.effect("resolves a registered provider adapter", () =>
35 Effect.gen(function* () {37 Effect.gen(function* () {
36 const registry = yield* ProviderAdapterRegistry;38 const registry = yield* ProviderAdapterRegistry;
37 const adapter = yield* registry.getByProvider("codex");39 const adapter = yield* registry.getByProvider("codex");
...@@ -39,12 +41,14 @@ layer("ProviderAdapterRegistryLive", (it) => {...@@ -39,12 +41,14 @@ layer("ProviderAdapterRegistryLive", (it) => {
3941
40 const providers = yield* registry.listProviders();42 const providers = yield* registry.listProviders();
41 assert.deepEqual(providers, ["codex"]);43 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", () =>
45 Effect.gen(function* () {48 Effect.gen(function* () {
46 const registry = yield* ProviderAdapterRegistry;49 const registry = yield* ProviderAdapterRegistry;
47 const adapter = yield* registry.getByProvider("unknown" as ProviderKind).pipe(Effect.result);50 const adapter = yield* registry.getByProvider("unknown" as ProviderKind).pipe(Effect.result);
48 assertFailure(adapter, new ProviderUnsupportedError({ provider: "unknown" }));51 assertFailure(adapter, new ProviderUnsupportedError({ provider: "unknown" }));
49 }));52 }),
53 );
50});54});
apps/server/src/provider/Layers/ProviderAdapterRegistry.ts-4
...@@ -51,7 +51,3 @@ export const ProviderAdapterRegistryLive = Layer.effect(...@@ -51,7 +51,3 @@ export const ProviderAdapterRegistryLive = Layer.effect(
51 ProviderAdapterRegistry,51 ProviderAdapterRegistry,
52 makeProviderAdapterRegistry(),52 makeProviderAdapterRegistry(),
53);53);
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"...@@ -31,6 +31,7 @@ import { ProviderAdapterRegistry } from "../Services/ProviderAdapterRegistry.ts"
31import { ProviderService } from "../Services/ProviderService.ts";31import { ProviderService } from "../Services/ProviderService.ts";
32import { ProviderServiceLive } from "./ProviderService.ts";32import { ProviderServiceLive } from "./ProviderService.ts";
33import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts";33import { ProviderSessionDirectoryLive } from "./ProviderSessionDirectory.ts";
34import { NodeServices } from "@effect/platform-node";
3435
35function makeFakeCodexAdapter() {36function makeFakeCodexAdapter() {
36 const sessions = new Map<string, ProviderSession>();37 const sessions = new Map<string, ProviderSession>();
...@@ -253,15 +254,17 @@ function makeProviderServiceLayer() {...@@ -253,15 +254,17 @@ function makeProviderServiceLayer() {
253 listProviders: () => Effect.succeed(["codex"]),254 listProviders: () => Effect.succeed(["codex"]),
254 };255 };
255256
257 const checkpointLayer = Layer.succeed(CheckpointService, checkpoint.service);
258 const providerAdapterLayer = Layer.succeed(ProviderAdapterRegistry, registry);
259
256 const layer = it.layer(260 const layer = it.layer(
257 ProviderServiceLive.pipe(261 Layer.mergeAll(
258 Layer.provide(262 ProviderServiceLive.pipe(
259 Layer.mergeAll(263 Layer.provide(providerAdapterLayer),
260 Layer.succeed(ProviderAdapterRegistry, registry),264 Layer.provide(ProviderSessionDirectoryLive),
261 ProviderSessionDirectoryLive,265 Layer.provide(checkpointLayer),
262 Layer.succeed(CheckpointService, checkpoint.service),
263 ),
264 ),266 ),
267 NodeServices.layer,
265 ),268 ),
266 );269 );
267270
...@@ -424,4 +427,40 @@ validation.layer("ProviderServiceLive validation", (it) => {...@@ -424,4 +427,40 @@ validation.layer("ProviderServiceLive validation", (it) => {
424 );427 );
425 }),428 }),
426 );429 );
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 );
427});466});
apps/server/src/provider/Layers/ProviderService.ts+11-1
...@@ -161,10 +161,20 @@ const makeProviderService = Effect.gen(function* () {...@@ -161,10 +161,20 @@ const makeProviderService = Effect.gen(function* () {
161 );161 );
162 }162 }
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
164 yield* directory.upsert({174 yield* directory.upsert({
165 sessionId: session.sessionId,175 sessionId: session.sessionId,
166 provider: session.provider,176 provider: session.provider,
167 ...(session.threadId !== undefined ? { threadId: session.threadId } : {}),177 threadId,
168 });178 });
169179
170 const checkpointCwd = session.cwd ?? input.cwd ?? process.cwd();180 const checkpointCwd = session.cwd ?? input.cwd ?? process.cwd();
apps/server/src/wsServer.test.ts+13-5
...@@ -30,7 +30,9 @@ import type {...@@ -30,7 +30,9 @@ import type {
30} from "@t3tools/contracts";30} from "@t3tools/contracts";
31import type { TerminalManager } from "./terminalManager";31import type { TerminalManager } from "./terminalManager";
32import { ProjectRepositoryLive } from "./persistence/Layers/Projects";32import { 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
35interface PendingMessages {37interface PendingMessages {
36 queue: unknown[];38 queue: unknown[];
...@@ -237,6 +239,7 @@ describe("WebSocket Server", () => {...@@ -237,6 +239,7 @@ describe("WebSocket Server", () => {
237239
238 function createTestServer(240 function createTestServer(
239 options: {241 options: {
242 persistenceLayer?: Layer.Layer<SqlClient.SqlClient, any>;
240 cwd?: string;243 cwd?: string;
241 devUrl?: string;244 devUrl?: string;
242 authToken?: string;245 authToken?: string;
...@@ -253,6 +256,7 @@ describe("WebSocket Server", () => {...@@ -253,6 +256,7 @@ describe("WebSocket Server", () => {
253 port: 0,256 port: 0,
254 cwd: options.cwd ?? "/test/project",257 cwd: options.cwd ?? "/test/project",
255 stateDir,258 stateDir,
259 persistenceLayer: options.persistenceLayer ?? SqlitePersistenceMemory,
256 ...(options.devUrl ? { devUrl: options.devUrl } : {}),260 ...(options.devUrl ? { devUrl: options.devUrl } : {}),
257 ...(options.authToken ? { authToken: options.authToken } : {}),261 ...(options.authToken ? { authToken: options.authToken } : {}),
258 ...(options.gitManager ? { gitManager: options.gitManager as never } : {}),262 ...(options.gitManager ? { gitManager: options.gitManager as never } : {}),
...@@ -1033,11 +1037,11 @@ describe("WebSocket Server", () => {...@@ -1033,11 +1037,11 @@ describe("WebSocket Server", () => {
1033 const stateDir = makeTempDir("t3code-ws-prune-state-");1037 const stateDir = makeTempDir("t3code-ws-prune-state-");
1034 const existing = makeTempDir("t3code-ws-existing-project-");1038 const existing = makeTempDir("t3code-ws-existing-project-");
1035 const missing = makeTempDir("t3code-ws-missing-project-");1039 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
1037 const projectRuntime = ManagedRuntime.make(1043 const projectRuntime = ManagedRuntime.make(
1038 ProjectRepositoryLive.pipe(1044 ProjectRepositoryLive.pipe(Layer.provide(persistenceLayer)),
1039 Layer.provide(makeSqlitePersistenceLive(path.join(stateDir, "orchestration.sqlite"))),
1040 ),
1041 );1045 );
1042 const projectRepository = await projectRuntime.runPromise(Effect.service(ProjectRepository));1046 const projectRepository = await projectRuntime.runPromise(Effect.service(ProjectRepository));
1043 await projectRuntime.runPromise(projectRepository.add({ cwd: existing }));1047 await projectRuntime.runPromise(projectRepository.add({ cwd: existing }));
...@@ -1045,7 +1049,11 @@ describe("WebSocket Server", () => {...@@ -1045,7 +1049,11 @@ describe("WebSocket Server", () => {
1045 await projectRuntime.dispose();1049 await projectRuntime.dispose();
1046 fs.rmSync(missing, { recursive: true, force: true });1050 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 });
1049 await server.start();1057 await server.start();
1050 const addr = server.httpServer.address();1058 const addr = server.httpServer.address();
1051 const port = typeof addr === "object" && addr !== null ? addr.port : 0;1059 const port = typeof addr === "object" && addr !== null ? addr.port : 0;
apps/server/src/wsServer.ts+181-124
...@@ -18,11 +18,11 @@ import {...@@ -18,11 +18,11 @@ import {
18 type WsResponse,18 type WsResponse,
19 wsRequestSchema,19 wsRequestSchema,
20} from "@t3tools/contracts";20} from "@t3tools/contracts";
21import { NodeServices } from "@effect/platform-node";
21import { Effect, Layer, ManagedRuntime } from "effect";22import { Effect, Layer, ManagedRuntime } from "effect";
22import { WebSocketServer, type WebSocket } from "ws";23import { WebSocketServer, type WebSocket } from "ws";
2324
24import { createLogger } from "./logger";25import { createLogger } from "./logger";
25import { ProviderManager } from "./providerManager";
26import { GitManager } from "./gitManager";26import { GitManager } from "./gitManager";
27import {27import {
28 checkoutGitBranch,28 checkoutGitBranch,
...@@ -43,6 +43,17 @@ import { makeSqlitePersistenceLive } from "./persistence/Layers/Sqlite";...@@ -43,6 +43,17 @@ import { makeSqlitePersistenceLive } from "./persistence/Layers/Sqlite";
43import { ProjectRepository, type ProjectRepositoryShape } from "./persistence/Services/Projects";43import { ProjectRepository, type ProjectRepositoryShape } from "./persistence/Services/Projects";
44import assert from "node:assert";44import assert from "node:assert";
45import { OrchestrationEventRepositoryLive } from "./persistence/Layers/OrchestrationEvents";45import { 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
47const MIME_TYPES: Record<string, string> = {58const MIME_TYPES: Record<string, string> = {
48 ".html": "text/html; charset=utf-8",59 ".html": "text/html; charset=utf-8",
...@@ -64,6 +75,9 @@ export interface ServerOptions {...@@ -64,6 +75,9 @@ export interface ServerOptions {
64 host?: string | undefined;75 host?: string | undefined;
65 cwd: string;76 cwd: string;
66 stateDir?: string | undefined;77 stateDir?: string | undefined;
78 persistenceLayer?:
79 | Layer.Layer<SqlClient.SqlClient, SqlError.SqlError | Migrator.MigrationError>
80 | undefined;
67 staticDir?: string | undefined;81 staticDir?: string | undefined;
68 devUrl?: string | undefined;82 devUrl?: string | undefined;
69 logWebSocketEvents?: boolean | undefined;83 logWebSocketEvents?: boolean | undefined;
...@@ -121,6 +135,7 @@ export function createServer(options: ServerOptions) {...@@ -121,6 +135,7 @@ export function createServer(options: ServerOptions) {
121 host,135 host,
122 cwd,136 cwd,
123 stateDir,137 stateDir,
138 persistenceLayer: customPersistenceLayer,
124 staticDir,139 staticDir,
125 devUrl,140 devUrl,
126 logWebSocketEvents: explicitLogWsEvents,141 logWebSocketEvents: explicitLogWsEvents,
...@@ -128,15 +143,15 @@ export function createServer(options: ServerOptions) {...@@ -128,15 +143,15 @@ export function createServer(options: ServerOptions) {
128 terminalManager = new TerminalManager(),143 terminalManager = new TerminalManager(),
129 authToken,144 authToken,
130 } = options;145 } = options;
131 const providerManager = new ProviderManager();
132146
133 const resolvedStateDir = stateDir ?? path.join(os.homedir(), ".t3", "userdata");147 const resolvedStateDir = stateDir ?? path.join(os.homedir(), ".t3", "userdata");
134 let effectRuntime: ManagedRuntime.ManagedRuntime<148 let effectRuntime: ManagedRuntime.ManagedRuntime<
135 ProjectRepository | OrchestrationEngineService,149 ProjectRepository | OrchestrationEngineService | ProviderService,
136 unknown150 unknown
137 > | null = null;151 > | null = null;
138 let projectRepository: ProjectRepositoryShape | null = null;152 let projectRepository: ProjectRepositoryShape | null = null;
139 let orchestrationEngine: OrchestrationEngineShape | null = null;153 let orchestrationEngine: OrchestrationEngineShape | null = null;
154 let providerService: ProviderServiceShape | null = null;
140 const clients = new Set<WebSocket>();155 const clients = new Set<WebSocket>();
141 const logger = createLogger("ws");156 const logger = createLogger("ws");
142 const logWebSocketEvents =157 const logWebSocketEvents =
...@@ -144,6 +159,7 @@ export function createServer(options: ServerOptions) {...@@ -144,6 +159,7 @@ export function createServer(options: ServerOptions) {
144 let keybindingsConfig = loadResolvedKeybindingsConfig(logger);159 let keybindingsConfig = loadResolvedKeybindingsConfig(logger);
145 let unsubscribeReadModel = noop;160 let unsubscribeReadModel = noop;
146 let unsubscribeDomainEvents = noop;161 let unsubscribeDomainEvents = noop;
162 let unsubscribeProviderEvents = noop;
147163
148 function logOutgoingPush(push: WsPush, recipients: number) {164 function logOutgoingPush(push: WsPush, recipients: number) {
149 if (!logWebSocketEvents) return;165 if (!logWebSocketEvents) return;
...@@ -169,8 +185,16 @@ export function createServer(options: ServerOptions) {...@@ -169,8 +185,16 @@ export function createServer(options: ServerOptions) {
169 return effectRuntime;185 return effectRuntime;
170 };186 };
171187
188 const getProviderService = () => {
189 assert(providerService, "Provider service is not started");
190 return providerService;
191 };
192
172 async function attachOrchestrationSubscriptions(193 async function attachOrchestrationSubscriptions(
173 runtime: ManagedRuntime.ManagedRuntime<ProjectRepository | OrchestrationEngineService, unknown>,194 runtime: ManagedRuntime.ManagedRuntime<
195 ProjectRepository | OrchestrationEngineService | ProviderService,
196 unknown
197 >,
174 orchestrationEngine: OrchestrationEngineShape,198 orchestrationEngine: OrchestrationEngineShape,
175 ) {199 ) {
176 unsubscribeReadModel = await runtime.runPromise(200 unsubscribeReadModel = await runtime.runPromise(
...@@ -215,114 +239,121 @@ export function createServer(options: ServerOptions) {...@@ -215,114 +239,121 @@ export function createServer(options: ServerOptions) {
215 );239 );
216 }240 }
217241
218 // Forward provider events to all connected WebSocket clients242 async function attachProviderSubscriptions(
219 providerManager.on("event", (event) => {243 runtime: ManagedRuntime.ManagedRuntime<
220 void (async () => {244 ProjectRepository | OrchestrationEngineService | ProviderService,
221 const liveOrchestrationEngine = orchestrationEngine;245 unknown
222 const runtime = effectRuntime;246 >,
223 if (!liveOrchestrationEngine || !runtime) {247 providerService: ProviderServiceShape,
224 return;248 orchestrationEngine: OrchestrationEngineShape,
225 }249 ) {
226 const snapshot = await runtime.runPromise(liveOrchestrationEngine.getSnapshot());250 unsubscribeProviderEvents = await runtime.runPromise(
227 const thread = snapshot.threads.find((entry) => entry.session?.sessionId === event.sessionId);251 providerService.subscribeToEvents((event) => {
228 if (!thread) return;252 void (async () => {
229 const now = event.createdAt;253 const snapshot = await runtime.runPromise(orchestrationEngine.getSnapshot());
230 if (event.method === "turn/started" || event.method === "turn/completed") {254 const thread = snapshot.threads.find(
231 await runtime.runPromise(255 (entry) => entry.session?.sessionId === event.sessionId,
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 }),
301 );256 );
302 }257 if (!thread) return;
303 }258 const now = event.createdAt;
304 if (event.kind === "error" && event.message) {259 if (event.method === "turn/started" || event.method === "turn/completed") {
305 await runtime.runPromise(260 await runtime.runPromise(
306 liveOrchestrationEngine.dispatch({261 orchestrationEngine.dispatch({
307 type: "thread.session",262 type: "thread.session",
308 commandId: crypto.randomUUID(),263 commandId: crypto.randomUUID(),
309 threadId: thread.id,264 threadId: thread.id,
310 session: {265 session: {
311 sessionId: event.sessionId,266 sessionId: event.sessionId,
312 provider: event.provider,267 provider: event.provider,
313 status: "error",268 status: event.method === "turn/started" ? "running" : "ready",
314 threadId: thread.id,269 threadId: thread.id,
315 activeTurnId: event.turnId ?? null,270 activeTurnId: event.method === "turn/started" ? (event.turnId ?? null) : null,
316 createdAt: thread.session?.createdAt ?? now,271 createdAt: thread.session?.createdAt ?? now,
317 updatedAt: now,272 updatedAt: now,
318 lastError: event.message,273 lastError: null,
319 },274 },
320 createdAt: now,275 createdAt: now,
321 }),276 }),
322 );277 );
323 }278 }
324 })().catch(() => undefined);279 if (event.textDelta && event.textDelta.length > 0) {
325 });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
327 const onTerminalEvent = (event: TerminalEvent) => {358 const onTerminalEvent = (event: TerminalEvent) => {
328 const push: WsPush = {359 const push: WsPush = {
...@@ -504,33 +535,36 @@ export function createServer(options: ServerOptions) {...@@ -504,33 +535,36 @@ export function createServer(options: ServerOptions) {
504 const orchestrationEngine = getOrchestrationEngine();535 const orchestrationEngine = getOrchestrationEngine();
505 const projectRepository = getProjectRepository();536 const projectRepository = getProjectRepository();
506 const effectRuntime = getEffectRuntime();537 const effectRuntime = getEffectRuntime();
538 const providerService = getProviderService();
507539
508 switch (request.method) {540 switch (request.method) {
509 case WS_METHODS.providersStartSession:541 case WS_METHODS.providersStartSession:
510 return providerManager.startSession(request.params as never);542 return effectRuntime.runPromise(providerService.startSession(request.params as never));
511543
512 case WS_METHODS.providersSendTurn:544 case WS_METHODS.providersSendTurn:
513 return providerManager.sendTurn(request.params as never);545 return effectRuntime.runPromise(providerService.sendTurn(request.params as never));
514546
515 case WS_METHODS.providersInterruptTurn:547 case WS_METHODS.providersInterruptTurn:
516 return providerManager.interruptTurn(request.params as never);548 return effectRuntime.runPromise(providerService.interruptTurn(request.params as never));
517549
518 case WS_METHODS.providersRespondToRequest:550 case WS_METHODS.providersRespondToRequest:
519 return providerManager.respondToRequest(request.params as never);551 return effectRuntime.runPromise(providerService.respondToRequest(request.params as never));
520552
521 case WS_METHODS.providersStopSession: {553 case WS_METHODS.providersStopSession: {
522 providerManager.stopSession(request.params as never);554 await effectRuntime.runPromise(providerService.stopSession(request.params as never));
523 return undefined;555 return undefined;
524 }556 }
525557
526 case WS_METHODS.providersListCheckpoints:558 case WS_METHODS.providersListCheckpoints:
527 return providerManager.listCheckpoints(request.params as never);559 return effectRuntime.runPromise(providerService.listCheckpoints(request.params as never));
528560
529 case WS_METHODS.providersGetCheckpointDiff:561 case WS_METHODS.providersGetCheckpointDiff:
530 return providerManager.getCheckpointDiff(request.params as never);562 return effectRuntime.runPromise(providerService.getCheckpointDiff(request.params as never));
531563
532 case WS_METHODS.providersRevertToCheckpoint: {564 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 );
534 const params = request.params as { sessionId?: string };568 const params = request.params as { sessionId?: string };
535 const sessionId = params.sessionId;569 const sessionId = params.sessionId;
536 if (typeof sessionId === "string") {570 if (typeof sessionId === "string") {
...@@ -735,25 +769,42 @@ export function createServer(options: ServerOptions) {...@@ -735,25 +769,42 @@ export function createServer(options: ServerOptions) {
735769
736 async function createEffectRuntime() {770 async function createEffectRuntime() {
737 const dbPath = path.join(resolvedStateDir, "orchestration.sqlite");771 const dbPath = path.join(resolvedStateDir, "orchestration.sqlite");
738 const persistenceLayer = makeSqlitePersistenceLive(dbPath);772 const persistenceLayer = customPersistenceLayer ?? makeSqlitePersistenceLive(dbPath);
739 const orchestrationLayer = Layer.provide(773 const orchestrationLayer = Layer.provide(
740 OrchestrationEngineLive,774 OrchestrationEngineLive,
741 OrchestrationEventRepositoryLive,775 OrchestrationEventRepositoryLive,
742 );776 );
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(
744 Layer.provide(persistenceLayer),791 Layer.provide(persistenceLayer),
792 Layer.provideMerge(NodeServices.layer),
745 );793 );
746 const runtime = ManagedRuntime.make(layer);794 const runtime = ManagedRuntime.make(layer);
747795
748 try {796 try {
749 const [nextOrchestrationEngine, repository] = await Promise.all([797 const [nextOrchestrationEngine, repository, nextProviderService] = await Promise.all([
750 runtime.runPromise(Effect.service(OrchestrationEngineService)),798 runtime.runPromise(Effect.service(OrchestrationEngineService)),
751 runtime.runPromise(Effect.service(ProjectRepository)),799 runtime.runPromise(Effect.service(ProjectRepository)),
800 runtime.runPromise(Effect.service(ProviderService)),
752 ]);801 ]);
753 orchestrationEngine = nextOrchestrationEngine;802 orchestrationEngine = nextOrchestrationEngine;
754 projectRepository = repository;803 projectRepository = repository;
804 providerService = nextProviderService;
755 await runtime.runPromise(repository.pruneMissing());805 await runtime.runPromise(repository.pruneMissing());
756 await attachOrchestrationSubscriptions(runtime, nextOrchestrationEngine);806 await attachOrchestrationSubscriptions(runtime, nextOrchestrationEngine);
807 await attachProviderSubscriptions(runtime, nextProviderService, nextOrchestrationEngine);
757 } catch (error) {808 } catch (error) {
758 await runtime.dispose().catch(() => undefined);809 await runtime.dispose().catch(() => undefined);
759 throw error;810 throw error;
...@@ -765,18 +816,26 @@ export function createServer(options: ServerOptions) {...@@ -765,18 +816,26 @@ export function createServer(options: ServerOptions) {
765 async function disposeEffectRuntime() {816 async function disposeEffectRuntime() {
766 unsubscribeReadModel();817 unsubscribeReadModel();
767 unsubscribeDomainEvents();818 unsubscribeDomainEvents();
819 unsubscribeProviderEvents();
768 unsubscribeReadModel = noop;820 unsubscribeReadModel = noop;
769 unsubscribeDomainEvents = noop;821 unsubscribeDomainEvents = noop;
822 unsubscribeProviderEvents = noop;
770823
771 const runtime = effectRuntime;824 const runtime = effectRuntime;
825 const liveProviderService = providerService;
772 effectRuntime = null;826 effectRuntime = null;
773 orchestrationEngine = null;827 orchestrationEngine = null;
774 projectRepository = null;828 projectRepository = null;
829 providerService = null;
775830
776 if (!runtime) {831 if (!runtime) {
777 return;832 return;
778 }833 }
779834
835 if (liveProviderService) {
836 await runtime.runPromise(liveProviderService.stopAll()).catch(() => undefined);
837 }
838
780 await runtime.dispose();839 await runtime.dispose();
781 }840 }
782841
...@@ -804,8 +863,6 @@ export function createServer(options: ServerOptions) {...@@ -804,8 +863,6 @@ export function createServer(options: ServerOptions) {
804 async function stop(): Promise<void> {863 async function stop(): Promise<void> {
805 await disposeEffectRuntime();864 await disposeEffectRuntime();
806 terminalManager.off("event", onTerminalEvent);865 terminalManager.off("event", onTerminalEvent);
807 providerManager.stopAll();
808 providerManager.dispose();
809 terminalManager.dispose();866 terminalManager.dispose();
810867
811 for (const client of clients) {868 for (const client of clients) {
bun.lock-61
...@@ -35,7 +35,6 @@...@@ -35,7 +35,6 @@
35 },35 },
36 "dependencies": {36 "dependencies": {
37 "@effect/platform-node": "4.0.0-beta.10",37 "@effect/platform-node": "4.0.0-beta.10",
38 "@effect/sql-sqlite-node": "4.0.0-beta.10",
39 "effect": "4.0.0-beta.10",38 "effect": "4.0.0-beta.10",
40 "node-pty": "^1.1.0",39 "node-pty": "^1.1.0",
41 "open": "^10.1.0",40 "open": "^10.1.0",
...@@ -154,8 +153,6 @@...@@ -154,8 +153,6 @@
154153
155 "@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=="],154 "@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
159 "@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=="],156 "@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
161 "@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=="],158 "@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 @@...@@ -610,28 +607,18 @@
610607
611 "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],608 "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
612609
613 "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
614
615 "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="],610 "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
619 "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],612 "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
623 "birpc": ["birpc@4.0.0", "", {}, "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw=="],614 "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
627 "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],616 "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="],
628617
629 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],618 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
630619
631 "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=="],620 "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
635 "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],622 "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
636623
637 "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],624 "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
...@@ -662,8 +649,6 @@...@@ -662,8 +649,6 @@
662649
663 "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=="],650 "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
667 "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],652 "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
668653
669 "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],654 "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
...@@ -698,8 +683,6 @@...@@ -698,8 +683,6 @@
698683
699 "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],684 "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
703 "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],686 "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
704687
705 "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],688 "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
...@@ -774,8 +757,6 @@...@@ -774,8 +757,6 @@
774757
775 "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],758 "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
779 "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],760 "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
780761
781 "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],762 "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
...@@ -788,8 +769,6 @@...@@ -788,8 +769,6 @@
788769
789 "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],770 "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
793 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],772 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
794773
795 "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],774 "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="],
...@@ -798,8 +777,6 @@...@@ -798,8 +777,6 @@
798777
799 "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=="],778 "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
803 "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=="],780 "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
805 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],782 "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
...@@ -818,8 +795,6 @@...@@ -818,8 +795,6 @@
818795
819 "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],796 "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
823 "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],798 "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
824799
825 "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=="],800 "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 @@...@@ -868,14 +843,10 @@
868843
869 "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="],844 "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
873 "import-from": ["import-from@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ=="],846 "import-from": ["import-from@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ=="],
874847
875 "import-without-cache": ["import-without-cache@0.2.5", "", {}, "sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A=="],848 "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
879 "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],850 "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="],
880851
881 "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],852 "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
...@@ -1082,8 +1053,6 @@...@@ -1082,8 +1053,6 @@
10821053
1083 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],1054 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
10841055
1085 "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
1086
1087 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],1056 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
10881057
1089 "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="],1058 "msgpackr": ["msgpackr@1.11.8", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA=="],
...@@ -1098,10 +1067,6 @@...@@ -1098,10 +1067,6 @@
10981067
1099 "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],1068 "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
1105 "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],1070 "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
11061071
1107 "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=="],1072 "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 @@...@@ -1150,8 +1115,6 @@
11501115
1151 "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=="],1116 "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
1155 "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],1118 "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
11561119
1157 "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],1120 "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
...@@ -1168,8 +1131,6 @@...@@ -1168,8 +1131,6 @@
11681131
1169 "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],1132 "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
1173 "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],1134 "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
11741135
1175 "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],1136 "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 @@...@@ -1178,8 +1139,6 @@
11781139
1179 "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],1140 "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
1183 "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],1142 "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
11841143
1185 "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=="],1144 "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 @@...@@ -1234,8 +1193,6 @@
12341193
1235 "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="],1194 "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
1239 "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],1196 "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
12401197
1241 "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],1198 "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
...@@ -1254,10 +1211,6 @@...@@ -1254,10 +1211,6 @@
12541211
1255 "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],1212 "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
1261 "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],1214 "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
12621215
1263 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],1216 "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
...@@ -1278,14 +1231,10 @@...@@ -1278,14 +1231,10 @@
12781231
1279 "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=="],1232 "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
1283 "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=="],1234 "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
1285 "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],1236 "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
1289 "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],1238 "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
12901239
1291 "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],1240 "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
...@@ -1306,10 +1255,6 @@...@@ -1306,10 +1255,6 @@
13061255
1307 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],1256 "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
1313 "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],1258 "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
13141259
1315 "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],1260 "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
...@@ -1346,8 +1291,6 @@...@@ -1346,8 +1291,6 @@
13461291
1347 "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=="],1292 "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
1351 "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=="],1294 "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
1353 "turbo-darwin-64": ["turbo-darwin-64@2.8.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xr4TO/oDDwoozbDtBvunb66g//WK8uHRygl72vUthuwzmiw48pil4IuoG/QbMHd9RE8aBnVmzC0WZEWk/WWt3A=="],1296 "turbo-darwin-64": ["turbo-darwin-64@2.8.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xr4TO/oDDwoozbDtBvunb66g//WK8uHRygl72vUthuwzmiw48pil4IuoG/QbMHd9RE8aBnVmzC0WZEWk/WWt3A=="],
...@@ -1400,8 +1343,6 @@...@@ -1400,8 +1343,6 @@
14001343
1401 "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=="],1344 "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
1405 "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],1346 "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
14061347
1407 "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],1348 "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 @@...@@ -1514,8 +1455,6 @@
15141455
1515 "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],1456 "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
1519 "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],1458 "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
15201459
1521 "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],1460 "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],