authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-04-01 08:53:25-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-04-01 15:53:25+00:00
loga58290ed79061f4bad98336419d8f9e405963f8f
treebfc73e305a0bf312c41bf31fa63a439093bbf2ad
parent3488ca3570060449d3855eeba79a40e1f9938f9c
signature Signed by PGP key B5690EEEBB952194

[codex] migrate Effect.fn in apps/server/src/git/Layers/CodexTextGeneration.ts (#1624)

Co-authored-by: codex <codex@users.noreply.github.com>

1 files changed, 147 insertions(+), 149 deletions(-)

apps/server/src/git/Layers/CodexTextGeneration.ts+147-149
...@@ -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));
8787
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 }
10099
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 }
106105
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 });
124123
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", "");
153151
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));
158156
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 );
197203
198 const child = yield* commandSpawner204 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 );
218216
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 > 0224 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 });
232230
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);
239237
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 );
254252
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 });
277275
278 const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn(276 const generateCommitMessage: TextGenerationShape["generateCommitMessage"] = Effect.fn(
279 "CodexTextGeneration.generateCommitMessage",277 "CodexTextGeneration.generateCommitMessage",