authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-14 19:57:35-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-14 19:57:35-07:00
loga62e7d670c67bf221a5699b5a988367781fead74
treeda0473ffac1db68798598cd213874f51b9db9877
parent08abda9dcf6fee89561f861004efb48bad665df2
signature Signed by PGP key B5690EEEBB952194

refactor(server): align title generation with Effect conventions (#11847)


7 files changed, 112 insertions(+), 15 deletions(-)

apps/server/scripts/evaluate-thread-titles.ts+7-5
......@@ -6,7 +6,7 @@
66// Add --initial to evaluate only the opening request.
77import * as NodeUtil from "node:util";
88import * as NodeCrypto from "node:crypto";
9import { FetchHttpClient } from "effect/unstable/http";
9import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
1010import * as NodeServices from "@effect/platform-node/NodeServices";
1111import { CodexSettings, ProviderInstanceId } from "@t3tools/contracts";
1212import * as Effect from "effect/Effect";
......@@ -15,13 +15,13 @@ import * as Duration from "effect/Duration";
1515import * as FileSystem from "effect/FileSystem";
1616import * as Path from "effect/Path";
1717import * as Schema from "effect/Schema";
18import { makeCodexTextGeneration } from "../src/textGeneration/CodexTextGeneration.ts";
18import * as CodexTextGeneration from "../src/textGeneration/CodexTextGeneration.ts";
1919import { threadTitleEvaluationCases } from "./threadTitleEvaluationCases.ts";
2020import {
2121 formatThreadTitleContext,
2222 type ThreadTitleMessage,
2323} from "../src/textGeneration/ThreadTitleContext.ts";
24import { resolveThreadTitleLinks } from "../src/textGeneration/ThreadTitleLinks.ts";
24import * as ThreadTitleLinks from "../src/textGeneration/ThreadTitleLinks.ts";
2525import * as SourceControlProviderRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts";
2626import * as GitHubCli from "../src/sourceControl/GitHubCli.ts";
2727import * as GitLabCli from "../src/sourceControl/GitLabCli.ts";
......@@ -66,7 +66,9 @@ await Effect.runPromise(
6666 const fs = yield* FileSystem.FileSystem;
6767 const path = yield* Path.Path;
6868 const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-title-evaluation-" });
69 const generation = yield* makeCodexTextGeneration(yield* decodeSettings({}));
69 const generation = yield* CodexTextGeneration.makeCodexTextGeneration(
70 yield* decodeSettings({}),
71 );
7072 const baseline = values.baseline
7173 ? yield* fs.readFileString(values.baseline).pipe(Effect.flatMap(decodeResults))
7274 : [];
......@@ -84,7 +86,7 @@ await Effect.runPromise(
8486 const message = values.initial ? firstMessage.text : context.message;
8587 const attachments = values.initial ? firstMessage.attachments : context.attachments;
8688 const [elapsed, { generated, linkedContextDigest }] = yield* Effect.gen(function* () {
87 const linkedContext = yield* resolveThreadTitleLinks({
89 const linkedContext = yield* ThreadTitleLinks.resolveThreadTitleLinks({
8890 cwd,
8991 message,
9092 });
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+4-2
......@@ -1857,7 +1857,8 @@ const make = Effect.gen(function* () {
18571857 return Effect.interrupt;
18581858 }
18591859 return Effect.logWarning("provider command reactor failed to find pending thread titles", {
1860 cause: Cause.pretty(cause),
1860 failureKind: Cause.hasDies(cause) ? "defect" : "failure",
1861 reasonCount: cause.reasons.length,
18611862 }).pipe(Effect.as({ interruptedRegenerations: [], refinementThreadIds: [] }));
18621863 }),
18631864 );
......@@ -1900,7 +1901,8 @@ const make = Effect.gen(function* () {
19001901 return Effect.logWarning(
19011902 "provider command reactor failed to recover pending thread titles",
19021903 {
1903 cause: Cause.pretty(cause),
1904 failureKind: Cause.hasDies(cause) ? "defect" : "failure",
1905 reasonCount: cause.reasons.length,
19041906 },
19051907 );
19061908 }),
apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts+35
......@@ -444,3 +444,38 @@ for (const kind of ["pull", "issues"]) {
444444 }),
445445 );
446446}
447
448for (const stage of ["read", "decode"] as const) {
449 it.effect(`retains the ${stage} failure without exposing its raw contents`, () =>
450 Effect.gen(function* () {
451 const cause = new GitHubCli.GitHubCliCommandError({
452 command: "gh",
453 cwd: "/repo",
454 cause: new Error("private response text"),
455 });
456 const provider = yield* makeProvider({
457 execute: () =>
458 stage === "read"
459 ? Effect.fail(cause)
460 : Effect.succeed({
461 exitCode: ChildProcessSpawner.ExitCode(0),
462 stdout: "private response text",
463 stderr: "",
464 stdoutTruncated: false,
465 stderrTruncated: false,
466 }),
467 });
468 const lookup = provider.resolveLink?.({
469 cwd: "/repo",
470 url: new URL("https://github.com/owner/repo/issues/42"),
471 });
472 assert.ok(lookup);
473 const error = yield* Effect.flip(lookup);
474 assert.strictEqual(error.operation, stage === "read" ? "resolveLink" : "resolveLink.decode");
475 assert.strictEqual(error.detail, "The linked subject could not be read.");
476 assert.notInclude(error.message, "private response text");
477 if (stage === "read") assert.strictEqual(error.cause, cause);
478 else assert.propertyVal(error.cause, "_tag", "SchemaError");
479 }),
480 );
481}
apps/server/src/sourceControl/GitHubSourceControlProvider.ts+14-3
......@@ -219,7 +219,7 @@ export const make = Effect.gen(function* () {
219219 input: { readonly cwd: string; readonly url: URL },
220220 endpoint: string,
221221 ) {
222 return yield* github
222 const result = yield* github
223223 .execute({
224224 cwd: input.cwd,
225225 args: ["api", "--hostname", input.url.host, endpoint, "--jq", "{title, body}"],
......@@ -228,8 +228,6 @@ export const make = Effect.gen(function* () {
228228 maxOutputBytes: 32_000,
229229 })
230230 .pipe(
231 Effect.flatMap((result) => decodeLinkSubject(result.stdout)),
232 Effect.map((subject) => ({ title: subject.title, body: subject.body })),
233231 Effect.mapError(
234232 (cause) =>
235233 new SourceControlProviderError({
......@@ -241,6 +239,19 @@ export const make = Effect.gen(function* () {
241239 }),
242240 ),
243241 );
242 const subject = yield* decodeLinkSubject(result.stdout).pipe(
243 Effect.mapError(
244 (cause) =>
245 new SourceControlProviderError({
246 provider: "github",
247 operation: "resolveLink.decode",
248 cwd: input.cwd,
249 detail: "The linked subject could not be read.",
250 cause,
251 }),
252 ),
253 );
254 return { title: subject.title, body: subject.body };
244255 });
245256
246257 return SourceControlProvider.SourceControlProvider.of({
apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts+36
......@@ -271,3 +271,39 @@ for (const kind of ["merge_requests", "issues"]) {
271271 }),
272272 );
273273}
274
275for (const stage of ["read", "decode"] as const) {
276 it.effect(`retains the ${stage} failure without exposing its raw contents`, () =>
277 Effect.gen(function* () {
278 const cause = new GitLabCli.GitLabCliCommandError({
279 command: "glab",
280 cwd: "/repo",
281 operation: "execute",
282 cause: new Error("private response text"),
283 });
284 const provider = yield* makeProvider({
285 execute: () =>
286 stage === "read"
287 ? Effect.fail(cause)
288 : Effect.succeed({
289 exitCode: ChildProcessSpawner.ExitCode(0),
290 stdout: "private response text",
291 stderr: "",
292 stdoutTruncated: false,
293 stderrTruncated: false,
294 }),
295 });
296 const lookup = provider.resolveLink?.({
297 cwd: "/repo",
298 url: new URL("https://gitlab.com/owner/repo/-/issues/42"),
299 });
300 assert.ok(lookup);
301 const error = yield* Effect.flip(lookup);
302 assert.strictEqual(error.operation, stage === "read" ? "resolveLink" : "resolveLink.decode");
303 assert.strictEqual(error.detail, "The linked subject could not be read.");
304 assert.notInclude(error.message, "private response text");
305 if (stage === "read") assert.strictEqual(error.cause, cause);
306 else assert.propertyVal(error.cause, "_tag", "SchemaError");
307 }),
308 );
309}
apps/server/src/sourceControl/GitLabSourceControlProvider.ts+14-3
......@@ -116,7 +116,7 @@ export const make = Effect.gen(function* () {
116116 input: { readonly cwd: string; readonly url: URL },
117117 endpoint: string,
118118 ) {
119 return yield* gitlab
119 const result = yield* gitlab
120120 .execute({
121121 cwd: input.cwd,
122122 args: ["api", "--hostname", input.url.host, endpoint],
......@@ -124,8 +124,6 @@ export const make = Effect.gen(function* () {
124124 maxOutputBytes: 32_000,
125125 })
126126 .pipe(
127 Effect.flatMap((result) => decodeLinkSubject(result.stdout)),
128 Effect.map((subject) => ({ title: subject.title, body: subject.description })),
129127 Effect.mapError(
130128 (cause) =>
131129 new SourceControlProviderError({
......@@ -137,6 +135,19 @@ export const make = Effect.gen(function* () {
137135 }),
138136 ),
139137 );
138 const subject = yield* decodeLinkSubject(result.stdout).pipe(
139 Effect.mapError(
140 (cause) =>
141 new SourceControlProviderError({
142 provider: "gitlab",
143 operation: "resolveLink.decode",
144 cwd: input.cwd,
145 detail: "The linked subject could not be read.",
146 cause,
147 }),
148 ),
149 );
150 return { title: subject.title, body: subject.description };
140151 });
141152
142153 return SourceControlProvider.SourceControlProvider.of({
apps/server/src/textGeneration/TextGeneration.ts+2-2
......@@ -7,7 +7,7 @@ import { TextGenerationError } from "@t3tools/contracts";
77import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts";
88import type { ProviderInstance } from "../provider/ProviderDriver.ts";
99import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
10import { resolveThreadTitleLinks } from "./ThreadTitleLinks.ts";
10import * as ThreadTitleLinks from "./ThreadTitleLinks.ts";
1111import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts";
1212
1313export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "grok" | "opencode";
......@@ -158,7 +158,7 @@ export const make = Effect.gen(function* () {
158158 Effect.gen(function* () {
159159 const linkedContext =
160160 input.linkedContext ??
161 (yield* resolveThreadTitleLinks(input).pipe(
161 (yield* ThreadTitleLinks.resolveThreadTitleLinks(input).pipe(
162162 Effect.provideService(
163163 SourceControlProviderRegistry.SourceControlProviderRegistry,
164164 sourceControl,