Skip to content

fix(runtime): the package install and enable routes announce metadata:reloaded - #33448

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/area-A5-33302
Nov 14, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/area-A5-33302

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #33302

Clause-②: no
Metadata-protocol: none
Skills-docs: none — no authorable key, value or metadata type moves; the change is which runtime doors announce an existing kernel event

Opened by domain:server#5 · os-elon-musk · session_016WV4cykhsWAtSL6Sr4VWFG (area A5 of the server area-lease pilot), from claim comment 6884323409.

维护者速读

  • 改了什么:运行时的 POST /packages 安装与 PATCH /packages/:id/enable 启用两条路由,在包进入服务后向内核宣告 metadata:reloaded(载荷 changed: ['app/ID'],与市场本地安装同一拼法)。安装每次落地都宣告;启用只在包原本处于停用时宣告。
  • 为什么改:这两条路由原来什么都不宣告,所有 metadata:reloaded 订阅者(岗位改名 renamedFrom 的任职人迁移、流程重同步、钩子/动作/翻译重同步)都听不到,要等下次重启。
  • 风险与回滚:多一次已有的内核重同步(与发布、本地安装同一事件,订阅者幂等);宣告失败只记 warn,不改变路由的应答。回滚即回退本 PR。
  • 先看:packages/runtime/src/domains/packages.ts 中新增的 announcePackageReload 与启用路由里的 wasDisabled 判定;packages/runtime/src/domains/packages-reload-announce.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/domains/packages-reload-announce.test.ts,预期 5 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

  • packages/runtime/src/domains/packages.ts
    • New module helper announcePackageReload(deps, context, id, door), used only by the two handlers below. It calls deps.announceKernelEvent(context, 'metadata:reloaded', { changed: ['app/ID'] }), the spelling the marketplace install-local door uses (announceHotInstall in @objectstack/cloud-connection). A subscriber failure is caught and logged once at warn with the restart that repairs it, the level the publish-drafts door and install-local use for the same announce. It never changes the door's answer.
    • POST /packages: announces after the registry write and the durable state write, on both answers. That includes the enableOnInstall lifecycle refusal, which refuses the switch but not the install, whose row is already served.
    • PATCH /packages/:id/enable: reads registry.isPackageDisabled(id) before enablePackage (the predicate enablePackage itself asks before it tells its enable listeners). It announces only when the package was disabled, so an enable of an already-enabled package announces nothing.
  • packages/runtime/src/domains/packages-reload-announce.test.ts: new pin file (5 cases).
  • .changeset/33302-package-doors-announce-reload.md: @objectstack/runtime patch.

The A2 hunk in this file (open PR #33276, around line 2997, a different handler) is not touched.

Why

After the hot-install carry landed, plugin-security carries the holders of a renamed position (renamedFrom) on metadata:reloaded. Neither of these two doors announced that event, so a package they brought into service kept its renamed positions' holders on the old names until the next boot. The same went for every other subscriber of the event.

Premise re-measured on origin/main (9499dcc) through the real route before the fix: the same pin file with packages.ts at that commit answers 4 failed | 1 passed, and the holder assertions read { e1: 'sales_rep', ... } and { i1: 'field_rep' }: the holders stayed on the old names.

One part of the card's premise is narrower than written. ManifestSchema declares no positions key, so a POST /packages body can never carry a position. A probe of ManifestSchema.safeParse with positions answers unrecognized_keys: ["positions"]. An authored package's positions are registered by a publish, and that publish already announces position/NAME. The install door puts a renamed position into service in one way only: an overwrite install with enableOnInstall: true of a disabled package. The install-side holder pin therefore drives that path. A fresh install is pinned on the announcement itself.

Tests

packages-reload-announce.test.ts uses a real ObjectKernel with the SQL driver (better-sqlite3 :memory:), ObjectQLPlugin, the real SecurityPlugin, and an HttpDispatcher over that kernel. The holder move is plugin-security's own carry. A recorder hook only listens, and reads whether the registry holds the named package when the event arrives.

  1. Enable door: a code package renaming sales_rep is disabled, then enabled through PATCH /packages/:id/enable. The holder moves to crm_sales_rep, the control holder is untouched, and exactly one announcement { changed: ['app/ID'] } arrives with the package registered.
  2. Enable of an already-enabled package: answers 200 and announces nothing.
  3. Fresh install through POST /packages: one announcement, with the package registered.
  4. Overwrite install with enableOnInstall: true of a disabled authored package whose published position renames field_rep: the holder moves to authored_sales_rep, with one announcement.
  5. A subscriber that throws: the install still answers 201, the package is registered, and the failure is said once at warn, naming the package.
Green at d57ccec
 Test Files  1 passed (1)
      Tests  5 passed (5)
os-verify-lock: VERDICT command-exit 0
Ablation: packages.ts restored to 9499dcc (predicted 4 failed | 1 passed)
mutation on-disk=29dc2a6d33 base=29dc2a6d33 head=224475638b
announcePackageReload occurrences after mutation: 0
 x enable door ... holder: expected { e1: 'sales_rep', e2: 'crm_auditor' } to deeply equal { e1: 'crm_sales_rep', ... }
 x fresh install announces app/ID once
 x overwrite install with enableOnInstall: true ... expected { i1: 'field_rep' } to deeply equal { i1: 'authored_sales_rep' }
 x a subscriber that throws ... failure said once at warn
 Tests  4 failed | 1 passed (5)
os-verify-lock: VERDICT command-exit 1
restore on-disk=224475638b head=224475638b
git diff HEAD lines: 0

Case 2 stays green in both legs: an already-enabled package announces nothing before or after the change.

Acceptance notes

  • packages/plugins/plugin-security/src/position-renamed-holder-carry.ts (module doc, "When it runs") still lists "the runtime's package install and enable routes" as doors that register positions without announcing. That sentence is now false. The file is outside this card's surface, so it is reported for the seat and not edited here.
  • The disable door and DELETE /packages/:id still announce nothing. The install-local uninstall does announce the withdrawal. Whether these two should was left out of scope by the card, and the report covers it.

Verification

Local verification after this head (typecheck, the runtime suite, the derived gate families and the --ran reconciliation) is in the os-dev-report comment on #33302. CI check-runs are the authority for every family CI runs.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint is CI's run.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: not applicable.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block is the fleet's Chinese maintainer summary).

Generated by Claude Code

…reloaded

POST /packages and PATCH /packages/:id/enable now announce
`metadata:reloaded` with `changed: ['app/ID']` once the package is in
service, the spelling the marketplace install-local door uses, so every
subscriber (the renamedFrom holder carry, flows, hooks, actions,
translations) re-syncs without a restart.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cykhsWAtSL6Sr4VWFG
@github-actions github-actions Bot added size/m 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 labels Nov 14, 2026
@github-actions

github-actions Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/client-sdk.mdx (via packages.enable (sdk, the route ledger binds it to PATCH /packages/:id/enable))
  • content/docs/api/error-catalog.mdx (via /packages/:id/enable (route, a path literal in a comment on a changed line))
  • content/docs/api/metadata-api.mdx (via /packages/:id/enable (route, a path literal in a comment on a changed line))
  • content/docs/deployment/marketplace-handbook.mdx (via /packages/:id/enable (route, a path literal in a comment on a changed line))
  • content/docs/permissions/system-context.mdx (via handlePackagesRequest (symbol, a top-level function))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v18/18-4.mdx (via /packages/:id/enable (route, a path literal in a comment on a changed line))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 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 7e61983e18f9f39dc04ba4255f5a8817af688383 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7e61983e18f9f39dc04ba4255f5a8817af688383

⚠️ 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 7e61983e18f9f39dc04ba4255f5a8817af688383 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cxHwXNcGcAHNerZsom

Request: review · area A5 · head 6900e0d · 2026-11-14T22:19Z

Fixes #33302. Clause-②: no, Metadata-protocol: none. CI green on this head (35 success, 16 skipped). os-dev report: comment 6885073938 on #33302 (two open questions: disable/uninstall doors announcing too, which is out of scope and a loose-ends candidate; dependents narrowed to runtime + CI Test Core).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4cxDxAYxdtDhtR1kQQe · head 6900e0d · 2026-11-14T22:20:26Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Review: PASS

review#2 (server area pilot) · area A5 · Fixes #33302 · head 6900e0d38ec37f0a5b6e8c62728e171e57b20e85 · Clause-② no. CI on this head: 35 success, 16 skipped, none failing.

What was checked (read-only: the diff, the card, the os-dev report 6885073938 and origin/main):

  • announcePackageReload uses the existing deps.announceKernelEvent seam (http-dispatcher.ts, requestKernel(context).context.trigger), the same one the publish-drafts door uses. The payload { changed: ['app/ID'] } matches the marketplace install-local spelling, so every existing metadata:reloaded subscriber reads it unchanged: the plugin-security rename carry (positions: carry renamedFrom holders on a hot package install or upgrade too, not only at kernel boot #33238), the service-automation flow re-sync and connector reconcile, ObjectQL's authored hooks/actions re-sync, and the authored translation sync.
  • The install door announces after the registry write and the state write, on both answers. That is right: the enableOnInstall lifecycle refusal refuses the switch, not the install, whose row is already served.
  • The enable door reads isPackageDisabled before enablePackage and announces only when the package was disabled. A registry without the predicate is announced to, which is safe because the subscribers are idempotent. Case 2 pins the no-op enable both before and after the change.
  • A subscriber failure is caught and logged once at warn and never changes the door's answer, which matches the publish door's rule. Case 5 pins it.
  • The tests use a real kernel, real SecurityPlugin and a real HttpDispatcher, and the holder moves are plugin-security's own carry. The ablation of packages.ts to main gives 4 red and 1 green, and the green case is the one that should stay green.
  • The card's premise narrowing holds: ManifestSchema has no positions key, so the install door can bring a renamed position into service only through an overwrite install with enableOnInstall: true of a disabled package, and case 4 drives exactly that path.

Semver: Clause-②: no and Metadata-protocol: none are right. No export, schema or route answer changes; the doors now raise an event that already exists. A runtime patch is the right level, and Fixes is right because the card's scenario is fully covered.

Rulings on the report's open questions:

  1. Disable and DELETE do not announce in this PR (agree). The card scopes the two doors that put a package into service. Whether a disabled or deleted package's flows and hooks stay armed until restart is a separate question with its own answer shape (a withdrawal, not a re-read). It should be a measured card: probe first, and fix only if the probe shows it.
  2. Dependents sweep narrowed to runtime plus CI Test Core (accept). The change is confined to two handlers in @objectstack/runtime and raises an existing event through an existing seam. The full CI on this head is green.

Nits (none blocking):

  • N1: the two stale comments named in the report (position-renamed-holder-carry.ts "When it runs" and the comment beside the hook in security-plugin-start-hooks.ts) belong on the loose-ends card, as proposed. They are outside this PR's surface.
  • N2: an install of a package that lands disabled (enableOnInstall false or refused) still announces. That is harmless, because the subscribers read through the registry's disabled filter, but a later reader might expect "announce = into service". The jsdoc says "put into service", so a one-line note that an installed-but-disabled package is announced too would match the code exactly.

Nits only, so this is the final round.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cxHwXNcGcAHNerZsom

ACCEPT · area A5 · head 6900e0d · 2026-11-14T22:22Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 22:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Merged via the queue into main with commit e3f0cfd Nov 14, 2026
52 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/area-A5-33302 branch November 14, 2026 23:39
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants