Three Quarters of My Reference Notes Never Reached the Agent: Measuring What head Cuts Away
I fed reference notes to a scheduled agent with cat and head, and the lines that mattered were quietly cut. Here is the measurement, and how I replaced a line count with a section-level contract.
My morning update run walked straight into a bug my own notes told it how to avoid. The prompt had not changed since the day before. The workaround was right there in the file.
The agent was not the problem. The head -150 I had written when handing over that file was cutting the text a few lines above the workaround.
The same prompt, and notes that sometimes arrive
As an indie developer, when I put app update work on a schedule, the agent gets reference material: release steps, known issues, device-specific branches. The usual approach is to cat those files into the prompt.
# how I used to hand them over (the flawed version)cat "${NOTES}/release_steps.md" | head -150cat "${NOTES}/known_issues.md" | head -150
The head -150 was there for a reason. Notes grow, and a swollen prompt dilutes the actual instruction. As a brake, it was the right instinct.
But that line assumes the top 150 lines are the most important ones. I had never once checked that assumption.
The cut is measured in lines; the value sits in sections
I started by measuring the files I was actually shipping, rather than arguing from intuition.
for f in "${NOTES}"/*.md "${NOTES}"/*.txt; do L=$(wc -l < "$f"); B=$(wc -c < "$f"); H=$(head -150 "$f" | wc -c) printf '%s: lines=%s bytes=%s head150=%s reach=%s%%\n' \ "$(basename "$f")" "$L" "$B" "$H" "$(( H * 100 / B ))"done
Three files, three answers.
File
Lines
Total bytes
Bytes delivered by head -150
Reach
Update history notes (append-on-top)
633
69,130
19,252
27%
Keyword and takeaway notes (append-on-top)
860
105,540
23,174
21%
Audience and assumptions notes (rewritten in place)
43
2,919
2,919
100%
Seventy to eighty percent never arrived. Worse, the reach depended entirely on how each file grows. Append-on-top files stack new sections at the head, so a line budget is consumed almost immediately. Files rewritten in place stay small and pass whole. The same head -150 meant something different for every file.
The boundary was the uglier half. Around line 150, the cut landed in the middle of a dated section: the heading and its first two bullets crossed over, and the conclusion of that section did not. From the agent's side, a half-written fragment had been presented as reference material.
A line count is a unit that knows nothing about a file's structure. I was trying to enforce a byte budget and putting the scissors somewhere meaning had not agreed to.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Real numbers: a 633-line, 69,130-byte notes file delivered only 19,252 bytes (27%) under head -150, with the cut landing mid-section
✦The asymmetry that hides the failure: a wrong path, an empty file, and a mid-section cut all exit 0 and look identical downstream
✦Full implementation of a byte-budgeted section packer plus a preflight that refuses to launch the agent when required sections are missing
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
This is the part that bothered me most. Nothing in this path reports a failure.
What happens
Exit status
What lands in the prompt
Detectable?
Wrong path, cat fails
Pipeline still 0
Empty string
No
File exists but is empty
0
Empty string
No
Cut at line 150
0
A plausible fragment
Least of all
The exit status of cat a | head -150 belongs to the last command in the pipeline. head succeeds on empty input. So a typo in a path, a vanished file, and a mid-sentence truncation all flow downstream wearing the same face: a clean exit with nothing, or almost nothing, behind it.
The agent will not report the gap either. Information that never arrived does not exist from its point of view. What looked like "it ignored the note" was really "the note was never handed over."
In unattended runs this kind of silence is expensive. I have closed a similar path before, where agent output disappeared for an unrelated reason; that one is written up in the pre-commit gate for agent output swallowed by .gitignore. The shape is identical: unless a failure raises its hand, a broken setup runs happily for weeks.
Make the boundary a section-level contract
The fix followed from the diagnosis. Cut by section instead of by line, budget in bytes, and always break immediately before a heading. Sections that do not fit are dropped whole rather than sliced.
The notes are Markdown with ## section headings, so the packer is small.
#!/usr/bin/env python3"""notes_pack.py — pack reference notes into a byte budget, section by section"""import reimport sysfrom pathlib import PathHEADING = re.compile(r"^##\s+(.+)$")def split_sections(text: str): """Split into a preamble plus one entry per ## section.""" lines = text.splitlines(keepends=True) preamble, sections = [], [] current = None for line in lines: m = HEADING.match(line) if m: if current: sections.append(current) current = {"title": m.group(1).strip(), "body": [line]} elif current: current["body"].append(line) else: preamble.append(line) if current: sections.append(current) for s in sections: s["text"] = "".join(s["body"]) s["bytes"] = len(s["text"].encode("utf-8")) return "".join(preamble), sectionsdef pack(path: Path, budget: int, keep_titles=()): text = path.read_text(encoding="utf-8") preamble, sections = split_sections(text) out, used, dropped = [], len(preamble.encode("utf-8")), [] # (1) reserve sections that are explicitly required pinned = [s for s in sections if any(k in s["title"] for k in keep_titles)] rest = [s for s in sections if s not in pinned] for s in pinned + rest: if used + s["bytes"] <= budget: out.append(s) used += s["bytes"] else: dropped.append(s["title"]) # (2) drop whole sections, never slice body = preamble + "".join(s["text"] for s in out) return body, used, droppedif __name__ == "__main__": src = Path(sys.argv[1]) budget = int(sys.argv[2]) if len(sys.argv) > 2 else 20000 keep = sys.argv[3].split(",") if len(sys.argv) > 3 else [] body, used, dropped = pack(src, budget, keep) sys.stdout.write(body) # (3) never let a drop happen quietly if dropped: sys.stderr.write( f"[notes_pack] {src.name}: {used}/{budget} bytes, " f"dropped {len(dropped)} sections: {', '.join(dropped[:5])}\n" )
Three things carry the design.
Required sections are reserved first. A section like "device-specific branches" is needed regardless of how new it is, so keep_titles claims it before anything else competes for the remaining budget.
Sections that do not fit are dropped whole. A fragment is worse than an absence, because a fragment persuades the agent that the topic was considered and no conclusion was reached.
Every drop goes to stderr. It is the smallest possible mechanism for refusing to lose things quietly.
The only real difference from head is that the cut now sits where a human decided meaning ends. It is not clever. It still changed the quality of what arrives.
Deciding what survives the budget
The moment you drop whole sections, someone has to choose which ones. I could not automate that, so I moved the decision into the notes themselves.
The packer reserves pinned sections, fills fresh ones newest-first, and leaves undeclared sections for last.
I set the budget itself against the instruction, not against the model's window. My rule of thumb: if the reference material exceeds roughly three times the length of the instruction, tighten the budget. When reference overwhelms instruction, the agent drifts toward summarizing the material and quietly demotes the job I asked for. The same dilution shows up with long attachments, which is what the grounding gate for long PDFs the agent skims past addresses from the reading side. Seen from the handing-over side, the first defense is simply a budget that refuses to overfill.
A preflight that catches the gap
Even with the packer, a mistyped path and an empty file still slip through in silence. So I added a gate that runs before anything launches.
#!/usr/bin/env bash# notes_preflight.sh — verify notes exist, are non-empty, and carry required sectionsset -euo pipefailNOTES="${1:?usage: notes_preflight.sh <notes-dir>}"FAIL=0check() { local file="$1"; shift local path="${NOTES}/${file}" if [ ! -f "$path" ]; then echo "MISSING notes file: ${path}"; FAIL=$((FAIL + 1)); return fi if [ ! -s "$path" ]; then echo "EMPTY notes file: ${path}"; FAIL=$((FAIL + 1)); return fi # required sections: a file without these is considered broken for key in "$@"; do if ! grep -qF "$key" "$path"; then echo "MISSING section: ${path} -> ${key}"; FAIL=$((FAIL + 1)) fi done echo "OK ${file} ($(wc -c < "$path") bytes)"}check "release_steps.md" "## Device-specific branches" "## Rollback"check "known_issues.md" "## Open"if [ "$FAIL" -gt 0 ]; then echo "STOP: ${FAIL} notes problem(s). Not launching the agent." exit 1fi
The order in the unattended run is now fixed.
Run notes_preflight.sh first and do not launch the agent if anything is off. Doing nothing today beats working from broken material.
Run notes_pack.py per file, keeping stderr in the run log.
Assemble the prompt and start the agent.
Refusing to proceed on a broken premise is the same instinct that governs the schedule as a whole; where to hand work over and where to stop is something I worked through in how I split scheduled work after moving to the Antigravity CLI. This preflight simply adds one more stop condition, this time on the material rather than the task.
What three weeks showed, and what stayed broken
I watched the morning runs for about three weeks after the switch.
Aspect
Under head -150
Under the section contract
Reach for the update history notes
27% (19,252 / 69,130 bytes)
Whole sections within a 20,000-byte budget, zero fragments
Required section (device branches) arriving
Fell out as the file grew
Every run, via pinned
Sections delivered as fragments
Roughly one per run
0, by dropping whole sections
Detecting broken material
Impossible (exit 0 all the way)
Preflight halts before launch
Preflight cost
—
About 20-40ms for three files
The reassuring part was not the percentages. It was that runs stopped walking into workarounds I had already written down. Once the cause turned out to be my own handover, I spent far less time second-guessing the agent's output.
To be honest about the limits: sections that exceed the budget still do not arrive. Dropping them whole is a choice of absence over fragments, not a way of delivering more. I read the stderr drops and decide by hand when to fold the notes down. I tried routing that to an automatic summarizer, but the summaries kept losing the caveats that made a section worth keeping, so for now I prefer doing it myself.
One more limit: this design depends on the notes being structured with headings. A file of endless running bullets has no section boundaries, and the packer can do nothing with it. The work of structuring notes stays on my side of the table.
The one thing to do next
If you hand reference material to an agent anywhere in your setup, measure the reach before changing anything. The for loop at the top of this article tells you, in about a minute, what share you were actually delivering.
For me, that one number changed who I suspected. If it does the same for you, the rest of the design tends to follow on its own.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.