Skip to content

Commit ffceb61

Browse files
t3dotggclaude
andauthored
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 <[email protected]>
1 parent 0ae1091 commit ffceb61

7 files changed

Lines changed: 168 additions & 35 deletions

File tree

‎.github/ISSUE_TEMPLATE/via-triage.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ crashes, auth failures, broken setups, slow launches, or anything else. Your job
66
find out what went wrong, unblock the user if you can, and turn what you learned into
77
a well written GitHub issue when one is warranted.
88

9-
A triage context file with machine facts (version, OS, paths, server liveness) was
9+
A triage context file with machine facts (versions, OS, paths, server liveness) was
1010
provided 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

21-
Read the triage context file before investigating. It tells you the installed
22-
version, the OS, whether the server process is currently running, and the exact
23-
paths for state, logs, and the database.
21+
Read the triage context file before investigating. It tells you the OS, whether
22+
the server process is currently running, and the exact paths for state, logs, and
23+
the database.
24+
25+
Its triage CLI version (called "Installed version" in older context files) is only
26+
the version of the `t3` that ran `t3 triage`. It is often not the build the bug
27+
happened on: `npx t3 triage` runs whatever `t3` npm resolves, while the user may
28+
run the desktop app, the background service, or a nightly. The server process line
29+
gives 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

33-
Clone the repo at the tag matching the user's installed version, into the source
34-
cache directory named in the context file, one subdirectory per commit hash:
39+
Before cloning, ask the user which version, device, and surface the bug happened
40+
on. The bug may have happened on another machine, a remote server, the mobile app,
41+
or an older build they have since updated. Check their answer against local facts
42+
where 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
44+
the user nor the machine can tell you the version, say it is unknown. Do not guess.
45+
46+
Clone the repo at the release tag for that version (`v` followed by the version,
47+
such as `v0.0.42`), into the source cache directory named in the context file,
48+
one 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

8397
Search existing issues in pingdotgg/t3code (use `gh`, or the public GitHub search
8498
API if `gh` is missing or not logged in). Then check whether the problem is already
85-
fixed in a release newer than the user's version: compare versions, read release
86-
notes and recent commits touching the relevant code.
99+
fixed in a release newer than the version the bug happened on: compare versions,
100+
read release notes and recent commits touching the relevant code.
87101

88102
If the user is behind and the fix likely shipped, say so plainly and give them the
89103
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.
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‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import * as NodeServices from "@effect/platform-node/NodeServices";
2+
import { assert, it } from "@effect/vitest";
3+
import * as Effect from "effect/Effect";
4+
import * as FileSystem from "effect/FileSystem";
5+
import * as Path from "effect/Path";
6+
7+
import packageJson from "../../package.json" with { type: "json" };
8+
import {
9+
makePersistedServerRuntimeState,
10+
persistServerRuntimeState,
11+
} from "../serverRuntimeState.ts";
12+
import { describeServerProcess } from "./triage.ts";
13+
14+
it.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+
36+
it.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‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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`. */
81-
const 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+
*/
84+
export 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

96103
const 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‎

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ it("launch prompt stays a single argv-safe line naming the prompt file", () => {
4141
it("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+
74+
it("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‎

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ crashes, auth failures, broken setups, slow launches, or anything else. Your job
1818
find out what went wrong, unblock the user if you can, and turn what you learned into
1919
a well written GitHub issue when one is warranted.
2020
21-
A triage context file with machine facts (version, OS, paths, server liveness) was
21+
A triage context file with machine facts (versions, OS, paths, server liveness) was
2222
provided 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
33-
Read the triage context file before investigating. It tells you the installed
34-
version, the OS, whether the server process is currently running, and the exact
35-
paths for state, logs, and the database.
33+
Read the triage context file before investigating. It tells you the OS, whether
34+
the server process is currently running, and the exact paths for state, logs, and
35+
the database.
36+
37+
Its triage CLI version (called "Installed version" in older context files) is only
38+
the version of the \`t3\` that ran \`t3 triage\`. It is often not the build the bug
39+
happened on: \`npx t3 triage\` runs whatever \`t3\` npm resolves, while the user may
40+
run the desktop app, the background service, or a nightly. The server process line
41+
gives 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
45-
Clone the repo at the tag matching the user's installed version, into the source
46-
cache directory named in the context file, one subdirectory per commit hash:
51+
Before cloning, ask the user which version, device, and surface the bug happened
52+
on. The bug may have happened on another machine, a remote server, the mobile app,
53+
or an older build they have since updated. Check their answer against local facts
54+
where 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
56+
the user nor the machine can tell you the version, say it is unknown. Do not guess.
57+
58+
Clone the repo at the release tag for that version (\`v\` followed by the version,
59+
such as \`v0.0.42\`), into the source cache directory named in the context file,
60+
one 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
95109
Search existing issues in pingdotgg/t3code (use \`gh\`, or the public GitHub search
96110
API if \`gh\` is missing or not logged in). Then check whether the problem is already
97-
fixed in a release newer than the user's version: compare versions, read release
98-
notes and recent commits touching the relevant code.
111+
fixed in a release newer than the version the bug happened on: compare versions,
112+
read release notes and recent commits touching the relevant code.
99113
100114
If the user is behind and the fix likely shipped, say so plainly and give them the
101115
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.
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) =>
153169
export const buildTriageSeedPrompt = (contextFilePath: string) => `A T3 Code user is \
154170
having a problem with their install and started this session with \`t3 triage\`.
155171
156-
Machine facts (version, OS, paths, server liveness) are in the triage context file:
172+
Machine 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. */
167183
export 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
194209
Generated 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‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import * as Option from "effect/Option";
55
import * as Schema from "effect/Schema";
66

77
import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite";
8+
import packageJson from "../package.json" with { type: "json" };
89
import type * as ServerConfig from "./config.ts";
910
import { formatHostForUrl, isWildcardHost } from "./startupAccess.ts";
1011

1112
export 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
});
2937
export 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

7382
export const persistServerRuntimeState = (input: {

0 commit comments

Comments
 (0)