| ... | @@ -85,44 +85,43 @@ const makeCodexTextGeneration = Effect.gen(function* () { | ... | @@ -85,44 +85,43 @@ const makeCodexTextGeneration = Effect.gen(function* () { |
| 85 | const safeUnlink = (filePath: string): Effect.Effect<void, never> => | 85 | const safeUnlink = (filePath: string): Effect.Effect<void, never> => |
| 86 | fileSystem.remove(filePath).pipe(Effect.catch(() => Effect.void)); | 86 | fileSystem.remove(filePath).pipe(Effect.catch(() => Effect.void)); |
| 87 | | 87 | |
| 88 | const materializeImageAttachments = ( | 88 | const materializeImageAttachments = Effect.fn("materializeImageAttachments")(function* ( |
| 89 | _operation: | 89 | _operation: |
| 90 | | "generateCommitMessage" | 90 | | "generateCommitMessage" |
| 91 | | "generatePrContent" | 91 | | "generatePrContent" |
| 92 | | "generateBranchName" | 92 | | "generateBranchName" |
| 93 | | "generateThreadTitle", | 93 | | "generateThreadTitle", |
| 94 | attachments: BranchNameGenerationInput["attachments"], | 94 | attachments: BranchNameGenerationInput["attachments"], |
| 95 | ): Effect.Effect<MaterializedImageAttachments, TextGenerationError> => | 95 | ): Effect.fn.Return<MaterializedImageAttachments, TextGenerationError> { |
| 96 | Effect.gen(function* () { | 96 | if (!attachments || attachments.length === 0) { |
| 97 | if (!attachments || attachments.length === 0) { | 97 | return { imagePaths: [] }; |
| 98 | return { imagePaths: [] }; | 98 | } |
| 99 | } | | |
| 100 | | 99 | |
| 101 | const imagePaths: string[] = []; | 100 | const imagePaths: string[] = []; |
| 102 | for (const attachment of attachments) { | 101 | for (const attachment of attachments) { |
| 103 | if (attachment.type !== "image") { | 102 | if (attachment.type !== "image") { |
| 104 | continue; | 103 | continue; |
| 105 | } | 104 | } |
| 106 | | 105 | |
| 107 | const resolvedPath = resolveAttachmentPath({ | 106 | const resolvedPath = resolveAttachmentPath({ |
| 108 | attachmentsDir: serverConfig.attachmentsDir, | 107 | attachmentsDir: serverConfig.attachmentsDir, |
| 109 | attachment, | 108 | attachment, |
| 110 | }); | 109 | }); |
| 111 | if (!resolvedPath || !path.isAbsolute(resolvedPath)) { | 110 | if (!resolvedPath || !path.isAbsolute(resolvedPath)) { |
| 112 | continue; | 111 | continue; |
| 113 | } | | |
| 114 | const fileInfo = yield* fileSystem | | |
| 115 | .stat(resolvedPath) | | |
| 116 | .pipe(Effect.catch(() => Effect.succeed(null))); | | |
| 117 | if (!fileInfo || fileInfo.type !== "File") { | | |
| 118 | continue; | | |
| 119 | } | | |
| 120 | imagePaths.push(resolvedPath); | | |
| 121 | } | 112 | } |
| 122 | return { imagePaths }; | 113 | const fileInfo = yield* fileSystem |
| 123 | }); | 114 | .stat(resolvedPath) |
| | 115 | .pipe(Effect.catch(() => Effect.succeed(null))); |
| | 116 | if (!fileInfo || fileInfo.type !== "File") { |
| | 117 | continue; |
| | 118 | } |
| | 119 | imagePaths.push(resolvedPath); |
| | 120 | } |
| | 121 | return { imagePaths }; |
| | 122 | }); |
| 124 | | 123 | |
| 125 | const runCodexJson = <S extends Schema.Top>({ | 124 | const runCodexJson = Effect.fn("runCodexJson")(function* <S extends Schema.Top>({ |
| 126 | operation, | 125 | operation, |
| 127 | cwd, | 126 | cwd, |
| 128 | prompt, | 127 | prompt, |
| ... | @@ -142,138 +141,137 @@ const makeCodexTextGeneration = Effect.gen(function* () { | ... | @@ -142,138 +141,137 @@ const makeCodexTextGeneration = Effect.gen(function* () { |
| 142 | imagePaths?: ReadonlyArray<string>; | 141 | imagePaths?: ReadonlyArray<string>; |
| 143 | cleanupPaths?: ReadonlyArray<string>; | 142 | cleanupPaths?: ReadonlyArray<string>; |
| 144 | modelSelection: CodexModelSelection; | 143 | modelSelection: CodexModelSelection; |
| 145 | }): Effect.Effect<S["Type"], TextGenerationError, S["DecodingServices"]> => | 144 | }): Effect.fn.Return<S["Type"], TextGenerationError, S["DecodingServices"]> { |
| 146 | Effect.gen(function* () { | 145 | const schemaPath = yield* writeTempFile( |
| 147 | const schemaPath = yield* writeTempFile( | 146 | operation, |
| 148 | operation, | 147 | "codex-schema", |
| 149 | "codex-schema", | 148 | JSON.stringify(toJsonSchemaObject(outputSchemaJson)), |
| 150 | JSON.stringify(toJsonSchemaObject(outputSchemaJson)), | 149 | ); |
| 151 | ); | 150 | const outputPath = yield* writeTempFile(operation, "codex-output", ""); |
| 152 | const outputPath = yield* writeTempFile(operation, "codex-output", ""); | | |
| 153 | | 151 | |
| 154 | const codexSettings = yield* Effect.map( | 152 | const codexSettings = yield* Effect.map( |
| 155 | serverSettingsService.getSettings, | 153 | serverSettingsService.getSettings, |
| 156 | (settings) => settings.providers.codex, | 154 | (settings) => settings.providers.codex, |
| 157 | ).pipe(Effect.catch(() => Effect.undefined)); | 155 | ).pipe(Effect.catch(() => Effect.undefined)); |
| 158 | | 156 | |
| 159 | const runCodexCommand = Effect.gen(function* () { | 157 | const runCodexCommand = Effect.fn("runCodexJson.runCodexCommand")(function* () { |
| 160 | const normalizedOptions = normalizeCodexModelOptionsWithCapabilities( | 158 | const normalizedOptions = normalizeCodexModelOptionsWithCapabilities( |
| 161 | getCodexModelCapabilities(modelSelection.model), | 159 | getCodexModelCapabilities(modelSelection.model), |
| 162 | modelSelection.options, | 160 | modelSelection.options, |
| 163 | ); | 161 | ); |
| 164 | const reasoningEffort = | 162 | const reasoningEffort = |
| 165 | modelSelection.options?.reasoningEffort ?? CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT; | 163 | modelSelection.options?.reasoningEffort ?? CODEX_GIT_TEXT_GENERATION_REASONING_EFFORT; |
| 166 | const command = ChildProcess.make( | 164 | const command = ChildProcess.make( |
| 167 | codexSettings?.binaryPath || "codex", | 165 | codexSettings?.binaryPath || "codex", |
| 168 | [ | 166 | [ |
| 169 | "exec", | 167 | "exec", |
| 170 | "--ephemeral", | 168 | "--ephemeral", |
| 171 | "-s", | 169 | "-s", |
| 172 | "read-only", | 170 | "read-only", |
| 173 | "--model", | 171 | "--model", |
| 174 | modelSelection.model, | 172 | modelSelection.model, |
| 175 | "--config", | 173 | "--config", |
| 176 | `model_reasoning_effort="${reasoningEffort}"`, | 174 | `model_reasoning_effort="${reasoningEffort}"`, |
| 177 | ...(normalizedOptions?.fastMode ? ["--config", `service_tier="fast"`] : []), | 175 | ...(normalizedOptions?.fastMode ? ["--config", `service_tier="fast"`] : []), |
| 178 | "--output-schema", | 176 | "--output-schema", |
| 179 | schemaPath, | 177 | schemaPath, |
| 180 | "--output-last-message", | 178 | "--output-last-message", |
| 181 | outputPath, | 179 | outputPath, |
| 182 | ...imagePaths.flatMap((imagePath) => ["--image", imagePath]), | 180 | ...imagePaths.flatMap((imagePath) => ["--image", imagePath]), |
| 183 | "-", | 181 | "-", |
| 184 | ], | 182 | ], |
| 185 | { | 183 | { |
| 186 | env: { | 184 | env: { |
| 187 | ...process.env, | 185 | ...process.env, |
| 188 | ...(codexSettings?.homePath ? { CODEX_HOME: codexSettings.homePath } : {}), | 186 | ...(codexSettings?.homePath ? { CODEX_HOME: codexSettings.homePath } : {}), |
| 189 | }, | | |
| 190 | cwd, | | |
| 191 | shell: process.platform === "win32", | | |
| 192 | stdin: { | | |
| 193 | stream: Stream.encodeText(Stream.make(prompt)), | | |
| 194 | }, | | |
| 195 | }, | 187 | }, |
| | 188 | cwd, |
| | 189 | shell: process.platform === "win32", |
| | 190 | stdin: { |
| | 191 | stream: Stream.encodeText(Stream.make(prompt)), |
| | 192 | }, |
| | 193 | }, |
| | 194 | ); |
| | 195 | |
| | 196 | const child = yield* commandSpawner |
| | 197 | .spawn(command) |
| | 198 | .pipe( |
| | 199 | Effect.mapError((cause) => |
| | 200 | normalizeCliError("codex", operation, cause, "Failed to spawn Codex CLI process"), |
| | 201 | ), |
| 196 | ); | 202 | ); |
| 197 | | 203 | |
| 198 | const child = yield* commandSpawner | 204 | const [stdout, stderr, exitCode] = yield* Effect.all( |
| 199 | .spawn(command) | 205 | [ |
| 200 | .pipe( | 206 | readStreamAsString(operation, child.stdout), |
| | 207 | readStreamAsString(operation, child.stderr), |
| | 208 | child.exitCode.pipe( |
| 201 | Effect.mapError((cause) => | 209 | Effect.mapError((cause) => |
| 202 | normalizeCliError("codex", operation, cause, "Failed to spawn Codex CLI process"), | 210 | normalizeCliError("codex", operation, cause, "Failed to read Codex CLI exit code"), |
| 203 | ), | | |
| 204 | ); | | |
| 205 | | | |
| 206 | const [stdout, stderr, exitCode] = yield* Effect.all( | | |
| 207 | [ | | |
| 208 | readStreamAsString(operation, child.stdout), | | |
| 209 | readStreamAsString(operation, child.stderr), | | |
| 210 | child.exitCode.pipe( | | |
| 211 | Effect.mapError((cause) => | | |
| 212 | normalizeCliError("codex", operation, cause, "Failed to read Codex CLI exit code"), | | |
| 213 | ), | | |
| 214 | ), | 211 | ), |
| 215 | ], | 212 | ), |
| 216 | { concurrency: "unbounded" }, | 213 | ], |
| 217 | ); | 214 | { concurrency: "unbounded" }, |
| | 215 | ); |
| 218 | | 216 | |
| 219 | if (exitCode !== 0) { | 217 | if (exitCode !== 0) { |
| 220 | const stderrDetail = stderr.trim(); | 218 | const stderrDetail = stderr.trim(); |
| 221 | const stdoutDetail = stdout.trim(); | 219 | const stdoutDetail = stdout.trim(); |
| 222 | const detail = stderrDetail.length > 0 ? stderrDetail : stdoutDetail; | 220 | const detail = stderrDetail.length > 0 ? stderrDetail : stdoutDetail; |
| 223 | return yield* new TextGenerationError({ | 221 | return yield* new TextGenerationError({ |
| 224 | operation, | 222 | operation, |
| 225 | detail: | 223 | detail: |
| 226 | detail.length > 0 | 224 | detail.length > 0 |
| 227 | ? `Codex CLI command failed: ${detail}` | 225 | ? `Codex CLI command failed: ${detail}` |
| 228 | : `Codex CLI command failed with code ${exitCode}.`, | 226 | : `Codex CLI command failed with code ${exitCode}.`, |
| 229 | }); | 227 | }); |
| 230 | } | 228 | } |
| 231 | }); | 229 | }); |
| 232 | | 230 | |
| 233 | const cleanup = Effect.all( | 231 | const cleanup = Effect.all( |
| 234 | [schemaPath, outputPath, ...cleanupPaths].map((filePath) => safeUnlink(filePath)), | 232 | [schemaPath, outputPath, ...cleanupPaths].map((filePath) => safeUnlink(filePath)), |
| 235 | { | 233 | { |
| 236 | concurrency: "unbounded", | 234 | concurrency: "unbounded", |
| 237 | }, | 235 | }, |
| 238 | ).pipe(Effect.asVoid); | 236 | ).pipe(Effect.asVoid); |
| 239 | | 237 | |
| 240 | return yield* Effect.gen(function* () { | 238 | return yield* Effect.gen(function* () { |
| 241 | yield* runCodexCommand.pipe( | 239 | yield* runCodexCommand().pipe( |
| 242 | Effect.scoped, | 240 | Effect.scoped, |
| 243 | Effect.timeoutOption(CODEX_TIMEOUT_MS), | 241 | Effect.timeoutOption(CODEX_TIMEOUT_MS), |
| 244 | Effect.flatMap( | 242 | Effect.flatMap( |
| 245 | Option.match({ | 243 | Option.match({ |
| 246 | onNone: () => | 244 | onNone: () => |
| 247 | Effect.fail( | 245 | Effect.fail( |
| 248 | new TextGenerationError({ operation, detail: "Codex CLI request timed out." }), | 246 | new TextGenerationError({ operation, detail: "Codex CLI request timed out." }), |
| 249 | ), | 247 | ), |
| 250 | onSome: () => Effect.void, | 248 | onSome: () => Effect.void, |
| 251 | }), | 249 | }), |
| 252 | ), | 250 | ), |
| 253 | ); | 251 | ); |
| 254 | | 252 | |
| 255 | return yield* fileSystem.readFileString(outputPath).pipe( | 253 | return yield* fileSystem.readFileString(outputPath).pipe( |
| 256 | Effect.mapError( | 254 | Effect.mapError( |
| 257 | (cause) => | 255 | (cause) => |
| 258 | new TextGenerationError({ | 256 | new TextGenerationError({ |
| 259 | operation, | 257 | operation, |
| 260 | detail: "Failed to read Codex output file.", | 258 | detail: "Failed to read Codex output file.", |
| 261 | cause, | 259 | cause, |
| 262 | }), | 260 | }), |
| 263 | ), | 261 | ), |
| 264 | Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson))), | 262 | Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(outputSchemaJson))), |
| 265 | Effect.catchTag("SchemaError", (cause) => | 263 | Effect.catchTag("SchemaError", (cause) => |
| 266 | Effect.fail( | 264 | Effect.fail( |
| 267 | new TextGenerationError({ | 265 | new TextGenerationError({ |
| 268 | operation, | 266 | operation, |
| 269 | detail: "Codex returned invalid structured output.", | 267 | detail: "Codex returned invalid structured output.", |
| 270 | cause, | 268 | cause, |
| 271 | }), | 269 | }), |
| 272 | ), | | |
| 273 | ), | 270 | ), |
| 274 | ); | 271 | ), |
| 275 | }).pipe(Effect.ensuring(cleanup)); | 272 | ); |
| 276 | }); | 273 | }).pipe(Effect.ensuring(cleanup)); |
| | 274 | }); |
| 277 | | 275 | |
| 278 | const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn( | 276 | const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn( |
| 279 | "CodexTextGeneration.generateCommitMessage", | 277 | "CodexTextGeneration.generateCommitMessage", |