Skip to content

Commit 2dba828

Browse files
authored
Merge pull request #5919 from nexu-io/backport-5814-to-release/v0.16.0
[backport release/v0.16.0] feat(daemon): record runtime version and retry provenance
2 parents 9b0ef73 + d7eb20c commit 2dba828

9 files changed

Lines changed: 247 additions & 3 deletions

File tree

apps/daemon/src/langfuse-bridge.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,14 @@ import {
5454
collectStdoutTailSummary,
5555
summarizeRunDiagnosticsForAnalytics,
5656
} from './run-diagnostics.js';
57-
import { classifyRunFailure } from './run-failure-classification.js';
57+
import {
58+
classifyRunFailure,
59+
type RunFailureClassification,
60+
} from './run-failure-classification.js';
5861
import { deriveRunErrorCode, runResultFromStatus } from './run-result.js';
5962
import { buildTraceObjectManifests } from './trace-object-manifest.js';
6063
import type { TraceArtifactObjectSource, TraceObjectUploadManifests } from './trace-object-manifest.js';
64+
import { getDetectedRuntimeVersions } from './runtimes/detection.js';
6165

6266
interface DaemonRunRecord {
6367
id: string;
@@ -98,6 +102,10 @@ interface DaemonRunRecord {
98102
promptTelemetry?: PromptStackTelemetry;
99103
projectAttachmentPaths?: string[];
100104
projectMetadata?: Record<string, unknown> | null;
105+
retryAttemptCount?: number;
106+
retryFinalResult?: string;
107+
retrySuppressedReason?: string;
108+
retryOriginalFailure?: RunFailureClassification;
101109
contextBudget?: {
102110
action: string;
103111
source: string;
@@ -1048,6 +1056,7 @@ export async function reportRunCompletedFromDaemon(
10481056
const runtime: RuntimeInfo = {
10491057
...getRuntimeInfo(opts.appVersion ?? null),
10501058
...(run.clientType ? { clientType: run.clientType } : {}),
1059+
...(getDetectedRuntimeVersions(run.agentId) ?? {}),
10511060
};
10521061
const artifacts = summarizeProducedFiles(traceObjectFilesRaw);
10531062
const diagnostics = summarizeRunDiagnosticsForAnalytics({
@@ -1100,6 +1109,16 @@ export async function reportRunCompletedFromDaemon(
11001109
...(stderr ? { stderr } : {}),
11011110
...(stdout ? { stdout } : {}),
11021111
diagnostics,
1112+
retryAttemptCount: run.retryAttemptCount ?? 0,
1113+
...(run.retryFinalResult
1114+
? { retryFinalResult: run.retryFinalResult }
1115+
: {}),
1116+
...(run.retrySuppressedReason
1117+
? { retrySuppressedReason: run.retrySuppressedReason }
1118+
: {}),
1119+
...(run.retryOriginalFailure
1120+
? { retryOriginalFailure: run.retryOriginalFailure }
1121+
: {}),
11031122
...(run.contextBudget ? { contextBudget: run.contextBudget } : {}),
11041123
},
11051124
message: {

apps/daemon/src/langfuse-trace.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ export interface RunSummary {
134134
truncated: boolean;
135135
};
136136
diagnostics?: unknown;
137+
retryAttemptCount?: number;
138+
retryFinalResult?: string;
139+
retrySuppressedReason?: string;
140+
retryOriginalFailure?: RunFailureClassification;
137141
contextBudget?: {
138142
action: string;
139143
source: string;
@@ -293,6 +297,11 @@ export interface RuntimeInfo {
293297
packaged?: boolean;
294298
/** Front-end carrier — `desktop` (Electron), `web` (browser), or unknown. */
295299
clientType?: 'desktop' | 'web' | 'unknown';
300+
/** Exact CLI version observed by the daemon's bounded detection probe. */
301+
agentCliVersion?: string;
302+
/** Optional companion runtime used behind the selected CLI (AMR → OpenCode). */
303+
runtimeCompanionName?: string;
304+
runtimeCompanionVersion?: string;
296305
}
297306

298307
export interface TurnInfo {
@@ -1521,10 +1530,25 @@ export function buildTracePayload(ctx: ReportContext): unknown[] {
15211530
osRelease: ctx.runtime?.osRelease,
15221531
arch: ctx.runtime?.arch,
15231532
clientType: ctx.runtime?.clientType,
1533+
agentCliVersion: ctx.runtime?.agentCliVersion,
1534+
runtimeCompanionName: ctx.runtime?.runtimeCompanionName,
1535+
runtimeCompanionVersion: ctx.runtime?.runtimeCompanionVersion,
1536+
retryAttemptCount: ctx.run.retryAttemptCount,
1537+
retryFinalResult: ctx.run.retryFinalResult,
1538+
retrySuppressedReason: ctx.run.retrySuppressedReason,
1539+
retryOriginalFailureCategory:
1540+
ctx.run.retryOriginalFailure?.failure_category,
1541+
retryOriginalFailureDetail:
1542+
ctx.run.retryOriginalFailure?.failure_detail,
1543+
retryOriginalFailureStage:
1544+
ctx.run.retryOriginalFailure?.failure_stage,
15241545
...promptStackFlatMetadata,
15251546
...promptStackBlameMetadata,
15261547
};
15271548

1549+
const observationVersion =
1550+
ctx.runtime?.agentCliVersion ?? ctx.runtime?.appVersion;
1551+
15281552
// Generation-level model parameters mirror the Langfuse schema so the UI
15291553
// shows them in the dedicated Model Parameters card and filters work.
15301554
const modelParameters: Record<string, unknown> | undefined =
@@ -1554,6 +1578,8 @@ export function buildTracePayload(ctx: ReportContext): unknown[] {
15541578
input: inputText,
15551579
output: outputText,
15561580
metadata: traceMetadata,
1581+
release: ctx.runtime?.appVersion,
1582+
version: observationVersion,
15571583
timestamp: startTimeIso,
15581584
},
15591585
},
@@ -1571,6 +1597,7 @@ export function buildTracePayload(ctx: ReportContext): unknown[] {
15711597
output: outputText,
15721598
level: success ? 'DEFAULT' : 'ERROR',
15731599
statusMessage: ctx.run.error ?? undefined,
1600+
version: observationVersion,
15741601
metadata: {
15751602
status: ctx.run.status,
15761603
messageId: ctx.message.messageId || undefined,
@@ -1606,6 +1633,7 @@ export function buildTracePayload(ctx: ReportContext): unknown[] {
16061633
output: outputText,
16071634
level: success ? 'DEFAULT' : 'ERROR',
16081635
statusMessage: ctx.run.error ?? undefined,
1636+
version: observationVersion,
16091637
usage,
16101638
metadata: {
16111639
durationMs: ctx.eventsSummary.durationMs,
@@ -1636,6 +1664,7 @@ export function buildTracePayload(ctx: ReportContext): unknown[] {
16361664
output: outputText,
16371665
level: 'ERROR',
16381666
statusMessage: ctx.run.error ?? undefined,
1667+
version: observationVersion,
16391668
metadata: {
16401669
durationMs: ctx.eventsSummary.durationMs,
16411670
cost_usd: costBreakdown.cost_usd,

apps/daemon/src/routes/runs.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import {
4141
upsertMessage,
4242
} from '../db.js';
4343
import { readVelaLoginStatus } from '../integrations/vela.js';
44+
import { getDetectedRuntimeVersions } from '../runtimes/detection.js';
4445
import {
4546
deriveLangfuseDeliveryState,
4647
readTelemetrySinkConfig,
@@ -170,6 +171,13 @@ interface ChatRun {
170171
retryAttemptCount?: number;
171172
retryFinalResult?: string;
172173
retrySuppressedReason?: string;
174+
retryOriginalFailure?: {
175+
failure_category?: string;
176+
failure_detail?: string;
177+
failure_stage?: string;
178+
retryable?: boolean;
179+
user_action?: string;
180+
};
173181
contextBudget?: {
174182
action: 'unmeasured' | 'within_budget' | 'blocked' | 'rollover';
175183
source: 'model_metadata' | 'known_model_family' | 'unknown';
@@ -1257,6 +1265,7 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
12571265
const finishedModelId = hasExplicitRequestedModelForAnalytics(reqBody.model)
12581266
? modelIdForTracking(reqBody.model)
12591267
: modelIdForTracking(usageAnalytics.agent_reported_model);
1268+
const runtimeVersions = getDetectedRuntimeVersions(run.agentId);
12601269
for (const [index, retryEvent] of runRetryEventsForAnalytics(run.events).entries()) {
12611270
design.analytics.capture({
12621271
eventName: retryEvent.event,
@@ -1288,6 +1297,33 @@ export function registerRunRoutes(app: Express, ctx: RegisterRunRoutesDeps) {
12881297
asked_user_question: runAskedUserQuestion(run.events),
12891298
retry_attempt_count: run.retryAttemptCount ?? 0,
12901299
retry_final_result: run.retryFinalResult ?? 'not_attempted',
1300+
...(runtimeVersions?.agentCliVersion
1301+
? { agent_cli_version: runtimeVersions.agentCliVersion }
1302+
: {}),
1303+
...(runtimeVersions?.runtimeCompanionName
1304+
? { runtime_companion_name: runtimeVersions.runtimeCompanionName }
1305+
: {}),
1306+
...(runtimeVersions?.runtimeCompanionVersion
1307+
? { runtime_companion_version: runtimeVersions.runtimeCompanionVersion }
1308+
: {}),
1309+
...(run.retryOriginalFailure?.failure_category
1310+
? {
1311+
retry_original_failure_category:
1312+
run.retryOriginalFailure.failure_category,
1313+
}
1314+
: {}),
1315+
...(run.retryOriginalFailure?.failure_detail
1316+
? {
1317+
retry_original_failure_detail:
1318+
run.retryOriginalFailure.failure_detail,
1319+
}
1320+
: {}),
1321+
...(run.retryOriginalFailure?.failure_stage
1322+
? {
1323+
retry_original_failure_stage:
1324+
run.retryOriginalFailure.failure_stage,
1325+
}
1326+
: {}),
12911327
context_budget_action: run.contextBudget?.action ?? 'unmeasured',
12921328
context_budget_source: run.contextBudget?.source ?? 'unknown',
12931329
...(run.contextBudget?.estimatedPromptTokens !== undefined

apps/daemon/src/runtimes/detection.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { spawnEnvForAgent } from './env.js';
1010
import { probeAgentAuthStatus } from './auth.js';
1111
import { agentCapabilities } from './capabilities.js';
1212
import { installMetaForAgent } from './metadata.js';
13+
import { resolveAmrOpenCodeExecutable } from './executables.js';
1314
import { resolveAmrProfile } from '../integrations/vela.js';
1415
import {
1516
buildAuthDiagnostic,
@@ -31,6 +32,25 @@ type FetchedRuntimeModels = {
3132
source: RuntimeModelSource;
3233
};
3334

35+
export interface DetectedRuntimeVersions {
36+
agentCliVersion?: string;
37+
runtimeCompanionName?: string;
38+
runtimeCompanionVersion?: string;
39+
}
40+
41+
// Detection already pays the bounded `--version` probe cost used by Settings.
42+
// Keep the result as daemon-lifetime provenance so run telemetry can name the
43+
// exact executable family without spawning another process on every turn.
44+
const detectedRuntimeVersions = new Map<string, DetectedRuntimeVersions>();
45+
46+
export function getDetectedRuntimeVersions(
47+
agentId: string | null | undefined,
48+
): DetectedRuntimeVersions | null {
49+
if (!agentId) return null;
50+
const remembered = detectedRuntimeVersions.get(agentId);
51+
return remembered ? { ...remembered } : null;
52+
}
53+
3454
function configuredEnvForAgent(
3555
configuredEnvByAgent: Record<string, Record<string, string>>,
3656
agentId: string,
@@ -153,6 +173,24 @@ async function probeVersionAtPath(
153173
}
154174
}
155175

176+
async function probeAmrOpenCodeVersion(
177+
def: RuntimeAgentDef,
178+
env: NodeJS.ProcessEnv,
179+
): Promise<string | null> {
180+
if (def.id !== 'amr') return null;
181+
const companion = resolveAmrOpenCodeExecutable(env);
182+
if (!companion) return null;
183+
try {
184+
const { stdout } = await execAgentFile(companion, ['--version'], {
185+
env,
186+
timeout: def.versionProbeTimeoutMs ?? 3000,
187+
});
188+
return String(stdout).trim().split('\n')[0] || null;
189+
} catch {
190+
return null;
191+
}
192+
}
193+
156194
function unavailableAgent(
157195
def: RuntimeAgentDef,
158196
diagnostics: AgentDiagnostic[] = [],
@@ -200,6 +238,7 @@ async function probe(
200238
def: RuntimeAgentDef,
201239
configuredEnv: Record<string, string> = {},
202240
): Promise<DetectedAgent> {
241+
detectedRuntimeVersions.delete(def.id);
203242
// Detection must probe the exact path the runtime will spawn, not just the
204243
// PATH-visible shim. This is load-bearing for Codex under nvm/fnm/mise:
205244
// the discovered `codex` entry is often a `#!/usr/bin/env node` wrapper
@@ -236,16 +275,29 @@ async function probe(
236275
// so a single agent's detection wall is max(help, models, auth) ≈ 5s rather
237276
// than the sum ≈ 15s. `--help` capabilities are cached on `agentCapabilities`
238277
// for buildArgs to consult.
239-
const [caps, modelResult, auth] = await Promise.all([
278+
const [caps, modelResult, auth, amrOpenCodeVersion] = await Promise.all([
240279
probeCapabilities(def, launch.launchPath, probeEnv),
241280
fetchModels(def, launch.launchPath, probeEnv),
242281
probeAgentAuthStatus(def, launch.launchPath, probeEnv),
282+
probeAmrOpenCodeVersion(def, probeEnv),
243283
]);
244284
const surfacedModelResult = withRememberedAmrModels(def, probeEnv, modelResult);
245285
if (caps) {
246286
agentCapabilities.set(def.id, caps);
247287
}
248288
const authDiagnostic = auth ? buildAuthDiagnostic(def, auth) : null;
289+
const runtimeVersions: DetectedRuntimeVersions = {
290+
...(outcome.version ? { agentCliVersion: outcome.version } : {}),
291+
...(amrOpenCodeVersion
292+
? {
293+
runtimeCompanionName: 'opencode',
294+
runtimeCompanionVersion: amrOpenCodeVersion,
295+
}
296+
: {}),
297+
};
298+
if (Object.keys(runtimeVersions).length > 0) {
299+
detectedRuntimeVersions.set(def.id, runtimeVersions);
300+
}
249301
return {
250302
...stripFns(def),
251303
models: surfacedModelResult.models,

apps/daemon/src/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5493,6 +5493,7 @@ export async function startServer({
54935493
sideEffects,
54945494
});
54955495
if (decision.shouldRetry && !design.runs.isTerminal(run.status)) {
5496+
run.retryOriginalFailure ??= failure ?? undefined;
54965497
if ((run.retryAttemptCount ?? 0) === 0) {
54975498
run.retryOriginFailure = failure ? { ...failure } : null;
54985499
run.retryOriginErrorCode = errorCode ?? null;

apps/daemon/tests/langfuse-trace.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,21 @@ describe('buildTracePayload', () => {
940940
it('mirrors runtime + turn fields into trace metadata for query / export', () => {
941941
const batch = buildTracePayload(
942942
makeCtx({
943+
run: {
944+
runId: 'run-1',
945+
status: 'succeeded',
946+
startedAt: 1_700_000_000_000,
947+
endedAt: 1_700_000_004_500,
948+
retryAttemptCount: 1,
949+
retryFinalResult: 'success',
950+
retryOriginalFailure: {
951+
failure_category: 'upstream_unavailable',
952+
failure_detail: 'stream_disconnected',
953+
failure_stage: 'first_token_wait',
954+
retryable: true,
955+
user_action: 'retry',
956+
},
957+
},
943958
turn: { model: 'claude-sonnet-4-5', skillId: 'landing-page' },
944959
runtime: {
945960
os: 'linux',
@@ -949,10 +964,14 @@ describe('buildTracePayload', () => {
949964
appChannel: 'beta',
950965
packaged: true,
951966
clientType: 'web',
967+
agentCliVersion: 'claude 3.4.5',
968+
runtimeCompanionName: 'opencode',
969+
runtimeCompanionVersion: '1.2.3',
952970
},
953971
}),
954972
);
955-
const m = (batch[0] as any).body.metadata;
973+
const trace = (batch[0] as any).body;
974+
const m = trace.metadata;
956975
expect(m.model).toBe('claude-sonnet-4-5');
957976
expect(m.skillId).toBe('landing-page');
958977
expect(m.os).toBe('linux');
@@ -962,8 +981,20 @@ describe('buildTracePayload', () => {
962981
expect(m.appChannel).toBe('beta');
963982
expect(m.packaged).toBe(true);
964983
expect(m.clientType).toBe('web');
984+
expect(m.agentCliVersion).toBe('claude 3.4.5');
985+
expect(m.runtimeCompanionName).toBe('opencode');
986+
expect(m.runtimeCompanionVersion).toBe('1.2.3');
987+
expect(m.retryAttemptCount).toBe(1);
988+
expect(m.retryFinalResult).toBe('success');
989+
expect(m.retryOriginalFailureCategory).toBe('upstream_unavailable');
990+
expect(m.retryOriginalFailureDetail).toBe('stream_disconnected');
991+
expect(m.retryOriginalFailureStage).toBe('first_token_wait');
965992
expect(m.projectId).toBe('proj-1');
966993
expect(m.agent).toBe('claude');
994+
expect(trace.release).toBe('0.5.0');
995+
expect(trace.version).toBe('claude 3.4.5');
996+
expect(bodyOf(batch, 'span-create', 'agent-run').version).toBe('claude 3.4.5');
997+
expect(bodyOf(batch, 'generation-create', 'llm').version).toBe('claude 3.4.5');
967998
});
968999

9691000
it('mirrors session rollover and compaction evidence into trace metadata', () => {

0 commit comments

Comments
 (0)