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 business refusal from a flow reaches the toast as the engine chain — "Flow 'renewal_start' failed: Node 'refuse_existing' failed: script function 'clm_backfill_refuse' …" #156
Filing gate: ① user-visible defect, reproduced in the browser on @objectstack 18.2.0 (hotclm PR #155 branch).
Symptom. A requester presses 发起续签 a second time on an active contract that already has a renewal draft. The refusal is right, but the toast reads, verbatim:
Flow 'renewal_start' failed: Node 'refuse_existing' failed: script function 'clm_backfill_refuse' (node 'refuse_existing') failed: This contract already has a renewal draft (NDA-2026-0025). Open that one instead of starting a second.
The same shape was on screen under 18.1 (hotclm-regression-2026-10-26/r39-req1-renew-2.png), so this is not an 18.2 regression; 18.2's console change (objectstack#25985) reads userMessage and innerRefusal, and a throwing script node carries neither, so the console falls back to the run's raw message.
Where. Every hotclm refusal thrown from a registered function: clm_backfill_refuse (renewal_start refuse_existing, executed_upload refuse_date / refuse_party, renewal_notice) and clm_intake_refuse (contract_intake). The 400 FLOW_FAILED envelope carries no userMessage and no details.innerRefusal.
Expected. The toast reads only the sentence the app wrote: "This contract already has a renewal draft (NDA-2026-0025). Open that one instead of starting a second."
Route to measure first. The platform has a first-class refusal, an end node with outcome: 'refused' and an interpolated message (objectstack#15788, translation key since #22450). The header of src/flows/executed-upload.flow.ts says a throwing script is "the only node that can end a run with a message the caller actually receives"; that predates #15788/#22450. Measure whether an endoutcome: 'refused' run answers the action door with a toast-ready message. If it does, the fix is metadata in hotclm. If it does not, it is a platform issue (the action door does not surface a refused run's message), filed in objectstack and cross-linked here.
Business reason. Every requester who presses a guarded button sees node ids, a function name and the word "failed" in English for an ordinary business rule; it reads as a crash.
Reader. hotclm execution seat (PM seat #36), next dispatch round.
Duplicate search. hotclm issues, all states, "refusal toast / script function / Flow failed": 0 hits. objectstack: #24914 (write-node refusal, closed, different node), #25985 (console reads userMessage, closed).
Filed from the 18.2.0 browser pass (hotclm PR #155, landed as 21f2319). Defect, user-visible, reproduced: accepted.
Route: app-side first. Measure whether an end node with outcome: 'refused' (objectstack#15788, translation key since #22450) answers the action door with a toast-ready message on 18.2.0. If it does, the hotclm refusals move to it, and the header of src/flows/executed-upload.flow.ts is corrected. If it does not, the dev files the gap in objectstack with the measurement, and this card goes pm:blocked on it. No app-side workaround.
Claim: PM loop round 17
Ruling-ref: none (triaged defect, comment 6437433679)
Session: session_017nzWFRUo2U7evMkPdVniAn
Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-156-flow-refusal-toast
Worktree: hotclm-issue-156
Domain: repo:hotclm (single-lane sister repo — no domain:*)
Seat: repo:hotclm#1
File surface: src/flows/** (the refusal nodes of renewal_start, executed_upload, renewal_notice, contract_intake) · the registered refusal functions (clm_backfill_refuse, clm_intake_refuse) and their registration · flow translations — measure first; on a platform gap stop and report (the seat files it)
Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo), effort: medium
Clause-②: no
Metadata-protocol: none
Responsibility: this app's flow refusals (what a user reads when a flow says no) | possibly a platform gap | every user refused by renewal, backfill or intake
Thread-read: 6437433679
Premise: re-measured by the dev on origin/main37369b9 (@objectstack 18.2.0) with README personas first
Serial constraints cleared: #127 landed (PR #166); in flight #158 (lifecycle actions), #31 (datasets), #157 (hook display_name) — disjoint files; #99 and #43 (also flows) wait for this card.
{
"issue": 156,
"status": "done",
"branch": "claude/issue-156-flow-refusal-toast",
"pr": "https://github.com/objectstack-ai/hotclm/pull/170",
"session": "session_017nzWFRUo2U7evMkPdVniAn (subagent of the PM seat)",
"premise_still_valid": true,
"summary": "Measured first on renewal_start: on @objectstack 18.2.0 an end node with outcome 'refused' and a {{ }} message answers the action door 200 {data:{success:true,status:'refused',refusalMessage}} rendered in the caller's locale. The run is recorded 'refused' and the action's successMessage is suppressed. The console (judgeFlowLaunch refused arm) shows a Close-only notice titled with the action label, whose body is only the app's sentence (zh-CN and en, measured). So there is no platform gap. Every hotclm refusal moved onto refused ends with the same wording and interpolation: renewal_start refuse_existing; executed_upload refuse_date_missing/_invalid/_future plus refuse_party; contract_intake refuse_no_type, refuse_type, refuse_party_missing, refuse_blocked plus refuse_blocked_no_note. Computed variants became separate ends so each has a flows.FLOW.refusals.NODE.message key in zh-CN and en. clm_backfill_refuse and clm_intake_refuse are removed, and the executed-upload and contract-intake headers are corrected. The card says 'toast'; the platform's ruled surface is a Close-only notice, not a toast.",
"tests": "pnpm demo:zh on 18.2.0, port 3148, fresh .objectstack, before origin/main 37369b9 and after ed589f2. README personas: create-user, change-password, sys_user_position, set-user-manager, sharing re-evaluate, demo re-run. Playwright on /opt/pw-browsers/chromium, signed in as each persona. (1) BR1 presses 发起续签 twice. Before: press 2 answers 400 FLOW_FAILED with a toast of the engine chain. After: press 2 answers 200 status refused with the notice 发起续签 · 这份合同已有续签草稿(LSE-2026-0007)。请打开那一份,不要再发起第二份。· 关闭, and in en 'Start Renewal · This contract already has a renewal draft (ORD-2026-0017). Open that one instead of starting a second. · Close'. No toast, and the renewal count stays at 1. (2) BR1 launches a contract while Legal Counsel 1 flags the counsel-owned party blocked. Before: 400 FLOW_FAILED with the chain in the toast and inline. After: 200 refused with the notice 相对方 … 已被标记为「禁止合作」,不能用于新合同:NOTE (zh-CN), the no-note variant, and en; the owned contract count is unchanged. The clean-party control still pauses on the 合同信息 wizard. (3) Through the action door, headless, as BR1: unknown type and unknown party answer 200 refused in zh-CN and en; no type is refused 400 by the param check, before and after alike. (4) Run history after: renewal_start refused x2, completed x2; contract_intake refused x3, paused x1; no failed. (5) Reverse check: a refusals key on a get_record node turns validate red (translation-target-unknown), restored by git checkout HEAD, git diff HEAD empty. (6) executed_upload: NOT MEASURED. Records Manager 1 is refused 403 'This caller may not start this flow…' before and after, so no node runs and 0 rows are written; that is #43's start check. No ablation: no test suite pins these flows.",
"gates": "On ed589f2, exit captured before any pipe: pnpm install 0; pnpm validate 0 (✓ Validation passed, 7 ⚠, the same set origin/main 37369b9 prints); pnpm lint 0 (6 suggestions, 0 warnings, the same as main 37369b9, measured in a temporary worktree; the common.md 'one warning' is no longer on main); pnpm typecheck 0; pnpm lint:i18n-gate 0 (0 missing keys across 2 locales); CLM_DEMO_SEED=1 pnpm validate 0. The PR's CI check-runs are in_progress at report time.",
"dogfood_battery": "n/a — hotclm, no ! in the title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 relay writes: pr_create objectstack-ai/hotclm#170 (fleet-write run 40487923063) and this os-dev-report comment on #156. Plus git push (not REST). Local demo DB writes only on throwaway port 3148.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #43 — 补录已签合同 (executed_upload, autolaunched + runAs system) is still refused 403 'This caller may not start this flow' for Records Manager 1 on 18.2.0. Re-measured here before and after; it blocks reading the backfill refusals in the browser. Noted, not filed.",
"class: observation (not filed) · the dev admin's PATCH clm_party.risk_flag answers 403 'Field write denied … [risk_flag]', and Legal Counsel 1's PATCH on a seeded (admin-owned) party answers 403 'You do not have access to this record'. Both match #152's owner scope and the field model. carrier: #152.",
"class: observation (not filed) · in the en console the list toolbar does not show the 'Launch Contract' button that zh-CN shows (发起合同 appears twice in zh-CN; en shows only the nav item 'Launch a Contract'). Read from one button listing only, not reproduced further. carrier: none — Acceptance notes only if the seat wants it."
],
"governed_text_proposal": "none — no DESIGN.md/AGENTS.md text depends on this; DESIGN.md §06 does not name the refusal mechanism."
}
Release: when renewal, backfill or launch refuses, the user sees only the app's sentence, in their language, instead of the engine chain. The backfill refusals are re-read in the browser once #43 lets the records manager start that flow. State: pm:dispatched removed, assignee cleared.
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage — 2026-10-27
Filed from the 18.2.0 browser pass (hotclm PR #155, landed as
21f2319). Defect, user-visible, reproduced: accepted.Route: app-side first. Measure whether an
endnode withoutcome: 'refused'(objectstack#15788, translation key since #22450) answers the action door with a toast-ready message on 18.2.0. If it does, the hotclm refusals move to it, and the header ofsrc/flows/executed-upload.flow.tsis corrected. If it does not, the dev files the gap in objectstack with the measurement, and this card goespm:blockedon it. No app-side workaround.State: →
pm:queue+bug.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 17
Ruling-ref: none (triaged defect, comment 6437433679)
Session:
session_017nzWFRUo2U7evMkPdVniAnAccount:
zhuangjianguo(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-156-flow-refusal-toastWorktree:
hotclm-issue-156Domain:
repo:hotclm(single-lane sister repo — nodomain:*)Seat:
repo:hotclm#1File surface:
src/flows/**(the refusal nodes of renewal_start, executed_upload, renewal_notice, contract_intake) · the registered refusal functions (clm_backfill_refuse,clm_intake_refuse) and their registration · flow translations — measure first; on a platform gap stop and report (the seat files it)Container & model:
S,mode:subagent,model: opus(TIER_DEFAULT; hand-derived for a sister repo),effort: mediumClause-②: no
Metadata-protocol: none
Responsibility:
this app's flow refusals (what a user reads when a flow says no) | possibly a platform gap | every user refused by renewal, backfill or intakeThread-read: 6437433679
Premise: re-measured by the dev on
origin/main37369b9(@objectstack 18.2.0) with README personas firstSerial constraints cleared: #127 landed (PR #166); in flight #158 (lifecycle actions), #31 (datasets), #157 (hook display_name) — disjoint files; #99 and #43 (also flows) wait for this card.
Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 156, "status": "done", "branch": "claude/issue-156-flow-refusal-toast", "pr": "https://github.com/objectstack-ai/hotclm/pull/170", "session": "session_017nzWFRUo2U7evMkPdVniAn (subagent of the PM seat)", "premise_still_valid": true, "summary": "Measured first on renewal_start: on @objectstack 18.2.0 an end node with outcome 'refused' and a {{ }} message answers the action door 200 {data:{success:true,status:'refused',refusalMessage}} rendered in the caller's locale. The run is recorded 'refused' and the action's successMessage is suppressed. The console (judgeFlowLaunch refused arm) shows a Close-only notice titled with the action label, whose body is only the app's sentence (zh-CN and en, measured). So there is no platform gap. Every hotclm refusal moved onto refused ends with the same wording and interpolation: renewal_start refuse_existing; executed_upload refuse_date_missing/_invalid/_future plus refuse_party; contract_intake refuse_no_type, refuse_type, refuse_party_missing, refuse_blocked plus refuse_blocked_no_note. Computed variants became separate ends so each has a flows.FLOW.refusals.NODE.message key in zh-CN and en. clm_backfill_refuse and clm_intake_refuse are removed, and the executed-upload and contract-intake headers are corrected. The card says 'toast'; the platform's ruled surface is a Close-only notice, not a toast.", "tests": "pnpm demo:zh on 18.2.0, port 3148, fresh .objectstack, before origin/main 37369b9 and after ed589f2. README personas: create-user, change-password, sys_user_position, set-user-manager, sharing re-evaluate, demo re-run. Playwright on /opt/pw-browsers/chromium, signed in as each persona. (1) BR1 presses 发起续签 twice. Before: press 2 answers 400 FLOW_FAILED with a toast of the engine chain. After: press 2 answers 200 status refused with the notice 发起续签 · 这份合同已有续签草稿(LSE-2026-0007)。请打开那一份,不要再发起第二份。· 关闭, and in en 'Start Renewal · This contract already has a renewal draft (ORD-2026-0017). Open that one instead of starting a second. · Close'. No toast, and the renewal count stays at 1. (2) BR1 launches a contract while Legal Counsel 1 flags the counsel-owned party blocked. Before: 400 FLOW_FAILED with the chain in the toast and inline. After: 200 refused with the notice 相对方 … 已被标记为「禁止合作」,不能用于新合同:NOTE (zh-CN), the no-note variant, and en; the owned contract count is unchanged. The clean-party control still pauses on the 合同信息 wizard. (3) Through the action door, headless, as BR1: unknown type and unknown party answer 200 refused in zh-CN and en; no type is refused 400 by the param check, before and after alike. (4) Run history after: renewal_start refused x2, completed x2; contract_intake refused x3, paused x1; no failed. (5) Reverse check: a refusals key on a get_record node turns validate red (translation-target-unknown), restored by git checkout HEAD, git diff HEAD empty. (6) executed_upload: NOT MEASURED. Records Manager 1 is refused 403 'This caller may not start this flow…' before and after, so no node runs and 0 rows are written; that is #43's start check. No ablation: no test suite pins these flows.", "gates": "On ed589f2, exit captured before any pipe: pnpm install 0; pnpm validate 0 (✓ Validation passed, 7 ⚠, the same set origin/main 37369b9 prints); pnpm lint 0 (6 suggestions, 0 warnings, the same as main 37369b9, measured in a temporary worktree; the common.md 'one warning' is no longer on main); pnpm typecheck 0; pnpm lint:i18n-gate 0 (0 missing keys across 2 locales); CLM_DEMO_SEED=1 pnpm validate 0. The PR's CI check-runs are in_progress at report time.", "dogfood_battery": "n/a — hotclm, no ! in the title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "2 relay writes: pr_create objectstack-ai/hotclm#170 (fleet-write run 40487923063) and this os-dev-report comment on #156. Plus git push (not REST). Local demo DB writes only on throwaway port 3148.", "open_questions": [], "out_of_scope_findings": [ "carrier: #43 — 补录已签合同 (executed_upload, autolaunched + runAs system) is still refused 403 'This caller may not start this flow' for Records Manager 1 on 18.2.0. Re-measured here before and after; it blocks reading the backfill refusals in the browser. Noted, not filed.", "class: observation (not filed) · the dev admin's PATCH clm_party.risk_flag answers 403 'Field write denied … [risk_flag]', and Legal Counsel 1's PATCH on a seeded (admin-owned) party answers 403 'You do not have access to this record'. Both match #152's owner scope and the field model. carrier: #152.", "class: observation (not filed) · in the en console the list toolbar does not show the 'Launch Contract' button that zh-CN shows (发起合同 appears twice in zh-CN; en shows only the nav item 'Launch a Contract'). Read from one button listing only, not reproduced further. carrier: none — Acceptance notes only if the seat wants it." ], "governed_text_proposal": "none — no DESIGN.md/AGENTS.md text depends on this; DESIGN.md §06 does not name the refusal mechanism." }objectstack-fleet commentedon Oct 27, 2026
Ruling-ref: none (triaged defect, comment 6437433679)
Landed — PR #170 · 2026-10-27
MERGED(squash onmain), merged at its ACCEPT headed589f2;Fixes #156closes this cardRelease: when renewal, backfill or launch refuses, the user sees only the app's sentence, in their language, instead of the engine chain. The backfill refusals are re-read in the browser once #43 lets the records manager start that flow. State:
pm:dispatchedremoved, assignee cleared.Generated by Claude Code