@deepseek-ai/dsh-api-session-controller 0.2.0-rc.2 → 0.2.1-alpha.2

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.
@@ -85,11 +85,11 @@ export interface ISessions {
85
85
  /**
86
86
  * Resolve an already discovered direct-parent address without opening it.
87
87
  * @param id - possible addressed child id.
88
- * @returns a retained or loaded-catalog address, without retaining a new selection or scope.
88
+ * @returns a retained, catalog-derived, or unknown-mode listed address, without retaining a new selection or scope.
89
89
  */
90
90
  subagentAddress(id: SessionId): SubagentAddress | undefined;
91
91
  /**
92
- * Load all Session projections once per connection; retry an unsuccessful initial read.
92
+ * Read projections without starting migration; retry failed or migration-deferred reads.
93
93
  * @param sessionId - Session to inspect without opening its conversation.
94
94
  * @returns completion of the current or newly started refresh.
95
95
  */
@@ -126,6 +126,8 @@ export interface ISessions {
126
126
  atSeq?: number;
127
127
  increaseTitle?: boolean;
128
128
  onCreated?: (childId: SessionId) => void;
129
+ /** False refuses source migration; omission preserves the Host default. */
130
+ allowMigration?: boolean;
129
131
  }): Promise<SessionId>;
130
132
  /**
131
133
  * Borrow an already-retained Agent-scoped Context without extending its lifetime.
@@ -21,6 +21,8 @@ export interface SessionListEntry {
21
21
  parentSessionId?: SessionId;
22
22
  /** Coarse durable origin for navigation filtering; not a continuation capability. */
23
23
  origin?: 'subagent';
24
+ /** Format classification supplied by the Host list. */
25
+ formatStatus?: SessionSummary['formatStatus'];
24
26
  cwd?: string;
25
27
  /** Current host-computed projection values for list consumers. */
26
28
  projectionValues?: Readonly<Partial<SessionProjectionMap>>;
