The trend card gains a title-adjacent Duration toggle: every request's step active time (`activeMs`) drawn as a curve over the bars, read off a right-hand quartile axis; turn granularity sums the turn's steps, and a bar without the figure breaks the line instead of faking a point. The fold stamps it at `step/end` onto the step's committed request record: the whole step window (the model call plus its tool runs, per the agent-loop's in-step `executeToolCalls`) minus the step's user waits — approval decision intervals (`approval/asked` → `approval/decided`, a crash-dangling pair closing at the step's own end) and `ask_user_question` answer windows (the tool's body is the wait itself, in both the blocking and the timed schema's foreground phase). Overlapping waits price their clamped union, never twice. The wire field is additive-optional and bumps the timeline projection's stateVersion (25 → 26): requests already folded can never gain the figure later, so cached rows refold from the durable log on upgrade.
799 lines
33 KiB
TypeScript
799 lines
33 KiB
TypeScript
/**
|
||
* Shared wire contract — the snapshot model exchanged between the Host and Client halves. Delivered as the `view()` payload of the
|
||
* `contextTimeline`/`contextHeaders` session projections (registered on `ctx.sessionProjections`; the registry pushes finished views as
|
||
* `session/projection` frames — see host/timeline.ts). TYPE-ONLY host-side module: both halves import these as `import type`, so nothing
|
||
* from here ever reaches the runtime bundles.
|
||
*/
|
||
|
||
import type { ActivityState } from '../host/activity'
|
||
import type { HeadersState } from '../host/headers'
|
||
import type { TimelineState } from '../host/fold'
|
||
// The registry package ROOT carries the `@deepseek-ai/cordis` Context
|
||
// augmentation (`sessionProjections` service); the `/types` subpath below
|
||
// only declares the merge-extensible maps.
|
||
import type {} from '@deepseek-ai/dsh-session-projection'
|
||
import type {} from '@deepseek-ai/dsh-session-projection/types'
|
||
|
||
declare module '@deepseek-ai/dsh-session-projection/types' {
|
||
// Both maps take the plugin keys: `SessionProjectionMap` names the
|
||
// client-visible wire values, `SessionProjectionStateMap` constrains the
|
||
// unit `key`s and their fold-state types (the split arrived with
|
||
// dsh 0.1.1 — the definition's K constraint moved to the state map).
|
||
interface SessionProjectionMap {
|
||
/**
|
||
* The plugin's context timeline: current composition, counters, and the
|
||
* headline anchor. Since the split generation the wire value is the SLIM
|
||
* head (every delivery channel — session.list rows, control baselines,
|
||
* push frames — carries it whole); the per-request history, context
|
||
* events, and the surface/archive collections ride the on-demand detail
|
||
* channel ({@link ContextTimelineDetail}, host/detail.ts). Channel-less
|
||
* hosts keep the inline generation (the collections stay in the 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 per-day activity ledger (billed tokens + completed requests keyed
|
||
* by local day) behind the Context Dashboard's heatmap — the timeline's
|
||
* "current snapshot" cannot draw a per-day chart, so the overview reads
|
||
* this off every session-list row's projection column. Tiny (one small
|
||
* record per day, retention-capped), so riding every list row costs
|
||
* nothing next to the timeline head. Key absence = older host: the
|
||
* heatmap degrades to its empty note.
|
||
*/
|
||
contextActivity: ContextActivity
|
||
}
|
||
interface SessionProjectionStateMap {
|
||
contextTimeline: TimelineState
|
||
contextHeaders: HeadersState
|
||
contextActivity: ActivityState
|
||
}
|
||
}
|
||
|
||
/**
|
||
* The priced surface buckets. `skill` carries every skill-machinery content
|
||
* the harness injects (issue #66): the `<available_skills>` catalog digest,
|
||
* a user-explicit `/name` invocation's instructions message, and the content
|
||
* a `skill`-tool load returns (modeled as a tool result by the harness).
|
||
*/
|
||
export type Category = 'user' | 'inject' | 'skill' | 'assistant' | 'tool'
|
||
|
||
/**
|
||
* One live system-prompt node (Snapshot.systems) — the harness models the
|
||
* system prompt as a surface node, so its TEXT is fetched on demand from the
|
||
* `system/message` event at `seq`. `tokens` is the node's heuristic
|
||
* price (0 for a dormant empty node, which the harness reads as "no system
|
||
* prompt"); the effective figure is the LAST node with `tokens > 0`.
|
||
*/
|
||
export interface SystemPromptNode {
|
||
seq: number
|
||
time: number
|
||
tokens: number
|
||
}
|
||
|
||
/**
|
||
* The stats board's count figures, precomputed host-side over the RETAINED
|
||
* request/event records (the same set the detail payload serves). Carried by
|
||
* the split-generation wire head so the board — and the Agent card's
|
||
* per-session request tally — never need the collections themselves.
|
||
* `steps` doubles as the retained request-record count.
|
||
*/
|
||
export interface TimelineCounts {
|
||
turns: number
|
||
steps: number
|
||
injects: number
|
||
compactions: number
|
||
prunes: number
|
||
}
|
||
|
||
/**
|
||
* The newest retained request record's billing summary — the headline's
|
||
* derived-occupancy anchor (`prompt + surface movement since`), carried by
|
||
* the split-generation wire head so the headline never needs the request
|
||
* records themselves.
|
||
*/
|
||
export interface TimelineLast {
|
||
seq: number
|
||
total: number
|
||
prompt?: number
|
||
}
|
||
|
||
/** One day's ledger entry in the `contextActivity` projection. */
|
||
export interface ActivityDay {
|
||
/**
|
||
* Billed tokens folded from provider-reported usage that day (prompt-side
|
||
* input + cache read/write + output). Requests without a usage settlement
|
||
* count only toward `requests` — a fabricated 0 never understates the day.
|
||
*/
|
||
tokens: number
|
||
/** Completed model calls (assistant settlements) that day. */
|
||
requests: number
|
||
/**
|
||
* The day's billed buckets keyed provider → model → pricing period — the
|
||
* same SessionCostUsage raw material the timeline's session-cost totals
|
||
* carry, scoped to the day the requests INITIATED in, so the client prices
|
||
* each day off the same model-price book as the KPI band. ADDITIVE-OPTIONAL:
|
||
* absent on rows folded before the field existed (older plugin builds), on
|
||
* days whose settlements predate any model header, and on settlements the
|
||
* provider left unmetered — the usage chart degrades to tokens-only bars.
|
||
*/
|
||
cost?: SessionCostUsage
|
||
}
|
||
|
||
/**
|
||
* The per-session daily activity ledger (`contextActivity` wire value):
|
||
* day key (`YYYY-MM-DD`, host-local — see shared/days.ts) → that day's
|
||
* billed volume, retention-capped by the fold. The Context Dashboard merges
|
||
* every listed session's ledger into its activity heatmap.
|
||
*/
|
||
export interface ContextActivity {
|
||
days: Record<string, ActivityDay>
|
||
}
|
||
|
||
/**
|
||
* The per-user display-preference vocabulary of the `dsh-context` settings
|
||
* namespace — the ONE declaration both halves share: the Host registers the
|
||
* namespace schema against it (host/settings.ts), the Client binds the scope
|
||
* and edits fields by name (client/settings.ts). Type-only, so both bundles
|
||
* erase it.
|
||
*/
|
||
export type DefaultGranularity = 'step' | 'turn'
|
||
|
||
export type DefaultTrendMode = 'total' | 'delta'
|
||
|
||
/** The browser delta baseline: against the immediately preceding record, or the previous turn's last step. */
|
||
export type DefaultDeltaBase = 'step' | 'turn'
|
||
|
||
/** File Activity row order: most operations first, most-recently-touched first, or path ascending. */
|
||
export type DefaultFileSort = 'count' | 'latest' | 'path'
|
||
|
||
/** Tool-definition row order: largest schema first, most call hits first, or name ascending. */
|
||
export type DefaultToolSort = 'size' | 'count' | 'name'
|
||
|
||
/** Where the Context view is offered: the conversation tab, the right Sidebar, or both. */
|
||
export type DefaultPlacement = 'all' | 'tab' | 'sidebar'
|
||
|
||
/** Whether the Context Insights panel's sidebar entry is offered at all. */
|
||
export type InsightsEntry = 'show' | 'hide'
|
||
|
||
export interface PluginSettings {
|
||
defaultPlacement: DefaultPlacement
|
||
defaultGranularity: DefaultGranularity
|
||
defaultTrendMode: DefaultTrendMode
|
||
defaultDeltaBase: DefaultDeltaBase
|
||
defaultToolSort: DefaultToolSort
|
||
defaultFileSort: DefaultFileSort
|
||
insightsEntry: InsightsEntry
|
||
}
|
||
|
||
/** The section fields the settings card edits, as the Host schema names them. */
|
||
export type SettingsField = keyof PluginSettings
|
||
|
||
export interface Snapshot {
|
||
ok: boolean
|
||
/**
|
||
* The host's baseline-gate record, present ONLY when the running harness
|
||
* is below the plugin's supported baseline: the host then folds nothing
|
||
* and every figure in this snapshot is zero/empty. The client keeps
|
||
* rendering the (blank) cards and pops the upgrade gate modal naming
|
||
* `current` (the detected harness version) and `minimum` (the baseline).
|
||
*/
|
||
unsupported?: {
|
||
current: string
|
||
minimum: string
|
||
}
|
||
model?: string
|
||
provider?: string
|
||
contextWindow?: number
|
||
current: {
|
||
system: number
|
||
tools: number
|
||
user: number
|
||
inject: number
|
||
skill: number
|
||
assistant: number
|
||
tool: number
|
||
total: number
|
||
}
|
||
/**
|
||
* Image blocks live in the CURRENT context (user uploads plus tool-result
|
||
* images, nested blocks included) — the sum over the live surface nodes'
|
||
* `imgs`, so compaction/prune shrink it. Absent from older hosts; clients
|
||
* treat absence as zero.
|
||
*/
|
||
images?: number
|
||
/**
|
||
* Tool calls whose result is live in the CURRENT context (one `tool/result`
|
||
* folds to one `tool` surface node). Calls still in flight and results
|
||
* compacted/pruned out of the surface are not counted. Absent from older
|
||
* hosts; clients treat absence as zero.
|
||
*/
|
||
toolCalls?: number
|
||
/**
|
||
* Whole-session human-input tally: every non-injection `user/message`
|
||
* (the user's own messages) plus every answered `ask_user_question`
|
||
* result (one per answer submission). A running total over the COMPLETE
|
||
* log — turns the retained window no longer holds still count. Absent
|
||
* from older hosts; clients treat absence as zero.
|
||
*/
|
||
humanInputs?: number
|
||
/**
|
||
* The user's newest own message as a one-line bounded preview (first text
|
||
* block, whitespace collapsed, ~80 chars): the session cards' footer line.
|
||
* Additive-optional — absent from rows folded before the field existed
|
||
* (older hosts, idle sessions' cached rows); clients hide the line then.
|
||
*/
|
||
lastUser?: string
|
||
/**
|
||
* Split-generation head fields — present exactly when the host serves the
|
||
* SLIM head (the heavy collections moved to the on-demand detail channel,
|
||
* host/detail.ts) and absent on the inline generation (older or
|
||
* channel-less hosts serve the collections in place). `detailRev` is the
|
||
* detail's revision marker: it bumps whenever the detail collections
|
||
* change, so an open tab refetches on the push alone.
|
||
*/
|
||
counts?: TimelineCounts
|
||
last?: TimelineLast
|
||
detailRev?: number
|
||
/**
|
||
* The per-request history / context-event collections. On the split
|
||
* generation these stay ABSENT from the wire value (every session.list row,
|
||
* control baseline, and push frame would carry them whole otherwise); the
|
||
* client fills them from the detail channel ({@link ContextTimelineDetail}).
|
||
*/
|
||
requests: RequestRecord[]
|
||
events: ContextEventRecord[]
|
||
/**
|
||
* Cumulative session-cost raw material (per-provider, per-model billed
|
||
* token totals — see SessionCostUsage). Absent until a request with a
|
||
* known model reports usage.
|
||
*/
|
||
cost?: SessionCostUsage
|
||
/**
|
||
* Whole-session timing totals (see TimingTotals). Absent until the first
|
||
* step lifecycle completes in the log (older plugin builds never folded
|
||
* one — clients treat absence as an empty timing card).
|
||
*/
|
||
timing?: TimingTotals
|
||
/**
|
||
* The live system-prompt nodes, oldest first — the browser's per-step source
|
||
* for the System section. Absent on rows folded before this field existed
|
||
* (older plugin builds; the client then falls back to the header epoch's
|
||
* own `systemTokens`, the legacy wire shape those builds served).
|
||
*/
|
||
systems?: SystemPromptNode[]
|
||
/**
|
||
* 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). 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 fold-derived file-operation log and its trim floor — present on the
|
||
* INLINE wire value (channel-less hosts) and on the detail payload
|
||
* (ContextTimelineDetail), absent from the slim head (they ride the detail
|
||
* channel there).
|
||
*/
|
||
fileOps?: FileOpRecord[]
|
||
fileOpsFloor?: number
|
||
/**
|
||
* The timing strip's painted spans (see TimingSpan) — present on the INLINE
|
||
* wire value and on the detail payload, absent from the slim head (they ride
|
||
* the detail channel there) and from rows folded before the collection
|
||
* existed (older hosts; the client then shows no strip).
|
||
*/
|
||
spans?: TimingSpan[]
|
||
}
|
||
|
||
/**
|
||
* The on-demand DETAIL payload of the split `contextTimeline` generation —
|
||
* the heavy collections (per-request records, context events, the served
|
||
* surface window, and the removed-node archive) that the slim wire head no
|
||
* longer carries through every delivery channel. The host serves it off the
|
||
* live fold state at the `/dsh-context` `detail` endpoint (host/detail.ts);
|
||
* `rev` mirrors the head's `detailRev` at build time and acts as the
|
||
* client's latest-wins cursor.
|
||
*/
|
||
export interface ContextTimelineDetail {
|
||
rev: number
|
||
/**
|
||
* The slim wire head at the SAME fold cut as the collections: the
|
||
* composition scalars (`current`), the window/model envelope, and the
|
||
* precomputed counts. Sessions listed cold (never attached since the
|
||
* requesting unit last changed) carry no `contextTimeline` projection row
|
||
* for the browser's list reads, so the Agent network card fetches this
|
||
* head per node to render their composition rings. The host always serves
|
||
* it; optional so a payload missing it still serves the collections (the
|
||
* detail cards) and only the ring composition degrades.
|
||
*/
|
||
head?: ContextTimeline
|
||
requests: RequestRecord[]
|
||
events: ContextEventRecord[]
|
||
nodes: SurfaceNode[]
|
||
droppedNodes: number
|
||
archive: SurfaceNode[]
|
||
surfaceFloor?: number
|
||
archiveFloor?: number
|
||
/**
|
||
* The fold-derived file-operation log (shared/fileOps.ts): one record per
|
||
* executed file op, newest-retained, covering the full log (never
|
||
* window-bound like the client-side join derivation it replaces on this
|
||
* generation). Code-Mode nested dispatches book ops located on their
|
||
* parent run_code result (`parent`).
|
||
*/
|
||
fileOps?: FileOpRecord[]
|
||
/** The newest dropped op's seq when the op log trimmed (coverage honesty, same family as archiveFloor). */
|
||
fileOpsFloor?: number
|
||
/**
|
||
* The timing strip's painted spans (see TimingSpan): the completed steps'
|
||
* time slices in log order, positioned by their real instants. Additive-
|
||
* optional: a payload folded before the collection existed (older hosts)
|
||
* serves without it, and the timing card shows no strip then.
|
||
*/
|
||
spans?: TimingSpan[]
|
||
}
|
||
|
||
/**
|
||
* One executed file operation (a settled file-tool call with a resolved
|
||
* target), folded host-side from the durable tool lifecycle: the call's
|
||
* name+arguments (`tool/call`), the result's presentation meta and error
|
||
* (`tool/result`), or a nested Code-Mode settle (`tool/ptc-dispatch`,
|
||
* located on its parent run_code result via `parent` + `program`).
|
||
*
|
||
* `gone` is NOT host-stamped: the client joins it from the detail's archive
|
||
* at render time (the op's result node leaving the live surface marks where
|
||
* its content is still viewable). Line deltas are estimates read off the
|
||
* call ARGUMENTS (an edit's old/new strings, a write's content), never off
|
||
* result payloads.
|
||
*/
|
||
export interface FileOpRecord {
|
||
seq: number
|
||
/** The op's file; for a pathless search the searched PATTERN (`pattern: true`). */
|
||
path: string
|
||
kind: 'read' | 'write' | 'search'
|
||
tool: string
|
||
time?: number
|
||
err: boolean
|
||
added: number
|
||
removed: number
|
||
/** What was searched for, when a search named both a path and a pattern. */
|
||
detail?: string
|
||
/** Meta-attributed search op only: matched lines the result reported for this file. */
|
||
hits?: number
|
||
/** Read ops only: the exact 1-based window the result meta reported, else the `limit`-argument estimate (`est: true`). */
|
||
read?: { start: number; count: number } | { count: number; est: true }
|
||
/** Nested Code-Mode op only: the run_code result node the op ran under (the locate target). */
|
||
parent?: number
|
||
/** Nested Code-Mode op only: the run_code program's model-authored description. */
|
||
program?: string
|
||
/** The searched-pattern marker: `path` is a pattern, not a file — display must not relativize it. */
|
||
pattern?: true
|
||
/** Client-joined archive stamp (see the type note); absent on the wire. */
|
||
gone?: number
|
||
}
|
||
|
||
/**
|
||
* The `contextTimeline` projection's whole value — the same snapshot the Client has always rendered. `ok` is always `true` here (a
|
||
* delivered projection is by definition available); 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
|
||
}
|
||
|
||
/**
|
||
* The official token-meter `contextBreakdown` projection: the heuristic
|
||
* composition rows the chat ring's click-open panel shows (system prompt,
|
||
* tool schemas, conversation). The Client reads this key directly so the
|
||
* composition card's proportions AND counts stay identical to the panel's
|
||
* by construction; the message bucket is subdivided into the plugin's four
|
||
* surface categories by the fold's per-category ratios. Absent key/value =
|
||
* an older harness without the meter's projection units — callers fall back
|
||
* to the fold's own sums (identical estimator, minus the image correction).
|
||
*/
|
||
export interface ContextBreakdown {
|
||
systemTokens: number
|
||
toolsTokens: number
|
||
messageTokens: number
|
||
}
|
||
|
||
/**
|
||
* The official token-meter `tokenUsage` projection (registered by
|
||
* `@deepseek-ai/dsh-token-meter` on the same `SessionProjectionMap`): durable
|
||
* cumulative provider-reported usage across the COMPLETE session log. The four
|
||
* buckets are disjoint (reasoning tokens are already inside `outputTokens`).
|
||
* The Client reads this key directly to compute the cache-hit share — the
|
||
* exact same data the chat stats line below the input box shows, same formula
|
||
* — instead of the Host mirroring it inside `contextTimeline`. Absent until a
|
||
* provider reports usage.
|
||
*/
|
||
export interface TokenUsage {
|
||
/** Billed prompt tokens that missed the provider cache. */
|
||
uncachedInputTokens: number
|
||
/** Billed output tokens (reasoning included). */
|
||
outputTokens: number
|
||
/** Billed prompt tokens served from the provider cache. */
|
||
cacheReadTokens: number
|
||
/** Billed prompt tokens written into the provider cache. */
|
||
cacheWriteTokens: number
|
||
}
|
||
|
||
/**
|
||
* One painted span of the timing card's session-time strip (the card's bottom
|
||
* band): a completed step's time slice — the TTFT wait, one decode block (in
|
||
* stream order), one tool-run window, or an in-step residue gap — folded
|
||
* host-side at `step/end`. The instants are the slice's REAL occurrence times
|
||
* (the hover tip reads them); the strip packs the spans gapless in log order,
|
||
* each band's width its share of the session's cumulative active time (idle
|
||
* time BETWEEN steps carries no span and takes no track). `end` > `start`
|
||
* always (the fold drops zero/negative slices).
|
||
*/
|
||
export interface TimingSpan {
|
||
/** The slice kind — the timing card's own slice vocabulary (same keys, same colors). */
|
||
kind: 'ttft' | 'reasoning' | 'text' | 'toolarg' | 'tools' | 'other'
|
||
start: number
|
||
end: number
|
||
}
|
||
|
||
/**
|
||
* Cumulative billed-token totals for one pricing bucket of the session-cost
|
||
* estimate (host-folded, never trimmed — running totals over the COMPLETE
|
||
* session log, immune to the request/event retention bounds).
|
||
*/
|
||
export interface CostBucketTotals {
|
||
uncached: number
|
||
cacheRead: number
|
||
cacheWrite: number
|
||
output: number
|
||
}
|
||
|
||
/**
|
||
* One completed tool name's whole-session call tally behind the timing
|
||
* card's top-tools ranking (running totals, never trimmed).
|
||
*/
|
||
export interface ToolTimingTotals {
|
||
calls: number
|
||
ms: number
|
||
}
|
||
|
||
/**
|
||
* Whole-session timing totals, host-folded from the durable `step/start` /
|
||
* `step/end` / `tool/call` / `tool/result` lifecycle plus the model call's
|
||
* first token (running totals over the COMPLETE session log — the same
|
||
* never-trimmed framing as `cost`). The first token comes from the call's
|
||
* own embedded stream (`assistant/message.data.stream` /
|
||
* `assistant/attempt.data.stream`), matching the harness's own
|
||
* session-stats fold. Durations are wall-clock milliseconds: `wallMs` sums
|
||
* whole steps, `ttftMs` the step-start → decode-start slice (the model wait)
|
||
* and `genMs` the decode-start → assistant-message slice (the generation) —
|
||
* both only over calls whose stream carried a token delta, `toolsMs` the sum
|
||
* of per-call tool durations (parallel calls each count, so it can overlap).
|
||
* The decode window opens at the first OBSERVABLE instant — the first token,
|
||
* or an earlier `block-start` marker the token packing could not stamp (a
|
||
* redacted reasoning block leaves no chunk behind): anchoring at the token
|
||
* would charge the marker-tiled window to the wait as well, double-counting
|
||
* it past 100% in the legend. Absent until the first step lifecycle
|
||
* completes in the log.
|
||
*
|
||
* The generation window itself splits by WHAT was being decoded, off the
|
||
* stream's `block-start` framing (`blockType`): `reasoningMs` (the model's
|
||
* thinking), `textMs` (the answer text), and `toolArgMs` (the tool-call
|
||
* arguments). Each marker owns the interval up to the next one (the last one
|
||
* up to the assistant message), so the three tile the marker span — exactly
|
||
* `genMs` when the first marker leads the first token (the window opens
|
||
* there); when the first marker sits marginally PAST the token the lead-in
|
||
* gap stays inside `genMs` unattributed, so the buckets never exceed it.
|
||
* They are ADDITIVE-OPTIONAL: cached projection rows written before
|
||
* the split carry `genMs` without them, so the card falls back to the
|
||
* un-split shape instead of the cache row being discarded (the
|
||
* stateVersion-15 rationale in host/timeline.ts).
|
||
*/
|
||
export interface TimingTotals {
|
||
/** Summed wall time of completed steps (the session's active time). */
|
||
wallMs: number
|
||
/** Summed step-start → decode-start time (the model wait, TTFT). */
|
||
ttftMs: number
|
||
/** Summed decode-start → assistant-message time (the generation). */
|
||
genMs: number
|
||
/** Reasoning-decode slice of `genMs` (the model's thinking). */
|
||
reasoningMs?: number
|
||
/** Counted reasoning-decode blocks (the Thinking slice's tally). */
|
||
reasoningBlocks?: number
|
||
/** Answer-text decode slice of `genMs`. */
|
||
textMs?: number
|
||
/** Counted answer-text decode blocks (the Answer slice's tally). */
|
||
textBlocks?: number
|
||
/** Tool-call-argument decode slice of `genMs`. */
|
||
toolArgMs?: number
|
||
/**
|
||
* Counted tool-call-argument decode blocks — one per tool call whose
|
||
* arguments the stream decoded (the Tool args slice's tally). The counts are
|
||
* ADDITIVE-OPTIONAL like their spans: a row cached before they existed (or a
|
||
* call whose stream framed no blocks) carries none, and the card qualifies
|
||
* only what was actually counted.
|
||
*/
|
||
toolArgBlocks?: number
|
||
/**
|
||
* The decode-throughput seat, paired exactly as the harness's own
|
||
* session-stats fold pairs them: `speedTokens` sums provider-reported
|
||
* output tokens and `speedMs` the first-token → assistant-message
|
||
* windows, over the calls that carried BOTH a first-token stamp and a
|
||
* usage report — a subset of `genMs`, which counts every token-stamped
|
||
* call regardless of usage. `speedMs` KEEPS the first-token anchor (not
|
||
* the marker-aware decode start): it is the harness-parity figure.
|
||
* Additive-optional: cached rows written before
|
||
* the seat existed lack them, and the card falls back to no chip.
|
||
*/
|
||
speedTokens?: number
|
||
speedMs?: number
|
||
/** Completed model calls (assistant messages folded). */
|
||
calls: number
|
||
/** Summed per-call durations of completed tool calls. */
|
||
toolsMs: number
|
||
/** Completed tool calls (call/result pairs folded). */
|
||
toolCalls: number
|
||
/** Per-tool-name tallies behind the timing card's ranking (bounded). */
|
||
tools: Record<string, ToolTimingTotals>
|
||
}
|
||
|
||
/**
|
||
* One billed model's cumulative totals split by pricing period. Providers
|
||
* without period-based pricing book everything under `peak` (the list-price
|
||
* period); DeepSeek splits at fold time — peak windows bill at list price,
|
||
* off-peak (all other hours) at half.
|
||
*/
|
||
export interface CostModelUsage {
|
||
peak?: CostBucketTotals
|
||
off?: CostBucketTotals
|
||
}
|
||
|
||
/**
|
||
* The session-cost estimate's raw material: cumulative provider-reported
|
||
* billed-token totals, keyed by the request envelope's DSH provider id (''
|
||
* when a log carries none) and then by its model id — the exact (provider,
|
||
* model) faces the Client's model-price book resolves (the models.dev
|
||
* registry, client/modelPrices.ts). Running totals per key; absent until a
|
||
* request with a known model reports usage.
|
||
*/
|
||
export interface SessionCostUsage {
|
||
[provider: string]: { [model: string]: CostModelUsage }
|
||
}
|
||
|
||
/** One model-visible message on the surface, with its heuristic token price. */
|
||
export interface SurfaceNode {
|
||
seq: number
|
||
time?: number
|
||
cat: Category
|
||
tokens: number
|
||
/** Image blocks inside this node's message (absent when zero). */
|
||
imgs?: 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
|
||
/**
|
||
* The producer identity the matching inject event names (host pricing.ts
|
||
* `injectionSourceName`: the plugin id, the reconciled instruction files,
|
||
* or the durable kind). Stamped on injection nodes alongside the event, so
|
||
* the browser rows label them the way the events card does; absent when the
|
||
* source carries no readable identity or the node predates the stamp.
|
||
*/
|
||
name?: 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
|
||
/**
|
||
* Skill-machinery tokens of this request (the `skill` composition
|
||
* category — catalog digests, invocation instructions, `skill`-tool
|
||
* loads). Always written by the current fold; absent on rows folded
|
||
* before the category existed (read as 0).
|
||
*/
|
||
skill?: number
|
||
/**
|
||
* Billed cache-read (served) prompt tokens of this request — the
|
||
* hit-rate numerator against `prompt` (input + cacheRead + cacheWrite).
|
||
* Absent on older hosts / usage-less requests; zero is a real value.
|
||
*/
|
||
cacheRead?: number
|
||
output?: number
|
||
/**
|
||
* The request's step ACTIVE milliseconds (`step/start` → `step/end`: the
|
||
* model call plus its tool runs) minus the in-step user waits — approval
|
||
* decisions and `ask_user_question` answer windows — the trend chart's
|
||
* duration overlay. Stamped at `step/end` onto the step's committed record;
|
||
* absent while that step is still open (the live tail) or when the step
|
||
* closed without one (an unpaired/hostile log).
|
||
*/
|
||
activeMs?: 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
|
||
/**
|
||
* Delta-mode signed net change, set by the Client's deltaOf (only present
|
||
* on the delta-transformed records the TrendChart plots). The Host never
|
||
* sets it.
|
||
*/
|
||
net?: number
|
||
}
|
||
|
||
/** A notable context event (compaction, prune, injection, model switch). */
|
||
export interface ContextEventRecord {
|
||
seq: number
|
||
time: number
|
||
kind: 'compaction' | 'prune' | 'inject' | 'model' | 'mode'
|
||
form?: string
|
||
tokens?: number
|
||
count?: number
|
||
sub?: string
|
||
name?: string
|
||
/** One-line producer account (notice-form summary), shown after the name. */
|
||
detail?: 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
|
||
}
|
||
|
||
/**
|
||
* Sentinel `HeaderTool.plugin` value marking a tool whose provider could not
|
||
* be attributed: it was already registered in the harness tool service when
|
||
* this plugin's runtime attribution hook installed (boot-time third-party
|
||
* tools — e.g. local-link plugins that apply before dsh-context). The client
|
||
* renders a localized "unknown plugin" tag with an explanatory tooltip. No
|
||
* real plugin name can collide (it is not a valid package identifier).
|
||
*/
|
||
export const UNKNOWN_TOOL_SOURCE = '<unknown-plugin>'
|
||
|
||
/** One tool of a request-header epoch, with its display price. */
|
||
export interface HeaderTool {
|
||
name: string
|
||
tokens: number
|
||
/**
|
||
* The registering plugin's label, when attribution is known: the host's
|
||
* best-effort attribution (`mcp:<server>` for MCP tools, or the pinned
|
||
* first-party package map), or a `plugin` field carried by the raw header
|
||
* entry — no supported-baseline harness path writes one, but the read
|
||
* stays defensive for foreign/newer producers. `UNKNOWN_TOOL_SOURCE` marks
|
||
* a tool whose provider predates the attribution hook; absent means
|
||
* nothing is known and the browser shows no tag.
|
||
*/
|
||
plugin?: string
|
||
}
|
||
|
||
/**
|
||
* One request-header epoch's METADATA: the epoch boundaries and token prices
|
||
* in force from this event's seq until the next epoch. The epoch CONTENT
|
||
* (full system prompt text, tool descriptions/schemas) is not projected —
|
||
* every session.list row, control baseline, push frame, and projection-cache
|
||
* checkpoint would otherwise carry it per session × epoch. The client
|
||
* fetches one epoch's content on demand (a seq-anchored history read off
|
||
* `seq`) as a {@link HeaderEpochContent}.
|
||
*/
|
||
export interface HeaderRecord {
|
||
seq: number
|
||
time: number
|
||
/** The epoch's estimated system-prompt tokens; absent when it logged no system prompt. */
|
||
systemTokens?: number
|
||
tools: HeaderTool[]
|
||
}
|
||
|
||
/** The `contextHeaders` projection value: the bounded epoch list (newest last). */
|
||
export interface ContextHeaders {
|
||
headers: HeaderRecord[]
|
||
}
|
||
|
||
/**
|
||
* The fetched CONTENT of one request-header epoch — the full system prompt
|
||
* text and per-tool descriptions/schemas, mapped client-side off the raw
|
||
* durable event (see historyPage.ts). Tool identity joins the epoch metadata
|
||
* by `name`; absent description/schema means the raw entry carried none.
|
||
*/
|
||
export interface HeaderEpochContent {
|
||
system?: string
|
||
tools: Array<{
|
||
name: string
|
||
description?: string
|
||
/** The raw JSON schema object the model received (plain JSON). */
|
||
schema?: unknown
|
||
}>
|
||
}
|
||
|
||
/** One currency's DeepSeek open-platform balance figures. */
|
||
export interface PlatformBalanceEntry {
|
||
/** The ISO code the platform reported (`CNY` / `USD`). */
|
||
currency: string
|
||
/** Total available funds: `granted` + `toppedUp`. Derived here rather than read
|
||
* from the platform's own `total_balance`, which rounds independently of its
|
||
* parts and can land a cent away from what the breakdown beside it shows. */
|
||
total: number
|
||
/** The not-expired granted (gift) balance. */
|
||
granted: number
|
||
/** The topped-up balance. */
|
||
toppedUp: number
|
||
}
|
||
|
||
/**
|
||
* The DeepSeek open-platform balance, served by the plugin's
|
||
* `/api/dsh-context/balance` fetch route (host/balance.ts). `null` on the
|
||
* wire — and nothing rendered client-side — whenever the platform is not
|
||
* configured or the read fails: the capsule only ever shows a live figure.
|
||
*/
|
||
export interface PlatformBalance {
|
||
/** Whether the platform reports the balance sufficient for API calls. */
|
||
isAvailable: boolean
|
||
/** One entry per currency the account holds; at least one. */
|
||
balances: PlatformBalanceEntry[]
|
||
}
|