# 遥测、调试与提示词轨迹

> traceId 从会话一路传到回合、工具调用和模型请求头；OpenTelemetry 默认关闭，配了 OTLP 端点才加载；错误与端点怎样脱敏；日志的格式、位置与 7 天保留；模型输入输出的本地录制与 prompt-trajectory；debug 包的抓包代理与时间线界面。

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.org/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.org/manual/zcode/telemetry-debug
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

Agent CLI 的可观测性分三层：本地的结构化日志、本地的模型输入输出录制（model-io），以及可选的 OpenTelemetry 导出。前两层默认就开着，数据只落在本机；第三层默认关闭。给开发者用的还有两件工具：`apps/zcode-cli/packages/debug`（读日志与会话库、带网络抓包的时间线界面）和 `apps/zcode-cli/tools/prompt-trajectory`（把请求还原成可复现的轨迹）。桌面端另有模型轨迹面板、会话调试快照和自己的 ARMS 前端监控，这里只点到。

代码分布：`apps/zcode-cli/packages/telemetry` 是 OpenTelemetry 的实现，10 个文件、3874 行；日志在 `apps/zcode-cli/packages/adapters/src/logging`；model-io 在 `apps/zcode-cli/packages/adapters/src/model/runner-debug.ts`；core 侧的入口是 `RuntimeTelemetryFacade`，它只调用 contracts 里定义的遥测端口（`apps/zcode-cli/packages/core/src/telemetry/runtime-telemetry.ts:34`）。

## 怎么用：数据在哪、默认开不开

| 项 | 位置 | 默认 | 调整 |
| --- | --- | --- | --- |
| 结构化日志 | `~/.zcode/cli/log/zcode-<本地日期>.jsonl` | 开；生产 `info` 起，开发态 `debug` 起；保留 7 天 | `ZCODE_LOG_DIR` 换目录，`ZCODE_LOG_CONSOLE=1` 同时写 stderr |
| 模型输入输出 | `~/.zcode/cli/rollout/model-io-<会话>.jsonl`，开发态在 `debug/` | 开，测试环境除外；生产最多 3 个会话文件、单文件 64 MiB | 桌面设置“完整保留模型 I/O”取消压缩与限额 |
| 用量事实 | 会话库的 `model_usage`、`turn_usage`、`tool_usage` 三张表 | 开，保留 30 天 | 无开关 |
| OpenTelemetry | 发往 `OTEL_EXPORTER_OTLP_*` 指定的端点 | 关，没配端点连 SDK 都不加载 | 配端点即开；`ZCODE_MODEL_TELEMETRY_ENABLED=0` 强制关 |
| 进程资源样本 | 以协议通知发给桌面 Host | 协议模式下每 60 秒一次 | 无开关 |

