@deepseek-ai/dsh-api-session-controller 0.1.6-alpha.1 → 0.1.7-alpha.1

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.
Files changed (44) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +42 -7
  3. package/README.zh.md +42 -7
  4. package/lib/client.js +1037 -992
  5. package/lib/index.js +314 -183
  6. package/lib/typert.host.js +539 -331
  7. package/lib/typert.remote-client.d.ts +10 -5
  8. package/lib/typert.remote-client.js +374 -264
  9. package/lib/types/agent.d.ts +1 -1
  10. package/lib/types/agent.js +8 -1
  11. package/lib/types/archived-session-gate.d.ts +30 -0
  12. package/lib/types/archived-session-gate.js +52 -0
  13. package/lib/types/client/contract/session.d.ts +2 -0
  14. package/lib/types/client/contract/sessions.d.ts +65 -35
  15. package/lib/types/client/contract/snapshot.d.ts +0 -14
  16. package/lib/types/client/index.d.ts +12 -3
  17. package/lib/types/client/index.js +16 -5
  18. package/lib/types/client/scope.d.ts +13 -3
  19. package/lib/types/client/scope.js +15 -22
  20. package/lib/types/client/sessions/lineage.d.ts +5 -5
  21. package/lib/types/client/sessions/lineage.js +3 -4
  22. package/lib/types/client/sessions/manager.d.ts +91 -105
  23. package/lib/types/client/sessions/manager.js +307 -434
  24. package/lib/types/client/sessions/projection-store.d.ts +34 -21
  25. package/lib/types/client/sessions/projection-store.js +48 -24
  26. package/lib/types/client/sessions/remotes.d.ts +1 -2
  27. package/lib/types/client/sessions/service.d.ts +53 -150
  28. package/lib/types/client/sessions/service.js +267 -264
  29. package/lib/types/client/sessions/session.d.ts +24 -35
  30. package/lib/types/client/sessions/session.js +131 -73
  31. package/lib/types/commands.d.ts +6 -4
  32. package/lib/types/commands.js +82 -40
  33. package/lib/types/control.d.ts +2 -4
  34. package/lib/types/control.js +2 -84
  35. package/lib/types/history.js +1 -1
  36. package/lib/types/index.d.ts +34 -7
  37. package/lib/types/index.js +118 -14
  38. package/lib/types/list.js +24 -15
  39. package/lib/types/skill-catalog.js +60 -50
  40. package/lib/types/types.d.ts +40 -40
  41. package/lib/types/types.js +0 -1
  42. package/package.json +71 -76
  43. package/lib/types/client/sessions/queue-mirror.d.ts +0 -26
  44. package/lib/types/client/sessions/queue-mirror.js +0 -61
@@ -3,7 +3,7 @@ import type { AttachmentIdType, ImageAttachmentRef } from '@deepseek-ai/dsh-atta
3
3
  import type { SubagentAddress } from '@deepseek-ai/dsh-subagent/client';
4
4
  import type { MessageId } from '@deepseek-ai/dsh-llm/brand';
5
5
  import { SessionSeq, type SessionId } from '@deepseek-ai/dsh-session/types';
6
- import type { PromptContentPart, QueueAction, SessionControlFrame, SessionQueuedItem, SessionRequestId } from '../../types.ts';
6
+ import type { PromptContentPart, QueueAction, SessionRequestId } from '../../types.ts';
7
7
  import type { BeginSubmissionInput, SessionFace, SubmissionHandle } from '../contract/session.ts';
8
8
  import type { SessionSnapshot } from '../contract/snapshot.ts';
9
9
  import { MutableSessionEventSource } from '../contract/events.ts';
