Repository navigation
fix(automation): a flow's answer also names the read-only fields its map items and resumed subflow children dropped (#33019) - #33068
Conversation
…and resumed subflow children dropped Every leg that credits a child run's writes to the parent's summary now folds the child's droppedFields into the parent's answer through the one fold: the map node's exits (synchronous and paused), both callers of creditChildRun on the resumed legs, and bubbleToParent. The fold skips what is not well-typed instead of throwing, so a host executor's malformed droppedFields never fails a completed run. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxDAXkDm3BEbMTZWFc
… drop Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxDAXkDm3BEbMTZWFc
…droppedFields describe Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxDAXkDm3BEbMTZWFc
📓 Docs Drift CheckThis PR changes 2 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
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 2786df4085e1be8cfd8816bfe966676d17119276 && git checkout 2786df4085e1be8cfd8816bfe966676d17119276
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0a5035757602ec5ad6b580e9f535496cd3bd8579 d7a8d98fcf7685b467abcb64605fc52d57e724d7 && git checkout -B drift-repro 0a5035757602ec5ad6b580e9f535496cd3bd8579 && git merge --no-ff d7a8d98fcf7685b467abcb64605fc52d57e724d7
node scripts/docs-audit/affected-docs.mjs --json 0a5035757602ec5ad6b580e9f535496cd3bd8579
|
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract-review-claim: session_01Q4EjjMnyLqJdzuktEmRPXH · head fb720b6 · 2026-11-13T07:21Z Taken on the re-request (6846806652). The lane's account has no fable budget left (its owner, verbatim: 「fable 不多了。后续契约复审找分诊」), which meets this seat's rule from the maintainer (「契约复审只有在车道没有fable的时候才会找你执行」). Schema face (published describe text in |
|
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH Contract reviewServed-tier: Widened, read and graded because the brief asked: Check-runs on this head, read 2026-11-13 (twice, same counts): 24 success, 10 skipped, 31 in_progress, 0 failed.
① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentmust-fix: none. What holds, read in the tree
should-fix S1 (pins,
should-fix S2 (both hits and the changeset): the failed child is not the only leg that still answers no drops.
nit N1: the refusal exit's nit N2: the step log keeps what the answer skips. nit N3: nit N4: no case runs a chain deeper than two, and none runs on the durable store. Both hold by reading (induction over the terminal exit; the step log is persisted whole). N6 of record 6843734657 stands as accepted. Implemented-by: VERDICT: PASS Generated by Claude Code |
… name the cancelled map item Two pins the contract review found unguarded: an earlier item's drop surviving a later item's contained failure, and a paused child's drops merging with those its awaiting step already holds. The published remainder now names the operator-cancelled paused map item beside the failed child. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxDAXkDm3BEbMTZWFc
…-map-item clause Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cxDAXkDm3BEbMTZWFc
|
domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J Contract reviewServed-tier: Delta review of record 6846914053 (triage#1, PASS on Check-runs on this head, read at 2026-11-13T08:20Z: 49 runs, 15 success, 11 skipped, 23 in_progress, 0 failed.
① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentmust-fix: none. S1 closes: both pins depend on the code they name, by reading.
S2 closes: the cancelled paused
Carried unchanged from record 6846914053: nits N1 (the refusal exit's spread reaches no answer), N3 (the dead "drops but no summary" branch) and N4 (no chain deeper than two, no durable-store case). N2 is carried on #33020. No new finding in the delta. Implemented-by: VERDICT: PASS |
|
domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J ACCEPT · PR #33068 · head 席位核对
|
Requested by Elon · project thread
Fixes #33019
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
droppedFields(哪些字段因只读没写进去)现在也带上「逐条处理(map)」每条子运行丢掉的字段,以及暂停后再恢复的子流程丢掉的字段,不论恢复时调用方给的是父流程的运行号还是子流程自己的。三条路共用同一个折叠函数,仍按对象+原因合并成一条。折叠函数同时加固:外部注册的节点交回格式不对的droppedFields时直接跳过,不会把已完成的运行变成失败或重试。summary写着「改了 N 条」,这 N 条包括 map 每条子运行和恢复过的子流程写的,但droppedFields只算流程自己的写步骤和不暂停的子流程。最常见的「对每一行提交」场景(map 里每条都写只读的status)回答里一个字都没有。try_catch/ 故障边接住时,回答里没有该成员不能当作「没丢」);暂停后被操作员取消的 map 条目也不报(map 在该条目位置放「已取消」标记继续)。回滚即 revert。packages/services/service-automation/src/run-dropped-fields.ts(foldDropEvents:唯一的折叠 + 跳过格式不对的输入);run-resume.ts(creditChildRun把子运行的丢弃字段记到等待它的那一步,两个恢复路径都走它);builtin/map-node.ts(map 四个出口带上本轮已完成条目的丢弃字段)。pnpm --filter @objectstack/service-automation exec vitest run --maxWorkers=2 src/run-dropped-fields.test.ts,应见Tests 18 passed (18)。What changes
This is the delegated half of #32861 (Part of #32861, which stays open for its console half #33009). The contract review of PR #33007 (record 6843734657, should-fix S1; delta 6844555869) measured that
summarycredits a child run's writes from asubflowand from eachmapitem, on the synchronous leg and on both resumed legs, whiledroppedFieldsrolled up only from asubflowchild that completed without pausing. Re-read atc54a23d2(main, with #33007 merged):builtin/map-node.tsnever read a child'sdroppedFields, and the delegated resume leg andbubbleToParentcarriedoutputandsummaryonly.Every leg that credits a child run's writes to the parent's
summarynow folds the child'sdroppedFieldsinto the parent's answer, through one fold:run-dropped-fields.ts: the fold body moves intofoldDropEvents(lists), anddropEventsOf(steps)becomes the fold over the steps' lists. There is no second copy;run-resume.ts's terminal exit computesdropEventsOf(steps)once and uses it for both its answer and its up-bubble.builtin/map-node.ts: each entry collects the answers of the items that completed synchronously and rolls them onto its step at all four exits (completion, the exit that suspends on a pausing item, failure, refusal), beside the totals those exits already carry. The failure exit keeps the earlier items' drops because atry_catchorfaultedge that contains the failure lets the run complete, and its answer folds that step.run-resume.tscreditChildRuntakes the child's drop events and folds them onto the same awaiting step its totals land on. Both callers pass them: the up-bubble (childDroppedFields, a new trailingresumeInternalparameter) and the delegated leg (childRes.droppedFields).engine.tsbubbleToParenttakes the completing child's drop events (the ones its own answer carries) and hands them to the parent'sresumeInternal. The refusal arm passes none: a refused run's answer carries none, the same as the synchronoussubflow/maprefusal arms.Array.isArrayand each event withDroppedFieldsEventSchema.safeParse, and skips what fails. The callers run inside thetrywhosecatchreads a throw as a node or run failure, so a host-registered executor's malformeddroppedFieldscan no longer turn a completed run into a failed or retried one.TriggerFlowResponseSchema.data.droppedFieldsdescribe and theAutomationResult.droppedFieldsjsdoc drop the "do not yet" sentence and state the rule as built. The "absent is not proof" clause is narrowed, not removed, because two delegated legs still do not report: a child run that FAILED answers no drops, whilesummarystill credits its writes; and amapitem whose paused run an operator cancelled (service-automation: a parent-id resume of amapwhose parked item run was cancelled still advances past that item #27971) answers none, as it is credited tosummarynowhere either. The regeneratedcontent/docs/references/api/automation-api.mdxrow equals the describe. Card ids stay in the adjacent//comment (check:doc-authoring).Tests
packages/services/service-automation/src/run-dropped-fields.test.ts(8 → 18):mapitems that each dropstatusanswer one event on the parent, withsummary.acted: 3.mapitem that paused and was resumed through its own run id rolls its drop up (the up-bubble).mapitem that completed before a later item paused keeps its drop (the map's suspend exit). The later item drops nothing, so the parent can only learn of the drop from that exit.mapwhose first item drops and whose second item fails under afaultedge: the run completes and the first item's drop reaches the answer through the map's failure exit.adropsamountsynchronously, itembpauses and, resumed, dropsmemo; the answer holds one event with both fields.subflowchild that paused, resumed through the parent's run id (the delegated leg), rolls its drop up.subflowchild that paused, resumed through its own run id (the up-bubble), rolls its drop up.null, a string,fieldsnot a list, an unknownreason) are skipped: the run completes and the well-typed event beside them still answers.droppedFieldsthat is array-like but not a list (it reaches the step, and iterating it would throw) leaves the run completed with no member.foldDropEventsis total, does not mutate its inputs, and answersundefinedwhen nothing well-typed remains.The children that pause write
statusonly after their resume, so each resumed-leg case can pass only if the resumed child's drops reach the parent. The up-bubble cases park the parent once more on aholdnode after thesubflow/map, so the parent's own answer (given on its resume) is the thing asserted.Ablation: each leg cut alone (the test imports src, so no rebuild is involved)
All on committed heads, through
scripts/ablation-replace.mjs, each restored withblob == HEADandgit diff HEADempty.mapsynchronous exit (itemDrops.push(child.droppedFields);x1 -> x0):Tests 1 failed | 14 passed (15), the map synchronous case red.childDroppedFieldsremoved fromresumeInternal'screditChildRuncall):Tests 2 failed | 13 passed (15), the map up-bubble and subflow up-bubble cases red.childRes.droppedFieldsremoved):Tests 1 failed | 14 passed (15), the subflow delegated case red.bubbleToParenthand-off (droppedFieldsremoved from itsresumeInternalcall):Tests 2 failed | 13 passed (15), the same two up-bubble cases red.mapsuspend exit (...rolledDrops()removed there):Tests 1 failed | 15 passed (16), the earlier-item case red.safeParseguard (raw event used as-is):Tests 2 failed | 14 passed (16), the malformed-event engine case and thefoldDropEventsunit case red.Array.isArrayguard:Tests 11 failed | 5 passed (16). The direction is broader than the not-a-list case: the guard also skips theundefinedlist every step without drops carries, so every engine case that reaches a terminal exit reds.mapfailure exit (...rolledDrops()removed there; on4bc2b134):Tests 1 failed | 17 passed (18), the fault-edge case alone red.creditChildRunmerge (foldDropEvents([steps[i].droppedFields, childDroppedFields])-> the child's list alone; on4bc2b134):Tests 1 failed | 17 passed (18), the merge case alone red.Acceptance notes
droppedFields(only a terminal success carries the member), so a failedsubflowchild ormapitem contained bytry_catchor afaultedge contributes its writes tosummarybut its drops to nothing. Carrying drops on a failed answer would widenAutomationResult's failure shape; not done here. And amapitem whose paused run an operator cancelled (service-automation: a parent-id resume of amapwhose parked item run was cancelled still advances past that item #27971) answers none:continuePastCancelledMapItemlets the map go on with the cancelled marker in that item'sresultsslot, and neither its totals nor its drops reach the parent. Both clauses are published on the describe, the jsdoc and the changeset..changeset/32861-flow-answer-dropped-fields.md, not yet released) still says a paused child and amapitem "do not yet" roll up. It is left as written; this PR's changeset states that it lifts that limit, so the release notes read in order.builtin/subflow-node.tsis unchanged: its synchronous roll-up already reads the child's answer, which is the fold's output.yes (widening): the answer carries more events in two shapes; the member's shape and the omit-when-empty rule are unchanged, soMetadata-protocol: noneand a patch changeset for@objectstack/specand@objectstack/service-automation.Verification
Head
d7a8d98f. Round 2 executes seat rulings 6846930076 item 1 (contract review record 6846914053, PASS onfb720b6c, should-fix S1 and S2) in one push of two commits:4bc2b134(the two S1 pins; the S2 clause on the describe, the jsdoc and the changeset) andd7a8d98f(the regenerated reference page). No merge ofmainin this round.Round 2 on `d7a8d98f`: affected tests and gates
pnpm exec turbo run build --concurrency=2under the verify lock, exit 0 (rebuildspackages/specfor the changed describe).run-dropped-fields.test.ts:Tests 18 passed (18);pnpm --filter @objectstack/service-automation typecheck: exit 0.src/api/automation-api.zod.test.ts+src/contracts/:Test Files 53 passed (53),Tests 555 passed (555).fb720b6c(the same 116 commands). The 67 that readpackages/spec, docs, the changeset, issue citations, NUL bytes or test sources were re-run: 67 exit 0, none exit 3. Record:gates-ran-affected-d7a8d98f.txt.Round 1, head
fb720b6c(origin/mainc54a23d2merged before the PR opened; main had not moved). Every exit captured before any pipe.Package tests and typecheck
pnpm --filter @objectstack/service-automation test:Test Files 279 passed (279),Tests 3827 passed (3827).pnpm --filter @objectstack/spec test:Test Files 839 passed (839),Tests 26796 passed | 1 todo (26797).pnpm --filter @objectstack/service-automation typecheck(tsc --noEmit+check:test-typecheck): exit 0,0 file(s) / 0 error(s).run-dropped-fields.test.ts:Tests 16 passed (16).pnpm exec turbo run build --concurrency=2under the verify lock, exit 0; the plugin-teardown fixture commit fetched.Derived gates (`dispatch-gates.mjs --commands`, 116) and lint
node scripts/pm/dispatch-gates.mjs --ran gates-ran-fb720b6c.txt:✓ dispatch-gates --ran: 116 derived famil(ies) accounted for — 116 run, 0 NOT-MEASURED (a DERIVED zero — all 116 recorded an exit code and none of them is 3).pnpm --filter @objectstack/spec run check:generatedandcheck:docsamong them: the regenerated reference page is current.pnpm lint(eslint . --no-inline-config): exit 0 (recorded outside the derivation).Generated by Claude Code