Files
dsh-context/src/shared/types.ts
T
Bowen Liang beaa746409 feat(host): pin live inject nodes into the served surface slice
Injections land on the surface first, so in a long session the plain
maxNodes tail window dropped their identity while their tokens kept
counting (sums cover the full surface) — the browser's inject section
showed a gray '0 items' row with a real token sum and no way to see
what was inside.

The served slice is now the newest maxNodes tail PLUS every live inject
node older than the tail (they are few; the concatenation stays
seq-ordered). droppedNodes and surfaceFloor count only the unserved
non-inject overflow, so the 'N earlier messages' note no longer
double-counts pinned injects, and per-step reconstruction sees them too.
2026-08-18 04:12:45 +08:00

228 lines
8.1 KiB
TypeScript

/**
* Shared wire contract — the snapshot model exchanged between the Host and
* Client halves.
*
* The Host half no longer serves this over a custom RPC channel: it is the
* `view()` payload of the `contextTimeline` session projection, registered on
* the harness's `ctx.sessionProjections` registry. The registry drives
* `apply(state, event)` over every committed session event, persists the state
* through `ctx.sessionProjectionCache`, and pushes the finished value to the
* browser as a `session/projection` frame (with a tail-page baseline), where
* the Client reads it through the framework-standard `useProjection` seat.
*
* TYPE-ONLY host-side module: both halves import these as `import type`, so
* nothing from here ever reaches the runtime bundles.
*/
import type {} from '@deepseek-ai/dsh-session-projection/types'
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionMap {
/**
* The plugin's whole-value context timeline: current composition,
* per-request history, context events, and the model-visible surface.
* The Host folds it from the session log; clients receive the finished
* value (key absence = the plugin's host half is not composed).
*/
contextTimeline: ContextTimeline
/**
* The request-header CONTENT epochs (full system prompt + tool schemas)
* behind the timeline's envelope figures. A separate unit so the hot
* `contextTimeline` value stays lean: headers change rarely, so this
* value (and its pushes) change only when a `request/header` lands.
* The Context browser card reads it to show the actual prompt/schema
* content of a picked step (key absence = older host: tokens only).
*/
contextHeaders: ContextHeaders
}
}
/** The five priced context categories (plus system/tools handled separately). */
export type Category = 'user' | 'inject' | 'assistant' | 'tool'
export interface Snapshot {
ok: boolean
model?: string
provider?: string
contextWindow?: number
current: {
system: number
tools: number
user: number
inject: number
assistant: number
tool: number
total: number
}
/**
* Provider-anchored occupancy of the NEXT request. LEGACY since 0.11: the
* Host no longer folds this — the Client reads the official token-meter
* `contextPressure` projection key (`useProjection('contextPressure')`)
* instead. Kept optional for wire compatibility with older clients.
*/
occupancy?: {
/** Provider-reported prompt size of the most recent request (input + cache). */
pressureTokens?: number
/** Heuristic total over the current model-visible surface. */
surfaceTokens: number
/** `surfaceTokens` at the newest usage sample. */
sampledSurfaceTokens?: number
/** pressureTokens + surface movement since the sample (clamped ≥ 0). */
projectedTokens?: number
/** Newest recorded route capacity (last-wins). */
contextWindow?: number
}
toolList: { name: string; tokens: number }[]
requests: RequestRecord[]
events: ContextEventRecord[]
/**
* The served live surface: the newest `maxNodes` tail PLUS every live
* inject node older than the tail (injections land first and are few, so
* they are pinned — otherwise a long session would price them while the
* browser could list none). Seq-ordered, oldest first.
*/
nodes: SurfaceNode[]
/** Live nodes not served (the overflow beyond `maxNodes`, minus pinned injects — see `nodes`). */
droppedNodes: number
/**
* Recently REMOVED surface nodes (compaction/prune shadows), each stamped
* with `gone` (the replacing event's seq). Together with `nodes` this lets
* the Context browser reconstruct the assembled surface of any retained
* step: alive at request R = seq < R.seq && (gone undefined || gone > R.seq).
*/
archive: SurfaceNode[]
/**
* Coverage floor of the served live `nodes`: the newest seq among the
* `droppedNodes` live nodes not served. Present only when droppedNodes > 0.
*/
surfaceFloor?: number
/**
* Coverage floor of `archive`: the newest `gone` among archive entries the
* retention bounds dropped. Steps with seq < archiveFloor may miss removed
* nodes (the browser shows the reconstruction as approximate).
*/
archiveFloor?: number
}
/**
* The `contextTimeline` projection's whole value — the same snapshot the
* Client has always rendered, now delivered through the session-projection
* pipeline. `ok` is always `true` here (a delivered projection is by
* definition available); it is kept for wire compatibility with the
* snapshot shape.
*/
export type ContextTimeline = Snapshot
/**
* The official token-meter `contextPressure` projection (registered by
* `@deepseek-ai/dsh-token-meter` on the same `SessionProjectionMap`): the
* provider-anchored occupancy of the NEXT request. The Client reads this key
* directly instead of the Host mirroring it inside `contextTimeline`
* (token-meter owns estimation and replay — the docs' stated division of
* labor). Fields are independent last-wins records; absent until a provider
* reports usage. Absent key/value = the registry (or the meter) is not
* composed — the Client falls back to its derived anchor.
*/
export interface ContextPressure {
/** Provider-reported prompt size of the most recent request (input + cache). */
pressureTokens?: number
/** pressureTokens + heuristic surface movement since the sample (clamped ≥ 0). */
projectedTokens?: number
/** Newest recorded route capacity (last-wins). */
contextWindow?: number
}
/** One model-visible message on the surface, with its heuristic token price. */
export interface SurfaceNode {
seq: number
/** Event timestamp (ms epoch); the Client shows it when present. */
time?: number
cat: Category
tokens: number
/**
* Removal marker, present only on `archive` entries: the seq of the
* replacement surface event that shadowed this node (compaction/prune).
* The node is part of the assembled context of every request with
* seq > this node.seq and seq < gone.
*/
gone?: number
form?: string
text?: string
tool?: string
err?: boolean
skill?: string
calls?: string[]
}
/** One answered model call (a step); consecutive records of one turn form it. */
export interface RequestRecord {
turn?: number
step?: number
time: number
seq: number
system: number
tools: number
user: number
inject: number
assistant: number
tool: number
total: number
prompt?: number
output?: number
/**
* Turn-mode aggregate marker, set by the Client's aggregateByTurn (one bar
* per turn shows its LAST step's record). The Host never sets it.
*/
stepCount?: number
}
/** A notable context event (compaction, prune, injection, model switch). */
export interface ContextEventRecord {
seq: number
time: number
kind: 'compaction' | 'prune' | 'inject' | 'model'
form?: string
tokens?: number
count?: number
sub?: string
name?: string
from?: string
to?: string
/** Turn/step of the request logged right BEFORE the event (host-stamped). */
fromTurn?: number
fromStep?: number
/** Turn/step of the request this event contributed to (host-stamped). */
turn?: number
step?: number
}
// ---- contextHeaders projection (request-header content epochs) -------------
/** One tool schema as assembled into a request header, with its display price. */
export interface HeaderTool {
name: string
tokens: number
/** Producer-declared description (may be long; the browser truncates). */
description?: string
/** The raw JSON schema object the model received (plain JSON). */
schema?: unknown
}
/**
* One request-header epoch: the full system prompt and tool schemas in force
* from this event's seq until the next epoch. Headers change rarely (the loop
* only logs `request/header` on change), so this unit's pushes are rare and
* carrying full content is cheap.
*/
export interface HeaderRecord {
seq: number
time: number
system?: string
tools: HeaderTool[]
}
/** The `contextHeaders` projection value: the bounded epoch list (newest last). */
export interface ContextHeaders {
headers: HeaderRecord[]
}