Files
deepseek-harness/packages/support/invariants
Tianyi Cui d6a2ab30c8 feat(types): brand bash ids + stop brand erosion; extract Branded to dsh-brand
Type-only change (brands are zero-cost casts; no runtime/wire impact). Closes
the two gaps in the "brand ids that cross package boundaries" policy and fixes
the dependency direction so a capability package never pulls in an unrelated one.

- Extract the `Branded<B>` primitive into a new standalone type-only package
  `@deepseek-ai/dsh-brand` (packages/util/brand) with no harness-package deps.
  dsh-llm keeps its owned CallId but imports Branded from dsh-brand; dsh-session,
  dsh-agent, and dsh-bash all import Branded from there. dsh-bash depends on
  dsh-brand ALONE — never on dsh-llm or dsh-session (the architectural fix: a
  generic execution backend must not couple to the LLM or session vocabulary).
- Mint BashTaskId + OwnerToken in dsh-bash and thread them through BashTask.id,
  the get/ownerOf/list/readOutput/kill seam, the bash-local generation site, and
  the dsh-tool-bash validate/access surface. OwnerToken is a DISTINCT brand from
  SessionId so the seam stays decoupled; dsh-tool-bash is the single boundary
  that casts SessionId -> OwnerToken.
- Brand at the SOURCE, not via mid-pipeline casts: agent-loop's Config types
  agents[].id as AgentId and resumeSessionId as SessionId, so the brand enters
  at the config boundary and the inner create()/resume casts disappear (only the
  genuinely-new per-run session-id string is cast).
- Stop brand erosion: propagate CallId/SessionId/AgentId to the registry/store
  Map keys and public params/exports (SessionStore, AgentRegistry + factory
  options, the ACP session-id surface + ToolPresenter CallId map, the
  persistence coordinator, invariants pendingCalls, the pi-ai tool-call maps).
- Docs: document BashTaskId/OwnerToken in bash.md (type-equiv re-pasted), point
  the Branded type-equiv at dsh-brand, fix stale param types in the session/
  agent/bash READMEs, regenerate the cordis catalog + module graph.

Implements docs/rfc/proposed/architecture/2026-06-20-branded-ids.md
2026-06-21 07:19:59 +08:00
..

dsh-invariants

Dev-mode event-contract invariants and session-log freeze. A pure-listener plugin (everything is a plugin) that asserts the harness event contract at runtime and, optionally, freezes logged session-event data so any code that mutates history throws instead of corrupting silently.

Off in production. Enable it in tests and the demos, where a contract violation should fail loudly. It costs nothing when not registered, and doubles as executable documentation of the event taxonomy — the assertions are the contract.

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)                     // freeze on (default)
await ctx.plugin(Invariants, { freeze: false })  // assert contract, don't freeze

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 doesn't falsely reject the next event). It listens on session/created, session/event, and agent/status.

Config

Key Default Meaning
freeze true Deep-freeze each logged event's data so mutating a logged event throws. Set false to assert the contract without freezing.

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.
  • a tool/result needs a prior tool/call — but NOT the converse: a tool/call may have no result (a thrown tools/execute waterfall ends the step with no tool/result, which is legal).

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.

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

Why runtime, not deep-readonly types

A DeepReadonly<SessionEvent> is high type-noise across every log consumer, and a plugin can cast straight through it. A dev-mode freeze plus these assertions catch real corruption at zero production cost and zero type noise. The always-on half of that defense — cloning derived messages so request/adapter mutation can't reach back into the log — lives in dsh-session's deriveMessages. This package is the dev-mode tripwire. See dev-mode invariants.

Seeded sessions

A seeded/forked session arrives with events already in its log (the Session constructor copies the seed without emitting session/event). On session/created the plugin replays the existing log through the checker and freezes those entries, so seeded history is held to the same contract.