refactor(web): project usage and snapshot request context

This commit is contained in:
Hypatia May
2026-07-29 15:27:59 +08:00
parent e37cb23336
commit bf618dabf9
78 changed files with 748 additions and 934 deletions
@@ -6,7 +6,7 @@
import type { CommandId } from '@deepseek-ai/dsh-commands/brand'
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type {
RpcError, SessionId, SessionMetrics, ToolCallView, ToolResultView,
ModelRequestTelemetry, RpcError, SessionId, ToolCallView, ToolResultView,
} from '@deepseek-ai/dsh-client-connection/client'
import type { PendingInteraction } from './pending.ts'
@@ -267,16 +267,6 @@ export interface ConversationSnapshot {
*/
blank: boolean
lastAgentError: string | null
/**
* Host-owned cumulative usage/current pressure. Independent of `nodes`
* pagination; null until a tail response or live metrics frame supplies a
* current durable value.
*/
metrics: SessionMetrics | null
/**
* Capacity from the latest model-request attempt observed on this mux
* generation. Absent before the first such request, after a request whose
* registration exposes no capacity, and after `session/subscribed`.
*/
modelRequestContextWindow?: number
/** Latest atomic model-request snapshot on this mux generation. */
modelRequest: ModelRequestTelemetry | null
}
@@ -2,7 +2,10 @@
// dispatch entry + list state, constructed and held by SessionsService (one per client runtime).
// List data never enters zustand; React connects via subscribe/getListSnapshot.
import type { IApiClient, HostFrame, MuxFrame, RpcError, RpcRequest, RpcResult, SessionId, SessionSummary, WorkspaceId } from '@deepseek-ai/dsh-client-connection/client'
import type {
HostFrame, IApiClient, ModelRequestTelemetry, MuxFrame, RpcError, RpcRequest,
RpcResult, SessionId, SessionSummary, WorkspaceId,
} from '@deepseek-ai/dsh-client-connection/client'
// Value import from the inline-safe wire layer (not the connection plugin):
// plugin-to-plugin value imports are a bundle purity error.
import { transportError } from '@deepseek-ai/dsh-host-apiproxy/api'
@@ -58,11 +61,11 @@ export class SessionManager {
* frames are low-frequency; overflow drops oldest) and dropped on session-removed (audit S7). */
private readonly pendingBuffers = new Map<SessionId, RpcRequest<MuxFrame>[]>()
/**
* Latest model capacity observed for an uninstantiated session on the
* Latest request telemetry observed for an uninstantiated session on the
* current mux generation. Unlike durable history, this transient frame
* cannot be backfilled when get() lazily creates the Session.
*/
private readonly modelRequestContextWindows = new Map<SessionId, number>()
private readonly modelRequests = new Map<SessionId, ModelRequestTelemetry>()
/** Outstanding approval questions per session, keyed by approvalId (idempotent under mux-open
* replays of the same requested frame). Manager-owned rather than read off Session instances
* because the sidebar must light up for sessions never instantiated. Cleared per connection
@@ -171,7 +174,7 @@ export class SessionManager {
}
private createSession(sessionId: SessionId): Session {
const modelRequestContextWindow = this.modelRequestContextWindows.get(sessionId)
const modelRequest = this.modelRequests.get(sessionId)
return new Session(sessionId, this.api, {
// The sender's local first-send flip mirrors into the list row so the
// session surfaces (lists filter on blank) before any host frame lands.
@@ -179,7 +182,7 @@ export class SessionManager {
this.recordMutation({ kind: 'engaged', sessionId: engaged.sessionId })
},
projections: this.projectionStore(sessionId),
...(modelRequestContextWindow === undefined ? {} : { modelRequestContextWindow }),
...(modelRequest === undefined ? {} : { modelRequest }),
})
}
@@ -355,13 +358,13 @@ export class SessionManager {
return
}
if (frame.type === 'session/model-request') {
// Transient and non-replayable: retain the latest capacity until lazy
// instantiation. An absent value explicitly clears an earlier one.
if (frame.contextWindow === undefined) this.modelRequestContextWindows.delete(frame.sessionId)
else this.modelRequestContextWindows.set(frame.sessionId, frame.contextWindow)
// Transient and non-replayable: retain the whole latest request until
// lazy instantiation. Missing fields replace rather than inherit.
const { type: _type, sessionId, ...modelRequest } = frame
this.modelRequests.set(sessionId, modelRequest)
}
if (frame.type === 'session/subscribed') {
this.modelRequestContextWindows.delete(frame.sessionId)
this.modelRequests.delete(frame.sessionId)
// Rows past the host's durable baseline rode state a restart lost; drop
// them so last-wins cannot pin a phantom value over recomputed truth.
this.projectionStores.get(frame.sessionId)?.truncate(frame.lastSeq)
@@ -440,7 +443,7 @@ export class SessionManager {
this.recordMutation({ kind: 'remove', sessionId: frame.sessionId })
this.sessions.get(frame.sessionId)?.handleRemoved() // instance survives (resident-instance rule), only flagged in the snapshot
this.pendingBuffers.delete(frame.sessionId) // a removed session's buffered frames must not replay on a future instantiation
this.modelRequestContextWindows.delete(frame.sessionId) // connection-local request capacity dies with the Host session
this.modelRequests.delete(frame.sessionId) // connection-local request telemetry dies with the Host session
this.waitingApprovals.delete(frame.sessionId) // a removed session cannot wait on anyone
this.projectionStores.delete(frame.sessionId) // removed sessions drop their projection rows with the instance
return
@@ -481,7 +484,7 @@ export class SessionManager {
if (kept.length === 0) this.pendingBuffers.delete(sessionId)
else this.pendingBuffers.set(sessionId, kept)
}
this.modelRequestContextWindows.clear()
this.modelRequests.clear()
for (const session of this.sessions.values()) session.handleReconnecting()
}
@@ -5,7 +5,7 @@ import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { SessionEvent } from '@deepseek-ai/dsh-session/types'
import type {
HistoryEntry, IApiClient, MuxFrame, RpcError, RpcId, RpcResult,
SessionId, SessionMetrics, ToolEventView,
ModelRequestTelemetry, SessionId, ToolEventView,
} from '@deepseek-ai/dsh-client-connection/client'
// Value import from the inline-safe wire layer (not the connection plugin):
// plugin-to-plugin value imports are a bundle purity error.
@@ -43,8 +43,8 @@ export interface SessionOptions {
* private store (bare object-layer construction).
*/
projections?: ProjectionValueStore
/** Model capacity already observed on this mux generation before lazy construction. */
modelRequestContextWindow?: number
/** Request telemetry already observed on this mux generation before lazy construction. */
modelRequest?: ModelRequestTelemetry
}
/** Queue-row preview cap: the dock renders one line, the full content never leaves the host mirror. */
@@ -110,10 +110,8 @@ export class Session implements SessionFace {
private queueCache: { rev: number; value: QueuedMessage[] } | null = null
private frozenRev = 0
private nodesCache: { folded: readonly ConversationNode[]; frozenRev: number; value: readonly ConversationNode[] } | null = null
/** Host-owned durable usage/current-pressure projection. */
private metrics: SessionMetrics | null = null
/** Latest capacity observed on this mux connection, independent of durable metrics arrival. */
private contextWindow: number | undefined
/** Latest atomic request snapshot observed on this mux connection. */
private modelRequest: ModelRequestTelemetry | null
/** `run_code` sub-dispatches by parent callId (window-derived, like openCalls). Appends
* copy-on-write the per-parent array so published snapshot references never mutate. */
private codeDispatches = new Map<string, readonly CodeSubCall[]>()
@@ -175,7 +173,7 @@ export class Session implements SessionFace {
private readonly options: SessionOptions = {},
) {
this.projections = options.projections ?? new ProjectionValueStore()
this.contextWindow = options.modelRequestContextWindow
this.modelRequest = options.modelRequest ?? null
this.snapshotCache = this.buildSnapshot()
}
@@ -331,10 +329,10 @@ export class Session implements SessionFace {
* in-flight open first — its history request rode the dead connection and must not settle
* the fresh generation into 'error' (audit S4). */
async resync(): Promise<void> {
// Queue, metrics, and request capacity are NOT cleared here: onConnected
// Queue and request telemetry are NOT cleared here: onConnected
// (which drives resync) races the mux frames — fresh-generation state may
// have landed already, and the host never resends it. session/subscribed
// owns the generation reset before the queue snapshot and metrics frames.
// have landed already, and the host never resends request telemetry.
// session/subscribed owns the reset before the queue snapshot.
if (this.openState === 'cold') return // never opened: no window to rebuild (doOpen flips to 'loading' synchronously, so cold implies no in-flight open)
this.openGeneration++
this.openPromise = null
@@ -415,24 +413,22 @@ export class Session implements SessionFace {
this.queueRev++
changed = true
}
if (this.contextWindow !== undefined) {
this.contextWindow = undefined
changed = true
}
if (this.metrics !== null) {
this.metrics = null
if (this.modelRequest !== null) {
this.modelRequest = null
changed = true
}
if (changed) this.notifier.markDirty()
return
}
case 'session/metrics': {
this.installMetrics(frame.metrics)
return
}
case 'session/model-request': {
if (this.contextWindow === frame.contextWindow) return
this.contextWindow = frame.contextWindow
const {
type: _type,
sessionId: _sessionId,
...modelRequest
} = frame
// Whole-frame replacement is load-bearing: an omitted numerator or
// capacity clears that field from the preceding request.
this.modelRequest = modelRequest
this.notifier.markDirty()
return
}
@@ -508,17 +504,16 @@ export class Session implements SessionFace {
/** Connection-loss boundary: clear values that are not replayed before the next stream starts. */
handleReconnecting(): void {
this.openGeneration++
if (this.metrics === null && this.contextWindow === undefined) return
this.metrics = null
this.contextWindow = undefined
if (this.modelRequest === null) return
this.modelRequest = null
this.notifier.markDirty()
}
/** host/session-removed relay: flag the resident snapshot and clear connection-local capacity. */
/** host/session-removed relay: flag the resident snapshot and clear request telemetry. */
handleRemoved(): void {
const changed = !this.removed || this.contextWindow !== undefined
const changed = !this.removed || this.modelRequest !== null
this.removed = true
this.contextWindow = undefined
this.modelRequest = null
if (changed) this.notifier.markDirty()
}
@@ -567,7 +562,6 @@ export class Session implements SessionFace {
result.value.events,
result.value.hasMore,
result.value.projections,
result.value.metrics,
)
// Gap detection (§D.3-4): baseline past the window tail and liveBuffer did not cover it -> pull the tail page once more.
const tailSeq = this.windowTailSeq()
@@ -579,7 +573,6 @@ export class Session implements SessionFace {
result.value.events,
result.value.hasMore,
result.value.projections,
result.value.metrics,
)
}
}
@@ -606,7 +599,6 @@ export class Session implements SessionFace {
entries: HistoryEntry[],
hasMore: boolean,
projections: ProjectionsBaseline | undefined,
metrics: SessionMetrics | undefined,
): void {
this.events = entries.map(e => e.event)
this.views = entries.map(e => e.view)
@@ -615,7 +607,6 @@ export class Session implements SessionFace {
this.foldAdapter.reset(this.events, this.baseSeq, this.views)
this.rebuildDerivedFromWindow()
if (projections !== undefined) this.projections.seed(projections)
if (metrics !== undefined) this.installMetrics(metrics)
const buffered = this.liveBuffer
this.liveBuffer = []
for (const item of buffered) this.appendLive(item.event, item.view)
@@ -668,7 +659,6 @@ export class Session implements SessionFace {
result.value.events,
result.value.hasMore,
result.value.projections,
result.value.metrics,
)
}
} catch (error) {
@@ -854,20 +844,6 @@ export class Session implements SessionFace {
return tail === undefined ? null : tail.seq
}
/** Install a metrics snapshot unless a newer durable or publication revision already landed. */
private installMetrics(metrics: SessionMetrics): void {
const current = this.metrics
if (
current !== null
&& (
metrics.logRevision < current.logRevision
|| metrics.projectionRevision < current.projectionRevision
)
) return
this.metrics = metrics
this.notifier.markDirty()
}
private buildSnapshot(): ConversationSnapshot {
const { nodes: folded, degraded } = this.foldAdapter.nodes()
// Frozen interrupted nodes ride fractional seqs: a stable merge keeps them in flow order.
@@ -920,10 +896,7 @@ export class Session implements SessionFace {
promptError: this.promptError,
blank: this.blankBit,
lastAgentError: this.lastAgentError,
metrics: this.metrics,
...(this.contextWindow === undefined
? {}
: { modelRequestContextWindow: this.contextWindow }),
modelRequest: this.modelRequest,
}
}
}