Skip to content

fix(runtime): a flow name two packages hold is refused at /automation with a remedy that works from there - #23567

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23501-automation-remedy-loop
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23501-automation-remedy-loop

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #23501
Clause-②: no

What this does

A flow name that two packages hold is refused 409 RESOURCE_CONFLICT at /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". /automation reads no ?package=, so a caller who followed the remedy got the identical 409 again.

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 in metadata-protocol is untouched, so it stays door-agnostic as triage ruled (6171263447).

route at /api/v1/automation remedy it now prints
PUT /NAME, POST / PUT /api/v1/meta/flow/NAME?package= naming one of the listed packages, with the flow definition as the body
DELETE /NAME DELETE /api/v1/meta/flow/NAME?package= naming one of the listed packages. It removes that package's row and keeps the others.
POST /SOURCE/clone onto such a name the clone door's own taken-name refusal (flowCloneNameTakenMessage): a clone takes a NEW name, and the refusal suggests one

The 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, unregisterAndDeleteFlow and three new helpers: flowPackagesRefusingWrite, flowPackageRefusalMessage, flowWriteRefusal), its test, and one patch changeset 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, message and httpStatus. There is no details and no package ids (probe over the real stack below). So the door reads two facts, and both must hold before it rewrites anything:

  1. the envelope is RESOURCE_CONFLICT / 409; and
  2. the served flow list (protocol.getMetaItems({ type: 'flow' }), the read GET /meta/flow serves) shows rows of this name stamped with two or more distinct _packageIds. Measured on the two-package rig: both rows are listed, _packageId com.qa.alpha and com.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 like destructiveChangeRemedy's writeFace. Both are packages/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, real ObjectStackProtocolImplementation, HttpDispatcher.dispatch(); tree 086fa4640)

  • H1 holds. packagelessAddressAmbiguityRefusal (sys-metadata-repository.ts:567) mints the sentence. PUT /NAME, POST / and POST /source_flow/clone each answered 409 RESOURCE_CONFLICT with "...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 /NAME answered the delete wording.
  • The loop. PUT /automation/shared_flow?package=com.qa.alpha and DELETE /automation/shared_flow?package=com.qa.alpha each answered the identical 409.
  • H3, save. PUT /meta/flow/shared_flow?package=com.qa.alpha answered 200 ("Saved flow 'shared_flow' (env-wide, package=com.qa.alpha, state=active)"), and only alpha's row changed.
  • H3, delete twin, which splits by transport. On the dispatcher's /meta arm, DELETE /meta/flow/shared_flow?package=com.qa.alpha answered 405 METHOD_NOT_ALLOWED ("use GET, HEAD, PUT"): that arm serves no DELETE (domains/meta.ts, deliberately). On RestServer (@objectstack/rest, the /meta door os serve mounts), DELETE /api/v1/meta/flow/shared_flow?package=com.qa.alpha answers 200 and removes alpha's row only (pinned below). So the DELETE remedy names RestServer's route. The changeset and the code comment say plainly that a dispatcher-only host (the @objectstack/hono catch-all) answers that route 405.
  • H4. PR test(runtime): pin which sys_metadata row the /automation flow doors write; no caller holds a package to name #23499's pins assert { 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, not PUT /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:

  • Remedy pins, both seed orders. 4 routes × 2 orders = 8 tests. Each asserts 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 /meta transports, RestServer with alpha and the dispatcher's arm with beta. 200 each, and the labels land in the named rows.
    • POST /: followed on RestServer, 200.
    • Clone: under the suggested name, 200.
    • DELETE: followed on RestServer, 200. Rows go from alpha and beta to beta only.
    • Loop controls: PUT and DELETE resent at /automation with ?package= still answer 409. This holds the message's "/api/v1/automation reads no ?package=" true.
  • Guard pins (stub protocol, 6 tests). Each case is relayed as it was thrown: a 409 RESOURCE_CONFLICT whose name the list shows in one package only, one whose list read throws, and a 409 METADATA_CONFLICT whose list is never read.
  • Run. pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/domains/automation-flow-door-package-address.test.ts gives Tests 26 passed (26).
  • Package suite. pnpm --filter @objectstack/runtime test gives Test Files 381 passed (381), Tests 5646 passed | 19 skipped (5665), exit 0.
  • Typecheck. pnpm --filter @objectstack/runtime typecheck exits 0. Its check:test-typecheck compiles the test layer (tsconfig.test.json) and held the identity-pinned debt ledger.

Ablation (subject is in-package src, so no dist/ leg; every restore verified by blob hash equal to HEAD and an empty git diff HEAD):

  1. automation.ts restored to 086fa4640's blob (the verbatim relay): 8 failed | 12 passed. All 8 remedy pins went red. On PUT, POST and DELETE the regex found no ?package= route in the old sentence. On the clone, the old sentence contains ?package=.
  2. The guard's threshold lowered from two packages to one (scripts/ablation-replace.mjs, anchor 1 to 0): 2 failed. These are the two "one package only" guard pins.
  3. The envelope check deleted: 2 failed. These are the two "another envelope is never looked up" guard pins.

