做 #5636 时在同一条 #3017 降级路径上扫到的,但在**另一个文件**(`engine.ts`)、另一个方法里,契约也不同,因此不在那一单范围内,按 Prime Directive #10 单开。 ### 现象 `packages/services/service-automation/src/engine.ts:1659`(origin/main `5b60b3669`): ```ts registerDegradedConnector(def: Connector, reason: string, origin: ConnectorOrigin = 'declarative'): void { const parsed = ConnectorSchema.parse(def); this.assertSameOriginOrFree(parsed.name, origin); this.connectors.set(parsed.name, { def: parsed, handlers: {}, origin, state: 'degraded', degradedReason: reason }); this.logger.warn(`Connector registered DEGRADED: ${parsed.name} (origin: ${origin}) — ${reason}`); } ``` 那个 `reason` 参数就是 #5636 处理的同一个值:`plugin.ts` 的 `degradeConnectorInstance` 把 `ConnectorUpstreamUnavailableError.message` 原样传进来,而该 message 由**第三方 provider factory** 构造(ADR-0097 明确鼓励第三方去写 provider factory;spec 只定义错误类、不约束 文本),完全可以是多行。 ### 为什么它比 #5636 修掉的那两处更容易被忽略 调用顺序决定了它**先**发生,而且发生在**常见分支**上: 1. `degradeConnectorInstance` → `engine.registerDegradedConnector(husk, info.reason, …)` → **这一条 warn**(husk 注册成功时就会打,也就是每次首次降级都打); 2. 只有 husk 注册**抛错**时才走到 plugin 侧那条 warn(#5636 已修); 3. 然后才是 plugin 侧的降级公告 error(#5636 已修)。 也就是说 #5636 修完之后,首次降级的默认路径上仍然留着一条会溢出的 warn。 ### 危害机制(与 #5636 同一条,已实测) - `ObjectLogger` 把 `warn` 送 **stdout**,`serve` 的启动静默窗口只包了 `process.stdout.write`,窗口从 config 加载前一直开到 banner 打印; - 冷启动的 `materializeDeclaredConnectors(ctx, { fatal: true })` 遇到上游不可达是**降级、 不抛错**,所以这条路径在窗口内就会跑; - `BootLogCapture.offer()` 只在 `classifyBootLogLine` 能在物理行上找到 `<ts> <LEVEL>` 头时 才保留该行,续行**直接丢弃**。 #5636 的 PR 里对一份 13 行的插值 ZodError dump 实测:写出 13 行,缓冲保留 1 行(止于 Zod `[` 的头行)、丢弃 12 行。这里的载荷不是 ZodError 而是 provider 的 message,机制一样。 即 cloud#971 的原始形态。 ### 可达性(为什么是 finding 而不是 bug) 与 #5575 / #5636 同样的理由:今天 `packages/connectors/*/src` 里 `ConnectorUpstreamUnavailableError` 的 message 在 openapi / mcp / rest / slack 里都是我们自己写的单行文本。第一个把上游 SDK 的 多行错误塞进该错误类的第三方 provider 插件会撞上。 ### 修法上需要先定的一件事(不是零决策) 与 #5636 的两处不同,这里**没有**抛出值可用:`registerDegradedConnector` 的签名只收 `reason: string`。而且这个字符串同时是 `degradedReason`,会经 `GET /connectors` 和 `connector_action` 被拒时的文本出去 —— #5636 刻意保持它逐字不变(人透过 JSON 读,不经按行 切分的消费者)。所以至少两条路: - **A:签名加一个可选 `cause?: unknown`**,让 `warn` 用 `describeThrownForLog(cause)` 走 meta, `reason` 与 `degradedReason` 一字不动。调用点(只有 plugin.ts 一处)顺手把 `err` 传进来。 - **B:这条 warn 不再插值 `reason`**,只报 name/origin,reason 作为 meta 字段 (`{ degradedReason: reason }`,注意字段名不得含 `key`/`token`/`secret`/`password` 子串, 见 #5573)。不动签名。 倾向 **A**:它与 #5048 / #5575 / #5636 建立的形状一致(抛出值一路带到报告点,不在边界先 拍平成字符串),而且 `ConnectorUpstreamUnavailableError` 本身还带一个 `cause`(底层 connect 错误),A 之后才有可能把它也渲染出来。B 更小,但把「reason 是给人读的文本」和「日志要结构化」 这两件事压在同一个字符串上,下一个接缝还会再遇到。 ### 关联 #5636 / PR(plugin 侧两处,同一条路径)、#5575 / PR #5639、#5048 / PR #5572、#5573(脱敏按 子串匹配)、#4632、#3017(降级/重试本身)、cloud#971、ADR-0097。
Activity
os-zhuang commentedon Aug 5, 2026
发现分诊(#4949 纪律):晋级——摘
finding换pm:queue,保留domain:services。一行理由:同族先例 #5048 / #5575 / #5636 已确立形状并全部入队(#5636 上轮晋级),本条是同一冷启动路径上默认分支先发生的那条 warn,可达性与家族一致;修向裁定按 A 路(
registerDegradedConnector签名加可选cause?: unknown,warn 经describeThrownForLog走 meta,reason/degradedReason一字不动)——与家族既定形状同源,B 路正文已自述会把两种职责压在同一字符串上。落点service-automation/src/engine.ts:1659。车道注意:#5636 若仍在飞,同路径(不同文件)按依赖顺序串行。维护者可否决。本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
os-zhuang commentedon Aug 6, 2026
认领(services 车道 PM 派发,
session_01BWS4heBoAitLmzCLhcYdbK)claude/issue-5660-degraded-register-cause../objectstack-issue-5660(dev 子代理按 worktree-first 自建)packages/services/service-automation/src/engine.ts(registerDegradedConnector签名加可选cause?: unknown+ warn 走 meta,立单行号 :1659 随 feat(spec,service-automation,devx): 未声明 resumeAuthority 的 pausing 节点类型不再静默 fail-open (#5561) #5725 落地已漂)、src/plugin.ts(唯一调用点顺手传err)、src/thrown-cause-diagnostics.ts(复用)、同包测试、.changeset/*.md。reason/degradedReason一字不动。resumeAuthoritydefaults to'any', so every future pausing node ships fail-open — ADR-0044 says this is "tracked separately" and nothing tracks it #5561(PR feat(spec,service-automation,devx): 未声明 resumeAuthority 的 pausing 节点类型不再静默 fail-open (#5561) #5725)、plugin.ts 前序 finding(service-automation): plugin.ts 里还有三处外来${err.message}插进日志 message —— 其中两条是耐久性诊断,正是最不该被切碎的记录 #5661(PR fix(service-automation): 启动路径三条日志的外来 cause 改走结构化 meta (#5661) #5738)均已 merged,automation 槽位空。Generated by Claude Code
fix(service-automation): the degrade REGISTRATION logs its cause as m…
fix(spec): 重锚 authorable-surface.base.json 改为显式动作,构建不再顺手推进删除门的锚点 (obj…