feat: add economy/maximum presets, tool-lab and subagent-cursor extensions
Some checks failed
CI / windows node 24 / wine blocking (push) Has been skipped
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / wine apt cache (push) Successful in 7s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 1m25s
Deploy documentation / deploy (push) Has been skipped
Landlock Run / Matrix (push) Successful in 5s
Release (vendor) / Pack npm tarballs (push) Failing after 2m47s
Release (dsh) / Pack npm tarballs (push) Failing after 1m56s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m57s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m19s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
E2E (real DeepSeek API) / e2e (push) Failing after 1m24s

- new economy and maximum agent presets with three-role pipeline skill
- new packages/extensions/tool-lab (home-lab ComfyUI/Docling/Whishper tools)
- new packages/subagent/subagent-cursor provider
- openrouter balance UI with on-demand refresh
- session projection context-seed boundary fold
- regenerate docs catalogs; keep local searxng benchmark scripts
This commit is contained in:
2026-08-23 22:12:30 +07:00
parent e818e37f0a
commit 8719fa84f0
112 changed files with 5686 additions and 169 deletions

View File

@@ -19,6 +19,7 @@ import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-
// Empty type import: applies the package's cordis Context merge
// (`ctx.sessionPersistence`), which this service reads on the cold path.
import type {} from '@deepseek-ai/dsh-session-persistence'
import { foldContextOf } from '@deepseek-ai/dsh-session-projection'
import type { ProjectionCheckpoint, ProjectionSnapshot } from '@deepseek-ai/dsh-session-projection'
import type { KvTable } from '@deepseek-ai/dsh-storage-domain'
import { projectionCacheDomainSpec } from './spec.ts'
@@ -183,14 +184,14 @@ export class SessionProjectionCache extends Service {
const related = record === undefined || identityMatches(record.identity, identityOf(tail.meta))
try {
if (!related) throw new Error('unrelated log identity')
restored = this.ctx.sessionProjections.restore(cached, tail.events, floor)
restored = this.ctx.sessionProjections.restore(cached, tail.events, floor, foldContextOf(tail.meta))
} catch {
// The recoverable restore failures: an unrelated record, or a row
// overreaching the stored log end (or predating the floor). Both imply
// floor > 0 (baseSeq-0 restores never throw and an unrelated record
// still carried a usable watermark), so the full log is a fresh read.
const whole = await persistence.readFrom(id, 0, signal)
restored = this.ctx.sessionProjections.restore({}, whole.events, 0)
restored = this.ctx.sessionProjections.restore({}, whole.events, 0, foldContextOf(whole.meta))
}
await this.putSoft(id, identityOf(tail.meta), restored.checkpoint, 'cold-read write-back')
return restored.snapshot

View File

@@ -11,15 +11,18 @@ Session-projection Service Definition and drive registry. It owns `ctx.sessionPr
- `ctx.sessionProjections.register(definition): () => void` Register one domain's unit. Duplicate keys and invalid `stateVersion` throw; the registration is an effect on the calling fiber, so an unloaded domain plugin's key (with its cached cells) disappears from subsequent drives and snapshots — clients read that as capability absence.
- `ctx.sessionProjections.onChanged(listener): () => void` Subscribe to the change feed: one call per unit whose state reference changed, per committed event, carrying the schema-validated view and the causing seq. Effect-tied like `register`.
- `ctx.sessionProjections.snapshot(session): ProjectionSnapshot` One consistent synchronous cut over every registered unit — `{ asOfSeq, values }` with `asOfSeq` = the seq of the last event every value reflects (`-1` for an empty log).
- `foldContextOf(header): ProjectionFoldContext` Derive the per-session fold context from a session header. Every `apply` call receives one; a caller folding a detached log (`restore`) supplies it explicitly.
### Key Types
- `SessionProjectionMap` — the single merge-extensible type table for the whole chain (host unit, wire block, React hook). Values are wire-JSON whole values; rendering belongs to the slot system, never this layer.
- `ProjectionDefinition<K, S>` — `{ key, schema, init(), apply(state, event), view(state), stateVersion }`: a state-driven computation unit of three pure synchronous functions plus declarations, never an opaque getter.
- `ProjectionDefinition<K, S>` — `{ key, schema, init(), apply(state, event, context), view(state), stateVersion }`: a state-driven computation unit of three pure synchronous functions plus declarations, never an opaque getter.
- `ProjectionFoldContext` — per-session header facts a unit folds against beyond the event stream: `seedLength`, the durable fork-lineage boundary.
## Contract
- **The framework drives, the domain computes.** The registry subscribes to `session/event` once; every committed event passes every unit's `apply` eagerly. Domains hold no subscriptions. Cells (`{state, observedSeq}` per unit per session, WeakMap-keyed) build lazily — a unit registered after events flowed, or a read of a session predating the registration, folds `init` over the in-memory log on first touch.
- **A unit reads session facts through the fold context, never a Session.** Every `apply` call carries `ProjectionFoldContext`, derived from the session's durable header. Its `seedLength` is the fork-lineage boundary: events with `seq < seedLength` were inherited from the forked parent rather than produced by this session. A unit whose value must describe only this session's OWN work (spend, step counts) skips them, so a parent value plus its children's values is a sum rather than a double count; a unit describing the whole conversation (context pressure, the visible transcript) folds them like any other event. A unit needing neither declares two parameters and ignores the third.
- **Same-reference means no work.** `apply` MUST return the same state reference for events that do not concern the unit; the drive gates the change feed on `Object.is`, so non-matching events cost one call and nothing downstream.
- **Whole-value event rule (load-bearing).** A state-carrying log event MUST carry the complete post-change state, never a bare delta — it keeps every transition trivially cheap and every served value self-describing (last-wins for consumers).
- **Synchronous unit discipline.** `init`/`apply`/`view` MUST be synchronous; carriers read `snapshot()` in the same tick as their page slice, which is what makes `asOfSeq` one consistent cut. An accidentally-async `view` returns a Promise, which fails the boundary `schema.parse` loudly.

View File

@@ -19,7 +19,7 @@
import { Context, Service } from '@deepseek-ai/cordis'
import type { ZodType } from 'zod'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent, SessionHeader } from '@deepseek-ai/dsh-session'
declare module '@deepseek-ai/cordis' {
interface Context {
@@ -31,6 +31,32 @@ import type { SessionProjectionMap } from './types.ts'
export type { SessionProjectionMap } from './types.ts'
/**
* Per-session facts a unit folds against beyond the event stream itself.
* Supplied by the framework on every `apply` call and derived from the
* session's durable header, so a unit never reads a Session.
*/
export interface ProjectionFoldContext {
/**
* The session header's durable fork-lineage boundary: events with
* `seq < seedLength` were inherited from the forked parent rather than
* produced by this session. A unit whose value must describe only this
* session's OWN work skips them; a unit describing the whole conversation
* (context pressure, the visible transcript) folds them like any other
* event. 0 for a session with no inherited prefix.
*/
readonly seedLength: number
}
/**
* Derive the fold context of one session's header.
* @param header - the session's durable header.
* @returns the context every `apply` call for that session receives.
*/
export function foldContextOf(header: Pick<SessionHeader, 'seedLength'>): ProjectionFoldContext {
return { seedLength: header.seedLength ?? 0 }
}
/**
* One domain's state-driven computation unit: three pure synchronous
* functions plus declarations — never an opaque getter. The framework drives
@@ -55,9 +81,10 @@ export interface ProjectionDefinition<K extends keyof SessionProjectionMap, S> {
* unchanged reference (`Object.is`) produces zero downstream work.
* @param state - the state covering all prior events.
* @param event - the next committed session event.
* @param context - per-session header facts; a unit that needs none may declare two parameters.
* @returns the next state (same reference when the event is not the unit's).
*/
apply(state: S, event: SessionEvent): S
apply(state: S, event: SessionEvent, context: ProjectionFoldContext): S
/**
* State → wire payload (the read-side projection).
* @param state - the current state.
@@ -122,7 +149,7 @@ interface ErasedDefinition {
key: string
schema: { parse(value: unknown): unknown }
init(): unknown
apply(state: unknown, event: SessionEvent): unknown
apply(state: unknown, event: SessionEvent, context: ProjectionFoldContext): unknown
view(state: unknown): unknown
stateVersion: number
}
@@ -348,12 +375,17 @@ export class SessionProjectionRegistry extends Service {
* @param checkpoint - persisted rows for one session (possibly stale or empty).
* @param events - the stored events with `seq >= baseSeq`, in seq order.
* @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
* @param context - the stored header's fold context (see {@link foldContextOf}).
* @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
* supplied event's seq, `baseSeq - 1` for an empty tail) plus the
* refreshed checkpoint rows at that cut, ready for a durable write-back.
*/
restore(checkpoint: ProjectionCheckpoint, events: readonly SessionEvent[], baseSeq: number):
{ snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } {
restore(
checkpoint: ProjectionCheckpoint,
events: readonly SessionEvent[],
baseSeq: number,
context: ProjectionFoldContext,
): { snapshot: ProjectionSnapshot; checkpoint: ProjectionCheckpoint } {
const endSeq = events.at(-1)?.seq ?? baseSeq - 1
const values: Record<string, unknown> = {}
const refreshed: ProjectionCheckpoint = {}
@@ -373,7 +405,7 @@ export class SessionProjectionRegistry extends Service {
let state = usable ? row.val : def.init()
const from = usable ? row.seq : baseSeq - 1
for (const event of events) {
if (event.seq > from) state = def.apply(state, event)
if (event.seq > from) state = def.apply(state, event, context)
}
values[def.key] = def.schema.parse(def.view(state))
refreshed[def.key] = { ver: def.stateVersion, seq: endSeq, val: state }
@@ -385,9 +417,13 @@ export class SessionProjectionRegistry extends Service {
}
/** Fold one unit from init over `events`, producing a cell watermarked at the last folded event. */
private buildCell(def: ErasedDefinition, events: readonly SessionEvent[]): UnitCell {
private buildCell(
def: ErasedDefinition,
events: readonly SessionEvent[],
context: ProjectionFoldContext,
): UnitCell {
let state = def.init()
for (const event of events) state = def.apply(state, event)
for (const event of events) state = def.apply(state, event, context)
return { state, observedSeq: (events.at(-1)?.seq ?? -1) }
}
@@ -395,7 +431,7 @@ export class SessionProjectionRegistry extends Service {
private cellFor(registration: Registration, session: Session): UnitCell {
let cell = registration.cells.get(session)
if (cell === undefined) {
cell = this.buildCell(registration.def, session.events)
cell = this.buildCell(registration.def, session.events, foldContextOf(session.header))
registration.cells.set(session, cell)
}
return cell
@@ -403,15 +439,16 @@ export class SessionProjectionRegistry extends Service {
/** Eager drive: pass one committed event through every registered unit; notify on changed references. */
private drive(session: Session, event: SessionEvent): void {
const context = foldContextOf(session.header)
for (const registration of this.registrations.values()) {
let cell = registration.cells.get(session)
if (cell === undefined) {
// Late build mid-stream: fold history before this event (seq = log
// index, so the prefix slice is exact), then take the normal gate.
cell = this.buildCell(registration.def, session.events.slice(0, event.seq))
cell = this.buildCell(registration.def, session.events.slice(0, event.seq), context)
registration.cells.set(session, cell)
}
const next = registration.def.apply(cell.state, event)
const next = registration.def.apply(cell.state, event, context)
const changed = !Object.is(next, cell.state)
cell.state = next
cell.observedSeq = event.seq

View File

@@ -12,9 +12,12 @@ import { Context } from '@deepseek-ai/cordis'
import { z } from 'zod'
import SessionStore from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SessionProjectionRegistry, { foldContextOf } from '@deepseek-ai/dsh-session-projection'
import type { ProjectionDefinition } from '@deepseek-ai/dsh-session-projection'
/** The fold context of a session with no inherited fork prefix. */
const NO_SEED = foldContextOf({})
declare module '@deepseek-ai/dsh-session-projection/types' {
interface SessionProjectionMap {
'test/marks': { marks: string[] }
@@ -49,6 +52,19 @@ const countUnit = (): ProjectionDefinition<'test/count', number> => ({
stateVersion: 1,
})
/**
* A unit counting only events the fold context marks as this session's OWN
* work, so its value proves the context reached `apply`.
*/
const ownCountUnit = (): ProjectionDefinition<'test/count', number> => ({
key: 'test/count',
schema: z.number().int().nonnegative(),
init: () => 0,
apply: (state, event, context) => (event.seq < context.seedLength ? state : state + 1),
view: state => state,
stateVersion: 1,
})
async function harness(): Promise<{ ctx: Context; session: Session }> {
const ctx = new Context()
await ctx.plugin(SessionStore)
@@ -263,7 +279,7 @@ describe('SessionProjectionRegistry drive', () => {
expect(() => ctx.sessionProjections.restore({
'test/marks': { ver: 1, seq: 2, val: { marks: ['old'] } },
'test/count': { ver: 99, seq: 2, val: 3 },
}, tail, 3)).toThrow(/re-read from seq 0/)
}, tail, 3, NO_SEED)).toThrow(/re-read from seq 0/)
// The full-log re-read (baseSeq 0) refolds the mismatched key from init.
const full: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 0, data: { turn: 1 } },
@@ -274,7 +290,7 @@ describe('SessionProjectionRegistry drive', () => {
const { snapshot, checkpoint } = ctx.sessionProjections.restore({
'test/marks': { ver: 1, seq: 2, val: { marks: ['old', '2'] } },
'test/count': { ver: 99, seq: 2, val: 3 },
}, full, 0)
}, full, 0, NO_SEED)
expect(snapshot.asOfSeq).toBe(4)
expect(snapshot.values['test/marks']).toEqual({ marks: ['new'] })
expect(snapshot.values['test/count']).toBe(5) // refolded from init over all 5 events
@@ -295,7 +311,7 @@ describe('SessionProjectionRegistry drive', () => {
{ type: 'turn/start', seq: 3, time: 3, data: { turn: 2 } },
{ type: 'turn/end', seq: 4, time: 4, data: { turn: 2, reason: { kind: 'completed' } } },
]
const { snapshot } = ctx.sessionProjections.restore(rows, tail, 3)
const { snapshot } = ctx.sessionProjections.restore(rows, tail, 3, NO_SEED)
expect(snapshot.asOfSeq).toBe(4)
// marks already covers the tail (watermark 4): nothing re-applied.
expect(snapshot.values['test/marks']).toEqual({ marks: ['done'] })
@@ -306,7 +322,7 @@ describe('SessionProjectionRegistry drive', () => {
const { snapshot: current } = ctx.sessionProjections.restore({
'test/marks': { ver: 1, seq: 4, val: { marks: ['done'] } },
'test/count': { ver: 1, seq: 4, val: 5 },
}, [], 5)
}, [], 5, NO_SEED)
expect(current.asOfSeq).toBe(4)
expect(current.values['test/count']).toBe(5)
})
@@ -324,6 +340,48 @@ describe('SessionProjectionRegistry drive', () => {
expect(ctx.sessionProjections.viewCheckpoint({})).toEqual({})
})
it('folds every unit against the fold context of the session header', async () => {
const { ctx, session: parent } = await harness()
ctx.sessionProjections.register(ownCountUnit())
mark(parent, ['a'])
mark(parent, ['b'])
const inherited = parent.seq
expect(ctx.sessionProjections.snapshot(parent).values['test/count']).toBe(inherited)
// The forked child inherits the parent's log; only its own appends count.
const seed = [...parent.events]
const child = ctx.sessions.create(undefined, { seed, meta: { seedLength: seed.length } })
mark(child, ['own'])
expect(child.header.seedLength).toBe(seed.length)
expect(ctx.sessionProjections.snapshot(child).values['test/count'])
.toBe(child.seq - seed.length)
})
it('passes the fold context to a cell built lazily after events flowed', async () => {
const { ctx, session: parent } = await harness()
mark(parent, ['a'])
const seed = [...parent.events]
const child = ctx.sessions.create(undefined, { seed, meta: { seedLength: seed.length } })
mark(child, ['own'])
// Registered only now: the lazy full-log build must see the same context.
ctx.sessionProjections.register(ownCountUnit())
expect(ctx.sessionProjections.snapshot(child).values['test/count'])
.toBe(child.seq - seed.length)
})
it('restore folds against the caller-supplied fold context', async () => {
const { ctx } = await harness()
ctx.sessionProjections.register(ownCountUnit())
const events: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 0, data: { turn: 1 } },
{ type: 'turn/end', seq: 1, time: 1, data: { turn: 1, reason: { kind: 'completed' } } },
{ type: 'turn/start', seq: 2, time: 2, data: { turn: 2 } },
]
expect(ctx.sessionProjections.restore({}, events, 0, NO_SEED).snapshot.values['test/count']).toBe(3)
expect(ctx.sessionProjections.restore({}, events, 0, foldContextOf({ seedLength: 2 }))
.snapshot.values['test/count']).toBe(1)
})
it('restore rejects a row claiming events past the supplied log end (shrunk log ⇒ re-read)', async () => {
const { ctx } = await harness()
ctx.sessionProjections.register(countUnit())
@@ -334,16 +392,16 @@ describe('SessionProjectionRegistry drive', () => {
expect(floor).toBe(9)
// …an intact log serves the anchor event and the checkpoint stands as-is.
const anchor: SessionEvent = { type: 'turn/end', seq: 9, time: 9, data: { turn: 2, reason: { kind: 'completed' } } }
expect(ctx.sessionProjections.restore(rows, [anchor], 9).snapshot.values['test/count']).toBe(10)
expect(ctx.sessionProjections.restore(rows, [anchor], 9, NO_SEED).snapshot.values['test/count']).toBe(10)
// …while a log crash-repaired down to fewer events returns an empty tail:
// the row overreaches the proven end and a tail read cannot fix this key.
expect(() => ctx.sessionProjections.restore(rows, [], 9)).toThrow(/re-read from seq 0/)
expect(() => ctx.sessionProjections.restore(rows, [], 9, NO_SEED)).toThrow(/re-read from seq 0/)
// The full re-read discards the overreaching row and refolds from init.
const events: SessionEvent[] = [
{ type: 'turn/start', seq: 0, time: 0, data: { turn: 1 } },
{ type: 'turn/end', seq: 1, time: 1, data: { turn: 1, reason: { kind: 'completed' } } },
]
const { snapshot } = ctx.sessionProjections.restore(rows, events, 0)
const { snapshot } = ctx.sessionProjections.restore(rows, events, 0, NO_SEED)
expect(snapshot.asOfSeq).toBe(1)
expect(snapshot.values['test/count']).toBe(2)
})

View File

@@ -15,10 +15,14 @@ import { Context } from '@deepseek-ai/cordis'
import { createMessage } from '@deepseek-ai/dsh-llm'
import SessionStore, { SessionId } from '@deepseek-ai/dsh-session'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
import SessionProjectionRegistry, { foldContextOf } from '@deepseek-ai/dsh-session-projection'
import * as SessionStatsPlugin from '@deepseek-ai/dsh-session-stats'
import { sessionStatsProjectionDefinition } from '@deepseek-ai/dsh-session-stats/src/projection.ts'
import type { SessionStatsProjection } from '@deepseek-ai/dsh-session-stats/types'
/** The unit under test ignores the fold context; this session inherits no fork prefix. */
const applyStats = (state: Parameters<typeof sessionStatsProjectionDefinition.apply>[0], event: SessionEvent) =>
sessionStatsProjectionDefinition.apply(state, event, foldContextOf({}))
async function harness(withStatsPlugin: boolean): Promise<{ ctx: Context; session: Session }> {
const ctx = new Context()
@@ -155,7 +159,7 @@ function at(time: number, type: string, data: unknown): SessionEvent {
/** Fold a synthetic event list through the definition and view the result. */
function fold(events: readonly SessionEvent[]): SessionStatsProjection {
const state = events.reduce(
(folded, event) => sessionStatsProjectionDefinition.apply(folded, event),
(folded, event) => applyStats(folded, event),
sessionStatsProjectionDefinition.init(),
)
return sessionStatsProjectionDefinition.view(state)
@@ -270,10 +274,10 @@ describe('sessionStats wall-time fold (controlled timestamps)', () => {
// The first message closed the step boundary; a defensive duplicate finds
// no open step and folds to the same reference.
const state = events.reduce(
(folded, event) => sessionStatsProjectionDefinition.apply(folded, event),
(folded, event) => applyStats(folded, event),
sessionStatsProjectionDefinition.init(),
)
expect(sessionStatsProjectionDefinition.apply(
expect(applyStats(
state,
at(2_050, 'assistant/message', { turn: 1, step: 1, message }),
)).toBe(state)
@@ -281,7 +285,7 @@ describe('sessionStats wall-time fold (controlled timestamps)', () => {
it('accrues nothing for unrelated events and clamps negative clock skew to zero', () => {
const state = sessionStatsProjectionDefinition.init()
const untouched = sessionStatsProjectionDefinition.apply(state, at(1, 'user/message', { content: [] }))
const untouched = applyStats(state, at(1, 'user/message', { content: [] }))
expect(untouched).toBe(state)
expect(fold([
at(2_000, 'step/start', { turn: 1, step: 1 }),