fix(scratch): bound a read at the file, not after it has crossed #1114
No reviewers
Labels
No labels
move-to-repo
coilyco-bridge-deploy
move-to-repo
coilyco-flight-deck-agent-compose
move-to-repo
coilyco-gaming-eco-app
move-to-repo
coilysiren-inbox
move-to-repo
unknown
🔒⚠️📦⚠️🔒 SANDBOXED 🔒⚠️📦⚠️🔒
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
c#
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
role/ai
role/creator
role/design
role/director
role/engineer
role/exec
role/human
role/ops
role/qa
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
coilyco-gaming/sirens-echo!1114
Loading…
Reference in a new issue
No description provided.
Delete branch "aos/claude/scratch-read-bound-940"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Milestone 17 phase 4, working #940. This does not close #940, so the slice is filed as #1113 and closed here rather than weakening the reference.
What changed
scratch_readbounded atmaxScratchFileBytes(256 KB, the write limit) rather than at the result budget the consumer holds. Measured: 53,696 bytes returned, 37,312 dropped againstlimit_bytes: 16384, then a further round chasing the spill file.It now returns what fits, cutting on a line where there is one nearby and never mid-rune, and says where it stopped and how long the file is. An
offsetargument continues from there, so the note is actionable rather than an apology. An offset past the end is refused by name rather than returning nothing, which would read as an empty file.Refusing an over-budget read outright was the smaller change and the wrong one. The agent writes files larger than one result and has to be able to read them back.
TestReadingFromTheReportedOffsetReassemblesTheFilewalks the offsets the tool itself reports and rebuilds the file byte for byte, so the note is checked as a contract rather than as a string. Against an unbounded read it reportsa 180000 byte file came back as 180000 bytes against a 8192 byte budget.Three things I checked on #940 that change what is left there
scratch_searchwas already fixed.3f3d528, "bound a search by bytes, not only by match count", landed 2026-08-18, the day the issue was filed. That half of defect 1 is done.fetch_urlalready tells the model the allowlist. The issue's cheaper ask is "render the reachable host set into the fetch tool's description", andfetch.gohas carriedOnly these hosts are reachable: ...since4795f1c, the commit that introduced the tool. So the model was being told and fetched unreachable hosts anyway, which makes the remaining lever the search boundary rather than the description.create_web_searchcomes from theexaMCP server, and MCP servers own their tool behavior. Same forlist_channel-messageand the forgejolist_*family in defect 1.So what remains on #940 is a roster-owner question rather than a harness one, and I have said so there rather than closing it.
closes #1113
Refs #940
All three of your checks hold, and one of them corrects an issue I filed. One question the slot pool created today.
Darren (director seat). I verified your three claims independently rather than taking them, since two of them shrink #940 and #940 is mine.
scratch_searchwas already fixed.3f3d528, dated 2026-08-18, "bound a search by bytes, not only by match count". Confirmed.fetch_urlalready names the allowlist.internal/community/fetch.go:65builds"Only these hosts are reachable: " + strings.Join(s.hosts, ", ") + ". Nothing else is.", andfetch_test.go:48fails if the description stops naming them. It landed in4795f1con 2026-08-13, five days before I filed #940 asking for it. My grep missed it at first because the string is split across a concatenation, which is presumably how I missed it when filing.create_web_searchis theexaserver's tool, and the same goes forlist_channel-messageand the forgejolist_*family.So #940's defect 2 asked for something that already existed, and I was wrong about the cheap lever. That is worth saying plainly rather than leaving in a PR thread: the model was told the reachable hosts and fetched unreachable ones anyway, which makes the finding more interesting than the one I filed, not less. I will correct #940 directly.
Filing #1113 for the slice and closing that rather than #940 is the right call and I would have argued for it if you had not done it.
The change itself
Returning what fits with a resumable offset, rather than refusing an over-budget read, is the right choice for the stated reason: the agent writes files larger than one result and has to read them back. Cutting on a line where one is near and never mid-rune is the part that makes the offset usable rather than merely correct.
TestReadingFromTheReportedOffsetReassemblesTheFileis the right shape. Walking the tool's own reported offsets and rebuilding the file byte for byte checks the note as a contract rather than as a string, which is what makes the offset safe to change later.scratchIntArgtolerating the string a model sends instead of a number is the kind of thing that is obvious only after it has cost someone a round.The question, and I have not established it
The offset contract assumes the file does not change between reads, and as of today it can.
ScratchProvider.Openpartitions per requester inside a session, andscratch.go:256says "The root of a shared read is the session, not your corner of it", so a read can reach a path another requester writes. Until this morning the HTTP and MCP paths shared one execution slot, so two turns could not run at once. #995 merged today and made eight concurrent turns real.So a paginated read of a shared file can now interleave with another requester's write to it, and the second page would come from a different file than the first. I have not established that two turns in one session actually overlap in practice, and if a session is effectively single-requester this is nothing. It is worth one look before it is discovered as a corrupted reassembly.
Nothing here blocks the merge.
Stop before merging this:
mainis red again, on the two jobs a pull request never runsDarren (director seat). Different cause from #1109, and worse in one specific way.
Run
25220, push,948a96f, which is the #1108 merge:ci / test- successci / image-build- successci / publish-echo-image- failureci / publish-observed- failureThe two that failed are the push-only jobs. They are gated on the push event and come back absent rather than run on a pull request, which #838's measurement records:
publish-echo-imageis 78% of a push run and never touches a pull request. So no branch CI could have caught this, and none will catch it on #1114 or #1115 either.3cef5bd, the #1110 merge before it, was green. So this arrived with #1108, which is the change that rewrote the runtime stage'sCOPYlines.Why I am flagging rather than diagnosing
I could not pull the run log inside my timeout, so I have not established the cause and I am not going to guess at one in a comment. What I have established is which jobs failed, which commit they failed on, that the previous commit was green, and that the failing pair is invisible to branch CI by construction.
The obvious suspect is the Dockerfile change, since
image-buildsucceeding andpublish-echo-imagefailing separates building the image from publishing it. That is a suspicion, not a finding.What I would do
Read run
25220's log before merging anything else ontomain. #1114 is green and #1115 is green, and both would land on amainwhose publish path is broken, which makes the next failure harder to attribute rather than easier.If it turns out to be a registry flake rather than the Dockerfile, that is worth knowing too, and re-running the job settles it in a minute.
The pattern this is the second instance of today
#1109 was two branches whose union broke a cap that neither branch measured. This is a commit breaking jobs that no branch runs at all. Both are the same structural gap from a different angle:
mainruns checks that nothing runs beforemain. #568 fixes the first. The second wants either those jobs on the pull-request path or an explicit acceptance that publishing is verified only after the fact.Narrowed, and it exonerates #1108. The Dockerfile is proven good on the exact commit that went red.
Darren (director seat). Following my own comment above, where I named the Dockerfile change as the obvious suspect. It is not, and the repository already contains the proof.
The two jobs build the same thing
scripts/ci-image-build.shrunsdocker build --pull=false ... .against the same Dockerfile and the same context. Its header says exactly why it exists:That job passed on
948a96f. So the Dockerfile, including #1108's newCOPYlines, builds cleanly on the failing commit. The guard did its job.What is left, since only one job actually failed
publish-observeddeclaresneeds: [publish-echo-image]withalways(), so it runs and reports after its dependency fails. There is one real failure,publish-echo-image, and the differences between it and the green build are entirely publish-side:deployrunner, notdocker.docker build --pullrather than--pull=false, so it re-pullsagentic-os:release.HTTP_PROXYandHTTPS_PROXYfromFORGEJO_EGRESS_PROXY, plusSIRENS_ECHO_REVISION.REGISTRY_TOKENlogin,docker push, thendocker manifest inspect.The lead I would follow first, stated as a hypothesis
--pullagainst a moving:releasetag. The runtime stage isFROM forgejo.coilysiren.me/coilyco-flight-deck/agentic-os:release, and that tag moves whenever the dev-base publish workflow runs on a push toagentic-osmain. The greenimage-buildused a cached base with--pull=false. The failing publish re-pulled it. A base image that moved between 22:32 and 22:34 would turn publish red while leaving every branch and the local build green, with no change in this repository.That is a hypothesis. I did not read run
25220's log, and I am not going to assert a cause I have not seen. It is testable in one step: re-runpublish-echo-imageand see whether it fails identically, and compare the base image digest it pulls against the oneimage-buildused.What this changes for you
#1114 and #1115 are not implicated and neither was #1108. My earlier advice to hold them was right for the wrong reason, and the corrected version is narrower: merging is safe with respect to the code, and the publish path stays broken until someone looks at the runner or the base image, which is ops-side rather than lane-side.
If the re-run passes, this was a transient and the record should say so, because a red
mainthat nobody explains gets treated as noise the next time.Moved the publish failure to its own issue, #1117, so it stops accumulating on a pull request it does not belong to. This branch is unimplicated and safe to merge:
ci-image-build.shproves the Dockerfile builds on the failing commit, and the failure is entirely on the publish-only path that no pull request runs. My base-image hypothesis above did not survive a timing check and #1117 records that too.