Skip to content

fix(metadata-protocol): a view container's reset heals the expanded views its publish registered - #23196

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23149-reset-retires-view-expansions
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23149-reset-retires-view-expansions

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23149

Clause-②: no

What was wrong

DELETE /api/v1/meta/view/NAME on a published overlay of a packaged view container answered { success: true, reset: true }, but the delete path's registry heal (restoreArtifactRegistryView) healed the container's plain key only. The publish had also registered every view the container expands to (hydrateExpandedViewItems, one entry per OBJECT.VIEWKEY name), and those entries stayed in the process registry until a restart. GET /meta/view and GET /meta/view?object=OBJECT kept listing them, so the end-user list kept rendering the overlay's label and columns.

Mechanism, measured (the dispatch's six premises)

Measured on the real composition at e540e7ffb (before the change): the real ObjectQL engine and SchemaRegistry, the real ObjectStackProtocolImplementation on a host-config kernel (no environment), an in-memory driver, and a manifest shaped like examples/app-showcase's showcase_task container (list with 7 columns, plus listViews.in_progress). The registry's view entries:

step entries
boot com.example.showcase:showcase_task, com.example.showcase:showcase_task.default (All Tasks, 7 columns), com.example.showcase:showcase_task.in_progress
publish of the overlay (All Tasks (X), progress dropped) the three above, unchanged, plus bare showcase_task, showcase_task.default (All Tasks (X), 6 columns), showcase_task.in_progress
deleteMetaItem answered {success: true, reset: true} the three package entries, plus bare showcase_task.default (All Tasks (X), 6) and showcase_task.in_progress: only bare showcase_task was lifted

After that reset, getMetaItems({ type: 'view' }) listed showcase_task.default twice (All Tasks/7 and All Tasks (X)/6), and getMetaItem of showcase_task.default served All Tasks (X)/6.

  1. The stale entries are the expansions hydrateExpandedViewItems registers, and the heal never addresses them. Confirmed by the table.
  2. "The publish overwrote the packaged base under the same key, so retiring alone could leave no showcase_task.default." Falsified. A package's expansions sit under the composite PACKAGE:NAME key and the publish writes the bare key, so the base is never overwritten. Lifting the bare entry (tier 1, removeRuntimeShadow) un-shadows it. Tiers 2 and 3 still run per name, for a name no package ships.
  3. Another package's name. The hydration skips a name anotherPackageShips. The change records only the names it registered, so the heal never touches a skipped name. Pinned and ablated below.
  4. Scope gates. The expansion heal runs after the organizationId gate and through the same walk, so it inherits both gates: an organization-scoped heal touches nothing (and the record is kept for a later environment-wide heal), and re-registration stays control-plane only. Pinned.
  5. Which source each door reads. Confirmed, with one correction. Both list doors read getMetaItems, which starts from registry.listItems (bare and composite entries alike). With no stored row left, nothing is merged over that list, so it is the stale source. The by-name read asks the MetadataService (its step 2) before the registry (step 3). That is why the card saw a correct item read on the showcase: the metadata plugin answers it from the artifact. With no MetadataService, the by-name read reaches the registry and is stale too. The pins run without one, so they exercise the registry.
  6. The dashboard leg resets cleanly because a dashboard expands to nothing. Confirmed on the same harness (Ops (X) published, then reset to Ops). It is the control.

The change (packages/metadata-protocol/src/protocol.ts only)

  • hydrateExpandedViewItems records each name it registers, per registry and per container name (recordViewExpansion, a private static WeakMap keyed on the registry object, the way meta-overlay-cache.ts keys its bucket on the engine).
  • restoreArtifactRegistryView, after its organizationId gate, takes the container's recorded names (takeViewExpansions, views only) and heals each one through the same tier walk, as a (type, name) of its own. It does this before the container's own walk, which returns early from three of its tiers.

Why a record and not zone 3's re-derivation. Running expandRuntimeViewContainer over the deleted body at reset time has three gaps, each measurable in the code:

The record holds what was registered, so the two sides cannot drift.

Pins (protocol.view-container-reset-expansions.test.ts, 6 cases)

The registry double carries the real SchemaRegistry's key shapes, getItem precedence, artifact lookup, listing and both heal verbs with their refusals. @objectstack/objectql cannot be imported from this package because it depends on it. The engine double is protocol.served-content-hash.test.ts's.

  1. The card's repro: after the reset, the list, the ?object= list and the by-name read all serve the packaged showcase_task.default (All Tasks, 7) exactly once, and the bare names are empty.
  2. A view the overlay added, which no package ships, is retired by the reset (tier 3).
  3. Publish, reset, publish serves the second overlay on all three doors, and its reset heals again.
  4. Control: a packaged dashboard resets as before.
  5. An expansion name another package ships is never touched: a bare entry held under it survives the reset by identity, and the other package's artifact is unchanged. The name the container did register is healed.
  6. An organization-scoped heal leaves every expansion and the container in place, and an environment-wide heal after it retires them.

The same six legs on the real composition (a scratch file in packages/objectql/src, not committed), over the rebuilt dist/: 6/6 green, and 121/121 across it plus the 8 objectql suites driving this heal (protocol-registry-shadow, environment-overlay-five-types, protocol-packaged-view-base, protocol-org-overlay-registry-gate, protocol-delete-object-registry-heal, protocol-object-overlay-layer, view-container-divergent-name-registrars, view-container-name-refusal).

Ablation (one-off, committed fix first; scripts/ablation-replace.mjs WRAP mode; this suite imports ./protocol.js, so it resolves from src/ and needs no build)

  • Heal loop removed (await this.restoreArtifactRegistryView(type, expanded, organizationId); replaced by a no-op; anchor count 1 to 0, blob ff5f56386 to 7cd556184): 5 red, 1 green (the dashboard control, as expected). Case 5 went red only at its "the registered name is healed" half. Restored: blob equals HEAD, git diff HEAD empty.
  • The skipped name recorded too (the record call moved ahead of the anotherPackageShips skip; anchor 1 to 0, blob to af2c40b08): only case 5 red, at toBe(held). The heal lifted the entry it must leave. Restored the same way. A first attempt at this leg was a no-op: its replacement restated the anchor, and the tool refused it with "the anchor count moved 1 to 1", so nothing ran.

Verification

  • pnpm --filter @objectstack/metadata-protocol typecheck: exit 0. tsc --listFiles lists the new test once.
  • pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 on the merged tree d03cbad29: 233 files passed, 3 skipped; 28284 tests passed, 19 skipped. Also at 70a7be16a before the merge: 232 passed, 28259 tests.
  • Gates: dispatch-gates --repo objectstack-ai/objectstack --ran at fd1d615cb reports 73 derived, 71 run with exit 0, 2 NOT MEASURED, 0 unrun. The new test's three pinned engine doubles were added to scripts/engine-double-contract.pinned.json (--write, +15 lines, this file's rows only). check:engine-double-contract is then green at 913 rows.
  • NOT MEASURED: check-plugin-teardown-shape.mjs --self-test exited 3 because this shallow clone cannot reach its pinned fixture commit 621a48760. That run grades the script, not this diff. check:dual-build-cjs-loads exited 3 because 65 packages are unbuilt here. As a narrow stand-in, require('packages/metadata-protocol/dist/index.cjs') loads ObjectStackProtocolImplementation.
  • Built declarations: dist/index.d.ts gains three untyped private static member names on ObjectStackProtocolImplementation (registeredViewExpansions, recordViewExpansion, takeViewExpansions), beside the 279 private members already there. No public member, export, schema or accept-set change, so Clause-②: no stands.

