Skip to content

fix(runtime): PATCH reaches a declared AI route through the /ai/* wildcards, and an undeclared method answers 405 - #21823

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21806-ai-patch-mount
Oct 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-21806-ai-patch-mount

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21806

Clause-②: no

A declared PATCH AI route is reachable over HTTP, and a method the AI route table does not declare for a path still answers 405. Both directions are pinned over HTTP at both bases registerAIRoutes serves. The declared PATCH route here is PATCH /api/v1/ai/conversations/:id: the SDK's ai.conversations.update and the console's conversation rename.

What changed

  • packages/runtime/src/dispatcher-plugin.ts. registerAIRoutes mounts patch on the /ai/* method wildcards beside get, post, delete and put, at every base. There is no per-route case. The wildcard hands every verb to the AI route table.
  • packages/runtime/src/domains/ai.ts. The AI route table now tells its two misses apart, and the rule is the same for every verb:
    • A method the table does not declare on a path it does declare answers 405 METHOD_NOT_ALLOWED. The Allow header names exactly the declared methods, and no handler runs.
    • A path the table declares under no method still answers 404 ROUTE_NOT_FOUND.
    • The 405 body is built by buildApiError, which derives the code from the status (ADR-0112). It is hand-rolled only to add the Allow header, as domains/meta.ts and domains/mcp.ts do. No new error code.
  • scripts/check-route-envelope.mjs. The hand-built count for ai.ts goes from 1 to 2. The note gives the reason: the 405 has to carry Allow.
  • Two sibling test headers, comments only. dispatcher-plugin.route-auth-deny-body.test.ts and dispatcher-plugin.streaming-fallback.test.ts said that a PATCH route under /ai/* reaches the concrete hook-route mounts unshadowed. This PR makes that false, so the comments now describe the new behaviour. Their drives call the mounted handler directly, so what they pin is unchanged.
  • .changeset/21806-runtime-ai-patch-mount.md. @objectstack/runtime patch.

Where the 405 came from (measured)

Each reading below was taken over HTTP with the new pin file: plugin-hono-server plus the dispatcher, driven with real fetch, one committed state at a time. Both bases gave the same answers.

commit declared PATCH PATCH to a GET-only path PUT to a GET+PATCH path PATCH to an undeclared path
49c3bfb1 (pins only, main's source) adapter 405, Allow: DELETE, GET, HEAD, POST, PUT adapter 405 AI table 404 ROUTE_NOT_FOUND adapter 405
a687964e (adds the patch mount only) 200 from the handler AI table 404 ROUTE_NOT_FOUND AI table 404 AI table 404
41a9c5d7 onward (adds the table's 405) 200 from the handler 405 METHOD_NOT_ALLOWED, Allow: GET 405, Allow: GET, PATCH 404 ROUTE_NOT_FOUND

The first row reproduces the card's measurement exactly: same status, same code, same Allow list.

The dispatch order listed five mechanism assumptions. Each was measured:

  • H1 holds. The wildcard list was get, post, delete and put only.
  • H2 holds. IHttpServer.patch is a required member. The new mount is really served: a real PATCH reaches the handler at both bases (row 2).
  • H3. registerAIRoutes serves two bases: ${prefix} and ${prefix}/environments/:environmentId.
    • Under projectResolution: 'auto' it serves both.
    • Under required it serves only the scoped base.
    • With scoping off it serves only the unscoped base.
    • The pin uses auto, so every case runs at both bases.
  • H4. Nothing in this repo produces an AI route table: ai:routes and __aiRoutes are written by cloud's service-ai. The pin installs a test table instead:
    • The table has GET and PATCH on /api/v1/ai/conversations/:id and GET on /api/v1/ai/models, all auth: false.
    • It is installed on the kernel after boot, so no concrete hook-route mount exists. The wildcards are therefore the only door, as on the host where the defect was measured. That host's Allow list had no PATCH, so it had no concrete mount either.
  • H5 is falsified.
    • The 405 for an undeclared method came from the server adapter (plugin-hono-server's unmatchedResponse()), and only because PATCH was not mounted.
    • The AI table itself answered an undeclared method with 404 (row 1, the PUT column).
    • Mounting patch alone therefore turns an undeclared PATCH into a 404 (row 2), which breaks the ruling "An undeclared method still answers 405."
    • So the table now produces the 405 itself, the same way for every verb. A PATCH-only branch would have been a per-verb special case.

What a caller sees change besides the declared PATCH

  • A GET, POST, PUT or DELETE to a declared AI path under the wrong method used to answer 404 ROUTE_NOT_FOUND. It now answers 405 METHOD_NOT_ALLOWED with Allow.
  • A PATCH to an AI path the table does not declare used to answer the adapter's 405. It now answers 404 ROUTE_NOT_FOUND.
  • No request that was refused before is served now, except a PATCH to a route the table declares, so no accept set widens (Clause-② no). Only refusal codes move, as listed above.

Tests

All readings come from commits on this branch.

  • New packages/runtime/src/dispatcher-plugin.ai-wildcard-methods.integration.test.ts. At each base it pins four cases:

    1. A PATCH to the declared route answers 200 with the handler's body. The handler is called exactly once, with params.id and the JSON body.
    2. A PATCH to the GET-only path answers 405. error.code is METHOD_NOT_ALLOWED, the header is Allow: GET, and no handler is called.
    3. A PUT to the GET+PATCH path answers 405 with Allow: GET, PATCH, and no handler is called.
    4. A PATCH to an undeclared path answers 404 ROUTE_NOT_FOUND with no Allow header, and no handler is called.

    At f51a2b39 with --reporter=verbose, the result is Tests 8 passed (8).

  • Red before the fix. Reading the first two rows of the table above as failing pins: 49c3bfb1 gave 8 failed of 8, and a687964e gave 4 failed and 4 passed. The 4 that failed there were the two 405 cases at each base.

  • Ablating the mount from the finished state (936c2ca7).

    • scripts/ablation-replace.mjs deleted the patch wildcard line. The anchor went from 1 to 0, and the file's blob went from 0e4af971f8ca to 015022c8289e.
    • Result: 6 failed and 2 passed. The two that passed are the PUT cases, which the table's 405 still holds.
    • The file was restored to the HEAD blob 0e4af971f8ca, and git diff HEAD was empty afterwards.
    • The test imports the dispatcher from src, so there is no dist hop in the path being ablated.
  • @objectstack/runtime, run at 936c2ca7. The only commit after it, f51a2b39, touches only scripts/check-route-envelope.mjs, and no runtime test reads that file.

    • vitest run --project local: Test Files 326 passed (326), Tests 4632 passed | 19 skipped (4651).
    • --project repo: Test Files 3 passed (3), Tests 751 passed (751).
    • typecheck: tsc --noEmit is green. check:test-typecheck reports OK, with the debt ledger unchanged.
    • tsc -p tsconfig.test.json --listFiles includes the new test file and reports zero errors for it.
  • Gates at f51a2b39.

    • node scripts/pm/dispatch-gates.mjs --commands derived 82 families. All 82 were run, all exited 0, and --ran reconciled the list: 82 derived, 82 run, 0 NOT-MEASURED, 0 UNRUN, with every family carrying its exit code.
    • pnpm check:pm-dispatch-gates was run detached, as its header prescribes: dispatch-gates self-test: 1976 cases pass.
    • pnpm lint (the full eslint . --no-inline-config) exited 0.

Acceptance notes

  • Anonymous callers and the table's miss exits. The pre-existing 404 exit and the new 405 exit both answer without consulting the family-default anonymous gate. The pin drives them anonymously, against auth: false routes.
    • Before this PR, an anonymous PUT to a declared path got a 404, and an anonymous PATCH got the adapter's 405 listing the wildcard verbs.
    • It now gets a 405 whose Allow header lists the methods the table declares.
    • This posture is left unchanged here and raised to the seat as an open question.
  • mountRouteOnServer has no put arm. It mounts get, post, delete and patch, so a PUT route emitted through ai:routes is never mounted concretely. Under /ai/* the wildcard serves PUT, so this has no measured reach, and no producer of a PUT route outside /ai/* is named. Noted, not filed.
  • HEAD and OPTIONS routes are unreachable through the wildcards. Both are in RouteDefinition.method's vocabulary, but the wildcards never dispatch either. Hono answers HEAD from the GET wildcard and dispatches it as GET. Dormant: no producer is measured. Noted, not filed.
  • HEAD on the raw-method catch-all. createHonoApp's catch-all dispatches the raw method. On that composition, a HEAD to a GET-only AI path now answers 405 with Allow: GET instead of 404. This is the same table rule, not a special case.
  • PATCH now takes the same request path as the other verbs. The patch wildcard is registered before the concrete hook-route mounts, as the other four already were. A PATCH route that ai:routes also mounts concretely is now shadowed by the wildcard, so PATCH uses the ExecutionContext-backed req.user like the other verbs.
  • NOT MEASURED: cloud's hosted composition. There is no cloud checkout here, and per the card, staging egress is blocked. objectstack-ai/cloud#2622 moves its pin once this lands.

Generated by Claude Code

claude added 5 commits October 5, 2026 04:18
Both directions at both bases registerAIRoutes serves. Red on the unfixed
source: a declared PATCH answers the adapter's 405, and the AI route table
answers an undeclared PUT with 404 ROUTE_NOT_FOUND.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
…where they are read; changeset

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
…which carries Allow

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
@github-actions github-actions Bot added the size/m label Oct 5, 2026
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 2 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/production-readiness.mdx (via createDispatcherPlugin (symbol, a top-level function))
  • content/docs/permissions/system-context.mdx (via handleAIRequest (symbol, a top-level function))
  • content/docs/plugins/packages.mdx (via createDispatcherPlugin (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json dd58618c2838c01f343a6f859fd4bb28531097d2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7a12da1370e5326ec42a0e4db6be2efcb0c5767f — the merge of head f51a2b3944ff12ab92773b85b442ecca3a2c7067 into base dd58618c2838c01f343a6f859fd4bb28531097d2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7a12da1370e5326ec42a0e4db6be2efcb0c5767f && git checkout 7a12da1370e5326ec42a0e4db6be2efcb0c5767f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dd58618c2838c01f343a6f859fd4bb28531097d2 f51a2b3944ff12ab92773b85b442ecca3a2c7067 && git checkout -B drift-repro dd58618c2838c01f343a6f859fd4bb28531097d2 && git merge --no-ff f51a2b3944ff12ab92773b85b442ecca3a2c7067

node scripts/docs-audit/affected-docs.mjs --json dd58618c2838c01f343a6f859fd4bb28531097d2

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs dd58618c2838c01f343a6f859fd4bb28531097d2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 5, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 5, 2026 05:59
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 5, 2026 05:59
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 5, 2026
Merged via the queue into main with commit 088428f Oct 5, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21806-ai-patch-mount branch October 5, 2026 06:41
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…rehydrate refused as not loaded (objectstack-ai#21822) (objectstack-ai#21833)

Fixes objectstack-ai#21822

Clause-②: no

## What changed

After a restart whose `kernel:ready` rehydrate refused a
protocol-incompatible package (ADR-0087 D1), `GET
/api/v1/marketplace/install-local` listed it like any loaded package,
and each GET logged a `warn` that it could not read the package's seed
rows. Now the rehydrate records each entry it refuses, and the listing
serves that entry with a `notLoaded` marker carrying the refusal's code
and the declared range. The listing reads no seed rows for it, so the
per-request warn is gone. This is the direction triage ruled
(`5988934231`): **a marker, not omission**.

`packages/cloud-connection/src/marketplace-install-local-plugin.ts`
only. ⛔ No change to the refusal itself (its line, level and wording),
to DELETE, to the install route or to `handleReseed`. No `packages/spec`
path.

## The wire shape (for objectstack-ai/objectui#11645 to render)

A refused entry, as an operator (`manage_metadata`) reads it:

```json
{
  "packageId": "com.example.crm",
  "versionId": "…",
  "manifestId": "com.example.crm",
  "version": "…",
  "installedAt": "…",
  "notLoaded": { "code": "OS_PROTOCOL_INCOMPATIBLE", "requiredRange": "^16" },
  "installedBy": "…"
}
```

- `notLoaded` is CLOSED: exactly `code` and `requiredRange`, the
handshake diagnostic's own member names (the same `requiredRange` the
install door's 422 carries in `error.details`). It is not a spread of
the diagnostic.
- `notLoaded` stands **in place of** `withSampleData`. The listing does
not read the package's rows, so it makes no claim about them: the key is
omitted, not `false`. This follows the same rule as `installedBy` for a
narrowed caller.
- A narrowed caller (authenticated, no `manage_metadata`) gets the same
item without `installedBy`. The marker is served to every authenticated
caller.
- A loaded entry is byte-identical to before: `withSampleData`, no
`notLoaded` key. `total` counts marked entries.
- After `DELETE` the entry is gone from the ledger and the listing. Once
a compatible version is installed over it, the entry is listed as
loaded, with no marker.

## Where "not loaded" comes from (H3, measured on the code)

I compared two sources:
- **A**, the rehydrate's own record of what it refused.
- **B**, re-running `checkProtocolCompat` on each ledger entry per
request.

I chose **A**. It records what the rehydrate did, so it agrees with it
by construction. B is a second judgement, and it disagrees with what
happened in reachable states:
- `rehydrate` returns before the handshake loop when there is no
`manifest` service ("no `manifest` service — rehydrate skipped"). B
would mark entries whose refusal never happened.
- The listing re-reads the ledger directory on every request. The
rehydrate's own comment names a ledger shared with "a runtime of another
protocol", so an entry written there after this boot was never judged
here, and B would report a refusal this runtime never made.

The record is keyed by manifest id and holds the refused entry's
`installedAt`, which says which entry was refused. The install door is
the only writer of a new entry, it always stamps a fresh `installedAt`,
and it runs the same handshake first, so it can never write an
incompatible entry. So a compatible re-install stops the marker without
any write to the record from the install door or from DELETE.
`rehydrate` clears the record when it starts.

## Pins (the ruling's, verbatim, plus the order's addition)

- "after a restart whose rehydrate refused an entry, `GET
/install-local` lists it with the marker and the code."
- "A loadable entry is unchanged, and DELETE on the marked entry still
works."
- No seed-row read and no `withSampleData` warn for the marked entry.

Unit (`src/marketplace-install-local-listing-not-loaded.test.ts`, 7
cases): a fresh plugin over a ledger with one refused and one loadable
entry. The engine double answers only for registered objects, the way
the real one answers `Object '…' not found`. The cases cover:
- the marker, on the operator's item and the narrowed caller's;
- the closed member set;
- zero reads of the marked package's object and zero seed-row warnings,
with a lit control that the loadable entry's object was read;
- the loadable item, byte-equal to the one a ledger without the refused
entry serves;
- DELETE;
- a compatible re-install clears the marker.

Door
(`packages/qa/dogfood/test/install-local-listing-not-loaded.dogfood.test.ts`,
7 cases): real showcase boots over one `databaseFile` and one ledger.
- Boot 1 installs the CRM package (28 seed rows) and a small loadable
package.
- Between the boots, the CRM ledger entry is rewritten to declare
`^(major-1)`, an install made for an older runtime.
- Boot 2's rehydrate refuses it; the capture holds the `error` line.
- The GET serves the marker. The loadable item is byte-identical to its
boot-1 item. DELETE answers 200, and the listing then holds the loadable
package alone.
- The no-warning assertion has a lit control: an unreadable ledger file
planted before boot 2 makes the same GET log its own `warn` through the
same logger, in the same capture window.

## Reverse verification (one-off, not kept)

The plugin file was restored to the merge base (`c4d57131`, blob
`50a71c2f`) with a trap that restores `HEAD`. Each leg was proven on
disk: the `refusedAtRehydrate` count went 6 → 0 → 6, the hash equalled
the BASE blob, then the `HEAD` blob `411ad7a9`, and `git diff HEAD` came
back empty. Both suites import the plugin from `src`: the unit test by a
relative path, and the dogfood `isolated` project aliases
`@objectstack/cloud-connection` to
`../../cloud-connection/src/index.ts`. So no `dist` leg applies.

- Unit, pre-fix: **3 failed | 4 passed (7)**. The marker cases fail, and
the read/warn case shows both facts: `readsOfMarkedEntry:
["qa_old_account"]` and the warning `com.example.qaold21822: the
installed-apps listing could not read this package's seed rows
(qa_old_account: Object 'qa_old_account' not found), so it answers
withSampleData: false for it`. With the fix: 7 passed.
- Door, pre-fix: **2 failed | 5 passed (7)**. `notLoaded` is
`undefined`, and the GET logs `WARN [MarketplaceInstallLocal]
com.example.crm: the installed-apps listing could not read this
package's seed rows (crm_account: Object 'crm_account' not found;
crm_contact: …; crm_opportunity: …; crm_lead: …; crm_activity: …)`. This
is the card's own reading, reproduced. With the fix: 7 passed.

The preservation pins (the loadable entry, DELETE, the compatible
re-install, the preconditions and the capture control) pass on both
sides, as preservation pins should.

## Tests and gates

All readings are from `claude/issue-21822-listing-not-loaded-marker`
after one merge of `origin/main` (`07bf21ff`). Final head: `36817931`.

- **Build:** the `@objectstack/cloud-connection` and
`@objectstack/dogfood` dependency closures, rebuilt after the merge:
`turbo run build`, 63/63 tasks.
- **`pnpm --filter @objectstack/cloud-connection test`:** 40 files / 492
tests passed (at `6bb9f960`). The one later commit, `36817931`, changes
only the new test's engine double, and that file was re-run at
`36817931`: 7/7.
- **`pnpm --filter @objectstack/cloud-connection typecheck`:** exit 0.
`tsc --listFiles` includes the new test file.
- **`pnpm --filter @objectstack/dogfood typecheck`:** exit 0. `tsc
--listFiles` includes the new dogfood file.
- **Dogfood, narrowed:** `vitest run --project isolated` over
`install-local-listing-not-loaded`, `install-local-listing-sample-data`,
`install-local-purge-sample-data` and
`install-local-no-active-organization`: 4 files / 27 tests passed. This
narrowing is proven, not assumed: `git grep -l -E
'MarketplaceInstallLocalPlugin|marketplace/install-local' --
packages/qa/dogfood/test` at HEAD names exactly these four files. The
rest of the suite belongs to CI's Dogfood Regression Gate.
- **Gate battery:** the 67 commands `dispatch-gates --commands` derives
for this change set (the dispatch's 49 plus 18 more), all exit 0 at
`36817931`. `dispatch-gates --ran`: "67 derived, 67 run, 0 NOT-MEASURED,
0 UNRUN".
- `check:dual-build-cjs-loads` first answered `PREREQUISITE NOT MET`:
eight packages outside this closure had no `dist`. They were built and
the gate re-ran green: "106 published require entry point(s) across 66
package(s) load".
- `check:where-matcher` caught the new engine double reading a
combinator as a field name. The double now refuses what it does not
implement: 461/461 conforming.
- **`pnpm lint`** (`eslint . --no-inline-config`, the whole repository):
exit 0 at `36817931`.
- **Stale-tree note:** after the one merge, `origin/main` gained
`088428fb` (objectstack-ai#21823: the runtime dispatcher and
`scripts/check-route-envelope.mjs`). So `check:route-envelope` ran on
its pre-objectstack-ai#21823 copy. CI runs on the merge ref.

## Acceptance notes

- **Other not-loaded states stay unmarked; they are outside this ruling,
which names the protocol refusal.** None was measured through a public
door, and none is filed.
- A rehydrate whose `register` throws ("rehydrate failed for …", at
`error`) is still listed with `withSampleData`, and gets the per-request
seed-row warning.
- A cloud-snapshot install whose hot-register failed ("will load on next
restart") answers 200 and is listed as installed until the restart.
- No `manifest` service at `kernel:ready` ("rehydrate skipped") lists
every entry as installed.
- If a second not-loaded cause is ever marked, `notLoaded.code` is its
slot. Today the shape is closed to the one code.
- **Not measured here:** reseed and purge on a marked entry.
objectstack-ai/objectui#11645 already says Details offers no action that
needs a loaded package.
- **Release text:** the pending objectstack-ai#21775 changeset's "(for example, a
package the runtime did not load)" stays true, for the first case above.
- **The refusal line is unchanged:** same level (`error`), same wording.
Only its docblock now says the listing marks the entry.
- **Concurrency:** objectstack-ai#21776's dev edits `handleReseed` in the same file.
This PR touches only the rehydrate's refusal branch, the record field,
`handleList` and a helper after it, so there is no overlapping region.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/m tests tooling

Projects

None yet

2 participants