feat(analytics): add structured run failure evidence (#7534)

* feat(analytics): add structured run failure evidence

* fix(daemon): preserve run failure evidence boundaries

Generated-By: looper 0.12.0 (runner=fixer, agent=codex)

* fix(daemon): correct structured failure attribution

Generated-By: looper 0.12.0 (runner=fixer, agent=codex)

* fix(daemon): preserve specific failure evidence

Generated-By: looper 0.12.0 (runner=fixer, agent=codex)
This commit is contained in:
Ray Xi
2026-08-31 06:20:27 +00:00
committed by GitHub
parent 5a62b0edad
commit 59d7a1d9c7
8 changed files with 586 additions and 32 deletions
+240 -10
View File
@@ -2,8 +2,13 @@ import type {
TrackingRunCancelOrigin,
TrackingRunFailureCategory,
TrackingRunFailureDetail,
TrackingRunFailureDomain,
TrackingRunFailureMechanism,
TrackingRunFailureStage,
TrackingRunFailureUserAction,
TrackingRunEvidenceLevel,
TrackingRunAdmissionStatus,
TrackingRunRepairOwner,
TrackingRunTerminalTrigger,
} from '@open-design/contracts/analytics';
import {
@@ -38,6 +43,12 @@ export interface RunFailureClassification {
failure_category: TrackingRunFailureCategory;
failure_detail: TrackingRunFailureDetail;
failure_stage: TrackingRunFailureStage;
failure_mechanism?: TrackingRunFailureMechanism;
failure_domain?: TrackingRunFailureDomain;
evidence_level?: TrackingRunEvidenceLevel;
repair_owner?: TrackingRunRepairOwner;
admission_status?: TrackingRunAdmissionStatus;
classifier_version?: 'run-failure-v2';
retryable: boolean;
user_action: TrackingRunFailureUserAction;
/** Distinguishes an explicit user stop from lifecycle-driven cancellation. */
@@ -245,6 +256,19 @@ function isCliNotInstalledText(text: string): boolean {
.test(text);
}
function isBundledBinaryMissingText(text: string): boolean {
return /\bbundled (?:OpenCode|agent) binary (?:is )?missing\b/i.test(text);
}
function clientEnvironmentFailureDetail(text: string): TrackingRunFailureDetail | null {
if (/\b(Windows Application Control|AppLocker)\b/i.test(text)) return 'host_policy_block';
if (/\b(SQLite|WAL).*(?:I\/O|readonly|locked|corrupt|failed)\b/i.test(text)) return 'local_storage_failure';
if (/\b(certificate|CERT_|self[- ]signed|unable to verify)\b/i.test(text)) return 'certificate_failure';
if (/\b(unsupported proxy protocol|proxy configuration)\b/i.test(text)) return 'proxy_configuration';
if (/\b(ECONNREFUSED|ENETUNREACH|network unreachable|local connection failed)\b/i.test(text)) return 'network_configuration';
return null;
}
function isGitBashMissingText(text: string): boolean {
return /\bClaude Code on Windows requires git-bash\b|\bCLAUDE_CODE_GIT_BASH_PATH\b|\bgit-bash\b/i
.test(text);
@@ -262,6 +286,10 @@ function isAgentProtocolErrorText(text: string): boolean {
/\bfailed to parse request\b/i.test(text);
}
function isAcpFrameTooLargeText(text: string): boolean {
return /\bACP input line exceeds maximum size\b/i.test(text);
}
function isFabricatedRoleMarkerText(text: string): boolean {
return /\bmodel emitted fabricated role marker\b/i.test(text);
}
@@ -679,11 +707,126 @@ function classification(
failure_stage: TrackingRunFailureStage,
retryable: boolean,
user_action: TrackingRunFailureUserAction,
options: {
structuredProviderEvidence?: boolean;
evidenceLevel?: TrackingRunEvidenceLevel;
} = {},
): RunFailureClassification {
const policy = [
'hard_quota',
'model_window_limit',
'membership_concurrency_limit',
'workspace_credits_exhausted',
'amr_insufficient_balance',
'amr_tier_upgrade_required',
].includes(failure_detail) || failure_category === 'entitlement_required';
const localModel = [
'cli_version_incompatible',
'local_model_not_loaded',
].includes(failure_detail);
const clientRequest = [
'attachment_media_type_unsupported',
'tool_schema_invalid',
'prompt_tokenization_failed',
'provider_resource_not_found',
].includes(failure_detail);
const transport = !options.structuredProviderEvidence && [
'stream_disconnected',
'network_error',
].includes(failure_detail);
const provider = (failure_category === 'model_unavailable' && !localModel)
|| (failure_category === 'upstream_unavailable' && !transport && !clientRequest)
|| (failure_category === 'rate_limit' && !policy);
const environment = [
'auth_required', 'stale_profile', 'refresh_token_reused', 'missing_api_key',
'invalid_api_key', 'cli_not_installed', 'git_bash_missing',
'agent_config_invalid', 'cpu_unsupported', 'host_policy_block',
'local_storage_failure', 'certificate_failure', 'proxy_configuration',
'network_configuration',
].includes(failure_detail) || localModel;
const product = [
'agent_protocol_error', 'acp_frame_too_large', 'bundled_binary_missing', 'empty_output', 'fabricated_role_marker',
'permission_request_not_found', 'plugin_artifact_missing',
].includes(failure_detail) || clientRequest || failure_category === 'prompt_too_large';
const failure_mechanism: TrackingRunFailureMechanism = policy
? 'policy_rejection'
: provider
? 'provider_rejection'
: transport
? 'transport_failure'
: failure_detail === 'acp_frame_too_large'
? 'frame_too_large'
: failure_detail === 'agent_protocol_error'
? 'protocol_violation'
: failure_category === 'empty_output'
? 'empty_completion'
: failure_category === 'timeout'
? failure_detail === 'inactivity_timeout'
? 'stream_idle_timeout'
: failure_stage === 'post_tool_resume'
? 'post_tool_resume_timeout'
: 'acp_response_deadline'
: failure_category === 'tool_error'
? 'tool_execution_failure'
: failure_detail === 'interrupted'
? 'unknown'
: failure_category === 'process_exit'
? 'child_exit'
: 'unknown';
const failure_domain: TrackingRunFailureDomain = policy
? 'policy_admission'
: provider
? 'provider_control_plane'
: transport
? 'cross_boundary'
: environment
? 'client_environment'
: product
? 'client_product'
: failure_category === 'timeout' || failure_category === 'process_exit'
? 'cross_boundary'
: 'unknown';
const inferredEvidenceLevel: TrackingRunEvidenceLevel = failure_detail === 'membership_concurrency_limit'
? 'structured_code'
: failure_detail === 'interrupted'
? 'lifecycle_signal'
: transport
? 'legacy_text'
: provider
? options.structuredProviderEvidence ? 'structured_code' : 'legacy_text'
: failure_detail === 'agent_protocol_error' || failure_detail === 'acp_frame_too_large'
? 'protocol_error'
: failure_category === 'timeout'
? 'lifecycle_signal'
: failure_detail === 'bundled_binary_missing' || environment
? 'stderr_fallback'
: ['fatal_rpc_error', 'stream_error', 'exit_nonzero'].includes(failure_detail)
? 'close_reason'
: failure_detail === 'unknown'
? 'unknown'
: 'legacy_text';
const evidence_level = options.evidenceLevel ?? inferredEvidenceLevel;
const repair_owner: TrackingRunRepairOwner = failure_domain === 'policy_admission'
? 'policy_owner'
: failure_domain === 'provider_control_plane'
? 'provider_owner'
: failure_domain === 'client_environment'
? 'client_environment'
: failure_domain === 'client_product'
? 'open_design'
: failure_domain === 'cross_boundary'
? 'shared_boundary'
: 'unknown';
return {
failure_category,
failure_detail,
failure_stage,
failure_mechanism,
failure_domain,
evidence_level,
repair_owner,
admission_status: policy ? 'rejected_policy' : 'admitted',
classifier_version: 'run-failure-v2',
retryable,
user_action,
};
@@ -699,13 +842,11 @@ function classifyRunFailureBase(
return {
// Preserve the legacy category/detail for dashboard compatibility.
// `cancel_origin` is the authoritative SLO eligibility signal.
...classification(
'user_cancel',
'user_cancelled',
inferFailureStageFromEvents(events, 'first_token_wait'),
false,
'none',
),
failure_category: 'user_cancel',
failure_detail: 'user_cancelled',
failure_stage: inferFailureStageFromEvents(events, 'first_token_wait'),
retryable: false,
user_action: 'none',
cancel_origin: cancelOrigin,
terminal_trigger: cancelOrigin,
};
@@ -723,6 +864,16 @@ function classifyRunFailureBase(
text,
);
if (errorCode === 'DAEMON_RESTARTED') {
return classification(
'process_exit',
'interrupted',
'finalize',
true,
'retry',
);
}
if (
errorCode === 'AMR_INSUFFICIENT_BALANCE' ||
amrFailure?.code === 'AMR_INSUFFICIENT_BALANCE'
@@ -733,6 +884,9 @@ function classifyRunFailureBase(
'session_init',
false,
'recharge',
errorCode === 'AMR_INSUFFICIENT_BALANCE'
? { evidenceLevel: 'structured_code' }
: {},
);
}
@@ -746,6 +900,9 @@ function classifyRunFailureBase(
'session_init',
false,
'upgrade',
errorCode === 'AMR_TIER_UPGRADE_REQUIRED'
? { evidenceLevel: 'structured_code' }
: {},
);
}
@@ -761,6 +918,13 @@ function classifyRunFailureBase(
'session_init',
false,
'login',
[
'AMR_AUTH_REQUIRED',
'AGENT_AUTH_REQUIRED',
'UNAUTHORIZED',
].includes(errorCode ?? '')
? { evidenceLevel: 'structured_code' }
: {},
);
}
@@ -772,6 +936,9 @@ function classifyRunFailureBase(
'prompt_send',
false,
'reduce_context',
errorCode === 'AGENT_PROMPT_TOO_LARGE'
? { evidenceLevel: 'structured_code' }
: {},
);
}
@@ -788,6 +955,7 @@ function classifyRunFailureBase(
: 'model_select',
false,
'switch_model',
{ structuredProviderEvidence: errorCode === 'AMR_MODEL_UNAVAILABLE' },
);
}
@@ -828,6 +996,33 @@ function classifyRunFailureBase(
);
}
if (isBundledBinaryMissingText(text)) {
return classification(
'process_exit',
'bundled_binary_missing',
'spawn',
false,
'none',
);
}
const serviceFailure = classifyAgentServiceFailure(text);
const environmentDetail = clientEnvironmentFailureDetail(text);
const hasStructuredServiceCode = [
'RATE_LIMITED',
'UPSTREAM_UNAVAILABLE',
'AGENT_CONNECTION_DROPPED',
].includes(errorCode ?? '');
if (environmentDetail && !hasStructuredServiceCode) {
return classification(
'process_exit',
environmentDetail,
'spawn',
false,
'none',
);
}
if (isCliNotInstalledText(text)) {
return classification(
'process_exit',
@@ -868,6 +1063,16 @@ function classifyRunFailureBase(
);
}
if (isAcpFrameTooLargeText(text)) {
return classification(
'process_exit',
'acp_frame_too_large',
inferFailureStageFromEvents(events, 'child_close'),
false,
'none',
);
}
// A protocol failure from AFTER the handshake: a session existed, so the run
// may simply have hit a bad moment and the old transient treatment stands.
// Handshake-numbered frames (ids 1 and 2) are deliberately NOT claimed here
@@ -886,7 +1091,6 @@ function classifyRunFailureBase(
);
}
const serviceFailure = classifyAgentServiceFailure(text);
if (serviceFailure === 'AGENT_AUTH_REQUIRED' || isAuthDetailText(text)) {
return classification(
'auth',
@@ -938,6 +1142,7 @@ function classifyRunFailureBase(
'session_init',
retryable,
retryable ? 'retry' : workspaceCredits ? 'recharge' : 'none',
{ structuredProviderEvidence: errorCode === 'RATE_LIMITED' },
);
}
@@ -948,6 +1153,9 @@ function classifyRunFailureBase(
isUpstreamDetailText(text) ||
byokOpenCodeProviderNotFound
) {
const structuredProviderEvidence =
errorCode === 'UPSTREAM_UNAVAILABLE' ||
errorCode === 'AGENT_CONNECTION_DROPPED';
const upstreamClientError =
byokOpenCodeProviderNotFound || isUpstreamClientErrorText(text);
// A provider/SDK 4xx or request-shape rejection will deterministically fail
@@ -960,6 +1168,7 @@ function classifyRunFailureBase(
inferFailureStageFromEvents(events, 'first_token_wait'),
retryable,
retryable ? 'retry' : 'none',
{ structuredProviderEvidence },
);
}
@@ -1205,9 +1414,30 @@ export function classifyRunFailure(
input: RunFailureClassificationInput,
): RunFailureClassification | undefined {
const failure = classifyRunFailureBase(input);
if (!failure || !input.terminalTrigger) return failure;
if (!failure) return failure;
const terminalTrigger = input.terminalTrigger ?? failure.terminal_trigger;
const failureText = collectFailureText({
...input,
events: terminalAttemptEvents(input.events),
});
const failureMechanism = failure.failure_category === 'timeout'
? /(?:readiness|ready) deadline[^\n]*(?:timed out|timeout|expired|failed)|(?:readiness failed|failed to become ready|did not become ready|never became ready)/i.test(failureText)
? 'startup_readiness_timeout'
: terminalTrigger === 'first_output_deadline'
? 'first_output_deadline'
: terminalTrigger === 'acp_stage_timeout'
? failure.failure_stage === 'post_tool_resume'
? 'post_tool_resume_timeout'
: failure.failure_stage === 'tool_execution' || failure.failure_stage === 'tool_outstanding'
? 'tool_execution_failure'
: 'acp_response_deadline'
: terminalTrigger === 'inactivity_watchdog'
? 'stream_idle_timeout'
: failure.failure_mechanism
: failure.failure_mechanism;
return {
...failure,
terminal_trigger: input.terminalTrigger,
...(failureMechanism ? { failure_mechanism: failureMechanism } : {}),
...(terminalTrigger ? { terminal_trigger: terminalTrigger } : {}),
};
}
@@ -32,6 +32,18 @@ import {
const TERMINAL_STATUSES = new Set(['succeeded', 'failed', 'canceled']);
const RECONCILED_STATUS_MESSAGE = 'Run terminal state reconciled after daemon restart.';
const STRONGER_TERMINAL_INTEGRITY = new Set([
'late',
'overwritten',
'permanently_missing',
'post_terminal_activity',
]);
function reconciledTerminalIntegrity(value: unknown): string {
return typeof value === 'string' && STRONGER_TERMINAL_INTEGRITY.has(value)
? value
: 'reconciled';
}
interface AnalyticsRecovery {
context: Record<string, unknown>;
@@ -408,24 +420,15 @@ export async function reconcileDurableRunTerminals(
: deriveRunErrorCode(state)
: undefined;
const failure = failed
? recoveryReason === 'daemon_restart'
? {
failure_category: 'process_exit' as const,
failure_detail: 'interrupted' as const,
failure_stage: 'finalize' as const,
retryable: true,
user_action: 'retry' as const,
terminal_trigger: 'daemon_restart' as const,
}
: classifyRunFailure({
result: runResult,
status: state,
...(errorCode ? { errorCode } : {}),
agentId: state.agentId,
cancelOrigin: state.cancelOrigin ?? null,
terminalTrigger: state.terminalTrigger ?? null,
events,
})
? classifyRunFailure({
result: runResult,
status: state,
...(errorCode ? { errorCode } : {}),
agentId: state.agentId,
cancelOrigin: state.cancelOrigin ?? null,
terminalTrigger: state.terminalTrigger ?? null,
events,
})
: undefined;
const properties: Record<string, unknown> = {
...state.analyticsRecovery.properties,
@@ -438,6 +441,9 @@ export async function reconcileDurableRunTerminals(
total_duration_ms: Math.max(0, state.updatedAt - state.createdAt),
langfuse_trace_id: state.id,
terminal_reconciled: true,
terminal_integrity: reconciledTerminalIntegrity(
state.analyticsRecovery.properties.terminal_integrity,
),
terminal_recovery_reason: recoveryReason,
...(errorCode ? { error_code: errorCode } : {}),
...(failure ?? {}),
@@ -961,6 +961,7 @@ export function createRunAnalyticsLifecycle(
stable_prompt_changed_sections: run.promptCache?.changedSections ?? undefined,
area: isDesignSystemRun ? 'design_system_generation' : 'chat_panel',
result,
terminal_integrity: 'canonical',
...(activationMilestones ? { $set_once: activationMilestones } : {}),
model_id: finishedModelId,
artifact_count: artifactCount,
@@ -36,7 +36,11 @@ vi.mock('../src/runtimes/auth.js', () => ({
if (value.includes('http 429') || value.includes('too many requests') || value.includes('session limit')) {
return 'RATE_LIMITED' as const;
}
if (value.includes('503 upstream unavailable') || value.includes('upstream unavailable')) {
if (
value.includes('503 upstream unavailable') ||
value.includes('upstream unavailable') ||
value.includes('503 service unavailable')
) {
return 'UPSTREAM_UNAVAILABLE' as const;
}
return null;
@@ -224,6 +228,7 @@ describe('classifyRunFailure', () => {
failure_category: 'auth',
failure_detail: 'auth_required',
failure_stage: 'session_init',
evidence_level: 'structured_code',
retryable: false,
user_action: 'login',
});
@@ -551,6 +556,14 @@ describe('classifyRunFailure', () => {
});
});
it('records a direct AMR insufficient-balance code as structured evidence', () => {
expect(classify('AMR_INSUFFICIENT_BALANCE')).toMatchObject({
failure_category: 'insufficient_balance',
failure_detail: 'amr_insufficient_balance',
evidence_level: 'structured_code',
});
});
it('maps prompt-size failures to reduce-context guidance', () => {
expect(classify('AGENT_PROMPT_TOO_LARGE', 'context window exceeded')).toMatchObject({
failure_category: 'prompt_too_large',
@@ -561,6 +574,14 @@ describe('classifyRunFailure', () => {
});
});
it('records a direct prompt-too-large code as structured evidence', () => {
expect(classify('AGENT_PROMPT_TOO_LARGE')).toMatchObject({
failure_category: 'prompt_too_large',
failure_detail: 'prompt_too_large',
evidence_level: 'structured_code',
});
});
it('maps empty output to an explicit retryable category', () => {
expect(
classify(
@@ -619,10 +640,36 @@ describe('classifyRunFailure', () => {
).toMatchObject({
failure_category: 'timeout',
failure_detail: 'inactivity_timeout',
failure_mechanism: 'first_output_deadline',
failure_domain: 'cross_boundary',
terminal_trigger: 'first_output_deadline',
});
});
it('does not treat a positive readiness signal as a readiness timeout', () => {
const timeoutMessage = 'Agent stalled after the runtime became ready without emitting any new output.';
expect(
classifyRunFailure({
result: 'failed',
status: {
status: 'failed',
error: timeoutMessage,
signal: 'SIGTERM',
exitCode: null,
errorCode: 'AGENT_SIGNAL_SIGTERM',
},
errorCode: 'AGENT_SIGNAL_SIGTERM',
terminalTrigger: 'inactivity_watchdog',
events: [errorEvent('AGENT_SIGNAL_SIGTERM', timeoutMessage, true)],
}),
).toMatchObject({
failure_category: 'timeout',
failure_mechanism: 'stream_idle_timeout',
terminal_trigger: 'inactivity_watchdog',
});
});
it('keeps an explicit watchdog trigger when a provider error supplies the failure bucket', () => {
expect(
classifyRunFailure({
@@ -644,6 +691,40 @@ describe('classifyRunFailure', () => {
});
});
it('keeps explicit service codes ahead of client-environment text heuristics', () => {
expect(
classify('UPSTREAM_UNAVAILABLE', 'ECONNREFUSED provider endpoint'),
).toMatchObject({
failure_category: 'upstream_unavailable',
failure_mechanism: 'provider_rejection',
failure_domain: 'provider_control_plane',
evidence_level: 'structured_code',
repair_owner: 'provider_owner',
});
expect(
classify('RATE_LIMITED', 'Request blocked by account policy'),
).toMatchObject({
failure_category: 'rate_limit',
failure_mechanism: 'provider_rejection',
failure_domain: 'provider_control_plane',
evidence_level: 'structured_code',
repair_owner: 'provider_owner',
});
});
it('keeps text-only provider failures at legacy-text evidence', () => {
expect(
classify('AGENT_EXECUTION_FAILED', 'HTTP 503 service unavailable'),
).toMatchObject({
failure_category: 'upstream_unavailable',
failure_mechanism: 'provider_rejection',
failure_domain: 'provider_control_plane',
evidence_level: 'legacy_text',
repair_owner: 'provider_owner',
});
});
it('classifies only the terminal attempt after an automatic retry', () => {
const timeoutMessage = 'Agent stalled without emitting any new output for 120s.';
@@ -661,6 +742,7 @@ describe('classifyRunFailure', () => {
agentId: 'claude',
events: [
{ event: 'start', data: { attempt: 1 } },
errorEvent('TIMEOUT', 'Runtime readiness deadline timed out.', true),
{ event: 'agent', data: { type: 'text_delta', delta: 'Working.' } },
{ event: 'agent', data: { type: 'tool_use', id: 'tool-1', name: 'Read' } },
errorEvent('UPSTREAM_UNAVAILABLE', '503 upstream unavailable', true),
@@ -673,6 +755,7 @@ describe('classifyRunFailure', () => {
failure_category: 'timeout',
failure_detail: 'inactivity_timeout',
failure_stage: 'first_token_wait',
failure_mechanism: 'stream_idle_timeout',
retryable: true,
user_action: 'retry',
});
@@ -1588,11 +1671,28 @@ describe('execution_failed close-reason refinement', () => {
failure_category: 'rate_limit',
failure_detail: 'membership_concurrency_limit',
failure_stage: 'session_init',
failure_mechanism: 'policy_rejection',
failure_domain: 'policy_admission',
evidence_level: 'structured_code',
repair_owner: 'policy_owner',
admission_status: 'rejected_policy',
classifier_version: 'run-failure-v2',
retryable: false,
user_action: 'none',
});
});
it('does not exclude an ordinary provider 429 as a pre-run policy rejection', () => {
expect(classifyForAgent('amr', 'RATE_LIMITED', 'HTTP 429: too many requests')).toMatchObject({
failure_category: 'rate_limit',
failure_detail: 'rate_limit_429',
failure_domain: 'provider_control_plane',
failure_mechanism: 'provider_rejection',
admission_status: 'admitted',
repair_owner: 'provider_owner',
});
});
it('keeps a non-AMR membership concurrency envelope retryable', () => {
const message =
'[code=tier_limit_exceeded] membership concurrency limit exceeded: 3/2 resets 2026-08-25T10:42:00Z';
@@ -1609,6 +1709,121 @@ describe('execution_failed close-reason refinement', () => {
});
});
it('classifies an oversized ACP input frame as a local product protocol failure', () => {
const message = 'ACP input line exceeds maximum size (1048576 bytes)';
expect(
classifyForAgent('amr', 'AGENT_EXECUTION_FAILED', message, [
errorEvent('AGENT_EXECUTION_FAILED', message, false),
runtimeCloseEvent('fatal_rpc_error'),
]),
).toMatchObject({
failure_category: 'process_exit',
failure_detail: 'acp_frame_too_large',
failure_mechanism: 'frame_too_large',
failure_domain: 'client_product',
evidence_level: 'protocol_error',
repair_owner: 'open_design',
admission_status: 'admitted',
classifier_version: 'run-failure-v2',
});
});
it('keeps a wrapped oversized ACP input frame non-retryable without a retry hint', () => {
const message =
'json-rpc id 4: failed to parse request: ACP input line exceeds maximum size (1048576 bytes)';
expect(classifyForAgent('amr', 'AGENT_EXECUTION_FAILED', message, [])).toMatchObject({
failure_category: 'process_exit',
failure_detail: 'acp_frame_too_large',
failure_mechanism: 'frame_too_large',
evidence_level: 'protocol_error',
retryable: false,
user_action: 'none',
});
});
it('classifies a missing bundled OpenCode binary as a local product packaging failure', () => {
const message = 'bundled OpenCode binary is missing';
expect(classifyForAgent('amr', 'AGENT_EXECUTION_FAILED', message)).toMatchObject({
failure_category: 'process_exit',
failure_detail: 'bundled_binary_missing',
failure_mechanism: 'child_exit',
failure_domain: 'client_product',
evidence_level: 'stderr_fallback',
repair_owner: 'open_design',
});
});
it('keeps host policy blocks separate from client product failures', () => {
const message = 'OpenCode launch was blocked by Windows Application Control policy';
expect(classifyForAgent('amr', 'AGENT_EXECUTION_FAILED', message)).toMatchObject({
failure_detail: 'host_policy_block',
failure_domain: 'client_environment',
failure_mechanism: 'child_exit',
evidence_level: 'stderr_fallback',
repair_owner: 'client_environment',
});
});
it('does not treat a text-only account policy rejection as a host policy block', () => {
expect(classify('AGENT_EXECUTION_FAILED', 'Request blocked by account policy')).toMatchObject({
failure_category: 'process_exit',
failure_detail: 'execution_failed',
failure_domain: 'cross_boundary',
repair_owner: 'shared_boundary',
});
});
it('keeps CLI version incompatibility client-owned', () => {
expect(classify(null, "error: unknown option '--trust'")).toMatchObject({
failure_category: 'model_unavailable',
failure_detail: 'cli_version_incompatible',
failure_mechanism: 'unknown',
failure_domain: 'client_environment',
evidence_level: 'stderr_fallback',
repair_owner: 'client_environment',
});
});
it('keeps an unloaded local model client-owned', () => {
expect(
classify(
'AGENT_EXECUTION_FAILED',
"No models loaded. Please use the 'lms load' command.",
),
).toMatchObject({
failure_category: 'model_unavailable',
failure_detail: 'local_model_not_loaded',
failure_mechanism: 'unknown',
failure_domain: 'client_environment',
evidence_level: 'stderr_fallback',
repair_owner: 'client_environment',
});
});
it('keeps text-only network errors at the shared transport boundary', () => {
expect(
classify('AGENT_EXECUTION_FAILED', 'Transport error: network error'),
).toMatchObject({
failure_category: 'upstream_unavailable',
failure_detail: 'network_error',
failure_mechanism: 'transport_failure',
failure_domain: 'cross_boundary',
evidence_level: 'legacy_text',
repair_owner: 'shared_boundary',
});
});
it('keeps a structured upstream code provider-owned', () => {
expect(classify('AGENT_CONNECTION_DROPPED')).toMatchObject({
failure_category: 'upstream_unavailable',
failure_detail: 'network_error',
failure_mechanism: 'provider_rejection',
failure_domain: 'provider_control_plane',
evidence_level: 'structured_code',
repair_owner: 'provider_owner',
});
});
it('honors an explicit non-retryable hint on fatal close reasons', () => {
const result = classify('AGENT_EXECUTION_FAILED', '', [
errorEvent('AGENT_EXECUTION_FAILED', '', false),
@@ -1669,6 +1884,7 @@ describe('classifyRunFailure — AMR/vela reclassification out of execution_fail
failure_category: 'entitlement_required',
failure_detail: 'amr_tier_upgrade_required',
failure_stage: 'session_init',
evidence_level: 'structured_code',
retryable: false,
user_action: 'upgrade',
});
@@ -2279,6 +2495,10 @@ describe('classifyRunFailure — sampled 0.15.1 provider request failures', () =
failure_category: 'upstream_unavailable',
failure_detail: 'attachment_media_type_unsupported',
failure_stage: 'prompt_send',
failure_mechanism: 'unknown',
failure_domain: 'client_product',
evidence_level: 'legacy_text',
repair_owner: 'open_design',
retryable: false,
user_action: 'none',
},
@@ -2292,6 +2512,10 @@ describe('classifyRunFailure — sampled 0.15.1 provider request failures', () =
failure_category: 'upstream_unavailable',
failure_detail: 'tool_schema_invalid',
failure_stage: 'prompt_send',
failure_mechanism: 'unknown',
failure_domain: 'client_product',
evidence_level: 'legacy_text',
repair_owner: 'open_design',
retryable: false,
user_action: 'none',
},
@@ -2305,6 +2529,10 @@ describe('classifyRunFailure — sampled 0.15.1 provider request failures', () =
failure_category: 'upstream_unavailable',
failure_detail: 'prompt_tokenization_failed',
failure_stage: 'prompt_send',
failure_mechanism: 'unknown',
failure_domain: 'client_product',
evidence_level: 'legacy_text',
repair_owner: 'open_design',
retryable: false,
user_action: 'none',
},
@@ -2344,6 +2572,10 @@ describe('classifyRunFailure — sampled 0.15.1 provider request failures', () =
failure_category: 'upstream_unavailable',
failure_detail: 'provider_resource_not_found',
failure_stage: 'prompt_send',
failure_mechanism: 'unknown',
failure_domain: 'client_product',
evidence_level: 'legacy_text',
repair_owner: 'open_design',
retryable: false,
user_action: 'none',
},
@@ -2357,6 +2589,10 @@ describe('classifyRunFailure — sampled 0.15.1 provider request failures', () =
failure_category: 'upstream_unavailable',
failure_detail: 'stream_disconnected',
failure_stage: 'first_token_wait',
failure_mechanism: 'transport_failure',
failure_domain: 'cross_boundary',
evidence_level: 'legacy_text',
repair_owner: 'shared_boundary',
retryable: true,
user_action: 'retry',
},
@@ -104,10 +104,17 @@ describe('durable run terminal reconciliation', () => {
failure_category: 'process_exit',
failure_detail: 'interrupted',
failure_stage: 'finalize',
failure_mechanism: 'unknown',
failure_domain: 'cross_boundary',
evidence_level: 'lifecycle_signal',
repair_owner: 'shared_boundary',
admission_status: 'admitted',
classifier_version: 'run-failure-v2',
retryable: true,
user_action: 'retry',
terminal_trigger: 'daemon_restart',
terminal_reconciled: true,
terminal_integrity: 'reconciled',
terminal_recovery_reason: 'daemon_restart',
}),
}));
@@ -185,6 +192,7 @@ describe('durable run terminal reconciliation', () => {
project_id: 'p1',
conversation_id: 'c1',
run_id: runId,
terminal_integrity: 'overwritten',
},
insertId: 'run-created-analytics-incomplete',
},
@@ -228,6 +236,7 @@ describe('durable run terminal reconciliation', () => {
retryable: false,
user_action: 'login',
terminal_reconciled: true,
terminal_integrity: 'overwritten',
terminal_recovery_reason: 'analytics_incomplete',
}),
}));
@@ -15,7 +15,7 @@ import type { ArtifactOriginEntrySurface, ArtifactOriginStatus } from '../../api
import type { AgentDiagnosticReason, AgentDiagnosticSeverity } from '../../api/registry.js';
import type { TrackingDesignSystemEditSurface, TrackingDesignSystemKind, TrackingDesignSystemLengthBucket, TrackingDesignSystemOrigin, TrackingDesignSystemRunEntryFrom } from './design-systems.js';
import type { TrackingSettingsPage } from './event-names.js';
import type { TrackingAmrOpenCodeErrorPhase, TrackingAmrOpenCodeLastEventType, TrackingAmrOpenCodeLastToolKind, TrackingAmrOpenCodeLastToolStatus, TrackingArtifactKind, TrackingArtifactWriteSource, TrackingArtifactWriteStatus, TrackingByokPreflightBlockReason, TrackingByokProviderId, TrackingCliProviderId, TrackingDesignSystemSource, TrackingExecutionMode, TrackingExportFormat, TrackingExportResult, TrackingFeedbackAction, TrackingFeedbackProviderId, TrackingFeedbackRating, TrackingFeedbackRatingWithNone, TrackingFeedbackReasonCode, TrackingFidelity, TrackingFileSizeBucket, TrackingFileType, TrackingFirstModelEventType, TrackingHarness, TrackingLabsItemId, TrackingLabsOptOutReason, TrackingLabsSystemReason, TrackingLabsToggleSource, TrackingLangfuseDeliveryStatus, TrackingLangfuseDropReason, TrackingLangfuseReportResult, TrackingLangfuseReportSkipReason, TrackingProjectKind, TrackingProjectSource, TrackingPublishErrorCode, TrackingResult, TrackingRunCancelOrigin, TrackingRunCloseReason, TrackingRunDiagnosticSource, TrackingRunFailureCategory, TrackingRunFailureDetail, TrackingRunFailureStage, TrackingRunFailureUserAction, TrackingRunLifecyclePhase, TrackingRunPhaseTimingStatus, TrackingRunResult, TrackingRunRetryFinalResult, TrackingRunRetryStrategy, TrackingRunRetrySuppressedReason, TrackingRunTerminalTrigger, TrackingStderrLineCountBucket, TrackingTestResult, TrackingTokenCountSource } from './shared-enums.js';
import type { TrackingAmrOpenCodeErrorPhase, TrackingAmrOpenCodeLastEventType, TrackingAmrOpenCodeLastToolKind, TrackingAmrOpenCodeLastToolStatus, TrackingArtifactKind, TrackingArtifactWriteSource, TrackingArtifactWriteStatus, TrackingByokPreflightBlockReason, TrackingByokProviderId, TrackingCliProviderId, TrackingDesignSystemSource, TrackingExecutionMode, TrackingExportFormat, TrackingExportResult, TrackingFeedbackAction, TrackingFeedbackProviderId, TrackingFeedbackRating, TrackingFeedbackRatingWithNone, TrackingFeedbackReasonCode, TrackingFidelity, TrackingFileSizeBucket, TrackingFileType, TrackingFirstModelEventType, TrackingHarness, TrackingLabsItemId, TrackingLabsOptOutReason, TrackingLabsSystemReason, TrackingLabsToggleSource, TrackingLangfuseDeliveryStatus, TrackingLangfuseDropReason, TrackingLangfuseReportResult, TrackingLangfuseReportSkipReason, TrackingProjectKind, TrackingProjectSource, TrackingPublishErrorCode, TrackingResult, TrackingRunAdmissionStatus, TrackingRunCancelOrigin, TrackingRunCloseReason, TrackingRunDiagnosticSource, TrackingRunEvidenceLevel, TrackingRunFailureCategory, TrackingRunFailureDetail, TrackingRunFailureDomain, TrackingRunFailureMechanism, TrackingRunFailureStage, TrackingRunFailureUserAction, TrackingRunLifecyclePhase, TrackingRunPhaseTimingStatus, TrackingRunRepairOwner, TrackingRunResult, TrackingRunRetryFinalResult, TrackingRunRetryStrategy, TrackingRunRetrySuppressedReason, TrackingRunTerminalIntegrity, TrackingRunTerminalTrigger, TrackingStderrLineCountBucket, TrackingTestResult, TrackingTokenCountSource } from './shared-enums.js';
import type { ConversationForkAnalyticsContext, TrackingFileVersionSource, TrackingPluginImportSource, TrackingSessionMode, TrackingSettingsArea } from './ui-click.js';
// ---- Result events -------------------------------------------------------
@@ -480,6 +480,14 @@ export interface RunFinishedProps extends Omit<RunCreatedProps, 'area'> {
/** v4 name; failure_detail remains during the compatibility window. */
failure_reason?: TrackingRunFailureDetail;
failure_stage?: TrackingRunFailureStage;
/** Versioned causal classification added alongside the legacy category/detail. */
failure_mechanism?: TrackingRunFailureMechanism;
failure_domain?: TrackingRunFailureDomain;
evidence_level?: TrackingRunEvidenceLevel;
repair_owner?: TrackingRunRepairOwner;
admission_status?: TrackingRunAdmissionStatus;
terminal_integrity?: TrackingRunTerminalIntegrity;
classifier_version?: 'run-failure-v2';
retryable?: boolean;
/** v4 name; retryable remains during the compatibility window. */
is_automatic_retry_eligible?: boolean;
@@ -325,6 +325,12 @@ export type TrackingRunFailureDetail =
| 'tool_error'
| 'plugin_artifact_missing'
| 'cli_not_installed'
| 'bundled_binary_missing'
| 'host_policy_block'
| 'local_storage_failure'
| 'certificate_failure'
| 'proxy_configuration'
| 'network_configuration'
| 'git_bash_missing'
| 'agent_config_invalid'
| 'spawn_failed'
@@ -333,6 +339,7 @@ export type TrackingRunFailureDetail =
| 'spawn_eperm'
| 'stdin_write_eof'
| 'agent_protocol_error'
| 'acp_frame_too_large'
| 'session_resume_expired'
| 'fabricated_role_marker'
| 'permission_request_not_found'
@@ -362,6 +369,55 @@ export type TrackingRunFailureStage =
| 'artifact_write'
| 'child_close'
| 'finalize';
export type TrackingRunFailureMechanism =
| 'policy_rejection'
| 'provider_rejection'
| 'protocol_violation'
| 'frame_too_large'
| 'startup_readiness_timeout'
| 'first_output_deadline'
| 'acp_response_deadline'
| 'post_tool_resume_timeout'
| 'tool_execution_failure'
| 'child_exit'
| 'stream_idle_timeout'
| 'empty_completion'
| 'transport_failure'
| 'unknown';
export type TrackingRunFailureDomain =
| 'client_product'
| 'client_environment'
| 'provider_control_plane'
| 'policy_admission'
| 'cross_boundary'
| 'unknown';
export type TrackingRunEvidenceLevel =
| 'structured_error'
| 'structured_code'
| 'protocol_error'
| 'lifecycle_signal'
| 'stderr_fallback'
| 'close_reason'
| 'legacy_text'
| 'unknown';
export type TrackingRunRepairOwner =
| 'open_design'
| 'client_environment'
| 'provider_owner'
| 'policy_owner'
| 'shared_boundary'
| 'unknown';
export type TrackingRunAdmissionStatus =
| 'admitted'
| 'rejected_policy'
| 'unknown';
export type TrackingRunTerminalIntegrity =
| 'canonical'
| 'late'
| 'reconciled'
| 'overwritten'
| 'permanently_missing'
| 'post_terminal_activity';
export type TrackingRunLifecyclePhase =
| 'queued'
| 'prompt_build'
@@ -604,4 +660,3 @@ export type TrackingLabsOptOutReason =
| 'not_what_i_wanted'
| 'other'
| 'skipped';
@@ -107,6 +107,13 @@ describe('analytics run_finished contract', () => {
error_code: 'RATE_LIMITED',
failure_category: 'rate_limit',
failure_stage: 'session_init',
failure_mechanism: 'policy_rejection',
failure_domain: 'policy_admission',
evidence_level: 'structured_code',
repair_owner: 'policy_owner',
admission_status: 'rejected_policy',
terminal_integrity: 'reconciled',
classifier_version: 'run-failure-v2',
retryable: true,
user_action: 'retry',
terminal_reconciled: true,
@@ -241,7 +248,9 @@ describe('analytics run_finished contract', () => {
expect(payload.props.failure_category).toBe('rate_limit');
expect(payload.props.conversation_turn_index).toBe(2);
expect(payload.props.failure_stage).toBe('session_init');
expect(payload.props.failure_stage).toBe('session_init');
expect(payload.props.failure_domain).toBe('policy_admission');
expect(payload.props.classifier_version).toBe('run-failure-v2');
expect(payload.props.terminal_reconciled).toBe(true);
expect(payload.props.terminal_recovery_reason).toBe('daemon_restart');
expect(payload.props.user_action).toBe('retry');