Skip to content

fix(automation): a flow's answer also names the read-only fields its map items and resumed subflow children dropped (#33019) - #33068

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-a51kss
Nov 13, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-a51kss

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #33019

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

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 summary credits a child run's writes from a subflow and from each map item, on the synchronous leg and on both resumed legs, while droppedFields rolled up only from a subflow child that completed without pausing. Re-read at c54a23d2 (main, with #33007 merged): builtin/map-node.ts never read a child's droppedFields, and the delegated resume leg and bubbleToParent carried output and summary only.

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 one fold:

  • run-dropped-fields.ts: the fold body moves into foldDropEvents(lists), and dropEventsOf(steps) becomes the fold over the steps' lists. There is no second copy; run-resume.ts's terminal exit computes dropEventsOf(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 a try_catch or fault edge that contains the failure lets the run complete, and its answer folds that step.
  • run-resume.ts creditChildRun takes 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 trailing resumeInternal parameter) and the delegated leg (childRes.droppedFields).
  • engine.ts bubbleToParent takes the completing child's drop events (the ones its own answer carries) and hands them to the parent's resumeInternal. The refusal arm passes none: a refused run's answer carries none, the same as the synchronous subflow / map refusal arms.
  • Hardening (review nit N1): the fold checks each list with Array.isArray and each event with DroppedFieldsEventSchema.safeParse, and skips what fails. The callers run inside the try whose catch reads a throw as a node or run failure, so a host-registered executor's malformed droppedFields can no longer turn a completed run into a failed or retried one.
  • Published sentences: the TriggerFlowResponseSchema.data.droppedFields describe and the AutomationResult.droppedFields jsdoc 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, while summary still credits its writes; and a map item whose paused run an operator cancelled (service-automation: a parent-id resume of a map whose parked item run was cancelled still advances past that item #27971) answers none, as it is credited to summary nowhere either. The regenerated content/docs/references/api/automation-api.mdx row 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):

  • map items that each drop status answer one event on the parent, with summary.acted: 3.
  • A map item that paused and was resumed through its own run id rolls its drop up (the up-bubble).
  • A map item 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.
  • A map whose first item drops and whose second item fails under a fault edge: the run completes and the first item's drop reaches the answer through the map's failure exit.
  • A paused child's drops merge with the drops its awaiting step already holds: item a drops amount synchronously, item b pauses and, resumed, drops memo; the answer holds one event with both fields.
  • A subflow child that paused, resumed through the parent's run id (the delegated leg), rolls its drop up.
  • A subflow child that paused, resumed through its own run id (the up-bubble), rolls its drop up.
  • A host executor's malformed events (null, a string, fields not a list, an unknown reason) are skipped: the run completes and the well-typed event beside them still answers.
  • A droppedFields that is array-like but not a list (it reaches the step, and iterating it would throw) leaves the run completed with no member.
  • foldDropEvents is total, does not mutate its inputs, and answers undefined when nothing well-typed remains.

The children that pause write status only 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 a hold node after the subflow / 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 with blob == HEAD and git diff HEAD empty.

  • A. map synchronous exit (itemDrops.push(child.droppedFields); x1 -> x0): Tests 1 failed | 14 passed (15), the map synchronous case red.
  • B. up-bubble credit (childDroppedFields removed from resumeInternal's creditChildRun call): Tests 2 failed | 13 passed (15), the map up-bubble and subflow up-bubble cases red.
  • C. delegated credit (childRes.droppedFields removed): Tests 1 failed | 14 passed (15), the subflow delegated case red.
  • D. bubbleToParent hand-off (droppedFields removed from its resumeInternal call): Tests 2 failed | 13 passed (15), the same two up-bubble cases red.
  • E. map suspend exit (...rolledDrops() removed there): Tests 1 failed | 15 passed (16), the earlier-item case red.
  • F. fold safeParse guard (raw event used as-is): Tests 2 failed | 14 passed (16), the malformed-event engine case and the foldDropEvents unit case red.
  • G. fold Array.isArray guard: Tests 11 failed | 5 passed (16). The direction is broader than the not-a-list case: the guard also skips the undefined list every step without drops carries, so every engine case that reaches a terminal exit reds.
  • H. map failure exit (...rolledDrops() removed there; on 4bc2b134): Tests 1 failed | 17 passed (18), the fault-edge case alone red.
  • I. creditChildRun merge (foldDropEvents([steps[i].droppedFields, childDroppedFields]) -> the child's list alone; on 4bc2b134): Tests 1 failed | 17 passed (18), the merge case alone red.

Acceptance notes

  • What remains of the limit: a child run that FAILED answers no droppedFields (only a terminal success carries the member), so a failed subflow child or map item contained by try_catch or a fault edge contributes its writes to summary but its drops to nothing. Carrying drops on a failed answer would widen AutomationResult's failure shape; not done here. And a map item whose paused run an operator cancelled (service-automation: a parent-id resume of a map whose parked item run was cancelled still advances past that item #27971) answers none: continuePastCancelledMapItem lets the map go on with the cancelled marker in that item's results slot, and neither its totals nor its drops reach the parent. Both clauses are published on the describe, the jsdoc and the changeset.
  • The 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 changeset (.changeset/32861-flow-answer-dropped-fields.md, not yet released) still says a paused child and a map item "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.ts is unchanged: its synchronous roll-up already reads the child's answer, which is the fold's output.
  • Clause-② yes (widening): the answer carries more events in two shapes; the member's shape and the omit-when-empty rule are unchanged, so Metadata-protocol: none and a patch changeset for @objectstack/spec and @objectstack/service-automation.

Verification

Head d7a8d98f. Round 2 executes seat rulings 6846930076 item 1 (contract review record 6846914053, PASS on fb720b6c, 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) and d7a8d98f (the regenerated reference page). No merge of main in this round.

Round 2 on `d7a8d98f`: affected tests and gates
  • Prerequisite: whole-tree pnpm exec turbo run build --concurrency=2 under the verify lock, exit 0 (rebuilds packages/spec for the changed describe).
  • run-dropped-fields.test.ts: Tests 18 passed (18); pnpm --filter @objectstack/service-automation typecheck: exit 0.
  • spec src/api/automation-api.zod.test.ts + src/contracts/: Test Files 53 passed (53), Tests 555 passed (555).
  • The derivation is unchanged from fb720b6c (the same 116 commands). The 67 that read packages/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/main c54a23d2 merged 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).
  • Prerequisites: whole-tree pnpm exec turbo run build --concurrency=2 under 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).

  • 116 exit 0, none red, none exit 3.
  • pnpm --filter @objectstack/spec run check:generated and check:docs among them: the regenerated reference page is current.
  • pnpm lint (eslint . --no-inline-config): exit 0 (recorded outside the derivation).

Generated by Claude Code

…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
…droppedFields describe

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cxDAXkDm3BEbMTZWFc
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 13, 2026
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/spec, touching 10 documentable anchor(s).

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via AutomationResult (symbol, a top-level interface))
  • content/docs/api/declarative-endpoints.mdx (via /api/v1/automation/:name/trigger (route, bridged from symbol TriggerFlowResponseSchema — its route source's handler names it))
  • content/docs/automation/flows.mdx (via AutomationResult (symbol, a top-level interface), bubbleToParent (symbol, a method of class AutomationEngine), /api/v1/automation/:name/trigger (route, bridged from symbol TriggerFlowResponseSchema — its route source's handler names it))
  • content/docs/kernel/cluster.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/protocol/kernel/http-protocol.mdx (via /api/v1/automation/:name/trigger (route, bridged from symbol TriggerFlowResponseSchema — its route source's handler names it))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via AutomationEngine (symbol, a top-level class))
  • content/docs/releases/v17/17-1.mdx (via AutomationResult (symbol, a top-level interface))
  • content/docs/releases/v17/17-3.mdx (via AutomationResult (symbol, a top-level interface))
  • content/docs/releases/v17/17-6.mdx (via AutomationEngine (symbol, a top-level class), AutomationResult (symbol, a top-level interface))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 171 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 0a5035757602ec5ad6b580e9f535496cd3bd8579 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2786df4085e1be8cfd8816bfe966676d17119276 — the merge of head d7a8d98fcf7685b467abcb64605fc52d57e724d7 into base 0a5035757602ec5ad6b580e9f535496cd3bd8579, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 0a5035757602ec5ad6b580e9f535496cd3bd8579 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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 packages/spec/src/api/automation-api.zod.ts) at CONTRACT_REVIEW_TIER.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fb720b6cbb82e4c7e74438044da889ccf09e5c60
Local-runs: none
Scope: packages/spec/src/api/automation-api.zod.ts, packages/spec/src/contracts/automation-service.ts + packages/services/service-automation/src/engine.ts, packages/services/service-automation/src/run-dropped-fields.ts, packages/services/service-automation/src/run-resume.ts, packages/services/service-automation/src/builtin/map-node.ts

Widened, read and graded because the brief asked: packages/services/service-automation/src/run-dropped-fields.test.ts, .changeset/33019-flow-answer-delegated-dropped-fields.md, and the regenerated content/docs/references/api/automation-api.mdx. Read at this head, never reviewed: builtin/subflow-node.ts, dag-traversal.ts (step writing, region suspension), run-resume.ts outside the hunks (the map-item gate, the cancelled-item continuation, the refusal arm), spec data/data-engine.zod.ts, .changeset/32861-flow-answer-dropped-fields.md, AGENTS.md Post-Task Checklist item 3. Prior records 6843734657 and 6844555869 on PR #33007 were read; this PR is the follow-up their S1 and N1 named.

Check-runs on this head, read 2026-11-13 (twice, same counts): 24 success, 10 skipped, 31 in_progress, 0 failed.

  • Green: every lint job, Type Check · source gates, Check Changeset, PR-body guards, Spec property liveness, Governed Surface Queue Guard, Check Documentation Links, Check PR Size.
  • Not concluded: Test Core 1 to 8, Test UI 1 to 16, Build Core, Build Docs, Build & E2E UI, Live E2E UI, and the workspace, UI, consumer-gates and debt-ledger type checks.
  • The shard that runs the 8 new cases, the workspace type check and the docs build are among those still running. Their green rests on the dev report until the seat re-reads before queueing.

① Derived judgments

  1. No accept set and no type moves: right. The automation-api.zod.ts hunk is one // comment and the describe string; z.array(DroppedFieldsEventSchema).optional() is untouched. The automation-service.ts hunk is jsdoc only; droppedFields?: DroppedFieldsEvent[] is untouched. The parity guard between the two members has nothing new to bind.
  2. The behaviour is a widening of what the answer carries: right. The member now appears on a terminal success in two shapes where it was absent before (a map item that dropped; a subflow or map child that paused and resumed). Shape, omit-when-empty and "terminal success only" are unchanged.
  3. The widening is exactly the card's Expected: right. Every leg that credits a child's totals to the parent's summary now also folds the child's drops. Read leg by leg in ④; no leg that summary credits on a completed child is left out, and no leg is added that summary does not credit.
  4. Clause-②: yes (widening) rather than the card's tentative none: right. The card said to judge at the time. A published answer member is set in situations where it was not, and the published describe changes its promise, so the conservative arm is the true one. The claim (6845576912) declared the same line before the first push.
  5. Metadata-protocol: none: right. Nothing a metadata author can write changes.
  6. Skipping malformed events is not an undeclared narrowing: right. The answer's element was always DroppedFieldsEventSchema; an event outside it (an unknown reason, fields not a list) was never inside the contract, and both in-tree producers are well-typed. The changeset's second bullet states the skip.
  7. No new vocabulary: right. One fold, the engine's own event, one event per object and reason.

② Semver level

  • patch on @objectstack/spec and @objectstack/service-automation: right. AGENTS.md Post-Task Checklist item 3: the level follows the kind alone, none takes patch, and a non-protocol widening ships patch. Check Changeset is green on this head.
  • Declaration lines: identical. Clause-②: yes (widening) and Metadata-protocol: none are line-initial and the same in the PR body and the changeset.
  • Changeset prose against the tree: true but for one phrase. The three bullets match the code (map synchronous and pausing exits, both resumed legs, the total fold, the member's shape unchanged). "What remains: a child run that failed answers no drops" reads as the whole remainder and is not; see S2.
  • nit (changeset prose): two unreleased entries will ship side by side and disagree. .changeset/32861-flow-answer-dropped-fields.md still says a paused child and a map item "do not yet" roll up. The new entry says it lifts that. CHANGELOG order within one release is not by card number. Editing the unreleased 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 sentence is cleaner; the PR declares the choice, so it is accepted as written.
  • Docs page: agrees. The droppedFields row in content/docs/references/api/automation-api.mdx equals the new describe string character for character and carries no card id. Its type column is unchanged, as the schema is.

③ Boundary flags

④ Design judgment

must-fix: none.

What holds, read in the tree

  • automation-api.zod.ts (hit). Each new sentence is true of the code.
    • "a subflow child's run and each map item's run, whether it completed without pausing or paused and resumed": true on all five legs, listed under the implementing files below.
    • "A child run that failed answers no drops": true. Only the three terminal-success exits set the member; subflow-node.ts and map-node.ts read nothing from a failed child.
    • "where a try_catch or fault edge contains that failure an absent member is not proof": true, and the earlier items' drops still answer (the map's failure exit).
    • First consumer (console: a record-button flow whose write step dropped a read-only field shows a warning toast naming the fields (console half of #32861) #33009, the console reader): the entry shape it parses is unchanged; it only receives events in more cases.
  • automation-service.ts (hit). The jsdoc says the same as the describe, adds "as {@link summary} credits them" (true: the same seams) and "Unlike summary, a child run that FAILED answers no drops" (true: the failure arms roll the failed child's totals and no drops).
  • run-dropped-fields.ts. dropEventsOf is now foldDropEvents over the steps' lists, so there is one fold. It skips a list that is not an array and an event that fails DroppedFieldsEventSchema.safeParse, copies fields, and answers undefined for nothing. This closes N1 of record 6843734657: the three callers inside a try (the terminal exits) can no longer throw over a host executor's value.
  • builtin/map-node.ts. itemDrops is per entry, like the totals, and is pushed only on synchronous completion. All four exits spread rolledDrops(). dag-traversal.ts copies an executor's droppedFields onto the step on the success, suspend, refuse and failure arms, so each exit's value reaches the step log.
  • run-resume.ts and engine.ts. creditChildRun folds the child's events with whatever the awaiting step already holds, onto the same step its totals land on. Both callers pass them: the up-bubble (childDroppedFields, handed in by bubbleToParent) and the delegated leg (childRes.droppedFields). The terminal exit computes dropEventsOf(steps) once for its answer and its up-bubble. RunResumeHost types these three members off AutomationEngine, so the new trailing parameters need no second declaration.
  • Every path, by reading.
    • Direct map, items complete synchronously: the completion exit. Reported.
    • map, an earlier item completed and a later one pauses: the suspend exit holds the earlier drops; the paused item's are credited on its bubble; the re-entry writes a fresh step. Reported.
    • map, item resumed: only the up-bubble exists (resuming the parent while an item is parked is refused MAP_ITEM_PENDING). Reported.
    • subflow, synchronous: unchanged from fix(automation): a flow run's answer names the read-only fields its write steps dropped (#32861) #33007. Reported.
    • subflow, paused then resumed through the parent's id (delegated) or its own (up-bubble): reported. Chains of any depth follow by induction, because each frame's answer is the fold of its credited steps.
    • A drop before the child's own pause: in the child's whole-run answer, so it rides the same seam.
    • Refused child: the parent refuses and a refused answer carries none; the refusal arm of bubbleToParent passes none. Consistent.
    • A pause inside a structured region is refused by the engine, so there is no region leg to credit.
    • Failed child: not reported, declared.
    • Cancelled paused map item: not reported, not declared. See S2.
  • Existing callers. A run with no map and no paused child answers byte for byte as before, unless a host executor returned an event outside the schema, which is now skipped (declared). The execute() and retried-attempt exits are untouched and read the same fold.

should-fix S1 (pins, run-dropped-fields.test.ts): two added behaviours stay green when cut.

  • The map's failure exit. No case runs an item that drops, then an item that fails, under a try_catch or fault edge. Deleting ...rolledDrops() at that exit leaves all 16 green. The PR's ablation list (A to G) has no cut for it, yet the code comment and the describe both rest on it.
  • The merge in creditChildRun. No case credits a child's drops onto a step that already holds some. In the earlier-item case the paused item drops nothing, so the credit is skipped; in the up-bubble and delegated cases the awaiting step holds none. Replacing foldDropEvents([steps[i].droppedFields, childDroppedFields]) with the child's list alone leaves all 16 green, and would erase the earlier items' drops.
  • One case closes both: map over two items where the first drops field A synchronously and the second pauses, resumes and drops field B; expect both. A second, small case closes the failure exit.
  • The other legs are pinned and the reported reds are what the code predicts: A (map synchronous), B and D (both up-bubble cases), C (delegated), E (suspend exit), F (the safeParse guard). G reds 11 because the guard also skips the undefined list of a drop-free step, so it does not isolate the not-a-list case; the PR says so.

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 ...rolledDrops() cannot reach an answer. A refused run's answer carries no droppedFields. The spread only writes the completed items' events to the step log. Harmless symmetry with the totals.

nit N2: the step log keeps what the answer skips. dag-traversal.ts puts any droppedFields with a truthy length on the step, so a host executor's malformed value is persisted and served by the runs routes verbatim while the fold drops it from the answer. Carry on #33020 with the wider step coverage noted in ③.

nit N3: creditChildRun's "drops but no summary" branch is unreachable. Both callers pass a summary whenever they pass drops. The early return it adds is dead today.

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: claude/project-thread-a51kss
Reviewed-by: session_01Q4EjjMnyLqJdzuktEmRPXH

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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d7a8d98fcf7685b467abcb64605fc52d57e724d7
Local-runs: none
Scope: packages/spec/src/api/automation-api.zod.ts, packages/spec/src/contracts/automation-service.ts, packages/services/service-automation/src/run-dropped-fields.test.ts, .changeset/33019-flow-answer-delegated-dropped-fields.md, content/docs/references/api/automation-api.mdx

Delta review of record 6846914053 (triage#1, PASS on fb720b6c). The delta is fb720b6c..d7a8d98f: commits 4bc2b134 and d7a8d98f, 5 files, +88/-6, no main merge. It was read whole. No file under packages/services/service-automation/src other than the test changed, so every code judgment of the prior record stands on the same lines. Read at this head to check the new pins and the new clause, not re-reviewed: builtin/map-node.ts (the four exits, the cancelled-marker re-entry), run-resume.ts (creditChildRun, continuePastCancelledMapItem), run-dropped-fields.ts (foldDropEvents), dag-traversal.ts (the failure and success step arms). Dev report 6847795395 was read.

Check-runs on this head, read at 2026-11-13T08:20Z: 49 runs, 15 success, 11 skipped, 23 in_progress, 0 failed.

  • Green: both PR-body guards, Spec property liveness, Governed Surface Queue Guard, Check Documentation Links, Check PR Size, one of the two Check Changeset runs, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Dogfood Regression Gate.
  • Not concluded: Test Core 1 to 8, Build Core, Build Docs, Build UI Deps, Live E2E UI, all four Type Check jobs, six Lint jobs (among them Lint · docs & code guards, which runs the doc-authoring check), and the second Check Changeset run.
  • The shard that runs the two new cases is still running. The 18-of-18 green and ablations H and I rest on the dev report until the seat re-reads before queueing.

① Derived judgments

  1. No accept set and no type moves: right. In automation-api.zod.ts the delta touches two // comment lines and the describe string. z.array(DroppedFieldsEventSchema).optional() is untouched. In automation-service.ts it adds three jsdoc lines. droppedFields?: DroppedFieldsEvent[] is untouched.
  2. No behaviour moves: right. No runtime source changed. The delta is two test cases and prose.
  3. Clause-②: yes (widening) carries unchanged. The new clause narrows no promise. It states a limit that was already true at fb720b6c.
  4. Metadata-protocol: none carries unchanged. Nothing a metadata author can write changes.
  5. No new vocabulary. "the cancelled marker" is the existing MapItemCancelledMarker of service-automation: a parent-id resume of a map whose parked item run was cancelled still advances past that item #27971.

② Semver level

  • patch on both packages: unchanged and right. The changeset's frontmatter and its two declaration lines are outside the hunk.
  • Changeset prose: now true. "What remains" names the failed child and the cancelled paused map item. That is the whole remainder the prior record found.
  • Docs page: agrees. The droppedFields row in automation-api.mdx ends with the same new sentence as the describe, word for word, and carries no card id.
  • Prior nit stands, accepted as before: the unreleased .changeset/32861-... entry still says "do not yet".

③ Boundary flags

④ Design judgment

must-fix: none.

S1 closes: both pins depend on the code they name, by reading.

  • The map's failure exit (case at test line 310). Item a completes and drops amount; item b fails; a fault edge to handler contains it.
    • Item a's event lives only in the map entry's itemDrops.
    • The !child.success return in map-node.ts spreads ...rolledDrops(), and the failure arm of dag-traversal.ts copies result.droppedFields onto the failure step.
    • The terminal fold reads that step. With the spread cut, no step holds the event and res.droppedFields is undefined, so the case reds. This matches ablation H.
  • The merge in creditChildRun (case at test line 346). Item a drops amount synchronously; item b pauses at gate, and on resume drops memo.
    • The suspend exit puts a's event on the map step. When b bubbles back, foldDropEvents([steps[i].droppedFields, childDroppedFields]) merges the two under one object and reason.
    • The re-entry writes a fresh step with no drops, so the expected fields: ['amount', 'memo'] can only come from that merge.
    • With the child's list alone, the answer is ['memo'] and the case reds. This matches ablation I.
  • Each case fails only its own cut: the first never reaches creditChildRun with drops, and the second never reaches the failure exit.

S2 closes: the cancelled paused map item is named on all four places.

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: claude/project-thread-a51kss
Reviewed-by: session_016WV4cwqTK71kjQzHae8T5J

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#8 · os-elon-musk · session_016WV4cwqTK71kjQzHae8T5J

ACCEPT · PR #33068 · head d7a8d98fcf7685b467abcb64605fc52d57e724d7 · 2026-11-13T08:45Z · Fixes #33019

席位核对

  1. Shape: draft PR to main, Fixes #33019. Path surface (9 files): packages/services/service-automation/src/{builtin/map-node.ts,engine.ts,run-dropped-fields.ts,run-resume.ts} + run-dropped-fields.test.ts, packages/spec/src/{api/automation-api.zod.ts,contracts/automation-service.ts} (describe and jsdoc), content/docs/references/api/automation-api.mdx (regenerated), one changeset. No governed path.
  2. Premise measured: a delegated child's dropped-field events (a paused subflow resumed, a map item) stopped at the child run; the fix rolls them into the parent's answer through creditChildRun and the map exits, folded by foldDropEvents.
  3. Gates: check-runs on this head: 60 success, 12 skipped, 0 failed, 0 pending.
  4. Contract review: CONTRACT_REVIEW_TIER PASS 6846914053 (triage#1) on fb720b6c; delta fb720b6c..d7a8d98f PASS 6847860092 on this head (S1 two pins and S2 cancelled-map-item clause closed; no must-fix; nits N1, N3, N4 accepted, N2 carried on spec: declare warnings and droppedFields on ExecutionStepLogSchema (run history serves both undeclared) #33020).
  5. Prose checked against the diff: the changeset ("What remains" names the failed child and the operator-cancelled paused map item), the describe string, the jsdoc and the regenerated mdx row say the same thing and match the code the records read. The PR body lists ablations A to I.
  6. Out of scope and carried: ExecutionStepLogSchema members → spec: declare warnings and droppedFields on ExecutionStepLogSchema (run history serves both undeclared) #33020; console toast → console: a record-button flow whose write step dropped a read-only field shows a warning toast naming the fields (console half of #32861) #33009 (landed).
  • Landing: Clause-②: yes (widening), Metadata-protocol: none, patch. main not merged since the PR opened; K = 4 (0a50357576, this head's Test Core affected-set base, to origin/main), under 15. In-flight p1 under the maintainer's rule (「后续只派 p0 p1 和阻塞 p0 p1 的卡」).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 13, 2026 08:46
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit e003ebb Nov 13, 2026
73 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-a51kss branch November 13, 2026 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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)

2 participants