@@ -18,15 +18,12 @@ export declare const JUMP_PAGE_MESSAGES = 200;
18
18
  export interface SessionOptions {
19
19
  /** Catalog-discovered address selecting non-activating subagent transport. */
20
20
  address?: SubagentAddress;
21
- /** Whether the exact direct parent Agent was live at the latest catalog read; absent before that read. */
21
+ /** Whether the exact direct parent Agent is available in Host summaries; absent until known. */
22
22
  parentAvailable?: boolean;
23
23
  /**
24
- * First ACCEPTED prompt on a blank session (fires at most once, on the
25
- * prompt RPC's success response): the manager mirrors the blank→false flip
26
- * into its list row so the session surfaces without waiting for a host
27
- * frame. Acceptance is the flip point because it proves the user message
28
- * is in the host log; a rejected first prompt keeps the session blank
29
- * (hidden, still reusable by connectWorkspace).
24
+ * Publish each accepted prompt to the Manager, including after this Session
25
+ * object is replaced. Acceptance converts display state, but does not
26
+ * establish that a turn started or reached durable history.
30
27
  */
31
28
  onEngaged?(session: Session): void;
32
29
  /**
@@ -59,8 +56,8 @@ export declare class Session implements SessionFace {
59
56
  private jumpTargetSeq;
60
57
  /** The running jump loop's completion, shared by retargeting callers. */
61
58
  private jumpPromise;
62
- /** Authoritative stream-only inbox snapshot; pending work never hits history. */
63
- private readonly queueMirror;
59
+ private pendingHistory;
60
+ private readonly stopObservingInbox;
64
61
  private readonly assistantStream;
65
62
  private running;
66
63
  private address;
@@ -73,7 +70,7 @@ export declare class Session implements SessionFace {
73
70
  private promptAttempted;
74
71
  /** A first accepted prompt stays in the engaging phase until its turn is observable. */
75
72
  private firstPromptPendingTurn;
76
- /** Empty-log mirror (see ConversationSnapshot.blank); unknown bare sessions begin conservatively blank. */
73
+ /** New Session display state; unknown bare sessions begin conservatively blank. */
77
74
  private blankBit;
78
75
  private removed;
79
76
  private promptError;
@@ -81,7 +78,7 @@ export declare class Session implements SessionFace {
81
78
  /** Local submission echoes, insertion-ordered (see SessionSnapshot.pendingSubmissions). */
82
79
  private pendingSubmissions;
83
80
  /** Per-echo settlement state; `retiring` latches the first observation so a
84
- * queue frame and its durable event cannot both retire one echo. */
81
+ * Inbox projection and its durable event cannot both retire one echo. */
85
82
  private readonly submissionSettlements;
86
83
  /** Owns the addressed page/follow lifecycle while this Session is open. */
87
84
  private events;
@@ -89,8 +86,9 @@ export declare class Session implements SessionFace {
89
86
  * Per-session projection value store (push model; see the session-projection
90
87
  * subsystem page, docs/subsystems/session-projection.md): finished whole
91
88
  * values computed on the Host, seeded by the tail page's
92
- * projections block and updated by Session Controller control frames under the
93
- * one higher-seq-wins rule. Keys are read via `projections.faceOf(key)`
89
+ * projections block and updated by Session Controller control frames;
90
+ * Host-sequenced writes merge under higher-seq-wins and cached list blocks
91
+ * yield to them (projection-store.ts). Keys are read via `projections.faceOf(key)`
94
92
  * (the useProjection resolution face); the conversation snapshot never
95
93
  * carries projection values, and no client-side domain folding exists.
96
94
  * Manager-owned when constructed through SessionManager (frames route and
@@ -199,7 +197,7 @@ export declare class Session implements SessionFace {
199
197
  /** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
200
198
  loadThrough(seq: SessionSeq): Promise<void>;
201
199
  /** Rebuild an opened history source after address replacement.
202
- * Invalidates any in-flight open first; queue state belongs to the independently
200
+ * Invalidates any in-flight open first; projection state belongs to the independently
203
201
  * reconnecting control stream and remains untouched. */
204
202
  resync(): Promise<void>;
205
203
  /**
@@ -213,18 +211,6 @@ export declare class Session implements SessionFace {
213
211
  * @returns the cached reference (stable until the next flush).
214
212
  */
215
213
  getSnapshot(): SessionSnapshot;
216
- /**
217
- * Replace every transient control value for this Session from one stream baseline.
218
- * @param queue - complete pending queue for this Session.
219
- */
220
- replaceControl(queue: readonly SessionQueuedItem[]): void;
221
- /**
222
- * Apply one Session-addressed live control update.
223
- * @param frame - queue replacement addressed to this Session.
224
- */
225
- handleControlFrame(frame: Extract<SessionControlFrame, {
226
- type: 'queue';
227
- }>): void;
228
214
  /**
229
215
  * Running-bit relay from the host stream (list entry and snapshot stay consistent).
230
216
  * @param running - the new running state.
@@ -243,11 +229,12 @@ export declare class Session implements SessionFace {
243
229
  */
244
230
  handleSubagentParentAvailable(available: boolean): void;
245
231
  /**
246
- * Blank-bit relay from the authoritative summary source (`session.list` and
247
- * `api-session/added`). Monotone: once any signal (local first send,
248
- * running flip, an earlier summary) cleared it, a stale true never
249
- * re-blanks.
250
- * @param blank - the summary's derived empty-log bit.
232
+ * Apply the Manager's effective display blank, further reconciled with the
233
+ * current `sessionListMetadata` projection. Local send attempts and current
234
+ * running state prevent re-blanking; an earlier false summary alone does not.
235
+ * The Manager retains acceptance and earlier running observations across
236
+ * Session-object replacement.
237
+ * @param blank - New Session display state after Manager reconciliation.
251
238
  */
252
239
  handleBlank(blank: boolean): void;
253
240
  /** `api-session/removed` relay: flag the snapshot while retaining the resident instance. */
@@ -273,10 +260,12 @@ export declare class Session implements SessionFace {
273
260
  private prependWindow;
274
261
  /** Append one stream-validated live event. */
275
262
  private appendLive;
276
- /** Retire the matching echo when a durable browser-prompt `user/message` becomes visible. */
263
+ /** Observe durable acceptance even when insertion and claim share one projection notification. */
277
264
  private observeSubmissionEvent;
278
- /** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
279
- private observeSubmissionQueue;
265
+ private observeSteeringInsertions;
266
+ private observeSubmissionMessage;
267
+ /** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
268
+ private observeSubmissionInbox;
280
269
  /**
281
270
  * Latch one observed settlement and remove the echo an animation frame
282
271
  * later. The delay keeps the echo in the snapshot until the frame in which
@@ -8,7 +8,6 @@ import { isRemoteFailure } from '@deepseek-ai/dsh-api-gateway/client';
8
8
  import { RemoteError } from '@deepseek-ai/dsh-typert-protocol';
9
9
  import { ProjectionValueStore } from "./projection-store.js";
10
10
  import { resolvedClientTimeZone } from "../time-zone.js";
11
- import { SessionQueueMirror } from "./queue-mirror.js";
12
11
  import { ClientAssistantStream, } from "./assistant-stream.js";
13
12
  function projectionsBaseline(value) {
14
13
  return {
@@ -44,8 +43,8 @@ export class Session {
44
43
  jumpTargetSeq = null;
45
44
  /** The running jump loop's completion, shared by retargeting callers. */
46
45
  jumpPromise = null;
47
- /** Authoritative stream-only inbox snapshot; pending work never hits history. */
48
- queueMirror = new SessionQueueMirror();
46
+ pendingHistory = null;
47
+ stopObservingInbox;
49
48
  assistantStream = new ClientAssistantStream();
50
49
  running = false;
51
50
  address;
@@ -58,7 +57,7 @@ export class Session {
58
57
  promptAttempted = false;
59
58
  /** A first accepted prompt stays in the engaging phase until its turn is observable. */
60
59
  firstPromptPendingTurn = false;
61
- /** Empty-log mirror (see ConversationSnapshot.blank); unknown bare sessions begin conservatively blank. */
60
+ /** New Session display state; unknown bare sessions begin conservatively blank. */
62
61
  blankBit = true;
63
62
  removed = false;
64
63
  promptError = null;
@@ -66,7 +65,7 @@ export class Session {
66
65
  /** Local submission echoes, insertion-ordered (see SessionSnapshot.pendingSubmissions). */
67
66
  pendingSubmissions = [];
68
67
  /** Per-echo settlement state; `retiring` latches the first observation so a
69
- * queue frame and its durable event cannot both retire one echo. */
68
+ * Inbox projection and its durable event cannot both retire one echo. */
70
69
  submissionSettlements = new Map();
71
70
  /** Owns the addressed page/follow lifecycle while this Session is open. */
72
71
  events;
@@ -74,8 +73,9 @@ export class Session {
74
73
  * Per-session projection value store (push model; see the session-projection
75
74
  * subsystem page, docs/subsystems/session-projection.md): finished whole
76
75
  * values computed on the Host, seeded by the tail page's
77
- * projections block and updated by Session Controller control frames under the
78
- * one higher-seq-wins rule. Keys are read via `projections.faceOf(key)`
76
+ * projections block and updated by Session Controller control frames;
77
+ * Host-sequenced writes merge under higher-seq-wins and cached list blocks
78
+ * yield to them (projection-store.ts). Keys are read via `projections.faceOf(key)`
79
79
  * (the useProjection resolution face); the conversation snapshot never
80
80
  * carries projection values, and no client-side domain folding exists.
81
81
  * Manager-owned when constructed through SessionManager (frames route and
@@ -111,6 +111,9 @@ export class Session {
111
111
  this.snapshotCache = this.buildSnapshot();
112
112
  });
113
113
  this.snapshotCache = this.buildSnapshot();
114
+ this.stopObservingInbox = this.projections.faceOf('inbox').subscribe(() => {
115
+ this.observeSubmissionInbox();
116
+ });
114
117
  }
115
118
  /**
116
119
  * Bind the Agent-scoped context minted by ClientSessions (single write;
@@ -139,16 +142,15 @@ export class Session {
139
142
  */
140
143
  beginSubmission(input) {
141
144
  const requestId = randomUUID();
145
+ const placement = this.running ? input.mode === 'steer' ? 'steering' : 'queued' : 'transcript';
142
146
  this.pendingSubmissions = [...this.pendingSubmissions, {
143
147
  requestId,
144
- placement: this.running
145
- ? input.mode === 'steer' ? 'steering' : 'queued'
146
- : 'transcript',
148
+ placement,
147
149
  time: Date.now(),
148
150
  text: input.text,
149
151
  attachments: input.attachments,
150
152
  }];
151
- this.submissionSettlements.set(requestId, { onRetire: input.onRetire, retiring: false });
153
+ this.submissionSettlements.set(requestId, { placement, onRetire: input.onRetire, retiring: false });
152
154
  // The blank → engaging edge flips here, ahead of prompt(): the composer
153
155
  // docks and the echo renders on the click's own frame.
154
156
  this.promptAttempted = true;
@@ -212,19 +214,12 @@ export class Session {
212
214
  this.notifier.markDirty();
213
215
  return result;
214
216
  }
215
- // Blank flips on ACCEPTANCE, not attempt: an accepted prompt starts the
216
- // conversation's first turn on the host (the host criterion — a logged
217
- // turn/start — is fact, not optimism; standalone command and projection
218
- // events never flip it), while a rejected first prompt must keep the
219
- // session blank — the client-side blank mirror only ever lowers, so
220
- // flipping early on a failure would surface the session forever and
221
- // strip its connectWorkspace reuse eligibility against the host's
222
- // authority.
217
+ // Rejection must leave a first prompt blank and eligible for workspace reuse.
223
218
  if (this.blankBit) {
224
219
  this.blankBit = false;
225
- this.options.onEngaged?.(this);
226
220
  this.notifier.markDirty();
227
221
  }
222
+ this.options.onEngaged?.(this);
228
223
  return result;
229
224
  }
230
225
  /**
@@ -346,6 +341,15 @@ export class Session {
346
341
  // loop starts here.
347
342
  if (this.loadingOlder)
348
343
  return Promise.resolve();
344
+ const events = this.events;
345
+ if (events === undefined)
346
+ return Promise.resolve();
347
+ const pending = {
348
+ beforeSeq: this.baseSeq,
349
+ hasMore: this.hasMore,
350
+ pages: [],
351
+ };
352
+ this.pendingHistory = pending;
349
353
  this.jumpTargetSeq = seq;
350
354
  this.loadingOlder = true;
351
355
  this.notifier.markDirty();
@@ -355,17 +359,14 @@ export class Session {
355
359
  const generation = this.openGeneration;
356
360
  this.jumpPromise = (async () => {
357
361
  try {
358
- while (this.hasMore && this.jumpTargetSeq !== null && this.baseSeq > this.jumpTargetSeq) {
362
+ while (pending.hasMore && this.jumpTargetSeq !== null && pending.beforeSeq > this.jumpTargetSeq) {
359
363
  if (generation !== this.openGeneration)
360
364
  return;
361
- const events = this.events;
362
- if (events === undefined)
363
- return;
364
- const before = this.baseSeq;
365
- await events.prepend({ beforeSeq: this.baseSeq, maxMessages: JUMP_PAGE_MESSAGES });
365
+ const before = pending.beforeSeq;
366
+ await events.prepend({ beforeSeq: before, maxMessages: JUMP_PAGE_MESSAGES });
366
367
  // No-progress guard: an empty or dropped page that still claims more
367
368
  // history must end the loop, not spin it.
368
- if (this.baseSeq >= before)
369
+ if (pending.beforeSeq >= before)
369
370
  return;
370
371
  }
371
372
  }
@@ -377,14 +378,18 @@ export class Session {
377
378
  finally {
378
379
  this.jumpTargetSeq = null;
379
380
  this.jumpPromise = null;
381
+ this.pendingHistory = null;
380
382
  this.loadingOlder = false;
383
+ if (generation === this.openGeneration && pending.pages.length > 0) {
384
+ this.prependWindow(pending.pages.reverse().flat(), pending.hasMore);
385
+ }
381
386
  this.notifier.markDirty();
382
387
  }
383
388
  })();
384
389
  return this.jumpPromise;
385
390
  }
386
391
  /** Rebuild an opened history source after address replacement.
387
- * Invalidates any in-flight open first; queue state belongs to the independently
392
+ * Invalidates any in-flight open first; projection state belongs to the independently
388
393
  * reconnecting control stream and remains untouched. */
389
394
  async resync() {
390
395
  if (this.openState === 'cold')
@@ -418,31 +423,12 @@ export class Session {
418
423
  return this.snapshotCache;
419
424
  }
420
425
  // ---- Manager-only entry points (@internal; never called by the UI) ----
421
- /**
422
- * Replace every transient control value for this Session from one stream baseline.
423
- * @param queue - complete pending queue for this Session.
424
- */
425
- replaceControl(queue) {
426
- this.queueMirror.replace(queue);
427
- this.observeSubmissionQueue(queue);
428
- this.notifier.markDirty();
429
- }
430
- /**
431
- * Apply one Session-addressed live control update.
432
- * @param frame - queue replacement addressed to this Session.
433
- */
434
- handleControlFrame(frame) {
435
- this.queueMirror.replace(frame.items);
436
- this.observeSubmissionQueue(frame.items);
437
- this.notifier.markDirty();
438
- }
439
426
  /**
440
427
  * Running-bit relay from the host stream (list entry and snapshot stay consistent).
441
428
  * @param running - the new running state.
442
429
  */
443
430
  handleRunning(running) {
444
- // Turn-start conversion: a blank session never runs, so the first
445
- // running:true proves another side's first message landed.
431
+ // Running converts display state without establishing durable turn history.
446
432
  if (running && this.blankBit) {
447
433
  this.blankBit = false;
448
434
  this.notifier.markDirty();
@@ -482,13 +468,15 @@ export class Session {
482
468
  this.notifier.markDirty();
483
469
  }
484
470
  /**
485
- * Blank-bit relay from the authoritative summary source (`session.list` and
486
- * `api-session/added`). Monotone: once any signal (local first send,
487
- * running flip, an earlier summary) cleared it, a stale true never
488
- * re-blanks.
489
- * @param blank - the summary's derived empty-log bit.
471
+ * Apply the Manager's effective display blank, further reconciled with the
472
+ * current `sessionListMetadata` projection. Local send attempts and current
473
+ * running state prevent re-blanking; an earlier false summary alone does not.
474
+ * The Manager retains acceptance and earlier running observations across
475
+ * Session-object replacement.
476
+ * @param blank - New Session display state after Manager reconciliation.
490
477
  */
491
478
  handleBlank(blank) {
479
+ blank = blank && this.projections.values().sessionListMetadata?.blank !== false;
492
480
  if (blank === this.blankBit)
493
481
  return;
494
482
  if (blank && (this.promptAttempted || this.running))
@@ -514,6 +502,7 @@ export class Session {
514
502
  * @returns when the Remote iterator has completed teardown.
515
503
  */
516
504
  async dispose() {
505
+ this.stopObservingInbox();
517
506
  // Unsettled echoes retire as failed so their owners can restore or
518
507
  // release browser resources; echoes already scheduled as observed keep
519
508
  // that settlement.
@@ -586,13 +575,31 @@ export class Session {
586
575
  const visible = this.assistantStream.replace(entries, assistantStream);
587
576
  this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
588
577
  this.hasMore = hasMore;
578
+ if (this.pendingHistory !== null) {
579
+ this.pendingHistory.beforeSeq = this.baseSeq;
580
+ this.pendingHistory.hasMore = hasMore;
581
+ this.pendingHistory.pages.length = 0;
582
+ }
589
583
  if (visible.some(entry => entry.event.type === 'turn/start'))
590
584
  this.firstPromptPendingTurn = false;
591
585
  if (projections !== undefined)
592
586
  this.projections.seed(projections);
593
587
  this.eventSource.replace(visible, hasMore);
588
+ // A new follow baseline replaces optimistic steering with Host-owned rows.
589
+ // Receipt-backed inputs are accepted, not failed, even if their history is outside this window.
590
+ if (projections !== undefined) {
591
+ for (const [requestId, { receipt }] of this.submissionSettlements) {
592
+ if (receipt !== undefined && receipt.seq <= projections.asOfSeq) {
593
+ this.scheduleObservedRetirement(requestId, receipt.attachments);
594
+ }
595
+ }
596
+ }
594
597
  for (const entry of visible)
595
598
  this.observeSubmissionEvent(entry.event);
599
+ if (projections !== undefined) {
600
+ const inbox = projections.values.inbox;
601
+ this.observeSteeringInsertions(inbox?.['next-step'] ?? [], 0, projections.asOfSeq);
602
+ }
596
603
  this.notifier.markDirty();
597
604
  }
598
605
  publishAssistantEntry(result) {
@@ -625,6 +632,13 @@ export class Session {
625
632
  }
626
633
  /** Prepend one stream-validated history page. */
627
634
  prependWindow(entries, hasMore) {
635
+ if (this.pendingHistory !== null) {
636
+ const pending = this.pendingHistory;
637
+ pending.beforeSeq = entries[0] === undefined ? pending.beforeSeq : SessionLogOffset(entries[0].event.seq);
638
+ pending.hasMore = hasMore;
639
+ pending.pages.push(entries);
640
+ return;
641
+ }
628
642
  this.baseSeq = entries[0] === undefined ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
629
643
  this.hasMore = hasMore;
630
644
  this.eventSource.prepend(entries, hasMore);
@@ -635,36 +649,78 @@ export class Session {
635
649
  const awaitingFirstTurn = this.firstPromptPendingTurn;
636
650
  if (event.type === 'turn/start')
637
651
  this.firstPromptPendingTurn = false;
638
- const queueChanged = this.queueMirror.acceptDurable(event);
639
652
  this.eventSource.append(entry);
640
653
  // After the feed append: the conversation assembly's animation frame is
641
654
  // registered by the feed subscribers above, so the echo-retirement frame
642
655
  // scheduled here always runs after the durable node became renderable.
643
656
  this.observeSubmissionEvent(event);
644
- return queueChanged || awaitingFirstTurn !== this.firstPromptPendingTurn;
657
+ return awaitingFirstTurn !== this.firstPromptPendingTurn;
645
658
  }
646
- /** Retire the matching echo when a durable browser-prompt `user/message` becomes visible. */
659
+ /** Observe durable acceptance even when insertion and claim share one projection notification. */
647
660
  observeSubmissionEvent(event) {
648
- if (this.submissionSettlements.size === 0 || event.type !== 'user/message')
661
+ if (this.submissionSettlements.size === 0)
662
+ return;
663
+ if (event.type === 'agent/inbox/spliced') {
664
+ const { target, start, removedCount = 0, inserted, outcome } = event.data;
665
+ if (target === 'next-step') {
666
+ for (const [requestId, settlement] of this.submissionSettlements) {
667
+ const receipt = settlement.receipt;
668
+ if (receipt === undefined || receipt.index === null || receipt.seq >= event.seq)
669
+ continue;
670
+ const removed = receipt.index >= start && receipt.index < start + removedCount;
671
+ if (removed && outcome === 'canceled')
672
+ this.retireFailedSubmission(requestId);
673
+ else
674
+ settlement.receipt = {
675
+ ...receipt,
676
+ seq: event.seq,
677
+ index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount,
678
+ };
679
+ }
680
+ this.observeSteeringInsertions(inserted, start, event.seq);
681
+ }
682
+ for (const message of inserted)
683
+ this.observeSubmissionMessage(message, false);
684
+ return;
685
+ }
686
+ if (event.type === 'request/context' || event.type === 'turn/end') {
687
+ for (const [requestId, settlement] of this.submissionSettlements) {
688
+ if (settlement.receipt?.index === null && settlement.receipt.seq < event.seq)
689
+ this.retireFailedSubmission(requestId);
690
+ }
649
691
  return;
650
- // Structural read: window entries may be compact history records, so the
651
- // fields are narrowed rather than trusted (same posture as Conversation
652
- // assembly matchers).
653
- const data = event.data;
654
- const source = data?.source;
655
- if (source?.kind !== 'user' || typeof source.rpcId !== 'string')
692
+ }
693
+ if (event.type === 'user/message')
694
+ this.observeSubmissionMessage(event.data, true);
695
+ }
696
+ observeSteeringInsertions(messages, start, seq) {
697
+ for (const [index, message] of messages.entries()) {
698
+ const source = message.source;
699
+ if (source.kind !== 'user' || !('rpcId' in source))
700
+ continue;
701
+ const settlement = this.submissionSettlements.get(source.rpcId);
702
+ if (settlement?.placement !== 'steering' || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq)
703
+ continue;
704
+ settlement.receipt = { seq, index: start + index, attachments: attachmentRefsIn(message.content) };
705
+ }
706
+ }
707
+ observeSubmissionMessage(message, admitted) {
708
+ const source = message.source;
709
+ if (source.kind !== 'user' || !('rpcId' in source))
656
710
  return;
657
- this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(data?.content));
711
+ if (!admitted && this.submissionSettlements.get(source.rpcId)?.placement === 'steering')
712
+ return;
713
+ this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
658
714
  }
659
- /** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
660
- observeSubmissionQueue(items) {
715
+ /** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
716
+ observeSubmissionInbox() {
661
717
  if (this.submissionSettlements.size === 0)
662
718
  return;
663
- for (const item of items) {
664
- if (item.rpcId !== undefined) {
665
- this.scheduleObservedRetirement(item.rpcId, attachmentRefsIn(item.message.content));
666
- }
667
- }
719
+ const inbox = this.projections.get('inbox');
720
+ if (inbox === undefined)
721
+ return;
722
+ for (const message of [...inbox['next-turn'], ...inbox['next-step']])
723
+ this.observeSubmissionMessage(message, false);
668
724
  }
669
725
  /**
670
726
  * Latch one observed settlement and remove the echo an animation frame
@@ -713,15 +769,17 @@ export class Session {
713
769
  this.notifier.markDirty();
714
770
  }
715
771
  buildSnapshot() {
772
+ const identity = this.projections.values().subagent;
716
773
  return {
717
774
  sessionId: this.sessionId,
718
- queue: this.queueMirror.snapshot(),
719
775
  pendingSubmissions: this.pendingSubmissions,
720
776
  running: this.running,
721
777
  subagent: this.address === undefined
722
778
  ? null
723
779
  : {
724
- address: this.address,
780
+ address: this.address.mode === 'unknown' && identity != null
781
+ ? { ...this.address, mode: identity.mode }
782
+ : this.address,
725
783
  ...(this.parentAvailable === undefined ? {} : { parentAvailable: this.parentAvailable }),
726
784
  },
727
785
  removed: this.removed,
@@ -32,8 +32,10 @@ export declare class SessionCommandController {
32
32
  */
33
33
  rename(request: SessionRenameRequest): Promise<SessionRenameValue>;
34
34
  /**
35
- * Create a new ordinary Session from one completed-turn prefix.
36
- * @param request - source Session and optional event anchor.
35
+ * Create a new ordinary Session from an exact event prefix. An explicit
36
+ * `atSeq` is the inclusive cut; an omitted value selects the latest
37
+ * completed-turn prefix. An open cut receives synthetic fork closers.
38
+ * @param request - source Session and optional exact event boundary.
37
39
  * @returns the new Session identity.
38
40
  */
39
41
  fork(request: SessionForkRequest): Promise<SessionForkValue>;
@@ -50,11 +52,11 @@ export declare class SessionCommandController {
50
52
  */
51
53
  attachment(request: SessionAttachmentRequest): Promise<SessionAttachmentValue>;
52
54
  /**
53
- * Mutate one still-pending queue occurrence without resuming a cold Agent.
55
+ * Mutate one pending Inbox occurrence, restoring an ordinary cold Agent when needed.
54
56
  * @param request - Session, queue item, and requested mutation.
55
57
  * @returns acknowledgement that the queue mutation was applied.
56
58
  */
57
- updateQueue(request: SessionUpdateQueueRequest): SessionUpdateQueueValue;
59
+ updateQueue(request: SessionUpdateQueueRequest): Promise<SessionUpdateQueueValue>;
58
60
  /**
59
61
  * Cancel one live ordinary Agent while retaining pending inbox work.
60
62
  * @param request - Session whose active Agent turn is cancelled.