@deepseek-ai/dsh-api-session-controller 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.
@@ -1,5 +1,6 @@
1
1
  // Sessions remain resident after creation so their open Remote sources keep running off-screen.
2
2
  import { randomUUID } from '@deepseek-ai/dsh-util-crypto';
3
+ import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session/types';
3
4
  import { SessionEventStream } from "../transport.js";
4
5
  import { MutableSessionEventSource } from "../contract/events.js";
5
6
  import { Notifier } from "./notifier.js";
@@ -7,8 +8,16 @@ import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client';
7
8
  import { ProjectionValueStore } from "./projection-store.js";
8
9
  import { resolvedClientTimeZone } from "../time-zone.js";
9
10
  import { SessionQueueMirror } from "./queue-mirror.js";
11
+ function projectionsBaseline(value) {
12
+ return {
13
+ ...value,
14
+ asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq),
15
+ };
16
+ }
10
17
  /** Messages requested per history page. */
11
18
  export const PAGE_MESSAGES = 50;
19
+ /** Messages requested per page while a turn jump loops backwards (fewer, larger round trips). */
20
+ export const JUMP_PAGE_MESSAGES = 200;
12
21
  /**
13
22
  * Owns a session's event window, lifecycle state, and observable
14
23
  * snapshot. React bindings remain outside this data layer. Features see only
@@ -20,7 +29,7 @@ export class Session {
20
29
  remote;
21
30
  options;
22
31
  // ---- Window and derived state (all private; the snapshot is the only read API) ----
23
- baseSeq = 0;
32
+ baseSeq = SessionLogOffset(0);
24
33
  hasMore = false;
25
34
  openState = 'cold';
26
35
  openError = null;
@@ -29,6 +38,10 @@ export class Session {
29
38
  * passes drop all writes once the generation moves on. */
30
39
  openGeneration = 0;
31
40
  loadingOlder = false;
41
+ /** Shared low-water target of the running jump loop; null when no jump is paging. */
42
+ jumpTargetSeq = null;
43
+ /** The running jump loop's completion, shared by retargeting callers. */
44
+ jumpPromise = null;
32
45
  /** Authoritative stream-only inbox snapshot; pending work never hits history. */
33
46
  queueMirror = new SessionQueueMirror();
34
47
  running = false;
