Skip to content

feat: report "no response" as NA latency, not a zeroed success#67

Merged
guohai merged 1 commit into
mainfrom
feat/na-latency-partial-response
Jul 10, 2026
Merged

feat: report "no response" as NA latency, not a zeroed success#67
guohai merged 1 commit into
mainfrom
feat/na-latency-partial-response

Conversation

@guohai

@guohai guohai commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Context

When the target AI agent doesn't respond (job 25770 triggered this), aeval still exits 0, so the daemon marked the job completed with 0 ms latencies. That's actively wrong: the leaderboard/realtime pages rank and average by latency, so a 0 ms "response" from a dead agent looked like the fastest agent in the fleet and dragged every average toward zero — while the real signal ("answered 0% of prompts") was hidden.

The model

Agent didn't respond (partially or fully) is a VALID result → record it:

Metric No response Why
response/interrupt latency (med/sd/p95) NA (null) can't time a response that never happened; must not count as 0 ms
response rate 0 the measured signal — answered 0% of prompts
interrupt rate / false barge-in NA (null) no opportunities to measure

A genuine aeval failure (non-zero exit) records no result — you didn't measure the product. A "Partial response" amber tag shows whenever responseRate < 1 (job detail header + jobs list).

Changes

  • Schema + migration: 6 latency columns nullable; 0020_eval_result_latency_nullable.sql, registered as version 21.
  • Daemon (vox-agentd.ts): null latency defaults, null-safe parse guards, structure-aware Priority-1 gate (keeps a valid no-response metrics.json instead of falling through to stdout and losing rawData/rates), dropped the partial-results salvage path. Rate math (computePerCaseAndRates) was already correct — unchanged.
  • Complete route: inserts only on success (results && !jobError); passes latencies as ?? null instead of || 0.
  • Metrics: leaderboard + api-v1 leaderboard exclude NA from averages, sort NA last, and guard composite normalization; SQL daily-bucket avg() already ignores NULLs.
  • Jobs list: batched responseRate (getResponseRatesByJobIds, one query) so the list can flag "Partial response".
  • UI: NA rendering on job detail, realtime cards, and leaderboard; "Partial response" tags on job detail + jobs list.

Deploy order (safe by construction)

Server merges/deploys first — migration makes the columns nullable, and old daemons still sending 0 insert fine. Then upgrade the daemon (vox-upgrade.sh) so agents start emitting null.

Verification

  • npm run check — clean.
  • Daemon units — 89/89 pass (mirror updated to the null contract + added a no-response case).
  • New integration tests pass: no-response stores NA latency + rate 0 (job completed); a failed run stores no result (job failed); /api/eval-jobs exposes responseRate.
  • The other failing api tests are pre-existing (workflow /run region validation, clone, built-in protection, version-gating, mergeEvalConfig) — none appear in this diff and they fail identically in isolation.

Generated with SMT smt@agora.io

When the target agent doesn't respond, aeval still exits 0, so the daemon
marked the job completed with 0 ms latencies. That ranked a dead agent as
the fastest in the fleet and dragged every average toward zero.

Now "no response" is a valid, honest result:
- response/interrupt latency -> NULL (NA), never 0 — kept out of averages
  and rankings instead of looking fastest.
- response rate -> 0 — the real "answered 0% of prompts" signal.
- interrupt rate / false barge-in -> NA (no opportunities to measure).
A "Partial response" amber tag shows whenever responseRate < 1 (job detail
header + jobs list). A genuine aeval failure records NO result.

Changes:
- schema + migration 0020: 6 latency columns nullable (registered v21).
- daemon: null latency defaults, null-safe parse guards, structure-aware
  Priority-1 gate (keeps a no-response metrics.json instead of falling
  through to stdout), dropped partial-results salvage.
- complete route: insert only on success; pass latencies as ?? null.
- leaderboard + api-v1 leaderboard: exclude NA from averages, sort NA last,
  guard composite normalization; display NA.
- jobs list: batched responseRate (getResponseRatesByJobIds) for the tag.
- UI: NA rendering on job detail, realtime cards, leaderboard.
- tests: no-response stores NA + rate 0; failure stores no result; list
  exposes responseRate; daemon mirror updated to the null contract.

