Repository navigation
fix(agent): bound local_loop config and stop returning dead stack storage - #1046
Closed
vernonstinebaker wants to merge 2 commits into
Closed
vernonstinebaker wants to merge 2 commits into
vernonstinebaker wants to merge 2 commits into
Conversation
Add cache-friendly prompt prefix/tail split, tool-result compression, identical-call loop guard, parallel read-only batch execution, and agent.local_loop config defaults so local models survive longer loops without changing the cloud provider path. Co-authored-by: Cursor <[email protected]>
…rage Addresses the boundary findings on nullclaw#987. Third and last PR splitting that change. ## Slice into a dead stack frame `stackLowerAscii` lowercased into a local `[256]u8` array and returned a slice of it; `extractErrorSignature` then searched that invalid slice. Deleted the helper and scan case-insensitively in place, so no stack storage escapes. Detection behaviour is unchanged. ## Truncation marker exceeded the budget `max_result_chars: 1` returned the whole 16-byte "\n… [truncated]" marker, because the code built `body_len = cap -| suffix.len` and emitted the suffix even when `body_len` was 0. When the budget cannot hold the marker the output is now a literal truncation with no marker, so the cap is never exceeded. ## Explicit max_result_chars was indistinguishable from unset The field defaulted to 8192 -- the same value the documentation's example uses -- so setting it explicitly to 8192 could not be told apart from leaving it alone, and the 400-character tightening cap silently replaced it. That contradicts the documented promise that an explicit `max_result_chars` is honoured. `0` is now the "unset" sentinel, resolved per mode in `toolResultCompressOptions`: the tightening cap applies only when the feature is on, otherwise the general default does. Resolving it there rather than in the struct default keeps the sentinel correct on its own, independent of whether history compression is separately gated on `enabled`. ## Unchecked casts All six numeric `local_loop` fields were `@intCast`ed unchecked, so a positive value wider than u32 trapped in a safety build instead of reporting a configuration error. Each now returns `error.InvalidLocalLoopConfig`. ## Tests - budget of 0..16 bytes never produces output longer than the budget - error-signature detection is case-insensitive and mixed-case still matches - all six fields reject a value wider than u32; in-range values still parse - an explicit 8192 is honoured when the feature is on, and an unset value applies the 400 cap Note: a literal beyond i64 (e.g. 1e30) is dropped by the JSON tokenizer rather than reaching the bounds check, so the tests use 5000000000 -- wider than u32, still representable as i64. ## Scope Builds on `feat/local-loop-phase0`, where the code this fixes is introduced. Part of the nullclaw#987 split: nullclaw#1044 gating, nullclaw#1045 concurrency, this the boundary fixes. nullclaw#987 closes with a finding-to-PR mapping once all three are open.
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Third and last PR splitting #987. Addresses the boundary findings. #1044 was the gating fix, #1045 the concurrency fix.
1. Slice into a dead stack frame
extractErrorSignaturethen ranindexOfover that invalid slice. The helper is deleted and the scan is case-insensitive in place, so nothing escapes the frame. Detection behaviour is unchanged — there's a mixed-case test to prove it.2. Truncation marker exceeded the budget
max_result_chars: 1returned the whole 16-byte"\n… [truncated]": the code computedbody_len = cap -| suffix.len, got 0, and emitted the suffix anyway. When the budget can't hold the marker, output is now a literal truncation with no marker — the cap is never exceeded. Tested across budgets 0–16.3. Explicit
max_result_charswas indistinguishable from unsetThe field defaulted to 8192 — the same value the documentation's own example uses. So setting it explicitly to 8192 could not be distinguished from omitting it, and the 400-character tightening cap silently replaced it. That directly contradicts:
0is now the "unset" sentinel, resolved per mode intoolResultCompressOptions: the tightening cap applies when the feature is on, the general default otherwise. I resolved it there rather than in the struct default deliberately — that keeps the sentinel correct on its own, independent of whether compression is separately gated onenabled(which is #1044's change). Stacking that assumption would have made this PR secretly depend on the other.4. Unchecked casts
All six numeric
local_loopfields were@intCasted unchecked, so a positive value wider than u32 trapped in a safety build instead of reporting a configuration error. Each now returnserror.InvalidLocalLoopConfig.One boundary worth recording: a literal beyond i64 (e.g.
1e30) never becomes an integer token, so the JSON parser drops the field rather than the bounds check rejecting it. The tests use5000000000— wider than u32, still representable as i64. Tightening the tokenizer is a separate concern and is not attempted here.Tests
8192is honoured; unset applies the 400 capValidation
zig fmt --check src/exit 0 ·ReleaseSmallexit 0 ·zig build test --summary all13/13 steps, 7400/7409 passed, 9 skipped, 0 failures, 0 leaksmain: clean merge, fmt exit 0,ReleaseSmallexit 0, 13/13 steps, 7518/7527 passed, 9 skipped, 0 failures, 0 leaksDocs
Both languages updated: the example now shows
max_result_chars: 0and the note explains that0means unset and any other value is used as-is.#987 split — complete
enabledactually gates)stackLowerAscii, marker, sentinel, casts)With this open, #987 can be closed with a finding-to-PR mapping and its authorship preserved. All three build on
feat/local-loop-phase0, since that is where the affected code is introduced — none of them have a target onmainuntil the feature branch lands.