forked from deepseek-ai/deepseek-harness
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
497 lines (471 loc) · 21.7 KB
/
Copy pathindex.ts
File metadata and controls
497 lines (471 loc) · 21.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
/**
* Service Definition and drive registry for the session-projection capability seam: the merge-extensible state and client-view type
* tables, the `ProjectionDefinition` state-driven computation unit contract,
* and the `ctx.sessionProjections` registry that DRIVES every registered unit
* forward eagerly over committed session events. Domain host plugins
* contribute pure folds and optional client views; the framework owns the
* subscription, the per-session watermark cache, and change notification;
* carriers consume the snapshot read face and the change feed. Neither side
* knows the other
* (capability-seam three-way split). Design authority: the session-projection
* RFC (.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md).
*
* 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
* unit's transition trivially cheap and every served value self-describing.
*
* @module @deepseek-ai/dsh-session-projection
*/
import { Context, Service } from '@deepseek-ai/cordis'
import type { ZodType } from 'zod'
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
declare module '@deepseek-ai/cordis' {
interface Context {
sessionProjections: SessionProjectionRegistry
}
}
import type { SessionProjectionMap, SessionProjectionStateMap } from './types.ts'
export type { SessionProjectionMap, SessionProjectionStateMap } from './types.ts'
/**
* One domain's state-driven computation unit: a pure synchronous fold plus
* declarations and an optional client view — never an opaque getter. The framework drives
* `apply` on every committed session event; the domain holds no
* subscriptions and owns only the computation. All functions MUST be
* synchronous (an async unit would tear the carriers' consistency cut), and
* `state` MUST be plain JSON (the persisted-cache precondition).
*/
export interface ProjectionDefinition<
K extends keyof SessionProjectionStateMap,
S extends SessionProjectionStateMap[K] = SessionProjectionStateMap[K],
> {
/** The projection key this unit owns (its `SessionProjectionStateMap` entry). */
key: K
/** Validates persisted state before it seeds a fold. */
stateSchema: ZodType<S>
/**
* State for the empty log.
* @returns the initial state.
*/
init(): NoInfer<S>
/**
* Pure transition: previous state + one committed event → next state. A
* unit uninterested in an event MUST return the same state reference — an
* unchanged reference (`Object.is`) produces zero downstream work.
* @param state - the state covering all prior events.
* @param event - the next committed session event.
* @returns the next state (same reference when the event is not the unit's).
*/
apply(state: NoInfer<S>, event: SessionEvent): NoInfer<S>
/** Client view. Omit for host-only units. */
wire?: K extends keyof SessionProjectionMap ? {
/** Validates the wire payload before it leaves the host. */
viewSchema: ZodType<SessionProjectionMap[K]>
/**
* State → wire payload (the read-side projection).
* @param state - the current state.
* @returns the whole current value for this unit's key.
*/
view(state: NoInfer<S>): SessionProjectionMap[K]
} : never
/**
* Persisted-cache invalidation version: bump whenever the serialized state fields or the
* fold semantics change, so persisted `(sessionId, key, ver, seq, val)`
* rows from an older unit are discarded instead of being forward-applied
* into garbage. Non-negative integer.
*/
stateVersion: number
}
/**
* Change-feed listener: one unit's value changed for one session. `value` is
* the schema-validated `view` output; `seq` is the unit's watermark at
* emission (the seq of the event that caused the change).
*/
export type ProjectionChangeListener = (
session: Session,
key: Extract<keyof SessionProjectionMap, string>,
value: unknown,
seq: number,
) => void
/**
* One consistent read cut over every registered client-visible unit for one session.
* `asOfSeq` is the shared watermark — the seq of the last event every value
* reflects (`-1` for an empty log, mirroring `session/subscribed.lastSeq`).
*/
export interface ProjectionSnapshot {
/** Seq of the last event the values reflect; -1 for an empty log. */
asOfSeq: number
/** Whole current client value per registered key. */
values: Partial<SessionProjectionMap>
}
/**
* One unit's checkpoint: its internal state (plain JSON by the unit
* contract), the seq of the last event folded into it, and the unit
* `stateVersion` that produced it — the persisted projection-cache row
* `(sessionId, key, ver, seq, val)` minus the two outer keys. A row is
* never authoritative, only a fold shortcut: `restore` discards it on a
* version mismatch or when it claims events past the stored log end.
*/
export interface ProjectionCheckpointRow {
/** The registering unit's `stateVersion` at fold time. */
ver: number
/** Seq of the last event folded into `val`; -1 for the empty log. */
seq: number
/** The unit's internal state — plain JSON per the unit contract. */
val: unknown
}
/** Checkpoint rows keyed by projection key (one session's persisted cache value). */
export type ProjectionCheckpoint = Record<string, ProjectionCheckpointRow>
/** Type-erased unit view the drive machinery works with (the registration contract already proved the typed form). */
interface ErasedDefinition {
key: string
stateSchema: { parse(value: unknown): unknown }
init(): unknown
apply(state: unknown, event: SessionEvent): unknown
wire: { viewSchema: { parse(value: unknown): unknown }; view(state: unknown): unknown } | undefined
stateVersion: number
}
/** Per-session per-unit watermark cache row. */
interface UnitCell {
state: unknown
/** Seq of the last event passed through `apply` (regardless of change). */
observedSeq: number
}
/**
* One live registration: the unit plus its per-session cells (dropped whole
* once the last registrant releases it).
*
* `refs` exists because one unit definition already serves every session — the
* cells are keyed by `Session` — while the registrants are now per-session:
* an agent preset mounts the same tool package once per agent, so N sessions
* on one preset register the same key N times. Without a count the first
* registrant would own the disposer, and its session ending would strip the
* projection from every other live session.
*/
interface Registration {
readonly def: ErasedDefinition
readonly cells: WeakMap<Session, UnitCell>
/** Live registrants sharing this unit; the last one out removes the key. */
refs: number
}
/**
* `ctx.sessionProjections`: the projection unit table and its drive. The
* service subscribes to `session/event` once; every committed event passes
* every registered unit's `apply` (eager drive), and a changed state
* reference in a client-visible unit notifies the change feed with the
* schema-validated view.
* Cells build lazily — a unit registered after events flowed, or a session
* older than the registry, folds `init` over the in-memory log on first
* touch (event or read). Registration is an effect (disposer rides the
* calling fiber): an unloaded domain plugin's key disappears from snapshots
* and clients read it as capability absence. Domain
* plugins register under `ctx.inject(['sessionProjections'], …)` so headless
* assemblies without the registry stay unaffected. Registrants sharing a key
* share one unit and are counted: the same tool package mounted in N agent
* presets registers N times, and the key survives until the last one
* unloads.
*/
export class SessionProjectionRegistry extends Service {
private readonly registrations = new Map<string, Registration>()
private readonly listeners = new Set<ProjectionChangeListener>()
/**
* Create and install the registry as `ctx.sessionProjections`.
* @param ctx - Cordis context that owns the service.
*/
constructor(ctx: Context) {
super(ctx, 'sessionProjections')
ctx.on('session/event', (session: Session, event: SessionEvent) => {
this.drive(session, event)
})
}
/**
* Register one domain's unit. The registration is an effect on the calling
* context's fiber: disposing the fiber (or calling the returned disposer)
* removes the key — and the unit's cached cells — from subsequent drives
* and snapshots.
* @param definition - key, state schema, pure unit functions, and stateVersion.
* @returns the exact disposer that unregisters this unit.
*/
register<
K extends keyof SessionProjectionMap,
S extends SessionProjectionStateMap[K],
>(
definition: Omit<ProjectionDefinition<K, S>, 'wire'> & {
wire: NonNullable<ProjectionDefinition<K, S>['wire']>
},
): () => void
/**
* Register one host-only unit. Its state is omitted from client snapshots
* and always checkpointed like every other unit.
* @param definition - key, state schema, pure unit functions, and stateVersion.
* @returns the exact disposer that unregisters this unit.
*/
register<
K extends Exclude<keyof SessionProjectionStateMap, keyof SessionProjectionMap>,
S extends SessionProjectionStateMap[K],
>(
definition: Omit<ProjectionDefinition<K, S>, 'wire'>,
): () => void
register<K extends keyof SessionProjectionStateMap, S extends SessionProjectionStateMap[K]>(
definition: ProjectionDefinition<K, S>,
): () => void {
const wire = definition.wire as {
viewSchema: ZodType
view(state: S): unknown
} | undefined
const erased: ErasedDefinition = {
key: definition.key,
stateSchema: definition.stateSchema,
init: () => definition.init(),
apply: (state, event) => definition.apply(state as S, event),
wire: wire === undefined
? undefined
: { viewSchema: wire.viewSchema, view: state => wire.view(state as S) },
stateVersion: definition.stateVersion,
}
if (!Number.isSafeInteger(definition.stateVersion) || definition.stateVersion < 0) {
throw new Error(`session projection ${JSON.stringify(definition.key)} stateVersion must be a non-negative integer, got ${String(definition.stateVersion)}`)
}
const dispose = this.ctx.effect(function* (this: SessionProjectionRegistry) {
const key = erased.key
const existing = this.registrations.get(key)
if (existing === undefined) {
this.registrations.set(key, { def: erased, cells: new WeakMap(), refs: 1 })
} else {
if (existing.def.stateVersion !== erased.stateVersion) {
throw new Error(`session projection key ${JSON.stringify(key)} is already registered at stateVersion ${String(existing.def.stateVersion)}; refusing to share it with stateVersion ${String(erased.stateVersion)}`)
}
existing.refs += 1
}
yield () => {
const live = this.registrations.get(key)
/* v8 ignore next -- the disposer runs once per successful registration, so the entry it counted is still here */
if (live === undefined) return
live.refs -= 1
if (live.refs === 0) this.registrations.delete(key)
}
}.bind(this), 'sessionProjections.register()')
return () => void dispose()
}
/**
* Subscribe to the change feed. The registration is an effect on the
* calling context's fiber.
* @param listener - called once per client-visible unit whose state reference changed, per committed event.
* @returns the exact disposer that unsubscribes.
*/
onChanged(listener: ProjectionChangeListener): () => void {
const dispose = this.ctx.effect(() => {
this.listeners.add(listener)
return () => {
this.listeners.delete(listener)
}
}, 'sessionProjections.onChanged()')
return () => void dispose()
}
/**
* Read one unit's current host state without computing unrelated views.
* The returned value is live; callers must not mutate it.
* @param session - the session whose state is read.
* @param key - the registered unit key.
* @returns current state, or `undefined` when the key is not registered.
*/
stateOf<K extends keyof SessionProjectionStateMap>(
session: Session,
key: K,
): SessionProjectionStateMap[K] | undefined {
const registration = this.registrations.get(key)
if (registration === undefined) return undefined
return this.cellFor(registration, session).state as SessionProjectionStateMap[K]
}
/**
* One consistent cut over every registered client-visible unit for one session, read from
* the watermark cache (missing cells fold lazily over the in-memory log).
* Fully synchronous — every value and `asOfSeq` reflect the same log
* position. Each value passes its unit's `viewSchema` before leaving.
* @param session - the session whose projection values are read.
* @returns the snapshot; `values` is empty when no client-visible unit is registered.
*/
snapshot(session: Session): ProjectionSnapshot {
const values: Record<string, unknown> = {}
for (const registration of this.registrations.values()) {
if (registration.def.wire === undefined) continue
const cell = this.cellFor(registration, session)
values[registration.def.key] = registration.def.wire.viewSchema.parse(registration.def.wire.view(cell.state))
}
return { asOfSeq: session.seq - 1, values }
}
/**
* State-level checkpoint of every persisted unit for one session, read
* from the watermark cache (missing cells fold lazily over the in-memory
* log). This is the write side of the persisted projection cache: the
* returned rows are the `(key → {ver, seq, val})` part of the durable
* `(sessionId, key, ver, seq, val)`
* rows. Every `val` is a DETACHED structured clone — never the live
* cell reference: the watermark cache is this registry's authoritative
* mutable state, and a caller reaching the live reference could corrupt
* every subsequent snapshot and frame through it (plain JSON by the unit
* contract, so the clone is total).
* @param session - the session whose unit states are checkpointed.
* @returns one row per registered key.
*/
checkpoint(session: Session): ProjectionCheckpoint {
const rows: ProjectionCheckpoint = {}
for (const registration of this.registrations.values()) {
const cell = this.cellFor(registration, session)
rows[registration.def.key] = {
ver: registration.def.stateVersion,
seq: cell.observedSeq,
val: structuredClone(cell.state),
}
}
return rows
}
/**
* The stored seq a {@link restore} tail read over `checkpoint` must start
* at: one event BELOW the lowest usable watermark (a row is usable when
* its `ver` matches the live unit's `stateVersion`; an absent or mismatched row
* pulls the floor to `0` — that key must refold the full log). The
* one-below anchor is load-bearing: the tail then proves how far the
* stored log still extends, so {@link restore} can detect a log that
* shrank below a row's watermark (crash-repair truncation) instead of
* serving the stale row as current — an empty tail read from the anchor
* yields an end below every watermark and the restore rejects for a full
* re-read.
* @param checkpoint - persisted rows for one session (possibly stale or empty).
* @returns the seq to hand the persistence `readFrom`, or `undefined`
* when no unit is registered (no read needed — {@link restore} would
* serve empty values regardless).
*/
restoreFloor(checkpoint: ProjectionCheckpoint): number | undefined {
let floor: number | undefined
for (const registration of this.registrations.values()) {
const row = checkpoint[registration.def.key]
const need = row !== undefined && row.ver === registration.def.stateVersion
? Math.max(row.seq + 1, 0)
: 0
floor = floor === undefined ? need : Math.min(floor, need)
}
return floor === undefined ? undefined : Math.max(floor - 1, 0)
}
/**
* View a checkpoint's rows without any log read: for every registered
* client-visible unit whose row's `ver` matches, serve the schema-validated
* `view` of the schema-validated stored state; mismatched, malformed, or absent rows leave their key
* absent (a cold or listing consumer treats it as not-yet-available and a
* fuller read path refolds it). The zero-I/O rung of the read ladder —
* values are as stale as their rows, never wrong.
* @param checkpoint - persisted rows for one session (possibly stale or empty).
* @returns whole values per key with a usable row; empty when none.
*/
viewCheckpoint(checkpoint: ProjectionCheckpoint): Partial<SessionProjectionMap> {
const values: Record<string, unknown> = {}
for (const registration of this.registrations.values()) {
const def = registration.def
if (def.wire === undefined) continue
const row = checkpoint[def.key]
if (row === undefined || row.ver !== def.stateVersion) continue
let state: unknown
try {
state = def.stateSchema.parse(row.val)
} catch {
continue
}
values[def.key] = def.wire.viewSchema.parse(def.wire.view(state))
}
return values
}
/**
* Cold read: fold every persisted unit over a stored log suffix, seeding
* each from its checkpoint row when usable — the one read recipe (cached
* state + forward tail replay + `view`) applied without a live `Session`.
* Call with the events returned by a persistence
* `readFrom(id, restoreFloor(checkpoint))` and that same floor as
* `baseSeq`; the floor's one-below anchor makes the supplied end honest,
* so a shrunk log is detected here. A row is usable iff its
* `ver` matches the live unit's `stateVersion`, it does not predate `baseSeq`
* (`seq >= baseSeq - 1`), and it does not claim events past the
* supplied end (`seq <= endSeq`); an unusable row is discarded
* and its key refolds from `init` — which is only sound over the full
* log, so a discarded row with `baseSeq > 0` throws (the caller re-reads
* from seq 0, e.g. after a crash-repair truncation shrank the log below
* a row's watermark).
* @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).
* @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 } {
const endSeq = events.at(-1)?.seq ?? baseSeq - 1
const values: Record<string, unknown> = {}
const refreshed: ProjectionCheckpoint = {}
for (const registration of this.registrations.values()) {
const def = registration.def
const row = checkpoint[def.key]
const usable = row !== undefined
&& row.ver === def.stateVersion
&& row.seq >= baseSeq - 1
&& row.seq <= endSeq
if (!usable && baseSeq > 0) {
throw new Error(
`session projection ${JSON.stringify(def.key)} cannot restore from seq ${baseSeq}: `
+ 'its checkpoint row is missing, version-mismatched, or beyond the supplied log end; re-read from seq 0',
)
}
let state = usable ? def.stateSchema.parse(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 (def.wire !== undefined) values[def.key] = def.wire.viewSchema.parse(def.wire.view(state))
refreshed[def.key] = { ver: def.stateVersion, seq: endSeq, val: state }
}
return {
snapshot: { asOfSeq: endSeq, values: values },
checkpoint: refreshed,
}
}
/** Fold one unit from init over `events`, producing a cell watermarked at the last folded event. */
private buildCell(def: ErasedDefinition, events: readonly SessionEvent[]): UnitCell {
let state = def.init()
for (const event of events) state = def.apply(state, event)
return { state, observedSeq: (events.at(-1)?.seq ?? -1) }
}
/** Read (or lazily build, folding the full in-memory log) one unit's cell. */
private cellFor(registration: Registration, session: Session): UnitCell {
let cell = registration.cells.get(session)
if (cell === undefined) {
cell = this.buildCell(registration.def, session.events)
registration.cells.set(session, cell)
}
return cell
}
/** Eager drive: pass one committed event through every registered unit; notify on changed references. */
private drive(session: Session, event: SessionEvent): void {
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))
registration.cells.set(session, cell)
}
const next = registration.def.apply(cell.state, event)
const changed = !Object.is(next, cell.state)
cell.state = next
cell.observedSeq = event.seq
if (changed && registration.def.wire !== undefined && this.listeners.size > 0) {
const value = registration.def.wire.viewSchema.parse(registration.def.wire.view(next))
for (const listener of this.listeners) {
listener(session, registration.def.key as Extract<keyof SessionProjectionMap, string>, value, event.seq)
}
}
}
}
}
export default SessionProjectionRegistry