Repository navigation
feat: hooks and record-triggered flows set the save notice (ctx.notice, save_notice node) - #28753
Conversation
…nslation key Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
…ies, add the save_notice node 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
…tifacts 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
…ype new slot lookups Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
📓 Docs Drift CheckThis PR changes 7 package(s): 32 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 180 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 e4ea437604fa364500a132b2b488cbc171be59e9 && git checkout e4ea437604fa364500a132b2b488cbc171be59e9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d08b859a7177cf16d6190163f2e42efa2d980d3a 36c1fb8f0982c07095b112c8dab0bd2b9343f221 && git checkout -B drift-repro d08b859a7177cf16d6190163f2e42efa2d980d3a && git merge --no-ff 36c1fb8f0982c07095b112c8dab0bd2b9343f221
node scripts/docs-audit/affected-docs.mjs --json d08b859a7177cf16d6190163f2e42efa2d980d3a
|
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
…try and sites Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
…-9kerqb # Conflicts: # packages/spec/dropped-refinements.baseline.json
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5omF1nCbQTGgW1nbRA
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH Contract reviewServed-tier: Inputs read: card #28251 (body; comments 6532349633, 6533331783, 6560220427, 6562102476, 6562118323 — the last is the seat ruling on the dev's open question); PR #28753 body and 63-file list; the net diff ① Derived judgmentsAccept-set and public-surface changes the diff implies, each judged:
② Semver levelChangeset
Clause-②: yes (widening) ③ Boundary flags
④ Design judgmentEach
Lane remainder, read for the judgments above and not reviewed here (TIER_DEFAULT is the lane seat's): should-fix for the lane — No must-fix. Three should-fix items above. Implemented-by: VERDICT: PASS |
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH 席位核对 · ACCEPT · PR #28753 · 2026-11-01T19:16ZHead-sha: 维护者速读(席位终稿)
核对六项
|
domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH
Fixes #28251
Clause-②: yes (widening)
Metadata-protocol: added
维护者速读
ctx.notice(message)设置保存提示,新建/更新的应答里带回notice。save_notice节点,同样设置这次写入的notice。只有一条通道,就是写入自己的onNotice。save_notice只能放在由记录新建/更新触发的流程里,其他地方在发布时就会被响亮拒绝。审批或等待之后恢复的分支、子流程、map 子项在运行时会按名字拒绝。flows.<flow>.notices.<node_id>.message,按写入者的语言渲染。notice在线上仍是纯字符串。hooks.<hook>.notices.<KEY>.message):席位已裁定 A,暂不做(卡上6562118323),等第一个需要本地化提示的钩子再立卡。不需要你拍板。What changes
Hook face:
ctx.notice(message).HookContextSchemagains an optionalnoticemember (HookNoticeSetter). ObjectQL'striggerHooksbinds it on everybeforeInsert/afterInsert/beforeUpdate/afterUpdatedispatch (packages/objectql/src/hook-notice.ts). Every dispatch site therefore has it, and no site can forget it. A call reads the write's ownonNoticelistener offctx.input.optionsat call time and hands it the message. A write that passed no listener sets nothing. A message that is not a non-empty string throws aTypeErrornaming the call, so the write fails loudly instead of silently dropping the notice. Delete and read dispatches carry no member. The body runner passes the member through, and the QuickJS runner installs it as a host function, so shipped bodies (and handlers the build lowers into bodies) can call it. It needs no capability.Flow face: the
save_noticenode.FlowNodeActiongainssave_notice, and its config isSaveNoticeConfigSchema({ message }, one{{ }}text slot). The record-change trigger already runs the flow inside the write that fired it. It now hands the run that write's channel (AutomationContext.saveNotice, wrappingctx.notice) and closes the channel when the handler returns. The executor renders the message and sets it.FlowSchemaparse refuses the node at any region depth unless the flow's start is a create or update record trigger (save-notice-placement.ts). The refusal isINVALID_METADATA422 atnodes.<i>.type.execute, or a subflow or map child (both delete the channel from the child context). It also refuses when the channel is closed, which covers a leg resumed in memory after the write answered.Why a node, not a member of an existing node (measured). The flow that asked for this (HotCRM's deal submission, #27380 row 3) sends the record for approval. Its graph has no record-write node before the
approvalnode that suspends the run, so a member onupdate_recordwould have nowhere to go. A member onendwould run only after the approval, on a resumed leg, when the write has already answered. A dedicated node is the smallest surface that can stand before the suspension.Message form (Wanted 3). The platform's translatable-message shape is a catalog key that the server renders in the reader's locale (
flows.<flow>.refusals.<node_id>.message,hooks.<hook>.refusals.<CODE>.message). The flow notice uses that shape:flows.<flow>.notices.<node_id>.message(flowNoticeMessageKey), picked in the writer's locale before the holes are filled.os i18n extractscaffolds the key, the coverage gate demands it, andobjectstack validaterefuses a key over an unknown flow or node or a non-save_noticenode. So the wirenoticestays a plain string, and no widening ofCreateDataResponseSchema.notice/UpdateDataResponseSchema.noticeis needed. A hook-side catalogue is left to a maintainer decision (see the report).Other surfaces.
flows.noticesislive, cited to its reader.save_notice, which binds nothing.showcase_task_completed(with zh-CN).Why
#28247 added
noticeto the create and update answers, but only an in-process caller passingonNoticecould set it. Shipped hook bodies and flows had no way in, so HotCRM faked the sentence with arecord:alertbanner.Verification
pnpm --filter @objectstack/example-showcase dev -- --fresh). The probe installedapp.probe1, saved an object, a hook whose body callsctx.notice, and arecord-after-updateflow with asave_noticenode, then created and updated a record.4bf48a8c(spec, objectql, runtime, trigger and automation rebuilt from base):POST→ 201 with nonotice;PATCH→ 200 with nonotice. Asave_noticeflow underrecord-after-deletewas saved, 200.POST→ 201"notice":"Sent Acme for approval";PATCH→ 200"notice":"Moved Acme to review"; the delete-trigger flow → 422INVALID_METADATA,issues[0].pathnodes.1.type.spec/src/automation/save-notice-node.test.ts(17)objectql/src/protocol-save-notice-answer.test.ts, the Save notice from a sandboxed hook or a flow: hook context and flow node set the write answer's notice #28251 block (8 of 22)runtime/src/sandbox/hook-notice.integration.test.ts: real ObjectQL, SqlDriver and a QuickJS bodytrigger-record-change:save-notice-channel.test.ts(3), plus 2 cases inrecord-change-integration.test.tsservice-automation/src/builtin/save-notice-node.test.ts(7, real in-memory i18n)lint/.../validate-translation-references.test.ts(3)cli/test/i18n-flow-save-notice-coverage.test.ts(6)scripts/ablation-replace.mjs). Each one below was broken in turn:bindHookNoticecalldispatch-gates.mjs --commands: 116 derived;--ranreconciles 115 run and 1 NOT MEASURED (check:dual-build-cjs-loads, exit 3, needs every package's dist).eslintover the seven touched packages: exit 0.Acceptance notes
after*hook that resolves after the write answered is dropped, because the answer has already gone. Await the work, or set the notice in abefore*hook.ctx.notice(42)in a body fails the write as a hook body fault, the same way any otherTypeErrorin a body does.ui/.../flow-canvas-parts.tsxNODE_PALETTE) does not listsave_noticeyet. The node still saves and runs, and the actions feed carries its descriptor.record:alertworkaround once this lands. That belongs to the hotcrm lane; this PR does not touch hotcrm.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code