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
rest: POST /api/v1/batch answers 500 for an update or delete op whose id is the empty string, where a missing id answers 400 VALIDATION_FAILED #24781
Filing gate: ① a product defect with a named location and a live reproduction.
Reader: the packages/rest lane (batch-routes.ts), next PR touching the cross-object batch door.
Dedupe (open AND closed): "batch empty string id update delete 500 DATABASE_ERROR requires an id" → 0 hits; "cross-object /api/v1/batch blank id answers 500 instead of 400" → 7 hits, none on this (nearest: #24768, a NOT NULL door-parity card; #23491, the dispatcher PATCH schema gate — both different defects).
reach: public HTTP entry, measured once (twice) — POST /api/v1/batch on a stock showcase boot answers 500.
Path: api-backend.batch-transactional-discovery
QA-source: #24779 · api-backend.batch-transactional-discovery · observed beside the clauses (no clause failed)
Checklist item priority: P1
type: Bug
What happens
POST /api/v1/batch checks up front that every update / delete op names a target id, and answers 400 VALIDATION_FAILED "Operation 'update' on 'showcase_task' requires an id" when it does not. The check is op.id == null && op.data?.id == null (packages/rest/src/batch-routes.ts, the loop after enforceBatchSize), so an id that is the empty string passes it and reaches the engine, which throws Update requires an ID or options.multi=true. That throw is answered as a server fault:
update with "id": "" → 500{"error":"Internal data error","code":"DATABASE_ERROR"}
delete with "id": "" → 500{"error":"Internal server error","code":"INTERNAL_ERROR"}
Server log: [REST] Unhandled error: Error: Update requires an ID or options.multi=true (and Delete requires …).
A malformed client request is answered as a 5xx (the caller is told the server broke, and a retry policy keyed on 5xx retries it), and the two verbs disagree on the code.
Reproduction (stock showcase, objectstack dev --seed-admin, framework 580fd4ed)
Sign in as admin@objectos.ai (POST /api/v1/auth/sign-in/email), keep the cookie.
POST /api/v1/batch body {"operations":[{"object":"showcase_task","action":"update","id":"","data":{"done":false}}],"atomic":true}
expected: 400 VALIDATION_FAILED "… requires an id" (what the same op with no id key answers)
actual: 500 DATABASE_ERROR "Internal data error"
Same with {"object":"showcase_task","action":"delete","id":""} → actual 500 INTERNAL_ERROR.
Controls on the same boot:
no id key → 400 VALIDATION_FAILED "Operation 'update' on 'showcase_task' requires an id";
"id":"nonexistent123" → 404 RECORD_NOT_FOUND;
the per-object doors already treat the empty string as absent: POST /api/v1/data/showcase_task/batch and /updateMany with {"id":""} answer a per-row VALIDATION_FAILED "Record id is required for update".
Reproduced twice on fresh requests.
Mechanism and fix direction
The cross-object door's up-front check uses == null, while the per-object doors' row check treats a blank id as missing. Treat an empty (or non-string-ish) id as missing in that check so it answers the same 400, and pin both verbs (status + code, and that the engine is never called).
Claim: PM loop round 6 · domain:cli seat 2 · p3 (filer-labelled; claimed under exception 2: the lane fits the table, and the body carries the filing gate's three answers. Triage spot-checks after) · 2026-10-20T07:07Z
Session: session_01LmjU2x1faLZ39DVTFyVca6
Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24781-batch-empty-id-400
Worktree: objectstack-issue-24781
Domain: domain:cli
Seat: domain:cli#2 (seat post #22648)
File surface (read on origin/maina43405d59; line numbers move; stop on a breach and explain it in the report):
packages/rest/src/batch-routes.ts, the cross-object /batch door's up-front id check (about :206). It reads op.id == null && op.data?.id == null, so an update / delete op whose id is the empty string passes it and reaches the engine, which throws. That throw answers 500 (DATABASE_ERROR for update, INTERNAL_ERROR for delete), where a missing id answers 400 VALIDATION_FAILED.
The fix makes a blank id answer exactly what a missing id answers today, with the same code and sentence, before any write. One rule serves both. Every other door that serves the same operation and has the same up-front check is measured: the per-object /data/:object/batch and the dispatcher's twin, if it has one. Where one shares the gap, it is fixed in the same rule and named in the PR body.
Pins in packages/rest (next to rest-batch-endpoint.test.ts): update and delete with "id": "" answer 400 VALIDATION_FAILED and write nothing. A missing id and a real id answer as before.
{
"issue": 24781,
"status": "done",
"branch": "claude/issue-24781-batch-empty-id-400",
"pr": "#24820",
"session": "session_01LmjU2x1faLZ39DVTFyVca6 (mode:subagent, the parent PM session's id; the relay envelope stamped it)",
"premise_still_valid": true,
"summary": "Premise reproduced on main a43405d with a real-engine harness: update with id "" answers 500 DATABASE_ERROR, delete with id "" answers 500 INTERNAL_ERROR. packages/rest/src/batch-routes.ts now reads each update/delete op's row address once (batchOpRowAddress: update = id, else data.id; delete = id alone, all its arm ever sent), judges it with the engine's own by-id predicates (resolveEngineUpdateDispatch / resolveEngineDeleteDispatch from @objectstack/metadata-core), and both write arms read the same address. Every address that can never name a row ('', 0, booleans, objects, arrays) now gets the missing-id 400 VALIDATION_FAILED sentence before the transaction opens. A whitespace id is a by-id address and keeps answering 404, and a data.id {$ref} keeps resolving. Also fixed in place under the bounded exemption (same check, same defect class): a delete addressed only by data.id used to pass the check and then answer 400 INVALID_FILTER from the engine; it now gets the same missing-id 400. Zone 2 item 3 measured and falsified: the per-object /data/:object/batch, updateMany and deleteMany already refuse a blank id per row (protocol falsy check), and the runtime dispatcher has no /batch branch, so there is no shared helper. Changeset graded minor, not the dispatched patch (see deviations).",
"tests": "New packages/rest/src/rest-batch-blank-id.test.ts (real ObjectQL + sqlite SqlDriver + real protocol + real route, objectQLProvider wired): 11 refusal rows assert full {status, body} plus engine.transaction never called plus the seeded row unchanged; 1 row proves an earlier create in the batch is not written; 8 controls. On main a43405d: 'Tests 12 failed | 8 passed (20)' (update rows 500 DATABASE_ERROR 'Internal data error'; delete '' rows 500 INTERNAL_ERROR 'Internal server error'; delete by data.id only 400 INVALID_FILTER). Ablation 1 (fix committed at c2ad0e6dac7e blob; ablation-replace.mjs swapped the check back to the old == null test, anchor 1 to 0, blob c2ad0e6dac7e to 7be150e2b515): 'Tests 12 failed | 8 passed (20)', the same rows; restored blob == HEAD, git diff HEAD empty. Ablation 2 (the isBatchRef carve-out deleted): exactly the $ref control failed, 'expected 400 to be 200', 'Tests 1 failed | 19 passed (20)'; restored hash-equal. The path is src (relative import of ./rest-server), so no dist rebuild was involved. Final head 0d4ea3e (after merging origin/main 307ce4c): 'pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2' gave 'Test Files 305 passed (305)', 'Tests 6144 passed | 326 skipped (6470)', VERDICT command-exit 0. 'pnpm --filter @objectstack/rest typecheck' VERDICT command-exit 0 ('check:test-typecheck: OK ... 0 file(s) / 0 error(s)'). Pre-merge, the same suite on 3c1d02d also passed 305/305.",
"gates": "Local, final head 0d4ea3e: 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' derived 67 commands (merge base 307ce4c, 3 paths). All 67 ran with exit 0, each captured before any pipe. '--ran' reconciliation: '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. pnpm lint (eslint . --no-inline-config, the family the derivation does not name) on 0d4ea3e: exit 0. Earlier run on 3c1d02d: check:query-options-erasure exit 1 (test surface 236 to 237, from my test's 'as any' read-back), fixed in 7ee83bc by typing the query, now green at 236; check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (no dist on other packages), then exit 0 on the final head. Changeset gates: check-adr-0087-registration exit 0 ('[BREAKING+bang+clause-2-narrowing] not-required (no-migration-prescription)'). check-changeset-no-major with this PR's body exit 0 ('LEVEL AXIS ... no (narrowing)'). Not run locally: test:repo (the diff touches none of its 5 files; CI Test Core); check-platform-checklist (no exported symbol renamed or deleted). CI on PR #24820 head 0d4ea3e at report time: 24 check-runs, 21 in_progress, 2 skipped, 1 success; Check Changeset in_progress; Governed Surface Queue Guard in_progress; Lint & Repo Gates, TypeScript Type Check, Test Core and Build Core not yet listed. All in_progress.",
"mcp_calls": "0 (none)",
"api_writes": "2 relay dispatches through scripts/pm/fleet-write (POST /repos/objectstack-ai/objectstack/dispatches, run as objectstack-fleet[bot]): (1) pr_create, which ran POST /repos/objectstack-ai/objectstack/pulls (draft, #24820) and POST /repos//issues/24820/assignees (os-justin); the relay read back 8796 bytes sent and 8796 stored, identical. (2) comment, which ran POST /repos//issues/24781/comments (this report). No label write: the dispatch named none, and skip-changeset does not apply. git push of the branch x5 is not REST.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #24768 · the engine's 'Update requires an ID...' / 'Delete requires...' throws still answer 500 wherever another caller reaches them, and DATABASE_ERROR and INTERNAL_ERROR disagree between the two verbs. This is the held-off door-parity region; noted in the PR's Acceptance notes, not filed."
],
"line_budget": "287 changed lines (+280 / -7, 3 files) against the 3000-line human-merge threshold: under. No skills/** and no governed surface touched.",
"deviations": [
"Changeset graded minor, not the dispatched patch. Measured on a throwaway commit object: check-changeset-no-major.mjs with the claim's 'no (narrowing)' declaration and a patch grade on @objectstack/rest exits 1 ('clause-2 declares no (narrowing) while no moved package is graded minor or above'). With minor it exits 0. AGENTS.md: a narrowing is breaking and ships minor in the launch window. The changeset carries the declaration line and an ADR-0087 not-required (no-migration-prescription) marker.",
"Scope: a delete addressed only by data.id (before 400 INVALID_FILTER, after 400 VALIDATION_FAILED missing-id) was fixed in place under the bounded exemption: same check, same defect class, same file, same gate family. It is listed in the PR's Contract table.",
"Zone 2 item 2, 'atomic true and false alike': on this door atomic:false is refused 400 BATCH_NOT_ATOMIC before the id check (measured), so the rule holds with atomic true and with atomic omitted.",
"Merged origin/main 307ce4c before opening the PR (AGENTS.md section 10). Install, build, the rest suite, typecheck, the gate union and lint were all re-run on the merge head 0d4ea3e.",
"Commit trailers use the model-free Claude-Session / Co-authored-by pair that AGENTS.md and the pre-push hook require, not the harness reminder's model-named trailer.",
"Worktree cleaned after the PR opened: node_modules removed and 'git worktree remove' exited 0."
],
"files_changed": [
".changeset/24781-rest-batch-blank-id-400.md (+23)",
"packages/rest/src/batch-routes.ts (+70 / -7)",
"packages/rest/src/rest-batch-blank-id.test.ts (+187, new)"
]
}
ACCEPT → PR #24820 comment 6286890326 · domain:cli seat 2 · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T08:34Z · head 0d4ea3e5b. The review of record is 6286879313 (CONTRACT_REVIEW_TIER, PASS). A blank, falsy or non-scalar id at POST /batch now gets the missing-id 400 before the transaction opens, and no request that succeeded before is refused. Landing waits on every check of the head and on this lane's main-red #24780 (row 3).
Merged through the merge queue (added_to_merge_queue 2026-10-20T09:17Z, merged 2026-10-20T09:45Z)
origin/main
5fcc79497 is on it, with a single parent 99e4eaa22 (squash). 3 files, +280 / −7. Each file's blob equals the ACCEPTed head 0d4ea3e5b's (c2ad0e6da, 64f091a3a, ca4482a4c)
Queue branch
No gh-readonly-queue/main/*pr-24820* head remains
This card
Closed completed by the PR's Fixes. In the stroke after this comment, pm:dispatched comes off
Review chain: claim 6285421221 → report 6286676584 → review of record 6286879313 (CONTRACT_REVIEW_TIER, PASS) → ACCEPT 6286890326 on the PR → pointer on this card.
What landed: the cross-object POST /batch, and its environment-scoped twin, judge an update / delete op's row address with the engine's own by-id predicates. A blank, falsy or non-scalar address gets the missing-id 400 VALIDATION_FAILED before the transaction opens. No request that answered 2xx or wrote a row before is refused. @objectstack/rest ships it as minor, BREAKING, under the launch-window convention.
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 6 ·
domain:cliseat 2 · p3 (filer-labelled; claimed under exception 2: the lane fits the table, and the body carries the filing gate's three answers. Triage spot-checks after) · 2026-10-20T07:07ZSession:
session_01LmjU2x1faLZ39DVTFyVca6Account:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24781-batch-empty-id-400Worktree:
objectstack-issue-24781Domain:
domain:cliSeat:
domain:cli#2(seat post #22648)File surface (read on
origin/maina43405d59; line numbers move; stop on a breach and explain it in the report):packages/rest/src/batch-routes.ts, the cross-object/batchdoor's up-front id check (about:206). It readsop.id == null && op.data?.id == null, so anupdate/deleteop whose id is the empty string passes it and reaches the engine, which throws. That throw answers500(DATABASE_ERRORfor update,INTERNAL_ERRORfor delete), where a missing id answers400 VALIDATION_FAILED./data/:object/batchand the dispatcher's twin, if it has one. Where one shares the gap, it is fixed in the same rule and named in the PR body.packages/rest(next torest-batch-endpoint.test.ts):updateanddeletewith"id": ""answer400 VALIDATION_FAILEDand write nothing. A missing id and a real id answer as before.@objectstack/rest(patch).Update requires an ID…/Delete requires…throws and their error mapping, which are A database NOT NULL refusal answers differently per door: the dispatcher /data twin answers 500 INTERNAL_ERROR, and REST batch/import/form/action rows name the column key (RestServer POST/PATCH /data answer 400 with the label after #24715) #24768's region of door parity (open, untriaged);content/docs/references/api/batch.mdx, which PR feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression #24778 touches;packages/spec/**.Container & model:
S,mode:subagent,model: default tier(dispatch-gates.mjs --tier: "no path-derived mandate")Clause-②: no (narrowing)
Basis for the Clause-② line: a request the door accepted into the engine (and answered
500) is refused400up front. That narrows a public REST door's accept set and widens nothing. It owes oneCONTRACT_REVIEW_TIERreview of record before the queue, and it stays in this lane.Responsibility: this lane's REST door (
packages/rest). No platform path covers it: the engine refuses the write but answers it as a server fault. It is reachable by any authenticated/api/v1/batchclient, and it was measured on a stock showcase boot (card).Thread-read: none
Main-red hold: this lane's main-red main red on the full battery since 04:01Z: Dogfood 1/3 (audit-log-audit-capability, SystemWriteOrganizationRequiredError on sys_user_permission_set after the walled lift window) and Console Pin Gate (pinned objectui reads retired filterableFields) #24780 is open, so this card's PR does not arm the queue until main red on the full battery since 04:01Z: Dogfood 1/3 (audit-log-audit-capability, SystemWriteOrganizationRequiredError on sys_user_permission_set after the walled lift window) and Console Pin Gate (pinned objectui reads retired filterableFields) #24780's row 1 lands.
Serial constraints cleared:
packages/rest/src/batch-routes.tsor its batch tests. All 18 open PRs were read in this act. PR fix(lint)!: the overlay save door refuses a list view bound to an object that exists nowhere (#24668) #24789 touches three otherpackages/rest/src/meta-*tests, and PR feat(spec,objectql): report a caller value a stored expression replaced, as DroppedFieldsEvent reason stored_expression #24778 touchesbatch.mdx(held off) and anotherresttest.plugin-hono-server, CORS) and main red on the full battery since 04:01Z: Dogfood 1/3 (audit-log-audit-capability, SystemWriteOrganizationRequiredError on sys_user_permission_set after the walled lift window) and Console Pin Gate (pinned objectui reads retired filterableFields) #24780 (a dogfood fixture), are disjoint. Seat 1 has no card in flight in this lane at this read.pm:on-holdcards'Restart-touch:lines (15 cards, 1 line:scripts/test-shard-timings.json) have no hit.qa(checklist): clauses for three defects round 3 found outside the le…
qa(checklist): record the a43405d re-measure of the batch and validat…
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24781,
"status": "done",
"branch": "claude/issue-24781-batch-empty-id-400",
"pr": "#24820",
"session": "session_01LmjU2x1faLZ39DVTFyVca6 (mode:subagent, the parent PM session's id; the relay envelope stamped it)",
"premise_still_valid": true,
"summary": "Premise reproduced on main a43405d with a real-engine harness: update with id "" answers 500 DATABASE_ERROR, delete with id "" answers 500 INTERNAL_ERROR. packages/rest/src/batch-routes.ts now reads each update/delete op's row address once (batchOpRowAddress: update = id, else data.id; delete = id alone, all its arm ever sent), judges it with the engine's own by-id predicates (resolveEngineUpdateDispatch / resolveEngineDeleteDispatch from @objectstack/metadata-core), and both write arms read the same address. Every address that can never name a row ('', 0, booleans, objects, arrays) now gets the missing-id 400 VALIDATION_FAILED sentence before the transaction opens. A whitespace id is a by-id address and keeps answering 404, and a data.id {$ref} keeps resolving. Also fixed in place under the bounded exemption (same check, same defect class): a delete addressed only by data.id used to pass the check and then answer 400 INVALID_FILTER from the engine; it now gets the same missing-id 400. Zone 2 item 3 measured and falsified: the per-object /data/:object/batch, updateMany and deleteMany already refuse a blank id per row (protocol falsy check), and the runtime dispatcher has no /batch branch, so there is no shared helper. Changeset graded minor, not the dispatched patch (see deviations).",
"tests": "New packages/rest/src/rest-batch-blank-id.test.ts (real ObjectQL + sqlite SqlDriver + real protocol + real route, objectQLProvider wired): 11 refusal rows assert full {status, body} plus engine.transaction never called plus the seeded row unchanged; 1 row proves an earlier create in the batch is not written; 8 controls. On main a43405d: 'Tests 12 failed | 8 passed (20)' (update rows 500 DATABASE_ERROR 'Internal data error'; delete '' rows 500 INTERNAL_ERROR 'Internal server error'; delete by data.id only 400 INVALID_FILTER). Ablation 1 (fix committed at c2ad0e6dac7e blob; ablation-replace.mjs swapped the check back to the old == null test, anchor 1 to 0, blob c2ad0e6dac7e to 7be150e2b515): 'Tests 12 failed | 8 passed (20)', the same rows; restored blob == HEAD, git diff HEAD empty. Ablation 2 (the isBatchRef carve-out deleted): exactly the $ref control failed, 'expected 400 to be 200', 'Tests 1 failed | 19 passed (20)'; restored hash-equal. The path is src (relative import of ./rest-server), so no dist rebuild was involved. Final head 0d4ea3e (after merging origin/main 307ce4c): 'pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2' gave 'Test Files 305 passed (305)', 'Tests 6144 passed | 326 skipped (6470)', VERDICT command-exit 0. 'pnpm --filter @objectstack/rest typecheck' VERDICT command-exit 0 ('check:test-typecheck: OK ... 0 file(s) / 0 error(s)'). Pre-merge, the same suite on 3c1d02d also passed 305/305.",
"gates": "Local, final head 0d4ea3e: 'node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack' derived 67 commands (merge base 307ce4c, 3 paths). All 67 ran with exit 0, each captured before any pipe. '--ran' reconciliation: '67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN', exit 0. pnpm lint (eslint . --no-inline-config, the family the derivation does not name) on 0d4ea3e: exit 0. Earlier run on 3c1d02d: check:query-options-erasure exit 1 (test surface 236 to 237, from my test's 'as any' read-back), fixed in 7ee83bc by typing the query, now green at 236; check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (no dist on other packages), then exit 0 on the final head. Changeset gates: check-adr-0087-registration exit 0 ('[BREAKING+bang+clause-2-narrowing] not-required (no-migration-prescription)'). check-changeset-no-major with this PR's body exit 0 ('LEVEL AXIS ... no (narrowing)'). Not run locally: test:repo (the diff touches none of its 5 files; CI Test Core); check-platform-checklist (no exported symbol renamed or deleted). CI on PR #24820 head 0d4ea3e at report time: 24 check-runs, 21 in_progress, 2 skipped, 1 success; Check Changeset in_progress; Governed Surface Queue Guard in_progress; Lint & Repo Gates, TypeScript Type Check, Test Core and Build Core not yet listed. All in_progress.",
"mcp_calls": "0 (none)",
"api_writes": "2 relay dispatches through scripts/pm/fleet-write (POST /repos/objectstack-ai/objectstack/dispatches, run as objectstack-fleet[bot]): (1) pr_create, which ran POST /repos/objectstack-ai/objectstack/pulls (draft, #24820) and POST /repos//issues/24820/assignees (os-justin); the relay read back 8796 bytes sent and 8796 stored, identical. (2) comment, which ran POST /repos//issues/24781/comments (this report). No label write: the dispatch named none, and skip-changeset does not apply. git push of the branch x5 is not REST.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #24768 · the engine's 'Update requires an ID...' / 'Delete requires...' throws still answer 500 wherever another caller reaches them, and DATABASE_ERROR and INTERNAL_ERROR disagree between the two verbs. This is the held-off door-parity region; noted in the PR's Acceptance notes, not filed."
],
"line_budget": "287 changed lines (+280 / -7, 3 files) against the 3000-line human-merge threshold: under. No skills/** and no governed surface touched.",
"deviations": [
"Changeset graded minor, not the dispatched patch. Measured on a throwaway commit object: check-changeset-no-major.mjs with the claim's 'no (narrowing)' declaration and a patch grade on @objectstack/rest exits 1 ('clause-2 declares no (narrowing) while no moved package is graded minor or above'). With minor it exits 0. AGENTS.md: a narrowing is breaking and ships minor in the launch window. The changeset carries the declaration line and an ADR-0087 not-required (no-migration-prescription) marker.",
"Scope: a delete addressed only by data.id (before 400 INVALID_FILTER, after 400 VALIDATION_FAILED missing-id) was fixed in place under the bounded exemption: same check, same defect class, same file, same gate family. It is listed in the PR's Contract table.",
"Zone 2 item 2, 'atomic true and false alike': on this door atomic:false is refused 400 BATCH_NOT_ATOMIC before the id check (measured), so the rule holds with atomic true and with atomic omitted.",
"Merged origin/main 307ce4c before opening the PR (AGENTS.md section 10). Install, build, the rest suite, typecheck, the gate union and lint were all re-run on the merge head 0d4ea3e.",
"Commit trailers use the model-free Claude-Session / Co-authored-by pair that AGENTS.md and the pre-push hook require, not the harness reminder's model-named trailer.",
"Worktree cleaned after the PR opened: node_modules removed and 'git worktree remove' exited 0."
],
"files_changed": [
".changeset/24781-rest-batch-blank-id-400.md (+23)",
"packages/rest/src/batch-routes.ts (+70 / -7)",
"packages/rest/src/rest-batch-blank-id.test.ts (+187, new)"
]
}
objectstack-fleet commentedon Oct 20, 2026
ACCEPT → PR #24820 comment
6286890326·domain:cliseat 2 · sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-20T08:34Z · head0d4ea3e5b. The review of record is6286879313(CONTRACT_REVIEW_TIER, PASS). A blank, falsy or non-scalar id atPOST /batchnow gets the missing-id400before the transaction opens, and no request that succeeded before is refused. Landing waits on every check of the head and on this lane's main-red #24780 (row 3).Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Still reproduces at 307ce4c · QA-source: #24841 · api-backend.batch-transactional-discovery · observed beside the clauses (update with
"id":""→ 500 DATABASE_ERROR, delete → 500 INTERNAL_ERROR, twice each)Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
Landed: PR #24820 →
5fcc79497(Fixes #24781)domain:cliseat 2 ·os-justin· sessionsession_01LmjU2x1faLZ39DVTFyVca6· 2026-10-20T09:45Zadded_to_merge_queue2026-10-20T09:17Z, merged 2026-10-20T09:45Z)origin/main5fcc79497is on it, with a single parent99e4eaa22(squash). 3 files, +280 / −7. Each file's blob equals the ACCEPTed head0d4ea3e5b's (c2ad0e6da,64f091a3a,ca4482a4c)gh-readonly-queue/main/*pr-24820*head remainscompletedby the PR'sFixes. In the stroke after this comment,pm:dispatchedcomes off6285421221→ report6286676584→ review of record6286879313(CONTRACT_REVIEW_TIER, PASS) → ACCEPT6286890326on the PR → pointer on this card.POST /batch, and its environment-scoped twin, judge anupdate/deleteop's row address with the engine's own by-id predicates. A blank, falsy or non-scalar address gets the missing-id400 VALIDATION_FAILEDbefore the transaction opens. No request that answered2xxor wrote a row before is refused.@objectstack/restships it asminor, BREAKING, under the launch-window convention.… requires an IDthrows still answer500for other callers, which is A database NOT NULL refusal answers differently per door: the dispatcher /data twin answers 500 INTERNAL_ERROR, and REST batch/import/form/action rows name the column key (RestServer POST/PATCH /data answer 400 with the label after #24715) #24768's door-parity region.Generated by Claude Code