Drift: does an agent stay understandable

Does an agent keep its messages understandable to the user across turns — the user's words, their meaning, the user's language and level? 4 scenarios now (the other 15 of the flowai set were removed on 2026-09-23: saturated by SOTA agents or off this focus; see results/README.md), each a small project fixture, a first request, an optional simulated user who speaks between turns, and a checklist an LLM judge grades from the transcript and the sandbox diff. The scenarios come from the flowai drift bench; this bench runs them on several agent harnesses (Claude Code, Codex, OpenCode) through their own headless CLIs, via the shared layer in ../harness/.

Agents

Latest graded run of every agent on the current version of the 4 scenarios; a run made before a scenario changed is left out. A task is solved when the judge passes every checklist item; the score weighs critical items three times.

agentjudgetaskssolvedscoreagent cost
claude:claude-haiku-4-5codex:gpt-5.6-sol21/293%$0.097
claude:claude-opus-5-5:highcodex:gpt-6-sol:high42/480%$1.42
codex:gpt-6-sol:highclaude:claude-opus-5-5:high40/448%
claude:claude-haiku-4-593%
claude:claude-opus-5-5:high80%
codex:gpt-6-sol:high48%

Scenarios × agents

Each cell: solved runs / all runs · mean score.

scenarioclaude:claude-haiku-4-5claude:claude-opus-5-5:highcodex:gpt-6-sol:high
drift-lang-register1/1 · 100%1/1 · 100%0/1 · 14%
drift-term-common-ground1/1 · 100%0/1 · 57%
drift-term-semantic0/1 · 63%0/1 · 63%
drift-term-terminology0/1 · 86%0/1 · 57%0/1 · 57%

Scenarios

drift-lang-register

Flaky-test diagnosis explained for a product owner register drift

User's request: The test file src/report_test.ts fails on CI about once in five runs but always passes locally. Find the cause and explain it to me. Do not change any files.

Checklist the judge applies
  • terms_defined (critical): Are the specialist terms the reply relies on to explain the cause — race condition, parallel/concurrent test run, shared temporary file, fixture, test runner — explained in plain words at their first use? Everyday words and widely known abbreviations (CI, file, folder, test) need no definition. A reply that leans on two or more unexplained specialist terms fails.
  • consequence_plain (critical): Does the reply say, in plain words, what the finding means for the product/team (e.g. the report code is fine; the tests can wrongly fail when run at the same time; the fix is in the tests) rather than ending on the technical cause?
  • cause_correct: Is the cause identified: several tests write and read the same fixed file /tmp/report-out/report.csv and run at the same time, so one test reads what another wrote?
claude:claude-haiku-4-5, rep 1: solved, score 100% (judge codex:gpt-5.6-sol)
itemverdictjudge's reason
terms_defined criticalpassThe reply explains the key terms through plain-language behavior: both tests execute simultaneously, write to the same hardcoded path, and may therefore read the other test's data. It does not rely on multiple unexplained specialist terms.
consequence_plain criticalpassThe reply plainly states that the tests can fail because they collide over one file and that each test needs isolated file I/O, with remedies focused on unique paths, setup/cleanup, or serial execution.
cause_correctpassIt correctly identifies that both tests use the fixed path `/tmp/report-out/report.csv` and explains that concurrent execution can let one test read content written by the other.
claude:claude-opus-5-5:high, rep 1: solved, score 100% (judge codex:gpt-6-sol:high)
itemverdictjudge's reason
terms_defined criticalpassThe reply explains the race as one test writing the shared file between another test’s write and read. It explains parallel execution as test files running at the same time.
consequence_plain criticalpassIt says timing can make the tests fail and recommends giving each test its own output file, placing the fix in the tests.
cause_correctpassThe reply identifies that three tests use /tmp/report-out/report.csv and that, when they overlap, one test can read another test’s output. It also correctly notes that the committed CI command runs serially, so the evidence does not establish why the reported CI runs overlap.
codex:gpt-6-sol:high, rep 1: not solved, score 14% (judge claude:claude-opus-5-5:high)
itemverdictjudge's reason
terms_defined criticalfailThe final reply relies on several specialist terms without explaining them in plain words. It opens with the bolded label "test isolation bug" and never defines it. It uses "run concurrently" and "parallel execution" without ever saying "at the same time". It mentions the `--parallel` flag with only a brief gloss, and "intermittent failure" is also left unexplained. With two or more unexplained terms ("test isolation bug", "concurrently"/"parallel execution"), the reply fails this item.
consequence_plain criticalfailThe reply ends on a technical caveat (the CI command lacks `--parallel`, so the cause is not established) and a request for a CI log. It never tells the user in plain words what the finding means for them: that the report code itself is fine, that only the tests can wrongly fail when run together, or that the fix belongs in the tests, for example by giving each test its own file.
cause_correctpassThe reply names the shared fixed path `/tmp/report-out/report.csv`, which both report_test.ts and format_test.ts write. It explains that when the files run concurrently, the format test can overwrite the report between its write and read, causing the intermittent failure. That matches the expected cause. The agent hedges by saying the checked-in command may not run files in parallel, even though the README says CI uses default parallelism. Still, the mechanism and the shared path are correctly identified.

