Repository navigation
Description
repo:cloud#1 · os-warren · session_01JQwxbYK6SZCmyzbGnrvmkk
Filing gate: ③ a task the maintainer dispatched directly. #31100 was ruled B, in two phases, with the maintainer's 「同意」 (2026-11-07T14:47Z, triage ruling comment 6708423049). Its execution line: "domain:server files phase 1 as its own card now". No card was filed then, because PR #31009 had already merged and the server seat signed off. This card is that phase-1 card. Phase 2 (the worker pool) is filed beside it as its own card, Blocked-by: this one.
- Business reason: on the shared cloud, every tenant shares the same server processes.
read_fileextracts PDF and.docxtext synchronously on the request thread, with no time budget and no concurrency cap. One tenant's assistant reading large files in a row holds the event loop for hundreds of milliseconds per file ([Decision] read_file on a shared cloud: how PDF text extraction is bounded so one tenant cannot slow everyone #31100's table: about 300 ms for a 9 MiB, 200-page PDF), and every other tenant's request in that process waits. - Who acts: a
domain:serverseat builds it. The cloud lane (repo:cloud#1) then sets the shared cloud's values in its deployment config, which is the [Decision] read_file on a shared cloud: how PDF text extraction is bounded so one tenant cannot slow everyone #31100 cloud half. - Dedupe:
repo:objectstack-ai/objectstack is:issue "read_file PDF budget"→ 1 hit ([Decision] read_file on a shared cloud: how PDF text extraction is bounded so one tenant cannot slow everyone #31100);"extraction concurrency"→ only [Decision] read_file on a shared cloud: how PDF text extraction is bounded so one tenant cannot slow everyone #31100 and closed QA records;31100→ [Decision] read_file on a shared cloud: how PDF text extraction is bounded so one tenant cannot slow everyone #31100 and seat posts. No phase-1 card exists (search 2026-11-09).
Reading (objectstack main 01cbbba5)
packages/mcp/src/file-content.ts:190extractFileContentand:240extractPdfText(unpdf1.9.1, dynamic import). It has only per-file caps:READ_FILE_MAX_BYTES10 MiB (L38),READ_FILE_TEXT_MAX_CHARS100,000 (L41),READ_FILE_PDF_MAX_PAGES200 (L44), andREAD_FILE_DOCX_XML_MAX_BYTES32 MiB (L47).grep -cE "concurren|semaphore|timeout|AbortSignal|deadline|budget" packages/mcp/src/file-content.ts→ 0.- Its one caller is
file-content.ts:437, theread_filedoor.
Do (ruling B, phase 1 only)
- Measure first: a test that holds the event loop. Use [Decision] read_file on a shared cloud: how PDF text extraction is bounded so one tenant cannot slow everyone #31100's method: a 1 ms
setIntervalcounted during the extraction of a fixture PDF that is heavy in drawing operators. Record the CPU time and the number of missed ticks onmain. - CPU-time budget per extraction. When it runs out, stop between pages. Answer with the text read so far,
truncated: true, and a reason that says the rest is in the download link (the answer already carries that link). Use process CPU time, not wall time, as ADR-0102 D1 does for sandboxed scripts. A wall-clock ceiling may sit beside it, as a separate cap. - Process-wide concurrency cap on extractions that run at once. Over the cap, refuse at once (no queueing) with a retryable answer: the code, plus "try again shortly", and a
Retry-Afterhint where the door has one. - Host-configured values: two host environment variables, read once at boot, validated loudly when malformed. Name them per the repo's env-var conventions. Document both defaults in
content/docs/ai/connect-mcp.mdxunder "Files:read_fileandcreate_upload_link". - Ablation tests: remove the budget and a heavy fixture reads past it; remove the cap and N+1 concurrent extractions all run. Each pin goes red under its ablation.
- ⛔ Phase 2 (moving extraction off the request thread) is not in this card. It has its own card.
Surface
packages/mcp/src/file-content.ts, its tests, content/docs/ai/connect-mcp.mdx, and one @objectstack/mcp changeset. Clause-②: the over-budget and over-cap answers are new refusals on a door that used to answer in full, so the dev states yes or no with the reason. Metadata-protocol: none (no spec or /meta change, per #31100).
Done when
PR merged, and both defaults are documented. The cloud lane is then told, so that it sets the shared cloud's values (the #31100 cloud half).
Activity
objectstack-fleet commentedon Nov 9, 2026
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM
Claim: PM loop round 14 · 2026-11-09T06:15Z
Session:
session_01ADHkmRXBnCezvSh9d2LLK6(card thread, dev) · seatsession_01ADHkmQKZzp4DLtqiCkZZRMAccount:
os-steveBranch:
claude/project-thread-z645c5Worktree:
objectstack-issue-31826Domain:
domain:serverSeat:
domain:server#2File surface:
packages/mcp/src/file-content.ts, its tests,content/docs/ai/connect-mcp.mdx, root.changeset/; the env reader's boot site is named in the PR if it lives elsewhere. ⛔ Nopackages/spec/**,ui/**Container & model: M (heavy PDF fixture, timing),
mode:thread,model: default tier,effort: highClause-②: yes (the seat's reading: a truncated answer and a new refusal on a door that used to answer in full; the dev may argue
no (narrowing)with its reason, and the seat re-rules the face)Metadata-protocol: none
Responsibility: phase 1 of #31100, ruling B (6708423049), filed by repo:cloud#1
Thread-read: none
Premise: holds as filed, read at claim on
origin/main0a4d8b18:file-content.tshas no concurrency, timeout or budget term; it has per-file size, char and page caps onlySerial constraints cleared: none; no other in-flight card on this seat touches
packages/mcpScope: Fixes #31826. Each PDF or
.docxextraction gets a CPU-time budget, checked between pages, with a truncated answer when it runs out. A process-wide concurrency cap refuses at once with a retryable answer. Two host env vars set both, and both are documented. Phase 2 (a worker pool) is out. Order:/mnt/project-files/server-seat2/standard-order.md+/mnt/project-files/server-seat2/31826-delta.md. Summons: the maintainer's 「server 任务很多,你应该跟进并发5」 (project chat, 2026-11-09T02:28Z).objectstack-fleet commentedon Nov 9, 2026
domain:server#2 · os-steve · session_01ADHkmRXBnCezvSh9d2LLK6
os-dev-report
{ "issue": 31826, "status": "done", "branch": "claude/project-thread-z645c5", "pr": "https://github.com/objectstack-ai/objectstack/pull/31848", "session": "session_01ADHkmRXBnCezvSh9d2LLK6", "premise_still_valid": true, "summary": "read_file's PDF and Word extraction now runs under a per-extraction process-CPU budget (OS_MCP_READ_FILE_CPU_BUDGET_MS, default 2000; checked between PDF pages and every 64 Word paragraphs; out of budget answers the text so far, truncated: true, and a note naming the budget and the download link) and a process-wide in-flight cap (OS_MCP_READ_FILE_MAX_CONCURRENT, default 4; over it refused at once as SERVICE_UNAVAILABLE 503 with details.retryAfterSeconds, released in finally). Both are read once in MCPServerPlugin.init (boot site packages/mcp/src/plugin.ts) and a malformed value fails the boot naming the variable. Measured on main 4ed20928: a 9.21 MiB 200-page drawing-heavy PDF takes 316-646 ms CPU with zero 1 ms ticks firing; after, defaults read it unchanged and a 100 ms budget stops it at ~101-106 ms CPU.", "tests": "pnpm --filter @objectstack/mcp exec vitest run --maxWorkers=2 on head 645f4183: Test Files 62 passed (62), Tests 987 passed (987), lock VERDICT command-exit 0. New: 7 cases in mcp-file-tools.test.ts (env reader, CONTROL small PDF+docx in full, PDF budget, docx budget, door refusal shape, N+1 refused, release after throw) + 1 boot pin in __tests__/plugin.test.ts. Ablations via scripts/ablation-replace.mjs, src-imported (no dist leg), each 'ok mutation landed anchor 1 -> 0' then 'ok restored blob == HEAD': A1 PDF budget check -> if(false): 1 failed/27; A2 docx budget check: 1 failed/27; A3 cap check removed: 2 failed/26 (N+1th received content); A4 release out of finally: 1 failed/27; A5 door busy branch removed: 1 failed/27 (details.retryAfterSeconds missing).", "gates": "head 645f4183: dispatch-gates --commands -> 98 derived; --ran: 97 run, 1 NOT-MEASURED, 0 UNRUN; 96 exit 0. check:skill-examples exit 3 PREREQUISITE NOT MET (NOT MEASURED). check:dual-build-cjs-loads exit 1 with 122 'types NOT emitted' findings, an artefact of the local OS_SKIP_DTS=1 workspace build, no exports map in the diff; CI runs both. teardown self-test and lean-entry-closure exit 0 after fixture fetch + build. pnpm --filter @objectstack/mcp typecheck exit 0; eslint --no-inline-config packages/mcp exit 0. PR check-runs: in_progress at report time.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "6 — mcp__github__issue_read x5 (31826 get + comments x2, 31100 get + comments), mcp__github__pull_request_read get x1; no write tools", "api_writes": "2 — fleet relay pr_create (PR #31848), post-stamped os-dev-report comment on #31826", "files_changed": [ ".changeset/31826-mcp-read-file-extraction-bounds.md", "content/docs/ai/connect-mcp.mdx", "packages/mcp/src/file-content.ts", "packages/mcp/src/mcp-http-tools.ts", "packages/mcp/src/plugin.ts", "packages/mcp/src/mcp-file-tools.test.ts", "packages/mcp/src/__tests__/plugin.test.ts" ], "line_budget": "+555 / -20 across 7 files (under the 3000 human-merge threshold)", "deviations": [ "Two files outside the named surface: packages/mcp/src/plugin.ts (the boot site, named in the PR as the delta asks) and packages/mcp/src/mcp-http-tools.ts (the read_file door's catch, which renders the busy refusal with details.retryAfterSeconds through the existing codedErrorResult).", "No assignee/reviewer set on the PR: the standard order forbids MCP writes and the relay pr_create sends no assignees.", "No PR activity subscription: os-dev's dispatch contract overrides the platform's auto-subscribe." ], "open_questions": [ { "question": "Names of the two host variables (seat's call).", "options": [ "A OS_MCP_READ_FILE_CPU_BUDGET_MS + OS_MCP_READ_FILE_MAX_CONCURRENT (as landed)", "B OS_MCP_READ_FILE_MAX_CONCURRENT_EXTRACTIONS for the cap" ], "recommendation": "A: OS_{DOMAIN}_{NAME}, DOMAIN=MCP, unit in the key, short and grouped with OS_MCP_*." }, { "question": "Code of the over-cap refusal.", "options": [ "A SERVICE_UNAVAILABLE 503 + details.retryAfterSeconds (as landed: the process is busy, nothing about the caller's rate)", "B RATE_LIMIT_EXCEEDED 429 (the retired CONCURRENT_LIMIT_EXCEEDED's prescription for request pacing)", "C a registered extension code (e.g. READ_FILE_BUSY), which is a packages/spec error-code-ledger edit, its own card" ], "recommendation": "A, because 503 is overload, both are retryable by the standard client, and C needs a spec surface this card excludes." }, { "question": "Defaults 2000 ms / 4.", "options": [ "A keep (no file within today's caps measured near 2000 ms; single-tenant behaviour unchanged)", "B lower the budget toward 1000 ms" ], "recommendation": "A; the shared cloud sets its own values (cloud half of #31100)." }, { "question": "Clause-②: yes without an arm, argued not (narrowing).", "options": [ "A yes (as declared): transient retryable refusal + truncated answer in the door's existing shape", "B yes (narrowing): full dogfood battery + BREAKING banner + adr-0087 marker" ], "recommendation": "A, because no accepted input is refused for good and defaults change no measured file." }, { "question": "A between-pages setImmediate yield would let other requests run between PDF pages (the 1 ms timer still never fires during an extraction).", "options": [ "A leave to phase 2 (#31828)", "B add in phase 1 as a follow-up" ], "recommendation": "A: phase 2 removes the hold entirely; a yield also makes process-CPU deltas charge other requests' work." } ], "security_findings": "none", "out_of_scope_findings": [] }Stamped 2026-11-09T07:03Z.
Generated by Claude Code
objectstack-fleet commentedon Nov 9, 2026
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM
ACCEPT → PR #31848 comment 6734582951 · head
645f4183· 2026-11-09T07:23Z · landing: pr_ready + automerge_enableobjectstack-fleet commentedon Nov 9, 2026
domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM
落地 · 2026-11-09T07:55Z
bd09b216(ACCEPT 6734582951; TIER_DEFAULT PASS 6734311777)Fixes #31826Landed:
read_filebounds PDF and Word text extraction with a per-file process-CPU budget,OS_MCP_READ_FILE_CPU_BUDGET_MS(default 2000). The budget is checked between PDF pages and every 64 Word paragraphs. Out of budget, the answer is the text so far withtruncated: trueand a note naming the budget and the download link.OS_MCP_READ_FILE_MAX_CONCURRENT(default 4). Over the cap, the call is refused at once as a retryableSERVICE_UNAVAILABLE(503) withdetails.retryAfterSeconds, never queued.Carried:
plugin.tstoo.