fix(session): bound trajectory history projections
This commit is contained in:
@@ -114,6 +114,16 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
private callSchemas = new Map<string, ToolSchema>()
|
||||
private callSchemasRev = 0
|
||||
private callSchemasCache: { rev: number; value: ReadonlyMap<string, ToolSchema> } | null = null
|
||||
private modelRequestsRev = 0
|
||||
private modelRequestsCache: {
|
||||
rev: number
|
||||
value: readonly ModelRequestView[]
|
||||
} | null = null
|
||||
private compactionRequestsRev = 0
|
||||
private compactionRequestsCache: {
|
||||
rev: number
|
||||
value: readonly CompactionRequestView[]
|
||||
} | null = null
|
||||
private promptChangesRev = 0
|
||||
private promptChangesCache: {
|
||||
rev: number
|
||||
@@ -614,6 +624,8 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
/** Per-event side effects (right column of the §A.9 dispatch table):
|
||||
* chunk accumulation / partial clear on finalize / openCalls add-remove. */
|
||||
private applyEventSideEffects(event: SessionEvent, view?: ToolEventView): void {
|
||||
if (affectsModelRequests(event)) this.modelRequestsRev++
|
||||
if (affectsCompactionRequests(event)) this.compactionRequestsRev++
|
||||
// The `tool/code-dispatch-start`/`tool/code-dispatch` pair is declared by
|
||||
// the host-side dsh-tools plugin whose types cannot enter the client
|
||||
// program (its host Context merges collide with the client's), so this
|
||||
@@ -783,6 +795,8 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
this.activeToolSchemas = new Map()
|
||||
this.callSchemas = new Map()
|
||||
this.callSchemasRev++
|
||||
this.modelRequestsRev++
|
||||
this.compactionRequestsRev++
|
||||
this.promptChangesRev++
|
||||
for (let i = 0; i < this.events.length; i++) {
|
||||
const event = this.events[i]
|
||||
@@ -823,6 +837,24 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
if (this.callSchemasCache === null || this.callSchemasCache.rev !== this.callSchemasRev) {
|
||||
this.callSchemasCache = { rev: this.callSchemasRev, value: new Map(this.callSchemas) }
|
||||
}
|
||||
if (
|
||||
this.modelRequestsCache === null
|
||||
|| this.modelRequestsCache.rev !== this.modelRequestsRev
|
||||
) {
|
||||
this.modelRequestsCache = {
|
||||
rev: this.modelRequestsRev,
|
||||
value: deriveModelRequests(this.events),
|
||||
}
|
||||
}
|
||||
if (
|
||||
this.compactionRequestsCache === null
|
||||
|| this.compactionRequestsCache.rev !== this.compactionRequestsRev
|
||||
) {
|
||||
this.compactionRequestsCache = {
|
||||
rev: this.compactionRequestsRev,
|
||||
value: deriveCompactionRequests(this.events),
|
||||
}
|
||||
}
|
||||
if (this.queueCache === null || this.queueCache.rev !== this.queueRev) {
|
||||
this.queueCache = { rev: this.queueRev, value: this.queued.map(entry => entry.row) }
|
||||
}
|
||||
@@ -840,8 +872,8 @@ export class Session implements ObservableSnapshot<ConversationSnapshot> {
|
||||
sessionId: this.sessionId,
|
||||
nodes,
|
||||
contexts,
|
||||
compactionRequests: deriveCompactionRequests(this.events),
|
||||
requestAttempts: deriveModelRequests(this.events),
|
||||
compactionRequests: this.compactionRequestsCache.value,
|
||||
requestAttempts: this.modelRequestsCache.value,
|
||||
promptChanges: this.promptChangesCache.value,
|
||||
foldDegraded: degraded,
|
||||
partial,
|
||||
@@ -901,6 +933,28 @@ function modelRequestKey(turn: number, step: number): string {
|
||||
return `${turn}\u0000${step}`
|
||||
}
|
||||
|
||||
function affectsModelRequests(event: SessionEvent): boolean {
|
||||
switch (event.type) {
|
||||
case 'request/header':
|
||||
case 'step/start':
|
||||
case 'assistant/message':
|
||||
case 'step/end':
|
||||
return true
|
||||
case 'turn/end':
|
||||
return event.data.reason.kind === 'error'
|
||||
default:
|
||||
return (event.type as string) === 'llm/retry'
|
||||
}
|
||||
}
|
||||
|
||||
function affectsCompactionRequests(event: SessionEvent): boolean {
|
||||
const type = event.type as string
|
||||
return type === 'compact/start'
|
||||
|| type === 'compact/summary'
|
||||
|| type === 'compact/end'
|
||||
|| (event.type === 'user/message' && isCompactionSource(event.data.source))
|
||||
}
|
||||
|
||||
/** Project every durable step into one provider request, retaining failed retry attempts. */
|
||||
function deriveModelRequests(events: readonly SessionEvent[]): readonly ModelRequestView[] {
|
||||
const requests: ModelRequestView[] = []
|
||||
|
||||
@@ -844,20 +844,27 @@ describe('reference stability (the memo contract)', () => {
|
||||
await session.open()
|
||||
const feed = (event: SessionEvent) => { session.handleMuxEnvelope('r' as never, { type: 'session/event', sessionId: SID, event }) }
|
||||
feed(ev.turnStart(6, 1))
|
||||
feed(ev.toolCall(7, 1, 'c1', 'echo', '{}'))
|
||||
feed(ev.stepStart(7, 1))
|
||||
feed(ev.toolCall(8, 1, 'c1', 'echo', '{}'))
|
||||
session.handleMuxEnvelope('ra' as never, { type: 'approval/requested', sessionId: SID, approvalId: 'ap1' as never, toolName: 'rm' })
|
||||
const before = session.getSnapshot()
|
||||
// A chunk storm touches partial/nodes only: runningCalls and pending must keep identity.
|
||||
feed(ev.chunkStart(8, 1))
|
||||
feed(ev.chunkText(9, 1, '与工具无关的流式'))
|
||||
// A chunk storm touches partial/nodes only: unrelated projections keep identity.
|
||||
feed(ev.chunkStart(9, 1))
|
||||
feed(ev.chunkText(10, 1, '与工具无关的流式'))
|
||||
const after = session.getSnapshot()
|
||||
expect(after).not.toBe(before)
|
||||
expect(after.runningCalls).toBe(before.runningCalls)
|
||||
expect(after.pending).toBe(before.pending)
|
||||
expect(after.requestAttempts).toBe(before.requestAttempts)
|
||||
expect(after.compactionRequests).toBe(before.compactionRequests)
|
||||
// And a mutation on the tracked domain swaps that array.
|
||||
feed(ev.toolResult(10, 1, 'c1', 'ECHO'))
|
||||
feed(ev.toolResult(11, 1, 'c1', 'ECHO'))
|
||||
const resolved = session.getSnapshot()
|
||||
expect(resolved.runningCalls).not.toBe(after.runningCalls)
|
||||
expect(resolved.pending).toBe(after.pending)
|
||||
feed(ev.assistant(12, 1, '完成'))
|
||||
const completed = session.getSnapshot()
|
||||
expect(completed.requestAttempts).not.toBe(resolved.requestAttempts)
|
||||
expect(completed.compactionRequests).toBe(resolved.compactionRequests)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user