diff --git a/apps/server/scripts/evaluate-thread-titles.ts b/apps/server/scripts/evaluate-thread-titles.ts index 0761e501f24f09a0b9441f95f67691e2d058e049..78273a9ef7908cb589b4b5ff201e38d0b3217ba9 100644 --- a/apps/server/scripts/evaluate-thread-titles.ts +++ b/apps/server/scripts/evaluate-thread-titles.ts @@ -6,7 +6,7 @@ // Add --initial to evaluate only the opening request. import * as NodeUtil from "node:util"; import * as NodeCrypto from "node:crypto"; -import { FetchHttpClient } from "effect/unstable/http"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; import * as NodeServices from "@effect/platform-node/NodeServices"; import { CodexSettings, ProviderInstanceId } from "@t3tools/contracts"; import * as Effect from "effect/Effect"; @@ -15,13 +15,13 @@ import * as Duration from "effect/Duration"; import * as FileSystem from "effect/FileSystem"; import * as Path from "effect/Path"; import * as Schema from "effect/Schema"; -import { makeCodexTextGeneration } from "../src/textGeneration/CodexTextGeneration.ts"; +import * as CodexTextGeneration from "../src/textGeneration/CodexTextGeneration.ts"; import { threadTitleEvaluationCases } from "./threadTitleEvaluationCases.ts"; import { formatThreadTitleContext, type ThreadTitleMessage, } from "../src/textGeneration/ThreadTitleContext.ts"; -import { resolveThreadTitleLinks } from "../src/textGeneration/ThreadTitleLinks.ts"; +import * as ThreadTitleLinks from "../src/textGeneration/ThreadTitleLinks.ts"; import * as SourceControlProviderRegistry from "../src/sourceControl/SourceControlProviderRegistry.ts"; import * as GitHubCli from "../src/sourceControl/GitHubCli.ts"; import * as GitLabCli from "../src/sourceControl/GitLabCli.ts"; @@ -66,7 +66,9 @@ await Effect.runPromise( const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const cwd = yield* fs.makeTempDirectoryScoped({ prefix: "t3-title-evaluation-" }); - const generation = yield* makeCodexTextGeneration(yield* decodeSettings({})); + const generation = yield* CodexTextGeneration.makeCodexTextGeneration( + yield* decodeSettings({}), + ); const baseline = values.baseline ? yield* fs.readFileString(values.baseline).pipe(Effect.flatMap(decodeResults)) : []; @@ -84,7 +86,7 @@ await Effect.runPromise( const message = values.initial ? firstMessage.text : context.message; const attachments = values.initial ? firstMessage.attachments : context.attachments; const [elapsed, { generated, linkedContextDigest }] = yield* Effect.gen(function* () { - const linkedContext = yield* resolveThreadTitleLinks({ + const linkedContext = yield* ThreadTitleLinks.resolveThreadTitleLinks({ cwd, message, }); diff --git a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts index c35d6177a650fbd33c5e0e5e5070a170d32df0db..b653047d82552c1f99cbadf64ba27ca261cd267a 100644 --- a/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts +++ b/apps/server/src/orchestration/Layers/ProviderCommandReactor.ts @@ -1857,7 +1857,8 @@ const make = Effect.gen(function* () { return Effect.interrupt; } return Effect.logWarning("provider command reactor failed to find pending thread titles", { - cause: Cause.pretty(cause), + failureKind: Cause.hasDies(cause) ? "defect" : "failure", + reasonCount: cause.reasons.length, }).pipe(Effect.as({ interruptedRegenerations: [], refinementThreadIds: [] })); }), ); @@ -1900,7 +1901,8 @@ const make = Effect.gen(function* () { return Effect.logWarning( "provider command reactor failed to recover pending thread titles", { - cause: Cause.pretty(cause), + failureKind: Cause.hasDies(cause) ? "defect" : "failure", + reasonCount: cause.reasons.length, }, ); }), diff --git a/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts b/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts index c716267d19aee9ca1839780b30d579c413907dcf..4c41b323f17b36d05c70a795ea39b3630e4ec6dc 100644 --- a/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts +++ b/apps/server/src/sourceControl/GitHubSourceControlProvider.test.ts @@ -444,3 +444,38 @@ for (const kind of ["pull", "issues"]) { }), ); } + +for (const stage of ["read", "decode"] as const) { + it.effect(`retains the ${stage} failure without exposing its raw contents`, () => + Effect.gen(function* () { + const cause = new GitHubCli.GitHubCliCommandError({ + command: "gh", + cwd: "/repo", + cause: new Error("private response text"), + }); + const provider = yield* makeProvider({ + execute: () => + stage === "read" + ? Effect.fail(cause) + : Effect.succeed({ + exitCode: ChildProcessSpawner.ExitCode(0), + stdout: "private response text", + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + }), + }); + const lookup = provider.resolveLink?.({ + cwd: "/repo", + url: new URL("https://github.com/owner/repo/issues/42"), + }); + assert.ok(lookup); + const error = yield* Effect.flip(lookup); + assert.strictEqual(error.operation, stage === "read" ? "resolveLink" : "resolveLink.decode"); + assert.strictEqual(error.detail, "The linked subject could not be read."); + assert.notInclude(error.message, "private response text"); + if (stage === "read") assert.strictEqual(error.cause, cause); + else assert.propertyVal(error.cause, "_tag", "SchemaError"); + }), + ); +} diff --git a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts b/apps/server/src/sourceControl/GitHubSourceControlProvider.ts index a48299e9680a15c004427f216c4305b5516e9f91..bb866292868802b17e77706c3c8e83d454f8aac7 100644 --- a/apps/server/src/sourceControl/GitHubSourceControlProvider.ts +++ b/apps/server/src/sourceControl/GitHubSourceControlProvider.ts @@ -219,7 +219,7 @@ export const make = Effect.gen(function* () { input: { readonly cwd: string; readonly url: URL }, endpoint: string, ) { - return yield* github + const result = yield* github .execute({ cwd: input.cwd, args: ["api", "--hostname", input.url.host, endpoint, "--jq", "{title, body}"], @@ -228,8 +228,6 @@ export const make = Effect.gen(function* () { maxOutputBytes: 32_000, }) .pipe( - Effect.flatMap((result) => decodeLinkSubject(result.stdout)), - Effect.map((subject) => ({ title: subject.title, body: subject.body })), Effect.mapError( (cause) => new SourceControlProviderError({ @@ -241,6 +239,19 @@ export const make = Effect.gen(function* () { }), ), ); + const subject = yield* decodeLinkSubject(result.stdout).pipe( + Effect.mapError( + (cause) => + new SourceControlProviderError({ + provider: "github", + operation: "resolveLink.decode", + cwd: input.cwd, + detail: "The linked subject could not be read.", + cause, + }), + ), + ); + return { title: subject.title, body: subject.body }; }); return SourceControlProvider.SourceControlProvider.of({ diff --git a/apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts b/apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts index c8bdec6a68fd4a98b6abc0a4e629746637fc2ee2..deca59c48b90ba67b005d659b7690c0b81e891b3 100644 --- a/apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts +++ b/apps/server/src/sourceControl/GitLabSourceControlProvider.test.ts @@ -271,3 +271,39 @@ for (const kind of ["merge_requests", "issues"]) { }), ); } + +for (const stage of ["read", "decode"] as const) { + it.effect(`retains the ${stage} failure without exposing its raw contents`, () => + Effect.gen(function* () { + const cause = new GitLabCli.GitLabCliCommandError({ + command: "glab", + cwd: "/repo", + operation: "execute", + cause: new Error("private response text"), + }); + const provider = yield* makeProvider({ + execute: () => + stage === "read" + ? Effect.fail(cause) + : Effect.succeed({ + exitCode: ChildProcessSpawner.ExitCode(0), + stdout: "private response text", + stderr: "", + stdoutTruncated: false, + stderrTruncated: false, + }), + }); + const lookup = provider.resolveLink?.({ + cwd: "/repo", + url: new URL("https://gitlab.com/owner/repo/-/issues/42"), + }); + assert.ok(lookup); + const error = yield* Effect.flip(lookup); + assert.strictEqual(error.operation, stage === "read" ? "resolveLink" : "resolveLink.decode"); + assert.strictEqual(error.detail, "The linked subject could not be read."); + assert.notInclude(error.message, "private response text"); + if (stage === "read") assert.strictEqual(error.cause, cause); + else assert.propertyVal(error.cause, "_tag", "SchemaError"); + }), + ); +} diff --git a/apps/server/src/sourceControl/GitLabSourceControlProvider.ts b/apps/server/src/sourceControl/GitLabSourceControlProvider.ts index da3becc62323f50fdb03c27666278f777afdb65f..00753bef7dea7f13c402b2401105c0556ee990b0 100644 --- a/apps/server/src/sourceControl/GitLabSourceControlProvider.ts +++ b/apps/server/src/sourceControl/GitLabSourceControlProvider.ts @@ -116,7 +116,7 @@ export const make = Effect.gen(function* () { input: { readonly cwd: string; readonly url: URL }, endpoint: string, ) { - return yield* gitlab + const result = yield* gitlab .execute({ cwd: input.cwd, args: ["api", "--hostname", input.url.host, endpoint], @@ -124,8 +124,6 @@ export const make = Effect.gen(function* () { maxOutputBytes: 32_000, }) .pipe( - Effect.flatMap((result) => decodeLinkSubject(result.stdout)), - Effect.map((subject) => ({ title: subject.title, body: subject.description })), Effect.mapError( (cause) => new SourceControlProviderError({ @@ -137,6 +135,19 @@ export const make = Effect.gen(function* () { }), ), ); + const subject = yield* decodeLinkSubject(result.stdout).pipe( + Effect.mapError( + (cause) => + new SourceControlProviderError({ + provider: "gitlab", + operation: "resolveLink.decode", + cwd: input.cwd, + detail: "The linked subject could not be read.", + cause, + }), + ), + ); + return { title: subject.title, body: subject.description }; }); return SourceControlProvider.SourceControlProvider.of({ diff --git a/apps/server/src/textGeneration/TextGeneration.ts b/apps/server/src/textGeneration/TextGeneration.ts index 703da54ce6d1a9ff93ace5616ae5b31ace39898b..27fc28df7bf7c30ee7e90a3fad8350b38e775fca 100644 --- a/apps/server/src/textGeneration/TextGeneration.ts +++ b/apps/server/src/textGeneration/TextGeneration.ts @@ -7,7 +7,7 @@ import { TextGenerationError } from "@t3tools/contracts"; import * as ProviderInstanceRegistry from "../provider/Services/ProviderInstanceRegistry.ts"; import type { ProviderInstance } from "../provider/ProviderDriver.ts"; import * as SourceControlProviderRegistry from "../sourceControl/SourceControlProviderRegistry.ts"; -import { resolveThreadTitleLinks } from "./ThreadTitleLinks.ts"; +import * as ThreadTitleLinks from "./ThreadTitleLinks.ts"; import type { TextGenerationPolicy } from "./TextGenerationPolicy.ts"; export type TextGenerationProvider = "codex" | "claudeAgent" | "cursor" | "grok" | "opencode"; @@ -158,7 +158,7 @@ export const make = Effect.gen(function* () { Effect.gen(function* () { const linkedContext = input.linkedContext ?? - (yield* resolveThreadTitleLinks(input).pipe( + (yield* ThreadTitleLinks.resolveThreadTitleLinks(input).pipe( Effect.provideService( SourceControlProviderRegistry.SourceControlProviderRegistry, sourceControl,