@deepseek-ai/dsh-session 0.1.2-alpha.2 → 0.1.2-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@
8
8
  import { Context, Service } from '@deepseek-ai/cordis';
9
9
  import type { Scoped } from '@deepseek-ai/dsh-scope';
10
10
  import type { Message } from '@deepseek-ai/dsh-llm';
11
+ import { SessionLogOffset, SessionSeq } from './types.ts';
11
12
  import type { TypertLookup } from '@deepseek-ai/dsh-typert-protocol';
12
13
  import type { CreateSessionOptions, EpochHeader, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SessionId, SurfaceIntent, SurfaceEventType } from './types.ts';
13
14
  import type { SessionSurface } from './surface.ts';
@@ -108,13 +109,15 @@ export declare class Session {
108
109
  get surface(): SessionSurface;
109
110
  /**
110
111
  * Detached, deep-frozen creation metadata (format version, cwd, lineage,
111
- * seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
112
+ * and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
112
113
  * `Session` is created without a store-owned header, a minimal header is
113
114
  * synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
114
115
  * `session.header` is always present. Kept out of the event log — it is a
115
116
  * storage concern, not replayable conversation state.
116
117
  */
117
118
  readonly header: SessionHeader;
119
+ /** Number of leading events inherited from this Session's fork parent. */
120
+ readonly inheritedEventCount: SessionLogOffset;
118
121
  /** The session identity, derived from its durable header's single copy. */
119
122
  get id(): SessionId;
120
123
  /**
@@ -123,9 +126,9 @@ export declare class Session {
123
126
  * construction — replay, fork, or resume — and were never published on the
124
127
  * `session/event` firehose (constructor seeds do not emit), so consumers
125
128
  * that replay the log as a publication substitute (telemetry adoption)
126
- * start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
127
- * boundary: a resumed session's constructor seed is its full stored log,
128
- * while its header keeps the original fork value — this field is the
129
+ * start here. Distinct from {@link inheritedEventCount}, the DURABLE
130
+ * fork-lineage cut: a resumed session's constructor seed is its full stored
131
+ * log, while the inherited count keeps the original fork value — this field is the
129
132
  * in-process construction fact.
130
133
  *
131
134
  * Not persisted itself: a seeded session projects it into the log as the
@@ -139,16 +142,17 @@ export declare class Session {
139
142
  * store attaches and therefore does not publish either. Otherwise this seq
140
143
  * holds an ordinary published write.
141
144
  */
142
- readonly firstLiveSeq: number;
145
+ readonly firstLiveSeq: SessionLogOffset;
143
146
  /**
144
147
  * Create a detached session by validating and snapshotting borrowed seed
145
148
  * events and storage metadata.
146
149
  * @param id - session identity.
147
150
  * @param seed - optional borrowed replay or fork events.
148
151
  * @param header - optional borrowed storage metadata.
152
+ * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
149
153
  * @returns a detached session.
150
154
  */
151
- static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader): Session;
155
+ static create(id: SessionId, seed?: readonly SessionEvent[], header?: SessionHeader, inheritedEventCount?: SessionLogOffset): Session;
152
156
  /**
153
157
  * Restore a detached session by taking ownership of fresh persistence values.
154
158
  * The storage format, event envelopes, sequence continuity, surface transitions,
@@ -156,21 +160,41 @@ export declare class Session {
156
160
  * @param id - restored session identity.
157
161
  * @param seed - fresh detached events whose ownership is transferred.
158
162
  * @param header - fresh detached metadata whose ownership is transferred.
163
+ * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
159
164
  * @returns a restored detached session.
160
165
  */
161
- static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader): Session;
166
+ static fromRestore(id: SessionId, seed: readonly SessionEvent[], header: SessionHeader, inheritedEventCount: SessionLogOffset): Session;
162
167
  private constructor();
163
- /** Cached immutable public snapshot of the private append-only log. */
168
+ /** Cached immutable full snapshot of the private append-only log. */
164
169
  private eventsSnapshot;
165
170
  /**
166
- * An immutable snapshot of the append-only event log. The snapshot is reused
167
- * until the next append; a previously returned array does not grow later.
168
- * Events and their nested data are deep-frozen at acceptance, so neither a
169
- * cast nor ordinary JavaScript can rewrite durable history.
171
+ * Return the immutable event stored at one exact sequence number.
172
+ * @param seq - event sequence number.
173
+ * @returns the accepted event, or undefined when the log does not contain it.
170
174
  */
