feat(spec)!: retire CONCURRENT_LIMIT_EXCEEDED from StandardErrorCode - #19957
Conversation
Removes the producerless 429 member from the closed catalogue (ADR-0049 enforce-or-remove, ADR-0112), on ruling A narrowed to this code alone. QUOTA_EXCEEDED stays unchanged. - The retired spelling answers with its prescription at every catalogue door: StandardErrorCode, ErrorCode (ApiErrorSchema.code) and makeApiErrorSchema, through a package-internal error map (api/retired-error-codes.ts). - The hand-written error catalogue loses its entry and its wire count moves 52 -> 51; the unpinned-status baseline is regenerated by its gate. - check-error-status-conformance's vocabulary parse now stops at the enum array's own bracket (an options object no longer lets it read the next declarations), with a self-test battery for it. - ADR-0087 D3 semantic entry, registry regenerated. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
…EEDED retirement Produced by `pnpm --filter @objectstack/spec check:generated --fix` (only check:docs was proven stale): contract.mdx, error-code-ledger.mdx and errors.mdx drop the retired member and their enum counts move by one. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 45ae7dca4cd5599bb77eca0e2fdb74a3e1f2842f && git checkout 45ae7dca4cd5599bb77eca0e2fdb74a3e1f2842f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 805af4f290565955d6e6b56ee46fed45f721d955 628738ddf436e3a4eda155861318821e14de2ded && git checkout -B drift-repro 805af4f290565955d6e6b56ee46fed45f721d955 && git merge --no-ff 628738ddf436e3a4eda155861318821e14de2ded
node scripts/docs-audit/affected-docs.mjs --json 805af4f290565955d6e6b56ee46fed45f721d955
|
Contract reviewServed-tier: Reviewed and posted 2026-09-24T08:05Z by the at-tier review subagent the ① Derived judgmentsClosure. Doors that parse a code, enumerated from every Regression. Gate script edit. New anchor ( Pins ( Sentences that ship. Changeset: ② Semver level
③ Boundary flags
Blocking: none. Implemented-by: VERDICT: PASS Generated by Claude Code |
CI reading on head
|
…regenerate contract.mdx) Brings the branch up to origin/main 03d6cb0, which already carries the VALUE_TOO_LONG / VALUE_TOO_SHORT catalog edit. The os-regen driver deferred content/docs/references/api/contract.mdx and content/docs/references/api/error-code-ledger.mdx (both sides edited them); this commit carries origin/main's side of both, per scripts/pm/os-regen-merge.sh step 2, and the regeneration of the merged tree is the next commit. content/docs/api/error-catalog.mdx merged clean: main's VALUE_TOO_LONG / VALUE_TOO_SHORT entries plus this branch's removal and count line. Claude-Session: https://claude.ai/code/session_01AsCNgFBs8HCjwhyHQsFbx3 Co-authored-by: Claude <noreply@anthropic.com>
…merged tree Discharges the os-regen deferral the merge commit recorded. Generated by `pnpm --filter @objectstack/spec build` then `pnpm --filter @objectstack/spec gen:docs` (packages/spec/scripts/build-docs.ts); a second gen:docs run is a no-op and check:docs reports 226 files in sync. The regenerated hunks are this branch's own on origin/main's side: the CONCURRENT_LIMIT_EXCEEDED rows leave the StandardErrorCode lists and the two enum summaries count one member fewer. Claude-Session: https://claude.ai/code/session_01AsCNgFBs8HCjwhyHQsFbx3 Co-authored-by: Claude <noreply@anthropic.com>
|
Patch round on this PR (director dispatch, run by session Regen-provenance: 932cbd2 — content/docs/references/api/contract.mdx regenerated by What was done (head
The
Local runs (packages/spec; exit codes read from the verify-lock VERDICT lines)
Not changed in this round: the two For the review record: the pure-regeneration carry test ( Generated by Claude Code |
Contract reviewServed-tier: ① Derived judgmentsScope of this record: the base-merge hop
② Semver levelUnchanged from 5810313042: ③ Boundary flags
Implemented-by: VERDICT: PASS Rendered at tier by the director seat (summon #30 续) on the fetched head. Not a governed path; the landing pre-checks are met on this head (fresh PASS, CI green, no conflict), so ready and auto-merge follow through the relay. Generated by Claude Code |
…level codes that really arrive (objectstack-ai#20004) Fixes objectstack-ai#19879 Clause-②: no ## What `content/docs/api/error-catalog.mdx`, the `VALUE_TOO_LONG` and `VALUE_TOO_SHORT` entries only. Both entries gave a live cause and a fix, as if a client could branch on the code. No producer emits either code. Both entries now say so, following the shape the `INVALID_FORMAT` entry got in objectstack-ai#19878: the code is reserved, no route emits it today, and a length miss arrives as a field-level `max_length` / `min_length` entry. Each entry now says where that entry rides on each path: `400 VALIDATION_FAILED` with `fields[]` for record writes and Zod-parsed request bodies (top-level on `/data`, under `details` through the runtime dispatcher), and `400 SETTINGS_VALIDATION` with `details.fields[]` for a settings write. The fix line names both envelopes. Both entries stay because the enum still declares the codes. ## Evidence (measured on `origin/main` `e8f163fc`) - **No producer.** `git grep -nE 'VALUE_TOO_(LONG|SHORT)'` outside tests hits only the enum members `packages/spec/src/api/errors.zod.ts:58-59`, the baseline rows `scripts/error-status-unpinned-baseline.json:27-28`, this page, the generated `content/docs/references/**` pages, and ADR-0114, which records these members as a known wart. A grep for other spellings (`VALUE_TOO`, `TOO_LONG`, `TOO_SHORT`) finds only the unrelated `PASSWORD_TOO_SHORT` in a plugin-auth test. Positive control: `INVALID_FORMAT` hits `errors.zod.ts:57`. - **Record writes.** `packages/objectql/src/validation/record-validator.ts:695-699` sends `fail('max_length', { maxLength, actual })` and `fail('min_length', { minLength, actual })` for `BOUNDED_STRING_FIELD_TYPES`. `buildFieldError` puts that object on the wire as `fields[].constraint`, and the envelope's top-level code is `VALIDATION_FAILED` (`VALIDATION_FAILED_CODE`, `:195`). - **Zod-parsed request bodies.** `packages/spec/src/api/zod-issues-to-fields.ts:74-81` maps `too_small` / `too_big` to `min_length` / `max_length` when the value is not a number, bigint, date, array or set. Numbers and dates map to `min_value` / `max_value`, and arrays and sets to `min_items` / `max_items`. That is why the page says "a string". The REST routes that use this send `code: 'VALIDATION_FAILED'` (for example `packages/rest/src/rest-server.ts:8960-8963`). - **Settings writes (a different envelope).** `packages/services/service-settings/src/settings-service.ts:397-400` returns `max_length` / `min_length` with `constraint { minLength?, maxLength?, actual }` for a settings value outside its declared length window. `:2145` pushes it into the errors list, and `:2167` throws `SettingsValidationError` (`settings-service.types.ts:583-584`, `code = 'SETTINGS_VALIDATION'`). `packages/services/service-settings/src/settings-routes.ts:215-217` serves it as `sendError(res, 400, 'SETTINGS_VALIDATION', …, { details: { namespace, fields } })`, and `packages/types/src/response-envelope.ts` `sendError` writes that as `{ success: false, error: { code, message, details } }`. So a settings length miss is top-level `SETTINGS_VALIDATION` with the entry in `error.details.fields[]`, not `VALIDATION_FAILED`. - **Where `VALIDATION_FAILED` puts the list.** On the `/data` routes it is flat (`packages/rest/src/error-response.ts:1152-1160`, `mapDataError`: top-level `fields`). Through the runtime dispatcher it is nested (`packages/runtime/src/dispatcher-plugin.ts:645`, `validationFailureDetails`: `details.fields`). The page's own `VALIDATION_FAILED` callout already documents both, so the entries link to it rather than restating it. - The field-level spellings are the same on all three paths: `max_length` / `min_length`. The envelope differs: `VALIDATION_FAILED` for records and Zod bodies, `SETTINGS_VALIDATION` for settings. ## Not touched - The wire-count sentence at `:6`, the `CONCURRENT_LIMIT_EXCEEDED` entry and `scripts/error-status-unpinned-baseline.json` are not changed. Open PR objectstack-ai#19957 edits them. - `VALUE_OUT_OF_RANGE` and `MISSING_REQUIRED_FIELD` are not changed. Both have producers. - The page's frontmatter and headings are not changed. ## Changeset None. This is a docs-only change under `content/docs/`, and no published package's `files[]` changes, so it falls under `skip-changeset`. The PM seat applies the label. ## Gates Derived with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` from the merge-base change set (1 path). All 41 derived commands ran on HEAD `37431df1` and exited 0. The derivation is unchanged from the first head, `ab8b19ea`. They include `pnpm check:doc-authoring`, `pnpm check:doc-anchors`, `pnpm check:nul-bytes`, `pnpm check:error-status-conformance`, `pnpm check:docs-spec-enumerations` and `pnpm --filter @objectstack/spec run check:docs`. The prerequisite closures (`lint` / `formula` / `client-react`, which pulls in `spec`) were built under the verify lock first. Reconciliation with `--ran` and the recorded exit codes: `41 derived, 41 run, 0 NOT-MEASURED, 0 UNRUN` (a derived zero). No package source changed, so no package tests or typecheck are owed. ## Rework (PM review) The first head said every length miss arrives in `VALIDATION_FAILED`, which is wrong for settings writes. The second commit, `37431df1`, names `SETTINGS_VALIDATION` + `details.fields[]` for that path in both the Cause and the Fix lines. The `#validation_failed` link resolves: `check:doc-anchors` passes, with 377 fragment links resolved. ## Acceptance notes - The page's intro counts "52 error codes reachable on the wire", but the page carries 53 code headings, and several of them are reserved codes with no emitter (`INVALID_FORMAT`, `INVALID_REFERENCE`, and now these two). Whether that count should include reserved codes is a question for the line PR objectstack-ai#19957 already edits. It is not changed here. --- _Generated by [Claude Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…gent chat route (objectstack-ai#20220) Fixes objectstack-ai#19958 Clause-②: no Docs only. One section of `content/docs/protocol/kernel/error-handling.mdx` is rewritten: the `QUOTA_EXCEEDED` entry. No runtime, spec or error-code-ledger change. `QUOTA_EXCEEDED` stays registered, as the narrowed ruling on objectstack-ai#17707 (`5807013768`) keeps it. ## What the page said, and what it says now Removed (it was false): > **Envelope:** none — no producer emits it > > **Not emitted today.** `QUOTA_EXCEEDED` is a registered member of the standard error-code catalog (…), but no ObjectStack producer emits it. No response carries this code, and none carries a quota `details` bag — do not write a client branch against it. Added (each sentence rests on a measurement below): > **Envelope:** nested — emitted by one route only, the ObjectOS agent chat route, and only to a JSON-mode request > > **One producer.** `POST /api/v1/ai/agents/:agentName/chat` answers `QUOTA_EXCEEDED` when the deployment has switched on its per-user daily chat-turn cap and the calling user has spent that cap. […] No other route emits this code. > > **JSON mode only.** The route streams by default. A request whose `stream` flag is absent or `true` gets the refusal as an ordinary assistant text message on HTTP 200, and no error code reaches it. Only a request with `stream: false` gets the 429 body below. > > `error.details.resetAt` is an ISO-8601 timestamp: the moment the cap lifts. It is the only recovery time the response carries. The route sets no `Retry-After` header and sends no `retryAfterSeconds`. Kept as it was: the SMS sentence (`the SMS daily quota answers TOO_MANY_REQUESTS`) and the pointer to `RATE_LIMIT_EXCEEDED` for request pacing. The SMS sentence still holds on `main`: `packages/services/service-sms/src/sms-daily-quota.ts:85` is `SMS_QUOTA_EXCEEDED_CODE = 'TOO_MANY_REQUESTS'`. The only change there is its first word, "Quota enforcement that does exist" → "Other quota enforcement". ## The producer, measured (cloud `main` `48d70663`) The producer and reader are cited here, not in the doc, following the dispatch's route. - `packages/service-ai/src/routes/agent-routes.ts:572-575`: `return sendError(429, 'QUOTA_EXCEEDED', message, { details: { resetAt: decision.resetAt }, category: 'rate_limit' })`. - `agent-routes.ts:527`: `const wantStreamMode = body.stream !== false;`. At `:552-565` a streaming request gets `status: 200` with the copy as one `text-delta` part, so there is no code on that path. - `packages/service-ai/src/routes/envelope.ts:188-198`: `sendError` writes `{ success: false, error: { code, message, httpStatus: status, ...extra } }`. - `packages/service-ai/src/plugin.ts:1154-1159`: the gate exists only when the deployment sets its daily-turn knob to a positive number, and the only implementation wired is `DailyMessageQuota`. Its refusal always sets `resetAt` (`quota/agent-chat-quota.ts:92-96`). - No `Retry-After` on this path. `sendError` returns status and body only, and no cloud writer adds that header to this route. - Pinned in cloud by `packages/service-ai/src/__tests__/agent-error-envelope.conformance.test.ts:300-315` (status 429, code, `details` equal to `{ resetAt }`). - Control: objectstack `main` has no producer of this code. `git grep QUOTA_EXCEEDED` at `e7f69dbb` gives 40 lines: docs, generated references, the registration in `packages/spec/src/api/errors.zod.ts:106`, a ledger test, a status baseline, and the SMS `SMS_QUOTA_EXCEEDED_*` constants whose value is `TOO_MANY_REQUESTS`. ## The readers, measured objectui, pin `f8a9d0fb` (current `.objectui-sha`) and `main` `25c7d584`. `tool-display.ts`, `tool-display.test.ts` and `useObjectChat.ts` are byte-identical between the two. - `packages/plugin-chatbot/src/tool-display.ts:314-326` and `:336`: `parseAiQuotaError` deliberately does NOT recognize `QUOTA_EXCEEDED`. - The 429 is routed by `isUnsentSendError` (`:404`) and `isRateLimitError` (`:423`). They key on the HTTP status tagged by `sendAwareFetch`, or on a raw-text regex. - objectui reads no field of this body: not `code`, not `details.resetAt`, not `category`. The card's "objectui branches on it (`error.details.resetAt`, `category`)" comes from a comment at `:317` that describes the producer. No read in objectui matches it. - objectui's chat defaults to `streamingEnabled = true` (`useObjectChat.ts:768`, sent as `stream` at `:931`). Its default path therefore receives the in-band text on HTTP 200. The 429 branch is reached only when a chatbot schema sets `streamingEnabled: false`, and it is pinned by fixture (`tool-display.test.ts:365-390`). `@objectstack/client`, this repo at `e7f69dbb`: - `client.ai.agents.chat()` sends `stream: false` (`packages/client/src/index.ts:6648-6653`). It throws an error carrying `code`, `category`, `details` and `httpStatus` (from `res.status`) (`:7376-7405`). - `chatStream()` sends `stream: true` (`:6662-6667`). Field mismatches (dispatch Zone 2 item 2): - **Sent by the producer, ignored by the reader:** - objectui ignores all of `code`, `details.resetAt`, `category` and `httpStatus`, and reads `message` only through the regex probe. - The SDK reads every sent field. - **Read by a reader, not sent:** the SDK's `error.retryable`, which is `undefined` here. ## Choices this PR settled - **"ObjectOS", not "hosted".** Triage's wording was 托管的 AI agent 对话路由. The docs' own term for where this route lives is the callout in `content/docs/ai/index.mdx`: the in-product chat runtime "ships in **ObjectOS**". The section links there. - **The example shows `httpStatus: 429`.** Triage ruled 「⛔ 不要自行补充字段」. `httpStatus` is not an added field: the producer's `sendError` writes it on every body (`envelope.ts:198`), and leaving it out would misdescribe the wire. The field list under the example names only what a client acts on: `code`, `details.resetAt`, `category` and `message`. `retryAfterSeconds` and `Retry-After` are named only as absent. - **The example `message` is illustrative.** It is the English half of `DailyMessageQuota`'s copy with an example limit of 50. The real copy is bilingual, Chinese first. The doc says to display `message`, never to parse it. - **The "JSON mode only" paragraph was not in triage's text.** It is measured and it changes what a client can rely on: a streaming client never sees the code. Dispatch Zone 2 item 3 asked for the one emitting route, not a platform-wide promise. This paragraph narrows that route to its one mode. ## Acceptance notes - **`content/docs/api/error-catalog.mdx` disagrees with the rewritten section. It is not edited here (claim file surface).** - `:507-510`: the entry sends readers to "check `retryAfterSeconds`", which this producer never sends; `details.resetAt` is the field. Its cause line says "API usage quota for the current period", while the only producer is a per-user daily chat-turn cap. - The 429 row at `:913` (`rate_limit`) agrees. - Open PR objectstack-ai#19957 has that entry only as unchanged context, and PR objectstack-ai#20170 does not touch it. Both leave the disagreement in place. - **The page's general "Nested envelope" field list** says no route-module body carries `httpStatus`, and it does not list `category`. That is true of the writer it names, `packages/types/src/response-envelope.ts`. The ObjectOS route writes through its own `sendError`, which carries both. Not edited. - **The `fetchWithRetry` example under `RATE_LIMIT_EXCEEDED` retries any 429 by `Retry-After`.** Against this 429 it would wait zero seconds and retry. The new section warns against that. The loop under "Implement Retry Logic" keys on the code and lets `QUOTA_EXCEEDED` throw, which is right. Its comment "it is present on every 429" is inaccurate for this code's 429, but the comment sits inside the `RATE_LIMIT_EXCEEDED` branch, so behaviour is unaffected. Carrier: none. - **Card pin moved.** The card cited objectui `62597c5880`; `.objectui-sha` is now `f8a9d0fb`. The cited lines are unchanged at both. ## Verification (head `6e0bb38e`) - **Derived gate set.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` gives 41 commands on the actual changed paths, identical to the dispatch lead. - **Readings.** All 41 ran, every exit code was recorded to disk before it was read, and all 41 read exit 0. - **Prerequisite refusals.** Four lines first refused with PREREQUISITE NOT MET (exit 3): `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples` and `check:docs-transcript-drift`. `check:docs` was held back until spec was built. None of those refusals counted as a reading. The lines were re-run after builds under `scripts/pm/os-verify-lock.sh`: - `turbo run build` over `@objectstack/lint...`, `@objectstack/formula` and `@objectstack/spec`: VERDICT command-exit 0. - The same over `@objectstack/client-react...` and `@objectstack/client...`: VERDICT command-exit 0. `check:skill-examples` needed this second build because it refused again, on missing client declarations. - `check:skill-examples` then read "259 prose examples type-check across 3 surface(s)". - **Reconciliation.** `dispatch-gates.mjs --ran ran.list` prints: "✓ dispatch-gates --ran: 41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero — all 41 recorded an exit code and none of them is 3)." - **Control bytes.** `pnpm check:nul-bytes` exits 0, and the control-byte self-scan of the edited file has no hits. - **Tests.** No test and no pin was added or changed: the change is prose only, and nothing is accepted or refused differently. - **Changeset.** `skip-changeset`: 0 of 69 non-private workspace packages list a `files[]` entry reaching `content/docs`. - **Newer `main`.** `origin/main` moved to `7e7fab73`, and no commit since BASE `e7f69dbb` touches either doc. --- _Generated by [Claude Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #17707
Clause-②: no
Rewritten short by the
domain:spec#5seat (2026-09-24T07:48Z). Ruling A5651023241, narrowed by5807013768; the dev report is on #17158 (5809956647). #17158 did not ride: the objectui pin imports three of its types, so it went back to the decision box (5810005336).CONCURRENT_LIMIT_EXCEEDEDleavesStandardErrorCode. The retired spelling is refused, with its prescription, at the three catalogue doors:StandardErrorCode/EnhancedApiErrorSchema.code;ErrorCode/ApiErrorSchema.code;makeApiErrorSchema.QUOTA_EXCEEDEDis unchanged.What changed
errors.zod.ts,error-code-ledger.zod.tsandcontract.zod.tspass one error map, taken from the package-internalapi/retired-error-codes.ts.content/docs/api/error-catalog.mdxloses the entry. The reference pages andscripts/error-status-unpinned-baseline.jsonwere regenerated.scripts/check-error-status-conformance.mjs: its vocabulary parse now stops at the enum array's own bracket. Without that, the new options object made it misread. The fix is pinned in the script's self-test.standard-error-code-concurrent-limit-exceeded-retired;registry.tsregenerated.@objectstack/specatminor, with a BREAKING banner, FROM → TO, and out-of-repo consumers marked NOT MEASURED.Verification (the dev's, at
e9f9b060f3)🤖 Generated with Claude Code
https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1