Skip to content

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

Closed

Description

@objectstack-fleet

Ruled: 6303788292 · letter B′ · 2026-10-20T23:12Z

Blocked-by: #24996

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.

Activity

  1. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    Why readings first. The fix spans:

    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/main 71696121a6.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  3. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    决策请求 · domain:services seat 1 · os-justin · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-20T17:30Z

    依据:只读测量报告 6296914512(本卡),读数基于 objectstack origin/main 71696121a6、objectui 878a4af / 8cd4db4。认领 6296417944 的范围是只读,没有写任何代码。

    一句话问题: 用户在 screen flow 里点保存,被平台拒绝(重复记录、校验规则、权限)时,弹窗和提示条显示的是一串英文开发者文本,里面带着节点 id 和对象 API 名。同样的拒绝如果发生在普通记录表单里,用户看到的是能看懂、能照着处理的话。

    背景

    • 用户今天看到的(HotCRM 线索转换,zh-CN 下仍是英文):Node 'create_account' failed: create_record(crm_account) failed: Duplicate record refused on 'crm_account': …
    • 原因: 拒绝在流程里逐层被压扁成一段字符串。
      • 写节点的 catch 只留下 message(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);
      • 控制台 FlowRunner 再原样显示(objectui flowResponse.ts:345-355)。
    • 记录表单为什么不同: 那条路径先看生产方标记的 userMessage,权限拒绝替换成本地化句子,其余去掉一层前缀(objectui form.tsx:3023-3026)。
    • flow 门的现状:
      • 不发 userMessage;
      • details 由裁决定死(/actions 门恰好两个键);
      • AutomationResult.code 是闭合联合;
      • 所以内层拒绝没有现成的承载位。

    Governing text

    协议声明、是否改协议

    推荐的路线要改协议:在 AutomationResult 上加一个可选成员,并给三门的 FLOW_FAILED details 声明一个新成员。这是加宽,按 Clause-②: yes (widening) 声明。它放宽了 #9585 的两键上限,所以按定级判据属协议类,由维护者裁决。

    前提(各带 re-check 命令)

    1. 写节点 catch 丢掉内层的 code、userMessage 和 details。re-check:git grep -n "failed: \${(err as Error).message}" packages/services/service-automation/src/builtin/crud-nodes.ts,应命中 4 处。
    2. flow 门的 FLOW_FAILED details 不带内层 code。re-check:git grep -n "the #9585 ruling does not cover" packages/runtime/src/action-execution.ts(应命中 1 处,:1050)
    3. 控制台 FLOW_FAILED 分支不读 userMessage。re-check:在 objectui 里跑 git grep -n userMessage packages/app-shell/src/utils/flowResponse.ts,应为 0 命中。

    选项 × 真实代价 × 工作量

    选项 做什么 客户可感知的后果 工作量
    A 只改服务端 message 把 message 改写成内层句子 前缀没了,但唯一约束拒绝仍点名 crm_account、仍是英文,权限拒绝露出 [Security] Access denied 原文;SDK、MCP、审批侧读到的诊断文本少了节点和操作 spec、services、cli 三车道;无先例
    B A 加内层 code 在 A 的基础上把 code 带上信封,控制台按 code 出文案 同 A,再加每个 code 10 种语言的新文案(记录表单今天也没有这些文案) A 再加 objectui,并需裁 wire 拼写;无先例
    C 只改控制台 在 runner 里按 code 映射 今天做不到:没有 code 到达控制台,只能对文本跑正则 —
    D 只补 userMessage 通道 写节点读出生产方标记的 userMessage,三门作为信封顶层 userMessage 发出,控制台优先显示 带标记的应用 hook 拒绝能原样到达用户(HotCRM 有 10 个 hook 文件做了标记);本卡实测的唯一约束拒绝没有标记,不受影响 spec、services、cli、ui 四车道,加宽;无先例
    B′ 结构化携带内层拒绝(含 D) 在 D 的基础上,把内层拒绝的 {code(wire 拼写), message} 作为 FLOW_FAILED details 的一个成员,在 spec 声明一次、三门都发;控制台沿用记录表单的规则显示 三类拒绝都和记录表单一致:唯一约束显示去掉前缀的内层句子,校验显示作者写的规则文本,权限显示本地化替换句,hook 显示标记文本;message 不变,诊断方不受影响,现有措辞断言一条不动 spec、services、cli、ui 四车道,加一条裁决;无先例
    E 控制台通用替换 没有作者文案时显示一句本地化的通用句 没有内部标识,但用户看不到校验规则和 hook 给出的原因,无从处理 只动 ui;无先例

    业务含义直译:

    • A:把给开发者看的报错单改成给顾客看的,但顾客看到的内容仍然不对。
    • B:A 之上再开一张新的翻译清单,今天没人用它。
    • C:没有原料,做不出来。
    • D:店员已经写好的那张便条,终于能递到顾客手里。
    • B′:流程里的拒绝和柜台上的拒绝说同一种话。
    • E:一律只说「出错了,请稍后再试」。

    四轴(业务立场)

    • 实际业务需求:
      • 实测的 9 个带写节点的 screen flow 里,有 8 个没有作者文案(HotCRM 6 个、showcase 1 个、todo 1 个),它们用的就是这条默认路径。
      • 三类拒绝都有真实生产方:平台的唯一约束、作者的校验规则,以及 HotCRM 带标记的 hook。
      • B′ 是唯一能让这三类都对齐记录表单的服务端路线。
    • 项目长远合理性: B′ 在源头修复「结构化拒绝被压扁成字符串」,且只修一次、三门共用;message 仍是诊断通道,符合 ApiErrorSchema 对两个通道的拆分。A 和 B 反过来改写诊断通道。
    • 防 AI 犯错: 出错时,用户看到可执行的原因,开发者在 run 记录里仍看到节点和操作。B′ 只有一个声明过的承载位,控制台不靠正则(C 只能靠正则);AI 写的流程不用另写任何东西,就得到和记录表单一样的行为。
    • 创业阶段不扩散: B′ 不新增错误码、不新增门禁、不新增文案;代价是四个车道和一条裁决。只要最小的一步,可以选 D,它是 B′ 的严格子集,但关不了本卡实测的唯一约束案例。

    推荐:B′,内层 code 用 UNIQUE_VIOLATION 拼写

    • 终态句: 两年后,流程运行里的任何拒绝,都以和记录保存相同的结构化形态(code + 用户文本 + 诊断文本)到达客户端,客户端用一套规则显示。参照:Salesforce 的 DML 错误本身是结构化的(statusCode、message、fields),记录保存与程序调用读同一个结构;本平台的数据门已经如此,流程门是唯一把它压扁的地方。
    • 置信缺口:
      • 真实引擎上的三类拒绝探针没有跑(只读测量不建闭包)。链路靠 18.0.0-next.5 与 main 的字节一致,承接了卡上 HotCRM 的公开门实测。
      • 内层句子本身点名对象 API 名(duplicate-record-error.ts:131-138),任何路线都要靠一张引擎子卡另行处理。
    • 其余选项不取的理由:
      • A:改写诊断通道,且仍点名 API 名。
      • B:新文案没有需求拉动。
      • C:无料可做。
      • D:关不了本卡。
      • E:遮住了用户能据以处理的原因。
      • 应用侧在 HotCRM 给 6 个流程补 errorMessage:属于应用补位;按「平台相关的功能,应该尽量先改平台」(维护者 2026-10-18,台账 6245607810),席位不推荐它作为本卡的修法。
    • 自检: 只看①选 B′;②③④ 是否翻转:否。

    裁后执行(选 B′ 时)

    1. spec 席位: 在 AutomationResult 加可选成员 userMessage 与内层拒绝成员,并在三门的 FLOW_FAILED details 声明一次(仿 ResumeFailureDetailsSchema)。按 Clause-②: yes (widening) 做契约复核。
    2. 本车道:
    3. domain:cli: trigger、resume、/actions、endpoint 四门与 MCP 桥发出新成员,deps.error 增加 userMessage 槽。在 [PM seat] domain:cli · seat 2 — ⏳ vacant #22648 声明。
    4. objectui: FLOW_FAILED 分支依次读作者 errorMessage、userMessage、内层拒绝(套记录表单规则)、message。
    5. 引擎子卡: 唯一约束句不再点名对象 API 名。席位随本决策立卡,见下。

    相关单与 PR

    os-decision-facets

    • ① 项目长远合理性:B′ 收缩特例——拒绝在流程门与数据门以同一结构到达,消除「压扁成字符串」这一处特例;新增一个声明成员,不新增错误码。
    • ② 实际业务拉动:今天撞上的是 HotCRM 线索转换的销售代表;实测 9 个带写节点的 screen flow 中 8 个依赖这条默认路径。
    • ③ 防 AI 犯错:一个声明过的承载位,客户端零正则;出错时用户看到可执行原因,开发者在 run 记录里仍看到节点与操作。
    • ④ 创业阶段不扩散:不新增错误码、门禁与文案;新增的一个 details 成员是永久义务,由 spec 声明一次、三门共用。

    Prior rulings read: FLOW_FAILED details · AutomationResult.code closed · userMessage · UNIQUE_VIOLATION wire → 6 hits (#9378 5321043983, #9384 5321043167, #9446 5322867819, #9585 claim 5339708379, #22507 6093472708, objectui#5210 5344182262); ADR none named by the code comments; thread: none.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 20, 2026

    @objectstack-fleet
    ContributorAuthor

    指针 · domain:services seat 1 · session_011TdZ8GjixNZFLz1xJD9zzK · 2026-10-20T17:34Z:决策 6297041774 裁后执行第 5 步的引擎子卡已立为 #24951(唯一约束拒绝句点名对象 API 名,domain:engine 由分诊定级)。它不随本卡的裁决变化。


    Generated by Claude Code

  5. 8 remaining items

  6. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main abc25a353):


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  8. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    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.error userMessage 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.

    Generated by Claude Code

  9. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25147 comment 6329997478 · domain:services seat 2 · session_01UnHcTADjeg3VwxF21QHj7W · 2026-10-22T00:54Z · contract review PASS 6329711145 · ready and auto-merge set through the relay; the landing note follows the merge.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 2 · session_01UnHcTADjeg3VwxF21QHj7W · 2026-10-22T01:19Z

    Reading Value
    PR #25147 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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:servicespriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions