Skip to content

fix(service-automation): connector 降级路径的两条日志改用结构化 meta (#5636) - #5662

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5636-degrade-connector-cause
Aug 5, 2026
Merged

fix(service-automation): connector 降级路径的两条日志改用结构化 meta (#5636)#5662
os-zhuang merged 2 commits into
mainfrom
claude/issue-5636-degrade-connector-cause

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #5636

前提重验:两处接缝原样成立

立单时基于 cc5b048a0,其后 PR #5639(#5575)已合入同文件,行号确实漂移了。对
origin/main 5b60b3669 逐条核对后,两处接缝的代码形态一字未变,只是位置移到
plugin.ts:1273-1275(husk 注册失败的 warn)与 plugin.ts:1288-1294(降级公告的
error)。issue 描述的 reason 来源链也成立:plugin.ts:1187
reason: (err as Error).messageConnectorUpstreamUnavailableError.message 原样
传进 degradeConnectorInstance,而该 message 由第三方 provider factory 构造 ——
packages/spec/src/integration/connector-provider-errors.ts 只定义错误类,构造函数收
什么文本就是什么文本,不约束行数。

issue 提醒要实测而非照抄的两件事,都实测了:

危害:这条 warn 的下游与 #5575error 不同,而且是量出来的

ObjectLoggerwarn 送 stdout、error/fatal 送 stderr,serve 的启动静默窗口
只包了 process.stdout.write#5575 的接缝全是 error,所以那一单的结论是「启动缓冲
根本看不到」。这一条不同,四个条件都核实过:

  1. 它是 warn → stdout,缓冲确实看得到;
  2. 它在冷启动就会跑 —— materializeDeclaredConnectors(ctx, { fatal: true }) 遇到
    上游不可达是降级、不抛错(plugin.ts:1180);
  3. 窗口此时正开着 —— serve.ts:710 在 config 加载前接管 stdout,直到 banner 打印才
    恢复;
  4. BootLogCapture.offer() 只在 classifyBootLogLine 能在该物理行上找到时间戳 + 等级
    头时才保留它,续行直接丢弃

拿真 ObjectLogger + 真 BootLogCapture 对一份 13 行的插值 ZodError dump 跑了一遍:

### PRE-FIX  warn(`… : ${err.message}`)
physical stdout lines written : 13
BootLogCapture retained lines : 1
  | "2026-08-05T22:48:09.508Z WARN [Automation] could not register degraded husk for 'gh_mcp': ["
DROPPED (written but not retained): 12

### POST-FIX warn(msg, describeThrownForLog(err))
physical stdout lines written : 1
BootLogCapture retained lines : 1
  | "…WARN [Automation] could not register degraded husk for 'gh_mcp' (#3017). {"issues":[{…}]}"
DROPPED: 0

唯一被留下的那一行不含任何事实 —— 它止于 Zod 的 [。这是 cloud#971 的原始形态,不只是
「不好解析」。error 那一条走 stderr、不经缓冲,危害是 #5575 那一串按行消费者(文件
sink、docker logs/journald 送采集、grep ERROR):一条诊断散成 N 个无法归属的碎片。

改法

两条都复用同包 thrown-cause-diagnostics.tsdescribeThrownForLog(#5572/#5639
落地):message 是不含换行的自足句子,cause 走结构化 meta,位置按上面核实过的契约区分。
没有新增 meta 字段名,因此不涉及 #5573 的子串脱敏(issues / error 是既有字段,被拒
键名仍在 unrecognized 里)。

刻意没有改的一件事(取舍,附代码证据)

degradedReason —— engine.ts:1659 存进 registry、经 getConnectorDescriptors()
GET /connectors、并被 getConnectorDegradedReason() 用于 connector_action 被拒时
的文本 —— 仍然逐字保留 provider 自己的 message,包含换行。它是人透过 JSON 读的字段,
不经按行切分的消费者;重塑它属于另一次契约变更,不该搭在这一单里。

因此调用点同时传两个值,而不是把 reason 换成 cause:

// plugin.ts:1187 附近
reason: (err as Error).message,   // 喂 husk 与重试簿记,逐字不变
cause: err,                        // 只喂日志记录的结构化 meta

connector-degrade-cause.test.ts 双向钉住了这个分离:一条断言 degradedReason
getConnectorDegradedReason() 都等于那段多行原文,另一条断言日志 message 不含换行。

测试

新增 packages/services/service-automation/src/connector-degrade-cause.test.ts(9 例),
结构与 #5639connector-fail-cause.test.ts 对齐:端到端跑 logger、读字节,
不信 spy 单独作证。

pnpm --filter @objectstack/service-automation test
  Test Files  61 passed (61)
        Tests  730 passed (730)      # 新增前 60 / 721

connector-degrade-cause + connector-fail-cause + connector-materialization 三件套:
Tests 53 passed (53)

反向验证:方向先预测,再跑

预测(跑之前写下):把两处插值还原成修前形状,新增用例里读日志形状的那些必须变红
(插值多行 cause 会让 pretty 下不止一条物理行、msg 含换行、meta 里没有 cause),而
不读日志形状的那些必须保持绿。这是最常见的「红」方向,不是 #5018 那种反转。

实测:Tests 5 failed | 4 passed (9)

× a multi-line upstream message never reaches the log message
× renders as a single head-bearing line in `pretty` too
× calls error(message, undefined, meta) — the contract's third slot
✓ leaves `degradedReason` verbatim — the husk field is not the log message
× the ZodError becomes structured meta, not a 13-line stdout spill
× calls warn(message, meta) — `warn` has no Error slot
✓ the retry bookkeeping survives a husk that could not register
✓ the pre-fix `warn` shape loses its every fact to the boot buffer
✓ the pre-fix `error` shape splits one degrade into unattributable fragments

AssertionError: expected '[Automation] connector instance 'gh_…' not to contain a newline
AssertionError: expected [ …(3) ] to have a length of 1 but got 3

一处如实订正:我预测的是「6 红 / 3 绿」,实测「5 红 / 4 绿」。每一条用例的方向
都与预测一致(读日志形状的全红、不读的全绿),差的是我数错了有几条读日志形状 —— 9 例里
只有 5 例读,另外 4 例分别是 degradedReason 逐字保留、重试簿记存活、以及两条本地
「代价度量」复现(它们不经接缝,还原与否都绿)。记在这里而不是改成 5/4 假装当初就这么
预测。

用例怎么触到 warn 那一条

不 stub engine:buildDegradedHuskDefentry.type 经一次 cast 抄进 husk def,所以
声明一个 ConnectorTypeSchema 枚举外的 type(如 mcp_server)就让
registerDegradedConnector 里的 ConnectorSchema.parse 真抛 ZodError —— 正是那个 catch
注释写的「the entry's def no longer parses」。

门检查

check:nul-bytes            OK (5593 tracked text files, no raw ASCII control bytes)
check:type-check-coverage  OK — service-automation 的 DEBT 帐未变(raw tsc: 5 处既有,新增 0)
check:durability-log-level OK (24 个耐久性 catch 接缝)
check:startup-registry-verdict OK
check:adr-anchors / check:error-code-casing / check:doc-authoring  OK
eslint(改动的两个文件)      clean

字节纪律,两次命中,都如实记下:

  1. 用例里那个 ANSI strip 正则需要一个 ESC 转义(反斜杠 u 0 0 1 B 六个 ASCII 字符)。
    写文件时它被落成了 ESC 字节;grep -naP 自查抓到后用脚本按字节替换回转义序列,
    源码现在干净(check:nul-bytes 与自查都绿)。
  2. 同一条纪律也适用于工具载荷:本 PR 正文的初版里也带了一个真 ESC 字节 —— 恰恰是
    在写上面第 1 条、描述这个字节的那句话里落进去的。回读存档正文时发现,已改成纯文字
    描述。这正是 .claude/skills/** 的 markdown 不被任何门禁扫描 —— check:nul-bytes 只看 JS/TS,check:doc-authoring 的 ROOTS 不含 .claude/ #4890 的原样重演(在写「禁止真 NUL」这条规则的同时把真 NUL 写进了
    SKILL.md),也是 check:nul-bytes 只扫 NUL(0x00)—— 0x01-0x08 等控制字节不在扫描面,#5140 实测一个 0x01 会从 NUL-only 修复下溜走 #5157 的教训:写关于控制字符的东西时最容易把它写成真字节,
    所以正文必须回读校验,不能只信写出去的那一份。

消费半径扫过

retrying with backoff, attempt / register degraded husk / degradedReason
framework 全仓、../cloud../objectui 里都没有其他消费者(唯一命中就是本文件与新用例),
所以改 message 文本不牵动任何 fixture 或断言。

范围外发现(已按 Prime Directive #10 立单,未在本 PR 修)

搜重后确认无同题 issue,两单都 unassigned、finding 标签、无 pm:queue:

claude added 2 commits August 5, 2026 23:03
`degradeConnectorInstance`(#3017 的降级/重试路径)有两条记录报告的是外来失败,
却把它插进了日志 message —— 与 #5048(flow 绑定)、#5575(`fail()`)同一类,是那
两单范围之外的第三个接缝:

  • husk 注册失败(`warn`):`err` 来自 `registerDegradedConnector` →
    `ConnectorSchema.parse`,catch 注释自己写着「the entry's def no longer
    parses」,即预期接到的正是多行 `ZodError.message`(第一行只有一个 `[`)。
  • 降级公告(`error`):文本是 `ConnectorUpstreamUnavailableError.message`,由
    第三方 provider factory 构造(ADR-0097 鼓励第三方去写),spec 不约束其文本。

这条 `warn` 的下游与 #5575 的 `error` 不同,且是实测的:`warn` 走 stdout,`serve`
的启动静默窗口只包了 `process.stdout.write`,而冷启动的
`materializeDeclaredConnectors(ctx, { fatal: true })` 遇到上游不可达是降级不抛错,
所以它在窗口内就跑。`BootLogCapture.offer()` 只保留能被 `classifyBootLogLine` 找到
等级头的物理行 —— 对一份 13 行的插值 dump 实测:保留 1 行(止于 `[` 的头行)、丢弃
12 行,唯一留下的那行不含任何事实。即 cloud#971 的原始形态。

两条都复用同包 `describeThrownForLog`:message 单行自足,cause 走结构化 meta。位置
按 `Logger` 契约核实后区分 —— `warn(message, meta?)` 用第二参,
`error(message, error?, meta?)` 用第三参。

`degradedReason`(`GET /connectors` 与 `connector_action` 被拒时的文本)刻意逐字不变:
它是人透过 JSON 读的字段。因此调用点同时传 `reason`(那段文本)与 `cause`(抛出值),
测试双向钉住这个分离。

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

vercel Bot commented Aug 5, 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 5, 2026 11:06pm

Request Review

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

github-actions Bot commented Aug 5, 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

Development

Successfully merging this pull request may close these issues.

finding(service-automation): connector 降级路径(#3017)还有两处 ${err.message} 单行插值,是 #5575 之外的第三个接缝

2 participants