Skip to content

Commit f1183ef

Browse files
Your Nameclaude
andcommitted
feat(server): Wave 6 -- source/understand pagination hardening (items a-e)
Audit follow-up (2026-08-23), final wave of the 6-wave plan. - item a: source()'s edit_symbol suggestion no longer says "no preview needed" when the response was truncated -- the etag/expected_hash is still valid (always hashes the FULL range), but only part of the body was actually seen; the reason text now says so and points at resume_from_line. - item b: understand()'s embedded source_output now honors max_lines/ resume_from_line via the same paginate_range() source() uses, instead of always returning the whole symbol body unpaginated. - item c: max_lines/max_chars <= 0 are now rejected with INVALID_PARAMS at the tool boundary (source() and, via the same check, understand()) instead of silently falling back to "unlimited". - item d: new max_chars param (SourceParams + UnderstandParams) hard-caps rendered text length on top of whatever max_lines already selected -- counts whole lines only, never splits one line's own characters across pages (apply_char_budget/narrow_by_char_budget, shared by source(), source_range(), and understand()). - item e: resuming a paginated source() read with a stale if_none_match (the range changed since that etag was issued) now returns a new RANGE_CHANGED_SINCE_PAGINATION error (needs_verification) instead of silently serving a page sliced against new bytes at old coordinates. The pre-existing not_modified shortcut is now gated on resume_from_line being unset, so it no longer swallows a legitimate page-2+ request that happens to carry a still-valid etag. 10 new/changed tests (5 new: truncated-suggestion wording, max_lines/ max_chars <= 0 rejection, max_chars narrowing, stale-if_none_match rejection, understand() pagination; plus SourceParams/UnderstandParams literals updated at every existing call site for the 4 new fields). 1291 calm-core + 465 calm-server tests green, clippy/fmt clean. Dogfooding note: hit a reproducible false-positive PARSE_ERROR from edit_lines/edit_symbol specific to re-editing understand()'s tail a second time this session, on trivially-valid Rust (confirmed via direct cargo check + isolated sub-edits). Worked around via one native Edit for that single hunk, with explicit user sign-off; everything else in this wave went through CALM's own edit tools as usual. Worth a root-cause pass on CALM's own edit-validation path separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 5d9b16b commit f1183ef

5 files changed

Lines changed: 619 additions & 8 deletions

File tree

crates/calm-server/src/__toolsnaps__/source.snap

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@
7373
"null"
7474
],
7575
"format": "int64"
76+
},
77+
"max_chars": {
78+
"description": "Wave 6 (item d): hard character cap on the rendered `source` text,\napplied on top of whatever `max_lines` already selected (never\nwidens a page, only narrows it further) -- counts whole lines only,\nnever splitting a single line's own characters across pages. Known,\naccepted limitation: a single line alone longer than `max_chars` is\nstill returned whole (can't be sub-divided by byte offset), and\n`next_cursor` on the following page points straight back to that\nsame line rather than skipping past it.",
79+
"type": [
80+
"integer",
81+
"null"
82+
],
83+
"format": "int64"
7684
}
7785
},
7886
"type": "object"

crates/calm-server/src/__toolsnaps__/understand.snap

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,30 @@
1818
"string",
1919
"null"
2020
]
21+
},
22+
"max_lines": {
23+
"description": "Wave 6 (item b, \"response budget for understand\"): same meaning as\n`SourceParams::max_lines` -- caps how many lines of the embedded\n`source.source` come back in one call. `None` (default) is today's\nunlimited behavior. When set and the resolved symbol has more lines\nthan this, `source.truncated`/`source.next_cursor` are populated\nthe same way `source()` itself reports them (pass `next_cursor`\nback as `resume_from_line` to continue).",
24+
"type": [
25+
"integer",
26+
"null"
27+
],
28+
"format": "int64"
29+
},
30+
"resume_from_line": {
31+
"description": "Wave 6 (item b): same meaning as `SourceParams::resume_from_line`\n-- 1-indexed absolute line to resume reading the embedded source\nfrom, pairing with a prior response's `source.next_cursor`.",
32+
"type": [
33+
"integer",
34+
"null"
35+
],
36+
"format": "int64"
37+
},
38+
"max_chars": {
39+
"description": "Wave 6 (item b/d): same meaning as `SourceParams::max_chars` --\nhard character cap on the embedded `source.source` text, applied\non top of whatever `max_lines` already selected.",
40+
"type": [
41+
"integer",
42+
"null"
43+
],
44+
"format": "int64"
2145
}
2246
}
2347
},

0 commit comments

Comments
 (0)