Repository navigation
Commit ce70876
docs(error-handling): QUOTA_EXCEEDED has one producer, the ObjectOS agent chat route (#20220)
Fixes #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 #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 #19957 has that entry only as unchanged context, and PR #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>1 parent e8fcf55 commit ce70876
1 file changed
Lines changed: 42 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
708 | 708 | | |
709 | 709 | | |
710 | 710 | | |
711 | | - | |
712 | | - | |
| 711 | + | |
| 712 | + | |
| 713 | + | |
| 714 | + | |
| 715 | + | |
| 716 | + | |
| 717 | + | |
| 718 | + | |
| 719 | + | |
| 720 | + | |
| 721 | + | |
| 722 | + | |
| 723 | + | |
| 724 | + | |
| 725 | + | |
| 726 | + | |
| 727 | + | |
| 728 | + | |
| 729 | + | |
| 730 | + | |
| 731 | + | |
| 732 | + | |
| 733 | + | |
| 734 | + | |
| 735 | + | |
| 736 | + | |
| 737 | + | |
| 738 | + | |
| 739 | + | |
| 740 | + | |
| 741 | + | |
| 742 | + | |
| 743 | + | |
| 744 | + | |
| 745 | + | |
| 746 | + | |
| 747 | + | |
| 748 | + | |
713 | 749 | | |
714 | | - | |
715 | | - | |
716 | | - | |
717 | | - | |
| 750 | + | |
| 751 | + | |
| 752 | + | |
718 | 753 | | |
719 | | - | |
| 754 | + | |
720 | 755 | | |
721 | 756 | | |
722 | 757 | | |
| |||
0 commit comments