You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
finding(service-storage, service-settings): the storage settings "Limits" keys (max_upload_mb, presigned_ttl, session_ttl) render in Setup and save, but nothing reads them #22283
Filing gate: ① a product defect, class (b): a declared setting that is not honoured. reach: a named producer. packages/services/service-settings/src/manifests/storage.manifest.ts (:63-70) renders a "Limits" group in Setup's storage settings: presigned_ttl, session_ttl and max_upload_mb ("Max upload size (MB)", default 100). An admin's save persists them.
Found by #22269's design-round dev (os-dev-report6059301379, out_of_scope_findings[0]). Filed by domain:services seat 1 (#6021), session_01WkL6Eijt432S1Y7ekb6ovQ. ⛔ Not graded or routed here; ⛔ not a claim.
Measured (objectstack c8bb3c8d)
StorageServicePlugin.applySettings (packages/services/service-storage/src/storage-service-plugin.ts:530-574) reads only the adapter keys, by name. presignedTtl and sessionTtl come only from constructor options (:96-101, :487-488).
against controls in the same manifest: local_root 8 and s3_bucket 4.
So an admin who sets "Max upload size (MB)" sees it saved and nothing enforces it. The same holds for the two TTLs.
Direction (for triage)
The base ruling principle applies: a declared key that is not honoured is an implementation gap, closed by implementing or retiring it. Either the storage plugin reads and enforces the three keys at the doors that own them (the upload doors for the size, the presign and session issuance for the TTLs), or the manifest retires the keys it will not honour.
#22269's design round chose not to enforce max_upload_mb at its archive entry alone (its OQ8), so this card carries the family.
Dedupe: MCP search_issues 「storage settings max_upload_mb presigned_ttl session_ttl not enforced no reader upload size limit」 gave 4 hits (#22046, #21730, #21147, #10702), all closed and other classes.
Triage: first grade, priority:p2 · domain:services · area:files · pm:queue (finding removed). Direction: honour the three keys at the doors that own them
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T13:00Z. ⛔ Not a claim, ⛔ not a dispatch.
Triage: lands in packages/services/service-storage/src/storage-service-plugin.ts (applySettings, and the upload, presign and session doors) ⇒ domain:services; rationale: packages/services/* is that lane's.
Why p2: an admin sets "Max upload size (MB)", sees it saved, and nothing enforces it. It is a resource-protection setting that silently does nothing.
Implement, not retire: the keys are declared, rendered and saved, the TTLs already exist as constructor options, and a size limit is a real operator need. Retiring would remove a control operators reasonably expect.
Pins: an upload over the saved limit is refused at the upload door; a presigned URL and a session take the saved TTLs. Controls: the defaults hold when nothing is saved.
Claim: PM loop round 3 · 2026-10-08T13:23Z
Session: session_01WkL6Eijt432S1Y7ekb6ovQ
Account: os-bill (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-22283-storage-limits-honoured
Worktree: objectstack-issue-22283
Domain: domain:services
Seat: domain:services#1 (seat post #6021)
Executes triage's direction (6060434386): honour the three keys at the doors that own them. Implement, not retire.
File surface at origin/main238222d8:
packages/services/service-storage/src/storage-service-plugin.ts:applySettings reads max_upload_mb, presigned_ttl and session_ttl, beside the adapter keys it reads today.
The doors that own them:
the upload doors (storage-routes.ts) refuse a file over the saved size, both the single-request upload and the chunked session (by declared size at start, and by the bytes received);
presigned URL issuance takes the saved TTL;
upload-session issuance takes the saved TTL.
The refusal reuses an existing code from the error-code ledger. The lead is PAYLOAD_TOO_LARGE (413), measured first. ⛔ No new code: a new ledger entry would be a packages/spec change, and it would go back to triage.
Tests in service-storage, and one @objectstack/service-storage changeset.
Pins: an upload over the saved limit is refused at each upload door; a presigned URL and a session carry the saved TTLs.
Controls: with nothing saved, the declared defaults hold; a file under the limit is accepted.
⛔ No change to the settings manifest's keys, defaults or bounds.
⛔ No change to which organization an upload belongs to.
Stop on breach; explain in the report.
Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate; default tier)
Clause-②: no (narrowing)
A declared default that was never enforced now is: an upload over the resolved limit (100 MB when nothing is saved) used to land and is refused. That is declared breaking, with an ADR-0087 disposition. Nothing widens. The dev measures the resolved defaults first, and states the shipped consequence in the changeset.
Responsibility: this repository's own code: StorageServicePlugin.applySettings reads only the adapter keys, so the manifest's Limits group is persisted and never read | none: no other layer caps an upload or a URL lifetime from these settings | administrators who set an upload size or a TTL in Setup
Thread-read: 6060434386
Serial constraints cleared:
Of the 14 open PRs (each file list read), only the release PR touches service-storage, in its CHANGELOG and package.json.
os-dev-report
{
"issue": 22283,
"status": "done",
"branch": "claude/issue-22283-storage-limits-honoured",
"pr": "#22311",
"session": "session_01WkL6Eijt432S1Y7ekb6ovQ — the dispatching PM session (mode:subagent, the parent's id)",
"premise_still_valid": true,
"summary": "Implemented triage's direction (implement, not retire). New package-internal module service-storage/src/storage-limits.ts reads presigned_ttl, session_ttl and max_upload_mb out of the storage namespace payload (readStorageLimits) and resolves them per request (resolveStorageLimits). StorageServicePlugin.applySettings reads the Limits group on every pass AHEAD of the adapter gate (that gate returns early on an unauthored adapter) and stores the snapshot on the SwappableStorageService it registers as storage; composeStorageRoutes binds it for the doors from that service, so the plugin mount and a host mountStorageRoutes mount both honour a save (the same path an adapter save takes). A save reaches the next request without a restart; issued URLs and sessions keep their stamped lifetime. Size gate before any row or byte: presign (declared size), chunked start (declared totalSize), local raw PUT (content-length pre-check, then bytes read), chunk door (bytes held so far plus this chunk, both ways). Refusal: 413 VALIDATION_ERROR in the ADR-0112 envelope, message naming the size, the limit in MB and bytes, and the setting key. H1 holds (0 readers of the three keys at 238222d; controls local_root 4 files, s3_bucket 1 file). H2: manifest defaults equal the built-ins for both TTLs (3600, 86400); max_upload_mb 100 vs no limit, so the one nothing-saved change is the 100 MB (104857600 bytes) cap, stated in the changeset (minor, Clause-② no (narrowing), ADR-0087 not-required (no-migration-prescription)). FALSIFIED lead (precedence): the adapter keys let an AUTHORED save win over the constructor (buildAdapterFromValues; only the manifest default yields to it), so followed as measured: saved (admin or env) beats host option (constructor or mountStorageRoutes presignedTtl/sessionTtl), which beats the namespace default, which beats the built-in. FALSIFIED lead (code): PAYLOAD_TOO_LARGE is registered only under @objectstack/rest, so stamping it from service-storage reds check:error-code-provenance and needs a packages/spec row (fenced); used the standard member the platform derives for 413 (standardErrorCodeForHttpStatus(413) is VALIDATION_ERROR), no code added; see open_questions. Found and fixed in-scope (rule: a defect this change touches): the chunk door's running uploaded_size counts a retried chunk index twice; measured on this branch that a 1 MiB-limit upload whose 600 KiB chunk 0 was re-sent was refused 413 while holding 600 KiB. Each stored parts entry now records its size and the limit sums the latest size per index (both adapters overwrite a re-sent part); a session whose parts predate sizes is judged by its running total. The progress accounting itself is untouched and handed over as a finding. One dogfood file over a real boot (bootStack + SettingsServicePlugin + StorageServicePlugin bound to settings): defaults enforced; PUT /api/settings/storage reaches presign, TTL and raw PUT.",
"files_changed": [
".changeset/22283-storage-limits-honoured.md (new)",
"packages/services/service-storage/src/storage-limits.ts (new)",
"packages/services/service-storage/src/storage-limits.test.ts (new, 18 cases)",
"packages/services/service-storage/src/storage-routes.ts",
"packages/services/service-storage/src/storage-service-plugin.ts",
"packages/services/service-storage/src/swappable-storage-service.ts",
"packages/services/service-storage/src/mount-storage-routes.ts (option docs only)",
"packages/services/service-storage/src/index.ts (type exports StorageLimitsSnapshot, StorageLimitReading)",
"packages/services/service-storage/src/error-envelope.conformance.test.ts (+1 case, 413)",
"packages/services/service-storage/README.md",
"packages/qa/dogfood/test/storage-upload-limits.dogfood.test.ts (new, 2 cases)"
],
"doors_before_after": [
"POST /storage/upload/presigned (declared size over limit): before 200 + pending sys_file row + URL minted; after 413 VALIDATION_ERROR, createFile and getPresignedUpload not called",
"POST /storage/upload/chunked (declared totalSize over limit): before 200 + file and session rows + backend multipart; after 413 VALIDATION_ERROR, createFile/createSession/initiateChunkedUpload not called",
"PUT /storage/_local/raw/:token (body over limit): before 200, bytes stored; after 413 VALIDATION_ERROR, upload not called, key absent on disk; content-length over limit refused before rawBody is read",
"PUT /storage/upload/chunked/:uploadId/chunk/:chunkIndex (held + chunk over limit): before 200, chunk stored; after 413 VALIDATION_ERROR, uploadChunk not called, uploaded_size unchanged; content-length pre-check likewise",
"Each door at exactly the limit: 200 (control). No settings namespace bound (bare kernel, bindToSettings false): no limit, 1 GiB declared answers 200 (control). Before-column evidence: main has no reader, and ablations A1-A4 (check removed) answer 200 to the same request."
],
"ttls_before_after": "Nothing saved: presigned URL 3600 s and session 86400 s, before and after (manifest default = built-in). Saved presigned_ttl 120 / session_ttl 600: before 3600 / 86400 (unread); after expiresIn 120 and token exp ~120 s on presigned upload AND non-gated download URLs, expiresAt now+600 on new sessions. Host option presignedTtl 600 with nothing saved: 600 (host beats namespace default); with saved 1200: 1200. A session issued before a save keeps its original expiresAt (pinned via the progress door).",
"tests": "Final head 46d7554 (origin/main 799eb00 merged in; the two main merges touched no file of this diff). | All builds/tests/typechecks under os-verify-lock (slot dev-22283, NODE_OPTIONS 3072, turbo --concurrency=1, vitest --maxWorkers=2). Closure builds: service-storage^... exit 0; dogfood^... exit 0 (63/63) after each merge. | pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2: VERDICT command-exit 0, Test Files 44 passed (44), Tests 736 passed (736); this PR adds 19 (18 storage-limits.test.ts + 1 envelope case). | pnpm --filter @objectstack/service-storage typecheck: exit 0, "check:test-typecheck: OK ... 0 file(s) / 0 error(s)"; tsc --listFiles counts 44 test files in BOTH tsconfig.json and tsconfig.test.json programs, storage-limits.test.ts included. | Dogfood at 46d7554: storage-upload-limits + field-file-collection + attachments-permission-matrix + route-ledger-live-mount-parity: Test Files 4 passed, Tests 34 passed, 1 skipped (declared enterprise-package skip); pnpm --filter @objectstack/dogfood typecheck exit 0 (tsconfig include test/**). | Consumer sweep (direction ...@objectstack/service-storage = downstream; storage-touching files only, declared narrowing, full suites are CI's) at 73ce441: organizations 2 wall files 20 passed; plugin-dev 9 files 86 passed; cli --project unit 4 storage files 32 passed (integration tier declared to CI); dogfood 10 storage files 93 passed, 1 declared skip. | Ablations (unit pins import src relatively, no dist involved; each via scripts/ablation-replace.mjs: anchor hit x1, blob changed, restore blob == HEAD and git diff HEAD empty after every leg; run at 071760b and 102ce46): A1 presign check removed -> 5 red; A2 chunked-start -> 2 red; A3 raw bytes -> 1; A3b raw content-length -> 1; A4 chunk bytes -> 1; A4b chunk content-length -> 1; A5 plugin stops storing snapshot -> 4 (all section 3 except the bindToSettings:false control); A6 composition stops binding -> 5; A7 saved-beats-host removed -> 2; R1 chunk door counts running total -> 1 (retry case); R2 legacy branch removed -> 1. Every direction as predicted (red). | Dogfood ablation D1 through dist: mutation landed (anchor 1->0, blob 955e04c930 -> 15416c4e87), pnpm --filter @objectstack/service-storage build VERDICT exit 0, ablation-dist-preflight marker ABLATED-22283-D1 present in dist/index.js and dist/index.cjs (exit 0), dogfood file 2 failed of 2 ("expected 200 to be 413"); restore proven (blob == HEAD 955e04c930, git diff HEAD empty), rebuild exit 0, preflight --absent exit 0 (marker absent from all 6 built files, tree clean), rerun 2 passed. Attempt 1 was VOID and not counted: comment marker stripped by the bundler and the replacement broke the DTS build (build exit 1, preflight exit 1). | Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 46d7554 -> 71 commands (identical list at 73ce441 and 102ce46); all 71 run at 46d7554 plus pnpm --filter @objectstack/spec run check:error-code-provenance: 72 of 72 exit 0. --ran reconciliation: "71 derived famil(ies) accounted for — 71 run, 0 NOT-MEASURED (a DERIVED zero — all 71 recorded an exit code and none of them is 3)". | Error/envelope family verdict lines: check:error-code-provenance "OK — every registered-code stamp site is listed under its own owner key or carries a recorded waiver (10 waiver(s), all live)"; check:dispatcher-error-vocabulary "OK — 54 unregistered code-stamping site(s), all classified"; check:route-envelope "✓ Dispatcher domains — 17 audited" (exit 0); check:nul-bytes "OK (... no raw ASCII control bytes)"; check:adr-0087-registration "✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition"; check:changeset-no-major "✓ This diff introduces no major bump"; check:dual-build-cjs-loads "✓ 106 published require entry point(s) across 66 package(s) load" (it answered PREREQUISITE NOT MET exit 3 at 73ce441 until eight unbuilt packages were built; re-measured green at 102ce46 and 46d7554). | Lint (narrowed, proven): eslint --no-inline-config --format json over all 11 changed paths at 46d7554: eslint's own config lints 9 and ignores the 2 markdown files ("no matching configuration"); 9 linted, 0 errors, 0 warnings. eslint.config.mjs enables no type-aware linting (no parserOptions.project, no projectService), so the diff cannot move an untouched file's verdict. Full pnpm lint is CI's.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 — all through scripts/pm over the fleet-write relay (repository_dispatch, executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #22311; relay run 37797168255; read-back 13037 bytes sent, 13037 stored, identical); (2) label-write --issue 22311 --assign os-bill -> POST /repos//issues/22311/assignees (run 37797317510; read-back MATCHES, no label written); (3) this os-dev-report comment -> POST /repos//issues/22283/comments (post-stamped). git push is a git op, not REST: the empty-branch probe plus 6 pushes of the feature branch.",
"deviations": [
"Two merges of origin/main (6729e10 before the PR as ordered, then 799eb00 because main moved with a service-settings change my dogfood path exercises); every verification above was re-run at the second merge head 46d7554.",
"Commit trailers use the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude); the harness reminder asked for a model-named Co-Authored-By, which the repo's pre-push hook refuses. PR footer uses the AGENTS.md session-URL form.",
"Consumer sweep narrowed to storage-touching files in each downstream package (declared above); full downstream suites and the cli integration tier are CI's.",
"check:* gates ran outside the verify lock, per the agent definition; builds and tests ran inside it.",
"Scope addition inside the fence: the retried-chunk accounting for the size limit (a defect this change touches, measured before fixing); it records a size field on each new sys_upload_session.parts entry (additive JSON in a text column)."
],
"open_questions": [
{
"question": "The 413 refusal stamps VALIDATION_ERROR (the platform's derived member for 413). Should @objectstack/service-storage instead stamp the ledger's existing PAYLOAD_TOO_LARGE, which needs one provenance row under the @objectstack/service-storage owner key in packages/spec/src/api/error-code-ledger.zod.ts (no new code)?",
"options": [
"A — keep VALIDATION_ERROR. Business need (measured): no client in this repo branches on PAYLOAD_TOO_LARGE or on 413 for uploads (packages/client, client-react: 0 hits; objectui not checked out here, NOT MEASURED). Long-term: generic code, the 413 status carries the condition. AI-error-proofing: weaker — VALIDATION_ERROR also names field validation, so a client must read the status. Startup: zero spec change.",
"B — register PAYLOAD_TOO_LARGE under @objectstack/service-storage (one ledger row, packages/spec, triage-gated per the claim) and switch the one constant in storage-routes.ts. Business need: same measurement (no reader yet). Long-term: one code for "too large" across doors (rest already answers 413 PAYLOAD_TOO_LARGE for import row limits). AI-error-proofing: stronger — a specific, catalogued code is branchable without the status. Startup: one row, no new vocabulary; but it must land BEFORE the release that ships this changeset, or the switch is a second wire break."
],
"recommendation": "B, landed before the next release: it adds no vocabulary (the code already exists), makes the refusal branchable on its own code as rest's 413 already is, and switching after a release would cost a second breaking wire change; if it cannot land before the release, A stands and the question closes."
}
],
"out_of_scope_findings": [
"class: a · reach: public doors measured on this branch's build (unchanged from main), driven in the order the SDK's resumeUpload drives them (GET progress, PUT chunks from uploadedChunks on, POST complete listing only those parts): a 2-chunk upload whose chunk 0 (5 MiB) was stored in the first pass answers complete 200 with size 5242890 while the stored file is 10 bytes — the completion door assembles only the parts the request lists, and resumeUpload lists only the parts it uploaded itself · Seam: consumer client:resumeUpload (packages/client/src/index.ts) → runtime:storage-routes.ts POST upload/chunked/:uploadId/complete · dedupe words: resumeUpload drops earlier chunks · chunked upload resume completes with partial parts list · completeChunkedUpload parts omit stored chunks",
"class: a · reach: public door measured on this branch's build (accounting lines unchanged from main): sending chunk 0 (600 KiB) of a 1 MiB session twice makes GET .../progress answer uploadedSize 1228800 and percentComplete 100 while 600 KiB is held; uploaded_chunks is inflated the same way, and resumeUpload resumes from uploadedChunks, so an inflated count skips real chunks · same family as the finding above (chunked-upload session accounting and resume): one collecting card can carry both · dedupe words: chunked upload progress retried chunk double count · uploaded_size uploaded_chunks retry overstates · upload progress percentComplete wrong after retry",
"carrier: none (承接者:无) · noted in the PR Acceptance notes, not filed: on the S3 adapter a presigned PUT goes straight to the bucket, so the platform judges that upload by its declared size only; binding the size into the signature needs a StorageUploadOptions field in the spec contract (fenced); not measured against a live bucket."
]
}
os-dev-report
{
"issue": 22283,
"round": 2,
"status": "done",
"branch": "claude/issue-22283-storage-limits-honoured",
"pr": "#22311",
"head": "736713afc8",
"session": "session_01WkL6Eijt432S1Y7ekb6ovQ — the dispatching PM session (mode:subagent, the parent's id)",
"premise_still_valid": true,
"summary": "R1 fixed by documentation, as ruled (this PR ships A; the PAYLOAD_TOO_LARGE switch is #22314). VALIDATION_ERROR's 413 is documented as an exception to its 400 on both pages, mirroring MISSING_REQUIRED_FIELD's 422. error-catalog.mdx gets a callout in the VALIDATION_ERROR section and a 413 exception row in the HTTP Status Quick Reference. error-handling.mdx gets an HTTP Status line ("400 — with one documented exception, which answers 413"), a bullet under Where it comes from, and a 413 row in its status table. Every line is scoped to the storage upload doors and max_upload_mb, so the switch retires exactly these lines, and once 413 is no longer emitted for VALIDATION_ERROR, the gate's direction B reds on any leftover. Measured which page the gate needs: either page's claim alone suffices. No code change this round: the worktree was recreated from the remote branch (46d7554), and main had moved by 6 commits, none onto this PR's files, so no merge.",
"files_changed_round2": [
"content/docs/api/error-catalog.mdx (+13: callout in the VALIDATION_ERROR section, 413 exception row in the quick reference)",
"content/docs/protocol/kernel/error-handling.mdx (+8/-1: HTTP Status line with the 413 exception, bullet, 413 status-table row)"
],
"tests": "Head 736713a (one docs commit on top of 46d7554; origin/main dc4a5c6 not merged — its 6 commits touch none of this PR's 13 paths). | pnpm check:error-status-conformance (runs its --self-test first): BEFORE at 46d7554, exit 1, "✗ VALIDATION_ERROR: the runtime can emit HTTP 413, and no doc publishes that status for it (documented: 400). Emitted at packages/services/service-storage/src/storage-routes.ts:291: sendError" / "✗ check:error-status-conformance — 1 finding(s)". AFTER at 736713a, exit 0, "✓ check-error-status-conformance --self-test: 70 cases pass" and "✓ every derivable runtime status is documented, and every documented status is reachable", reconciled 29 (code, status) pairs (28 before). | Which page the gate requires: ablations via scripts/ablation-replace.mjs (--delete / --replacement, --hold, restore proven blob == HEAD, tree clean after). Catalog row removed: exit 0, 29 pairs. Protocol HTTP Status line reduced to 400: exit 0, 29 pairs. Both removed: exit 1, 1 finding, 28 pairs. So either page's claim alone satisfies it; both are kept for consistency. First attempt of leg 1 refused by the tool itself (an empty --replacement counts 0 to 0), restored, and redone with --delete; not a reading. | Workspace build (fresh worktree): turbo run build --filter=!@objectstack/docs --concurrency=1 under the lock, VERDICT exit 0, 72/72 (71 cached). | Under the lock at 736713a: service-storage vitest 44 files / 736 passed; service-storage typecheck exit 0 (check:test-typecheck OK); dogfood storage-upload-limits 2 passed. | Gates: dispatch-gates --commands --repo objectstack-ai/objectstack at 736713a gives 97 commands (71 before + 26 from the doc paths, check:error-status-conformance now among them). All 97 run, plus pnpm --filter @objectstack/spec run check:error-code-provenance, 98 of 98 exit 0. --ran: "✓ dispatch-gates --ran: 97 derived famil(ies) accounted for — 97 run, 0 NOT-MEASURED (a DERIVED zero — all 97 recorded an exit code and none of them is 3)". | CI, latest per name. On 46d7554: Lint & Repo Gates failure (R1). Test Core (1/6) cancelled at 15:20:41, after 19 min, when the push of 736713a superseded the run; the shard takes 25 to 32 min on recent main (dc4a5c6: 32 min, 9ad3d24: 25 min, a3bcbcf: 31 min). The aggregate Test Core failed with "1 of 6 declared shard(s) of test published no positive attestation (test-1-of-6) ... The leg was cancelled mid-run": a cancellation, not a test failure. On 736713a: 31 success, 2 skipped, 1 in progress. Lint & Repo Gates, TypeScript Type Check, Build Core, Dogfood Regression Gate, Temporal Conformance, Governed Surface Queue Guard and Test Core 2 to 6 all succeeded. Test Core (1/6) was in_progress at report time (NOT MEASURED: not concluded).",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "1 — this round-2 os-dev-report comment, POST /repos//issues/22283/comments via scripts/pm/post-stamped.mjs over the fleet-write relay. The PR body was NOT patched: the append text is pr_body_append below. git push: 1 (736713a), a git op, not REST. Reads only otherwise (check-runs, annotations).",
"deviations": [
"The CI job log could not be fetched here (its blob host answered with an error through the proxy), so R1's failing line was taken from the seat's quote, reproduced locally, and read from the check-run annotations. The other gates' CI verdicts are read from check-run conclusions, not logs.",
"Main not merged this round: it moved by 6 commits (dc4a5c6) onto none of this PR's files, which the order made the merge condition."
],
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: measured. scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 46d7554 printed 71 commands without pnpm check:error-status-conformance. CI's Lint & Repo Gates failed on exactly that family for packages/services/service-storage/src/storage-routes.ts:291 · position: the derivation places the family by its four file literals only (--json artifactRosterSilences: packages/spec/src/api/errors.zod.ts, content/docs/protocol/kernel/error-handling.mdx, content/docs/api/error-catalog.mdx, scripts/error-status-unpinned-baseline.json; coversYourPath false), so the family is silent for any diff under packages/. The gate actually walks packages/ through const SCAN_ROOT = 'packages' (scripts/check-error-status-conformance.mjs:222, walk at :2099). That bare single-segment literal is refused by the hint extractor by design (dispatch-gates.mjs bare-word / bare-root refusals, comment near :6600, self-test near :18825). Nor does the content-KIND convention that places check:dispatcher-error-vocabulary for files carrying an ADR-0112 code name this family. Of the 11 gates with a bare SCAN_ROOT = 'packages' constant, only this one was both roster-silent and underived at 46d7554. The derivation's own roster footnote puts the fix on the gate side: declare the scan surface beside the roster in a subtree spelling. No gate added · dedupe words: dispatch-gates error-status-conformance not derived · artifact roster silent packages scan root · bare SCAN_ROOT hint refused"
],
"pr_body_append": "## Round 2 (seat review, R1)\n\n- R1: check:error-status-conformance was red on 46d7554dc8. Per the ruling, this PR ships 413VALIDATION_ERROR (option A). The switch to PAYLOAD_TOO_LARGE is carried by #22314. The 413 is now documented as an exception to the code's 400, mirroring how MISSING_REQUIRED_FIELD documents its 422.\n - content/docs/api/error-catalog.mdx: a callout in the VALIDATION_ERROR section, and a 413 exception row in the HTTP Status Quick Reference ("this row is an exception to the 400 row, not a second home for the code").\n - content/docs/protocol/kernel/error-handling.mdx: **HTTP Status:** 400 — with one documented exception, which answers **413** (see below), a "the one 413" bullet under Where it comes from, and a 413 row in the HTTP Status Codes table.\n - Every added line names the storage upload doors and max_upload_mb, so the switch removes exactly these lines. Once VALIDATION_ERROR is no longer emitted at 413, the gate's direction B goes red on any line left behind.\n- Which page the gate needs. Measured at 736713afc8 with proven-restore ablations: either page's claim is enough on its own.\n - Catalog row removed: green, 29 pairs.\n - Protocol status line reduced to 400: green, 29 pairs.\n - Both removed: red, 1 finding, 28 pairs.\n - Before the docs (46d7554dc8) the gate was red with the CI finding. After: ✓ every derivable runtime status is documented, and every documented status is reachable, 29 (code, status) pairs.\n- Why the derivation did not name this gate.dispatch-gates places check:error-status-conformance only by four file literals: errors.zod.ts, the two doc pages and its baseline.\n - The gate walks packages/** through const SCAN_ROOT = 'packages' (scripts/check-error-status-conformance.mjs:222, walked at :2099). That is a bare single-segment literal, and the hint extractor refuses those by design.\n - So for a diff under packages/** the gate sat in the artifact-roster block, silent and outside the 71 commands. With this round's doc paths it is matched, and it is now one of the 97.\n - Reported to the seat as a finding; no gate added.\n- CI.\n - On 46d7554dc8, Test Core (1/6) was cancelled at 15:20:41 when the round-2 push superseded the run. It had run 19 minutes; the shard takes 25 to 33 minutes on recent main runs. The aggregate Test Core then reported a missing shard attestation, not a test failure.\n - On 736713afc8: Lint & Repo Gates, TypeScript Type Check, Build Core, Dogfood Regression Gate, Temporal Conformance and Governed Surface Queue Guard succeeded, as did Test Core 2 to 6. Test Core (1/6) was still in progress at report time.\n- Local verification at 736713afc8.\n - Derivation: 97 families (the docs added 26); all run, plus check:error-code-provenance, 98 of 98 exit 0. --ran reconciliation: 97 of 97, 0 NOT-MEASURED.\n - service-storage: 736 passed. Typecheck: exit 0. Dogfood storage-limits file: 2 passed.\n"
}
Checked against GitHub and the branch, not the reports' prose (os-dev-report6062816879, round 2 6063587901).
Form:
The PR is a draft against main. Its first line is Fixes #22283, with a line-start Clause-②: no (narrowing). The PR assignee is os-bill.
Net diff: 13 files: round 1's 11, plus the two error-doc pages in round 2. No packages/spec path and the arm is no, so no contract review is owed. check-governed-merges --pr 22311: NOT governed.
It merges clean with main.
The scope beyond the claim is appended to the claim (6060843707).
Diff, read by the seat:
applySettings reads the Limits group on every pass, ahead of the adapter gate, and stores a snapshot on the registered storage service. The route composition hands that snapshot to the doors, so the plugin's mount and a host's mountStorageRoutes mount both honour a save without a restart.
Four upload doors refuse a file over the resolved limit before any row or byte is written: the presigned upload by its declared size, the chunked start by its declared total, the local raw upload by content-length and then by bytes read, and the chunk door by bytes held plus this chunk.
A mount with no settings namespace keeps no limit.
The changeset, checked sentence by sentence:
It is minor with the BREAKING banner and an ADR-0087 disposition (not-required, no migration prescription).
It states the one nothing-saved change: the 100 MB default cap. It states the precedence, the S3 declared-size limitation and the remedy before upgrading.
The measured falsifications are accepted:
Precedence: a saved value beats the host's option, which beats the declared default. That is how the adapter keys already behave, and the seat's lead said to follow measurement.
The refusal's code:PAYLOAD_TOO_LARGE is registered only under @objectstack/rest, so this PR answers 413 with the derived standard member VALIDATION_ERROR. No code is added and no packages/spec edit is made.
The in-place adjacent fix is accepted: the size limit counts a re-sent chunk index once, at its latest size. Each new parts entry records its size. This was measured before the fix: a retry was refused while the session held less than the limit.
Dogfood: 4 files, 34 passed, including the new over-HTTP file.
The storage-touching files of the downstream consumers are green.
Ablations A1–A7, R1, R2 and the dogfood dist leg D1 each turned their pins red, and each was restored to HEAD. Attempt 1 of D1 was declared void and not counted.
Gates: 71 of 71 derived, plus check:error-code-provenance; --ran is a derived zero.
Round 2 (REWORK, one red gate) is resolved:
check:error-status-conformance was red because VALIDATION_ERROR was emitted at 413 and documented only at 400.
PR #22311 merged through the merge queue as 59d993c9. On origin/main, @objectstack/service-storage (minor, BREAKING accept-set narrowing): the storage settings' Limits group is honoured.
An upload over the resolved max_upload_mb is refused with 413 at the four upload doors before anything is stored. It is 100 MB when nothing is saved.
A saved presigned_ttl and session_ttl are the lifetimes of new URLs and sessions.
A save reaches the doors without a restart.
The PR's Fixes line closed the card completed. This note also removes pm:dispatched and the assignee.
Activity
objectstack-fleet commentedon Oct 8, 2026
Triage: first grade,
priority:p2·domain:services·area:files·pm:queue(findingremoved). Direction: honour the three keys at the doors that own themTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T13:00Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/services/service-storage/src/storage-service-plugin.ts(applySettings, and the upload, presign and session doors) ⇒domain:services; rationale:packages/services/*is that lane's.objectstack-fleet commentedon Oct 8, 2026
Claim: PM loop round 3 · 2026-10-08T13:23Z
Session:
session_01WkL6Eijt432S1Y7ekb6ovQAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22283-storage-limits-honouredWorktree:
objectstack-issue-22283Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)Executes triage's direction (
6060434386): honour the three keys at the doors that own them. Implement, not retire.File surface at
origin/main238222d8:packages/services/service-storage/src/storage-service-plugin.ts:applySettingsreadsmax_upload_mb,presigned_ttlandsession_ttl, beside the adapter keys it reads today.The doors that own them:
storage-routes.ts) refuse a file over the saved size, both the single-request upload and the chunked session (by declared size at start, and by the bytes received);The refusal reuses an existing code from the error-code ledger. The lead is
PAYLOAD_TOO_LARGE(413), measured first. ⛔ No new code: a new ledger entry would be apackages/specchange, and it would go back to triage.Tests in
service-storage, and one@objectstack/service-storagechangeset.Seat append, 2026-10-08T15:10Z, from the dev report's
files_changedanddeviations(PR fix(service-storage)!: enforce the storage settings Limits group (max_upload_mb, presigned_ttl, session_ttl) at the upload doors #22311):storage-limits.tsis a new module. It reads the Limits group.swappable-storage-service.tscarries the snapshot.mount-storage-routes.tschanges its option docs only.index.tsadds two type exports.packages/qa/dogfood/test/storage-upload-limits.dogfood.test.ts(the over-HTTP case).sys_upload_session.partsentry records its size, which is an additive field in the stored JSON. This is an in-place adjacent fix: the limit this card adds would otherwise refuse a retry. The session's progress accounting is untouched and is filed as finding(service-storage, client): a resumed chunked upload can complete with 200 while the stored file holds only the parts the resuming client sent, and a re-sent chunk is counted twice in the session progress #22313.Seat append, 2026-10-08T15:45Z, round 2:
content/docs/api/error-catalog.mdxandcontent/docs/protocol/kernel/error-handling.mdxdocumentVALIDATION_ERROR's413as an exception at the storage upload doors.check:error-status-conformancerequires it, and spec(error-code-ledger), service-storage: the upload size refusal (PR #22311) answers 413 VALIDATION_ERROR; register the existing PAYLOAD_TOO_LARGE under @objectstack/service-storage and stamp it, before the release that ships #22311 #22314's switch retires it.Exclusions:
packages/spec.Stop on breach; explain in the report.
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier)Clause-②: no (narrowing)
Responsibility:
this repository's own code: StorageServicePlugin.applySettings reads only the adapter keys, so the manifest's Limits group is persisted and never read | none: no other layer caps an upload or a URL lifetime from these settings | administrators who set an upload size or a TTL in SetupThread-read: 6060434386
Serial constraints cleared:
service-storage, in its CHANGELOG andpackage.json.pm:blocked, this lane) does not touchservice-storage's doors.objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{
"issue": 22283,
"status": "done",
"branch": "claude/issue-22283-storage-limits-honoured",
"pr": "#22311",
"session": "session_01WkL6Eijt432S1Y7ekb6ovQ — the dispatching PM session (mode:subagent, the parent's id)",
"premise_still_valid": true,
"summary": "Implemented triage's direction (implement, not retire). New package-internal module service-storage/src/storage-limits.ts reads presigned_ttl, session_ttl and max_upload_mb out of the storage namespace payload (readStorageLimits) and resolves them per request (resolveStorageLimits). StorageServicePlugin.applySettings reads the Limits group on every pass AHEAD of the adapter gate (that gate returns early on an unauthored adapter) and stores the snapshot on the SwappableStorageService it registers as storage; composeStorageRoutes binds it for the doors from that service, so the plugin mount and a host mountStorageRoutes mount both honour a save (the same path an adapter save takes). A save reaches the next request without a restart; issued URLs and sessions keep their stamped lifetime. Size gate before any row or byte: presign (declared size), chunked start (declared totalSize), local raw PUT (content-length pre-check, then bytes read), chunk door (bytes held so far plus this chunk, both ways). Refusal: 413 VALIDATION_ERROR in the ADR-0112 envelope, message naming the size, the limit in MB and bytes, and the setting key. H1 holds (0 readers of the three keys at 238222d; controls local_root 4 files, s3_bucket 1 file). H2: manifest defaults equal the built-ins for both TTLs (3600, 86400); max_upload_mb 100 vs no limit, so the one nothing-saved change is the 100 MB (104857600 bytes) cap, stated in the changeset (minor, Clause-② no (narrowing), ADR-0087 not-required (no-migration-prescription)). FALSIFIED lead (precedence): the adapter keys let an AUTHORED save win over the constructor (buildAdapterFromValues; only the manifest default yields to it), so followed as measured: saved (admin or env) beats host option (constructor or mountStorageRoutes presignedTtl/sessionTtl), which beats the namespace default, which beats the built-in. FALSIFIED lead (code): PAYLOAD_TOO_LARGE is registered only under @objectstack/rest, so stamping it from service-storage reds check:error-code-provenance and needs a packages/spec row (fenced); used the standard member the platform derives for 413 (standardErrorCodeForHttpStatus(413) is VALIDATION_ERROR), no code added; see open_questions. Found and fixed in-scope (rule: a defect this change touches): the chunk door's running uploaded_size counts a retried chunk index twice; measured on this branch that a 1 MiB-limit upload whose 600 KiB chunk 0 was re-sent was refused 413 while holding 600 KiB. Each stored parts entry now records its size and the limit sums the latest size per index (both adapters overwrite a re-sent part); a session whose parts predate sizes is judged by its running total. The progress accounting itself is untouched and handed over as a finding. One dogfood file over a real boot (bootStack + SettingsServicePlugin + StorageServicePlugin bound to settings): defaults enforced; PUT /api/settings/storage reaches presign, TTL and raw PUT.",
"files_changed": [
".changeset/22283-storage-limits-honoured.md (new)",
"packages/services/service-storage/src/storage-limits.ts (new)",
"packages/services/service-storage/src/storage-limits.test.ts (new, 18 cases)",
"packages/services/service-storage/src/storage-routes.ts",
"packages/services/service-storage/src/storage-service-plugin.ts",
"packages/services/service-storage/src/swappable-storage-service.ts",
"packages/services/service-storage/src/mount-storage-routes.ts (option docs only)",
"packages/services/service-storage/src/index.ts (type exports StorageLimitsSnapshot, StorageLimitReading)",
"packages/services/service-storage/src/error-envelope.conformance.test.ts (+1 case, 413)",
"packages/services/service-storage/README.md",
"packages/qa/dogfood/test/storage-upload-limits.dogfood.test.ts (new, 2 cases)"
],
"doors_before_after": [
"POST /storage/upload/presigned (declared size over limit): before 200 + pending sys_file row + URL minted; after 413 VALIDATION_ERROR, createFile and getPresignedUpload not called",
"POST /storage/upload/chunked (declared totalSize over limit): before 200 + file and session rows + backend multipart; after 413 VALIDATION_ERROR, createFile/createSession/initiateChunkedUpload not called",
"PUT /storage/_local/raw/:token (body over limit): before 200, bytes stored; after 413 VALIDATION_ERROR, upload not called, key absent on disk; content-length over limit refused before rawBody is read",
"PUT /storage/upload/chunked/:uploadId/chunk/:chunkIndex (held + chunk over limit): before 200, chunk stored; after 413 VALIDATION_ERROR, uploadChunk not called, uploaded_size unchanged; content-length pre-check likewise",
"Each door at exactly the limit: 200 (control). No settings namespace bound (bare kernel, bindToSettings false): no limit, 1 GiB declared answers 200 (control). Before-column evidence: main has no reader, and ablations A1-A4 (check removed) answer 200 to the same request."
],
"ttls_before_after": "Nothing saved: presigned URL 3600 s and session 86400 s, before and after (manifest default = built-in). Saved presigned_ttl 120 / session_ttl 600: before 3600 / 86400 (unread); after expiresIn 120 and token exp ~120 s on presigned upload AND non-gated download URLs, expiresAt now+600 on new sessions. Host option presignedTtl 600 with nothing saved: 600 (host beats namespace default); with saved 1200: 1200. A session issued before a save keeps its original expiresAt (pinned via the progress door).",
"tests": "Final head 46d7554 (origin/main 799eb00 merged in; the two main merges touched no file of this diff). | All builds/tests/typechecks under os-verify-lock (slot dev-22283, NODE_OPTIONS 3072, turbo --concurrency=1, vitest --maxWorkers=2). Closure builds: service-storage^... exit 0; dogfood^... exit 0 (63/63) after each merge. | pnpm --filter @objectstack/service-storage exec vitest run --maxWorkers=2: VERDICT command-exit 0, Test Files 44 passed (44), Tests 736 passed (736); this PR adds 19 (18 storage-limits.test.ts + 1 envelope case). | pnpm --filter @objectstack/service-storage typecheck: exit 0, "check:test-typecheck: OK ... 0 file(s) / 0 error(s)"; tsc --listFiles counts 44 test files in BOTH tsconfig.json and tsconfig.test.json programs, storage-limits.test.ts included. | Dogfood at 46d7554: storage-upload-limits + field-file-collection + attachments-permission-matrix + route-ledger-live-mount-parity: Test Files 4 passed, Tests 34 passed, 1 skipped (declared enterprise-package skip); pnpm --filter @objectstack/dogfood typecheck exit 0 (tsconfig include test/**). | Consumer sweep (direction ...@objectstack/service-storage = downstream; storage-touching files only, declared narrowing, full suites are CI's) at 73ce441: organizations 2 wall files 20 passed; plugin-dev 9 files 86 passed; cli --project unit 4 storage files 32 passed (integration tier declared to CI); dogfood 10 storage files 93 passed, 1 declared skip. | Ablations (unit pins import src relatively, no dist involved; each via scripts/ablation-replace.mjs: anchor hit x1, blob changed, restore blob == HEAD and git diff HEAD empty after every leg; run at 071760b and 102ce46): A1 presign check removed -> 5 red; A2 chunked-start -> 2 red; A3 raw bytes -> 1; A3b raw content-length -> 1; A4 chunk bytes -> 1; A4b chunk content-length -> 1; A5 plugin stops storing snapshot -> 4 (all section 3 except the bindToSettings:false control); A6 composition stops binding -> 5; A7 saved-beats-host removed -> 2; R1 chunk door counts running total -> 1 (retry case); R2 legacy branch removed -> 1. Every direction as predicted (red). | Dogfood ablation D1 through dist: mutation landed (anchor 1->0, blob 955e04c930 -> 15416c4e87), pnpm --filter @objectstack/service-storage build VERDICT exit 0, ablation-dist-preflight marker ABLATED-22283-D1 present in dist/index.js and dist/index.cjs (exit 0), dogfood file 2 failed of 2 ("expected 200 to be 413"); restore proven (blob == HEAD 955e04c930, git diff HEAD empty), rebuild exit 0, preflight --absent exit 0 (marker absent from all 6 built files, tree clean), rerun 2 passed. Attempt 1 was VOID and not counted: comment marker stripped by the bundler and the replacement broke the DTS build (build exit 1, preflight exit 1). | Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 46d7554 -> 71 commands (identical list at 73ce441 and 102ce46); all 71 run at 46d7554 plus pnpm --filter @objectstack/spec run check:error-code-provenance: 72 of 72 exit 0. --ran reconciliation: "71 derived famil(ies) accounted for — 71 run, 0 NOT-MEASURED (a DERIVED zero — all 71 recorded an exit code and none of them is 3)". | Error/envelope family verdict lines: check:error-code-provenance "OK — every registered-code stamp site is listed under its own owner key or carries a recorded waiver (10 waiver(s), all live)"; check:dispatcher-error-vocabulary "OK — 54 unregistered code-stamping site(s), all classified"; check:route-envelope "✓ Dispatcher domains — 17 audited" (exit 0); check:nul-bytes "OK (... no raw ASCII control bytes)"; check:adr-0087-registration "✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition"; check:changeset-no-major "✓ This diff introduces no major bump"; check:dual-build-cjs-loads "✓ 106 published require entry point(s) across 66 package(s) load" (it answered PREREQUISITE NOT MET exit 3 at 73ce441 until eight unbuilt packages were built; re-measured green at 102ce46 and 46d7554). | Lint (narrowed, proven): eslint --no-inline-config --format json over all 11 changed paths at 46d7554: eslint's own config lints 9 and ignores the 2 markdown files ("no matching configuration"); 9 linted, 0 errors, 0 warnings. eslint.config.mjs enables no type-aware linting (no parserOptions.project, no projectService), so the diff cannot move an untouched file's verdict. Full pnpm lint is CI's.",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "3 — all through scripts/pm over the fleet-write relay (repository_dispatch, executed as objectstack-fleet[bot]): (1) pr_create -> POST /repos/objectstack-ai/objectstack/pulls (draft #22311; relay run 37797168255; read-back 13037 bytes sent, 13037 stored, identical); (2) label-write --issue 22311 --assign os-bill -> POST /repos//issues/22311/assignees (run 37797317510; read-back MATCHES, no label written); (3) this os-dev-report comment -> POST /repos//issues/22283/comments (post-stamped). git push is a git op, not REST: the empty-branch probe plus 6 pushes of the feature branch.",
"deviations": [
"Two merges of origin/main (6729e10 before the PR as ordered, then 799eb00 because main moved with a service-settings change my dogfood path exercises); every verification above was re-run at the second merge head 46d7554.",
"Commit trailers use the AGENTS.md model-free pair (Claude-Session + Co-authored-by: Claude); the harness reminder asked for a model-named Co-Authored-By, which the repo's pre-push hook refuses. PR footer uses the AGENTS.md session-URL form.",
"Consumer sweep narrowed to storage-touching files in each downstream package (declared above); full downstream suites and the cli integration tier are CI's.",
"check:* gates ran outside the verify lock, per the agent definition; builds and tests ran inside it.",
"Scope addition inside the fence: the retried-chunk accounting for the size limit (a defect this change touches, measured before fixing); it records a size field on each new sys_upload_session.parts entry (additive JSON in a text column)."
],
"open_questions": [
{
"question": "The 413 refusal stamps VALIDATION_ERROR (the platform's derived member for 413). Should @objectstack/service-storage instead stamp the ledger's existing PAYLOAD_TOO_LARGE, which needs one provenance row under the @objectstack/service-storage owner key in packages/spec/src/api/error-code-ledger.zod.ts (no new code)?",
"options": [
"A — keep VALIDATION_ERROR. Business need (measured): no client in this repo branches on PAYLOAD_TOO_LARGE or on 413 for uploads (packages/client, client-react: 0 hits; objectui not checked out here, NOT MEASURED). Long-term: generic code, the 413 status carries the condition. AI-error-proofing: weaker — VALIDATION_ERROR also names field validation, so a client must read the status. Startup: zero spec change.",
"B — register PAYLOAD_TOO_LARGE under @objectstack/service-storage (one ledger row, packages/spec, triage-gated per the claim) and switch the one constant in storage-routes.ts. Business need: same measurement (no reader yet). Long-term: one code for "too large" across doors (rest already answers 413 PAYLOAD_TOO_LARGE for import row limits). AI-error-proofing: stronger — a specific, catalogued code is branchable without the status. Startup: one row, no new vocabulary; but it must land BEFORE the release that ships this changeset, or the switch is a second wire break."
],
"recommendation": "B, landed before the next release: it adds no vocabulary (the code already exists), makes the refusal branchable on its own code as rest's 413 already is, and switching after a release would cost a second breaking wire change; if it cannot land before the release, A stands and the question closes."
}
],
"out_of_scope_findings": [
"class: a · reach: public doors measured on this branch's build (unchanged from main), driven in the order the SDK's resumeUpload drives them (GET progress, PUT chunks from uploadedChunks on, POST complete listing only those parts): a 2-chunk upload whose chunk 0 (5 MiB) was stored in the first pass answers complete 200 with size 5242890 while the stored file is 10 bytes — the completion door assembles only the parts the request lists, and resumeUpload lists only the parts it uploaded itself · Seam: consumer client:resumeUpload (packages/client/src/index.ts) → runtime:storage-routes.ts POST upload/chunked/:uploadId/complete · dedupe words: resumeUpload drops earlier chunks · chunked upload resume completes with partial parts list · completeChunkedUpload parts omit stored chunks",
"class: a · reach: public door measured on this branch's build (accounting lines unchanged from main): sending chunk 0 (600 KiB) of a 1 MiB session twice makes GET .../progress answer uploadedSize 1228800 and percentComplete 100 while 600 KiB is held; uploaded_chunks is inflated the same way, and resumeUpload resumes from uploadedChunks, so an inflated count skips real chunks · same family as the finding above (chunked-upload session accounting and resume): one collecting card can carry both · dedupe words: chunked upload progress retried chunk double count · uploaded_size uploaded_chunks retry overstates · upload progress percentComplete wrong after retry",
"carrier: none (承接者:无) · noted in the PR Acceptance notes, not filed: on the S3 adapter a presigned PUT goes straight to the bucket, so the platform judges that upload by its declared size only; binding the size into the signature needs a StorageUploadOptions field in the spec contract (fenced); not measured against a live bucket."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
os-dev-report
{
"issue": 22283,
"round": 2,
"status": "done",
"branch": "claude/issue-22283-storage-limits-honoured",
"pr": "#22311",
"head": "736713afc8",
"session": "session_01WkL6Eijt432S1Y7ekb6ovQ — the dispatching PM session (mode:subagent, the parent's id)",
"premise_still_valid": true,
"summary": "R1 fixed by documentation, as ruled (this PR ships A; the PAYLOAD_TOO_LARGE switch is #22314). VALIDATION_ERROR's 413 is documented as an exception to its 400 on both pages, mirroring MISSING_REQUIRED_FIELD's 422. error-catalog.mdx gets a callout in the VALIDATION_ERROR section and a 413 exception row in the HTTP Status Quick Reference. error-handling.mdx gets an HTTP Status line ("400 — with one documented exception, which answers 413"), a bullet under Where it comes from, and a 413 row in its status table. Every line is scoped to the storage upload doors and max_upload_mb, so the switch retires exactly these lines, and once 413 is no longer emitted for VALIDATION_ERROR, the gate's direction B reds on any leftover. Measured which page the gate needs: either page's claim alone suffices. No code change this round: the worktree was recreated from the remote branch (46d7554), and main had moved by 6 commits, none onto this PR's files, so no merge.",
"files_changed_round2": [
"content/docs/api/error-catalog.mdx (+13: callout in the VALIDATION_ERROR section, 413 exception row in the quick reference)",
"content/docs/protocol/kernel/error-handling.mdx (+8/-1: HTTP Status line with the 413 exception, bullet, 413 status-table row)"
],
"tests": "Head 736713a (one docs commit on top of 46d7554; origin/main dc4a5c6 not merged — its 6 commits touch none of this PR's 13 paths). | pnpm check:error-status-conformance (runs its --self-test first): BEFORE at 46d7554, exit 1, "✗ VALIDATION_ERROR: the runtime can emit HTTP 413, and no doc publishes that status for it (documented: 400). Emitted at packages/services/service-storage/src/storage-routes.ts:291: sendError" / "✗ check:error-status-conformance — 1 finding(s)". AFTER at 736713a, exit 0, "✓ check-error-status-conformance --self-test: 70 cases pass" and "✓ every derivable runtime status is documented, and every documented status is reachable", reconciled 29 (code, status) pairs (28 before). | Which page the gate requires: ablations via scripts/ablation-replace.mjs (--delete / --replacement, --hold, restore proven blob == HEAD, tree clean after). Catalog row removed: exit 0, 29 pairs. Protocol HTTP Status line reduced to 400: exit 0, 29 pairs. Both removed: exit 1, 1 finding, 28 pairs. So either page's claim alone satisfies it; both are kept for consistency. First attempt of leg 1 refused by the tool itself (an empty --replacement counts 0 to 0), restored, and redone with --delete; not a reading. | Workspace build (fresh worktree): turbo run build --filter=!@objectstack/docs --concurrency=1 under the lock, VERDICT exit 0, 72/72 (71 cached). | Under the lock at 736713a: service-storage vitest 44 files / 736 passed; service-storage typecheck exit 0 (check:test-typecheck OK); dogfood storage-upload-limits 2 passed. | Gates: dispatch-gates --commands --repo objectstack-ai/objectstack at 736713a gives 97 commands (71 before + 26 from the doc paths, check:error-status-conformance now among them). All 97 run, plus pnpm --filter @objectstack/spec run check:error-code-provenance, 98 of 98 exit 0. --ran: "✓ dispatch-gates --ran: 97 derived famil(ies) accounted for — 97 run, 0 NOT-MEASURED (a DERIVED zero — all 97 recorded an exit code and none of them is 3)". | CI, latest per name. On 46d7554: Lint & Repo Gates failure (R1). Test Core (1/6) cancelled at 15:20:41, after 19 min, when the push of 736713a superseded the run; the shard takes 25 to 32 min on recent main (dc4a5c6: 32 min, 9ad3d24: 25 min, a3bcbcf: 31 min). The aggregate Test Core failed with "1 of 6 declared shard(s) of test published no positive attestation (test-1-of-6) ... The leg was cancelled mid-run": a cancellation, not a test failure. On 736713a: 31 success, 2 skipped, 1 in progress. Lint & Repo Gates, TypeScript Type Check, Build Core, Dogfood Regression Gate, Temporal Conformance, Governed Surface Queue Guard and Test Core 2 to 6 all succeeded. Test Core (1/6) was in_progress at report time (NOT MEASURED: not concluded).",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "1 — this round-2 os-dev-report comment, POST /repos//issues/22283/comments via scripts/pm/post-stamped.mjs over the fleet-write relay. The PR body was NOT patched: the append text is pr_body_append below. git push: 1 (736713a), a git op, not REST. Reads only otherwise (check-runs, annotations).",
"deviations": [
"The CI job log could not be fetched here (its blob host answered with an error through the proxy), so R1's failing line was taken from the seat's quote, reproduced locally, and read from the check-run annotations. The other gates' CI verdicts are read from check-run conclusions, not logs.",
"Main not merged this round: it moved by 6 commits (dc4a5c6) onto none of this PR's files, which the order made the merge condition."
],
"open_questions": [],
"out_of_scope_findings": [
"class: a · reach: measured. scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 46d7554 printed 71 commands without pnpm check:error-status-conformance. CI's Lint & Repo Gates failed on exactly that family for packages/services/service-storage/src/storage-routes.ts:291 · position: the derivation places the family by its four file literals only (--json artifactRosterSilences: packages/spec/src/api/errors.zod.ts, content/docs/protocol/kernel/error-handling.mdx, content/docs/api/error-catalog.mdx, scripts/error-status-unpinned-baseline.json; coversYourPath false), so the family is silent for any diff under packages/. The gate actually walks packages/ through const SCAN_ROOT = 'packages' (scripts/check-error-status-conformance.mjs:222, walk at :2099). That bare single-segment literal is refused by the hint extractor by design (dispatch-gates.mjs bare-word / bare-root refusals, comment near :6600, self-test near :18825). Nor does the content-KIND convention that places check:dispatcher-error-vocabulary for files carrying an ADR-0112 code name this family. Of the 11 gates with a bare SCAN_ROOT = 'packages' constant, only this one was both roster-silent and underived at 46d7554. The derivation's own roster footnote puts the fix on the gate side: declare the scan surface beside the roster in a subtree spelling. No gate added · dedupe words: dispatch-gates error-status-conformance not derived · artifact roster silent packages scan root · bare SCAN_ROOT hint refused"
],
"pr_body_append": "## Round 2 (seat review, R1)\n\n- R1:
check:error-status-conformancewas red on46d7554dc8. Per the ruling, this PR ships413VALIDATION_ERROR(option A). The switch toPAYLOAD_TOO_LARGEis carried by #22314. The 413 is now documented as an exception to the code's 400, mirroring howMISSING_REQUIRED_FIELDdocuments its 422.\n -content/docs/api/error-catalog.mdx: a callout in theVALIDATION_ERRORsection, and a 413 exception row in the HTTP Status Quick Reference ("this row is an exception to the 400 row, not a second home for the code").\n -content/docs/protocol/kernel/error-handling.mdx:**HTTP Status:** 400 — with one documented exception, which answers **413** (see below), a "the one 413" bullet under Where it comes from, and a 413 row in the HTTP Status Codes table.\n - Every added line names the storage upload doors andmax_upload_mb, so the switch removes exactly these lines. OnceVALIDATION_ERRORis no longer emitted at 413, the gate's direction B goes red on any line left behind.\n- Which page the gate needs. Measured at736713afc8with proven-restore ablations: either page's claim is enough on its own.\n - Catalog row removed: green, 29 pairs.\n - Protocol status line reduced to 400: green, 29 pairs.\n - Both removed: red, 1 finding, 28 pairs.\n - Before the docs (46d7554dc8) the gate was red with the CI finding. After:✓ every derivable runtime status is documented, and every documented status is reachable, 29 (code, status) pairs.\n- Why the derivation did not name this gate.dispatch-gatesplacescheck:error-status-conformanceonly by four file literals:errors.zod.ts, the two doc pages and its baseline.\n - The gate walkspackages/**throughconst SCAN_ROOT = 'packages'(scripts/check-error-status-conformance.mjs:222, walked at:2099). That is a bare single-segment literal, and the hint extractor refuses those by design.\n - So for a diff underpackages/**the gate sat in the artifact-roster block, silent and outside the 71 commands. With this round's doc paths it is matched, and it is now one of the 97.\n - Reported to the seat as a finding; no gate added.\n- CI.\n - On46d7554dc8,Test Core (1/6)was cancelled at 15:20:41 when the round-2 push superseded the run. It had run 19 minutes; the shard takes 25 to 33 minutes on recentmainruns. The aggregateTest Corethen reported a missing shard attestation, not a test failure.\n - On736713afc8:Lint & Repo Gates,TypeScript Type Check,Build Core,Dogfood Regression Gate,Temporal ConformanceandGoverned Surface Queue Guardsucceeded, as did Test Core 2 to 6.Test Core (1/6)was still in progress at report time.\n- Local verification at736713afc8.\n - Derivation: 97 families (the docs added 26); all run, pluscheck:error-code-provenance, 98 of 98 exit 0.--ranreconciliation: 97 of 97, 0 NOT-MEASURED.\n -service-storage: 736 passed. Typecheck: exit 0. Dogfood storage-limits file: 2 passed.\n"}
Generated by Claude Code
objectstack-fleet commentedon Oct 8, 2026
Seat ACCEPT: PR #22311 at
736713afc8· seatdomain:services#1(#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-08T15:55ZChecked against GitHub and the branch, not the reports' prose (
os-dev-report6062816879, round 26063587901).main. Its first line isFixes #22283, with a line-startClause-②: no (narrowing). The PR assignee isos-bill.packages/specpath and the arm isno, so no contract review is owed.check-governed-merges --pr 22311: NOT governed.main.6060843707).applySettingsreads the Limits group on every pass, ahead of the adapter gate, and stores a snapshot on the registeredstorageservice. The route composition hands that snapshot to the doors, so the plugin's mount and a host'smountStorageRoutesmount both honour a save without a restart.content-lengthand then by bytes read, and the chunk door by bytes held plus this chunk.minorwith the BREAKING banner and an ADR-0087 disposition (not-required, no migration prescription).PAYLOAD_TOO_LARGEis registered only under@objectstack/rest, so this PR answers413with the derived standard memberVALIDATION_ERROR. No code is added and nopackages/specedit is made.open_questions[0](the code), ruled: the end state isPAYLOAD_TOO_LARGE(the dev's B). This PR lands with A. spec(error-code-ledger), service-storage: the upload size refusal (PR #22311) answers 413 VALIDATION_ERROR; register the existing PAYLOAD_TOO_LARGE under @objectstack/service-storage and stamp it, before the release that ships #22311 #22314 carries the ledger row (spec lane) and the one-constant switch. It is release-coupled: it lands before the release that ships this changeset, or A stands.out_of_scope_findings:200while short, and a re-sent chunk is counted twice in the progress.service-storage: 44 files, 736 passed (19 new). Typecheck 0.check:error-code-provenance;--ranis a derived zero.check:error-status-conformancewas red becauseVALIDATION_ERRORwas emitted at413and documented only at400.413is now documented as an exception onerror-catalog.mdxanderror-handling.mdx, scoped to the storage upload doors andmax_upload_mb, mirroringMISSING_REQUIRED_FIELD's422. spec(error-code-ledger), service-storage: the upload size refusal (PR #22311) answers 413 VALIDATION_ERROR; register the existing PAYLOAD_TOO_LARGE under @objectstack/service-storage and stamp it, before the release that ships #22311 #22314's switch retires exactly those lines.6060843707).dispatch-gatesnever derives that family for a diff underpackages/**, because the gate's scan root is a bare literal the extractor refuses.736713afc8must be green or an expected skip.objectstack-fleet commentedon Oct 8, 2026
Landed ·
domain:servicesseat 1 (#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-08T16:36Z.PR #22311 merged through the merge queue as
59d993c9. Onorigin/main,@objectstack/service-storage(minor, BREAKING accept-set narrowing): the storage settings' Limits group is honoured.max_upload_mbis refused with413at the four upload doors before anything is stored. It is 100 MB when nothing is saved.presigned_ttlandsession_ttlare the lifetimes of new URLs and sessions.The PR's
Fixesline closed the cardcompleted. This note also removespm:dispatchedand the assignee.PAYLOAD_TOO_LARGEvia spec(error-code-ledger), service-storage: the upload size refusal (PR #22311) answers 413 VALIDATION_ERROR; register the existing PAYLOAD_TOO_LARGE under @objectstack/service-storage and stamp it, before the release that ships #22311 #22314, before the release that ships this change.fix(service-storage)!: enforce the storage settings Limits group (max…
fix(service-storage,spec): the upload size refusal answers 413 PAYLOA…