171
- get events(): readonly SessionEvent[];
175
+ eventAt(seq: SessionSeq): SessionEvent | undefined;
176
+ /**
177
+ * Materialize an immutable snapshot of a half-open event sequence range.
178
+ * A full current snapshot is reused until the next append; every previously
179
+ * returned snapshot remains stable after later appends.
180
+ * @param fromSeq - non-negative inclusive sequence number; defaults to the log start.
181
+ * @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
182
+ * @returns a frozen array of the selected deeply frozen events.
183
+ */
184
+ snapshotEvents(fromSeq?: SessionLogOffset, toSeqExclusive?: SessionLogOffset): readonly SessionEvent[];
185
+ /**
186
+ * Return this Session's events after its fork-inherited prefix.
187
+ * @returns a fresh array containing child-owned events in log order.
188
+ */
189
+ ownEvents(): readonly SessionEvent[];
190
+ /**
191
+ * Whether one existing event position is outside the fork-inherited prefix.
192
+ * @param seq - event position in this Session.
193
+ * @returns true when the event belongs to this Session rather than its parent.
194
+ */
195
+ isOwnSeq(seq: SessionSeq): boolean;
172
196
  /** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
173
- get seq(): number;
197
+ get seq(): SessionLogOffset;
174
198
  /**
175
199
  * Append one typed event to the log and synchronously notify observers via
176
200
  * the store-owned, module-private publication hooks. The hot path never blocks
@@ -215,7 +239,7 @@ export declare class Session {
215
239
  * The {@link EpochHeader} in force after the log's last header event — the
216
240
  * header the NEXT request will be compared against — or undefined before
217
241
  * the first `request/header` snapshot. The live, incrementally-maintained
218
- * form of `foldRequestHeader(session.events)`: each header event is folded
242
+ * form of `foldRequestHeader(session.snapshotEvents())`: each header event is folded
219
243
  * once, when first seen, so a per-step read costs O(new events).
220
244
  * @returns the folded header, or undefined when no header event exists yet.
221
245
  */
@@ -407,7 +431,7 @@ export declare class SessionStore extends Service {
407
431
  * `SessionStore`'s id policy.
408
432
  * @returns The created live child session.
409
433
  */
410
- fork(source: SessionForkSource, boundary?: number, childSessionId?: SessionId): Session;
434
+ fork(source: SessionForkSource, boundary?: SessionSeq, childSessionId?: SessionId): Session;
411
435
  private _forkSeed;
412
436
  private _resolveForkSource;
413
437
  }
@@ -10,7 +10,7 @@ import { isAbsolute } from 'node:path';
10
10
  import { brandString } from '@deepseek-ai/dsh-brand';
11
11
  import { deepFreeze, snapshotJsonValue } from '@deepseek-ai/dsh-util-values';
12
12
  import { scopeOf, scopeTarget } from '@deepseek-ai/dsh-scope';
13
- import { SESSION_FORMAT_VERSION } from "./types.js";
13
+ import { SESSION_FORMAT_VERSION, SessionLogOffset, SessionSeq } from "./types.js";
14
14
  import { deriveEventMessage, SurfaceManager } from "./surface.js";
15
15
  import { foldRequestHeader } from "./request-header.js";
16
16
  export * from "./types.js";
@@ -26,6 +26,9 @@ function validateSessionHeader(id, input) {
26
26
  throw new Error('session header is not a plain JSON record');
27
27
  }
28
28
  const record = input;
29
+ if (Object.hasOwn(record, 'seedLength')) {
30
+ throw new Error('session header has invalid field "seedLength"');
31
+ }
29
32
  if (record.version !== SESSION_FORMAT_VERSION) {
30
33
  throw new Error(`session header version must be ${SESSION_FORMAT_VERSION}, got ${String(record.version)}`);
31
34
  }
