From ffceb61fcbbdc7ac5268812a1cc5dfedd7a6f7b9 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Wed, 14 Oct 2026 02:57:52 -0700 Subject: [PATCH] fix(cli): t3 triage reports the server's version, not its own, as the user's build (#19359) Co-authored-by: Claude Opus 5.5 --- .github/ISSUE_TEMPLATE/via-triage.yml | 2 +- .github/triage/PLAYBOOK.md | 32 +++++++++--- apps/server/src/cli/triage.test.ts | 62 ++++++++++++++++++++++++ apps/server/src/cli/triage.ts | 23 +++++---- apps/server/src/cli/triagePrompt.test.ts | 35 +++++++++++-- apps/server/src/cli/triagePrompt.ts | 40 ++++++++++----- apps/server/src/serverRuntimeState.ts | 9 ++++ 7 files changed, 168 insertions(+), 35 deletions(-) create mode 100644 apps/server/src/cli/triage.test.ts diff --git a/.github/ISSUE_TEMPLATE/via-triage.yml b/.github/ISSUE_TEMPLATE/via-triage.yml index 5b8465b8798ebb375e38a182a274268f3cf3d7fd..b339d59e2ba318840061bac701175bb8eac0e123 100644 --- a/.github/ISSUE_TEMPLATE/via-triage.yml +++ b/.github/ISSUE_TEMPLATE/via-triage.yml @@ -37,7 +37,7 @@ body: id: version attributes: label: Version - description: Installed t3 version or commit. + description: Version the bug happened on (not the `t3 triage` CLI version), or unknown. placeholder: 0.0.33 validations: required: true diff --git a/.github/triage/PLAYBOOK.md b/.github/triage/PLAYBOOK.md index 32def6bc0c1681784ef8d4a93c85d414005b9b17..13ca38fed4ad61f66cc5aabb89c83af5d8b4e7c3 100644 --- a/.github/triage/PLAYBOOK.md +++ b/.github/triage/PLAYBOOK.md @@ -6,7 +6,7 @@ crashes, auth failures, broken setups, slow launches, or anything else. Your job find out what went wrong, unblock the user if you can, and turn what you learned into a well written GitHub issue when one is warranted. -A triage context file with machine facts (version, OS, paths, server liveness) was +A triage context file with machine facts (versions, OS, paths, server liveness) was provided alongside this playbook. Everything machine-specific lives there, not here. ## 1. Ask what went wrong @@ -18,9 +18,15 @@ valuable thing you can extract from this conversation. ## 2. Read the machine facts -Read the triage context file before investigating. It tells you the installed -version, the OS, whether the server process is currently running, and the exact -paths for state, logs, and the database. +Read the triage context file before investigating. It tells you the OS, whether +the server process is currently running, and the exact paths for state, logs, and +the database. + +Its triage CLI version (called "Installed version" in older context files) is only +the version of the `t3` that ran `t3 triage`. It is often not the build the bug +happened on: `npx t3 triage` runs whatever `t3` npm resolves, while the user may +run the desktop app, the background service, or a nightly. The server process line +gives the local server's version when the server recorded one. ## 3. Check for a newer playbook @@ -30,8 +36,16 @@ instead of this one. The user may be on an old release with an old copy. ## 4. Get the source -Clone the repo at the tag matching the user's installed version, into the source -cache directory named in the context file, one subdirectory per commit hash: +Before cloning, ask the user which version, device, and surface the bug happened +on. The bug may have happened on another machine, a remote server, the mobile app, +or an older build they have since updated. Check their answer against local facts +where they apply: the server version in the context file, or `serverVersion` from +`/.well-known/t3/environment` when a local server is running. If neither +the user nor the machine can tell you the version, say it is unknown. Do not guess. + +Clone the repo at the release tag for that version (`v` followed by the version, +such as `v0.0.42`), into the source cache directory named in the context file, +one subdirectory per commit hash: git clone --depth 1 --filter=blob:none --branch \ https://github.com/pingdotgg/t3code / @@ -82,8 +96,8 @@ comes from this repo's `main` branch. Search existing issues in pingdotgg/t3code (use `gh`, or the public GitHub search API if `gh` is missing or not logged in). Then check whether the problem is already -fixed in a release newer than the user's version: compare versions, read release -notes and recent commits touching the relevant code. +fixed in a release newer than the version the bug happened on: compare versions, +read release notes and recent commits touching the relevant code. If the user is behind and the fix likely shipped, say so plainly and give them the exact update command for how they run the CLI (the context file records how it was @@ -105,6 +119,8 @@ of `main` for that work, never the tag-pinned diagnosis clone. - Match the structure of the `via-triage` issue template (`.github/ISSUE_TEMPLATE/via-triage.yml` in the repo): what happened, diagnosis, repro steps, environment, evidence, related issues. +- Under Version, give the version the bug happened on from step 4, or say it is + unknown. Name the triage CLI version only as the CLI's. - Label it `via-triage`. Use a plain, specific title with no prefix. - Show the user the complete final issue text and get an explicit yes before posting. Never post without it. diff --git a/apps/server/src/cli/triage.test.ts b/apps/server/src/cli/triage.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..0d06f7ee78dfab89ba2a2ce7533899f60b54f0d6 --- /dev/null +++ b/apps/server/src/cli/triage.test.ts @@ -0,0 +1,62 @@ +import * as NodeServices from "@effect/platform-node/NodeServices"; +import { assert, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Path from "effect/Path"; + +import packageJson from "../../package.json" with { type: "json" }; +import { + makePersistedServerRuntimeState, + persistServerRuntimeState, +} from "../serverRuntimeState.ts"; +import { describeServerProcess } from "./triage.ts"; + +it.effect("reports the version the running server recorded, not the CLI's", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const statePath = path.join( + yield* fs.makeTempDirectoryScoped({ prefix: "t3-triage-" }), + "server-runtime.json", + ); + // The test process stands in for the server, so its pid is alive. + const state = yield* makePersistedServerRuntimeState({ + config: { host: undefined, devUrl: undefined }, + port: 3773, + }); + yield* persistServerRuntimeState({ path: statePath, state }); + + assert.equal( + yield* describeServerProcess(statePath), + `running (pid ${String(process.pid)}, http://127.0.0.1:3773, version ${packageJson.version})`, + ); + }).pipe(Effect.provide(NodeServices.layer)), +); + +it.effect("says the server version is unknown when the state file has none", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const statePath = path.join( + yield* fs.makeTempDirectoryScoped({ prefix: "t3-triage-" }), + "server-runtime.json", + ); + // A server from before versions were recorded, such as the 0.0.43 desktop + // app in #13337. + yield* fs.writeFileString( + statePath, + JSON.stringify({ + version: 1, + pid: process.pid, + port: 3773, + origin: "http://127.0.0.1:3773", + startedAt: "2026-09-24T00:08:56.777Z", + }), + ); + + assert.equal( + yield* describeServerProcess(statePath), + `running (pid ${String(process.pid)}, http://127.0.0.1:3773, version not recorded)`, + ); + }).pipe(Effect.provide(NodeServices.layer)), +); diff --git a/apps/server/src/cli/triage.ts b/apps/server/src/cli/triage.ts index 7bce1c0251ca0fd4d9247ec19997c641467ea3f8..8c666f09ed997c89289a64aef849caa47e86cb26 100644 --- a/apps/server/src/cli/triage.ts +++ b/apps/server/src/cli/triage.ts @@ -2,7 +2,7 @@ * `t3 triage` - hand a misbehaving install to the user's own coding agent. * * The command is deliberately thin: it writes a `context.md` with machine facts - * (version, paths, server liveness), then launches claude or codex + * (versions, paths, server liveness), then launches claude or codex * interactively, seeded with the playbook from `triagePrompt.ts`. The agent * asks the user what went wrong, investigates, and files the issue; the * harness's own permission prompts gate anything it wants to run. With no @@ -77,8 +77,11 @@ export class TriageAgentSpawnError extends Schema.TaggedError) => @@ -183,16 +190,12 @@ export const triageCommand = Command.make("triage", { ); yield* fs.makeDirectory(scratchDir, { recursive: true }); - const version = packageJson.version; const contextFilePath = path.join(scratchDir, "context.md"); yield* fs.writeFileString( contextFilePath, buildTriageContext({ generatedAt: DateTime.formatIso(now), - version, - releaseTag: /^[^-+]+-(?:nightly|preview)\./.test(version) - ? `v${version} (prerelease build; if this tag does not exist, clone main)` - : `v${version}`, + cliVersion: packageJson.version, os: `${yield* HostProcess.Platform} ${yield* HostProcess.Architecture} (${NodeOS.release()})`, nodeVersion: process.version, launchedAs: yield* resolveCliCommand("triage"), diff --git a/apps/server/src/cli/triagePrompt.test.ts b/apps/server/src/cli/triagePrompt.test.ts index fe65bf0dc444192581c87089c66400e75c444038..a9b2092fa2bd7302bde0ef7c5a81ada67272e711 100644 --- a/apps/server/src/cli/triagePrompt.test.ts +++ b/apps/server/src/cli/triagePrompt.test.ts @@ -41,8 +41,7 @@ it("launch prompt stays a single argv-safe line naming the prompt file", () => { it("context file carries every path the playbook depends on", () => { const context = buildTriageContext({ generatedAt: "2026-08-13T00:00:00.000Z", - version: "0.0.33", - releaseTag: "v0.0.33", + cliVersion: "0.0.33", os: "linux x64 (7.0.0)", nodeVersion: "v24.0.0", launchedAs: "npx t3 triage", @@ -70,5 +69,35 @@ it("context file carries every path the playbook depends on", () => { assert.include(context, "/home/u/.t3/userdata/secrets"); assert.include(context, "/home/u/.t3/source"); assert.include(context, "npx t3 triage"); - assert.include(context, "v0.0.33"); +}); + +it("context file labels the CLI version as the CLI's, not the user's build", () => { + // `npx t3 triage` can resolve a different `t3` than the app the bug + // happened on (#13337), so the agent must not read it as the user's version. + const context = buildTriageContext({ + generatedAt: "2026-09-24T00:00:00.000Z", + cliVersion: "0.0.42", + os: "linux x64 (7.2.0)", + nodeVersion: "v26.8.2", + launchedAs: "npx t3 triage", + server: "running (pid 42, http://127.0.0.1:3773, version 0.0.43-nightly.20260923.2173)", + paths: { + stateDir: "/home/u/.t3/userdata", + dbPath: "/home/u/.t3/userdata/state.sqlite", + settingsPath: "/home/u/.t3/userdata/settings.json", + logsDir: "/home/u/.t3/userdata/logs", + serviceLogPath: "/home/u/.t3/userdata/logs/boot-service.log", + desktopBackendLogGlob: "/home/u/.t3/userdata/logs/server-child*.log*", + serverTracePath: "/home/u/.t3/userdata/logs/server.trace.ndjson", + providerEventLogPath: "/home/u/.t3/userdata/logs/provider/events.log", + terminalLogsDir: "/home/u/.t3/userdata/logs/terminals", + providerStatusCacheDir: "/home/u/.t3/caches", + secretsDir: "/home/u/.t3/userdata/secrets", + sourceCacheDir: "/home/u/.t3/source", + }, + }); + assert.notInclude(context, "Installed version"); + assert.notInclude(context, "Release tag"); + assert.include(context, "- Triage CLI version: 0.0.42 ("); + assert.include(context, "version 0.0.43-nightly.20260923.2173"); }); diff --git a/apps/server/src/cli/triagePrompt.ts b/apps/server/src/cli/triagePrompt.ts index 1df7128542635dc1a852c96bc4d937aeceb8c061..c36bcc2b752fa58d3691c75720b51200f454c74d 100644 --- a/apps/server/src/cli/triagePrompt.ts +++ b/apps/server/src/cli/triagePrompt.ts @@ -18,7 +18,7 @@ crashes, auth failures, broken setups, slow launches, or anything else. Your job find out what went wrong, unblock the user if you can, and turn what you learned into a well written GitHub issue when one is warranted. -A triage context file with machine facts (version, OS, paths, server liveness) was +A triage context file with machine facts (versions, OS, paths, server liveness) was provided alongside this playbook. Everything machine-specific lives there, not here. ## 1. Ask what went wrong @@ -30,9 +30,15 @@ valuable thing you can extract from this conversation. ## 2. Read the machine facts -Read the triage context file before investigating. It tells you the installed -version, the OS, whether the server process is currently running, and the exact -paths for state, logs, and the database. +Read the triage context file before investigating. It tells you the OS, whether +the server process is currently running, and the exact paths for state, logs, and +the database. + +Its triage CLI version (called "Installed version" in older context files) is only +the version of the \`t3\` that ran \`t3 triage\`. It is often not the build the bug +happened on: \`npx t3 triage\` runs whatever \`t3\` npm resolves, while the user may +run the desktop app, the background service, or a nightly. The server process line +gives the local server's version when the server recorded one. ## 3. Check for a newer playbook @@ -42,8 +48,16 @@ instead of this one. The user may be on an old release with an old copy. ## 4. Get the source -Clone the repo at the tag matching the user's installed version, into the source -cache directory named in the context file, one subdirectory per commit hash: +Before cloning, ask the user which version, device, and surface the bug happened +on. The bug may have happened on another machine, a remote server, the mobile app, +or an older build they have since updated. Check their answer against local facts +where they apply: the server version in the context file, or \`serverVersion\` from +\`/.well-known/t3/environment\` when a local server is running. If neither +the user nor the machine can tell you the version, say it is unknown. Do not guess. + +Clone the repo at the release tag for that version (\`v\` followed by the version, +such as \`v0.0.42\`), into the source cache directory named in the context file, +one subdirectory per commit hash: git clone --depth 1 --filter=blob:none --branch \\ https://github.com/pingdotgg/t3code / @@ -94,8 +108,8 @@ comes from this repo's \`main\` branch. Search existing issues in pingdotgg/t3code (use \`gh\`, or the public GitHub search API if \`gh\` is missing or not logged in). Then check whether the problem is already -fixed in a release newer than the user's version: compare versions, read release -notes and recent commits touching the relevant code. +fixed in a release newer than the version the bug happened on: compare versions, +read release notes and recent commits touching the relevant code. If the user is behind and the fix likely shipped, say so plainly and give them the exact update command for how they run the CLI (the context file records how it was @@ -117,6 +131,8 @@ of \`main\` for that work, never the tag-pinned diagnosis clone. - Match the structure of the \`via-triage\` issue template (\`.github/ISSUE_TEMPLATE/via-triage.yml\` in the repo): what happened, diagnosis, repro steps, environment, evidence, related issues. +- Under Version, give the version the bug happened on from step 4, or say it is + unknown. Name the triage CLI version only as the CLI's. - Label it \`via-triage\`. Use a plain, specific title with no prefix. - Show the user the complete final issue text and get an explicit yes before posting. Never post without it. @@ -153,7 +169,7 @@ export const buildTriageLaunchPrompt = (promptFilePath: string) => export const buildTriageSeedPrompt = (contextFilePath: string) => `A T3 Code user is \ having a problem with their install and started this session with \`t3 triage\`. -Machine facts (version, OS, paths, server liveness) are in the triage context file: +Machine facts (versions, OS, paths, server liveness) are in the triage context file: ${contextFilePath} @@ -166,8 +182,7 @@ ${TRIAGE_PLAYBOOK}`; /** Machine facts for one triage run, pre-formatted so the template stays plain. */ export interface TriageContextInput { readonly generatedAt: string; - readonly version: string; - readonly releaseTag: string; + readonly cliVersion: string; readonly os: string; readonly nodeVersion: string; readonly launchedAs: string; @@ -193,8 +208,7 @@ export const buildTriageContext = (input: TriageContextInput) => `# T3 Code tria Generated by \`t3 triage\` at ${input.generatedAt}. -- Installed version: ${input.version} -- Release tag for this version: ${input.releaseTag} +- Triage CLI version: ${input.cliVersion} (the \`t3\` that ran this command; the bug may have happened on another build) - OS: ${input.os} - Node: ${input.nodeVersion} - CLI launched as: ${input.launchedAs} diff --git a/apps/server/src/serverRuntimeState.ts b/apps/server/src/serverRuntimeState.ts index 862b3174b7949a704f2ec008eaef529ab06c98a1..fd11a74f94573e2db71553bd08ae71cf4db6daa1 100644 --- a/apps/server/src/serverRuntimeState.ts +++ b/apps/server/src/serverRuntimeState.ts @@ -5,10 +5,12 @@ import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; +import packageJson from "../package.json" with { type: "json" }; import type * as ServerConfig from "./config.ts"; import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts"; export const PersistedServerRuntimeState = Schema.Struct({ + // Schema version of this file, not the server build. See `serverVersion`. version: Schema.Literal(1), pid: Schema.Int, ownerId: Schema.optional(Schema.String), @@ -25,6 +27,12 @@ export const PersistedServerRuntimeState = Schema.Struct({ * the difference between "restart the service" and "stop your terminal". */ serviceManaged: Schema.optional(Schema.Boolean), + /** + * The server build that wrote this record. `t3 triage` reports it because + * the CLI that runs triage can be a different build. Missing on records + * written by older servers. + */ + serverVersion: Schema.optional(Schema.String), }); export type PersistedServerRuntimeState = typeof PersistedServerRuntimeState.Type; @@ -68,6 +76,7 @@ export const makePersistedServerRuntimeState = (input: { ...(input.config.devUrl ? { devUrl: input.config.devUrl.toString() } : {}), startedAt: DateTime.formatIso(now), ...(input.serviceManaged ? { serviceManaged: true } : {}), + serverVersion: packageJson.version, })); export const persistServerRuntimeState = (input: { -- 2.54.0