Revive the reviewed session-telemetry packages from the closed
session-telemetry-otlp-rfc branch (PR #222/#231) on current master, renamed
to @deepseek-ai/dsh-session-telemetry{,-otel} (the SDK component-telemetry
package holds the dsh-telemetry name).
Delta over the branch version: every record now passes a telemetry/redact
waterfall between projection and emit() — the innermost next() applies a
non-configurable conservative credential-shape rule set, listeners stack
stricter rules, a throwing rule withholds the record fail-closed, and the
canonical log is never rewritten. This answers the export-side concern that
closed PR #222; the boundary axiom (our aspect ends at emit(); delivery is
the reporting SDK's) is unchanged, and the runtime-telemetry RFC's outbox /
readCommitted lane is recorded as deferred in the Agent Note.
Covered by seam/redact/OTel-wire unit tiers (100% per-file) and a keyless
Loader-composition e2e that boots the examples fixture against a mock OTLP
collector and pins redaction on the wire plus the untouched canonical log.
@deepseek-ai/dsh-session-telemetry
The telemetry seam: the CAPTURE side of session-event reporting, behind a backend contract any reporting SDK satisfies with zero bending. The boundary axiom that shapes everything here: this package's aspect ends at 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.
The backend contract
TelemetryBackend is three members: emit(record) (MUST be a non-blocking enqueue — it runs synchronously on the session/event hot path), optional flush() (a turn-boundary hint, fire-and-forget), and shutdown() (the lifecycle forward: flush-and-quiesce, awaited at dispose). Telemetry is its service-registered form under the telemetry context key — one implementation per context, duplicate load throws. A backend composes TelemetryCoordinator in its constructor.
Capture points
The coordinator registers, all through the composing fiber's effects: session/created (adopt: record the header, read the log back through the projection — seed events from fork/resume never re-emit on the firehose), session/event (project, deep-copy, redact, 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), agent/error (the one live-bus relay; turn-enclosure structurally bars those errors from the log), a dispose effect (per adopted session emit its shutdown operational record, 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).
The redact waterfall
Every record passes the telemetry/redact waterfall between projection and emit() — nothing reaches a backend unredacted. The innermost next() applies the built-in conservative rule set (applyDefaultRedaction: credential shapes — API keys, GitHub/Slack tokens, AWS/Google keys, JWTs, PEM blocks, URL userinfo — replaced with [REDACTED] in body strings and string attribute values). Listeners stack stricter rules by transforming next()'s return value; returning without next() replaces the default rule set, and a throwing listener withholds that one record fail-closed inside the coordinator's containment. The built-in pattern list is a security invariant, deliberately not configurable from cordis.yml. Redaction applies to the exported copy only; the canonical session log is never rewritten.
The handoff cursor
A module-scope WeakMap<Session, seq> marks the highest seq HANDED OFF (not delivered) per session, advanced at emit time. It survives reloads that do not re-evaluate this module — config re-applies and backend source reloads, which is where iteration happens; that asymmetry is why the cursor lives in the seam. On re-adoption 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 full re-hand, absorbed by receiver-side dedupe on (session.id, event.seq). 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.
The fixed chunk projection
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.
The logical record
TelemetryRecord: channel (ledger | ops), time (epoch ms), severity (pre-mapped: ERROR for tool/result.isError, turn/end error reasons, compact/end errors; WARN for prompt/blocked; INFO otherwise), identity-only attributes (session.id, event.type, event.seq, plus session.cwd/session.parent_id when the header has them), and the complete deep-copied event.data as body — post-redaction. Operational records carry telemetry.op (agent-error | shutdown) and session.id, and deliberately NO event.seq/event.type — signals to alert on, not entries to sum. 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).
Model Experience
None, as the seam only observes the session stream and hands redacted copies to a reporting backend; it never contributes to a model request.
KV Cache effect
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.
- Redaction is shape-based — the default rules catch known credential shapes, not every secret; a deployment with stricter needs stacks
telemetry/redactlisteners, and exported data is only as clean as the mounted rules.