Repository navigation
fix: save_notice follow-ups: name the array triggerType, pin the persisted approval round trip, classify the node in flow lint - #28805
Conversation
…nt refusal Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
…ode lists Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
A save_notice before an approval answers the triggering create; the run suspends into sys_automation_run without the channel's function, and a save_notice on the leg the decision resumes from storage refuses by name. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
📓 Docs Drift Check4 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 30344b92cd008d8e421e7e27fd22bdf064634113 && git checkout 30344b92cd008d8e421e7e27fd22bdf064634113
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c064d3a24610508918ccefd1e0a94b6a759448d9 6f15ad27f7406f90547af738b90b99797147bbb3 && git checkout -B drift-repro c064d3a24610508918ccefd1e0a94b6a759448d9 && git merge --no-ff 6f15ad27f7406f90547af738b90b99797147bbb3
node scripts/docs-audit/affected-docs.mjs --json c064d3a24610508918ccefd1e0a94b6a759448d9 |
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: Inputs read: card #28786 (body; comments 6562950408 triage, 6562970286 claim, 6563740409 os-dev-report); PR #28805 body and 6-file list; the net diff ① Derived judgments
② Semver level
Clause-②: no ③ Boundary flags
④ Design judgment
No finding on the hits file. Lane remainder, read for ③ and not reviewed here: the integration test and the lint rows hold against the code they cite (①3, ③). One note for the lane seat: the test's header says a store that "started keeping (or re-binding) the channel" is caught here — true for No must-fix, no should-fix. One escalation (③): file the authoring-trap lint finding as its own card. Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #28805 · 2026-11-01T21:15ZHead-sha: 维护者速读(席位终稿)
核对六项
|
domain:server#3 · os-project-manager · session_01FEkQ5omF1nCbQTGgW1nbRA
Fixes #28786
Clause-②: no
Metadata-protocol: none
维护者速读
save_notice)合并后审查 6562490492 提出的三条 should-fix,只补措辞、测试和 lint 分类,契约、放置判定和线上的notice都没变。triggerType(比如['record-after-create', 'record-after-update'])仍然被拒,但拒绝文案现在会照原样引出这个数组,说明这种写法不支持、永远不会绑定,并让作者只写一个触发类型('record-after-write'同时覆盖新建和更新)。sys_automation_run持久化往返:审批之前的save_notice让新建应答带回了提示;审批决定后从存储恢复的分支上,第二个save_notice按名字被拒。实测行为与推断一致,没有发现缺陷。save_notice:写在它上面的condition会提示无效,放在没有try_catch的循环体里会提示可能失败。What changes
Spec: the array trigger form is named (
save-notice-placement.ts). Asave_noticenode in a flow whose start writestriggerTypeas an array was refused with "This flow's start has no record trigger", which misnames the shape. The verdict is unchanged, and the refusal now says what the author wrote:A string trigger and a missing trigger keep their existing sentence.
Coverage: the persisted approval round trip (
plugin-approvals/src/save-notice-approval-round-trip.integration.test.ts). It uses a real kernel:ObjectQLPluginoverSqlDriver(better-sqlite3),AutomationServicePluginwith its default durable store (ObjectStoreSuspendedRunStoreoversys_automation_run), andRecordChangeTriggerPlugin. The realApprovalServiceruns behind the real approval node. The flow isrecord-after-create→save_notice→approval→ (approve)save_notice→ end. The test reads:onNoticeheard "Sent Acme to your manager for approval".pausedrow at the approval node. Itscontext_jsonholds nosaveNotice.set: JSON drops the function.save_noticeis refused by name:save_notice 'tell_after': this run holds no record write to answer. The run's history row isfailedwith that error, and the listener heard nothing more.decideanswers the approvals' existing stranded-decision failure ([automation/approvals] 进程重启后审批决策静默失效:挂起 flow run 仍只存内存(#1518 标记 COMPLETED 但 17.0.0-rc.1 未生效),approve 落库却永不推进且零报错 #4420). That failure names the refusal and carriesstrandedDecisionDetails{ finalized: true, decision: 'approve', runId }. This is existing behaviour, read here, not a new refusal.Lint:
save_noticein the node-type lists (lint-flow-patterns.ts), classified by reading its executor, as the lists' headers ask:INERT_CONDITION_NODE_TYPES: the executor parsesmessageonly and never readscondition.FALLIBLE_NODE_TYPES: it returnssuccess: falseon data alone. That happens when the run holds no write (a resumed leg, a subflow or map child), when the channel is already closed, or when every hole of the message rendered empty (save-notice-node.ts:67, :94, :104).Why
These are the three should-fixes of review 6562490492 on #28753.
Verification
spec/src/automation/save-notice-node.test.ts) was 1 failed of 18 before the fix and 18/18 after.lint/src/lint-flow-patterns.test.ts: inertconditionon asave_notice, andsave_noticein the fallible-builtin list) were 2 failed before and 245/245 after.refuseNode(...)withreturn { success: true }. The round-trip test went red:decideno longer threw, so "expected 'undefined' to contain 'save_notice 'tell_after': this run …'". I restored it and checked it was byte-identical by blob (ea8be53d), and the test was green again. The test resolves@objectstack/service-automationthrough its builtdist/, so I ablated the built file the test runs.dispatch-gates.mjs --commandsderived 84;--ranreconciles 84 run, 0 NOT-MEASURED, 0 UNRUN. Five gates first exited 3 (prerequisite not met): four spec gates read a specdistolder than a later test edit, andcheck:dual-build-cjs-loadsneeds every package'sdist. I rebuilt them and reran all five: exit 0.vitest runof@objectstack/spec,@objectstack/lintand@objectstack/plugin-approvals: exit 0 each.eslint --no-inline-configoverpackages/spec,packages/lintandpackages/plugins/plugin-approvalsexits 0.Acceptance notes
save_noticeplaced after an approval in a record-triggered flow, the parse still accepts the flow, because it cannot see where a run suspends. The approver's decision is recorded, and the resumed run fails at the node. The decision answers the stranded-decision failure, which names the node. This PR pins that reading and does not change it.ui/**and no hotcrm changes.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: no generated artifact moves (the change is refusal text only).content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code