fix(service-automation): 降级版挂起态读取器的 warn cause 移出 message,改走 meta (#6230) - #6297
Conversation
) `engine.ts` 的 `loadSuspendedRun`(`loadSuspendedRunStrict` 的降级版读取器)在 catch 里把数据源驱动自己的失败文本插进了 `logger.warn` 的 message。 `ObjectLogger.write()` 一次调用只加一个「时间戳 + 级别」记录头,message 里的 换行把一条记录切成多个物理行,后几行无级别无时间戳。 这条比 #5912(PR #6228)那条多一层危害:`warn` 走 stdout,`serve` 的 boot-quiet 窗口只包了 `process.stdout.write`,`BootLogCapture.offer()` 仅保留带级别头的 物理行 —— 无头续行被直接丢弃,不只是被误读;且 boot 期真实可达 (plugin.ts start() -> rearmSuspendedWaitTimers -> engine.resume -> gate)。 实测:三行 better-sqlite3 驱动错误把这条告警切成 3 个物理行,过 boot 缓冲过滤 后只剩 1 行,而留下的那行不含任何驱动事实。 改法与同族前六单一致,零新词汇:message 单行自足并补上后果句(读失败被翻译成 null,调用方看到的与「本来就没有这个挂起运行」完全一样),cause 走 `Logger` 契约 `warn(message, meta?)` 的第二参 —— 与 `error` 的第三参不同,warn 没有 Error 槽。级别刻意仍是 `warn`:这是功能性降级(#4632),上调才是镜像误用。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015a5qkLzpGXhLL2F5gvJ7dD
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
顺带发现的 issue 号正文最后一节提到的三处同形拼接,已单开为 #6299(observation-class, 开单前按 objectstack#4949 做了检索(关键字 + 方法名 + 文件路径,
Generated by Claude Code |
Fixes #6230
前提复核(先做的事)
issue 正文按
1549605f6写的是:2931。本次在origin/maindbe92a7e1上按内容定位,方法定义仍在engine.ts:2931(#6228 昨日的改动落在同文件更靠后的:3036,没有推动这一段),logger.warn在:2935,文本逐字一致:前提成立,行号也没漂。
(err as Error).message来自loadSuspendedRunStrict底下的数据源驱动,与 #5912 是同一个 thrown 值 —— 我们不控制它有几行。顺带复核了正文的三条断言,均成立:
ObjectLogger.write()按isErrorLevel分流,warn走 stdout(packages/core/src/logger.ts:341-343);BootLogCapture.offer()对每个物理行先跑isBootDiagnostic,拿不到级别头就return—— 丢弃,不是降级保留(packages/cli/src/utils/boot-log-capture.ts)。plugin.tsstart()→rearmSuspendedWaitTimers→ 对 overdue 运行engine.resume(run.runId)(builtin/wait-node.ts)→resume()先跑refuseGatedResume→resolveEffectiveSuspension→ 这个降级版读取器。warn是第二参:Logger契约warn(message, meta?)(packages/spec/src/contracts/logger.ts:29),与error(message, error?, meta?)不同 ——warn没有Error槽。这是照抄 PR fix(service-automation): resumeInternal 存储不可达的日志 cause 移出 message,改走 meta (#5912) #6228 唯一会抄错的地方,单独一条用例钉住。危害(实测,不是推演)
反向验证跑出来的两个读数:
WARN头;offer()直接丢弃。第二个读数就是本单相对 #5912 多出来的那一档:#5912 是
error/stderr,被误读;这条是warn/stdout,被误读 + boot 期被丢弃。改法(同族第七次套既定模板,零新词汇)
复用同包
thrown-cause-diagnostics.ts的describeThrownForLog(engine.ts已有该 import):warn(message, meta?)的第二参;error/issues,天然不含 key/token/secret/password 子串(finding(core): ObjectLogger 的脱敏表按子串匹配,一个叫keys的普通字段会被整块换成***REDACTED***#5573);null,调用方(resume gate、screen 取数)看到的与「本来就没有这个挂起运行」完全一样,而运行本身未被触碰、仍停在原处;并指明严格读会报STORE_UNAVAILABLE而不降级。刻意不动的一处(已钉上回归测试)
级别仍是
warn。 按 #4632 的判据,这是一个刻意的功能性降级读取器 —— 它的 JSDoc 写明服务于「只需要 best-effort 答案」的顺带读取方,真正需要区分「存储挂了」与「运行没了」的resumeInternal用的是严格版。上调到error会在整个故障期间对每次 gate 查询报警,正是 #4632 明确警告的镜像误用。the level is not raised一条用例钉住(同时断言 stderr 为空)。⛔ 未扩面:
resumeInternal(#6228 已治)与返回值信封字段一行未动。反向验证 —— 方向先声明,再跑
预判:还原被删的拼接肢(cause 塞回 message、去掉 meta 第二参)⇒ 7 条拼接/行数/丢弃/参数槽钉转红;2 条保持绿 ——
the level is not raised(还原不改级别)与still degrades to null(还原不改行为),后者不动正是它们存在的意义,不是漏测。实跑,与预判逐条相符(7 红 2 绿):
1 → 3是危害本身的读数;Array(1) vs …(3)是丢弃的读数 —— 3 行进过滤器,1 行出来。测试
新增
packages/services/service-automation/src/degraded-suspended-run-load-log-cause.test.ts(9 例)。断言在真实ObjectLogger的真实字节上读(#5662 / #5661 / #5737 / #5912 的先例):spy 只用在「参数槽本身就是待验事实」的那一条。三点与 PR #6228 不同,都是本单形态决定的:
warn隔开;本单反过来。主路径走getSuspendedScreen()—— 到这个降级读取器最干净的公开入口,一次调用一条记录,所以物理行计数是这一条记录自己的读数。bootFilterRetains()就地复述classifyBootLogLine+isBootDiagnostic的判据 —— 本包不能依赖@objectstack/cli,而这个判据本来就是所有按行消费者共用的那个。这样「丢弃」是被量到的,不是在正文里描述的。resume()同时走两个 catch,现在 stdout 一条干净warn、stderr 一条干净error,两条都过 boot 过滤。这是 finding(service-automation): engine.ts 的 resumeInternal 把驱动错误插进 message —— #5737 修完后,这条 resume 路径上仅剩的一处外来 cause 拼接 #5912 与 finding(service-automation): engine.ts 的 loadSuspendedRun 把驱动错误插进 warn 的 message —— 与 #5912 同文件、同一次失败的另一半,且这条会被 boot 缓冲丢弃 #6230 单独任何一单都取不到的整条路径读数。未引入任何新的 fake ObjectQL engine(用的是手写
SuspendedRunStore,delete(runId)是该 store 契约、不是IDataEngine),因此不涉及assertEngineDeleteDispatch;pnpm check:engine-double-contract仍绿。一个过程中的教训(已在代码里结构性堵掉)
写 boot 过滤器的 SGR 剥离时,编辑工具把源码里的一个 backslash-u001B 形式的转义物化成了真实的 0x1B 控制字节 —— 正是在「写关于控制字符的代码」时发生的那个事故形态(#4890 / PR #5140)。自扫(
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]')当场抓到并修掉了。修法不是把转义再写一遍,而是换一种写不出控制字节的写法:check:nul-bytes现在绿,自扫也绿。Changeset
走 changeset(
@objectstack/service-automation: patch),不走skip-changeset:与 PR #6228 同型判断 —— 这不是纯测试 / 纯 workflow 改动,而是运维可见的日志形状变更(按记录末尾驱动文本字面量 grep 这条记录的查询,需要改成读记录的error字段)。同族前六单无一例外都写了 changeset。顺带发现(未在本 PR 修,已另开单)
同文件还有三处完全同形的
logger.warn拼接,分别在forgetSuspendedRun/cancelRun/listSuspendedRunsDurable。它们在其它方法、其它分支上,分诊评论已把本单范围钉死在loadSuspendedRun一处,故按 Prime Directive #10 / objectstack#4949 单开为 observation-class finding,未在此 PR 顺手扩面 —— 见下方评论里的 issue 号。Generated by Claude Code