Repository navigation
fix(runtime): a flow name two packages hold is refused at /automation with a remedy that works from there - #23567
Conversation
…t works from there
A flow name two packages hold is refused 409 RESOURCE_CONFLICT at
/automation, and the relayed remedy ("on the REST door add ?package=")
named a query this door does not read, so following it looped. The door
now answers the same 409 with the metadata door's ?package= route (PUT
for the save doors, DELETE for the delete), or the clone door's own
taken-name refusal for a clone.
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG Co-Authored-By: Claude <noreply@anthropic.com>
…yed as thrown Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG Co-Authored-By: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 26 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8516255111ebfc1811a37f8b9e79d591fd0afadb && git checkout 8516255111ebfc1811a37f8b9e79d591fd0afadb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 252775d1d307df0d039f297c6c1c1822b7af5b57 7044dd98dd3aa5ff830da8a1dabef262f0704db9 && git checkout -B drift-repro 252775d1d307df0d039f297c6c1c1822b7af5b57 && git merge --no-ff 7044dd98dd3aa5ff830da8a1dabef262f0704db9
node scripts/docs-audit/affected-docs.mjs --json 252775d1d307df0d039f297c6c1c1822b7af5b57
|
✅ ACCEPT: PR #23567 at
|
…orFromThrown The durability log-level guard reads the catch around the door's saveMetaItem as delivering only through a declared callee (errorFromThrown). The door's own 409 was answered through deps.error on a second path, which the guard read as a swallowed failure. The door now builds the refusal as a value (flowWriteRefusal) and every path out of the catch, the delete's included, answers it through errorFromThrown. Same wire answer: 409 RESOURCE_CONFLICT with the same text. Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG Co-Authored-By: Claude <noreply@anthropic.com>
✅ ACCEPT (round 2): PR #23567 at
|
Fixes #23501
Clause-②: no
What this does
A flow name that two packages hold is refused
409 RESOURCE_CONFLICTat/api/v1/automation, because the door saves and deletes without naming a package. The refusal is correct, but the door relayed the metadata store's remedy as it was: "on the REST door add ?package= with the package id"./automationreads no?package=, so a caller who followed the remedy got the identical409again.The answer is still
409 RESOURCE_CONFLICT. Only its remedy changes, and it now names a route that works from this door. No new error code and no new door input. The minting sentence inmetadata-protocolis untouched, so it stays door-agnostic as triage ruled (6171263447)./api/v1/automationPUT /NAME,POST /PUT /api/v1/meta/flow/NAME?package=naming one of the listed packages, with the flow definition as the bodyDELETE /NAMEDELETE /api/v1/meta/flow/NAME?package=naming one of the listed packages. It removes that package's row and keeps the others.POST /SOURCE/cloneonto such a nameflowCloneNameTakenMessage): a clone takes a NEW name, and the refusal suggests oneThe facts the protocol's refusal states are kept: nothing was written or deleted, the flow name, and the packages that hold it (sorted, first five, then a count).
Landing:
packages/runtime/src/domains/automation.ts(registerAndSaveFlow,unregisterAndDeleteFlowand three new helpers:flowPackagesRefusingWrite,flowPackageRefusalMessage,flowWriteRefusal), its test, and onepatchchangeset for@objectstack/runtime.How the door recognises the refusal (H2): not by the text
Measured: the thrown value carries its ADR-0112 envelope and nothing else. The wire body is
code,messageandhttpStatus. There is nodetailsand no package ids (probe over the real stack below). So the door reads two facts, and both must hold before it rewrites anything:RESOURCE_CONFLICT/409; andprotocol.getMetaItems({ type: 'flow' }), the readGET /meta/flowserves) shows rows of this name stamped with two or more distinct_packageIds. Measured on the two-package rig: both rows are listed,_packageIdcom.qa.alphaandcom.qa.beta.The protocol stays the one decider. The list read supplies the package ids the answer names. When the two facts disagree, the store's refusal is relayed exactly as before: fewer than two packages listed, a list read that throws, or any other envelope (which is never looked up). Six pins hold that half.
The cleaner long-term shape would be a structured carrier on the producer's refusal, such as
details.packages, or a face-aware remedy likedestructiveChangeRemedy'swriteFace. Both arepackages/metadata-protocol/src/**edits, which are outside this card's surface: read-only here, and held by #23429's in-flight claim. See Acceptance notes.Measurements (real store: better-sqlite3, real
ObjectQL, realObjectStackProtocolImplementation,HttpDispatcher.dispatch(); tree086fa4640)packagelessAddressAmbiguityRefusal(sys-metadata-repository.ts:567) mints the sentence.PUT /NAME,POST /andPOST /source_flow/cloneeach answered409 RESOURCE_CONFLICTwith "...on the REST door add ?package= with the package id, in process pass packageId. Nothing was written. flow 'shared_flow' belongs to 2 packages ('com.qa.alpha', 'com.qa.beta')...".DELETE /NAMEanswered the delete wording.PUT /automation/shared_flow?package=com.qa.alphaandDELETE /automation/shared_flow?package=com.qa.alphaeach answered the identical409.PUT /meta/flow/shared_flow?package=com.qa.alphaanswered200("Saved flow 'shared_flow' (env-wide, package=com.qa.alpha, state=active)"), and only alpha's row changed./metaarm,DELETE /meta/flow/shared_flow?package=com.qa.alphaanswered405 METHOD_NOT_ALLOWED("use GET, HEAD, PUT"): that arm serves no DELETE (domains/meta.ts, deliberately). OnRestServer(@objectstack/rest, the/metadooros servemounts),DELETE /api/v1/meta/flow/shared_flow?package=com.qa.alphaanswers200and removes alpha's row only (pinned below). So the DELETE remedy namesRestServer's route. The changeset and the code comment say plainly that a dispatcher-only host (the@objectstack/honocatch-all) answers that route405.{ status: 409, code: 'RESOURCE_CONFLICT' }only, never the remedy wording. None flips; all 10 still pass.Deviation from the suggested route (Zone 3): the clone
For
POST /:name/clone, the remedy is the clone door's own taken-name refusal, notPUT /meta/flow/NAME?package=. A clone writes a NEW flow and carries no package envelope (ADR-0126 §7.1). Writing the copy into one package's row of a name two packages hold would be the same-name shadow that the clone door's taken-name refusal exists to stop. Followed as printed, the refusal clones under the suggested name:200, saved as a package-unbound row, and the two-package rows unchanged.Tests (
packages/runtime/src/domains/automation-flow-door-package-address.test.ts, extended)Head
84124fefd:code+status, the method and route the remedy names, and the two packages it states, then follows the remedy as printed: the method and path are read out of the answer, never restated by the test.PUT: followed on both/metatransports,RestServerwith alpha and the dispatcher's arm with beta.200each, and the labels land in the named rows.POST /: followed onRestServer,200.200.DELETE: followed onRestServer,200. Rows go from alpha and beta to beta only.PUTandDELETEresent at/automationwith?package=still answer409. This holds the message's "/api/v1/automation reads no ?package=" true.409 RESOURCE_CONFLICTwhose name the list shows in one package only, one whose list read throws, and a409 METADATA_CONFLICTwhose list is never read.pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/domains/automation-flow-door-package-address.test.tsgivesTests 26 passed (26).pnpm --filter @objectstack/runtime testgivesTest Files 381 passed (381),Tests 5646 passed | 19 skipped (5665), exit 0.pnpm --filter @objectstack/runtime typecheckexits 0. Itscheck:test-typecheckcompiles the test layer (tsconfig.test.json) and held the identity-pinned debt ledger.Ablation (subject is in-package
src, so nodist/leg; every restore verified by blob hash equal to HEAD and an emptygit diff HEAD):automation.tsrestored to086fa4640's blob (the verbatim relay): 8 failed | 12 passed. All 8 remedy pins went red. OnPUT,POSTandDELETEthe regex found no?package=route in the old sentence. On the clone, the old sentence contains?package=.scripts/ablation-replace.mjs, anchor 1 to 0): 2 failed. These are the two "one package only" guard pins.Gates (run after the last commit,
7044dd98d)Patch round 1 (
7044dd98d, the round-2os-dev-report6182631933on automation door: a flow name two packages hold answers409with "on the REST door add ?package=", but/automationreads no?package=, so a caller who follows the remedy gets the same409again #23501):pnpm check:durability-log-levelexits 0: "durability-degradation log levels: 46 durability-critical catch seam(s), all loud, rethrowing or propagating to the caller (7 propagating, declared)". It was red at84124fefd: the save catch inregisterAndSaveFlowleft throughdeps.error, which is not a declared failure-propagation callee. Every catch path now answersdeps.errorFromThrown(await flowWriteRefusal(...)). The wire answer is unchanged. The derived 64 families were re-run at7044dd98d, all exit 0.domain:cliseat 1 PM, sessionsession_01FLwgbTSmGMrqsXkBDsiwfG, as a read-modify-write of the live body. The dev does not PATCH a PR body. The bullets below are round 1's readings at84124fefd.)node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 64 commands. All 64 ran locally, each exit code captured before any pipe.--ranreconciliation: "64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED".Of note:
check:dispatcher-error-vocabulary0,check:error-status-conformance0,check:route-envelope0,check:system-context-census0,check:doc-authoring0,check:engine-double-contract0,check:type-check-debt0 (its closure build ran under the verify lock).check:dual-build-cjs-loadsfirst exited 3 (PREREQUISITE NOT MET, other packages unbuilt). Aftercheck:type-check-debt's full closure build it was re-run and exited 0. The re-run is the recorded reading.Lint (the lane blind spot):
pnpm exec eslint packages/runtime --no-inline-configexits 0 with no output.@objectstack/runtimehas no lint script of its own. Repo-widepnpm lintis CI's.Acceptance notes
@objectstack/honocatch-all),/metaserves no DELETE (405, deliberate indomains/meta.ts, which names it as needing its own card). So on that host no door removes one package's row of a two-package flow, and the DELETE remedy printed here answers405there. It is loud, not a loop. Onos serve(RestServer) it works./metaarm is a surface widening, not taken here. Carrier: none known.details), which is why the door confirms by a list read. Carrier: none; this is not a defect.main, not merged. The two commits are7ffc57846(metadata-protocolreference-sites.ts) andb57228913(a spec test). Neither touches the save, the delete or the automation door. CI runs on the merge ref.Generated by Claude Code