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
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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
Reference in New Issue
Block a user