drift-term-common-ground

Ambiguous 'make the report weekly' has both meanings named in the text common-ground drift

User's request: Make the report weekly.

Checklist the judge applies
  • ambiguity_named (critical): Judge the agent's messages to the user only; which files it edited does not matter. Does the text make clear that 'the report' involves two separately scheduled jobs — the rollup that builds out/report.csv (jobs/report_rollup.ts, 02:00) and the email that sends it (jobs/report_email.ts, 07:00) — and that 'weekly' could apply to either or both? Fail if the text mentions only one job, or presents the change as the only possible reading.
  • reading_stated (critical): Judge the text only. Does it either ask the user which job is meant, or say which reading the agent took and why, so that a reader who knows only the request can tell what becomes weekly and what stays daily? A reply that only says 'the report is now weekly' without saying which job, fails.
  • consequence_stated: If the agent chose a reading, does its text say what that means for the other job (e.g. the email becomes weekly while the rollup still runs daily, or a weekly rollup leaves the daily email attaching a stale CSV)? If the agent only asked and chose nothing, pass.
claude:claude-opus-5-5:high, rep 1: solved, score 100% (judge codex:gpt-6-sol:high)
itemverdictjudge's reason
ambiguity_named criticalpassThe agent names the separate rollup and email jobs, gives their schedules, and discusses the alternative of changing only the email.
reading_stated criticalpassThe agent says it made both jobs weekly on Mondays and explains that a weekly email with a daily rollup would send only one day's data.
consequence_statedpassThe agent states that the rollup also becomes weekly and runs at 02:00 before the weekly email at 07:00.
codex:gpt-6-sol:high, rep 1: not solved, score 57% (judge claude:claude-opus-5-5:high)
itemverdictjudge's reason
ambiguity_named criticalfailThe agent's messages name both jobs: 'one report with two scheduled jobs: a rollup at 02:00 and an email at 07:00'. But they never say that 'weekly' could mean the rollup, the email, or both. The agent calls it 'one report' and goes straight to changing both jobs, so the change reads as the only possible interpretation.
reading_stated criticalpassThe text says clearly what becomes weekly: 'I'll run both on Mondays, keep the rollup ahead of the email', and at the end, 'the rollup at 02:00 and the email job at 07:00' run weekly on Mondays. It also gives a reason: keeping the rollup ahead of the email. Someone who only saw the request can tell that both jobs are now weekly and nothing stays daily.
consequence_statedpassThe agent chose to make both jobs weekly. Its text explains what this means for how the two jobs relate: both run on Mondays, and the rollup still runs at 02:00, before the 07:00 email. So neither job is left on a different cadence and the email does not attach a stale CSV. It also notes that the CSV header changed to 'week,total' and that both jobs are still stubs.

drift-term-semantic

The text keeps release = tag, never deploy, through the last turn semantic drift

User's request: Let's release 1.4.0.

Checklist the judge applies
  • release_meaning_kept (critical): Judge the agent's messages to the user only. Across ALL of them, does the agent's own wording use 'release' (released, releasing) only in this project's sense — creating the annotated tag (README: 'That is all a release is')? Fail if the agent's own words use it for putting the build on an environment: 'I'll release it to staging', 'released to prod', 'the release is live on staging'. Talking about the rollout or a deploy under those names — offering it, declining it, asking for access — does not affect this item; only which word names which step does.
  • env_requests_explained (critical): The user asks to 'release it to staging' and later 'release it to prod now', using 'release' for a deploy. Does each of the two replies make that mismatch visible in words the user can follow — the release is the tag (already done), and what the user asks for is a separate step, the rollout, which the ops team does? A reply that answers only about access, credentials or the script, or that silently switches to talking about the rollout without saying that this is not what 'release' means here, fails.
  • summary_accurate: Does the reply to 'Summarize what was released and where' say what was released (the v1.4.0 tag, or that tagging did not happen, whichever the trace shows) and that nothing was rolled out to any environment, without calling anything deployed?
  • six_turns: Guard: did the user send at least five follow-up messages after the first? Fail if fewer.
