Skip to content

fix(runtime)!: the action route answers by who holds the name on the route, and the environment address answers its own item - #33274

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/area-E2b-32996
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/area-E2b-32996

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #32996
Fixes #32995
Fixes #32989
Fixes #32988

Clause-②: yes (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill under skills/** teaches how the /actions route answers when several holders (packages, a disabled package, the environment, the platform) share an action name.

维护者速读

What changes

The bare route /actions/:object/NAME (no package named) used to take its declaration from the protocol's bare by-name read. That read cannot see the routed object, so when the name has a package holder beside a package-less item (the environment's own, or the platform's) or beside a disabled package, its answer depended on who else held the name and on registration order. The route now decides those compositions from who holds the name on the route: the items bound to the routed object, else the object-less ones, read from the registry's listing of every holder (disabled packages included) and the environment's own store (readRouteHolders, routeHolderCensus):

  1. no served package's action owns the route and a disabled package's does: no declaration (main's own answer beside the environment's item, now the answer whatever other holders the name has);
  2. the environment's own item on the route's first rung: that item, no package (ADR-0150 D3, environment first on one owner key);
  3. exactly one served package on the route's first rung and no platform item there: that package's own declaration, read at its exact address and served with no package (unqualified dispatch, as before);
  4. anything else: the bare read answers, as before.

The environment's exact address /actions/_env/:object/NAME answers the environment's own item from the same reading when the merged listing omits it (it folds a package-less item into a stand-in for a package holder), whatever the other holders. environmentOwnerOfUnheldRoute (#32751) now reads through the same reader; its docblock no longer cites ruling 6824698376 letter A for the disabled-holder answer and grounds it in main's own answer (#32995 item 1).

Moved answers, measured with a probe on BASE (e540baf8) and on this head, each cell in both registration orders:

Composition Route FROM TO
env item on lead, disabled D on lead (or object-less), one enabled holder elsewhere /actions/lead/NAME env's item no declaration (narrowing; 0 and 2 enabled holders elsewhere already answered no declaration)
env item on lead or object-less, served B on contact, disabled A object-less /actions/lead/NAME env's item when A registered after B, no declaration when before no declaration (narrowing)
served B on contact, env item on lead or object-less, with or without a disabled co-holder A /actions/contact/NAME no declaration, or env's object-less item (order-dependent) B's own declaration
served B on contact, platform item on lead /actions/contact/NAME no declaration B's own declaration
served A on account, disabled D and platform item on lead /actions/account/NAME no declaration A's own declaration
env item on contact, served B on contact, disabled A registered before B /actions/contact/NAME no declaration env's item (as when A registered after B, or is absent)
env item on lead or object-less, served B on contact, disabled A on another object registered before B /actions/lead/NAME no declaration env's item (as in the other order)
env item on lead (or object-less), one or two package holders elsewhere /actions/_env/lead/NAME 404 env's own item, no package

Unchanged: one holder alone; two served packages on the route's key (refused naming both); the platform's item on the routed object (bare read's answer); every caller that names its package; every route on a host whose registry cannot list every holder or whose engine has no store.

Not fixed here, needs a decision (#32996 item 2): the registry keeps ONE package-less item per name, so a platform item bound to lead and the environment's object-less item replace each other and the later registration stays. With two package holders elsewhere, lead answers the environment's object-less item when the environment registered last, and no declaration when the platform did; the lead-bound handler runs in both. In the environment-last order the platform item is gone from the registry, so the route cannot see it; making both orders answer alike needs the registry to keep both (packages/objectql/src/registry.ts, out of this area's surface).

#32995 item 3: the clause "an item bound to lead comes before one with no object" landed in the released packages/runtime/CHANGELOG.md entry for 6aa658a. Release-owned, so not edited in this code PR; the new docblock of readRouteHolders says why it is unobservable (one package-less slot per name), and the correction of the released entry is owed as a docs-only PR.

Tests

Pins in packages/runtime/src/action-package-addressing.test.ts, every cell in both registration orders; ablations and gate readings in the report comment on #32996 (os-dev-report).

Acceptance notes

  • The protocol's bare by-name read stamps the environment's package-less item with a package holder's _packageId, and its answer beside a disabled package moves with registration order. Both live in packages/metadata-protocol (area E1); the route no longer depends on them for these compositions.

Verification

Readings follow in the os-dev-report comment on #32996; this body is the seat's to bring to the final head.

Session: https://claude.ai/code/session_016WV4cyWLSMyXZGc6dwhCAu

Head b30e9bb4 adds review#3's M1 fix: the route's composition guards run on the listing before any store read; pinned with zero store reads and no degrade for a one-holder name when the store throws.

🤖 Generated with Claude Code

…oute

WIP: holder census for the bare route, _env own-address read.

Claude-Session: https://claude.ai/code/session_016WV4cyWLSMyXZGc6dwhCAu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

11 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 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 — 30 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 b4624440e699395d1929b1eb7d77f60b4185c481 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9ec3573a06cdb448b721c9a560dab8879ad3c865 — the merge of head df8b83647c867d4e756566e0c13704549df6b044 into base b4624440e699395d1929b1eb7d77f60b4185c481, 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 9ec3573a06cdb448b721c9a560dab8879ad3c865 && git checkout 9ec3573a06cdb448b721c9a560dab8879ad3c865
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b4624440e699395d1929b1eb7d77f60b4185c481 df8b83647c867d4e756566e0c13704549df6b044 && git checkout -B drift-repro b4624440e699395d1929b1eb7d77f60b4185c481 && git merge --no-ff df8b83647c867d4e756566e0c13704549df6b044

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

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

… the route

Every cell of the four cards in both registration orders; the #32995
object-less disabled holder; the ruling-A citation reworded; changeset.

Claude-Session: https://claude.ai/code/session_016WV4cyWLSMyXZGc6dwhCAu
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface and removed size/m labels Nov 14, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head e1cae2e · 2026-11-14T15:25:21Z

review#3 (area-lease pilot #33059).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e1cae2e5934b1b05dbe9c678c154d34b7fdb6f8a
Local-runs: none
Scope: packages/runtime/src/action-execution.ts, packages/runtime/src/action-package-addressing.test.ts + packages/objectql/src/action-governance.ts (read only)

① Derived judgments

  • The decision order in routeHolderCensus (1 disabled-only on the route → none; 2 environment's item on the first rung; 3 one served package on the first rung, no platform item → that package's exact-address read, served unqualified; else the bare read) matches the FROM→TO table, and each row is pinned in both registration orders. environmentOwnerOfUnheldRoute keeps main's answer through the shared reader. The _env 404 branch only answers the environment's own item from the store-checked rung, never a package's or the platform's. Refusals of the exact-address read fall back to the bare read; other throws are degraded (ADR-0110 D3). Correct.
  • must-fix M1 — the census reads the environment store on every dispatch of a package-less action, before it knows the census applies. resolveRouteActionDeclaration now calls routeHolderCensus after every successful bare read (before this PR the shared reader ran only on the refused-read path). readRouteHolders runs environmentStoredActionNames(ql)() (full sys_metadata reads of every active action and object row, JSON-parsing each, plus the legacy-row read) whenever ANY package-less item owns the route, and only afterwards does routeHolderCensus apply its guards (!named.some(packagedItem) → bare read). Consequences:
    1. Hot path: every call of a host/plugin action or an environment-authored action (the one-holder, no-package case, i.e. most actions) pays two-plus table scans per request.
    2. Red line "phase-1 one-holder behaviour unchanged": if that store read throws, a one-holder platform or environment action that dispatched on main now answers degraded. One-holder dispatch now depends on a read it did not use before.
      Fix: read the listing first and apply the composition guards (a packaged item in named, AND a package-less item or a disabled package) before touching the store; read the store only when the census can still answer, and only for rules 1/2/3 that need it. Add a pin: one holder (platform item, and environment item) with a store whose find throws still dispatches as on BASE, and a counting find shows no store read for a one-holder name.
  • nit N1: registry.listItems('action', undefined, { includeDisabledPackages: true }) now runs per dispatch too and filters every action by name; with M1's guard it at least short-circuits cheaply. A by-name holder read (packageScopedHoldersOf) would be cheaper if the registry offers one; optional.
  • Not fixed here, correctly surfaced: runtime action route: the bare route's answer with a disabled package on the object depends on the other holders and on registration order (pre-existing on main) #32996 item 2 (one package-less slot per name in registry.ts) needs a decision; the released CHANGELOG sentence (runtime action route (#32751 follow-up): stop citing ruling A for the disabled-holder answer in code, and pin a disabled package's object-less action #32995 item 3) is owed as a docs-only PR. Both should be on the seat's list.

② Semver level

@objectstack/runtime: patch with fix(runtime)! and Clause-②: yes (narrowing) (one narrowing row: environment item beside a disabled route holder and one enabled holder elsewhere → no declaration; the rest widen) is right. No export or schema moves. Metadata-protocol: none holds. Skills-docs: none — REASON acceptable.

③ Boundary flags

④ Design judgment

Answering the bare route from who holds the name on the route, instead of from a by-name read that cannot see the object, is the right model for ADR-0150 D3/D4 and removes the registration-order dependence the cards describe. The narrowing row is main's own answer applied consistently and is declared. The design fails only on cost and isolation: the census must not touch the store, or change any outcome, for compositions it does not decide. With M1 fixed and pinned this is PASS.

Implemented-by: claude/area-E2b-32996
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: FAIL

M1 (code + pins) → delta review by review#3. N1 optional.

…ore reading the store

A name one holder holds never reads the environment's store: no scans per
dispatch, and no degraded answer while the store is down. Review M1.

Claude-Session: https://claude.ai/code/session_016WV4cyWLSMyXZGc6dwhCAu
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyWLSMyXZGc6dwhCAu
Request: review · area E2b · head b30e9bb · 2026-11-14T17:40Z

Delta for review#3 M1 (record 6879733874): composition guards run on the listing before any store read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head b30e9bb · 2026-11-14T17:40:46Z

review#3 (delta).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b30e9bb456b6a423e09b5e76a00d80044fc5dbc0
Local-runs: none
Scope: packages/runtime/src/action-execution.ts, packages/runtime/src/action-package-addressing.test.ts

Delta over my FAIL at e1cae2e5934b1b05dbe9c678c154d34b7fdb6f8a (comment 6879733874). The diff e1cae2e5...b30e9bb4 is one commit, two files, +55/-6.

① Derived judgments

  • M1 fixed. readRouteHolders now takes an admit(named, isDisabled) predicate that it asks of the registry listing before environmentStoredActionNames(ql) runs, and it answers undefined with no store read when admit refuses. routeHolderCensus passes exactly the two guards it used to apply after the read: some package holder, and either a package-less holder or a disabled one beside it. The decided compositions are therefore unchanged, and every other composition takes the bare read without touching the store. isDisabled is built before admit from the registry alone, so the predicate needs no store state. The default admit (() => true) keeps any other caller of readRouteHolders as it was.
  • Pins. For each one-holder shape (only the environment's item, only the platform's item, one package's item): with a store whose find throws, the route answers that holder and the call dispatches, so it is never degraded; and with a counting store, the route makes zero sys_metadata reads. These pin both halves of M1 (cost and outage isolation).
  • Residual, accepted: a mixed composition (a package holder beside a package-less or disabled one) still reads the store per dispatch. That is the composition the census exists to decide. N1 (by-name holder listing instead of listItems + filter) stays optional.

② Semver level

Unchanged from the first record: @objectstack/runtime: patch, fix(runtime)!, Clause-②: yes (narrowing).

③ Boundary flags

④ Design judgment

Unchanged: answering the bare route from who holds the name on the route is the right model for ADR-0150 D3/D4. With the census now consulting the store only for the compositions it decides, its cost and failure stay inside those compositions. One-holder behaviour is unchanged in both outcome and cost. The design holds.

Implemented-by: claude/area-E2b-32996
Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyWLSMyXZGc6dwhCAu
ACCEPT · area E2b (area-lease pilot #33059) · 2026-11-14T18:17Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 18:17
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 43627457171 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Test Core (8/8) — 失败步骤: Run this shard's tests

    @objectstack/client:test:  FAIL  src/auth-rotated-session-token.test.ts > #16534 ① the card's own probe, with the manual re-set deleted > login → enable → verifyTotp → disable → deleteUser, no hand-re
      ↳ 失败原因: @objectstack/client:test: Error: Test timed out in 5000ms.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • src/auth-rotated-session-token.test.ts — 24h 窗口内只有本 PR 撞到过,暂不汇总(再有一个不同 PR 撞到就会自动开汇总 issue)。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 5 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cyWLSMyXZGc6dwhCAu
Re-queue once · area E2b · head b30e9bb4 unchanged · 2026-11-14T18:53Z

Queue build 43627457171 red on a new signature: @objectstack/client src/auth-rotated-session-token.test.ts (#16534 probe) — Test timed out in 5000ms. Three-fact receipt:

  1. Import closure disjoint from the diff: the test imports @objectstack/objectql, driver-sqlite-wasm, plugin-auth, platform-objects/identity and the client's own ./index; none of packages/client/src or packages/plugins/plugin-auth/src imports @objectstack/runtime, and this PR changes only packages/runtime/src/action-execution.ts + its test + a changeset.
  2. Queue base green: main push CI run 43627446452 at e0ce1b52 is success.
  3. First error is a timeout (login → TOTP → delete flow), not an assertion.
    Re-enqueued once; the same signature again stops here and goes to the seat.

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 14, 2026
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
# Conflicts:
#	packages/runtime/src/action-package-addressing.test.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cz4f7zEQZ2C9GTG382
Request: review · area E2b · head df8b836 · delta (merge-conflict resolution only)

The queued head b30e9bb4 (PASS 6881629932) no longer merged into main: #33318 (fc283d6d, #32681) appended its own describe block to packages/runtime/src/action-package-addressing.test.ts and added a @objectstack/metadata-core import there. df8b8364 merges origin/main and resolves that one file by keeping both sides: main's import line beside this PR's widened ./action-execution.js import, and main's [#32681] describe block after this PR's [#32996, #32989, #32988] block. action-execution.ts auto-merged (with c05c0c76, #33363). No line of this PR's own code or pins changed.

Local on the merged tree: @objectstack/runtime full suite 603 files / 12098 passed, 19 skipped, 0 failed.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Review carry — PASS stands

Head-sha: df8b83647c867d4e756566e0c13704549df6b044

This head is a merge of main at fc283d6d (#33318) into the PASSed head b30e9bb456b6a423e09b5e76a00d80044fc5dbc0. I compared what this PR changes on each side: e540baf8...b30e9bb4 against fc283d6d...df8b83647c867d4e756566e0c13704549df6b044. Both touch the same file set. Every file's added and removed lines are identical, except packages/runtime/src/action-package-addressing.test.ts, where only the hunk context differs: the sorted sets of added and removed lines are identical. The conflict resolution keeps main's @objectstack/metadata-core import and its [#32681] block alongside this PR's widened action-execution import and its block. No source or pin of this PR changed. The delta PASS at b30e9bb4 carries to this head (Regen-provenance: merge-only move). Before landing, every check at the landing head must be green.

Reviewed-by: session_016WV4d1PGptBW3QRQNmy5e6 (review#3)

@objectstack-fleet

objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cz4f7zEQZ2C9GTG382
ACCEPT · area E2b (area-lease pilot #33059) · 2026-11-14T21:15Z · re-arm after merge-conflict resolution

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment