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 @@...@@ -6,7 +6,7 @@
6// Add --initial to evaluate only the opening request.6// Add --initial to evaluate only the opening request.
7import * as NodeUtil from "node:util";7import * as NodeUtil from "node:util";
8import * as NodeCrypto from "node:crypto";8import * as NodeCrypto from "node:crypto";
9import { FetchHttpClient } from "effect/unstable/http";9import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
10import * as NodeServices from "@effect/platform-node/NodeServices";10import * as NodeServices from "@effect/platform-node/NodeServices";
11import { CodexSettings, ProviderInstanceId } from "@t3tools/contracts";11import { CodexSettings, ProviderInstanceId } from "@t3tools/contracts";
12import * as Effect from "effect/Effect";12import * as Effect from "effect/Effect";
...@@ -15,13 +15,13 @@ import * as Duration from "effect/Duration";...@@ -15,13 +15,13 @@ import * as Duration from "effect/Duration";
15import * as FileSystem from "effect/FileSystem";15import * as FileSystem from "effect/FileSystem";
16import * as Path from "effect/Path";16import * as Path from "effect/Path";
17import * as Schema from "effect/Schema";17import * as Schema from "effect/Schema";
18import { makeCodexTextGeneration } from "../src/textGeneration/CodexTextGeneration.ts";18import * as CodexTextGeneration from "../src/textGeneration/CodexTextGeneration.ts";
19import { threadTitleEvaluationCases } from "./threadTitleEvaluationCases.ts";19import { threadTitleEvaluationCases } from "./threadTitleEvaluationCases.ts";
20import {20import {
21 formatThreadTitleContext,21 formatThreadTitleContext,
22 type ThreadTitleMessage,22 type ThreadTitleMessage,
23} from "../src/textGeneration/ThreadTitleContext.ts";23} from "../src/textGeneration/ThreadTitleContext.ts";
24import { resolveThreadTitleLinks } from "../src/textGeneration/ThreadTitleLinks.ts";24import * as ThreadTitleLinks from "../src/textGeneration/ThreadTitleLinks.ts";
25import * as SourceControlProviderRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts";25import * as SourceControlProviderRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts";
26import * as GitHubCli from "../src/sourceControl/GitHubCli.ts";26import * as GitHubCli from "../src/sourceControl/GitHubCli.ts";
27import * as GitLabCli from "../src/sourceControl/GitLabCli.ts";27import * as GitLabCli from "../src/sourceControl/GitLabCli.ts";
...@@ -66,7 +66,9 @@ await Effect.runPromise(...@@ -66,7 +66,9 @@ await Effect.runPromise(
66 const fs = yield* FileSystem.FileSystem;66 const fs = yield* FileSystem.FileSystem;
67 const path = yield* Path.Path;67 const path = yield* Path.Path;
68 const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-title-evaluation-" });68 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 );
70 const baseline = values.baseline72 const baseline = values.baseline
71 ? yield* fs.readFileString(values.baseline).pipe(Effect.flatMap(decodeResults))73 ? yield* fs.readFileString(values.baseline).pipe(Effect.flatMap(decodeResults))
72 : [];74 : [];
...@@ -84,7 +86,7 @@ await Effect.runPromise(...@@ -84,7 +86,7 @@ await Effect.runPromise(
84 const message = values.initial ? firstMessage.text : context.message;86 const message = values.initial ? firstMessage.text : context.message;
85 const attachments = values.initial ? firstMessage.attachments : context.attachments;87 const attachments = values.initial ? firstMessage.attachments : context.attachments;
86 const [elapsed, { generated, linkedContextDigest }] = yield* Effect.gen(function* () {88 const [elapsed, { generated, linkedContextDigest }] = yield* Effect.gen(function* () {
87 const linkedContext = yield* resolveThreadTitleLinks({89 const linkedContext = yield* ThreadTitleLinks.resolveThreadTitleLinks({
88 cwd,90 cwd,
89 message,91 message,
90 });92 });
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+4-2
...@@ -1857,7 +1857,8 @@ const make = Effect.gen(function* () {...@@ -1857,7 +1857,8 @@ const make = Effect.gen(function* () {
1857 return Effect.interrupt;1857 return Effect.interrupt;
1858 }1858 }
1859 return Effect.logWarning("provider command reactor failed to find pending thread titles", {1859 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,
1861 }).pipe(Effect.as({ interruptedRegenerations: [], refinementThreadIds: [] }));1862 }).pipe(Effect.as({ interruptedRegenerations: [], refinementThreadIds: [] }));
1862 }),1863 }),
1863 );1864 );
...@@ -1900,7 +1901,8 @@ const make = Effect.gen(function* () {...@@ -1900,7 +1901,8 @@ const make = Effect.gen(function* () {
1900 return Effect.logWarning(1901 return Effect.logWarning(
1901 "provider command reactor failed to recover pending thread titles",1902 "provider command reactor failed to recover pending thread titles",
1902 {1903 {
1903 cause: Cause.pretty(cause),1904 failureKind: Cause.hasDies(cause) ? "defect" : "failure",
1905 reasonCount: cause.reasons.length,
1904 },1906 },
1905 );1907 );
1906 }),1908 }),
apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts+35
...@@ -444,3 +444,38 @@ for (const kind of ["pull", "issues"]) {...@@ -444,3 +444,38 @@ for (const kind of ["pull", "issues"]) {
444 }),444 }),
445 );445 );
446}446}
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* () {...@@ -219,7 +219,7 @@ export const make = Effect.gen(function* () {
219 input: { readonly cwd: string; readonly url: URL },219 input: { readonly cwd: string; readonly url: URL },
220 endpoint: string,220 endpoint: string,
221 ) {221 ) {
222 return yield* github222 const result = yield* github
223 .execute({223 .execute({
224 cwd: input.cwd,224 cwd: input.cwd,
225 args: ["api", "--hostname", input.url.host, endpoint, "--jq", "{title, body}"],225 args: ["api", "--hostname", input.url.host, endpoint, "--jq", "{title, body}"],
...@@ -228,8 +228,6 @@ export const make = Effect.gen(function* () {...@@ -228,8 +228,6 @@ export const make = Effect.gen(function* () {
228 maxOutputBytes: 32_000,228 maxOutputBytes: 32_000,
229 })229 })
230 .pipe(230 .pipe(
231 Effect.flatMap((result) => decodeLinkSubject(result.stdout)),
232 Effect.map((subject) => ({ title: subject.title, body: subject.body })),
233 Effect.mapError(231 Effect.mapError(
234 (cause) =>232 (cause) =>
235 new SourceControlProviderError({233 new SourceControlProviderError({
...@@ -241,6 +239,19 @@ export const make = Effect.gen(function* () {...@@ -241,6 +239,19 @@ export const make = Effect.gen(function* () {
241 }),239 }),
242 ),240 ),
243 );241 );
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 };
244 });255 });
245256
246 return SourceControlProvider.SourceControlProvider.of({257 return SourceControlProvider.SourceControlProvider.of({
apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts+36
...@@ -271,3 +271,39 @@ for (const kind of ["merge_requests", "issues"]) {...@@ -271,3 +271,39 @@ for (const kind of ["merge_requests", "issues"]) {
271 }),271 }),
272 );272 );
273}273}
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* () {...@@ -116,7 +116,7 @@ export const make = Effect.gen(function* () {
116 input: { readonly cwd: string; readonly url: URL },116 input: { readonly cwd: string; readonly url: URL },
117 endpoint: string,117 endpoint: string,
118 ) {118 ) {
119 return yield* gitlab119 const result = yield* gitlab
120 .execute({120 .execute({
121 cwd: input.cwd,121 cwd: input.cwd,
122 args: ["api", "--hostname", input.url.host, endpoint],122 args: ["api", "--hostname", input.url.host, endpoint],
...@@ -124,8 +124,6 @@ export const make = Effect.gen(function* () {...@@ -124,8 +124,6 @@ export const make = Effect.gen(function* () {
124 maxOutputBytes: 32_000,124 maxOutputBytes: 32_000,
125 })125 })
126 .pipe(126 .pipe(
127 Effect.flatMap((result) => decodeLinkSubject(result.stdout)),
128 Effect.map((subject) => ({ title: subject.title, body: subject.description })),
129 Effect.mapError(127 Effect.mapError(
130 (cause) =>128 (cause) =>
131 new SourceControlProviderError({129 new SourceControlProviderError({
...@@ -137,6 +135,19 @@ export const make = Effect.gen(function* () {...@@ -137,6 +135,19 @@ export const make = Effect.gen(function* () {
137 }),135 }),
138 ),136 ),
139 );137 );
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 };
140 });151 });
141152
142 return SourceControlProvider.SourceControlProvider.of({153 return SourceControlProvider.SourceControlProvider.of({
apps/server/src/textGeneration/TextGeneration.ts+2-2
...@@ -7,7 +7,7 @@ import { TextGenerationError } from "@t3tools/contracts";...@@ -7,7 +7,7 @@ import { TextGenerationError } from "@t3tools/contracts";
7import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts";7import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts";
8import type { ProviderInstance } from "../provider/ProviderDriver.ts";8import type { ProviderInstance } from "../provider/ProviderDriver.ts";
9import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";9import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts";
10import { resolveThreadTitleLinks } from "./ThreadTitleLinks.ts";10import * as ThreadTitleLinks from "./ThreadTitleLinks.ts";
11import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts";11import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts";
1212
13export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "grok" | "opencode";13export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "grok" | "opencode";
...@@ -158,7 +158,7 @@ export const make = Effect.gen(function* () {...@@ -158,7 +158,7 @@ export const make = Effect.gen(function* () {
158 Effect.gen(function* () {158 Effect.gen(function* () {
159 const linkedContext =159 const linkedContext =
160 input.linkedContext ??160 input.linkedContext ??
161 (yield* resolveThreadTitleLinks(input).pipe(161 (yield* ThreadTitleLinks.resolveThreadTitleLinks(input).pipe(
162 Effect.provideService(162 Effect.provideService(
163 SourceControlProviderRegistry.SourceControlProviderRegistry,163 SourceControlProviderRegistry.SourceControlProviderRegistry,
164 sourceControl,164 sourceControl,