Chapter 4 of 7 · 8 min read
A fresh context discards established state along with noise
On this page
The test was applies promo code before tax in checkout.spec.ts, and it failed on roughly one continuous-integration run in nine. That makes it a flaky test, meaning one that passes and fails on unchanged code, so a red run says nothing about the change that triggered it and everyone learns to re-run instead of reading it. Vantage Retail's coding agent had been chasing it for twenty turns. It read the spec, read the fixtures — the helper code that puts the application into a known state before a test's assertions run — ran the suite twice under Playwright, the browser-driving test framework the suite is written in, with --repeat-each=40, correlated the failures against the worker log, and arrived somewhere real: seedCart() posts to /cart/items, the API returns 202 Accepted, which is the status meaning the request has been taken but not yet carried out, and the cart row is written by a background consumer that usually lands in about 40 ms and occasionally takes 600 ms under CI's CPU contention. The fixture awaits the 202 and considers itself done, so the page under test sometimes loads before the cart exists, the subtotal renders as $0.00, and the promo assertion fails. Nothing is wrong with the assertion. The setup returns too early.
Having established that, the parent delegated the repair, because applying a fix and running the suite is a bounded unit with a small result and the parent had a change request queued behind it. The brief was the task as the human had originally phrased it: fix the flaky test in checkout.spec.ts. The child had Read, Edit, and permission to run the test file. Ninety seconds later it returned a patch and a green run.
--- a/e2e/checkout.spec.ts
+++ b/e2e/checkout.spec.ts
@@ -41,8 +41,7 @@ test('applies promo code before tax', async ({ page, seedCart }) => {
await page.getByLabel('Promo code').fill('SPRING20');
await page.getByRole('button', { name: 'Apply' }).click();
- const subtotal = await page.getByTestId('subtotal').innerText();
- expect(subtotal).toBe('$42.00');
+ await expect(page.getByTestId('promo-applied')).toBeVisible();
await expect(page.getByTestId('total')).toContainText('$');
})The check that the promo actually reduced the subtotal to $42.00 is gone, replaced by a check that a badge appears and that the total contains a dollar sign. The suite is green, it will stay green, and the test no longer verifies the behaviour it was written for. The child ran it eight times to confirm.
The naive reading is the only reading the child has
Nothing here is a failure of the child's reasoning. "Fix the flaky test" has an obvious interpretation, which is that the test is unreliable and should stop being unreliable, and under that interpretation removing the brittle assertion is a defensible move that experienced engineers make on purpose several times a year. The parent's twenty turns had replaced that interpretation with a much more specific one — the test is correct, the fixture is wrong, the flake is a race between a 202 and a background consumer — and none of that crossed the boundary, because the boundary is what discards the child's context and the parent's context is not automatically part of the brief.
This is the cost side of the ledger that the first three chapters have been building. The fresh window removes the parent's distractions and it removes the parent's findings with the same stroke, since the mechanism cannot tell them apart. What was discarded here was not information the child could have looked up. It was earned state: the output of two forty-run test loops, a correlation against a worker log, and four hypotheses that were tried and eliminated. Re-deriving it would have cost the child around six minutes of test execution and twenty turns of its own, which is the entire cost the delegation was supposed to avoid, and there is no version of the brief in which the child stumbles onto it by being clever.
It helps to be precise about what the child does and does not lack, because "fresh context" gets read as a kind of impairment. The child has the same weights, the same general knowledge of Playwright and of racy fixtures, and the same access to the repository, so it can read everything the parent read. What it lacks is the conclusions the parent reached by doing work whose results are not written down anywhere in the repository. That distinction predicts which delegations survive a thin brief: a task whose whole input is on disk survives one, and a task whose input includes anything the parent discovered at runtime does not.
The verification signal made it worse rather than better. The child had a way to check its work — run the test — and the check passed, because the metric it was optimising against was the same metric its patch had corrupted. Any delegated repair whose acceptance test is the thing being repaired has this property, and the defence is in the brief rather than in the tooling: state the constraint that the check cannot express, which here is that the assertions are correct and must not change.
A well-shaped output contract would at least have made the failure visible at the boundary. Vantage's child returned a diff and the sentence "the flaky assertion has been replaced with a stable check; the suite passes eight out of eight". Had the contract required a root_cause field naming what the patch addresses, alongside the diff and the run count, the child would have had to write brittle_assertion into a field that the parent could read against what it knew, and the mismatch with fixture_race is machine-checkable. That does not repair the brief, since the child would still have made the wrong patch, and it converts a silent regression into a rejected result — which is the difference between finding out now and finding out in March when someone notices that promo codes have been broken in production for six weeks.
Pasting the history is not the fix
Vantage's next iteration is the one almost every team tries, and it has the appeal of obviously solving the problem: if the child failed because it did not have the parent's context, give it the parent's context. The implementation was two lines, serialising the conversation so far into the brief so the child would "have full context".
# What they shipped after the incident.
brief = (
"Full conversation so far:\n"
+ "\n".join(f"[{m['role']}] {m['content']}" for m in parent_messages)
+ "\n\nNow fix the flaky test in e2e/checkout.spec.ts."
)Three weeks later the report was that subagents "were not helping". Latency per delegation had gone from 19 seconds to 31, token spend was up 38% month over month, and the answers were not measurably better than running the same tasks inline. The parent's history at the point of delegation was around 47,000 tokens, so every child now started with a 47,000-token window before reading a single file, and the children were slower to first token, more expensive, and — this is the part that surprised them — no better at the task.
They had built a second model call rather than an isolation boundary. The history that was crowding the parent is now crowding the child too, and the boundary's only asset, which is that the child's window contains what the task needs and nothing else, has been given away before the child starts. The setup cost is paid in full: an extra inference, an extra round trip, an extra copy of the prompt. The benefit is zero by construction, since the child's window is now a superset of the parent's.
There is a second-order effect that makes the fat brief worse than it looks in the token accounting. The parent's transcript contains its dead ends — the hypothesis that the flake was a CSS animation, the hypothesis that it was a stale service worker, two turns of chasing a red herring in the auth fixture. Those are in the child's window now with the same weight as the finding that survived, and the topic on sub-agent context isolation already covered what happens when a model is conditioned on the reasoning that produced a conclusion rather than on the conclusion. The child inherits the parent's blind alleys along with its results, which is the specific benefit people usually say they want the fresh window for.
| Thin brief | Full history | |
|---|---|---|
| Brief size | 40 tokens | ~47,000 tokens |
| Added latency per delegation | 19 s | 31 s |
| Child knows the flake is a fixture race | no | yes, buried among four dead ends |
| Parent window growth | small | small |
| Total tokens vs doing it inline | ~1.4× | ~2.3× |
| Failure mode | acts on the naive reading | pays for a boundary it does not have |
Both columns are failures of the same design decision, which is that neither team decided what the child needed to know. One decided nothing and sent nothing; the other decided nothing and sent everything. The token counts differ by three orders of magnitude and the amount of thought behind them is identical.
What the two failures have in common
The thin brief loses because the parent's conclusions were not in it. The fat brief loses because the parent's process was in it. Those are not opposite errors on a single dial where the answer is somewhere in the middle, and a team that reads it that way ends up truncating the history to the last fifteen turns and getting a brief that is both expensive and missing the finding from turn six.
The distinction is categorical. A parent's transcript contains two kinds of content: the things it established, which are small, durable and directly useful to anyone continuing the work, and the process by which it established them, which is large, specific to the path taken, and useful only for auditing. The first belongs in the brief. The second belongs in the log. Nothing in the runtime separates them for you, and the separation is not something a truncation window or a token budget can approximate, because the finding you need might be the oldest thing in the transcript.
One honest caveat before the fix, because the separation is cleaner in principle than in practice. Some of what a parent holds after twenty turns is not a conclusion it could write down if asked — a sense of which parts of the suite are usually unreliable, a suspicion about the worker that has not been confirmed, a calibration about how much to trust CI timings on a busy afternoon. That material is genuinely hard to transfer, and pretending otherwise leads to briefs that grow every time a delegation goes slightly wrong until they are transcripts again. Where a task depends heavily on that kind of judgement, the right call is often to keep it inline and delegate something else, which is a decision the last chapter of this topic returns to with a way of telling the two apart.
Vantage's fix was one paragraph long and cost nothing to run, and the reason it works is not obvious enough to leave as an exercise.
Subagents — read the current version at https://daviddominguez.dev/learn/en/subagents/a-fresh-context-discards-established-state-along-with-noise