@deepseek-ai/dsh-session-telemetry 0.1.1-rc.2 → 0.1.2-alpha.2

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.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/session/session-telemetry/README.md
5
- README.md: e4ebee1324bc1228d6d97d891159685b05f94e4f
6
- README.zh.md: 718f07db3fb8d8f8a21236dcb917a33ed3e0cad8
5
+ README.md: 2970106b7b5f27ff2c88deb6c4327b5b8dc12466
6
+ README.zh.md: 47f954e5d1f32d9db7a84bbd0fc38414e7af31c5
package/README.md CHANGED
@@ -1,49 +1,127 @@
1
+ ---
2
+ description: "Session-telemetry capture seam for deployments and backend authors choosing a reporting backend, mounting redaction rules, or implementing the backend contract."
3
+ kind: "package-library"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-session-telemetry
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- The telemetry Service Definition declares the `SessionTelemetrySink` contract, and its capture coordinator passes session records to any reporting SDK backend that implements it. Capture can follow live session events or replay a canonical session-log prefix on demand. This package stops after it calls `emit()`: batching, retry, queueing, and loss policy belong to the backend's SDK and are neither specified nor wrapped. Rationale and rejected alternatives: [the revival Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md), [feedback-gated delivery](../../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.md), and [buffer-free feedback replay](../../../.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.md).
10
+ ## Summary
11
+
12
+ `dsh-session-telemetry` captures session activity for outbound reporting: it projects session events into telemetry records, lets a deployment redact them, and hands them to a reporting backend that implements its contract. Deployments do not load this package directly — they load exactly one backend (the shipped OpenTelemetry backend is `dsh-session-telemetry-otel`), which registers `ctx.sessionTelemetry` and composes the capture coordinator. The seam owns capture, redaction, and the sharing disclosure; batching, retry, queueing, and loss policy belong to the backend's SDK and stop at `emit()`. Every mounted backend discloses its deployment-selected sharing policy so acknowledgement surfaces can report whether and how a session is shared. The contract and capture behavior come first; the implementation internals live in a collapsible developer section below.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ As a deployment, choose a backend, mount it, and add redaction rules when records must not leave the process as captured. As a backend author, implement the three-member contract and compose the coordinator with a capture mode.
29
+
30
+ ### Choosing and mounting a backend
31
+
32
+ Load exactly one backend plugin; it registers `ctx.sessionTelemetry` with the capture coordinator and its own delivery pipeline, and a duplicate load throws. The mounted backend discloses its sharing policy through the required [`sharing` member](#the-sharing-disclosure), which the `/feedback` acknowledgement renders; a consumer renders "not configured" only when no telemetry service is mounted.
33
+
34
+ ### The backend contract
35
+
36
+ A backend implements three members: `emit(record)` must be a non-blocking enqueue because it runs synchronously on the session-event path; optional `flush()` is a fire-and-forget hint after a turn ends, which most backends omit in favor of their SDK's own batching schedule; `shutdown()` drains queued records and resolves when the SDK stops, and disposal awaits it. A backend that implements `flush()` must order concurrent flushes with the final `shutdown()` drain.
37
+
38
+ ### What gets captured
39
+
40
+ Capture runs in one of two modes. `live` capture follows session events as they are appended, replays already-live sessions at mount time, and records lifecycle markers; `on-demand` capture reads the canonical session log only when the backend requests a prefix through `captureSession(session, throughSeq?)`. Ledger records mirror session events one to one except for one projection: only the first `assistant/chunk` of each `(turn, step)` ships, so `seq` gaps on the wire are routine and never a loss signal. Each record carries the event's complete data, minimal identity attributes, and a pre-mapped severity (`error` for `tool/result.isError`, `turn/end` error reasons, and `agent-error`; `info` otherwise).
41
+
42
+ ### The sharing disclosure
43
+
44
+ <a id="the-sharing-disclosure"></a>
6
45
 
7
- ## The backend contract
46
+ Every backend discloses its deployment-selected sharing policy through the seam's `sharing` vocabulary: `full` (every event is handed over as it happens), `feedback-only` (nothing is handed over until a `feedback/record` event releases the unreleased prefix), or `disabled` (nothing is handed over at all). The acknowledgement of a recorded feedback entry reports this status; the disclosure never claims delivery — handoff is the non-blocking enqueue, and batching, retry, and loss policy stay the backend SDK's.
8
47
 
9
- `SessionTelemetrySink` has three members: `emit(record)` MUST enqueue without blocking because it runs synchronously during `session/event` or explicit canonical-log replay; optional `flush()` is a fire-and-forget hint after a turn ends, and most backends omit it and use their SDK's normal batching schedule; `shutdown()` drains queued records and resolves when the SDK stops, and disposal awaits it. An implementation that provides `flush()` must order concurrent flushes with the final `shutdown()` drain. `SessionTelemetryBackend` registers this API under the `sessionTelemetry` context key; each context accepts one implementation, and a duplicate load throws. A backend constructs `SessionTelemetryCoordinator` with `live` or `on-demand` capture and calls `captureSession(session, throughSeq?)` at its chosen trigger.
48
+ ### Redacting records
10
49
 
11
- The service also carries the required [`SessionTelemetrySharingStatus`](#the-sharing-disclosure) `sharing` member: the deployment-selected sharing policy every backend must disclose to human-facing acknowledgement surfaces (the `/feedback` command's confirmation). A consumer renders "not configured" only when no telemetry service is mounted. The seam owns the vocabulary (`full` | `feedback-only` | `disabled`) so any backend can disclose a policy without depending on the OTel package.
50
+ <a id="the-redact-waterfall"></a>
12
51
 
13
- ## The sharing disclosure
52
+ Every outbound record passes the `sessionTelemetry/record` waterfall immediately after projection. This package ships no rules: with no listener mounted, records reach the backend exactly as captured, so exported data is as clean as the rules a deployment mounts. Listeners stack by transforming `next()`'s return value; a throwing listener withholds that one record fail-closed. Redaction applies to the outbound copy only — the canonical session log is never rewritten.
14
53
 
15
- The acknowledgement of a recorded feedback entry reports whether and how the session is shared, read from the mounted backend's `sharing`. A backend sets the property from its deployment configuration: `full` (every event is handed over as it happens), `feedback-only` (nothing is handed over until a `feedback/record` event releases the unreleased prefix through it), or `disabled` (nothing is handed over at all). Consumers map the status onto user-facing copy; the disclosure never claims delivery — handoff is the non-blocking enqueue, and batching, retry, and loss policy stay the backend SDK's.
54
+ -----
16
55
 
17
- ## Capture points
56
+ <a id="understand-the-implementation"></a>
57
+ ## Understand the implementation
18
58
 
19
- In `live` mode the coordinator registers, all through the composing fiber's effects: `session/created` (adopt: record the header, read the log back through the projection from the construction boundary — constructor seeds from fork/resume never re-emit on the firehose and never re-export), `session/event` (project, deep-copy, redact, then hand off; zero I/O), `session/flush` (forward the optional `flush()` hint and return void — the loop's awaited parallel must never wait on telemetry), `session/disposed` (capture the session's `shutdown` operational record at its termination edge, then retire it), `agent/error` (the one live-bus relay; the session event vocabulary intentionally has no operational-error record), a dispose effect (capture shutdown for each still-live session, then await the backend's `shutdown()`; failures warn instead of throwing), and an adoption sweep of `ctx.sessions.list()` (a hot reload does not replay `session/created`). In `on-demand` mode it registers only the dispose effect: `captureSession()` reads the canonical log through an optional inclusive sequence boundary, while flush hints and operational events remain local.
59
+ <details>
60
+ <summary>Implementation internals — click to expand</summary>
20
61
 
21
- ## The redact waterfall
62
+ This section explains the capture design; the observable behavior is fully covered in [Use this package](#use-this-package).
22
63
 
23
- Every record passes the `sessionTelemetry/record` waterfall immediately after projection — the Service Definition's scrubbing extension point. This package 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. Live capture runs the waterfall at append time; on-demand capture runs it while replaying the canonical log, using the rules mounted at that time. Redaction applies to the outbound copy only; the canonical session log is never rewritten.
64
+ ### Design concept
24
65
 
25
- ## The handoff cursor
66
+ The seam is built on one boundary: the harness's aspect ends at `emit()`. Capture, projection, redaction, and the handoff cursor live here; batching, retry, queueing, and loss policy are the reporting SDK's, deliberately not modelled or wrapped. The design and rejected alternatives are pinned in the [revival Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md).
26
67
 
27
- A module-scope `WeakMap<Session, seq>` marks the highest seq HANDED OFF (not delivered) per session. Live capture advances it at append time; on-demand capture advances it only while `captureSession()` hands a requested prefix to the backend. An uncaptured prefix remains solely in the canonical log, so a coordinator reload adds no telemetry-owned recovery state. On replay the coordinator re-hands only events past the cursor (events at or below it still rebuild the chunk-projection state); a missing cursor safely degrades to a re-hand from the session's construction boundary (`Session.firstLiveSeq` — seq 0 for a session born in this process), absorbed by receiver-side dedupe on `(session.id, event.seq)`. Constructor seeds never re-export: a resumed session's history shipped from the previous process under the same id, and a fork's inherited prefix lives in the parent's stream (receivers stitch on `session.parent_id` + `session.seed_length`). The accepted cost, consistent with at-most-once delivery: a resume does not backfill records a previous process failed to deliver — a deployment with a backfill requirement needs the deferred outbox, not replay. This is a deliberate, narrow exception to the registrations-are-effects discipline: entries die with their sessions, the value is a monotonic watermark, and losing it is never an error.
68
+ ### Source map
28
69
 
29
- ## The fixed chunk projection
70
+ | File | Role |
71
+ |---|---|
72
+ | [`src/index.ts`](src/index.ts) | Service Definition: `SessionTelemetryBackend`/`SessionTelemetrySink` contract, record vocabulary, `session-telemetry/record` waterfall declaration |
73
+ | [`src/coordinator.ts`](src/coordinator.ts) | Capture: live listeners, on-demand replay, chunk projection, redaction, handoff cursor, containment |
30
74
 
31
- Only the first `assistant/chunk` of each `(turn, step)` ships; the rest are dropped at capture and never advance the cursor. That one chunk is the stream-started signal: `step/start` + first-chunk presence + `assistant/message` presence + the `turn/end` reason distinguish "the request never started" from "the stream died midway" without chunk volume, and time-to-first-token stays computable. Chunk elision makes `seq` gaps routine on the wire — a gap is never a loss signal. Every other event type, including ones merged by plugins this package never heard of, passes through whole.
75
+ ### Capture flow
32
76
 
33
- ## The logical record
77
+ Live capture registers, through the composing fiber's effects: `session/created` adopts the session and replays its log from the handoff cursor; `session/event` projects, deep-copies, redacts, and hands off with zero I/O; `session/flush` forwards the optional hint and returns void so the loop's awaited parallel never waits on telemetry; `session/disposed` captures the session's `shutdown` marker and retires it; `agent/error` is the one live-bus relay, because the session-event vocabulary intentionally has no operational-error record. Disposal captures shutdown markers for still-live sessions, then awaits the backend's `shutdown()`. On-demand capture registers only the disposal effect and reads the canonical log on request. Every synchronous handler runs inside containment so a failing backend or rule can never starve other listeners or reach the agent loop.
34
78
 
35
- `SessionTelemetryRecord`: `channel` (`ledger` | `ops`), `time` (epoch ms), `severity` (pre-mapped: ERROR for `tool/result.isError`, `turn/end` error reasons, and `agent-error`; INFO for other captured records, while `sessionTelemetry/record` policies may assign WARN), identity-only `attributes` (`session.id`, `event.type`, `event.seq`, plus `session.cwd`/`session.parent_id`/`session.seed_length` when the header has them), and the complete deep-copied `event.data` as `body` — post-redaction. Operational records carry `sessionTelemetry.op` (`agent-error` | `shutdown`) and `session.id`, and deliberately NO `event.seq`/`event.type` — signals to alert on, not entries to sum; `agent-error` normalizes its arbitrary thrown value into a stable `{ name, message }` body. Delivery downstream of the handoff is the backend SDK's; duplicates remain possible (cursor-less re-adoption, SDK retries), so receivers dedupe on `(session.id, event.seq)`.
79
+ ### The handoff cursor
36
80
 
81
+ A module-scope `WeakMap<Session, seq>` records, per session, the highest seq handed off (not delivered). Live capture advances it at append time; on-demand capture advances it only while handing a requested prefix. An uncaptured prefix remains solely in the canonical log, so a coordinator reload adds no telemetry-owned recovery state; a missing cursor safely degrades to re-handing from the session's construction boundary, absorbed by receiver-side dedupe on `(session.id, event.seq)`. This is a narrow, documented exception to the registrations-are-effects discipline: entries die with their sessions, the value is a monotonic watermark, and losing it is never an error. The accepted cost matches at-most-once delivery: a resumed session does not backfill records a previous process failed to deliver.
82
+
83
+ </details>
84
+
85
+ -----
86
+
87
+ <a id="further-exploration"></a>
88
+ ## Further Exploration
89
+
90
+ Read these pages when the seam contract is not enough. They move from the shipped backend to the subsystem reference and the decision evidence.
91
+
92
+ - [OpenTelemetry telemetry backend](../session-telemetry-otel/README.md) — the shipped backend deployments load, with mode and exporter configuration.
93
+ - [Session telemetry subsystem](../../../docs/subsystems/session-telemetry.md) — the capability split and type declarations.
94
+ - [Session telemetry revival decision](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.md) — rationale, trade-offs, and rejected alternatives.
95
+ - [Session package map](../README.md) — adjacent persistence, projection, title, and telemetry packages.
96
+
97
+ -----
98
+
99
+ <a id="model-experience"></a>
37
100
  ## Model Experience
38
101
 
39
- None, as this package only observes the session stream and hands redacted copies to a reporting backend; it never contributes to a model request.
102
+ None, as the seam observes the session stream and hands redacted copies outward; it registers nothing model-facing.
40
103
 
41
104
  #### KV Cache effect
42
105
 
43
- None; this package neither assembles nor sends a provider request.
106
+ None; the package neither assembles nor sends a provider request.
44
107
 
45
108
  ## Known Limitations and Deferred Work
46
109
 
47
- - **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).
110
+ <a id="known-limitations-and-deferred-work"></a>
111
+
112
+
113
+ These limits define the delivery and data-protection guarantees a deployment gets. They are current package constraints.
114
+
115
+ - **Best-effort delivery** — the cursor marks handed-off, not delivered; a session torn down inside a reload window cannot be re-adopted, and 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.
48
116
  - **No built-in redaction rules** — with no `sessionTelemetry/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.
49
- - **On-demand redaction uses current state** — uncaptured events exist only in the canonical session log. A later `captureSession()` deep-copies and redacts their current values with the policy mounted at that time; there is no capture-time telemetry snapshot or durable pre-capture spool.
117
+ - **On-demand redaction uses current state** — uncaptured events exist only in the canonical session log; a later `captureSession()` deep-copies and redacts their current values with the policy mounted at that time, and there is no capture-time telemetry snapshot or durable pre-capture spool.
118
+
119
+ <a id="dev-note"></a>
120
+ ### Dev Note
121
+
122
+ <details>
123
+ <summary>Working context for maintainers — click to expand</summary>
124
+
125
+ None.
126
+
127
+ </details>
package/README.zh.md CHANGED
@@ -1,53 +1,127 @@
1
+ ---
2
+ description: "面向部署方与后端作者的会话遥测捕获 seam 说明,用于选择上报后端、挂载脱敏规则或实现后端约定。"
3
+ kind: "package-library"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-session-telemetry
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- 遥测(telemetry)Service Definition 声明 `SessionTelemetrySink` 后端约定,捕获协调器把会话记录传给实现该约定的任意上报 SDK 后端。捕获侧可跟随实时会话事件,也可按需回放权威会话日志前缀。本包调用 `emit()` 后就停止处理:批处理、重试、排队与丢失策略都属于后端自身的 SDK,本包既不规定也不包装。设计依据与被否决的替代方案见[复活 Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md)、[反馈门控投递](../../../.agents/notes/implemented/feature/2026-08-05-feedback-gated-session-telemetry.zh.md)与[无缓冲反馈回放](../../../.agents/notes/implemented/simplification/2026-08-06-buffer-free-feedback-telemetry.zh.md)。
10
+ ## 概述
6
11
 
7
- ## 后端约定
12
+ `dsh-session-telemetry` 捕获会话活动用于对外上报:它把会话事件投影为遥测记录,允许部署方脱敏,再交给实现其约定的上报后端。部署方不直接加载本包——它们只加载一个后端(随附的 OpenTelemetry 后端是 `dsh-session-telemetry-otel`),由它注册 `ctx.sessionTelemetry` 并组装捕获协调器。seam 拥有捕获、脱敏与共享披露;批处理、重试、排队与丢失策略属于后端自身的 SDK,止于 `emit()`。每个已挂载后端都披露其部署级共享策略,使确认 surface 能够报告会话是否以及如何被共享。约定与捕获行为在前;实现内部细节放在下方可折叠的开发者章节中。
8
13
 
9
- `SessionTelemetrySink` 有三个成员:`emit(record)` 必须入队且不能阻塞,因为它会在 `session/event` 或显式权威日志回放期间同步执行;可选的 `flush()` 是轮次结束后的提示,调用方不等待结果,多数后端省略它并使用 SDK 的常规批处理计划;`shutdown()` 排空已入队记录,并在 SDK 停止后结束,dispose(资源释放)会等待它。提供 `flush()` 的实现必须安排并发 flush 与 `shutdown()` 最终排空的先后顺序。`SessionTelemetryBackend` 将此 API 注册在 `sessionTelemetry` 上下文键下:每个上下文只允许一个实现,重复加载会抛出异常。后端以 `live` 或 `on-demand` 捕获构造 `SessionTelemetryCoordinator`,并在自己选择的触发器中调用 `captureSession(session, throughSeq?)`。
14
+ ## 目录
10
15
 
11
- 该服务还携带必需的 [`SessionTelemetrySharingStatus`](#the-sharing-disclosure) `sharing` 成员:每个后端都必须向面向用户的确认 surface(`/feedback` 命令的确认文本)披露的部署级共享策略。消费方只有在未挂载任何遥测服务时才渲染「未配置」。seam 拥有该词汇(`full` | `feedback-only` | `disabled`),因此任何后端都可以披露策略,而无需依赖 OTel 包。
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
12
22
 
13
- <a id="the-sharing-disclosure"></a>
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 作为部署方,选择一个后端并挂载它,当记录不能以捕获原样离开进程时添加脱敏规则。作为后端作者,实现三成员约定,并以一种捕获模式组装协调器。
14
29
 
15
- ## 共享披露
30
+ ### 选择并挂载后端
16
31
 
17
- 一条已记录的反馈条目的确认文本会报告该会话是否以及如何被共享,读取自已挂载后端的 `sharing`。后端根据其部署配置设置该属性:`full`(每个事件在发生时立即交接)、`feedback-only`(在 `feedback/record` 事件释放其之前的未释放前缀之前,不交接任何内容)或 `disabled`(完全不交接任何内容)。消费方把状态映射为面向用户的文案;披露从不声称投递——交接是非阻塞入队,批处理、重试与丢失策略仍归后端 SDK。
32
+ 只加载一个后端插件;它把捕获协调器与自己的投递流水线注册为 `ctx.sessionTelemetry`,重复加载会抛出异常。已挂载后端通过必需的 [`sharing` 成员](#the-sharing-disclosure) 披露共享策略,`/feedback` 的确认文本会渲染它;只有在未挂载任何遥测服务时,消费方才渲染「未配置」。
18
33
 
19
- ## 捕获点
34
+ ### 后端约定
20
35
 
21
- `live` 模式中,协调器的全部注册都经由组合方 fiber 的 effect 完成:`session/created`(收养:记录 header,并经投影从构造边界起回读日志;来自 fork 或恢复的构造函数种子绝不会在 firehose 上再次发出,也绝不会再次导出)、`session/event`(投影、深拷贝、脱敏,再交接;零 I/O)、`session/flush`(转发可选的 `flush()` 提示并返回 void;循环所等待的并行任务绝不能等待遥测)、`session/disposed`(在会话自身的终止边缘捕获该会话的 `shutdown` 运维记录,然后将其退役)、`agent/error`(唯一的实时总线转发;会话事件词汇有意不包含运维错误记录)、一个 dispose effect(捕获每个仍存活会话的 shutdown,再等待后端的 `shutdown()`;失败只发出警告而不抛出),以及对 `ctx.sessions.list()` 的收养扫描(热重载不会重放 `session/created`)。在 `on-demand` 模式中,协调器只注册 dispose effect:`captureSession()` 读取权威日志,直至可选的序列号边界(含边界);flush 提示与运维事件留在本地。
36
+ 后端实现三个成员:`emit(record)` 必须是非阻塞入队,因为它会在会话事件路径上同步执行;可选的 `flush()` 是轮次结束后的即发即忘提示,多数后端为了遵循 SDK 自身的批处理计划而省略它;`shutdown()` 排空已入队记录,并在 SDK 停止后结束,dispose(资源释放)会等待它。实现 `flush()` 的后端必须安排并发 flush 与最终 `shutdown()` 排空的先后顺序。
37
+
38
+ ### 捕获内容
39
+
40
+ 捕获以两种模式之一运行。`live` 捕获在追加时跟随会话事件、在挂载时回放已存活会话并记录生命周期标记;`on-demand` 捕获只在后端通过 `captureSession(session, throughSeq?)` 请求前缀时读取权威会话日志。ledger 记录与会话事件一一对应,唯有一个投影例外:每个 `(turn, step)` 只发出第一条 `assistant/chunk`,因此导出流中的 `seq` 缺口是常态,绝不是丢失信号。每条记录携带事件的完整数据、最小身份属性与预先映射的严重级别(`tool/result.isError`、`turn/end` 的错误原因与 `agent-error` 映射为 `error`;其余为 `info`)。
41
+
42
+ ### 共享披露
43
+
44
+ <a id="the-sharing-disclosure"></a>
45
+
46
+ 每个后端都通过 seam 的 `sharing` 词汇披露其部署级共享策略:`full`(每个事件在发生时立即交接)、`feedback-only`(在 `feedback/record` 事件释放其之前的未释放前缀之前,不交接任何内容)或 `disabled`(完全不交接任何内容)。已记录反馈条目的确认文本会报告该状态;披露从不声称投递——交接是非阻塞入队,批处理、重试与丢失策略仍归后端 SDK。
47
+
48
+ ### 脱敏记录
22
49
 
23
50
  <a id="the-redact-waterfall"></a>
24
51
 
25
- ## 脱敏 waterfall(瀑布式事件)
52
+ 每条外发记录在投影后立即经过 `sessionTelemetry/record` waterfall(瀑布式事件)。本包不带任何规则:未挂载监听器时,记录以捕获时的原样到达后端,因此导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;抛出异常的监听器以 fail-closed 方式拦下这一条记录。脱敏只作用于外发副本——权威会话日志永不改写。
53
+
54
+ -----
55
+
56
+ <a id="understand-the-implementation"></a>
57
+ ## 理解实现
58
+
59
+ <details>
60
+ <summary>实现细节——点击展开</summary>
61
+
62
+ 本节解释捕获设计;可观察行为已在[使用本包](#use-this-package)中完整说明。
26
63
 
27
- 每条记录在投影后立即经过 `sessionTelemetry/record` waterfall,这是 Service Definition 的脱敏扩展点。本包自身不带任何规则:最内层的 `next()` 原样透传记录,因此未挂载监听器时,记录以捕获时的原样到达后端;导出数据能干净到什么程度,恰恰取决于部署方挂载了什么规则。监听器通过变换 `next()` 的返回值来堆叠;不调用 `next()` 就返回,即替换其下方的全部逻辑;抛出异常的监听器会在协调器的隔离范围内以 fail-closed 方式拦下这一条记录。实时捕获在追加时运行 waterfall;按需捕获则在回放权威日志时使用当时挂载的规则运行 waterfall。脱敏只作用于外发副本;权威会话日志永不改写。
64
+ ### 设计理念
28
65
 
29
- ## handoff 游标
66
+ seam 建立在一个边界之上:harness 的职责止于 `emit()`。捕获、投影、脱敏与 handoff 游标都在这里;批处理、重试、排队与丢失策略属于上报 SDK,本包有意不建模也不包装。设计与被否决的替代方案见[复活 Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md)。
30
67
 
31
- 一个模块作用域的 `WeakMap<Session, seq>` 记录每个会话已交接(而非已投递)的最高 seq。实时捕获在追加时推进游标;按需捕获只有在 `captureSession()` 将请求的前缀交给后端时才推进游标。未捕获的前缀只留在权威日志中,因此协调器重载不会增加遥测自有的恢复状态。回放时,协调器只重新交接游标之后的事件(游标及其之前的事件仍用于重建分片投影状态);游标缺失时安全退化为从会话构造边界起的重新交接(`Session.firstLiveSeq`,对在本进程中诞生的会话即 seq 0),由接收端基于 `(session.id, event.seq)` 的去重吸收。构造函数种子绝不会再次导出:恢复会话的历史已由上一个进程以同一 id 发出,fork 继承的前缀则位于父会话的流中(接收端基于 `session.parent_id` + `session.seed_length` 拼接)。由此接受的代价与至多一次(at-most-once)投递一致:恢复不会回填上一个进程未能投递的记录;有回填要求的部署需要的是已推迟的 outbox,而不是回放。这是对「注册即 effect」纪律的一次有意且范围极窄的例外:条目随其会话消亡,值是单调水位线,丢失它绝不是错误。
68
+ ### 源码地图
32
69
 
33
- ## 固定分片投影
70
+ | 文件 | 职责 |
71
+ |---|---|
72
+ | [`src/index.ts`](src/index.ts) | Service Definition:`SessionTelemetryBackend`/`SessionTelemetrySink` 约定、记录词汇、`session-telemetry/record` waterfall 声明 |
73
+ | [`src/coordinator.ts`](src/coordinator.ts) | 捕获:live 监听器、on-demand 回放、分片投影、脱敏、handoff 游标、异常隔离 |
34
74
 
35
- 每个 `(turn, step)` 只发出第一条 `assistant/chunk`;其余分片在捕获时丢弃,且绝不推进游标。这一条分片就是「流已开始」的信号:`step/start`、首分片是否存在、`assistant/message` 是否存在,加上 `turn/end` 的原因,无需分片流量即可区分「请求从未开始」与「流中途夭折」,首个 token 延迟(time-to-first-token)也仍然可以计算。分片省略使导出流中的 `seq` 缺口成为常态:缺口绝不是丢失信号。其余所有事件类型都会完整透传,包括本包从未听说过的插件所合并的事件类型。
75
+ ### 捕获流程
36
76
 
37
- ## 逻辑记录
77
+ live 捕获通过组合方 fiber 的 effect 注册:`session/created` 收养会话并从 handoff 游标起回放其日志;`session/event` 投影、深拷贝、脱敏并交接,零 I/O;`session/flush` 转发可选的提示并返回 void,使循环所等待的并行任务绝不等待遥测;`session/disposed` 捕获会话的 `shutdown` 标记并退役它;`agent/error` 是唯一的实时总线转发,因为会话事件词汇有意不包含运维错误记录。dispose 会为仍存活的会话捕获 shutdown 标记,然后等待后端的 `shutdown()`。on-demand 捕获只注册 dispose effect,并在请求时读取权威日志。每个同步处理器都运行在异常隔离之内,使失败的后端或规则永远不会饿死其他监听器,也永远不会触及 agent loop。
38
78
 
39
- `SessionTelemetryRecord` 包含:`channel`(`ledger` | `ops`)、`time`(epoch 毫秒)、`severity`(预先映射好的严重级别:`tool/result.isError`、`turn/end` 的错误原因与 `agent-error` 映射为 ERROR,其他已捕获记录映射为 INFO,而 `sessionTelemetry/record` 策略可以指定 WARN)、只含身份信息的 `attributes`(`session.id`、`event.type`、`event.seq`,header 中存在时再加 `session.cwd`/`session.parent_id`/`session.seed_length`),以及作为 `body` 的完整深拷贝 `event.data`,且以脱敏后的内容为准。运维记录携带 `sessionTelemetry.op`(`agent-error` | `shutdown`)和 `session.id`,并刻意不带 `event.seq`/`event.type`:它们是用来告警的信号,不是用来累加的条目;`agent-error` 会把任意抛出值规范化为稳定的 `{ name, message }` 记录主体。交接之后的投递由后端 SDK 负责;重复仍然可能出现(无游标的重新收养、SDK 重试),因此接收端基于 `(session.id, event.seq)` 去重。
79
+ ### handoff 游标
40
80
 
81
+ 一个模块作用域的 `WeakMap<Session, seq>` 按会话记录已交接(而非已投递)的最高 seq。live 捕获在追加时推进它;on-demand 捕获只在交接所请求的前缀时推进它。未捕获的前缀只留在权威日志中,因此协调器重载不会增加遥测自有的恢复状态;游标缺失时安全退化为从会话构造边界起重新交接,由接收端基于 `(session.id, event.seq)` 的去重吸收。这是对「注册即 effect」纪律的一次有意的、有文档说明的窄例外:条目随其会话消亡,值是单调水位线,丢失它绝不是错误。由此接受的代价与至多一次(at-most-once)投递一致:恢复的会话不会回填上一个进程未能投递的记录。
82
+
83
+ </details>
84
+
85
+ -----
86
+
87
+ <a id="further-exploration"></a>
88
+ ## 进一步探索
89
+
90
+ 当 seam 约定不够用时阅读以下页面。它们从随附后端逐步进入子系统参考与决策证据。
91
+
92
+ - [OpenTelemetry 遥测后端](../session-telemetry-otel/README.zh.md)——部署方加载的随附后端,含模式与导出器配置。
93
+ - [会话遥测子系统](../../../docs/subsystems/session-telemetry.zh.md)——能力拆分与类型声明。
94
+ - [会话遥测复活决策](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md)——理由、权衡与被否决的替代方案。
95
+ - [会话包映射](../README.zh.md)——相邻的持久化、投影、标题与遥测包。
96
+
97
+ -----
98
+
99
+ <a id="model-experience"></a>
41
100
  ## 模型体验
42
101
 
43
- 无。本包只观察会话流,并把脱敏后的副本交给上报后端;它绝不向模型请求贡献任何内容。
102
+ 无,因为该 seam 观察会话流并把脱敏后的副本交给外部;它不注册任何面向模型的内容。
44
103
 
45
104
  #### KV Cache 影响
46
105
 
47
106
  无;本包既不组装也不发送提供方请求。
48
107
 
49
- ## 已知限制与暂缓事项
108
+ ## 已知限制与延期工作
109
+
110
+ <a id="known-limitations-and-deferred-work"></a>
111
+
112
+
113
+ 这些限制定义部署方能得到的投递与数据保护保证。它们是当前包约束。
114
+
115
+ - **尽力而为的投递**——游标标记的是已交接而非已投递;在重载窗口内被拆除的会话无法重新收养,崩溃时留在后端队列中的内容会丢失。持久化 outbox(spool、每 sink 游标、at-least-once)推迟到有部署方提出明确的崩溃丢失要求时再实现。
116
+ - **不内置脱敏规则**——未挂载 `sessionTelemetry/record` 监听器时,记录以捕获时的原样离开进程,包括文件内容或命令输出中内嵌的任何凭据;向共享 collector 导出的部署方自行负责其规则集。
117
+ - **按需脱敏使用当前状态**——未捕获的事件只存在于权威会话日志中;后续的 `captureSession()` 会使用当时挂载的策略,深拷贝并脱敏其当前值,且不存在捕获时的遥测快照或持久化的捕获前 spool。
118
+
119
+ <a id="dev-note"></a>
120
+ ### 开发备注
121
+
122
+ <details>
123
+ <summary>维护者的工作上下文——点击展开</summary>
124
+
125
+ 无。
50
126
 
51
- - **尽力而为的投递**:游标标记的是已交接而非已投递;在重载窗口内被拆除的会话无法重新收养;崩溃时留在后端队列中的内容会丢失。持久化 outbox(spool、每 sink 游标、at-least-once)推迟到有部署方提出明确的崩溃丢失要求时再实现;见[复活 Agent Note](../../../.agents/notes/implemented/feature/2026-07-23-session-telemetry-otel-revival.zh.md)。
52
- - **不内置脱敏规则**:未挂载 `sessionTelemetry/record` 监听器时,记录以捕获时的原样离开进程,包括文件内容或命令输出中内嵌的任何凭据;向共享 collector 导出的部署方自行负责其规则集。
53
- - **按需脱敏使用当前状态**:未捕获的事件只存在于权威会话日志中。后续的 `captureSession()` 会使用当时挂载的策略,深拷贝并脱敏其当前值;不存在捕获时的遥测快照或持久化的捕获前 spool。
127
+ </details>
package/lib/index.js CHANGED
@@ -130,8 +130,8 @@ var SessionTelemetryCoordinator = class {
130
130
  * at or below the start still feed the projection state (first-chunk
131
131
  * tracking) without being re-handed, so a resumed fiber drops mid-step
132
132
  * chunk continuations exactly like the fiber that saw the step begin. The
133
- * cost, accepted with the capture contract's at-most-once stance: a resume no longer
134
- * backfills records a previous process failed to deliver.
133
+ * cost, accepted with the capture contract's at-most-once stance: a resume
134
+ * does not backfill records a previous process failed to deliver.
135
135
  * @param session - the live session to adopt; a second adoption is a no-op.
136
136
  */
137
137
  adopt(session) {
@@ -72,8 +72,8 @@ export declare class SessionTelemetryCoordinator {
72
72
  * at or below the start still feed the projection state (first-chunk
73
73
  * tracking) without being re-handed, so a resumed fiber drops mid-step
74
74
  * chunk continuations exactly like the fiber that saw the step begin. The
75
- * cost, accepted with the capture contract's at-most-once stance: a resume no longer
76
- * backfills records a previous process failed to deliver.
75
+ * cost, accepted with the capture contract's at-most-once stance: a resume
76
+ * does not backfill records a previous process failed to deliver.
77
77
  * @param session - the live session to adopt; a second adoption is a no-op.
78
78
  */
79
79
  private adopt;
@@ -126,9 +126,8 @@ export interface SessionTelemetrySink {
126
126
  /**
127
127
  * Deployment-selected session-sharing policy disclosed by a mounted
128
128
  * {@link SessionTelemetryBackend} backend to human-facing acknowledgement surfaces (the
129
- * `/feedback` command's confirmation text). The seam owns the vocabulary so
130
- * any backend can disclose a policy without depending on the OTel package;
131
- * the values mirror the OTel backend's serialized `SessionTelemetryMode` choices.
129
+ * `/feedback` command's confirmation text). The Service Definition owns the
130
+ * vocabulary so consumers and backends do not depend on a specific provider.
132
131
  */
133
132
  export type SessionTelemetrySharingStatus = 'full' | 'feedback-only' | 'disabled';
134
133
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-session-telemetry",
3
3
  "description": "SessionTelemetryBackend seam for the DeepSeek Harness: session-event capture, projection, redaction, and handoff to a reporting backend",
4
- "version": "0.1.1-rc.2",
4
+ "version": "0.1.2-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -32,15 +32,15 @@
32
32
  ],
33
33
  "license": "MIT",
34
34
  "peerDependencies": {
35
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
36
- "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
37
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
38
- "@deepseek-ai/cordis": "^4.0.1"
35
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
36
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
37
+ "@deepseek-ai/cordis": "^4.0.2",
38
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2"
39
39
  },
40
40
  "devDependencies": {
41
- "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
42
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
43
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
44
- "@deepseek-ai/cordis": "^4.0.1"
41
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
42
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
43
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2",
44
+ "@deepseek-ai/cordis": "^4.0.2"
45
45
  }
46
46
  }