refactor(web): project usage and snapshot request context
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user