@@ -47,9 +50,8 @@ function validateSessionHeader(id, input) {
47
50
  if (record.parentSession !== undefined && typeof record.parentSession !== 'string') {
48
51
  throw new Error('session header parentSession must be a string');
49
52
  }
50
- if (record.seedLength !== undefined
51
- && (typeof record.seedLength !== 'number' || !Number.isSafeInteger(record.seedLength) || record.seedLength < 0)) {
52
- throw new Error('session header seedLength must be a non-negative safe integer');
53
+ if (typeof record.isSeeded !== 'boolean') {
54
+ throw new Error('session header isSeeded must be a boolean');
53
55
  }
54
56
  if (record.origin !== undefined && record.origin !== 'subagent') {
55
57
  throw new Error('session header origin must be "subagent"');
@@ -76,7 +78,7 @@ function validateRestoredSessionHeader(id, input) {
76
78
  /** Detach, validate, and freeze the creation metadata published by a session. */
77
79
  function snapshotSessionHeader(id, source) {
78
80
  const input = source === undefined
79
- ? { version: SESSION_FORMAT_VERSION, id, createdAt: Date.now() }
81
+ ? { version: SESSION_FORMAT_VERSION, id, createdAt: Date.now(), isSeeded: false }
80
82
  : source;
81
83
  const snapshot = snapshotJsonValue(input);
82
84
  if (snapshot === undefined)
@@ -155,7 +157,7 @@ function assertSessionEventEnvelope(value, index) {
155
157
  const seq = event['seq'];
156
158
  const time = event['time'];
157
159
  if (typeof type !== 'string'
158
- || typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < 0
160
+ || typeof seq !== 'number' || !Number.isSafeInteger(seq) || seq < 0 || Object.is(seq, -0)
159
161
  || typeof time !== 'number' || !Number.isSafeInteger(time)
160
162
  || event['data'] === undefined
161
163
  || (event['ignorable'] !== undefined && event['ignorable'] !== true)) {
@@ -326,13 +328,15 @@ export class Session {
326
328
  }
327
329
  /**
328
330
  * Detached, deep-frozen creation metadata (format version, cwd, lineage,
329
- * seed boundary). Supplied by the store via `ctx.sessions.create()`. When a
331
+ * and whether fork history exists). Supplied by the store via `ctx.sessions.create()`. When a
330
332
  * `Session` is created without a store-owned header, a minimal header is
331
333
  * synthesized (stamped with the current {@link SESSION_FORMAT_VERSION}) so
332
334
  * `session.header` is always present. Kept out of the event log — it is a
333
335
  * storage concern, not replayable conversation state.
334
336
  */
335
337
  header;
338
+ /** Number of leading events inherited from this Session's fork parent. */
339
+ inheritedEventCount;
336
340
  /** The session identity, derived from its durable header's single copy. */
337
341
  get id() {
338
342
  return this.header.id;
@@ -343,9 +347,9 @@ export class Session {
343
347
  * construction — replay, fork, or resume — and were never published on the
344
348
  * `session/event` firehose (constructor seeds do not emit), so consumers
345
349
  * that replay the log as a publication substitute (telemetry adoption)
346
- * start here. Distinct from `header.seedLength`, the DURABLE fork-lineage
347
- * boundary: a resumed session's constructor seed is its full stored log,
348
- * while its header keeps the original fork value — this field is the
350
+ * start here. Distinct from {@link inheritedEventCount}, the DURABLE
351
+ * fork-lineage cut: a resumed session's constructor seed is its full stored
352
+ * log, while the inherited count keeps the original fork value — this field is the
349
353
  * in-process construction fact.
350
354
  *
351
355
  * Not persisted itself: a seeded session projects it into the log as the
@@ -366,10 +370,11 @@ export class Session {
366
370
  * @param id - session identity.
367
371
  * @param seed - optional borrowed replay or fork events.
368
372
  * @param header - optional borrowed storage metadata.
373
+ * @param inheritedEventCount - exact fork-inherited prefix length for a seeded header.
369
374
  * @returns a detached session.
370
375
  */
371
- static create(id, seed, header) {
372
- return new Session(id, seed, header);
376
+ static create(id, seed, header, inheritedEventCount) {
377
+ return new Session(id, seed, header, 'snapshot', inheritedEventCount);
373
378
  }
374
379
  /**
375
380
  * Restore a detached session by taking ownership of fresh persistence values.
@@ -378,12 +383,13 @@ export class Session {
378
383
  * @param id - restored session identity.
379
384
  * @param seed - fresh detached events whose ownership is transferred.
380
385
  * @param header - fresh detached metadata whose ownership is transferred.
386
+ * @param inheritedEventCount - exact fork-inherited prefix length decoded from storage.
381
387
  * @returns a restored detached session.
382
388
  */
383
- static fromRestore(id, seed, header) {
384
- return new Session(id, seed, header, 'restore');
389
+ static fromRestore(id, seed, header, inheritedEventCount) {
390
+ return new Session(id, seed, header, 'restore', inheritedEventCount);
385
391
  }
386
- constructor(id, seed, header, mode = 'snapshot') {
392
+ constructor(id, seed, header, mode = 'snapshot', suppliedInheritedEventCount) {
387
393
  const restoredHeader = mode === 'restore'
388
394
  ? validateRestoredSessionHeader(id, header)
389
395
  : undefined;
@@ -419,8 +425,22 @@ export class Session {
419
425
  this.log.push(mode === 'restore' ? freezeRestoredObject(snapshot) : deepFreeze(snapshot));
420
426
  }
421
427
  }
422
- this.firstLiveSeq = this.log.length;
428
+ this.firstLiveSeq = SessionLogOffset(this.log.length);
423
429
  this.header = restoredHeader ?? snapshotSessionHeader(id, header);
430
+ if (this.header.isSeeded && seed === undefined) {
431
+ throw new Error('seeded session requires an explicit constructor seed');
432
+ }
433
+ if (this.header.isSeeded && suppliedInheritedEventCount === undefined) {
434
+ throw new Error('seeded session requires an inherited event count');
435
+ }
436
+ const inheritedEventCount = SessionLogOffset(suppliedInheritedEventCount ?? 0);
437
+ if (!this.header.isSeeded && inheritedEventCount !== 0) {
438
+ throw new Error('unseeded session inherited event count must be 0');
439
+ }
440
+ if (inheritedEventCount > this.log.length) {
441
+ throw new Error('session inherited event count exceeds its event log');
442
+ }
443
+ this.inheritedEventCount = inheritedEventCount;
424
444
  // Appended here so the marker is already in `events` when a backend
425
445
  // captures the creation seed: no load-time write. Re-marking is skipped
426
446
  // because a cold session is resumed on first touch, so repeatedly opening
@@ -429,21 +449,49 @@ export class Session {
429
449
  this.append('session/end-seed', {});
430
450
  }
431
451
  }
432
- /** Cached immutable public snapshot of the private append-only log. */
452
+ /** Cached immutable full snapshot of the private append-only log. */
433
453
  eventsSnapshot;
434
454
  /**
435
- * An immutable snapshot of the append-only event log. The snapshot is reused
436
- * until the next append; a previously returned array does not grow later.
437
- * Events and their nested data are deep-frozen at acceptance, so neither a
438
- * cast nor ordinary JavaScript can rewrite durable history.
455
+ * Return the immutable event stored at one exact sequence number.
456
+ * @param seq - event sequence number.
457
+ * @returns the accepted event, or undefined when the log does not contain it.
458
+ */
459
+ eventAt(seq) {
460
+ return this.log[seq];
461
+ }
462
+ /**
463
+ * Materialize an immutable snapshot of a half-open event sequence range.
464
+ * A full current snapshot is reused until the next append; every previously
465
+ * returned snapshot remains stable after later appends.
466
+ * @param fromSeq - non-negative inclusive sequence number; defaults to the log start.
467
+ * @param toSeqExclusive - non-negative exclusive sequence number; defaults to the current end.
468
+ * @returns a frozen array of the selected deeply frozen events.
469
+ */
470
+ snapshotEvents(fromSeq = SessionLogOffset(0), toSeqExclusive = this.seq) {
471
+ if (fromSeq === 0 && toSeqExclusive === this.log.length) {
472
+ this.eventsSnapshot ??= Object.freeze([...this.log]);
473
+ return this.eventsSnapshot;
474
+ }
475
+ return Object.freeze(this.log.slice(fromSeq, toSeqExclusive));
476
+ }
477
+ /**
478
+ * Return this Session's events after its fork-inherited prefix.
479
+ * @returns a fresh array containing child-owned events in log order.
480
+ */
481
+ ownEvents() {
482
+ return this.snapshotEvents(this.inheritedEventCount);
483
+ }
484
+ /**
485
+ * Whether one existing event position is outside the fork-inherited prefix.
486
+ * @param seq - event position in this Session.
487
+ * @returns true when the event belongs to this Session rather than its parent.
439
488
  */
440
- get events() {
441
- this.eventsSnapshot ??= Object.freeze([...this.log]);
442
- return this.eventsSnapshot;
489
+ isOwnSeq(seq) {
490
+ return seq >= this.inheritedEventCount && seq < this.seq;
443
491
  }
444
492
  /** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
445
493
  get seq() {
446
- return this.log.length;
494
+ return SessionLogOffset(this.log.length);
447
495
  }
448
496
  /**
449
497
  * Append one typed event to the log and synchronously notify observers via
@@ -501,7 +549,7 @@ export class Session {
501
549
  }
502
550
  const event = deepFreeze({
503
551
  type,
504
- seq: this.log.length,
552
+ seq: SessionSeq(this.log.length),
505
553
  time: Date.now(),
506
554
  data: dataSnapshot,
507
555
  ...surfaceMetadataSnapshot,
@@ -538,7 +586,7 @@ export class Session {
538
586
  * The {@link EpochHeader} in force after the log's last header event — the
539
587
  * header the NEXT request will be compared against — or undefined before
540
588
  * the first `request/header` snapshot. The live, incrementally-maintained
541
- * form of `foldRequestHeader(session.events)`: each header event is folded
589
+ * form of `foldRequestHeader(session.snapshotEvents())`: each header event is folded
542
590
  * once, when first seen, so a per-step read costs O(new events).
543
591
  * @returns the folded header, or undefined when no header event exists yet.
544
592
  */
@@ -724,7 +772,7 @@ export class SessionStore extends Service {
724
772
  if (this.store.has(sessionId))
725
773
  throw new Error(`session "${sessionId}" already exists`);
726
774
  if (options?.seedSource === 'persistence') {
727
- return Session.fromRestore(sessionId, options.seed, options.meta);
775
+ return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount);
728
776
  }
729
777
  const seed = options?.seed;
730
778
  const meta = options?.meta;
@@ -734,12 +782,12 @@ export class SessionStore extends Service {
734
782
  createdAt: meta?.createdAt ?? Date.now(),
735
783
  ...meta?.cwd === undefined ? {} : { cwd: meta.cwd },
736
784
  ...meta?.parentSession === undefined ? {} : { parentSession: meta.parentSession },
737
- ...meta?.seedLength === undefined ? {} : { seedLength: meta.seedLength },
785
+ isSeeded: meta?.isSeeded ?? false,
738
786
  ...meta?.origin === undefined ? {} : { origin: meta.origin },
739
787
  ...meta?.delegationDepth === undefined ? {} : { delegationDepth: meta.delegationDepth },
740
788
  ...meta?.agentPreset === undefined ? {} : { agentPreset: meta.agentPreset },
741
789
  };
742
- return Session.create(sessionId, seed, header);
790
+ return Session.create(sessionId, seed, header, options?.inheritedEventCount);
743
791
  }
744
792
  /**
745
793
  * Enter a {@link prepare}d session into the store: install the module-private
@@ -941,16 +989,16 @@ export class SessionStore extends Service {
941
989
  const seed = this._forkSeed(liveSource, boundary);
942
990
  return this.create(childSessionId, {
943
991
  seed,
992
+ inheritedEventCount: SessionLogOffset(seed.length),
944
993
  meta: {
945
994
  ...liveSource.header.cwd !== undefined ? { cwd: liveSource.header.cwd } : {},
946
995
  parentSession: liveSource.id,
947
- seedLength: seed.length,
996
+ isSeeded: true,
948
997
  },
949
998
  });
950
999
  }
951
1000
  _forkSeed(session, requestedBoundary) {
952
- const events = session.events;
953
- const lastEvent = events.at(-1);
1001
+ const lastEvent = session.snapshotEvents().at(-1);
954
1002
  let boundary;
955
1003
  if (requestedBoundary !== undefined) {
956
1004
  boundary = requestedBoundary;
@@ -963,20 +1011,21 @@ export class SessionStore extends Service {
963
1011
  if (!Number.isSafeInteger(boundary) || boundary < 0) {
964
1012
  throw new SessionForkError(`fork boundary for session "${session.id}" must be a non-negative safe integer, got ${String(boundary)}`, 'INVALID_BOUNDARY');
965
1013
  }
966
- if (boundary >= events.length) {
967
- const lastSeq = events.at(-1)?.seq;
1014
+ if (boundary >= session.seq) {
1015
+ const lastSeq = lastEvent?.seq;
968
1016
  throw new SessionForkError(`fork boundary ${boundary} does not exist in session "${session.id}" (last seq: ${lastSeq ?? 'none'})`, 'INVALID_BOUNDARY');
969
1017
  }
970
- const boundaryEvent = events[boundary];
1018
+ const boundaryEvent = session.eventAt(boundary);
971
1019
  if (boundaryEvent === undefined || boundaryEvent.seq !== boundary) {
972
1020
  throw new SessionForkError(`fork boundary ${boundary} does not match a contiguous event seq in session "${session.id}"`, 'INVALID_BOUNDARY');
973
1021
  }
974
- const lastTurnBoundary = events.slice(0, boundary + 1)
1022
+ const events = session.snapshotEvents(SessionLogOffset(0), SessionLogOffset(boundary + 1));
1023
+ const lastTurnBoundary = events
975
1024
  .findLast(event => event.type === 'turn/start' || event.type === 'turn/end');
976
1025
  if (lastTurnBoundary?.type === 'turn/start') {
977
1026
  throw new SessionForkError(`fork boundary ${boundary} in session "${session.id}" ends inside open turn ${lastTurnBoundary.data.turn}`, 'OPEN_TURN');
978
1027
  }
979
- return events.slice(0, boundary + 1);
1028
+ return events;
980
1029
  }
981
1030
  _resolveForkSource(source) {
982
1031
  if (typeof source === 'string') {
@@ -159,7 +159,7 @@ const install = Object.assign((ctx, fail) => {
159
159
  const seedSession = (session) => {
160
160
  const trace = freshTrace();
161
161
  traces.set(session, trace);
162
- for (const event of session.events) {
162
+ for (const event of session.snapshotEvents()) {
163
163
  applyTransition(trace, validateEvent(trace, event, fail));
164
164
  }
165
165
  return trace;
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { brandString } from '@deepseek-ai/dsh-brand';
8
8
  import { deepFreeze } from '@deepseek-ai/dsh-util-values';
9
+ import { SessionSeq } from "./types.js";
9
10
  /** Recovery code for an assistant tool request that never reached a recorded call start. */
10
11
  export const TOOL_NOT_STARTED = 'TOOL_NOT_STARTED';
11
12
  /** Recovery code for a recorded tool call whose completed outcome was not durably recorded. */
@@ -102,7 +103,7 @@ export function interruptedTurnClosers(events) {
102
103
  });
103
104
  closers.push({
104
105
  type: 'tool/result',
105
- seq: seq++,
106
+ seq: SessionSeq(seq++),
106
107
  time,
107
108
  data: {
108
109
  turn: openTurn,
@@ -119,9 +120,9 @@ export function interruptedTurnClosers(events) {
119
120
  // Close an open step next — a turn/end while a step is open is an invariant
120
121
  // violation, so the step's boundary must be synthesized before the turn's.
121
122
  if (openStep !== null) {
122
- closers.push({ type: 'step/end', seq: seq++, time, data: { turn: openTurn, step: openStep } });
123
+ closers.push({ type: 'step/end', seq: SessionSeq(seq++), time, data: { turn: openTurn, step: openStep } });
123
124
  }
124
- closers.push({ type: 'turn/end', seq: seq++, time, data: { turn: openTurn, reason: { kind: 'interrupted' } } });
125
+ closers.push({ type: 'turn/end', seq: SessionSeq(seq++), time, data: { turn: openTurn, reason: { kind: 'interrupted' } } });
125
126
  return closers;
126
127
  }
127
128
  //# sourceMappingURL=repair.js.map
@@ -1,4 +1,5 @@
1
1
  /** Lossless range encoding for JSONL `sourceEventSeqs` arrays. */
2
+ import type { SessionSeq as SessionSeqType } from './types.ts';
2
3
  /** A stored source sequence or inclusive consecutive range. */
3
4
  export type EncodedSeq = number | [number, number];
4
5
  /**
@@ -6,12 +7,12 @@ export type EncodedSeq = number | [number, number];
6
7
  * @param values - validated in-memory source sequences.
7
8
  * @returns a lossless JSON storage form.
8
9
  */
9
- export declare function encodeSeqRanges(values: readonly number[]): EncodedSeq[];
10
+ export declare function encodeSeqRanges(values: readonly SessionSeqType[]): EncodedSeq[];
10
11
  /**
11
12
  * Expand a JSON storage-form source sequence array.
12
13
  * @param value - parsed storage value.
13
14
  * @param maxEntries - largest list permitted by the owning event.
14
15
  * @returns the in-memory source sequences.
15
16
  */
16
- export declare function decodeSeqRanges(value: unknown, maxEntries?: number): number[];
17
+ export declare function decodeSeqRanges(value: unknown, maxEntries?: number): SessionSeqType[];
17
18
  //# sourceMappingURL=seq-ranges.d.ts.map
@@ -1,4 +1,5 @@
1
1
  /** Lossless range encoding for JSONL `sourceEventSeqs` arrays. */
2
+ import { SessionSeq } from "./types.js";
2
3
  function isStrictlyIncreasing(values) {
3
4
  return values.every((value, index) => index === 0 || value > values[index - 1]);
4
5
  }
@@ -40,7 +41,7 @@ export function decodeSeqRanges(value, maxEntries = Number.MAX_SAFE_INTEGER) {
40
41
  assertSeq(entry);
41
42
  if (decoded.length >= maxEntries)
42
43
  throw new TypeError('sourceEventSeqs exceeds its event sequence');
43
- decoded.push(entry);
44
+ decoded.push(SessionSeq(entry));
44
45
  continue;
45
46
  }
46
47
  if (!Array.isArray(entry) || entry.length !== 2) {
@@ -57,7 +58,7 @@ export function decodeSeqRanges(value, maxEntries = Number.MAX_SAFE_INTEGER) {
57
58
  throw new TypeError('sourceEventSeqs range exceeds its event sequence');
58
59
  }
59
60
  for (let seq = start; seq <= end; seq += 1)
60
- decoded.push(seq);
61
+ decoded.push(SessionSeq(seq));
61
62
  hasRange = true;
62
63
  }
63
64
  if (hasRange && !isStrictlyIncreasing(decoded)) {
@@ -8,6 +8,7 @@
8
8
  * @module @deepseek-ai/dsh-session/surface
9
9
  */
10
10
  import type { Message } from '@deepseek-ai/dsh-llm';
11
+ import { SessionLogOffset, SessionSeq } from './types.ts';
11
12
  import type { SessionEvent, SurfaceEvent, SurfaceOp } from './types.ts';
12
13
  /**
13
14
  * Whether an event type can join the model-visible surface.
@@ -64,25 +65,25 @@ export declare function deriveEventMessage(event: SessionEvent): Message | null;
64
65
  /** One replacement operation observed while folding a session surface. */
65
66
  export interface SurfaceFoldReplacement {
66
67
  /** Seq of the event that replaced the prior surface range. */
67
- seq: number;
68
+ seq: SessionSeq;
68
69
  /** Declared inclusive start seq of the replaced surface range. */
69
- start: number;
70
+ start: SessionSeq;
70
71
  /** Declared inclusive end seq of the replaced surface range. */
71
- end: number;
72
+ end: SessionSeq;
72
73
  /** Actual surface entries removed by the operation, in surface order. */
73
- shadowedSeqs: number[];
74
+ shadowedSeqs: SessionSeq[];
74
75
  }
75
76
  /** Complete result of replaying the surface operations in a session log. */
76
77
  export interface SurfaceFoldResult {
77
78
  /** Current surface event sequences in model-visible order. */
78
- nodes: number[];
79
+ nodes: SessionSeq[];
79
80
  /** Replacement operations in event order. */
80
81
  replacements: SurfaceFoldReplacement[];
81
82
  }
82
83
  /** Readonly live projection of the message-producing session events. */
83
84
  export interface SessionSurface {
84
85
  /** Current surface event sequences in model-visible order. */
85
- readonly nodes: readonly number[];
86
+ readonly nodes: readonly SessionSeq[];
86
87
  /** Monotonic count of committed positional replacements. */
87
88
  readonly replaceGeneration: number;
88
89
  }
@@ -107,7 +108,7 @@ export declare class SurfaceManager implements SessionSurface {
107
108
  * @param log - Contiguous complete log or loaded event window.
108
109
  * @param baseSeq - Absolute sequence of the window's first event.
109
110
  */
110
- constructor(log: readonly SessionEvent[], baseSeq?: number);
111
+ constructor(log: readonly SessionEvent[], baseSeq?: SessionLogOffset);
111
112
  /**
112
113
  * Validate the next candidate without mutating the committed surface.
113
114
  * @param event - candidate event that has not entered the log yet.
@@ -116,7 +117,7 @@ export declare class SurfaceManager implements SessionSurface {
116
117
  /** Monotonic count of folded positional replacements. */
117
118
  get replaceGeneration(): number;
118
119
  /** Surface event sequences in model-visible order. */
119
- get nodes(): readonly number[];
120
+ get nodes(): readonly SessionSeq[];
120
121
  /** Fold events appended since the previous access. */
121
122
  private _processDelta;
122
123
  }
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * @module @deepseek-ai/dsh-session/surface
9
9
  */
10
+ import { SessionLogOffset, SessionSeq } from "./types.js";
10
11
  /** Runtime counterpart of the message-producing event union. */
11
12
  const SURFACE_EVENT_TYPES = new Set([
12
13
  'user/message',
@@ -107,7 +108,10 @@ function createFoldState() {
107
108
  }
108
109
  /** Whether a runtime value is a non-negative safe event sequence. */
109
110
  function isEventSeq(value) {
110
- return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
111
+ return typeof value === 'number'
112
+ && Number.isSafeInteger(value)
113
+ && value >= 0
114
+ && !Object.is(value, -0);
111
115
  }
112
116
  /** Whether a runtime value is the exact positional-replacement shape. */
113
117
  function isReplaceOp(value) {
@@ -303,7 +307,7 @@ export function foldSurface(events) {
303
307
  const state = createFoldState();
304
308
  const replacements = [];
305
309
  for (const [index, event] of events.entries()) {
306
- const replacement = applySurfaceEvent(state, event, index, events, 0);
310
+ const replacement = applySurfaceEvent(state, event, SessionSeq(index), events, SessionLogOffset(0));
307
311
  if (replacement !== undefined)
308
312
  replacements.push(replacement);
309
313
  }
@@ -323,10 +327,10 @@ export class SurfaceManager {
323
327
  * @param log - Contiguous complete log or loaded event window.
324
328
  * @param baseSeq - Absolute sequence of the window's first event.
325
329
  */
326
- constructor(log, baseSeq = 0) {
330
+ constructor(log, baseSeq = SessionLogOffset(0)) {
327
331
  this.log = log;
328
332
  this.baseSeq = baseSeq;
329
- this._lastProcessedSeq = baseSeq - 1;
333
+ this._lastProcessedSeq = baseSeq === 0 ? -1 : SessionSeq(baseSeq - 1);
330
334
  }
331
335
  /**
332
336
  * Validate the next candidate without mutating the committed surface.
@@ -335,7 +339,7 @@ export class SurfaceManager {
335
339
  validateNext(event) {
336
340
  if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1)
337
341
  this._processDelta();
338
- const expectedSeq = this.baseSeq + this.log.length;
342
+ const expectedSeq = SessionSeq(this.baseSeq + this.log.length);
339
343
  this._pendingPlan = {
340
344
  event,
341
345
  expectedSeq,
@@ -366,11 +370,11 @@ export class SurfaceManager {
366
370
  applySurfacePlan(this._state, pending.plan);
367
371
  }
368
372
  else {
369
- applySurfaceEvent(this._state, event, seq, this.log, this.baseSeq);
373
+ applySurfaceEvent(this._state, event, SessionSeq(seq), this.log, this.baseSeq);
370
374
  }
371
375
  if (pending !== undefined && pending.expectedSeq <= seq)
372
376
  this._pendingPlan = undefined;
373
- this._lastProcessedSeq = seq;
377
+ this._lastProcessedSeq = SessionSeq(seq);
374
378
  }
375
379
  }
376
380
  }