Repository navigation
service-automation: a declared override in <base>.custom is the packaged flow — armed over the code package, rebound in one reload, one ledger switch #28412
Description
epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
Filing gate: ④ child of epic #28236 (P1, cut after ADR-0146's rewrite merged in #28295); follows #28390 ruling A (comment 6542177429: only the paired <base>.custom declares an override), not ADR-0146 D9 item 1's "or from any package in the closure", which that amendment narrows. ⛔ Not a claim.
Blocked-by: #28405, #28410, #28409
Part of #28236 (P1, child N-11) · governs: ADR-0146 D9 items 1–3, Supersede rows ADR-0126 D1 / D4 wall 1 / §7.1 · lane: domain:server (packages/services/service-automation, the flow view in packages/metadata-protocol) · size: L.
Business reason. Today changing HotCRM's flow means cloning it, and both copies run. With this card the customer's version in hotcrm.custom is HotCRM's flow: one run per change, the same switch, and deleting it restores the original.
Measured (75ecaf4ad8).
service-automation/src/flow-precedence.ts#resolveFlowPrecedencerankspackage(0) overruntime(1);classifyContendermakes every stored rowruntimewhatever its stamps; the remedy printed is "clone the flow under a new name, or switch the packaged flow off" (:306).metadata-protocol/src/protocol-lock-state.ts#declinesStoredRow/#isShippedFlowNamekeep every stored row of a shipped flow name out of the protocol's flow view, so thekernel:ready/metadata:reloadedsyncs (plugin.ts#resolveFlowContenders) never see one.engine.ts:4511re-binds a name in one synchronous step (deactivateFlowTriggerthenactivateFlowTrigger);plugin.ts#resyncFlowsFromProtocolwithdraws names absent from the fresh view after registering.engine.ts#packagedFlowOwnerreads the loader's set, never body stamps; the ledger is unique on(metadata_type, name)(sys-metadata-activation.object.ts:189).- A flow row of a shipped name in a writable package is refused by
refusePackagedBaseOverride.
Wanted.
- Declared rank. A contender is a declared override only when its stored binding (the
package_idthe protocol read, ⛔ never the body's_packageId) is<owner>.custom(N-08's constant) and its body declaresoverrides.package= the owner — N-10's exported predicate, not a copy. It ranks over the loader's body. Any other declarer is undeclared and stays shadowed and warned. - The flow view admits it.
isShippedFlowName/declinesStoredRowlet exactly that row into the execution view, through N-10's served-override predicate. ⛔ No second predicate. - One reload. Every arming step (boot pull,
kernel:ready,metadata:reloaded, the metadata-mutation sync) arms the override in place of the base under the one name. Deleting the override re-arms the base in the same step, never withdraw-then-re-add. - Ledger identity.
packagedFlowOwner(name)still answers the base. One ledger row switches whichever body is armed; the toggle door, the §7.3 subflow guard and theFLOW_DISABLEDconsult treat the override as the packaged flow. - Write gate. N-09 lands with
flowheld closed (its Not here); this card opens it for the.customdeclaration in that same gate. ⛔ No second gate. - The undeclared remedy names the declaration in
<owner>.customor a new name, not the clone.
Tests. New cases in flow-name-shadowing.test.ts, flow-precedence-loader-set.test.ts, flow-sync-one-precedence.test.ts: declared .custom row armed; a dependent's declaration or a body-stamped _packageId not armed. One record change fires exactly one body across publish and delete of the override. Ledger off silences the override and on re-arms it. With N-12, a run suspended on the base finishes on the base after the override arms (the epic check 「流程定制只跑新定义、挂起运行按旧版完成」).
Not here. Pinning (N-12); Studio "Customize this flow" (N-18); "new similar" (N-21/N-22); action override (D9, out of this epic).
Dedupe. Related, closed: #11665 (clone + takeover design), #12563 / #12028 (shadowing sentence, banner). Searched flow shadowing, flow precedence shadowed: none arms a declared override.
Links back: #28236, #28390, ADR-0146 D9, ADR-0126 §2/§7.1/§7.3, ADR-0009.
Generated by Claude Code
Activity
objectstack-fleet commentedon Nov 1, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: unblocked · p1 ·
pm:queue· 2026-11-01T11:56Z⛔ Not a claim.
Blocked-bytarget is closed as completed: service-automation: a suspended run resumes on the flow version it started on, not on the definition registered under its name #28405 (N-12), metadata-protocol: one effective read — package-less, base-named, layered, the env-wide list and export serve the.customdeclared override, and a withdrawn public form stays withdrawn #28409 (N-10) and metadata-protocol: the override write gate — only the base's paired.custommay declare; undeclared same names and a dependent'soverridesare refused at publish,os validateand install #28410 (N-09, PR feat(metadata-protocol,cli,cloud-connection)!: the override write gate — only the base's paired .custom declares an override (ADR-0146 D2) #28554). The card waits on nothing else, sopm:blocked→pm:queueat p1, with no assignee.flowin the same gate N-09 built, with no second gate.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Claim: PM loop round 1 · card thread · 2026-11-01T12:17Z
Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01FEkQ5oTRCUNQ4EA3YV84sZAccount:
os-project-manager(the seat's linked user; the card's assignee)Branch:
claude/project-thread-jqgfyc(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#1Ruling-ref: 「28412 插队,现在就派」 (Leehom, 2026-11-01T12:11Z); triage unblock record
6557603861(p1,pm:queue). Server seats 2 and 3 were told before this claim.File surface:
packages/services/service-automation/src/(flow precedence, the arming syncs inplugin.ts/engine.ts, the shadowing remedy sentence) and its three named test files;packages/metadata-protocol/src/(protocol-lock-state.tsflow view throughisServedOverrideRowOf,protocol-packaged-base-refusal.tsopeningflowinisOpenOverrideType) and their tests; changesets on the two packages. ⛔ Nopackages/spec/**shape change, ⛔ nodocs/adr/**hunk.Container & model:
S,mode:thread, model and effort as the thread was openedClause-②: yes (expected: a declared
flowoverride refused today becomes admitted, and the flow precedence changes); contract review owed on the final head before landingMetadata-protocol: measured by the dev
Responsibility: the code is the flow precedence and arming path of
@objectstack/service-automationand the override write gate and flow view of@objectstack/metadata-protocol· the platform path is a customer's<base>.customflow declaringoverrides.package= the base (ADR-0146 D9) · reached by every flow save, reload and trigger of a shipped flow nameThread-read: none
Premise: holds (read 2026-11-01T12:17Z at
origin/main0b6a52e8a0:isOpenOverrideTypeinprotocol-packaged-base-refusal.tsrefuses a declaredflowwithdeclared_type_not_yet_available;classifyContenderinflow-precedence.tsranks every stored rowruntimeunder the package; the remedy sentence says "clone the flow under a new name";isServedOverrideRowOfis exported fromserved-override.ts)Serial constraints cleared: open PR #28651 (console connector connections) touches only
service-automation/src/connector-credentials-ui*.ts, which is outside this surface; mergeorigin/mainonce if it lands first. No other open PR touchesmetadata-protocol/src/orservice-automation/src/.Resume-from: none — fresh from
origin/main0b6a52e8a0State:
pm:queue→pm:dispatched, assigned toos-project-manager, recorded by this claim.Generated by Claude Code
objectstack-fleet commentedon Nov 1, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
os-dev-report
{
"issue": 28412,
"status": "done",
"branch": "claude/project-thread-jqgfyc",
"pr": "#28693",
"session": "session_01FEkQ5oTRCUNQ4EA3YV84sZ",
"premise_still_valid": true,
"summary": "A flow saved into the shipping package BASE paired BASE.custom under the base flow name, declaring overrides: { package: BASE, basedOn }, is now admitted by N-09 judgeOverrideWrite (isOpenOverrideType opens flow through a CONDITIONS_DELIVERED set; action is still not yet available), served by every read in the base slot via N-10 isServedOverrideRowOf on the stored package_id (declinesStoredRow now takes the row), and armed by the boot pull, kernel:ready, metadata:reloaded and the mutation sync in place of the base through a new override precedence rank read off the stored binding (the registry hydration records package_id on the hydrated flow body as a non-enumerable Symbol.for property: bindStoredRow / storedRowBindingOf). Ledger identity stays the base (name and packagedFlowOwner unchanged). A /meta delete naming the pair is admitted (isPairRowRemoval, runtime-only intent), so removing the override re-arms the base by one registerFlow, never a withdraw. The undeclared remedy and the flow sealed save sentence name the .custom declaration or a new name instead of the clone.",
"tests": "Red first on main 0b6a52e (production sources restored, the helper module kept, metadata-protocol and service-automation rebuilt): service-automation shadowing/loader-set/sync Tests 14 failed | 41 passed (55); metadata-protocol flow-stored-row-shipped-name + packaged-base-refusal Tests 15 failed | 71 passed (86); objectql protocol-flow-custom-override (new, real ObjectQL engine) Tests 3 failed | 1 passed (4); cloud-connection install gate 1 failed | 6 passed; cli override-declarations 1 failed | 6 passed. Restored with git checkout HEAD -- packages, porcelain empty, rebuilt. Green on main by construction and kept as pins: the five not-admitted rows, ledger off/on, and the N-12 suspended run finishing on the base. At head all pass: service-automation 55/55, metadata-protocol full suite 21954 passed after the two pin updates, objectql 4/4, cloud-connection 7/7, cli 7/7 plus validate-override-declarations 2/2, rest 4/4, runtime 15/15. Ablations via ablation-replace.mjs (metadata-protocol rebuilt in each leg, ablation-dist-preflight exit 0): ABL1 precedence override branch off gives Tests 9 failed | 46 passed, each arming step red on its own (boot pull, kernel:ready, metadata:reloaded, mutation sync); ABL2 view exemption off gives metadata-protocol 5 failed and objectql "by-name (no package): expected SHIPPED to be OVERRIDE"; ABL3 hydration binding off gives the hydration case red and objectql "expected [ undefined, undefined ] to include com.example.showcase.custom"; ABL4 pair-row delete told to the store as override-artifact gives the objectql delete case red. Each leg: ok restored, blob == HEAD, git diff HEAD empty; after all four, rebuild, preflight --absent exit 0 for ABL1 to ABL4, porcelain empty. Pins whose meaning changed, same commit: the flow save sentence (protocol.packaged-base-refusal, sealed-remedy-kind, rest-meta-packaged-flow-refusal, runtime automation-packaged-base-lock: a save names the declaration, a delete still names the clone), register-item-call-sites args (the body is built one line above to record the binding; disposition unchanged), cli override-declarations (flow now admitted; the not-yet-available row moves to action).",
"gates": "dispatch-gates --commands: 68 families, each run with its exit captured before any pipe, plus the roster gates under touched dirs (check:authz-resolver, check:changeset-fixed, check:error-code-casing, check:route-ledger-census), all exit 0. First pass: check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity exit 3 (unbuilt packages, nothing measured) and check-plugin-teardown-shape --self-test exit 3 (pinned fixture 621a487 missing from the shallow clone); re-run after the type-check-debt closure build and a git fetch of 621a487: all exit 0. --ran: 68 derived, 68 run, 0 NOT-MEASURED, 0 UNRUN. typecheck exit 0 for metadata-protocol, service-automation, objectql, cli (retried with a 6 GB heap after one out-of-memory under concurrent gates), rest, runtime, cloud-connection. eslint --no-inline-config over those seven packages exit 0. CI on PR #28693 had started at report time and was not awaited (in_progress).",
"dogfood_battery": "0e67bba0 · os-verify-lock: VERDICT command-exit 0 · held the lock 1428s · Test Files 369 passed | 3 skipped (372) · Tests 2981 passed | 28 skipped (3009) · no failures (pnpm turbo run build --filter=@objectstack/dogfood, then pnpm turbo run test --filter=@objectstack/dogfood, unsharded)",
"mcp_calls": "3 — mcp__github__pull_request_read x2 (get #28651 for the body shape, get_check_runs #28693), mcp__github__list_pull_requests x1 (serial check); no write tool",
"api_writes": "3 — git push of claude/project-thread-jqgfyc; relay fleet-write pr_create (repository_dispatch, run 41314284001, read-back identical, assignees none); this os-dev-report via post-stamped.mjs --comment=28412",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: the by-name and layered read envelope of a served flow override still reports deletable: false (it follows the base seal), while the /meta delete naming the pair is now admitted · evidence: servedLockState reads the base item; PR #28693 acceptance notes · dedupe words: flow override deletable envelope Studio · carrier: N-18 #28570 (Studio Customize this flow) owns that surface; noted, not filed",
"class: c · reach: a package-less stored row beside the pair row of a shipped flow name can take the one bare registry slot, so the boot pull sees it instead of the override until kernel:ready re-arms from the view · evidence: the SchemaRegistry bare slot holds one stored row per name · dedupe words: bare slot package-less flow row override boot pull · carrier: N-16 migration of legacy rows; noted, not filed"
]
}
Generated by Claude Code
test(metadata-protocol): the hydrated flow body's _packageId is the c…
objectstack-fleet commentedon Nov 1, 2026
domain:server#1 · os-project-manager · session_01R1q2xdtYgU6FKrZ7LgjPCC
Landing record · #28412 · PR #28693
PR #28693 merged by the merge queue as
3ee6af44fdonmain(Fixes #28412). The contract review FAIL6559358939was followed by triage#1's delta PASS6561125936on56a1fe59, and the seat's ACCEPT is6561151030. The card closed on the merge.git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-28693-*'origin/mainby contentgit diff --quiet 3ee6af44fd origin/main --the PR's 23 filesgit merge-base --is-ancestor 3ee6af44fd origin/main.customflow override is now armed end to end:bindStoredRow)./metadelete naming the pair removes its row and restores the base. That delete is admitted forflowonly (CONDITIONS_DELIVERED), and every other open type keepsmain's delete verdict, which is pinned.@objectstack/metadata-protocoland@objectstack/service-automationare minor, withClause-②: yes (widening).main. Its first commit carries three comment corrections: thedeleteMetaItemreceipt-intent comment (triage nit), thestored-row-binding.tsheader sentence that fix(metadata-protocol)!: every metadata read states a stored row's package from its column, never from its body #28734 made false, and the#23233header inhydrateOverlayIntoRegistry(fix(metadata-protocol)!: every metadata read states a stored row's package from its column, never from its body #28734's should-fix S1).deletable: falseon the served override's envelope.Release: branch
claude/project-thread-jqgfycmerged; nothing unpushed.pm:dispatchedremoved and the card unassigned.Generated by Claude Code