refactor(session): drop the unused predicate helper and tighten the docs
`isInheritedSeq` had no production caller — only its own tests — so it was a public core export shaped by nothing. A bracket owner reads the boundary positionally; the helper belongs with the compaction seam, where a real consumer decides its signature. Also condense the `session/inherited` and `lastActivityTime` docs.
This commit is contained in:
@@ -23,7 +23,7 @@ export * from './types.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, isInheritedSeq, lastActivityTime, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from './repair.ts'
|
||||
export { interruptedTurnClosers, lastActivityTime, 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'
|
||||
|
||||
@@ -11,44 +11,11 @@ import type { ToolResultMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { SessionEvent } from './types.ts'
|
||||
|
||||
/**
|
||||
* Whether the event at `seq` was inherited rather than written by the lifecycle
|
||||
* that owns `events` — the stored-history reading of `Session.firstLiveSeq`.
|
||||
*
|
||||
* An owner of a standalone open/close bracket calls this on an unmatched
|
||||
* opening marker: `true` means the operation cannot still be running, because
|
||||
* the lifecycle that opened it has ended (a crashed writer, a succeeding
|
||||
* process, or a parent the events were forked out of). `false` means it belongs
|
||||
* to the current lifecycle and must be treated as live.
|
||||
*
|
||||
* Reads the log rather than a `Session`, so it serves a consumer holding only
|
||||
* loaded events; in-process, compare against `session.firstLiveSeq` instead.
|
||||
* @param events - the log to scan, contiguous from seq 0.
|
||||
* @param seq - the event seq to classify.
|
||||
* @returns true when a `session/inherited` boundary sits at or above `seq`.
|
||||
*/
|
||||
export function isInheritedSeq(events: readonly SessionEvent[], seq: number): boolean {
|
||||
// Tail-first: an unmarked log costs no full scan, and bracket queries are
|
||||
// usually about recent events.
|
||||
for (let index = events.length - 1; index >= 0; index -= 1) {
|
||||
const event = events[index]
|
||||
/* v8 ignore next -- a contiguous log has no holes; the guard is for the index type */
|
||||
if (event === undefined) continue
|
||||
if (event.seq < seq) return false
|
||||
if (event.type === 'session/inherited') return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* The `time` of the log's last event that represents actual work, skipping the
|
||||
* `session/inherited` boundary.
|
||||
*
|
||||
* Picking a session up is not activity, and lazy resume means browsing writes a
|
||||
* boundary, so activity ordering (a resume picker, a session list) must skip it
|
||||
* or every opened session sorts as freshly worked in.
|
||||
* The `time` of the log's last event representing actual work, skipping the
|
||||
* `session/inherited` boundary — picking a session up is not activity, so
|
||||
* activity ordering must exclude it.
|
||||
* @param events - the log to scan, in seq order.
|
||||
* @returns the latest non-boundary event's `time`, or undefined when the log has
|
||||
* no such event (empty, or nothing but boundaries).
|
||||
* @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/inherited')?.time
|
||||
|
||||
@@ -251,23 +251,17 @@ export interface SessionEventMap {
|
||||
*/
|
||||
'request/header': { header: EpochHeader; reason: RequestHeaderReason }
|
||||
/**
|
||||
* The log-only durable projection of {@link Session.firstLiveSeq}: everything
|
||||
* BELOW it was inherited through a constructor seed — resume, fork, or replay
|
||||
* — and no writer in this session's lifecycle produced it. Appended as the
|
||||
* first live event of every seeded session.
|
||||
* Log-only durable projection of {@link Session.firstLiveSeq}: everything
|
||||
* below it was inherited through a constructor seed (resume, fork, or replay)
|
||||
* and no writer in this lifecycle produced it. Payload is empty — position
|
||||
* and `time` carry the meaning.
|
||||
*
|
||||
* A plugin owning a standalone open/close bracket (`compact/start` …
|
||||
* `compact/end`) needs it because inherited history and live work are
|
||||
* otherwise byte-identical: an unmatched opening marker below this boundary
|
||||
* belongs to an ended lifecycle, so it is dead whether the writer crashed,
|
||||
* the process succeeded it, or the events were forked out of a parent that is
|
||||
* still running. Read it through `isInheritedSeq`.
|
||||
*
|
||||
* NOT a liveness signal about other writers: a concurrently live session may
|
||||
* hold an open bracket over the same stored history with its own boundary
|
||||
* An owner of a standalone open/close bracket (`compact/start` …
|
||||
* `compact/end`) reads it because inherited history and live work are
|
||||
* otherwise byte-identical: an unmatched opening marker below the boundary
|
||||
* belongs to an ended lifecycle, whatever ended it. NOT a liveness signal
|
||||
* about other writers — a concurrently live session holds its own boundary
|
||||
* elsewhere, so tolerating concurrent writers needs a signal beyond the log.
|
||||
*
|
||||
* The payload is empty by design — position and `time` carry the meaning.
|
||||
*/
|
||||
'session/inherited': Record<string, never>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CallId , createMessage, createToolResultMessage } from '@deepseek-ai/dsh-llm'
|
||||
import { interruptedTurnClosers, isInheritedSeq, lastActivityTime, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '../src/index.ts'
|
||||
import { interruptedTurnClosers, lastActivityTime, TOOL_NOT_STARTED, TOOL_OUTCOME_UNKNOWN } from '../src/index.ts'
|
||||
import type { SessionEvent, SurfaceEvent } from '../src/index.ts'
|
||||
|
||||
/**
|
||||
@@ -274,64 +274,6 @@ describe('interruptedTurnClosers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* The stored-history reading of the inherited boundary. A bracket owner calls
|
||||
* this on an unmatched opening marker to decide whether the operation can still
|
||||
* be running, so the classification of the marker's own seq — and of the
|
||||
* boundary seq itself — is the contract.
|
||||
*/
|
||||
describe('isInheritedSeq', () => {
|
||||
const inheritedAt = (seq: number): SessionEvent =>
|
||||
({ type: 'session/inherited', seq, time: seq, data: {} })
|
||||
|
||||
it('classifies nothing as inherited in a log without a boundary', () => {
|
||||
const events: SessionEvent[] = [
|
||||
userTurnStart(1, 0),
|
||||
{ type: 'turn/end', seq: 1, time: 1, data: { turn: 1, reason: { kind: 'completed' } } },
|
||||
]
|
||||
expect(isInheritedSeq(events, 0)).toBe(false)
|
||||
expect(isInheritedSeq(events, 1)).toBe(false)
|
||||
})
|
||||
|
||||
it('treats an empty log as owning nothing', () => {
|
||||
expect(isInheritedSeq([], 0)).toBe(false)
|
||||
})
|
||||
|
||||
it('splits the log at the boundary', () => {
|
||||
// seqs 0-1 inherited; the boundary at 2; seq 3 written by this lifecycle.
|
||||
const events: SessionEvent[] = [
|
||||
userTurnStart(1, 0),
|
||||
{ type: 'turn/end', seq: 1, time: 1, data: { turn: 1, reason: { kind: 'completed' } } },
|
||||
inheritedAt(2),
|
||||
userTurnStart(2, 3),
|
||||
]
|
||||
expect(isInheritedSeq(events, 0)).toBe(true)
|
||||
expect(isInheritedSeq(events, 1)).toBe(true)
|
||||
// The boundary's own seq counts as inherited: it belongs to the pickup.
|
||||
expect(isInheritedSeq(events, 2)).toBe(true)
|
||||
expect(isInheritedSeq(events, 3)).toBe(false)
|
||||
})
|
||||
|
||||
it('reports inherited for an event below a later boundary', () => {
|
||||
// Two pickups in turn: the tail scan must not stop at the nearer boundary.
|
||||
const events: SessionEvent[] = [
|
||||
userTurnStart(1, 0),
|
||||
inheritedAt(1),
|
||||
userTurnStart(2, 2),
|
||||
inheritedAt(3),
|
||||
userTurnStart(3, 4),
|
||||
]
|
||||
expect(isInheritedSeq(events, 0)).toBe(true)
|
||||
expect(isInheritedSeq(events, 2)).toBe(true)
|
||||
expect(isInheritedSeq(events, 4)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Activity ordering excludes the pickup boundary. A resume picker or session
|
||||
* list sorting by log tail would otherwise promote every session the user
|
||||
* merely opened above the ones they actually worked in.
|
||||
*/
|
||||
describe('lastActivityTime', () => {
|
||||
const inheritedAt = (seq: number, time: number): SessionEvent =>
|
||||
({ type: 'session/inherited', seq, time, data: {} })
|
||||
|
||||
Reference in New Issue
Block a user