fix(telemetry): rename record waterfall

This commit is contained in:
Tianyi Cui
2026-07-27 21:30:28 +08:00
parent 4182dbdc86
commit 0af16387ae
21 changed files with 57 additions and 55 deletions
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-23-session-telemetry-otel-revival.md: 1c855ad8d3263f5ff3343b480983f7cf14d9d4c6
2026-07-23-session-telemetry-otel-revival.zh.md: 371e8f9fbfbdcc7c74fa5ae39406ea4bf159bc81
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md
2026-07-23-session-telemetry-otel-revival.md: a58598d8a956d47cb0cf6aa3e659f38314bc4b17
2026-07-23-session-telemetry-otel-revival.zh.md: cc09717e349d5ae2ab5157bf46de30b1823c775f
@@ -13,7 +13,7 @@ Every deployment that wants harness sessions in an observability stack must hand
`packages/telemetry/` revives the two reviewed packages under the SDK stance — the harness provides the capability, the deployment configures where records go and owns what leaves in them:
- **`@deepseek-ai/dsh-session-telemetry`** — the seam. `TelemetryBackend` (`emit`/`flush?`/`shutdown`), the service-registered `Telemetry` form, and `TelemetryCoordinator` owning capture: adoption with cursor read-back, the per-append firehose (project → `structuredClone` → redact → `emit`, zero I/O), the fixed first-chunk-per-(turn, step) projection, the `agent/error` relay, and dispose-time `shutdown` records.
- **The `telemetry/redact` waterfall** — the delta over the branch version. Every record passes it before reaching any backend; the seam ships NO rules of its own — the innermost `next()` is a pass-through, deployments mount their rules as listeners (stacking by transforming `next()`'s return value), and a throwing rule withholds the record fail-closed. Redaction applies to the exported copy only; the canonical log is never rewritten.
- **The `telemetry/record` waterfall** — the delta over the branch version and the seam's redaction extension point. Every record passes it before reaching any backend; the seam ships NO rules of its own — the innermost `next()` is a pass-through, deployments mount their rules as listeners (stacking by transforming `next()`'s return value), and a throwing rule withholds the record fail-closed. Redaction applies to the exported copy only; the canonical log is never rewritten.
- **`@deepseek-ai/dsh-session-telemetry-otel`** — the reference backend: OTel JS SDK log pipeline (`LoggerProvider``BatchLogRecordProcessor` → OTLP/HTTP exporter), configured verbatim through `exporter`/`processor` passthroughs. `exporter.url` is required and validated at load; unmounted or unconfigured, nothing leaves the process.
The boundary axiom holds: the harness's aspect ends at `emit()`. Batching, retry, queueing, and loss policy are the reporting SDK's, configured through passthroughs — delivery is best-effort (at-most-once across a crash), which the READMEs state plainly.
@@ -34,4 +34,4 @@ The boundary axiom holds: the harness's aspect ends at `emit()`. Batching, retry
## Consequences
A deployment adds one `cordis.yml` entry with an OTLP endpoint and gets its session stream in any OTel-compatible stack; removing the entry is the opt-out, with no residual state. A rule-free deployment exports records exactly as captured — including any credentials embedded in file contents or command output — so a deployment crossing a trust boundary must mount `telemetry/redact` listeners, and both READMEs state this plainly. Where rules are mounted, exported bodies can differ from canonical log bytes, so receivers must not treat telemetry as a byte-exact replica; the log remains the source of truth. Crash durability is explicitly out of scope until the outbox decision above is revisited.
A deployment adds one `cordis.yml` entry with an OTLP endpoint and gets its session stream in any OTel-compatible stack; removing the entry is the opt-out, with no residual state. A rule-free deployment exports records exactly as captured — including any credentials embedded in file contents or command output — so a deployment crossing a trust boundary must mount `telemetry/record` listeners, and both READMEs state this plainly. Where rules are mounted, exported bodies can differ from canonical log bytes, so receivers must not treat telemetry as a byte-exact replica; the log remains the source of truth. Crash durability is explicitly out of scope until the outbox decision above is revisited.
@@ -13,7 +13,7 @@ Status: implemented
`packages/telemetry/` 以 SDK 立场复活这两个经过评审的包——harness 提供能力,部署方配置上报去向并对导出内容负责:
- **`@deepseek-ai/dsh-session-telemetry`** —— seam 本体。`TelemetryBackend``emit`/`flush?`/`shutdown`)、服务注册形态的 `Telemetry`、以及拥有捕获侧的 `TelemetryCoordinator`:带游标回读的收养、逐 append 的 firehose(投影 → `structuredClone` → 脱敏 → `emit`,零 I/O)、固定的每 (turn, step) 首 chunk 投影、`agent/error` 转发、以及 dispose 时的 `shutdown` 记录。
- **`telemetry/redact` waterfall** —— 相对分支版本的增量。每条记录抵达任何 backend 前必经此处;seam 自身不带任何规则——最内层 `next()` 原样透传,部署方以监听器挂载自己的规则(通过变换 `next()` 的返回值堆叠),抛异常的规则将该记录 fail-closed 扣下。脱敏只作用于导出副本;canonical log 永不改写。
- **`telemetry/record` waterfall** —— 相对分支版本的增量,也是该 seam 的脱敏扩展点。每条记录抵达任何 backend 前必经此处;seam 自身不带任何规则——最内层 `next()` 原样透传,部署方以监听器挂载自己的规则(通过变换 `next()` 的返回值堆叠),抛异常的规则将该记录 fail-closed 扣下。脱敏只作用于导出副本;canonical log 永不改写。
- **`@deepseek-ai/dsh-session-telemetry-otel`** —— 参考 backendOTel JS SDK 日志管线(`LoggerProvider``BatchLogRecordProcessor` → OTLP/HTTP exporter),经 `exporter`/`processor` passthrough 原样配置。`exporter.url` 必填且加载时校验;未挂载或未配置时,任何数据都不会离开进程。
边界公理保持不变:harness 的职责止于 `emit()`。批处理、重试、排队与丢失策略属于 reporting SDK,经 passthrough 配置——投递是尽力而为(崩溃时至多一次),README 对此如实陈述。
@@ -34,4 +34,4 @@ Status: implemented
## Consequences
部署方在 `cordis.yml` 加一个带 OTLP endpoint 的条目即可把会话流接入任何 OTel 兼容体系;删除条目即退出,无残留状态。未挂载规则的部署导出的记录与捕获时完全一致——包括文件内容与命令输出中内嵌的任何凭据——因此跨信任边界的部署必须挂载 `telemetry/redact` 监听器,两个 README 对此如实陈述。挂载规则后,导出的 body 可能与 canonical log 字节不同,接收端不得把遥测当作字节精确副本;日志仍是唯一事实源。崩溃持久性在上述 outbox 决定重启前明确不在范围内。
部署方在 `cordis.yml` 加一个带 OTLP endpoint 的条目即可把会话流接入任何 OTel 兼容体系;删除条目即退出,无残留状态。未挂载规则的部署导出的记录与捕获时完全一致——包括文件内容与命令输出中内嵌的任何凭据——因此跨信任边界的部署必须挂载 `telemetry/record` 监听器,两个 README 对此如实陈述。挂载规则后,导出的 body 可能与 canonical log 字节不同,接收端不得把遥测当作字节精确副本;日志仍是唯一事实源。崩溃持久性在上述 outbox 决定重启前明确不在范围内。
+7 -6
View File
@@ -894,14 +894,15 @@ Source: [`packages/core/system-prompt/src/index.ts:35`](../../packages/core/syst
## `telemetry/*`
### `telemetry/redact` — waterfall
### `telemetry/record` — waterfall
Redact one outbound record before it reaches the backend the seam's scrubbing extension point. The seam ships NO rules of its own: the innermost `next()` passes the record through unchanged, and with no listener mounted records reach the backend as captured, so exported data is exactly as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath. Dispatched synchronously on the capture hot path inside the coordinator's containment: a throwing listener withholds that one record (fail-closed) and never reaches the agent loop. Redaction applies to the exported copy only; the canonical session log is never rewritten.
Transform one outbound record before it reaches the backend. This waterfall is the seam's redaction extension point. It ships NO rules of its own: the innermost `next()` passes the record through unchanged, and with no listener mounted records reach the backend as captured, so exported data is exactly as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath. Dispatched synchronously on the capture hot path inside the coordinator's containment: a throwing listener withholds that one record (fail-closed) and never reaches the agent loop. Redaction applies to the exported copy only; the canonical session log is never rewritten.
```ts cordis-catalog
/**
* Redact one outbound record before it reaches the backend — the seam's
* scrubbing extension point. The seam ships NO rules of its own: the
* Transform one outbound record before it reaches the backend. This
* waterfall is the seam's redaction extension point. It ships NO rules
* of its own: the
* innermost `next()` passes the record through unchanged, and with no
* listener mounted records reach the backend as captured, so exported
* data is exactly as clean as the rules a deployment mounts. Listeners
@@ -915,10 +916,10 @@ Redact one outbound record before it reaches the backend — the seam's scrubbin
* copy; listeners return a (possibly new) record and must not mutate it.
* @mode waterfall
*/
'telemetry/redact'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord
'telemetry/record'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord
```
Source: [`packages/telemetry/session-telemetry/src/index.ts:40`](../../packages/telemetry/session-telemetry/src/index.ts)
Source: [`packages/telemetry/session-telemetry/src/index.ts:41`](../../packages/telemetry/session-telemetry/src/index.ts)
## `tools/*`
+1 -1
View File
@@ -1772,7 +1772,7 @@ flush?(): void
abstract shutdown(): Promise<void>
```
Source: [`packages/telemetry/session-telemetry/src/index.ts:134`](../../packages/telemetry/session-telemetry/src/index.ts)
Source: [`packages/telemetry/session-telemetry/src/index.ts:135`](../../packages/telemetry/session-telemetry/src/index.ts)
## `ctx.tokenMeter` — `TokenMeterService`
+1 -1
View File
@@ -48,7 +48,7 @@ This matrix shows which packages dispatch each harness-owned event and which pac
| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:130`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude`](../packages/hooks/hooks-claude), [`subagent`](../packages/subagent/subagent) |
| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:29`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`system-prompt`](../packages/core/system-prompt) |
| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:35`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
| `telemetry/redact` | `waterfall` | [`packages/telemetry/session-telemetry/src/index.ts:40`](../packages/telemetry/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/telemetry/session-telemetry) (`waterfall`) | - |
| `telemetry/record` | `waterfall` | [`packages/telemetry/session-telemetry/src/index.ts:41`](../packages/telemetry/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/telemetry/session-telemetry) (`waterfall`) | - |
| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:156`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`emit`) | - |
| `tools/code-dispatch-log` | `waterfall` | [`packages/core/tools/src/index.ts:138`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`spill-policy`](../packages/spill/spill-policy) |
| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:113`](../packages/core/tools/src/index.ts) | [`tools`](../packages/core/tools) (`waterfall`) | [`session-checkpoint-policy`](../packages/session-persistence/session-checkpoint-policy), [`timeout-policy`](../packages/timeout/timeout-policy) |
@@ -1,7 +1,7 @@
# Test-only composition: session-telemetry-otel through the real Loader/app
# path, exporting to the mock OTLP collector the driver starts (url via env).
# The redact-rule entry models a deployment mounting its own scrub rule on the
# telemetry/redact waterfall — the seam itself ships no rules.
# telemetry/record waterfall — the seam itself ships no rules.
- id: cli-mock-llm
name: './cli-mock-llm.ts'
@@ -3,7 +3,7 @@ import type { Context } from 'cordis'
/**
* Deployment-style redaction rule for the telemetry e2e: scrubs the fixture
* credential from body strings, exactly as a real deployment would mount its
* own rules on the `telemetry/redact` waterfall.
* own rules on the `telemetry/record` waterfall.
*/
const SECRET = /sk-e2efixture[0-9]+/g
@@ -22,7 +22,7 @@ export const name = 'telemetry-redact-rule'
/** Mount the fixture scrub rule onto the redact waterfall. */
export function apply(ctx: Context): void {
ctx.on('telemetry/redact', (_record, next) => {
ctx.on('telemetry/record', (_record, next) => {
const record = next()
return { ...record, body: scrub(record.body) }
})
@@ -1280,11 +1280,11 @@ export const EVENT_API: readonly EventApiEntry[] = [
summary: 'Emitted when any prompt provider changes.',
},
{
name: 'telemetry/redact',
name: 'telemetry/record',
mode: 'waterfall',
signature: '\'telemetry/redact\'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord',
jsDoc: '/**\n * Redact one outbound record before it reaches the backend — the seam\'s\n * scrubbing extension point. The seam ships NO rules of its own: the\n * innermost `next()` passes the record through unchanged, and with no\n * listener mounted records reach the backend as captured, so exported\n * data is exactly as clean as the rules a deployment mounts. Listeners\n * stack by transforming `next()`\'s return value; returning without\n * `next()` replaces everything beneath. Dispatched synchronously on the\n * capture hot path inside the coordinator\'s containment: a throwing\n * listener withholds that one record (fail-closed) and never reaches the\n * agent loop. Redaction applies to the exported copy only; the canonical\n * session log is never rewritten.\n * @param record - the candidate record, already the coordinator\'s own deep\n * copy; listeners return a (possibly new) record and must not mutate it.\n * @mode waterfall\n */',
summary: 'Redact one outbound record before it reaches the backend — the seam\'s scrubbing extension point.',
signature: '\'telemetry/record\'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord',
jsDoc: '/**\n * Transform one outbound record before it reaches the backend. This\n * waterfall is the seam\'s redaction extension point. It ships NO rules\n * of its own: the\n * innermost `next()` passes the record through unchanged, and with no\n * listener mounted records reach the backend as captured, so exported\n * data is exactly as clean as the rules a deployment mounts. Listeners\n * stack by transforming `next()`\'s return value; returning without\n * `next()` replaces everything beneath. Dispatched synchronously on the\n * capture hot path inside the coordinator\'s containment: a throwing\n * listener withholds that one record (fail-closed) and never reaches the\n * agent loop. Redaction applies to the exported copy only; the canonical\n * session log is never rewritten.\n * @param record - the candidate record, already the coordinator\'s own deep\n * copy; listeners return a (possibly new) record and must not mutate it.\n * @mode waterfall\n */',
summary: 'Transform one outbound record before it reaches the backend.',
},
{
name: 'tools/change',
+3 -3
View File
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: 3e20e3ce31de36311529eb5d91382b9bb0049ddd
README.zh.md: a7036f4f119b144aa61de1c25836c980256d4db7
# pnpm run verify-translation-pairing --write packages/telemetry/README.md
README.md: 944cb3f9bac6169feddf8b49bc481cfbe7c6fa9d
README.zh.md: 7be8af93654565cd9fb4d8b4376e0960fd7eb72b
+1 -1
View File
@@ -2,7 +2,7 @@
English | [中文](README.zh.md)
Outbound session reporting: the telemetry seam plus its OpenTelemetry backend. The design — the boundary axiom (the harness's aspect ends at `emit()`; delivery is the reporting SDK's), the `telemetry/redact` waterfall (deployment-mounted rules; the seam ships none), the fixed chunk projection, the handoff cursor, and the operational-record channel — is pinned in [the revival Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md).
Outbound session reporting: the telemetry seam plus its OpenTelemetry backend. The design — the boundary axiom (the harness's aspect ends at `emit()`; delivery is the reporting SDK's), the `telemetry/record` waterfall (deployment-mounted redaction rules; the seam ships none), the fixed chunk projection, the handoff cursor, and the operational-record channel — is pinned in [the revival Agent Note](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md).
| Package | Role |
|---|---|
+1 -1
View File
@@ -2,7 +2,7 @@
[English](README.md) | 中文
面向外部的会话上报:遥测(telemetryseam 及其 OpenTelemetry 后端。整套设计归档于[复活 Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md):边界公理(harness 的职责止于 `emit()`,投递由上报 SDK 负责)、`telemetry/redact` waterfall(瀑布式事件;脱敏规则由部署方挂载,seam 自身不带任何规则)、固定分片投影、handoff 游标,以及运维记录通道。
面向外部的会话上报:遥测(telemetryseam 及其 OpenTelemetry 后端。整套设计归档于[复活 Agent Noteagent 决策记录)](../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md):边界公理(harness 的职责止于 `emit()`,投递由上报 SDK 负责)、`telemetry/record` waterfall(瀑布式事件;脱敏规则由部署方挂载,seam 自身不带任何规则)、固定分片投影、handoff 游标,以及运维记录通道。
| 包 | 职责 |
|---|---|
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: e345449033b066103fb14e546e8d9909d68fe696
README.zh.md: 6cad58ebc1ad9bab6dc722fcf0d702f500891d32
# pnpm run verify-translation-pairing --write packages/telemetry/session-telemetry-otel/README.md
README.md: 9b208e291e77bee50d9d4fd14808268dca75f2db
README.zh.md: f36cfe74146b779c4ddb1101227cb45a06f0968c
@@ -21,7 +21,7 @@ The OpenTelemetry backend for [the telemetry seam](../session-telemetry/) — th
## What leaves the machine
Records carry the complete `event.data` as the seam's `telemetry/redact` waterfall returns it — user and assistant message content, tool arguments and results (command output, file contents), the full system prompt and tool schemas (`request/header`), todo text, compaction summaries, hook `stderrSummary`, and the session `cwd` (a local path). The seam ships no redaction rules: with no `telemetry/redact` listener mounted, that is the raw captured copy, so a deployment exporting beyond a trusted boundary mounts its own rules (see [the seam README](../session-telemetry/README.md#the-redact-waterfall)). Provider credentials never appear regardless: adapter API keys are constructor parameters, not session events, so they are structurally absent from the log and therefore from telemetry.
Records carry the complete `event.data` as the seam's `telemetry/record` waterfall returns it — user and assistant message content, tool arguments and results (command output, file contents), the full system prompt and tool schemas (`request/header`), todo text, compaction summaries, hook `stderrSummary`, and the session `cwd` (a local path). The seam ships no redaction rules: with no `telemetry/record` listener mounted, that is the raw captured copy, so a deployment exporting beyond a trusted boundary mounts its own rules (see [the seam README](../session-telemetry/README.md#the-redact-waterfall)). Provider credentials never appear regardless: adapter API keys are constructor parameters, not session events, so they are structurally absent from the log and therefore from telemetry.
## Field mapping
@@ -21,7 +21,7 @@
## 哪些数据会离开本机
记录携带完整的 `event.data`,内容以 seam 的 `telemetry/redact` waterfall(瀑布式事件)返回的结果为准:用户与 assistant 消息内容、工具参数与工具结果(命令输出、文件内容)、完整的系统提示词与工具 schema(`request/header`)、todo 文本、压缩(compaction)摘要、钩子的 `stderrSummary`,以及会话 `cwd`(一个本地路径)。seam 不带任何脱敏规则:未挂载 `telemetry/redact` 监听器时,导出的就是捕获原样的副本,因此向可信边界之外导出的部署方要挂载自己的规则(见 [seam README](../session-telemetry/README.md#the-redact-waterfall))。无论如何,提供方凭据都不会出现:适配器的 API key 是构造函数参数而非会话事件,因此它们在结构上就不存在于日志中,也就不存在于遥测中。
记录携带完整的 `event.data`,内容以 seam 的 `telemetry/record` waterfall(瀑布式事件)返回的结果为准:用户与 assistant 消息内容、工具参数与工具结果(命令输出、文件内容)、完整的系统提示词与工具 schema(`request/header`)、todo 文本、压缩(compaction)摘要、钩子的 `stderrSummary`,以及会话 `cwd`(一个本地路径)。seam 不带任何脱敏规则:未挂载 `telemetry/record` 监听器时,导出的就是捕获原样的副本,因此向可信边界之外导出的部署方要挂载自己的规则(见 [seam README](../session-telemetry/README.md#the-redact-waterfall))。无论如何,提供方凭据都不会出现:适配器的 API key 是构造函数参数而非会话事件,因此它们在结构上就不存在于日志中,也就不存在于遥测中。
## 字段映射
@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
README.md: 76f9b242ca0b4779a9ab10e5ae93bab1ffb5e581
README.zh.md: d930dc92e41e85c597aaeff840e6e6b03712b06d
# pnpm run verify-translation-pairing --write packages/telemetry/session-telemetry/README.md
README.md: a3820ee8e91a5c513c08df76b149cf3a670ee2ca
README.zh.md: d607a4ab4d953242ede53ff0b5e50bdf69e29ea4
@@ -14,7 +14,7 @@ The coordinator registers, all through the composing fiber's effects: `session/c
## The redact waterfall
Every record passes the `telemetry/redact` waterfall between projection and `emit()` — the seam's scrubbing extension point. The seam ships NO rules of its own: the innermost `next()` passes the record through unchanged, so with no listener mounted records reach the backend exactly as captured, and exported data is precisely as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath, and a throwing listener withholds that one record fail-closed inside the coordinator's containment. Redaction applies to the exported copy only; the canonical session log is never rewritten.
Every record passes the `telemetry/record` waterfall between projection and `emit()` — the seam's scrubbing extension point. The seam ships NO rules of its own: the innermost `next()` passes the record through unchanged, so with no listener mounted records reach the backend exactly as captured, and exported data is precisely as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; returning without `next()` replaces everything beneath, and a throwing listener withholds that one record fail-closed inside the coordinator's containment. Redaction applies to the exported copy only; the canonical session log is never rewritten.
## The handoff cursor
@@ -39,4 +39,4 @@ None; this package neither assembles nor sends a provider request.
## Known Limitations and Deferred Work
- **Best-effort delivery** — the cursor marks handed-off, not delivered; a session torn down inside a reload window cannot be re-adopted; whatever sits in a backend queue at crash time is lost. A durable outbox (spool, per-sink cursors, at-least-once) is deferred until a deployment states a crash-loss requirement — see [the revival Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md).
- **No built-in redaction rules** — with no `telemetry/redact` listener mounted, records leave the process exactly as captured, including any credentials embedded in file contents or command output; a deployment exporting to a shared collector owns its rule set.
- **No built-in redaction rules** — with no `telemetry/record` listener mounted, records leave the process exactly as captured, including any credentials embedded in file contents or command output; a deployment exporting to a shared collector owns its rule set.
@@ -14,7 +14,7 @@
## 脱敏 waterfall
每条记录在投影与 `emit()` 之间都要经过 `telemetry/redact` waterfall(瀑布式事件),这是该 seam 的擦除扩展点。seam 自身不带任何规则:最内层的 `next()` 原样透传记录,因此未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式扣下这一条记录。脱敏只作用于导出副本;权威会话日志永不改写。
每条记录在投影与 `emit()` 之间都要经过 `telemetry/record` waterfall(瀑布式事件),这是该 seam 的擦除扩展点。seam 自身不带任何规则:最内层的 `next()` 原样透传记录,因此未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式扣下这一条记录。脱敏只作用于导出副本;权威会话日志永不改写。
## handoff 游标
@@ -39,4 +39,4 @@
## 已知限制与延期工作
- **尽力而为的投递**:游标标记的是已交接而非已投递;在重载窗口内被拆除的会话无法重新收养;崩溃时留在后端队列中的内容会丢失。持久 outbox(spool、每 sink 游标、at-least-once)推迟到有部署方提出明确的崩溃丢失要求时再实现;见[复活 Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md)。
- **不内置脱敏规则**:未挂载 `telemetry/redact` 监听器时,记录以捕获时的原样离开进程,包括文件内容或命令输出中内嵌的任何凭据;向共享 collector 导出的部署方自行负责其规则集。
- **不内置脱敏规则**:未挂载 `telemetry/record` 监听器时,记录以捕获时的原样离开进程,包括文件内容或命令输出中内嵌的任何凭据;向共享 collector 导出的部署方自行负责其规则集。
@@ -2,8 +2,8 @@
* Capture coordinator: the seam's upstream half. Subscribes to the session
* firehose plus the one live-bus relay (`agent/error`), applies the fixed
* chunk projection, builds logical records, runs each through the
* `telemetry/redact` waterfall (deployment-mounted rules; pass-through when
* none), and hands the result to the backend — synchronously, with every
* `telemetry/record` waterfall (deployment-mounted redaction rules;
* pass-through when none), and hands the result to the backend — synchronously, with every
* handler self-contained so a failing backend can never starve other
* subscribers (cordis `emit` is stop-on-throw) or touch the agent loop.
* Composed by a backend in its constructor.
@@ -178,14 +178,14 @@ export class TelemetryCoordinator {
}
/**
* Run the `telemetry/redact` waterfall over one record and hand the result
* Run the `telemetry/record` waterfall over one record and hand the result
* to the backend. The innermost `next` passes the record through unchanged
* — the seam ships no rules; exported data is as clean as the listeners a
* deployment mounts. Callers run inside {@link contain}, so a throwing
* rule withholds the record instead of reaching the loop (fail-closed).
*/
private handOff(record: TelemetryRecord): void {
this.backend.emit(this.ctx.waterfall('telemetry/redact', record, () => record))
this.backend.emit(this.ctx.waterfall('telemetry/record', record, () => record))
}
/** Forward the turn-end boundary to the backend's optional flush hint. */
@@ -22,8 +22,9 @@ declare module 'cordis' {
interface Events {
/**
* Redact one outbound record before it reaches the backend — the seam's
* scrubbing extension point. The seam ships NO rules of its own: the
* Transform one outbound record before it reaches the backend. This
* waterfall is the seam's redaction extension point. It ships NO rules
* of its own: the
* innermost `next()` passes the record through unchanged, and with no
* listener mounted records reach the backend as captured, so exported
* data is exactly as clean as the rules a deployment mounts. Listeners
@@ -37,7 +38,7 @@ declare module 'cordis' {
* copy; listeners return a (possibly new) record and must not mutate it.
* @mode waterfall
*/
'telemetry/redact'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord
'telemetry/record'(record: TelemetryRecord, next: () => TelemetryRecord): TelemetryRecord
}
}
@@ -1,5 +1,5 @@
/**
* The `telemetry/redact` waterfall contract: pass-through when no listener is
* The `telemetry/record` waterfall contract: pass-through when no listener is
* mounted, listener stacking and replacement, ops-record coverage, the
* untouched canonical log, and the fail-closed containment of a throwing rule.
*/
@@ -35,7 +35,7 @@ async function setup() {
return { ctx, backend, fiber }
}
describe('telemetry/redact waterfall', () => {
describe('telemetry/record waterfall', () => {
it('passes records through unchanged when no listener is mounted', async () => {
const { ctx, backend } = await setup()
const session = ctx.sessions.create(SessionId('w'))
@@ -46,7 +46,7 @@ describe('telemetry/redact waterfall', () => {
it('applies a mounted rule to every outbound record, ops records included', async () => {
const { ctx, backend, fiber } = await setup()
ctx.on('telemetry/redact', (_record, next) => {
ctx.on('telemetry/record', (_record, next) => {
const record = next()
return { ...record, body: { scrubbed: true } }
})
@@ -62,7 +62,7 @@ describe('telemetry/redact waterfall', () => {
it('keeps the canonical log untouched by a mounted rule', async () => {
const { ctx } = await setup()
ctx.on('telemetry/redact', (_record, next) => ({ ...next(), body: null }))
ctx.on('telemetry/record', (_record, next) => ({ ...next(), body: null }))
const session = ctx.sessions.create(SessionId('log'))
session.append('user/message', { content: [{ type: 'text', text: FIXTURE_SECRET }], source: { kind: 'user' } }, { surfaceOp: 'append' })
const logged = session.events[0]!.data as { content: { text: string }[] }
@@ -72,13 +72,13 @@ describe('telemetry/redact waterfall', () => {
it('stacks listeners outermost-first around next()', async () => {
const { ctx, backend } = await setup()
const order: string[] = []
ctx.on('telemetry/redact', (_record, next) => {
ctx.on('telemetry/record', (_record, next) => {
order.push('outer-before')
const record = next()
order.push('outer-after')
return { ...record, attributes: { ...record.attributes, outer: 1 } }
})
ctx.on('telemetry/redact', (_record, next) => {
ctx.on('telemetry/record', (_record, next) => {
order.push('inner')
const record = next()
return { ...record, attributes: { ...record.attributes, inner: 1 } }
@@ -92,8 +92,8 @@ describe('telemetry/redact waterfall', () => {
it('a listener that skips next() replaces everything beneath it', async () => {
const { ctx, backend } = await setup()
const inner = { called: false }
ctx.on('telemetry/redact', () => ({ channel: 'ops', time: 0, severity: 'info', attributes: {}, body: 'replaced' } satisfies TelemetryRecord))
ctx.on('telemetry/redact', (_record, next) => {
ctx.on('telemetry/record', () => ({ channel: 'ops', time: 0, severity: 'info', attributes: {}, body: 'replaced' } satisfies TelemetryRecord))
ctx.on('telemetry/record', (_record, next) => {
inner.called = true
return next()
})
@@ -105,7 +105,7 @@ describe('telemetry/redact waterfall', () => {
it('a throwing rule withholds the record fail-closed without disturbing the log', async () => {
const { ctx, backend } = await setup()
ctx.on('telemetry/redact', () => {
ctx.on('telemetry/record', () => {
throw new Error('rule exploded')
})
const session = ctx.sessions.create(SessionId('closed'))