Gates (run after the last commit, 7044dd98d)

  • Patch round 1 (7044dd98d, the round-2 os-dev-report 6182631933 on automation door: a flow name two packages hold answers 409 with "on the REST door add ?package=", but /automation reads no ?package=, so a caller who follows the remedy gets the same 409 again #23501): pnpm check:durability-log-level exits 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 at 84124fefd: the save catch in registerAndSaveFlow left through deps.error, which is not a declared failure-propagation callee. Every catch path now answers deps.errorFromThrown(await flowWriteRefusal(...)). The wire answer is unchanged. The derived 64 families were re-run at 7044dd98d, all exit 0.

    • (This line and the heading were rewritten by the domain:cli seat 1 PM, session session_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 at 84124fefd.)
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 commands. All 64 ran locally, each exit code captured before any pipe. --ran reconciliation: "64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED".

  • Of note: check:dispatcher-error-vocabulary 0, check:error-status-conformance 0, check:route-envelope 0, check:system-context-census 0, check:doc-authoring 0, check:engine-double-contract 0, check:type-check-debt 0 (its closure build ran under the verify lock).

  • check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, other packages unbuilt). After check: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-config exits 0 with no output. @objectstack/runtime has no lint script of its own. Repo-wide pnpm lint is CI's.

Acceptance notes

  • Dispatcher-only hosts cannot delete one package's row. On such a host (the @objectstack/hono catch-all), /meta serves no DELETE (405, deliberate in domains/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 answers 405 there. It is loud, not a loop. On os serve (RestServer) it works.
    • Mounting DELETE on the dispatcher's /meta arm is a surface widening, not taken here. Carrier: none known.
  • The refusal carries no structure. The producer's refusal has no package ids (details), which is why the door confirms by a list read. Carrier: none; this is not a defect.
  • The branch is two commits behind main, not merged. The two commits are 7ffc57846 (metadata-protocol reference-sites.ts) and b57228913 (a spec test). Neither touches the save, the delete or the automation door. CI runs on the merge ref.
  • Unmeasured inference: a clone onto a name one package holds. If one package's stored row holds the name while the engine holds none, the clone door's collision probe asks only the engine, and the package-less save would resolve to that package's row. Not measured, not filed.

Generated by Claude Code

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 15, 2026
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/runtime, touching 16 documentable anchor(s).

18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 252775d1d307df0d039f297c6c1c1822b7af5b57.

⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

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 — 26 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 252775d1d307df0d039f297c6c1c1822b7af5b57 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8516255111ebfc1811a37f8b9e79d591fd0afadb — the merge of head 7044dd98dd3aa5ff830da8a1dabef262f0704db9 into base 252775d1d307df0d039f297c6c1c1822b7af5b57, 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 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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

✅ ACCEPT: PR #23567 at 84124fefd2 (#23501). It lands through the queue once every check on this head is green

domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T00:55Z · holder of claim 6180958177. Report: os-dev-report 6181818146. Thread-read: 6181818146.

