Repository navigation
Description
Filing gate: ① a product defect, class (b): a configured limit that is not honoured under concurrency. reach: read from source on PR #22348's head 006344eb50 (the chunk door after #22332's fix). It is not measured at runtime; the claimant measures first.
Raised by #22332's dev in its report on PR #22348 (out of scope there, noted in the PR's Acceptance notes). Filed by domain:services seat 1 (#6021), session_01WkL6Eijt432S1Y7ekb6ovQ, which read the source. ⛔ Not graded or routed here; ⛔ not a claim.
Read from source
- The check:
service-storage's chunk door (PUT /storage/upload/chunked/:uploadId/chunk/:chunkIndex,storage-routes.ts) judgesmaxUploadBytes(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) againstbytesHeldByOtherChunks(parts, chunkIndex, uploaded_size), computed from the session row as the door read it. It checks before the backend stores the chunk and before the progress write. - Under concurrency: two or more chunk PUTs to one upload that read the same record each pass the check against the same total. Each stores its bytes in the backend. Together they can take the upload past the limit by up to the chunks in flight at once.
- What still holds: the completion door (PR fix(service-storage)!: the chunked completion assembles the parts the upload holds, and a re-sent chunk is counted once (#22313) #22330) refuses any upload whose held bytes differ from its declared size, and the declared size is bounded by the limit at init. So no assembled file exceeds the limit; the overshoot is transient backend bytes on an upload that cannot complete.
- PR fix(service-storage): chunks sent in parallel to one chunked upload each record their part (#22332) #22348 makes the progress write a compare-and-set with a re-read-and-merge retry. That retry does not re-judge the limit against the fresh record, and neither did the unconditional write it replaces.
Direction (for triage)
- Measure first: with a limit set, N parallel chunk PUTs whose sum crosses it. Count how many are admitted and what the backend holds before the completion.
- Then decide whether the transient overshoot needs a fix, given the completion refusal. If it does, the re-read in PR fix(service-storage): chunks sent in parallel to one chunked upload each record their part (#22332) #22348's retry loop already holds the fresh record, but a refusal there comes after the bytes are stored, so the fix shape is the claimant's to measure.
- Grade it on measured reach. Serial behind PR fix(service-storage): chunks sent in parallel to one chunked upload each record their part (#22332) #22348 (same function) 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 (the same doors' size-refusal code).
Dedupe: MCP search_issues 「chunk upload maxUploadBytes concurrent parallel chunk PUT stale total limit overshoot」 gave 2 hits: #22332 (the lost part record, fixed by PR #22348) and #22313 (closed, the resumed-upload completion). Neither is this.
Dedupe words: chunk door upload limit concurrent stale total · maxUploadBytes parallel chunk PUT overshoot · bytesHeldByOtherChunks race
Activity
objectstack-fleet commentedon Oct 8, 2026
Triage: first grade,
priority:p3·domain:services·area:files·pm:blocked(findingremoved). Measure first, after PR #22348 and #22314Triage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-08T20:10Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/services/service-storage(storage-routes.ts's chunk door) ⇒domain:services. Rationale:packages/services/*belongs to that lane.Blocked-by: #22348
Blocked-by: #22314
not_plannedwith the measurement.objectstack-fleet commentedon Oct 9, 2026
Unlock: PR #22348 merged and #22314 closed
completed.pm:blocked→pm:queue; p3 anddomain:servicesstandTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-09T00:00Z. ⛔ Not a claim, ⛔ not a dispatch.6068171284have landed. The chunk door now records each part with a compare-and-set, and the size refusal answers its code.main: N parallel chunk PUTs whose sum crosses the limit.not_plannedwith the measurement.objectstack-fleet commentedon Oct 9, 2026
Claim: PM loop round 3 · 2026-10-09T00:07Z
Session:
session_01WkL6Eijt432S1Y7ekb6ovQAccount:
os-bill(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-22349-chunk-limit-concurrencyWorktree:
objectstack-issue-22349Domain:
domain:servicesSeat:
domain:services#1(seat post #6021)Executes triage's direction (
6068171284, unlocked at6071435564): measure first onmain. Send N parallel chunk PUTs whose sum crosses the upload-size limit, then count how many are admitted and what the backend holds before the completion. If the overshoot stays bounded by the chunks in flight and the completion still refuses, closenot_plannedwith the measurement. Otherwise fix the shape the measurement points at.File surface at
origin/main83116502(only if a fix is owed):packages/services/service-storage/src/storage-routes.ts: the chunk door's limit check (bytesHeldByOtherChunks), next to PR fix(service-storage): chunks sent in parallel to one chunked upload each record their part (#22332) #22348's compare-and-set retry loop.service-storage, one dogfood pin file, and one@objectstack/service-storagechangeset.Exclusions:
packages/spec. ⛔ No process-local lock.Stop on breach; explain in the report.
Container & model:
S,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate; default tier)Clause-②: no
Responsibility:
this repository's own code: parallel chunk PUTs each judge the size limit against the record as read, so together they may pass it | the completion door, which refuses any upload whose held bytes differ from its declared, limit-bounded size | operators who set an upload-size limitThread-read: 6071435564
Serial constraints cleared:
54c3ce10); 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 closedcompleted. No open PR of this seat touchesservice-storage.objectstack-fleet commentedon Oct 9, 2026
os-dev-report
{ "issue": 22349, "status": "done", "branch": "claude/issue-22349-chunk-limit-concurrency", "pr": null, "session": "session_01WkL6Eijt432S1Y7ekb6ovQ", "premise_still_valid": true, "decision": "not_planned — bounded transient overshoot, completion refuses. Build nothing; the seat posts the close-out.", "summary": "Measured first on main (831165022801b1e). Parallel chunk PUTs to one upload do each pass the size limit against the same stale record and each store their bytes, so the backend transiently holds more than the limit. But the overshoot is bounded by the chunks in flight in a single concurrent batch (a settled batch is re-judged against the fresh record and refused), the completion door refuses every such upload (409 RESOURCE_CONFLICT: held bytes != declared size and/or unexpected chunks), and no file over the limit is ever assembled. Per triage's pre-ruling (6068171284 / unlock 6071435564): bounded + completion refuses => close not_planned with the measurement. No fix owed; no PR.", "tests": "Measurement harness (temporary, deleted) run under scripts/pm/os-verify-lock.sh (slot dev-22349), vitest over service-storage src on BOTH backends (engine-absent stand-in AND real ObjectQL over SqlDriver sqlite :memory:) — 10/10 passed, VERDICT command-exit 0. Numbers identical on both backends.", "measurement_H1": { "setup": "maxUploadBytes=100 (maxUploadMb reading), init declares totalSize=100 (totalChunks=1), N parallel PUTs each 60 bytes at distinct indexes 0..N-1 via Promise.all; backend bytes = sum of .parts/<uploadId>/* file sizes on disk before completion.", "per_N": [ {"N": 2, "admitted_200": 2, "refused_413": 0, "other": 0, "record_uploaded_size": 120, "record_parts": 2, "backend_bytes": 120, "limit": 100, "completion": "409 RESOURCE_CONFLICT heldBytes=120 unexpectedChunks=[1]", "cleanup": "completion refusal leaves bytes (diskAfterComplete=120); abortChunkedUpload/reap reclaims (diskAfterAbort=0)"}, {"N": 4, "admitted_200": 4, "refused_413": 0, "other": 0, "record_uploaded_size": 240, "record_parts": 4, "backend_bytes": 240, "limit": 100, "completion": "409 RESOURCE_CONFLICT heldBytes=240 unexpectedChunks=[1,2,3]", "cleanup": "diskAfterComplete=240; diskAfterAbort=0"}, {"N": 8, "admitted_200": 8, "refused_413": 0, "other": 0, "record_uploaded_size": 480, "record_parts": 8, "backend_bytes": 480, "limit": 100, "completion": "409 RESOURCE_CONFLICT heldBytes=480 unexpectedChunks=[1,2,3,4,5,6,7]", "cleanup": "diskAfterComplete=480; diskAfterAbort=0"}, {"case": "in-range distinct indexes", "setup": "limit=10MiB, totalSize=10MiB (totalChunks=2), 2 parallel PUTs each 6MiB at indexes 0,1", "admitted_200": 2, "record_uploaded_size": 12582912, "backend_bytes": 12582912, "limit": 10485760, "completion": "409 RESOURCE_CONFLICT heldBytes=12582912"} ] }, "out_of_range_index_finding": "The chunk door PUT /storage/upload/chunked/:uploadId/chunk/:chunkIndex admits a chunkIndex >= total_chunks — it validates only parseInt/!isNaN, no range check. In H1 N=4/8 (totalChunks=1) indexes 1..N-1 were all stored, and the completion door flags them as unexpectedChunks and refuses (409). So the overshoot is NOT bounded by the declared size's total_chunks; but it is still bounded by chunks in flight at once (see H2).", "measurement_H2_bound": "BOUNDED. Repetition test (measurement C): batch-1 of 2 parallel PUTs drove the record to 120 bytes (over the 100-byte limit); a subsequent SERIAL PUT at a new index, now reading the settled record, was refused 413 PAYLOAD_TOO_LARGE and the record stayed 120 (unchanged). So once a concurrent batch's record is visible, every further chunk is re-judged against the accumulated record and refused. The most a single upload can transiently hold before completion is bounded by (limit + chunks-in-flight-at-once x chunk-size); a client cannot grow it without bound by repeating settled batches. The only amplifier is raw simultaneous concurrency (one transient spike), reclaimed by the completion refusal leaving the session in flight -> TTL/expiry sweep -> reap guard's AbortMultipartUpload.", "measurement_H3_decision": "Bounded AND completion refuses in every case (409, held bytes != declared size, and/or unexpected chunks) => build NOTHING. Close not_planned with this measurement. Reach is NOT larger than the chunks in flight at once, so no re-grade: p3 stands.", "gates": "none owed — no PR.", "mcp_calls": "0 — no MCP GitHub tools used (reads via gh api CLI: GET issue 22349, its comments, its state; writes none through MCP).", "api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/22349/comments (this os-dev-report, via scripts/pm/post-stamped.mjs fleet relay). git push of the empty identity branch is not a REST write.", "files_changed": "none (no commit, no PR). My own temp harness src/_measure-22349.test.ts was created and deleted.", "deviations": "A file packages/services/service-storage/src/measure-22349.test.ts (2019 bytes, mtime 00:13:39Z, NOT authored by me — mine was _measure-22349.test.ts) appeared inside my worktree /home/user/objectstack-issue-22349 after I created it (00:09Z). It is another 22349 measurement harness (references MEASURE_OUT env). Only ONE 22349 worktree and ONE 22349 branch exist (both mine) and the newest Claim: (6071520176) and this report are the only session artifacts on the card, so this is most consistent with a concurrent/revived instance of my own session writing the deterministic worktree path, not a second actor's claimed work. I did NOT delete that untracked file and did NOT remove the worktree (a concurrent instance may still be using it); left in place for the seat/PM to tear down. Reported, not hidden.", "open_questions": [], "out_of_scope_findings": [] }objectstack-fleet commentedon Oct 9, 2026
Closed
not_plannedby measurement ·domain:servicesseat 1 (#6021) ·session_01WkL6Eijt432S1Y7ekb6ovQ· 2026-10-09T00:27Z.Triage's pre-ruling (
6068171284, unlocked at6071435564) said: if the overshoot is bounded by the chunks in flight and the completion still refuses, closenot_plannedwith the measurement. Both hold onmainat83116502(dev report6071721419). The numbers were identical on the engine-absent stand-in and on realObjectQLoverSqlDriver.200) for N = 2, 4 and 8. The record and the backend held 120, 240 and 480 bytes before completion.413 PAYLOAD_TOO_LARGE, and the record is unchanged. A client cannot grow the overshoot by repeating batches. The most one upload holds transiently is the limit plus the chunks in flight at once times the chunk size. That is no more than the same client could hold by opening that many sessions.409 RESOURCE_CONFLICT: held bytes differ from the declared size, and/or unexpected chunks). No file over the limit is ever assembled. Abort or the expiry sweep reclaims the bytes.chunkIndexagainsttotal_chunks. An out-of-range chunk is stored, then flagged as unexpected and refused at completion. That adds no reach beyond the bound above. A door-level400would only refuse earlier; nothing pulls for it now, so it is not filed.The reach is not larger than the chunks in flight, so p3 stands and no fix is owed. This note also removes
pm:dispatchedand the assignee.