做 #5737(PR #5911)时在同一条 resume 路径上扫到的旁生发现,但在另一个文件、另一个方法里,因此不在那一单范围内,按 Prime Directive #10 / objectstack#4949 单开。
现象
packages/services/service-automation/src/engine.ts:3014(origin/main 7b005b4),resumeInternal 读挂起态存储失败的那一支:
try {
run = await this.loadSuspendedRunStrict(runId);
} catch (err) {
const message = (err as Error).message;
this.logger.error(
`[automation] durable suspended-run store unreachable while resuming '${runId}': ${message}`,
);
return {
success: false,
code: 'STORE_UNAVAILABLE',
error: `Durable suspended-run store unreachable for run '${runId}' — retry once the store is available: ${message}`,
};
}
message 来自 loadSuspendedRunStrict,即数据源驱动自己的失败文本 —— 我们不控制它有几行。它被插进 logger.error 的 message(第 3017 行)。
为什么 #5737 没有覆盖它,以及为什么现在它更显眼
#5737 修的是 builtin/wait-node.ts 的五处。其中 :98 那处的 cause 正是这里第 3022 行造出来的信封字符串 —— 也就是说驱动的多行文本经这个信封两跳到达 wait-node 的记录。PR #5911 已经把信封那一跳接住了(落 meta),但第 3017 行这条 logger.error 自己照旧拼接。
所以修完 #5737 之后,同一次「resume 时存储不可达」会产生两条记录:wait-node 那条现在是干净的一行,engine 这条仍会被切碎。
危害机制(与同族四单同一条)
ObjectLogger.write() 一次调用只加一个「时间戳 + 级别」记录头,message 里的换行把一条记录变成多个物理行,后几行无级别无时间戳。pretty / text(os dev / os serve 默认)下文件 sink 当独立记录存,grep ERROR 只捞到不含事实的那一行。error 走 stderr,不经 serve 的启动静默缓冲,所以危害是被误读而非被丢弃(与 #5661 第 2、3 条同形)。cloud#971 即此形态。
级别本身是对的:存储不可达而运行仍在盘上,是 #4632 定义的耐久性降级,error 不应下调。
可达性(为什么是 finding)
与 #5575 / #5636 / #5661 / #5737 同样的理由:今天库内的驱动错误均为单行,所以这是 finding 而不是事故报告。第一个包装多行 SDK 错误的驱动撞上 —— 数据库驱动的多行错误(Postgres 的 detail: / hint: 续行、better-sqlite3 包装器)在生态里很常见,#5737 的实测就是照这个形状造的。
修法(同族既定模板,零新词汇)
复用同包 thrown-cause-diagnostics.ts 的 describeThrownForLog:message 保持单行自足,cause 走 meta。按 Logger 契约(packages/spec/src/contracts/logger.ts)选参数位 —— error(message, error?, meta?) 用第三参,第二参传 undefined,否则每条记录都带整个栈(#5575)。新增字段名不得含 key / token / secret / password 子串(#5573)。
一个需要顺带判断的点(不是阻塞项):第 3022 行的信封字段 error 是否也该停止插值。倾向不动 —— 它是给调用方读的结构化返回值(经 REST 出去时是 JSON,不经按行切分的消费者),#5636 对 degradedReason 就是刻意保持逐字不变的;而且 PR #5911 已经让 wait-node 侧把它整体放进 meta,多行文本由 JSON.stringify 转义后完整保留。
关联
#5737 / PR #5911(wait-node 五处,本发现的来源)、#5661(plugin.ts 三处)、#5636 / PR #5662、#5575 / PR #5639、#5048 / PR #5572、#5660(同在 engine.ts 但在 registerDegradedConnector,已闭)、#5573、#4632、#4420、cloud#971。
做 #5737(PR #5911)时在同一条 resume 路径上扫到的旁生发现,但在另一个文件、另一个方法里,因此不在那一单范围内,按 Prime Directive #10 / objectstack#4949 单开。
现象
packages/services/service-automation/src/engine.ts:3014(origin/main7b005b4),resumeInternal读挂起态存储失败的那一支:message来自loadSuspendedRunStrict,即数据源驱动自己的失败文本 —— 我们不控制它有几行。它被插进logger.error的 message(第 3017 行)。为什么 #5737 没有覆盖它,以及为什么现在它更显眼
#5737 修的是
builtin/wait-node.ts的五处。其中:98那处的 cause 正是这里第 3022 行造出来的信封字符串 —— 也就是说驱动的多行文本经这个信封两跳到达 wait-node 的记录。PR #5911 已经把信封那一跳接住了(落 meta),但第 3017 行这条logger.error自己照旧拼接。所以修完 #5737 之后,同一次「resume 时存储不可达」会产生两条记录:wait-node 那条现在是干净的一行,engine 这条仍会被切碎。
危害机制(与同族四单同一条)
ObjectLogger.write()一次调用只加一个「时间戳 + 级别」记录头,message 里的换行把一条记录变成多个物理行,后几行无级别无时间戳。pretty/text(os dev/os serve默认)下文件 sink 当独立记录存,grep ERROR只捞到不含事实的那一行。error走 stderr,不经serve的启动静默缓冲,所以危害是被误读而非被丢弃(与 #5661 第 2、3 条同形)。cloud#971 即此形态。级别本身是对的:存储不可达而运行仍在盘上,是 #4632 定义的耐久性降级,
error不应下调。可达性(为什么是 finding)
与 #5575 / #5636 / #5661 / #5737 同样的理由:今天库内的驱动错误均为单行,所以这是
finding而不是事故报告。第一个包装多行 SDK 错误的驱动撞上 —— 数据库驱动的多行错误(Postgres 的detail:/hint:续行、better-sqlite3 包装器)在生态里很常见,#5737 的实测就是照这个形状造的。修法(同族既定模板,零新词汇)
复用同包
thrown-cause-diagnostics.ts的describeThrownForLog:message 保持单行自足,cause 走 meta。按Logger契约(packages/spec/src/contracts/logger.ts)选参数位 ——error(message, error?, meta?)用第三参,第二参传undefined,否则每条记录都带整个栈(#5575)。新增字段名不得含 key / token / secret / password 子串(#5573)。一个需要顺带判断的点(不是阻塞项):第 3022 行的信封字段
error是否也该停止插值。倾向不动 —— 它是给调用方读的结构化返回值(经 REST 出去时是 JSON,不经按行切分的消费者),#5636 对degradedReason就是刻意保持逐字不变的;而且 PR #5911 已经让 wait-node 侧把它整体放进 meta,多行文本由JSON.stringify转义后完整保留。关联
#5737 / PR #5911(wait-node 五处,本发现的来源)、#5661(
plugin.ts三处)、#5636 / PR #5662、#5575 / PR #5639、#5048 / PR #5572、#5660(同在engine.ts但在registerDegradedConnector,已闭)、#5573、#4632、#4420、cloud#971。