@deepseek-ai/dsh-api-session-controller 0.2.1-alpha.1 → 0.2.1-alpha.3

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.
@@ -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 });
@@ -416,11 +437,14 @@ export class SessionManager {
416
437
  * Contract session.create; on success merge into summaries immediately (no
417
438
  * wait for the next refresh). A created session is blank by definition
418
439
  * (entity birth precedes the first message).
419
- * @param opts - target workspace or working directory, plus an optional caller-owned id.
420
- * @returns the create result.
440
+ * @param opts - target workspace or working directory, an optional blank Session to reuse, and an optional Agent preset.
441
+ * @returns the create result, naming the Session the Host created or reused.
421
442
  */
422
443
  async create(opts = {}) {
423
- const shared = opts.sessionId === undefined ? {} : { sessionId: opts.sessionId };
444
+ const shared = {
445
+ ...(opts.sessionId === undefined ? {} : { sessionId: opts.sessionId }),
446
+ ...(opts.agentPreset === undefined ? {} : { agentPreset: opts.agentPreset }),
447
+ };
424
448
  const payload = opts.workspaceId !== undefined
425
449
  ? { workspaceId: opts.workspaceId, ...shared }
426
450
  : { ...(opts.cwd === undefined ? {} : { cwd: opts.cwd }), ...shared };
@@ -454,7 +478,7 @@ export class SessionManager {
454
478
  * preserve it or lower it after an exact cut before the first `turn/start`;
455
479
  * lineage rides parentSessionId. A child published before Workspace
456
480
  * attachment fails is also reconciled into the list.
457
- * @param opts - source session and the optional exact inclusive boundary seq.
481
+ * @param opts - source session, optional exact inclusive boundary seq, and permission to start migration.
458
482
  * @returns the fork result (the child session id).
459
483
  */
460
484
  async fork(opts) {
@@ -462,6 +486,7 @@ export class SessionManager {
462
486
  const result = await this.remote.session.fork({
463
487
  sessionId: opts.sessionId,
464
488
  ...opts.atSeq === undefined ? {} : { atSeq: opts.atSeq },
489
+ ...opts.allowMigration === undefined ? {} : { allowMigration: opts.allowMigration },
465
490
  });
466
491
  const childId = result.ok
467
492
  ? result.value.sessionId
@@ -691,6 +716,7 @@ export class SessionManager {
691
716
  && prev.blank === entry.blank
692
717
  && prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd
693
718
  && prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth
719
+ && prev.formatStatus === entry.formatStatus
694
720
  && prev.projectionValues === entry.projectionValues)
695
721
  return prev;
696
722
  this.entryCache.set(entry.sessionId, entry);
@@ -732,6 +758,7 @@ function applyMutation(summaries, mutation) {
732
758
  ...(mutation.kind === 'upsert' ? {
733
759
  agentAvailable: mutation.summary.agentAvailable,
734
760
  running: mutation.summary.running,
761
+ ...(mutation.summary.formatStatus === undefined ? {} : { formatStatus: mutation.summary.formatStatus }),
735
762
  } : {}),
736
763
  ...(existing.cwd === undefined && mutation.summary.cwd !== undefined ? { cwd: mutation.summary.cwd } : {}),
737
764
  ...(existing.parentSessionId === undefined && mutation.summary.parentSessionId !== undefined
@@ -741,7 +768,8 @@ function applyMutation(summaries, mutation) {
741
768
  };
742
769
  if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId
743
770
  && filled.origin === existing.origin && filled.blank === existing.blank
744
- && filled.agentAvailable === existing.agentAvailable && filled.running === existing.running)
771
+ && filled.agentAvailable === existing.agentAvailable && filled.running === existing.running
772
+ && filled.formatStatus === existing.formatStatus)
745
773
  return [...summaries];
746
774
  return summaries.map(summary => summary.sessionId === mutation.summary.sessionId ? filled : summary);
747
775
  }
@@ -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>;
@@ -172,14 +175,15 @@ export declare class ClientSessions implements ISessions {
172
175
  /**
173
176
  * Create a Host Session and publish its catalog row before resolving.
174
177
  * Callers retain the returned identity before borrowing its binding.
175
- * @param opts - target workspace or directory and an optional preallocated id.
176
- * @returns the new session id.
178
+ * @param opts - target workspace or directory, an optional blank Session to reuse, and an optional Agent preset.
179
+ * @returns the created or reused session id.
177
180
  * @throws {SessionCreateError} with the requested id.
178
181
  */
179
182
  create(opts?: {
180
183
  workspaceId?: WorkspaceId;
181
184
  cwd?: string;
182
185
  sessionId?: SessionId;
186
+ agentPreset?: string;
183
187
  }): Promise<SessionId>;
184
188
  /**
185
189
  * Fork a session from an exact inclusive prefix of the source (same
@@ -189,7 +193,7 @@ export declare class ClientSessions implements ISessions {
189
193
  * seq (a real event seq the caller already knows; a cut inside an open
190
194
  * turn is balanced Host-side with synthetic closers, and omission selects
191
195
  * the latest completed-turn prefix), and whether to increment an
192
- * inherited durable title before resolving.
196
+ * inherited durable title before resolving, plus permission to start source migration.
193
197
  * @returns the child session id.
194
198
  * @throws {SessionForkError} with the source id.
195
199
  * @throws {Error} when a requested child-title rename fails after creation.
@@ -199,6 +203,7 @@ export declare class ClientSessions implements ISessions {
199
203
  atSeq?: number;
200
204
  increaseTitle?: boolean;
201
205
  onCreated?: (childId: SessionId) => void;
206
+ allowMigration?: boolean;
202
207
  }): Promise<SessionId>;
203
208
  /**
204
209
  * 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) {
@@ -312,8 +312,8 @@ export class ClientSessions {
312
312
  /**
313
313
  * Create a Host Session and publish its catalog row before resolving.
314
314
  * Callers retain the returned identity before borrowing its binding.
315
- * @param opts - target workspace or directory and an optional preallocated id.
316
- * @returns the new session id.
315
+ * @param opts - target workspace or directory, an optional blank Session to reuse, and an optional Agent preset.
316
+ * @returns the created or reused session id.
317
317
  * @throws {SessionCreateError} with the requested id.
318
318
  */
319
319
  async create(opts = {}) {
@@ -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);
@@ -202,7 +202,7 @@ export class Session {
202
202
  // wire type is used; this array is not filtered or reordered.
203
203
  const routedContent = content;
204
204
  const routed = await this.remote.subagents.prompt({
205
- requestId: randomUUID(),
205
+ requestId: requestId ?? randomUUID(),
206
206
  parentSessionId: this.address.parentSessionId,
207
207
  childSessionId: this.address.childSessionId,
208
208
  mode: 'continuable',
@@ -1,7 +1,17 @@
1
1
  /** Session commands whose activation policy is explicit at each Remote method. */
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
+ import type { MessageSource } from '@deepseek-ai/dsh-llm';
3
4
  import { ApiSessionAgentController } from './agent.ts';
4
5
  import type { SessionAttachmentRequest, SessionAttachmentValue, SessionCancelRequest, SessionCancelValue, SessionCreateRequest, SessionCreateValue, SessionForkRequest, SessionForkValue, SessionPromptRequest, SessionPromptValue, SessionRenameRequest, SessionRenameValue, SessionSelectModelRequest, SessionSelectModelValue, SessionUpdateQueueRequest, SessionUpdateQueueValue } from './types.ts';
6
+ type PromptContentCandidate = SessionPromptRequest['content'][number] | Extract<SessionUpdateQueueRequest['action'], {
7
+ readonly kind: 'edit';
8
+ }>['content'][number];
9
+ /**
10
+ * Whether prompt content carries non-whitespace text or an attachment.
11
+ * @param content - submitted or edited prompt parts.
12
+ * @returns false for content the Agent must not receive.
13
+ */
14
+ export declare function hasPromptContent(content: readonly PromptContentCandidate[]): boolean;
5
15
  /** Implements Session business commands delegated by the Session Controller Remote service. */
6
16
  export declare class SessionCommandController {
7
17
  private readonly ctx;
@@ -14,9 +24,10 @@ export declare class SessionCommandController {
14
24
  */
15
25
  constructor(ctx: Context, agents: ApiSessionAgentController, defaultCwd: string);
16
26
  /**
17
- * Create or idempotently adopt one ordinary Session.
18
- * @param request - requested identity, location, and Agent preset.
19
- * @returns the Session identity and resolved preset when configured.
27
+ * Create one ordinary Session for a new conversation, reusing the named
28
+ * blank Session while it is an unarchived member of the requested Workspace.
29
+ * @param request - optional blank Session to reuse, location, and Agent preset.
30
+ * @returns the created or reused Session identity and its preset when configured.
20
31
  */
21
32
  create(request: SessionCreateRequest): Promise<SessionCreateValue>;
22
33
  /**
@@ -40,11 +51,13 @@ export declare class SessionCommandController {
40
51
  */
41
52
  fork(request: SessionForkRequest): Promise<SessionForkValue>;
42
53
  /**
43
- * Reject empty content, then admit one prompt after Agent and attachment validation.
44
- * @param request - Session identity, prompt content, source metadata, and delivery mode.
54
+ * Admit one prompt after Agent and attachment validation; the Remote method
55
+ * has already validated the content and built the durable message source.
56
+ * @param request - Session identity, prompt content, and delivery mode.
57
+ * @param source - the `user-rpc` source recorded on the accepted message.
45
58
  * @returns acknowledgement that the Agent accepted the prompt.
46
59
  */
47
- prompt(request: SessionPromptRequest): Promise<SessionPromptValue>;
60
+ prompt(request: SessionPromptRequest, source: MessageSource): Promise<SessionPromptValue>;
48
61
  private requireModel;
49
62
  /**
50
63
  * Read one durable image after proving the Session log references it.
@@ -69,4 +82,5 @@ export declare class SessionCommandController {
69
82
  private readSessionState;
70
83
  private forkWorkspace;
71
84
  }
85
+ export {};
72
86
  //# sourceMappingURL=commands.d.ts.map
@@ -60,11 +60,15 @@ import { buildForkSeed } from '@deepseek-ai/dsh-session/fork';
60
60
  import { SessionLogOffset, SessionSeq } from '@deepseek-ai/dsh-session';
61
61
  import { SessionQueryError } from '@deepseek-ai/dsh-session-query';
62
62
  import { SessionTitleInvalidError } from '@deepseek-ai/dsh-session-title';
63
- import { canonicalClientTimeZone } from '@deepseek-ai/dsh-util-time';
64
63
  import { assertNever } from '@deepseek-ai/dsh-util-values';
65
64
  import { RemoteError, remoteErrorOf } from '@deepseek-ai/dsh-typert-protocol';
66
65
  import { ApiSessionCwdConflict, ApiSessionNotFound, ApiSessionPresetConflict, ApiSessionSubagentOwnership, apiSessionSubagentOwnershipError, hasApiSessionSubagentOwner, inspectApiSession, } from "./agent.js";
67
- function hasPromptContent(content) {
66
+ /**
67
+ * Whether prompt content carries non-whitespace text or an attachment.
68
+ * @param content - submitted or edited prompt parts.
69
+ * @returns false for content the Agent must not receive.
70
+ */
71
+ export function hasPromptContent(content) {
68
72
  return content.some(part => part.type !== 'text' || part.text.trim().length > 0);
69
73
  }
70
74
  /**
@@ -100,15 +104,15 @@ export class SessionCommandController {
100
104
  this.defaultCwd = defaultCwd;
101
105
  }
102
106
  /**
103
- * Create or idempotently adopt one ordinary Session.
104
- * @param request - requested identity, location, and Agent preset.
105
- * @returns the Session identity and resolved preset when configured.
107
+ * Create one ordinary Session for a new conversation, reusing the named
108
+ * blank Session while it is an unarchived member of the requested Workspace.
109
+ * @param request - optional blank Session to reuse, location, and Agent preset.
110
+ * @returns the created or reused Session identity and its preset when configured.
106
111
  */
107
112
  async create(request) {
108
113
  if (request.workspaceId !== undefined && request.cwd !== undefined) {
109
114
  throw new RemoteError('gateway/bad-request', 'session.create accepts workspaceId or cwd, not both', {});
110
115
  }
111
- const sessionId = request.sessionId ?? brandString(`session-${randomUUID()}`);
112
116
  let workspace;
113
117
  if (request.workspaceId !== undefined) {
114
118
  workspace = this.ctx.workspaceRegistry.get(request.workspaceId);
@@ -119,13 +123,20 @@ export class SessionCommandController {
119
123
  }
120
124
  }
121
125
  const cwd = workspace?.path ?? request.cwd ?? this.defaultCwd;
122
- let adopted;
126
+ let created;
123
127
  try {
124
- adopted = await this.agents.ensureSession(sessionId, cwd, request.sessionId !== undefined, request.agentPreset);
128
+ created = await this.agents.createSession({
129
+ ...(request.sessionId === undefined ? {} : { sessionId: request.sessionId }),
130
+ cwd,
131
+ ...(request.agentPreset === undefined ? {} : { agentPreset: request.agentPreset }),
132
+ reusable: sessionId => !this.ctx.workspaceRegistry.archivedSessionIds.includes(sessionId)
133
+ && (workspace === undefined || workspace.sessionIds.includes(sessionId)),
134
+ });
125
135
  }
126
136
  catch (error) {
127
- this.rejectCreation(sessionId, error);
137
+ this.rejectCreation(error);
128
138
  }
139
+ const sessionId = created.agent.id;
129
140
  if (workspace !== undefined) {
130
141
  try {
131
142
  await workspace.attachSession(sessionId);
@@ -134,8 +145,7 @@ export class SessionCommandController {
134
145
  throw new RemoteError('session/workspace-attach-failed', `session "${sessionId}" was created but could not attach to workspace "${workspace.id}": ${String(error)}`, { sessionId, workspaceId: workspace.id });
135
146
  }
136
147
  }
137
- const agentPreset = this.agents.presetForSession(adopted.session);
138
- return { sessionId, ...(agentPreset === undefined ? {} : { agentPreset }) };
148
+ return { sessionId, ...(created.agentPreset === undefined ? {} : { agentPreset: created.agentPreset }) };
139
149
  }
140
150
  /**
141
151
  * Validate and install one Session-local model selection; save the default in the background.
@@ -283,28 +293,16 @@ export class SessionCommandController {
283
293
  }
284
294
  }
285
295
  /**
286
- * Reject empty content, then admit one prompt after Agent and attachment validation.
287
- * @param request - Session identity, prompt content, source metadata, and delivery mode.
296
+ * Admit one prompt after Agent and attachment validation; the Remote method
297
+ * has already validated the content and built the durable message source.
298
+ * @param request - Session identity, prompt content, and delivery mode.
299
+ * @param source - the `user-rpc` source recorded on the accepted message.
288
300
  * @returns acknowledgement that the Agent accepted the prompt.
289
301
  */
290
- async prompt(request) {
291
- if (!hasPromptContent(request.content)) {
292
- throw new RemoteError('gateway/bad-request', 'prompt content must include non-whitespace text or an attachment', {});
293
- }
294
- const clientTimeZone = request.clientTimeZone === undefined
295
- ? undefined
296
- : canonicalClientTimeZone(request.clientTimeZone);
297
- if (request.clientTimeZone !== undefined && clientTimeZone === undefined) {
298
- throw new RemoteError('session/invalid-time-zone', 'clientTimeZone must be UTC or a valid IANA Area/Location name', { value: request.clientTimeZone });
299
- }
302
+ async prompt(request, source) {
300
303
  const agent = await this.resolveAgent(request.sessionId);
301
304
  if (hasPromptRequest(agent, request.requestId))
302
305
  return { accepted: true };
303
- const source = {
304
- kind: 'user',
305
- rpcId: request.requestId,
306
- ...(clientTimeZone === undefined ? {} : { clientTimeZone }),
307
- };
308
306
  const hasImage = request.content.some(part => part.type === 'image');
309
307
  const admit = async () => {
310
308
  try {
@@ -482,17 +480,13 @@ export class SessionCommandController {
482
480
  throw found.error;
483
481
  return found.agent;
484
482
  }
485
- rejectCreation(sessionId, error) {
483
+ rejectCreation(error) {
486
484
  if (remoteErrorOf(error) !== undefined)
487
485
  throw error;
488
- if (error instanceof Error && error.name === 'SessionAlreadyOwnedError') {
489
- throw new RemoteError('session/writer-held', error.message, { sessionId });
490
- }
491
486
  if (error instanceof ApiSessionPresetConflict) {
492
487
  throw new RemoteError('agent-preset/conflict', error.message, {
493
488
  sessionId: error.sessionId,
494
489
  requestedPreset: error.requestedPreset,
495
- ...(error.existingPreset === undefined ? {} : { existingPreset: error.existingPreset }),
496
490
  });
497
491
  }
498
492
  if (error instanceof ApiSessionCwdConflict) {
@@ -505,7 +499,7 @@ export class SessionCommandController {
505
499
  if (error instanceof ApiSessionSubagentOwnership) {
506
500
  throw apiSessionSubagentOwnershipError(error.sessionId);
507
501
  }
508
- throw new RemoteError('gateway/internal', `failed to create session "${sessionId}": ${String(error)}`, {});
502
+ throw new RemoteError('gateway/internal', `failed to create session: ${String(error)}`, {});
509
503
  }
510
504
  async readSessionState(sessionId) {
511
505
  const attached = this.ctx.sessions.get(sessionId);
@@ -85,16 +85,18 @@ export declare class SessionController extends TypertRemoteService {
85
85
  */
86
86
  list(_request: SessionListRequest, signal: AbortSignal): Promise<SessionListValue>;
87
87
  /**
88
- * Search visible Session content without resuming an Agent.
88
+ * Search visible Session content without resuming an Agent. A non-owner
89
+ * receives only the hits whose Session `allows()` admits; `hasMore`
90
+ * describes the bounded result before that filtering.
89
91
  * @param request - literal message-content query.
90
92
  * @param signal - cancellation for list and search reads.
91
93
  * @returns authorized bounded Session search results.
92
94
  */
93
95
  search(request: SessionSearchRequest, signal: AbortSignal): Promise<SessionSearchValue>;
94
96
  /**
95
- * Create or idempotently adopt one ordinary Session.
96
- * @param request - requested identity, location, and Agent preset.
97
- * @returns the Session identity and resolved preset when configured.
97
+ * Create one ordinary Session for a new conversation, reusing the named blank Session while it is reusable.
98
+ * @param request - optional blank Session to reuse, location, and Agent preset.
99
+ * @returns the created or reused Session identity and its preset when configured.
98
100
  */
99
101
  create(request: SessionCreateRequest): Promise<SessionCreateValue>;
100
102
  /**
@@ -156,7 +158,7 @@ export declare class SessionController extends TypertRemoteService {
156
158
  * Fork one cold-readable exact event prefix into a new Session. An omitted
157
159
  * boundary selects the latest completed-turn prefix; an open cut receives
158
160
  * synthetic fork closers.
159
- * @param request - source Session and optional exact inclusive event boundary.
161
+ * @param request - source Session, optional exact inclusive event boundary, and migration preflight choice.
160
162
  * @returns the new Session identity.
161
163
  */
162
164
  fork(request: SessionForkRequest): Promise<SessionForkValue>;
@@ -201,12 +203,13 @@ export declare class SessionController extends TypertRemoteService {
201
203
  */
202
204
  follow(request: SessionFollowRequest, signal: AbortSignal): AsyncIterable<SessionFollowFrame>;
203
205
  /**
204
- * Read all registered projections without activating an Agent.
206
+ * Read exact projections, returning cached hints only when migration is required.
205
207
  * @param request - Session whose current values are required.
206
- * @param signal - cancellation for the Session observation.
207
- * @returns complete baseline, or null when the Session does not exist.
208
+ * @param signal - cancellation for the Session read.
209
+ * @returns a sequenced baseline, cached hints when migration is deferred, or null when absent.
208
210
  */
209
211
  projections(request: SessionProjectionsRequest, signal: AbortSignal): Promise<SessionProjectionsValue>;
212
+ private liveProjections;
210
213
  /**
211
214
  * Stream a complete live-control baseline followed by replacement frames.
212
215
  * @param signal - cancellation owned by the Remote stream carrier.