Skip to content

objectql: a foreign bound action named like an action the target object already declares is not attached and nothing reports it, while the two packages' handlers overwrite each other silently — the collision follow-up of #24400 (PR #24458) #24472

Closed

Description

@objectstack-fleet

Blocked-by: #24400

Filing gate ①, class (a): a defect with named landings.

Filed by domain:spec seat 2 (#18549) · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK. It comes from PR #24458's contract review 6260702525 (③, question 2: "accepted as a deferral on one condition, escalated to the seat: file the card for B"). It also comes from the #24400 dev's open question 2 and out_of_scope_findings[2] (report 6259877793). ⛔ Not a claim. Self-labelled domain:engine under the filer exception (packages/objectql). Grade and area:* are triage's.

What happens (read on PR #24458's head 0027ae7fe0; the attach lands with #24400)

  1. The listing face.
    • SchemaRegistry.attachBoundActions (packages/objectql/src/registry.ts:3516) skips a bound action whose name the object already carries (present.has(action.name), :3527), and logs nothing.
    • The object's own declaration, with its own gate, is served. The other package's declaration is listed nowhere on the object.
  2. The handler face (pre-existing on main 31ce45c071).
    • ObjectQL.registerAction (packages/objectql/src/engine.ts:5522) keys handlers by OBJECT:ACTION.
    • this.actions.set (:5524) overwrites with no diagnostic.
    • The handler that runs is whichever package registered last, while the router's rung 1 serves the object's declaration.

So, for a same-name pair, the served declaration and the handler that runs can come from different packages, and registration order decides which.

The work

One PR on packages/objectql, after #24400 lands.

Reader who acts: the domain:engine seat, in one PR. #24318's seat reads this card's outcome before it stages the change_slug move.

Grade

p2: a registration-time diagnostic and a handler-face agreement, with no /meta door answer changed. Triage may regrade.

Dedupe

Two semantic searches of this repository's issues:

Dedupe words: bound action name collision two packages same object; action handler key collision OBJECT:ACTION overwrite; attached action stale overlay uninstall.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Unblocked: #24400 landed as fb0d85b7fb · pm:blocked → pm:queue · domain:spec seat 2 (the filer) · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-19T06:39Z

    ⛔ Not a claim. Lane and grade remain triage's.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Maintainer decision: 18.x, report first · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-19T07:30Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

    Provenance. The seat put its 18.0 GA leans to the maintainer in chat under the standing rule 6261447528 ("a metadata-protocol change is put to the maintainer for pulling forward"). The maintainer answered 「同意你的建议」.

    • The seat's lean, verbatim: 「18.x,先只告警」.
    • What it binds: the collision is reported (an author-visible warning, plus the listing reports the collision), and the bound action is not refused.
      • Reporting is a widening, so it can land in any 18.x minor.
      • Refusing a foreign bound action that collides is a narrowing. That would go back to the maintainer under 6261447528 before anyone builds it.
    • Labels: none change: p2, pm:queue, no target:v18. It stays Blocked-by: #24400.

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 12 · 2026-10-19T09:53Z · after the maintainer decision 6262054499 ("18.x, report first": the collision is reported, never refused) and the unblock 6261310013 (#24400 landed as fb0d85b7fb). The registry and the plugin report a same-name pair at kernel:ready, naming the object, the action and both packages. The handler face reports registerAction's silent overwrite the same way.
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24472-bound-action-collision-report
    Worktree: objectstack-issue-24472
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T09:55Z: mode:cloud. The dev session is session_01PmE7dZP8v5iv3z1dgybAtT. It builds at the default tier on branch claude/issue-24472-bound-action-collision-report, under claim 6264504693.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24472,
    "status": "done",
    "branch": "claude/issue-24472-bound-action-collision-report",
    "pr": "#24526",
    "session": "session_01PmE7dZP8v5iv3z1dgybAtT",
    "premise_still_valid": true,
    "summary": "Measured first on a booted composition on main 3ae0b77: two independent packages (two AppPlugins on one host), each with a script action whoami on crm_account. Both registration orders serve the owner's declaration, and the boot logs nothing. In the core-first order the binder's handler runs while the core's declaration is served. Built, on the ruling 6262054499 (report, never refuse): (1) SchemaRegistry.findBoundActionCollisions (@internal) with a sameness test. The test compares declarations minus the _-prefixed stamps, protection and objectName. The plugin warns once per differing pair at kernel:ready, naming the object, the action and both packages. A late artifact (the manifest service after the seal) is judged after it registers, narrowed to its own packages. (2) registerAction warns when an OBJECT:ACTION key held by another owner is replaced, naming both owners; the same owner re-registering and the authored resync stay silent. After the change, the served declaration and the running handler are unchanged in all four M rows; one listing warning appears for a differing pair and none for the same-declaration control. Stock CRM, showcase and todo boots show 0 and 0. The #24318 pair (change_slug on sys_organization from a late install-local artifact) now warns live. Rider measured and dropped: the card's step 2 answers NOT_OVERRIDABLE and step 3 answers WRITABLE_PACKAGE_REQUIRED, and the one withdrawal door that exists (install-local DELETE) leaves no stale action in either copy. The optional index rider was not taken.",
    "tests": "On head f679753 (after merging origin/main f4755a6): pnpm --filter @objectstack/objectql test exit 0 (Test Files 447 passed, Tests 8406 passed); pnpm --filter @objectstack/objectql typecheck exit 0 (incl. check:test-typecheck OK); new pin file src/bound-action-collision-report.test.ts 14/14, neighbour bound-action-foreign-object.test.ts 14/14 unmoved; pnpm --filter @objectstack/core test:repo exit 0 (6 files, 82 tests, measured on b848c4a); pnpm --filter @objectstack/spec check:generated exit 0 after the merge. Ablations went through scripts/ablation-replace.mjs (each anchor hit 1 -> 0, blob changed, restored, git diff HEAD empty after). No dist rebuild was needed because the suite imports src by relative path. A1, listing audit (the collision test replaced by continue): 7 of 14 red. A2, handler report (the replacement guard replaced by false): 2 of 14 red. A3, sameness (the top-level key set replaced by a sentinel): 3 of 14 red, exactly the 3 same-declaration controls. The first A3 attempt was refused by the tool as a no-op (the replacement contained the anchor) and was redone with a different anchor. M before/after on a booted os dev --fresh composition (both orders plus the same-declaration control), recorded in the PR body table.",
    "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths), derived at f679753: 72 commands, all run on f679753, 72 x exit 0, each captured before any pipe. --ran reconciliation: '72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero, all 72 recorded an exit code and none of them is 3)'. Outside the derivation and recorded: objectql typecheck, objectql test, core test:repo, all exit 0. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET: 8 packages without dist); those 8 were built and the gate re-ran with exit 0. The repo was deepened with --shallow-since=2026-07-13 before check-engine-split-ratio --days 90 and check-plugin-teardown-shape. The list was derived three times (at b848c4a, 5b8e5df and f679753) and was identical each time. CI on PR #24526 at f679753, read at report time: 37 check-runs, 9 success, 6 skipped, 22 in_progress, including Build Core, Governed Surface Queue Guard and Check Changeset in_progress. That is the honest value; I am not waiting for CI.",
    "mcp_calls": "0",
    "api_writes": "3: (1) git push to claude/issue-24472-bound-action-collision-report: the empty-branch probe plus 4 later pushes of the same branch from local tmp-24472, a git transport and not REST; (2) one fleet-write relay pr_create, request fw-20261019T121751Z-47f2cb, run 39250958605: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/24526/assignees [os-project-manager], read back identical 12238/12238 bytes; (3) one fleet-write relay comment on #24472 (this report): POST /repos//issues/24472/comments. Reads were unauthenticated-header REST GETs through the container proxy (the issue, its comments, comment 6260702525, PR 24526, check-runs); none of them is a write.",
    "line_budget": "none set; diff vs merge base 913fd6d (+/-): .changeset/24472-bound-action-collision-report.md +14/-0; packages/objectql/src/bound-action-collision-report.test.ts +235/-0; packages/objectql/src/engine.ts +19/-0; packages/objectql/src/plugin.ts +28/-1; packages/objectql/src/registry.ts +112/-2; total +408/-3",
    "files_changed": [
    ".changeset/24472-bound-action-collision-report.md",
    "packages/objectql/src/bound-action-collision-report.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/plugin.ts",
    "packages/objectql/src/registry.ts"
    ],
    "deviations": [
    "The dispatch's control says one declaration under two packages 'gives no warning'. Measured: the listing face gives 0, but the handler face gives 1 (registerAction sees handlers, not declarations). It is kept because the warning is true: withdrawing the replacing package removes the handler the other declaration relies on (pinned). Recorded as open question 1.",
    "SchemaRegistry gains a public method findBoundActionCollisions (marked @internal, not re-exported from the index), because plugin.ts calls it the same way it calls #24400's findUnboundActionTargets. Clause-②: no was copied verbatim from the claim, as ruled.",
    "core test:repo was measured on b848c4a. A main merge brought in f679753 afterwards (incoming objectql/spec commits). objectql test, typecheck, the gate union and spec check:generated were all re-run on f679753; core test:repo was not.",
    "The two merge commits (b848c4a, f679753) carry git's default merge message, with no Claude-Session/Co-authored-by trailer pair. The task commits carry the model-free pair per AGENTS.md. The harness reminder's model-named Co-Authored-By trailer was not used, because os-dev.md/AGENTS.md take precedence; it is reported here, not adopted.",
    "The worktree was cut under the temporary local branch tmp-24472, because the shared primary checkout has the card's branch checked out, as the dispatch instructed. Every push went to claude/issue-24472-bound-action-collision-report.",
    "Measurement scratch compositions lived under examples/_m24472 and examples/_m24472r (untracked) while measuring and were deleted before the gates ran. Their configs and logs stay in the session scratchpad."
    ],
    "open_questions": [
    {
    "question": "Handler face for one declaration carried by two packages: should registerAction stay silent there, as the listing face does?",
    "options": [
    "A: warn on every cross-owner replacement (built). Four axes: business need is real, because the replacement couples the two packages' uninstalls, which is measured and pinned. Long-term it keeps one rule at one seam. AI-error: a loud signal for a double binding an author may not intend. Startup: no new surface.",
    "B: silence it for the same declaration by passing the declaration into registerAction. That widens a public engine signature and its runtime callers, which is outside the fence, and an AI author could then hide a real uninstall coupling."
    ],
    "recommendation": "A. The warning states a true consequence, and B buys silence at the cost of a widened seam."
    },
    {
    "question": "Which listing does the ruling's 'the listing reports the collision' mean?",
    "options": [
    "A: the boot report, one warn per pair at kernel:ready and per late artifact (built). No published surface moves, Clause-② no.",
    "B: a new key on the served object at /meta. That widens a published surface (Clause-②: yes (widening)) and is a domain:spec question with no named consumer."
    ],
    "recommendation": "A. Startup axis: no consumer pulls a /meta key, and the warning already reaches the author. B stays with domain:spec if a reader ever appears."
    },
    {
    "question": "Are artifacts registered after kernel:ready judged?",
    "options": [
    "A: yes, after they register, narrowed to pairs naming one of their packages (built and pinned). Measured live on a marketplace install-local artifact and on ledger rehydrate.",
    "B: no; boot only."
    ],
    "recommendation": "A. Business axis: the #24318 shape and marketplace installs are exactly where two vendors collide, and a warning needs no pre-registration judgment."
    },
    {
    "question": "Sameness key set: should anything beyond the _-prefixed stamps, protection and objectName be ignored?",
    "options": [
    "A: only those (built). Measured: the same-stack copy and a declaration carried twice compare equal, and a differing label or target is reported.",
    "B: also ignore cosmetic keys such as label or order."
    ],
    "recommendation": "A. AI-error axis: two different labels on one key are two buttons claiming one handler, which is the defect, so reporting them is right."
    }
    ],
    "out_of_scope_findings": [
    "carrier: maintainer (decision 6262054499 holds the refusal) · noted, not filed. After a cross-package registerAction replacement, removeActionsByPackage(loser) removes nothing and removeActionsByPackage(winner) deletes the key, so the loser's still-declared action has no handler. Pre-existing, now stated in the warning and pinned ('withdrawing the replacing owner leaves the key with no handler'). Fixing it means changing which handler runs, which is the refusal decision's territory.",
    "class: none (observation) · reach: n/a · Two duplicate same-meaning comparators: objectql's local sameActionDeclaration and spec's @internal deepEqualAuthored (not exported). Noted in the PR's Acceptance notes only."
    ]
    }


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 24472,
    "status": "done",
    "round": "fix round 1 for PR #24526 (seat record 6267837597, FAIL on 2 only)",
    "branch": "claude/issue-24472-bound-action-collision-report",
    "pr": "#24526",
    "head": "bb02d9415",
    "session": "session_01PmE7dZP8v5iv3z1dgybAtT",
    "premise_still_valid": true,
    "summary": "Route A, as on PR #24518. The audit is keyed on FIND_BOUND_ACTION_COLLISIONS, an exported-from-module 'unique symbol' (Symbol(...), not Symbol.for) declared in registry.ts and re-exported from neither index.ts nor core.ts. SchemaRegistry carries it as a computed member, FIND_BOUND_ACTION_COLLISIONS, and plugin.ts calls it through the key, imported from ./registry.js beside describeBoundActionCollision. Nothing else moved: the audit body, the sameness test, both reports, the late-artifact narrowing and every message are byte-identical. findUnboundActionTargets is untouched. The changeset stays '@objectstack/objectql': patch with Clause-②: no, and the PR body's Clause-② line stands. One face pin was added, in PR #24518's shape. No merge of origin/main: there is no conflict (git merge-tree HEAD origin/main 04669a4 exits 0).",
    "dts_evidence": "Built dist of @objectstack/objectql at bb02d94. 'findBoundActionCollisions' occurs 0 times in index.d.ts, index.d.mts, core.d.ts, core.d.mts and the shared chunk util-BlZMXoav.d.ts/.d.mts. In the chunk, SchemaRegistry declares '[FIND_BOUND_ACTION_COLLISIONS](packageIds?: ReadonlySet): BoundActionCollision[];' with 'declare const FIND_BOUND_ACTION_COLLISIONS: unique symbol;'. The chunk's export list carries no alias for FIND_BOUND_ACTION_COLLISIONS, BoundActionCollision or describeBoundActionCollision. index.d.ts and core.d.ts export lines name none of the three (0 hits on the export lines). The only FIND_BOUND_ACTION_COLLISIONS hit in index.d.ts is a TSDoc link in ObjectQLPlugin's private reportBoundActionCollisions comment.",
    "files_changed": [
    ".changeset/24472-bound-action-collision-report.md",
    "packages/objectql/src/bound-action-collision-report.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/plugin.ts",
    "packages/objectql/src/registry.ts"
    ],
    "files_changed_this_round": [
    "packages/objectql/src/registry.ts (+10/-4: the key and the computed member, TSDoc links)",
    "packages/objectql/src/plugin.ts (calls through the key, import)",
    "packages/objectql/src/bound-action-collision-report.test.ts (+11: calls through the key; the face pin)"
    ],
    "line_budget": "none set; whole PR vs merge base: .changeset +14/-0; bound-action-collision-report.test.ts +246/-0; engine.ts +19/-0; plugin.ts +28/-1; registry.ts +122/-2",
    "tests": "On bb02d94. Pin file 15/15 (14 earlier pins through the key, plus the face pin 'is no nameable member of the published face: keyed on a symbol neither package entry exports': FIND_BOUND_ACTION_COLLISIONS is in neither index nor core, Object.values(index) does not contain it, SchemaRegistry.prototype's own names do not include 'findBoundActionCollisions', and its own symbols include the key). Neighbour bound-action-foreign-object.test.ts 14/14. Ablations re-run through the new key with scripts/ablation-replace.mjs; each anchor hit 1 -> 0 and git diff HEAD was empty after each restore. A1 listing: 7/15 red. A2 handler: 2/15 red. A3 sameness: 3/15 red. A4 face (index.ts re-exports the key): 1/15 red, the face pin. The restored tree was re-run green, 29/29 across both files.",
    "gates": "Re-ran on bb02d94: pnpm turbo build of @objectstack/objectql... exit 0; pnpm --filter @objectstack/objectql typecheck exit 0 (check:test-typecheck OK); pnpm --filter @objectstack/core test:repo exit 0 (6 files, 82 tests). The full derived union was also re-run rather than a subset, because the derivation at bb02d94 is identical to f679753's (72 commands): 72 x exit 0, each captured before any pipe. check:dual-build-cjs-loads first exited 3 in the fresh worktree (8 packages without dist); they were built and the gate re-ran with exit 0. --ran: '72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)'. The objectql full suite was not re-run this round. It stands from f679753 (447 files, 8406 tests), because the change is a member rename to a symbol key with no behaviour change, exercised by the pin file and the typecheck. CI on bb02d94 had not concluded at report time.",
    "mcp_calls": "0",
    "api_writes": "2 this round: (1) git push to claude/issue-24472-bound-action-collision-report (f679753 -> bb02d94), a git transport; (2) this os-dev-report comment through the fleet-write relay, POST /repos//issues/24472/comments. Reads: unauthenticated-header REST GET of comment 6267837597; no other reads.",
    "suggested_pr_body_edit": [
    "In '## What changes', first bullet, replace: 'SchemaRegistry.findBoundActionCollisions() sits beside findUnboundActionTargets() and is marked @internal; it is not re-exported from the package index.' WITH: 'The audit sits beside findUnboundActionTargets() on SchemaRegistry, keyed on FIND_BOUND_ACTION_COLLISIONS, a unique symbol declared in registry.ts and re-exported from neither index.ts nor core.ts, so the published face gains no nameable member (the shape of PR #24518). The plugin calls it through that key.'",
    "In '## Verification', Pins: replace 'has 14 tests' WITH 'has 15 tests (plus a face pin: the key is exported from neither entry, and SchemaRegistry.prototype names no findBoundActionCollisions)'.",
    "In '## Verification', Ablations: replace '7 of 14', '2 of 14' and '3 of 14' WITH '7 of 15', '2 of 15' and '3 of 15', and add 'Face: index.ts re-exports the key, and 1 of 15 goes red (the face pin).' Replace the head 'f679753aa' WITH 'bb02d9415' in the gates line."
    ],
    "deviations": [
    "The objectql full local suite was not re-run on bb02d94 (reason in gates). The pin file, typecheck, build, core test:repo and all 72 derived gates were re-run."
    ],
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-19T14:30Z

    Probe Reading
    Review TIER_DEFAULT record 6267837597 FAIL on f679753aa, on ② alone: a public method on the exported SchemaRegistry. Delta record 6268615812 PASS on bb02d9415, after the fix round keyed the audit on an unexported symbol. ACCEPT 6269118660. Not governed.
    Queue It entered the merge queue at 2026-10-19T14:06Z and left it merged at 2026-10-19T14:29Z. No gh-readonly-queue/main/pr-24526-* branch remains.
    origin/main de43524606 is on origin/main. It carries PR #24526's 5 files, +429 / −3.
    Card Closed completed through Fixes #24472; pm:dispatched is cleared.

    What main now holds:

    • The listing face reports a same-name pair. The pair is a bound top-level action that its target object does not list, because the object already lists a different declaration under the same name.
      • The ObjectQL plugin warns once per pair at kernel:ready, naming the object, the action, the package whose declaration is served and the package whose declaration is not listed.
      • A late artifact is judged as it registers, and a pair already reported is not repeated.
      • The sameness test ignores the provenance stamps, so one declaration carried under two packages is not a collision.
    • The handler face reports a replacement. registerAction warns when it replaces an OBJECT:ACTION handler that another owner registered, naming both. The same owner re-registering, and the authored resync, stay silent.
    • Nothing is refused, by the maintainer decision 6262054499 ("18.x, report first"). The object keeps serving its own declaration (rung 1), the last registration still runs, and the boot goes on. No new error code, and no /meta key.
    • No public surface moves. The audit's key is not exported from either package entry, and @objectstack/objectql is patch.

    Downstream:

    The dev session session_01PmE7dZP8v5iv3z1dgybAtT is archived.


    Generated by Claude Code

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

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions