Repository navigation
fix(server): keep thread titles tied to user intent #10720
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
5fe7dbe
fix(server): keep thread titles tied to user intent
t3dotgg ef585e3
fix(server): expose title lookup dependencies and bound context work
t3dotgg b7b8384
fix(server): retain attachments in title evaluations
t3dotgg 5c27b80
fix(server): reject title comparisons when linked context changes
t3dotgg 5f1d079
fix(server): preserve supplied title context and select user input
t3dotgg 7e159da
fix(server): recover pending title refinement on startup
t3dotgg 2671afa
Merge branch 'main' into t3code/title-context-and-refinement
t3dotgg 8c6ca53
fix(server): cap generated thread titles and move evaluation fixtures…
t3dotgg File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| #!/usr/bin/env node | ||
| // This CLI uses Node argument parsing and random ordering at the application boundary. | ||
| // @effect-diagnostics nodeBuiltinImport:off | ||
| // Run with --model <configured-model> --out /tmp/title-eval. | ||
| // Pass --baseline /tmp/previous-eval/results.json to compare two generation runs. | ||
| // Add --initial to evaluate only the opening request. | ||
| import * as NodeUtil from "node:util"; | ||
| import * as NodeCrypto from "node:crypto"; | ||
| import * as NodeServices from "@effect/platform-node/NodeServices"; | ||
| import { CodexSettings, ProviderInstanceId } from "@t3tools/contracts"; | ||
| import * as Effect from "effect/Effect"; | ||
| import * as Layer from "effect/Layer"; | ||
| 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 { threadTitleEvaluationCases } from "./threadTitleEvaluationCases.ts"; | ||
| import { | ||
| formatThreadTitleContext, | ||
| type ThreadTitleMessage, | ||
| } from "../src/textGeneration/ThreadTitleContext.ts"; | ||
| import { resolveThreadTitleLinks } from "../src/textGeneration/ThreadTitleLinks.ts"; | ||
| import * as ProcessRunner from "../src/processRunner.ts"; | ||
| import * as ServerConfig from "../src/config.ts"; | ||
|
|
||
| const { values } = NodeUtil.parseArgs({ | ||
| options: { | ||
| model: { type: "string" }, | ||
| out: { type: "string" }, | ||
| baseline: { type: "string" }, | ||
| initial: { type: "boolean", default: false }, | ||
| }, | ||
| }); | ||
| if (!values.model || !values.out) | ||
| throw new Error("Use --model <configured-model> --out <directory>."); | ||
| const model = values.model; | ||
| const outputDirectory = values.out; | ||
| const Results = Schema.fromJsonString( | ||
| Schema.Array( | ||
| Schema.Struct({ | ||
| id: Schema.String, | ||
| title: Schema.String, | ||
| latencyMs: Schema.Number, | ||
| linkedContextDigest: Schema.String, | ||
| }), | ||
| ), | ||
| ); | ||
| const decodeResults = Schema.decodeUnknownEffect(Results); | ||
| const decodeSettings = Schema.decodeUnknownEffect(CodexSettings); | ||
| const encodeReport = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); | ||
|
|
||
| await Effect.runPromise( | ||
| Effect.gen(function* () { | ||
| 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 baseline = values.baseline | ||
| ? yield* fs.readFileString(values.baseline).pipe(Effect.flatMap(decodeResults)) | ||
| : []; | ||
| const results = []; | ||
| const review = []; | ||
| const answerKey = []; | ||
| for (const fixture of threadTitleEvaluationCases) { | ||
| const previous = baseline.find((entry) => entry.id === fixture.id); | ||
| if (values.baseline && !previous) throw new Error(`Baseline is missing ${fixture.id}.`); | ||
| const firstMessage: ThreadTitleMessage | undefined = fixture.messages.find( | ||
| (message) => message.role === "user", | ||
| ); | ||
| if (!firstMessage) throw new Error(`Fixture ${fixture.id} has no user message.`); | ||
| const context = formatThreadTitleContext(fixture.messages); | ||
|
t3dotgg marked this conversation as resolved.
|
||
| 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({ | ||
| cwd, | ||
| message, | ||
| }); | ||
| const linkedContextDigest = NodeCrypto.createHash("sha256") | ||
| .update(linkedContext ?? "") | ||
| .digest("hex"); | ||
| if (previous && previous.linkedContextDigest !== linkedContextDigest) { | ||
| throw new Error( | ||
| `Linked context changed for ${fixture.id}. Record a new baseline before comparing titles.`, | ||
| ); | ||
| } | ||
| const generated = yield* generation.generateThreadTitle({ | ||
| cwd, | ||
| message, | ||
| previousTitle: values.initial ? undefined : fixture.previousTitle, | ||
| attachments, | ||
| linkedContext, | ||
| modelSelection: { instanceId: ProviderInstanceId.make("codex"), model }, | ||
| }); | ||
| return { generated, linkedContextDigest }; | ||
| }).pipe(Effect.timed); | ||
| const oldTitle = previous?.title ?? fixture.previousTitle; | ||
| const newFirst = NodeCrypto.randomInt(2) === 0; | ||
| results.push({ | ||
| id: fixture.id, | ||
| title: generated.title, | ||
| latencyMs: Duration.toMillis(elapsed), | ||
| needsRefinement: generated.needsRefinement ?? false, | ||
| linkedContextDigest, | ||
| }); | ||
| review.push({ | ||
| id: fixture.id, | ||
| source: fixture.source, | ||
| request: fixture.request, | ||
| rubric: fixture.rubric, | ||
| A: newFirst ? generated.title : oldTitle, | ||
| B: newFirst ? oldTitle : generated.title, | ||
| preferred: "", | ||
| subjectAccuracy: "", | ||
| recognitionAmongNearbyThreads: "", | ||
| }); | ||
| answerKey.push({ id: fixture.id, candidate: newFirst ? "A" : "B" }); | ||
| } | ||
| yield* fs.makeDirectory(outputDirectory, { recursive: true }); | ||
| for (const [name, report] of [ | ||
| ["results", results], | ||
| ["review", review], | ||
| ["answer-key", answerKey], | ||
| ] as const) { | ||
| yield* fs.writeFileString( | ||
| path.join(outputDirectory, `${name}.json`), | ||
| yield* encodeReport(report), | ||
| ); | ||
| } | ||
| yield* Effect.log( | ||
| `Wrote ${results.length} cases to ${outputDirectory}. Score review.json before opening answer-key.json. Latency is in results.json.`, | ||
| ); | ||
| }).pipe( | ||
| Effect.provide( | ||
| Layer.mergeAll( | ||
| ProcessRunner.layer, | ||
| ServerConfig.layerTest(process.cwd(), { prefix: "t3-title-evaluation-state-" }), | ||
| ).pipe(Layer.provideMerge(NodeServices.layer)), | ||
| ), | ||
| Effect.scoped, | ||
| ), | ||
| ); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import type { ThreadTitleMessage } from "../src/textGeneration/ThreadTitleContext.ts"; | ||
|
|
||
| // Public PR subjects and existing title scenarios. Repeated text adds context pressure. | ||
| export const threadTitleEvaluationCases = [ | ||
| { | ||
| id: "linked-reset-credits", | ||
| source: "https://github.com/pingdotgg/t3code/pull/10462", | ||
| request: "Review the reset credit routing change.", | ||
| previousTitle: "Review PR 10462", | ||
| messages: [{ role: "user", text: "Review https://github.com/pingdotgg/t3code/pull/10462" }], | ||
| rubric: "Name reset credit routing. Distinguish it from displaying credit balances.", | ||
| }, | ||
| { | ||
| id: "onboarding-merge", | ||
| source: "https://github.com/pingdotgg/t3code/pull/10465", | ||
| request: "Make onboarding one shared wizard across computers, then merge when green.", | ||
| previousTitle: "Finish onboarding PR", | ||
| messages: [ | ||
| { role: "user", text: "Make onboarding one shared wizard across computers." }, | ||
| { | ||
| role: "assistant", | ||
| text: "The wizard now handles pairing, agent selection, and project import.", | ||
| }, | ||
| { role: "user", text: "File a PR and merge it when green." }, | ||
| ], | ||
| rubric: "Keep the multi-computer onboarding subject. Do not title it after merging.", | ||
| }, | ||
| { | ||
| id: "vague-opening", | ||
| source: "Existing lazy thread feed title scenario", | ||
| request: "A failing test is later identified as a lazy thread feed mismatch.", | ||
| previousTitle: "Fix failing test", | ||
| messages: [ | ||
| { role: "user", text: "Fix this failing test." }, | ||
| { | ||
| role: "assistant", | ||
| text: "The lazy thread feed test expects a full message body before the client requests it.", | ||
| }, | ||
| ], | ||
| rubric: "Name the lazy thread feed test. Do not invent a wider mobile regression.", | ||
| }, | ||
| { | ||
| id: "scope-change", | ||
| source: "Title context budget scenario", | ||
| request: "Change the goal from QR layout to pairing expiry, despite long assistant replies.", | ||
| previousTitle: "Improve QR layout", | ||
| messages: [ | ||
| { role: "user", text: "Improve QR sharing layout." }, | ||
| { | ||
| role: "user", | ||
| text: "Change of plan. Fix pairing token expiry. Keep remote access working.", | ||
| }, | ||
| { | ||
| role: "assistant", | ||
| text: "The token expires before redemption. " + "Implementation detail. ".repeat(800), | ||
| }, | ||
| { role: "user", text: "Ship it." }, | ||
| ], | ||
| rubric: "Name pairing expiry and honor the explicit scope change.", | ||
| }, | ||
| { | ||
| id: "review-umbrella", | ||
| source: "Existing subagent monitoring title scenario", | ||
| request: "Review subagent monitoring risks. A Codex roster issue is one finding.", | ||
| previousTitle: "Review subagent monitoring risks", | ||
| messages: [ | ||
| { role: "user", text: "Review subagent monitoring risks." }, | ||
| { | ||
| role: "assistant", | ||
| text: "One finding is a stale Codex roster. " + "Roster detail. ".repeat(800), | ||
| }, | ||
| { role: "user", text: "Fix the findings and babysit CI." }, | ||
| ], | ||
| rubric: "Preserve the monitoring review scope. The previous title can stay unchanged.", | ||
| }, | ||
| { | ||
| id: "long-opening", | ||
| source: "Title message truncation scenario", | ||
| request: "Investigate Android pairing while preserving the iOS flow.", | ||
| previousTitle: "Inspect logs", | ||
| messages: [ | ||
| { | ||
| role: "user", | ||
| text: | ||
| "Investigate Android pairing. " + | ||
| "Connection logs. ".repeat(800) + | ||
| " Preserve the iOS pairing flow.", | ||
| }, | ||
| ], | ||
| rubric: "Name Android pairing. Logs are supporting evidence.", | ||
| }, | ||
| { | ||
| id: "research", | ||
| source: "Maintainer title generation request", | ||
| request: "How can we improve title generation in T3 Code?", | ||
| previousTitle: "Research title gen improvements", | ||
| messages: [ | ||
| { role: "user", text: "How can we improve title gen further in T3 Code?" }, | ||
| { | ||
| role: "assistant", | ||
| text: "Prioritize user messages, refine vague titles once, and resolve PR subjects.", | ||
| }, | ||
| { | ||
| role: "user", | ||
| text: "Make these changes and file a PR. Babysit until everything is green.", | ||
| }, | ||
| ], | ||
| rubric: "Keep title generation as the subject. Do not focus on filing the PR.", | ||
| }, | ||
| ] satisfies ReadonlyArray<{ | ||
| id: string; | ||
| source: string; | ||
| request: string; | ||
| previousTitle: string; | ||
| messages: ReadonlyArray<ThreadTitleMessage>; | ||
| rubric: string; | ||
| }>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.