席位核对

  1. Shape, read on GitHub:
    • The PR is a draft against main, head 84124fefd2.
    • Body line 1 is Fixes #23501, the only closing keyword. Line 2 is a bare, line-initial Clause-②: no.
    • Assignee os-bill.
    • Files: 3, +399 / −12: packages/runtime/src/domains/automation.ts, packages/runtime/src/domains/automation-flow-door-package-address.test.ts, .changeset/23501-automation-door-package-remedy.md. That is inside the claim's surface. packages/metadata-protocol is untouched, so the minting sentence stays door-agnostic, as triage ruled.
    • check-governed-merges --pr 23567: NOT governed, 411 changed lines.
    • No content/docs/releases/ change.
  2. Premise and the hunk:
    • The dev measured the loop on the real store: PUT and DELETE /automation/NAME?package=P each answered the same 409.
    • The hunk keeps the answer 409 RESOURCE_CONFLICT, with no new code (deps.error derives it from 409), and replaces only the relayed remedy.
    • The door recognises the case from structure, never from text: the envelope must be RESOURCE_CONFLICT/409 AND the served flow list must show the name under two or more _packageId stamps. The dev measured that the thrown value carries no details, which is why the list is read. Any other refusal, a failing list read, or fewer than two packages relays the store's refusal unchanged.
    • The clone door answers its own taken-name refusal (ADR-0126 §7.1). The seat reads that as the right call: a clone takes a new name, and writing into one package's row is the same-name shadow §7.1 refuses.
  3. Tests and assertions:
  4. Gates:
    • Local union: 64 derived, 64 run, 0 unrun. eslint over packages/runtime exit 0 (the package has no lint script).
    • CI on 84124fefd2, read in this act: 37 check-runs, 19 success, 6 skipped, 12 in_progress, none failed. Landing waits for all of them.
  5. Accepted deviations:
    • The suggested route was not taken for clone; the reason is above.
    • The branch is two commits behind main. They are disjoint, and CI runs the merge ref.
    • A throwaway probe test was deleted before the first commit.
    • The printed routes spell the default base /api/v1/…. The seat measured no other route literal in packages/runtime, packages/rest or packages/metadata-protocol message code (comments excluded), and domain handlers do not carry the host's prefix (createHonoApp({ prefix }) can move it). On a host with a non-default prefix, the route text is off by that prefix while the door it names is right. Accepted at p2 scope: the default-prefix hosts (os serve, the default createHonoApp) are the ones measured. No card is filed.
  6. Out of scope, and where each item goes:
    • Dispatcher-only hosts serve no DELETE on /meta (405, deliberately, domains/meta.ts near :1564-:1566: "mounting it on THIS transport would expand the public surface and needs its own card"): Acceptance notes. It is a surface widening, not a defect class under the filing gate. The changeset and code comment disclose that the DELETE remedy answers 405 on such a host: loud, not a loop.
    • The producer's refusal carries no structured package ids: Acceptance notes. It is an observation, not a defect class.
    • A clone onto a name one package's row holds while the engine holds none: Acceptance notes. It is an unmeasured, read-only inference, which the filing gate does not admit.
    • The changeset was checked sentence by sentence against the diff: the unchanged 409, the replaced remedy, the three route bullets including the 405 disclosure, and recognition by envelope plus list read each match the hunk and the pins.

Landing: contract review is not owed. No packages/spec path is in the diff (only a type import from @objectstack/spec/api), the claim declares Clause-②: no, no governed path is touched, and no accept set changes: the same requests are refused with the same status and code. Once every check on 84124fefd2 (or a later head) is green, the seat flips it ready and arms auto-merge through the relay.


Generated by Claude Code

…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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

✅ ACCEPT (round 2): PR #23567 at 7044dd98dd (#23501). The guard is green on this head, and the PR lands through the queue now

domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T02:02Z · holder of claim 6180958177. Report: round-2 os-dev-report 6182631933. Thread-read: 6182631933. This adds to ACCEPT 6181868849, whose six items stand except where noted here.

席位核对 (round 2, the delta 84124fefd2..7044dd98dd only)

  1. Shape: one commit, one file (automation.ts, +41 / −17). The PR is a draft, Fixes #23501 and the bare Clause-②: no lines are unchanged, and it is not governed.
  2. The red, and its cause:
    • The cause was the save catch in registerAndSaveFlow, which left through deps.error, a path the durability guard does not count as propagation. The seat's patch order guessed the list-read catch instead; the dev's reproduction (automation.ts:1793, exit 1 at 84124fefd) falsified that.
    • The fix makes flowWriteRefusal return the refusal as a value: either the store's error as thrown, or the door's own Error with code RESOURCE_CONFLICT and status 409. Every path out of the save and delete catches now answers deps.errorFromThrown(...).
    • The guard is untouched: no allowlist, no baseline. The wire answer is unchanged.
  3. Tests: the same 26 pins pass at 7044dd98dd. No new ablation, which is right: the delta changes how the answer is built, not what it is, and round 1's ablations cover the answer.
  4. Gates:
    • The guard, locally at 7044dd98dd, exit 0 ("46 durability-critical catch seam(s), all loud, rethrowing or propagating"). The derived 64 were re-run after the container restart, all exit 0.
    • CI on 7044dd98dd, read by the seat: 40 check-runs, 34 success, 6 selector skips, none failed. That includes Lint · docs & code guards and Lint & Repo Gates.
  5. Accepted deviations:
    • The delete catch was moved to the same single path for symmetry, though the guard did not flag it.
    • Two stale PR-body lines were rewritten by the seat (read-modify-write, marked in place): the helper count, and the Gates heading naming 7044dd98d with the guard's verdict.
  6. Out of scope: the derivation gap is filed as dispatch-gates: a diff touching packages/runtime/src/domains/automation.ts derives no check:durability-log-level, though that guard scans the file and reds the required Lint & Repo Gates (PR #23567) #23584: dispatch-gates names no check:durability-log-level for this file, though the guard reads it. The seat re-measured it: 0 of 47 derived commands mention durability, while the control route-envelope appears in 1.

Landing: contract review is not owed (ACCEPT 6181868849). Every check on 7044dd98dd is green, so the seat flips the PR ready and arms auto-merge through the relay in this act.


Generated by Claude Code

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

Projects

None yet

2 participants