Skip to content

fix(project-context): read managed session-render outputs at the bound the writer can emit - #69

Merged
andrei-hasna merged 1 commit into
mainfrom
fix/b46ca2a3-managed-target-read-cap
Aug 7, 2026
Merged

fix(project-context): read managed session-render outputs at the bound the writer can emit#69
andrei-hasna merged 1 commit into
mainfrom
fix/b46ca2a3-managed-target-read-cap

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

The defect

Session render writes a home's instruction target with no size bound, while the guard that reads it back caps at 256 KiB. A home whose instruction corpus grows past the cap wedges permanently and silently: every later plan/apply for that home throws PROJECT_CONTEXT_INPUT_TOO_LARGE before it can do any work. The renderer produces a file that stops its own next run.

observeProjectContextSessionGuard (project-context.ts:653) hashes every path in projectContextSessionGuardPaths (:3206), which includes paths.target (:3214) — the file session render itself writes. currentFileHash carved out session-render-manifest.json at 8 MiB and left the target on the 256 KiB default. session-apply.ts and session-render.ts contain no size guard at all (0 hits for byteLength|maxBytes|TOO_LARGE), so writer unbounded, reader bounded.

Measured (station01, todos b46ca2a3)

Two-sided control, only variable = presence of the oversized file:

clean scratch home,  same cmd  -> rc=0  "Dry run only. No files were written."
same home + 273,860B AGENTS.md -> rc=1  PROJECT_CONTEXT_INPUT_TOO_LARGE: managed input exceeds 262144 bytes

Same real file, only the binary differs:

installed 0.4.19   session plan  rc=1  PROJECT_CONTEXT_INPUT_TOO_LARGE: managed input exceeds 262144 bytes
this build         session plan  rc=0

Full write-then-read cycle, clean home, 42 configs:

this build  session apply  rc=0   wrote 355,607 bytes   (cap 262,144)
0.4.19      session plan   rc=1   PROJECT_CONTEXT_INPUT_TOO_LARGE
this build  session plan   rc=0
this build  session apply  rc=0   355,607 bytes, idempotent

Live impact: ~/.codex/AGENTS.md is 273,860 B and its home has not re-rendered since 14:47:43Z — 4 sources behind.

Why raise the bound rather than shrink the corpus

The cap is not a content-size control, and never was. The same corpus already ships uncapped to claude homes, because adapterMode — not corpus size — decides whether the cap is reachable:

home adapterMode sources rendered state
claude native-imports 44 20:39:55Z OK — 291,752 B split across 44 files, largest 26,564 B
codex flattened-markdown 40 14:47:43Z wedged — 273,860 B in one file

Splitting adapters deliver the identical payload at rc=0 with no cap check. So the cap bounds nothing about prompt size; it fires only on adapters that flatten. And the content is not padding — measured redundancy is under 1% (1,246 B duplicated title/Source: preamble across 9 of 41 sources; 180 B cross-source verbatim at 120-char normalized windows).

Whether ~292 KB of instructions should be prepended to every prompt is a real question — it is a corpus-curation decision, tracked separately. Silently freezing two homes is not a way to enforce it, and it enforces nothing while claude still receives the full payload.

The change

Name the two bounds instead of repeating a magic number in three places:

  • FOREIGN_INPUT_MAX_BYTES (256 KiB) — unchanged, keeps guarding input this tool did not author.
  • SESSION_MANAGED_OUTPUT_MAX_BYTES — for files it does author, whose size is set by the corpus it is asked to write. Same value as the existing session-render-manifest.json carve-out, so this follows an in-repo precedent rather than inventing a bound.
  • managedObservationMaxBytes(relativePath) is now the single decision point, used by currentFileHash, anchoredFileObservation, and both planProjectContext / composeProjectContextSessionRender reads of paths.target — which carried the same latent wedge on the project-context path.

No content-parsing read is widened. The project-context fragment keeps PROJECT_CONTEXT_MAX_RENDERED_BYTES (4 KiB) and json records keep their own bound. The widening applies only to reads whose purpose is hashing a file this tool wrote.

Tests

Four tests in project-context.test.ts. Proven to fail without the fix: reintroducing the defect gives 63 pass, 2 fail, and the two failures are exactly the new behavioural tests. With the fix, 65 pass, 0 fail.

