Repository navigation
CRW-852: carry a pair's autoCompactTokenLimit as thread config - #814
Conversation
An execution-policy pair, and the legacy single model/reasoningEffort declaration that is the same pair written another way, may now carry an optional positive integer autoCompactTokenLimit. When the authorized pair holds it, settings.Contract.Config() sends config.model_auto_compact_token_limit, so create_thread, create_worktree_thread, send_message_to_thread's resume and the relay's managed start (which creates through bridge.CreateThread) all carry it. An IF DeepSeek child then compacts below the provider's real input ceiling instead of stopping without a receipt above about 664k tokens. A value that is not a positive integer is refused while the policy is read, through the existing PolicyError path: no new refusal name. A policy that declares no limit is unchanged, and the field is absent from the policy description, which is the authorization surface. The host has no field that reports the value back, so the resume observation never compares it: its silence withholds nothing and fails no receipt. The receipt records it under requested and unobservable, and never under verified. docs/role-execution-policy.md and the crw-run bridge reference state the field, the recommended 550000 and the probe behind it.
|
You have reached your Codex usage limits for security reviews. Please try again later. |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 46b0c908d1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
… and resume The execution policy's pair may declare an optional positive integer autoCompactTokenLimit. The bridge sends it as config.model_auto_compact_token_limit on thread/start and thread/resume, and the relay resolves it from the policy for every resume it makes from a task's record, including a settings-free resume. A host whose reported window is larger than what the provider behind a model accepts then compacts before the provider cuts the stream. A value that is not a positive integer is refused while the policy is read, through the existing policy error path; absent means today's behaviour and no new refusal name appears. The host never reports the value back, so the resume observation never compares it, its silence withholds nothing, and the receipt records it as requested and unobservable. Fixes the red Devin finding (relay resumes dropped the limit) and the Codex P2 (the crw-run bridge reference now states the recommended value and its evidence).
CI on head
|
| Job | Result |
|---|---|
| validate | pass |
| secrets | pass |
| go-product (lint) | pass |
| go-product (test-1) | pass |
| go-product (test-2) | pass |
| go-product (test-3) | pass |
| go-product (test-4) | pass |
| go-product (test-rest) | pass |
| go-product (dist) | pass |
| gui | pass |
| skill-scripts-node | pass |
| dev-gate | pass |
The test-* and test-rest legs report testSkipped: true, which is the light-mode design on pull_request: those legs skip their tests by design and the hosted full suite is the dev-gate path. Locally, go test -count=1 ./internal/bridge/... ./internal/relay/managed/ ./internal/relay/adapter/ is green on this same head.
Reviews. Devin Review completed once on 46b0c908d with one red finding; it is fixed on this head and the thread is resolved with code evidence. The GitHub Codex code review completed once on 46b0c908d with one P2; it is fixed on this head and the thread is resolved. The Codex security review was skipped by its usage limit ("You have reached your Codex usage limits for security reviews"), so it is not awaited.
Base. origin/dev is 9f8eb9bd2, later than this head's base; git merge-tree --write-tree HEAD origin/dev reports no conflict, and the merged tree builds, vets and passes the changed packages' tests.
The audit of this pull request found that a settings-free resume's refusal said "nothing was transmitted" or "only the MCP profile's overrides were transmitted". That resume can now also carry the pair's auto-compaction limit, which is neither a pair nor an MCP override, so the sentence under-reported what went out and sent a reader looking for the wrong cause. verifyResume now names what was actually sent, from a flag the resume sets when it adds the limit. The two wordings that were already there are unchanged, so the captured adapter goldens are unaffected: their policy declares no limit.
plugin.json version regenerated with crw-dev ci plugin --record-version on the merged tree (0.4.0+3c7e25c508a3); no other conflict.
The relay's resume resolved the pair's model_auto_compact_token_limit from the policy and put it in the config, but only set a transient flag on the copied settings: the host never reports the value back, so the stored and replayed receipt lost both it and the fact that it went out. The receipt of every relay resume that sends the limit now records it the way the bridge records a setting the host cannot confirm - the value under requested, the field under unobservable, and nothing under verified - and a resume that carried no limit records no such observation. The reference called 550000 a measured value. The probe measured the mechanism at a limit of 4000; 550000 is the recommended margin below the observed provider limit, and both documents now say so.
…pact-limit # Conflicts: # plugins/crw/.codex-plugin/plugin.json
A record need not cite its own role: the sender's gate resolves the role the task is bound to from the store binding and authorizes the send against that role's pair. Reading the role only from the record's own citedRole found none and sent no auto-compaction limit on a delivery the gate had accepted, which is the case the limit exists to cover. The role the gate confirmed now travels with the send, and the limit is read from it before the record's citation. The registry exposes it from the same read that authorized the send, the two settings loaders report it, the MCP copy keeps it, and the supervisor's settings fence compares it so a re-read that lands on a different binding still cancels the claim.
The policy editor's setRolePairs rebuilds a role's whole pairs list from the model and effort of each pair the change states. It did not know the pair's autoCompactTokenLimit, so a change that retained a limited pair silently published the same pair without its limit, and a legacy single declaration that carried the limit could not be moved to the pairs form at all: the editor left the limit beside the list, where the parser refuses it. The limit now travels with the pair. A retained pair keeps the limit the document already declared for its model and effort, a legacy declaration's limit follows its one pair into the list, and the read projection shows the limit on the pair that declares it. A pair without a limit is still written without the key.
The policy editor's pair type collapsed a supplied autoCompactTokenLimit into an int64, so a request that stated 0 or null was read as "no limit": the check approved a document whose pair silently kept an older limit, or dropped the invalid value. A fraction or a string failed the decode instead, which the API answers as a bad request rather than as the refused change the rule is. The pair now keeps the value exactly as the request spelled it, and the editor writes that spelling into the candidate document, where the same parser rule the policy file is held to refuses it. A pair that declares no limit is still written without the key. Both documents now also state the issue's measured application restriction: a resume does not install the value on a thread the host already holds; it applies at creation or on the next load, so an accepted resume receipt records that the limit was sent, not that it applies.
crw manage child-resume resumes a recorded child from the relay's settings-show answer, and that record cannot hold the auto-compaction limit (the host never reports it back). The command therefore reloaded a capped child with the host's own threshold and reported success, which is the failure the limit exists to prevent. The resume now resolves the limit from the execution policy by the pair the record states, using the role the relay's gate confirmed the task is bound to (falling back to the record's own citation), and puts it in the thread/resume config. A record that cites an exception, a role the policy does not declare, or a pair that declares no limit sends no key, exactly as the relay's own resume does. A configured policy that cannot be read is a refusal rather than a silent omission. Both documents now say which read wins: the bound role the gate confirmed, with the record's citation as the fallback.
crw manage child-resume resolved the pair's auto-compaction limit from
manage.bridge.execution_policy, but the gate that authorized the record is the
settings-show subprocess, which reads CODEX_THREAD_BRIDGE_EXECUTION_POLICY from
the environment it inherits. Under the supported environment-only configuration
the gate accepted a capped pair and the command then sent no limit, so a capped
child was reloaded under the host's own threshold while the command reported
success. The environment value is now authoritative, with the configuration
value as the fallback.
The report also says what went out: the value the resume sent, and that the host
cannot be asked about it. The host never reports the limit back, so without that
an operator cannot tell a capped reload from an uncapped one.
The execution-policy guide no longer tells operators that a pairs entry must be
exactly {model, reasoningEffort}, which the new field had made wrong.
plugin.json version regenerated with crw-dev ci plugin --record-version on the merged tree (0.4.0+c1e4515b60ba); no other conflict.
What this changes
config.model_auto_compact_token_limitnow reaches the host on the paths this issue names - thebridge's
thread/start(create_thread,create_worktree_thread) andthread/resume(
send_message_to_thread), the relay's managed start, and the relay's delivery and correctionresume - taken from the execution policy's role pair. It is not carried by every resume in the
repository:
crw manage child-resumebuilds its own config and is listed under limitations below.Problem. An
inferhub/deepseek-v4.1-flashchild whose input passes about 664,000 tokens is cutby the provider (
Provider stream error: upstream error) while the host reports a 950,000 window,so Codex never compacts first and the child stops without a receipt.
Size. 26 files: implementation 339 added and 36 removed lines across 12 files (the issue's
estimate was 80 to 200), tests 840 added lines, 2 documents and the re-recorded plugin version.
The estimate is exceeded because the limit has to be carried on four separate send paths (the
bridge's create and resume, the relay's managed start, and the relay's delivery and correction
resume) and each path is pinned by its own fakehost test; the two correction rounds added the
receipt record and the bound-role read.
Behaviour after this change. A role pair - the single
model/reasoningEffortform, or oneentry of
pairs- may declareautoCompactTokenLimit, a positive integer. When the pair holds it:create_thread,create_worktree_threadand the relay's managed start (which creates throughbridge.CreateThread) send it onthread/start;send_message_to_threadsends it onthread/resume;settings, which come from the host's creation response and so can never hold this value, so the
limit is resolved from the policy by the pair the record states and added to the resume config -
on the settings-free route as well, which transmits no pair but may still carry the limit of the
pair the record says the thread is on. A record on a pair that declares no limit sends none rather
than inheriting a sibling pair's, and a record citing an exception sends none.
A policy that declares no limit sends no key and behaves exactly as before. A value that is not a
positive integer - a string, a boolean,
0, a negative, a fraction, an exponent,null,NaN,Infinity, an integer past int64 - is refused while the policy is read, through the existingexecution_policy_unreadablepath; no new refusal name appears, and the refusalallowedlists andthe
get_capabilitiesdescription are unchanged (that description is the authorization surface, asit is for MCP profiles).
The host has no field that reports the value back, so the resume observation does not compare it. Its
silence withholds no message and fails no creation. The receipt records it under
requested(withthe value that was sent) and
unobservable, and never underverified;verificationstaysobserved_at_creationorobserved_at_resumefor the settings that were actually compared, andnot_requestedwhen the limit was the only thing asked for. A relay resume records the sameobservation on the receipt it keeps for its own send, because the host never reports the value back
and the stored receipt would otherwise lose it; a resume that carried no limit records no such
observation.
Acceptance criteria
and carried by
settings.Contract.Config()onthread/startandthread/resumeincluding therelay's correction delivery; the value is not compared in the resume observation and is recorded as
transmitted and unobservable; no new refusal name; the policy file itself is not edited;
docs/role-execution-policy.mdand the crw-run bridge reference state the field, the recommended550000 and its evidence.
gofmt,go vet(default andGOOS=darwin),make lint,git diff --check,the plugin checks and the hosted CI are below.
dev; the CI results for this head are in a comment on this PR.Verification
Red first, on the baseline commit
efe61652fb90e942f609f40f9e0110ab125c3880, with the new testfiles copied onto it:
and a behaviour-level red that compiles on the baseline:
Green on the head of this PR:
The head was also built and vetted on a temporary merge with
origin/dev(a5e850460, later thanthis branch's base):
git merge-tree --write-tree HEAD origin/devexits 0 with no conflict, and themerged tree builds, vets and passes
go test -count=1 ./internal/bridge/... ./internal/relay/adapter/.Review findings
internal/bridge/create.go): relay resumes lose the limit. Correct, and thecriteria name the relay correction delivery, so it is fixed rather than deferred:
internal/relay/adapter/autocompact.goresolves the limit from the policy by the pair the recordstates and
guardedSendadds it to the resume config on both routes. Tests:TestARelayDeliveryResumeCarriesTheRecordedPairsAutoCompactLimit(transmitted and settings-free),TestARelayDeliveryOnAPairWithoutALimitSendsNone,TestARelayDeliveryCitingAnExceptionSendsNoLimit.the reference now names 550000 and the probe behind it.
"nothing was transmitted" although that resume can now also carry the limit, so the refusal now
names what went out, pinned by
TestASettingsFreeRefusalNamesTheLimitItSent. The rest arerecorded as limitations below rather than changed.
Contract and golden impact
None. No contract file, no refusal reason, no exit code, no CLI and no golden changed, so
crw-dev ci contractswas not needed. Thesettingsreceipt gains one entry inrequestedandone in
unobservablewhen, and only when, a pair declares the limit.Correction generation 2 (the parent's pre-merge evaluation)
The parent's evaluation of head
e1a06e2cfound one P1 and three P2 defects, all introduced by thischange. All four are fixed here.
policy and put it in the resume config, but recorded only a transient flag on the copied settings,
so the stored and replayed receipt carried neither the value nor the fact that the host does not
report it. The receipt of every relay resume that sends the limit now records it in the same
notation the bridge uses: the value under
requested, the field underunobservable,not_requested, and nothing underverified; a resume that carried no limit records no suchobservation. Red first on the previous head:
Green here, on both the pair route and the settings-free route, including the stored receipt read
back through
GetOperationand a replayed send. A send whose pair declares no limit records nosettings observation at all (
TestARelayResumeWithoutALimitRecordsNoSettingsObservation).internal/relay/adapter/settings.goiswhere the refusal wording this change alters actually lives; it is now named with its reason in
"Files edited outside this node's declared edit regions" above.
at a limit of 4000; 550000 is the recommended margin below the observed provider limit, not a
measured threshold.
plugins/crw/skills/crw-run/references/bridge.mdanddocs/role-execution-policy.mdnow say exactly that, and the bridge reference describes thereceipt record as built after d1 rather than promising one the code did not make.
thread/startandthread/resumethis repository makes carries the limit;crw manage child-resumedoes not. Thesentence now names the paths the issue names, and the gap is a limitation below.
Correction generation 2, second pass (the evaluation of head
314149db)The evaluation of this generation's first head found one P1 and two P2 defects, all introduced by
this change. They are fixed here.
citedRole. A record need not cite its ownrole: the sender's gate resolves the role the task is bound to (from the store binding) and
authorizes the send against that role's pair. Reading the role from the record alone found none and
sent no limit on a delivery the gate had accepted, which is exactly the case the limit exists to
cover. The role the gate confirmed now travels with the send as
delivery.TaskSettings.BoundRoleand
autoCompactLimitreads it before the record's citation. Red first:Green here on both routes, with
TestTheDefaultRoleGateReportsTheBoundRoleItAuthorizedAgainstpinning the gate half (the role comes from the same read that authorized the send, and a task
bound to no role reports none).
internal/relay/adapter/json.go(the newsettingskey's place in the stored receipt's key order) andinternal/relay/adapter/settings.go(the refusal wording) are now named with their reasons, and the flag name the list quoted is the
field the code actually has.
restated in the limitations below.
The independent reviewer (GLM 5.3, high, a fresh context) read the whole diff of head
314149dbagainst its merge base with
devand returned PASS WITH NOTES. Its one substantive note - a boundrole whose record cites none sends no limit - is the P1 above, which this push fixes. Its other
notes are the PR-body accuracy items also fixed here; it recorded no P0, P1, red or security
finding against the reviewed head.
Correction generation 2, third pass (the evaluation of head
e9271aa70)The evaluation of the second pass's head found one P1, introduced by this change: the policy
editor could not carry the new field.
setRolePairssilently dropped a pair's limit. The editor rebuilds a role's whole pairslist from the model and effort of each pair a change states, so a change that retained a limited
pair published the same pair without its limit, and validation accepted it. A legacy single
declaration that carried the limit could not be moved to the pairs form at all: the editor left
the limit beside the list, where the parser refuses it. Red first, on head
e9271aa70:Green here: the limit travels with the pair (
Pair.AutoCompactTokenLimit, written only when thepair declares one), a retained pair keeps the limit the document already declared for its model
and effort, a legacy declaration's limit follows its one pair into the list, and
Readprojectsthe limit onto the pair that declares it. Tests:
TestSetRolePairsKeepsTheLimitOfAPairItRetains,TestSetRolePairsMovesALegacyLimitIntoItsPair,TestReadingShowsTheLimitOnThePairThatDeclaresIt.Correction generation 2, fifth pass (the evaluation of head
569d5180)The evaluation of the fourth pass's head found one more defect introduced by this change.
crw manage child-resumereloaded a capped child without the limit. That commandresumes a recorded child from the relay's
settings-showanswer, and that record cannot holdthis value either (the host never reports it back). It therefore reloaded a capped child under
the host's own threshold and reported success, which is exactly the failure the limit exists to
prevent. It now resolves the limit from the execution policy by the pair the record states,
using the role the relay's gate confirmed the task is bound to (falling back to the record's own
citation), and puts it in the
thread/resumeconfig. A record that cites an exception, a rolethe policy does not declare, or a pair that declares no limit sends no key, as the relay's own
resume does; a configured policy that cannot be read is a refusal rather than a silent omission.
Red first on head
569d5180, green here, pinned byTestResumeCarriesTheRecordedPairsAutoCompactLimitandTestResumeWithoutAPolicyLimitSendsNoKey.The independent reviewer (GLM 5.3, high, a fresh context) reviewed head
e9271aa70(the bound-rolefix) and returned PASS WITH NOTES with no P0, P1, red or security finding; its one substantive
note, that both documents should say which role read wins, is folded into this push.
Correction generation 2, fourth pass (the evaluation of head
4a9a7b73)The evaluation of the third pass's head found two more defects introduced by this change.
0ornull. The editor's pair type collapsedthe supplied value into an
int64, so0andnullwere read as "no limit": the check approveda document whose pair silently kept an older limit, or dropped the invalid value, and a fraction
or a string failed the decode (answered as a bad request rather than the refused change the rule
is). The pair now keeps the value exactly as the request spelled it and writes that spelling into
the candidate document, where the same parser rule the policy file is held to refuses it. Red
first on head
4a9a7b73, green here, withTestSetRolePairsRefusesASuppliedLimitThatIsNotAPositiveIntegerover0,-1,null, afraction and a string.
observation says a resume to a thread the host already holds answers with the thread's current
state, so the value applies at creation or when the host loads the thread again. Both
docs/role-execution-policy.mdand the crw-run bridge reference now say so explicitly: anaccepted resume receipt records that the limit was sent, not that it applies.
Files edited outside this node's declared edit regions
internal/bridge/worktree.go-create_worktree_threadis a path criterion c1 names; both ofits
settings.Contractliterals (the launch and the placement observation) now carry theauthorized limit.
internal/relay/adapter/autocompact.go(new) andinternal/relay/adapter/transport.go- therelay's delivery and correction send builds its resume parameters from the task record, which
cannot hold this value, so the limit is resolved from the policy there and added to the resume
config. This is criterion c1's "relay correction delivery" path.
internal/relay/adapter/autocompact_test.go(new) - the fakehost relay managed-start test and therelay-delivery tests.
internal/relay/managedcannot importinternal/relay/adapter(thatpackage imports it), so a managed start over the production bridge and adapter can only be
exercised from
internal/relay/adapter;internal/relay/managed/autocompact_test.gothereforedoes not exist.
internal/relay/delivery/settings.go- the settings-free refusal's wording fix above. The taskrecord gained two fields the send sets:
AutoCompactTokenLimit, the value the resume put in theconfig (the value is not in the record and neither
verifyResumenor the receipt can infer it),and
BoundRole, the role the sender's gate confirmed the task is bound to (see the correctionsection below). The two
wordings that were already there are unchanged, so the captured adapter goldens are unaffected
(their policy declares no limit).
internal/relay/adapter/settings.go- the file that actually carries those refusal sentences.Two of them now name the pair's auto-compaction limit among what a settings-free resume sent,
which is the wording fix described above; the switch it reads moved from a boolean flag to the
limit value the send resolved.
internal/relay/adapter/json.go- the guarded send's receipt now carries asettingskey, andreceiptObjectis the list that fixes the key order every stored receipt is serialized in, sothe new key is named there (before
settingsFindings, which is where the send writes them).internal/relay/adapter/mcp_profile.go,internal/relay/adapter/daemon.go,internal/relay/adapter/cli.go,internal/relay/registry/record.goandinternal/relay/supervisor/proposal.go- the bound-role plumbing of the correction sectionbelow: the role the gate confirmed travels with the send (the MCP copy keeps it, the two settings
loaders report it, the registry exposes it from the same read that authorized the send, and the
supervisor's settings-fence comparison includes it so a re-read that lands on a different binding
still cancels the claim).
internal/policystore/policy.goandinternal/policystore/check.go- the policy editor has toknow the new field or it silently drops it.
setRolePairsrebuilds a role's whole pairs list, sothe pair type and its wire form gained the limit, the read projection shows it on the pair that
declares it, and a retained pair keeps the limit the document already declared for its model and
effort (a legacy single declaration's limit follows its one pair into the list). See the third
correction section below.
internal/manage/childresume.go-crw manage child-resumeresumes a recorded child from therelay's
settings-showanswer, which cannot hold the limit either, so it resolves the limit thesame way the relay's own resume does. See the fifth correction section below.
Contract line citations (D-19)
Re-checked against
devatefe61652. The parent decision comments name no file:line this changetouches, and the contract sections this issue consumes are the issue body's own sections, which carry
no line citations of their own. The one source citation the issue makes -
create.go 137,settings.Contract.Config- still holds: thethread/startconfig is built fromsettings.Contract.Config()ininternal/bridge/create.go. No drift found.Limitations and follow-ups (not done here)
internal/manage/childresume.gobuilds athread/resumeconfig directly and does not carry thelimit. That path is outside this issue's four transmit paths (bridge create/resume, relay managed
start, relay delivery/correction); whether it should carry the value is a decision for the issue
that owns it.
resume sends no limit and says nothing about it on the receipt. That follows the documented
no-inheritance rule (a pair that declares no limit never inherits a sibling's), but a policy edit
that renames a pair would silently drop the value.
crw manage child-resumebuilds its ownthread/resumeconfig and does not carry it; whether it should is a decision for the issue thatowns that path, not an extension of this one.
autoCompactTokenLimit: 550000onto the pair after install, which is when the value starts to apply.