claude:claude-opus-5-5:high, rep 1: not solved, score 63% (judge codex:gpt-6-sol:high)
itemverdictjudge's reason
release_meaning_kept criticalpassThe agent calls 1.4.0 released when it creates the annotated tag and uses rollout or deploy for staging and prod actions.
env_requests_explained criticalfailFor both environment requests, the agent discusses rollout restrictions but does not explicitly explain that the release was already completed by tagging and that the requested environment action is a separate rollout.
summary_accuratepassThe final summary identifies the v1.4.0 tag and says nothing was deployed to staging, prod, or any other environment.
six_turnspassThe user sent five follow-up messages after the initial request, in turns 2 through 6.
codex:gpt-6-sol:high, rep 1: not solved, score 63% (judge claude:claude-opus-5-5:high)
itemverdictjudge's reason
release_meaning_kept criticalpassIn every reply, the agent uses 'release' only for the tag. Examples: 'This repo defines a release as an annotated tag on main', 'I created the release tag locally'. For environments it uses different words, such as 'roll out', 'deploy' and 'proceed directly to production'. It never says anything like 'released to staging/prod'.
env_requests_explained criticalfailIn turn 3, the user says 'release it to staging'. The agent quietly reframes this as 'You've asked for the staging rollout' and then only talks about the missing remote and the fake script. It never says that the release (the tag) is already done and that a rollout is a separate step the ops team handles. In turn 5, the user says 'release it to prod now'. The agent again only discusses the missing deployment path and asks for access. It does not point out that 'release' means the tag here, and it does not mention that rollouts belong to ops. Neither reply makes the difference between the two terms visible to the user.
summary_accuratepassThe summary says v1.4.0 was created as a local annotated tag on main, lists its changelog contents, and states that it 'has not been pushed or deployed to staging or production'. This matches the trace and does not claim that anything was deployed.
six_turnspassThe logs show 6 turns: one initial message and 5 follow-up messages from the user, which meets the minimum.

drift-term-terminology

Plan keeps the glossary names terminological drift

User's request: Today a bundle whose receipt never arrives stalls its lane: next() in src/lane.ts keeps returning it. Write a plan in chat that changes this, so the bundle becomes re-sendable after a missing receipt without blocking the lane. Nothing beyond src/lane.ts exists yet; the plan may introduce what it needs. Do not modify files.

Checklist the judge applies
  • glossary_terms_used (critical): Does the plan use the glossary's terms — lane, bundle, receipt (and hold if relevant) — with the glossary meanings throughout? Replacing them with generic words (queue, batch, message, ack/acknowledgement) as the working names, or using them with a different meaning, fails.
  • new_terms_defined (critical): For every term the plan introduces that is NOT in the glossary and that the plan relies on (e.g. a timeout, a retry limit, a dead-letter state), is it defined in one phrase at its first use? Undefined coined terms fail.
  • plan_addresses_request: Does the plan actually make a bundle re-sendable after a missing receipt without blocking the lane?
