Skip to content

feat(ai): a repository states its own read window, and GATE-READ-1 says which caller it means - #415

Merged
tpouyer merged 1 commit into
mainfrom
feat/configurable-read-window
Sep 9, 2026
Merged

feat(ai): a repository states its own read window, and GATE-READ-1 says which caller it means#415
tpouyer merged 1 commit into
mainfrom
feat/configurable-read-window

Conversation

@tpouyer

@tpouyer tpouyer commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Closes #414.

MAX_READ_CHARS was 64,000 because I measured this repository. That is a fine way to pick a
default and no way to pick a number nobody else can change.

lockstep.workshop = Workshop(
    max_turns=30,
    max_idle_turns=20,
    max_read_chars=120_000,
)

The value travels the road max_test_runs already walks — InvokePolicy → the binding site →
ToolRunnerImpl — with _window at the end instead of the test counter. All three session
constructors take it, not only the one that executes: a review reads files exactly as an implement
does, and a cap honoured in one and not the other is two answers to one question.

Workshop.max_read_chars is int | None rather than a literal, because lockstep may not import
ai under the layering contract and a copied default is two numbers to keep in step.
InvokePolicy.under resolves the absence.

One number, not two

max_tool_result_chars stops being a field and becomes a property returning the read window. #413
set the two equal so they could not be edited apart; making both settable would put that defect
back in through the front door — a repository raising its window and leaving the result bound at
the shipped default would be halved again, silently.
test_the_result_bound_cannot_fall_below_the_configured_read_window drives that over three
configured sizes rather than over the default alone.

Which caller GATE-READ-1 means

The row has said "the cap is not negotiable by the caller" since #402 without saying which caller,
and the two it could mean are not alike:

  • The model reaches _window through read_file's offset and limit. offset moves the
    window, limit selects lines, and neither is a byte budget. That is the whole of the clause, and
    it is why the value is read from the session the framework built and never from the request.
  • The adopter writing lockstep.py owns the repository, the spend and the context budget the
    cap rations, and already states max_turns, max_tokens and deadline_seconds.

Leaving it ambiguous is how a reader resolves it the wrong way — either weakening the rule that
stops a model widening its own prompt, or abandoning the configuration because "the gate says no".
The row now names the model, and
test_gate_read_1_a_model_cannot_widen_the_window_it_reads_through runs against a non-default
cap over four argument shapes, so it proves the rule rather than the constant.

A contributed policy layer may lower the window and may not raise it — the direction every
ceiling in the stack moves, since a layer narrowing what a model may pull into a prompt is a control
and one widening it is not.

Objectives

O2 — what a person writes by hand is what nobody could discover for them, and the size of their
files is exactly that. O8 — until now the only way to change this number was to edit the
framework. O13 — the bound on what one read pulls into every later turn should be stateable by
the repository paying for it.

Verification

  • make check green from the committed tree: 2874 passed, 6 skipped; ruff format reports no
    rewrites, so what is pushed is what passed.

  • GATE-POLICY-2's field pin in test_core.py is updated rather than worked around: that gate
    requires every ResolvedPolicy field to be composed by under(), and this one is, with
    test_a_policy_layer_may_lower_the_read_window_and_may_not_raise_it driving it through the stack
    instead of trusting the field's presence.

  • Five falsification controls fired and restored from the commit, each asserting its own edit
    landed first:

    • the wire from the policy to the tool runner cut → the Workshop end-to-end test goes red;
    • a policy layer allowed to widen → the monotonicity test goes red;
    • the result bound settable apart from the read window → the invariant test goes red;
    • the whole-file slice back on the module constant → the model-cannot-widen test goes red;
    • the ranged slice back on the module constant → the same test goes red.

    A sixth was written and discarded as a no-op: flipping _window's whole-file condition to
    the module constant left the test green, because the fixture is larger than both values and the
    branch is not taken either way. The two slice controls above replace it. Recording it because a
    control that cannot fire looks exactly like one that did.

…ys which caller it means