@@ -125,6 +138,9 @@ export class Session {
125
138
  const requestId = randomUUID();
126
139
  this.pendingSubmissions = [...this.pendingSubmissions, {
127
140
  requestId,
141
+ placement: this.running
142
+ ? input.mode === 'steer' ? 'steering' : 'queued'
143
+ : 'transcript',
128
144
  time: Date.now(),
129
145
  text: input.text,
130
146
  images: input.images,
@@ -247,9 +263,11 @@ export class Session {
247
263
  */
248
264
  async rename(title) {
249
265
  const result = await this.remote.session.rename({ sessionId: this.sessionId, title });
250
- if (result.ok)
251
- this.projections.apply('title', result.value.title, result.value.seq);
252
- return result;
266
+ if (!result.ok)
267
+ return result;
268
+ const seq = SessionSeq(result.value.seq);
269
+ this.projections.apply('title', result.value.title, seq);
270
+ return { ok: true, value: { title: result.value.title, seq } };
253
271
  }
254
272
  /**
255
273
  * Execute one slash-command line against this session's agent — pure
@@ -300,6 +318,58 @@ export class Session {
300
318
  this.notifier.markDirty();
301
319
  }
302
320
  }
321
+ /** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
322
+ loadThrough(seq) {
323
+ if (this.openState !== 'open' || !this.hasMore || this.baseSeq <= seq)
324
+ return Promise.resolve();
325
+ if (this.jumpPromise !== null) {
326
+ // Retarget the running loop to the lowest requested seq.
327
+ this.jumpTargetSeq = SessionSeq(Math.min(this.jumpTargetSeq ?? seq, seq));
328
+ return this.jumpPromise;
329
+ }
330
+ // A plain single-page pull owns the busy flag; the jump does not queue
331
+ // behind it (the caller retries once it settles) and must leave no
332
+ // target behind — only the loop's finally clears that field, and no
333
+ // loop starts here.
334
+ if (this.loadingOlder)
335
+ return Promise.resolve();
336
+ this.jumpTargetSeq = seq;
337
+ this.loadingOlder = true;
338
+ this.notifier.markDirty();
339
+ // Stale-pass guard (the doOpen pattern): a resync mid-loop replaces the
340
+ // stream generation; this pass then stops instead of paging the new
341
+ // generation toward its old target.
342
+ const generation = this.openGeneration;
343
+ this.jumpPromise = (async () => {
344
+ try {
345
+ while (this.hasMore && this.jumpTargetSeq !== null && this.baseSeq > this.jumpTargetSeq) {
346
+ if (generation !== this.openGeneration)
347
+ return;
348
+ const events = this.events;
349
+ if (events === undefined)
350
+ return;
351
+ const before = this.baseSeq;
352
+ await events.prepend({ beforeSeq: this.baseSeq, maxMessages: JUMP_PAGE_MESSAGES });
353
+ // No-progress guard: an empty or dropped page that still claims more
354
+ // history must end the loop, not spin it.
355
+ if (this.baseSeq >= before)
356
+ return;
357
+ }
358
+ }
359
+ catch (error) {
360
+ if (!isRemoteFailure(error)) {
361
+ console.error('[session-controller] loadThrough failed:', error);
362
+ }
363
+ }
364
+ finally {
365
+ this.jumpTargetSeq = null;
366
+ this.jumpPromise = null;
367
+ this.loadingOlder = false;
368
+ this.notifier.markDirty();
369
+ }
370
+ })();
371
+ return this.jumpPromise;
372
+ }
303
373
  /** Rebuild an opened history source after address replacement.
304
374
  * Invalidates any in-flight open first; queue state belongs to the independently
305
375
  * reconnecting control stream and remains untouched. */
@@ -313,7 +383,7 @@ export class Session {
313
383
  this.openPromise = null;
314
384
  this.openState = 'cold';
315
385
  this.openError = null;
316
- this.baseSeq = 0;
386
+ this.baseSeq = SessionLogOffset(0);
317
387
  this.notifier.markDirty();
318
388
  await this.open();
319
389
  }
@@ -483,7 +553,7 @@ export class Session {
483
553
  acceptEventChange(change) {
484
554
  switch (change.type) {
485
555
  case 'replace':
486
- this.installWindow(change.entries, change.hasMore, change.page.projections);
556
+ this.installWindow(change.entries, change.hasMore, change.page.projections === undefined ? undefined : projectionsBaseline(change.page.projections));
487
557
  return;
488
558
  case 'prepend':
489
559
  this.prependWindow(change.entries, change.hasMore);
@@ -495,7 +565,7 @@ export class Session {
495
565
  }
496
566
  /** Replace the complete contiguous window and apply page-owned projection metadata. */
497
567
  installWindow(entries, hasMore, projections) {
498
- this.baseSeq = entries[0]?.event.seq ?? 0;
568
+ this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
499
569
  this.hasMore = hasMore;
500
570
  if (entries.some(entry => entry.event.type === 'turn/start'))
501
571
  this.firstPromptPendingTurn = false;
@@ -508,7 +578,7 @@ export class Session {
508
578
  }
509
579
  /** Prepend one stream-validated history page. */
510
580
  prependWindow(entries, hasMore) {
511
- this.baseSeq = entries[0]?.event.seq ?? this.baseSeq;
581
+ this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
512
582
  this.hasMore = hasMore;
513
583
  this.eventSource.prepend(entries, hasMore);
514
584
  }
@@ -53,8 +53,9 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
53
53
  });
54
54
  import { randomUUID } from 'node:crypto';
55
55
  import { brandString } from '@deepseek-ai/dsh-brand';
56
- import { AttachmentError, admitEncodedImages } from '@deepseek-ai/dsh-attachment';
56
+ import { AttachmentError, admitPromptContent } from '@deepseek-ai/dsh-attachment';
57
57
  import { ReasoningEffortId, createUserMessage, freezeMessage, } from '@deepseek-ai/dsh-llm';
58
+ import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session';
58
59
  import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
59
60
  import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title';
60
61
  import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time';
@@ -182,9 +183,12 @@ export class SessionCommandController {
182
183
  async fork(request) {
183
184
  const env_1 = { stack: [], error: void 0, hasError: false };
184
185
  try {
185
- if (request.atSeq !== undefined
186
- && (!Number.isInteger(request.atSeq) || request.atSeq < 0)) {
187
- throw new RemoteError('gateway/bad-request', 'atSeq must be a non-negative integer', {});
186
+ let atSeq;
187
+ try {
188
+ atSeq = request.atSeq === undefined ? undefined : SessionSeq(request.atSeq);
189
+ }
190
+ catch {
191
+ throw new RemoteError('gateway/bad-request', 'atSeq must be a non-negative safe integer', {});
188
192
  }
189
193
  let observed;
190
194
  try {
@@ -201,7 +205,6 @@ export class SessionCommandController {
201
205
  }
202
206
  const source = __addDisposableResource(env_1, observed, false);
203
207
  const lastSeq = source.events.at(-1)?.seq ?? -1;
204
- const atSeq = request.atSeq;
205
208
  const anchoredBoundary = atSeq === undefined
206
209
  ? undefined
207
210
  : source.events.find(event => event.type === 'turn/end' && event.seq >= atSeq);
@@ -214,9 +217,10 @@ export class SessionCommandController {
214
217
  ? `session "${request.sessionId}" has not completed the turn containing event ${String(atSeq)}`
215
218
  : `session "${request.sessionId}" has no completed turn to fork from`, { sessionId: request.sessionId });
216
219
  }
217
- let cut = boundary.seq + 1;
218
- while (cut < source.events.length && source.events[cut]?.type !== 'turn/start')
219
- cut++;
220
+ let cut = SessionLogOffset(boundary.seq + 1);
221
+ while (cut < source.events.length && source.events[cut]?.type !== 'turn/start') {
222
+ cut = SessionLogOffset(cut + 1);
223
+ }
220
224
  let workspace;
221
225
  try {
222
226
  workspace = await this.forkWorkspace(source.header);
@@ -231,10 +235,11 @@ export class SessionCommandController {
231
235
  await this.ctx.agents.create({
232
236
  sessionId: childId,
233
237
  seed: source.events.slice(0, cut),
238
+ inheritedEventCount: cut,
234
239
  meta: {
235
240
  ...(source.header.cwd === undefined ? {} : { cwd: source.header.cwd }),
236
241
  parentSession: source.header.id,
237
- seedLength: cut,
242
+ isSeeded: true,
238
243
  ...(composition.agentPreset === undefined
239
244
  ? {}
240
245
  : { agentPreset: composition.agentPreset }),
@@ -296,7 +301,7 @@ export class SessionCommandController {
296
301
  throw new RemoteError('session/attachment-invalid', `Model "${current.model}" does not support image input.`, { reason: 'MODEL_DOES_NOT_SUPPORT_IMAGES' });
297
302
  }
298
303
  }
299
- const content = await durablePromptContent(this.ctx, request.content);
304
+ const content = await admitPromptContent(this.ctx.attachments, request.content);
300
305
  const message = createUserMessage({ content, source });
301
306
  if (request.mode === 'steer')
302
307
  agent.steer(message);
@@ -438,7 +443,7 @@ export class SessionCommandController {
438
443
  async readSessionState(sessionId) {
439
444
  const attached = this.ctx.sessions.get(sessionId);
440
445
  if (attached !== undefined) {
441
- return { id: attached.id, header: attached.header, events: [...attached.events] };
446
+ return { id: attached.id, header: attached.header, events: attached.snapshotEvents() };
442
447
  }
443
448
  const inspected = await inspectApiSession(this.ctx, sessionId);
444
449
  return { id: inspected.meta.id, header: inspected.meta, events: inspected.events };
@@ -457,17 +462,6 @@ export class SessionCommandController {
457
462
  return undefined;
458
463
  }
459
464
  }
460
- async function durablePromptContent(ctx, content) {
461
- if (content.every(part => part.type === 'text')) {
462
- return content.map(part => ({ type: 'text', text: part.text }));
463
- }
464
- const refs = await admitEncodedImages(ctx.attachments, content.filter(part => part.type === 'image'));
465
- let next = 0;
466
- return content.map(part => part.type === 'text'
467
- ? { type: 'text', text: part.text }
468
- // admitEncodedImages returns one reference per image part in order.
469
- : { type: 'image', attachment: refs[next++] });
470
- }
471
465
  function imageBlockIn(content, match) {
472
466
  if (!Array.isArray(content))
473
467
  return undefined;
@@ -52,7 +52,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
52
52
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
53
53
  });
54
54
  import { Deque } from '@deepseek-ai/dsh-deque';
55
- import { isAppendSurfaceEvent } from '@deepseek-ai/dsh-session';
55
+ import { isAppendSurfaceEvent, SessionLogOffset, SessionSeq, } from '@deepseek-ai/dsh-session';
56
56
  import { isChunkRow, packChunkRuns } from '@deepseek-ai/dsh-session/chunk-rows';
57
57
  import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
58
58
  import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
@@ -86,18 +86,24 @@ export class SessionHistoryController {
86
86
  const env_1 = { stack: [], error: void 0, hasError: false };
87
87
  try {
88
88
  validatePageRequest(request);
89
+ const throughSeq = request.throughSeq === -1
90
+ ? -1
91
+ : SessionSeq(request.throughSeq);
92
+ const beforeSeq = request.beforeSeq === undefined
93
+ ? undefined
94
+ : SessionLogOffset(request.beforeSeq);
89
95
  const source = __addDisposableResource(env_1, await this.sourceFor(request.address, signal, false), false);
90
96
  signal.throwIfAborted();
91
97
  const sourceLog = source.events;
92
98
  const sourceCursor = sourceLog.at(-1)?.seq ?? -1;
93
- if (request.throughSeq > sourceCursor) {
94
- throw new RemoteError('gateway/bad-request', `session page through seq ${String(request.throughSeq)} is past cursor ${String(sourceCursor)}`, {});
99
+ if (throughSeq > sourceCursor) {
100
+ throw new RemoteError('gateway/bad-request', `session page through seq ${String(throughSeq)} is past cursor ${String(sourceCursor)}`, {});
95
101
  }
96
102
  /* v8 ignore next -- Session and persistence validation guarantee a dense zero-based event prefix. */
97
- if (request.throughSeq >= 0 && sourceLog[request.throughSeq]?.seq !== request.throughSeq) {
98
- throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(request.throughSeq)}`, {});
103
+ if (throughSeq >= 0 && sourceLog[throughSeq]?.seq !== throughSeq) {
104
+ throw new RemoteError('gateway/internal', `session log does not contain through seq ${String(throughSeq)}`, {});
99
105
  }
100
- const page = paginate(sourceLog, request.beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, request.throughSeq);
106
+ const page = paginate(sourceLog, beforeSeq, request.maxMessages ?? DEFAULT_MAX_MESSAGES, throughSeq);
101
107
  const records = pageRecords(page.events);
102
108
  return {
103
109
  records,
@@ -148,9 +154,9 @@ export class SessionHistoryController {
148
154
  // Constructor seed events have no session/event notification. Normally
149
155
  // only the end-seed suffix is new; if persistence advanced after the
150
156
  // opening observation, replay everything beyond that snapshot cursor.
151
- const suffix = session.events.slice(snapshotCursor === undefined
157
+ const suffix = session.snapshotEvents(snapshotCursor === undefined
152
158
  ? session.firstLiveSeq
153
- : snapshotCursor + 1);
159
+ : SessionLogOffset(snapshotCursor + 1));
154
160
  for (let index = suffix.length - 1; index >= 0; index -= 1) {
155
161
  buffered.pushFront(suffix[index]);
156
162
  }
@@ -169,7 +175,7 @@ export class SessionHistoryController {
169
175
  const page = paginate(events, undefined, request.maxMessages ?? DEFAULT_MAX_MESSAGES);
170
176
  yield {
171
177
  type: 'snapshot',
172
- header: source.header,
178
+ header: wireHeader(source.header, source.inheritedEventCount),
173
179
  cursor,
174
180
  records: pageRecords(page.events),
175
181
  hasMore: page.hasMore,
@@ -187,19 +193,20 @@ export class SessionHistoryController {
187
193
  throw error;
188
194
  }
189
195
  }
190
- let nextSeq = cursor + 1;
196
+ let nextOffset = SessionLogOffset(cursor + 1);
191
197
  while (!follower.closed && !signal.aborted) {
192
198
  const item = buffered.popFront();
193
199
  if (item === undefined) {
194
200
  await new Promise((resolve) => { wake = resolve; });
195
201
  continue;
196
202
  }
197
- if (item.seq < nextSeq)
203
+ const expectedSeq = SessionSeq(nextOffset);
204
+ if (item.seq < expectedSeq)
198
205
  continue;
199
- if (item.seq !== nextSeq) {
200
- throw new RemoteError('gateway/internal', `session event stream skipped seq ${String(nextSeq)}`, {});
206
+ if (item.seq !== expectedSeq) {
207
+ throw new RemoteError('gateway/internal', `session event stream skipped seq ${String(expectedSeq)}`, {});
201
208
  }
202
- nextSeq++;
209
+ nextOffset = SessionLogOffset(nextOffset + 1);
203
210
  yield entryFor(item);
204
211
  }
205
212
  }
@@ -230,7 +237,7 @@ export class SessionHistoryController {
230
237
  rejectNotFound(address);
231
238
  }
232
239
  try {
233
- validateAddress(address, observation.header, observation.projections);
240
+ validateAddress(address, observation.header, observation.inheritedEventCount, observation.projections);
234
241
  }
235
242
  catch (error) {
236
243
  observation[Symbol.dispose]();
@@ -254,11 +261,15 @@ function projectionBlock(snapshot) {
254
261
  };
255
262
  }
256
263
  function validatePageRequest(request) {
257
- if (!Number.isSafeInteger(request.throughSeq) || request.throughSeq < -1) {
264
+ if (!Number.isSafeInteger(request.throughSeq)
265
+ || request.throughSeq < -1
266
+ || Object.is(request.throughSeq, -0)) {
258
267
  throw new RemoteError('gateway/bad-request', 'throughSeq must be an integer greater than or equal to -1', {});
259
268
  }
260
269
  if (request.beforeSeq !== undefined
261
- && (!Number.isSafeInteger(request.beforeSeq) || request.beforeSeq < 0)) {
270
+ && (!Number.isSafeInteger(request.beforeSeq)
271
+ || request.beforeSeq < 0
272
+ || Object.is(request.beforeSeq, -0))) {
262
273
  throw new RemoteError('gateway/bad-request', 'beforeSeq must be a non-negative safe integer', {});
263
274
  }
264
275
  if (request.maxMessages !== undefined
@@ -275,7 +286,7 @@ function validateFollowRequest(request) {
275
286
  function addressId(address) {
276
287
  return address.kind === 'session' ? address.sessionId : address.childSessionId;
277
288
  }
278
- function validateAddress(address, header, projections) {
289
+ function validateAddress(address, header, inheritedEventCount, projections) {
279
290
  if (address.kind === 'session') {
280
291
  if (header.origin === 'subagent') {
281
292
  throw new RemoteError('session/agent-busy', 'subagent Sessions require their durable parent address', {
@@ -297,7 +308,7 @@ function validateAddress(address, header, projections) {
297
308
  reason: 'corrupt',
298
309
  });
299
310
  }
300
- if (identity === undefined || identity.seq < (header.seedLength ?? 0)) {
311
+ if (identity === undefined || identity.seq < inheritedEventCount) {
301
312
  throw new RemoteError('subagent/catalog-diagnostic', 'subagent descriptor is unavailable', {
302
313
  parentSessionId: address.parentSessionId,
303
314
  childSessionId: address.childSessionId,
@@ -320,9 +331,9 @@ function rejectNotFound(address) {
320
331
  });
321
332
  }
322
333
  function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.seq ?? -1) {
323
- const end = Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1);
334
+ const end = SessionLogOffset(Math.min(throughSeq + 1, beforeSeq ?? throughSeq + 1));
324
335
  let count = 0;
325
- let cut = 0;
336
+ let cut = SessionLogOffset(0);
326
337
  for (let index = end - 1; index >= 0; index--) {
327
338
  const event = events[index];
328
339
  if (!MESSAGE_TYPES.has(event.type) || !isAppendSurfaceEvent(event))
@@ -331,16 +342,26 @@ function paginate(events, beforeSeq, maxMessages, throughSeq = events.at(-1)?.se
331
342
  const sources = event.sourceEventSeqs;
332
343
  let groupStart = event.seq;
333
344
  if (sources !== undefined) {
334
- for (const source of sources)
335
- groupStart = Math.min(groupStart, source);
345
+ for (const source of sources) {
346
+ if (source < groupStart)
347
+ groupStart = source;
348
+ }
336
349
  }
337
350
  if (count >= maxMessages) {
338
- cut = groupStart;
351
+ cut = SessionLogOffset(groupStart);
339
352
  break;
340
353
  }
341
354
  }
342
355
  return { events: events.slice(cut, end), hasMore: cut > 0 };
343
356
  }
357
+ /** Translate logical Session metadata to the unchanged v0 browser wire. */
358
+ function wireHeader(header, inheritedEventCount) {
359
+ const { isSeeded, ...wire } = header;
360
+ return {
361
+ ...wire,
362
+ ...isSeeded ? { seedLength: inheritedEventCount } : {},
363
+ };
364
+ }
344
365
  function entryFor(event) {
345
366
  return {
346
367
  type: 'event',
@@ -1,7 +1,8 @@
1
1
  /** Session Remote owner: cold reads, explicit Agent commands, and live control state. */
2
2
  import { Context } from '@deepseek-ai/cordis';
3
3
  import z from '@deepseek-ai/schemastery';
4
- import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session';
4
+ import type { SessionId } from '@deepseek-ai/dsh-session';
5
+ import type { SessionInspection } from '@deepseek-ai/dsh-session-persistence';
5
6
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
6
7
  import { type ApiSessionAgentResult } from './agent.ts';
7
8
  import { buildModelCatalog } from './catalog.ts';
@@ -60,10 +61,7 @@ export declare class SessionController extends TypertRemoteService {
60
61
  * @param signal - optional caller cancellation for persistence reads.
61
62
  * @returns the current attached state or persisted header and event prefix.
62
63
  */
63
- inspect(sessionId: SessionId, signal?: AbortSignal): Promise<{
64
- meta: SessionHeader;
65
- events: SessionEvent[];
66
- }>;
64
+ inspect(sessionId: SessionId, signal?: AbortSignal): Promise<SessionInspection>;
67
65
  /**
68
66
  * Read all visible Session rows without resuming an Agent.
69
67
  * @param _request - reserved empty list request.
@@ -266,7 +266,11 @@ let SessionController = (() => {
266
266
  inspect(sessionId, signal) {
267
267
  const attached = this.ctx.sessions.get(sessionId);
268
268
  if (attached !== undefined) {
269
- return Promise.resolve({ meta: attached.header, events: [...attached.events] });
269
+ return Promise.resolve({
270
+ meta: attached.header,
271
+ inheritedEventCount: attached.inheritedEventCount,
272
+ events: attached.snapshotEvents(),
273
+ });
270
274
  }
271
275
  return inspectApiSession(this.ctx, sessionId, signal);
272
276
  }
package/lib/types/list.js CHANGED
@@ -52,6 +52,7 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
52
52
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
53
53
  });
54
54
  import { stat } from 'node:fs/promises';
55
+ import { SessionLogOffset } from '@deepseek-ai/dsh-session';
55
56
  import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
56
57
  import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
57
58
  import { z } from 'zod';
@@ -362,7 +363,9 @@ export class ApiSessionList {
362
363
  projectionsFor(header, session) {
363
364
  try {
364
365
  const block = session === undefined
365
- ? this.ctx.get('sessionProjectionCache')?.cachedSnapshot(header)
366
+ ? header.isSeeded
367
+ ? undefined
368
+ : this.ctx.get('sessionProjectionCache')?.cachedSnapshot(header, SessionLogOffset(0))
366
369
  : this.ctx.sessionProjections.cachedSnapshot(session);
367
370
  return block !== undefined && Object.keys(block.values).length > 0
368
371
  ? {
@@ -4,7 +4,7 @@ import type { Branded } from '@deepseek-ai/dsh-brand';
4
4
  import type { MessageId } from '@deepseek-ai/dsh-llm/brand';
5
5
  import type { ContentBlock } from '@deepseek-ai/dsh-llm/types';
6
6
  import type { ChunkRow } from '@deepseek-ai/dsh-session/chunk-rows';
7
- import type { SessionHeader, SessionId, SurfaceOp } from '@deepseek-ai/dsh-session/types';
7
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
8
8
  import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types';
9
9
  import type { JobId } from '@deepseek-ai/dsh-jobs/brand';
10
10
  import type { JsonValue } from '@deepseek-ai/dsh-util-values';
@@ -358,6 +358,25 @@ export interface SessionEventEntry {
358
358
  readonly type: 'event';
359
359
  readonly event: SessionWireEvent;
360
360
  }
361
+ /** v0-compatible Session metadata carried on the browser wire. */
362
+ export interface SessionWireHeader {
363
+ readonly version: number;
364
+ readonly id: SessionId;
365
+ readonly createdAt: number;
366
+ readonly cwd?: string;
367
+ readonly parentSession?: SessionId;
368
+ /** Exact inherited prefix length; absent for an unseeded Session. */
369
+ readonly seedLength?: number;
370
+ readonly origin?: 'subagent';
371
+ readonly delegationDepth?: number;
372
+ readonly agentPreset?: string;
373
+ }
374
+ /** Browser wire form of one Session surface operation. */
375
+ export type SessionWireSurfaceOp = 'append' | {
376
+ readonly op: 'replace';
377
+ readonly start: number;
378
+ readonly end: number;
379
+ };
361
380
  /** Event-shaped wire representation of one packed chunk row. */
362
381
  export type ChunkRowEvent = {
363
382
  [Kind in ChunkRow['type']]: {
@@ -384,7 +403,7 @@ export interface SessionWireEvent {
384
403
  readonly data: JsonValue;
385
404
  readonly ignorable?: true;
386
405
  readonly sourceEventSeqs?: number[];
387
- readonly surfaceOp?: SurfaceOp;
406
+ readonly surfaceOp?: SessionWireSurfaceOp;
388
407
  }
389
408
  /** One message-aligned backwards-history request. */
390
409
  export interface SessionPageRequest {
@@ -407,7 +426,7 @@ export interface SessionPage {
407
426
  /** Complete opening window followed by ordered events appended after its cursor. */
408
427
  export type SessionFollowFrame = {
409
428
  readonly type: 'snapshot';
410
- readonly header: SessionHeader;
429
+ readonly header: SessionWireHeader;
411
430
  readonly cursor: number;
412
431
  readonly records: readonly SessionHistoryRecord[];
413
432
  readonly hasMore: boolean;