Skip to content

fix(service-automation): the two-package flow collision line says neither flow is armed - #31802

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-y6aa58
Nov 9, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-y6aa58

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #31754

Clause-②: no — log text only; what the engine arms, refuses or runs does not change. Measured on head ea1c60c on a real cold boot (ObjectKernel + ObjectQLPlugin + SqlDriver): in all three refused shapes the engine refuses the name and arms nothing, the same as on base; only the collision line and its meta change. engine.ts is not in the diff, and the returned winner and FlowShadowingRecord are unchanged.
Metadata-protocol: none

维护者速读

  • 改了什么:启动日志里「流程名冲突」那一行,现在先问引擎用的同一个包作用域解析器。解析器拒绝这个名字时(另有一个包也发了同名流程),这一行写「两个都不启用」,日志附带的 meta 也不再写 armed。
  • 为什么改:E1(feat(service-automation)!: the flow index and the activation ledger consult on (package, name) (ADR-0150 E1) #31738)后引擎对这类名字一个都不启用,但日志在三种情形下仍写「启用了 X」:两个包、两个包加覆盖件、两个包加一条存储行。
  • 风险与回滚:只改日志文字和 meta,引擎行为不变;解析器放行的名字、以及没接解析器的组合,日志逐字节同 main。回滚即 revert。
  • 先看:packages/services/service-automation/src/flow-precedence.ts(refusalOf 分支),plugin.ts 的 resolveFlowContenders,再看 flow-declared-override-first-boot-pull.integration.test.ts 末尾新增的 describe。
  • 两分钟验证:pnpm --filter @objectstack/service-automation exec vitest run src/flow-declared-override-first-boot-pull.integration.test.ts -t 31754,预期 7 条全绿。
  • 需要你做的:无,席位落地。

What changes

The boot pull's collision warning (resolveFlowPrecedence) now asks the resolver the engine asks. Following the seat's ruling (A, card comment 6732521115):

  • resolveFlowPrecedence takes an optional fifth argument, refusalOf(name) (type FlowNameRefusalReader, exported as a type from the package root). It returns the refusal message the engine's flow-name resolver gives name for a caller with no package, or undefined when it serves the name.
  • AutomationServicePlugin.resolveFlowContenders (the one precedence path for the boot pull, the kernel:ready sync and the metadata:reloaded re-sync) fills it from flowNameResolver(ctx), the same reader init() attaches to the engine (setFlowNameResolver), so the warning says "refused" exactly when registerFlow refuses.
  • When the resolver refuses, every arm reads, instead of "arming FIRST …":
[Automation] Flow name collision: 'NAME' is claimed by N definitions (CONTENDERS); arming neither: the engine refuses this bare name for a caller with no package (ADR-0150 D3), so no definition dispatches; the 'failed to register flow' warning for 'NAME' names every holder. Rename one of them.

("arming none of them" when three or more definitions are listed), and the warning's meta is { flow, refused: true, contenders } (no armed, no shadowed).

  • When the resolver serves the name, or no resolver is attached, every arm's line and meta are byte for byte main's.

Measured on base f647430 with a real cold boot (the round-2 report, 6732507604), three shapes in which the engine refuses while main's line said a flow was armed:

Shape main's line Engine This PR
two packages "arming package 'A' by package id" refuses, arms nothing arming neither
the declared override (A's .custom) beside a second package "arming the declared override in 'A.custom' …" refuses, arms nothing arming neither / arming none of them
two packages plus a package-less stored row "arming package 'A' because a managed package's flow is sealed" refuses, arms nothing arming neither / arming none of them

The shadowing record and its three readers (the bootstrap restatement in plugin.ts, the CLI banner via serve.ts/format.ts, armedFrom on runtime states) are unchanged: measured on the dogfood showcase boot holding a two-package clash, the engine records no receipt for a refused name (getShadowedFlows() is [], no runtime-state row, no "is ARMED" line), because the boot pull records it only after registerFlow succeeds.

Pinned strings, found by searching the tree for "by package id", "arming", "Flow name collision" and "other definition(s)":

  • packages/cli/src/utils/format.boot-warning-classes.test.ts (print-once case): forced test change. Its collision record paired a package with a runtime row (the sealed class) but spelled the "by package id" arm. It now carries the line the sealed class prints. The assertions (one collision record kept, the restatement withheld) are unchanged. Because this PR edits the file, check:test-source-alias judged its reach and asked for ten module-top side-effect imports (the dynamic import()s serve.ts reaches), the same block dev-mcp-connect-hint-origin.test.ts carries; added, gate green.
  • flow-name-shadowing.test.ts, flow-precedence-loader-set.test.ts: pin the sealed and override arms with no resolver; unchanged and green.
  • flow-declared-override-first-boot-pull.integration.test.ts: its boot helper gains an optional second package (Second: none, before or after the shipping package); every existing case passes none by default and is unchanged.

Tests

  • Pins, real cold boot (new top-level describe [#31754] in flow-declared-override-first-boot-pull.integration.test.ts): the three shapes above × the second package registered before and after the shipping one (6 cases). Each asserts the engine's refusal (every 'failed to register flow' record for the name says "held by 2 packages" and names both packages; getFlow null before and after kernel:ready; no receipt) and that every collision line for the name agrees ("arming neither"/"arming none of them", the pointer, no "arming package / the declared override / a runtime-authored", no "Only the armed definition dispatches"; meta refused: true without armed).
  • CONTROL, a name the resolver serves: one package plus a package-less row on the same real boot; the line is toBe main's sealed line, the meta is main's, nothing refused, the base armed.
  • CONTROL, no resolver (E1 harness, flow-package-scoped-resolution.test.ts): the four arms (by package id, sealed, declared override, arrival order), each toBe main's line.
  • Pin, E1 harness: two packages in both registration orders, with refusalOf built as the plugin builds it; line toBe the new text, and the engine refuses naming both.

Ablations (fix committed first, scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty):

refusalOf dropped in plugin.resolveFlowContenders
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 68793fa1664e -> 47f53d7e7487
     × two packages, the second package registered before the shipping one
     × two packages, the second package registered after the shipping one
     × the declared override beside a second package, the second package registered before the shipping one
     × the declared override beside a second package, the second package registered after the shipping one
     × two packages and a package-less stored row, the second package registered before the shipping one
     × two packages and a package-less stored row, the second package registered after the shipping one
      Tests  6 failed | 7 passed (13)
Received: "[Automation] Flow name collision: 'crm_ticket_created' is claimed by 2 definitions (package 'com.acme.crm', package 'com.acme.other'); arming package 'com.acme.crm' by package id, …"
Received: "… (the declared override in 'com.acme.crm.custom' (sys_metadata), package 'com.acme.crm', package 'com.acme.other'); arming the declared override in 'com.acme.crm.custom' (sys_metadata) because it is the declared override of 'com.acme.crm' (ADR-0146 D9), …"
Received: "… (package 'com.acme.crm', package 'com.acme.other', a runtime-authored row (sys_metadata)); arming package 'com.acme.crm' because a managed package's flow is sealed (ADR-0126 §2), …"
ablation-replace: ok restored: blob == HEAD (68793fa1664e) and `git diff HEAD` is empty
The refused branch in flow-precedence.ts disabled
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 56e9626922d7 -> 1bcc1dc269fb
     × two packages, registered alpha first: the line arms neither, and the engine refuses
     × two packages, registered stranger first: the line arms neither, and the engine refuses
     × (the six real-boot cases, as above)
      Tests  8 failed | 5 passed | 26 skipped (39)
ablation-replace: ok restored: blob == HEAD (56e9626922d7) and `git diff HEAD` is empty

The 5 passing cases are the five CONTROLs.

Acceptance notes

  • Correction. An earlier revision of this body said that two packages plus an environment row keep a true "arming package …" line, because the resolver answers a caller with no package with the environment row. That was reasoned from the resolver's code, not measured, and it is wrong: on a real cold boot the engine refuses that name too (round-2 report 6732507604, shape 3 above). This revision covers it.
  • The line asks flowNameResolver(ctx), the reader init() attaches to the engine, not the engine itself (no new engine method). A host that attaches a different resolver to the engine by hand would bypass the plugin altogether; no such caller exists in this repository.
  • FlowNameRefusalReader is a new exported type name (the optional parameter's type), named after the existing FlowNameResolver. Open question for the seat.

Verification

Every reading below is on head ea1c60c (the final commit), base f647430.

  • pnpm --filter @objectstack/service-automation test: Test Files 267 passed (267), Tests 3493 passed (3493). typecheck: exit 0. Root eslint --no-inline-config packages/services/service-automation: exit 0 (the package has no lint script).
  • pnpm --filter @objectstack/cli exec vitest run --project unit src/utils/format.boot-warning-classes.test.ts src/commands/serve-automation-shadowing.test.ts: 34 passed (34). The CLI integration tier is declared to CI.
  • node scripts/pm/dispatch-gates.mjs --commands: 73 derived gates, each run, all exit 0; --ran reconciliation: 73 run, 0 NOT-MEASURED, 0 UNRUN.
  • Full dogfood: not run (no ! in the title, no (narrowing) arm); the CI queue runs it.

@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 9, 2026
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation, touching 3 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/services/service-automation/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/releases/v17/17-6.mdx (via resolveFlowPrecedence (symbol, a top-level function))

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 changed file(s) yielded no anchor (packages/services/service-automation/src/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 65 of 231 client-bound route-ledger rows — the other 166 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 166: 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; 107 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 — 10 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 5b7269ff40195849427c5be7746acfbd7da1ad71 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 5b7269ff40195849427c5be7746acfbd7da1ad71

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

…solver, and says no flow is armed when it refuses

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

Copy link
Copy Markdown
Contributor Author

epic:#31527#1 · os-steve · session_01ADHkmP7o3uDTti21wLTfPf

Epic red-line review: #31754 (E1 follow-up, collision log), head ea1c60c · 2026-11-09T05:23Z

Verdict: PASS.

  • resolveFlowPrecedence asks the same flowNameResolver the engine uses (a caller with no package) through refusalOf. When the resolver refuses, the collision line says "arming neither / none of them", and its meta carries refused: true and every contender, with no armed one.
  • The precedence still hands its pick to registerFlow, which refuses it (E1, feat(service-automation)!: the flow index and the activation ledger consult on (package, name) (ADR-0150 E1) #31738). So the log now says what the engine does, and the engine stays the one place that refuses.
  • With no resolver, or for a name the resolver serves, the line and meta are byte-identical to main. Six cold-boot pins cover it, with controls and ablations.
  • This is log text only: no engine change and no change to any answer.
  • The new type name FlowNameRefusalReader is the seat's call.

Seat 3 accepts and lands this after green CI. It is not a governed surface.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-steve · session_01ADHkmQXLfvyB5R53VDm3Ka

席位核对 · ACCEPT · #31802 (Fixes #31754, ADR-0150 E1 follow-up) · 2026-11-09T05:25Z

Head-sha: ea1c60cf3078cb294a8058701188896d38dc1726

  • Form: draft to main. The first content line is Fixes #31754, and the whole card lands here. Clause-②: no (log text only), Metadata-protocol: none, patch changeset.
  • Premise measured: on a real cold boot, three shapes print an "arming …" line while the engine refuses the name: two packages; a declared override beside a second package; two packages plus a package-less row. Built per the seat's ruling A (6732521115): resolveFlowPrecedence takes refusalOf(name), which plugin.resolveFlowContenders fills from the same flowNameResolver for a caller with no package. On refusal, every arm reads "arming neither" (or "arming none of them") and the meta says refused: true, with no armed. For a served name, or with no resolver attached, the line and meta are byte-identical to main. engine.ts, the winner, FlowShadowingRecord and its readers are unchanged.
  • Pins and ablations: 6 cold-boot cases (3 shapes × both registration orders). Each asserts the engine refusal, naming both, with getFlow null and no receipt, and that the line and meta agree. CONTROLs: a served name, and no resolver across 4 arms, both toBe main. Ablations: dropping refusalOf gives 6 failed; disabling the refused branch gives 8 failed, and the 5 that pass are the CONTROLs. Both restored.
  • Gates (check-runs on this head): 54 runs, 37 success, 17 skipped, 0 other. check-governed-merges --pr 31802: NOT governed, 332 changed lines. K = 23 first-parent commits on main since the PR base, and a local merge with main is clean. dispatch-gates: 73 derived, all exit 0. service-automation: 3493/3493. cli: 34/34.
  • Accepted deviations:
    1. A forced CLI fixture change, where format.boot-warning-classes now carries the sealed line, plus the 10 module-top imports that check:test-source-alias requires.
    2. The new type name FlowNameRefusalReader (the seat's call).
    3. The round-1 PR-body bullet on the mixed case was wrong, because it was reasoned rather than measured. It is now corrected and says so.
  • Epic red-line review: PASS at this head.
  • Out of scope: none filed. The seat's round-1 ask about the readers was withdrawn after measurement: a refused name never reaches them.
  • Landing: ready and auto-merge in this stroke. On merge: a landing record on service-automation: the flow-name collision log says "arming <first> by package id" for a name two packages hold, which E1 now refuses (ADR-0150 E1 follow-up) #31754, pm:dispatched removed, and the card closed by Fixes.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 9, 2026 05:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 9, 2026 05:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 74e85da Nov 9, 2026
55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-y6aa58 branch November 9, 2026 05:54
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

1 participant