authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-22 12:38:21-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-02-26 13:20:36-08:00
log150b3782b942e3b35899e134536efc0e4bbd51ac
treeaa4d40ddbef78137fe10ae4acf9b808630ea2483
parent5bfd0598e763ba8221ae90b6b68524391a8492a0

more effect: new service graph


17 files changed, 955 insertions(+), 463 deletions(-)

apps/server/src/checkpointing/Errors.ts created+148
......@@ -0,0 +1,148 @@
1import { Schema } from "effect";
2
3/**
4 * CheckpointValidationError - Invalid checkpoint API input.
5 */
6export class CheckpointValidationError extends Schema.TaggedErrorClass<CheckpointValidationError>()(
7 "CheckpointValidationError",
8 {
9 operation: Schema.String,
10 issue: Schema.String,
11 cause: Schema.optional(Schema.Defect),
12 },
13) {
14 override get message(): string {
15 return `Checkpoint validation failed in ${this.operation}: ${this.issue}`;
16 }
17}
18
19/**
20 * CheckpointGitCommandError - Git command execution failed in checkpoint store.
21 */
22export class CheckpointGitCommandError extends Schema.TaggedErrorClass<CheckpointGitCommandError>()(
23 "CheckpointGitCommandError",
24 {
25 operation: Schema.String,
26 command: Schema.String,
27 cwd: Schema.String,
28 detail: Schema.String,
29 cause: Schema.optional(Schema.Defect),
30 },
31) {
32 override get message(): string {
33 return `Checkpoint git command failed in ${this.operation}: ${this.command} (${this.cwd}) - ${this.detail}`;
34 }
35}
36
37/**
38 * CheckpointUnavailableError - Expected checkpoint does not exist.
39 */
40export class CheckpointUnavailableError extends Schema.TaggedErrorClass<CheckpointUnavailableError>()(
41 "CheckpointUnavailableError",
42 {
43 threadId: Schema.String,
44 turnCount: Schema.Number,
45 detail: Schema.String,
46 cause: Schema.optional(Schema.Defect),
47 },
48) {
49 override get message(): string {
50 return `Checkpoint unavailable for thread ${this.threadId} turn ${this.turnCount}: ${this.detail}`;
51 }
52}
53
54/**
55 * CheckpointRepositoryError - Checkpointing unavailable for cwd/repository.
56 */
57export class CheckpointRepositoryError extends Schema.TaggedErrorClass<CheckpointRepositoryError>()(
58 "CheckpointRepositoryError",
59 {
60 cwd: Schema.String,
61 detail: Schema.String,
62 cause: Schema.optional(Schema.Defect),
63 },
64) {
65 override get message(): string {
66 return `Checkpoint repository error for ${this.cwd}: ${this.detail}`;
67 }
68}
69
70/**
71 * CheckpointCatalogPersistenceError - Metadata persistence/read failure.
72 */
73export class CheckpointCatalogPersistenceError extends Schema.TaggedErrorClass<CheckpointCatalogPersistenceError>()(
74 "CheckpointCatalogPersistenceError",
75 {
76 operation: Schema.String,
77 detail: Schema.String,
78 cause: Schema.optional(Schema.Defect),
79 },
80) {
81 override get message(): string {
82 return `Checkpoint catalog persistence error in ${this.operation}: ${this.detail}`;
83 }
84}
85
86/**
87 * CheckpointServiceValidationError - Invalid application-level checkpoint request.
88 */
89export class CheckpointServiceValidationError extends Schema.TaggedErrorClass<CheckpointServiceValidationError>()(
90 "CheckpointServiceValidationError",
91 {
92 operation: Schema.String,
93 issue: Schema.String,
94 cause: Schema.optional(Schema.Defect),
95 },
96) {
97 override get message(): string {
98 return `Checkpoint service validation failed in ${this.operation}: ${this.issue}`;
99 }
100}
101
102/**
103 * CheckpointSessionNotFoundError - Session is missing in checkpoint workflow.
104 */
105export class CheckpointSessionNotFoundError extends Schema.TaggedErrorClass<CheckpointSessionNotFoundError>()(
106 "CheckpointSessionNotFoundError",
107 {
108 sessionId: Schema.String,
109 cause: Schema.optional(Schema.Defect),
110 },
111) {
112 override get message(): string {
113 return `Checkpoint session not found: ${this.sessionId}`;
114 }
115}
116
117/**
118 * CheckpointInvariantError - Inconsistent provider/filesystem/catalog state.
119 */
120export class CheckpointInvariantError extends Schema.TaggedErrorClass<CheckpointInvariantError>()(
121 "CheckpointInvariantError",
122 {
123 operation: Schema.String,
124 detail: Schema.String,
125 cause: Schema.optional(Schema.Defect),
126 },
127) {
128 override get message(): string {
129 return `Checkpoint invariant violation in ${this.operation}: ${this.detail}`;
130 }
131}
132
133export type CheckpointStoreError =
134 | CheckpointValidationError
135 | CheckpointGitCommandError
136 | CheckpointUnavailableError
137 | CheckpointRepositoryError;
138
139export type CheckpointCatalogError =
140 | CheckpointValidationError
141 | CheckpointCatalogPersistenceError;
142
143export type CheckpointServiceError =
144 | CheckpointStoreError
145 | CheckpointCatalogError
146 | CheckpointServiceValidationError
147 | CheckpointSessionNotFoundError
148 | CheckpointInvariantError;
apps/server/src/checkpointing/Services/CheckpointCatalog.ts created+83
......@@ -0,0 +1,83 @@
1/**
2 * CheckpointCatalog - Metadata repository for checkpoints.
3 *
4 * Stores user-facing checkpoint metadata used by list/revert APIs. It does not
5 * touch the filesystem or hidden Git refs; `CheckpointStore` owns that state.
6 * It also does not drive provider conversation rollback.
7 *
8 * @module CheckpointCatalog
9 */
10import type { ProviderCheckpoint } from "@t3tools/contracts";
11import { Option, ServiceMap } from "effect";
12import type { Effect } from "effect";
13
14import type { CheckpointCatalogError } from "../Errors.ts";
15
16export interface UpsertCheckpointInput {
17 readonly providerSessionId: string;
18 readonly threadId: string;
19 readonly turnCount: number;
20 readonly messageCount: number;
21 readonly label: string;
22 readonly preview?: string;
23 readonly createdAt: string;
24}
25
26export interface ListSessionCheckpointsInput {
27 readonly providerSessionId: string;
28}
29
30export interface GetCheckpointInput {
31 readonly providerSessionId: string;
32 readonly turnCount: number;
33}
34
35export interface DeleteAfterTurnInput {
36 readonly providerSessionId: string;
37 readonly maxTurnCount: number;
38}
39
40export interface DeleteAllForSessionInput {
41 readonly providerSessionId: string;
42}
43
44export interface CheckpointCatalogShape {
45 /**
46 * Insert or update one checkpoint metadata row.
47 */
48 readonly upsertCheckpoint: (input: UpsertCheckpointInput) => Effect.Effect<void, CheckpointCatalogError>;
49
50 /**
51 * List checkpoints for a provider session.
52 */
53 readonly listCheckpoints: (
54 input: ListSessionCheckpointsInput,
55 ) => Effect.Effect<ReadonlyArray<ProviderCheckpoint>, CheckpointCatalogError>;
56
57 /**
58 * Read one checkpoint by turn count.
59 */
60 readonly getCheckpoint: (
61 input: GetCheckpointInput,
62 ) => Effect.Effect<Option.Option<ProviderCheckpoint>, CheckpointCatalogError>;
63
64 /**
65 * Delete checkpoint metadata newer than the provided turn count.
66 */
67 readonly deleteAfterTurn: (input: DeleteAfterTurnInput) => Effect.Effect<void, CheckpointCatalogError>;
68
69 /**
70 * Delete all checkpoint metadata for a provider session.
71 */
72 readonly deleteAllForSession: (
73 input: DeleteAllForSessionInput,
74 ) => Effect.Effect<void, CheckpointCatalogError>;
75}
76
77/**
78 * CheckpointCatalog - Service tag for checkpoint metadata persistence.
79 */
80export class CheckpointCatalog extends ServiceMap.Service<
81 CheckpointCatalog,
82 CheckpointCatalogShape
83>()("checkpointing/CheckpointCatalog") {}
apps/server/src/checkpointing/Services/CheckpointService.ts created+78
......@@ -0,0 +1,78 @@
1/**
2 * CheckpointService - Application orchestrator for checkpoint workflows.
3 *
4 * Coordinates `CheckpointStore` (Git refs), `CheckpointCatalog` (metadata), and
5 * provider timeline operations resolved through `ProviderSessionDirectory` and
6 * `ProviderAdapterRegistry` (read/rollback/hasSession on adapters).
7 *
8 * This service owns checkpoint workflow rules and validation, while concrete
9 * storage/process details stay in lower-level services.
10 *
11 * @module CheckpointService
12 */
13import type {
14 ProviderGetCheckpointDiffInput,
15 ProviderGetCheckpointDiffResult,
16 ProviderListCheckpointsInput,
17 ProviderListCheckpointsResult,
18 ProviderRevertToCheckpointInput,
19 ProviderRevertToCheckpointResult,
20} from "@t3tools/contracts";
21import { ServiceMap } from "effect";
22import type { Effect } from "effect";
23
24import type { CheckpointServiceError } from "../Errors.ts";
25
26export interface InitializeCheckpointSessionInput {
27 readonly providerSessionId: string;
28 readonly cwd: string;
29}
30
31export interface CaptureCurrentTurnInput {
32 readonly providerSessionId: string;
33 readonly turnId?: string;
34 readonly status?: string;
35}
36
37export interface CheckpointServiceShape {
38 /**
39 * Initialize root checkpoint and metadata for a provider session.
40 */
41 readonly initializeForSession: (
42 input: InitializeCheckpointSessionInput,
43 ) => Effect.Effect<void, CheckpointServiceError>;
44
45 /**
46 * Capture the current thread turn into checkpoint refs/metadata.
47 */
48 readonly captureCurrentTurn: (input: CaptureCurrentTurnInput) => Effect.Effect<void, CheckpointServiceError>;
49
50 /**
51 * List checkpoints for a provider session.
52 */
53 readonly listCheckpoints: (
54 input: ProviderListCheckpointsInput,
55 ) => Effect.Effect<ProviderListCheckpointsResult, CheckpointServiceError>;
56
57 /**
58 * Diff two checkpoint turn counts for a provider session.
59 */
60 readonly getCheckpointDiff: (
61 input: ProviderGetCheckpointDiffInput,
62 ) => Effect.Effect<ProviderGetCheckpointDiffResult, CheckpointServiceError>;
63
64 /**
65 * Revert provider/filesystem state to a checkpoint.
66 */
67 readonly revertToCheckpoint: (
68 input: ProviderRevertToCheckpointInput,
69 ) => Effect.Effect<ProviderRevertToCheckpointResult, CheckpointServiceError>;
70}
71
72/**
73 * CheckpointService - Service tag for checkpoint orchestration.
74 */
75export class CheckpointService extends ServiceMap.Service<
76 CheckpointService,
77 CheckpointServiceShape
78>()("checkpointing/CheckpointService") {}
apps/server/src/checkpointing/Services/CheckpointStore.ts created+95
......@@ -0,0 +1,95 @@
1/**
2 * CheckpointStore - Repository interface for filesystem-backed workspace checkpoints.
3 *
4 * Owns hidden Git-ref checkpoint capture/restore and diff computation for a
5 * workspace thread timeline. It does not store user-facing checkpoint metadata
6 * and does not coordinate provider conversation rollback.
7 *
8 * Uses Effect `ServiceMap.Service` for dependency injection and exposes typed
9 * domain errors for checkpoint storage operations.
10 *
11 * @module CheckpointStore
12 */
13import { ServiceMap } from "effect";
14import type { Effect } from "effect";
15
16import type { CheckpointStoreError } from "../Errors.ts";
17
18export interface CaptureCheckpointInput {
19 readonly cwd: string;
20 readonly threadId: string;
21 readonly turnCount: number;
22}
23
24export interface EnsureRootCheckpointInput {
25 readonly cwd: string;
26 readonly threadId: string;
27}
28
29export interface RestoreCheckpointInput {
30 readonly cwd: string;
31 readonly threadId: string;
32 readonly turnCount: number;
33}
34
35export interface DiffCheckpointsInput {
36 readonly cwd: string;
37 readonly threadId: string;
38 readonly fromTurnCount: number;
39 readonly toTurnCount: number;
40}
41
42export interface PruneAfterTurnInput {
43 readonly cwd: string;
44 readonly threadId: string;
45 readonly maxTurnCount: number;
46}
47
48export interface CheckpointStoreShape {
49 /**
50 * Check whether cwd is inside a Git worktree.
51 */
52 readonly isGitRepository: (cwd: string) => Effect.Effect<boolean, CheckpointStoreError>;
53
54 /**
55 * Capture a checkpoint for a thread at a turn count.
56 */
57 readonly captureCheckpoint: (input: CaptureCheckpointInput) => Effect.Effect<void, CheckpointStoreError>;
58
59 /**
60 * Check whether a checkpoint ref exists.
61 */
62 readonly hasCheckpoint: (input: RestoreCheckpointInput) => Effect.Effect<boolean, CheckpointStoreError>;
63
64 /**
65 * Ensure a turn-0 checkpoint exists for a thread.
66 */
67 readonly ensureRootCheckpoint: (
68 input: EnsureRootCheckpointInput,
69 ) => Effect.Effect<boolean, CheckpointStoreError>;
70
71 /**
72 * Restore workspace/staging state to a checkpoint.
73 */
74 readonly restoreCheckpoint: (
75 input: RestoreCheckpointInput,
76 ) => Effect.Effect<boolean, CheckpointStoreError>;
77
78 /**
79 * Compute patch diff between two checkpoint turn counts.
80 */
81 readonly diffCheckpoints: (input: DiffCheckpointsInput) => Effect.Effect<string, CheckpointStoreError>;
82
83 /**
84 * Delete checkpoints newer than the provided turn count.
85 */
86 readonly pruneAfterTurn: (input: PruneAfterTurnInput) => Effect.Effect<void, CheckpointStoreError>;
87}
88
89/**
90 * CheckpointStore - Service tag for checkpoint persistence and restore operations.
91 */
92export class CheckpointStore extends ServiceMap.Service<
93 CheckpointStore,
94 CheckpointStoreShape
95>()("checkpointing/CheckpointStore") {}
apps/server/src/orchestration/Service.ts+6-2
......@@ -1,7 +1,11 @@
11/**
22 * OrchestrationEngineService - Service interface for orchestration command handling.
33 *
4 * Uses Effect `Context.Tag` for dependency injection. Command dispatch,
4 * Owns command validation/dispatch and in-memory read-model updates backed by
5 * `OrchestrationEventRepository` persistence. It does not own provider process
6 * management or transport concerns (e.g. websocket request parsing).
7 *
8 * Uses Effect `ServiceMap.Service` for dependency injection. Command dispatch,
59 * replay, and unknown-input decoding all return typed domain errors.
610 *
711 * @module OrchestrationEngineService
......@@ -79,7 +83,7 @@ export interface OrchestrationEngineShape {
7983}
8084
8185/**
82 * OrchestrationEngineService - Context tag for orchestration engine access.
86 * OrchestrationEngineService - Service tag for orchestration engine access.
8387 *
8488 * @example
8589 * ```ts
apps/server/src/persistence/Services/OrchestrationEvents.ts+5-2
......@@ -1,7 +1,10 @@
11/**
22 * OrchestrationEventRepository - Repository interface for orchestration events.
33 *
4 * Uses Effect `Context.Tag` for dependency injection and exposes typed
4 * Owns durable append/replay access to the orchestration event stream. It does
5 * not reduce events into read models or apply command validation rules.
6 *
7 * Uses Effect `ServiceMap.Service` for dependency injection and exposes typed
58 * persistence/decode errors for event append and replay operations.
69 *
710 * @module OrchestrationEventRepository
......@@ -44,7 +47,7 @@ export interface OrchestrationEventRepositoryShape {
4447}
4548
4649/**
47 * OrchestrationEventRepository - Context tag for orchestration event persistence.
50 * OrchestrationEventRepository - Service tag for orchestration event persistence.
4851 *
4952 * @example
5053 * ```ts
apps/server/src/persistence/Services/Projects.ts+5-2
......@@ -1,7 +1,10 @@
11/**
22 * ProjectRepository - Repository interface for project persistence.
33 *
4 * Uses Effect `Context.Tag` for dependency injection and returns typed
4 * Owns persisted project records (cwd, scripts, and cleanup of missing paths).
5 * It does not manage provider sessions, orchestration events, or websocket APIs.
6 *
7 * Uses Effect `ServiceMap.Service` for dependency injection and returns typed
58 * domain errors for all operations.
69 *
710 * @module ProjectRepository
......@@ -62,7 +65,7 @@ export interface ProjectRepositoryShape {
6265}
6366
6467/**
65 * ProjectRepository - Context tag for project persistence dependency injection.
68 * ProjectRepository - Service tag for project persistence dependency injection.
6669 *
6770 * @example
6871 * ```ts
apps/server/src/provider/Errors.ts+40-105
......@@ -1,163 +1,104 @@
11import { Schema } from "effect";
22
3/**
4 * CheckpointValidationError - Invalid checkpoint API input.
5 */
6export class CheckpointValidationError extends Schema.TaggedErrorClass<CheckpointValidationError>()(
7 "CheckpointValidationError",
8 {
9 operation: Schema.String,
10 issue: Schema.String,
11 cause: Schema.optional(Schema.Defect),
12 },
13) {
14 override get message(): string {
15 return `Checkpoint validation failed in ${this.operation}: ${this.issue}`;
16 }
17}
18
19/**
20 * CheckpointGitCommandError - Git command execution failed in checkpoint store.
21 */
22export class CheckpointGitCommandError extends Schema.TaggedErrorClass<CheckpointGitCommandError>()(
23 "CheckpointGitCommandError",
24 {
25 operation: Schema.String,
26 command: Schema.String,
27 cwd: Schema.String,
28 detail: Schema.String,
29 cause: Schema.optional(Schema.Defect),
30 },
31) {
32 override get message(): string {
33 return `Checkpoint git command failed in ${this.operation}: ${this.command} (${this.cwd}) - ${this.detail}`;
34 }
35}
36
37/**
38 * CheckpointUnavailableError - Expected checkpoint does not exist.
39 */
40export class CheckpointUnavailableError extends Schema.TaggedErrorClass<CheckpointUnavailableError>()(
41 "CheckpointUnavailableError",
42 {
43 threadId: Schema.String,
44 turnCount: Schema.Number,
45 detail: Schema.String,
46 cause: Schema.optional(Schema.Defect),
47 },
48) {
49 override get message(): string {
50 return `Checkpoint unavailable for thread ${this.threadId} turn ${this.turnCount}: ${this.detail}`;
51 }
52}
53
54/**
55 * CheckpointRepositoryError - Checkpointing unavailable for cwd/repository.
56 */
57export class CheckpointRepositoryError extends Schema.TaggedErrorClass<CheckpointRepositoryError>()(
58 "CheckpointRepositoryError",
59 {
60 cwd: Schema.String,
61 detail: Schema.String,
62 cause: Schema.optional(Schema.Defect),
63 },
64) {
65 override get message(): string {
66 return `Checkpoint repository error for ${this.cwd}: ${this.detail}`;
67 }
68}
3import type { CheckpointServiceError } from "../checkpointing/Errors.ts";
694
705/**
71 * CodexValidationError - Invalid Codex API input.
6 * ProviderAdapterValidationError - Invalid adapter API input.
727 */
73export class CodexValidationError extends Schema.TaggedErrorClass<CodexValidationError>()(
74 "CodexValidationError",
8export class ProviderAdapterValidationError extends Schema.TaggedErrorClass<ProviderAdapterValidationError>()(
9 "ProviderAdapterValidationError",
7510 {
11 provider: Schema.String,
7612 operation: Schema.String,
7713 issue: Schema.String,
7814 cause: Schema.optional(Schema.Defect),
7915 },
8016) {
8117 override get message(): string {
82 return `Codex validation failed in ${this.operation}: ${this.issue}`;
18 return `Provider adapter validation failed (${this.provider}) in ${this.operation}: ${this.issue}`;
8319 }
8420}
8521
8622/**
87 * CodexSessionNotFoundError - Session id is unknown.
23 * ProviderAdapterSessionNotFoundError - Adapter-owned session id is unknown.
8824 */
89export class CodexSessionNotFoundError extends Schema.TaggedErrorClass<CodexSessionNotFoundError>()(
90 "CodexSessionNotFoundError",
25export class ProviderAdapterSessionNotFoundError extends Schema.TaggedErrorClass<ProviderAdapterSessionNotFoundError>()(
26 "ProviderAdapterSessionNotFoundError",
9127 {
28 provider: Schema.String,
9229 sessionId: Schema.String,
9330 cause: Schema.optional(Schema.Defect),
9431 },
9532) {
9633 override get message(): string {
97 return `Unknown codex session: ${this.sessionId}`;
34 return `Unknown ${this.provider} adapter session: ${this.sessionId}`;
9835 }
9936}
10037
10138/**
102 * CodexSessionClosedError - Session exists but is closed.
39 * ProviderAdapterSessionClosedError - Adapter session exists but is closed.
10340 */
104export class CodexSessionClosedError extends Schema.TaggedErrorClass<CodexSessionClosedError>()(
105 "CodexSessionClosedError",
41export class ProviderAdapterSessionClosedError extends Schema.TaggedErrorClass<ProviderAdapterSessionClosedError>()(
42 "ProviderAdapterSessionClosedError",
10643 {
44 provider: Schema.String,
10745 sessionId: Schema.String,
10846 cause: Schema.optional(Schema.Defect),
10947 },
11048) {
11149 override get message(): string {
112 return `Codex session is closed: ${this.sessionId}`;
50 return `${this.provider} adapter session is closed: ${this.sessionId}`;
11351 }
11452}
11553
11654/**
117 * CodexProtocolError - Invalid/unexpected JSON-RPC payload from app-server.
55 * ProviderAdapterProtocolError - Invalid/unexpected provider protocol payload.
11856 */
119export class CodexProtocolError extends Schema.TaggedErrorClass<CodexProtocolError>()(
120 "CodexProtocolError",
57export class ProviderAdapterProtocolError extends Schema.TaggedErrorClass<ProviderAdapterProtocolError>()(
58 "ProviderAdapterProtocolError",
12159 {
60 provider: Schema.String,
12261 operation: Schema.String,
12362 detail: Schema.String,
12463 cause: Schema.optional(Schema.Defect),
12564 },
12665) {
12766 override get message(): string {
128 return `Codex protocol error in ${this.operation}: ${this.detail}`;
67 return `Provider adapter protocol error (${this.provider}) in ${this.operation}: ${this.detail}`;
12968 }
13069}
13170
13271/**
133 * CodexRequestError - JSON-RPC request failed or timed out.
72 * ProviderAdapterRequestError - Provider protocol request failed or timed out.
13473 */
135export class CodexRequestError extends Schema.TaggedErrorClass<CodexRequestError>()(
136 "CodexRequestError",
74export class ProviderAdapterRequestError extends Schema.TaggedErrorClass<ProviderAdapterRequestError>()(
75 "ProviderAdapterRequestError",
13776 {
77 provider: Schema.String,
13878 method: Schema.String,
13979 detail: Schema.String,
14080 cause: Schema.optional(Schema.Defect),
14181 },
14282) {
14383 override get message(): string {
144 return `Codex request failed for ${this.method}: ${this.detail}`;
84 return `Provider adapter request failed (${this.provider}) for ${this.method}: ${this.detail}`;
14585 }
14686}
14787
14888/**
149 * CodexProcessError - codex app-server process lifecycle failure.
89 * ProviderAdapterProcessError - Provider process lifecycle failure.
15090 */
151export class CodexProcessError extends Schema.TaggedErrorClass<CodexProcessError>()(
152 "CodexProcessError",
91export class ProviderAdapterProcessError extends Schema.TaggedErrorClass<ProviderAdapterProcessError>()(
92 "ProviderAdapterProcessError",
15393 {
94 provider: Schema.String,
15495 sessionId: Schema.String,
15596 detail: Schema.String,
15697 cause: Schema.optional(Schema.Defect),
15798 },
15899) {
159100 override get message(): string {
160 return `Codex process error for session ${this.sessionId}: ${this.detail}`;
101 return `Provider adapter process error (${this.provider}) for session ${this.sessionId}: ${this.detail}`;
161102 }
162103}
163104
......@@ -257,19 +198,13 @@ export class ProviderFilesystemError extends Schema.TaggedErrorClass<ProviderFil
257198 }
258199}
259200
260export type CheckpointStoreError =
261 | CheckpointValidationError
262 | CheckpointGitCommandError
263 | CheckpointUnavailableError
264 | CheckpointRepositoryError;
265
266export type CodexServiceError =
267 | CodexValidationError
268 | CodexSessionNotFoundError
269 | CodexSessionClosedError
270 | CodexProtocolError
271 | CodexRequestError
272 | CodexProcessError;
201export type ProviderAdapterError =
202 | ProviderAdapterValidationError
203 | ProviderAdapterSessionNotFoundError
204 | ProviderAdapterSessionClosedError
205 | ProviderAdapterProtocolError
206 | ProviderAdapterRequestError
207 | ProviderAdapterProcessError;
273208
274209export type ProviderServiceError =
275210 | ProviderValidationError
......@@ -278,5 +213,5 @@ export type ProviderServiceError =
278213 | ProviderCheckpointUnavailableError
279214 | ProviderCheckpointRangeError
280215 | ProviderFilesystemError
281 | CodexServiceError
282 | CheckpointStoreError;
216 | ProviderAdapterError
217 | CheckpointServiceError;
apps/server/src/provider/Services/CheckpointStore.ts deleted-91
......@@ -1,91 +0,0 @@
1/**
2 * CheckpointStore - Repository interface for filesystem-backed workspace checkpoints.
3 *
4 * Uses Effect `ServiceMap.Service` for dependency injection and exposes typed
5 * domain errors for checkpoint lifecycle operations.
6 *
7 * @module CheckpointStore
8 */
9import { ServiceMap } from "effect";
10import type { Effect } from "effect";
11
12import type { CheckpointStoreError } from "../Errors.ts";
13
14export interface CaptureCheckpointInput {
15 readonly cwd: string;
16 readonly threadId: string;
17 readonly turnCount: number;
18}
19
20export interface EnsureRootCheckpointInput {
21 readonly cwd: string;
22 readonly threadId: string;
23}
24
25export interface RestoreCheckpointInput {
26 readonly cwd: string;
27 readonly threadId: string;
28 readonly turnCount: number;
29}
30
31export interface DiffCheckpointsInput {
32 readonly cwd: string;
33 readonly threadId: string;
34 readonly fromTurnCount: number;
35 readonly toTurnCount: number;
36}
37
38export interface PruneAfterTurnInput {
39 readonly cwd: string;
40 readonly threadId: string;
41 readonly maxTurnCount: number;
42}
43
44export interface CheckpointStoreShape {
45 /**
46 * Check whether cwd is inside a Git worktree.
47 */
48 readonly isGitRepository: (cwd: string) => Effect.Effect<boolean, CheckpointStoreError>;
49
50 /**
51 * Capture a checkpoint for a thread at a turn count.
52 */
53 readonly captureCheckpoint: (input: CaptureCheckpointInput) => Effect.Effect<void, CheckpointStoreError>;
54
55 /**
56 * Check whether a checkpoint ref exists.
57 */
58 readonly hasCheckpoint: (input: RestoreCheckpointInput) => Effect.Effect<boolean, CheckpointStoreError>;
59
60 /**
61 * Ensure a turn-0 checkpoint exists for a thread.
62 */
63 readonly ensureRootCheckpoint: (
64 input: EnsureRootCheckpointInput,
65 ) => Effect.Effect<boolean, CheckpointStoreError>;
66
67 /**
68 * Restore workspace/staging state to a checkpoint.
69 */
70 readonly restoreCheckpoint: (
71 input: RestoreCheckpointInput,
72 ) => Effect.Effect<boolean, CheckpointStoreError>;
73
74 /**
75 * Compute patch diff between two checkpoint turn counts.
76 */
77 readonly diffCheckpoints: (input: DiffCheckpointsInput) => Effect.Effect<string, CheckpointStoreError>;
78
79 /**
80 * Delete checkpoints newer than the provided turn count.
81 */
82 readonly pruneAfterTurn: (input: PruneAfterTurnInput) => Effect.Effect<void, CheckpointStoreError>;
83}
84
85/**
86 * CheckpointStore - Context tag for checkpoint persistence and restore operations.
87 */
88export class CheckpointStore extends ServiceMap.Service<
89 CheckpointStore,
90 CheckpointStoreShape
91>()("provider/CheckpointStore") {}
apps/server/src/provider/Services/Codex.ts deleted-110
......@@ -1,110 +0,0 @@
1/**
2 * CodexService - Service interface for codex app-server session lifecycle and RPC.
3 *
4 * Uses Effect `ServiceMap.Service` for dependency injection and returns typed
5 * domain errors for process, protocol, and session operations.
6 *
7 * @module CodexService
8 */
9import type {
10 ProviderApprovalDecision,
11 ProviderEvent,
12 ProviderSendTurnInput,
13 ProviderSession,
14 ProviderSessionStartInput,
15 ProviderTurnStartResult,
16} from "@t3tools/contracts";
17import { ServiceMap } from "effect";
18import type { Effect } from "effect";
19
20import type { CodexServiceError } from "../Errors.ts";
21
22export interface CodexThreadTurnSnapshot {
23 readonly id: string;
24 readonly items: ReadonlyArray<unknown>;
25}
26
27export interface CodexThreadSnapshot {
28 readonly threadId: string;
29 readonly turns: ReadonlyArray<CodexThreadTurnSnapshot>;
30}
31
32export interface CodexServiceShape {
33 /**
34 * Start a codex app-server-backed provider session.
35 */
36 readonly startSession: (
37 input: ProviderSessionStartInput,
38 ) => Effect.Effect<ProviderSession, CodexServiceError>;
39
40 /**
41 * Send a new turn to an active codex session.
42 */
43 readonly sendTurn: (
44 input: ProviderSendTurnInput,
45 ) => Effect.Effect<ProviderTurnStartResult, CodexServiceError>;
46
47 /**
48 * Interrupt an active turn.
49 */
50 readonly interruptTurn: (
51 sessionId: string,
52 turnId?: string,
53 ) => Effect.Effect<void, CodexServiceError>;
54
55 /**
56 * Read a thread snapshot (including turns).
57 */
58 readonly readThread: (sessionId: string) => Effect.Effect<CodexThreadSnapshot, CodexServiceError>;
59
60 /**
61 * Roll back a thread by N turns.
62 */
63 readonly rollbackThread: (
64 sessionId: string,
65 numTurns: number,
66 ) => Effect.Effect<CodexThreadSnapshot, CodexServiceError>;
67
68 /**
69 * Respond to an interactive approval request from codex app-server.
70 */
71 readonly respondToRequest: (
72 sessionId: string,
73 requestId: string,
74 decision: ProviderApprovalDecision,
75 ) => Effect.Effect<void, CodexServiceError>;
76
77 /**
78 * Stop one session.
79 */
80 readonly stopSession: (sessionId: string) => Effect.Effect<void, CodexServiceError>;
81
82 /**
83 * List active sessions.
84 */
85 readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>;
86
87 /**
88 * Check whether session id is currently active.
89 */
90 readonly hasSession: (sessionId: string) => Effect.Effect<boolean>;
91
92 /**
93 * Stop all sessions.
94 */
95 readonly stopAll: () => Effect.Effect<void, CodexServiceError>;
96
97 /**
98 * Subscribe to provider events emitted by codex app-server.
99 */
100 readonly subscribeToEvents: (
101 callback: (event: ProviderEvent) => void,
102 ) => Effect.Effect<() => void, CodexServiceError>;
103}
104
105/**
106 * CodexService - Context tag for codex session and RPC operations.
107 */
108export class CodexService extends ServiceMap.Service<CodexService, CodexServiceShape>()(
109 "provider/CodexService",
110) {}
apps/server/src/provider/Services/CodexAdapter.ts created+27
......@@ -0,0 +1,27 @@
1/**
2 * CodexAdapter - Codex implementation of the generic provider adapter contract.
3 *
4 * This service owns Codex app-server process / JSON-RPC semantics and emits
5 * Codex provider events. It does not perform cross-provider routing, shared
6 * event fan-out, or checkpoint orchestration.
7 *
8 * Uses Effect `ServiceMap.Service` for dependency injection and returns the
9 * shared provider-adapter error channel with `provider: "codex"` context.
10 *
11 * @module CodexAdapter
12 */
13import { ServiceMap } from "effect";
14
15import type { ProviderAdapterError } from "../Errors.ts";
16import type { ProviderAdapterShape } from "./ProviderAdapter.ts";
17
18export interface CodexAdapterShape extends ProviderAdapterShape<ProviderAdapterError> {
19 readonly provider: "codex";
20}
21
22/**
23 * CodexAdapter - Service tag for Codex provider adapter operations.
24 */
25export class CodexAdapter extends ServiceMap.Service<CodexAdapter, CodexAdapterShape>()(
26 "provider/CodexAdapter",
27) {}
apps/server/src/provider/Services/Provider.ts deleted-113
......@@ -1,113 +0,0 @@
1/**
2 * ProviderService - Service interface for provider sessions, turns, and checkpoints.
3 *
4 * Uses Effect `ServiceMap.Service` for dependency injection and returns typed
5 * domain errors for validation, session, codex, and checkpoint workflows.
6 *
7 * @module ProviderService
8 */
9import type {
10 ProviderEvent,
11 ProviderGetCheckpointDiffInput,
12 ProviderGetCheckpointDiffResult,
13 ProviderInterruptTurnInput,
14 ProviderListCheckpointsInput,
15 ProviderListCheckpointsResult,
16 ProviderRespondToRequestInput,
17 ProviderRevertToCheckpointInput,
18 ProviderRevertToCheckpointResult,
19 ProviderSendTurnInput,
20 ProviderSession,
21 ProviderSessionStartInput,
22 ProviderStopSessionInput,
23 ProviderTurnStartResult,
24} from "@t3tools/contracts";
25import { ServiceMap } from "effect";
26import type { Effect } from "effect";
27
28import type { ProviderServiceError } from "../Errors.ts";
29
30export interface ProviderServiceShape {
31 /**
32 * Start a provider session.
33 */
34 readonly startSession: (
35 input: ProviderSessionStartInput,
36 ) => Effect.Effect<ProviderSession, ProviderServiceError>;
37
38 /**
39 * Send a provider turn.
40 */
41 readonly sendTurn: (
42 input: ProviderSendTurnInput,
43 ) => Effect.Effect<ProviderTurnStartResult, ProviderServiceError>;
44
45 /**
46 * Interrupt a running provider turn.
47 */
48 readonly interruptTurn: (
49 input: ProviderInterruptTurnInput,
50 ) => Effect.Effect<void, ProviderServiceError>;
51
52 /**
53 * Respond to a provider approval request.
54 */
55 readonly respondToRequest: (
56 input: ProviderRespondToRequestInput,
57 ) => Effect.Effect<void, ProviderServiceError>;
58
59 /**
60 * Stop a provider session.
61 */
62 readonly stopSession: (input: ProviderStopSessionInput) => Effect.Effect<void, ProviderServiceError>;
63
64 /**
65 * List active provider sessions.
66 */
67 readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>;
68
69 /**
70 * List checkpoints for a provider session.
71 */
72 readonly listCheckpoints: (
73 input: ProviderListCheckpointsInput,
74 ) => Effect.Effect<ProviderListCheckpointsResult, ProviderServiceError>;
75
76 /**
77 * Diff two checkpoints for a provider session.
78 */
79 readonly getCheckpointDiff: (
80 input: ProviderGetCheckpointDiffInput,
81 ) => Effect.Effect<ProviderGetCheckpointDiffResult, ProviderServiceError>;
82
83 /**
84 * Revert a provider session to a checkpoint.
85 */
86 readonly revertToCheckpoint: (
87 input: ProviderRevertToCheckpointInput,
88 ) => Effect.Effect<ProviderRevertToCheckpointResult, ProviderServiceError>;
89
90 /**
91 * Stop all provider sessions.
92 */
93 readonly stopAll: () => Effect.Effect<void, ProviderServiceError>;
94
95 /**
96 * Dispose provider resources (logs/listeners).
97 */
98 readonly dispose: () => Effect.Effect<void, ProviderServiceError>;
99
100 /**
101 * Subscribe to provider event stream.
102 */
103 readonly subscribeToEvents: (
104 callback: (event: ProviderEvent) => void,
105 ) => Effect.Effect<() => void, ProviderServiceError>;
106}
107
108/**
109 * ProviderService - Context tag for provider orchestration.
110 */
111export class ProviderService extends ServiceMap.Service<ProviderService, ProviderServiceShape>()(
112 "provider/ProviderService",
113) {}
apps/server/src/provider/Services/ProviderAdapter.ts created+104
......@@ -0,0 +1,104 @@
1/**
2 * ProviderAdapter - Provider-specific runtime adapter contract.
3 *
4 * Defines the provider-native session/protocol operations that `ProviderService`
5 * routes to after resolving the target provider. Implementations should focus
6 * on provider behavior only and avoid cross-provider orchestration concerns.
7 *
8 * @module ProviderAdapter
9 */
10import type {
11 ProviderApprovalDecision,
12 ProviderEvent,
13 ProviderKind,
14 ProviderSendTurnInput,
15 ProviderSession,
16 ProviderSessionStartInput,
17 ProviderTurnStartResult,
18} from "@t3tools/contracts";
19import type { Effect } from "effect";
20
21export interface ProviderThreadTurnSnapshot {
22 readonly id: string;
23 readonly items: ReadonlyArray<unknown>;
24}
25
26export interface ProviderThreadSnapshot {
27 readonly threadId: string;
28 readonly turns: ReadonlyArray<ProviderThreadTurnSnapshot>;
29}
30
31export interface ProviderAdapterShape<TError> {
32 /**
33 * Provider kind implemented by this adapter.
34 */
35 readonly provider: ProviderKind;
36
37 /**
38 * Start a provider-backed session.
39 */
40 readonly startSession: (
41 input: ProviderSessionStartInput,
42 ) => Effect.Effect<ProviderSession, TError>;
43
44 /**
45 * Send a turn to an active provider session.
46 */
47 readonly sendTurn: (
48 input: ProviderSendTurnInput,
49 ) => Effect.Effect<ProviderTurnStartResult, TError>;
50
51 /**
52 * Interrupt an active turn.
53 */
54 readonly interruptTurn: (sessionId: string, turnId?: string) => Effect.Effect<void, TError>;
55
56 /**
57 * Respond to an interactive approval request.
58 */
59 readonly respondToRequest: (
60 sessionId: string,
61 requestId: string,
62 decision: ProviderApprovalDecision,
63 ) => Effect.Effect<void, TError>;
64
65 /**
66 * Stop one provider session.
67 */
68 readonly stopSession: (sessionId: string) => Effect.Effect<void, TError>;
69
70 /**
71 * List currently active provider sessions for this adapter.
72 */
73 readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>;
74
75 /**
76 * Check whether this adapter owns an active session id.
77 */
78 readonly hasSession: (sessionId: string) => Effect.Effect<boolean>;
79
80 /**
81 * Read a provider thread snapshot.
82 */
83 readonly readThread: (sessionId: string) => Effect.Effect<ProviderThreadSnapshot, TError>;
84
85 /**
86 * Roll back a provider thread by N turns.
87 */
88 readonly rollbackThread: (
89 sessionId: string,
90 numTurns: number,
91 ) => Effect.Effect<ProviderThreadSnapshot, TError>;
92
93 /**
94 * Stop all sessions owned by this adapter.
95 */
96 readonly stopAll: () => Effect.Effect<void, TError>;
97
98 /**
99 * Subscribe to provider events emitted by this adapter.
100 */
101 readonly subscribeToEvents: (
102 callback: (event: ProviderEvent) => void,
103 ) => Effect.Effect<() => void, TError>;
104}
apps/server/src/provider/Services/ProviderAdapterRegistry.ts created+37
......@@ -0,0 +1,37 @@
1/**
2 * ProviderAdapterRegistry - Lookup boundary for provider adapter implementations.
3 *
4 * Maps a provider kind to the concrete adapter service (Codex, Claude, etc).
5 * It does not own session lifecycle or routing rules; `ProviderService` uses
6 * this registry together with `ProviderSessionDirectory`.
7 *
8 * @module ProviderAdapterRegistry
9 */
10import type { ProviderKind } from "@t3tools/contracts";
11import { ServiceMap } from "effect";
12import type { Effect } from "effect";
13
14import type { ProviderAdapterError, ProviderUnsupportedError } from "../Errors.ts";
15import type { ProviderAdapterShape } from "./ProviderAdapter.ts";
16
17export interface ProviderAdapterRegistryShape {
18 /**
19 * Resolve the adapter for a provider kind.
20 */
21 readonly getByProvider: (
22 provider: ProviderKind,
23 ) => Effect.Effect<ProviderAdapterShape<ProviderAdapterError>, ProviderUnsupportedError>;
24
25 /**
26 * List provider kinds currently registered.
27 */
28 readonly listProviders: () => Effect.Effect<ReadonlyArray<ProviderKind>>;
29}
30
31/**
32 * ProviderAdapterRegistry - Service tag for provider adapter lookup.
33 */
34export class ProviderAdapterRegistry extends ServiceMap.Service<
35 ProviderAdapterRegistry,
36 ProviderAdapterRegistryShape
37>()("provider/ProviderAdapterRegistry") {}
apps/server/src/provider/Services/ProviderService.ts created+117
......@@ -0,0 +1,117 @@
1/**
2 * ProviderService - Service interface for provider sessions, turns, and checkpoints.
3 *
4 * Acts as the cross-provider facade used by transports (WebSocket/RPC). It
5 * resolves provider adapters through `ProviderAdapterRegistry`, routes
6 * session-scoped calls via `ProviderSessionDirectory`, and exposes one unified
7 * provider event stream to callers.
8 *
9 * Uses Effect `ServiceMap.Service` for dependency injection and returns typed
10 * domain errors for validation, session, codex, and checkpoint workflows.
11 *
12 * @module ProviderService
13 */
14import type {
15 ProviderEvent,
16 ProviderGetCheckpointDiffInput,
17 ProviderGetCheckpointDiffResult,
18 ProviderInterruptTurnInput,
19 ProviderListCheckpointsInput,
20 ProviderListCheckpointsResult,
21 ProviderRespondToRequestInput,
22 ProviderRevertToCheckpointInput,
23 ProviderRevertToCheckpointResult,
24 ProviderSendTurnInput,
25 ProviderSession,
26 ProviderSessionStartInput,
27 ProviderStopSessionInput,
28 ProviderTurnStartResult,
29} from "@t3tools/contracts";
30import { ServiceMap } from "effect";
31import type { Effect } from "effect";
32
33import type { ProviderServiceError } from "../Errors.ts";
34
35export interface ProviderServiceShape {
36 /**
37 * Start a provider session.
38 */
39 readonly startSession: (
40 input: ProviderSessionStartInput,
41 ) => Effect.Effect<ProviderSession, ProviderServiceError>;
42
43 /**
44 * Send a provider turn.
45 */
46 readonly sendTurn: (
47 input: ProviderSendTurnInput,
48 ) => Effect.Effect<ProviderTurnStartResult, ProviderServiceError>;
49
50 /**
51 * Interrupt a running provider turn.
52 */
53 readonly interruptTurn: (
54 input: ProviderInterruptTurnInput,
55 ) => Effect.Effect<void, ProviderServiceError>;
56
57 /**
58 * Respond to a provider approval request.
59 */
60 readonly respondToRequest: (
61 input: ProviderRespondToRequestInput,
62 ) => Effect.Effect<void, ProviderServiceError>;
63
64 /**
65 * Stop a provider session.
66 */
67 readonly stopSession: (
68 input: ProviderStopSessionInput,
69 ) => Effect.Effect<void, ProviderServiceError>;
70
71 /**
72 * List active provider sessions.
73 */
74 readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>;
75
76 /**
77 * List checkpoints for a provider session.
78 */
79 readonly listCheckpoints: (
80 input: ProviderListCheckpointsInput,
81 ) => Effect.Effect<ProviderListCheckpointsResult, ProviderServiceError>;
82
83 /**
84 * Diff two checkpoints for a provider session.
85 */
86 readonly getCheckpointDiff: (
87 input: ProviderGetCheckpointDiffInput,
88 ) => Effect.Effect<ProviderGetCheckpointDiffResult, ProviderServiceError>;
89
90 /**
91 * Revert a provider session to a checkpoint.
92 */
93 readonly revertToCheckpoint: (
94 input: ProviderRevertToCheckpointInput,
95 ) => Effect.Effect<ProviderRevertToCheckpointResult, ProviderServiceError>;
96
97 /**
98 * Stop all provider sessions.
99 */
100 readonly stopAll: () => Effect.Effect<void, ProviderServiceError>;
101
102 /**
103 * Subscribe to provider event stream.
104 *
105 * Fan-out is owned by ProviderService (not by a standalone event-bus service).
106 */
107 readonly subscribeToEvents: (
108 callback: (event: ProviderEvent) => void,
109 ) => Effect.Effect<() => void, ProviderServiceError>;
110}
111
112/**
113 * ProviderService - Service tag for provider orchestration.
114 */
115export class ProviderService extends ServiceMap.Service<ProviderService, ProviderServiceShape>()(
116 "provider/ProviderService",
117) {}
apps/server/src/provider/Services/ProviderSessionDirectory.ts created+61
......@@ -0,0 +1,61 @@
1/**
2 * ProviderSessionDirectory - Session ownership index across provider adapters.
3 *
4 * Tracks which provider owns each `sessionId` so `ProviderService` can route
5 * session-scoped calls to the correct adapter. It is metadata only and does not
6 * perform provider RPC or checkpoint operations.
7 *
8 * @module ProviderSessionDirectory
9 */
10import type { ProviderKind } from "@t3tools/contracts";
11import { Option, ServiceMap } from "effect";
12import type { Effect } from "effect";
13
14import type { ProviderSessionNotFoundError, ProviderValidationError } from "../Errors.ts";
15
16export interface ProviderSessionBinding {
17 readonly sessionId: string;
18 readonly provider: ProviderKind;
19 readonly threadId?: string;
20}
21
22export interface ProviderSessionDirectoryShape {
23 /**
24 * Record or update ownership for one provider session.
25 */
26 readonly upsert: (
27 binding: ProviderSessionBinding,
28 ) => Effect.Effect<void, ProviderValidationError>;
29
30 /**
31 * Resolve the provider owner for a session id.
32 */
33 readonly getProvider: (
34 sessionId: string,
35 ) => Effect.Effect<ProviderKind, ProviderSessionNotFoundError>;
36
37 /**
38 * Resolve the tracked thread id for a session, if known.
39 */
40 readonly getThreadId: (
41 sessionId: string,
42 ) => Effect.Effect<Option.Option<string>, ProviderSessionNotFoundError>;
43
44 /**
45 * Remove a session binding.
46 */
47 readonly remove: (sessionId: string) => Effect.Effect<void>;
48
49 /**
50 * List tracked session ids.
51 */
52 readonly listSessionIds: () => Effect.Effect<ReadonlyArray<string>>;
53}
54
55/**
56 * ProviderSessionDirectory - Service tag for session ownership lookup.
57 */
58export class ProviderSessionDirectory extends ServiceMap.Service<
59 ProviderSessionDirectory,
60 ProviderSessionDirectoryShape
61>()("provider/ProviderSessionDirectory") {}
apps/server/src/provider/contracts.test.ts+149-38
......@@ -1,22 +1,31 @@
11import { describe, expect, it } from "vitest";
22
3import { Effect, Layer, ManagedRuntime } from "effect";
3import { Effect, Layer, ManagedRuntime, Option } from "effect";
44
55import {
6 CheckpointValidationError,
7 CodexValidationError,
6 ProviderAdapterValidationError,
87 ProviderFilesystemError,
8 ProviderSessionNotFoundError,
99 ProviderUnsupportedError,
10 type CheckpointStoreError,
11 type CodexServiceError,
10 type ProviderAdapterError,
1211 type ProviderServiceError,
1312} from "./Errors.ts";
14import { CheckpointStore } from "./Services/CheckpointStore.ts";
15import { CodexService } from "./Services/Codex.ts";
16import { ProviderService } from "./Services/Provider.ts";
13import {
14 CheckpointValidationError,
15 type CheckpointCatalogError,
16 type CheckpointServiceError,
17 type CheckpointStoreError,
18} from "../checkpointing/Errors.ts";
19import { CheckpointCatalog } from "../checkpointing/Services/CheckpointCatalog.ts";
20import { CheckpointService } from "../checkpointing/Services/CheckpointService.ts";
21import { CheckpointStore } from "../checkpointing/Services/CheckpointStore.ts";
22import { CodexAdapter } from "./Services/CodexAdapter.ts";
23import { ProviderAdapterRegistry } from "./Services/ProviderAdapterRegistry.ts";
24import { ProviderService } from "./Services/ProviderService.ts";
25import { ProviderSessionDirectory } from "./Services/ProviderSessionDirectory.ts";
1726
18describe("provider service contracts", () => {
19 it("resolves provider, codex, and checkpoint services from ServiceMap tags", async () => {
27describe("provider/checkpoint service contracts", () => {
28 it("resolves all service tags from ServiceMap", async () => {
2029 const providerLive: typeof ProviderService.Service = {
2130 startSession: () =>
2231 Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })),
......@@ -35,34 +44,58 @@ describe("provider service contracts", () => {
3544 revertToCheckpoint: () =>
3645 Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })),
3746 stopAll: () => Effect.void,
38 dispose: () => Effect.void,
3947 subscribeToEvents: () => Effect.succeed(() => undefined),
4048 };
4149
42 const codexLive: typeof CodexService.Service = {
50 const codexLive: typeof CodexAdapter.Service = {
51 provider: "codex",
4352 startSession: () =>
44 Effect.fail<CodexServiceError>(
45 new CodexValidationError({ operation: "startSession", issue: "not implemented" }),
53 Effect.fail<ProviderAdapterError>(
54 new ProviderAdapterValidationError({
55 provider: "codex",
56 operation: "startSession",
57 issue: "not implemented",
58 }),
4659 ),
4760 sendTurn: () =>
48 Effect.fail<CodexServiceError>(
49 new CodexValidationError({ operation: "sendTurn", issue: "not implemented" }),
61 Effect.fail<ProviderAdapterError>(
62 new ProviderAdapterValidationError({
63 provider: "codex",
64 operation: "sendTurn",
65 issue: "not implemented",
66 }),
5067 ),
5168 interruptTurn: () =>
52 Effect.fail<CodexServiceError>(
53 new CodexValidationError({ operation: "interruptTurn", issue: "not implemented" }),
69 Effect.fail<ProviderAdapterError>(
70 new ProviderAdapterValidationError({
71 provider: "codex",
72 operation: "interruptTurn",
73 issue: "not implemented",
74 }),
5475 ),
5576 readThread: () =>
56 Effect.fail<CodexServiceError>(
57 new CodexValidationError({ operation: "readThread", issue: "not implemented" }),
77 Effect.fail<ProviderAdapterError>(
78 new ProviderAdapterValidationError({
79 provider: "codex",
80 operation: "readThread",
81 issue: "not implemented",
82 }),
5883 ),
5984 rollbackThread: () =>
60 Effect.fail<CodexServiceError>(
61 new CodexValidationError({ operation: "rollbackThread", issue: "not implemented" }),
85 Effect.fail<ProviderAdapterError>(
86 new ProviderAdapterValidationError({
87 provider: "codex",
88 operation: "rollbackThread",
89 issue: "not implemented",
90 }),
6291 ),
6392 respondToRequest: () =>
64 Effect.fail<CodexServiceError>(
65 new CodexValidationError({ operation: "respondToRequest", issue: "not implemented" }),
93 Effect.fail<ProviderAdapterError>(
94 new ProviderAdapterValidationError({
95 provider: "codex",
96 operation: "respondToRequest",
97 issue: "not implemented",
98 }),
6699 ),
67100 stopSession: () => Effect.void,
68101 listSessions: () => Effect.succeed([]),
......@@ -71,7 +104,7 @@ describe("provider service contracts", () => {
71104 subscribeToEvents: () => Effect.succeed(() => undefined),
72105 };
73106
74 const checkpointLive: typeof CheckpointStore.Service = {
107 const checkpointStoreLive: typeof CheckpointStore.Service = {
75108 isGitRepository: () => Effect.succeed(false),
76109 captureCheckpoint: () => Effect.void,
77110 hasCheckpoint: () => Effect.succeed(false),
......@@ -87,39 +120,117 @@ describe("provider service contracts", () => {
87120 pruneAfterTurn: () => Effect.void,
88121 };
89122
123 const checkpointCatalogLive: typeof CheckpointCatalog.Service = {
124 upsertCheckpoint: () => Effect.void,
125 listCheckpoints: () => Effect.succeed([]),
126 getCheckpoint: () => Effect.succeed(Option.none()),
127 deleteAfterTurn: () => Effect.void,
128 deleteAllForSession: () => Effect.void,
129 };
130
131 const checkpointServiceLive: typeof CheckpointService.Service = {
132 initializeForSession: () => Effect.void,
133 captureCurrentTurn: () => Effect.void,
134 listCheckpoints: () =>
135 Effect.fail<CheckpointServiceError>(
136 new CheckpointValidationError({ operation: "listCheckpoints", issue: "not implemented" }),
137 ),
138 getCheckpointDiff: () =>
139 Effect.fail<CheckpointServiceError>(
140 new CheckpointValidationError({
141 operation: "getCheckpointDiff",
142 issue: "not implemented",
143 }),
144 ),
145 revertToCheckpoint: () =>
146 Effect.fail<CheckpointServiceError>(
147 new CheckpointValidationError({
148 operation: "revertToCheckpoint",
149 issue: "not implemented",
150 }),
151 ),
152 };
153
154 const providerAdapterRegistryLive: typeof ProviderAdapterRegistry.Service = {
155 getByProvider: (provider) =>
156 provider === "codex"
157 ? Effect.succeed(codexLive)
158 : Effect.fail<ProviderUnsupportedError>(new ProviderUnsupportedError({ provider })),
159 listProviders: () => Effect.succeed(["codex"]),
160 };
161
162 const providerSessionDirectoryLive: typeof ProviderSessionDirectory.Service = {
163 upsert: () => Effect.void,
164 getProvider: (sessionId) =>
165 Effect.fail<ProviderSessionNotFoundError>(new ProviderSessionNotFoundError({ sessionId })),
166 getThreadId: (sessionId) =>
167 Effect.fail<ProviderSessionNotFoundError>(new ProviderSessionNotFoundError({ sessionId })),
168 remove: () => Effect.void,
169 listSessionIds: () => Effect.succeed([]),
170 };
171
90172 const runtime = ManagedRuntime.make(
91173 Layer.mergeAll(
92174 Layer.succeed(ProviderService, providerLive),
93 Layer.succeed(CodexService, codexLive),
94 Layer.succeed(CheckpointStore, checkpointLive),
175 Layer.succeed(CodexAdapter, codexLive),
176 Layer.succeed(CheckpointStore, checkpointStoreLive),
177 Layer.succeed(CheckpointCatalog, checkpointCatalogLive),
178 Layer.succeed(CheckpointService, checkpointServiceLive),
179 Layer.succeed(ProviderAdapterRegistry, providerAdapterRegistryLive),
180 Layer.succeed(ProviderSessionDirectory, providerSessionDirectoryLive),
95181 ),
96182 );
97183
98 const [provider, codex, checkpoint] = await Promise.all([
184 const [
185 provider,
186 codex,
187 checkpointStore,
188 checkpointCatalog,
189 checkpointService,
190 providerAdapterRegistry,
191 providerSessionDirectory,
192 ] = await Promise.all([
99193 runtime.runPromise(Effect.service(ProviderService)),
100 runtime.runPromise(Effect.service(CodexService)),
194 runtime.runPromise(Effect.service(CodexAdapter)),
101195 runtime.runPromise(Effect.service(CheckpointStore)),
196 runtime.runPromise(Effect.service(CheckpointCatalog)),
197 runtime.runPromise(Effect.service(CheckpointService)),
198 runtime.runPromise(Effect.service(ProviderAdapterRegistry)),
199 runtime.runPromise(Effect.service(ProviderSessionDirectory)),
102200 ]);
103201
104202 expect(provider).toBe(providerLive);
105203 expect(codex).toBe(codexLive);
106 expect(checkpoint).toBe(checkpointLive);
204 expect(checkpointStore).toBe(checkpointStoreLive);
205 expect(checkpointCatalog).toBe(checkpointCatalogLive);
206 expect(checkpointService).toBe(checkpointServiceLive);
207 expect(providerAdapterRegistry).toBe(providerAdapterRegistryLive);
208 expect(providerSessionDirectory).toBe(providerSessionDirectoryLive);
107209
108210 await runtime.dispose();
109211 });
110212});
111213
112describe("provider error taxonomy", () => {
113 it("keeps defect cause and message on tagged errors", () => {
114 const cause = new Error("filesystem failed");
115 const error = new ProviderFilesystemError({
214describe("provider/checkpoint errors", () => {
215 it("preserves cause and message on tagged errors", () => {
216 const fsCause = new Error("filesystem failed");
217 const fsError = new ProviderFilesystemError({
116218 sessionId: "sess-1",
117219 detail: "capture failed",
118 cause,
220 cause: fsCause,
221 });
222
223 expect(fsError._tag).toBe("ProviderFilesystemError");
224 expect(fsError.message).toContain("sess-1");
225 expect(fsError.cause).toBe(fsCause);
226 });
227
228 it("accepts checkpoint catalog error channel type", () => {
229 const error: CheckpointCatalogError = new CheckpointValidationError({
230 operation: "upsertCheckpoint",
231 issue: "turnCount must be >= 0",
119232 });
120233
121 expect(error._tag).toBe("ProviderFilesystemError");
122 expect(error.message).toContain("sess-1");
123 expect(error.cause).toBe(cause);
234 expect(error._tag).toBe("CheckpointValidationError");
124235 });
125236});