Acceptance notes

  • Sibling defect, not addressed here. A republish of the container that drops a member keeps serving that member's expanded view until restart. This was measured on the real composition: publish with an added listViews.added_by_overlay, then publish again without it. The list and the by-name read still serve showcase_task.added_by_overlay. The write-through registers the new expansion set and retires nothing it no longer contains. A reset still heals it, because the record is the union of every name this container registered. It is listed in the report for the seat to file.
  • Residual, documented in the docblock. An expansion whose walk stops on a degraded MetadataService read (tier 2, which a packaged name never reaches) stays registered until the container is published and reset again, or until a restart. The container's own name keeps its existing "a later delete heals it" behaviour.
  • Two rows of one container name (a package-less row and a package-bound one) record into one set. A reset of either heals the set, as the plain-key heal already lifts the name's single bare entry, and the remaining row is hydrated again on the next list read.
  • Not done: a live API run on examples/app-showcase. The full workspace build does not fit the foreground window here, so the real-composition harness stands in. A committed real-registry pin would live in packages/objectql/src, which is outside this claim's file surface.
  • main was merged at 881421194, including fix(metadata-protocol)!: the metadata save door never sets, changes or removes an item's lock: a body's _lock* family is kept from the stored row, echoed, or refused #23167 on protocol.ts, with no conflict.

Generated by Claude Code

… its publish registered (#23149)

WIP: record each expansion hydrateExpandedViewItems registers, per registry
and container name, and heal each recorded name through the same tier walk
the container's plain key takes.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 6 documentable anchor(s).

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

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

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

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (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
  • 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 — 11 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 8323cad97e148fb1955511479adacad19aee10fd → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8323cad97e148fb1955511479adacad19aee10fd

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

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/l tests tooling

Projects

None yet

2 participants