@@ -35,7 +35,7 @@ export interface SessionListSnapshot {
35
35
  /** Shared projection values and the lifecycle of their explicit baseline read. */
36
36
  export interface SessionProjectionSnapshot {
37
37
  readonly values: Readonly<Partial<SessionProjectionMap>>;
38
- readonly state: 'idle' | 'loading' | 'ready' | 'error';
38
+ readonly state: 'idle' | 'loading' | 'ready' | 'migration-required' | 'error';
39
39
  readonly error: RemoteFailure | null;
40
40
  }
41
41
  /** Instance cluster + frame entry + the session list. */
@@ -86,8 +86,9 @@ export declare class SessionManager {
86
86
  resolveTarget(target: SessionTarget): SessionId;
87
87
  /**
88
88
  * Resolve an address for breadcrumb navigation without retaining transport authority.
89
- * @param sessionId - possible child id in an already-loaded catalog.
90
- * @returns A retained or catalog-derived direct-parent address.
89
+ * @param sessionId - possible child id in received Session metadata.
90
+ * @returns A retained address, then a catalog address, then an unknown-mode address
91
+ * from a listed subagent header; undefined without that evidence.
91
92
  */
92
93
  subagentAddress(sessionId: SessionId): SubagentAddress | undefined;
93
94
  /**
@@ -130,7 +131,7 @@ export declare class SessionManager {
130
131
  /** Resident per-session projection store (create-on-demand; outlives instantiation). */
131
132
  private projectionStore;
132
133
  /**
133
- * Load a complete projection baseline once per connection; retry unsuccessful reads.
134
+ * Read projections without starting migration; retry failed or migration-deferred reads.
134
135
  * @param sessionId - Session to inspect without opening its conversation.
135
136
  * @returns completion of the current or newly started read.
136
137
  */
@@ -171,12 +172,13 @@ export declare class SessionManager {
171
172
  * preserve it or lower it after an exact cut before the first `turn/start`;
172
173
  * lineage rides parentSessionId. A child published before Workspace
173
174
  * attachment fails is also reconciled into the list.
174
- * @param opts - source session and the optional exact inclusive boundary seq.
175
+ * @param opts - source session, optional exact inclusive boundary seq, and permission to start migration.
175
176
  * @returns the fork result (the child session id).
176
177
  */
177
178
  fork(opts: {
178
179
  sessionId: SessionId;
179
180
  atSeq?: SessionSeq;
181
+ allowMigration?: boolean;
180
182
  }): Promise<RemoteResult<{
181
183
  sessionId: SessionId;
182
184
  }>>;
@@ -78,8 +78,9 @@ export class SessionManager {
78
78
  }
79
79
  /**
80
80
  * Resolve an address for breadcrumb navigation without retaining transport authority.
81
- * @param sessionId - possible child id in an already-loaded catalog.
82
- * @returns A retained or catalog-derived direct-parent address.
81
+ * @param sessionId - possible child id in received Session metadata.
82
+ * @returns A retained address, then a catalog address, then an unknown-mode address
83
+ * from a listed subagent header; undefined without that evidence.
83
84
  */
84
85
  subagentAddress(sessionId) {
85
86
  const retained = this.addresses.get(sessionId);
@@ -88,7 +89,7 @@ export class SessionManager {
88
89
  for (const parentSessionId of this.projectionStores.keys()) {
89
90
  const child = this.projectionStores.get(parentSessionId)?.values().subagentCatalog
90
91
  ?.find(entry => entry.id === sessionId);
91
- if (child !== undefined) {
92
+ if (child !== undefined && child.mode !== 'external') {
92
93
  return {
93
94
  parentSessionId,
94
95
  childSessionId: sessionId,
@@ -96,7 +97,14 @@ export class SessionManager {
96
97
  };
97
98
  }
98
99
  }
99
- return undefined;
100
+ const summary = this.summaries.find(summary => summary.sessionId === sessionId);
101
+ if (summary?.origin !== 'subagent' || summary.parentSessionId === undefined)
102
+ return undefined;
103
+ return {
104
+ parentSessionId: summary.parentSessionId,
105
+ childSessionId: sessionId,
106
+ mode: 'unknown',
107
+ };
100
108
  }
101
109
  // ---- Instance management ----
102
110
  /**
@@ -169,7 +177,7 @@ export class SessionManager {
169
177
  const address = this.addresses.get(sessionId);
170
178
  const child = address === undefined ? undefined : this.projectionStores.get(address.parentSessionId)?.values().subagentCatalog
171
179
  ?.find(entry => entry.id === sessionId);
172
- if (child !== undefined) {
180
+ if (child !== undefined && child.mode !== 'external') {
173
181
  // A catalogued child exists only after its delegated session has
174
182
  // durable history, even though child rows do not carry `blank`.
175
183
  session.handleBlank(false);
@@ -252,7 +260,7 @@ export class SessionManager {
252
260
  return store;
253
261
  }
254
262
  /**
255
- * Load a complete projection baseline once per connection; retry unsuccessful reads.
263
+ * Read projections without starting migration; retry failed or migration-deferred reads.
256
264
  * @param sessionId - Session to inspect without opening its conversation.
257
265
  * @returns completion of the current or newly started read.
258
266
  */
@@ -273,14 +281,27 @@ export class SessionManager {
273
281
  if (controller.signal.aborted)
274
282
  return;
275
283
  if (result.ok) {
276
- if (result.value !== null) {
277
- store.seed({ ...result.value, asOfSeq: sessionSeqCursor(result.value.asOfSeq) });
284
+ const value = result.value;
285
+ if (value !== null) {
286
+ switch (value.kind) {
287
+ case 'sequenced':
288
+ store.seed({ values: value.values, asOfSeq: sessionSeqCursor(value.asOfSeq) });
289
+ break;
290
+ case 'migration-required':
291
+ store.applyCached(value.values);
292
+ break;
293
+ default:
294
+ assertNever(value, 'session projections result');
295
+ }
278
296
  }
279
297
  else if (store.values() === initialValues) {
280
298
  // A later frame proves existence independently of an earlier missing read.
281
299
  store.clear();
282
300
  }
283
- this.projectionLoads.set(sessionId, { state: 'ready', error: null });
301
+ this.projectionLoads.set(sessionId, {
302
+ state: value?.kind === 'migration-required' ? 'migration-required' : 'ready',
303
+ error: null,
304
+ });
284
305
  }
285
306
  else {
286
307
  this.projectionLoads.set(sessionId, { state: 'error', error: result.error });
@@ -454,7 +475,7 @@ export class SessionManager {
454
475
  * preserve it or lower it after an exact cut before the first `turn/start`;
455
476
  * lineage rides parentSessionId. A child published before Workspace
456
477
  * attachment fails is also reconciled into the list.
457
- * @param opts - source session and the optional exact inclusive boundary seq.
478
+ * @param opts - source session, optional exact inclusive boundary seq, and permission to start migration.
458
479
  * @returns the fork result (the child session id).
459
480
  */
460
481
  async fork(opts) {
@@ -462,6 +483,7 @@ export class SessionManager {
462
483
  const result = await this.remote.session.fork({
463
484
  sessionId: opts.sessionId,
464
485
  ...opts.atSeq === undefined ? {} : { atSeq: opts.atSeq },
486
+ ...opts.allowMigration === undefined ? {} : { allowMigration: opts.allowMigration },
465
487
  });
466
488
  const childId = result.ok
467
489
  ? result.value.sessionId
@@ -691,6 +713,7 @@ export class SessionManager {
691
713
  && prev.blank === entry.blank
692
714
  && prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd
693
715
  && prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth
716
+ && prev.formatStatus === entry.formatStatus
694
717
  && prev.projectionValues === entry.projectionValues)
695
718
  return prev;
696
719
  this.entryCache.set(entry.sessionId, entry);
@@ -732,6 +755,7 @@ function applyMutation(summaries, mutation) {
732
755
  ...(mutation.kind === 'upsert' ? {
733
756
  agentAvailable: mutation.summary.agentAvailable,
734
757
  running: mutation.summary.running,
758
+ ...(mutation.summary.formatStatus === undefined ? {} : { formatStatus: mutation.summary.formatStatus }),
735
759
  } : {}),
736
760
  ...(existing.cwd === undefined && mutation.summary.cwd !== undefined ? { cwd: mutation.summary.cwd } : {}),
737
761
  ...(existing.parentSessionId === undefined && mutation.summary.parentSessionId !== undefined
@@ -741,7 +765,8 @@ function applyMutation(summaries, mutation) {
741
765
  };
742
766
  if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId
743
767
  && filled.origin === existing.origin && filled.blank === existing.blank
744
- && filled.agentAvailable === existing.agentAvailable && filled.running === existing.running)
768
+ && filled.agentAvailable === existing.agentAvailable && filled.running === existing.running
769
+ && filled.formatStatus === existing.formatStatus)
745
770
  return [...summaries];
746
771
  return summaries.map(summary => summary.sessionId === mutation.summary.sessionId ? filled : summary);
747
772
  }
@@ -6,8 +6,8 @@
6
6
  * (`{ value, seq }`) comes from the connected Host — a follow opening
7
7
  * baseline, a Session Controller `projection` frame, a list block the Host
8
8
  * computed for an attached Session — and merges under **higher seq wins**
9
- * against other sequenced rows. A `cached` row (`{ value }`) comes from the
10
- * session list's view of the persisted checkpoint, carries no comparable
9
+ * against other sequenced rows. A `cached` row (`{ value }`) comes from
10
+ * a Host view of the persisted checkpoint, carries no comparable
11
11
  * seq, and yields to every sequenced write. No client-side domain folding
12
12
  * exists: a domain ships projection support with zero client code. Per-key
13
13
  * bare observable faces feed `useProjection` (ui-renderer binds them).
@@ -101,7 +101,7 @@ export declare class ProjectionValueStore {
101
101
  */
102
102
  apply(key: string, value: unknown, seq: SessionSeqCursor): void;
103
103
  /**
104
- * Fill keys from a session-list block the Host labeled `cached`: a zero-I/O
104
+ * Fill keys from a Host block labeled `cached`: a zero-I/O
105
105
  * view of the persisted checkpoint. A cached value lands only where no
106
106
  * sequenced row exists: a connected Session has already answered for such
107
107
  * a key, and the list's view of the persisted checkpoint cannot be newer
@@ -82,7 +82,7 @@ export class ProjectionValueStore {
82
82
  this.changed(key);
83
83
  }
84
84
  /**
85
- * Fill keys from a session-list block the Host labeled `cached`: a zero-I/O
85
+ * Fill keys from a Host block labeled `cached`: a zero-I/O
86
86
  * view of the persisted checkpoint. A cached value lands only where no
87
87
  * sequenced row exists: a connected Session has already answered for such
88
88
  * a key, and the list's view of the persisted checkpoint cannot be newer
@@ -12,6 +12,7 @@ import type { AgentContext, ISessions, SessionReference, SessionRetainInfo, Sess
12
12
  import { SessionManager } from './manager.ts';
13
13
  import type { SessionRemotes } from './remotes.ts';
14
14
  import type { SessionListPhase, SessionSearchResultItem, SessionProjectionSnapshot } from './manager.ts';
15
+ import type { SessionListEntry } from './lineage.ts';
15
16
  /** Session list row projected from the host list RPC plus live stream increments. */
16
17
  export interface SessionSummary {
17
18
  id: SessionId;
@@ -23,6 +24,8 @@ export interface SessionSummary {
23
24
  parentId?: SessionId;
24
25
  /** Coarse durable origin for navigation filtering; not a continuation capability. */
25
26
  origin?: 'subagent';
27
+ /** Format classification supplied by the Host list, when available. */
28
+ formatStatus?: SessionListEntry['formatStatus'];
26
29
  /** Host running state for `ids` members; a display fallback for other rows. */
27
30
  running: boolean;
28
31
  /** Local ownership counts; Host metadata refreshes cannot overwrite them. */
@@ -30,7 +33,7 @@ export interface SessionSummary {
30
33
  /**
31
34
  * New Session presentation and reuse eligibility, derived from the Host
32
35
  * summary, `sessionListMetadata`, and client acceptance/running observations.
33
- * New Session reuses a blank one targeting the same workspace. Filtering
36
+ * Workspace reconnection may reuse a blank in the same workspace. Filtering
34
37
  * stays with the consumer: the store carries every row, while the Workspace
35
38
  * browser shows only the selected blank entry.
36
39
  */
@@ -113,11 +116,11 @@ export declare class ClientSessions implements ISessions {
113
116
  * Resolve an already discovered direct-parent address without opening it.
114
117
  * Feature plugins use this to avoid Agent-bound RPCs in persisted child views.
115
118
  * @param id - possible addressed child id.
116
- * @returns A retained or loaded-catalog address, without retaining a new selection or scope.
119
+ * @returns A retained, catalog-derived, or unknown-mode listed address, without retaining a new selection or scope.
117
120
  */
118
121
  subagentAddress(id: SessionId): SubagentAddress | undefined;
119
122
  /**
120
- * Load all Session projections once per connection; retry an unsuccessful initial read.
123
+ * Read projections without starting migration; retry failed or migration-deferred reads.
121
124
  * @param sessionId - Session to inspect without opening its conversation.
122
125
  */
123
126
  refreshProjections(sessionId: SessionId): Promise<void>;
@@ -189,7 +192,7 @@ export declare class ClientSessions implements ISessions {
189
192
  * seq (a real event seq the caller already knows; a cut inside an open
190
193
  * turn is balanced Host-side with synthetic closers, and omission selects
191
194
  * the latest completed-turn prefix), and whether to increment an
192
- * inherited durable title before resolving.
195
+ * inherited durable title before resolving, plus permission to start source migration.
193
196
  * @returns the child session id.
194
197
  * @throws {SessionForkError} with the source id.
195
198
  * @throws {Error} when a requested child-title rename fails after creation.
@@ -199,6 +202,7 @@ export declare class ClientSessions implements ISessions {
199
202
  atSeq?: number;
200
203
  increaseTitle?: boolean;
201
204
  onCreated?: (childId: SessionId) => void;
205
+ allowMigration?: boolean;
202
206
  }): Promise<SessionId>;
203
207
  /**
204
208
  * Borrow an already-retained Agent-scoped Context.
@@ -234,13 +234,13 @@ export class ClientSessions {
234
234
  * Resolve an already discovered direct-parent address without opening it.
235
235
  * Feature plugins use this to avoid Agent-bound RPCs in persisted child views.
236
236
  * @param id - possible addressed child id.
237
- * @returns A retained or loaded-catalog address, without retaining a new selection or scope.
237
+ * @returns A retained, catalog-derived, or unknown-mode listed address, without retaining a new selection or scope.
238
238
  */
239
239
  subagentAddress(id) {
240
240
  return this.manager.subagentAddress(id);
241
241
  }
242
242
  /**
243
- * Load all Session projections once per connection; retry an unsuccessful initial read.
243
+ * Read projections without starting migration; retry failed or migration-deferred reads.
244
244
  * @param sessionId - Session to inspect without opening its conversation.
245
245
  */
246
246
  refreshProjections(sessionId) {
@@ -331,7 +331,7 @@ export class ClientSessions {
331
331
  * seq (a real event seq the caller already knows; a cut inside an open
332
332
  * turn is balanced Host-side with synthetic closers, and omission selects
333
333
  * the latest completed-turn prefix), and whether to increment an
334
- * inherited durable title before resolving.
334
+ * inherited durable title before resolving, plus permission to start source migration.
335
335
  * @returns the child session id.
336
336
  * @throws {SessionForkError} with the source id.
337
337
  * @throws {Error} when a requested child-title rename fails after creation.
@@ -343,6 +343,7 @@ export class ClientSessions {
343
343
  const result = await this.manager.fork({
344
344
  sessionId: opts.sessionId,
345
345
  ...(opts.atSeq === undefined ? {} : { atSeq: SessionSeq(opts.atSeq) }),
346
+ ...(opts.allowMigration === undefined ? {} : { allowMigration: opts.allowMigration }),
346
347
  });
347
348
  if (!result.ok)
348
349
  throw new SessionForkError(result.error, opts.sessionId);
@@ -502,6 +503,7 @@ export class ClientSessions {
502
503
  retainedBy: this.retentionSnapshot(entry.sessionId).retainedBy,
503
504
  blank: entry.blank,
504
505
  updatedAt: entry.updatedAt,
506
+ formatStatus: entry.formatStatus,
505
507
  ...(entry.projectionValues === undefined
506
508
  ? {}
507
509
  : { projectionValues: entry.projectionValues }),
@@ -513,6 +515,8 @@ export class ClientSessions {
513
515
  }
514
516
  for (const [parentId, projection] of Object.entries(projectionsBySession)) {
515
517
  for (const child of projection.values.subagentCatalog ?? []) {
518
+ if (child.mode === 'external')
519
+ continue;
516
520
  const childId = child.id;
517
521
  const summary = byId[childId];
518
522
  const projectionValues = summary?.projectionValues ?? this.manager.projectionValues(childId);
@@ -22,7 +22,13 @@ declare module '@deepseek-ai/cordis' {
22
22
  export interface Config {
23
23
  /** Override platform desktop-opener detection. */
24
24
  readonly nativeOpen?: boolean;
25
+ /** Positive integral milliseconds of list work before yielding between complete rows. */
26
+ readonly listWorkSliceMs?: number;
25
27
  }
28
+ /** Deployment policy after schema defaults have been applied. */
29
+ type ResolvedConfig = Config & {
30
+ readonly listWorkSliceMs: number;
31
+ };
26
32
  /** Host integrations replaceable by direct unit tests. */
27
33
  export interface SessionControllerInternals {
28
34
  /** Native default-application handoff. */
@@ -39,7 +45,7 @@ export interface SessionControllerInternals {
39
45
  /** Host service backing the generated `ctx.remote.session` namespace. */
40
46
  export declare class SessionController extends TypertRemoteService {
41
47
  static inject: string[];
42
- static Config: z<Config>;
48
+ static Config: z<Config, ResolvedConfig>;
43
49
  private readonly agents;
44
50
  private readonly commands;
45
51
  private readonly controlState;
@@ -53,7 +59,7 @@ export declare class SessionController extends TypertRemoteService {
53
59
  private readonly promotions;
54
60
  /**
55
61
  * @param ctx - Host context containing the Session capability assembly.
56
- * @param config - native-opener deployment policy.
62
+ * @param config - native-opener and list-scheduling deployment policy.
57
63
  * @param internals - host integrations replaceable by direct unit tests.
58
64
  */
59
65
  constructor(ctx: Context, config: Config, internals?: SessionControllerInternals);
@@ -74,7 +80,7 @@ export declare class SessionController extends TypertRemoteService {
74
80
  /**
75
81
  * Read all visible Session rows without resuming an Agent.
76
82
  * @param _request - reserved empty list request.
77
- * @param signal - cancellation for persistence reads.
83
+ * @param signal - cancellation for persistence reads and summary generation.
78
84
  * @returns visible Session summaries ordered by activity.
79
85
  */
80
86
  list(_request: SessionListRequest, signal: AbortSignal): Promise<SessionListValue>;
@@ -150,7 +156,7 @@ export declare class SessionController extends TypertRemoteService {
150
156
  * Fork one cold-readable exact event prefix into a new Session. An omitted
151
157
  * boundary selects the latest completed-turn prefix; an open cut receives
152
158
  * synthetic fork closers.
153
- * @param request - source Session and optional exact inclusive event boundary.
159
+ * @param request - source Session, optional exact inclusive event boundary, and migration preflight choice.
154
160
  * @returns the new Session identity.
155
161
  */
156
162
  fork(request: SessionForkRequest): Promise<SessionForkValue>;
@@ -195,12 +201,13 @@ export declare class SessionController extends TypertRemoteService {
195
201
  */
196
202
  follow(request: SessionFollowRequest, signal: AbortSignal): AsyncIterable<SessionFollowFrame>;
197
203
  /**
198
- * Read all registered projections without activating an Agent.
204
+ * Read exact projections, returning cached hints only when migration is required.
199
205
  * @param request - Session whose current values are required.
200
- * @param signal - cancellation for the Session observation.
201
- * @returns complete baseline, or null when the Session does not exist.
206
+ * @param signal - cancellation for the Session read.
207
+ * @returns a sequenced baseline, cached hints when migration is deferred, or null when absent.
202
208
  */
203
209
  projections(request: SessionProjectionsRequest, signal: AbortSignal): Promise<SessionProjectionsValue>;
210
+ private liveProjections;
204
211
  /**
205
212
  * Stream a complete live-control baseline followed by replacement frames.
206
213
  * @param signal - cancellation owned by the Remote stream carrier.
@@ -187,6 +187,7 @@ let SessionController = (() => {
187
187
  ];
188
188
  static Config = z.object({
189
189
  nativeOpen: z.boolean(),
190
+ listWorkSliceMs: z.natural().min(1).default(16),
190
191
  });
191
192
  agents = __runInitializers(this, _instanceExtraInitializers);
192
193
  commands;
@@ -201,11 +202,12 @@ let SessionController = (() => {
201
202
  promotions = new Set();
202
203
  /**
203
204
  * @param ctx - Host context containing the Session capability assembly.
204
- * @param config - native-opener deployment policy.
205
+ * @param config - native-opener and list-scheduling deployment policy.
205
206
  * @param internals - host integrations replaceable by direct unit tests.
206
207
  */
207
208
  constructor(ctx, config, internals = {}) {
208
209
  super(ctx, 'sessionController', { namespace: 'session' });
210
+ const resolved = SessionController.Config(config);
209
211
  installModelSelectionProjection(ctx);
210
212
  this.agents = new ApiSessionAgentController(ctx);
211
213
  this.commands = new SessionCommandController(ctx, this.agents, process.cwd());
@@ -222,7 +224,7 @@ let SessionController = (() => {
222
224
  await Promise.allSettled([...this.promotions]);
223
225
  }, 'session-controller.promotions');
224
226
  this.history = new SessionHistoryController(ctx, (observation) => { this.promote(observation); });
225
- this.listState = new ApiSessionList(ctx);
227
+ this.listState = new ApiSessionList(ctx, resolved.listWorkSliceMs);
226
228
  this.fileApplications = internals.fileApplications ?? nativeFileApplications;
227
229
  this.openFileApplication = internals.openFileApplication ?? openNativeFileApplication;
228
230
  this.openPath = internals.openPath ?? openNativeAssociatedPath;
@@ -318,7 +320,7 @@ let SessionController = (() => {
318
320
  /**
319
321
  * Read all visible Session rows without resuming an Agent.
320
322
  * @param _request - reserved empty list request.
321
- * @param signal - cancellation for persistence reads.
323
+ * @param signal - cancellation for persistence reads and summary generation.
322
324
  * @returns visible Session summaries ordered by activity.
323
325
  */
324
326
  async list(_request, signal) {
@@ -461,10 +463,18 @@ let SessionController = (() => {
461
463
  * Fork one cold-readable exact event prefix into a new Session. An omitted
462
464
  * boundary selects the latest completed-turn prefix; an open cut receives
463
465
  * synthetic fork closers.
464
- * @param request - source Session and optional exact inclusive event boundary.
466
+ * @param request - source Session, optional exact inclusive event boundary, and migration preflight choice.
465
467
  * @returns the new Session identity.
466
468
  */
467
- fork(request) {
469
+ async fork(request) {
470
+ if (request.allowMigration === false && this.ctx.sessions.get(request.sessionId) === undefined) {
471
+ const stored = await this.ctx.get('sessionPersistence')?.stat(request.sessionId);
472
+ if (stored?.formatStatus === 'migration-required' && this.ctx.sessions.get(request.sessionId) === undefined) {
473
+ throw new RemoteError('session/migration-required', 'Open the source Session to complete migration before forking', {
474
+ sessionId: request.sessionId,
475
+ });
476
+ }
477
+ }
468
478
  return this.commands.fork(request);
469
479
  }
470
480
  /**
@@ -521,10 +531,10 @@ let SessionController = (() => {
521
531
  return this.history.follow(request, signal);
522
532
  }
523
533
  /**
524
- * Read all registered projections without activating an Agent.
534
+ * Read exact projections, returning cached hints only when migration is required.
525
535
  * @param request - Session whose current values are required.
526
- * @param signal - cancellation for the Session observation.
527
- * @returns complete baseline, or null when the Session does not exist.
536
+ * @param signal - cancellation for the Session read.
537
+ * @returns a sequenced baseline, cached hints when migration is deferred, or null when absent.
528
538
  */
529
539
  async projections(request, signal) {
530
540
  const { sessionId } = request;
@@ -534,12 +544,27 @@ let SessionController = (() => {
534
544
  try {
535
545
  const env_2 = { stack: [], error: void 0, hasError: false };
536
546
  try {
547
+ signal.throwIfAborted();
548
+ const live = this.liveProjections(sessionId);
549
+ if (live !== undefined)
550
+ return live;
551
+ const stored = await this.ctx.get('sessionPersistence')?.stat(sessionId, { signal });
552
+ signal.throwIfAborted();
553
+ const attached = this.liveProjections(sessionId);
554
+ if (attached !== undefined)
555
+ return attached;
556
+ if (stored === undefined)
557
+ return null;
558
+ if (stored.formatStatus === 'migration-required') {
559
+ const cached = this.ctx.get('sessionProjectionCache')?.cachedSnapshot(stored.header);
560
+ return { kind: 'migration-required', values: { ...cached?.values } };
561
+ }
537
562
  const observation = __addDisposableResource(env_2, await this.ctx.sessionQuery.observeSession(sessionId, { signal }), false);
538
563
  const projections = observation.projections;
539
564
  if (projections === undefined) {
540
565
  throw new RemoteError('session/projections-unavailable', 'Session projections are unavailable', {});
541
566
  }
542
- return { asOfSeq: projections.asOfSeq, values: projections.values };
567
+ return { kind: 'sequenced', asOfSeq: projections.asOfSeq, values: projections.values };
543
568
  }
544
569
  catch (e_2) {
545
570
  env_2.error = e_2;
@@ -550,10 +575,10 @@ let SessionController = (() => {
550
575
  }
551
576
  }
552
577
  catch (error) {
553
- if (error instanceof SessionQueryError && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND')
554
- return null;
555
- if (signal.aborted
556
- || (error instanceof SessionQueryError && error.code === 'SESSION_QUERY_ABORTED')) {
578
+ if (error instanceof SessionQueryError && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') {
579
+ return this.liveProjections(sessionId) ?? null;
580
+ }
581
+ if (signal.aborted || (error instanceof SessionQueryError && error.code === 'SESSION_QUERY_ABORTED')) {
557
582
  throw new RemoteError('gateway/cancelled', 'Session projection read was cancelled', {}, { cause: error });
558
583
  }
559
584
  if (error instanceof RemoteError)
@@ -561,6 +586,13 @@ let SessionController = (() => {
561
586
  throw new RemoteError('gateway/internal', 'Session projection read failed', {}, { cause: error });
562
587
  }
563
588
  }
589
+ liveProjections(sessionId) {
590
+ const session = this.ctx.sessions.get(sessionId);
591
+ if (session === undefined)
592
+ return undefined;
593
+ const snapshot = this.ctx.sessionProjections.snapshot(session);
594
+ return { kind: 'sequenced', asOfSeq: snapshot.asOfSeq, values: snapshot.values };
595
+ }
564
596
  /**
565
597
  * Stream a complete live-control baseline followed by replacement frames.
566
598
  * @param signal - cancellation owned by the Remote stream carrier.
@@ -19,8 +19,12 @@ export declare function truncateUnicodeCodePoints(value: string, maximum: number
19
19
  /** Owns list projection registration, bounded cold summaries, and authorized search. */
20
20
  export declare class ApiSessionList {
21
21
  private readonly ctx;
22
- /** @param ctx - Host context carrying Session, query, persistence, and projection services. */
23
- constructor(ctx: Context);
22
+ private readonly workSliceMs;
23
+ /**
24
+ * @param ctx - Host context carrying Session, query, persistence, and projection services.
25
+ * @param workSliceMs - Resolved positive integral list-work budget in milliseconds.
26
+ */
27
+ constructor(ctx: Context, workSliceMs: number);
24
28
  /**
25
29
  * Build one current attached-Session summary.
26
30
  * @param session - attached Session to summarize.
@@ -29,7 +33,7 @@ export declare class ApiSessionList {
29
33
  summaryFor(session: Session): SessionSummary;
30
34
  /**
31
35
  * Read every visible attached and persisted Session without activating an Agent.
32
- * @param signal - optional cancellation for persistence reads.
36
+ * @param signal - optional cancellation for persistence reads and summary generation.
33
37
  * @returns visible Session summaries ordered by activity.
34
38
  */
35
39
  list(signal?: AbortSignal): Promise<SessionSummary[]>;