Files
deepseek-harness/docs/rfc/implemented/simplification/2026-06-20-collapse-trace-only-session-events.md
Tianyi Cui e6fad266a6 docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.

Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.

Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.

The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00

5.1 KiB

RFC: Fold trace-only session facts into load-bearing events

Status: implemented

Problem

The session event vocabulary includes first-class events that are not part of replayable conversation history and have little or no production consumption. usage is already present as a model stream chunk before the loop also appends a separate usage event. error duplicates the turn/end { kind: 'error', message, code } reason for loop failures; ACP settlement reads the turn-end reason, ACP rendering ignores the error event, and deriveMessages() skips it.

These events make the canonical transcript look more useful as telemetry than it currently is. They add event variants, invariants, tests, snapshots, and persistence cases, but they are not load-bearing as separate records. The facts they carry can still be useful: token usage should remain available for accounting, and an error's step number should not silently disappear. The simplification is to fold those facts into nearby events consumers already must understand, not to record less information.

Decision

Standalone trace-only events are removed exactly where their information is preserved without a parallel record:

  • Successful-step usage folds into the matching assistant/message (assistant/message { turn, step, content, usage? }), so the assembled model output and its accounting travel together.
  • A failed or aborted step that has usage but no assistant content carries the usage on an empty-content assistant/message (the implementation note below carries the no-information-loss proof) — no persisted usage chunk goes unrepresented.
  • The step number from the standalone error event folds into turn/end.reason for kind: 'error' ({ kind: 'error', step, message, code? }) — turn/end is the durable turn outcome ACP and resume already consume.
  • agent/error and logging stay for live diagnostics; there is no second session-log error record after turn/end.

The user conversation log contains what is needed to render, resume, audit, and account for the interaction without consumers reconciling duplicate trace rows.

Alternatives considered

Keep the standalone rows as telemetry — the events made the canonical transcript look more useful as telemetry than it was, at the cost of event variants, invariants, tests, snapshots, and persistence cases nothing consumed. If analytics become real, the shape is a projection helper or a dedicated telemetry store with its own retention policy — not duplicate trace rows in the conversation log.

Verification

SessionEventMap carries no standalone usage or error; the loop appends no separate usage event and records durable failures through turn/end { kind: 'error', step, message, code? }; ACP snapshots and persistence tests assert no trace-only lines; recorded fixtures are on the new event shape with the session format version pinned at 0 (backends reject any non-0 stored log per the pre-release format policy); and the docs state where token usage and operational errors are observed.

Consequences

A consumer can no longer filter the canonical log for standalone usage or step-level error rows. It must read those facts from the assistant/failure events that carry them. That is a reasonable simplification only if the implementing PR proves the same facts remain present; otherwise the standalone events should stay.

Implementation note

Shipped as proposed, with one scope refinement (per AGENTS.md "RFCs are proposals, not golden truth"):

  • Empty-content assistant/message hosts usage with no data loss. The proof the proposal demanded (no persisted usage chunk becomes unrepresented) lands on the max-tokens path: a step cut off with usage but empty content (e.g. only a dropped tool call) previously emitted a standalone usage. It now records an empty-content assistant/message { content: [], usage }. To keep that from injecting a spurious content-less assistant turn into the provider transcript, deriveMessages() skips empty-content assistant/message events. A regression test asserts usage stays represented AND derived history is uncorrupted.

Format version. The persisted SessionEventMap shape changed (usage folded onto assistant/message, standalone usage/error removed, step on turn/end.reason.error). The session log uses the pinned-0 "unstable / pre-release" format stance (one of the two stances AGENTS.md § pre-release sanctions): SESSION_FORMAT_VERSION stays 0 and absorbs this and every other pre-release shape change without a monotonic bump — bumping on each tweak would dress up an unstable format as a sequence of stable boundaries that mean nothing yet. The constant is centralized in dsh-session and read by both write sites and the coordinator's load-time check, which rejects any non-0 log (no migration — there is no persisted user data to preserve; a real monotonic policy begins at the first tagged release). turn/end.reason.error.step is required for newly-written logs.

Usage is now observed on assistant/message.usage; an operational error's step on turn/end.reason for kind: 'error'. agent/error + logging are unchanged for live diagnostics.