Skip to content

docs(error-handling): QUOTA_EXCEEDED has one producer, the ObjectOS agent chat route - #20220

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-19958-quota-exceeded-doc
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-19958-quota-exceeded-doc

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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).
  • 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

…gent chat route

The section said no producer emits QUOTA_EXCEEDED and told readers not to
branch on it. The ObjectOS agent chat route (POST
/api/v1/ai/agents/:agentName/chat) answers it with a nested 429 when a
deployment's per-user daily chat-turn cap is spent, but only to a JSON-mode
request (stream: false); a streaming request gets the refusal as in-band
assistant text on HTTP 200. Document that route, the measured body
(error.details.resetAt, category rate_limit, no Retry-After), and keep the
SMS TOO_MANY_REQUESTS sentence.

Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Sep 27, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 27, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 11:03
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit ce70876 Sep 27, 2026
38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19958-quota-exceeded-doc branch September 27, 2026 11:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants