fix(apiproxy): harden cold session metadata probing
This commit is contained in:
@@ -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,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,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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user