`MAX_READ_CHARS` was 64,000 because I measured this repository — 97% of its Python files fit whole.
That is a fine way to pick a default and no way to pick a number nobody else can change. A
repository of short modules pays for a window it never fills, on every read, on every later turn;
one carrying generated clients an order of magnitude larger cannot read its own code and has no way
to say so. `Workshop(max_read_chars=...)` states it, a contributed policy layer may lower it, and
`ls` prints what the stack contributed.

The value travels the road `max_test_runs` already walks — `InvokePolicy` to the binding site to
`ToolRunnerImpl` — with `_window` at the end instead of the test counter. All three session
constructors take it, not only the one that executes: a review reads files exactly as an implement
does, and a cap honoured in one and not the other is two answers to one question.

`max_tool_result_chars` stops being a field and becomes a property returning the read window.
#413 set the two equal so they could not be edited apart, and making both settable would put that
defect back in through the front door: a repository raising its window and leaving the result bound
at the shipped default would be cut in half again, with nothing saying so. One number an adopter
states.

GATE-READ-1 has said "the cap is not negotiable by the caller" for a year without saying which
caller, and the two it could mean are not alike. The caller is the MODEL, which reaches the reader
through `read_file`'s `offset` and `limit` — neither of which is a byte budget — and the row now
says so, because leaving it ambiguous is how a reader resolves it the wrong way: either weakening
the rule that stops a model widening its own prompt, or abandoning the configuration because "the
gate says no". `test_gate_read_1_a_model_cannot_widen_the_window_it_reads_through` now runs against
a non-default cap, so it proves the rule rather than the constant.

Closes #414

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

in-lockstep review — intent

succeeded · $0.0779 · 3 in / 779 out tokens

location finding
⚠️ tests/in_lockstep/test_read_delivery.py:209 test_every_session_type_honours_the_configured_window imports read_only but uses the bare name without importing it from in_lockstep.ai.builtins.
⚠️ src/in_lockstep/ai/invoker.py:130 max_tool_result_chars is now a derived property, but test_lens.py and test_ai.py are not touched — tests there may still set or read it as a field.
⚠️ src/in_lockstep/lockstep.py:84 Workshop.max_read_chars defaults to None, but InvokePolicy.max_read_chars defaults to MAX_READ_CHARS — the resolution path through InvokePolicy.under is the only place this is reconciled.
ℹ️ the lens was given this change's blast radius (6027 chars)

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

in-lockstep review — performance

succeeded · $0.0593 · 3 in / 116 out tokens

No findings.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

in-lockstep review — security

succeeded · $0.0910 · 6 in / 1089 out tokens

location finding
⚠️ src/in_lockstep/ai/invoker.py:157 A workshop-supplied max_read_chars above the framework default passes through with no upper bound when no policy layer contributes a ceiling.
ℹ️ the lens was given this change's blast radius (6027 chars)

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

in-lockstep review — tests

succeeded · $0.0790 · 3 in / 854 out tokens

location finding
⚠️ tests/in_lockstep/test_read_delivery.py:233 Test asserts max_read_chars on the runner but uses object() as the module, which may silently skip policy resolution.
⚠️ tests/in_lockstep/test_read_delivery.py:283 No test for the case where max_read_chars flows through read_write_execute via AiStrategy (the execute path).
⚠️ tests/in_lockstep/test_read_delivery.py:289 test_a_policy_layer_may_lower_the_read_window_and_may_not_raise_it passes max_read_chars=CONFIGURED to InvokePolicy.under but leaves max_turns at 10 — the assertion checks max_read_chars on the returned policy, not on a runner, so the test does not prove the value reaches the tool that rea
ℹ️ the lens was given this change's blast radius (6027 chars)

@tpouyer
tpouyer merged commit 102dee1 into main Sep 9, 2026
4 checks passed
@tpouyer
tpouyer deleted the feat/configurable-read-window branch September 9, 2026 21:12
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.

feat(ai): a repository states its own read window, and GATE-READ-1 says which caller it means

1 participant