出处依次是：日志目录与级别 `apps/zcode-cli/packages/adapters/src/logging/index.ts:173`、`logging/index.ts:222`、`logging/index.ts:226`，保留天数 `apps/zcode-cli/packages/adapters/src/logging/retention.ts:6`；model-io 的目录与上限 `apps/zcode-cli/packages/bootstrap/src/app/paths.ts:13`、`apps/zcode-cli/packages/adapters/src/model/runner-debug.ts:35`；用量保留见[SQLite 会话库](https://daiw.org/manual/zcode/session-store)；遥测开关 `apps/zcode-cli/packages/telemetry/src/bootstrap.ts:124`；采样周期 `packages/shared/src/processResourceTelemetry.ts:29`。`ZCODE_RUNTIME_ENV` 未设置时按生产处理（`packages/shared/src/runtimeEnv.ts:120`）。NOTICE 对 model-io 的说明（`NOTICE.md:62`）：

> 共享 Agent 的模型输入输出日志在开发和生产运行中默认写入本地，测试环境除外。日志可能含提示词、代码、上下文、工具参数和模型回复；请求头及部分图像、视频数据有脱敏处理，但不能认为所有用户文本均已脱敏。

## traceId：一条任务链一根

CLI 的 AGENTS.md 把 traceId 定成硬约定（`apps/zcode-cli/AGENTS.md:74`）：

> `traceId` 默认对应一次顶层 session 的完整任务链，session 内创建的子 session、subagent、重试任务、后台队列任务和异步 I/O 都应归属到同一个 `traceId`。

下一条接着规定 `sessionId`、`turnId`、`messageId`、`toolCallId`、`spanId`、`parentSpanId` 都是 traceId 之下的结构化子标识（`apps/zcode-cli/AGENTS.md:75`）。落到代码里，贯穿全程的是一个很小的结构（`apps/zcode-cli/packages/contracts/src/tracing/tracer.ts:14`）：

```ts
export interface TraceContext {
  traceId: TraceId;
  queryId?: QueryId;
  spanId?: string;
  parentSpanId?: string;
  parentId?: string;
  sessionId?: SessionId;
  turnId?: TurnId;
  attributes?: Record<string, string | number | boolean>;
}
```

它靠 `AsyncLocalStorage` 在异步调用间隐式传递（`tracer.ts:166`），各层只用 `createChildTraceContext` 派生：traceId 原样继承，`spanId` 新生成，旧的变成 `parentSpanId`（`tracer.ts:212`）。

| 层级 | 何时生成 | 出处 |
| --- | --- | --- |
| `traceId` | 创建应用时随根上下文生成，是一个 UUID；协议模式优先沿用客户端带来的 traceId | `apps/zcode-cli/packages/bootstrap/src/app/create-app.ts:153`、`apps/zcode-cli/packages/contracts/src/interfaces/shared.ts:43`、`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/server-types.ts:274` |
| `sessionId` | `sess_` 加 UUID；会话行的 `trace_id` 列记下所属 traceId，分叉出的子会话沿用父会话的 | `shared.ts:31`、`apps/zcode-cli/packages/core/src/runtime/methods/session-fork.ts:159` |
| `turnId` 与 `queryId` | 每回合派生一次子上下文 | `apps/zcode-cli/packages/core/src/runtime/methods/turn.ts:111` |
| 模型请求 | 每一步再派生，请求头带上 `x-zcode-trace-id`、`x-request-id`、`x-session-id`、`x-query-id` 与 `x-zcode-session-type` | `apps/zcode-cli/packages/core/src/runtime/methods/turn-model-step.ts:158`、`apps/zcode-cli/packages/adapters/src/model/runner-attribution.ts:36` |
| `toolCallId` | 每次工具调用派生，工具 ID 和工具名作为属性 | `apps/zcode-cli/packages/core/src/tool/executor/call-runner.ts:110` |
| `spanId` | 随每次派生生成，取 UUID 的前 16 位 | `tracer.ts:100` |

这组字段最终出现在三个地方：每一行结构化日志的顶层字段（`apps/zcode-cli/packages/adapters/src/logging/serialize.ts:72`），会话库里会话与用量表的 `trace_id` 列，以及发往模型服务的请求头。最后这一项 NOTICE 也写到了：模型请求“可带认证信息、客户端环境及会话／请求／追踪标识”（`NOTICE.md:36`）。

```mermaid
flowchart TD
  T["traceId：顶层会话的任务链"] --> S["sessionId：会话与分叉子会话"]
  S --> R["turnId 与 queryId：每回合"]
  R --> M["模型请求：x-zcode-trace-id 等请求头"]
  R --> C["toolCallId：每次工具调用"]
  M --> O["输出：JSONL 日志、model-io、用量表"]
  C --> O
  R -.->|"另起一套 trace"| X["OpenTelemetry：每个顶层回合一条 trace"]
```

图里最后一条虚线要单独说明：OpenTelemetry 的 trace 与上面的 traceId 不是一回事。`agent_turn` span 默认从空上下文起一条新 trace，会话 ID、回合 ID 只作为属性挂在上面（`apps/zcode-cli/packages/telemetry/src/agent-trace-runtime.ts:151`、`apps/zcode-cli/packages/telemetry/src/agent-trace-support.ts:311`）。前台子 Agent 以 `child` 方式挂进父回合的 trace，后台子 Agent 与工作流子会话则各起一条，再用 `spawned_by` 链接回来（`apps/zcode-cli/packages/core/src/runtime/methods/subagent.ts:295`、`apps/zcode-cli/packages/bootstrap/src/app/workflow-facade.ts:298`）。

## OpenTelemetry：默认关，配了端点才开

开关的判断在 CLI 异步启动的边界（`bootstrap.ts:120`）：

```ts
export async function prepareModelTelemetryEnv(
  env: EnvRecord,
  options: PrepareModelTelemetryOptions = {},
): Promise<EnvRecord> {
  if (!resolveOtlpTraceEndpoint(env) || isExplicitlyDisabled(env.ZCODE_MODEL_TELEMETRY_ENABLED)) {
    return env;
  }
  const existingInstallationId = normalizeTelemetryDeviceMid(env.ZCODE_TELEMETRY_DEVICE_MID);
  const installationId =
    existingInstallationId ?? (await resolveStandaloneDeviceMid(env.ZCODE_HOME?.trim()));
  const preparedEnv = installationId ? { ...env, ZCODE_TELEMETRY_DEVICE_MID: installationId } : env;

  if (!preparingOwner && !preparedOwner) {
    preparingOwner = createPreparedOwner(preparedEnv, options);
  }
  preparedOwner = await preparingOwner;
  return preparedEnv;
}
```

端点只认环境变量：trace 取 `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`，没有就取 `OTEL_EXPORTER_OTLP_ENDPOINT` 再拼 `/v1/traces`；指标同理，两者都没有时沿用 trace 端点，注释说 ARMS 的自定义接入点对两者共用一个 URL（`bootstrap.ts:75`、`bootstrap.ts:98`）。请求头来自 `OTEL_EXPORTER_OTLP_TRACES_HEADERS` 或 `OTEL_EXPORTER_OTLP_HEADERS`，服务名缺省 `zcode-cli-agent`（`bootstrap.ts:173`）。`ZCODE_MODEL_TELEMETRY_ENABLED` 取 `0`、`false`、`off`、`disabled` 时强制关闭（`bootstrap.ts:414`）。没有端点时直接返回，`createModelTelemetry` 交出的是空实现（`bootstrap.ts:44`），OTel SDK 也只在确认启用后才动态加载（`bootstrap.ts:176`）。

开源代码里没有内置任何 OTLP 端点。桌面端拉起 Agent 时，只把自己继承到的 `OTEL_*` 转给它，没有端点就一个遥测变量都不传（`packages/services/src/zcode-agent/agentTelemetryEnv.ts:13`）；桌面自己的 ARMS 前端监控与数仓事件端点同样“由运行时环境变量提供，未配置即停用，构建产物不内嵌”（`packages/shared/src/env.ts:52`、`env.ts:56`）。NOTICE 第二节的十七类对外请求里没有单列这条遥测链路，最接近的是上面引过的“追踪标识”随模型请求发出。

启用后的参数（`apps/zcode-cli/packages/telemetry/src/otlp-exporter.ts:31`、`otlp-exporter.ts:56`）：

| 项 | 值 |
| --- | --- |
| 协议 | OTLP，protobuf over HTTP，gzip 压缩 |
| 超时 | 3000 毫秒 |
| trace 采样 | 父级优先，顶层按 traceId 抽 10% |
| span 批处理 | 每批 100 条，队列 2000 条，每 5000 毫秒一批 |
| 指标 | 每 300000 毫秒导出一次，DELTA 时间性 |
| 同时存活的 span 写入器 | 最多 5000 个，超出就丢弃并计数（`agent-trace-runtime.ts:115`） |

span 一共八种：`agent_turn`、`agent_step`、`tool_execution`、`command_execution`、`context_compaction`、`detached_operation`、`model_call`、`model_attempt`，每种各有一个时长直方图，另有调用尝试次数、按类型的 Token 增量、首个事件、首段内容、首段文字的延迟、流停顿次数等指标（`apps/zcode-cli/packages/telemetry/src/agent-metrics.ts:57`）。属性记 ID、枚举、计数、耗时和清洗过的错误信息，不记提示词、回复或工具参数正文；模型 span 上的服务地址是脱敏后的 origin 加路由（`apps/zcode-cli/packages/telemetry/src/provider-endpoint.ts:47`）。

身份方面有三点：安装 ID 存在 `~/.zcode/v2/telemetry-state.json`，只有遥测启用时才会生成（`bootstrap.ts:226`）；桌面端传给 Agent 的用户身份是账号 ID 的 sha256，注释说 Trace 可以按用户关联，但不会上传账号、邮箱或登录名（`agentTelemetryEnv.ts:27`）；每次启动的实例 ID 与安装 ID 只进 trace 的资源属性，不进指标，免得指标序列基数无限膨胀（`otlp-exporter.ts:240`）。另外，`OTEL_*` 与 `ZCODE_TELEMETRY_*` 这些变量会先被捕获进进程私有区，再从所有子进程环境里剔除，Bash、MCP 与工具子进程拿不到 OTLP 端点和鉴权头（`packages/shared/src/runtimeEnv.ts:74`、`apps/zcode-cli/packages/bootstrap/src/telemetry-bootstrap.ts:9`）。

## 脱敏

遥测、日志、model-io 各有各的脱敏规则，强度不同：

| 数据 | 规则 | 出处 |
| --- | --- | --- |
| span 属性 | ID 只收 `[A-Za-z0-9._:-]` 且不超过 128 字符，枚举转小写，不合格就整项丢弃 | `agent-trace-support.ts:22` |
| 指标标签 | 每种指标一份白名单，高基数的执行 ID 与业务内容只进 trace | `agent-metrics.ts:39`、`agent-metrics.ts:195` |
| 错误信息 | 先截到 4096 字符再清洗，结果最多 2048 字符；同一个异常沿调用链冒泡时，只在最先认领它的 span 上记正文 | `apps/zcode-cli/packages/telemetry/src/error-sanitizer.ts:54`、`error-sanitizer.ts:39` |
| 服务端点 | 去掉用户名、密码、查询串，路径里的邮箱、UUID、长数字、长十六进制、疑似令牌分段替换成占位符 | `provider-endpoint.ts:47` |
| 日志 | 键名含 api key、authorization、cookie、credential、password、secret、token 的值替换为 `[Redacted]`，只看键名不看内容 | `serialize.ts:15` |
| model-io | 请求与响应头里的鉴权、密钥、令牌、Cookie 一类改成 `[redacted]`，图片与视频的 base64 只留一句说明 | `apps/zcode-cli/packages/adapters/src/model/runner-network-headers.ts:3`、`apps/zcode-cli/packages/adapters/src/model/runner-debug-redaction.ts:35` |

错误信息的清洗规则最细（`error-sanitizer.ts:58`）：

```ts
  const sanitized = value
    .slice(0, 4_096)
    .replace(/\bhttps?:\/\/[^\s"'<>]+/giu, sanitizeUrl)
    .replace(
      /(\bauthorization\b["']?\s*[:=])\s*(?:(?:Bearer|Basic)\s+)?[^\s,"'};]+/giu,
      "$1 {redacted}",
    )
    // ...
    .replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/giu, "$1 {redacted}")
    .replace(/\b(?:sk|rk|pk)-[A-Za-z0-9_-]{12,}\b/giu, "{secret}")
    .replace(/\bgh[pousr]_[A-Za-z0-9]{20,}\b/gu, "{secret}")
    .replace(/\bAKIA[A-Z0-9]{16}\b/gu, "{secret}")
    .replace(/\bAIza[0-9A-Za-z_-]{30,}\b/gu, "{secret}")
    .replace(/\b[A-Za-z0-9]{4,32}@[0-9a-f]{12,}\b/giu, "{secret}")
    .replace(/\b[^/@\s]+@[^/@\s]+\.[^/@\s]+\b/gu, "{email}")
```

后面还会把 `/Users/...`、`/home/...`、临时目录和 Windows 盘符路径折成 `{path}`。相比之下，日志的脱敏只按键名判断，写进消息正文或其他字段的内容原样落盘；AGENTS.md 要求需要高敏信息时“必须显式进入受控 debug 路径”（`apps/zcode-cli/AGENTS.md:82`）。从代码看，按子串匹配键名还有个副作用：`totalTokens`、`tokenCount`、`thresholdTokens` 这类计数字段的键名也含 token，写进文件时同样变成 `[Redacted]`（`serialize.ts:17`，这类字段见 `apps/zcode-cli/packages/core/src/runtime/methods/microcompact.ts:67`）。

## 日志

`createNodeLoggerFactory` 每写一条就同步追加一行 JSON，写失败直接吞掉，“Logging must never break the agent execution path”（`logging/index.ts:134`）。文件按本地日期命名为 `zcode-YYYY-MM-DD.jsonl`（`logging/index.ts:248`）。每行固定的顶层字段是 `timestamp`、`level`、`event`、`module`、`message`、`traceId`、`spanId`、`parentSpanId`、`sessionId`、`turnId`、`toolCallId`、`durationMs`、`status`，其余上下文进 `context`，错误进 `error`（`serialize.ts:72`）；`status` 只允许 `started`、`waiting`、`completed`、`failed`、`cancelled` 五种（`serialize.ts:101`）。最低级别在开发态是 `debug`，生产与测试是 `info`（`logging/index.ts:226`）；仓库根的 AGENTS.md 第 79 行规定 `debug` 留给协议原始数据、流式分块这类高频诊断，“生产环境不落盘”。

保留策略：每个进程启动 60 秒后清理一次，删掉 7 天之前的日志文件，只动文件名严格匹配日期格式的，结果本身记一条 debug 日志（`retention.ts:6`、`retention.ts:7`、`retention.ts:132`）；应用和协议入口各调度一次，同一个工厂只会调度一次（`logging/index.ts:207`）。启动过程由 `StartupTimer` 记账，每个阶段一条 `info`，带本段耗时与累计耗时（`apps/zcode-cli/packages/bootstrap/src/startup-logging.ts:26`），比如会话库迁移的开始与结束。

## 模型请求录制：model-io

每次模型请求（流式与非流式都算）结束后，适配层把请求与响应写进 model-io，一行一条 `type: "model_io"` 记录：请求体、请求头、消息、工具名、采样参数，响应的正文、推理文本、工具调用、用量，以及 `sessionId`、`traceId`、`turnId`、`querySource`（`runner-debug.ts:109`）。目录由 bootstrap 算好后交给模型适配器（`apps/zcode-cli/packages/bootstrap/src/model-factory.ts:29`）；`AgentRuntime` 也收了一份 `modelIoDir`（`apps/zcode-cli/packages/core/src/runtime/agent-runtime.ts:270`），但 core 里没有任何代码读它。容量限制写在文件开头（`runner-debug.ts:34`）：

```ts
// 生产环境 rollout 目录最多保留的 model-io 会话文件数。超出删最旧。
const MAX_ROLLOUT_FILES = 3;
// 生产环境单个 session 的 model-io 文件硬上限。诊断日志不能因为无限增长影响 agent 主流程。
const MAX_ROLLOUT_SESSION_BYTES = 64 * 1024 * 1024;
// 开发态保留更多上下文，但仍避免单个 debug 文件无限膨胀。
const MAX_DEBUG_SESSION_BYTES = 256 * 1024 * 1024;
// 缓存缺失或文件超限后写 baseline 时，仅保留最近上下文，避免长 session 重启后再次写出巨型记录。
const MAX_ROLLOUT_BASELINE_MESSAGES = 64;
const MAX_DEBUG_BASELINE_MESSAGES = 256;
```

一个会话一个文件，生产态在新会话建文件时按修改时间淘汰最旧的（`runner-debug.ts:497`）。同一会话里，后一次请求只记相对上一次新增的消息，标成 `delta`，避免完整历史在文件里一遍遍重复（`runner-debug.ts:742`）；失败的请求保留完整消息，方便排查 400 一类错误（`runner-debug.ts:703`）。文件超过上限时整个重写成一条只带最近上下文的基线记录，而不是继续追加（`runner-debug.ts:523`）。记忆抽取这种后台请求不写进来，免得占掉槽位、又在下一次抽取时被自己读到（`apps/zcode-cli/packages/core/src/runtime/helpers/project-memory-agent.ts:48`）。

桌面设置里的“完整保留模型 I/O”（`packages/ui/src/i18n/locales/zh-CN.ts:1823`）会让写入跳过轮转、限额和压缩，但仍过脱敏（`runner-debug.ts:487`）；这个偏好经协议下发，同时改掉已有会话和以后新建会话的行为（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/model-io-preferences.ts:8`）。桌面端的模型轨迹面板 `ModelTrajectoryPane`（`packages/ui/src/ModelTrajectoryPane.tsx:39`）就是读这些文件：Host 从文件尾部最多读 32 MiB，按 `sessionId` 精确匹配，默认只取最近 200 次调用（`packages/services/src/zcode-agent/modelTrajectoryFileTail.ts:7`、`packages/services/src/zcode-agent/modelTrajectory.ts:20`）。资源管理器把 `cli/debug` 与 `cli/rollout` 归为“模型轨迹”，标成可以放心清理（`packages/services/src/storage/domain/storageCatalog.ts:92`、`storageCatalog.ts:30`）。

## prompt-trajectory：把请求还原成轨迹

`tools/prompt-trajectory` 放在 pnpm workspace 里，但不进生产 CLI 和单文件打包（`apps/zcode-cli/tools/prompt-trajectory/README.md:5`）。README 给的命令（`prompt-trajectory/README.md:11`）：

```bash
pnpm --filter @zcode/bootstrap^... build
pnpm --filter @zcode/bootstrap build

pnpm --filter @zcode/prompt-trajectory record -- \
  --fixture /path/to/recording.json \
  --out /tmp/zcode-prompt-trajectory/basic-live

pnpm --filter @zcode/prompt-trajectory record:prompt -- \
  --prompt "Say hello in one short sentence."

pnpm --filter @zcode/prompt-trajectory derive -- \
  --out /tmp/zcode-prompt-trajectory/basic-live

pnpm --filter @zcode/prompt-trajectory model-io -- \
  --input ~/.zcode/cli/debug/model-io-<session>.jsonl \
  --out /tmp/zcode-prompt-trajectory/model-io-session
```

- `record` 与 `record:prompt` 真正跑一遍 Agent：在 `127.0.0.1` 的随机端口起一个代理，把模型 provider 的 `baseURL` 换成它，上游请求照常转发，同时把整条对话记进 `trajectory.jsonl`，流式增量先拼成一条完整的助手消息再写（`apps/zcode-cli/tools/prompt-trajectory/src/openai-provider-proxy.ts:38`、`prompt-trajectory/README.md:32`）。不给 `--model` 等参数时沿用 CLI 自己的模型配置（`prompt-trajectory/README.md:41`），所以会产生真实的模型调用与费用。
- `derive` 从 `trajectory.jsonl` 推出每一步完整的请求体快照，OpenAI 与 Anthropic 两种形状各一份（`prompt-trajectory/README.md:56`）。
- `model-io` 把一份真实的 model-io 文件展开 `delta`，默认只留 `querySource` 为 `main_turn` 的主对话，排除标题生成之类的旁路调用，转成可复用的 Anthropic 轨迹（`apps/zcode-cli/tools/prompt-trajectory/src/model-io.ts:11`）。

NOTICE 专门提醒过这类工具：配成真实上游的录制代理会转发请求，“不能因名称含测试／录制就认为完全离线”（`NOTICE.md:51`）。

## debug 包：抓包代理与时间线

README 开头一句（`apps/zcode-cli/packages/debug/README.md:3`）：

> Development-only trace and context viewer for ZCode.

它是一个 Hono 服务加一个 React 前端，只读三类本地数据：`~/.zcode/cli/log` 下的结构化日志、会话库（以只读方式打开，`apps/zcode-cli/packages/debug/server/sources.ts:75`）、界面里选定的会话事件 JSONL，不回写 Agent 的库（`debug/README.md:11`）。另外默认起一个基于 `http-mitm-proxy` 的本地抓包代理。

```mermaid
flowchart LR
  CLI["被观察的 zcode 进程"] -->|"ZCODE_HTTP_PROXY"| P["抓包代理 127.0.0.1:4184"]
  P --> U["模型服务等上游"]
  CLI -->|"写入"| L["日志 JSONL 与会话库"]
  L --> API["debug API 127.0.0.1:4174"]
  P -->|"请求头与字节数"| API
  API --> UI["时间线界面"]
```

用法是 `pnpm --filter debug dev`（`debug/README.md:8`），API 在 `127.0.0.1:4174`（`apps/zcode-cli/packages/debug/scripts/dev.ts:7`），代理在 4184（`apps/zcode-cli/packages/debug/server/network-capture.ts:30`）。README 写的是“API/UI: `http://127.0.0.1:4174`”（`debug/README.md:21`），这只对构建后 `start` 的方式成立，那时同一端口也托管前端（`apps/zcode-cli/packages/debug/server/index.ts:180`）；`dev` 模式下界面由 Vite 在 `127.0.0.1:5174` 提供，`/api` 再转到 4174（`apps/zcode-cli/packages/debug/vite.config.ts:8`）。

要让某个 CLI 走代理，按网络面板给的两个变量启动它：`ZCODE_HTTP_PROXY` 指向代理，`ZCODE_AGENT_CA_CERT` 指向代理自签的 CA（`network-capture.ts:127`）。CA 与私钥生成在包内的 `certs/network-ca/` 下，已被 git 忽略（`apps/zcode-cli/packages/debug/certs/README.md:3`）；Agent 只在受控的子进程边界把它们换成标准的代理与证书变量（`debug/README.md:25`，机制见[执行边界：子进程、环境与网络](https://daiw.org/manual/zcode/exec-boundary)）。代理只记方法、地址、状态码、请求与响应头、正文字节数，不存正文（`network-capture.ts:230`）；`authorization`、`cookie`、`x-api-key` 等头改成 `[redacted]`（`network-capture.ts:34`）；靠 `x-zcode-trace-id`、`x-trace-id` 或 `traceparent` 请求头把请求归到 trace 下（`network-capture.ts:33`），默认保留最近 300 条（`network-capture.ts:31`）。只想看日志与数据库时，用 `ZCODE_DEBUG_NETWORK_CAPTURE=0` 关掉代理（`network-capture.ts:307`）。

界面按 traceId 列出任务链，点进一条，服务端把日志、事件、数据库里的消息与 part 拼成时间线，按事件配对推出回合、模型、工具、权限、子 Agent 几条泳道的 span，再附上上下文快照、上下文用量拆解与缓存报告（`apps/zcode-cli/packages/debug/server/analyzer.ts:64`）。上下文快照来自日志里带全文的 `context.built` 一类事件（见[系统提示词、上下文与提醒](https://daiw.org/manual/zcode/context-builder)）；代理不存正文，要看完整的模型请求体，还得回到上面的 model-io。

## 进程资源采样与其他

- **资源样本**。协议模式的 Agent 每 60 秒采一次 CPU 占用、常驻内存与堆用量、运行时长和物理内存，用协议通知发给桌面 Host（`apps/zcode-cli/packages/bootstrap/src/process-resource-sampler.ts:100`、`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/resource-sampler.ts:26`）。进程标识是一个 8 字节随机数，注释说不用 pid 是“隐私红线要求”（`process-resource-sampler.ts:18`）。同一节拍还顺带淘汰驻留会话，并按“变化或心跳”门控写一条本地内存诊断日志（`resource-sampler.ts:21`、`resource-sampler.ts:32`）。MCP 子进程的启动、崩溃与资源事件也走协议通知（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-entrypoint.ts:321`）；桌面端再经 ARMS 的 `sendCustom` 上报（`packages/desktop/src/main/desktopResourceTelemetry.ts:157`），而 ARMS 只在运行时给了端点时才初始化（`packages/desktop/src/main/appARMSBootstrap.ts:268`）。
- **会话调试快照**。协议方法 `session/debug` 返回某个会话最近 200 轮的用量与缓存命中、最近 100 条网络状态（`packages/shared/src/session-debug.ts:3`、`packages/shared/src/zcode-protocol/index.ts:3571`），请求头用的是已脱敏的那份，并限制条数与长度（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/session-debug.ts:24`）。
- **`scripts/shadow-replay.mjs` 不是遥测**。它是交付前的对账工具：把真实会话库复制一份，全部历史会话过一遍冷恢复与产品投影，统计崩溃和静默丢失（`apps/zcode-cli/scripts/shadow-replay.mjs:1`），与会话库的关系见[SQLite 会话库](https://daiw.org/manual/zcode/session-store)。

下一篇：[命令行入口、无头模式与打包](https://daiw.org/manual/zcode/cli-surface)——`main.ts` 怎样守住 stdout，全部子命令与全局选项，`-p` 无头模式的输出格式，以及单文件可执行怎么打出来。
