fix(apiproxy): harden cold session metadata probing

This commit is contained in:
_Kerman
2026-08-13 15:09:21 +08:00
parent c4226840f4
commit 2be3e12965
18 changed files with 138 additions and 131 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ export type { SessionPreparationOptions } from './preparation.ts'
export type { AssistantMessage, ToolResultMessage, UserMessage } from '@deepseek-ai/dsh-llm'
export { isJsonValue, snapshotJsonValue } from './json.ts'
export type { JsonValue } from './json.ts'
export { interruptedTurnClosers, lastActivityTime, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts'
export { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts'
export { decodeStorageRecord, packChunkRuns } from './chunk-rows.ts'
export type { ChunkRow, StorageRecord } from './chunk-rows.ts'
export type { SessionSurface, SurfaceFoldReplacement, SurfaceFoldResult } from './surface.ts'
+1 -20
View File
@@ -1,10 +1,7 @@
/**
* Crash-recovery repair for an interrupted session log. It preserves a fully
* written final turn and supplies the missing tool, step, and turn boundaries
* needed to resume with a provider-valid transcript, plus the activity-time
* read that must skip the end-seed boundary — which this module does
* not write (`Session`'s constructor does) but whose synthetic closers can
* inherit that boundary's timestamp, the one real coupling between the two.
* needed to resume with a provider-valid transcript.
* @module @deepseek-ai/dsh-session/repair
*/
@@ -12,22 +9,6 @@ import { MessageId, freezeMessage, type CallId } from '@deepseek-ai/dsh-llm'
import type { ToolResultMessage } from '@deepseek-ai/dsh-llm'
import type { SessionEvent } from './types.ts'
/**
* The `time` of the log's last event representing actual work, skipping the
* `session/end-seed` boundary — picking a session up is not activity, so
* activity ordering must exclude it.
*
* Excluded by type, so a pickup time still leaks when a boundary is the last
* event of an open turn: {@link interruptedTurnClosers} copies it onto the
* synthetic `turn/end`, which this counts as work. Reachable only by seeding an
* unbalanced log directly — `load()` balances first.
* @param events - the log to scan, in seq order.
* @returns the latest non-boundary event's `time`, or undefined when there is none.
*/
export function lastActivityTime(events: readonly SessionEvent[]): number | undefined {
return events.findLast(event => event.type !== 'session/end-seed')?.time
}
/** Recovery code for an assistant tool request that never reached a recorded call start. */
export const TOOL_NOT_STARTED = 'TOOL_NOT_STARTED'
+1 -42
View File
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
import { interruptedTurnClosers, lastActivityTime, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '../src/index.ts'
import { interruptedTurnClosers, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '../src/index.ts'
import type { SessionEvent, SurfaceEvent } from '../src/index.ts'
/**
@@ -273,44 +273,3 @@ describe('interruptedTurnClosers', () => {
expect(closers.map(e => e.type)).toEqual(['step/end', 'turn/end'])
})
})
describe('lastActivityTime', () => {
const endSeedAt = (seq: number, time: number): SessionEvent =>
({ type: 'session/end-seed', seq, time, data: {} })
it('has no answer for an empty log', () => {
expect(lastActivityTime([])).toBeUndefined()
})
it('reports the log tail when no boundary is present', () => {
const events: SessionEvent[] = [
userTurnStart(1, 0),
{ type: 'turn/end', seq: 1, time: 500, data: { turn: 1, reason: { kind: 'completed' } } },
]
expect(lastActivityTime(events)).toBe(500)
})
it('skips a trailing boundary in favour of the last real work', () => {
const events: SessionEvent[] = [
userTurnStart(1, 0),
{ type: 'turn/end', seq: 1, time: 500, data: { turn: 1, reason: { kind: 'completed' } } },
endSeedAt(2, 9_000),
]
// Resumed long after the work, but never worked in again.
expect(lastActivityTime(events)).toBe(500)
})
it('reports work appended after end-seed', () => {
const events: SessionEvent[] = [
userTurnStart(1, 0),
endSeedAt(1, 9_000),
{ type: 'turn/end', seq: 2, time: 9_500, data: { turn: 1, reason: { kind: 'completed' } } },
]
expect(lastActivityTime(events)).toBe(9_500)
})
it('has no answer for a log of nothing but boundaries', () => {
// Unreachable via the constructor, but the projection is a pure function.
expect(lastActivityTime([endSeedAt(0, 1), endSeedAt(1, 2)])).toBeUndefined()
})
})