做 #6230 (PR #6297 )时在同一个文件 扫到的旁生发现。#6230 的分诊评论已把那一单范围钉死在 loadSuspendedRun 一处、⛔ 不扩面,故按 Prime Directive #10 / objectstack#4949 单开。
现象
packages/services/service-automation/src/engine.ts(基线 origin/main dbe92a7e1 + PR #6297 ;⚠️ 行号会漂,以内容定位 )还有三处 与 #5912 / #6230 完全同形的拼接 —— 都是 logger.warn,都把我们不控制文本 的数据源驱动 message 插进 message:
forgetSuspendedRun(PR fix(service-automation): 降级版挂起态读取器的 warn cause 移出 message,改走 meta (#6230) #6297 后在 :1273)——
`[automation] failed to delete suspended run '${run.runId}' from durable store: ${(err as Error).message}`
cancelRun(:3528)——
`[automation] cancelRun: failed to load suspended run '${runId}' from durable store: ${(err as Error).message}`
listSuspendedRunsDurable(:3603)——
`[automation] failed to list suspended runs from durable store: ${(err as Error).message}`
三条的 thrown 值都来自 SuspendedRunStore 底下的同一个驱动,和 #5912 / #6230 是同一族。
危害机制
与 #6230 逐字相同,不重复推演:ObjectLogger.write() 一次调用只加一个「时间戳 + 级别」记录头,message 里的换行把一条 记录变成多个物理行,后几行无级别无时间戳,grep WARN 只捞到不含事实的那一行。
且三条都是 warn ⇒ 走 stdout ⇒ 落在 serve boot-quiet 缓冲的过滤面上(BootLogCapture.offer() 只保留有级别头的行),无头续行是被直接丢弃 。PR #6297 已把这个「丢弃」量出来了:三行驱动错误进过滤器,只剩 1 行 出来,而留下的那行不含任何驱动事实。
与 #6230 不同的地方:级别判定要逐条做,不能整批照抄
⚠️ 不要把这三条当成一次批量替换。 #6230 之所以判「级别不上调」,是因为 loadSuspendedRun 的 JSDoc 明写它是刻意的功能性 降级读取器。这三条各自的 #4632 判定要单独看,而且至少有一条看起来不一样:
所以这一单如果被晋级,接手的人必须逐条给出 #4632 判定并各自钉测试 ,而不是把 #6230 的模板套三遍。message 拼接那一半是三条共通的,级别那一半不是。
可达性(为什么是 finding)
与 #5575 / #5636 / #5661 / #5737 / #5912 / #6230 同样的理由:今天库内的驱动错误均为单行,所以是 finding 而非事故报告。第一个包装多行 SDK 错误的驱动撞上 —— Postgres 的 detail: / hint: 续行、better-sqlite3 包装器在生态里很常见,同族历次实测都是照这个形状造的。
关联
#6230 / PR #6297 (本发现的来源,同文件另三处)、#5912 / PR #6228 、#5737 / PR #5911 、#5661 、#5636 / PR #5662 、#5575 / PR #5639 、#5048 / PR #5572 、#5660 (同在 engine.ts 的 registerDegradedConnector,已闭)、#5573 、#5186 、#4632 、#4420 、cloud#971。
治完这三处,engine.ts 这一族就全净 了。
做 #6230(PR #6297)时在同一个文件扫到的旁生发现。#6230 的分诊评论已把那一单范围钉死在
loadSuspendedRun一处、⛔ 不扩面,故按 Prime Directive #10 / objectstack#4949 单开。现象
packages/services/service-automation/src/engine.ts(基线origin/maindbe92a7e1+ PR #6297;logger.warn,都把我们不控制文本的数据源驱动message插进 message:forgetSuspendedRun(PR fix(service-automation): 降级版挂起态读取器的 warn cause 移出 message,改走 meta (#6230) #6297 后在:1273)——`[automation] failed to delete suspended run '${run.runId}' from durable store: ${(err as Error).message}`cancelRun(:3528)——`[automation] cancelRun: failed to load suspended run '${runId}' from durable store: ${(err as Error).message}`listSuspendedRunsDurable(:3603)——`[automation] failed to list suspended runs from durable store: ${(err as Error).message}`三条的 thrown 值都来自
SuspendedRunStore底下的同一个驱动,和 #5912 / #6230 是同一族。危害机制
与 #6230 逐字相同,不重复推演:
ObjectLogger.write()一次调用只加一个「时间戳 + 级别」记录头,message 里的换行把一条记录变成多个物理行,后几行无级别无时间戳,grep WARN只捞到不含事实的那一行。且三条都是
warn⇒ 走 stdout ⇒ 落在serveboot-quiet 缓冲的过滤面上(BootLogCapture.offer()只保留有级别头的行),无头续行是被直接丢弃。PR #6297 已把这个「丢弃」量出来了:三行驱动错误进过滤器,只剩 1 行出来,而留下的那行不含任何驱动事实。与 #6230 不同的地方:级别判定要逐条做,不能整批照抄
loadSuspendedRun的 JSDoc 明写它是刻意的功能性降级读取器。这三条各自的 #4632 判定要单独看,而且至少有一条看起来不一样:forgetSuspendedRun的store.delete()失败意味着挂起态没有被消费却已从热缓存里删掉(this.suspendedRuns.delete(run.runId)在 try 之前),下次重启会把一个已经走完的运行当成仍然挂起 —— 这更像 [convention] best-effort 降级导致"看起来正常、实则不持久"时不应记 warn——把 #4460 的点状修复定成规则 #4632 的耐久性形态,warn是否正确需要判,不能默认沿用。cancelRun的读失败会让取消静默变成return false(幂等成功),调用方以为「本来就没有这个运行」。listSuspendedRunsDurable的列举失败会让durable 里的运行从列表里消失,只剩内存里那批 —— 这是check:durability-log-level结构性看不见「读接缝把故障答成空值」这一类 —— #4825 / #5108 全家都在闸门盲区里 #5186 read-seam invention 规则关心的形态,只是那条规则的扫描根(packages/metadata/metadata-protocol/objectql)不覆盖本包,所以pnpm check:durability-log-level今天不会报它。所以这一单如果被晋级,接手的人必须逐条给出 #4632 判定并各自钉测试,而不是把 #6230 的模板套三遍。message 拼接那一半是三条共通的,级别那一半不是。
可达性(为什么是
finding)与 #5575 / #5636 / #5661 / #5737 / #5912 / #6230 同样的理由:今天库内的驱动错误均为单行,所以是
finding而非事故报告。第一个包装多行 SDK 错误的驱动撞上 —— Postgres 的detail:/hint:续行、better-sqlite3 包装器在生态里很常见,同族历次实测都是照这个形状造的。关联
#6230 / PR #6297(本发现的来源,同文件另三处)、#5912 / PR #6228、#5737 / PR #5911、#5661、#5636 / PR #5662、#5575 / PR #5639、#5048 / PR #5572、#5660(同在
engine.ts的registerDegradedConnector,已闭)、#5573、#5186、#4632、#4420、cloud#971。治完这三处,
engine.ts这一族就全净了。