@lqc123qwe/car-runtime 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -226,9 +226,33 @@ node --experimental-transform-types --test test/*.spec.ts
226
226
  - **1.6 工程收口(2026-09-28,`6e0315d`)**:候选池三块全部兑现(1.4/1.5 冻结表「登记后续」全数出清)。全量 **318(315 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 + secrets-scan commit 后复跑 117 文件 0 命中 + contract-check 4/4(golden 不动)。**1.6-插曲-1(stdio 响应乱序)为 spawn E2E 按序断言直接产出**;**1.6-BUG-1(jiti interopDefault 缺省 true)被 s35 零改动验收线捕获**。
227
227
  - **1.6.0 正式发布(2026-09-28,S5 收官)**:npm `@lqc123qwe/car-runtime@1.6.0` 在架——**dist-tags latest → 1.6.0、rc 保留 1.0.0-rc.1** + provenance。Release `1.6.0`(id 398135305,agent GCM 凭据 API 创建)→ **j16 run 36408091143 全步 success**(构建/keyless 签名/自动上传五件套/npm publish)→ **post-publish 13 PASS / 0 DRY-RUN / 0 FAIL(REMOTE_CHECK=1 全实查)**;外部等价检查 **SHA256 MATCH(`dadc1a3c…`)+ Rekor 交叉验证**(logIndex 2981799459 双端一致 + 透明日志 body 内制品哈希三方一致——tree 键形态差异登记见收口报告 §5)。
228
228
  - **遗留登记(1.7 候选池)**:遥测 OTLP 增强(两度延后);MCP legacy HTTP+SSE 维持不追溯;写类工具人审 confirm 通道宿主面外推(D-25 口径);插件 jiti 轨收敛观察项(插件仍走原生剥离,可选演进非缺陷)。规划:`car-docs/10-内核v1/1.6-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.6-收口终验报告.md`。
229
+
230
+ ## 1.7 状态(OTLP span 语义收口 + metrics 时序修正 + mcp-serve span 面)
231
+
232
+ | `src/runtime-core/telemetry.ts` | 1.7-S1(D-26)**OTLP span 语义标准化**:recordException → OTel 规范形态(span event name='exception',exception.type/exception.message 事件属性 + timeUnixNano)+ span status → STATUS_CODE_ERROR——替换 1.1 起「仅 exception.message 属性 + 恒 UNSET」非标准形态;**stacktrace 缺省不出站**(防路径/源码面外泄——隐私三原则正向收紧,可选开关不随本迭代引入);addEvent → 标准 events 数组编码(name + 原形态属性 + 事件时刻——替换 `event.<name>` JSON 属性串,零生产调用点依赖旧编码实证);出站体 spans[] 加法字段 events/status(无事件 span 不出 events 键——空形态合法);正常 span 恒 STATUS_CODE_UNSET 行为不变 | 1.4 遗留「OTLP 增强」出清主体 |
233
+ | `src/runtime-core/telemetry.ts` | 1.7-S2(D-27)**metrics CUMULATIVE 时序修正**:flush 去 counters.clear()——每次导出全量累计快照(真 CUMULATIVE);1.2-S3 起 clear() 与 CUMULATIVE 标注矛盾出清(intervalMs>0 自动导出时下游把增量解读为总量回落;缺省手动单次 flush 无害——潜伏缺陷面);内存有界由 label 基数保证(零内容枚举红线:AllowedLabels 3 计数器);DELTA 备选不采纳(生态主流 cumulative + S17 计数语义即累计) | 1.2-BUG-3 同源缝隙出清 |
234
+ | `src/runtime-core/trace.ts` + `src/cli.ts` | 1.7-S3(D-28)**mcp-serve turn span 面**:sessionTurn 于装配层拦截(1.4-S6 纪律同款)挂 car.turn span——facade 内部自增派生 turnId(T{n})调用前不可知,**TurnSpanHandle 加法 setTurnId 事后回填**(startTurn('') 空值不设占位属性;car run 直传路径不受影响);outcome = 归一化 turnEnd reason;handle 层 `{ok:false}` 吞错路径 span 以 error 收口 + exception 事件(D-26 形态在 turn 级成立);**不挂 step span**(mcp-serve 无 model() 循环——面外推登记不适用);离线运维工具(verify/replay/export)无 span 属口径 | span 生产覆盖出清(car run turn+step + mcp-serve turn) |
235
+ | `test/s39~s41` | s39 5 测(exception 事件形态+status ERROR/非 Error 兜底/addEvent 标准编码/无事件不出键/多事件保序)+ s40 3 测(双 flush 累计递增 5→7/无新增同值重出/多计数器独立累计)+ s41 2 测(spawn E2E turn span 出站 turnId=T1 回填+零内容断言/未知会话 error 收口+exception 事件) | 1.7-GO |
236
+
237
+ - **1.7 工程收口(2026-09-28,`5e39275`)**:1.4 遗留「遥测 OTLP 增强」(三度登记)经现状盘点拆解三块全部兑现(重试/采样/批上限/env 已随 1.2-S3、turn/step span 已随 1.4-S6——剩余缺口为语义正确性与覆盖面)。全量 **328(325 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 + secrets-scan 117 文件 0 命中 + contract-check 4/4(golden 不动)+ 零依赖静态断言保持。**1.7-插曲-1(TurnSpanHandle 缺 recordException——mcp-serve 失败路径 TypeError)为 s41 失败路径断言直接产出**;存量断言零改动(s29/s32 无出站体形态钉死——复核实证)。
238
+ - **1.7.0 正式发布(2026-09-28,S5 收官)**:npm `@lqc123qwe/car-runtime@1.7.0` 在架——**dist-tags latest → 1.7.0、rc 保留 1.0.0-rc.1** + provenance。Release `1.7.0`(id 398173870,agent GCM 凭据 API 创建)→ **j16 run 36414031672 全步 success**(构建/keyless 签名/自动上传五件套/npm publish)→ **post-publish 13 PASS / 0 DRY-RUN / 0 FAIL(REMOTE_CHECK=1 全实查)**;外部等价检查 **SHA256 MATCH(`24bb6c40…`)+ Rekor 交叉验证**(logIndex 2981897662 双端一致 + integratedTime 1790593823 + 透明日志 body 内制品哈希三方一致——tree 键形态差异延续 1.6 登记口径,详见收口报告 §5)。
239
+ - **遗留登记(1.8 候选池)**:W3C traceparent 跨进程传播(MCP stdio JSON-RPC 两端 + PTC worker 桥挂链——协议两端协同面大,1.7 延后登记);MCP legacy HTTP+SSE 维持不追溯(D-21);写类工具人审 confirm 通道宿主面外推(D-25 口径——前置未满足);插件 jiti 轨收敛观察项(可选演进非缺陷)。规划:`car-docs/10-内核v1/1.7-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.7-收口终验报告.md`。
240
+
241
+ ## 1.8 状态(W3C traceparent 跨进程传播 + LLM 出站通道修复)
242
+
243
+ | `src/runtime-core/trace.ts` | 1.8-S1~S3(D-29/D-30/D-31)**W3C traceparent 三边界挂链**:formatTraceparent/parseTraceparent(自研零依赖——version 00/32hex/16hex/全零拒绝,畸形 null fail-open 禁 fail-hard);TurnSpanHandle 加法 `mcpToolSpan`(car.mcp.tool client span 挂 turn——step span 已收口后工具执行;car.mcp_server/car.tool 登记标识符面)+ `ptcRelay`(workerTrace 载荷 + complete 收口 car.ptc span——error 路径 D-26 形态 + 负时长钳制);startTurn 加法 opts.trace(mcp-serve 远端挂链——traceId 复用 + parentSpanId);telemetry.ts 加法 time.startMs/end(atMs) 时刻覆盖(relay span——采样/buffer/出站路径零改动);自研 traceId 16B/spanId 8B hex 与 W3C 格式天然合规 | trace 隔离于单进程 → 跨进程可挂链 |
244
+ | `src/mcp/gateway.ts` + `src/host/stdio.ts` + `src/cli.ts` | 1.8-S1/S2(D-29/D-30)**MCP stdio 两端传播**:client 侧 callTool 加法 opts.traceparent → params._meta(MCP `_meta` 保留扩展点;**otel 关 = 不注入,JSON-RPC 字节面不变**;tools/list 不注入);server 侧 dispatch 第三参 meta 原样透传(解析归装配层),session_turn 提取挂链(畸形 fail-open 新 trace),非会话工具 meta 忽略(D-28 口径) | 响应路径回传登记不适用(W3C 请求携带语义) |
245
+ | `src/ptc/runCode.ts` + `worker-entry.ts` | 1.8-S3(D-31)**PTC worker 计时 relay**:worker 侧程序计时(同进程同时钟域)经 done 加法 span 字段回传,主线程经 spanBridge 单点收口 car.ptc span——**worker 零 trace 载荷注入、零独立出站通道(单 exporter 纪律)**;主侧 killer/worker error 兜底以主线程时钟收口(口径登记) | 1.8-插曲-1:workerData 载荷方案收敛为纯 relay(装饰性载荷无测试承载) |
246
+ | `src/runtime-core/llm.ts` | **1.8-BUG-1**:LLM 出站缺省通道 undici fetch → `nodeHttpSseFetch`(node:http(s) 流式 POST——AsyncIterable<Uint8Array> 契约不变;AbortSignal → req.destroy 对接 D-19;注入缝 fetchImpl 保留)——car run ≥3 次模型请求 + worker/子进程并存时 win32 退出 libuv 断言崩溃(0xC0000409,1.4-BUG-2 同源:彼时仅修遥测出站漏 LLM 面;E2E 三请求形态暴露 + 二分矩阵定位) | 出站通道口径须按通道类收口,非按调用点逐修 |
247
+ | `test/s42` | 7 测:W3C 编解码(畸形 fail-open 全集)/ mcpToolSpan(挂链+traceparent 形态+noop 零注入)/ startTurn 远端挂链 / ptcRelay(时刻覆盖+error+钳制+noop)/ callTool `_meta` 注入与缺省无键 / E2E-A car run 全链三点闭合(回显 traceparent ↔ OTLP car.mcp.tool spanId)+ car.ptc 计时窗口 / E2E-B mcp-serve 挂链+畸形 fail-open | 1.8-GO-5 生产调用点双向核查承载 |
248
+
249
+ - **1.8 工程收口(2026-09-29,`6522e98`)**:1.7 延后登记「traceparent 跨进程传播」出清(TRACE-1/2/3 三块)。全量 **335(332 PASS + 3 能力门控 skip / 0 fail)** + tsc strict 0 + secrets-scan 121 文件 0 命中 + contract-check 4/4(golden 不动)+ 零依赖静态断言保持。**1.8-BUG-1 于迭代内修复**(真缺陷登记 + 二分矩阵验证 + s42 E2E-A 回归承载);存量断言零改动(s12/s29/s32/s41 无新增面钉死——复核实证)。
250
+ - **1.8.0 正式发布(2026-09-29,S5 收官)**:npm `@lqc123qwe/car-runtime@1.8.0` 在架——**dist-tags latest → 1.8.0、rc 保留 1.0.0-rc.1** + provenance(slsa.dev/provenance/v1——registry attestations 端点实查,npm 形态演进登记)。Release `1.8.0`(id 398799183,agent GCM 凭据 API 创建,全 SHA 挂载——插曲:短 SHA 被拒)→ **j16 run 36517038845 全步 success**(构建/keyless 签名/自动上传五件套/npm publish)→ **post-publish 13 PASS / 0 DRY-RUN / 0 FAIL(全实查)**;外部等价检查 **SHA256 MATCH(`15c5e2cf…`)+ Rekor 交叉验证**(logIndex 2992647008 双端一致 + integratedTime 1790652315 + 透明日志 body 内制品哈希三方一致——tree 键形态差异延续 1.6/1.7 登记口径,详见收口报告 §6)。
251
+ - **遗留登记(1.9 候选池)**:远程 MCP(Streamable HTTP)traceparent 传播——HTTP `traceparent` 头通道(D-21 通道既有,1.8 延后登记);mcp-serve 响应路径回传 span 上下文(D-30 登记不适用——宿主消费面不存在);MCP legacy HTTP+SSE 维持不追溯(D-21);写类工具人审外推(D-25 口径维持);插件 jiti 轨维持观察;metrics 多标签 dataPoints 维持。规划:`car-docs/10-内核v1/1.8-迭代规划.md`;收口报告:`car-docs/10-内核v1/1.8-收口终验报告.md`。
252
+
229
253
  ## 状态
230
254
 
231
- - **版本主线**:S1-S4 48/48 | M2 89/89(v0.2.0)| M3 127/127(v0.3.0)| M4 139/139(**1.0.0-rc.1 已发布**)| M5 156/156(DEC-4 预埋项转正)| M6 179/179(2026-09-15 收口)| M7 全量 195(192 PASS + 3 能力门控 skip,2026-09-19 收口)| **M5-S28 装载签名门 全量 204(201 PASS + 3 skip,2026-09-26)** | **M8 运行时底座 全量 231(228 PASS + 3 skip,2026-09-26 收口,§3.2 八模块全部收口)** | **1.1 签名收尾最小集 全量 249(246 PASS + 3 skip,2026-09-27 收口,1.1.0 已发布,enforce 翻转准备就绪)** | **1.2 会话/遥测小项 全量 263(260 PASS + 3 skip,2026-09-27 收口,1.2.0 已发布)** | **1.3 发布资产自动化 全量 270(267 PASS + 3 skip,2026-09-27 收口,1.3.0 已发布,全链零人工拖拽兑现)** | **1.4 真实模型接入 全量 282(279 PASS + 3 skip,2026-09-27 收口,1.4.0 已发布)** | **1.5 provider 扩展+远程 MCP+配置完整形态 全量 300(297 PASS + 3 skip,2026-09-28 收口,1.5.0 已发布)** | **1.6 宿主工具面+投影收敛+jiti 载体 全量 318(315 PASS + 3 skip,2026-09-28 收口,1.6.0 已发布)**
255
+ - **版本主线**:S1-S4 48/48 | M2 89/89(v0.2.0)| M3 127/127(v0.3.0)| M4 139/139(**1.0.0-rc.1 已发布**)| M5 156/156(DEC-4 预埋项转正)| M6 179/179(2026-09-15 收口)| M7 全量 195(192 PASS + 3 能力门控 skip,2026-09-19 收口)| **M5-S28 装载签名门 全量 204(201 PASS + 3 skip,2026-09-26)** | **M8 运行时底座 全量 231(228 PASS + 3 skip,2026-09-26 收口,§3.2 八模块全部收口)** | **1.1 签名收尾最小集 全量 249(246 PASS + 3 skip,2026-09-27 收口,1.1.0 已发布,enforce 翻转准备就绪)** | **1.2 会话/遥测小项 全量 263(260 PASS + 3 skip,2026-09-27 收口,1.2.0 已发布)** | **1.3 发布资产自动化 全量 270(267 PASS + 3 skip,2026-09-27 收口,1.3.0 已发布,全链零人工拖拽兑现)** | **1.4 真实模型接入 全量 282(279 PASS + 3 skip,2026-09-27 收口,1.4.0 已发布)** | **1.5 provider 扩展+远程 MCP+配置完整形态 全量 300(297 PASS + 3 skip,2026-09-28 收口,1.5.0 已发布)** | **1.6 宿主工具面+投影收敛+jiti 载体 全量 318(315 PASS + 3 skip,2026-09-28 收口,1.6.0 已发布)** | **1.7 OTLP span 语义收口+metrics 时序修正+mcp-serve span 面 全量 328(325 PASS + 3 skip,2026-09-28 收口,1.7.0 已发布)** | **1.8 W3C traceparent 跨进程传播+LLM 出站通道修复 全量 335(332 PASS + 3 skip,2026-09-29 收口,1.8.0 已发布)**
232
256
  - **1.0.0 正式发布(2026-09-26,S29/S30)**:npm `@lqc123qwe/car-runtime@1.0.0` 在架——**dist-tag latest → 1.0.0,rc 保留指向 1.0.0-rc.1**(§1.1 口径);SLSA provenance(Actions OIDC,透明日志在案);GitHub Release v1.0.0 六资产(tgz/bundle/SHA256SUMS/SBOM/审计/公钥);**post-publish 全链 13 PASS / 0 DRY-RUN / 0 FAIL** + 外部验证者等价检查(Release 实下载件 SHA256 MATCH + cosign Verified OK)。**宽限期时钟:2026-09-26 起算,2026-12-25 到期 verifier 默认值 warn→enforce 翻转(计划内变更)**。终验报告:`car-docs/10-内核v1/M5-S30-1.0收口终验报告.md`。发布工程事件:j16 首跑 E403(npm 2FA 策略 vs granular token 无 bypass——清单预设回退路径兑现)→ bypass token re-run 绿;G-09 期望 tag 动态化(`4c84b17`)在真实发布兑现价值。
233
257
  - **1.0.0-rc.1 已发布(2026-09-09)**:npm `@lqc123qwe/car-runtime@1.0.0-rc.1`(rc + latest 双 tag,SLSA provenance 在案)+ GitHub Release 6 制品;发布预演 **13 PASS / 0 DRY-RUN / 0 FAIL**(c762b72)。S22 GO 清单已全绿收口。
234
258
  - **轨道 A 三项全部完成**:E-1 WSL2 逃逸矩阵 **20/20 PASS 零逃逸**(2026-09-07,G-07 转 PASS)| E-3 远端发布通道 **13/13 全门禁 PASS**(2026-09-09,G-08/09/10 转 PASS)| E-2 Windows koffi FFI **四判据双环境全 PASS**(S24/S25/S26,2026-09-09~10;本机非提权 + windows-latest 提权 runner 双绿,CI 门禁已接入)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lqc123qwe/car-runtime",
3
- "version": "1.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Composable Agent Runtime (CAR) - microkernel plugin system with sandboxed PTC, append-only hashed session log, and MCP host bridge",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -32,7 +32,7 @@ import { CredentialService } from './runtime-core/credentials.ts'
32
32
  import { chatStep } from './runtime-core/chatStep.ts'
33
33
  import { toToolDefinitions } from './runtime-core/tools.ts'
34
34
  import { createTelemetryFacade, telemetryConfigFromEnv } from './runtime-core/telemetry.ts'
35
- import { TurnTracer } from './runtime-core/trace.ts'
35
+ import { TurnTracer, parseTraceparent, type TurnSpanHandle } from './runtime-core/trace.ts'
36
36
  import type { CounterName, AllowedLabels } from './telemetry/metrics.ts'
37
37
 
38
38
  const HELP = `用法: car <command> [args]
@@ -216,6 +216,8 @@ async function main(): Promise<number> {
216
216
  }), { default: true })
217
217
  // MCP 工具进 turn(D-18):--mcp <serverId>(可重复;server 启动规格在 car.config.json mcp.servers 登记)
218
218
  const mcpIds: string[] = []
219
+ // 1.8-D29:活跃 turn span 句柄(startTurn 后赋值——MCP 工具闭包运行期取 client span 挂链面)
220
+ let activeTurn: TurnSpanHandle | null = null
219
221
  for (let i = 0; i < rest.length; i++) {
220
222
  if (rest[i] === '--mcp') {
221
223
  const id = rest[++i]
@@ -243,7 +245,22 @@ async function main(): Promise<number> {
243
245
  if (toolsMap.has(modelName)) { console.error(`CAR-E-MCP: 工具名冲突 ${modelName}(跨源同名禁静默覆盖)`); return 1 }
244
246
  toolsMap.set(modelName, {
245
247
  declaredSideEffect: t.declaredSideEffect ?? 'write',
246
- run: async (args: any) => { const r = await gw.callTool(id, t.name, args); if (!r.ok) throw new Error(r.error ?? 'mcp call failed'); return r.result },
248
+ // 1.8-D29:client span(car.mcp.tool 挂 turn)+ traceparent 注入 _meta——
249
+ // noop 门面 traceparent='' → 不注入(线上 JSON-RPC 字节面不变)
250
+ run: async (args: any) => {
251
+ const span = activeTurn?.mcpToolSpan(id, t.name)
252
+ const tp = span?.traceparent ?? ''
253
+ try {
254
+ const r = await gw.callTool(id, t.name, args, tp ? { traceparent: tp } : {})
255
+ if (!r.ok) throw new Error(r.error ?? 'mcp call failed')
256
+ span?.end('ok')
257
+ return r.result
258
+ } catch (e) {
259
+ span?.recordException(e)
260
+ span?.end('error')
261
+ throw e
262
+ }
263
+ },
247
264
  ...(t.description !== undefined ? { description: t.description } : {}),
248
265
  ...(t.inputSchema !== undefined ? { parameters: t.inputSchema } : {}),
249
266
  })
@@ -260,7 +277,7 @@ async function main(): Promise<number> {
260
277
  const { renderSdkFromRegistry } = await import('./ptc/sdk.ts')
261
278
  const registry = new Map([...toolsMap].map(([name, t]) => [name, { run: t.run, description: t.description, inputSchema: t.parameters }]))
262
279
  sdkBlock = renderSdkFromRegistry(registry)
263
- const ptc = makePtcToolDefinition({ tools: registry, audit: () => {} })
280
+ const ptc = makePtcToolDefinition({ tools: registry, audit: () => {}, spanBridge: () => activeTurn?.ptcRelay() ?? null })
264
281
  toolsMap.set(ptc.name, ptc)
265
282
  console.log(`[PTC] run_code 入工具面(桥接 ${registry.size} 工具;SDK 声明块进 system)`)
266
283
  }
@@ -270,6 +287,7 @@ async function main(): Promise<number> {
270
287
  if (sdkBlock) log.append('runtime', 'system', 'system', sdkBlock)
271
288
  log.append('user', 'user', 'T0', prompt)
272
289
  const turn = tracer.startTurn('T0')
290
+ activeTurn = turn // 1.8-D29/D-31:MCP 工具闭包 / PTC spanBridge 运行期取挂链面
273
291
  let stepNo = 0
274
292
  r = await runTurn({
275
293
  log, turnId: 'T0',
@@ -530,9 +548,14 @@ async function main(): Promise<number> {
530
548
  // 挂点在装配层(1.4-S6 纪律同款);facade 内部自增派生 turnId(T{n})——调用前不可知,
531
549
  // 经 setTurnId 回填;outcome = 归一化 turnEnd reason(枚举面零内容)。缺省关 = noop 句柄零出站。
532
550
  const tracer = new TurnTracer(otel)
533
- const server = createStdioServer(async (tool, args) => {
551
+ const server = createStdioServer(async (tool, args, meta) => {
534
552
  if (tool !== 'session_turn') return gw.handle(hostId, tool, args)
535
- const turn = tracer.startTurn('')
553
+ // 1.8-D30:远端挂链——_meta.traceparent 合法 → turn span 加入宿主 trace
554
+ // (traceId 复用 + parentSpanId=远端 spanId;畸形 fail-open 新 trace——遥测禁 fail-hard)
555
+ const parent = otel.enabled && meta?.traceparent ? parseTraceparent(meta.traceparent) : null
556
+ const turn = parent
557
+ ? tracer.startTurn('', { trace: { traceId: parent.traceId, parentSpanId: parent.parentSpanId } })
558
+ : tracer.startTurn('')
536
559
  try {
537
560
  const r = await gw.handle(hostId, tool, args)
538
561
  if (!r.ok) {
package/src/host/stdio.ts CHANGED
@@ -6,12 +6,16 @@
6
6
  * - 方法面:initialize/notifications 握手(MCP 协议兼容)+ tools/list(10 tool 能力发现)+ tools/call(分发到 HostGateway.handle);
7
7
  * - 所有到达请求与响应均经 HostGateway 审计(无旁路);未登记宿主在 handle 层拒绝;
8
8
  * - stdin 结束(宿主退出)→ 通道关闭,进程内状态由审计日志承载(BD-02 等价:不静默丢数据)。
9
+ *
10
+ * 1.8(D-30 W3C traceparent server 侧提取):tools/call params._meta 原样透传 dispatch 第三参——
11
+ * 解析/校验归装配层(trace.ts parseTraceparent fail-open),传输层保持无协议语义;未知键对端
12
+ * 忽略(MCP `_meta` 保留扩展点),缺席 = meta 缺省 undefined(既有调用方零改动)。
9
13
  */
