| author | |
| committer | |
| log | e5542b7a880aa40ab5a4b5ad8012822c11643471 |
| tree | 5095728ead2c41212ce5918ff55c1d2ebc43dcf7 |
| parent | 76208f79de4cb79146f1501feed6162eef71e24b |
25 files changed, 361 insertions(+), 354 deletions(-)
apps/server/src/checkpointing/Layers/CheckpointStore.ts+11-13| ... | @@ -78,7 +78,7 @@ const makeCheckpointStore = Effect.gen(function* () { | ... | @@ -78,7 +78,7 @@ const makeCheckpointStore = Effect.gen(function* () { |
| 78 | git | 78 | git |
| 79 | .execute({ | 79 | .execute({ |
| 80 | operation: "CheckpointStore.isGitRepository", | 80 | operation: "CheckpointStore.isGitRepository", |
| 81 | cwd: cwd.trim(), | 81 | cwd, |
| 82 | args: ["rev-parse", "--is-inside-work-tree"], | 82 | args: ["rev-parse", "--is-inside-work-tree"], |
| 83 | allowNonZeroExit: true, | 83 | allowNonZeroExit: true, |
| 84 | }) | 84 | }) |
| ... | @@ -90,8 +90,6 @@ const makeCheckpointStore = Effect.gen(function* () { | ... | @@ -90,8 +90,6 @@ const makeCheckpointStore = Effect.gen(function* () { |
| 90 | const captureCheckpoint: CheckpointStoreShape["captureCheckpoint"] = (input) => | 90 | const captureCheckpoint: CheckpointStoreShape["captureCheckpoint"] = (input) => |
| 91 | Effect.gen(function* () { | 91 | Effect.gen(function* () { |
| 92 | const operation = "CheckpointStore.captureCheckpoint"; | 92 | const operation = "CheckpointStore.captureCheckpoint"; |
| 93 | const normalizedCwd = input.cwd.trim(); | ||
| 94 | const checkpointRef = input.checkpointRef.trim(); | ||
| 95 | 93 | ||
| 96 | yield* Effect.acquireUseRelease( | 94 | yield* Effect.acquireUseRelease( |
| 97 | fs.makeTempDirectory({ prefix: "t3-fs-checkpoint-" }), | 95 | fs.makeTempDirectory({ prefix: "t3-fs-checkpoint-" }), |
| ... | @@ -107,11 +105,11 @@ const makeCheckpointStore = Effect.gen(function* () { | ... | @@ -107,11 +105,11 @@ const makeCheckpointStore = Effect.gen(function* () { |
| 107 | GIT_COMMITTER_EMAIL: "t3code@users.noreply.github.com", | 105 | GIT_COMMITTER_EMAIL: "t3code@users.noreply.github.com", |
| 108 | }; | 106 | }; |
| 109 | 107 | ||
| 110 | const headExists = yield* hasHeadCommit(normalizedCwd); | 108 | const headExists = yield* hasHeadCommit(input.cwd); |
| 111 | if (headExists) { | 109 | if (headExists) { |
| 112 | yield* git.execute({ | 110 | yield* git.execute({ |
| 113 | operation, | 111 | operation, |
| 114 | cwd: normalizedCwd, | 112 | cwd: input.cwd, |
| 115 | args: ["read-tree", "HEAD"], | 113 | args: ["read-tree", "HEAD"], |
| 116 | env: commitEnv, | 114 | env: commitEnv, |
| 117 | }); | 115 | }); |
| ... | @@ -119,14 +117,14 @@ const makeCheckpointStore = Effect.gen(function* () { | ... | @@ -119,14 +117,14 @@ const makeCheckpointStore = Effect.gen(function* () { |
| 119 | 117 | ||
| 120 | yield* git.execute({ | 118 | yield* git.execute({ |
| 121 | operation, | 119 | operation, |
| 122 | cwd: normalizedCwd, | 120 | cwd: input.cwd, |
| 123 | args: ["add", "-A", "--", "."], | 121 | args: ["add", "-A", "--", "."], |
| 124 | env: commitEnv, | 122 | env: commitEnv, |
| 125 | }); | 123 | }); |
| 126 | 124 | ||
| 127 | const writeTreeResult = yield* git.execute({ | 125 | const writeTreeResult = yield* git.execute({ |
| 128 | operation, | 126 | operation, |
| 129 | cwd: normalizedCwd, | 127 | cwd: input.cwd, |
| 130 | args: ["write-tree"], | 128 | args: ["write-tree"], |
| 131 | env: commitEnv, | 129 | env: commitEnv, |
| 132 | }); | 130 | }); |
| ... | @@ -135,15 +133,15 @@ const makeCheckpointStore = Effect.gen(function* () { | ... | @@ -135,15 +133,15 @@ const makeCheckpointStore = Effect.gen(function* () { |
| 135 | return yield* new GitCommandError({ | 133 | return yield* new GitCommandError({ |
| 136 | operation, | 134 | operation, |
| 137 | command: "git write-tree", | 135 | command: "git write-tree", |
| 138 | cwd: normalizedCwd, | 136 | cwd: input.cwd, |
| 139 | detail: "git write-tree returned an empty tree oid.", | 137 | detail: "git write-tree returned an empty tree oid.", |
| 140 | }); | 138 | }); |
| 141 | } | 139 | } |
| 142 | 140 | ||
| 143 | const message = `t3 checkpoint ref=${checkpointRef}`; | 141 | const message = `t3 checkpoint ref=${input.checkpointRef}`; |
| 144 | const commitTreeResult = yield* git.execute({ | 142 | const commitTreeResult = yield* git.execute({ |
| 145 | operation, | 143 | operation, |
| 146 | cwd: normalizedCwd, | 144 | cwd: input.cwd, |
| 147 | args: ["commit-tree", treeOid, "-m", message], | 145 | args: ["commit-tree", treeOid, "-m", message], |
| 148 | env: commitEnv, | 146 | env: commitEnv, |
| 149 | }); | 147 | }); |
| ... | @@ -152,15 +150,15 @@ const makeCheckpointStore = Effect.gen(function* () { | ... | @@ -152,15 +150,15 @@ const makeCheckpointStore = Effect.gen(function* () { |
| 152 | return yield* new GitCommandError({ | 150 | return yield* new GitCommandError({ |
| 153 | operation, | 151 | operation, |
| 154 | command: "git commit-tree", | 152 | command: "git commit-tree", |
| 155 | cwd: normalizedCwd, | 153 | cwd: input.cwd, |
| 156 | detail: "git commit-tree returned an empty commit oid.", | 154 | detail: "git commit-tree returned an empty commit oid.", |
| 157 | }); | 155 | }); |
| 158 | } | 156 | } |
| 159 | 157 | ||
| 160 | yield* git.execute({ | 158 | yield* git.execute({ |
| 161 | operation, | 159 | operation, |
| 162 | cwd: normalizedCwd, | 160 | cwd: input.cwd, |
| 163 | args: ["update-ref", checkpointRef, commitOid], | 161 | args: ["update-ref", input.checkpointRef, commitOid], |
| 164 | }); | 162 | }); |
| 165 | }), | 163 | }), |
| 166 | (tempDir) => fs.remove(tempDir, { recursive: true }), | 164 | (tempDir) => fs.remove(tempDir, { recursive: true }), |
apps/server/src/checkpointing/Utils.ts+2-10| ... | @@ -9,11 +9,6 @@ export function checkpointRefForThreadTurn(threadId: ThreadId, turnCount: number | ... | @@ -9,11 +9,6 @@ export function checkpointRefForThreadTurn(threadId: ThreadId, turnCount: number |
| 9 | ); | 9 | ); |
| 10 | } | 10 | } |
| 11 | 11 | ||
| 12 | function trimToNonEmpty(value: string | null | undefined): string | undefined { | ||
| 13 | const normalized = value?.trim(); | ||
| 14 | return normalized && normalized.length > 0 ? normalized : undefined; | ||
| 15 | } | ||
| 16 | |||
| 17 | export function resolveThreadWorkspaceCwd(input: { | 12 | export function resolveThreadWorkspaceCwd(input: { |
| 18 | readonly thread: { | 13 | readonly thread: { |
| 19 | readonly projectId: ProjectId; | 14 | readonly projectId: ProjectId; |
| ... | @@ -24,13 +19,10 @@ export function resolveThreadWorkspaceCwd(input: { | ... | @@ -24,13 +19,10 @@ export function resolveThreadWorkspaceCwd(input: { |
| 24 | readonly workspaceRoot: string; | 19 | readonly workspaceRoot: string; |
| 25 | }>; | 20 | }>; |
| 26 | }): string | undefined { | 21 | }): string | undefined { |
| 27 | const worktreeCwd = trimToNonEmpty(input.thread.worktreePath); | 22 | const worktreeCwd = input.thread.worktreePath ?? undefined; |
| 28 | if (worktreeCwd) { | 23 | if (worktreeCwd) { |
| 29 | return worktreeCwd; | 24 | return worktreeCwd; |
| 30 | } | 25 | } |
| 31 | 26 | ||
| 32 | const workspaceRoot = input.projects.find( | 27 | return input.projects.find((project) => project.id === input.thread.projectId)?.workspaceRoot; |
| 33 | (project) => project.id === input.thread.projectId, | ||
| 34 | )?.workspaceRoot; | ||
| 35 | return trimToNonEmpty(workspaceRoot); | ||
| 36 | } | 28 | } |
apps/server/src/codexAppServerManager.ts+2-2| ... | @@ -169,8 +169,8 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve | ... | @@ -169,8 +169,8 @@ export class CodexAppServerManager extends EventEmitter<CodexAppServerManagerEve |
| 169 | updatedAt: now, | 169 | updatedAt: now, |
| 170 | }; | 170 | }; |
| 171 | 171 | ||
| 172 | const codexBinaryPath = input.codexBinaryPath?.trim() || "codex"; | 172 | const codexBinaryPath = input.codexBinaryPath ?? "codex"; |
| 173 | const codexHomePath = input.codexHomePath?.trim(); | 173 | const codexHomePath = input.codexHomePath; |
| 174 | const child = spawn(codexBinaryPath, ["app-server"], { | 174 | const child = spawn(codexBinaryPath, ["app-server"], { |
| 175 | cwd: resolvedCwd, | 175 | cwd: resolvedCwd, |
| 176 | env: { | 176 | env: { |
apps/server/src/git/Layers/GitService.ts-2| ... | @@ -71,10 +71,8 @@ const makeGitService = Effect.gen(function* () { | ... | @@ -71,10 +71,8 @@ const makeGitService = Effect.gen(function* () { |
| 71 | const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; | 71 | const commandSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; |
| 72 | 72 | ||
| 73 | const execute: GitServiceShape["execute"] = Effect.fnUntraced(function* (input) { | 73 | const execute: GitServiceShape["execute"] = Effect.fnUntraced(function* (input) { |
| 74 | const normalizedCwd = input.cwd.trim(); | ||
| 75 | const commandInput = { | 74 | const commandInput = { |
| 76 | ...input, | 75 | ...input, |
| 77 | cwd: normalizedCwd, | ||
| 78 | args: [...input.args], | 76 | args: [...input.args], |
| 79 | } as const; | 77 | } as const; |
| 80 | const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS; | 78 | const timeoutMs = input.timeoutMs ?? DEFAULT_TIMEOUT_MS; |
apps/server/src/keybindings.ts+3-6| ... | @@ -265,14 +265,11 @@ function parseKeybindingWhenExpression(expression: string): KeybindingWhenNode | | ... | @@ -265,14 +265,11 @@ function parseKeybindingWhenExpression(expression: string): KeybindingWhenNode | |
| 265 | 265 | ||
| 266 | /** @internal - Exported for testing */ | 266 | /** @internal - Exported for testing */ |
| 267 | export function compileResolvedKeybindingRule(rule: KeybindingRule): ResolvedKeybindingRule | null { | 267 | export function compileResolvedKeybindingRule(rule: KeybindingRule): ResolvedKeybindingRule | null { |
| 268 | const key = rule.key.trim(); | 268 | const shortcut = parseKeybindingShortcut(rule.key); |
| 269 | if (key.length === 0) return null; | ||
| 270 | const shortcut = parseKeybindingShortcut(key); | ||
| 271 | if (!shortcut) return null; | 269 | if (!shortcut) return null; |
| 272 | 270 | ||
| 273 | const when = rule.when?.trim(); | 271 | if (rule.when !== undefined) { |
| 274 | if (when && when.length > 0) { | 272 | const whenAst = parseKeybindingWhenExpression(rule.when); |
| 275 | const whenAst = parseKeybindingWhenExpression(when); | ||
| 276 | if (!whenAst) return null; | 273 | if (!whenAst) return null; |
| 277 | return { | 274 | return { |
| 278 | command: rule.command, | 275 | command: rule.command, |
apps/server/src/orchestration/Layers/CheckpointReactor.ts+5-12| ... | @@ -33,13 +33,7 @@ type ReactorInput = | ... | @@ -33,13 +33,7 @@ type ReactorInput = |
| 33 | }; | 33 | }; |
| 34 | 34 | ||
| 35 | function toTurnId(value: string | undefined): TurnId | null { | 35 | function toTurnId(value: string | undefined): TurnId | null { |
| 36 | const normalized = value?.trim(); | 36 | return value === undefined ? null : TurnId.makeUnsafe(value); |
| 37 | return normalized && normalized.length > 0 ? TurnId.makeUnsafe(normalized) : null; | ||
| 38 | } | ||
| 39 | |||
| 40 | function trimToNonEmpty(value: string | null | undefined): string | undefined { | ||
| 41 | const normalized = value?.trim(); | ||
| 42 | return normalized && normalized.length > 0 ? normalized : undefined; | ||
| 43 | } | 37 | } |
| 44 | 38 | ||
| 45 | function checkpointStatusFromRuntime(status: string | undefined): "ready" | "missing" | "error" { | 39 | function checkpointStatusFromRuntime(status: string | undefined): "ready" | "missing" | "error" { |
| ... | @@ -129,18 +123,17 @@ const make = Effect.gen(function* () { | ... | @@ -129,18 +123,17 @@ const make = Effect.gen(function* () { |
| 129 | const readModel = yield* orchestrationEngine.getReadModel(); | 123 | const readModel = yield* orchestrationEngine.getReadModel(); |
| 130 | const thread = readModel.threads.find((entry) => entry.id === threadId); | 124 | const thread = readModel.threads.find((entry) => entry.id === threadId); |
| 131 | const projectedSessionId = thread?.session?.providerSessionId ?? null; | 125 | const projectedSessionId = thread?.session?.providerSessionId ?? null; |
| 132 | const projectedProviderThreadId = trimToNonEmpty(thread?.session?.providerThreadId); | 126 | const projectedProviderThreadId = thread?.session?.providerThreadId ?? undefined; |
| 133 | 127 | ||
| 134 | const sessions = yield* providerService.listSessions(); | 128 | const sessions = yield* providerService.listSessions(); |
| 135 | 129 | ||
| 136 | const findSessionWithCwd = ( | 130 | const findSessionWithCwd = ( |
| 137 | session: (typeof sessions)[number] | undefined, | 131 | session: (typeof sessions)[number] | undefined, |
| 138 | ): Option.Option<{ readonly sessionId: ProviderSessionId; readonly cwd: string }> => { | 132 | ): Option.Option<{ readonly sessionId: ProviderSessionId; readonly cwd: string }> => { |
| 139 | const cwd = session?.cwd?.trim(); | 133 | if (!session?.cwd) { |
| 140 | if (!session || !cwd) { | ||
| 141 | return Option.none(); | 134 | return Option.none(); |
| 142 | } | 135 | } |
| 143 | return Option.some({ sessionId: session.sessionId, cwd }); | 136 | return Option.some({ sessionId: session.sessionId, cwd: session.cwd }); |
| 144 | }; | 137 | }; |
| 145 | 138 | ||
| 146 | if (projectedSessionId !== null) { | 139 | if (projectedSessionId !== null) { |
| ... | @@ -153,7 +146,7 @@ const make = Effect.gen(function* () { | ... | @@ -153,7 +146,7 @@ const make = Effect.gen(function* () { |
| 153 | 146 | ||
| 154 | if (projectedProviderThreadId) { | 147 | if (projectedProviderThreadId) { |
| 155 | const matchedSession = sessions.find( | 148 | const matchedSession = sessions.find( |
| 156 | (session) => session.threadId?.trim() === projectedProviderThreadId, | 149 | (session) => session.threadId === projectedProviderThreadId, |
| 157 | ); | 150 | ); |
| 158 | const fromProviderThread = findSessionWithCwd(matchedSession); | 151 | const fromProviderThread = findSessionWithCwd(matchedSession); |
| 159 | if (Option.isSome(fromProviderThread)) { | 152 | if (Option.isSome(fromProviderThread)) { |
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+7-10| ... | @@ -28,7 +28,7 @@ type ProviderIntentEvent = Extract< | ... | @@ -28,7 +28,7 @@ type ProviderIntentEvent = Extract< |
| 28 | } | 28 | } |
| 29 | >; | 29 | >; |
| 30 | 30 | ||
| 31 | function trimToNonEmptyOrUndefined(value: string | undefined): string | undefined { | 31 | function toNonEmptyProviderInput(value: string | undefined): string | undefined { |
| 32 | const normalized = value?.trim(); | 32 | const normalized = value?.trim(); |
| 33 | return normalized && normalized.length > 0 ? normalized : undefined; | 33 | return normalized && normalized.length > 0 ? normalized : undefined; |
| 34 | } | 34 | } |
| ... | @@ -43,15 +43,12 @@ function resolveEffectiveThreadCwd(input: { | ... | @@ -43,15 +43,12 @@ function resolveEffectiveThreadCwd(input: { |
| 43 | readonly workspaceRoot: string; | 43 | readonly workspaceRoot: string; |
| 44 | }>; | 44 | }>; |
| 45 | }): string | undefined { | 45 | }): string | undefined { |
| 46 | const worktreeCwd = trimToNonEmptyOrUndefined(input.thread.worktreePath ?? undefined); | 46 | const worktreeCwd = input.thread.worktreePath ?? undefined; |
| 47 | if (worktreeCwd) { | 47 | if (worktreeCwd) { |
| 48 | return worktreeCwd; | 48 | return worktreeCwd; |
| 49 | } | 49 | } |
| 50 | 50 | ||
| 51 | const workspaceRoot = input.projects.find( | 51 | return input.projects.find((project) => project.id === input.thread.projectId)?.workspaceRoot; |
| 52 | (project) => project.id === input.thread.projectId, | ||
| 53 | )?.workspaceRoot; | ||
| 54 | return trimToNonEmptyOrUndefined(workspaceRoot); | ||
| 55 | } | 52 | } |
| 56 | 53 | ||
| 57 | function mapProviderSessionStatusToOrchestrationStatus( | 54 | function mapProviderSessionStatusToOrchestrationStatus( |
| ... | @@ -158,7 +155,7 @@ const make = Effect.gen(function* () { | ... | @@ -158,7 +155,7 @@ const make = Effect.gen(function* () { |
| 158 | const startedSession = yield* providerService.startSession({ | 155 | const startedSession = yield* providerService.startSession({ |
| 159 | ...(preferredProvider ? { provider: preferredProvider } : {}), | 156 | ...(preferredProvider ? { provider: preferredProvider } : {}), |
| 160 | ...(effectiveCwd ? { cwd: effectiveCwd } : {}), | 157 | ...(effectiveCwd ? { cwd: effectiveCwd } : {}), |
| 161 | ...(trimToNonEmptyOrUndefined(thread.model) ? { model: thread.model } : {}), | 158 | ...(thread.model ? { model: thread.model } : {}), |
| 162 | }); | 159 | }); |
| 163 | 160 | ||
| 164 | yield* setThreadSession({ | 161 | yield* setThreadSession({ |
| ... | @@ -193,15 +190,15 @@ const make = Effect.gen(function* () { | ... | @@ -193,15 +190,15 @@ const make = Effect.gen(function* () { |
| 193 | return; | 190 | return; |
| 194 | } | 191 | } |
| 195 | const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt); | 192 | const sessionId = yield* ensureSessionForThread(input.threadId, input.createdAt); |
| 196 | const normalizedInput = trimToNonEmptyOrUndefined(input.messageText); | 193 | const normalizedInput = toNonEmptyProviderInput(input.messageText); |
| 197 | const normalizedAttachments = input.attachments ?? []; | 194 | const normalizedAttachments = input.attachments ?? []; |
| 198 | 195 | ||
| 199 | yield* providerService.sendTurn({ | 196 | yield* providerService.sendTurn({ |
| 200 | sessionId, | 197 | sessionId, |
| 201 | ...(normalizedInput ? { input: normalizedInput } : {}), | 198 | ...(normalizedInput ? { input: normalizedInput } : {}), |
| 202 | ...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}), | 199 | ...(normalizedAttachments.length > 0 ? { attachments: normalizedAttachments } : {}), |
| 203 | ...(trimToNonEmptyOrUndefined(input.model) ? { model: input.model } : {}), | 200 | ...(input.model !== undefined ? { model: input.model } : {}), |
| 204 | ...(trimToNonEmptyOrUndefined(input.effort) ? { effort: input.effort } : {}), | 201 | ...(input.effort !== undefined ? { effort: input.effort } : {}), |
| 205 | }); | 202 | }); |
| 206 | }); | 203 | }); |
| 207 | 204 |
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+1-2| ... | @@ -21,8 +21,7 @@ const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId | ... | @@ -21,8 +21,7 @@ const providerCommandId = (event: ProviderRuntimeEvent, tag: string): CommandId |
| 21 | CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); | 21 | CommandId.makeUnsafe(`provider:${event.eventId}:${tag}:${crypto.randomUUID()}`); |
| 22 | 22 | ||
| 23 | function toTurnId(value: string | undefined): TurnId | undefined { | 23 | function toTurnId(value: string | undefined): TurnId | undefined { |
| 24 | const normalized = value?.trim(); | 24 | return value === undefined ? undefined : TurnId.makeUnsafe(value); |
| 25 | return normalized?.length ? TurnId.makeUnsafe(normalized) : undefined; | ||
| 26 | } | 25 | } |
| 27 | 26 | ||
| 28 | function truncateDetail(value: string, limit = 180): string { | 27 | function truncateDetail(value: string, limit = 180): string { |
apps/server/src/provider/Layers/ProviderService.ts+2-1| ... | @@ -242,8 +242,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => | ... | @@ -242,8 +242,9 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) => |
| 242 | Effect.gen(function* () { | 242 | Effect.gen(function* () { |
| 243 | const adapter = yield* registry.getByProvider(input.provider); | 243 | const adapter = yield* registry.getByProvider(input.provider); |
| 244 | const activeSessions = yield* adapter.listSessions(); | 244 | const activeSessions = yield* adapter.listSessions(); |
| 245 | const expectedProviderThreadId = asProviderThreadId(input.threadId); | ||
| 245 | const existing = activeSessions.find( | 246 | const existing = activeSessions.find( |
| 246 | (session) => session.threadId?.trim() === input.threadId.trim(), | 247 | (session) => session.threadId === expectedProviderThreadId, |
| 247 | ); | 248 | ); |
| 248 | if (existing) { | 249 | if (existing) { |
| 249 | const existingThreadId = yield* upsertSessionBinding( | 250 | const existingThreadId = yield* upsertSessionBinding( |
apps/server/src/provider/Layers/ProviderSessionDirectory.test.ts+3-3| ... | @@ -79,12 +79,12 @@ layer("ProviderSessionDirectoryLive", (it) => { | ... | @@ -79,12 +79,12 @@ layer("ProviderSessionDirectoryLive", (it) => { |
| 79 | assertFailure(missingProvider, new ProviderSessionNotFoundError({ sessionId: "sess-1" })); | 79 | assertFailure(missingProvider, new ProviderSessionNotFoundError({ sessionId: "sess-1" })); |
| 80 | })); | 80 | })); |
| 81 | 81 | ||
| 82 | it("fails upsert for empty session id", () => | 82 | it("fails upsert when thread id is unavailable", () => |
| 83 | Effect.gen(function* () { | 83 | Effect.gen(function* () { |
| 84 | const directory = yield* ProviderSessionDirectory; | 84 | const directory = yield* ProviderSessionDirectory; |
| 85 | const result = yield* Effect.result( | 85 | const result = yield* Effect.result( |
| 86 | directory.upsert({ | 86 | directory.upsert({ |
| 87 | sessionId: sessionId(" "), | 87 | sessionId: sessionId("sess-no-thread"), |
| 88 | provider: "codex", | 88 | provider: "codex", |
| 89 | }), | 89 | }), |
| 90 | ); | 90 | ); |
| ... | @@ -92,7 +92,7 @@ layer("ProviderSessionDirectoryLive", (it) => { | ... | @@ -92,7 +92,7 @@ layer("ProviderSessionDirectoryLive", (it) => { |
| 92 | result, | 92 | result, |
| 93 | new ProviderValidationError({ | 93 | new ProviderValidationError({ |
| 94 | operation: "ProviderSessionDirectory.upsert", | 94 | operation: "ProviderSessionDirectory.upsert", |
| 95 | issue: "sessionId must be a non-empty string.", | 95 | issue: "threadId must be a non-empty string.", |
| 96 | }), | 96 | }), |
| 97 | ); | 97 | ); |
| 98 | })); | 98 | })); |
apps/server/src/provider/Layers/ProviderSessionDirectory.ts+60-111| ... | @@ -1,9 +1,4 @@ | ... | @@ -1,9 +1,4 @@ |
| 1 | import { | 1 | import { ProviderSessionId, type ProviderKind } from "@t3tools/contracts"; |
| 2 | ProviderSessionId, | ||
| 3 | ProviderThreadId, | ||
| 4 | ThreadId, | ||
| 5 | type ProviderKind, | ||
| 6 | } from "@t3tools/contracts"; | ||
| 7 | import { Effect, Layer, Option } from "effect"; | 2 | import { Effect, Layer, Option } from "effect"; |
| 8 | 3 | ||
| 9 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; | 4 | import { ProviderSessionRuntimeRepository } from "../../persistence/Services/ProviderSessionRuntime.ts"; |
| ... | @@ -27,31 +22,6 @@ function toPersistenceError(operation: string) { | ... | @@ -27,31 +22,6 @@ function toPersistenceError(operation: string) { |
| 27 | }); | 22 | }); |
| 28 | } | 23 | } |
| 29 | 24 | ||
| 30 | function normalizeBinding(binding: ProviderSessionBinding): ProviderSessionBinding { | ||
| 31 | const sessionId = binding.sessionId.trim(); | ||
| 32 | const threadId = binding.threadId?.trim(); | ||
| 33 | const adapterKey = binding.adapterKey?.trim(); | ||
| 34 | const providerThreadId = | ||
| 35 | binding.providerThreadId === null ? null : binding.providerThreadId?.trim(); | ||
| 36 | |||
| 37 | return { | ||
| 38 | sessionId: ProviderSessionId.makeUnsafe(sessionId), | ||
| 39 | provider: binding.provider, | ||
| 40 | ...(adapterKey !== undefined && adapterKey.length > 0 ? { adapterKey } : {}), | ||
| 41 | ...(threadId !== undefined && threadId.length > 0 | ||
| 42 | ? { threadId: ThreadId.makeUnsafe(threadId) } | ||
| 43 | : {}), | ||
| 44 | ...(providerThreadId === null | ||
| 45 | ? { providerThreadId: null } | ||
| 46 | : providerThreadId !== undefined && providerThreadId.length > 0 | ||
| 47 | ? { providerThreadId: ProviderThreadId.makeUnsafe(providerThreadId) } | ||
| 48 | : {}), | ||
| 49 | ...(binding.status !== undefined ? { status: binding.status } : {}), | ||
| 50 | ...(binding.resumeCursor !== undefined ? { resumeCursor: binding.resumeCursor } : {}), | ||
| 51 | ...(binding.runtimePayload !== undefined ? { runtimePayload: binding.runtimePayload } : {}), | ||
| 52 | }; | ||
| 53 | } | ||
| 54 | |||
| 55 | function decodeProviderKind( | 25 | function decodeProviderKind( |
| 56 | providerName: string, | 26 | providerName: string, |
| 57 | operation: string, | 27 | operation: string, |
| ... | @@ -90,54 +60,6 @@ function mergeRuntimePayload( | ... | @@ -90,54 +60,6 @@ function mergeRuntimePayload( |
| 90 | const makeProviderSessionDirectory = Effect.gen(function* () { | 60 | const makeProviderSessionDirectory = Effect.gen(function* () { |
| 91 | const repository = yield* ProviderSessionRuntimeRepository; | 61 | const repository = yield* ProviderSessionRuntimeRepository; |
| 92 | 62 | ||
| 93 | const persistUpsert = (binding: ProviderSessionBinding) => | ||
| 94 | Effect.gen(function* () { | ||
| 95 | const existing = yield* repository | ||
| 96 | .getBySessionId({ | ||
| 97 | providerSessionId: binding.sessionId, | ||
| 98 | }) | ||
| 99 | .pipe( | ||
| 100 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.upsert:getBySessionId")), | ||
| 101 | ); | ||
| 102 | |||
| 103 | const existingRuntime = Option.getOrUndefined(existing); | ||
| 104 | const resolvedThreadId = binding.threadId ?? existingRuntime?.threadId; | ||
| 105 | if (!resolvedThreadId) { | ||
| 106 | return yield* new ProviderValidationError({ | ||
| 107 | operation: "ProviderSessionDirectory.upsert", | ||
| 108 | issue: "threadId must be a non-empty string.", | ||
| 109 | }); | ||
| 110 | } | ||
| 111 | |||
| 112 | const now = new Date().toISOString(); | ||
| 113 | yield* repository | ||
| 114 | .upsert({ | ||
| 115 | providerSessionId: binding.sessionId, | ||
| 116 | threadId: resolvedThreadId, | ||
| 117 | providerName: binding.provider, | ||
| 118 | adapterKey: binding.adapterKey ?? existingRuntime?.adapterKey ?? binding.provider, | ||
| 119 | providerThreadId: binding.providerThreadId ?? existingRuntime?.providerThreadId ?? null, | ||
| 120 | status: binding.status ?? existingRuntime?.status ?? "running", | ||
| 121 | lastSeenAt: now, | ||
| 122 | resumeCursor: | ||
| 123 | binding.resumeCursor !== undefined | ||
| 124 | ? binding.resumeCursor | ||
| 125 | : (existingRuntime?.resumeCursor ?? null), | ||
| 126 | runtimePayload: mergeRuntimePayload( | ||
| 127 | existingRuntime?.runtimePayload ?? null, | ||
| 128 | binding.runtimePayload, | ||
| 129 | ), | ||
| 130 | }) | ||
| 131 | .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.upsert:upsert"))); | ||
| 132 | }); | ||
| 133 | |||
| 134 | const persistDelete = (sessionId: ProviderSessionId) => | ||
| 135 | repository | ||
| 136 | .deleteBySessionId({ providerSessionId: sessionId }) | ||
| 137 | .pipe( | ||
| 138 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.remove:deleteBySessionId")), | ||
| 139 | ); | ||
| 140 | |||
| 141 | const getBinding = (sessionId: ProviderSessionId) => | 63 | const getBinding = (sessionId: ProviderSessionId) => |
| 142 | repository.getBySessionId({ providerSessionId: sessionId }).pipe( | 64 | repository.getBySessionId({ providerSessionId: sessionId }).pipe( |
| 143 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.getBinding:getBySessionId")), | 65 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.getBinding:getBySessionId")), |
| ... | @@ -158,38 +80,43 @@ const makeProviderSessionDirectory = Effect.gen(function* () { | ... | @@ -158,38 +80,43 @@ const makeProviderSessionDirectory = Effect.gen(function* () { |
| 158 | ), | 80 | ), |
| 159 | ); | 81 | ); |
| 160 | 82 | ||
| 161 | const listBindings = () => | 83 | const upsert: ProviderSessionDirectoryShape["upsert"] = Effect.fn(function* (binding) { |
| 162 | repository.list().pipe( | 84 | const existing = yield* repository |
| 163 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.listSessionIds:list")), | 85 | .getBySessionId({ |
| 164 | Effect.flatMap((rows) => | 86 | providerSessionId: binding.sessionId, |
| 165 | Effect.forEach( | 87 | }) |
| 166 | rows, | 88 | .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.upsert:getBySessionId"))); |
| 167 | (row) => | 89 | |
| 168 | decodeProviderKind(row.providerName, "ProviderSessionDirectory.listSessionIds").pipe( | 90 | const existingRuntime = Option.getOrUndefined(existing); |
| 169 | Effect.map((provider) => ({ | 91 | const resolvedThreadId = binding.threadId ?? existingRuntime?.threadId; |
| 170 | sessionId: row.providerSessionId, | 92 | if (!resolvedThreadId) { |
| 171 | provider, | 93 | return yield* new ProviderValidationError({ |
| 172 | threadId: row.threadId, | 94 | operation: "ProviderSessionDirectory.upsert", |
| 173 | })), | 95 | issue: "threadId must be a non-empty string.", |
| 174 | ), | 96 | }); |
| 175 | { concurrency: "unbounded" }, | ||
| 176 | ), | ||
| 177 | ), | ||
| 178 | ); | ||
| 179 | |||
| 180 | const upsert: ProviderSessionDirectoryShape["upsert"] = (binding) => { | ||
| 181 | const normalized = normalizeBinding(binding); | ||
| 182 | if (normalized.sessionId.length === 0) { | ||
| 183 | return Effect.fail( | ||
| 184 | new ProviderValidationError({ | ||
| 185 | operation: "ProviderSessionDirectory.upsert", | ||
| 186 | issue: "sessionId must be a non-empty string.", | ||
| 187 | }), | ||
| 188 | ); | ||
| 189 | } | 97 | } |
| 190 | 98 | ||
| 191 | return persistUpsert(normalized); | 99 | const now = new Date().toISOString(); |
| 192 | }; | 100 | yield* repository |
| 101 | .upsert({ | ||
| 102 | providerSessionId: binding.sessionId, | ||
| 103 | threadId: resolvedThreadId, | ||
| 104 | providerName: binding.provider, | ||
| 105 | adapterKey: binding.adapterKey ?? existingRuntime?.adapterKey ?? binding.provider, | ||
| 106 | providerThreadId: binding.providerThreadId ?? existingRuntime?.providerThreadId ?? null, | ||
| 107 | status: binding.status ?? existingRuntime?.status ?? "running", | ||
| 108 | lastSeenAt: now, | ||
| 109 | resumeCursor: | ||
| 110 | binding.resumeCursor !== undefined | ||
| 111 | ? binding.resumeCursor | ||
| 112 | : (existingRuntime?.resumeCursor ?? null), | ||
| 113 | runtimePayload: mergeRuntimePayload( | ||
| 114 | existingRuntime?.runtimePayload ?? null, | ||
| 115 | binding.runtimePayload, | ||
| 116 | ), | ||
| 117 | }) | ||
| 118 | .pipe(Effect.mapError(toPersistenceError("ProviderSessionDirectory.upsert:upsert"))); | ||
| 119 | }); | ||
| 193 | 120 | ||
| 194 | const getProvider: ProviderSessionDirectoryShape["getProvider"] = (sessionId) => | 121 | const getProvider: ProviderSessionDirectoryShape["getProvider"] = (sessionId) => |
| 195 | getBinding(sessionId).pipe( | 122 | getBinding(sessionId).pipe( |
| ... | @@ -211,10 +138,32 @@ const makeProviderSessionDirectory = Effect.gen(function* () { | ... | @@ -211,10 +138,32 @@ const makeProviderSessionDirectory = Effect.gen(function* () { |
| 211 | ), | 138 | ), |
| 212 | ); | 139 | ); |
| 213 | 140 | ||
| 214 | const remove: ProviderSessionDirectoryShape["remove"] = (sessionId) => persistDelete(sessionId); | 141 | const remove: ProviderSessionDirectoryShape["remove"] = (sessionId) => |
| 142 | repository | ||
| 143 | .deleteBySessionId({ providerSessionId: sessionId }) | ||
| 144 | .pipe( | ||
| 145 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.remove:deleteBySessionId")), | ||
| 146 | ); | ||
| 215 | 147 | ||
| 216 | const listSessionIds: ProviderSessionDirectoryShape["listSessionIds"] = () => | 148 | const listSessionIds: ProviderSessionDirectoryShape["listSessionIds"] = () => |
| 217 | listBindings().pipe(Effect.map((bindings) => bindings.map((binding) => binding.sessionId))); | 149 | repository.list().pipe( |
| 150 | Effect.mapError(toPersistenceError("ProviderSessionDirectory.listSessionIds:list")), | ||
| 151 | Effect.flatMap((rows) => | ||
| 152 | Effect.forEach( | ||
| 153 | rows, | ||
| 154 | (row) => | ||
| 155 | decodeProviderKind(row.providerName, "ProviderSessionDirectory.listSessionIds").pipe( | ||
| 156 | Effect.map((provider) => ({ | ||
| 157 | sessionId: row.providerSessionId, | ||
| 158 | provider, | ||
| 159 | threadId: row.threadId, | ||
| 160 | })), | ||
| 161 | ), | ||
| 162 | { concurrency: "unbounded" }, | ||
| 163 | ), | ||
| 164 | ), | ||
| 165 | Effect.map((bindings) => bindings.map((binding) => binding.sessionId)), | ||
| 166 | ); | ||
| 218 | 167 | ||
| 219 | return { | 168 | return { |
| 220 | upsert, | 169 | upsert, |
packages/contracts/src/baseSchemas.ts created+42| ... | @@ -0,0 +1,42 @@ | ||
| 1 | import { Schema } from "effect"; | ||
| 2 | |||
| 3 | export const TrimmedString = Schema.Trim; | ||
| 4 | export const TrimmedNonEmptyString = TrimmedString.check(Schema.isNonEmpty()); | ||
| 5 | |||
| 6 | export const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)); | ||
| 7 | export const PositiveInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)); | ||
| 8 | |||
| 9 | export const IsoDateTime = Schema.String; | ||
| 10 | export type IsoDateTime = typeof IsoDateTime.Type; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Construct a branded identifier. Enforces non-empty trimmed strings | ||
| 14 | */ | ||
| 15 | const makeEntityId = <Brand extends string>(brand: Brand) => | ||
| 16 | TrimmedNonEmptyString.pipe(Schema.brand(brand)); | ||
| 17 | |||
| 18 | export const ThreadId = makeEntityId("ThreadId"); | ||
| 19 | export type ThreadId = typeof ThreadId.Type; | ||
| 20 | export const ProjectId = makeEntityId("ProjectId"); | ||
| 21 | export type ProjectId = typeof ProjectId.Type; | ||
| 22 | export const CommandId = makeEntityId("CommandId"); | ||
| 23 | export type CommandId = typeof CommandId.Type; | ||
| 24 | export const EventId = makeEntityId("EventId"); | ||
| 25 | export type EventId = typeof EventId.Type; | ||
| 26 | export const MessageId = makeEntityId("MessageId"); | ||
| 27 | export type MessageId = typeof MessageId.Type; | ||
| 28 | export const TurnId = makeEntityId("TurnId"); | ||
| 29 | export type TurnId = typeof TurnId.Type; | ||
| 30 | |||
| 31 | export const ProviderSessionId = makeEntityId("ProviderSessionId"); | ||
| 32 | export type ProviderSessionId = typeof ProviderSessionId.Type; | ||
| 33 | export const ProviderThreadId = makeEntityId("ProviderThreadId"); | ||
| 34 | export type ProviderThreadId = typeof ProviderThreadId.Type; | ||
| 35 | export const ProviderTurnId = makeEntityId("ProviderTurnId"); | ||
| 36 | export type ProviderTurnId = typeof ProviderTurnId.Type; | ||
| 37 | export const ProviderItemId = makeEntityId("ProviderItemId"); | ||
| 38 | export type ProviderItemId = typeof ProviderItemId.Type; | ||
| 39 | export const ApprovalRequestId = makeEntityId("ApprovalRequestId"); | ||
| 40 | export type ApprovalRequestId = typeof ApprovalRequestId.Type; | ||
| 41 | export const CheckpointRef = makeEntityId("CheckpointRef"); | ||
| 42 | export type CheckpointRef = typeof CheckpointRef.Type; | ||
packages/contracts/src/editor.ts+2-1| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; | ||
| 2 | 3 | ||
| 3 | export const EDITORS = [ | 4 | export const EDITORS = [ |
| 4 | { id: "cursor", label: "Cursor", command: "cursor" }, | 5 | { id: "cursor", label: "Cursor", command: "cursor" }, |
| ... | @@ -9,7 +10,7 @@ export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); | ... | @@ -9,7 +10,7 @@ export const EditorId = Schema.Literals(EDITORS.map((e) => e.id)); |
| 9 | export type EditorId = typeof EditorId.Type; | 10 | export type EditorId = typeof EditorId.Type; |
| 10 | 11 | ||
| 11 | export const OpenInEditorInput = Schema.Struct({ | 12 | export const OpenInEditorInput = Schema.Struct({ |
| 12 | cwd: Schema.String, | 13 | cwd: TrimmedNonEmptyString, |
| 13 | editor: EditorId, | 14 | editor: EditorId, |
| 14 | }); | 15 | }); |
| 15 | export type OpenInEditorInput = typeof OpenInEditorInput.Type; | 16 | export type OpenInEditorInput = typeof OpenInEditorInput.Type; |
packages/contracts/src/git.ts+49-46| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | import { NonNegativeInt, PositiveInt, TrimmedNonEmptyString } from "./baseSchemas"; | ||
| 2 | 3 | ||
| 3 | const TrimmedNonEmptyString = Schema.Trimmed.check(Schema.isNonEmpty()); | 4 | const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString; |
| 4 | 5 | ||
| 5 | // Domain Types | 6 | // Domain Types |
| 6 | 7 | ||
| ... | @@ -19,101 +20,103 @@ export const GitPrStepStatus = Schema.Literals([ | ... | @@ -19,101 +20,103 @@ export const GitPrStepStatus = Schema.Literals([ |
| 19 | ]); | 20 | ]); |
| 20 | 21 | ||
| 21 | export const GitBranch = Schema.Struct({ | 22 | export const GitBranch = Schema.Struct({ |
| 22 | name: TrimmedNonEmptyString, | 23 | name: TrimmedNonEmptyStringSchema, |
| 23 | current: Schema.Boolean, | 24 | current: Schema.Boolean, |
| 24 | isDefault: Schema.Boolean, | 25 | isDefault: Schema.Boolean, |
| 25 | worktreePath: TrimmedNonEmptyString.pipe(Schema.NullOr), | 26 | worktreePath: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr), |
| 26 | }); | 27 | }); |
| 27 | export type GitBranch = typeof GitBranch.Type; | 28 | export type GitBranch = typeof GitBranch.Type; |
| 28 | 29 | ||
| 29 | export const GitWorktree = Schema.Struct({ | 30 | export const GitWorktree = Schema.Struct({ |
| 30 | path: TrimmedNonEmptyString, | 31 | path: TrimmedNonEmptyStringSchema, |
| 31 | branch: TrimmedNonEmptyString, | 32 | branch: TrimmedNonEmptyStringSchema, |
| 32 | }); | 33 | }); |
| 33 | export type GitWorktree = typeof GitWorktree.Type; | 34 | export type GitWorktree = typeof GitWorktree.Type; |
| 34 | 35 | ||
| 35 | // RPC Inputs | 36 | // RPC Inputs |
| 36 | 37 | ||
| 37 | export const GitStatusInput = Schema.Struct({ | 38 | export const GitStatusInput = Schema.Struct({ |
| 38 | cwd: TrimmedNonEmptyString, | 39 | cwd: TrimmedNonEmptyStringSchema, |
| 39 | }); | 40 | }); |
| 40 | export type GitStatusInput = typeof GitStatusInput.Type; | 41 | export type GitStatusInput = typeof GitStatusInput.Type; |
| 41 | 42 | ||
| 42 | export const GitPullInput = Schema.Struct({ | 43 | export const GitPullInput = Schema.Struct({ |
| 43 | cwd: TrimmedNonEmptyString, | 44 | cwd: TrimmedNonEmptyStringSchema, |
| 44 | }); | 45 | }); |
| 45 | export type GitPullInput = typeof GitPullInput.Type; | 46 | export type GitPullInput = typeof GitPullInput.Type; |
| 46 | 47 | ||
| 47 | export const GitRunStackedActionInput = Schema.Struct({ | 48 | export const GitRunStackedActionInput = Schema.Struct({ |
| 48 | cwd: TrimmedNonEmptyString, | 49 | cwd: TrimmedNonEmptyStringSchema, |
| 49 | action: GitStackedAction, | 50 | action: GitStackedAction, |
| 50 | commitMessage: Schema.optional(TrimmedNonEmptyString.check(Schema.isMaxLength(10_000))), | 51 | commitMessage: Schema.optional(TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(10_000))), |
| 51 | }); | 52 | }); |
| 52 | export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type; | 53 | export type GitRunStackedActionInput = typeof GitRunStackedActionInput.Type; |
| 53 | 54 | ||
| 54 | export const GitListBranchesInput = Schema.Struct({ | 55 | export const GitListBranchesInput = Schema.Struct({ |
| 55 | cwd: TrimmedNonEmptyString, | 56 | cwd: TrimmedNonEmptyStringSchema, |
| 56 | }); | 57 | }); |
| 57 | export type GitListBranchesInput = typeof GitListBranchesInput.Type; | 58 | export type GitListBranchesInput = typeof GitListBranchesInput.Type; |
| 58 | 59 | ||
| 59 | export const GitCreateWorktreeInput = Schema.Struct({ | 60 | export const GitCreateWorktreeInput = Schema.Struct({ |
| 60 | cwd: TrimmedNonEmptyString, | 61 | cwd: TrimmedNonEmptyStringSchema, |
| 61 | branch: TrimmedNonEmptyString, | 62 | branch: TrimmedNonEmptyStringSchema, |
| 62 | newBranch: TrimmedNonEmptyString, | 63 | newBranch: TrimmedNonEmptyStringSchema, |
| 63 | path: TrimmedNonEmptyString.pipe(Schema.NullOr), | 64 | path: Schema.NullOr(TrimmedNonEmptyStringSchema), |
| 64 | }); | 65 | }); |
| 65 | export type GitCreateWorktreeInput = typeof GitCreateWorktreeInput.Type; | 66 | export type GitCreateWorktreeInput = typeof GitCreateWorktreeInput.Type; |
| 66 | 67 | ||
| 67 | export const GitRemoveWorktreeInput = Schema.Struct({ | 68 | export const GitRemoveWorktreeInput = Schema.Struct({ |
| 68 | cwd: TrimmedNonEmptyString, | 69 | cwd: TrimmedNonEmptyStringSchema, |
| 69 | path: TrimmedNonEmptyString, | 70 | path: TrimmedNonEmptyStringSchema, |
| 70 | force: Schema.optional(Schema.Boolean), | 71 | force: Schema.optional(Schema.Boolean), |
| 71 | }); | 72 | }); |
| 72 | export type GitRemoveWorktreeInput = typeof GitRemoveWorktreeInput.Type; | 73 | export type GitRemoveWorktreeInput = typeof GitRemoveWorktreeInput.Type; |
| 73 | 74 | ||
| 74 | export const GitCreateBranchInput = Schema.Struct({ | 75 | export const GitCreateBranchInput = Schema.Struct({ |
| 75 | cwd: TrimmedNonEmptyString, | 76 | cwd: TrimmedNonEmptyStringSchema, |
| 76 | branch: TrimmedNonEmptyString, | 77 | branch: TrimmedNonEmptyStringSchema, |
| 77 | }); | 78 | }); |
| 78 | export type GitCreateBranchInput = typeof GitCreateBranchInput.Type; | 79 | export type GitCreateBranchInput = typeof GitCreateBranchInput.Type; |
| 79 | 80 | ||
| 80 | export const GitCheckoutInput = Schema.Struct({ | 81 | export const GitCheckoutInput = Schema.Struct({ |
| 81 | cwd: TrimmedNonEmptyString, | 82 | cwd: TrimmedNonEmptyStringSchema, |
| 82 | branch: TrimmedNonEmptyString, | 83 | branch: TrimmedNonEmptyStringSchema, |
| 83 | }); | 84 | }); |
| 84 | export type GitCheckoutInput = typeof GitCheckoutInput.Type; | 85 | export type GitCheckoutInput = typeof GitCheckoutInput.Type; |
| 85 | 86 | ||
| 86 | export const GitInitInput = Schema.Struct({ | 87 | export const GitInitInput = Schema.Struct({ |
| 87 | cwd: TrimmedNonEmptyString, | 88 | cwd: TrimmedNonEmptyStringSchema, |
| 88 | }); | 89 | }); |
| 89 | export type GitInitInput = typeof GitInitInput.Type; | 90 | export type GitInitInput = typeof GitInitInput.Type; |
| 90 | 91 | ||
| 91 | // RPC Results | 92 | // RPC Results |
| 92 | 93 | ||
| 93 | export const GitStatusResult = Schema.Struct({ | 94 | export const GitStatusResult = Schema.Struct({ |
| 94 | branch: TrimmedNonEmptyString.pipe(Schema.NullOr), | 95 | branch: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr), |
| 95 | hasWorkingTreeChanges: Schema.Boolean, | 96 | hasWorkingTreeChanges: Schema.Boolean, |
| 96 | workingTree: Schema.Struct({ | 97 | workingTree: Schema.Struct({ |
| 97 | files: Schema.Array( | 98 | files: Schema.Array( |
| 98 | Schema.Struct({ | 99 | Schema.Struct({ |
| 99 | path: TrimmedNonEmptyString, | 100 | path: TrimmedNonEmptyStringSchema, |
| 100 | insertions: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), | 101 | insertions: NonNegativeInt, |
| 101 | deletions: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), | 102 | deletions: NonNegativeInt, |
| 102 | }), | 103 | }), |
| 103 | ), | 104 | ), |
| 104 | insertions: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), | 105 | insertions: NonNegativeInt, |
| 105 | deletions: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), | 106 | deletions: NonNegativeInt, |
| 106 | }), | 107 | }), |
| 107 | hasUpstream: Schema.Boolean, | 108 | hasUpstream: Schema.Boolean, |
| 108 | aheadCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), | 109 | aheadCount: NonNegativeInt, |
| 109 | behindCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)), | 110 | behindCount: NonNegativeInt, |
| 110 | openPr: Schema.Struct({ | 111 | openPr: Schema.NullOr( |
| 111 | number: Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)), | 112 | Schema.Struct({ |
| 112 | title: TrimmedNonEmptyString, | 113 | number: PositiveInt, |
| 113 | url: Schema.String, | 114 | title: TrimmedNonEmptyStringSchema, |
| 114 | baseBranch: TrimmedNonEmptyString, | 115 | url: Schema.String, |
| 115 | headBranch: TrimmedNonEmptyString, | 116 | baseBranch: TrimmedNonEmptyStringSchema, |
| 116 | }).pipe(Schema.NullOr), | 117 | headBranch: TrimmedNonEmptyStringSchema, |
| 118 | }), | ||
| 119 | ), | ||
| 117 | }); | 120 | }); |
| 118 | export type GitStatusResult = typeof GitStatusResult.Type; | 121 | export type GitStatusResult = typeof GitStatusResult.Type; |
| 119 | 122 | ||
| ... | @@ -132,29 +135,29 @@ export const GitRunStackedActionResult = Schema.Struct({ | ... | @@ -132,29 +135,29 @@ export const GitRunStackedActionResult = Schema.Struct({ |
| 132 | action: GitStackedAction, | 135 | action: GitStackedAction, |
| 133 | commit: Schema.Struct({ | 136 | commit: Schema.Struct({ |
| 134 | status: GitCommitStepStatus, | 137 | status: GitCommitStepStatus, |
| 135 | commitSha: Schema.optional(TrimmedNonEmptyString), | 138 | commitSha: Schema.optional(TrimmedNonEmptyStringSchema), |
| 136 | subject: Schema.optional(TrimmedNonEmptyString), | 139 | subject: Schema.optional(TrimmedNonEmptyStringSchema), |
| 137 | }), | 140 | }), |
| 138 | push: Schema.Struct({ | 141 | push: Schema.Struct({ |
| 139 | status: GitPushStepStatus, | 142 | status: GitPushStepStatus, |
| 140 | branch: Schema.optional(TrimmedNonEmptyString), | 143 | branch: Schema.optional(TrimmedNonEmptyStringSchema), |
| 141 | upstreamBranch: Schema.optional(TrimmedNonEmptyString), | 144 | upstreamBranch: Schema.optional(TrimmedNonEmptyStringSchema), |
| 142 | setUpstream: Schema.optional(Schema.Boolean), | 145 | setUpstream: Schema.optional(Schema.Boolean), |
| 143 | }), | 146 | }), |
| 144 | pr: Schema.Struct({ | 147 | pr: Schema.Struct({ |
| 145 | status: GitPrStepStatus, | 148 | status: GitPrStepStatus, |
| 146 | url: Schema.optional(Schema.String), | 149 | url: Schema.optional(Schema.String), |
| 147 | number: Schema.optional(Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))), | 150 | number: Schema.optional(PositiveInt), |
| 148 | baseBranch: Schema.optional(TrimmedNonEmptyString), | 151 | baseBranch: Schema.optional(TrimmedNonEmptyStringSchema), |
| 149 | headBranch: Schema.optional(TrimmedNonEmptyString), | 152 | headBranch: Schema.optional(TrimmedNonEmptyStringSchema), |
| 150 | title: Schema.optional(TrimmedNonEmptyString), | 153 | title: Schema.optional(TrimmedNonEmptyStringSchema), |
| 151 | }), | 154 | }), |
| 152 | }); | 155 | }); |
| 153 | export type GitRunStackedActionResult = typeof GitRunStackedActionResult.Type; | 156 | export type GitRunStackedActionResult = typeof GitRunStackedActionResult.Type; |
| 154 | 157 | ||
| 155 | export const GitPullResult = Schema.Struct({ | 158 | export const GitPullResult = Schema.Struct({ |
| 156 | status: Schema.Literals(["pulled", "skipped_up_to_date"]), | 159 | status: Schema.Literals(["pulled", "skipped_up_to_date"]), |
| 157 | branch: TrimmedNonEmptyString, | 160 | branch: TrimmedNonEmptyStringSchema, |
| 158 | upstreamBranch: TrimmedNonEmptyString.pipe(Schema.NullOr), | 161 | upstreamBranch: TrimmedNonEmptyStringSchema.pipe(Schema.NullOr), |
| 159 | }); | 162 | }); |
| 160 | export type GitPullResult = typeof GitPullResult.Type; | 163 | export type GitPullResult = typeof GitPullResult.Type; |
packages/contracts/src/index.ts+1| ... | @@ -1,3 +1,4 @@ | ... | @@ -1,3 +1,4 @@ |
| 1 | export * from "./baseSchemas"; | ||
| 1 | export * from "./ipc"; | 2 | export * from "./ipc"; |
| 2 | export * from "./terminal"; | 3 | export * from "./terminal"; |
| 3 | export * from "./provider"; | 4 | export * from "./provider"; |
packages/contracts/src/keybindings.ts+3-2| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | import { TrimmedString } from "./baseSchemas"; | ||
| 2 | 3 | ||
| 3 | export const MAX_KEYBINDING_COMMAND_LENGTH = 96; | 4 | export const MAX_KEYBINDING_COMMAND_LENGTH = 96; |
| 4 | export const MAX_KEYBINDING_VALUE_LENGTH = 64; | 5 | export const MAX_KEYBINDING_VALUE_LENGTH = 64; |
| ... | @@ -33,13 +34,13 @@ export const KeybindingCommand = Schema.Union([ | ... | @@ -33,13 +34,13 @@ export const KeybindingCommand = Schema.Union([ |
| 33 | ]); | 34 | ]); |
| 34 | export type KeybindingCommand = typeof KeybindingCommand.Type; | 35 | export type KeybindingCommand = typeof KeybindingCommand.Type; |
| 35 | 36 | ||
| 36 | export const KeybindingValue = Schema.Trim.check( | 37 | export const KeybindingValue = TrimmedString.check( |
| 37 | Schema.isMinLength(1), | 38 | Schema.isMinLength(1), |
| 38 | Schema.isMaxLength(MAX_KEYBINDING_VALUE_LENGTH), | 39 | Schema.isMaxLength(MAX_KEYBINDING_VALUE_LENGTH), |
| 39 | ); | 40 | ); |
| 40 | export type KeybindingValue = typeof KeybindingValue.Type; | 41 | export type KeybindingValue = typeof KeybindingValue.Type; |
| 41 | 42 | ||
| 42 | export const KeybindingWhen = Schema.Trim.check( | 43 | export const KeybindingWhen = TrimmedString.check( |
| 43 | Schema.isMinLength(1), | 44 | Schema.isMinLength(1), |
| 44 | Schema.isMaxLength(MAX_KEYBINDING_WHEN_LENGTH), | 45 | Schema.isMaxLength(MAX_KEYBINDING_WHEN_LENGTH), |
| 45 | ); | 46 | ); |
packages/contracts/src/orchestration.test.ts+41-1| ... | @@ -1,10 +1,15 @@ | ... | @@ -1,10 +1,15 @@ |
| 1 | import { assert, it } from "@effect/vitest"; | 1 | import { assert, it } from "@effect/vitest"; |
| 2 | import { Effect, Schema } from "effect"; | 2 | import { Effect, Schema } from "effect"; |
| 3 | 3 | ||
| 4 | import { OrchestrationGetTurnDiffInput, ThreadTurnDiff } from "./orchestration"; | 4 | import { |
| 5 | OrchestrationGetTurnDiffInput, | ||
| 6 | ProjectCreateCommand, | ||
| 7 | ThreadTurnDiff, | ||
| 8 | } from "./orchestration"; | ||
| 5 | 9 | ||
| 6 | const decodeTurnDiffInput = Schema.decodeUnknownEffect(OrchestrationGetTurnDiffInput); | 10 | const decodeTurnDiffInput = Schema.decodeUnknownEffect(OrchestrationGetTurnDiffInput); |
| 7 | const decodeThreadTurnDiff = Schema.decodeUnknownEffect(ThreadTurnDiff); | 11 | const decodeThreadTurnDiff = Schema.decodeUnknownEffect(ThreadTurnDiff); |
| 12 | const decodeProjectCreateCommand = Schema.decodeUnknownEffect(ProjectCreateCommand); | ||
| 8 | 13 | ||
| 9 | it.effect("parses turn diff input when fromTurnCount <= toTurnCount", () => | 14 | it.effect("parses turn diff input when fromTurnCount <= toTurnCount", () => |
| 10 | Effect.gen(function* () { | 15 | Effect.gen(function* () { |
| ... | @@ -44,3 +49,38 @@ it.effect("rejects thread turn diff when fromTurnCount > toTurnCount", () => | ... | @@ -44,3 +49,38 @@ it.effect("rejects thread turn diff when fromTurnCount > toTurnCount", () => |
| 44 | assert.strictEqual(result._tag, "Failure"); | 49 | assert.strictEqual(result._tag, "Failure"); |
| 45 | }), | 50 | }), |
| 46 | ); | 51 | ); |
| 52 | |||
| 53 | it.effect("trims branded ids and command string fields at decode boundaries", () => | ||
| 54 | Effect.gen(function* () { | ||
| 55 | const parsed = yield* decodeProjectCreateCommand({ | ||
| 56 | type: "project.create", | ||
| 57 | commandId: " cmd-1 ", | ||
| 58 | projectId: " project-1 ", | ||
| 59 | title: " Project Title ", | ||
| 60 | workspaceRoot: " /tmp/workspace ", | ||
| 61 | defaultModel: " gpt-5.2 ", | ||
| 62 | createdAt: "2026-01-01T00:00:00.000Z", | ||
| 63 | }); | ||
| 64 | assert.strictEqual(parsed.commandId, "cmd-1"); | ||
| 65 | assert.strictEqual(parsed.projectId, "project-1"); | ||
| 66 | assert.strictEqual(parsed.title, "Project Title"); | ||
| 67 | assert.strictEqual(parsed.workspaceRoot, "/tmp/workspace"); | ||
| 68 | assert.strictEqual(parsed.defaultModel, "gpt-5.2"); | ||
| 69 | }), | ||
| 70 | ); | ||
| 71 | |||
| 72 | it.effect("rejects command fields that become empty after trim", () => | ||
| 73 | Effect.gen(function* () { | ||
| 74 | const result = yield* Effect.exit( | ||
| 75 | decodeProjectCreateCommand({ | ||
| 76 | type: "project.create", | ||
| 77 | commandId: "cmd-1", | ||
| 78 | projectId: "project-1", | ||
| 79 | title: " ", | ||
| 80 | workspaceRoot: "/tmp/workspace", | ||
| 81 | createdAt: "2026-01-01T00:00:00.000Z", | ||
| 82 | }), | ||
| 83 | ); | ||
| 84 | assert.strictEqual(result._tag, "Failure"); | ||
| 85 | }), | ||
| 86 | ); |
packages/contracts/src/orchestration.ts+69-90| ... | @@ -1,4 +1,21 @@ | ... | @@ -1,4 +1,21 @@ |
| 1 | import { Option, Schema, SchemaIssue, Struct } from "effect"; | 1 | import { Option, Schema, SchemaIssue, Struct } from "effect"; |
| 2 | import { | ||
| 3 | ApprovalRequestId, | ||
| 4 | CheckpointRef, | ||
| 5 | CommandId, | ||
| 6 | EventId, | ||
| 7 | IsoDateTime, | ||
| 8 | MessageId, | ||
| 9 | NonNegativeInt, | ||
| 10 | ProjectId, | ||
| 11 | ProviderItemId, | ||
| 12 | ProviderSessionId, | ||
| 13 | ProviderThreadId, | ||
| 14 | ProviderTurnId, | ||
| 15 | ThreadId, | ||
| 16 | TrimmedNonEmptyString, | ||
| 17 | TurnId, | ||
| 18 | } from "./baseSchemas"; | ||
| 2 | 19 | ||
| 3 | export const ORCHESTRATION_WS_METHODS = { | 20 | export const ORCHESTRATION_WS_METHODS = { |
| 4 | getSnapshot: "orchestration.getSnapshot", | 21 | getSnapshot: "orchestration.getSnapshot", |
| ... | @@ -12,35 +29,6 @@ export const ORCHESTRATION_WS_CHANNELS = { | ... | @@ -12,35 +29,6 @@ export const ORCHESTRATION_WS_CHANNELS = { |
| 12 | domainEvent: "orchestration.domainEvent", | 29 | domainEvent: "orchestration.domainEvent", |
| 13 | } as const; | 30 | } as const; |
| 14 | 31 | ||
| 15 | export const IsoDateTime = Schema.String; | ||
| 16 | export type IsoDateTime = typeof IsoDateTime.Type; | ||
| 17 | |||
| 18 | export const ThreadId = Schema.String.pipe(Schema.brand("ThreadId")); | ||
| 19 | export type ThreadId = typeof ThreadId.Type; | ||
| 20 | export const ProjectId = Schema.String.pipe(Schema.brand("ProjectId")); | ||
| 21 | export type ProjectId = typeof ProjectId.Type; | ||
| 22 | export const CommandId = Schema.String.pipe(Schema.brand("CommandId")); | ||
| 23 | export type CommandId = typeof CommandId.Type; | ||
| 24 | export const EventId = Schema.String.pipe(Schema.brand("EventId")); | ||
| 25 | export type EventId = typeof EventId.Type; | ||
| 26 | export const MessageId = Schema.String.pipe(Schema.brand("MessageId")); | ||
| 27 | export type MessageId = typeof MessageId.Type; | ||
| 28 | export const TurnId = Schema.String.pipe(Schema.brand("TurnId")); | ||
| 29 | export type TurnId = typeof TurnId.Type; | ||
| 30 | |||
| 31 | export const ProviderSessionId = Schema.String.pipe(Schema.brand("ProviderSessionId")); | ||
| 32 | export type ProviderSessionId = typeof ProviderSessionId.Type; | ||
| 33 | export const ProviderThreadId = Schema.String.pipe(Schema.brand("ProviderThreadId")); | ||
| 34 | export type ProviderThreadId = typeof ProviderThreadId.Type; | ||
| 35 | export const ProviderTurnId = Schema.String.pipe(Schema.brand("ProviderTurnId")); | ||
| 36 | export type ProviderTurnId = typeof ProviderTurnId.Type; | ||
| 37 | export const ProviderItemId = Schema.String.pipe(Schema.brand("ProviderItemId")); | ||
| 38 | export type ProviderItemId = typeof ProviderItemId.Type; | ||
| 39 | export const ApprovalRequestId = Schema.String.pipe(Schema.brand("ApprovalRequestId")); | ||
| 40 | export type ApprovalRequestId = typeof ApprovalRequestId.Type; | ||
| 41 | export const CheckpointRef = Schema.String.pipe(Schema.brand("CheckpointRef")); | ||
| 42 | export type CheckpointRef = typeof CheckpointRef.Type; | ||
| 43 | |||
| 44 | export const ProviderKind = Schema.Literals(["codex", "claudeCode"]); | 32 | export const ProviderKind = Schema.Literals(["codex", "claudeCode"]); |
| 45 | export type ProviderKind = typeof ProviderKind.Type; | 33 | export type ProviderKind = typeof ProviderKind.Type; |
| 46 | export const ProviderApprovalPolicy = Schema.Literals([ | 34 | export const ProviderApprovalPolicy = Schema.Literals([ |
| ... | @@ -75,21 +63,12 @@ export const PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS = 14_000_000; | ... | @@ -75,21 +63,12 @@ export const PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS = 14_000_000; |
| 75 | export const CorrelationId = CommandId; | 63 | export const CorrelationId = CommandId; |
| 76 | export type CorrelationId = typeof CorrelationId.Type; | 64 | export type CorrelationId = typeof CorrelationId.Type; |
| 77 | 65 | ||
| 78 | export const NonNegativeInt = Schema.Number.check(Schema.isInt()).check( | ||
| 79 | Schema.isGreaterThanOrEqualTo(0), | ||
| 80 | ); | ||
| 81 | |||
| 82 | const TrimmedNonEmptyStringSchema = Schema.Trimmed.check(Schema.isNonEmpty()); | ||
| 83 | |||
| 84 | export const ChatImageAttachment = Schema.Struct({ | 66 | export const ChatImageAttachment = Schema.Struct({ |
| 85 | type: Schema.Literal("image"), | 67 | type: Schema.Literal("image"), |
| 86 | name: TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(255)), | 68 | name: TrimmedNonEmptyString.check(Schema.isMaxLength(255)), |
| 87 | mimeType: TrimmedNonEmptyStringSchema.check( | 69 | mimeType: TrimmedNonEmptyString.check(Schema.isMaxLength(100), Schema.isPattern(/^image\//i)), |
| 88 | Schema.isMaxLength(100), | ||
| 89 | Schema.isPattern(/^image\//i), | ||
| 90 | ), | ||
| 91 | sizeBytes: NonNegativeInt.check(Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES)), | 70 | sizeBytes: NonNegativeInt.check(Schema.isLessThanOrEqualTo(PROVIDER_SEND_TURN_MAX_IMAGE_BYTES)), |
| 92 | dataUrl: TrimmedNonEmptyStringSchema.check( | 71 | dataUrl: TrimmedNonEmptyString.check( |
| 93 | Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS), | 72 | Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_IMAGE_DATA_URL_CHARS), |
| 94 | ), | 73 | ), |
| 95 | }); | 74 | }); |
| ... | @@ -109,9 +88,9 @@ export const ProjectScriptIcon = Schema.Literals([ | ... | @@ -109,9 +88,9 @@ export const ProjectScriptIcon = Schema.Literals([ |
| 109 | export type ProjectScriptIcon = typeof ProjectScriptIcon.Type; | 88 | export type ProjectScriptIcon = typeof ProjectScriptIcon.Type; |
| 110 | 89 | ||
| 111 | export const ProjectScript = Schema.Struct({ | 90 | export const ProjectScript = Schema.Struct({ |
| 112 | id: Schema.String, | 91 | id: TrimmedNonEmptyString, |
| 113 | name: Schema.String, | 92 | name: TrimmedNonEmptyString, |
| 114 | command: Schema.String, | 93 | command: TrimmedNonEmptyString, |
| 115 | icon: ProjectScriptIcon, | 94 | icon: ProjectScriptIcon, |
| 116 | runOnWorktreeCreate: Schema.Boolean, | 95 | runOnWorktreeCreate: Schema.Boolean, |
| 117 | }); | 96 | }); |
| ... | @@ -119,9 +98,9 @@ export type ProjectScript = typeof ProjectScript.Type; | ... | @@ -119,9 +98,9 @@ export type ProjectScript = typeof ProjectScript.Type; |
| 119 | 98 | ||
| 120 | export const OrchestrationProject = Schema.Struct({ | 99 | export const OrchestrationProject = Schema.Struct({ |
| 121 | id: ProjectId, | 100 | id: ProjectId, |
| 122 | title: Schema.String, | 101 | title: TrimmedNonEmptyString, |
| 123 | workspaceRoot: Schema.String, | 102 | workspaceRoot: TrimmedNonEmptyString, |
| 124 | defaultModel: Schema.NullOr(Schema.String), | 103 | defaultModel: Schema.NullOr(TrimmedNonEmptyString), |
| 125 | scripts: Schema.Array(ProjectScript), | 104 | scripts: Schema.Array(ProjectScript), |
| 126 | createdAt: IsoDateTime, | 105 | createdAt: IsoDateTime, |
| 127 | updatedAt: IsoDateTime, | 106 | updatedAt: IsoDateTime, |
| ... | @@ -158,18 +137,18 @@ export type OrchestrationSessionStatus = typeof OrchestrationSessionStatus.Type; | ... | @@ -158,18 +137,18 @@ export type OrchestrationSessionStatus = typeof OrchestrationSessionStatus.Type; |
| 158 | export const OrchestrationSession = Schema.Struct({ | 137 | export const OrchestrationSession = Schema.Struct({ |
| 159 | threadId: ThreadId, | 138 | threadId: ThreadId, |
| 160 | status: OrchestrationSessionStatus, | 139 | status: OrchestrationSessionStatus, |
| 161 | providerName: Schema.NullOr(Schema.String), | 140 | providerName: Schema.NullOr(TrimmedNonEmptyString), |
| 162 | providerSessionId: Schema.NullOr(ProviderSessionId), | 141 | providerSessionId: Schema.NullOr(ProviderSessionId), |
| 163 | providerThreadId: Schema.NullOr(ProviderThreadId), | 142 | providerThreadId: Schema.NullOr(ProviderThreadId), |
| 164 | activeTurnId: Schema.NullOr(TurnId), | 143 | activeTurnId: Schema.NullOr(TurnId), |
| 165 | lastError: Schema.NullOr(Schema.String), | 144 | lastError: Schema.NullOr(TrimmedNonEmptyString), |
| 166 | updatedAt: IsoDateTime, | 145 | updatedAt: IsoDateTime, |
| 167 | }); | 146 | }); |
| 168 | export type OrchestrationSession = typeof OrchestrationSession.Type; | 147 | export type OrchestrationSession = typeof OrchestrationSession.Type; |
| 169 | 148 | ||
| 170 | export const OrchestrationCheckpointFile = Schema.Struct({ | 149 | export const OrchestrationCheckpointFile = Schema.Struct({ |
| 171 | path: Schema.String, | 150 | path: TrimmedNonEmptyString, |
| 172 | kind: Schema.String, | 151 | kind: TrimmedNonEmptyString, |
| 173 | additions: NonNegativeInt, | 152 | additions: NonNegativeInt, |
| 174 | deletions: NonNegativeInt, | 153 | deletions: NonNegativeInt, |
| 175 | }); | 154 | }); |
| ... | @@ -200,8 +179,8 @@ export type OrchestrationThreadActivityTone = typeof OrchestrationThreadActivity | ... | @@ -200,8 +179,8 @@ export type OrchestrationThreadActivityTone = typeof OrchestrationThreadActivity |
| 200 | export const OrchestrationThreadActivity = Schema.Struct({ | 179 | export const OrchestrationThreadActivity = Schema.Struct({ |
| 201 | id: EventId, | 180 | id: EventId, |
| 202 | tone: OrchestrationThreadActivityTone, | 181 | tone: OrchestrationThreadActivityTone, |
| 203 | kind: Schema.String, | 182 | kind: TrimmedNonEmptyString, |
| 204 | summary: Schema.String, | 183 | summary: TrimmedNonEmptyString, |
| 205 | payload: Schema.Unknown, | 184 | payload: Schema.Unknown, |
| 206 | turnId: Schema.NullOr(TurnId), | 185 | turnId: Schema.NullOr(TurnId), |
| 207 | createdAt: IsoDateTime, | 186 | createdAt: IsoDateTime, |
| ... | @@ -211,10 +190,10 @@ export type OrchestrationThreadActivity = typeof OrchestrationThreadActivity.Typ | ... | @@ -211,10 +190,10 @@ export type OrchestrationThreadActivity = typeof OrchestrationThreadActivity.Typ |
| 211 | export const OrchestrationThread = Schema.Struct({ | 190 | export const OrchestrationThread = Schema.Struct({ |
| 212 | id: ThreadId, | 191 | id: ThreadId, |
| 213 | projectId: ProjectId, | 192 | projectId: ProjectId, |
| 214 | title: Schema.String, | 193 | title: TrimmedNonEmptyString, |
| 215 | model: Schema.String, | 194 | model: TrimmedNonEmptyString, |
| 216 | branch: Schema.NullOr(Schema.String), | 195 | branch: Schema.NullOr(TrimmedNonEmptyString), |
| 217 | worktreePath: Schema.NullOr(Schema.String), | 196 | worktreePath: Schema.NullOr(TrimmedNonEmptyString), |
| 218 | latestTurnId: Schema.NullOr(TurnId), | 197 | latestTurnId: Schema.NullOr(TurnId), |
| 219 | createdAt: IsoDateTime, | 198 | createdAt: IsoDateTime, |
| 220 | updatedAt: IsoDateTime, | 199 | updatedAt: IsoDateTime, |
| ... | @@ -238,9 +217,9 @@ export const ProjectCreateCommand = Schema.Struct({ | ... | @@ -238,9 +217,9 @@ export const ProjectCreateCommand = Schema.Struct({ |
| 238 | type: Schema.Literal("project.create"), | 217 | type: Schema.Literal("project.create"), |
| 239 | commandId: CommandId, | 218 | commandId: CommandId, |
| 240 | projectId: ProjectId, | 219 | projectId: ProjectId, |
| 241 | title: Schema.String, | 220 | title: TrimmedNonEmptyString, |
| 242 | workspaceRoot: Schema.String, | 221 | workspaceRoot: TrimmedNonEmptyString, |
| 243 | defaultModel: Schema.optional(Schema.String), | 222 | defaultModel: Schema.optional(TrimmedNonEmptyString), |
| 244 | createdAt: IsoDateTime, | 223 | createdAt: IsoDateTime, |
| 245 | }); | 224 | }); |
| 246 | 225 | ||
| ... | @@ -248,9 +227,9 @@ export const ProjectMetaUpdateCommand = Schema.Struct({ | ... | @@ -248,9 +227,9 @@ export const ProjectMetaUpdateCommand = Schema.Struct({ |
| 248 | type: Schema.Literal("project.meta.update"), | 227 | type: Schema.Literal("project.meta.update"), |
| 249 | commandId: CommandId, | 228 | commandId: CommandId, |
| 250 | projectId: ProjectId, | 229 | projectId: ProjectId, |
| 251 | title: Schema.optional(Schema.String), | 230 | title: Schema.optional(TrimmedNonEmptyString), |
| 252 | workspaceRoot: Schema.optional(Schema.String), | 231 | workspaceRoot: Schema.optional(TrimmedNonEmptyString), |
| 253 | defaultModel: Schema.optional(Schema.String), | 232 | defaultModel: Schema.optional(TrimmedNonEmptyString), |
| 254 | scripts: Schema.optional(Schema.Array(ProjectScript)), | 233 | scripts: Schema.optional(Schema.Array(ProjectScript)), |
| 255 | }); | 234 | }); |
| 256 | 235 | ||
| ... | @@ -265,10 +244,10 @@ export const ThreadCreateCommand = Schema.Struct({ | ... | @@ -265,10 +244,10 @@ export const ThreadCreateCommand = Schema.Struct({ |
| 265 | commandId: CommandId, | 244 | commandId: CommandId, |
| 266 | threadId: ThreadId, | 245 | threadId: ThreadId, |
| 267 | projectId: ProjectId, | 246 | projectId: ProjectId, |
| 268 | title: Schema.String, | 247 | title: TrimmedNonEmptyString, |
| 269 | model: Schema.String, | 248 | model: TrimmedNonEmptyString, |
| 270 | branch: Schema.NullOr(Schema.String), | 249 | branch: Schema.NullOr(TrimmedNonEmptyString), |
| 271 | worktreePath: Schema.NullOr(Schema.String), | 250 | worktreePath: Schema.NullOr(TrimmedNonEmptyString), |
| 272 | createdAt: IsoDateTime, | 251 | createdAt: IsoDateTime, |
| 273 | }); | 252 | }); |
| 274 | 253 | ||
| ... | @@ -282,10 +261,10 @@ export const ThreadMetaUpdateCommand = Schema.Struct({ | ... | @@ -282,10 +261,10 @@ export const ThreadMetaUpdateCommand = Schema.Struct({ |
| 282 | type: Schema.Literal("thread.meta.update"), | 261 | type: Schema.Literal("thread.meta.update"), |
| 283 | commandId: CommandId, | 262 | commandId: CommandId, |
| 284 | threadId: ThreadId, | 263 | threadId: ThreadId, |
| 285 | title: Schema.optional(Schema.String), | 264 | title: Schema.optional(TrimmedNonEmptyString), |
| 286 | model: Schema.optional(Schema.String), | 265 | model: Schema.optional(TrimmedNonEmptyString), |
| 287 | branch: Schema.optional(Schema.NullOr(Schema.String)), | 266 | branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), |
| 288 | worktreePath: Schema.optional(Schema.NullOr(Schema.String)), | 267 | worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), |
| 289 | }); | 268 | }); |
| 290 | 269 | ||
| 291 | export const ThreadTurnStartCommand = Schema.Struct({ | 270 | export const ThreadTurnStartCommand = Schema.Struct({ |
| ... | @@ -298,8 +277,8 @@ export const ThreadTurnStartCommand = Schema.Struct({ | ... | @@ -298,8 +277,8 @@ export const ThreadTurnStartCommand = Schema.Struct({ |
| 298 | text: Schema.String, | 277 | text: Schema.String, |
| 299 | attachments: Schema.Array(ChatAttachment), | 278 | attachments: Schema.Array(ChatAttachment), |
| 300 | }), | 279 | }), |
| 301 | model: Schema.optional(Schema.String), | 280 | model: Schema.optional(TrimmedNonEmptyString), |
| 302 | effort: Schema.optional(Schema.String), | 281 | effort: Schema.optional(TrimmedNonEmptyString), |
| 303 | createdAt: IsoDateTime, | 282 | createdAt: IsoDateTime, |
| 304 | }); | 283 | }); |
| 305 | 284 | ||
| ... | @@ -449,9 +428,9 @@ export const OrchestrationActorKind = Schema.Literals(["client", "server", "prov | ... | @@ -449,9 +428,9 @@ export const OrchestrationActorKind = Schema.Literals(["client", "server", "prov |
| 449 | 428 | ||
| 450 | export const ProjectCreatedPayload = Schema.Struct({ | 429 | export const ProjectCreatedPayload = Schema.Struct({ |
| 451 | projectId: ProjectId, | 430 | projectId: ProjectId, |
| 452 | title: Schema.String, | 431 | title: TrimmedNonEmptyString, |
| 453 | workspaceRoot: Schema.String, | 432 | workspaceRoot: TrimmedNonEmptyString, |
| 454 | defaultModel: Schema.NullOr(Schema.String), | 433 | defaultModel: Schema.NullOr(TrimmedNonEmptyString), |
| 455 | scripts: Schema.Array(ProjectScript), | 434 | scripts: Schema.Array(ProjectScript), |
| 456 | createdAt: IsoDateTime, | 435 | createdAt: IsoDateTime, |
| 457 | updatedAt: IsoDateTime, | 436 | updatedAt: IsoDateTime, |
| ... | @@ -459,9 +438,9 @@ export const ProjectCreatedPayload = Schema.Struct({ | ... | @@ -459,9 +438,9 @@ export const ProjectCreatedPayload = Schema.Struct({ |
| 459 | 438 | ||
| 460 | export const ProjectMetaUpdatedPayload = Schema.Struct({ | 439 | export const ProjectMetaUpdatedPayload = Schema.Struct({ |
| 461 | projectId: ProjectId, | 440 | projectId: ProjectId, |
| 462 | title: Schema.optional(Schema.String), | 441 | title: Schema.optional(TrimmedNonEmptyString), |
| 463 | workspaceRoot: Schema.optional(Schema.String), | 442 | workspaceRoot: Schema.optional(TrimmedNonEmptyString), |
| 464 | defaultModel: Schema.optional(Schema.NullOr(Schema.String)), | 443 | defaultModel: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), |
| 465 | scripts: Schema.optional(Schema.Array(ProjectScript)), | 444 | scripts: Schema.optional(Schema.Array(ProjectScript)), |
| 466 | updatedAt: IsoDateTime, | 445 | updatedAt: IsoDateTime, |
| 467 | }); | 446 | }); |
| ... | @@ -474,10 +453,10 @@ export const ProjectDeletedPayload = Schema.Struct({ | ... | @@ -474,10 +453,10 @@ export const ProjectDeletedPayload = Schema.Struct({ |
| 474 | export const ThreadCreatedPayload = Schema.Struct({ | 453 | export const ThreadCreatedPayload = Schema.Struct({ |
| 475 | threadId: ThreadId, | 454 | threadId: ThreadId, |
| 476 | projectId: ProjectId, | 455 | projectId: ProjectId, |
| 477 | title: Schema.String, | 456 | title: TrimmedNonEmptyString, |
| 478 | model: Schema.String, | 457 | model: TrimmedNonEmptyString, |
| 479 | branch: Schema.NullOr(Schema.String), | 458 | branch: Schema.NullOr(TrimmedNonEmptyString), |
| 480 | worktreePath: Schema.NullOr(Schema.String), | 459 | worktreePath: Schema.NullOr(TrimmedNonEmptyString), |
| 481 | createdAt: IsoDateTime, | 460 | createdAt: IsoDateTime, |
| 482 | updatedAt: IsoDateTime, | 461 | updatedAt: IsoDateTime, |
| 483 | }); | 462 | }); |
| ... | @@ -489,10 +468,10 @@ export const ThreadDeletedPayload = Schema.Struct({ | ... | @@ -489,10 +468,10 @@ export const ThreadDeletedPayload = Schema.Struct({ |
| 489 | 468 | ||
| 490 | export const ThreadMetaUpdatedPayload = Schema.Struct({ | 469 | export const ThreadMetaUpdatedPayload = Schema.Struct({ |
| 491 | threadId: ThreadId, | 470 | threadId: ThreadId, |
| 492 | title: Schema.optional(Schema.String), | 471 | title: Schema.optional(TrimmedNonEmptyString), |
| 493 | model: Schema.optional(Schema.String), | 472 | model: Schema.optional(TrimmedNonEmptyString), |
| 494 | branch: Schema.optional(Schema.NullOr(Schema.String)), | 473 | branch: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), |
| 495 | worktreePath: Schema.optional(Schema.NullOr(Schema.String)), | 474 | worktreePath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)), |
| 496 | updatedAt: IsoDateTime, | 475 | updatedAt: IsoDateTime, |
| 497 | }); | 476 | }); |
| 498 | 477 | ||
| ... | @@ -511,8 +490,8 @@ export const ThreadMessageSentPayload = Schema.Struct({ | ... | @@ -511,8 +490,8 @@ export const ThreadMessageSentPayload = Schema.Struct({ |
| 511 | export const ThreadTurnStartRequestedPayload = Schema.Struct({ | 490 | export const ThreadTurnStartRequestedPayload = Schema.Struct({ |
| 512 | threadId: ThreadId, | 491 | threadId: ThreadId, |
| 513 | messageId: MessageId, | 492 | messageId: MessageId, |
| 514 | model: Schema.optional(Schema.String), | 493 | model: Schema.optional(TrimmedNonEmptyString), |
| 515 | effort: Schema.optional(Schema.String), | 494 | effort: Schema.optional(TrimmedNonEmptyString), |
| 516 | createdAt: IsoDateTime, | 495 | createdAt: IsoDateTime, |
| 517 | }); | 496 | }); |
| 518 | 497 | ||
| ... | @@ -571,7 +550,7 @@ export const OrchestrationEventMetadata = Schema.Struct({ | ... | @@ -571,7 +550,7 @@ export const OrchestrationEventMetadata = Schema.Struct({ |
| 571 | providerThreadId: Schema.optional(ProviderThreadId), | 550 | providerThreadId: Schema.optional(ProviderThreadId), |
| 572 | providerTurnId: Schema.optional(ProviderTurnId), | 551 | providerTurnId: Schema.optional(ProviderTurnId), |
| 573 | providerItemId: Schema.optional(ProviderItemId), | 552 | providerItemId: Schema.optional(ProviderItemId), |
| 574 | adapterKey: Schema.optional(Schema.String), | 553 | adapterKey: Schema.optional(TrimmedNonEmptyString), |
| 575 | requestId: Schema.optional(ApprovalRequestId), | 554 | requestId: Schema.optional(ApprovalRequestId), |
| 576 | ingestedAt: Schema.optional(IsoDateTime), | 555 | ingestedAt: Schema.optional(IsoDateTime), |
| 577 | }); | 556 | }); |
packages/contracts/src/project.ts+3-6| ... | @@ -1,15 +1,12 @@ | ... | @@ -1,15 +1,12 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | import { PositiveInt, TrimmedNonEmptyString } from "./baseSchemas"; | ||
| 2 | 3 | ||
| 3 | export const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200; | 4 | export const PROJECT_SEARCH_ENTRIES_MAX_LIMIT = 200; |
| 4 | const TrimmedNonEmptyString = Schema.Trimmed.check(Schema.isNonEmpty()); | ||
| 5 | 5 | ||
| 6 | export const ProjectSearchEntriesInput = Schema.Struct({ | 6 | export const ProjectSearchEntriesInput = Schema.Struct({ |
| 7 | cwd: TrimmedNonEmptyString, | 7 | cwd: TrimmedNonEmptyString, |
| 8 | query: Schema.NonEmptyString.check(Schema.isMaxLength(256)), | 8 | query: TrimmedNonEmptyString.check(Schema.isMaxLength(256)), |
| 9 | limit: Schema.Int.check( | 9 | limit: PositiveInt.check(Schema.isLessThanOrEqualTo(PROJECT_SEARCH_ENTRIES_MAX_LIMIT)), |
| 10 | Schema.isGreaterThanOrEqualTo(1), | ||
| 11 | Schema.isLessThanOrEqualTo(PROJECT_SEARCH_ENTRIES_MAX_LIMIT), | ||
| 12 | ), | ||
| 13 | }); | 10 | }); |
| 14 | export type ProjectSearchEntriesInput = typeof ProjectSearchEntriesInput.Type; | 11 | export type ProjectSearchEntriesInput = typeof ProjectSearchEntriesInput.Type; |
| 15 | 12 |
packages/contracts/src/provider.ts+10-8| ... | @@ -1,27 +1,29 @@ | ... | @@ -1,27 +1,29 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; | |
| 3 | import { | 3 | import { |
| 4 | ApprovalRequestId, | 4 | ApprovalRequestId, |
| 5 | ChatAttachment, | ||
| 6 | EventId, | 5 | EventId, |
| 7 | IsoDateTime, | 6 | IsoDateTime, |
| 8 | NonNegativeInt, | 7 | NonNegativeInt, |
| 8 | ProviderItemId, | ||
| 9 | ProviderSessionId, | ||
| 10 | ProviderThreadId, | ||
| 11 | ProviderTurnId, | ||
| 12 | ThreadId, | ||
| 13 | } from "./baseSchemas"; | ||
| 14 | import { | ||
| 15 | ChatAttachment, | ||
| 9 | PROVIDER_SEND_TURN_MAX_ATTACHMENTS, | 16 | PROVIDER_SEND_TURN_MAX_ATTACHMENTS, |
| 10 | PROVIDER_SEND_TURN_MAX_INPUT_CHARS, | 17 | PROVIDER_SEND_TURN_MAX_INPUT_CHARS, |
| 11 | ProviderApprovalDecision, | 18 | ProviderApprovalDecision, |
| 12 | ProviderApprovalPolicy, | 19 | ProviderApprovalPolicy, |
| 13 | ProviderKind, | 20 | ProviderKind, |
| 14 | ProviderItemId, | ||
| 15 | ProviderRequestKind, | 21 | ProviderRequestKind, |
| 16 | ProviderSandboxMode, | 22 | ProviderSandboxMode, |
| 17 | ProviderSessionId, | ||
| 18 | ProviderThreadId, | ||
| 19 | ProviderTurnId, | ||
| 20 | ThreadId, | ||
| 21 | TurnCountRange, | 23 | TurnCountRange, |
| 22 | } from "./orchestration"; | 24 | } from "./orchestration"; |
| 23 | 25 | ||
| 24 | const TrimmedNonEmptyStringSchema = Schema.Trimmed.check(Schema.isNonEmpty()); | 26 | const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString; |
| 25 | 27 | ||
| 26 | export const ProviderSessionStatus = Schema.Literals([ | 28 | export const ProviderSessionStatus = Schema.Literals([ |
| 27 | "connecting", | 29 | "connecting", |
packages/contracts/src/providerRuntime.ts+14-14| ... | @@ -1,21 +1,21 @@ | ... | @@ -1,21 +1,21 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; | ||
| 2 | 3 | ||
| 3 | import { | 4 | import { |
| 4 | ApprovalRequestId, | 5 | ApprovalRequestId, |
| 5 | EventId, | 6 | EventId, |
| 6 | IsoDateTime, | ||
| 7 | NonNegativeInt, | 7 | NonNegativeInt, |
| 8 | ProviderApprovalDecision, | ||
| 9 | ProviderItemId, | 8 | ProviderItemId, |
| 10 | ProviderKind, | ||
| 11 | ProviderRequestKind, | ||
| 12 | ProviderSessionId, | 9 | ProviderSessionId, |
| 13 | ProviderThreadId, | 10 | ProviderThreadId, |
| 14 | ProviderTurnId, | 11 | ProviderTurnId, |
| 15 | ThreadId, | 12 | ThreadId, |
| 16 | TurnId, | 13 | TurnId, |
| 17 | } from "./orchestration"; | 14 | IsoDateTime, |
| 15 | } from "./baseSchemas"; | ||
| 16 | import { ProviderApprovalDecision, ProviderKind, ProviderRequestKind } from "./orchestration"; | ||
| 18 | 17 | ||
| 18 | const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString; | ||
| 19 | const RuntimeThreadIdSchema = Schema.Union([ThreadId, ProviderThreadId]); | 19 | const RuntimeThreadIdSchema = Schema.Union([ThreadId, ProviderThreadId]); |
| 20 | const RuntimeTurnIdSchema = Schema.Union([TurnId, ProviderTurnId]); | 20 | const RuntimeTurnIdSchema = Schema.Union([TurnId, ProviderTurnId]); |
| 21 | 21 | ||
| ... | @@ -37,7 +37,7 @@ export const ProviderRuntimeSessionStartedEvent = Schema.Struct({ | ... | @@ -37,7 +37,7 @@ export const ProviderRuntimeSessionStartedEvent = Schema.Struct({ |
| 37 | sessionId: ProviderSessionId, | 37 | sessionId: ProviderSessionId, |
| 38 | createdAt: IsoDateTime, | 38 | createdAt: IsoDateTime, |
| 39 | threadId: Schema.optional(RuntimeThreadIdSchema), | 39 | threadId: Schema.optional(RuntimeThreadIdSchema), |
| 40 | message: Schema.optional(Schema.String), | 40 | message: Schema.optional(TrimmedNonEmptyStringSchema), |
| 41 | }); | 41 | }); |
| 42 | export type ProviderRuntimeSessionStartedEvent = typeof ProviderRuntimeSessionStartedEvent.Type; | 42 | export type ProviderRuntimeSessionStartedEvent = typeof ProviderRuntimeSessionStartedEvent.Type; |
| 43 | 43 | ||
| ... | @@ -48,7 +48,7 @@ export const ProviderRuntimeSessionExitedEvent = Schema.Struct({ | ... | @@ -48,7 +48,7 @@ export const ProviderRuntimeSessionExitedEvent = Schema.Struct({ |
| 48 | sessionId: ProviderSessionId, | 48 | sessionId: ProviderSessionId, |
| 49 | createdAt: IsoDateTime, | 49 | createdAt: IsoDateTime, |
| 50 | threadId: Schema.optional(RuntimeThreadIdSchema), | 50 | threadId: Schema.optional(RuntimeThreadIdSchema), |
| 51 | message: Schema.optional(Schema.String), | 51 | message: Schema.optional(TrimmedNonEmptyStringSchema), |
| 52 | }); | 52 | }); |
| 53 | export type ProviderRuntimeSessionExitedEvent = typeof ProviderRuntimeSessionExitedEvent.Type; | 53 | export type ProviderRuntimeSessionExitedEvent = typeof ProviderRuntimeSessionExitedEvent.Type; |
| 54 | 54 | ||
| ... | @@ -82,7 +82,7 @@ export const ProviderRuntimeTurnCompletedEvent = Schema.Struct({ | ... | @@ -82,7 +82,7 @@ export const ProviderRuntimeTurnCompletedEvent = Schema.Struct({ |
| 82 | threadId: Schema.optional(RuntimeThreadIdSchema), | 82 | threadId: Schema.optional(RuntimeThreadIdSchema), |
| 83 | turnId: Schema.optional(RuntimeTurnIdSchema), | 83 | turnId: Schema.optional(RuntimeTurnIdSchema), |
| 84 | status: Schema.optional(ProviderRuntimeTurnStatus), | 84 | status: Schema.optional(ProviderRuntimeTurnStatus), |
| 85 | errorMessage: Schema.optional(Schema.String), | 85 | errorMessage: Schema.optional(TrimmedNonEmptyStringSchema), |
| 86 | }); | 86 | }); |
| 87 | export type ProviderRuntimeTurnCompletedEvent = typeof ProviderRuntimeTurnCompletedEvent.Type; | 87 | export type ProviderRuntimeTurnCompletedEvent = typeof ProviderRuntimeTurnCompletedEvent.Type; |
| 88 | 88 | ||
| ... | @@ -121,8 +121,8 @@ export const ProviderRuntimeToolStartedEvent = Schema.Struct({ | ... | @@ -121,8 +121,8 @@ export const ProviderRuntimeToolStartedEvent = Schema.Struct({ |
| 121 | turnId: Schema.optional(RuntimeTurnIdSchema), | 121 | turnId: Schema.optional(RuntimeTurnIdSchema), |
| 122 | itemId: Schema.optional(ProviderItemId), | 122 | itemId: Schema.optional(ProviderItemId), |
| 123 | toolKind: ProviderRuntimeToolKind, | 123 | toolKind: ProviderRuntimeToolKind, |
| 124 | title: Schema.String, | 124 | title: TrimmedNonEmptyStringSchema, |
| 125 | detail: Schema.optional(Schema.String), | 125 | detail: Schema.optional(TrimmedNonEmptyStringSchema), |
| 126 | }); | 126 | }); |
| 127 | export type ProviderRuntimeToolStartedEvent = typeof ProviderRuntimeToolStartedEvent.Type; | 127 | export type ProviderRuntimeToolStartedEvent = typeof ProviderRuntimeToolStartedEvent.Type; |
| 128 | 128 | ||
| ... | @@ -136,8 +136,8 @@ export const ProviderRuntimeToolCompletedEvent = Schema.Struct({ | ... | @@ -136,8 +136,8 @@ export const ProviderRuntimeToolCompletedEvent = Schema.Struct({ |
| 136 | turnId: Schema.optional(RuntimeTurnIdSchema), | 136 | turnId: Schema.optional(RuntimeTurnIdSchema), |
| 137 | itemId: Schema.optional(ProviderItemId), | 137 | itemId: Schema.optional(ProviderItemId), |
| 138 | toolKind: ProviderRuntimeToolKind, | 138 | toolKind: ProviderRuntimeToolKind, |
| 139 | title: Schema.String, | 139 | title: TrimmedNonEmptyStringSchema, |
| 140 | detail: Schema.optional(Schema.String), | 140 | detail: Schema.optional(TrimmedNonEmptyStringSchema), |
| 141 | }); | 141 | }); |
| 142 | export type ProviderRuntimeToolCompletedEvent = typeof ProviderRuntimeToolCompletedEvent.Type; | 142 | export type ProviderRuntimeToolCompletedEvent = typeof ProviderRuntimeToolCompletedEvent.Type; |
| 143 | 143 | ||
| ... | @@ -152,7 +152,7 @@ export const ProviderRuntimeApprovalRequestedEvent = Schema.Struct({ | ... | @@ -152,7 +152,7 @@ export const ProviderRuntimeApprovalRequestedEvent = Schema.Struct({ |
| 152 | itemId: Schema.optional(ProviderItemId), | 152 | itemId: Schema.optional(ProviderItemId), |
| 153 | requestId: ApprovalRequestId, | 153 | requestId: ApprovalRequestId, |
| 154 | requestKind: ProviderRequestKind, | 154 | requestKind: ProviderRequestKind, |
| 155 | detail: Schema.optional(Schema.String), | 155 | detail: Schema.optional(TrimmedNonEmptyStringSchema), |
| 156 | }); | 156 | }); |
| 157 | export type ProviderRuntimeApprovalRequestedEvent = | 157 | export type ProviderRuntimeApprovalRequestedEvent = |
| 158 | typeof ProviderRuntimeApprovalRequestedEvent.Type; | 158 | typeof ProviderRuntimeApprovalRequestedEvent.Type; |
| ... | @@ -195,7 +195,7 @@ export const ProviderRuntimeErrorEvent = Schema.Struct({ | ... | @@ -195,7 +195,7 @@ export const ProviderRuntimeErrorEvent = Schema.Struct({ |
| 195 | threadId: Schema.optional(RuntimeThreadIdSchema), | 195 | threadId: Schema.optional(RuntimeThreadIdSchema), |
| 196 | turnId: Schema.optional(RuntimeTurnIdSchema), | 196 | turnId: Schema.optional(RuntimeTurnIdSchema), |
| 197 | itemId: Schema.optional(ProviderItemId), | 197 | itemId: Schema.optional(ProviderItemId), |
| 198 | message: Schema.String, | 198 | message: TrimmedNonEmptyStringSchema, |
| 199 | }); | 199 | }); |
| 200 | export type ProviderRuntimeErrorEvent = typeof ProviderRuntimeErrorEvent.Type; | 200 | export type ProviderRuntimeErrorEvent = typeof ProviderRuntimeErrorEvent.Type; |
| 201 | 201 |
packages/contracts/src/server.ts+2-1| ... | @@ -1,8 +1,9 @@ | ... | @@ -1,8 +1,9 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; | ||
| 2 | import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings"; | 3 | import { KeybindingRule, ResolvedKeybindingsConfig } from "./keybindings"; |
| 3 | 4 | ||
| 4 | export const ServerConfig = Schema.Struct({ | 5 | export const ServerConfig = Schema.Struct({ |
| 5 | cwd: Schema.NonEmptyString, | 6 | cwd: TrimmedNonEmptyString, |
| 6 | keybindings: ResolvedKeybindingsConfig, | 7 | keybindings: ResolvedKeybindingsConfig, |
| 7 | }); | 8 | }); |
| 8 | export type ServerConfig = typeof ServerConfig.Type; | 9 | export type ServerConfig = typeof ServerConfig.Type; |
packages/contracts/src/terminal.ts+4-8| ... | @@ -1,26 +1,22 @@ | ... | @@ -1,26 +1,22 @@ |
| 1 | import { Schema } from "effect"; | 1 | import { Schema } from "effect"; |
| 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; | ||
| 2 | 3 | ||
| 3 | export const DEFAULT_TERMINAL_ID = "default"; | 4 | export const DEFAULT_TERMINAL_ID = "default"; |
| 4 | 5 | ||
| 5 | const TrimmedNonEmptyStringSchema = Schema.Trim.check(Schema.isNonEmpty()); | 6 | const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString; |
| 6 | const TerminalColsSchema = Schema.Int.check(Schema.isGreaterThanOrEqualTo(20)).check( | 7 | const TerminalColsSchema = Schema.Int.check(Schema.isGreaterThanOrEqualTo(20)).check( |
| 7 | Schema.isLessThanOrEqualTo(400), | 8 | Schema.isLessThanOrEqualTo(400), |
| 8 | ); | 9 | ); |
| 9 | const TerminalRowsSchema = Schema.Int.check(Schema.isGreaterThanOrEqualTo(5)).check( | 10 | const TerminalRowsSchema = Schema.Int.check(Schema.isGreaterThanOrEqualTo(5)).check( |
| 10 | Schema.isLessThanOrEqualTo(200), | 11 | Schema.isLessThanOrEqualTo(200), |
| 11 | ); | 12 | ); |
| 12 | const TerminalIdSchema = Schema.Trim.check(Schema.isNonEmpty()).check(Schema.isMaxLength(128)); | 13 | const TerminalIdSchema = TrimmedNonEmptyStringSchema.check(Schema.isMaxLength(128)); |
| 13 | const TerminalEnvKeySchema = Schema.String.check( | 14 | const TerminalEnvKeySchema = Schema.String.check( |
| 14 | Schema.isPattern(/^[A-Za-z_][A-Za-z0-9_]*$/), | 15 | Schema.isPattern(/^[A-Za-z_][A-Za-z0-9_]*$/), |
| 15 | ).check(Schema.isMaxLength(128)); | 16 | ).check(Schema.isMaxLength(128)); |
| 16 | const TerminalEnvValueSchema = Schema.String.check(Schema.isMaxLength(8_192)); | 17 | const TerminalEnvValueSchema = Schema.String.check(Schema.isMaxLength(8_192)); |
| 17 | const TerminalEnvSchema = Schema.Record(TerminalEnvKeySchema, TerminalEnvValueSchema).check( | 18 | const TerminalEnvSchema = Schema.Record(TerminalEnvKeySchema, TerminalEnvValueSchema).check( |
| 18 | Schema.makeFilter<Record<string, string>>((env) => { | 19 | Schema.isMaxProperties(128), |
| 19 | if (Object.keys(env).length > 128) { | ||
| 20 | return { path: [], message: "Too many terminal env vars" }; | ||
| 21 | } | ||
| 22 | return true; | ||
| 23 | }), | ||
| 24 | ); | 20 | ); |
| 25 | 21 | ||
| 26 | const TerminalIdWithDefaultSchema = TerminalIdSchema.pipe( | 22 | const TerminalIdWithDefaultSchema = TerminalIdSchema.pipe( |
packages/contracts/src/ws.test.ts+19| ... | @@ -37,3 +37,22 @@ it.effect("rejects getTurnDiff requests when fromTurnCount > toTurnCount", () => | ... | @@ -37,3 +37,22 @@ it.effect("rejects getTurnDiff requests when fromTurnCount > toTurnCount", () => |
| 37 | assert.strictEqual(result._tag, "Failure"); | 37 | assert.strictEqual(result._tag, "Failure"); |
| 38 | }), | 38 | }), |
| 39 | ); | 39 | ); |
| 40 | |||
| 41 | it.effect("trims websocket request id and nested orchestration ids", () => | ||
| 42 | Effect.gen(function* () { | ||
| 43 | const parsed = yield* decodeWebSocketRequest({ | ||
| 44 | id: " req-1 ", | ||
| 45 | body: { | ||
| 46 | _tag: ORCHESTRATION_WS_METHODS.getTurnDiff, | ||
| 47 | threadId: " thread-1 ", | ||
| 48 | fromTurnCount: 0, | ||
| 49 | toTurnCount: 0, | ||
| 50 | }, | ||
| 51 | }); | ||
| 52 | assert.strictEqual(parsed.id, "req-1"); | ||
| 53 | assert.strictEqual(parsed.body._tag, ORCHESTRATION_WS_METHODS.getTurnDiff); | ||
| 54 | if (parsed.body._tag === ORCHESTRATION_WS_METHODS.getTurnDiff) { | ||
| 55 | assert.strictEqual(parsed.body.threadId, "thread-1"); | ||
| 56 | } | ||
| 57 | }), | ||
| 58 | ); |
packages/contracts/src/ws.ts+6-5| ... | @@ -1,4 +1,5 @@ | ... | @@ -1,4 +1,5 @@ |
| 1 | import { Schema, Struct } from "effect"; | 1 | import { Schema, Struct } from "effect"; |
| 2 | import { TrimmedNonEmptyString } from "./baseSchemas"; | ||
| 2 | 3 | ||
| 3 | import { | 4 | import { |
| 4 | ClientOrchestrationCommand, | 5 | ClientOrchestrationCommand, |
| ... | @@ -128,13 +129,13 @@ export const WebSocketRequestBody = Schema.Union([ | ... | @@ -128,13 +129,13 @@ export const WebSocketRequestBody = Schema.Union([ |
| 128 | ]); | 129 | ]); |
| 129 | 130 | ||
| 130 | export const WebSocketRequest = Schema.Struct({ | 131 | export const WebSocketRequest = Schema.Struct({ |
| 131 | id: Schema.String, | 132 | id: TrimmedNonEmptyString, |
| 132 | body: WebSocketRequestBody, | 133 | body: WebSocketRequestBody, |
| 133 | }); | 134 | }); |
| 134 | export type WebSocketRequest = typeof WebSocketRequest.Type; | 135 | export type WebSocketRequest = typeof WebSocketRequest.Type; |
| 135 | 136 | ||
| 136 | export const WebSocketResponse = Schema.Struct({ | 137 | export const WebSocketResponse = Schema.Struct({ |
| 137 | id: Schema.String, | 138 | id: TrimmedNonEmptyString, |
| 138 | result: Schema.optional(Schema.Unknown), | 139 | result: Schema.optional(Schema.Unknown), |
| 139 | error: Schema.optional( | 140 | error: Schema.optional( |
| 140 | Schema.Struct({ | 141 | Schema.Struct({ |
| ... | @@ -146,7 +147,7 @@ export type WebSocketResponse = typeof WebSocketResponse.Type; | ... | @@ -146,7 +147,7 @@ export type WebSocketResponse = typeof WebSocketResponse.Type; |
| 146 | 147 | ||
| 147 | export const WsPush = Schema.Struct({ | 148 | export const WsPush = Schema.Struct({ |
| 148 | type: Schema.Literal("push"), | 149 | type: Schema.Literal("push"), |
| 149 | channel: Schema.String, | 150 | channel: TrimmedNonEmptyString, |
| 150 | data: Schema.Unknown, | 151 | data: Schema.Unknown, |
| 151 | }); | 152 | }); |
| 152 | export type WsPush = typeof WsPush.Type; | 153 | export type WsPush = typeof WsPush.Type; |
| ... | @@ -159,7 +160,7 @@ export type WsResponse = typeof WsResponse.Type; | ... | @@ -159,7 +160,7 @@ export type WsResponse = typeof WsResponse.Type; |
| 159 | // ── Server welcome payload ─────────────────────────────────────────── | 160 | // ── Server welcome payload ─────────────────────────────────────────── |
| 160 | 161 | ||
| 161 | export const WsWelcomePayload = Schema.Struct({ | 162 | export const WsWelcomePayload = Schema.Struct({ |
| 162 | cwd: Schema.String, | 163 | cwd: TrimmedNonEmptyString, |
| 163 | projectName: Schema.String, | 164 | projectName: TrimmedNonEmptyString, |
| 164 | }); | 165 | }); |
| 165 | export type WsWelcomePayload = typeof WsWelcomePayload.Type; | 166 | export type WsWelcomePayload = typeof WsWelcomePayload.Type; |