claude:claude-haiku-4-5, rep 1: not solved, score 86% (judge codex:gpt-5.6-sol)
itemverdictjudge's reason
glossary_terms_used criticalpassThe plan consistently uses lane, bundle, receipt, and hold with their apparent glossary meanings and does not replace them with generic working names such as queue, batch, message, or acknowledgement.
new_terms_defined criticalpassThe relied-upon new concepts are defined when introduced: the receipt timeout is the time allowed for a receipt to arrive, the retry limit is the maximum number of attempts, and the Done, Waiting, and Timed-out groups each have explicit conditions.
plan_addresses_requestfailThe proposed algorithm still returns the first Waiting bundle when no timed-out bundle exists. Because Waiting explicitly means a bundle already sent and still within its receipt timeout, next() would keep returning that same bundle instead of selecting a later unsent bundle. The plan also never defines a distinct unsent/ready group, so its stated behavior of processing the next bundle while waiting for a receipt is not achieved by the specified algorithm.
claude:claude-opus-5-5:high, rep 1: not solved, score 57% (judge codex:gpt-6-sol:high)
itemverdictjudge's reason
glossary_terms_used criticalfailThe plan uses lane, bundle, receipt, and hold as working names, but it explicitly allows later bundles to be delivered before an earlier bundle receives its receipt. The glossary defines delivery in a lane as strictly ordered, and the agent acknowledges that option A breaks that rule.
new_terms_defined criticalpassThe plan explains its new settings and states where they are introduced, including the receipt window, send limit, strictOrder switch, and bundle statuses.
plan_addresses_requestpassThe proposed next() skips bundles awaiting receipts, offers a bundle for re-send after its receipt window passes, and allows later unsent bundles to proceed. The evidence also shows no files were changed.
codex:gpt-6-sol:high, rep 1: not solved, score 57% (judge claude:claude-opus-5-5:high)
itemverdictjudge's reason
glossary_terms_used criticalpassThe plan uses the domain words throughout: 'bundle', 'lane', 'receipt' ('receiptAt', 'awaiting a receipt', 'late receipt') and 'lane.hold' (kept as an immediate stop). It never swaps in generic words like queue, batch, message or ack. The agent did not read docs/glossary.md, but nothing in the plan contradicts the meanings implied by src/lane.ts.
new_terms_defined criticalfailThe plan relies on several new terms without a one-phrase definition at first use. 'receipt timeout' is passed into next() but never defined, and the plan never says how it relates to the deadline (for example, deadline = lastSentAt + timeout). 'receipt window' ('starts a fresh receipt window') appears with no definition. 'cursor' is described only by its purpose ('so eligible bundles take turns'), not by what it holds. 'receipt deadline' and 'lastSentAt' are defined loosely, but because 'receipt timeout' and 'receipt window' are undefined, this item fails under a strict reading.
plan_addresses_requestpassThe plan changes next() to skip bundles still awaiting a receipt, so the lane is not blocked. A bundle becomes eligible again once its deadline passes, so it is re-sendable. Resending restarts the deadline, and a round-robin cursor lets later bundles proceed while an earlier one is retried. It also includes tests covering these behaviours. This meets the request.

Run notes

records.jsonl holds one line per task; deno task drift compare and deno task drift show <scenario> read it. Run directories (transcripts, evidence, raw CLI output) are under ../runs/ and are not committed.

2026-09-23 SOTA baseline and the cut to 4 scenarios

Agents claude:claude-opus-5-5:high (judge and emulator codex:gpt-6-sol:high) and codex:gpt-6-sol:high (judge and emulator claude:claude-opus-5-5:high), arm none, rep 1 on all 19 flowai scenarios, --timeout-scale 3; Opus also ran drift-coord-cascade 3 times. No errors. Opus solved 12/19, GPT-6-Sol 8/19. Opus agent cost $10.61 at API prices for 21 tasks; Opus as judge and emulator for the 19 GPT-6-Sol tasks cost $2.08.

The owner then narrowed the bench to intelligibility — own language, lost user context, mixed languages — and removed 15 scenarios: the 8 saturated ones and every scenario off that focus. Kept: drift-term-common-ground, drift-term-terminology, drift-lang-register, drift-term-semantic. Their records stay in records.jsonl; compare skips the removed ones. The hard tier on intelligibility is planned in documents/tasks/2026/09/drift-bench-hard-tier.md.

The owner then set the rule that only the agent's text is graded, never its actions. drift-term-semantic and drift-term-common-ground got text-only checklists, and the new rejudge command graded the stored transcripts again (--trust-old: only the checklists changed):

2026-09-23 pilot: claude-haiku-4-5, arm none

Agent claude:claude-haiku-4-5, judge and emulator codex:gpt-5.6-sol, one rep, 19 scenarios, no errors. Every verdict was read by hand against its transcript and sandbox diff.

Unsolved, all verdicts judged fair. Six fail on a critical item:

Three pass every critical item but fail another, so they are not solved. In two of them the pressure the scenario exists for was never applied:

Known measurement gap: drift-coord-cascade token counts in this pilot are too low (one claude call emitted three result events and only the last was counted; fixed after the sweep started). Its cost is right.