10
14
  import { createInterface } from 'node:readline'
11
15
  import { HOST_TOOLS, type HostTool } from './hostGateway.ts'
12
16
 
13
17
  export interface StdioDispatcher {
14
- (tool: string, args: Record<string, unknown>): Promise<{ ok: boolean; result?: unknown; error?: string }>
18
+ (tool: string, args: Record<string, unknown>, meta?: { traceparent?: string }): Promise<{ ok: boolean; result?: unknown; error?: string }>
15
19
  }
16
20
 
17
21
  export interface StdioServer {
@@ -62,8 +66,10 @@ export function createStdioServer(dispatch: StdioDispatcher): StdioServer {
62
66
  if (req.method === 'tools/call') {
63
67
  const name = req.params?.name ?? ''
64
68
  const args = req.params?.arguments ?? {}
69
+ // 1.8-D30:_meta 原样透传(解析归装配层);缺席 = undefined
70
+ const meta = (req.params as { _meta?: { traceparent?: string } } | undefined)?._meta
65
71
  callChain = callChain
66
- .then(() => dispatch(name, args))
72
+ .then(() => dispatch(name, args, meta))
67
73
  .then(r => {
68
74
  if (r.ok) respond({ content: [{ type: 'text', text: JSON.stringify(r.result ?? {}) }], carOk: true })
69
75
  else respond(undefined, { code: -32000, message: r.error ?? 'host call failed' })
@@ -8,6 +8,16 @@
8
8
  * - Server 崩溃/超时 → BD-02 整体标记不可用,不传导进内核主链路(US-6 AC4)
9
9
  * - McpServerConfig.env 禁明文密钥(疑似凭据模式即拒绝,sk- 前缀等——§3.2.5 传参禁忌)
10
10
  * - 传输抽象:stdio 真实通道(JSON-RPC over 换行分隔 JSON)/ in-process 测试通道
11
+ *
12
+ * 1.8(D-29 W3C traceparent client 侧注入):callTool 加法 opts.traceparent → params._meta
13
+ * (MCP `_meta` 保留扩展点——未知键对端忽略,加法无害);仅 otel 显式开且 client span 存活时
14
+ * 非空(trace.ts mcpToolSpan)——**缺省不注入,JSON-RPC 字节面不变**;tools/list 不注入
15
+ * (注册期无 span 上下文,登记)。解析/校验归 trace.ts(parseTraceparent fail-open),本层原样透传。
16
+ *
17
+ * 1.9(D-32 远程 MCP 头通道):callTool 把 opts.traceparent 加法透传给 transport.send 第二参——
18
+ * 载体由传输层自选(HTTP 通道注入 `traceparent` 请求头〔W3C Trace Context 惯例〕;stdio 通道
19
+ * 忽略 opts,`params._meta` 载体不变)。`_meta` 注入口径一字节不动——otel 开时 HTTP 请求
20
+ * 双载体同值(头为主消费面,登记);otel 关时双载体零注入(头面与字节面不变)。
11
21
  */
12
22
  export interface JsonRpcRequest { jsonrpc: '2.0'; id: number; method: string; params?: unknown }
13
23
  export interface JsonRpcResponse { jsonrpc: '2.0'; id: number; result?: unknown; error?: { code: number; message: string } }
@@ -15,9 +25,10 @@ export interface JsonRpcResponse { jsonrpc: '2.0'; id: number; result?: unknown;
15
25
  /**
16
26
  * 客户端传输(CAR → MCP Server 方向)。M3-S11 双向分层:本接口为 client 侧对偶;
17
27
  * server 侧(宿主 → CAR)见 src/host/hostGateway.ts 的 ServerTransport。
28
+ * 1.9-D32:send 加法可选第二参 opts(既有 `send(req)` 实现结构兼容零改动)。
18
29
  */
19
30
  export interface ClientTransport {
20
- send(req: JsonRpcRequest): Promise<JsonRpcResponse>
31
+ send(req: JsonRpcRequest, opts?: { traceparent?: string }): Promise<JsonRpcResponse>
21
32
  /** 进程/通道存活状态 */
22
33
  alive(): boolean
23
34
  /** 主动终止 */
@@ -97,7 +108,7 @@ export class McpGateway {
97
108
  isAvailable(serverId: string): boolean { return this.#servers.has(serverId) && !this.#unavailable.has(serverId) }
98
109
 
99
110
  /** 调用:走统一权限/审计链路(无旁路);结果回填日志前必经 redact(调用方职责) */
100
- async callTool(serverId: string, tool: string, args: Record<string, unknown>, opts: { timeoutMs?: number } = {}): Promise<{ ok: boolean; result?: unknown; error?: string }> {
111
+ async callTool(serverId: string, tool: string, args: Record<string, unknown>, opts: { timeoutMs?: number; /** 1.8-D29:W3C traceparent(otel 显式开且 client span 存活时非空;缺省不注入 _meta) */ traceparent?: string } = {}): Promise<{ ok: boolean; result?: unknown; error?: string }> {
101
112
  const cfg = this.#servers.get(serverId)
102
113
  if (!cfg) throw new Error('CAR-A050001: unregistered MCP connection rejected(登记制)')
103
114
  if (this.#unavailable.has(serverId)) {
@@ -108,7 +119,12 @@ export class McpGateway {
108
119
  if (!this.#tools.has(key)) return { ok: false, error: `unknown tool "${tool}" on "${serverId}"` }
109
120
  try {
110
121
  const res = await Promise.race([
111
- cfg.transport.send({ jsonrpc: '2.0', id: this.#nextId++, method: 'tools/call', params: { name: tool, arguments: args } }),
122
+ // 1.9-D32:traceparent 加法透传第二参(载体归传输层——HTTP 头 / stdio 忽略);
123
+ // `_meta` 注入 1.8-D29 口径不动(otel 开 = 双载体同值)
124
+ cfg.transport.send({
125
+ jsonrpc: '2.0', id: this.#nextId++, method: 'tools/call',
126
+ params: { name: tool, arguments: args, ...(opts.traceparent ? { _meta: { traceparent: opts.traceparent } } : {}) },
127
+ }, opts.traceparent ? { traceparent: opts.traceparent } : undefined),
112
128
  new Promise<never>((_, rej) => setTimeout(() => rej(new Error('mcp timeout')), opts.timeoutMs ?? 30_000)),
113
129
  ])
114
130
  return { ok: true, result: (res as JsonRpcResponse).result }
@@ -14,13 +14,20 @@
14
14
  * - headers 凭据规则(CR-05 经配置通道执行):值支持 `${ENV_VAR}` 引用(env 缺席显式报错);
15
15
  * 字面值疑似明文凭据(containsPlaintextCredential 同款模式)直接拒绝——env 引用注入
16
16
  * 才是登记合规通道(展开后值不复查:引注入即目的,展开值是凭据本身);
17
- * - legacy HTTP+SSE(2024-11-05)不做(1.5 规划 D-21 登记不追溯)。
17
+ * - legacy HTTP+SSE(2024-11-05)不做(1.5 规划 D-21 登记不追溯);
18
+ * - 1.9(D-32 远程 MCP 头通道):send 加法可选第二参 opts.traceparent → tools/call 请求头
19
+ * 加法 `traceparent`(W3C Trace Context HTTP 载体惯例——第三方 OTel 接入面主消费通道);
20
+ * 注入前 parseTraceparent 校验,畸形**不注入不抛**(协议面 fail-open——严格格式天然排除
21
+ * CR/LF,兼防头注入越权面);otel 关 = 双载体零注入(头与 JSON-RPC 字节面不变);
22
+ * initialize/notifications/tools/list 不注入(无 span 上下文——1.8 tools/list 口径延伸);
23
+ * 配置 headers 含字面 `traceparent` 时注入优先(动态 span 上下文为请求级事实,登记)。
18
24
  */
19
25
  import { request as httpRequest } from 'node:http'
20
26
  import { request as httpsRequest } from 'node:https'
21
27
  import type { ClientTransport, JsonRpcRequest, JsonRpcResponse } from './gateway.ts'
22
28
  import { containsPlaintextCredential } from './gateway.ts'
23
29
  import { assertTlsOrLoopback } from '../runtime-core/llm.ts'
30
+ import { parseTraceparent } from '../runtime-core/trace.ts'
24
31
 
25
32
  export const MCP_PROTOCOL_VERSION = '2025-06-18'
26
33
 
@@ -127,8 +134,9 @@ export function createHttpMcpTransport(spec: HttpMcpServerSpec): ClientTransport
127
134
  throw new Error('CAR-E-MCP: SSE 流结束未见本请求响应(阶段:read-response)')
128
135
  }
129
136
 
130
- async function postAndRead(body: unknown, matchId: number | null, phase: string): Promise<JsonRpcResponse> {
131
- const res = await rawPost(spec.url, baseHeaders(), JSON.stringify(body), timeoutMs)
137
+ async function postAndRead(body: unknown, matchId: number | null, phase: string, extraHeaders: Record<string, string> = {}): Promise<JsonRpcResponse> {
138
+ // 1.9-D32:extraHeaders 展开在 baseHeaders 之后——配置头同名时注入优先(登记口径)
139
+ const res = await rawPost(spec.url, { ...baseHeaders(), ...extraHeaders }, JSON.stringify(body), timeoutMs)
132
140
  const sid = res.headers['mcp-session-id']
133
141
  if (typeof sid === 'string') sessionId = sid
134
142
  if (res.status < 200 || res.status >= 300) {
@@ -158,15 +166,25 @@ export function createHttpMcpTransport(spec: HttpMcpServerSpec): ClientTransport
158
166
  }
159
167
  }
160
168
 
169
+ /**
170
+ * 1.9-D32:tools/call 请求的 `traceparent` 头(W3C Trace Context HTTP 载体惯例)——
171
+ * parseTraceparent 校验 fail-open(畸形/空 = {} 不注入不抛;严格 hex 格式排除 CR/LF 头注入面);
172
+ * 与 body params._meta.traceparent 双载体同值并存(1.8-D29 `_meta` 面维持,头为主消费面)。
173
+ */
174
+ function callHeaders(tp: string | undefined): Record<string, string> {
175
+ if (!tp || !parseTraceparent(tp)) return {}
176
+ return { traceparent: tp }
177
+ }
178
+
161
179
  const transport: ClientTransport = {
162
- send(req: JsonRpcRequest): Promise<JsonRpcResponse> {
180
+ send(req: JsonRpcRequest, opts?: { traceparent?: string }): Promise<JsonRpcResponse> {
163
181
  return (async (): Promise<JsonRpcResponse> => {
164
182
  if (closed) throw new Error('CAR-E-MCP: http transport closed(server 已收口)')
165
183
  if (!initialized) {
166
184
  initPromise ??= doInitialize().then(() => { initialized = true })
167
185
  try { await initPromise } catch (e) { initPromise = null; throw e }
168
186
  }
169
- return postAndRead(req, req.id, 'request')
187
+ return postAndRead(req, req.id, 'request', callHeaders(opts?.traceparent))
170
188
  })()
171
189
  },
172
190
  alive: () => !closed,
@@ -8,6 +8,13 @@
8
8
  * - erasable-only 双挂点之一(本入口);挂点二在 loader 提交链(mountPlugin);
9
9
  * - **预算到期不掐 turn**:超限 = 工具错误结果交回模型(TurnEndReason 六值零扩展,ADR-001 兼容);
10
10
  * - 并发护栏(自研五维):并发 4 / 子调用超时 30s / 总 worker 存活由本执行器串行门管控。
11
+ *
12
+ * 1.8(D-31 PTC worker 桥挂链):opts.spanBridge 加法(otel 显式开且 turn span 存活时非空)——
13
+ * worker 侧程序计时(Date.now,worker_threads 同进程同时钟域)经 done 消息加法 span 字段回传,
14
+ * 主线程经 bridge.complete() 单点收口 car.ptc span(trace.ts ptcRelay——trace 挂链 turn /
15
+ * facade 时刻覆盖);**单 exporter 纪律:worker 零独立出站通道、零 trace 载荷注入**(worker 无
16
+ * outbound 面——挂链由主线程句柄归因 + 计时 relay 完成)。主侧 killer / worker error 兜底路径
17
+ * 无 done 回传(terminate/崩溃抢先),以主线程时钟收口(口径登记:兜底非 worker 计时)。
11
18
  */
12
19
  import { Worker } from 'node:worker_threads'
13
20
  import { fileURLToPath } from 'node:url'
@@ -22,13 +29,19 @@ export interface PtcResult { ok: boolean; result?: unknown; error?: string; wall
22
29
 
23
30
  export interface ToolBridge { run(args: unknown): Promise<unknown> }
24
31
 
32
+ /** 1.8-D31:PTC 桥挂链面(结构兼容 trace.ts PtcRelayHandle——ptc 模块不依赖 runtime-core/trace) */
33
+ export interface PtcSpanBridge {
34
+ complete(startMs: number, endMs: number, outcome: string, error?: unknown): void
35
+ }
36
+
25
37
  const WORKER_PATH = join(dirname(fileURLToPath(import.meta.url)), 'worker-entry.ts')
26
38
  const MAX_CONCURRENT = 4
27
39
  let active = 0
28
40
 
29
41
  /** PTC 程序执行:每次新 worker;子调用经消息桥回主线程执行工具(宿主权限门生效面) */
30
42
  export interface RunCodeOpts { tools: Map<string, ToolBridge>; audit?: (d: Record<string, unknown>) => void
31
- /** 授权门(S15):返回 false = 拒绝(无 worker 启动,审计留痕);authorizationId 幂等由 authz 服务承载 */ authorize?: (req: PtcRequest) => Promise<boolean> }
43
+ /** 授权门(S15):返回 false = 拒绝(无 worker 启动,审计留痕);authorizationId 幂等由 authz 服务承载 */ authorize?: (req: PtcRequest) => Promise<boolean>
44
+ /** 1.8-D31:桥挂链供给(每次 runCode 一次——每程序新 worker;null = 无遥测零 relay) */ spanBridge?: () => { complete(startMs: number, endMs: number, outcome: string, error?: unknown): void } | null }
32
45
 
33
46
  export async function runCode(req: PtcRequest, opts: RunCodeOpts): Promise<PtcResult> {
34
47
  // 双必填(description 为授权凭据——S15 授权门消费)
@@ -51,6 +64,8 @@ export async function runCode(req: PtcRequest, opts: RunCodeOpts): Promise<PtcRe
51
64
  }
52
65
  const started = Date.now()
53
66
  active++
67
+ // 1.8-D31:桥挂链供给(每次 runCode 一次);null = 无遥测零 relay
68
+ const relay = opts.spanBridge?.() ?? null
54
69
  opts.audit?.({ kind: 'ptc-start', description: req.description, toolCallId: req.toolCallId, budget })
55
70
  try {
56
71
  return await new Promise<PtcResult>((resolve) => {
@@ -61,9 +76,11 @@ export async function runCode(req: PtcRequest, opts: RunCodeOpts): Promise<PtcRe
61
76
  // 主线程侧兜底(worker 内计时器失效时强杀——双层预算)
62
77
  const killer = setTimeout(() => {
63
78
  void worker.terminate()
79
+ // 1.8-D31:兜底收口(主线程时钟——terminate 抢先无 done relay,口径登记)
80
+ relay?.complete(started, Date.now(), 'error', new Error(`budget-exceeded (maxWallMs=${budget.maxWallMs}, main-side guard)`))
64
81
  resolve({ ok: false, error: `budget-exceeded (maxWallMs=${budget.maxWallMs}, main-side guard)`, wallMs: Date.now() - started, outputBytes: 0, budgetExceeded: true })
65
82
  }, budget.maxWallMs + 1000)
66
- worker.on('message', (m: { type: string; callId?: string; name?: string; args?: unknown; result?: unknown; error?: string }) => {
83
+ worker.on('message', (m: { type: string; callId?: string; name?: string; args?: unknown; result?: unknown; error?: string; span?: { startMs: number; endMs: number } }) => {
67
84
  if (m.type === 'tool') {
68
85
  const def = opts.tools.get(m.name!)
69
86
  // 工具异常必须回传为 tool-result error(供程序体 try/catch 恢复)——
@@ -78,6 +95,8 @@ export async function runCode(req: PtcRequest, opts: RunCodeOpts): Promise<PtcRe
78
95
  }
79
96
  if (m.type === 'done') {
80
97
  clearTimeout(killer)
98
+ // 1.8-D31:worker 计时 relay 收口(error 路径同样收口——D-26 exception 形态成立)
99
+ if (m.span && relay) relay.complete(m.span.startMs, m.span.endMs, m.error ? 'error' : 'ok', m.error)
81
100
  // F12 出站覆盖:worker 输出回填上下文前强制 redact(M3安全设计增补 T-4)
82
101
  let result = m.result
83
102
  let secretsRedacted = 0
@@ -101,7 +120,7 @@ export async function runCode(req: PtcRequest, opts: RunCodeOpts): Promise<PtcRe
101
120
  })
102
121
  }
103
122
  })
104
- worker.on('error', (e) => { clearTimeout(killer); resolve({ ok: false, error: String(e), wallMs: Date.now() - started, outputBytes: 0 }) })
123
+ worker.on('error', (e) => { clearTimeout(killer); relay?.complete(started, Date.now(), 'error', e); resolve({ ok: false, error: String(e), wallMs: Date.now() - started, outputBytes: 0 }) })
105
124
  worker.on('exit', (code) => { clearTimeout(killer); active-- })
106
125
  // exit 可能先于 done(budget killer)——兜底 resolve 幂等由 Promise 语义保证
107
126
  })
@@ -112,7 +131,8 @@ export async function runCode(req: PtcRequest, opts: RunCodeOpts): Promise<PtcRe
112
131
  }
113
132
 
114
133
  /** PTC 作为一等 ToolDefinition:强制 write 侧效应(授权门无旁路),接入 runTurn 工具面 */
115
- export function makePtcToolDefinition(opts: { tools: Map<string, ToolBridge>; audit?: (d: Record<string, unknown>) => void }) {
134
+ export function makePtcToolDefinition(opts: { tools: Map<string, ToolBridge>; audit?: (d: Record<string, unknown>) => void
135
+ /** 1.8-D31:桥挂链供给透传(每次 runCode 一次;缺省 = 无遥测零 relay) */ spanBridge?: () => { complete(startMs: number, endMs: number, outcome: string, error?: unknown): void } | null }) {
116
136
  return {
117
137
  name: 'run_code',
118
138
  description: '执行一段 erasable TypeScript 程序(PTC)——程序体内经 tools.<name>(args) 调用已注册工具',
@@ -6,7 +6,10 @@
6
6
  * - 程序体 = async 函数体(顶层 await/return 支持,dsh 一手口径);
7
7
  * - tools proxy:await tools.<name>(args) → postMessage {type:'tool'} → 主线程执行 → {type:'tool-result'} 回传;
8
8
  * - 预算:maxWallMs 计时超限 → done(error='budget-exceeded (maxWallMs)');maxOutputBytes 序列化后检查;
9
- * - done 消息单发即退——程序体 = 一次原子 toolCall(收口点唯一在 executor 返回)。
9
+ * - done 消息单发即退——程序体 = 一次原子 toolCall(收口点唯一在 executor 返回);
10
+ * - 1.8-D31:done 加法 span 字段(程序窗口 startMs/endMs——worker 侧 Date.now 计时,
11
+ * worker_threads 同进程同时钟域;主线程 relay 收口 car.ptc span——单 exporter 纪律,
12
+ * 本隔离体零独立出站通道;程序未启动〔wallTimer 抢先〕不产 span 字段)。
10
13
  */
11
14
  import { parentPort, workerData } from 'node:worker_threads'
12
15
 
@@ -32,8 +35,10 @@ const tools: Record<string, (args: unknown) => Promise<unknown>> = new Proxy({},
32
35
  })
33
36
 
34
37
  const budget = (workerData as { budget: { maxWallMs: number; maxOutputBytes: number } }).budget
38
+ // 1.8-D31:程序窗口计时(null = 程序未启动——wallTimer 抢先不产 span 字段)
39
+ let spanStart: number | null = null
35
40
  const finish = (msg: { type: 'done'; result?: unknown; error?: string }) => {
36
- port.postMessage(msg)
41
+ port.postMessage({ ...msg, ...(spanStart !== null ? { span: { startMs: spanStart, endMs: Date.now() } } : {}) })
37
42
  setTimeout(() => process.exit(0), 50) // 让消息冲刷
38
43
  }
39
44
 
@@ -52,6 +57,7 @@ void (async () => {
52
57
  const body = js.replace(/^export default /, 'return ')
53
58
  // 程序体 = async 函数体(顶层 await/return);tools 为受控 proxy(仅显式调用,无宿主 import 面)
54
59
  const fn = new Function('tools', '"use strict";\n' + body + '\n') as (t: typeof tools) => () => Promise<unknown>
60
+ spanStart = Date.now()
55
61
  const result = await fn(tools)()
56
62
  clearTimeout(wallTimer)
57
63
  const out = JSON.stringify(result ?? null)
@@ -15,11 +15,18 @@
15
15
  * - declaredSideEffect 不出站(权限面只进权限门,不进模型请求体);
16
16
  * - 1.5-S1(D-20):内置 anthropic 适配器(Messages API 流式;max_tokens 必填缺省 4096;
17
17
  * system 顶层化;凭据 provider='anthropic')。
18
+ * - 1.8-BUG-1:出站缺省通道 node:http(s)(nodeHttpSseFetch——1.4-BUG-2 口径延伸到 LLM 面):
19
+ * 全局 fetch/undici socket 在 win32 进程退出时触发 libuv 断言崩溃(UV_HANDLE_CLOSING,
20
+ * 0xC0000409)——car run 真路径 ≥3 次模型请求 + PTC worker/MCP 子进程并存即确定性复现
21
+ * (1.4 彼时仅修遥测出站,LLM 出站同为 undici 默认——潜伏至 1.8 E2E 三请求形态暴露);
22
+ * 流式契约 body = AsyncIterable<Uint8Array> 不变;测试注入缝 fetchImpl 保留。
18
23
  */
19
24
  import { CarM8Error, providerUnreachable } from './errors.ts'
20
25
  import { CredentialService } from './credentials.ts'
21
26
  import type { Disposable, LlmAdapter, LlmChunk, LlmRequest, ToolCallDelta } from './types.ts'
22
27
  import type { PluginContext } from '../kernel/context.ts'
28
+ import { request as httpRequest } from 'node:http'
29
+ import { request as httpsRequest } from 'node:https'
23
30
 
24
31
  // ==================== 适配器注册表(registerLlmAdapter) ====================
25
32
 
@@ -140,6 +147,44 @@ interface SsePostSpec {
140
147
  sleep: (ms: number) => Promise<void>
141
148
  }
142
149
 
150
+ /** 1.8-BUG-1:node:http(s) 出站响应(streamSsePost 消费契约子集——ok/status/text()/body 流) */
151
+ interface SseFetchResponse { ok: boolean; status: number; text(): Promise<string>; body: AsyncIterable<Uint8Array> }
152
+
153
+ /**
154
+ * 1.8-BUG-1:LLM 出站缺省通道——node:http(s) 流式 POST(1.4-BUG-2 同源口径:全局 fetch/undici
155
+ * socket + win32 进程退出触发 libuv 断言崩溃 0xC0000409;详见面头注记)。流式契约:IncomingMessage
156
+ * 本体即 AsyncIterable<Uint8Array>(Buffer 子类);取消经 AbortSignal → req.destroy(流中途 destroy
157
+ * = 迭代器抛错,streamSsePost D-19 路径收口);传输层超时不设——§3.5.4 firstByte/overall 由调用方
158
+ * AbortController 单点承载。
159
+ */
160
+ const nodeHttpSseFetch = (url: string, init: { method?: string; headers?: Record<string, string>; body?: string; signal?: AbortSignal } = {}): Promise<SseFetchResponse> => new Promise((resolve, reject) => {
161
+ try {
162
+ const u = new URL(url)
163
+ const mod = u.protocol === 'https:' ? httpsRequest : httpRequest
164
+ const req = mod(u, { method: init.method ?? 'GET', headers: init.headers }, res => {
165
+ const status = res.statusCode ?? 0
166
+ resolve({
167
+ ok: status >= 200 && status < 300,
168
+ status,
169
+ text: () => new Promise<string>((res2, rej2) => {
170
+ let s = ''
171
+ res.on('data', c => { s += c.toString('utf-8') })
172
+ res.on('end', () => res2(s))
173
+ res.on('error', rej2)
174
+ }),
175
+ body: res,
176
+ })
177
+ })
178
+ const onAbort = () => { try { req.destroy(new Error('aborted')) } catch { /* 已完成 */ } }
179
+ if (init.signal) {
180
+ if (init.signal.aborted) onAbort()
181
+ else init.signal.addEventListener('abort', onAbort, { once: true })
182
+ }
183
+ req.on('error', reject)
184
+ req.end(init.body ?? null)
185
+ } catch (e) { reject(e) }
186
+ })
187
+
143
188
  /**
144
189
  * §3.5.4 流式 POST 引擎(openai-compat / anthropic 单点共用,1.5-S1 抽取——两适配器重试/
145
190
  * 超时/取消语义不漂移):仅网络错误/429/5xx、≤2 次、指数退避 1s/2s;首块前失败才可重试
@@ -300,7 +345,8 @@ export function createOpenAICompatAdapter(opts: OpenAICompatOptions): LlmAdapter
300
345
  // 1.4-S4(D-12a,1.2 规划期裁决):TLS 门 1.5-S1 抽取共用(口径与报错文案不变)
301
346
  assertTlsOrLoopback(baseUrl)
302
347
  const url = `${baseUrl}/chat/completions`
303
- const fetchImpl = opts.fetchImpl ?? fetch
348
+ // 1.8-BUG-1:缺省 node:http(s)(undici 退出崩溃——面头注记);注入缝保留
349
+ const fetchImpl = opts.fetchImpl ?? nodeHttpSseFetch as unknown as typeof fetch
304
350
  const sleep = opts.sleep ?? (ms => new Promise<void>(r => setTimeout(r, ms)))
305
351
  const timeoutMs = opts.timeoutMs ?? 120_000
306
352
  const firstByteMs = opts.firstByteMs ?? 30_000
@@ -503,7 +549,8 @@ export function createAnthropicAdapter(opts: AnthropicAdapterOptions): LlmAdapte
503
549
  const baseUrl = opts.baseUrl.replace(/\/+$/, '')
504
550
  assertTlsOrLoopback(baseUrl)
505
551
  const url = `${baseUrl}/v1/messages`
506
- const fetchImpl = opts.fetchImpl ?? fetch
552
+ // 1.8-BUG-1:缺省 node:http(s)(undici 退出崩溃——面头注记);注入缝保留
553
+ const fetchImpl = opts.fetchImpl ?? nodeHttpSseFetch as unknown as typeof fetch
507
554
  const sleep = opts.sleep ?? (ms => new Promise<void>(r => setTimeout(r, ms)))
508
555
  const timeoutMs = opts.timeoutMs ?? 120_000
509
556
  const firstByteMs = opts.firstByteMs ?? 30_000
@@ -32,6 +32,10 @@
32
32
  * - metrics 累计基数保持:flush 不再清空 counters——每次导出全量累计快照(真 CUMULATIVE)。
33
33
  * 1.2-S3 起 clear() 与 AGGREGATION_TEMPORALITY_CUMULATIVE 标注矛盾:intervalMs>0 自动导出时
34
34
  * 下游把增量解读为总量回落;内存有界由 label 基数保证(零内容枚举红线——AllowedLabels 3 计数器)。
35
+ *
36
+ * 1.8(D-31 PTC worker relay span 时刻覆盖):startSpan 加法 time.startMs / end 加法 atMs——
37
+ * 跨隔离体计时回传(worker_threads 同进程同时钟域)后主线程单点收口(trace.ts ptcRelay);
38
+ * 采样/buffer 上限/出站形态路径零改动(relay span 走既有 end() 全路径)。缺省时刻行为不变。
35
39
  */
36
40
  import { randomBytes } from 'node:crypto'
37
41
  import { request as httpRequest } from 'node:http'
@@ -67,7 +71,8 @@ export interface TelemetrySpan {
67
71
  setAttribute(key: string, value: string | number | boolean): this
68
72
  addEvent(name: string, attrs?: Record<string, string | number | boolean>): this
69
73
  recordException(err: unknown): this
70
- end(): void
74
+ /** 1.8-D31:atMs = 结束时刻覆盖(跨隔离体计时 relay;缺省 now() 行为不变) */
75
+ end(atMs?: number): void
71
76
  readonly traceId: string
72
77
  /** 1.4-S6:子 span 挂链需要父 spanId(TurnTracer turn→step 层级) */
73
78
  readonly spanId: string
@@ -78,6 +83,8 @@ export interface TelemetryTracer {
78
83
  attributes?: Record<string, string | number | boolean>
79
84
  /** 1.4-S6:trace 透传(traceId 复用 + parentSpanId 挂链);缺省 = 新独立 trace */
80
85
  trace?: { traceId: string; parentSpanId?: string }
86
+ /** 1.8-D31:起始时刻覆盖(跨隔离体计时 relay;缺省 now() 行为不变) */
87
+ time?: { startMs: number }
81
88
  }): TelemetrySpan
82
89
  }
83
90
 
@@ -341,7 +348,8 @@ export function createTelemetryFacade(
341
348
  spanId: randomBytes(8).toString('hex'),
342
349
  parentSpanId: startOpts?.trace?.parentSpanId ?? null,
343
350
  name,
344
- startMs: now(),
351
+ // 1.8-D31:起始时刻覆盖(跨隔离体计时 relay;缺省 now() 行为不变)
352
+ startMs: startOpts?.time?.startMs ?? now(),
345
353
  endMs: 0,
346
354
  attributes: { ...startOpts?.attributes },
347
355
  events: [],
@@ -367,8 +375,8 @@ export function createTelemetryFacade(
367
375
  record.status = 'STATUS_CODE_ERROR'
368
376
  return this
369
377
  },
370
- end() {
371
- record.endMs = now()
378
+ end(atMs) {
379
+ record.endMs = atMs ?? now()
372
380
  stats.spansEnded++
373
381
  // head 采样于 end() 决策(1.2-S3):缺省 always_on 全量(1.1 行为不变);采样只减少出站
374
382
  const keep = sampling === 'always_on' ? true