round 2: pin checkpoint recognition at compile time

Move COMPACT_CHECKPOINT_SOURCE and isCompactCheckpointSource into a
cordis-free src/checkpoint.ts leaf, re-exported from the root so every
host-side consumer keeps its import. The client can then type-import the
leaf without reaching dsh-session's root, whose Context merge declares the
host sessions service and collides with the client's -- the dsh-commands/brand
shape. Renaming the plugin id now fails the client typecheck.

Also: keep recoverable summary text when a compact/summary mixes text with
other block types, and capture the seeded-history provenance seqs from the
pushes that produce them instead of deriving them by arithmetic.
This commit is contained in:
Hypatia May
2026-07-30 14:07:08 +08:00
parent 8e5c792b50
commit 91ee264e16
21 files changed
+197 -108

No files matched your search

@@ -129,7 +129,14 @@ export interface CompactionSummaryNode {
summary: string | null
}
/** Fallback for surface events this UI version does not know. */
/**
* Fallback for surface events this UI version does not know: the documented
* default arm of `SessionEventMap`, which is merge-extensible, so the
* projection's switch cannot end in `assertNever`. No event produces this node
* today — `isAppendSurfaceEvent` admits only the four types in core's
* `SurfaceEventType`, and each has its own arm — and it exists so widening that
* set core-side degrades to a raw row instead of dropping the event silently.
*/
export interface UnknownSurfaceNode {
kind: 'unknown'
seq: number
@@ -12,25 +12,26 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
// browser bundle cannot resolve; surface.ts has no Node dependencies.
import { isAppendSurfaceEvent, isReplacementSurfaceEvent } from '@deepseek-ai/dsh-session/surface'
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
// Cordis-free leaf subpath (the dsh-commands/brand shape): the seam's own
// declaration of the checkpoint source, reachable as a TYPE from this program.
// The package ROOT is not — it reaches dsh-session's root, whose Context merge
// declares the HOST `sessions: SessionStore` against this program's
// `sessions: ISessions` (TS2717, the one-program-per-side rule in
// docs/development.md).
import type { COMPACT_CHECKPOINT_SOURCE } from '@deepseek-ai/dsh-compact/checkpoint'
import type { ToolCallView, ToolEventView, ToolResultView } from '@deepseek-ai/dsh-client-connection/client'
import type { CommandNode, CompactionSummaryNode, ConversationNode } from './conversation.ts'
import { toAssistantBlocks } from './conversation.ts'
/**
* The compaction seam's checkpoint plugin, restated locally.
*
* `dsh-compact` cannot be reached from this program in any form. A VALUE import
* fails the client purity gate (`packages/client/tsdown.client.ts`) and would
* pull the cordis `Service` base into the browser bundle; a TYPE-ONLY import of
* its `COMPACT_CHECKPOINT_SOURCE` fails typecheck, because `dsh-compact`'s root
* reaches `dsh-session`'s root, whose `Context` merge declares the HOST
* `sessions: SessionStore` against this program's `sessions: ISessions`
* (`TS2717` — the one-program-per-side rule in docs/development.md). The
* literal is pinned to the canonical const by
* `tests/compact-checkpoint-pin.spec.ts`, which runs in the client TEST program
* where that collision does not apply.
* The compaction seam's checkpoint plugin, pinned to the seam's own declaration
* at COMPILE time: renaming it there fails this annotation (`TS2322`). The
* import stays type-only because a value import would fail the client purity
* gate (`packages/client/tsdown.client.ts`) — cross-plugin value imports are
* forbidden in a browser bundle — while an erased type never reaches it.
* `tests/compact-checkpoint-pin.spec.ts` covers the same drift behaviorally.
*/
const COMPACT_PLUGIN = 'compact'
const COMPACT_PLUGIN: typeof COMPACT_CHECKPOINT_SOURCE.plugin = 'compact'
/** In-window tool/call index entry (result-card backfill + runningCalls material). */
export interface CallIndexEntry {
@@ -126,14 +127,20 @@ function isTranscriptEvent(event: SessionEvent): boolean {
return isAppendSurfaceEvent(event) || isCompactCheckpoint(event)
}
/** Concatenated text of a `compact/summary` payload, or null when it carries no usable text. */
/**
* Concatenated text of a `compact/summary` payload, or null when it carries no
* usable text. The payload is a `ContentBlock[]` whose union is
* merge-extensible, so a non-text block is skipped rather than discarding the
* text beside it; a payload with no text block at all falls to null through the
* empty check.
*/
function compactSummaryText(event: SessionEvent): string | null {
const summary = (event.data as unknown as { summary?: unknown }).summary
if (!Array.isArray(summary) || summary.length === 0) return null
if (!Array.isArray(summary)) return null
let text = ''
for (const block of summary as readonly unknown[]) {
const candidate = block as { type?: unknown; text?: unknown }
if (candidate.type !== 'text' || typeof candidate.text !== 'string') return null
if (candidate.type !== 'text' || typeof candidate.text !== 'string') continue
text += candidate.text
}
return text.trim() === '' ? null : text