monad-developers / monad-developers/ultrafuzz
Rendered prompts bust the prompt cache: loop copies share only 16% prefix, lanes share 3 bytes
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 85
- Forks
- 18
- Avg merge
- 11h 10m
- Merged PRs (30d)
- 194
Description
Summary
Rendered node prompts bust the prompt cache almost immediately. The k loop copies of a strategy node are 99.86% byte-identical, but the bytes that differ are absolute artifact paths carrying the loop index (.../artifacts/boundary-tests-0/... vs -2), and they start early and repeat throughout. The result is that only 16% of loop-copy prompt text sits in a shared cache prefix.
Separately, the 19 strategy lanes share a 3-byte common prefix, because each rendered prompt opens with its own lane title -- even though 27% of every lane prompt is identical boilerplate.
Neither is a correctness problem. Both are avoidable token spend on every campaign.
Observed
Measured against .ultrafuzz/runs/<run>/prompt-snapshots/ from a completed MetaMorpho v1.1 campaign (222-node graph, strategies group at loops: 3, 57 strategy attempts).
1. The loop copies differ only in a directory digit.
cmp -l between two copies of the same lane reports 26 differing bytes out of 18,452. Across all 19 lanes the differing character pairs are exclusively 0<->1, 0<->2, 1<->2. Every occurrence is the loop index inside a path:
.../runs/mm-a/artifacts/packed-action-parity-0/findings.json
.../runs/mm-a/artifacts/packed-action-parity-2/findings.json
^
2. Those digits collapse the shared prefix.
Because the first one lands around byte 2,143 and the rest are scattered to the end, the common prefix dies early:
| lane | prompt bytes | shared prefix | % | differing bytes |
|---|---|---|---|---|
| Packed Action Parity | 19,567 | 2,143 | 11.0% | 23 |
| Payable Fallback Accounting | 15,787 | 1,782 | 11.3% | 20 |
| State Machine Boundaries | 18,969 | 2,334 | 12.3% | 22 |
| Externalized-State Accounting | 22,646 | 3,238 | 14.3% | 32 |
| Admin / Config Boundaries | 21,555 | 3,556 | 16.5% | 30 |
| Round Trip | 16,445 | 3,971 | 24.1% | 24 |
| Boundary Tests | 18,452 | 5,587 | 30.3% | 26 |
Totals across the 57 strategy attempts: 981,351 bytes of prompt, of which 157,089 (16.0%) is in a shared prefix. About 824 KB -- on the order of 205k tokens -- is re-sent outside any cache prefix to express at most 32 bytes of difference.
3. Cross-lane sharing is zero despite 27% shared content.
The common prefix across the 19 distinct lane prompts is 3 bytes (\n# ), since the lane title is the first line. But 31 lines totalling ~5,230 bytes are byte-identical in all 19 lanes -- the reachability and note-key vocabulary, the Output Contract schema and validation rules, the generated-tests classification rules. That block is re-sent 57 times, roughly 298 KB.
It is not hoistable as it stands: those shared lines are interleaved from line 3 to line ~250 of each prompt and are never contiguous.
4. Long absolute paths are a compounding cost.
The interpolated project root is 66 characters and the full artifact path appears 26+ times per prompt. Several KB per attempt is path text that conveys nothing the agent could not obtain from a short relative path or an environment variable.
Reproduction
Against any completed run directory:
cd .ultrafuzz/runs/<run>/prompt-snapshots
# group snapshots by their '# <title>' heading; lanes with 3 copies are the loop copies
cmp -l <copy-a>.md <copy-b>.md # -> only 0/1/2 digits differ
diff <copy-a>.md <copy-b>.md # -> every hunk is an artifact/workspace path
Then compute the common prefix per lane group and sum. The snapshots are content-addressed, so identical renders would collapse to one file; three files per lane is itself the signal.
Possible resolutions
Roughly in payoff-per-effort order. These are complementary, not alternatives.
A. Stop interpolating attempt-specific absolute paths into prompt bodies.
This is the actual cache-buster and the highest-value change. Replace rendered absolutes with an indirection the agent resolves at runtime -- an exported ULTRAFUZZ_ARTIFACT_DIR / ULTRAFUZZ_WORKSPACE_DIR, or a stable path inside the per-attempt workspace. Prompt bodies then become byte-identical across loop copies and the prefix goes from 16% to effectively 100%. It also shortens every prompt.
Cost: the agent must resolve the variable before writing artifacts, and the validation commands shown in the Output Contract need the same treatment. Worth checking that the artifact-path safety checks (packages/artifacts/src/safe-paths.ts) still bind correctly when the prompt no longer names the absolute target.
B. Order the rendered prompt cache-friendliest-first.
Emit invariant shared preamble, then lane-specific prose, then anything attempt-specific, in that order. Today the shared block is interleaved throughout. Ordering it first makes the ~5.2 KB invariant section a single cache write per run instead of 57.
C. Move the remaining loop-varying token to the tail.
After A and B the only divergence is {{strategy_loop_index}} in the sharding rule (target_index % {{strategy_loop_count}} == {{strategy_loop_index}}). Either place it in the final lines, or have the attempt read it from the environment so the rendered body is fully identical.
D. Consider cache warm-up ordering.
With requested_concurrency: 4 a lane's loop copies launch together and all cold-miss regardless of prefix quality. Collecting the within-lane win needs attempt 0 to establish the entry before 1 and 2 launch. The cross-lane win from B does not depend on this, which is an argument for doing B first.
E. Instrument before and after.
The runtime already recognises cache accounting signals -- cache_read_input_tokens, cache_creation_input_tokens, promptCacheHitTokens and friends are enumerated in packages/runtime/test/agent-adapter-boundaries.test.ts. But usage.jsonl was 0 bytes for this completed run, so nothing was recorded. Getting that ledger populated should precede any of the above, so the change is measured rather than assumed.
Adapters shell out to provider CLIs and do not set cache breakpoints themselves, so all of this is about making the rendered bytes stable rather than about adding cache_control markers.
Scheduling
Not urgent and not a correctness issue. This touches prompt rendering and artifact path handling, both of which are load-bearing, and the payoff is cost rather than behaviour. It should be picked up once the runtime has settled -- suggested deferred-post-v0.1.
Environment
@ultrafuzz/cli/0.1.0 darwin-arm64 node-v26.3.1- repo at
ab3c03f4bbb0a4742dbcc2783e0156b43c0c9487 - evidence from a MetaMorpho v1.1 campaign, 222-node graph,
strategiesgrouploops: 3
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the prefix and path differences from .ultrafuzz/runs//prompt-snapshots, then trace the prompt-rendering entry points that produce those snapshots. Review packages/artifacts/src/safe-paths.ts and packages/runtime/test/agent-adapter-boundaries.test.ts for path safety and cache-accounting constraints. Done means attempt prompts avoid unnecessary path variation, shared content is ordered for reuse, and cache usage can be measured in usage.jsonl.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- nodejs, typescript
- Domain
- performance, tooling
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100