every managed session-render output gets the managed bound iterates SESSION_MANAGED_OUTPUT_PATHS, so adding a guard path without adding it to the list fails the suite — that is the class-level protection, not just this instance.

typecheck rc=0.

Pre-existing failures, not from this change

Full suite: 622 pass, 5 fail. All 5 reproduce identically on unmodified ca63d8a (verified by stashing this change and re-running the same two files): src/cli/output.test.ts (3) and src/cli/session.test.ts (2). Untouched here.

Correction to the originating report

The bug report grouped codex and opencode as both wedged. Only codex is. projectContextRuntimeForSessionTool returns null for opencode, so opencode never reaches this guard — measured session plan rc=0 and session apply --dry-run rc=0 against its own 274,060 B AGENTS.md. opencode's staleness has a different cause: renders are launch-triggered per profile via @hasna/accounts configs session apply, and no opencode session has launched since 14:43Z.

Todos: b46ca2a3


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…d the writer can emit

Session render wrote a home's instruction target with no size bound while the
guard that reads it back capped at 256 KiB, so a home whose instruction corpus
grew past the cap wedged permanently and silently: every later plan/apply for
that home threw PROJECT_CONTEXT_INPUT_TOO_LARGE before it could do any work.

observeProjectContextSessionGuard hashes every path in
projectContextSessionGuardPaths, which includes paths.target — the very file
session render writes. currentFileHash carved out session-render-manifest.json
at 8 MiB and left the target at the 256 KiB default, and session-apply.ts and
session-render.ts contain no size guard at all (0 hits for byteLength/maxBytes/
TOO_LARGE), so the writer was unbounded and the reader was not.

Measured on station01 against a real 273,860-byte ~/.codex/AGENTS.md; only the
binary differs:

  installed 0.4.19   session plan  rc=1  PROJECT_CONTEXT_INPUT_TOO_LARGE: managed input exceeds 262144 bytes
  this build         session plan  rc=0

Full write-then-read cycle, clean home, 42 configs: apply writes 355,607 bytes
rc=0; installed 0.4.19 refuses to re-plan rc=1; this build re-plans rc=0 and
re-applies idempotently rc=0.

Name the two bounds instead of repeating a magic number in three places.
FOREIGN_INPUT_MAX_BYTES (256 KiB) keeps guarding input this tool did not author.
SESSION_MANAGED_OUTPUT_MAX_BYTES applies to the files it does author, whose size
is set by the corpus it is asked to write. managedObservationMaxBytes is now the
single decision point, used by currentFileHash, anchoredFileObservation and both
planProjectContext/composeProjectContextSessionRender reads of paths.target —
which had the same latent wedge on the project-context path.

This does not widen any content-parsing read: the project-context fragment keeps
PROJECT_CONTEXT_MAX_RENDERED_BYTES (4 KiB) and json records keep their own bound.

Tests fail without the fix (verified by reintroducing the defect: 2 fail, both
new behavioural tests) and pass with it. The 5 pre-existing failures in
src/cli/output.test.ts and src/cli/session.test.ts reproduce identically on
unmodified ca63d8a and are untouched by this change.

Agent: Silvanus
@andrei-hasna
andrei-hasna merged commit 340aeca into main Aug 7, 2026
3 checks passed
andrei-hasna added a commit that referenced this pull request Aug 7, 2026
chore(release): prepare instructions 0.4.23

Carries 340aeca, which stops the renderer writing an instruction home larger than it will later agree
to read. The codex home has had no instruction update since 2026-08-07T14:47:43Z and cannot be repaired
by any published version, because the wedge is on reading the file it already has.

Diff is package.json 0.4.22 -> 0.4.23 plus a CHANGELOG entry. No logic.

REVIEW DECISION, RECORDED RATHER THAN SILENT. This mechanical release PR was NOT sent to an independent
adversarial reviewer, and that is a decision rather than an omission. The substance it releases was
independently reviewed and returned GO on #69, with the reviewer confirming the classifier's input is
tool-constructed at every call site and the tightened fragment and foreign-input bounds intact. This PR
adds no logic to review. Whether mechanical release PRs need their own review pass is an open fleet
question that nobody has ruled on; a silent skip and a reasoned one look identical afterwards, so this
is the reasoned one.

