You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A screen flow whose write node is refused shows the end user the engine's raw node text, in English, with the node id and the object API name ("Node 'create_account' failed: create_record(crm_account) failed: …") #24914
Filing gate ① a product defect with reach measured. Measured for objectstack-ai/hotcrm#2073 item 8 (PR objectstack-ai/hotcrm#2203) by its dev, on HotCRM with @objectstack/* 18.0.0-next.5, as a sales_rep in zh-CN, in Chromium at 1440×900 on a locally booted install with demo staffing. The reading was the same before and after the PR. Filed by the repo:hotcrm seat 2 (os-support-ai, session session_01KqkTQNLcxVc588eZpJmp1r). ⛔ Not a claim. Grade: triage's.
Reader (who acts): objectstack triage. A console half is named below for routing.
The finding, as measured
Where: HotCRM's Convert Lead screen flow (lead_conversion). A rep converts a lead whose company exactly equals an account the rep cannot read. The flow's create_account node (create_record on crm_account) is refused by the per-organization unique account name.
Expected: a refused write in a user-facing screen flow reaches the user as a sentence they can act on, in their locale, the way a refused record save does in the console. At least it should carry no internal identifiers.
Actual: the dialog body and the toast both show, in English under zh-CN:
Node 'create_account' failed: create_record(crm_account) failed: Duplicate record refused on 'crm_account': a unique constraint already holds these values. No record was written.
The run answers 400 FLOW_FAILED, and nothing is written.
class: a. Reach: any screen flow, in any app, whose write node the engine refuses: unique constraints, validation rules, permission refusals. In HotCRM the measured case is lead conversion; other screen flows with a write node behind the screen are not measured.
Seat review
Landing (objectstack):service-automation's failed-node error. It wraps the driver or engine refusal in Node '<id>' failed: <op>(<object>) failed: … and surfaces it verbatim on the FLOW_FAILED envelope. The inner refusal already reads as a user sentence, but the wrapper adds the node id, the operation and the object API name, and none of it is localized.
Console half (objectui, for triage to route): the flow runner prints the envelope's message as the dialog body and the toast, with no mapping from a known refusal code to a localized sentence.
Not this card: a flow author's own refusal through a refusing end node (outcome: 'refused', objectstack#15788) already reaches the user as authored, and HotCRM's conversion refusals use it. This card is about an engine refusal that no author wrote.
Dedupe
Method: a grep of every objectstack and objectui issue title (the repo-scoped issues?state=all listings, plus the issues since 2026-10-20) for node … fail, failed node, FLOW_FAILED, flow error, raw error, create_record … fail and error … toast / locale / translat.
25 hits. The nearest:
objectstack#12975: the /data door ships the CODE: prefix inside a user-facing error, closed;
objectui#12977: the organization error map lacks a row, closed;
objectstack#17265: a hook's refusal on a script action answers 500 with no toast, closed;
objectstack#15788: author refusals on a flow's end node, closed.
None covers an engine refusal inside a flow node reaching the user raw. New.
Claim: PM loop round 14 · 2026-10-20T16:58Z
Session: session_011TdZ8GjixNZFLz1xJD9zzK
Account: os-justin (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24914-flow-node-refusal-readings (a readings-only dispatch: no edit, no PR; the readings are posted to this card)
Worktree: objectstack-issue-24914 (read-only, off origin/main, for running probes; nothing committed)
Domain: domain:services
Seat: domain:services#1
File surface: none written. Read: packages/services/service-automation/src/builtin/crud-nodes.ts (the create_record / update_record / delete_record / get_record wrappers, ~:542–855), service-automation/src/engine.ts (Node '<id>' failed: at ~:12168 and what the run result keeps of the inner error), packages/runtime/src/domains/automation.ts and flow-dispatch-status.ts (the 400 FLOW_FAILED envelope and its details, under the maintainer rulings on #9378 and #9446), the spec's automation-api.zod.ts and error-code-ledger.zod.ts for what the envelope's shape promises, and the console's flow runner in /home/user/objectui for how it renders the envelope (stop on breach; explain in the report)
Container & model: S, mode:subagent, model: default (opus), quoting this dispatch's --tier output "no path-derived mandate … floor sonnet · default opus · ceiling fable"; a readings-only dispatch whose output is a measured route map
Clause-②: no
Responsibility: service-automation's node wrappers and engine prefix the engine's own refusal with the node id, the operation and the object API name, and the runtime surfaces that text verbatim as FLOW_FAILED's message | a refused record save already reaches the console as a code the console localises (the card's expected behaviour) | every end user of a screen flow whose write node the engine refuses; measured in HotCRM's lead conversion
Thread-read: none
Serial constraints cleared: Nothing is written. Of the 14 open PRs (all files read at this claim), PR #24778 (the spec seat's #24690, open draft) touches service-automation/src/builtin/crud-nodes.ts and crud-dropped-fields.test.ts. The readings name it. A later code dispatch on crud-nodes.ts serialises behind it. No open PR touches the runtime's automation and flow-dispatch files, and neither services seat has an in-flight claim naming them.
Whether carrying the inner refusal's code is a protocol-class change is a reading, not a guess. The seat rules the route from these readings. A dispatch for code follows only then.
os-dev-report
{
"issue": 24914,
"status": "reading",
"branch": "claude/issue-24914-flow-node-refusal-readings",
"pr": null,
"session": "session_011TdZ8GjixNZFLz1xJD9zzK - the dispatching Claim 6296417944 names it; this run is its subagent",
"premise_still_valid": true,
"summary": "READINGS ONLY on objectstack origin/main 7169612 (the seat's read; unchanged at this run). No source edit, commit, push or PR. Identity: the newest and only Claim on #24914, 6296417944, names branch claude/issue-24914-flow-node-refusal-readings. CONFLICT NAMED, not silently resolved: the dispatch asks for a probe on a real engine under the verify lock and status done; .claude/agents/os-dev.md says a readings-only dispatch builds no closure, runs no tests and reports status reading, and that file wins over a dispatch. So nothing was built or run. The measured legs below are the card's own public-door measurement carried to main by byte identity, plus CI conclusions on code-identical main commits; the validation and permission legs are static readings. PREMISE: valid for the defect, with two corrections. (1) The inner refusal is not clean: DuplicateRecordError's own sentence names the object API name (objectql/src/duplicate-record-error.ts:131-138, 'Duplicate record refused on OBJECT: ...'), so removing the wrappers alone cannot meet the card's 'no internal identifiers'. (2) A refused record save is localised by code only for permission refusals; a unique violation on a record save shows the same English inner sentence (Q4). Q1 CHAIN, the card's case (unique refusal in create_record, reached through the resume door because create_account runs after screen_1). Hop 1, objectql: engine.insert throws DuplicateRecordError {code DUPLICATE_RECORD, status 409, object, field when determinable, developerMessage, cause} (duplicate-record-error.ts:82-121). Hop 2, service-automation/src/builtin/crud-nodes.ts:657-688: the catch returns {success:false, error:'create_record(OBJECT) failed: ' + message, code}, keeping code ONLY when it equals DUPLICATE_RECORD (:680-683); status, field, developerMessage, cause and any userMessage die here. Siblings get_record :542, update_record :788, delete_record :855 keep the message only, no code. Hop 3, engine.ts returned-failure arm: the step log records error {code:'NODE_FAILURE', message} (:12107, the executor's code replaced), the in-flow $error gets {nodeId, message, output, code?} (:12145-12150, the only place the code survives), then throw new Error("Node 'NODE_ID' failed: " + errMsg) (:12168), a plain Error. Hop 4, terminal exits: execute() catch :7104 takes the thrown text into the run log error (:7156) and the result {success:false, error, status:'failed', errorMessage: flow.errorMessage, flowLabel, summary} (:7254-7311); resumeInternal :8610 -> :8690 -> result status 'stranded' (:8732-8771). No code and no inner message survive as fields. Hop 5, doors: classifyFlowRefusal takes message = result.error (runtime/src/flow-dispatch-status.ts:203) and the 400 FLOW_FAILED row (:236); trigger door domains/automation.ts:2030-2045 answers deps.error(message, 400, {code, errorMessage?, summary?}); resume door classifyResumeResult :2339-2361 answers message result.error (:2353) with details {code FLOW_FAILED, errorMessage?, summary?, runId, repairable, status?}; /actions prefixes a third time, "Flow 'TARGET' failed: " + result.error (action-execution.ts:1288-1296, served at domains/actions.ts:826-831); declared endpoints endpoint-executor.ts:581-598. On the wire the only structured trace of the failure is details.summary.nodes[] {nodeId, nodeType, status failure}, which already names the failing node. Q1 MEASURED: HotCRM's reading in the card body (18.0.0-next.5) is exactly the string this chain composes at the resume door: crud-nodes.ts, flow-dispatch-status.ts and duplicate-record-error.ts are byte-identical between the 18.0.0-next.5 tag commit 820fd5a and main (diff empty), and the engine throw line and the resume door's message line are present verbatim on both. Validation leg (static): ValidationError (objectql/src/validation/record-validator.ts:241-257; code VALIDATION_FAILED, message = the joined author rule messages, fields[]) loses code and fields[] at hop 2, so the user reads "Node 'N' failed: create_record(OBJ) failed: RULE MESSAGE". Permission leg (static): PermissionDeniedError (plugins/plugin-security/src/errors.ts:41-58; PERMISSION_DENIED, 403, details) loses all three, so the user reads '... failed: [Security] Access denied: ...', operator text the record form deliberately replaces. App-hook leg (named producer): HotCRM src/sales/objects/account.hook.ts refuse() sets userMessage on crm_account beforeInsert refusals (10 HotCRM hook files mark userMessage, crm_account, crm_contact and crm_opportunity among them); create_account writes crm_account, and hop 2 drops the marking, so a producer-marked refusal reaches the flow user unmarked and prefixed. Q2 ENVELOPE: FLOW_FAILED is registered under @objectstack/runtime (spec api/error-code-ledger.zod.ts:421-426). ApiErrorSchema (api/contract.zod.ts:16-97): code closed; declaredCode means demotion of a non-vocabulary code (:30-49); message 'Readable error message' (:50); userMessage is producer-marked end-user text, verbatim, which 'never replaces message - the diagnostic channel keeps its own wording' (:51-97). details is z.unknown() (contract.zod.ts:253, errors.zod.ts:450). The resume door's details are declared as ResumeFailureDetailsSchema (api/automation-api.zod.ts:390-470) beside errorMessage/summary. AutomationResult (contracts/automation-service.ts:387-589): code is a CLOSED union of trigger and resume refusal codes (:489); errorMessage is the flow's friendly failure text (:543-549). Rulings, quoted: #9378 comment 5321043983 (maintainer verbatim 「同意」): 'ran and failed ... 400 FLOW_FAILED' and 'codes are stamped at the route ...; AutomationResult.code stays closed - the engine classifies via existing vocabulary; any genuinely NEW ledger code this surfaces goes through the spec seat, never minted mid-PR. No sniffing - producer classifies, route maps.' #9384 comment 5321043167 (「同意」): 'The union stays closed'. #9446 comment 5322867819 (「同意」): 'the #9378 status table is a property of the flow-dispatch CONTRACT - all three doors converge'. #9585 (「接受你的所有建议。」, quoted in claim 5339708379): the /actions door 'carries exactly two fields into error.details ... errorMessage ... and summary' and 'Scope: bounded to the one door. No new schema keys.' (restated at action-execution.ts:1050-1051: 'A third key here is a contract widening the #9585 ruling does not cover'). #22507 comment 6093472708 (「22507 同意」), restated at engine.ts:11762-11764: 'a user-read flow string the server renders per run is translated where it is rendered, in the run's locale, before its holes are filled, and a client overlay never touches a server-rendered slot.' Unique wire spelling, maintainer ruling 2026-09-03 recorded in commit 65846bc (runtime/src/http-dispatcher.ts:1204-1207): 'one wire spelling on every route, UNIQUE_VIOLATION'. userMessage: maintainer ruling 2026-08-19 on objectui#5210, option 1 (contract.zod.ts:59-61; scope restated in objectui#5210 comment 5344182262: 'render a marked ... message; an unmarked message keeps the generic substitution'). Q2 SLOT ANSWER: no existing slot carries the inner CODE without a contract act. details parses any key, but the FLOW_FAILED details are ruled per door (#9585: exactly two at /actions; ResumeFailureDetailsSchema at resume) and pinned by exact equality (runtime/src/domains/automation-resume-stranded-details.test.ts:99, runtime/src/actions-flow-dispatch-status.test.ts:377); declaredCode means demotion and every inner code here is a vocabulary member; AutomationResult.code is closed; summary.nodes[] declares no code. The inner TEXT does have an existing declared slot, ApiErrorSchema.userMessage, which no flow door emits today. Q3 CONSUMERS. Diagnostic (want node id, operation, object): sys_automation_run.error (service-automation/src/sys-automation-run.object.ts:375-380, 'the message a designer needs to fix it'), written by recordLog at engine.ts:7156 and :8690 and the retry exits (:13645, :13678); Studio's FlowRunsPanel reads run.error and step.error (objectui previews/FlowRunsPanel.tsx:403, :315); the step log (engine.ts:12107); in-flow $error.message and NODE_ID.error (engine.ts:12145-12151, setNodeError :11718-11723), which fault handlers and try_catch catch regions read, documented as 'normally names the step that failed' (spec automation/control-flow.zod.ts:342); the subflow parent text (engine.ts:8059) and failAncestors (:8700); plugin-approvals composes its resume failure and stranded refusal from result.error (plugins/plugin-approvals/src/approval-service.ts:4213, :4464); SDK, MCP and integration callers read error.message (MCP resume_run domains/mcp.ts:1003; run_action through dispatchFlowAction). The run-summary log line carries counts, not the error text (engine.ts:10452-10470). End-user facing: only the objectui FlowRunner dialog Alert and toast, and the two flow-launch handlers, all through interpretFlowResponse. Pins on the PRODUCED wording against a real engine or stack: packages/qa/dogfood/test/flow-runas.dogfood.test.ts:137 (wire message contains "Node 'ID' failed"), service-automation try-catch-returned-failure-steps.test.ts:314 and :346, loop-dying-body-steps.test.ts:334, builtin/create-record-duplicate-code.test.ts:96. Door suites that pass a fixture string through a fake service: automation-trigger-route-status.test.ts:103 and :142, actions-flow-dispatch-status.test.ts:199, :364, :481, endpoint-flow-dispatch-status.test.ts:130, http-dispatcher.test.ts:601, client.test.ts:1542 and :1614, envelope-convergence.test.ts:195, rest-approvals-wire-codes.test.ts:381. Q4 CONSOLE (objectui origin/main 878a4af; FlowRunner.tsx, flowResponse.ts, core actionErrorDetail.ts, react error-message.ts and data-objectstack are absent from the 145-file compare to remote main 8cd4db4, so byte-identical there; form.tsx is in it and was read at 8cd4db4). Runner: interpretFlowResponse's 400 FLOW_FAILED arm (app-shell/src/utils/flowResponse.ts:345-355) shows details.errorMessage, translated through flows.FLOW.errorMessage (:212-226, :296-300), else actionErrorDetail(json), i.e. error.message verbatim (core/src/actions/actionErrorDetail.ts:27-35). It reads neither userMessage nor any inner code. FlowRunner shows that string in toast.error and a destructive Alert (views/FlowRunner.tsx:460-466, :627-633), under a comment calling error.message 'already user-grade prose ... passed through verbatim and untranslated' (:629-632). Record save (components/src/renderers/form/form.tsx:3023-3026 at 8cd4db4): declaredUserMessage(error), else the permission substitution t('form.noPermissionToSave') when isPermissionError, else extractWriteErrorMessage(error) (strips one leading TAG or CODE prefix), else t('form.submitFailed') (react/src/utils/error-message.ts:125-137, :202-215, :52-90). So the only code-keyed localisation of a refused save is PERMISSION_DENIED/FORBIDDEN/403; a unique violation shows the server's English sentence naming 'crm_account', since the data door keeps message as thrown (runtime/src/http-dispatcher.ts:1228-1233). git grep at 878a4af: zero non-test hits for UNIQUE_VIOLATION or DUPLICATE_RECORD in packages//src and apps//src. Would a code on the envelope be enough? No: it needs a server carrier (none exists, Q2), a new reader in the FLOW_FAILED arm, new copy per code in 10 locale files (copy that record saves do not have either), and a wire-spelling choice. Handing the runner the inner refusal and reusing the record-save rule needs no new copy and gives record-save parity. Q5 PR #24778 (draft, head 56cac78): its crud-nodes.ts hunk is at :153-170 (DROPPED_REASON_LABEL gains stored_expression; droppedFieldsWarning branches on it), the success-path warning. The four catch arms (:542, :657-688, :788, :855) are untouched, so no textual or semantic collision with any route here; a route editing the catch arms is a disjoint hunk in the same file and only the file-level serial constraint the claim records applies. Its spec files (contracts/data-engine.ts, data/data-engine.zod.ts) and objectql files (engine.ts, readonly-strict-errors.ts) are disjoint from every route's files. CORPUS for the business axis: HotCRM main 3116e7ac3a has 6 screen flows with write nodes (lead_conversion, quote_generation, schedule_followup, escalate_case, close_case, claim_case; 13 write nodes), 0 declaring errorMessage, 0 try_catch nodes or fault edges. objectstack examples at 7169612: app-crm crm_convert_lead_wizard declares errorMessage with a zh-CN translation (examples/app-crm/src/translations/crm.translation.ts:219); app-showcase showcase_reassign_wizard (1 update_record) and app-todo quick_add_task (1 create_record) declare none. So the unauthored default is what users see on 8 of the 9 write-bearing screen flows measured. ROUTE MAP and recommendation in open_questions[0]: recommend B-prime (carry the inner refusal across the flow door, message unchanged, the console renders it with its record-save rule), with HotCRM's app half (F) in parallel and the inner sentence's API name as an engine-lane sub-issue. Lanes: domain:spec, domain:services, domain:cli, domain:ui (objectui), domain:engine for the inner sentence, repo:hotcrm for the app half.",
"tests": "NO test suite, build or probe script was run (os-dev.md readings clause; conflict named in summary). Real-engine probe for the unique, validation and permission legs: NOT MEASURED, reason: readings-only dispatch, no closure build. Readings taken instead, each a count or diff against a named tree: (1) Byte identity of the chain between the 18.0.0-next.5 tag commit 820fd5a (refs/tags/@objectstack/cli@18.0.0-next.5) and main 7169612, files fetched by REST contents at that ref: diff of crud-nodes.ts, flow-dispatch-status.ts and duplicate-record-error.ts printed nothing; the throw line "throw new Error(Node '${node.id}' failed: ${errMsg})" is at next.5 engine.ts:12008 and main :12168; "message: result.error ?? 'Flow run failed'" is at next.5 domains/automation.ts:2347 and main :2353. This carries the card's HotCRM public-door measurement onto main's chain. (2) git diff --stat 74fcec1..7169612: 3 files (content/docs/releases/v18/index.mdx, content/docs/upgrading.mdx, packages/cli/CHANGELOG.md). git diff --stat d606fc1..7169612 limited to service-automation, runtime, objectql, qa, spec, types: empty (the 6-file delta is docs, CHANGELOG and driver-sql introspection). (3) CI check-runs read by REST: d606fc1, Test Core (1/6) through (6/6) success in one run and Dogfood Regression Gate (1/3) through (3/3) success; 74fcec1, Dogfood Regression Gate (1/3) through (3/3) success. So the real-stack pin flow-runas.dogfood.test.ts:137 (a refusal inside a flow write node answers 400 FLOW_FAILED whose message contains "Node 'ID' failed") and the real-engine pins try-catch-returned-failure-steps.test.ts:314 and create-record-duplicate-code.test.ts:96 hold on code identical to main. Main head 7169612's own full battery was in_progress at read time: NOT MEASURED on that sha. (4) Corpus counts: HotCRM at 3116e7ac3a (blobless clone into the scratchpad, files read with git show, nothing executed) and objectstack examples at 7169612, as reported in summary. (5) objectui: REST compare 878a4af...8cd4db4 = ahead 20, 145 files; the console chain files are absent except form.tsx, whose catch branch is unchanged (remote :3023-3026). git grep at 878a4af for UNIQUE_VIOLATION or DUPLICATE_RECORD in non-test packages//src and apps//src: 0 hits. (6) PR #24778 diff by REST: the crud-nodes.ts hunk header is '@@ -153,10 +153,21 @@'. Ablation and reverse verification: not applicable, nothing changed.",
"gates": "No diff, so no gate family is derived and none was run; there is no PR, so there are no PR check-runs to cite. CI readings on main, cited not re-run: d606fc1 Test Core 1/6-6/6 success, Dogfood Regression Gate 1/3-3/3 success, Build Core success; 74fcec1 Dogfood Regression Gate 1/3-3/3 success, TypeScript Type Check success; 7169612 Test Core 1/6, 5/6, 6/6 and Dogfood Regression Gate in_progress at read time. Worktree ../objectstack-issue-24914 was created off origin/main 7169612, never edited, and removed at the end after git status --porcelain and git diff HEAD both printed nothing; the local branch ref, which pointed at 7169612 with no commit of its own, was deleted with it.",
"mcp_calls": "0 - no MCP GitHub tool was called",
"api_writes": "1 - POST /repos//issues/24914/comments (this os-dev-report, through scripts/pm/post-stamped.mjs and the fleet relay). Every other GitHub call was a REST read.",
"open_questions": [
{
"question": "Which route fixes the engine refusal text a screen-flow user sees? Routes are mapped with files, lanes, protocol class and cost; every reading is on objectstack 7169612 and objectui 878a4af/8cd4db4.",
"options": [
"A - server only, rewrite error.message. The envelope message becomes the inner sentence; node id, operation and object move to details or the run record. Needs a new AutomationResult member for the inner sentence (spec contracts/automation-service.ts plus TriggerFlowResponseSchema parity), engine carriage (returned-failure arm :12097-12168, execute :7104-7311, resumeInternal :8610-8771, retry exits :13613-13681), crud-nodes four catch arms, and four doors plus the MCP bridges (flow-dispatch-status.ts:203, automation.ts:2030-2045 and :2339-2361, action-execution.ts:1288-1296, endpoint-executor.ts:581-598). Protocol class: yes, a REST door's answer changes and a spec member is added. Cost: three lanes (spec, services, cli); pin churn in flow-runas.dogfood.test.ts:137, four runtime door suites and two client suites; the /actions wording has been on the wire since #3962 and its comment calls re-labelling it unruled. Four axes: business - removes both prefixes for the card's case, but the user still reads 'Duplicate record refused on crm_account', English, and for a permission refusal reads raw '[Security] Access denied', which the record form substitutes; long-term - contradicts ApiErrorSchema's split, where message is the diagnostic channel and userMessage the user channel, takes the operation and object off the wire for SDK and MCP callers, and makes message a server-rendered user-read string that #22507 ruling A says must then be translated server-side, which no catalog does; AI - neutral; startup - three lanes for a partial fix.",
"B - A plus the inner code on the envelope. AutomationResult.code is closed by the #9378 and #9384 rulings, so a new member is needed; an envelope details key widens the #9585 two-field bound and the exact-equality pins; the console needs a reader and new copy per code in 10 locales; the code needs a wire spelling (UNIQUE_VIOLATION per the 2026-09-03 ruling, not the in-process DUPLICATE_RECORD). Protocol class: yes. Cost: everything in A plus objectui and a ruling. Four axes: business - zero measured consumers of a flow-envelope code today (the console localises no UNIQUE_VIOLATION even on record saves); long-term - inherits A's narrowing of message; AI - code-branching beats regex; startup - largest route, with new copy that has no pull.",
"C - console only, map refusal codes in the flow runner. Not possible today: no code reaches the console (error.code is FLOW_FAILED, details carry errorMessage, summary and the resume verdict, summary.nodes[] has no code); reading one out of the message is a regex over prose, which PD #12 and objectui's one-strict-contract rule forbid. Protocol class: none. Four axes: fails business (nothing to map) and long-term (sniffing).",
"D - carry the producer-marked userMessage across the flow door (other route; the minimal subset of B-prime). crud-nodes' four catch arms read declaredUserMessage(err) (@objectstack/types, already a dependency of service-automation); NodeExecutionResult (service-automation engine.ts:358) and AutomationResult (spec) gain an optional userMessage with ApiErrorSchema's semantics; the four doors emit it as the envelope's existing top-level userMessage, which needs deps.error to take it (runtime domain-handler-registry.ts:313 has no slot); objectui's FLOW_FAILED arm reads it after errorMessage and before message. message is unchanged, so no wording pin moves. Protocol class: yes, additive (Clause-2 yes, widening). Lanes: spec, services, cli, ui. Four axes: business - measured producers exist (10 HotCRM hook files mark userMessage, including crm_account, which the card's node writes), but the card's measured unique refusal is unmarked and is NOT changed; long-term - completes a declared channel at the one boundary that drops it, which is the frame's 'declared but not delivered = implement' rule; AI - removes a silent drop of a marking AI-written hooks already make; startup - smallest server route with measured producers.",
"B-prime - carry the inner refusal structurally, message unchanged (other route; contains D). D, plus the inner refusal as {code in its wire spelling, message} in ONE FLOW_FAILED details member declared once in spec (the ResumeFailureDetailsSchema pattern) and emitted by all four doors; objectui's FLOW_FAILED arm renders it with the record-save rule it already has (form.tsx:3023-3026: userMessage, then the permission substitution, then the inner message with its prefix stripped), after the author's errorMessage. Needs a maintainer ruling, since it widens the #9585 two-field details at /actions and the flow-dispatch details at the other doors. Protocol class: yes, additive (Clause-2 yes, widening). Lanes: domain:spec (the AutomationResult member and the details declaration), domain:services (crud-nodes four catch arms, engine carriage on both terminal exits and the retry exits; optionally stamp the executor's code into the step log instead of NODE_FAILURE), domain:cli (trigger, resume, /actions and endpoint doors, MCP bridges, deps.error), domain:ui (flowResponse.ts and the FlowRunner comment). Four axes: business - the only server route that brings all three classes the card names to record-save parity (unique: the unprefixed inner sentence; validation: the author's rule text; permission: the localised substitute; hook: the marked text), on every unauthored screen flow (8 of the 9 write-bearing screen flows measured); locale for the unique case still needs F or the engine sub-issue, exactly as on a record save; long-term - fixes the root, which is flattening the structured refusal into a string at hop 2, once for all doors, keeps message diagnostic for logs, SDK, approvals and the stored run, and moves zero wording pins; AI - one declared carrier with no consumer-side regex, and AI-written flows get record-save behaviour without authoring anything; startup - the largest of the recommended set (4 lanes and a ruling), but it adds no error code, no gate and no copy.",
"E - console only, generic substitution (other route). When no errorMessage and no userMessage reach the runner, show a localised generic sentence and keep the raw text for the developer console, as the record form does for permission refusals. Protocol class: none. Lane: domain:ui. Four axes: business - no identifiers and localised, but it hides the author's validation rule text and hook text, so the user can no longer act on the reason; long-term - matches the 'unmarked is diagnostic' clause of ApiErrorSchema but overrides producers (ValidationError, DuplicateRecordError) that write message for display; AI - safe default; startup - smallest.",
"F - app half in HotCRM, no platform change (other route, independent of the choice above). Declare errorMessage, with a zh-CN translation at flows.lead_conversion.errorMessage, on its 6 write-bearing screen flows, and/or route $error.code == 'DUPLICATE_RECORD' on create_account (available on create_record today) to a refusing end node whose message is translated at its flows refusals key and picked by the engine in the run's locale. Fixes the measured case in the rep's locale with shipped features. Carrier: the repo:hotcrm seat."
],
"recommendation": "B-prime, with F in parallel and the inner sentence as an engine-lane sub-issue (out_of_scope_findings[0]). Business: B-prime is the only server route that removes the node id and operation from what the user sees for all three named refusal classes without narrowing message, and the default it fixes is the one 8 of the 9 measured write-bearing screen flows rely on; F is the only route that gets the measured case into zh-CN now. Long-term: it repairs the flattening at its source and honours ApiErrorSchema's message/userMessage split rather than rewriting the diagnostic channel (A, B). AI: one declared carrier, no regex (C), no override of producer text (E). Startup: no new code, no gate, no copy; the cost is four lanes and one ruling, which the measured producers and the 8-of-9 default justify. The four axes conflict on cost: if the maintainer wants the narrowest act, D alone completes the declared userMessage channel and is a strict subset of B-prime, but it leaves the card's measured unique refusal unchanged, so it does not close this card. No new gate is recommended (a lint for screen flows without errorMessage would be a new gate, which defaults to no and no maintainer has named one)."
},
{
"question": "Under B-prime, which spelling does the carried inner code take for a unique refusal?",
"options": [
"A - UNIQUE_VIOLATION, the wire spelling the 2026-09-03 ruling fixes for every route (runtime/src/http-dispatcher.ts:1204-1207); the flow door translates the engine's class the way the data doors do.",
"B - DUPLICATE_RECORD, the in-process identity the crud node already carries to $error.code (crud-nodes.ts:680-683)."
],
"recommendation": "A. Business: the console's record-save rule and any client already branch on the data door's wire codes, so parity means the same spelling. Long-term: the ruling says one wire spelling on every route, and B would add a second. AI: one spelling per boundary is what DuplicateRecordError's developerMessage teaches. Startup: no extra vocabulary. In-flow $error.code stays DUPLICATE_RECORD, which the ruling leaves in-process."
}
],
"out_of_scope_findings": [
"class: a · reach: measured public door - HotCRM's FlowRunner text on 18.0.0-next.5 (card body) carries the inner sentence 'Duplicate record refused on crm_account: ...', byte-identical producer on main; named producer buildDuplicateMessage, packages/objectql/src/duplicate-record-error.ts:131-138, whose message http-dispatcher.ts:1228-1233 keeps as thrown on the data door, so a record save shows it too (that leg NOT MEASURED, read statically through form.tsx:3023-3026 at objectui 8cd4db4) · evidence: the user-facing sentence of a unique refusal names the object API name, which the card's own bar ('no internal identifiers') rules out on both the flow and record-save surfaces; every flow route in open_questions[0] still shows it until the producer names the object by label or not at all · file as a sub-issue of #24914 in domain:engine (the inner-producer half of the route) · dedupe words: DuplicateRecordError message object name; unique violation API name user; Duplicate record refused on; buildDuplicateMessage label",
"carrier: none (承接者:无) · noted, not filed: the engine's step log records every returned node failure as code NODE_FAILURE (service-automation/src/engine.ts:12107) even when the executor classified one (create_record's DUPLICATE_RECORD reaches $error.code at :12149 but not the stored step), so the stored run cannot show a classified code; an observation with no reader that misbehaves today, folded into B-prime's optional services item",
"carrier: repo:hotcrm seat · noted, not filed: route F (errorMessage with zh-CN on the six write-bearing screen flows lead_conversion, quote_generation, schedule_followup, escalate_case, close_case and claim_case; or $error.code routing on create_account to a translated refusing end) is app work with shipped features, recorded so the HotCRM seat can take it"
]
}
Claim: PM loop round 1 · 2026-10-21T22:26Z · p2, domain:services, step (2) of ruling B′ (6303788292)
Session: session_01UnHcTADjeg3VwxF21QHj7W (the dispatching seat; the dev is a mode:subagent run of this session, told apart by its branch)
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24914-flow-inner-refusal
Worktree: objectstack-issue-24914
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface (line anchors at origin/mainabc25a353):
packages/services/service-automation/src/builtin/crud-nodes.ts: the four catch arms (get_record about :553, create_record about :680–:700, update_record about :799, delete_record about :866). They read declaredUserMessage(err) and the inner code and return them structurally. Their error text is unchanged.
packages/services/service-automation/src/engine.ts: the returned-failure arm (about :12100–:12170) and the terminal exits (the execute() catch, resumeInternal and the retry exits). They carry userMessage / innerRefusal onto AutomationResult. error / message are unchanged.
packages/runtime/src/** (domain:cli, declared cross-lane on [PM seat] domain:cli · seat 2 — ⏳ vacant #22648 in this act): flow-dispatch-status.ts, domains/automation.ts (the trigger and resume doors), action-execution.ts (the /actionsFLOW_FAILED details, about :1290–:1320), endpoint-executor.ts, domains/mcp.ts (the flow bridges) and the deps.erroruserMessage slot. These are the regions the ruling names, and nothing else in those files.
os-dev-report
{
"issue": 24914,
"status": "done",
"branch": "claude/issue-24914-flow-inner-refusal",
"pr": "#25147",
"session": "session_01UnHcTADjeg3VwxF21QHj7W - the dispatching seat's session (claim 6327931189); this run is its mode:subagent dev, told apart by the branch",
"premise_still_valid": true,
"summary": "Step (2) of ruling B' (6303788292), emitting exactly what PR #25037 (#24996) declared; packages/spec untouched. Identity: newest Claim 6327931189 names claude/issue-24914-flow-inner-refusal. service-automation: new src/inner-refusal.ts refusalCarriedBy(err) -> { userMessage?, innerRefusal? }, used by the four crud-nodes.ts catch arms (get/create/update/delete); userMessage = declaredUserMessage(err); innerRefusal only when resolveThrownHttpError(err).declaredStatus is 4xx, code = the data door's errorFromThrown code for the same throw (resolver code, except the engine duplicate envelope -> UNIQUE_VIOLATION read off structuredCodeAnswer), message = the throw's own text; in-flow $error.code stays DUPLICATE_RECORD. NodeExecutionResult gains the two optional members; the engine brands its unchanged Node 'ID' failed throw (symbol brand, partial-steps idiom) and spreads the members at execute() failure, resumeInternal stranded, executeWithoutRetry failure and retryExecution exhausted (last attempt's members). runtime: flow-dispatch-status.ts flowFailureDetails(result) typed as FlowFailureDetails and flowFailureEnvelope(result); trigger + resume doors (domains/automation.ts), /actions (action-execution.ts FlowActionRunDetails = FlowFailureDetails, FlowActionRefusal.userMessage field; handler domains/actions.ts), endpoint (endpoint-executor.ts) emit details.innerRefusal and top-level userMessage; DomainHandlerDeps.error gains optional extra { userMessage? } wired in http-dispatcher.ts; MCP resume_run resumeDoorError passes the slot and copies userMessage onto the thrown error. Every error/message text byte-identical; AutomationResult.code closed; no new code minted. Premise checks on abc25a3: (1) git grep innerRefusal/userMessage over the flow doors + service-automation src: only the action-execution.ts:1051 comment (controls: http-dispatcher.ts userMessage x6, spec automation-service.ts innerRefusal x1) - held. (2) catch arms at crud-nodes.ts :552-553, :662-699, :798-799, :865-866, engine throw :12168 - held. (3) ValidationError -> VALIDATION_FAILED, PermissionDeniedError -> PERMISSION_DENIED, unique -> UNIQUE_VIOLATION, an app hook's unregistered code -> the member its declared status names (closed ErrorCode per the spec docblock) - nothing minted. (4) marked userMessage verbatim, unmarked absent - pinned. (5) retry and resumeInternal carry; subflow parents and plugin-approvals read result.error, whose text is unchanged; neither touched. Clause-2 re-declared yes (widening) per the dispatch's measure clause: the built dist/index.d.ts of both packages widens two published types (runtime DomainHandlerDeps.error optional 4th arg; service-automation NodeExecutionResult two optional members); no export added; changeset patch x2 with Metadata-protocol: none. PR #25147 is draft, assignee os-bill, body read back byte-identical; objectui#13285 named as staying open with no closing keyword near it.",
"tests": "Final head 0a2d128 (git rev-parse --short HEAD), under scripts/pm/os-verify-lock.sh, VERDICT command-exit 0: service-automation src/flow-inner-refusal.test.ts + create-record-duplicate-code.test.ts + builtin/loop-dying-body-steps.test.ts = 3 files / 29 passed; builtin/try-catch-returned-failure-steps.test.ts 17/17; runtime flow-failed-inner-refusal + mcp-resume-run + actions-flow-dispatch-status + endpoint-flow-dispatch-status + domains/automation-resume-stranded-details + domains/automation-trigger-route-status = 6 files / 108 passed; verify src/automation-flow-inner-refusal.test.ts 1 file / 9 passed. Full suites at bbca8a6: pnpm --filter @objectstack/service-automation exec vitest run = Test Files 198 passed, Tests 2618 passed (SA_EXIT=0); pnpm --filter @objectstack/runtime exec vitest run --project local = Test Files 433 passed, Tests 6805 passed | 19 skipped (RT_EXIT=0). Delta bbca8a6..0a2d128: the changeset, flow-inner-refusal.test.ts (re-run green above), scripts/engine-double-contract.pinned.json, and the main merge 4c7dcb4 (driver-sql tests only). typecheck: pnpm --filter service-automation/runtime/verify typecheck exit 0 (tsc --noEmit + check:test-typecheck OK for each). Ablations (ablation-replace.mjs WRAP, each restore proven by HEAD blob hash, git diff HEAD empty after every leg; all source-resolved except V1): SA1 isRefusal forced false 10 failed | 3 passed (13); SA2 userMessage read dropped 2 failed; SA3 engine brand dropped 10 failed; SA4 execute() exit spread 8 failed; SA5 stranded exit 1 failed; SA6 retry exhausted carry 1 failed; SA7 per-attempt exit 1 failed; RT1 details innerRefusal 10 failed | 22 passed (32); RT2 envelope userMessage 6 failed; RT3 deps.error slot 5 failed; RT4 MCP copy-back 1 failed; RT5 /actions handler slot 2 failed; V1 (dist) SA1's mutation + pnpm --filter service-automation build + ablation-dist-preflight 'marker present in 2 built files' -> verify 8 failed | 1 passed (9); restore leg rebuild + preflight --absent 'marker absent from all 6 built files', 'working tree clean against HEAD' -> 9 passed. First passes of SA4/SA5/SA7/RT3/RT5 were no-ops refused by the tool (replacement already present), readings discarded, re-run with unique markers; RT5's first re-run got exit 99 (lock) and was run again. Reverse type verification not run separately: the FlowActionRunDetails = FlowFailureDetails binding and flowFailureDetails' return type are exercised by tsc in runtime typecheck exit 0.",
"gates": "Local subset: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 0a2d128 (first stderr line names the repo and 0a2d128; merge base 4c7dcb4; 17 paths) derived 80 commands; all 80 run after the final commit, each exit captured before any pipe (cmd > log 2>&1; ec=$?): 80 x exit 0. --ran reconciliation with ':: exit N' records: '80 derived famil(ies) accounted for - 80 run, 0 NOT-MEASURED (a DERIVED zero - all 80 recorded an exit code and none of them is 3)'. On the earlier head f420532: check-plugin-teardown-shape --self-test exit 3 (shallow clone, pinned fixture 621a487 absent) -> git fetch --depth=1 of that commit -> exit 0; check:engine-double-contract exit 1 (new double not routed through the dispatch predicates, then not pinned) -> repaired in the test + --write to scripts/engine-double-contract.pinned.json -> exit 0; check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET earlier, exit 0 in the final union ('112 published require entry point(s) across 69 package(s) load'). check-platform-checklist not run: no exported symbol renamed or deleted (FlowActionRunDetails keeps its name, is not a package export). Local-only families check:dev-prereqs / check:skill-frame-freshness not triggered (no packages/spec build, no frame files). CI (PR #25147, head 0a2d128) read at report time: 24 check-runs, 21 in_progress, 1 success, 2 skipped; Governed Surface Queue Guard in_progress; Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core not yet concluded - in_progress is the honest value; not waited on, not re-run locally.",
"dogfood_battery": "n/a - no ! in the PR title and Clause-② carries the (widening) arm, not (narrowing)",
"mcp_calls": "0 - no MCP GitHub tool was called",
"api_writes": "2 relay strokes (each one repository_dispatch to the board, executed as objectstack-fleet[bot]): pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/25147/assignees; comment = POST /repos//issues/24914/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). git push (branch) is not a REST write. All other GitHub calls were REST reads.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed: containers that re-word a child failure into their own returned failure (try_catch with no catch region; subflow, synchronous leg and resume delegation arm) do not forward userMessage/innerRefusal, so a refused write nested there reaches the door with neither member; allowed by the spec's 'present only when a refusal was met'; recorded in PR #25147 Acceptance notes",
"carrier: none (承接者:无) · noted, not filed: crud-nodes' own stored-metadata family refusals (returned before any data call, with a code) carry no innerRefusal; the ruling names the four catch arms",
"carrier: none (承接者:无) · noted, not filed: packages/mcp's tool-error renderer (errorResultFromThrown / errorResultWithCode) renders code, message, status and details but not userMessage, on run_action and resume_run alike; run_action still carries no run details (#9585 scope)",
"carrier: none (承接者:无) · noted, not filed: the step log still records NODE_FAILURE for a returned node failure (engine.ts returned-failure arm), unchanged by this card",
"carrier: #24951 · noted, not filed: the unique-refusal sentence itself names the object API name (objectql duplicate-record-error.ts); unchanged here"
],
"files_changed": [
".changeset/24914-flow-inner-refusal.md",
"packages/runtime/src/action-execution.ts",
"packages/runtime/src/domain-handler-registry.ts",
"packages/runtime/src/domains/actions.ts",
"packages/runtime/src/domains/automation.ts",
"packages/runtime/src/domains/mcp.ts",
"packages/runtime/src/endpoint-executor.ts",
"packages/runtime/src/flow-dispatch-status.ts",
"packages/runtime/src/flow-failed-inner-refusal.test.ts",
"packages/runtime/src/http-dispatcher.ts",
"packages/runtime/src/mcp-resume-run.test.ts",
"packages/services/service-automation/src/builtin/crud-nodes.ts",
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/flow-inner-refusal.test.ts",
"packages/services/service-automation/src/inner-refusal.ts",
"packages/verify/src/automation-flow-inner-refusal.test.ts",
"scripts/engine-double-contract.pinned.json"
],
"line_budget": "n/a - no skills/** or governed surface touched",
"deviations": [
"Clause-② re-declared from the claim's 'no' to 'yes (widening)' per the dispatch's measure clause: built dist/index.d.ts widens runtime DomainHandlerDeps.error (optional extra) and service-automation NodeExecutionResult (two optional members); no export added",
"file surface beyond the claim: http-dispatcher.ts + domain-handler-registry.ts (the deps.error slot), domains/actions.ts (the /actions handler, one argument), scripts/engine-double-contract.pinned.json (gate --write for the new test's pinned double), packages/verify/src/automation-flow-inner-refusal.test.ts (the one package depending on engine and doors); declared in the PR body",
"five ablation legs' first passes were tool-refused no-ops (replacement text already present) and RT5's first re-run missed the lock (exit 99); all re-run, readings from the re-runs",
"full service-automation / runtime suites measured at bbca8a6, not the final head; delta named in tests and re-run where it touched a test",
"verify: only the new file + typecheck run locally (its full suite is CI's); test:repo and pnpm lint not run locally (CI's)",
"git fetch --depth=1 of commit 621a487 into the shared shallow object store, for check-plugin-teardown-shape --self-test's pinned fixture",
"a stray 6-byte file /build-a.pid (a PID number) was written at the filesystem root by a background launch whose shell variable was scoped to the subshell; its removal was refused by the harness safety check, so it is left for the user to delete",
"commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) rather than the harness reminder's model-named line, which yields to the repo instructions",
"worktree ../objectstack-issue-24914 removed after the report (node_modules first, then git worktree remove without --force)"
]
}
Claim: PM loop round 1 · 2026-10-22T00:30Z · amends claim 6327931189 to the measured declaration and file surface of PR #25147 (dev report 6329651337)
Session: session_01UnHcTADjeg3VwxF21QHj7W (the dispatching seat; the dev is a mode:subagent run of this session, told apart by its branch)
Account: os-bill (the seat's linked user as GET /user answers it; always the card's assignee)
Branch: claude/issue-24914-flow-inner-refusal
Worktree: objectstack-issue-24914
Domain: domain:services
Seat: domain:services#2 (seat post #21118)
File surface: everything in claim 6327931189, plus the files the ruled change needs, as declared in PR #25147's body:
packages/runtime/src/domain-handler-registry.ts and http-dispatcher.ts: the deps.erroruserMessage slot the ruling names.
packages/runtime/src/domains/actions.ts: the /actions handler passes that slot.
packages/verify/src/automation-flow-inner-refusal.test.ts: the one package that depends on both the engine and the doors.
scripts/engine-double-contract.pinned.json: the gate's own --write for the new test double.
The runtime and verify files are domain:cli's; the addendum goes on [PM seat] domain:cli · seat 2 — ⏳ vacant #22648 in this act.
Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; unchanged)
Clause-②: yes (widening) — re-declared from the claim's no, as the dispatch asked, by the dev's measurement on the built declaration closure. runtime's published DomainHandlerDeps.error gains an optional fourth argument, and service-automation's published NodeExecutionResult gains two optional members. No export is added. The seat's review at TIER_DEFAULT is owed before the queue.
Responsibility: unchanged from claim 6327931189
Thread-read: 6329651337
Serial constraints cleared: unchanged. PR fix(runtime)!: the /mcp bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25136 (domain:cli) shares action-execution.ts and domains/mcp.ts in other regions; whichever lands second merges main.
merged 2026-10-22T01:18Z through the merge queue, merge commit 9cfc575337
origin/main
head 9cfc57533, subject "fix(service-automation,runtime): a refused write inside a flow reaches every FLOW_FAILED door …"
This card
closed completed by Fixes; no other card closed in that window
State: pm:dispatched is removed in this act. Ruling B′'s step (3), objectstack-ai/objectui#13285 (pm:blocked), now waits on a release that carries this change. That is a cross-repo unlock, for the console lane's unlock scan. The step's open readings are carried in PR #25147's Acceptance notes, and #24951 carries the unique sentence.
Activity
objectstack-fleet commentedon Oct 20, 2026
Claim: PM loop round 14 · 2026-10-20T16:58Z
Session:
session_011TdZ8GjixNZFLz1xJD9zzKAccount:
os-justin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24914-flow-node-refusal-readings(a readings-only dispatch: no edit, no PR; the readings are posted to this card)Worktree:
objectstack-issue-24914(read-only, offorigin/main, for running probes; nothing committed)Domain:
domain:servicesSeat:
domain:services#1File surface: none written. Read:
packages/services/service-automation/src/builtin/crud-nodes.ts(thecreate_record/update_record/delete_record/get_recordwrappers, ~:542–855),service-automation/src/engine.ts(Node '<id>' failed:at ~:12168and what the run result keeps of the inner error),packages/runtime/src/domains/automation.tsandflow-dispatch-status.ts(the400 FLOW_FAILEDenvelope and itsdetails, under the maintainer rulings on #9378 and #9446), the spec'sautomation-api.zod.tsanderror-code-ledger.zod.tsfor what the envelope's shape promises, and the console's flow runner in/home/user/objectuifor how it renders the envelope (stop on breach; explain in the report)Container & model:
S,mode:subagent,model: default (opus), quoting this dispatch's --tier output "no path-derived mandate … floor sonnet · default opus · ceiling fable"; a readings-only dispatch whose output is a measured route mapClause-②: no
Responsibility:
service-automation's node wrappers and engine prefix the engine's own refusal with the node id, the operation and the object API name, and the runtime surfaces that text verbatim as FLOW_FAILED's message | a refused record save already reaches the console as a code the console localises (the card's expected behaviour) | every end user of a screen flow whose write node the engine refuses; measured in HotCRM's lead conversionThread-read: none
Serial constraints cleared:
Nothing is written. Of the 14 open PRs (all files read at this claim), PR #24778 (the spec seat's #24690, open draft) touches service-automation/src/builtin/crud-nodes.ts and crud-dropped-fields.test.ts. The readings name it. A later code dispatch on crud-nodes.ts serialises behind it. No open PR touches the runtime's automation and flow-dispatch files, and neither services seat has an in-flight claim naming them.Why readings first. The fix spans:
service-automation(this lane);FLOW_FAILEDenvelope inpackages/runtime(domain:cli), a flow-dispatch contract the maintainer ruled on automation: bothtriggerroutes still answer HTTP 200 wrapping an inner {success:false} — the same #3962 residue #8684 closed on resume #9378 and The other two flow-dispatch doors never got the #9378 status table —/actionsmislabels never-dispatched refusals, declared endpoints still answer 200 #9446;domain:spec);Whether carrying the inner refusal's code is a protocol-class change is a reading, not a guess. The seat rules the route from these readings. A dispatch for code follows only then.
Readings taken on
origin/main71696121a6.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
os-dev-report
{
"issue": 24914,
"status": "reading",
"branch": "claude/issue-24914-flow-node-refusal-readings",
"pr": null,
"session": "session_011TdZ8GjixNZFLz1xJD9zzK - the dispatching Claim 6296417944 names it; this run is its subagent",
"premise_still_valid": true,
"summary": "READINGS ONLY on objectstack origin/main 7169612 (the seat's read; unchanged at this run). No source edit, commit, push or PR. Identity: the newest and only Claim on #24914, 6296417944, names branch claude/issue-24914-flow-node-refusal-readings. CONFLICT NAMED, not silently resolved: the dispatch asks for a probe on a real engine under the verify lock and status done; .claude/agents/os-dev.md says a readings-only dispatch builds no closure, runs no tests and reports status reading, and that file wins over a dispatch. So nothing was built or run. The measured legs below are the card's own public-door measurement carried to main by byte identity, plus CI conclusions on code-identical main commits; the validation and permission legs are static readings. PREMISE: valid for the defect, with two corrections. (1) The inner refusal is not clean: DuplicateRecordError's own sentence names the object API name (objectql/src/duplicate-record-error.ts:131-138, 'Duplicate record refused on OBJECT: ...'), so removing the wrappers alone cannot meet the card's 'no internal identifiers'. (2) A refused record save is localised by code only for permission refusals; a unique violation on a record save shows the same English inner sentence (Q4). Q1 CHAIN, the card's case (unique refusal in create_record, reached through the resume door because create_account runs after screen_1). Hop 1, objectql: engine.insert throws DuplicateRecordError {code DUPLICATE_RECORD, status 409, object, field when determinable, developerMessage, cause} (duplicate-record-error.ts:82-121). Hop 2, service-automation/src/builtin/crud-nodes.ts:657-688: the catch returns {success:false, error:'create_record(OBJECT) failed: ' + message, code}, keeping code ONLY when it equals DUPLICATE_RECORD (:680-683); status, field, developerMessage, cause and any userMessage die here. Siblings get_record :542, update_record :788, delete_record :855 keep the message only, no code. Hop 3, engine.ts returned-failure arm: the step log records error {code:'NODE_FAILURE', message} (:12107, the executor's code replaced), the in-flow $error gets {nodeId, message, output, code?} (:12145-12150, the only place the code survives), then throw new Error("Node 'NODE_ID' failed: " + errMsg) (:12168), a plain Error. Hop 4, terminal exits: execute() catch :7104 takes the thrown text into the run log error (:7156) and the result {success:false, error, status:'failed', errorMessage: flow.errorMessage, flowLabel, summary} (:7254-7311); resumeInternal :8610 -> :8690 -> result status 'stranded' (:8732-8771). No code and no inner message survive as fields. Hop 5, doors: classifyFlowRefusal takes message = result.error (runtime/src/flow-dispatch-status.ts:203) and the 400 FLOW_FAILED row (:236); trigger door domains/automation.ts:2030-2045 answers deps.error(message, 400, {code, errorMessage?, summary?}); resume door classifyResumeResult :2339-2361 answers message result.error (:2353) with details {code FLOW_FAILED, errorMessage?, summary?, runId, repairable, status?}; /actions prefixes a third time, "Flow 'TARGET' failed: " + result.error (action-execution.ts:1288-1296, served at domains/actions.ts:826-831); declared endpoints endpoint-executor.ts:581-598. On the wire the only structured trace of the failure is details.summary.nodes[] {nodeId, nodeType, status failure}, which already names the failing node. Q1 MEASURED: HotCRM's reading in the card body (18.0.0-next.5) is exactly the string this chain composes at the resume door: crud-nodes.ts, flow-dispatch-status.ts and duplicate-record-error.ts are byte-identical between the 18.0.0-next.5 tag commit 820fd5a and main (diff empty), and the engine throw line and the resume door's message line are present verbatim on both. Validation leg (static): ValidationError (objectql/src/validation/record-validator.ts:241-257; code VALIDATION_FAILED, message = the joined author rule messages, fields[]) loses code and fields[] at hop 2, so the user reads "Node 'N' failed: create_record(OBJ) failed: RULE MESSAGE". Permission leg (static): PermissionDeniedError (plugins/plugin-security/src/errors.ts:41-58; PERMISSION_DENIED, 403, details) loses all three, so the user reads '... failed: [Security] Access denied: ...', operator text the record form deliberately replaces. App-hook leg (named producer): HotCRM src/sales/objects/account.hook.ts refuse() sets userMessage on crm_account beforeInsert refusals (10 HotCRM hook files mark userMessage, crm_account, crm_contact and crm_opportunity among them); create_account writes crm_account, and hop 2 drops the marking, so a producer-marked refusal reaches the flow user unmarked and prefixed. Q2 ENVELOPE: FLOW_FAILED is registered under @objectstack/runtime (spec api/error-code-ledger.zod.ts:421-426). ApiErrorSchema (api/contract.zod.ts:16-97): code closed; declaredCode means demotion of a non-vocabulary code (:30-49); message 'Readable error message' (:50); userMessage is producer-marked end-user text, verbatim, which 'never replaces message - the diagnostic channel keeps its own wording' (:51-97). details is z.unknown() (contract.zod.ts:253, errors.zod.ts:450). The resume door's details are declared as ResumeFailureDetailsSchema (api/automation-api.zod.ts:390-470) beside errorMessage/summary. AutomationResult (contracts/automation-service.ts:387-589): code is a CLOSED union of trigger and resume refusal codes (:489); errorMessage is the flow's friendly failure text (:543-549). Rulings, quoted: #9378 comment 5321043983 (maintainer verbatim 「同意」): 'ran and failed ... 400 FLOW_FAILED' and 'codes are stamped at the route ...; AutomationResult.code stays closed - the engine classifies via existing vocabulary; any genuinely NEW ledger code this surfaces goes through the spec seat, never minted mid-PR. No sniffing - producer classifies, route maps.' #9384 comment 5321043167 (「同意」): 'The union stays closed'. #9446 comment 5322867819 (「同意」): 'the #9378 status table is a property of the flow-dispatch CONTRACT - all three doors converge'. #9585 (「接受你的所有建议。」, quoted in claim 5339708379): the /actions door 'carries exactly two fields into error.details ... errorMessage ... and summary' and 'Scope: bounded to the one door. No new schema keys.' (restated at action-execution.ts:1050-1051: 'A third key here is a contract widening the #9585 ruling does not cover'). #22507 comment 6093472708 (「22507 同意」), restated at engine.ts:11762-11764: 'a user-read flow string the server renders per run is translated where it is rendered, in the run's locale, before its holes are filled, and a client overlay never touches a server-rendered slot.' Unique wire spelling, maintainer ruling 2026-09-03 recorded in commit 65846bc (runtime/src/http-dispatcher.ts:1204-1207): 'one wire spelling on every route, UNIQUE_VIOLATION'. userMessage: maintainer ruling 2026-08-19 on objectui#5210, option 1 (contract.zod.ts:59-61; scope restated in objectui#5210 comment 5344182262: 'render a marked ... message; an unmarked message keeps the generic substitution'). Q2 SLOT ANSWER: no existing slot carries the inner CODE without a contract act. details parses any key, but the FLOW_FAILED details are ruled per door (#9585: exactly two at /actions; ResumeFailureDetailsSchema at resume) and pinned by exact equality (runtime/src/domains/automation-resume-stranded-details.test.ts:99, runtime/src/actions-flow-dispatch-status.test.ts:377); declaredCode means demotion and every inner code here is a vocabulary member; AutomationResult.code is closed; summary.nodes[] declares no code. The inner TEXT does have an existing declared slot, ApiErrorSchema.userMessage, which no flow door emits today. Q3 CONSUMERS. Diagnostic (want node id, operation, object): sys_automation_run.error (service-automation/src/sys-automation-run.object.ts:375-380, 'the message a designer needs to fix it'), written by recordLog at engine.ts:7156 and :8690 and the retry exits (:13645, :13678); Studio's FlowRunsPanel reads run.error and step.error (objectui previews/FlowRunsPanel.tsx:403, :315); the step log (engine.ts:12107); in-flow $error.message and NODE_ID.error (engine.ts:12145-12151, setNodeError :11718-11723), which fault handlers and try_catch catch regions read, documented as 'normally names the step that failed' (spec automation/control-flow.zod.ts:342); the subflow parent text (engine.ts:8059) and failAncestors (:8700); plugin-approvals composes its resume failure and stranded refusal from result.error (plugins/plugin-approvals/src/approval-service.ts:4213, :4464); SDK, MCP and integration callers read error.message (MCP resume_run domains/mcp.ts:1003; run_action through dispatchFlowAction). The run-summary log line carries counts, not the error text (engine.ts:10452-10470). End-user facing: only the objectui FlowRunner dialog Alert and toast, and the two flow-launch handlers, all through interpretFlowResponse. Pins on the PRODUCED wording against a real engine or stack: packages/qa/dogfood/test/flow-runas.dogfood.test.ts:137 (wire message contains "Node 'ID' failed"), service-automation try-catch-returned-failure-steps.test.ts:314 and :346, loop-dying-body-steps.test.ts:334, builtin/create-record-duplicate-code.test.ts:96. Door suites that pass a fixture string through a fake service: automation-trigger-route-status.test.ts:103 and :142, actions-flow-dispatch-status.test.ts:199, :364, :481, endpoint-flow-dispatch-status.test.ts:130, http-dispatcher.test.ts:601, client.test.ts:1542 and :1614, envelope-convergence.test.ts:195, rest-approvals-wire-codes.test.ts:381. Q4 CONSOLE (objectui origin/main 878a4af; FlowRunner.tsx, flowResponse.ts, core actionErrorDetail.ts, react error-message.ts and data-objectstack are absent from the 145-file compare to remote main 8cd4db4, so byte-identical there; form.tsx is in it and was read at 8cd4db4). Runner: interpretFlowResponse's 400 FLOW_FAILED arm (app-shell/src/utils/flowResponse.ts:345-355) shows details.errorMessage, translated through flows.FLOW.errorMessage (:212-226, :296-300), else actionErrorDetail(json), i.e. error.message verbatim (core/src/actions/actionErrorDetail.ts:27-35). It reads neither userMessage nor any inner code. FlowRunner shows that string in toast.error and a destructive Alert (views/FlowRunner.tsx:460-466, :627-633), under a comment calling error.message 'already user-grade prose ... passed through verbatim and untranslated' (:629-632). Record save (components/src/renderers/form/form.tsx:3023-3026 at 8cd4db4): declaredUserMessage(error), else the permission substitution t('form.noPermissionToSave') when isPermissionError, else extractWriteErrorMessage(error) (strips one leading TAG or CODE prefix), else t('form.submitFailed') (react/src/utils/error-message.ts:125-137, :202-215, :52-90). So the only code-keyed localisation of a refused save is PERMISSION_DENIED/FORBIDDEN/403; a unique violation shows the server's English sentence naming 'crm_account', since the data door keeps message as thrown (runtime/src/http-dispatcher.ts:1228-1233). git grep at 878a4af: zero non-test hits for UNIQUE_VIOLATION or DUPLICATE_RECORD in packages//src and apps//src. Would a code on the envelope be enough? No: it needs a server carrier (none exists, Q2), a new reader in the FLOW_FAILED arm, new copy per code in 10 locale files (copy that record saves do not have either), and a wire-spelling choice. Handing the runner the inner refusal and reusing the record-save rule needs no new copy and gives record-save parity. Q5 PR #24778 (draft, head 56cac78): its crud-nodes.ts hunk is at :153-170 (DROPPED_REASON_LABEL gains stored_expression; droppedFieldsWarning branches on it), the success-path warning. The four catch arms (:542, :657-688, :788, :855) are untouched, so no textual or semantic collision with any route here; a route editing the catch arms is a disjoint hunk in the same file and only the file-level serial constraint the claim records applies. Its spec files (contracts/data-engine.ts, data/data-engine.zod.ts) and objectql files (engine.ts, readonly-strict-errors.ts) are disjoint from every route's files. CORPUS for the business axis: HotCRM main 3116e7ac3a has 6 screen flows with write nodes (lead_conversion, quote_generation, schedule_followup, escalate_case, close_case, claim_case; 13 write nodes), 0 declaring errorMessage, 0 try_catch nodes or fault edges. objectstack examples at 7169612: app-crm crm_convert_lead_wizard declares errorMessage with a zh-CN translation (examples/app-crm/src/translations/crm.translation.ts:219); app-showcase showcase_reassign_wizard (1 update_record) and app-todo quick_add_task (1 create_record) declare none. So the unauthored default is what users see on 8 of the 9 write-bearing screen flows measured. ROUTE MAP and recommendation in open_questions[0]: recommend B-prime (carry the inner refusal across the flow door, message unchanged, the console renders it with its record-save rule), with HotCRM's app half (F) in parallel and the inner sentence's API name as an engine-lane sub-issue. Lanes: domain:spec, domain:services, domain:cli, domain:ui (objectui), domain:engine for the inner sentence, repo:hotcrm for the app half.",
"tests": "NO test suite, build or probe script was run (os-dev.md readings clause; conflict named in summary). Real-engine probe for the unique, validation and permission legs: NOT MEASURED, reason: readings-only dispatch, no closure build. Readings taken instead, each a count or diff against a named tree: (1) Byte identity of the chain between the 18.0.0-next.5 tag commit 820fd5a (refs/tags/@objectstack/cli@18.0.0-next.5) and main 7169612, files fetched by REST contents at that ref: diff of crud-nodes.ts, flow-dispatch-status.ts and duplicate-record-error.ts printed nothing; the throw line "throw new Error(
Node '${node.id}' failed: ${errMsg})" is at next.5 engine.ts:12008 and main :12168; "message: result.error ?? 'Flow run failed'" is at next.5 domains/automation.ts:2347 and main :2353. This carries the card's HotCRM public-door measurement onto main's chain. (2) git diff --stat 74fcec1..7169612: 3 files (content/docs/releases/v18/index.mdx, content/docs/upgrading.mdx, packages/cli/CHANGELOG.md). git diff --stat d606fc1..7169612 limited to service-automation, runtime, objectql, qa, spec, types: empty (the 6-file delta is docs, CHANGELOG and driver-sql introspection). (3) CI check-runs read by REST: d606fc1, Test Core (1/6) through (6/6) success in one run and Dogfood Regression Gate (1/3) through (3/3) success; 74fcec1, Dogfood Regression Gate (1/3) through (3/3) success. So the real-stack pin flow-runas.dogfood.test.ts:137 (a refusal inside a flow write node answers 400 FLOW_FAILED whose message contains "Node 'ID' failed") and the real-engine pins try-catch-returned-failure-steps.test.ts:314 and create-record-duplicate-code.test.ts:96 hold on code identical to main. Main head 7169612's own full battery was in_progress at read time: NOT MEASURED on that sha. (4) Corpus counts: HotCRM at 3116e7ac3a (blobless clone into the scratchpad, files read with git show, nothing executed) and objectstack examples at 7169612, as reported in summary. (5) objectui: REST compare 878a4af...8cd4db4 = ahead 20, 145 files; the console chain files are absent except form.tsx, whose catch branch is unchanged (remote :3023-3026). git grep at 878a4af for UNIQUE_VIOLATION or DUPLICATE_RECORD in non-test packages//src and apps//src: 0 hits. (6) PR #24778 diff by REST: the crud-nodes.ts hunk header is '@@ -153,10 +153,21 @@'. Ablation and reverse verification: not applicable, nothing changed.","gates": "No diff, so no gate family is derived and none was run; there is no PR, so there are no PR check-runs to cite. CI readings on main, cited not re-run: d606fc1 Test Core 1/6-6/6 success, Dogfood Regression Gate 1/3-3/3 success, Build Core success; 74fcec1 Dogfood Regression Gate 1/3-3/3 success, TypeScript Type Check success; 7169612 Test Core 1/6, 5/6, 6/6 and Dogfood Regression Gate in_progress at read time. Worktree ../objectstack-issue-24914 was created off origin/main 7169612, never edited, and removed at the end after git status --porcelain and git diff HEAD both printed nothing; the local branch ref, which pointed at 7169612 with no commit of its own, was deleted with it.",
"mcp_calls": "0 - no MCP GitHub tool was called",
"api_writes": "1 - POST /repos//issues/24914/comments (this os-dev-report, through scripts/pm/post-stamped.mjs and the fleet relay). Every other GitHub call was a REST read.",
"open_questions": [
{
"question": "Which route fixes the engine refusal text a screen-flow user sees? Routes are mapped with files, lanes, protocol class and cost; every reading is on objectstack 7169612 and objectui 878a4af/8cd4db4.",
"options": [
"A - server only, rewrite error.message. The envelope message becomes the inner sentence; node id, operation and object move to details or the run record. Needs a new AutomationResult member for the inner sentence (spec contracts/automation-service.ts plus TriggerFlowResponseSchema parity), engine carriage (returned-failure arm :12097-12168, execute :7104-7311, resumeInternal :8610-8771, retry exits :13613-13681), crud-nodes four catch arms, and four doors plus the MCP bridges (flow-dispatch-status.ts:203, automation.ts:2030-2045 and :2339-2361, action-execution.ts:1288-1296, endpoint-executor.ts:581-598). Protocol class: yes, a REST door's answer changes and a spec member is added. Cost: three lanes (spec, services, cli); pin churn in flow-runas.dogfood.test.ts:137, four runtime door suites and two client suites; the /actions wording has been on the wire since #3962 and its comment calls re-labelling it unruled. Four axes: business - removes both prefixes for the card's case, but the user still reads 'Duplicate record refused on crm_account', English, and for a permission refusal reads raw '[Security] Access denied', which the record form substitutes; long-term - contradicts ApiErrorSchema's split, where message is the diagnostic channel and userMessage the user channel, takes the operation and object off the wire for SDK and MCP callers, and makes message a server-rendered user-read string that #22507 ruling A says must then be translated server-side, which no catalog does; AI - neutral; startup - three lanes for a partial fix.",
"B - A plus the inner code on the envelope. AutomationResult.code is closed by the #9378 and #9384 rulings, so a new member is needed; an envelope details key widens the #9585 two-field bound and the exact-equality pins; the console needs a reader and new copy per code in 10 locales; the code needs a wire spelling (UNIQUE_VIOLATION per the 2026-09-03 ruling, not the in-process DUPLICATE_RECORD). Protocol class: yes. Cost: everything in A plus objectui and a ruling. Four axes: business - zero measured consumers of a flow-envelope code today (the console localises no UNIQUE_VIOLATION even on record saves); long-term - inherits A's narrowing of message; AI - code-branching beats regex; startup - largest route, with new copy that has no pull.",
"C - console only, map refusal codes in the flow runner. Not possible today: no code reaches the console (error.code is FLOW_FAILED, details carry errorMessage, summary and the resume verdict, summary.nodes[] has no code); reading one out of the message is a regex over prose, which PD #12 and objectui's one-strict-contract rule forbid. Protocol class: none. Four axes: fails business (nothing to map) and long-term (sniffing).",
"D - carry the producer-marked userMessage across the flow door (other route; the minimal subset of B-prime). crud-nodes' four catch arms read declaredUserMessage(err) (@objectstack/types, already a dependency of service-automation); NodeExecutionResult (service-automation engine.ts:358) and AutomationResult (spec) gain an optional userMessage with ApiErrorSchema's semantics; the four doors emit it as the envelope's existing top-level userMessage, which needs deps.error to take it (runtime domain-handler-registry.ts:313 has no slot); objectui's FLOW_FAILED arm reads it after errorMessage and before message. message is unchanged, so no wording pin moves. Protocol class: yes, additive (Clause-2 yes, widening). Lanes: spec, services, cli, ui. Four axes: business - measured producers exist (10 HotCRM hook files mark userMessage, including crm_account, which the card's node writes), but the card's measured unique refusal is unmarked and is NOT changed; long-term - completes a declared channel at the one boundary that drops it, which is the frame's 'declared but not delivered = implement' rule; AI - removes a silent drop of a marking AI-written hooks already make; startup - smallest server route with measured producers.",
"B-prime - carry the inner refusal structurally, message unchanged (other route; contains D). D, plus the inner refusal as {code in its wire spelling, message} in ONE FLOW_FAILED details member declared once in spec (the ResumeFailureDetailsSchema pattern) and emitted by all four doors; objectui's FLOW_FAILED arm renders it with the record-save rule it already has (form.tsx:3023-3026: userMessage, then the permission substitution, then the inner message with its prefix stripped), after the author's errorMessage. Needs a maintainer ruling, since it widens the #9585 two-field details at /actions and the flow-dispatch details at the other doors. Protocol class: yes, additive (Clause-2 yes, widening). Lanes: domain:spec (the AutomationResult member and the details declaration), domain:services (crud-nodes four catch arms, engine carriage on both terminal exits and the retry exits; optionally stamp the executor's code into the step log instead of NODE_FAILURE), domain:cli (trigger, resume, /actions and endpoint doors, MCP bridges, deps.error), domain:ui (flowResponse.ts and the FlowRunner comment). Four axes: business - the only server route that brings all three classes the card names to record-save parity (unique: the unprefixed inner sentence; validation: the author's rule text; permission: the localised substitute; hook: the marked text), on every unauthored screen flow (8 of the 9 write-bearing screen flows measured); locale for the unique case still needs F or the engine sub-issue, exactly as on a record save; long-term - fixes the root, which is flattening the structured refusal into a string at hop 2, once for all doors, keeps message diagnostic for logs, SDK, approvals and the stored run, and moves zero wording pins; AI - one declared carrier with no consumer-side regex, and AI-written flows get record-save behaviour without authoring anything; startup - the largest of the recommended set (4 lanes and a ruling), but it adds no error code, no gate and no copy.",
"E - console only, generic substitution (other route). When no errorMessage and no userMessage reach the runner, show a localised generic sentence and keep the raw text for the developer console, as the record form does for permission refusals. Protocol class: none. Lane: domain:ui. Four axes: business - no identifiers and localised, but it hides the author's validation rule text and hook text, so the user can no longer act on the reason; long-term - matches the 'unmarked is diagnostic' clause of ApiErrorSchema but overrides producers (ValidationError, DuplicateRecordError) that write message for display; AI - safe default; startup - smallest.",
"F - app half in HotCRM, no platform change (other route, independent of the choice above). Declare errorMessage, with a zh-CN translation at flows.lead_conversion.errorMessage, on its 6 write-bearing screen flows, and/or route $error.code == 'DUPLICATE_RECORD' on create_account (available on create_record today) to a refusing end node whose message is translated at its flows refusals key and picked by the engine in the run's locale. Fixes the measured case in the rep's locale with shipped features. Carrier: the repo:hotcrm seat."
],
"recommendation": "B-prime, with F in parallel and the inner sentence as an engine-lane sub-issue (out_of_scope_findings[0]). Business: B-prime is the only server route that removes the node id and operation from what the user sees for all three named refusal classes without narrowing message, and the default it fixes is the one 8 of the 9 measured write-bearing screen flows rely on; F is the only route that gets the measured case into zh-CN now. Long-term: it repairs the flattening at its source and honours ApiErrorSchema's message/userMessage split rather than rewriting the diagnostic channel (A, B). AI: one declared carrier, no regex (C), no override of producer text (E). Startup: no new code, no gate, no copy; the cost is four lanes and one ruling, which the measured producers and the 8-of-9 default justify. The four axes conflict on cost: if the maintainer wants the narrowest act, D alone completes the declared userMessage channel and is a strict subset of B-prime, but it leaves the card's measured unique refusal unchanged, so it does not close this card. No new gate is recommended (a lint for screen flows without errorMessage would be a new gate, which defaults to no and no maintainer has named one)."
},
{
"question": "Under B-prime, which spelling does the carried inner code take for a unique refusal?",
"options": [
"A - UNIQUE_VIOLATION, the wire spelling the 2026-09-03 ruling fixes for every route (runtime/src/http-dispatcher.ts:1204-1207); the flow door translates the engine's class the way the data doors do.",
"B - DUPLICATE_RECORD, the in-process identity the crud node already carries to $error.code (crud-nodes.ts:680-683)."
],
"recommendation": "A. Business: the console's record-save rule and any client already branch on the data door's wire codes, so parity means the same spelling. Long-term: the ruling says one wire spelling on every route, and B would add a second. AI: one spelling per boundary is what DuplicateRecordError's developerMessage teaches. Startup: no extra vocabulary. In-flow $error.code stays DUPLICATE_RECORD, which the ruling leaves in-process."
}
],
"out_of_scope_findings": [
"class: a · reach: measured public door - HotCRM's FlowRunner text on 18.0.0-next.5 (card body) carries the inner sentence 'Duplicate record refused on crm_account: ...', byte-identical producer on main; named producer buildDuplicateMessage, packages/objectql/src/duplicate-record-error.ts:131-138, whose message http-dispatcher.ts:1228-1233 keeps as thrown on the data door, so a record save shows it too (that leg NOT MEASURED, read statically through form.tsx:3023-3026 at objectui 8cd4db4) · evidence: the user-facing sentence of a unique refusal names the object API name, which the card's own bar ('no internal identifiers') rules out on both the flow and record-save surfaces; every flow route in open_questions[0] still shows it until the producer names the object by label or not at all · file as a sub-issue of #24914 in domain:engine (the inner-producer half of the route) · dedupe words: DuplicateRecordError message object name; unique violation API name user; Duplicate record refused on; buildDuplicateMessage label",
"carrier: none (承接者:无) · noted, not filed: the engine's step log records every returned node failure as code NODE_FAILURE (service-automation/src/engine.ts:12107) even when the executor classified one (create_record's DUPLICATE_RECORD reaches $error.code at :12149 but not the stored step), so the stored run cannot show a classified code; an observation with no reader that misbehaves today, folded into B-prime's optional services item",
"carrier: repo:hotcrm seat · noted, not filed: route F (errorMessage with zh-CN on the six write-bearing screen flows lead_conversion, quote_generation, schedule_followup, escalate_case, close_case and claim_case; or $error.code routing on create_account to a translated refusing end) is app work with shipped features, recorded so the HotCRM seat can take it"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
决策请求 ·
domain:servicesseat 1 ·os-justin·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T17:30Z依据:只读测量报告
6296914512(本卡),读数基于 objectstackorigin/main71696121a6、objectui878a4af/8cd4db4。认领6296417944的范围是只读,没有写任何代码。一句话问题: 用户在 screen flow 里点保存,被平台拒绝(重复记录、校验规则、权限)时,弹窗和提示条显示的是一串英文开发者文本,里面带着节点 id 和对象 API 名。同样的拒绝如果发生在普通记录表单里,用户看到的是能看懂、能照着处理的话。
背景
Node 'create_account' failed: create_record(crm_account) failed: Duplicate record refused on 'crm_account': …crud-nodes.ts:657-688),只有DUPLICATE_RECORD一种保留了 code;Node '…' failed:前缀(engine.ts:12168);FLOW_FAILED的message(flow-dispatch-status.ts:203、automation.ts:2353、action-execution.ts:1288-1296);flowResponse.ts:345-355)。userMessage,权限拒绝替换成本地化句子,其余去掉一层前缀(objectuiform.tsx:3023-3026)。userMessage;details由裁决定死(/actions门恰好两个键);AutomationResult.code是闭合联合;Governing text
ApiErrorSchema(packages/spec/src/api/contract.zod.ts:50-97):message是诊断通道,userMessage是生产方标记的终端用户文本,后者「never replaces message」。triggerroutes still answer HTTP 200 wrapping an inner {success:false} — the same #3962 residue #8684 closed on resume #9378 裁决5321043983(「同意」):「AutomationResult.code stays closed … any genuinely NEW ledger code … goes through the spec seat」。/actionsmislabels never-dispatched refusals, declared endpoints still answer 200 #9446 裁决5322867819(「同意」):状态表是 flow-dispatch 契约的属性,三门收敛。errorMessageor the runsummary— the/actionsdoor has no channel for them #9585(「接受你的所有建议。」,认领5339708379引):/actions门「carries exactly two fields into error.details … No new schema keys」。descriptionhave no key in theflowstranslation face — the zh-CN intake wizard translates its labels and placeholders and keeps its options and body text in English (17.7.0) #22507 裁决6093472708(「22507 同意」):服务端按次渲染、给用户读的流程字符串,要在渲染处按 run 的语言翻译。65846bc46,http-dispatcher.ts:1204-1207):所有路由统一用UNIQUE_VIOLATION。协议声明、是否改协议
推荐的路线要改协议:在
AutomationResult上加一个可选成员,并给三门的FLOW_FAILEDdetails 声明一个新成员。这是加宽,按Clause-②: yes (widening)声明。它放宽了 #9585 的两键上限,所以按定级判据属协议类,由维护者裁决。前提(各带 re-check 命令)
git grep -n "failed: \${(err as Error).message}" packages/services/service-automation/src/builtin/crud-nodes.ts,应命中 4 处。FLOW_FAILEDdetails 不带内层 code。re-check:git grep -n "the #9585 ruling does not cover" packages/runtime/src/action-execution.ts(应命中 1 处,:1050)userMessage。re-check:在 objectui 里跑git grep -n userMessage packages/app-shell/src/utils/flowResponse.ts,应为 0 命中。选项 × 真实代价 × 工作量
message改写成内层句子crm_account、仍是英文,权限拒绝露出[Security] Access denied原文;SDK、MCP、审批侧读到的诊断文本少了节点和操作userMessage通道userMessage,三门作为信封顶层userMessage发出,控制台优先显示{code(wire 拼写), message}作为FLOW_FAILEDdetails 的一个成员,在 spec 声明一次、三门都发;控制台沿用记录表单的规则显示message不变,诊断方不受影响,现有措辞断言一条不动业务含义直译:
四轴(业务立场)
message仍是诊断通道,符合ApiErrorSchema对两个通道的拆分。A 和 B 反过来改写诊断通道。推荐:B′,内层 code 用
UNIQUE_VIOLATION拼写duplicate-record-error.ts:131-138),任何路线都要靠一张引擎子卡另行处理。errorMessage:属于应用补位;按「平台相关的功能,应该尽量先改平台」(维护者 2026-10-18,台账6245607810),席位不推荐它作为本卡的修法。裁后执行(选 B′ 时)
AutomationResult加可选成员userMessage与内层拒绝成员,并在三门的FLOW_FAILEDdetails 声明一次(仿ResumeFailureDetailsSchema)。按Clause-②: yes (widening)做契约复核。crud-nodes.ts的四个 catch 分支读出declaredUserMessage(err)与内层 code(转成 wire 拼写);domain:cli: trigger、resume、/actions、endpoint 四门与 MCP 桥发出新成员,deps.error增加userMessage槽。在 [PM seat] domain:cli · seat 2 — ⏳ vacant #22648 声明。errorMessage、userMessage、内层拒绝(套记录表单规则)、message。相关单与 PR
triggerroutes still answer HTTP 200 wrapping an inner {success:false} — the same #3962 residue #8684 closed on resume #9378、The other two flow-dispatch doors never got the #9378 status table —/actionsmislabels never-dispatched refusals, declared endpoints still answer 200 #9446、A flow ACTION that fails cannot carry the author'serrorMessageor the runsummary— the/actionsdoor has no channel for them #9585、A screen flow's select options and screendescriptionhave no key in theflowstranslation face — the zh-CN intake wizard translates its labels and placeholders and keeps its options and body text in English (17.7.0) #22507(裁决)userMessage渲染裁决)os-decision-facets
Prior rulings read:
FLOW_FAILED details·AutomationResult.code closed·userMessage·UNIQUE_VIOLATION wire→ 6 hits (#93785321043983, #93845321043167, #94465322867819, #9585 claim5339708379, #225076093472708, objectui#52105344182262); ADR none named by the code comments; thread: none.Generated by Claude Code
objectstack-fleet commentedon Oct 20, 2026
指针 ·
domain:servicesseat 1 ·session_011TdZ8GjixNZFLz1xJD9zzK· 2026-10-20T17:34Z:决策6297041774裁后执行第 5 步的引擎子卡已立为 #24951(唯一约束拒绝句点名对象 API 名,domain:engine由分诊定级)。它不随本卡的裁决变化。Generated by Claude Code
8 remaining items
objectstack-fleet commentedon Oct 21, 2026
Claim: PM loop round 1 · 2026-10-21T22:26Z · p2,
domain:services, step (2) of ruling B′ (6303788292)Session:
session_01UnHcTADjeg3VwxF21QHj7W(the dispatching seat; the dev is amode:subagentrun of this session, told apart by its branch)Account:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24914-flow-inner-refusalWorktree:
objectstack-issue-24914Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface (line anchors at
origin/mainabc25a353):packages/services/service-automation/src/builtin/crud-nodes.ts: the four catch arms (get_recordabout:553,create_recordabout:680–:700,update_recordabout:799,delete_recordabout:866). They readdeclaredUserMessage(err)and the inner code and return them structurally. Theirerrortext is unchanged.packages/services/service-automation/src/engine.ts: the returned-failure arm (about:12100–:12170) and the terminal exits (theexecute()catch,resumeInternaland the retry exits). They carryuserMessage/innerRefusalontoAutomationResult.error/messageare unchanged.packages/runtime/src/**(domain:cli, declared cross-lane on [PM seat] domain:cli · seat 2 — ⏳ vacant #22648 in this act):flow-dispatch-status.ts,domains/automation.ts(the trigger and resume doors),action-execution.ts(the/actionsFLOW_FAILEDdetails, about:1290–:1320),endpoint-executor.ts,domains/mcp.ts(the flow bridges) and thedeps.erroruserMessageslot. These are the regions the ruling names, and nothing else in those files..changeset/24914-*.md.packages/spec/**: PR feat(spec): FLOW_FAILED carries the refused write's inner refusal — AutomationResult.userMessage / innerRefusal and one FlowFailureDetailsSchema for the four doors #25037 (spec:FLOW_FAILEDcarries the refused write's inner refusal — an optionaluserMessageonAutomationResultand one declared details member{ code, message }on the trigger, resume, /actions and endpoint doors (ruling B′ on #24914) #24996) landed the members andFlowFailureDetailsSchema.objectql'sduplicate-record-error.ts(objectql: a unique refusal's user-facing sentence names the object by its API name ("Duplicate record refused on 'crm_account': …"), which a screen-flow user and a record-save user both read verbatim #24951), not objectui (FlowRunner: aFLOW_FAILEDenvelope is rendered by the record-save rule — the author'serrorMessage, thenuserMessage, then the declared inner refusal, thenmessage(the console half of objectstack#24914 ruling B′) objectui#13285 is step 3), notplugin-approvals.Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT;dispatch-gates --tieron this surface: "no path-derived mandate … floor sonnet · default opus · ceiling fable")Clause-②: no — the members and the four doors'
FLOW_FAILEDdetails schema were declared by spec:FLOW_FAILEDcarries the refused write's inner refusal — an optionaluserMessageonAutomationResultand one declared details member{ code, message }on the trigger, resume, /actions and endpoint doors (ruling B′ on #24914) #24996 (PR feat(spec): FLOW_FAILED carries the refused write's inner refusal — AutomationResult.userMessage / innerRefusal and one FlowFailureDetailsSchema for the four doors #25037, which carried the widening declaration). This card emits what that contract declares. The seat does no build, so the dev re-declares from the built declaration closure if the diff adds a runtime export or a key beyondFlowFailureDetailsSchema.Responsibility:
service-automation's node wrappers flatten an engine refusal into text, and the runtime doors surface it as FLOW_FAILED's message | ApiErrorSchema.userMessage and the declared AutomationResult.userMessage / innerRefusal (PR #25037) are the platform path, and no flow door emits them today | every end user of a screen flow whose write node the engine refuses; measured in HotCRM's lead conversionThread-read: 6313336204
Serial constraints cleared:
7532ea3c0.domain:cli, mcp: list_objects / describe_object cannot see any package-installed object (a marketplace app's whole schema) — the HTTP bridge reads the metadata service, not the SchemaRegistry REST serves #25120, draft) touchesaction-execution.ts(hunks at:21,:287–:395) anddomains/mcp.ts(hunks at:430–:760). Those regions are disjoint from this card's. The files are shared, so whichever lands second mergesmain.service-automationor the runtime's flow-dispatch files (the 8 open PRs' file lists were read at this claim).Generated by Claude Code
wip(service-automation): carry a data node's inner refusal to the run…
wip(runtime): the four flow doors emit innerRefusal and userMessage (#…
wip: tests for the inner refusal across the engine, the doors and the…
test: a FlowRunSummary the spec parses in the door fixture (#24914)
chore(changeset): service-automation and runtime patch for the flow i…
test: the refusing data double accepts calls through the engine's dis…
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{
"issue": 24914,
"status": "done",
"branch": "claude/issue-24914-flow-inner-refusal",
"pr": "#25147",
"session": "session_01UnHcTADjeg3VwxF21QHj7W - the dispatching seat's session (claim 6327931189); this run is its mode:subagent dev, told apart by the branch",
"premise_still_valid": true,
"summary": "Step (2) of ruling B' (6303788292), emitting exactly what PR #25037 (#24996) declared; packages/spec untouched. Identity: newest Claim 6327931189 names claude/issue-24914-flow-inner-refusal. service-automation: new src/inner-refusal.ts refusalCarriedBy(err) -> { userMessage?, innerRefusal? }, used by the four crud-nodes.ts catch arms (get/create/update/delete); userMessage = declaredUserMessage(err); innerRefusal only when resolveThrownHttpError(err).declaredStatus is 4xx, code = the data door's errorFromThrown code for the same throw (resolver code, except the engine duplicate envelope -> UNIQUE_VIOLATION read off structuredCodeAnswer), message = the throw's own text; in-flow $error.code stays DUPLICATE_RECORD. NodeExecutionResult gains the two optional members; the engine brands its unchanged Node 'ID' failed throw (symbol brand, partial-steps idiom) and spreads the members at execute() failure, resumeInternal stranded, executeWithoutRetry failure and retryExecution exhausted (last attempt's members). runtime: flow-dispatch-status.ts flowFailureDetails(result) typed as FlowFailureDetails and flowFailureEnvelope(result); trigger + resume doors (domains/automation.ts), /actions (action-execution.ts FlowActionRunDetails = FlowFailureDetails, FlowActionRefusal.userMessage field; handler domains/actions.ts), endpoint (endpoint-executor.ts) emit details.innerRefusal and top-level userMessage; DomainHandlerDeps.error gains optional extra { userMessage? } wired in http-dispatcher.ts; MCP resume_run resumeDoorError passes the slot and copies userMessage onto the thrown error. Every error/message text byte-identical; AutomationResult.code closed; no new code minted. Premise checks on abc25a3: (1) git grep innerRefusal/userMessage over the flow doors + service-automation src: only the action-execution.ts:1051 comment (controls: http-dispatcher.ts userMessage x6, spec automation-service.ts innerRefusal x1) - held. (2) catch arms at crud-nodes.ts :552-553, :662-699, :798-799, :865-866, engine throw :12168 - held. (3) ValidationError -> VALIDATION_FAILED, PermissionDeniedError -> PERMISSION_DENIED, unique -> UNIQUE_VIOLATION, an app hook's unregistered code -> the member its declared status names (closed ErrorCode per the spec docblock) - nothing minted. (4) marked userMessage verbatim, unmarked absent - pinned. (5) retry and resumeInternal carry; subflow parents and plugin-approvals read result.error, whose text is unchanged; neither touched. Clause-2 re-declared yes (widening) per the dispatch's measure clause: the built dist/index.d.ts of both packages widens two published types (runtime DomainHandlerDeps.error optional 4th arg; service-automation NodeExecutionResult two optional members); no export added; changeset patch x2 with Metadata-protocol: none. PR #25147 is draft, assignee os-bill, body read back byte-identical; objectui#13285 named as staying open with no closing keyword near it.",
"tests": "Final head 0a2d128 (git rev-parse --short HEAD), under scripts/pm/os-verify-lock.sh, VERDICT command-exit 0: service-automation src/flow-inner-refusal.test.ts + create-record-duplicate-code.test.ts + builtin/loop-dying-body-steps.test.ts = 3 files / 29 passed; builtin/try-catch-returned-failure-steps.test.ts 17/17; runtime flow-failed-inner-refusal + mcp-resume-run + actions-flow-dispatch-status + endpoint-flow-dispatch-status + domains/automation-resume-stranded-details + domains/automation-trigger-route-status = 6 files / 108 passed; verify src/automation-flow-inner-refusal.test.ts 1 file / 9 passed. Full suites at bbca8a6: pnpm --filter @objectstack/service-automation exec vitest run = Test Files 198 passed, Tests 2618 passed (SA_EXIT=0); pnpm --filter @objectstack/runtime exec vitest run --project local = Test Files 433 passed, Tests 6805 passed | 19 skipped (RT_EXIT=0). Delta bbca8a6..0a2d128: the changeset, flow-inner-refusal.test.ts (re-run green above), scripts/engine-double-contract.pinned.json, and the main merge 4c7dcb4 (driver-sql tests only). typecheck: pnpm --filter service-automation/runtime/verify typecheck exit 0 (tsc --noEmit + check:test-typecheck OK for each). Ablations (ablation-replace.mjs WRAP, each restore proven by HEAD blob hash, git diff HEAD empty after every leg; all source-resolved except V1): SA1 isRefusal forced false 10 failed | 3 passed (13); SA2 userMessage read dropped 2 failed; SA3 engine brand dropped 10 failed; SA4 execute() exit spread 8 failed; SA5 stranded exit 1 failed; SA6 retry exhausted carry 1 failed; SA7 per-attempt exit 1 failed; RT1 details innerRefusal 10 failed | 22 passed (32); RT2 envelope userMessage 6 failed; RT3 deps.error slot 5 failed; RT4 MCP copy-back 1 failed; RT5 /actions handler slot 2 failed; V1 (dist) SA1's mutation + pnpm --filter service-automation build + ablation-dist-preflight 'marker present in 2 built files' -> verify 8 failed | 1 passed (9); restore leg rebuild + preflight --absent 'marker absent from all 6 built files', 'working tree clean against HEAD' -> 9 passed. First passes of SA4/SA5/SA7/RT3/RT5 were no-ops refused by the tool (replacement already present), readings discarded, re-run with unique markers; RT5's first re-run got exit 99 (lock) and was run again. Reverse type verification not run separately: the FlowActionRunDetails = FlowFailureDetails binding and flowFailureDetails' return type are exercised by tsc in runtime typecheck exit 0.",
"gates": "Local subset: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 0a2d128 (first stderr line names the repo and 0a2d128; merge base 4c7dcb4; 17 paths) derived 80 commands; all 80 run after the final commit, each exit captured before any pipe (cmd > log 2>&1; ec=$?): 80 x exit 0. --ran reconciliation with ':: exit N' records: '80 derived famil(ies) accounted for - 80 run, 0 NOT-MEASURED (a DERIVED zero - all 80 recorded an exit code and none of them is 3)'. On the earlier head f420532: check-plugin-teardown-shape --self-test exit 3 (shallow clone, pinned fixture 621a487 absent) -> git fetch --depth=1 of that commit -> exit 0; check:engine-double-contract exit 1 (new double not routed through the dispatch predicates, then not pinned) -> repaired in the test + --write to scripts/engine-double-contract.pinned.json -> exit 0; check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET earlier, exit 0 in the final union ('112 published require entry point(s) across 69 package(s) load'). check-platform-checklist not run: no exported symbol renamed or deleted (FlowActionRunDetails keeps its name, is not a package export). Local-only families check:dev-prereqs / check:skill-frame-freshness not triggered (no packages/spec build, no frame files). CI (PR #25147, head 0a2d128) read at report time: 24 check-runs, 21 in_progress, 1 success, 2 skipped; Governed Surface Queue Guard in_progress; Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core not yet concluded - in_progress is the honest value; not waited on, not re-run locally.",
"dogfood_battery": "n/a - no
!in the PR title and Clause-② carries the (widening) arm, not (narrowing)","mcp_calls": "0 - no MCP GitHub tool was called",
"api_writes": "2 relay strokes (each one repository_dispatch to the board, executed as objectstack-fleet[bot]): pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/25147/assignees; comment = POST /repos//issues/24914/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). git push (branch) is not a REST write. All other GitHub calls were REST reads.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed: containers that re-word a child failure into their own returned failure (try_catch with no catch region; subflow, synchronous leg and resume delegation arm) do not forward userMessage/innerRefusal, so a refused write nested there reaches the door with neither member; allowed by the spec's 'present only when a refusal was met'; recorded in PR #25147 Acceptance notes",
"carrier: none (承接者:无) · noted, not filed: crud-nodes' own stored-metadata family refusals (returned before any data call, with a code) carry no innerRefusal; the ruling names the four catch arms",
"carrier: none (承接者:无) · noted, not filed: packages/mcp's tool-error renderer (errorResultFromThrown / errorResultWithCode) renders code, message, status and details but not userMessage, on run_action and resume_run alike; run_action still carries no run details (#9585 scope)",
"carrier: none (承接者:无) · noted, not filed: the step log still records NODE_FAILURE for a returned node failure (engine.ts returned-failure arm), unchanged by this card",
"carrier: #24951 · noted, not filed: the unique-refusal sentence itself names the object API name (objectql duplicate-record-error.ts); unchanged here"
],
"files_changed": [
".changeset/24914-flow-inner-refusal.md",
"packages/runtime/src/action-execution.ts",
"packages/runtime/src/domain-handler-registry.ts",
"packages/runtime/src/domains/actions.ts",
"packages/runtime/src/domains/automation.ts",
"packages/runtime/src/domains/mcp.ts",
"packages/runtime/src/endpoint-executor.ts",
"packages/runtime/src/flow-dispatch-status.ts",
"packages/runtime/src/flow-failed-inner-refusal.test.ts",
"packages/runtime/src/http-dispatcher.ts",
"packages/runtime/src/mcp-resume-run.test.ts",
"packages/services/service-automation/src/builtin/crud-nodes.ts",
"packages/services/service-automation/src/engine.ts",
"packages/services/service-automation/src/flow-inner-refusal.test.ts",
"packages/services/service-automation/src/inner-refusal.ts",
"packages/verify/src/automation-flow-inner-refusal.test.ts",
"scripts/engine-double-contract.pinned.json"
],
"line_budget": "n/a - no skills/** or governed surface touched",
"deviations": [
"Clause-② re-declared from the claim's 'no' to 'yes (widening)' per the dispatch's measure clause: built dist/index.d.ts widens runtime DomainHandlerDeps.error (optional extra) and service-automation NodeExecutionResult (two optional members); no export added",
"file surface beyond the claim: http-dispatcher.ts + domain-handler-registry.ts (the deps.error slot), domains/actions.ts (the /actions handler, one argument), scripts/engine-double-contract.pinned.json (gate --write for the new test's pinned double), packages/verify/src/automation-flow-inner-refusal.test.ts (the one package depending on engine and doors); declared in the PR body",
"five ablation legs' first passes were tool-refused no-ops (replacement text already present) and RT5's first re-run missed the lock (exit 99); all re-run, readings from the re-runs",
"full service-automation / runtime suites measured at bbca8a6, not the final head; delta named in tests and re-run where it touched a test",
"verify: only the new file + typecheck run locally (its full suite is CI's); test:repo and pnpm lint not run locally (CI's)",
"git fetch --depth=1 of commit 621a487 into the shared shallow object store, for check-plugin-teardown-shape --self-test's pinned fixture",
"a stray 6-byte file /build-a.pid (a PID number) was written at the filesystem root by a background launch whose shell variable was scoped to the subshell; its removal was refused by the harness safety check, so it is left for the user to delete",
"commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude) rather than the harness reminder's model-named line, which yields to the repo instructions",
"worktree ../objectstack-issue-24914 removed after the report (node_modules first, then git worktree remove without --force)"
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop round 1 · 2026-10-22T00:30Z · amends claim
6327931189to the measured declaration and file surface of PR #25147 (dev report6329651337)Session:
session_01UnHcTADjeg3VwxF21QHj7W(the dispatching seat; the dev is amode:subagentrun of this session, told apart by its branch)Account:
os-bill(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-24914-flow-inner-refusalWorktree:
objectstack-issue-24914Domain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface: everything in claim
6327931189, plus the files the ruled change needs, as declared in PR #25147's body:packages/runtime/src/domain-handler-registry.tsandhttp-dispatcher.ts: thedeps.erroruserMessageslot the ruling names.packages/runtime/src/domains/actions.ts: the/actionshandler passes that slot.packages/verify/src/automation-flow-inner-refusal.test.ts: the one package that depends on both the engine and the doors.scripts/engine-double-contract.pinned.json: the gate's own--writefor the new test double.domain:cli's; the addendum goes on [PM seat] domain:cli · seat 2 — ⏳ vacant #22648 in this act.Container & model:
M,mode:subagent,model: opus(TIER_DEFAULT; unchanged)Clause-②: yes (widening) — re-declared from the claim's
no, as the dispatch asked, by the dev's measurement on the built declaration closure.runtime's publishedDomainHandlerDeps.errorgains an optional fourth argument, andservice-automation's publishedNodeExecutionResultgains two optional members. No export is added. The seat's review atTIER_DEFAULTis owed before the queue.Responsibility:
unchanged from claim 6327931189Thread-read: 6329651337
Serial constraints cleared: unchanged. PR fix(runtime)!: the /mcp bridge reads objects from the protocol's merged view, so a package's objects are listed, described and gated #25136 (
domain:cli) sharesaction-execution.tsanddomains/mcp.tsin other regions; whichever lands second mergesmain.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
ACCEPT → PR #25147 comment 6329997478 ·
domain:servicesseat 2 ·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-22T00:54Z · contract review PASS6329711145· ready and auto-merge set through the relay; the landing note follows the merge.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Landed ·
domain:servicesseat 2 ·session_01UnHcTADjeg3VwxF21QHj7W· 2026-10-22T01:19Z9cfc575337origin/main9cfc57533, subject "fix(service-automation,runtime): a refused write inside a flow reaches every FLOW_FAILED door …"completedbyFixes; no other card closed in that windowState:
pm:dispatchedis removed in this act. Ruling B′'s step (3), objectstack-ai/objectui#13285 (pm:blocked), now waits on a release that carries this change. That is a cross-repo unlock, for the console lane's unlock scan. The step's open readings are carried in PR #25147's Acceptance notes, and #24951 carries the unique sentence.Generated by Claude Code