Skip to content

feat(spec,service-automation): surface per-run flow summaries — selected / acted / skipped (#4354) - #4377

Merged
os-zhuang merged 2 commits into
mainfrom
claude/flow-run-summaries-xddi6c
Jul 31, 2026
Merged

feat(spec,service-automation): surface per-run flow summaries — selected / acted / skipped (#4354)#4377
os-zhuang merged 2 commits into
mainfrom
claude/flow-run-summaries-xddi6c

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4354.

问题

success: true 从来不代表"它干活了"。一个定时 flow 选出 30 条记录、然后一条也没写 —— 从外部看,和"本来就没活可干"完全一样:同样绿、同样没输出、同样静默、明天同样按时再跑一次。平台里没有任何地方能区分这两种状态。

这不是假设。#4347 让 hotcrm 的三条生产 flow 完全失效 —— stalled-deal sweep 找齐了每一条停滞商机却没提醒任何人,续约 sweep 一单没约,campaign 动作一个 lead 没入。它们每天准时跑、一直是绿的,只有靠"断言写入记录"的运行时测试才被发现。自动化恰恰是没人盯着的那一类:UI bug 一小时内就有工单,死掉的 sweep 什么都不产生,跑得越久,静默看起来越正常。

做了什么

每个终态运行现在都带一份 FlowRunSummary,四个面同时暴露:

[automation] run flow=stalled_deal_sweep run=run_a1b2 status=completed durationMs=142 selected=30 acted=0 skipped=30 gate=check_stalled->send_nudge:30
字段 含义
selected 数据节点读到的记录数
acted 写入的记录数(create / update / delete)+ 已派发的效果(notify 投递数)
skipped 关闭的 gate 挡掉的节点执行次数(loop 每轮各计一次)
nodes[] 每个节点的终态 + runs / failures / skipped + 自己的 selected/acted
gates[] 哪些 gate 关上了、关了几次,按次数降序

暴露位置:AutomationResult.summarylistRuns / getRun、每次终态运行一行结构化日志、以及 sys_automation_run 上的可查询列

三个关键取舍

计数由执行器申报,不由引擎嗅探。 执行器返回 NodeExecutionResult.metrics,因为只有节点知道自己的 result 是什么意思:update_record 批量写返回行数、按 id 写返回记录,delete_record 可能返回布尔,notify 返回投递数。引擎去猜这些形状就是在猜,而"会猜的机器可读接口"比没有更糟(ADR-0076 D12)。不碰记录的节点(decisionassignment)完全不上报 —— 缺失 ≠ 0

gate 被点名。 条件出边判为 false 时,现在会记一条 skipped step,带上关掉它的 gate。这个事件此前完全没有痕迹,正是 #4347 隐形的原因。skipped step 明确不算一次运行:ADR-0044 重入守卫、每节点 runs、节点状态都排除它 —— 新的可观测信号绝不能改变执行语义。

可查询,所以能告警而不是靠"发现"。 sys_automation_run 新增 selected_count / acted_count / skipped_count 列 + summary_json 明细:

const suspect = await engine.find('sys_automation_run', {
  where: { status: 'completed', selected_count: { $gt: 0 }, acted_count: 0 },
  orderBy: [{ field: 'started_at', order: 'desc' }],
});

用列而不是塞进 JSON:运维只能对"可过滤的东西"告警,这是仪表盘和警报器的区别。此前写入的行是 null 而非 0 —— "没测量"不能读成"测量结果是零",否则第一次写这条查询时每一条历史行都是误报。

两个决定数字可信度的细节

  • summary 在历史压缩之前从完整 step log 折叠得出,重新水合时读持久化的 summary_json不是重新折叠被压缩过的 steps —— 否则 5000 轮的 sweep 会报成只写了两百来条。
  • subflow / map 子运行会上卷到父运行:同步完成走 metrics,子运行 pause 过再冒泡回来的走 creditChildRun(父节点那一步是在挂起时写的,当时子运行还什么都没做)。没有这个,把写操作全都委托出去的 sweep 会报 acted: 0 —— 健康的运行触发本该抓坏运行的探测器,比没信号更糟。

兼容性

全程增量:summary 处处可选,老的运行和 store 照常工作,执行行为零变化。日志行默认 info(在生产日志级别看不见的行,和本 issue 要消除的"无信号"是一回事),高频 flow 可用 AutomationServicePluginrunSummaryLog: 'debug' | 'off' 调低音量 —— 那是关于噪音的决定,不是关于要不要测量的决定。

不修 #4347 本身 —— 这是本该抓到它的那把尺子。

新增 spec 导出

FlowRunSummarySchemaFlowRunNodeSummarySchemaFlowRunGateSummarySchemaExecutionStepMetricsSchemaExecutionStepSkipReasonSchema(及推导类型);ExecutionLog.summaryExecutionStepLog.metrics / .skippedByservice-automation 导出 summarizeRun / formatRunSummaryLine,让自建可观测面的宿主复用平台定义而不是各写一套。

验证

  • @objectstack/service-automation 525 / 46(新增 26)
  • @objectstack/spec 7165 / 279(新增 5)
  • @objectstack/runtime 974 / 68@objectstack/plugin-approvals 330 / 13
  • spec 八项 check:generated 全绿,外加 check:livenesscheck:exported-any
  • eslint 干净;tsc --noEmit 停在 ledger 记录的 2 个既有错误上(未增加)

🤖 Generated with Claude Code

https://claude.ai/code/session_01SzJqHp1yXjri1tqKP8WoL4


Generated by Claude Code

…ted / acted / skipped (#4354)

`success: true` never meant "it did its job". A scheduled sweep that selects
thirty records and writes none is, from outside, identical to one with nothing
to do: same green status, same empty output, same silence, same schedule
tomorrow. Nothing anywhere separated "nothing to do" from "broken" — which is
how #4347 left three hotcrm production flows completely inert, running daily and
green for as long as they had existed.

Every terminal run now carries a `FlowRunSummary`: `selected` / `acted` /
`skipped` totals, a per-node breakdown, and which gates closed. It is surfaced
on `AutomationResult`, in `listRuns` / `getRun`, as one greppable log line, and
as queryable `selected_count` / `acted_count` / `skipped_count` columns on
`sys_automation_run` — so `selected > 0 AND acted = 0` over consecutive runs is
something an operator can alert on rather than notice.

The counts are declared by the executors (`NodeExecutionResult.metrics`), not
sniffed from node output: only the node knows whether its result is a row count,
a record, or a boolean. A closed conditional edge now records a `skipped` step
naming the gate — the event that previously left no trace at all — and that step
is explicitly not a run, so the ADR-0044 re-entry guard and the per-node `runs`
counts are unchanged.

Two details decide whether the numbers can be trusted: the summary is folded
from the full step log before history compaction (and rehydrated from
`summary_json`, never re-folded from the compacted steps), and `subflow` / `map`
children roll up into their parent — synchronously via `metrics`, and via
`creditChildRun` when the child paused and bubbles back.

Additive throughout: `summary` is optional everywhere, legacy rows keep `null`
counts rather than `0`, and no execution behaviour changes. The log line
defaults to `info`, with `runSummaryLog: 'debug' | 'off'` to lower the volume
without turning the measurement off.

Verified: service-automation 525/46 (26 new), spec 7165/279 (5 new), runtime
974/68, plugin-approvals 330/13; all 8 spec `check:generated` gates plus
`check:liveness` and `check:exported-any`; eslint clean; `tsc --noEmit` at the
ledgered 2 pre-existing errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzJqHp1yXjri1tqKP8WoL4
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 31, 2026 10:01am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/xl labels Jul 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/service-automation, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…ness-ledger count (#4354)

Two CI gates on the previous commit, both real:

- `check:nul-bytes` — the gate-dedup key joined its parts with LITERAL NUL bytes
  instead of the escape sequence. Byte-identical at runtime, but a raw NUL makes
  ripgrep treat the whole file as binary and silently return zero matches, so
  `run-summary.ts` would have dropped out of code search and every grep-based
  lint. Written as the escape now, with the reason the joiner is NUL at all
  spelled out (an edge id is author-supplied text, so a printable separator
  could collide two distinct gates).
- `check:strictness-ledger` — `automation/execution.zod.ts` gained 5 `z.object(`
  sites (the run-summary family), so its ledger row and the section total were
  stale. Re-read: they are engine-emitted telemetry that nobody authors, so the
  row's `wire` verdict covers them unchanged; count updated to 13 (98 for the
  section) with the additions named.

Verified: service-automation 525/46; `pnpm lint`, `check:nul-bytes` and
`check:slot-lookup` clean; all seven pure spec audits (liveness, empty-state,
variant-docs, strictness-ledger, react-conformance, skill-examples,
exported-any) green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzJqHp1yXjri1tqKP8WoL4
@os-zhuang
os-zhuang marked this pull request as ready for review July 31, 2026 10:29
@os-zhuang
os-zhuang merged commit 081aa6f into main Jul 31, 2026
18 checks passed
@os-zhuang
os-zhuang deleted the claude/flow-run-summaries-xddi6c branch July 31, 2026 10:29
os-zhuang added a commit that referenced this pull request Jul 31, 2026
…ncomplete, instead of guessing (#4354) (#4397)

#4354 shipped selected/acted counts sourced from the executors that know what
they did, but left out the four node types a flow uses to act on anything
OUTSIDE the platform. That gap was not cosmetic: a sweep whose whole job runs
through a connector reported `acted: 0` and looked exactly like the dead sweep
the counter exists to find. A detector that fires on healthy runs is worse than
none — operators tune it out, and then it is not watching the flows that really
did stop.

Closing it needed a third answer, because for two of those nodes the platform
genuinely cannot know what happened:

- `connector_action` — `ConnectorActionDescriptor` declares nothing about
  whether an action reads or writes, so `acted: 0` understates a create and
  `acted: 1` overstates a lookup (and makes the alert never fire, which is the
  original bug one layer out). Reports `unmeasuredEffect` instead. #4395
  proposes declaring the effect kind, which would make it a real count.
- `http` — knowable from the method. GET/HEAD/OPTIONS report a real `acted: 0`;
  an accepted mutating call reports 1; `durable: true` reports 1 (the outbox row
  is a durable effect this run caused); a rejected or timed-out mutating call
  reports unmeasured, because a 500 can arrive after the write landed.
- `script` — deliberately unchanged. A registered function is contractually pure
  (data I/O stays on the flow graph), so reporting no record metrics is accurate
  rather than a guess. Nothing enforces that purity — filed as #4396 rather than
  papered over, since a blanket `unmeasuredEffect` here would suppress the
  signal on every flow calling any function to cover one contract violation.

The alert gains a clause: `selected > 0 AND acted = 0 AND unmeasured = 0`, with
an `unmeasured_count` column to serve it — without the third clause it fires on
every healthy connector-driven flow. The log line gains `unmeasured=N` only when
non-zero, since its PRESENCE is what a reader must not miss.

`unmeasured` propagates through subflow/map roll-ups and `creditChildRun`, so a
parent whose child dispatched an uncountable effect knows its own `acted` is
incomplete. `FlowRunSummary.unmeasured` is optional and undefined is NOT 0: a
run recorded before this existed did not track uncountable effects at all.

Verified: service-automation 546/47 (21 new), spec 7193/281 (2 new); all 8
check:generated gates plus the seven pure audits; check:nul-bytes and eslint
clean. Branch restarted from main after #4377 merged; #4347's fix confirmed
first by running that issue's own repro.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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/xl tests tooling

Projects

None yet

2 participants