Deploy order is safe: server migrates first (columns nullable, old daemons
sending 0 still insert), then upgrade the daemon to emit null.

🤖 Built with SMT <smt@agora.build>
@github-actions

Copy link
Copy Markdown

🤖 Claude Code Review

Review complete. Here's my assessment.

Summary

Solid, well-reasoned change. The core idea — storing non-response latency as NULL (NA) rather than 0 so a dead agent isn't ranked fastest — is correct and applied consistently across the daemon (parse/fallback chain now uses == null sentinels), the schema/migration, the aggregation endpoints (null-excluding averages, NA-sinks-to-bottom sort, min/max over non-null only), and the UI (NA rendering, connectNulls gaps, partial-response badges). The migration is backward-compatible (DROP NOT NULL) and correctly registered. The batched getResponseRatesByJobIds avoids an N+1. No security issues.

A few things worth addressing:

Findings

1. Zeroed placeholder result row contradicts the PR's thesis (server/routes.ts:2932-2937) — medium
The store-artifacts endpoint still inserts a result row with responseLatencyMedian: 0, responseLatencySd: 0, ... when a job has no existing result. Now that failed runs never create a result via /complete (the daemon's partial-results path was removed), every failed job that uploads artifacts hits this branch and writes an all-zero-latency row.

  • For failed jobs this is harmless to metrics (tier conditions all require status = "completed"), but it's the one remaining place writing 0 where the PR's whole premise is "never write 0."
  • For a completed job that reaches /complete with results falsy (if (results && !jobError) skips row creation, job still finalized as completed), this placeholder would then be created as a 0 ms row on a completed job — which does enter mainline/community averages, reintroducing exactly the fake-fast-0ms poison this PR removes.

Since the columns are now nullable, set these six latency fields to null here for consistency and to close that latent hole.

2. Leaderboard rewards unreliable-but-fast providers (server/routes.ts composite; routes-api-v1) — low, arguably pre-existing
Excluding no-response runs from latency averages means a group of 9 non-responsive runs + 1 fast run now reports the fast latency (~200 ms) and ranks well, while responseRate is not part of the composite score (weights has no response-rate term) and isn't shown on the aggregated leaderboard. This PR is a strict improvement over the old behavior (0 ms dragged the average to a fake ~20 ms, which was worse), but the composite still can't see reliability. Consider folding responseRate into the score or surfacing it on the leaderboard so a mostly-dead agent can't top the rankings on its rare successes. Not a blocker.

3. Minor: median-from-summary can yield non-null median with null SD/P95
In the fallback chain, when turn_level is absent, median is filled from summary/aggregated sources but SD is not, so a result can show a real MED and NA for SD. This is more honest than the old 0 and averaging tolerates it (SQL/JS avg skip nulls), so no action needed — just noting the intentional asymmetry.

Nothing else stands out — the null-sentinel refactor in the daemon, the sort/normalization null-guards, and the client formatters all look correct.

@github-actions

Copy link
Copy Markdown

🤖 Codex Code Review

Findings

  • [P1] Failed evals can still create zero-latency result rows via artifact upload. server/routes.ts:2928 creates a placeholder eval_results row with all six latencies set to 0 whenever /api/eval-agent/jobs/:jobId/artifacts is called and no result exists. The daemon still queues uploads for failed jobs at vox_eval_agentd/vox-agentd.ts:1911, so an aeval non-zero exit can be marked failed by /complete, then later get a bogus result row from the artifact endpoint. That breaks the PR’s “failed run records no result” contract and makes job detail/API results show a zeroed result. The artifact route should not create metric rows for failed jobs; if a placeholder is needed for artifact metadata, gate it to completed jobs and use nullable latency fields/rates appropriately.

Open Questions

  • None.

Notes

  • I did not find security/auth regressions in the touched paths. The main issue is the remaining artifact-side backdoor for zeroed metric rows.

@guohai guohai merged commit 23efc34 into main Jul 10, 2026
3 checks passed
@guohai guohai deleted the feat/na-latency-partial-response branch July 10, 2026 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant