Repository navigation
docs(client,runtime): the AI slot answers 501, not 404 — with its 401-first and /ai/agents arms - #17844
Conversation
…-first and /ai/agents arms Three source sites still described the pre-`capabilityUnavailable` behaviour, saying `/ai/*` 404s "AI service is not configured" when no AI service is mounted. The dispatcher has answered 501 since the shared exit landed: the `/ai/*` routes are registered unconditionally, so a request reaches a handler with nothing behind it, which is 501 Not Implemented and not 404. The replacement is deliberately narrower than "`/ai/*` answers 501", because that sentence is not true either. Verified in `domains/ai.ts` against the unserveable-slot branch and its pins: - an anonymous caller is refused 401 first (ANONYMOUS_DENY_STATUS); - `GET /ai/agents` answers 200 with an empty list as a console courtesy; - every other route answers 501 via `capabilityUnavailable(deps, 'ai')`. The client docblock additionally gains the `/ai/agents` empty-list courtesy, which it never mentioned, and all three sites now say the 501 body comes from the shared `serviceUnavailableMessage` and so cannot drift from what discovery reports for the slot. Comment text only — no runtime behaviour changes. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
…ock correction The docblock is emitted into all four of the client's built artifacts, so the published surface moves and `skip-changeset` would be wrong. Measured on the built tree with a lit control; the runtime-side siblings publish nothing and are deliberately not named. Claude-Session: https://claude.ai/code/session_01TSf4DV7ziu4V5j73e46b7c Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 9 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 — 32 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 0d4c77d2dd74403ef47c062b8c868ce8917c5bd3 && git checkout 0d4c77d2dd74403ef47c062b8c868ce8917c5bd3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6059b29c03e470dbaa513cc3f0992a8e610855be c3b4c470f835e71ca501b385a422bf631876254c && git checkout -B drift-repro 6059b29c03e470dbaa513cc3f0992a8e610855be && git merge --no-ff c3b4c470f835e71ca501b385a422bf631876254c
node scripts/docs-audit/affected-docs.mjs --json 6059b29c03e470dbaa513cc3f0992a8e610855be
|
…ounted `packages/spec/src/api/protocol.zod.ts`'s AI Operations note said the dispatcher "404s \"AI service is not configured\"" when nothing serves the slot. It answers 501 via the shared `capabilityUnavailable` exit, and the quoted body is no longer a local string — it comes from the shared `serviceUnavailableMessage`, so the 501 body and `discovery.services.ai` cannot drift. The replacement carries the same three arms the other three live sites gained in PR #17844: anonymous -> 401 first, `GET /ai/agents` -> 200 with an empty list as a console courtesy, every other `/ai/*` route -> 501. All three measured against `domains/ai-anonymous-deny-ordering.test.ts` (13/13 passing), not copied from the card. Prose only: no schema key moves, no accept set changes, no runtime edit. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
…ounted (objectstack-ai#18597) Fixes objectstack-ai#17847 Clause-②: no `packages/spec/src/api/protocol.zod.ts`'s AI Operations note said this repo's dispatcher "404s `AI service is not configured`" when nothing serves the AI slot. It has answered **501** since the shared `capabilityUnavailable` exit landed, and the quoted body is no longer a local string either. This PR corrects that prose and nothing else — no runtime file is in the diff, and no status code, schema key or export moves. Attribution for this run, in prose so it survives any body rewrite: produced by Claude Code, session `session_01JbZnqu8bt6YqfJsr9vaFb3`, https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 ## The premise, re-measured — located by TEXT, not by line number The card warns its line numbers rotted three times in one day, so the sentence was re-located by its text against `origin/main` `e0d05538c0` (unmoved for the life of this branch): ``` $ grep -c '404s' packages/spec/src/api/protocol.zod.ts 1 $ grep -n '404s' packages/spec/src/api/protocol.zod.ts 2791:// `buildAIRoutes()` mounted, or 404s "AI service is not configured". So these ``` Counter-side, read the same round: `packages/runtime/src/domains/ai.ts` reaches `capabilityUnavailable(deps, 'ai')`, and `packages/runtime/src/domains/unavailable.ts` returns `deps.error(serviceUnavailableMessage(slot), 501)` under a docblock that states **501 Not Implemented** in words. The defect stands, and the premise is live. One extra reading the card did not carry: the literal string `AI service is not configured` survives **nowhere** in the tree except that stale comment (`grep` over `packages/`, `content/`, `examples/`, `scripts/` — one hit, the comment itself). So the sentence was stale in both halves: the status AND the body it quoted. ## Every arm was measured, not copied The card hands over its behaviour table explicitly unverified, and an unqualified "`/ai/*` answers 501" would manufacture a second inaccurate sentence. Each arm is pinned by `packages/runtime/src/domains/ai-anonymous-deny-ordering.test.ts`, run on this tree: ``` pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 --reporter=verbose \ src/domains/ai-anonymous-deny-ordering.test.ts Test Files 1 passed (1) Tests 13 passed (13) ``` | arm | the case that pins it | answer | |:--|:--|:--| | anonymous | `GET /ai/models` → 401, not the 501 remedy sentence · `GET /ai/agents` → 401, not the 200 empty-list courtesy | **401** first (`ANONYMOUS_DENY_STATUS = 401`, pinned in `core/src/security/anonymous-deny.test.ts`) | | `GET /ai/agents` | serves `/ai/agents` as the declared envelope with the payload relocated under `data` | **200**, `{ success: true, data: { agents: [] } }` | | every other `/ai/*` | 501s `/ai/models`, `/conversations`, `/usage` and `/chat` with the remedy sentence | **501** | | the body | keeps the 501 body string-identical to `/discovery`'s `services.ai` message | shared `serviceUnavailableMessage` |⚠️ One condition deliberately **not** claimed in the new prose: a serveable service whose route table has not been published yet answers **503** `AI service routes not yet initialized`. That is a different premise from "no AI service is mounted", the sentence's own scope, and the three sibling sites do not state it either. Naming it here would widen the sentence back into the tour the card forbids. ## The replacement The same three-arm text the other three live sites carry after objectstack-ai#16211 / PR objectstack-ai#17844 (`packages/client/src/index.ts`, `packages/runtime/src/route-ledger.ts`, `packages/runtime/src/domains/ai.ts`), condensed to the block-comment voice this file already uses. 12 lines replace 2. ## Changeset: owed, and measured rather than assumed `skip-changeset` is **not** owed. `packages/spec`'s `files[]` carries `src/**/*.zod.ts`, and `npm pack --dry-run --json` lists `src/api/protocol.zod.ts` among the tarball's 2016 entries (positive control: `README.md` present; negative control: zero `*.test.ts`). The sentence an upgrading author reads is a published byte, so it takes a **`patch`**. Not `major`, and `Clause-②: no` — `check:api-surface` and `check:authorable-surface` both green prove no export and no authorable key moved. ## Gates Derived off the merge base by the repo's own deriver, never by a hand-written diff: ``` node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands # 77 commands node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran RANFILE ✓ dispatch-gates --ran: 77 derived famil(ies) accounted for — 75 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3) ``` Every exit code was landed to a file before being read — never through a pipe. - **74 of 75 exit 0.** Plus the 6 artifact-roster families the deriver flags because their roster sits under one of my paths (`check-changeset-fixed`, `check:meta-url-spelling`, `check:spec-changes`, `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`) — read rather than assumed passed, all exit 0. - **`pnpm check:cross-package-test-inputs` exits 1** — the known signature (`packages/cli` descends `packages/spec/dist/`), red on a tree where `packages/spec` has been built, green where it has not. Already filed as objectstack-ai#18353 / objectstack-ai#18440; ⛔ not filed again and not caused by this diff. - **2 NOT-MEASURED, exit 3 = PREREQUISITE NOT MET:** `check:dual-build-cjs-loads` and `check:lean-entry-closure` both read built output across the whole workspace and refuse on an unbuilt package. Neither a pass nor a finding; they belong to CI's whole-tree build. - **`pnpm --filter @objectstack/spec check:generated` — all 15 generated artifacts up to date**, `check:docs` included. The block is a bare `//` comment, not a `.describe()`, so it feeds no `content/docs/references/**` page: `grep buildAIRoutes content/docs/references/` is empty while the positive control (a `.describe()` string from this same file) does land in `content/docs/references/api/protocol.mdx`. ⇒ nothing to regenerate, and nothing hand-edited under a generated tree. - **Three spec gates first answered exit 1 as a stale-`dist` refusal** (`check:api-surface`, `check:browser-reachable-entries`, `check:dual-source-exports`) — my src edit moved the build-input hash. All three exit 0 after `pnpm --filter @objectstack/spec build`, which is the documented caveat working, not a finding. ## Tests ``` pnpm --filter @objectstack/spec test Test Files 483 passed (483) · Tests 13781 passed (13781) pnpm --filter @objectstack/spec typecheck exit 0 (incl. check:test-typecheck, 144 pinned signatures held) ``` Dependency-closure build (step ①) is a no-op by construction: `packages/spec` declares no workspace dependencies, so `packages/spec^...` is empty. The package itself was built, because the `dist`-reading gates above require it. **Lint, as a declared narrowing rather than a repo sweep.** `pnpm exec eslint --no-inline-config --format json packages/spec/src/api/protocol.zod.ts` → **1 file, 0 errors, 0 warnings**, file count read from eslint's own JSON output. The checked population comes from `eslint.config.mjs` itself: every block matches `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` or a narrower glob, so the `.changeset/*.md` half of this diff is outside the linted set entirely and one file is the whole of it. Invariance for untouched files: this repo runs one `eslint.config.mjs` and it **never enables type-aware linting** — zero occurrences of `projectService`, and the only mention of `parserOptions.project` in the file is the comment stating it is absent — so no verdict on a file this diff did not touch can move because of this diff. The repo-wide `pnpm lint` stays CI's run. No ablation and no reverse-verification run is reported, and that is a statement rather than an omission: this diff adds no guard that could be ablated and changes no type that could be made to fail. The claim the prose makes is carried by an existing pin test, which is why it was run rather than read. ## Acceptance notes - `content/docs/api/plugin-endpoints.mdx` (hand-written) discusses the AI routes and does **not** repeat the false 404 claim — it simply says nothing about what an unmounted slot answers. An absence, not a wrong statement, so it is not one of the three fileable classes. noted, not filed; successor: none known — no queued PR touches that page's AI section. - The `503 AI service routes not yet initialized` exit noted above is correct as it stands and is deliberately left out of the corrected sentence. Recorded as a scope boundary, not a finding. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #16211
Clause-②: no
Re-derived on the final diff, not inherited. Every changed line is comment or docblock text with one exception, which is a data literal and not a comment: the
* /ai/**row'snote:string inROUTE_LEDGER(packages/runtime/src/route-ledger.ts). It moves no response byte all the same —route-ledger.tsis not re-exported frompackages/runtime/src/index.ts,ROUTE_LEDGERis absent frompackages/runtime/distentirely, and no non-test source reads.noteas a served value; the ledger is a CI-audit artifact whose conformance test asserts note presence (!e.note), never its text. Apart from that one string the diff adds and removes only*///prose, and a mechanical scan for any other non-comment added or removed line returns empty. No export, authorable key, accept set, status code or response byte moves.File face — declared in full
packages/client/src/index.tsainamespace docblockpackages/runtime/src/route-ledger.ts* /ai/**row'snote:prosepackages/runtime/src/domains/ai.ts.changeset/16211-ai-slot-501-not-404.md@objectstack/client: patchpackages/spec/src/api/protocol.zod.ts⛔ The out-of-face site, reported rather than fixed
packages/spec/src/api/protocol.zod.ts:2787(at6b2ec3b97; locate by the sentence) carries the same stale claim:「凡触
packages/spec一律转domain:spec座位,不论谁需要它」 — so it is left exactly as found, and ⛔ no card was filed in that lane by this seat. Thedomain:cliPM seat files the companion at ACCEPT.1. The sweep — triage's «four» is correct; the dispatch order's «three» is an artifact of its matcher
The order asked for this reconciliation as the first deliverable, with a lit and a negative control per pass. Tree:
objectstack-ai/objectstackat6b2ec3b97.Pass A — exact phrase, whole tree.
git grep -n 'AI service is not configured'→ 3 live source sites (client/src/index.ts,runtime/src/route-ledger.ts,spec/src/api/protocol.zod.ts).Lit control
not configured→ 45 files. Negative controlzzz_absent_16211→ exit 1, 0 lines.Pass B — the card's own executable criterion, widened repo-wide.
git grep -n '404s'excluding archive → 188 lines, among them the site Pass A structurally cannot see:Pass C/D — census cross-check. Every source file mentioning both
404and/ai/(49 candidates) was read. Exactly two other files state an AI-related 404 —skills/objectstack-ai/SKILL.md:344andpackages/lint/src/validate-ai-agent-authoring.ts:13— and both are a different mechanism: ADR-0063 §2 stack-authored agents refused byloadAgent(). They are correct as written and are not a fifth site.⇒ The live-source site census is four. Triage was right. The dispatch order's «three» is right for its matcher and misses
domains/ai.tsbecause that site never spells the phrase — which is precisely the site the addendum called the most surprising one, sitting two lines above its own falsifier.The order attributes «3 source files plus 4
CHANGELOG.mdoccurrences» togit grep -n 'AI service is not configured' origin/main. Re-run verbatim, that command returns 3 lines and zero CHANGELOG hits — the archive text is line-wrapped, so the exact phrase is split and cannot match:The 4 occurrences are real (2 in
packages/client/CHANGELOG.md, 2 inpackages/spec/CHANGELOG.md) but are found by the wrapped prefix, not by the matcher they are credited to. It changes no conclusion; recorded because the order asked for its own claims to be falsified rather than satisfied. ⛔CHANGELOG.mdis archive and was not edited.2. The replacement is narrower than "
/ai/*answers 501" — all three arms verified in sourceVerified directly against the unserveable-slot branch in
packages/runtime/src/domains/ai.ts, in evaluation order, ⛔ not taken from the order or the card:ANONYMOUS_DENY_STATUS = 401(packages/core/src/security/anonymous-deny.ts:66)GET /ai/agents{ agents: [] }deps.success({ agents: [] })/ai/*capabilityUnavailable(deps, 'ai')→deps.error(serviceUnavailableMessage(slot), 501)All three are already pinned in
domains/ai-anonymous-deny-ordering.test.ts(Group A anonymous-deny; Group B501with the remedy sentence and the200agents envelope). Replacing the sites with the obvious sentence would have manufactured a second inaccurate statement, so each site now carries the exceptions.Two further details the stale text lost, now stated:
/ai/*is registered unconditionally, which is why the answer is 501 and not 404; and the 501 body is not a local string — it comes from the sharedserviceUnavailableMessage, the same sentence discovery reports for the slot, so the two cannot drift.⭐ The
GET /ai/agentsempty-list courtesy was mentioned nowhere in the client docblock, the one an SDK reader actually opens. It is added there, as the deliverable triage named.3. Changeset, not
skip-changeset— measured@objectstack/clientshipsfiles: ["dist","README.md","CHANGELOG.md"], and this TSDoc is emitted into all four built artifacts (dist/index.d.ts,dist/index.d.mts,dist/index.js,dist/index.mjs) — measured on the built tree, with the stale sentence absent from every built file afterwards and a neighbouring docblock sentence present as the lit control. So the published surface moves:patch.The two runtime-side siblings publish nothing and are deliberately not named in the changeset:
ROUTE_LEDGERis absent frompackages/runtime/distentirely, and three pre-existingdomains/ai.tscomments probed as controls appear in zero built files (while that dist retains 896 comment lines, so this is not blanket comment-stripping).4. Verification — at
c3b4c470fGate families derived mechanically, ⛔ not hand-listed:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack. Each exit code captured before any pipe; each verdict read from the gate's own printed line.--ran:58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN.PREREQUISITE NOT MET(check:skill-examples,check:dual-build-cjs-loads— both read built output that did not exist yet). ⛔ Not recorded as failures: a fullpnpm buildwas run (73/73 tasks) and both were re-run to a real verdict —258 prose examples type-check across 3 surface(s)and104 published require entry point(s) across 67 package(s) load.pnpm typecheck+pnpm testfor both affected packages, under the shared heavy-verify lock (VERDICT command-exit 0):@objectstack/runtime260 files / 3623 tests passed;@objectstack/client**43 files / 519 tests passed`.pnpm lint(this lane always adds it): exit 0, no findings.check:nul-bytesgreen, plus an independent control-character scan of all four changed files: 0 hits.Named, not measured
Verified by Claude Code in session
session_01TSf4DV7ziu4V5j73e46b7c.Generated by Claude Code