Skip to content

fix(service-automation): wait 节点五条日志的外来 cause 移出 message,改走 meta (#5737) - #5911

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-5737-wait-node-cause-meta
Aug 6, 2026
Merged

fix(service-automation): wait 节点五条日志的外来 cause 移出 message,改走 meta (#5737)#5911
hotlong merged 1 commit into
mainfrom
claude/issue-5737-wait-node-cause-meta

Conversation

@hotlong

@hotlong hotlong commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #5737

packages/services/service-automation/src/builtin/wait-node.ts 里有五处把我们不控制文本的失败原因(数据源驱动、job 服务、engine.resume() 的错误信封)直接拼进日志 message。本 PR 把五处统一改成:message 单行自足,cause 走 meta。与 #5048 / PR #5572#5575 / PR #5639#5636 / PR #5662#5661 完全同一套修法,零新词汇。

前提复核(rule 6):成立,且行号表没有过期

派单里的机制假设 1 预判「五处行号表已过期(今日 58ffcab 触过该文件)」。实测证伪:在 origin/main 7b005b4 上,五处仍然精确落在 issue 表列出的行号上 —— 58ffcab(#5760)改的是 objectql,并未移动本文件的这些行。

我按要求自己枚举了该文件所有「外来 cause 进 message」的位点(不限 Cause: 拼写),全集就是这五处:

级别 参数位 cause 来源 原拼写
:98 error 第三参 engine.resume() 的信封字段 result.error Cause: ${...}
:216 warn 第二参 job 服务(job.schedule 抛出) (${...}) 括号形态,非 Cause:
:323 error 第三参 挂起态存储 / 驱动(issue 实测的那一条) Cause: ${...}
:346 error 第三参 engine.resume() 抛出 Cause: ${...}
:387 error 第三参 job 服务(job.schedule 抛出) Cause: ${...}

考察后排除的插值(留在 message 里,并在代码注释里写明理由)::362 / :384wakeAt 是本文件自己写入、又在两个分支之前用 typeof === 'string' 加非 NaN 的 Date.parse 守过的期限值;jobName / runId / node.id 是平台自己铸造或作者声明的标识符。都不是外来 cause。

:98 单独判断的结论(机制假设 2)

以 helper 实际签名为准:describeThrownForLog(err: unknown) 靠鸭子类型读抛出对象.issues / .message。而 :98 的 cause 是 AutomationResult.error,契约里就是 error?: string(packages/spec/src/contracts/automation-service.ts)—— 一个引擎已经拼好的字符串,不是抛出值。传给 helper 有两个后果:字符串上它无处可读;undefined 时它会把字面量 "undefined" 渲染进记录,顶掉原本的 ?? 'store unavailable' 默认值。

所以这一处按 issue 的 fallback 走 { error: result.error ?? 'store unavailable' } 直接给 meta,字段名仍是 helper 自己的 ThrownCauseMeta.error —— 五条记录的非校验类 cause 仍只有一个键。已加用例钉住空信封那一支返回 'store unavailable' 而不是 "undefined"

顺带记一笔可达性:这个信封字符串本身就是引擎把驱动的 message 插值进去构造的(resumeInternalSTORE_UNAVAILABLE 返回),所以多行驱动错误是经两跳到达这条记录的。

危害机制

ObjectLogger.write() 一次调用只加一个「时间戳 + 级别」记录头,所以 message 里的换行会把一条记录变成多个物理行,后几行既无级别也无时间戳。在 pretty / text 格式(os dev / os serve 的默认)下,文件 sink 当成独立记录存,采集器读成无主碎片,grep ERROR 只捞到不含任何事实的那一行 —— 而 :323 / :346 / :387 这三条是 #4632 亲自定为 error耐久性诊断,存在理由就是给人读的(文本自己写着 every wait/approval paused before this restart will hang indefinitely)。cloud#971 即此形态。

参数位按 Logger 契约选,不凭记忆

packages/spec/src/contracts/logger.ts:warn(message, meta?) 第二参,error(message, error?, meta?) 第三参(第二参传 undefined,否则每条记录都带整个栈,#5575)。两个级别各有一个 spy 用例专门钉参数位。

反向验证(方向在跑之前就已预判)

预判:普通的红。把拼接式渲染放回去,新钉子必须变红 —— meta 里的 error 字段消失、message 重新含换行、pretty 下 stderr 从 1 个物理行变回 3 个。没有反转,因为这些断言钉的是「只有修复才会产生的 meta 字段」的存在

实测(git checkout -- 回退 wait-node.ts、测试文件不动):15 个用例转红,失败信息正好点名机制 ——

AssertionError: expected '[wait] suspended wait-timer re-arm AB…' not to contain '\n'
AssertionError: expected [ …(3) ] to have a length of 1 but got 3
AssertionError: expected [ Array(1) ] to have a length of 2 but got 1     ← warn 少了 meta 参数
AssertionError: expected undefined to be 'store unavailable'              ← 空信封那一支
AssertionError: expected '' to contain 'no such table: sys_automation_run'

随后 git apply 复原,全绿。

测试

新增 packages/services/service-automation/src/builtin/wait-node-log-cause.test.ts(13 例)。照 #5662 / #5661 的先例,凡问题是「按行切分的下游会看到什么」就读真 ObjectLogger 的真字节,spy 只出现在参数位本身是待验事实的地方:

  • 五个接缝各自一例:记录恒为一个物理行、msg 不含换行、后果与修复仍在 msg 里、cause 完整落在 meta 的 error 上;
  • :323 额外加一例 pretty 格式(实测就是在这个格式下取的);
  • :98 加空信封回落一例;
  • error / warn 各一例 spy 钉参数位;
  • 健康路径不多写一个字节;
  • 两例反向验证,把拼接渲染的代价当场量出来。

改动到的既有测试(fixture 分诊,逐条判定而非批量重拼):

  • wait-node-rearm-log-level.test.ts —— [convention] best-effort 降级导致"看起来正常、实则不持久"时不应记 warn——把 #4460 的点状修复定成规则 #4632 的级别钉子一条不动;它的 capturing logger 原本只收 msg,现在按契约分参数位收,三处 cause 断言从 msg 改读 meta,并加断了 msg 不再含 cause;
  • wait-node.test.ts —— :98 那条 connection refused 断言同样改读 meta 并加了反向断言;
  • plugin-startup-log-cause.test.ts —— 只简化 failProbe 的注释:它原来写着本文件那条记录「still interpolates(out of this issue's scope, filed separately)」,现在不成立了;fixture 本身保留,因为那里的 toHaveLength(1) 是按接缝计数的,窄化的理由从「躲开一条被搅烂的记录」变成「把捕获限制在它自己点名的那个接缝」。

命令与结果(全程持容器级验证锁、--max-old-space-size=4096--maxWorkers=2):

pnpm --filter '@objectstack/service-automation' test    →  Test Files 65 passed (65) / Tests 775 passed (775)
pnpm exec tsc --noEmit -p packages/.../tsconfig.json    →  5 errors,全部在 engine.test.ts / nested-region-parity.test.ts
                                                            这两个我未触碰的文件里(既存,故该包未定义 typecheck 脚本)
                                                            我改动的 5 个文件:0 error
npx eslint --no-inline-config (本 PR 的 5 个文件)        →  exit 0

验证门(从 .github/workflows/lint.yml 逐个枚举后跑,非凭记忆挑):

pnpm check:durability-log-level    →  24 durability-critical catch seam(s), all loud … (三处 error 未降级、未改 rethrow)
pnpm check:nul-bytes               →  OK (5706 files, no raw ASCII control bytes)
pnpm check:engine-double-contract  →  OK — 64 pinned, 139 DEBT, 2 exempt
pnpm check:init-service-contract   →  OK — 34 declared / 1 self-provided / 3 without provider
pnpm check:resume-authority-declared → OK — wait-node.ts:183 wait → declared (6/6)
pnpm check:startup-registry-verdict  → OK — 40 seam(s), none recording a contradictable verdict
pnpm check:release-notes / check:objectui-changeset / check:role-word / check:adr-anchors / check:doc-authoring → OK

packages/spec 未触碰,无 spec 产物重生成。

一个字节纪律的自我记录

新测试文件第一稿里,ANSI 引导符以裸控制字节的形式落进了文件(写的是转义序列,编辑工具在我正写到控制字符时把它实体化了)—— 正是 AGENTS.md 与 scripts/check-nul-bytes.mjs 点名的那类事故(#4890 同形)。已改回小写 u 转义序列拼写,check:nul-bytesgrep -naP 自查均干净,并在该函数的 docblock 里留了记录。

#5660 的关系(派单必答项)

完全无影响 —— 且 #5660 已于 2026-08-06T04:25:47Z 关闭(completed),issue 正文「仍开:#5660」这一句已过期。

理由:#5660engine.tsregisterDegradedConnector(连接器降级路径,warn,cause 是 reason: string 参数),与本单是不同文件、不同方法、不同契约;两者唯一的共同点是复用同一个 describeThrownForLog,而本 PR 没有改动那个 helper(只是新增调用点)。#5660 正文里真正待定的那件事是「registerDegradedConnector 签名要不要加 cause?: unknown」,本 PR 既没有改那个签名,也没有为它设下任何先例 —— 我的 :98 那处虽然同样绕开了 helper,但绕开的理由是「信封字段本就是 string,不是抛出值」,与 #5660 选项 A/B 的取舍(要不要把抛出值一路带到报告点)不同源。


Generated by Claude Code

`builtin/wait-node.ts` 有五处把我们不控制文本的失败原因(数据源驱动、job 服务、
`engine.resume()` 的错误信封)拼进日志 message。`ObjectLogger.write()` 一次调用
只加一个记录头,message 里的换行会把一条记录切成多个物理行,后几行无级别无时间戳
—— `pretty` / `text`(`os dev` / `os serve` 默认)下文件 sink 当独立记录存,
`grep ERROR` 只捞到不含事实的那一行。cloud#971 同形。

五处统一改为:message 单行自足,cause 交给 logger 的结构化参数位 —— 按 `Logger`
契约选位,`warn(message, meta?)` 第二参、`error(message, error?, meta?)` 第三参
(第二参留空,否则每条记录带栈)。与 #5048 / #5575 / #5636 / #5661 同一套修法。

其中 `:98` 单独判断:那里的 cause 是 `AutomationResult.error` 这个字符串信封字段而
非抛出值,`describeThrownForLog` 的鸭子类型无处可读、且对 `undefined` 会渲染出字面
量 "undefined",故直接给 `{ error: … }` —— 字段名仍是 helper 自己的 `error`。

三处 #4632 耐久性诊断级别不变,`pnpm check:durability-log-level` 24 个接缝仍全绿。

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

vercel Bot commented Aug 6, 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 Aug 6, 2026 11:28am

Request Review

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 6, 2026
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-automation.

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

  • content/docs/automation/flows.mdx (via @objectstack/service-automation)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/service-automation)
  • content/docs/plugins/packages.mdx (via @objectstack/service-automation)
  • content/docs/releases/implementation-status.mdx (via @objectstack/service-automation)
  • content/docs/releases/v9.mdx (via @objectstack/service-automation)

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.

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/l tests tooling

Projects

None yet

2 participants