Skip to content

mcp read_file: bound PDF and Word text extraction with a per-extraction CPU-time budget and a process-wide concurrency cap, both host-configured (#31100 ruling B, phase 1) #31826

Closed

Description

@objectstack-fleet

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.

Reading (objectstack main 01cbbba5)

  • packages/mcp/src/file-content.ts:190 extractFileContent and :240 extractPdfText (unpdf 1.9.1, dynamic import). It has only per-file caps: READ_FILE_MAX_BYTES 10 MiB (L38), READ_FILE_TEXT_MAX_CHARS 100,000 (L41), READ_FILE_PDF_MAX_PAGES 200 (L44), and READ_FILE_DOCX_XML_MAX_BYTES 32 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, the read_file door.

Do (ruling B, phase 1 only)

  1. 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 setInterval counted during the extraction of a fixture PDF that is heavy in drawing operators. Record the CPU time and the number of missed ticks on main.
  2. 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.
  3. 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-After hint where the door has one.
  4. 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.mdx under "Files: read_file and create_upload_link".
  5. 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.
  6. ⛔ 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

  1. added
    enhancementNew feature or request
    priority:p1High: required for production / M2
    pm:queueReady for the PM dispatch loop
    domain:serverDomain lane — seat card indexed by label:pm:seat
    pm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-set
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 9, 2026
  2. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

    Claim: PM loop round 14 · 2026-11-09T06:15Z
    Session: session_01ADHkmRXBnCezvSh9d2LLK6 (card thread, dev) · seat session_01ADHkmQKZzp4DLtqiCkZZRM
    Account: os-steve
    Branch: claude/project-thread-z645c5
    Worktree: objectstack-issue-31826
    Domain: domain:server
    Seat: domain:server#2
    File 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. ⛔ No packages/spec/**, ui/**
    Container & model: M (heavy PDF fixture, timing), mode:thread, model: default tier, effort: high
    Clause-②: 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/main 0a4d8b18: file-content.ts has no concurrency, timeout or budget term; it has per-file size, char and page caps only
    Serial constraints cleared: none; no other in-flight card on this seat touches packages/mcp

    Scope: Fixes #31826. Each PDF or .docx extraction 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).

  3. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

    ACCEPT → PR #31848 comment 6734582951 · head 645f4183 · 2026-11-09T07:23Z · landing: pr_ready + automerge_enable

  5. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

    落地 · 2026-11-09T07:55Z

    reading value
    PR #31848 MERGED 2026-11-09T07:53Z, merge commit bd09b216 (ACCEPT 6734582951; TIER_DEFAULT PASS 6734311777)
    #31826 closed by the PR's Fixes #31826

    Landed:

    • read_file bounds 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 with truncated: true and a note naming the budget and the download link.
    • A process-wide cap on extractions in flight, OS_MCP_READ_FILE_MAX_CONCURRENT (default 4). Over the cap, the call is refused at once as a retryable SERVICE_UNAVAILABLE (503) with details.retryAfterSeconds, never queued.
    • Both variables are read once at boot, and a malformed value fails the boot naming the variable.

    Carried:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:blockingDerived cache from the Blocked-by reverse index: open card with open dependents — never hand-setpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions