Repository navigation
fix(automation): a flow run's answer names the read-only fields its write steps dropped (#32861) - #33007
Conversation
… dropped A record button whose flow wrote a read-only field answered a plain success: the step's drop events stopped at the run log. The terminal answer now carries them as AutomationResult.droppedFields (the data engine's own DroppedFieldsEvent, coalesced, omit-when-empty), mirrored on TriggerFlowResponseSchema; a subflow child's drops ride up. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwHRkD8EXD38KeqzFh
…docs and changeset Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwHRkD8EXD38KeqzFh
…door tests Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwHRkD8EXD38KeqzFh
📓 Docs Drift CheckThis PR changes 2 package(s): 26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 171 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 8021368beaa6358fdd26c752ebf8f1cf8058861c && git checkout 8021368beaa6358fdd26c752ebf8f1cf8058861c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e728d7a122fd68e1a3b1a4df27b07b4b13b2295a 3203683dc66c57c0425d74ab484b534ee7e07e4a && git checkout -B drift-repro e728d7a122fd68e1a3b1a4df27b07b4b13b2295a && git merge --no-ff 3203683dc66c57c0425d74ab484b534ee7e07e4a
node scripts/docs-audit/affected-docs.mjs --json e728d7a122fd68e1a3b1a4df27b07b4b13b2295a
|
|
domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J Contract-review-claim: session_016WV4cwqTK71kjQzHae8T5J · head 5cee1bb · 2026-11-13T03:58Z |
|
domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J Contract reviewServed-tier: Widened, read and graded because the brief asked: the four test files, Check-runs on this head, read 2026-11-13T04:12Z: 32 success, 10 skipped, 24 in_progress (Test Core 1 to 8, Test UI 1 to 16), 0 failed. Every type-check, lint, build, ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentmust-fix: none. What holds, read in the tree
should-fix S1 (both hits,
should-fix S2 (pin): the retried-attempt exit is unpinned.
nit N1: the fold runs after
nit N2: the step-log member reaches a published wire undeclared.
nit N3: "Set on a terminal success only" can be read to include nit N4: the union reading is unstated. A name means at least one write dropped it, not every write. nit N5: the failure-arm copy in nit N6: the pause pin runs on Implemented-by: VERDICT: PASS |
…built; pin the retried-attempt exit Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwHRkD8EXD38KeqzFh
…elds sentence Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwHRkD8EXD38KeqzFh
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwHRkD8EXD38KeqzFh
|
domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J Contract-review-claim: session_016WV4cwqTK71kjQzHae8T5J · head 3203683 · 2026-11-13T04:51Z |
|
domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J Contract reviewServed-tier: Delta review of record 6843734657 (PASS on Check-runs on this head, read 2026-11-13T04:56Z: 26 success, 12 skipped, 31 in_progress, 0 failed.
Check-runs on the round-1 head ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentmust-fix: none. should-fix: none. S1 closed. Each new sentence on the two hits is true of the code.
S2 closed.
The files the delta leaves untouched stand as reviewed.
nit (new, pin): the retry case cannot tell attempt 2's drop from attempt 1's. Both attempts drop the same field, while the title says "from the attempt that completed". The exit is what S2 asked to pin and it is pinned. Accepted as written. Implemented-by: VERDICT: PASS |
|
domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J ACCEPT · PR #33007 · head 席位核对
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 43371288757 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J Merge-queue kick · PR #33007 · head
|
Requested by Elon · project thread
Part of #32861
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
droppedFields:流程里的写入步骤因为字段只读(或按状态锁定、公式字段等)而没有写进去的字段,按对象和原因列出来。记录按钮(type: 'flow'的动作)、MCPrun_action、流程触发路由都把它原样带给调用方。运行结果本身不变:仍是成功,summary不变;没有字段被丢时这个成员不出现。status,服务器只在日志里警告,按钮答「成功」,员工什么也看不到,单子还是草稿。实测丢失发生在引擎组装运行结果这一层:步骤日志里有警告文字,但运行结果没有对应成员,所以任何门都带不出去。AutomationResult与TriggerFlowResponseSchema同步加)。不读它的调用方不受影响。同一字段在循环里被丢 30 次只算一条,回答大小不随运行长度增长。回滚即 revert。packages/services/service-automation/src/run-dropped-fields.ts(折叠规则:按对象+原因合并);engine.ts/run-resume.ts/dag-traversal.ts三个终态成功出口各一行...withDroppedFields(steps);builtin/subflow-node.ts(子流程的丢弃上卷到父流程)。pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/run-dropped-fields.test.ts,应见Tests 8 passed (8)。domain:ui);暂停过的子流程与map子运行的上卷是 automation: a flow's answer carries the dropped-field fact of its map items and of a subflow child that paused and resumed (delegated half of #32861) #33019。两张都落地后 runtime: a record-button flow whose update step drops a read-only field answers success and the console says nothing; tell the person who clicked that the step did not take effect #32861 才关,所以写的是Part of。What changes
Measured first (origin/main 04c35f0). The
update_record/create_recordstep already reports a strip:warnings(sentences) on the step,droppedFields(bare names) on its output, both on the step log since #3407. Traced to the caller:StepLogEntry.warnings) and run historyAutomationResult, built at the engine's terminal exits)dispatchFlowAction(packages/runtime/src/action-execution.ts)restAnswerOf:deps.success(result))run_action(invokeBusinessAction:result)So the layer that drops it is the engine's run result, and that is the only place this PR changes behaviour. The click on #32666's shape answered
{ success: true, flowLabel, summary }with the record stilldraft(the verify pin below, ablated, reproduces that answer).The carry.
NodeExecutionResult.droppedFields/StepLogEntry.droppedFields(service-internal): the write node hands back the engine'sDroppedFieldsEvents it already collected fromonFieldsDropped, beside thewarningsit already wrote;dag-traversal.tscopies them onto the step, likewarnings.run-dropped-fields.ts#dropEventsOf: a fold over the flat step log, likesummarizeRun. One event per(object, reason),fieldsthe union in first-seen order. Returnsundefinedwhen nothing was dropped.withDroppedFields(steps):execute()(engine.ts), a retried attempt (dag-traversal.ts), and a resumed run (run-resume.ts, over the whole run, so a drop before ascreenpause reaches the answer the run finally gives).subflownode: a child that completes without pausing hands itsdroppedFieldsup onto the parent's step, so the parent's answer carries them. A child that paused, and amapitem's run, do not roll up yet (automation: a flow's answer carries the dropped-field fact of its map items and of a subflow child that paused and resumed (delegated half of #32861) #33019); both published sentences (theTriggerFlowResponseSchemadescribe and theAutomationResultjsdoc) state that rule as built, and that a field named means at least one write dropped it.The contract.
AutomationResult.droppedFields?: DroppedFieldsEvent[](packages/spec/src/contracts/automation-service.ts) and its mirrorTriggerFlowResponseSchema.data.droppedFields(z.array(DroppedFieldsEventSchema).optional()); the compile-time guardTriggerFlowDataMatchesContractbinds the two. The element is the data engine's ownDroppedFieldsEventSchema, so no new vocabulary. The regenerated reference pagecontent/docs/references/api/automation-api.mdxgains the one row.Unchanged.
success,status,summary, the stepwarningsand the step output'sdroppedFields(names) all read as before. A refused run, a failed run and a paused answer carry nodroppedFields.Tests
packages/services/service-automation/src/run-dropped-fields.test.ts(8): the A runtime-authored flow that writes a readonly field as the user publishes clean; the button runs, reports success and changes nothing (flow-update-readonly-field is not run at the save/publish gate) #32666 shape (readonlystatus,update_record) answersdroppedFields: [{ object, fields: ['status'], reason: 'readonly' }]withsuccess: trueandsummary.failed: 0; a write that drops nothing answers without the member ('droppedFields' in res === false);create_recorddrops; a drop before a pause reaches the resumed answer (the paused answer carries none); undererrorHandling.strategy: 'retry'(attempt 1 fails, attempt 2 completes) the answer from the retried-attempt exit carries the drop; asubflowchild that completes without pausing rolls its drop up; the fold coalesces thirty identical drops into one fact; an empty log folds toundefined.packages/verify/src/action-flow-dropped-fields.test.ts(2), on the real kernel throughactions.run(the in-processPOST /actions/:object/:action): the record button on A runtime-authored flow that writes a readonly field as the user publishes clean; the button runs, reports success and changes nothing (flow-update-readonly-field is not run at the save/publish gate) #32666's shape answersdroppedFieldsnamingstatus, the record staysdraftand the writablenotebeside it lands; a button whose write drops nothing answers without the member.packages/runtime/src/action-flow-dropped-fields-relay.test.ts(4): both entrances of the action door (REST actions route, MCPrun_action) relaydroppedFieldsand omit it when absent.packages/spec/src/api/automation-api.zod.test.ts(+1): the member survivesTriggerFlowResponseSchema.parseand an off-vocabularyreasonis refused.Ablation (the carry stripped: withDroppedFields returns an empty object)
Committed first (
e0e5b6dd), then mutated throughscripts/ablation-replace.mjs(anchorreturn droppedFields ? { droppedFields } : {};x1 -> x0, blob127c6147->172b361b),service-automationrebuilt,ablation-dist-preflight --absent: marker absent from all 6 built files.run-dropped-fields.test.ts:Tests 4 failed | 3 passed (7): the A runtime-authored flow that writes a readonly field as the user publishes clean; the button runs, reports success and changes nothing (flow-update-readonly-field is not run at the save/publish gate) #32666 shape,create_record, resume and subflow cases red; the two fold cases and the drop-nothing case stay green (they do not read the exit).action-flow-dropped-fields.test.ts(verify, throughdist/):Tests 1 failed | 1 passed (2): the A runtime-authored flow that writes a readonly field as the user publishes clean; the button runs, reports success and changes nothing (flow-update-readonly-field is not run at the save/publish gate) #32666 click red, the drop-nothing click green.Restore: blob after restore
127c6147== HEAD,git diff HEADempty; rebuilt, preflight marker present in 2 built files; verifyTests 2 passed (2).Single-exit ablation of the retried-attempt exit (
db40c4a8, the spread removed fromdag-traversal.tsexecuteWithoutRetryalone; anchor x1 -> x0, blobaca7bf1a->7d968703):run-dropped-fields.test.tsTests 1 failed | 7 passed (8), the retry case alone red. Restored: blob == HEADaca7bf1a,git diff HEADempty.Acceptance notes
droppedFieldsentry (isWireDroppedFieldsEntry,asDroppedFieldsNotice, the skew arm for an unknownreason) is private toui/packages/data-objectstack; the console's flow launch (ui/packages/app-shell/src/utils/flowLaunch.ts) does not go through the adapter, so it needs that parser exported or a second copy;writeWarningToast.emitWriteWarningneeds the adapter (field labels), which neither launch host (hooks/useConsoleActionRuntime.tsx,views/RecordDetailView.tsx) holds today;screennode delivers the drop on the resume answer, whichviews/FlowRunner.tsxreads, a third reader.The answer already carries everything the toast needs (object, fields, reason); the existing
detail.writeStripped*wording fits it.subflowchild that PAUSES and is later resumed (through the parent's resume, which delegates down, or the child's own) does not put its drops on the parent's answer; only a child that completes synchronously does. The same holds for everymapitem's run. Carried on automation: a flow's answer carries the dropped-field fact of its map items and of a subflow child that paused and resumed (delegated half of #32861) #33019 (with the fold hardening, review nit N1); the undeclared step-log members (steps[].warnings,steps[].droppedFieldsonExecutionStepLogSchema, review nit N2) on spec: declare warnings and droppedFields on ExecutionStepLogSchema (run history serves both undeclared) #33020.status: 'refused') and a failed run carry nodroppedFields: the refusal and the failure are what the caller is shown there.Verification
Final head
3203683d. Contract review PASS on5cee1bbe(record 6843734657); this round executes seat rulings 6843826357 item 2, and only the affected gates were re-run on it (below). Package suites, typecheck and the full derived set are from5cee1bbe.Package tests and typecheck
pnpm --filter @objectstack/service-automation test:Test Files 279 passed (279),Tests 3818 passed (3818)(on4968c9c9;5cee1bbeadds only collection-time imports to two test files outside this package).pnpm --filter @objectstack/spec test:Test Files 837 passed (837),Tests 26772 passed | 1 todo (26773).pnpm --filter @objectstack/runtime exec vitest run --project local src/action-flow-dropped-fields-relay.test.ts:Tests 4 passed (4);pnpm --filter @objectstack/verify exec vitest run src/action-flow-dropped-fields.test.ts:Tests 2 passed (2)(both on5cee1bbe).pnpm --filter @objectstack/service-automation --filter @objectstack/runtime --filter @objectstack/verify --filter @objectstack/spec typecheck: exit 0.pnpm --filter @objectstack/spec check:generated:All 14 generated artifacts are up to date(after--fixregeneratedcontent/docs/references/**).Round 2 on `3203683d`: affected gates and tests
run-dropped-fields.test.ts:Tests 8 passed (8).src/api/automation-api.zod.test.ts+src/contracts/:Test Files 52 passed (52),Tests 551 passed (551); spectsc --noEmitexit 0.pnpm --filter @objectstack/spec check:generated:All 14 generated artifacts are up to date(the reference page regenerated twice in this round).5cee1bbe(same 119 commands). The 60 that read a changeset,packages/spec, docs, issue citations, NUL bytes or test sources were re-run: 60 exit 0. The first pass was red onpnpm check:doc-authoring(an issue id inside a.describe());3203683dmoves it to the adjacent comment and the re-run is green.Round 1 on `5cee1bbe`: derived gates (`dispatch-gates.mjs --commands`, 119) and lint
Run on head
5cee1bbe, every command's exit captured before any pipe;node scripts/pm/dispatch-gates.mjs --ran gates-ran-5cee1bbe.txt:✓ dispatch-gates --ran: 119 derived famil(ies) accounted for — 118 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).node scripts/check-plugin-teardown-shape.mjs --self-test, exit 3, prerequisite not met (the shallow clone cannot reach its pinned fixture commit; it grades the checker, not this diff). CI runs it.4968c9c9) was red onpnpm check:test-source-alias(the two new door tests reached the harness's dynamic imports inside a clocked case);5cee1bbeadds the module-top imports the gate prescribes, and the re-run above is green.pnpm lint(eslint . --no-inline-config) on5cee1bbe: exit 0.Generated by Claude Code