authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 02:57:52-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 02:57:52-07:00
logffceb61fcbbdc7ac5268812a1cc5dfedd7a6f7b9
tree0e9dd26b9523d782cf8bd866f276b75ea9049ca8
parent0ae1091d019484d474fce0fe0a89195a82134041
signature Signed by PGP key B5690EEEBB952194

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 <noreply@anthropic.com>

7 files changed, 168 insertions(+), 35 deletions(-)

.github/ISSUE_TEMPLATE/via-triage.yml+1-1
......@@ -37,7 +37,7 @@ body:
3737 id: version
3838 attributes:
3939 label: Version
40 description: Installed t3 version or commit.
40 description: Version the bug happened on (not the `t3 triage` CLI version), or unknown.
4141 placeholder: 0.0.33
4242 validations:
4343 required: true
.github/triage/PLAYBOOK.md+24-8
......@@ -6,7 +6,7 @@ crashes, auth failures, broken setups, slow launches, or anything else. Your job
66find out what went wrong, unblock the user if you can, and turn what you learned into
77a well written GitHub issue when one is warranted.
88
9A triage context file with machine facts (version, OS, paths, server liveness) was
9A triage context file with machine facts (versions, OS, paths, server liveness) was
1010provided alongside this playbook. Everything machine-specific lives there, not here.
1111
1212## 1. Ask what went wrong
......@@ -18,9 +18,15 @@ valuable thing you can extract from this conversation.
1818
1919## 2. Read the machine facts
2020
21Read the triage context file before investigating. It tells you the installed
22version, the OS, whether the server process is currently running, and the exact
23paths for state, logs, and the database.
21Read the triage context file before investigating. It tells you the OS, whether
22the server process is currently running, and the exact paths for state, logs, and
23the database.
24
25Its triage CLI version (called "Installed version" in older context files) is only
26the version of the `t3` that ran `t3 triage`. It is often not the build the bug
27happened on: `npx t3 triage` runs whatever `t3` npm resolves, while the user may
28run the desktop app, the background service, or a nightly. The server process line
29gives the local server's version when the server recorded one.
2430
2531## 3. Check for a newer playbook
2632
......@@ -30,8 +36,16 @@ instead of this one. The user may be on an old release with an old copy.
3036
3137## 4. Get the source
3238
33Clone the repo at the tag matching the user's installed version, into the source
34cache directory named in the context file, one subdirectory per commit hash:
39Before cloning, ask the user which version, device, and surface the bug happened
40on. The bug may have happened on another machine, a remote server, the mobile app,
41or an older build they have since updated. Check their answer against local facts
42where they apply: the server version in the context file, or `serverVersion` from
43`<origin>/.well-known/t3/environment` when a local server is running. If neither
44the user nor the machine can tell you the version, say it is unknown. Do not guess.
45
46Clone the repo at the release tag for that version (`v` followed by the version,
47such as `v0.0.42`), into the source cache directory named in the context file,
48one subdirectory per commit hash:
3549
3650 git clone --depth 1 --filter=blob:none --branch <release-tag> \
3751 https://github.com/pingdotgg/t3code <source-cache-dir>/<hash>
......@@ -82,8 +96,8 @@ comes from this repo's `main` branch.
8296
8397Search existing issues in pingdotgg/t3code (use `gh`, or the public GitHub search
8498API if `gh` is missing or not logged in). Then check whether the problem is already
85fixed in a release newer than the user's version: compare versions, read release
86notes and recent commits touching the relevant code.
99fixed in a release newer than the version the bug happened on: compare versions,
100read release notes and recent commits touching the relevant code.
87101
88102If the user is behind and the fix likely shipped, say so plainly and give them the
89103exact 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.
105119- Match the structure of the `via-triage` issue template
106120 (`.github/ISSUE_TEMPLATE/via-triage.yml` in the repo): what happened, diagnosis,
107121 repro steps, environment, evidence, related issues.
122- Under Version, give the version the bug happened on from step 4, or say it is
123 unknown. Name the triage CLI version only as the CLI's.
108124- Label it `via-triage`. Use a plain, specific title with no prefix.
109125- Show the user the complete final issue text and get an explicit yes before
110126 posting. Never post without it.
apps/server/src/cli/triage.test.ts created+62
......@@ -0,0 +1,62 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
6
7import packageJson from "../../package.json" with { type: "json" };
8import {
9 makePersistedServerRuntimeState,
10 persistServerRuntimeState,
11} from "../serverRuntimeState.ts";
12import { describeServerProcess } from "./triage.ts";
13
14it.effect("reports the version the running server recorded, not the CLI's", () =>
15 Effect.gen(function* () {
16 const fs = yield* FileSystem.FileSystem;
17 const path = yield* Path.Path;
18 const statePath = path.join(
19 yield* fs.makeTempDirectoryScoped({ prefix: "t3-triage-" }),
20 "server-runtime.json",
21 );
22 // The test process stands in for the server, so its pid is alive.
23 const state = yield* makePersistedServerRuntimeState({
24 config: { host: undefined, devUrl: undefined },
25 port: 3773,
26 });
27 yield* persistServerRuntimeState({ path: statePath, state });
28
29 assert.equal(
30 yield* describeServerProcess(statePath),
31 `running (pid ${String(process.pid)}, http://127.0.0.1:3773, version ${packageJson.version})`,
32 );
33 }).pipe(Effect.provide(NodeServices.layer)),
34);
35
36it.effect("says the server version is unknown when the state file has none", () =>
37 Effect.gen(function* () {
38 const fs = yield* FileSystem.FileSystem;
39 const path = yield* Path.Path;
40 const statePath = path.join(
41 yield* fs.makeTempDirectoryScoped({ prefix: "t3-triage-" }),
42 "server-runtime.json",
43 );
44 // A server from before versions were recorded, such as the 0.0.43 desktop
45 // app in #13337.
46 yield* fs.writeFileString(
47 statePath,
48 JSON.stringify({
49 version: 1,
50 pid: process.pid,
51 port: 3773,
52 origin: "http://127.0.0.1:3773",
53 startedAt: "2026-09-24T00:08:56.777Z",
54 }),
55 );
56
57 assert.equal(
58 yield* describeServerProcess(statePath),
59 `running (pid ${String(process.pid)}, http://127.0.0.1:3773, version not recorded)`,
60 );
61 }).pipe(Effect.provide(NodeServices.layer)),
62);
apps/server/src/cli/triage.ts+13-10
......@@ -2,7 +2,7 @@
22 * `t3 triage` - hand a misbehaving install to the user's own coding agent.
33 *
44 * The command is deliberately thin: it writes a `context.md` with machine facts
5 * (version, paths, server liveness), then launches claude or codex
5 * (versions, paths, server liveness), then launches claude or codex
66 * interactively, seeded with the playbook from `triagePrompt.ts`. The agent
77 * asks the user what went wrong, investigates, and files the issue; the
88 * harness's own permission prompts gate anything it wants to run. With no
......@@ -77,8 +77,11 @@ export class TriageAgentSpawnError extends Schema.TaggedError<TriageAgentSpawnEr
7777 }
7878}
7979
80/** One human-readable line about the local server, for `context.md`. */
81const describeServerProcess = Effect.fn("triage.describeServerProcess")(function* (
80/**
81 * One human-readable line about the local server, for `context.md`. Includes
82 * the version the server recorded, which can differ from this CLI's version.
83 */
84export const describeServerProcess = Effect.fn("triage.describeServerProcess")(function* (
8285 serverRuntimeStatePath: string,
8386) {
8487 // readPersistedServerRuntimeState swallows read/decode failures itself and
......@@ -87,10 +90,14 @@ const describeServerProcess = Effect.fn("triage.describeServerProcess")(function
8790 if (Option.isNone(state)) {
8891 return "not running (no server-runtime.json; the server may never have started here)";
8992 }
93 const version =
94 state.value.serverVersion === undefined
95 ? "version not recorded"
96 : `version ${state.value.serverVersion}`;
9097 if (!isProcessAlive(state.value.pid)) {
91 return `not running (state file is stale: pid ${String(state.value.pid)} is dead; last origin ${state.value.origin})`;
98 return `not running (state file is stale: pid ${String(state.value.pid)} is dead; last origin ${state.value.origin}, ${version})`;
9299 }
93 return `running (pid ${String(state.value.pid)}, ${state.value.origin})`;
100 return `running (pid ${String(state.value.pid)}, ${state.value.origin}, ${version})`;
94101});
95102
96103const pickAgent = (agents: ReadonlyArray<TriageAgent>) =>
......@@ -183,16 +190,12 @@ export const triageCommand = Command.make("triage", {
183190 );
184191 yield* fs.makeDirectory(scratchDir, { recursive: true });
185192
186 const version = packageJson.version;
187193 const contextFilePath = path.join(scratchDir, "context.md");
188194 yield* fs.writeFileString(
189195 contextFilePath,
190196 buildTriageContext({
191197 generatedAt: DateTime.formatIso(now),
192 version,
193 releaseTag: /^[^-+]+-(?:nightly|preview)\./.test(version)
194 ? `v${version} (prerelease build; if this tag does not exist, clone main)`
195 : `v${version}`,
198 cliVersion: packageJson.version,
196199 os: `${yield* HostProcess.Platform} ${yield* HostProcess.Architecture} (${NodeOS.release()})`,
197200 nodeVersion: process.version,
198201 launchedAs: yield* resolveCliCommand("triage"),
apps/server/src/cli/triagePrompt.test.ts+32-3
......@@ -41,8 +41,7 @@ it("launch prompt stays a single argv-safe line naming the prompt file", () => {
4141it("context file carries every path the playbook depends on", () => {
4242 const context = buildTriageContext({
4343 generatedAt: "2026-08-13T00:00:00.000Z",
44 version: "0.0.33",
45 releaseTag: "v0.0.33",
44 cliVersion: "0.0.33",
4645 os: "linux x64 (7.0.0)",
4746 nodeVersion: "v24.0.0",
4847 launchedAs: "npx t3 triage",
......@@ -70,5 +69,35 @@ it("context file carries every path the playbook depends on", () => {
7069 assert.include(context, "/home/u/.t3/userdata/secrets");
7170 assert.include(context, "/home/u/.t3/source");
7271 assert.include(context, "npx t3 triage");
73 assert.include(context, "v0.0.33");
72});
73
74it("context file labels the CLI version as the CLI's, not the user's build", () => {
75 // `npx t3 triage` can resolve a different `t3` than the app the bug
76 // happened on (#13337), so the agent must not read it as the user's version.
77 const context = buildTriageContext({
78 generatedAt: "2026-09-24T00:00:00.000Z",
79 cliVersion: "0.0.42",
80 os: "linux x64 (7.2.0)",
81 nodeVersion: "v26.8.2",
82 launchedAs: "npx t3 triage",
83 server: "running (pid 42, http://127.0.0.1:3773, version 0.0.43-nightly.20260923.2173)",
84 paths: {
85 stateDir: "/home/u/.t3/userdata",
86 dbPath: "/home/u/.t3/userdata/state.sqlite",
87 settingsPath: "/home/u/.t3/userdata/settings.json",
88 logsDir: "/home/u/.t3/userdata/logs",
89 serviceLogPath: "/home/u/.t3/userdata/logs/boot-service.log",
90 desktopBackendLogGlob: "/home/u/.t3/userdata/logs/server-child*.log*",
91 serverTracePath: "/home/u/.t3/userdata/logs/server.trace.ndjson",
92 providerEventLogPath: "/home/u/.t3/userdata/logs/provider/events.log",
93 terminalLogsDir: "/home/u/.t3/userdata/logs/terminals",
94 providerStatusCacheDir: "/home/u/.t3/caches",
95 secretsDir: "/home/u/.t3/userdata/secrets",
96 sourceCacheDir: "/home/u/.t3/source",
97 },
98 });
99 assert.notInclude(context, "Installed version");
100 assert.notInclude(context, "Release tag");
101 assert.include(context, "- Triage CLI version: 0.0.42 (");
102 assert.include(context, "version 0.0.43-nightly.20260923.2173");
74103});
apps/server/src/cli/triagePrompt.ts+27-13
......@@ -18,7 +18,7 @@ crashes, auth failures, broken setups, slow launches, or anything else. Your job
1818find out what went wrong, unblock the user if you can, and turn what you learned into
1919a well written GitHub issue when one is warranted.
2020
21A triage context file with machine facts (version, OS, paths, server liveness) was
21A triage context file with machine facts (versions, OS, paths, server liveness) was
2222provided alongside this playbook. Everything machine-specific lives there, not here.
2323
2424## 1. Ask what went wrong
......@@ -30,9 +30,15 @@ valuable thing you can extract from this conversation.
3030
3131## 2. Read the machine facts
3232
33Read the triage context file before investigating. It tells you the installed
34version, the OS, whether the server process is currently running, and the exact
35paths for state, logs, and the database.
33Read the triage context file before investigating. It tells you the OS, whether
34the server process is currently running, and the exact paths for state, logs, and
35the database.
36
37Its triage CLI version (called "Installed version" in older context files) is only
38the version of the \`t3\` that ran \`t3 triage\`. It is often not the build the bug
39happened on: \`npx t3 triage\` runs whatever \`t3\` npm resolves, while the user may
40run the desktop app, the background service, or a nightly. The server process line
41gives the local server's version when the server recorded one.
3642
3743## 3. Check for a newer playbook
3844
......@@ -42,8 +48,16 @@ instead of this one. The user may be on an old release with an old copy.
4248
4349## 4. Get the source
4450
45Clone the repo at the tag matching the user's installed version, into the source
46cache directory named in the context file, one subdirectory per commit hash:
51Before cloning, ask the user which version, device, and surface the bug happened
52on. The bug may have happened on another machine, a remote server, the mobile app,
53or an older build they have since updated. Check their answer against local facts
54where they apply: the server version in the context file, or \`serverVersion\` from
55\`<origin>/.well-known/t3/environment\` when a local server is running. If neither
56the user nor the machine can tell you the version, say it is unknown. Do not guess.
57
58Clone the repo at the release tag for that version (\`v\` followed by the version,
59such as \`v0.0.42\`), into the source cache directory named in the context file,
60one subdirectory per commit hash:
4761
4862 git clone --depth 1 --filter=blob:none --branch <release-tag> \\
4963 https://github.com/pingdotgg/t3code <source-cache-dir>/<hash>
......@@ -94,8 +108,8 @@ comes from this repo's \`main\` branch.
94108
95109Search existing issues in pingdotgg/t3code (use \`gh\`, or the public GitHub search
96110API if \`gh\` is missing or not logged in). Then check whether the problem is already
97fixed in a release newer than the user's version: compare versions, read release
98notes and recent commits touching the relevant code.
111fixed in a release newer than the version the bug happened on: compare versions,
112read release notes and recent commits touching the relevant code.
99113
100114If the user is behind and the fix likely shipped, say so plainly and give them the
101115exact 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.
117131- Match the structure of the \`via-triage\` issue template
118132 (\`.github/ISSUE_TEMPLATE/via-triage.yml\` in the repo): what happened, diagnosis,
119133 repro steps, environment, evidence, related issues.
134- Under Version, give the version the bug happened on from step 4, or say it is
135 unknown. Name the triage CLI version only as the CLI's.
120136- Label it \`via-triage\`. Use a plain, specific title with no prefix.
121137- Show the user the complete final issue text and get an explicit yes before
122138 posting. Never post without it.
......@@ -153,7 +169,7 @@ export const buildTriageLaunchPrompt = (promptFilePath: string) =>
153169export const buildTriageSeedPrompt = (contextFilePath: string) => `A T3 Code user is \
154170having a problem with their install and started this session with \`t3 triage\`.
155171
156Machine facts (version, OS, paths, server liveness) are in the triage context file:
172Machine facts (versions, OS, paths, server liveness) are in the triage context file:
157173
158174 ${contextFilePath}
159175
......@@ -166,8 +182,7 @@ ${TRIAGE_PLAYBOOK}`;
166182/** Machine facts for one triage run, pre-formatted so the template stays plain. */
167183export interface TriageContextInput {
168184 readonly generatedAt: string;
169 readonly version: string;
170 readonly releaseTag: string;
185 readonly cliVersion: string;
171186 readonly os: string;
172187 readonly nodeVersion: string;
173188 readonly launchedAs: string;
......@@ -193,8 +208,7 @@ export const buildTriageContext = (input: TriageContextInput) => `# T3 Code tria
193208
194209Generated by \`t3 triage\` at ${input.generatedAt}.
195210
196- Installed version: ${input.version}
197- Release tag for this version: ${input.releaseTag}
211- Triage CLI version: ${input.cliVersion} (the \`t3\` that ran this command; the bug may have happened on another build)
198212- OS: ${input.os}
199213- Node: ${input.nodeVersion}
200214- CLI launched as: ${input.launchedAs}
apps/server/src/serverRuntimeState.ts+9
......@@ -5,10 +5,12 @@ import * as Option from "effect/Option";
55import * as Schema from "effect/Schema";
66
77import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite";
8import packageJson from "../package.json" with { type: "json" };
89import type * as ServerConfig from "./config.ts";
910import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";
1011
1112export const PersistedServerRuntimeState = Schema.Struct({
13 // Schema version of this file, not the server build. See `serverVersion`.
1214 version: Schema.Literal(1),
1315 pid: Schema.Int,
1416 ownerId: Schema.optional(Schema.String),
......@@ -25,6 +27,12 @@ export const PersistedServerRuntimeState = Schema.Struct({
2527 * the difference between "restart the service" and "stop your terminal".
2628 */
2729 serviceManaged: Schema.optional(Schema.Boolean),
30 /**
31 * The server build that wrote this record. `t3 triage` reports it because
32 * the CLI that runs triage can be a different build. Missing on records
33 * written by older servers.
34 */
35 serverVersion: Schema.optional(Schema.String),
2836});
2937export type PersistedServerRuntimeState = typeof PersistedServerRuntimeState.Type;
3038
......@@ -68,6 +76,7 @@ export const makePersistedServerRuntimeState = (input: {
6876 ...(input.config.devUrl ? { devUrl: input.config.devUrl.toString() } : {}),
6977 startedAt: DateTime.formatIso(now),
7078 ...(input.serviceManaged ? { serviceManaged: true } : {}),
79 serverVersion: packageJson.version,
7180 }));
7281
7382export const persistServerRuntimeState = (input: {