Files
deepseek-harness/packages/support/invariants
Tianyi Cui eb5ae54a79 Merge branch 'codex/goal-tools' into codex/goal-session
# Conflicts:
#	.agents/notes/implemented/feature/2026-07-19-persisted-same-session-goal-domain.i18n.yaml
#	docs/cordis-catalog/events.md
#	docs/core-data-structures/core.md
#	docs/event-producer-consumer.md
#	docs/module-graph.md
#	examples/package.json
#	packages/core/agent/README.md
#	packages/core/agent/src/types.ts
#	website/zh-CN/api/harness/events.md
2026-07-20 20:35:15 +08:00
..

dsh-invariants

Runtime event-contract assertions intended for development diagnostics. This pure-listener plugin checks relationships among session events, agent states, scoped dispatches, and model requests; it does not own or change product behavior.

The plugin has no environment guard: it is active wherever it is registered. The default dsh-agent-spine-demo bundle mounts it unconditionally; a custom composition can omit it when the runtime cost is undesirable. It doubles as executable documentation of the event taxonomy — the assertions are the contract.

Session itself owns immutable, surface-valid log storage in every composition: it takes one lossless JSON snapshot of each candidate, validates complete provenance and positional replacement, restricts tool/result replacement to one current result's content, deep-freezes the accepted record, and exposes the log through immutable array snapshots. The invariants plugin checks the remaining cross-record and cross-seam rules that Session does not own.

Session-log assertions run during Cordis internal/dispatch, while Session.append() is resolving the session/event callback snapshot but before it pushes the candidate into the log. A valid transition is staged by exact event identity and applied to the live trace only when that same committed event reaches the plugin's contained post-commit listener. A later internal dispatch check can therefore veto without advancing either the log or the invariant trace, while ordinary session/event observer failures remain observe-only.

Plugin

A functional plugin — register the module namespace (this is what loading by name in cordis.yml does):

import type { Context } from 'cordis'
import * as Invariants from '@deepseek-ai/dsh-invariants'

declare const ctx: Context

await ctx.plugin(Invariants)

inject: ['sessions'] — it reads ctx.sessions.list() at apply time to rebuild trace state for sessions that already exist, so a hot reload mid-turn does not falsely reject the next event. The oracle listeners are explicitly global so pre-commit staging and post-commit application keep the same audience even if the plugin is mounted under a scoped context; their cleanup still belongs to that mounting fiber. The plugin has no configuration.

Invariants asserted

Session log (per session):

  • seq strictly increases — the spine of replay equivalence.
  • turns pair and nestturn/start opens a turn, turn/end closes the matching one; no overlapping turns.
  • steps nest in turnsstep/start opens a step in the open turn; step/end closes the matching step.
  • chunks belong to an open stepstep/start precedes its assistant/chunks.
  • an appended tool/result needs a prior tool/call — fresh surfaceOp: 'append' results name the open step and consume its pending call. A Session-validated replacement is a turn-enclosed rewrite, not another execution. A tool/call may still have no result when the execution pipeline throws.

Agent status (per agent):

  • legal transitions onlyidle↔running and (idle|running)→disposed. A no-op transition (setStatus dedups, so it never fires) and leaving the terminal disposed state are violations.

Model requests (on llm/stream):

  • a loop-built request is exactly what the log reconstructs — a frozen request with a live sessionId (the loop-built marker; hand-built one-shots like compaction's summarize are unfrozen and skipped) must carry frozen messages deep-equal to the derivation over the log prefix strictly before the in-flight step's step/start (rebuilt through a FRESH Session, so the live cache cannot vouch for itself — and boundary-correct: content logged after step/start legitimately belongs to the next request), and every non-content field must equal the latest logged request/header (see the reconstructability Agent Note). Registered with prepend: true so a short-circuiting llm/stream listener (the replay adapter) cannot silence it; prepend orders it against append-registered listeners only — correctness rests on the seq-bounded rebuild, never listener timing.

On any violation it throws InvariantError (code: 'INVARIANT').

Why runtime assertions remain useful

Session enforces the per-record storage boundary at runtime, where a cast cannot bypass it. Pervasive DeepReadonly<SessionEvent> types would add noise across consumers without expressing relationships such as turn/step nesting, subject-correct scoped dispatch, or equality between a request and its log reconstruction. This plugin checks those relationships wherever it is mounted while dsh-session keeps history immutable in every composition. See source-owned session immutability and dev-mode invariants.

Seeded sessions

A seeded or forked session arrives with events already in its log because construction does not emit session/event for each seed record. Session validates, snapshots, and freezes every seed record before accepting it; on session/created, this plugin replays the accepted log only to rebuild and check its relational trace state.

Model Experience

None, as this observer only validates events and frozen requests and never rewrites prompts, schemas, messages, or streams.

KV Cache effect

None; this package neither assembles nor sends a provider request.

Known Limitations and Deferred Work

  • The request-reconstructability assertion covers loop-built requests only — hand-built one-shots (e.g. compaction's summarize call) carry no live sessionId marker and are skipped.
  • Merge-extended event families get no family-specific assertionscompact/* lock pairing and hook/* invoked/result pairing are not checked here; only the core turn/step/chunk/tool-result contract is.