Repository navigation
feat(mcp-host): read PDF and XLSX attachments with clerum__document_read (supersedes #1099) - #1424
jozer-rami wants to merge 78 commits into
Conversation
Stack the document reader work on fix/internal-tools-hardening (#931) and bring it up to date with dev so origin/dev is an ancestor of the branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…renceV1 The parser now accepts the closed reader set text|pdf|xlsx|none, derived from the class: pdf takes none|pdf, xlsx takes none|xlsx, the text family takes text and every other class takes none. No schema version bump. This lands before any producer emits the new values (#656), so a parser deployed with this commit accepts references from both older and newer producers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes the interface the reader workstreams code against (#656, #1093): tool input and result shapes, typed reasons and limits, the worker job protocol, page selection and continuation cursors. pdfjs-dist is pinned to 5.7.x because 6.x requires @napi-rs/canvas ^1.0; 5.7.284 shares the canvas 0.1.100 mcp-host already ships (npm ls: deduped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the document reader behind clerum__document_read: - PDF text extraction and visual page rendering with pdfjs-dist, and XLSX row reading with exceljs, all inside a worker thread with heap, time, output and source-byte limits; - a validator at the worker boundary that rejects malformed outcomes as a defect instead of returning them to the model; - typed limit and failure reasons, with continuation tokens for reads that stop at a per-call page, character, row or cell budget; - registration of the tool only when the turn carries a pdf or xlsx attachment, and a pointer from clerum__attachment_read to it; - the attached-files instruction naming the reader for each file. Document content is never logged; worker diagnostics are discarded. Refs #656, #1093 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The document upload lane attaches a PDF and an XLSX through the Desktop composer and checks that the provider fixture receives the reader output from clerum__document_read. The image-capabilities fixture gains the matching scripted tool calls and evidence fields. Refs #656, #1093 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run every document parse in a child process with an empty environment, a heap cap and a resident-memory cap enforced by the Host. The caps follow the Host container: the effective cap is the lower of the configured value and the cgroup v2 headroom, the child is killed when the container nears its limit, and one document job runs at a time. XLSX uncompressed sizes are checked before load, pdfjs runs with image and canvas limits, and the output is bounded inside the child. Review fixes: - visual mode pages with a cursor instead of refusing long selections - embedded images get their own ~40 MP decode cap - no_text_layer tells a model without image input that the file is a scan and needs a vision-capable model - an XLSX formula without a cached value returns the marked formula - PNG dimension respects the delivery profile image-count threshold - document_empty separates empty selections from malformed files - an XLSX requires xl/workbook.xml; hyperlink rich text and invalid dates are handled - the E2E fixture reads the reader from the line tail and keeps the XLSX digest key order of the Host - tests for limits, registry absence, budget fences, evidence contracts and the composer notice Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* origin/dev: docs(control-api): r1-l9 narrow the allowlist-union docstring to its callers test(control-api): r1-l7 declare the grants stand-in and guard its meaning test(control-api): r1-l10 exercise the reported-namespace filter in the gate test(control-api): freeze the clock in the shared rate-limit tests fix(control-api): bound the Forbid skip-streak map and correct its docs test(control-api): r1-l3 seed real membership in real-PG suites test(control-api): r1-l2 make McpServer fixtures CRD-valid test(control-api): r1-l4 cover K8s failure in the user-scope gate fix(control-api): r1-m1 let agent-reached contexts enable per-user connect fix(control-api): r1-m3 share the allowlist union between gate and invoke test(control-api): r1-m4 key membership fixture by user id fix(control-api): allowlist hand-made legacy server in real-PG I11 fix(control-api): gate user-scope MCP OAuth by context allowlist test(control-api): pin skip-streak restart on a foreign schedule slot docs(control-api): correct Forbid worker and CRD wording test(e2e): pin concurrencyPolicy Allow on the schedule cadence E2E chore(workflow-recipes): drop the unused K8s checkConcurrency helper fix(workflow-recipes): sync schedule concurrencyPolicy into workflow_schedules fix(control-api): skip scheduled ticks under Forbid while a run is live fix(control-api): add workflow_schedules.concurrency_policy column # Conflicts: # mcp-host/package-lock.json # mcp-host/package.json
The unsupported-visual and malformed-file cases now also read a valid text PDF through the same tool and context, so a reader that never runs cannot pass them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* origin/dev: (452 commits) docs(storage): document the superseded split database test(e2e): upgrade the T5 Host without a repair step fix(hcc): supersede the stale split database of a Host back on stateful test(e2e): grant the upgrade lane's Hosts again after the head setup test(e2e): repair T5 before the upgrade in the upgrade lane test(e2e): install the v0.11.0 deploy tree in the upgrade lane chore: bump control-api and control-ui versions after merging dev test(e2e): name the suspend-wake stub token as a fixture docs(mcp-host): correct persistence comments and document the sweep window test(e2e): keep the suspend-wake login password out of argv fix(mcp-host): validate the stall timeout default, sync timeout and timer range test(mcp-host): seed sweep fixtures with the producer's end_reason fix(mcp-host): reject a non-positive or non-integer sweep limit fix(mcp-host): await the boot retention sweep before bootstrap test(mcp-host): signal the entrypoint test only after the SIGTERM handler exists fix(mcp-host): index sessions.parent_session_id for the retention sweep test(mcp-host): pin the idle reset and no total cap of the progress bound fix(mcp-host): bound close() when the db worker never opened fix(desktop): settle a superseded rename whose local write fails fix(desktop): keep a local rename over catalog pages requested before it settled ... # Conflicts: # docs/testing/desktop-headful-journeys.md # mcp-host/package-lock.json # mcp-host/package.json # tests/e2e/fixtures/package-lock.json # tests/e2e/fixtures/package.json
On Linux a process releases its address space before it becomes a zombie, and in that window /proc/<pid>/status has no Vm* lines while State is still R. The sampler threw on that status, the runner marked the job as a defect, and finished reads failed with "Document reader worker failed" (three documentRead tests in CI). A status with Name and State but no Vm* lines now reads as 0 bytes. A status with Vm* lines but no VmRSS still throws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… process A spawn that fails before creating a process emits `error` and `close` but never `exit`, so the Host-global reader slot was never released and every later document read waited forever. Release it when the child has no pid; a live child still holds the slot until its exit is observed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dary The outcome validator accepted a success with no pages, no images or no worksheet rows. The producers never emit one, so such a message is now a worker defect, matching what pdfText, pdfRender and xlsxParser guarantee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…available When the Host lacked headroom to start the reader, the tool reported a document limit (`heap`), telling the model the document was the cause. Admission refusal now returns `host_memory_unavailable` with a message that says the document is not the cause and to try again later. The mid-job pressure kill keeps `limit: heap`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A single worksheet row larger than a full page allowance returned the read-budget exhaustion notice on every turn, so a new turn could never make progress. With the turn budget intact it now fails as `document_limits_exceeded` with `limit: row_width`; a turn-budget shortfall keeps the notice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s declared class A password-protected XLSX or DOCX is a CFB container holding the EncryptionInfo and EncryptedPackage streams, so it was classified as binary_unsupported and the Host never reached the reader that reports `document_encrypted`. With an XLSX or DOCX declaration it now takes that class; without one, and for every other CFB file, it stays unsupported. Adds a real msoffcrypto-tool fixture, classifier vectors and a Host test through admission, the registry and clerum__document_read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The classifier's mismatch compares families, and XLSX and DOCX share one, so a workbook named or declared as a Word document produced no mismatch notice on reader failures. The declared-format check now maps .pdf, .xlsx, .docx and their media types and reports any declared format that differs from the detected one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pin pdfjs-dist to exactly 6.4.299; the reader relies on its private transport contract. - Offer visual mode only when the capability has delivery limits, the same predicate readVisual requires. - Merge overlapping redaction ranges as applyRedactionRanges does. - attachment_read pointers report the reference byte length instead of decoding PDF/XLSX bytes in the Host. - Correct the child environment comment: a memory and time boundary, not a security sandbox. - Mark the PDF/XLSX document upload journey as pending on the final head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * origin/dev: (117 commits) fix: use typed RPC denial error codes (R13-7) fix: reject case-variant synthetic bearer exemption (R18-L2) fix: reject malformed retirement error codes (R18-L3) test: require mandatory recovery cases for R18-L1 fix: exempt exact synthetic bearer fixture refactor: extract invitation action presentation fix: distinguish definite reset internal failures fix: preserve password callback diagnostic provenance docs: clarify CodeQL fixture listener scope fix: use typed control API retirement error codes fix: sanitize password mutation internal responses docs: correct CodeQL fixture topology details (R17-L1) test: prove TypeError rollback on PostgreSQL (R17-M1) docs: update CodeQL 1769 test topology (R17-L1) ci: gate recovery browser journeys in required REST check (R17-M2) fix: classify password callback failures safely (R17-M1) fix(profile-ui): route invitation password setup by purpose (r16-h1) fix(e2e): r15-l2 clarify synthetic recovery test values fix(tests): bind route guard to prepared agent for R15-L1 fix(profile-ui): preserve reset error states for R15-M1 ...
T1 runs every control-api *realPostgres*.test.ts file and refuses any pending test. The two Member Registration producer suites added in #1056 need a private producer that T1 does not provide, so they always report 3 skipped tests and T1 stops with pending=3 on every branch that contains #1056. run_suite now reads the private list from scripts/ci/password-recovery-suite-contract.mjs (verify, then private all), the same source the public CI lane uses, and leaves exactly those files out. It fails closed with ZERO_TESTS_EXECUTED when the contract fails, a named file is missing, or the contract does not name exactly 2 suites. The run_suite fixtures in the reporter and T2 contract tests gain a contract stand-in; the reporter test asserts the public suite reaches Vitest and no private suite does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…der child The XLSX reader returned up to 30 MB per call, and the Host measured that outcome synchronously many times, blocking its event loop for seconds. The child now measures each row as JSON.stringify(values).length and stops before the running total would exceed maxExtractedChars (400,000), with stoppedBy 'max_extracted_chars' and a continuation cursor. A first row that alone exceeds the budget fails as document_limits_exceeded with limit 'row_width'. The parent validator accepts the new stop reason and the new limit name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cache memory.current counts reclaimable file cache. On a warm 512 MiB Host the reader was refused with 47 MiB of anonymous memory in use, and the child's heap cap shrank long before that. Admission and the mid-job fence now use current - inactive_file from memory.stat, clamped at zero. A missing, duplicated or malformed inactive_file key fails closed, like the other cgroup files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Linux The resident-memory sampler runs every 25 ms and can lose to the kernel OOM killer, which then kills the Host. memory.oom.group is 0 on Docker and minikube, and oom_score_adj is the only control available without privileges. After spawn and before the job is sent, the runner writes 1000 to /proc/<pid>/oom_score_adj through a platform seam (no-op on macOS, error on unsupported platforms). A failed write takes the worker-defect path, so no job runs unprotected. Tests also pin two existing behaviours: an external SIGKILL resolves as limit 'heap', and the parent output_bytes check rejects an outcome above maxOutputBytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new Desktop built GFS references with reader 'pdf' or 'xlsx'. The parser of an older Host only accepts 'text' and 'none', so those references were rejected when the Desktop was deployed before the Host. buildGfsFileReference now sets reader to 'text' for text-readable files and 'none' otherwise, exactly the rule of the previous parser. The Host only reads reader === 'text' for GFS, and clerum__document_read does not read GFS files, so nothing is lost. Attachment references keep 'pdf' and 'xlsx'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
exceljs 4.4 drops an empty <v> element, so a formula whose cached result is the empty string was read as "[formula] =...", and a column of such formulas below the data extended the sheet's last row. When a selected worksheet has a formula cell without a result, parseXlsx reads that sheet's raw XML (JSZip plus saxes, exceljs's own parser) and collects the refs of t="str" cells with an <f> and an empty <v>. Those cells read as null. A formula with no <v> at all keeps "[formula] =...". A part that cannot be found or parsed returns document_parse_failed. saxes is declared as a direct dependency at the version already in the lock through exceljs; no new package enters the tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the request Visual limits applied per call only. A provider request carries every image of the turn, so several document_read calls could exceed the model's image count, aggregate bytes or the task's resident image memory. - requestImageUsage(messages, toolResults) counts the images already in the request (composer, GFS and tool results), their decoded bytes and their longest side; an image without geometry counts as unbounded. The batch and the approval-resume path pass it to the tool as requestImages, and visual mode throws without it. - readVisual limits pages to the remaining image slots, and bytes to the profile's remaining aggregate and VisualInputBudget.remainingResidentBytes. With nothing left it returns a notice and runs no worker; a call cut short continues with limit 'image_budget'. - On a profile with a many-image dimension threshold (Claude: above 20 images every image must be at most 2000 px), pages always render at the threshold dimension, and a request that already holds a larger image keeps the page count at the threshold. Other profiles keep 2048 px. - Page image ids use the sha256 of the reference id, so they match the Codex image id pattern. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… nits - Cursors end with the first 12 hex characters of the content digest that the reference id already carries. The same bytes reattached in a later message resume; a cursor of another document is invalid_range. - An explicit mode "text" on an XLSX reads like an omitted mode; only mode "visual" is rejected. - A PDF without a text layer gets a neutral message instead of claiming the file is scanned, because a blank PDF has no text layer either. - classifyBytes docstring: the declared type also selects XLSX or DOCX for an encrypted OOXML container. - Remove the dead isEvalSupported option and an unused test import; fix the stale fixture README labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
control-api admits one password verification every 7.5 s across all identities and answers 429 inside that window. Desktop reports that as a generic login failure. The XLSX test signed in 7.0 s after the PDF test and failed at login on dd34dfa (doc-upload r6). signIn now waits until a full pace has passed since the previous password login in the same worker. The scope is this spec only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s test items Each test passes on this head and fails under the mutant named here. - M6b: attachment_read redirects reader=xlsx to document_read without decoding it (mutant: redirect only pdf). - M19: a resumed visual read counts images in completed batch results (mutant: requestImageUsage(snapshot, [])). - M24: text cursors follow a page longer than two page budgets with no gap or repeat (mutant: drop `unit.from +` from the cut). - M28b: a row serializing to exactly maxExtractedChars is returned and one character more is skipped (mutant: `>=` at the final return; the loop-top M28 mutant is equivalent). - R1-H2: the long-scan chain follows real continuations with consecutive pages (mutants: no visual continuation; visual start: null). - zach88 R1-M9: fake runners validate outcomes with the worker's isJobOutcome (now exported); two invalid hand-built outcomes are fixed. - zach88 R2-L4: boundCursor builds cursors with the production encodeCursor. - zach88 R1-L5: zipLimits.test.ts, 45 tests on crafted ZIPs covering every rejection branch and the end-record invariant; 34 of 38 guard deletions fail, the other 4 are implied by earlier checks. Also: the visual-cursor refusal test passes requestImages, which every production visual read carries, after ce5a2c2 moved that check earlier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up to 94c3144 (review M2). When the sheet-name list nearly fills a page, no group of rows can pass the margin, so halving reached every row and measured about 2n probes, each with the whole list: 10,016 measures for 5,000 rows, more than the n of e1fe200. A call can emit at most the rows that fit a page even as `[]`, because the fit allowance never exceeds a page; later rows start a later call, which probes them again. Only those rows are checked now, so the output is unchanged and the work is bounded by the page: 137 measures in that band, and 7 instead of 6 in the common cases (still 73-201 ms for the 5,000-row probes timed in 94c3144). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rker and Tool Review M6 was pinned only through parseXlsx. The window now stops before a gap that does not fit, and the continuation names the next valued row, a shape the runner's isJobOutcome and the Tool's cursor handling had not seen. A 200k-row gap read with a 5-row window takes two calls through the real child and DocumentReadTool: rows [[1]] with a max_rows continuation to row 200001, then rows 200001-200002 with none. Fails with e1fe200's parser. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew-r2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evenfire Reviewer — Round 1Reviewed head: Decision: Blocked The reader design holds up: attachment binding, worker isolation, cursor binding, and the visual-budget pause split are coherent. Merge is blocked because this head does not merge with current Must fix
Verified
Validation gaps
PR descriptionUpdate required. After the rebase, name the new Pre-existingPre-existing: none identified during the scoped review. Next actionRebase onto current Full review archive: — Review performed by Evenfire Reviewer |
Independent review —
|
…ew-r2 Resolves mcp-host/package.json and package-lock.json: dev's version line and openai ^4.87.0, the reader's pdfjs-dist 6.4.299, saxes, exceljs 4.4.0 and @napi-rs/canvas ^1.0.11. The lockfile starts from dev's and keeps the canvas 1.0.11 entries the reader was tested with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
host_memory_unavailable covers refusals that never clear on retry: an ignored RLIMIT_DATA, an uncalibrated architecture and an unreadable cgroup. The message now says this Host cannot start the reader and points at the Host log. A test pins the text (review R1-M1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After an approval, the next request was rebuilt from the frozen messages plus text-only tool messages, so the image parts of the batch's completed results and of the approved call were dropped. A visual document read said the model had its pages while the request carried none, and the image count forwarded to the resumed call described images the request no longer held (review M1). The resume now appends the batch with appendToolResults, the loop's own append, seeded with the turn's attachments from before the batch. Spillover refs are already resolved into the content and stay off the rebuilt messages, as before. Other tools' same-batch images (screenshots) now also reach the resumed request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…release Review M4. Each test fails under the mutant the review found surviving: - the registry gives document_read the redactor that carries the turn's ConfigStore secrets (NR2): a configured secret in the page text is masked in the tool's own result; - document_read charges the same turn ledger as attachment_read (NR1); - the single reader slot is released when a pre-spawn premise throws, on an abort between admission and spawn, and on a synchronous spawn throw such as ENOMEM: each test then runs a real job that must start and settle within a deadline; - the RSS sampling interval is cleared when the job settles. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…smoke
Review M3: no gate ran dist worker.js as a child, so the production chain
(the image's /bin/sh, ulimit -d, exec of the dist entry, env: {}) was
unverified until T2. The image smoke now runs one pdf_text job through the
dist runner in every Host image CI builds, on amd64 and arm64, and prints
the full outcome when it is not ok. CI runs the smoke without --memory, so
it proves the spawn chain and IPC, not headroom admission.
The rendered page must now carry ink, as the comment already claimed.
Failures are written to stderr directly: dist/logger.js replaces
console.error and reduced an error to its name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…get fit Review M2: with a long sheet list, rows that each fit a page alone but not two together were all probed, each probe measuring the whole list. An Excel-legal workbook (1,300 31-character sheet names, 430 rows of 900 characters) took about 870 measures and 5 s of event loop per call to emit one row. The probes now stop once an exact measure shows that the smallest result holding the rows decided so far exceeds the allowance: no result emitting a later row can fit, so later rows and sheets are left to the call that starts at them. Every row a call can emit is decided as before. fit replaced plain bisection with a search aimed where the measured ends cross the allowance, falling back to bisection after two steps that do not halve the bracket, and reuses the chosen candidate's measure. A 400,000- character PDF page takes 6 measures instead of 21. Measures per call: reviewer's shape 874 -> 21, nearly-full sheet list 137 -> 83, 5,000-row window 37 -> 34. Tests assert a worst-case bound in emitted and window rows, and a differential pins 18 PDF and XLSX read chains (sheet boundaries, parser-skipped and too-wide rows, a partly spent turn budget) to the results of the previous implementation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew-r2 Brings in #1434 (lone UTF-16 surrogate repair). The only conflict is the mcp-host version line: dev 0.1.635, branch 0.1.639, merged as 0.1.640. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The public-boundary scan rejected 'const SECRET = ...' in fitScenarios.ts as a credential assignment. The value is synthetic; it now says so, at the same 17 characters, so the pinned chain digests are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent review, round 2:
|
| Finding | Status | Evidence |
|---|---|---|
| M1: visual pages lost after an approval resume | Fixed (057e45cc8) |
With the production hunk reverted, the M19 and M1 tests fail; at HEAD they pass (96/96). An old-vs-new table of the resume (23 rows × legacy/identity modes) shows only one change: other tools' images now reach the resumed request, which the body names as a decision. Adapters without image support withhold them with a notice, as the normal loop does. |
| M2: Host event-loop blocking | Fixed for the reported shapes; one case remains (19ec7babf) |
Reviewer workbook: 874 → 21 measures, about 5.4 s → 0.27 s with a production-shaped measure (BasicSafety preview + cl100k). 400k-char PDF page: 21 → 6 to 9 measures. Remaining: a long sheet list where every row is too wide still costs about 4.2 s per call (703 measures), the same at ee95833cb and HEAD. Blanked rows never feed the new stop. |
| M3: the shipped child chain ran in no gate | Fixed (f34977657) |
The smoke fails loudly on each of: a missing worker, a silent worker, an invalid outcome and a blank page. All 8 Host image CI jobs show the dist child ok with a non-null dataLimitKiB, so the Linux ulimit -d chain ran. |
| M4: security wiring and slot release unpinned | Fixed (581c9279a) |
Each of the 6 mutants fails on its intended assertion, and every test has a liveness witness. |
R1-H1 (Evenfire Reviewer): merge with dev |
Fixed at the time, stale again | Both merge resolutions are clean: only the version lines differ from the automatic merge, no test was lost, and the 12 canvas lock entries are at 1.0.11. dev has moved since (see above). |
| R1-M1 (Evenfire Reviewer): retry promise | Fixed (9c52cc3e0) |
Reverting the string turns the text test red. See N2 below. |
| Earlier Low findings | Mostly not fixed, as the body says | Fixed since then: the "ink check" text, the xlsxRowWidth count and the "in-memory" wording. @napi-rs/canvas is now named in the body. The rest are still present. |
New findings in the delta (none blocking)
- N1 (Low): the aimed
fitsearch can stop short (documentRead/index.ts:1197). The search assumes cost grows with n, but the production BPE measure is not monotonic: cost(1022)=485, cost(1023)=484.- In 11 of 6,565 production-measure calls (PDF pages with secrets and tags), HEAD emits 2-3 fewer characters than
ee95833cb, which matches the brute-force maximum. - In 1 case HEAD emits a fitting emoji where
ee95833cbreturned the exhausted notice. - Nothing is lost, and the allowance is never exceeded.
- The differential's "neither may change what a read returns" holds only under the bytes/4 measure.
- In 11 of 6,565 production-measure calls (PDF pages with secrets and tags), HEAD emits 2-3 fewer characters than
- N2 (Low): the new message is too absolute for
host_headroom.9c52cc3e0's "This Host cannot start the reader" also covers this condition, which is computed per job from the live working set (documentMemory.ts:97-99,158-169). The condition is transient, so a later retry can succeed. - N3 (Low): a repeated image can be sent twice after a cold resume (
taskExecutor.ts). The dedup seedapproval.attachmentsis not persisted to SQLite, while thecompleted_resultsimages are. After a cold restore, a frame that repeats an earlier-turn image is sent again: 2 parts instead of 1 on a hot resume, and as a full part instead ofsourceIdentityOnlyin identity mode. The cold loader was not run end to end. - N4 (Low, coverage): three resume mutations survive. Each passes all six resume suites (145/145), and a probe shows each changes behaviour:
- the dedup seed replaced by
[]; spillover_refkept;- the identity flag forced to false.
- the dedup seed replaced by
- N5 (Low, text): the smoke comment overstates the ink check (
mcp-host/test/image-runtime-smoke.cjs:103-104). The comment says the glyphs come from the shipped font data. With bothstandard_fontspaths in the distpdfLoad.jsbroken, a fallback face still draws ink and the smoke still passes. - N6 (Low, text): a test cites a commit that does not exist (
documentReadFitDifferential.test.ts:9). The cited5af3a16ceis not in the repository or on GitHub. The claim it supports does hold: the differential passes against theindex.tsfrom before19ec7babf.
Pre-existing (not caused by this delta)
- P1 (Medium): a second suspension leaves orphan tool messages (
taskExecutor.ts:910-925). When a second suspension in the same turn carries attachments, the first batch's tool results are re-appended after the new assistant message as orphan tool messages. This is the same atee95833cband HEAD, and the code predates this PR.- The second approval's shape was built from reading the code.
- Provider rejection was not executed.
- P2 (Medium): the M2 remainder above. About 4.2 s per call when every row is too wide, unchanged by the delta.
Verdict
- The four Medium findings and R1-M1 are fixed. M2 has a measured remainder (P2) at the same level as before.
- No new blocking issue was found in the delta.
- Before merge, two things remain:
devmust be merged again (conflicts in the mcp-host package files);- T2 /
make minikube-run-document-uploadhas still not run on this head.
- For the author:
- should
host_headroomkeep a retry hint (N2)? - should P1 and P2 get follow-up issues?
- should
…ew-r2 # Conflicts: # mcp-host/package-lock.json # mcp-host/package.json
…ing causes host_memory_unavailable covers configuration refusals that never clear and host_headroom, which is measured per job and can. The message now says this Host could not start the reader for this read, without promising a retry or claiming the condition is permanent (independent review round 2, N2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k check prove The differential's digests hold under its byte-count measure; the production token measure can dip by one token as a result grows, so the aimed search may settle a few characters away from a bisection, never past the allowance (N1). It now cites f349776, the commit before the M2 change, instead of a commit that does not exist (N6). The image smoke's ink check shows the page was drawn, not which font face drew it (N5). (Independent review round 2.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… spillover refs Three mutations of the approval resume rebuild survived every resume suite: an empty dedup seed, a kept spillover ref, and source identity forced off. A repeated frame is now checked with and without source identity (dropped, or named by reference only), and the rebuilt tool messages must carry no spillover ref, since they become the next suspension's snapshot and a cold start expires an approval whose snapshot names a missing blob (independent review round 2, N4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…suspends again When a resumed turn suspends a second time with attachments, the new approval's completed_results keep the earlier batch (for its attachments) while its context_snapshot already holds that batch's tool messages. The next resume appended them again after the new tool-call message, as tool results answering no open call, which providers reject, and counted their images twice. The rebuild and the forwarded image count now take only the results that answer the snapshot's last tool-call message and are not answered yet. Present on dev before this branch; visual document reads make the attachment case common (independent review round 2, P1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a long sheet list beside rows that are all too wide, every row is emitted as [] and needs its own exact probe, and halving spent about one more measure per row on groups that fail. After four blanked rows in a row, groups are now split without a probe until a row fits again, and blanked rows feed the early stop. Each row is still decided by its own probe, so no result changes: a new differential chain with 300 sheet names and 600 too-wide rows matches f349776, and its measures fall from 1,340 to 516 (turn ledger) and from 1,338 to 709 (fresh ledger) (independent review round 2, P2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The P2 row-probe comment now says results are unchanged under a measure that grows with the result, as the byte-count differential pins, and that the production token measure was not compared. The P1 comment and test no longer claim providers reject an orphan tool result; that was not run. The spillover test says it pins the rebuild's existing behaviour, since the loop's own append keeps the ref. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for round 2. Each finding against the new head (
T2 has still not run on this head. |
Independent review, round 3:
|
| Finding | Status | Evidence |
|---|---|---|
| P1: second suspension | Fixed when tool-call ids are distinct; see A-1 (a7af8232d) |
At 80d71c642, every second-suspension shape sends the first batch's results as orphan tool messages and counts the frame twice. The shapes checked: a mixed batch, cold (SQLite) resumes, connect_required and stopTurn. At HEAD every tool message answers an open call, and the frame is counted once. Reverting either site of the fix turns the P1 test red (352/353). |
| N4: three resume mutants | Fixed (cd4cb5aa6) |
seed-empty fails 2 tests, keep-ref fails 1 and identity-false fails 1. Each failing test has a liveness witness. |
| N3: cold-resume duplicate frame | Not fixed, as stated | Reproduced through a real on-disk SQLite store and the cold loader. In legacy mode a hot resume sends 1 image and a cold resume sends 2. In identity mode the cold resume sends the repeat in full. HEAD behaves the same as 80d71c642. |
N2: host_headroom wording |
Fixed (7b5bffb4e) |
The new text is true for all four refusal causes. Restoring the old string turns the test red. |
N1: aimed fit vs non-monotonic BPE |
Unchanged in code; the text now matches (c90f2fb77) |
A production-measure sweep finds 5 allowances where the result stops 2–3 characters short, at both heads. It never exceeds the allowance. |
| N5: smoke ink comment | Fixed (c90f2fb77) |
With both standard_fonts paths in the dist pdfLoad.js broken, the page still renders ink, which matches the new comment. Checked on macOS only. |
| N6: nonexistent commit cited | Fixed (c90f2fb77) |
f34977657 exists and reproduces all 20 pinned digests. All 42 SHAs cited in the PR body exist. |
| P2: every row too wide | Halved, not removed (9d78c9d7e) |
Round 2's shape: 1,300 sheet names, 430 rows of 900 characters, production measure (BasicSafety preview + cl100k), 8 runs per side in separate processes. 80d71c642: 703 measures, median 4,360 ms per call. HEAD: 374 measures, median 2,219 ms. The shapes fixed in round 2 did not regress: the reviewer workbook takes 21 measures and the 400k-char PDF page 6–10. Reverting the hunk turns the <= 750 test red. |
Behaviour preserved:
documentRead:80d71c642and HEAD return identical results on 652 read chains, under both the byte-count and the production measure. The new dense path ran 1,220 times.- Resume: the only changed verdicts are the P1 rows, plus one row that no producer can create.
- Tests: the earlier tests pass on HEAD. For
documentRead, the only failure is the expected N2 text.
New findings
A-1 (Medium, pre-existing): P1 still occurs with drivers whose tool-call ids restart on every response (taskExecutor.ts:327-340, used at :845-856 and :921).
- The id clash:
- The Gemini driver numbers synthetic ids from
call_0on every response (googleGenerative.ts:160-170). Its comment says they are unique only within one assistant turn. unansweredBatchResultsidentifies the earlier batch by tool-call id. On a second suspension, the earlier batch'scall_0result therefore counts as an answer to the newcall_0.
- The Gemini driver numbers synthetic ids from
- Reproduction: the real driver with a stubbed client. The resumed request carries
functionResponse some_tool "OLD screenshot result"before"NEW approved result".80d71c642is worse: it also leavescall_1as an orphan. - Root cause: the accumulation at
:941-953(ondevbefore this branch) keeps the earlier batch incompleted_results, while the snapshot already holds that batch's messages. A tool-call id cannot identify the batch for this driver. Marking which results belong to the current batch would close it. - Not run: a real Gemini endpoint.
Low (coverage):
- B-1 (
9d78c9d7e,index.ts:1013): no test pins the half "blanked rows feed the early stop". With thedecided(...)call removed, every test passes.- It saves measures only on the turn chain (707 → 516). It saves none on the fresh chain or on the P2 shape (374 either way).
- The likely reason, inferred from the code and not isolated: the floor
smallest(:1043-1062) leaves out theskippedRowsfield that every emitted result carries.
Low (text):
- A-2 (
taskExecutor.ts:908-911): the comment says the resolution drops the spillover ref. The resolution keeps it (:778-783); the explicitspillover_ref: undefinedmap at:921-924drops it. - A-3 (
taskExecutor.ts:320-324,a7af8232d): the comment sayscompleted_resultskeeps the earlier batch. It is kept only when the turn has attachments (:941). - B-2 (
index.ts:994-998): "blanked rows in a row … until a row fits again" does not match the counter. Only a fitting single-row probe resets it (:1015); rows accepted inside a group do not. - B-3 (
index.ts:999-1003): "the byte-count differential pins this" overstates what the differential covers. The pinned scenarios never reach a fitting row in dense mode. The reason given for not comparing the production measure also does not hold: the fixture ids do not prevent the comparison, which ran here on 652 chains. - B-4 (
fitScenarios.ts:256): for the new scenario the page is about 0.56 of a row past the list, not "about 1.5 rows". - B-5 (
documentReadFitDifferential.test.ts:47-48): the "1,340 measures" figure comes from this fixture; round 2 measured 703 on its own workbook.
Info:
- B-6: the fallback face that draws ink comes from the canvas library, not from pdf.js.
- B-7 (
documentReadFitDifferential.test.ts:13-14): one more character can cost up to 2 tokens less, not only 1. - B-8: a few mixed wide-row patterns take up to 4.4% more measures. The net change is about −8%.
Verdict
- The delta contains no blocking issue. N2, N4, N5 and N6 are fixed, and P1 is fixed for providers whose tool-call ids are unique across responses.
- Before merge:
- T2 /
make minikube-run-document-uploadhas still not run on this head. - P2 still costs about 2.2 s of event-loop time per call when every row is too wide.
- T2 /
- For the author:
- Fix A-1 here, or in the separate PR you offered for P1?
- Is the P2 remainder accepted, or does it get a follow-up issue?
A resumed turn that suspends again keeps the earlier batch in the new approval's completed_results for its attachments, while the new snapshot already holds that batch's tool messages. P1 told them apart by tool-call id, but the Gemini driver restarts ids at call_0 on every response, so the earlier batch's call_0 result was appended again as an answer to the new call_0 and its frame counted twice. Carried results are now marked answeredInSnapshot when they are carried, the resume skips them, and the GFS projection keeps the mark through the SQLite row. The id filter stays for approvals saved before the mark. Also corrects two comments on the same lines: the resolver keeps the spillover ref (the map drops it), and the earlier batch is carried only when the turn has attachments (independent review round 3, A-1, A-2, A-3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for round 3. A-1 is fixed here, in
On your questions: A-1 is fixed in this PR, next to P1. The P2 remainder (your 2,219 ms median per call) is listed under Known limits; whether it is accepted or gets a follow-up issue is still open. T2 has still not run on this head. |
clerum__document_read admits a reader only when the Host container's memory.max leaves the Host's working set plus 433 MiB of reserves plus at least 128 MiB (documentRead/documentMemory.ts). Idle Hosts on the dev cluster have a working set of 135-165 MiB, so at the 512Mi default the admission arithmetic refuses every read as host_memory_unavailable on a default OSS or minikube install. The T2 document-upload lane, which sets no Host memory, is expected to fail the same way; it has not run. The default limit is now 2Gi, the value hosted Hosts already run with. The request stays 128Mi, so scheduling is unchanged. On upgrade, HCC's resource drift check rolls each Host Deployment once. Operators can still set CONTEXT_MAPPER_HOST_RESOURCES_LIMIT_MEMORY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view Each test failed under the mutant that passed every test before: - An approved result whose id recurs in the next batch (Gemini restarts ids per response) is not sent again. Mutant: the approved result left unmarked when carried (540/540 resume tests passed under it). - P1 also runs with the marks removed, as on an approval saved by the build before the mark. Mutant: the id check dropped. - A page that ends inside the first sheet probes no later sheet: 26 measures, 352 with the early stop disabled, and the same result. - A PDF text cut backs off a surrogate pair, so the continuation starts on a whole code point. Mutant: the back-off removed, which leaves a cursor inside the emoji that the next call rejects as invalid_range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew-r2 # Conflicts: # host-context-controller/README.md # host-context-controller/package-lock.json # host-context-controller/package.json # mcp-host/package-lock.json # mcp-host/package.json
Summary
Supersedes #1099 (closed without merging on 2026-11-10). This branch is
feat/document-read-tool(head4b649f433) withdevmerged in atd02fcb9f1, plus the follow-up work from zach88's round-2 review, from a deep review ofe1fe20095, and from the two reviews ofee95833cb(Evenfire Reviewer round 1 and an independent review). The design, scope and approved deviations ofclerum__document_readare unchanged from #1099. See that PR's description, corrected by the sections below.Part of #656 and #1093. Neither closes yet:
visual_not_supported.resolveVisualDeliveryLimits('grok-subscription')is null (pinned indeliveryLimits.test.ts), so the criterion "visual mode returns a page image that a Grok model reads" is not met. Text mode works on Grok, Codex, Claude and every other provider.make minikube-run-document-uploadlane has not run on this head.Deep review fixes (
e1fe20095→ this head)Each finding below has a test that failed on
e1fe20095(or, for test-only gaps, fails under the mutant the review found surviving).workerRunner.test.tssampled during thesh→nodeexecd35702d8a/procshows resident pages with VmData 0, and the kernel renames the process tonodebefore node is mapped, so the name alone is not enough. In arm64 containers (node:24.22.1 slim and alpine), one sample per spawn read VmData 0 1/300 and 3/300 times right after spawn, and 0/300 after the wait.RLIMIT_DATAreserve measured through Rosettad35702d8a94c314423,14eedd3a2[]. 5,000 one-cell rows with the real preview and token counter: 1 sheet 474 → 75 ms; 300 sheet names 4,988 → 80 ms; a 10,000-character name 17,859 → 201 ms (5,015 → 21 measures). A sheet list crafted to nearly fill the page took 10,016 measures with grouping alone, and takes 137 with the reach bound. There is still no byte-length shortcut (see R1-H1 below).x:row) read asdocument_empty0b9f5f7ceb2713409crnaming the next row failed the workbook0b9f5f7ce0b9f5f7ce,426285b83,1752f041dimage_bytespaused on Codex for a page too large for an empty requestf41efbf68image_bytesinstead of looping on reattach. Only Codex had the loop: on Claude the call cap (20,971,520 bytes) is already below row 7 at zero usage (21,000,000), so this changes nothing there. The commit message and the comment atindex.ts:741still say "Codex and Claude".d35702d8a,9c52cc3e0host_memory_unavailable, asMEMORY_MODEL.mdsays, instead of a worker defect. Each refusal logsDocument reader refused to startwith itscondition. Since9c52cc3e0the model-facing message no longer mentions a retry (see below).9ff8622c9,f34977657image-runtime-smoke.cjsextracts and renders a one-page PDF through the shipped modules, and sincef34977657also runs the dist child (see below).requestImagesspent a readce5a2c257ledger.beginRead().3b27b51aeisJobOutcome, which caught two invalid hand-built outcomes.boundCursoruses the productionencodeCursor.zipLimits.test.tsadds 45 tests: 34 of 38 single-guard deletions fail, and the other 4 are implied by earlier checks.Review fixes for
ee95833cb→ this headFixes for the two reviews posted on
ee95833cb: the Evenfire Reviewer's round 1 (R1-) and the independent review (M1–M4). Each change has a test that fails without it. For M3 and M4 that is the mutant the review found surviving; for M2 it is the reviewer's workbook shape.deva992742dc,2e1f47022devmerged twice (see Merge with dev).host_memory_unavailablesaid "Retrying later may help"9c52cc3e07b5bffb4e) reworded "cannot start" for the passinghost_headroomcase. A test pins the text.057e45cc8appendToolResults, seeded with the turn's attachments from before the batch, so page images fromcompleted_resultsand from the approved call reach the request. The forwarded image count now describes what the request carries. This also applies to other tools: their same-batch images, e.g. screenshots, now reach the resumed request too. Tests count the image parts of the rebuilt request: the M19 batch (20) and a real approved visual read (1).19ec7babffitaims its search at the allowance instead of bisecting. Measures per call: the reviewer's workbook (1,300 31-character sheet names, 430 rows of 900 characters) 874 → 21; the nearly-full sheet list 137 → 83; a 400,000-character PDF page 21 → 6. Tests assert a worst-case bound in emitted and window rows. A differential pins 18 PDF and XLSX read chains (20 since9d78c9d7e) to the previous implementation's results under a byte-count measure; they include sheet boundaries, parser-skipped and too-wide rows, and a partly spent turn budget.f34977657pdf_textjob through the dist runner in every Host image CI builds (alpine, slim, full and desktop, on amd64 and arm64). That covers the image's/bin/sh,ulimit -d,execof the dist entry andenv: {}. It runs without--memory, so it proves the spawn chain and IPC, not headroom admission. The rendered page must also carry ink.581c9279adocument_readmasks a ConfigStore secret (NR2) and charges the same ledger asattachment_read(NR1). The reader slot is released on a throwing pre-spawn premise, on an abort between admission and spawn, and on a synchronous spawn throw; each test then runs a real job that must start. The RSS interval is cleared when the job settles.The independent review's Low findings are not fixed here. One of them comes from
0b9f5f7ce, which matches by local name: a foreign-namespace<foo:row>or<foo:c>element is now read as data.Review fixes for
80d71c642→ this headFixes for the independent review's round 2 on
80d71c642(N1–N6, P1, P2).c583c141adevat0ca497d37merged (see Merge with dev).fitsearch assumes cost grows with nc90f2fb77host_headroom7b5bffb4ecd4cb5aa6c90f2fb77c90f2fb77f34977657, the commit before the M2 change.devbefore this branch): a second suspension in one turn re-sent answered tool resultsa7af8232dtoolUseLoopToolBatchfreezes them. Before the fix, the second resume sentcall-captureandcall-approve-1again after the new tool-call message and counted the frame twice; a provider's response to that request was not run. It is its own commit, so it can move to a separate PR.9d78c9d7e,6fff76dc3f34977657, and a bound on measures fails at80d71c642(1,340 > 750).Review fixes for
6fff76dc3→ this headFixes for the independent review's round 3 on
6fff76dc3.devbefore this branch): P1 still occurred with drivers whose tool-call ids restart on every response367dd959ecall_0on every response, so the earlier batch'scall_0result was appended again as an answer to the newcall_0, and its frame was counted twice. Results carried from an earlier batch are now markedansweredInSnapshotwhen they are carried; the resume skips them, and the GFS projection keeps the mark through the SQLite row. The id filter stays for approvals saved before the mark. Tests: a two-suspension turn with restarting ids (red before the fix:Capturedre-sent after the new tool-call message), and the mark surviving projection and a cold load (red without the projection change). Each of the twotaskExecutorsites, removed alone, turns the first red.367dd959eNot changed: B-1–B-5 (coverage and wording of the P2 commit), B-6–B-8 (info).
Final review fixes (
367dd959e→ this head)A final review of the whole diff, by slice: resume path, Tool, reader child and memory, parsers, and boundary. Each finding was checked against the code.
07f7112dekubectl top). So every read on a default install returnedhost_memory_unavailable. The default is now 2Gi, the value hosted Hosts run with; the request stays 128Mi. A test pins the default. This PR now changes host-context-controller: each Host Deployment rolls once when HCC is upgraded. T2 has not run, so this is not yet proven on a minikube Host.45212ddbeNot changed: two minor findings, listed under Known limits (the oversized
cursorand the guardrail tool name).Round-2 review findings
The four blocking findings were already fixed in code on #1099's head, by the commits after
e7fa6846f. This branch adds the regression tests and design notes the review asked for, with one commit per finding:d8ac778deVISUAL_BUDGET.mddecision table (8 limit sources, the smallest one wins, and what happens when each runs out), plus a test that follows the cursor through a long scan and pushes the result through the realassertVisualRequestFitswith the Anthropic profile. Removing the request-byte term makes it fail (58,926,114 > 32,000,000).56d59ca9fMEMORY_MODEL.md: the protection layers, whyRLIMIT_DATAand notRLIMIT_AS, an evaluation of a delegated sub-cgroup (not implemented, because a non-root container can't create one), the remaining risk, and the cluster settings that limit it.heap1016349f5xlsxUncachedFormula.test.ts: a 150k-row sheet with one formula that has no saved result, plus its continuation. Fails one7fa6846f, passes on head.row_width844ee2a5cxlsxRowWidth.test.ts(5 tests in that commit;3b27b51aeadds 2): a 13×32,767-character row as row 2 and as row 1, a 12-cell control, the A1/A2/XFD2/A3 stray-cell case at 128k and 200k, and a cursor chain. 4 of 5 fail one7fa6846f.8c8079051SheetWindow.add, which serialized whole rows to measure them. A 51 KB workbook with one 32,767-character shared string repeated across 16,384 columns serialized to about 537 MB and failed the whole sheet asheap. Rows are now measured with an early exit (serializedRowLength). Includes a regression test.e1fe20095dev's attachment-save test listed a file asreader=binary. The strict document-reader fixture rejects that value, so the test now usesreader=text.R1-H1 (the Host's
fitstep): the 231–301 ms figure coveredfitonly, not the XLSX row probes. Both are reduced by19ec7babf(independent review M2, above). There is still no byte-length shortcut:previewForLlmcan make content longer (tag escaping) or shorter (redaction).Closed in writing:
document_readresults are not collapsed by C17. This is token cost, not correctness. A stub needs a strict parser for each result shape (PDF text, visual, XLSX), so it is left for a follow-up issue.binary_unsupported. No production caller passes partial prefixes.pdfText.test.ts. The real-pdfjs tests cover the same branches.Merge with dev
dev(d02fcb9f1) is merged in2d15076b5. 8 files conflicted:mcp-hostandtests/e2e/fixturespackage files.dev's versions.devalready runs only the public password-recovery suites, so feat(mcp-host): read PDF and XLSX attachments with clerum__document_read #1099's T1 commit33f037f62is no longer needed.provider.mjs: usesdev'stoolOutputPatternand keeps both output patterns.README.md: keeps both sections.devatb6db9c297(the Next.js 16.4.6 upgrade and the CI dependency gates) is merged inee95833cb. There were no conflicts and nomcp-hostfiles changed. The PR's diff againstdevstayed at 77 files then; it was 82 at367dd959eand is 89 at this head.devat4fd7b2878(184 commits) is merged ina992742dc. Onlymcp-host/package.jsonandpackage-lock.jsonconflicted.package.json:dev'sopenai^4.87.0, with the reader'spdfjs-dist6.4.299,saxes,exceljs4.4.0 and@napi-rs/canvas^1.0.11.package-lock.json: regenerated withnpm install --package-lock-only, starting fromdev's lockfile. Regeneration resolved canvas to 1.1.1. I put back the 12@napi-rs/canvasentries at the 1.0.11 the reader was tested with, then re-ran npm, which left them. Every entry in the merged lockfile now matches one parent, andnpm ciinstalls it.dev's 0.1.632.devat05419da54(#1434, the lone-surrogate repair) is merged in2e1f47022. Only the version line conflicted:dev0.1.635, branch 0.1.639, merged as 0.1.640.devat0ca497d37(59 commits) is merged inc583c141a. Only the version lines conflicted:dev0.1.659, branch 0.1.641, merged as 0.1.660.devchanged nomcp-hostdependency.devat1b1b1131e(54 commits) is merged in8686fb77a. Five files conflicted:mcp-hostandhost-context-controllerpackage files.mcp-host:dev0.1.661, branch 0.1.669, merged as 0.1.670.host-context-controller:dev0.1.445, branch 0.1.427, merged as 0.1.446.devchanged no dependency in either package, so the branch's dependencies stand.host-context-controller/README.md:devre-padded the Host env table. The merge keepsdev's table, with the memory limit row at2Gi.dev'sconfig.tschange (request deadlines for the NetworkPolicy pass) merged without a conflict and does not touch the Host resources.Every merge commit's tree equals the automatic merge, apart from those conflict resolutions. The PR's diff against
devis the same 89 files, with the same line counts, before and after8686fb77a.Release note (replaces #1099's)
There is no required deploy order between Desktop and the Host.
reader: none.clerum__document_readreads only files attached to the message.CONTEXT_MAPPER_HOST_RESOURCES_LIMIT_MEMORYis kept.clerum__attachment_readdoes not coverclerum__document_read; add a rule for it, or use a pattern such asclerum__*_read.Operator prerequisites (details in
MEMORY_MODEL.md). When one is missing, every read returnshost_memory_unavailable, and the Host logsDocument reader refused to startwith the reason:/sys/module/kernel/parameters/ignore_rlimit_datareadsN.07f7112de). An operator who setsCONTEXT_MAPPER_HOST_RESOURCES_LIMIT_MEMORYbelow about 730 MiB getshost_memory_unavailableon every read. Not yet measured on a T2 Host.singleProcessOOMKill: true(Kubernetes 1.32+), so a combined overrun kills only the reader.Corrections to #1099's description
heap"without touching the Host process". Corrected: the child's private memory is capped byRLIMIT_DATA, so an oversized document fails an allocation inside the child and returnsdocument_limits_exceeded/heap. The parent also samples memory every 25 ms, kills the child early, and marks it as the first OOM victim. This protects the Host from the document, but not from the Host's own growth during a job. If the Host and the child together reach the container limit, the kernel OOM killer runs, and withmemory.oom.group=1that restarts the container. KubeletsingleProcessOOMKill: true(Kubernetes 1.32+) makes the child the only victim.[], is listed inskippedRows, and the read continues. A row of about 13 maximal cells is skipped rather than shown cut, on purpose: redaction has to see whole values.no_text_layer. The message says the selected pages have no text layer and points tomode: "visual"; on a model without image input it says a vision-capable model can read visible content.Known limits (not fixed here)
e1fe20095). No spreadsheet writer seen emits them, and a saxescdatahandler measurably raised the streaming child's RSS.projectGfsApprovalstrips only GFS images. Since057e45cc8the resume uses those persisted pages. In the default loop the model's image parts depend on the same collection, so separating them is its own change.e1fe20095. It bisected the cause to0b9f5f7ce, without isolating a root cause. My earlier 3 passes in 3 runs do not refute that.fileReference.cjsacceptsreader: pdf/xlsxon GFS references. Only the Desktop producer emits references, and it never does.assertVisualRequestFitschecks only requests that also carry GFS images. A request whose only images aredocument_readpages relies on the 4 MB text reserve (VISUAL_BUDGET.md).attachments, is not written to SQLite, while itscompleted_resultsare. After a restart, a frame that repeats an image from an earlier iteration of the turn is sent again, or in full instead of by reference under source identity. Its image count is still charged. A fix needs attachment metadata persisted with the approval row.[]needs its own exact probe, each the size of a page. Beside a long sheet list that is about 1.3 s per call on this machine with the production measure; the independent review measured a median of 2,219 ms per call on its own workbook (round 3). Going lower would change what a read returns.cursorafter the read count is spent fails the turn (final review). The read-exhausted notice repeats the model'scursorandattachmentIdunchecked. One longer than a page (10% of the context window) makes the notice debit throw, and the turn ends with an error instead of the notice.clerum__attachment_readrepeats only the resolved id and a validated offset.clerum__attachment_readdo not cover this tool (final review). See the release note.Verification (this head)
8686fb77a; 63/63 passed at367dd959e,6fff76dc3and80d71c6428686fb77a(macOS arm64)tsc --noEmitclean8686fb77a(taskExecutor,pdfText, fit differential)tsc --noEmitclean. Each new test failed under its mutant at45212ddbe; the two resume mutants had passed all 540 tests in the 26 resume test files before.367dd959e:agent,core/conversation,visualInput, orchestration,ironclaw, session approval (macOS arm64,--fileParallelism=false)9d78c9d7e(6fff76dc3changes comments only):documentRead,agent, orchestration andnativeToolRegistry.documentRead(macOS arm64,--fileParallelism=false)2e1f47022ee95833cb.2e1f47022childPdfText(the dist child through/bin/sh -c 'ulimit -d … && exec') andpdfRenderInk. On alpine amd64 (x64 musl, its first run) the child finishedok: data limit 2,488,320 KiB; VmData 1,708 MiB with up to 132 MiB resident, so about 1,576 MiB non-resident, within 1534 + 128. That was one small job.tsc --noEmit, eslint, prettier (changed files)make minikube-t2-public-boundaryPUBLIC_BOUNDARY_PASSat8686fb77a(CI's shell job rejected2e1f47022for a fixture value; fixed in80d71c642).8686fb77a(host-context-controller 0.1.446, mcp-host 0.1.670)validate-release-version-bumps --previous origin/devand the desktop manifest check pass.d35702d8aworkerRunner.test.ts66/66 on each; B1 sampler test 20/20 on each; reserve witness 1289–1292 MiB (slim), 1281 MiB (alpine).Not run on this head:
make minikube-run-document-upload. This lane is now also the check that proves the 2Gi default lets a minikube Host read.memory.oom.group/singleProcessOOMKillsetting.🤖 Generated with Claude Code