Release gates exercised on the branch before merge: version 0.4.23 free on the registry;
bun install --frozen-lockfile --minimum-release-age 604800 rc=0 over 158 packages; typecheck rc=0;
build rc=0; PUBLISH_HOLD absent at origin/main, with package.json at 3604 bytes as the positive control.
The lockfile check mattered — bun.lock's root workspace block carries no version key, so the bump
cannot desync the frozen install, and that gate would otherwise have failed after the tag was pushed.

Base resolved from the branch: origin/main 340aeca; merge-tree produced a tree identical to the
reviewed head, so what was verified is what lands.

Agent: Silvanus
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

[REVIEW] GO — #69 @ b2401ae — lens: full-build, reviewer fable-instructions-render- (1 of 1)

Dispatch note: the review brief named hasna-internal/platform#69, which does not exist (API 404; that repo's highest PR is #60). The described scope — @hasna/instructions, PROJECT_CONTEXT_INPUT_TOO_LARGE past the 256KiB read-back cap on flattened codex renders — matches this PR exactly, so the verdict is posted here.

Independently verified, not taken from the PR body:

  • Regression test both sides, run in a scratch clone at head b2401ae: bun test src/lib/project-context.test.ts -t "managed output" -> 5 pass / 0 fail, rc=0. Defect reintroduced (managedObservationMaxBytes restored to the manifest-only carve-out): rc=1, 2 fail, literal failure line Expected: 8388608 / Received: 262144. The test can fail, and fails on exactly the fixed behaviour.
  • Guard-path coverage read from source at head: projectContextSessionGuardPaths() = {manifest, cache, fragment, target, sessionManifest, CODEWITH.override.md}. The three paths kept at FOREIGN_INPUT_MAX_BYTES are writer-bounded (fragment capped at 4 KiB by PROJECT_CONTEXT_RENDER_TOO_LARGE, bundle input at 8 KiB), so no currently-reachable guard path can wedge.
  • Gates at head: build (ubuntu-latest) SUCCESS, build (macos-latest) SUCCESS.
  • Shipped and live: 0.4.23 published 2026-08-07T22:18:16Z (SLSA attestation present; 0.4.22's absent, 0.4.21's present as positive control per task b46ca2a3). Installed here is 0.4.26 and the installed dist greps positive for SESSION_MANAGED_OUTPUT_MAX_BYTES. Live-path evidence on station01 today: ~/.codex/AGENTS.md is 355,994 B — past the old 262,144 cap — with a fresh render mtime 2026-08-09 04:03, so the previously wedged home renders repeatedly at the new bound. Post-fix full render (task b46ca2a3, 2026-08-07T22:55Z) took codex 40 -> 44 sources, per-home distribution 44x44 / 45x20, zero homes lost a source — 0 stranded on station01.
  • No credential content in the diff; the release path is OIDC trusted publishing, no ambient token fallback introduced. No doc/AGENTS.md contract divergence (grep for 262144|256 KiB|TOO_LARGE over AGENTS.md/docs/README: rc=1, no matches; same pattern hits src, so the probe can fire).

Non-blocking findings (P2/P3, follow-ups not gates):

  • P2: the PR body's class-level-protection claim is one-directional. The test asserts every entry of SESSION_MANAGED_OUTPUT_PATHS gets the managed bound; it does NOT assert projectContextSessionGuardPaths() is a subset of that list. A guard path added to runtimePaths()/projectContextSessionGuardPaths() without updating the list silently keeps the 256 KiB foreign bound — only a source comment guards the drift. Follow-up: derive the list from the guard-path function or add a set-comparison test.
  • P3: SESSION_MANAGED_OUTPUT_MAX_BYTES (8 MiB) is still a finite bound on a writer described as unbounded; a corpus past 8 MiB recreates the wedge. Headroom today is ~23x; note only.

Not checked: other machines' installed versions and render state (station01 only); opencode's staleness cause beyond the PR body's own correction; the 5 pre-existing suite failures on ca63d8a (out of scope, untouched files).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant