Skip to content

fix(objectql): an artifact hot reload refreshes every registry entry the boot registered, so an edited email template is what the next send renders - #27596

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-mtq6wj
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-mtq6wj

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #27488

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:os dev 热重载 artifact 后,引擎把它已装的包重新登记一遍,邮件模板、流程、仪表盘、页面、按钮等所有类型的列表读与发送读都跟上新版本。
  • 为什么改:开发者改了邮件模板源码,按名字读已经是新文案,但列表和实际发出的邮件还是旧文案,要冷重启才生效;流程热重载也绑的是旧定义。
  • 风险与回滚:只走开发热重载路径,冷启动和 PUT /meta 保存不受影响(有测试钉住);只刷新已装的包,禁用状态保留;回滚即还原 packages/objectql/src/plugin.ts 一处。
  • 先看:packages/objectql/src/plugin.ts(refreshReloadedPackages 与它在 metadata:reloaded 钩子里的调用)、packages/runtime/src/standalone-stack-artifact-reload-registry-refresh.test.ts、packages/plugins/plugin-email/src/template-artifact-reload.integration.test.ts。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/standalone-stack-artifact-reload-registry-refresh.test.ts,预期 3 条全绿;去掉钩子里那一行调用则第一条转红,列表读停在 v1。
  • 需要你做的:无,席位落地。

What changes

The boot registers an artifact twice: the artifact door (MetadataPlugin) into the metadata service, and AppPlugin into the SchemaRegistry through the manifest service. A hot reload (artifact-file watcher or HMR POST, both _reloadAndAnnounce) re-registered only the first, and ObjectQL's metadata:reloaded hook re-ingested objects alone. The by-name read asks the metadata service first, so it served the edit; the list read and its execution face (getMetaItemsForExecution) lay the registry over the metadata service, so they kept the boot copy until a cold restart. The email plugin renders a send from that execution face, and the automation engine re-binds flows from it on the same reload.

The hook now calls refreshReloadedPackages before the object ingest: each package body on the payload that the registry already holds (source code or marketplace) is re-registered through registerApp, the body door the boot used, under its installed source, with resolveArtifactPackageEntries for the order (the call the manifest service makes). installPackage keeps the lifecycle on a re-install, so a disabled package stays disabled. A package the boot never put in the registry is left to the artifact door as before (nothing of it is stale there). A body that fails to re-register is named in one warn line and keeps its previous entries; the reload goes on.

Scope (the card's first deliverable): measured on the real createStandaloneStack composition, every registry-backed type was stale, not only email_template. With each type at v1 at boot and v2 in the file, before the fix:

type list execution face by name
email_template, dashboard, flow, page, action v1 v1 v2
object v2 v2 v2

So the fix refreshes every collection registerApp decomposes. That changes no semantics for the other types: it makes their list and execution reads agree with the by-name read, the same defect. The flow re-bind on reload (service-automation's metadata:reloaded re-sync) now binds the edited flow, which is what that re-sync was written for.

File surface: packages/objectql/src/plugin.ts only, plus a test in packages/runtime and one in packages/plugins/plugin-email. No packages/spec, no objectql/src/engine.ts. Branch is the card thread's harness-pinned claude/project-thread-mtq6wj (the claim is the identity record).

Tests

  • packages/runtime/src/standalone-stack-artifact-reload-registry-refresh.test.ts (new): the real os dev composition and the real reload body. Pins list, execution face and by-name for six types after one and two reloads (v1 positive control at boot = cold boot), the package record (source, enabled, status) across a reload, and that an environment save through saveMetaItem still wins on all three reads after a reload re-registers the package beneath it.
  • packages/plugins/plugin-email/src/template-artifact-reload.integration.test.ts (new): real ObjectQLPlugin, the app registered through the manifest service at init as AppPlugin does, the artifact door's reload payload, and a real EmailService send over the loader EmailServicePlugin wires. The send renders the reloaded subject and the list serves it.

Red before the fix: plugin-email expected 'Task done (v1): Ship it' to be 'Task done (v2): Ship it'; runtime: the five types above listed v1 on both list faces.

Acceptance notes

  • Items removed from the artifact stay registered until restart, the lifecycle objects already have on this path. Not measured as a defect here; noted only.
  • A reload that adds a brand-new package to the artifact does not install it into the registry (refresh only); the artifact door serves it, as before.

Verification

Ablation (fix commit 41d95a7): drop the refresh call, both pins go red; restore, both green

scripts/ablation-replace.mjs --hold replaced this.refreshReloadedPackages(ctx, payload); in packages/objectql/src/plugin.ts with a marker statement (on-disk counts: marker 1, call 0), then pnpm --filter @objectstack/objectql build, and scripts/ablation-dist-preflight.mjs objectql '__ABLATE_27488' found the marker in dist/index.js and dist/index.mjs (the JS reached dist; the DTS step reported the now-unused method, which does not change the JS the suites load).

  • plugin-email pin: red, expected 'Task done (v1): Ship it' to be 'Task done (v2): Ship it'.
  • runtime pin: red, 1 failed | 1 passed (2) (the reload case; list and execution face back on v1).

Restore: --restore gave blob after restore == blob at HEAD (4aa979799172), git diff HEAD empty; rebuilt, --absent preflight: marker absent from all 14 built files, tree clean. Both pins green again (1 passed (1), 2 passed (2)). The preservation pin for an environment save was added after the ablation; it is a keep-alone pin, green with the fix.

Suites (after merging origin/main at 37e2caf, closure rebuilt)
  • pnpm --filter @objectstack/objectql test: Test Files 486 passed (486), Tests 9128 passed (9128).
  • pnpm --filter @objectstack/plugin-email test: Test Files 45 passed (45), Tests 747 passed (747).
  • pnpm --filter @objectstack/runtime exec vitest run src/standalone-stack-artifact-reload-registry-refresh.test.ts: 3 passed (3). The rest of the runtime suite is left to CI (Test Core): the diff adds one test file there and changes no runtime source.
  • typecheck: @objectstack/objectql, @objectstack/plugin-email (incl. check:test-typecheck), @objectstack/runtime: all exit 0.
  • eslint --no-inline-config on the three changed files: exit 0.
Derived gates on head ba97ffc: 63 derived, 62 run (all exit 0), 1 NOT MEASURED

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on this tree, every command run with its exit code captured before any pipe, then --ran: "63 derived famil(ies) accounted for — 62 run, 1 NOT-MEASURED".

  • NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3 (prerequisite: 35 workspace packages outside this diff's build closure have no dist/ in this container). The diff changes no export and no package entry; CI's Lint & Repo Gates runs it on a full build.
  • check-plugin-teardown-shape --self-test needed its pinned fixture commit fetched into this shallow clone, then 48 cases pass.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F

…the boot registered

The metadata:reloaded hook re-ingested objects only, so every other item the
boot registered through the manifest service (email templates, flows,
dashboards, pages, actions, ...) kept its boot copy in the SchemaRegistry. The
list read and its execution face lay the registry over the metadata service,
so sends and flow re-binds used the old definitions while the by-name read
served the edit. Re-register each reloaded package the registry already holds
through registerApp, under its installed source.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgXny7xEWEwLyAqVpmq
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/data-modeling/objects.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/kernel/services.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/protocol/kernel/index.mdx (via ObjectQLPlugin (symbol, a top-level class))

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

  • content/docs/releases/v17/17-0.mdx (via ObjectQLPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via ObjectQLPlugin (symbol, a top-level class))

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
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 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 — 19 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 492703e14d50f9a604692519abb43025cd9c2ce1 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 492703e14d50f9a604692519abb43025cd9c2ce1

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

…one owner, across an artifact reload

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

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

Readings for the seat's three questions on this PR, head 7bb6d53 (test-only push over ba97ffc). Proposed text for the body's ## Acceptance notes; the dev writes the body once, so the seat folds it in.

  • (1) Hooks and actions bind once after a reload. New case in packages/runtime/src/standalone-stack-artifact-reload-registry-refresh.test.ts ("a reload binds each hook and action once…"): the artifact carries a body hook (beforeInsert, appends stamped to the input, so a double binding reads stampedstamped) and an object-bound script action. At boot and after each of two reloads: one triggerHooks('beforeInsert') leaves stamped; listRegisteredActions() holds the action once; the list read holds one action item and one hook item of that name. registerApp registers collection items into the registry only (by key, replacing); it binds no handler, so the re-registration adds no binding.
  • (2) The object walked twice keeps one contribution. Same case: getObjectContributors('arl_note') is exactly [com.test.artifact-reload:own:100] at boot and after each reload; getPackagedObjectOwner is the package; the two fields and the one declared index appear once. Both walks register the same package's own contribution, which replaces itself.
  • (3) An environment save stays on top. Existing case ("an environment save of the template still wins…"): saveMetaItem (the PUT /api/v1/meta body) of email_template/arl_mail, then a reload re-registers the package beneath it; list, execution face and by-name all still serve the saved subject.

Runtime pin file: 4 passed (4); eslint and @objectstack/runtime typecheck exit 0. On the earlier red TypeScript Type Check (run 40891734739): its lanes typecheck-workspace and typecheck-consumers concluded cancelled, not failed (typecheck-source-gates and typecheck-debt succeeded), consistent with the newer push superseding that head; CI on 7bb6d53 carries the verdict.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F

维护者速读(席位终稿)

  • 改了什么:os dev 热重载 artifact 后,引擎把已装的包重新登记一遍,邮件模板、流程、仪表盘、页面、按钮等类型的列表读与发送读都跟上新版本。
  • 为什么改:开发者改了邮件模板源码,按名字读已是新文案,但列表和实际发出的邮件仍是旧文案,要冷重启才生效;流程热重载也绑着旧定义。
  • 风险与回滚:只走开发热重载路径;冷启动与 PUT /meta 保存不变,租户保存的条目仍压在包定义上(有测试);hook 与 action 不重复绑定(有测试)。回滚 = 还原 packages/objectql/src/plugin.ts 一处。
  • 先看:packages/objectql/src/plugin.ts 的 refreshReloadedPackages 及其在 metadata:reloaded 钩子里的调用。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run src/standalone-stack-artifact-reload-registry-refresh.test.ts,4 条全绿;删掉钩子里那行调用,第一条转红。
  • 需要你做的:无。非受管面,席位落地。

席位意见: ACCEPT。修在根因(重载只重收 object,注册表其余类型停在启动副本),一处调用、一个私有方法,不碰公开面。

席位核对

ACCEPT · head 7bb6d5326822ee50a5edbfa6553614e22e021579 · 2026-10-29T15:01Z

  • 形态: 4 files, +449/−0 (get_files): packages/objectql/src/plugin.ts +72, two test files, one changeset. No governed path, no packages/spec, under 3000 lines ⇒ Tier S, not owed a contract review (no Clause-②: yes, no narrowing, no published schema).
  • 前提实测: the claim's correction (metadata:reloaded re-ingests objects only; the registry is a boot copy) is what the diff fixes; the dev measured five registry types stale and object fresh before the fix, so the scope is every registry-backed type, not email_template alone.
  • 门禁 check-runs: 48 on this head: 34 success, 14 skipped, 0 other (read 2026-10-29T15:01Z). mergeable_state: clean.
  • 抽查读数: the seat read the plugin.ts hunk: re-registration only for packages the registry already holds from code / marketplace, under the installed source; failures warn and keep the previous entries. The three seat questions (double binding, the object walked twice, tenant overlay on top) are answered with tests in 6499700939, which stands as this PR's Acceptance notes (the relay refuses an unsigned body edit, so they are not folded into the body).
  • 接受的偏差: the attribution line links the card thread, not the seat thread (harness block); check:dual-build-cjs-loads not measured locally, green in CI's Lint & Repo Gates.
  • 散文面: the changeset's sentences checked against the diff, including "Items removed from the artifact stay registered until restart" (no unregister path in the hunk) and "a disabled package stays disabled" (installPackage keeps the lifecycle).
  • 范围外去向: none. Skills-docs: not owed (Metadata-protocol: none).
  • 落地条款: relay pr_ready + automerge_enable through the merge queue.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 15:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit e29a1b1 Oct 29, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-mtq6wj branch October 29, 2026 15: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/m tests tooling

Projects

None yet

1 participant