mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Merge remote-tracking branch 'origin/master' into xtr/react-loop-simplification
Resolve translation-pairing hashes after the merge and adapt the math-rendering e2e fixture to the simplified turn/start event shape.
This commit is contained in:
@@ -9,6 +9,8 @@ export interface SessionHistorySnapshot {
|
||||
state: 'cold' | 'loading' | 'ready' | 'error'
|
||||
error: RpcError | null
|
||||
hasMore: boolean
|
||||
/** Absolute sequence of the first loaded raw event, or zero for an empty window. */
|
||||
baseSeq: number
|
||||
inspection: SessionHistoryInspection
|
||||
}
|
||||
|
||||
@@ -17,11 +19,17 @@ export interface SessionHistoryFace
|
||||
extends ObservableSnapshot<SessionHistorySnapshot> {
|
||||
readonly sessionId: SessionId
|
||||
/**
|
||||
* Load the tail and exhaust every available older page.
|
||||
* @param signal - Consumer lifetime; abort is observed between page requests.
|
||||
* @returns When the available ledger is complete or stops advancing.
|
||||
* Load the current tail without reading older pages.
|
||||
* @param signal - Consumer lifetime.
|
||||
* @returns When the tail is ready or loading fails.
|
||||
*/
|
||||
loadAll(signal?: AbortSignal): Promise<void>
|
||||
loadTail(signal?: AbortSignal): Promise<void>
|
||||
/**
|
||||
* Prepend one older page when the current window has a predecessor.
|
||||
* @param signal - Consumer lifetime.
|
||||
* @returns Whether the loaded window advanced.
|
||||
*/
|
||||
loadOlder(signal?: AbortSignal): Promise<boolean>
|
||||
}
|
||||
|
||||
/** Runtime service resolving independent history sources. */
|
||||
|
||||
@@ -49,18 +49,10 @@ function assistantStepKey(turn: number, step: number): string {
|
||||
return `${turn}\u0000${step}`
|
||||
}
|
||||
|
||||
// Trajectory owns surface-window reconstruction so its immutable ledger does
|
||||
// not depend on Chat's live fold adapter or Session's mutable state.
|
||||
/* jscpd:ignore-start */
|
||||
function paddingEvent(seq: number): SessionEvent {
|
||||
return { type: 'noop/padding', seq, time: 0, data: {} } as unknown as SessionEvent
|
||||
}
|
||||
|
||||
function replacementCrossesWindowHead(event: SessionEvent, baseSeq: number): boolean {
|
||||
if (!isSurfaceEvent(event) || event.surfaceOp === 'append') return false
|
||||
return event.surfaceOp.start < baseSeq || event.surfaceOp.end < baseSeq
|
||||
}
|
||||
/* jscpd:ignore-end */
|
||||
|
||||
function contextOriginKind(event: SessionEvent | undefined): ConversationContextOriginKind {
|
||||
if (event?.type !== 'user/message') return 'rewrite'
|
||||
@@ -84,27 +76,63 @@ function isTokenDelta(chunk: SessionEvent<'assistant/chunk'>['data']['chunk']):
|
||||
}
|
||||
}
|
||||
|
||||
function foldContexts(events: readonly SessionEvent[]): readonly FoldedContext[] {
|
||||
function foldContexts(
|
||||
events: readonly SessionEvent[],
|
||||
): readonly FoldedContext[] {
|
||||
const replay: SessionEvent[] = []
|
||||
const originalSeqs: number[] = []
|
||||
const rebasedSeqByOriginal = new Map<number, number>()
|
||||
const surface = new SurfaceManager(replay)
|
||||
const contexts: FoldedContext[] = []
|
||||
let generation = 0
|
||||
let originSeq: number | undefined
|
||||
const originalNodes = () => surface.nodes.map((seq) => {
|
||||
const original = originalSeqs[seq]
|
||||
if (original === undefined) throw new Error(`rebased surface seq ${seq} has no origin`)
|
||||
return original
|
||||
})
|
||||
for (const event of events) {
|
||||
if (isSurfaceEvent(event) && event.surfaceOp !== 'append') {
|
||||
if (!isSurfaceEvent(event)) continue
|
||||
if (event.surfaceOp !== 'append') {
|
||||
contexts.push({
|
||||
generation,
|
||||
nodes: [...surface.nodes],
|
||||
nodes: originalNodes(),
|
||||
...(originSeq === undefined ? {} : { originSeq }),
|
||||
})
|
||||
generation++
|
||||
originSeq = event.seq
|
||||
}
|
||||
replay.push(event)
|
||||
const rebasedSeq = replay.length
|
||||
const {
|
||||
sourceEventSeqs: rawSources,
|
||||
...eventWithoutSources
|
||||
} = event as SessionEvent & { sourceEventSeqs?: readonly number[] }
|
||||
const mappedSourceEventSeqs = rawSources?.flatMap((seq) => {
|
||||
const rebased = rebasedSeqByOriginal.get(seq)
|
||||
return rebased === undefined ? [] : [rebased]
|
||||
})
|
||||
const sourceEventSeqs = mappedSourceEventSeqs?.length === 0
|
||||
? undefined
|
||||
: mappedSourceEventSeqs
|
||||
const surfaceOp = event.surfaceOp === 'append'
|
||||
? event.surfaceOp
|
||||
: {
|
||||
...event.surfaceOp,
|
||||
start: rebasedSeqByOriginal.get(event.surfaceOp.start) ?? event.surfaceOp.start,
|
||||
end: rebasedSeqByOriginal.get(event.surfaceOp.end) ?? event.surfaceOp.end,
|
||||
}
|
||||
originalSeqs.push(event.seq)
|
||||
rebasedSeqByOriginal.set(event.seq, rebasedSeq)
|
||||
replay.push({
|
||||
...eventWithoutSources,
|
||||
seq: rebasedSeq,
|
||||
surfaceOp,
|
||||
...(sourceEventSeqs === undefined ? {} : { sourceEventSeqs }),
|
||||
} as SessionEvent)
|
||||
}
|
||||
contexts.push({
|
||||
generation,
|
||||
nodes: [...surface.nodes],
|
||||
nodes: originalNodes(),
|
||||
...(originSeq === undefined ? {} : { originSeq }),
|
||||
})
|
||||
return contexts
|
||||
@@ -326,10 +354,7 @@ export function projectConversationHistory(
|
||||
): ConversationHistoryProjection {
|
||||
const events = entries.map(entry => entry.event)
|
||||
const baseSeq = events[0]?.seq ?? 0
|
||||
const padded = [
|
||||
...Array.from({ length: baseSeq }, (_, seq) => paddingEvent(seq)),
|
||||
...events,
|
||||
]
|
||||
const eventsBySeq = new Map(events.map(event => [event.seq, event]))
|
||||
const callIndex = new Map<string, CallIndexEntry>()
|
||||
const resultViews = new Map<number, ToolResultView>()
|
||||
const assistantSteps = new Map<string, AssistantStepMetadata>()
|
||||
@@ -399,7 +424,7 @@ export function projectConversationHistory(
|
||||
const materialize = (seq: number): ConversationNode | undefined => {
|
||||
const cached = nodeCache.get(seq)
|
||||
if (cached !== undefined) return cached
|
||||
const event = padded[seq]
|
||||
const event = eventsBySeq.get(seq)
|
||||
if (event === undefined || !isSurfaceEligibleType(event.type)) return
|
||||
const node = materializeNode(
|
||||
event,
|
||||
@@ -425,7 +450,7 @@ export function projectConversationHistory(
|
||||
}]
|
||||
} else {
|
||||
try {
|
||||
contexts = foldContexts(padded).map((context): ConversationContext => {
|
||||
contexts = foldContexts(events).map((context): ConversationContext => {
|
||||
const nodes = context.nodes.flatMap((seq) => {
|
||||
const node = materialize(seq)
|
||||
return node === undefined ? [] : [node]
|
||||
@@ -438,7 +463,7 @@ export function projectConversationHistory(
|
||||
nodes,
|
||||
}
|
||||
}
|
||||
const originEvent = padded[context.originSeq]
|
||||
const originEvent = eventsBySeq.get(context.originSeq)
|
||||
return {
|
||||
id: context.generation,
|
||||
parentId: context.generation - 1,
|
||||
|
||||
@@ -6,7 +6,9 @@ import { transportError } from '@deepseek-ai/dsh-host-apiproxy/api'
|
||||
import type {
|
||||
SessionHistoryFace, SessionHistorySnapshot,
|
||||
} from '../contract/session-history.ts'
|
||||
import { createHistoryInspection } from '../sessions/history.ts'
|
||||
import {
|
||||
compactHistoryInspectionEntries, createHistoryInspection,
|
||||
} from '../sessions/history.ts'
|
||||
import { Notifier } from '../sessions/notifier.ts'
|
||||
import { isVisibleAssistantChunk, PartialAccumulator } from '../sessions/partial.ts'
|
||||
|
||||
@@ -18,7 +20,8 @@ function isAborted(signal: AbortSignal | undefined): boolean {
|
||||
|
||||
/** Independent raw-history owner used only by inspection consumers. */
|
||||
export class SessionHistorySource implements SessionHistoryFace {
|
||||
private entries: readonly HistoryEntry[] = []
|
||||
private entries: HistoryEntry[] = []
|
||||
private inspectionEntries: readonly HistoryEntry[] = []
|
||||
private baseSeq = 0
|
||||
private hasMore = false
|
||||
private state: SessionHistorySnapshot['state'] = 'cold'
|
||||
@@ -36,7 +39,6 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
value: SessionHistorySnapshot['inspection']
|
||||
} | null = null
|
||||
private streamPublishToken: object | null = null
|
||||
private streamBaseInspection: SessionHistorySnapshot['inspection'] | null = null
|
||||
private streamPartial: PartialAccumulator | null = null
|
||||
private snapshotCache: SessionHistorySnapshot
|
||||
private readonly notifier = new Notifier(() => {
|
||||
@@ -73,37 +75,29 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the tail and exhaust all available older pages.
|
||||
* Load the current tail without reading older pages.
|
||||
* @param signal - Consumer lifetime.
|
||||
* @returns When paging completes, fails to advance, or is aborted.
|
||||
* @returns When the tail is ready or loading fails.
|
||||
*/
|
||||
async loadAll(signal?: AbortSignal): Promise<void> {
|
||||
if (signal?.aborted === true) return
|
||||
async loadTail(signal?: AbortSignal): Promise<void> {
|
||||
if (isAborted(signal)) return
|
||||
this.trackConsumer(signal)
|
||||
await this.open()
|
||||
while (
|
||||
!isAborted(signal)
|
||||
&& this.state === 'ready'
|
||||
&& this.hasMore
|
||||
) {
|
||||
const previousBaseSeq = this.baseSeq
|
||||
await this.loadOlder()
|
||||
if (isAborted(signal) || this.baseSeq === previousBaseSeq) return
|
||||
}
|
||||
}
|
||||
|
||||
/** Rebuild and page for whichever mounted consumers survive a reconnect. */
|
||||
private async loadForConsumers(): Promise<void> {
|
||||
/**
|
||||
* Prepend one older page when the current window has a predecessor.
|
||||
* @param signal - Consumer lifetime.
|
||||
* @returns Whether the loaded window advanced.
|
||||
*/
|
||||
async loadOlder(signal?: AbortSignal): Promise<boolean> {
|
||||
if (isAborted(signal)) return false
|
||||
this.trackConsumer(signal)
|
||||
await this.open()
|
||||
while (
|
||||
this.hasConsumer()
|
||||
&& this.state === 'ready'
|
||||
&& this.hasMore
|
||||
) {
|
||||
const previousBaseSeq = this.baseSeq
|
||||
await this.loadOlder()
|
||||
if (!this.hasConsumer() || this.baseSeq === previousBaseSeq) return
|
||||
}
|
||||
if (isAborted(signal)) return false
|
||||
const previousBaseSeq = this.baseSeq
|
||||
await this.loadOlderPage()
|
||||
return this.baseSeq !== previousBaseSeq
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,12 +138,13 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
this.liveBuffer = []
|
||||
this.subscribedLastSeq = null
|
||||
this.entries = []
|
||||
this.inspectionEntries = []
|
||||
this.baseSeq = 0
|
||||
this.hasMore = false
|
||||
this.state = 'cold'
|
||||
this.error = null
|
||||
this.publishDirtyNow()
|
||||
void this.loadForConsumers()
|
||||
void this.open()
|
||||
}
|
||||
|
||||
/** Stop future refresh work after the host removes the session. */
|
||||
@@ -161,7 +156,6 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
this.olderPromise = null
|
||||
this.liveBuffer = []
|
||||
this.streamPublishToken = null
|
||||
this.streamBaseInspection = null
|
||||
this.streamPartial = null
|
||||
}
|
||||
|
||||
@@ -234,7 +228,7 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
}
|
||||
}
|
||||
|
||||
private loadOlder(): Promise<void> {
|
||||
private loadOlderPage(): Promise<void> {
|
||||
if (this.olderPromise !== null) return this.olderPromise
|
||||
if (this.state !== 'ready' || !this.hasMore) return Promise.resolve()
|
||||
const generation = this.generation
|
||||
@@ -260,6 +254,7 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
return
|
||||
}
|
||||
this.entries = [...older, ...this.entries]
|
||||
this.inspectionEntries = compactHistoryInspectionEntries([...this.entries])
|
||||
this.baseSeq = older[0]?.event.seq ?? this.baseSeq
|
||||
this.hasMore = result.value.hasMore
|
||||
} catch (error) {
|
||||
@@ -291,6 +286,7 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
this.entries = [...prefix, ...tail]
|
||||
}
|
||||
this.baseSeq = this.entries[0]?.event.seq ?? 0
|
||||
this.inspectionEntries = compactHistoryInspectionEntries([...this.entries])
|
||||
const buffered = this.liveBuffer
|
||||
this.liveBuffer = []
|
||||
for (const entry of buffered) this.appendLive(entry)
|
||||
@@ -324,7 +320,11 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
private appendLive(entry: HistoryEntry): void {
|
||||
const tailSeq = this.tailSeq()
|
||||
if (tailSeq !== null && entry.event.seq <= tailSeq) return
|
||||
this.entries = [...this.entries, entry]
|
||||
this.entries.push(entry)
|
||||
this.inspectionEntries = [...this.inspectionEntries, entry]
|
||||
if (entry.event.type === 'assistant/message') {
|
||||
this.inspectionEntries = compactHistoryInspectionEntries(this.inspectionEntries)
|
||||
}
|
||||
}
|
||||
|
||||
/** Append a chunk against the cached finalized projection; false means no visible publish. */
|
||||
@@ -336,11 +336,10 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
if (!isVisibleAssistantChunk(chunk.type)) {
|
||||
const inspection = this.currentInspection()
|
||||
this.appendLive(entry)
|
||||
this.inspectionCache = { entries: this.entries, value: inspection }
|
||||
this.inspectionCache = { entries: this.inspectionEntries, value: inspection }
|
||||
return false
|
||||
}
|
||||
const base = this.streamBaseInspection ?? this.currentInspection()
|
||||
this.streamBaseInspection = base
|
||||
const base = this.currentInspection()
|
||||
if (
|
||||
this.streamPartial === null
|
||||
|| this.streamPartial.turn !== turn
|
||||
@@ -356,7 +355,7 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
this.streamPartial.push(chunk)
|
||||
this.appendLive(entry)
|
||||
this.inspectionCache = {
|
||||
entries: this.entries,
|
||||
entries: this.inspectionEntries,
|
||||
value: { ...base, partial: this.streamPartial.toPartial() },
|
||||
}
|
||||
return true
|
||||
@@ -382,7 +381,6 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
/** Publish structural changes immediately and invalidate an older scheduled stream publish. */
|
||||
private publishDirtyNow(): void {
|
||||
this.streamPublishToken = null
|
||||
this.streamBaseInspection = null
|
||||
this.streamPartial = null
|
||||
this.notifier.markDirty()
|
||||
}
|
||||
@@ -415,14 +413,15 @@ export class SessionHistorySource implements SessionHistoryFace {
|
||||
state: this.state,
|
||||
error: this.error,
|
||||
hasMore: this.hasMore,
|
||||
baseSeq: this.baseSeq,
|
||||
inspection: this.currentInspection(),
|
||||
}
|
||||
}
|
||||
|
||||
/** Inspection pinned to the source's current immutable entry array. */
|
||||
private currentInspection(): SessionHistorySnapshot['inspection'] {
|
||||
if (this.inspectionCache?.entries !== this.entries) {
|
||||
const entries = this.entries
|
||||
if (this.inspectionCache?.entries !== this.inspectionEntries) {
|
||||
const entries = this.inspectionEntries
|
||||
this.inspectionCache = {
|
||||
entries,
|
||||
value: createHistoryInspection(() => entries),
|
||||
|
||||
@@ -7,6 +7,24 @@ import type { ConversationContext } from './conversation-context.ts'
|
||||
import { projectConversationHistory } from '../session-history/history-fold.ts'
|
||||
import { inspectRequests, type RequestView } from './request-inspection.ts'
|
||||
|
||||
function assistantStepKey(turn: number, step: number): string {
|
||||
return `${turn}\u0000${step}`
|
||||
}
|
||||
|
||||
function isFirstTokenCandidate(entry: HistoryEntry): boolean {
|
||||
const event = entry.event
|
||||
if (event.type !== 'assistant/chunk') return false
|
||||
switch (event.data.chunk.type) {
|
||||
case 'text-delta':
|
||||
case 'reasoning-delta':
|
||||
return event.data.chunk.text !== ''
|
||||
case 'tool-call-delta':
|
||||
return event.data.chunk.argumentsDelta !== '' || event.data.chunk.name !== undefined
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Lazily derived inspection data for one immutable session-history window. */
|
||||
export interface SessionHistoryInspection {
|
||||
eventNodes: readonly ConversationNode[]
|
||||
@@ -19,6 +37,47 @@ export interface SessionHistoryInspection {
|
||||
codeDispatches: ReadonlyMap<string, readonly CodeSubCall[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove completed-step token payloads that no inspection projection reads.
|
||||
* The first visible token preserves timing, usage chunks preserve accounting,
|
||||
* and unfinished steps retain every chunk for live or interrupted content.
|
||||
* @param entries - Contiguous raw history entries in sequence order.
|
||||
* @returns A projection-equivalent, usually much smaller entry ledger.
|
||||
*/
|
||||
export function compactHistoryInspectionEntries(
|
||||
entries: readonly HistoryEntry[],
|
||||
): readonly HistoryEntry[] {
|
||||
const completedSteps = new Set<string>()
|
||||
for (const { event } of entries) {
|
||||
if (event.type === 'assistant/message') {
|
||||
completedSteps.add(assistantStepKey(event.data.turn, event.data.step))
|
||||
}
|
||||
}
|
||||
|
||||
const firstTokenSteps = new Set<string>()
|
||||
const compacted: HistoryEntry[] = []
|
||||
let changed = false
|
||||
for (const entry of entries) {
|
||||
const event = entry.event
|
||||
if (event.type !== 'assistant/chunk') {
|
||||
compacted.push(entry)
|
||||
continue
|
||||
}
|
||||
const key = assistantStepKey(event.data.turn, event.data.step)
|
||||
if (!completedSteps.has(key) || event.data.chunk.type === 'usage') {
|
||||
compacted.push(entry)
|
||||
continue
|
||||
}
|
||||
if (isFirstTokenCandidate(entry) && !firstTokenSteps.has(key)) {
|
||||
firstTokenSteps.add(key)
|
||||
compacted.push(entry)
|
||||
} else {
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
return changed ? compacted : entries
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a lazy inspection projection over an immutable history window.
|
||||
* Conversation consumers retain the cheap wrapper; only Trajectory snapshots
|
||||
|
||||
@@ -94,7 +94,8 @@ export interface RequestInspectionSnapshot {
|
||||
/**
|
||||
* Derive the request-centric read model from one immutable history window.
|
||||
* Compaction participates as a request purpose rather than a parallel
|
||||
* top-level collection.
|
||||
* top-level collection. A leading resume/change header exposes its prompt but
|
||||
* cannot project a change until the preceding header enters the window.
|
||||
* @param entries - Contiguous raw session history.
|
||||
* @returns Requests and call-time schemas derived from that history.
|
||||
*/
|
||||
@@ -218,6 +219,7 @@ function promptChange(
|
||||
prompt: ConversationPromptSnapshot,
|
||||
event: SessionEvent<'request/header'>,
|
||||
): RequestPromptChange | undefined {
|
||||
if (previous === undefined && event.data.reason !== 'initial') return
|
||||
const systemChanged = previous !== undefined && previous.system !== prompt.system
|
||||
const toolsChanged = previous !== undefined
|
||||
&& JSON.stringify(previous.tools) !== JSON.stringify(prompt.tools)
|
||||
|
||||
Reference in New Issue
Block a user