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 @@...@@ -1,7 +1,11 @@
1/**1/**
2 * OrchestrationEngineService - Service interface for orchestration command handling.2 * OrchestrationEngineService - Service interface for orchestration command handling.
3 *3 *
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,
5 * replay, and unknown-input decoding all return typed domain errors.9 * replay, and unknown-input decoding all return typed domain errors.
6 *10 *
7 * @module OrchestrationEngineService11 * @module OrchestrationEngineService
...@@ -79,7 +83,7 @@ export interface OrchestrationEngineShape {...@@ -79,7 +83,7 @@ export interface OrchestrationEngineShape {
79}83}
8084
81/**85/**
82 * OrchestrationEngineService - Context tag for orchestration engine access.86 * OrchestrationEngineService - Service tag for orchestration engine access.
83 *87 *
84 * @example88 * @example
85 * ```ts89 * ```ts
apps/server/src/persistence/Services/OrchestrationEvents.ts+5-2
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1/**1/**
2 * OrchestrationEventRepository - Repository interface for orchestration events.2 * OrchestrationEventRepository - Repository interface for orchestration events.
3 *3 *
4 * Uses Effect `Context.Tag` for dependency injection and exposes typed4 * 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
5 * persistence/decode errors for event append and replay operations.8 * persistence/decode errors for event append and replay operations.
6 *9 *
7 * @module OrchestrationEventRepository10 * @module OrchestrationEventRepository
...@@ -44,7 +47,7 @@ export interface OrchestrationEventRepositoryShape {...@@ -44,7 +47,7 @@ export interface OrchestrationEventRepositoryShape {
44}47}
4548
46/**49/**
47 * OrchestrationEventRepository - Context tag for orchestration event persistence.50 * OrchestrationEventRepository - Service tag for orchestration event persistence.
48 *51 *
49 * @example52 * @example
50 * ```ts53 * ```ts
apps/server/src/persistence/Services/Projects.ts+5-2
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1/**1/**
2 * ProjectRepository - Repository interface for project persistence.2 * ProjectRepository - Repository interface for project persistence.
3 *3 *
4 * Uses Effect `Context.Tag` for dependency injection and returns typed4 * 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
5 * domain errors for all operations.8 * domain errors for all operations.
6 *9 *
7 * @module ProjectRepository10 * @module ProjectRepository
...@@ -62,7 +65,7 @@ export interface ProjectRepositoryShape {...@@ -62,7 +65,7 @@ export interface ProjectRepositoryShape {
62}65}
6366
64/**67/**
65 * ProjectRepository - Context tag for project persistence dependency injection.68 * ProjectRepository - Service tag for project persistence dependency injection.
66 *69 *
67 * @example70 * @example
68 * ```ts71 * ```ts
apps/server/src/provider/Errors.ts+40-105
...@@ -1,163 +1,104 @@...@@ -1,163 +1,104 @@
1import { Schema } from "effect";1import { Schema } from "effect";
22
3/**3import type { CheckpointServiceError } from "../checkpointing/Errors.ts";
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}
694
70/**5/**
71 * CodexValidationError - Invalid Codex API input.6 * ProviderAdapterValidationError - Invalid adapter API input.
72 */7 */
73export class CodexValidationError extends Schema.TaggedErrorClass<CodexValidationError>()(8export class ProviderAdapterValidationError extends Schema.TaggedErrorClass<ProviderAdapterValidationError>()(
74 "CodexValidationError",9 "ProviderAdapterValidationError",
75 {10 {
11 provider: Schema.String,
76 operation: Schema.String,12 operation: Schema.String,
77 issue: Schema.String,13 issue: Schema.String,
78 cause: Schema.optional(Schema.Defect),14 cause: Schema.optional(Schema.Defect),
79 },15 },
80) {16) {
81 override get message(): string {17 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}`;
83 }19 }
84}20}
8521
86/**22/**
87 * CodexSessionNotFoundError - Session id is unknown.23 * ProviderAdapterSessionNotFoundError - Adapter-owned session id is unknown.
88 */24 */
89export class CodexSessionNotFoundError extends Schema.TaggedErrorClass<CodexSessionNotFoundError>()(25export class ProviderAdapterSessionNotFoundError extends Schema.TaggedErrorClass<ProviderAdapterSessionNotFoundError>()(
90 "CodexSessionNotFoundError",26 "ProviderAdapterSessionNotFoundError",
91 {27 {
28 provider: Schema.String,
92 sessionId: Schema.String,29 sessionId: Schema.String,
93 cause: Schema.optional(Schema.Defect),30 cause: Schema.optional(Schema.Defect),
94 },31 },
95) {32) {
96 override get message(): string {33 override get message(): string {
97 return `Unknown codex session: ${this.sessionId}`;34 return `Unknown ${this.provider} adapter session: ${this.sessionId}`;
98 }35 }
99}36}
10037
101/**38/**
102 * CodexSessionClosedError - Session exists but is closed.39 * ProviderAdapterSessionClosedError - Adapter session exists but is closed.
103 */40 */
104export class CodexSessionClosedError extends Schema.TaggedErrorClass<CodexSessionClosedError>()(41export class ProviderAdapterSessionClosedError extends Schema.TaggedErrorClass<ProviderAdapterSessionClosedError>()(
105 "CodexSessionClosedError",42 "ProviderAdapterSessionClosedError",
106 {43 {
44 provider: Schema.String,
107 sessionId: Schema.String,45 sessionId: Schema.String,
108 cause: Schema.optional(Schema.Defect),46 cause: Schema.optional(Schema.Defect),
109 },47 },
110) {48) {
111 override get message(): string {49 override get message(): string {
112 return `Codex session is closed: ${this.sessionId}`;50 return `${this.provider} adapter session is closed: ${this.sessionId}`;
113 }51 }
114}52}
11553
116/**54/**
117 * CodexProtocolError - Invalid/unexpected JSON-RPC payload from app-server.55 * ProviderAdapterProtocolError - Invalid/unexpected provider protocol payload.
118 */56 */
119export class CodexProtocolError extends Schema.TaggedErrorClass<CodexProtocolError>()(57export class ProviderAdapterProtocolError extends Schema.TaggedErrorClass<ProviderAdapterProtocolError>()(
120 "CodexProtocolError",58 "ProviderAdapterProtocolError",
121 {59 {
60 provider: Schema.String,
122 operation: Schema.String,61 operation: Schema.String,
123 detail: Schema.String,62 detail: Schema.String,
124 cause: Schema.optional(Schema.Defect),63 cause: Schema.optional(Schema.Defect),
125 },64 },
126) {65) {
127 override get message(): string {66 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}`;
129 }68 }
130}69}
13170
132/**71/**
133 * CodexRequestError - JSON-RPC request failed or timed out.72 * ProviderAdapterRequestError - Provider protocol request failed or timed out.
134 */73 */
135export class CodexRequestError extends Schema.TaggedErrorClass<CodexRequestError>()(74export class ProviderAdapterRequestError extends Schema.TaggedErrorClass<ProviderAdapterRequestError>()(
136 "CodexRequestError",75 "ProviderAdapterRequestError",
137 {76 {
77 provider: Schema.String,
138 method: Schema.String,78 method: Schema.String,
139 detail: Schema.String,79 detail: Schema.String,
140 cause: Schema.optional(Schema.Defect),80 cause: Schema.optional(Schema.Defect),
141 },81 },
142) {82) {
143 override get message(): string {83 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}`;
145 }85 }
146}86}
14787
148/**88/**
149 * CodexProcessError - codex app-server process lifecycle failure.89 * ProviderAdapterProcessError - Provider process lifecycle failure.
150 */90 */
151export class CodexProcessError extends Schema.TaggedErrorClass<CodexProcessError>()(91export class ProviderAdapterProcessError extends Schema.TaggedErrorClass<ProviderAdapterProcessError>()(
152 "CodexProcessError",92 "ProviderAdapterProcessError",
153 {93 {
94 provider: Schema.String,
154 sessionId: Schema.String,95 sessionId: Schema.String,
155 detail: Schema.String,96 detail: Schema.String,
156 cause: Schema.optional(Schema.Defect),97 cause: Schema.optional(Schema.Defect),
157 },98 },
158) {99) {
159 override get message(): string {100 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}`;
161 }102 }
162}103}
163104
...@@ -257,19 +198,13 @@ export class ProviderFilesystemError extends Schema.TaggedErrorClass<ProviderFil...@@ -257,19 +198,13 @@ export class ProviderFilesystemError extends Schema.TaggedErrorClass<ProviderFil
257 }198 }
258}199}
259200
260export type CheckpointStoreError =201export type ProviderAdapterError =
261 | CheckpointValidationError202 | ProviderAdapterValidationError
262 | CheckpointGitCommandError203 | ProviderAdapterSessionNotFoundError
263 | CheckpointUnavailableError204 | ProviderAdapterSessionClosedError
264 | CheckpointRepositoryError;205 | ProviderAdapterProtocolError
265206 | ProviderAdapterRequestError
266export type CodexServiceError =207 | ProviderAdapterProcessError;
267 | CodexValidationError
268 | CodexSessionNotFoundError
269 | CodexSessionClosedError
270 | CodexProtocolError
271 | CodexRequestError
272 | CodexProcessError;
273208
274export type ProviderServiceError =209export type ProviderServiceError =
275 | ProviderValidationError210 | ProviderValidationError
...@@ -278,5 +213,5 @@ export type ProviderServiceError =...@@ -278,5 +213,5 @@ export type ProviderServiceError =
278 | ProviderCheckpointUnavailableError213 | ProviderCheckpointUnavailableError
279 | ProviderCheckpointRangeError214 | ProviderCheckpointRangeError
280 | ProviderFilesystemError215 | ProviderFilesystemError
281 | CodexServiceError216 | ProviderAdapterError
282 | CheckpointStoreError;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 @@...@@ -1,22 +1,31 @@
1import { describe, expect, it } from "vitest";1import { describe, expect, it } from "vitest";
22
3import { Effect, Layer, ManagedRuntime } from "effect";3import { Effect, Layer, ManagedRuntime, Option } from "effect";
44
5import {5import {
6 CheckpointValidationError,6 ProviderAdapterValidationError,
7 CodexValidationError,
8 ProviderFilesystemError,7 ProviderFilesystemError,
8 ProviderSessionNotFoundError,
9 ProviderUnsupportedError,9 ProviderUnsupportedError,
10 type CheckpointStoreError,10 type ProviderAdapterError,
11 type CodexServiceError,
12 type ProviderServiceError,11 type ProviderServiceError,
13} from "./Errors.ts";12} from "./Errors.ts";
14import { CheckpointStore } from "./Services/CheckpointStore.ts";13import {
15import { CodexService } from "./Services/Codex.ts";14 CheckpointValidationError,
16import { ProviderService } from "./Services/Provider.ts";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", () => {27describe("provider/checkpoint service contracts", () => {
19 it("resolves provider, codex, and checkpoint services from ServiceMap tags", async () => {28 it("resolves all service tags from ServiceMap", async () => {
20 const providerLive: typeof ProviderService.Service = {29 const providerLive: typeof ProviderService.Service = {
21 startSession: () =>30 startSession: () =>
22 Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })),31 Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })),
...@@ -35,34 +44,58 @@ describe("provider service contracts", () => {...@@ -35,34 +44,58 @@ describe("provider service contracts", () => {
35 revertToCheckpoint: () =>44 revertToCheckpoint: () =>
36 Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })),45 Effect.fail<ProviderServiceError>(new ProviderUnsupportedError({ provider: "x" })),
37 stopAll: () => Effect.void,46 stopAll: () => Effect.void,
38 dispose: () => Effect.void,
39 subscribeToEvents: () => Effect.succeed(() => undefined),47 subscribeToEvents: () => Effect.succeed(() => undefined),
40 };48 };
4149
42 const codexLive: typeof CodexService.Service = {50 const codexLive: typeof CodexAdapter.Service = {
51 provider: "codex",
43 startSession: () =>52 startSession: () =>
44 Effect.fail<CodexServiceError>(53 Effect.fail<ProviderAdapterError>(
45 new CodexValidationError({ operation: "startSession", issue: "not implemented" }),54 new ProviderAdapterValidationError({
55 provider: "codex",
56 operation: "startSession",
57 issue: "not implemented",
58 }),
46 ),59 ),
47 sendTurn: () =>60 sendTurn: () =>
48 Effect.fail<CodexServiceError>(61 Effect.fail<ProviderAdapterError>(
49 new CodexValidationError({ operation: "sendTurn", issue: "not implemented" }),62 new ProviderAdapterValidationError({
63 provider: "codex",
64 operation: "sendTurn",
65 issue: "not implemented",
66 }),
50 ),67 ),
51 interruptTurn: () =>68 interruptTurn: () =>
52 Effect.fail<CodexServiceError>(69 Effect.fail<ProviderAdapterError>(
53 new CodexValidationError({ operation: "interruptTurn", issue: "not implemented" }),70 new ProviderAdapterValidationError({
71 provider: "codex",
72 operation: "interruptTurn",
73 issue: "not implemented",
74 }),
54 ),75 ),
55 readThread: () =>76 readThread: () =>
56 Effect.fail<CodexServiceError>(77 Effect.fail<ProviderAdapterError>(
57 new CodexValidationError({ operation: "readThread", issue: "not implemented" }),78 new ProviderAdapterValidationError({
79 provider: "codex",
80 operation: "readThread",
81 issue: "not implemented",
82 }),
58 ),83 ),
59 rollbackThread: () =>84 rollbackThread: () =>
60 Effect.fail<CodexServiceError>(85 Effect.fail<ProviderAdapterError>(
61 new CodexValidationError({ operation: "rollbackThread", issue: "not implemented" }),86 new ProviderAdapterValidationError({
87 provider: "codex",
88 operation: "rollbackThread",
89 issue: "not implemented",
90 }),
62 ),91 ),
63 respondToRequest: () =>92 respondToRequest: () =>
64 Effect.fail<CodexServiceError>(93 Effect.fail<ProviderAdapterError>(
65 new CodexValidationError({ operation: "respondToRequest", issue: "not implemented" }),94 new ProviderAdapterValidationError({
95 provider: "codex",
96 operation: "respondToRequest",
97 issue: "not implemented",
98 }),
66 ),99 ),
67 stopSession: () => Effect.void,100 stopSession: () => Effect.void,
68 listSessions: () => Effect.succeed([]),101 listSessions: () => Effect.succeed([]),
...@@ -71,7 +104,7 @@ describe("provider service contracts", () => {...@@ -71,7 +104,7 @@ describe("provider service contracts", () => {
71 subscribeToEvents: () => Effect.succeed(() => undefined),104 subscribeToEvents: () => Effect.succeed(() => undefined),
72 };105 };
73106
74 const checkpointLive: typeof CheckpointStore.Service = {107 const checkpointStoreLive: typeof CheckpointStore.Service = {
75 isGitRepository: () => Effect.succeed(false),108 isGitRepository: () => Effect.succeed(false),
76 captureCheckpoint: () => Effect.void,109 captureCheckpoint: () => Effect.void,
77 hasCheckpoint: () => Effect.succeed(false),110 hasCheckpoint: () => Effect.succeed(false),
...@@ -87,39 +120,117 @@ describe("provider service contracts", () => {...@@ -87,39 +120,117 @@ describe("provider service contracts", () => {
87 pruneAfterTurn: () => Effect.void,120 pruneAfterTurn: () => Effect.void,
88 };121 };
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
90 const runtime = ManagedRuntime.make(172 const runtime = ManagedRuntime.make(
91 Layer.mergeAll(173 Layer.mergeAll(
92 Layer.succeed(ProviderService, providerLive),174 Layer.succeed(ProviderService, providerLive),
93 Layer.succeed(CodexService, codexLive),175 Layer.succeed(CodexAdapter, codexLive),
94 Layer.succeed(CheckpointStore, checkpointLive),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),
95 ),181 ),
96 );182 );
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([
99 runtime.runPromise(Effect.service(ProviderService)),193 runtime.runPromise(Effect.service(ProviderService)),
100 runtime.runPromise(Effect.service(CodexService)),194 runtime.runPromise(Effect.service(CodexAdapter)),
101 runtime.runPromise(Effect.service(CheckpointStore)),195 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)),
102 ]);200 ]);
103201
104 expect(provider).toBe(providerLive);202 expect(provider).toBe(providerLive);
105 expect(codex).toBe(codexLive);203 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
108 await runtime.dispose();210 await runtime.dispose();
109 });211 });
110});212});
111213
112describe("provider error taxonomy", () => {214describe("provider/checkpoint errors", () => {
113 it("keeps defect cause and message on tagged errors", () => {215 it("preserves cause and message on tagged errors", () => {
114 const cause = new Error("filesystem failed");216 const fsCause = new Error("filesystem failed");
115 const error = new ProviderFilesystemError({217 const fsError = new ProviderFilesystemError({
116 sessionId: "sess-1",218 sessionId: "sess-1",
117 detail: "capture failed",219 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",
119 });232 });
120233
121 expect(error._tag).toBe("ProviderFilesystemError");234 expect(error._tag).toBe("CheckpointValidationError");
122 expect(error.message).toContain("sess-1");
123 expect(error.cause).toBe(cause);
124 });235 });
125});236});