@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
package/lib/client.js CHANGED
@@ -34,6 +34,27 @@ window.__ModuleLoader__.load({
34
34
  this.name = "RemoteError";
35
35
  }
36
36
  };
37
+ /** Generic invocation-owned values returned by synchronous Client Context resolvers. */
38
+ /** Shared identity across independently bundled Context providers and Gateway. */
39
+ const TYPERT_OWNED_VALUE = Symbol.for("dsh.typert.owned-value");
40
+ /**
41
+ * Transfer cleanup ownership without adding another resource reference count.
42
+ * @param value - resolved payload passed to the invocation.
43
+ * @param release - non-throwing synchronous release, called at most once.
44
+ * @returns an owned payload disposed after invocation and reply settlement.
45
+ */
46
+ function typertOwnedValue(value, release) {
47
+ let active = true;
48
+ return {
49
+ [TYPERT_OWNED_VALUE]: true,
50
+ value,
51
+ [Symbol.dispose]() {
52
+ if (!active) return;
53
+ active = false;
54
+ release();
55
+ }
56
+ };
57
+ }
37
58
  //#endregion
38
59
  //#region lib/types/client/sessions/history-records.js
39
60
  /** Client range access and type narrowing for aligned Session history records. */
@@ -128,6 +149,7 @@ window.__ModuleLoader__.load({
128
149
  "compaction/start",
129
150
  "compaction/summary",
130
151
  "deliverables/presented",
152
+ "developer/message",
131
153
  "feedback/message-delete",
132
154
  "feedback/message-put",
133
155
  "feedback/record",
@@ -170,13 +192,15 @@ window.__ModuleLoader__.load({
170
192
  "turn/end",
171
193
  "turn/start",
172
194
  "user/message",
173
- "web/deepseek-search-llm-request"
195
+ "web/deepseek-search-llm-request",
196
+ "workspace/changes"
174
197
  ]);
175
198
  //#endregion
176
199
  //#region ../../core/session/lib/types/surface.js
177
200
  /** Runtime counterpart of the message-producing event union. */
178
201
  const SURFACE_EVENT_TYPES = new Set([
179
202
  "system/message",
203
+ "developer/message",
180
204
  "user/message",
181
205
  "assistant/message",
182
206
  "tool/result"
@@ -184,7 +208,7 @@ window.__ModuleLoader__.load({
184
208
  /**
185
209
  * Whether an event type can join the model-visible surface.
186
210
  * @param type - event type to test.
187
- * @returns true for one of the four message-producing event types.
211
+ * @returns true for one of the message-producing event types.
188
212
  */
189
213
  function isSurfaceEligibleType(type) {
190
214
  return SURFACE_EVENT_TYPES.has(type);
@@ -194,14 +218,33 @@ window.__ModuleLoader__.load({
194
218
  return typeof value === "object" && value !== null && !Array.isArray(value);
195
219
  }
196
220
  /**
197
- * Reject noncanonical request-header fields and contradictory tool failure metadata.
221
+ * Reject noncanonical request-header fields, developer roles/content, and contradictory tool failure metadata.
198
222
  * This does not validate complete event payloads or embedded provider streams.
199
223
  * @param event - event whose locally related payload fields are inspected.
200
224
  * @param subject - event location to include in validation errors.
201
- * @throws when request data/header is not an object, optional header fields are empty, or tool failure metadata contradicts its message.
225
+ * @throws when request-header fields, developer roles/content, or tool failure metadata are invalid.
202
226
  */
203
227
  function validateSessionEventData(event, subject) {
204
228
  const data = event.data;
229
+ if (SURFACE_EVENT_TYPES.has(event.type) && isRecord(data)) {
230
+ const message = event.type === "user/message" ? data : data["message"];
231
+ if (isRecord(message)) {
232
+ if (event.type === "developer/message" !== (message["role"] === "developer")) throw new Error(`${subject} developer/message and developer role must occur together`);
233
+ if (message["role"] !== "developer" && Array.isArray(message["content"]) && message["content"].some((block) => isRecord(block) && (block["type"] === "tool-addition" || block["type"] === "tool-removal"))) throw new Error(`${subject} tool-change blocks require developer role`);
234
+ if (event.type === "developer/message" && Array.isArray(message["content"])) {
235
+ let hasAdditions = false;
236
+ for (const block of message["content"]) {
237
+ if (!isRecord(block) || block["type"] !== "tool-addition" && block["type"] !== "tool-removal") continue;
238
+ if (typeof block["toolName"] !== "string" || block["toolName"].length === 0) throw new Error(`${subject} ${block["type"]} requires a nonempty toolName`);
239
+ if (block["type"] === "tool-addition") {
240
+ hasAdditions = true;
241
+ if (Object.hasOwn(block, "tool")) throw new Error(`${subject} tool-addition must omit inline tool definitions`);
242
+ }
243
+ }
244
+ if (hasAdditions ? !isEventSeq(data["headerSeq"]) : Object.hasOwn(data, "headerSeq")) throw new Error(`${subject} requires headerSeq exactly when tool additions are present`);
245
+ }
246
+ }
247
+ }
205
248
  if (event.type === "request/header") {
206
249
  if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
207
250
  const header = data["header"];
@@ -214,9 +257,7 @@ window.__ModuleLoader__.load({
214
257
  if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
215
258
  if (data["error"] === void 0) return;
216
259
  const message = data["message"];
217
- const content = isRecord(message) ? message["content"] : void 0;
218
- const block = Array.isArray(content) ? content[0] : void 0;
219
- if (!isRecord(block) || block["isError"] !== true) throw new Error(`${subject} error requires message content[0].isError === true`);
260
+ if (!isRecord(message) || message["isError"] !== true) throw new Error(`${subject} error requires message.isError === true`);
220
261
  }
221
262
  }
222
263
  /** Whether a runtime value is a non-negative safe event sequence. */
@@ -309,7 +350,6 @@ window.__ModuleLoader__.load({
309
350
  }
310
351
  //#endregion
311
352
  //#region lib/types/types.js
312
- /** Browser-safe request, result, and lifecycle vocabulary for the Session Remote service. */
313
353
  /** Maximum number of Sessions returned by one search. */
314
354
  const SESSION_SEARCH_RESULT_LIMIT = 20;
315
355
  /** Maximum search snippet length in Unicode code points. */
@@ -454,23 +494,7 @@ window.__ModuleLoader__.load({
454
494
  }
455
495
  //#endregion
456
496
  //#region lib/types/client/scope.js
457
- /**
458
- * Client Agent-scope primitive: mint a Cordis context tagged with the owning
459
- * Agent's identity. The mechanism mirrors the host `dsh-scope` architecture
460
- * (no-op plugin fiber + context tag + `Context.filter` routing predicate);
461
- * the shape deliberately diverges: the filter lives on the actx itself
462
- * instead of a separate carrier object, so scoped dispatch is plain cordis —
463
- * `actx.bail(actx, event, payload)` / `actx.emit(actx, ...)` — with no
464
- * wrapper. The host needs a detached carrier because its dispatch subject is
465
- * the business Agent object; client scope events carry only ids, so the
466
- * actx is the natural subject. The second divergence stands: the scope key
467
- * is the branded `SessionId` (value compared), not an object identity — the
468
- * agent and its session share one id (1:1, same axis; no separate AgentId
469
- * brand), and a client scope's identity IS that wire id. Third divergence,
470
- * deliberate: the client scopes the Agent IDENTITY, not a live Agent object
471
- * — a cold session's host Agent is already disposed while its client actx
472
- * stays alive for history viewing.
473
- */
497
+ /** Client scope generations route local events independently of Host Agent residency. */
474
498
  /** Context tag written by {@link createScope}. */
475
499
  const kScope = Symbol("dsh.client.scope");
476
500
  /** Shared no-op plugin backing each Agent scope fiber. */
@@ -478,21 +502,22 @@ window.__ModuleLoader__.load({
478
502
  /**
479
503
  * Mint an Agent scope under `ctx`: a no-op plugin fiber whose context
480
504
  * carries the agent tag and the dispatch filter — untagged listeners are
481
- * admitted globally, tagged listeners only for a matching agent.
505
+ * admitted globally, tagged listeners only for the same Client generation.
482
506
  * Registrations through the returned ctx dispose with the fiber.
483
507
  * @param ctx - client root context the scope fiber mounts under.
484
- * @param key - owning agent identity (the routing tag; agent id === session id).
508
+ * @param key - durable Session identity carried by this generation.
485
509
  * @returns the tagged context and its backing fiber.
486
510
  */
487
511
  function createScope(ctx, key) {
488
512
  const fiber = ctx.plugin(agentScope);
513
+ const identity = { sessionId: key };
489
514
  return {
490
515
  fiber,
491
516
  ctx: fiber.ctx.extend({
492
- [kScope]: key,
517
+ [kScope]: identity,
493
518
  [_deepseek_ai_cordis.Context.filter](listenerCtx) {
494
- const tag = scopeOf(listenerCtx);
495
- return tag === void 0 || tag === key;
519
+ const tag = scopeIdentityOf(listenerCtx);
520
+ return tag === void 0 || tag === identity;
496
521
  }
497
522
  })
498
523
  };
@@ -503,6 +528,14 @@ window.__ModuleLoader__.load({
503
528
  * @returns its agent identity (the session id), or undefined for root contexts.
504
529
  */
505
530
  function scopeOf(ctx) {
531
+ return scopeIdentityOf(ctx)?.sessionId;
532
+ }
533
+ /**
534
+ * Read the exact generation identity inherited by a Client Context.
535
+ * @param ctx - scoped or root Client Context.
536
+ * @returns the generation identity, or undefined for an unscoped Context.
537
+ */
538
+ function scopeIdentityOf(ctx) {
506
539
  return ctx[kScope];
507
540
  }
508
541
  //#endregion
@@ -543,16 +576,219 @@ window.__ModuleLoader__.load({
543
576
  return merged;
544
577
  }
545
578
  //#endregion
579
+ //#region ../../util/values/lib/index.js
580
+ /** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
581
+ /**
582
+ * Mark an unreachable closed-union branch.
583
+ * @param value - impossible value; an unhandled typed variant fails at the call site.
584
+ * @param context - optional switch-site label included in the failure message.
585
+ * @returns never; a runtime value that escaped its type always throws.
586
+ */
587
+ function assertNever(value, context) {
588
+ const rendered = JSON.stringify(value) ?? String(value);
589
+ throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
590
+ }
591
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
592
+ function hasIntrinsicConstructor(prototype, name) {
593
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
594
+ if (typeof constructor !== "function") return false;
595
+ try {
596
+ return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
597
+ } catch {
598
+ return false;
599
+ }
600
+ }
601
+ /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
602
+ function isIntrinsicObjectPrototype(value) {
603
+ return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
604
+ }
605
+ /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
606
+ function hasPlainArrayPrototype(value) {
607
+ const prototype = Object.getPrototypeOf(value);
608
+ if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
609
+ const objectPrototype = Object.getPrototypeOf(prototype);
610
+ return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
611
+ }
612
+ /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
613
+ function hasPlainObjectPrototype(value) {
614
+ const prototype = Object.getPrototypeOf(value);
615
+ return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
616
+ }
617
+ /** Return every JSON-visible object key, or reject own data JSON would discard. */
618
+ function enumerableStringKeys(value) {
619
+ const keys = Reflect.ownKeys(value);
620
+ if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
621
+ return keys;
622
+ }
623
+ /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
624
+ function walkJsonValue(value, detach) {
625
+ const ancestors = /* @__PURE__ */ new Set();
626
+ let root;
627
+ const assign = (destination, item) => {
628
+ if (destination === void 0) return;
629
+ if (destination.kind === "root") root = item;
630
+ else if (destination.kind === "array") destination.target[destination.index] = item;
631
+ else Object.defineProperty(destination.target, destination.key, {
632
+ value: item,
633
+ enumerable: true,
634
+ configurable: true,
635
+ writable: true
636
+ });
637
+ };
638
+ const tasks = [{
639
+ kind: "visit",
640
+ value,
641
+ ...detach ? { destination: { kind: "root" } } : {}
642
+ }];
643
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
644
+ if (task.kind === "leave") {
645
+ ancestors.delete(task.source);
646
+ continue;
647
+ }
648
+ if (task.kind === "array-item") {
649
+ if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
650
+ tasks.push({
651
+ kind: "visit",
652
+ value: task.source[task.index],
653
+ ...task.target === void 0 ? {} : { destination: {
654
+ kind: "array",
655
+ target: task.target,
656
+ index: task.index
657
+ } }
658
+ });
659
+ continue;
660
+ }
661
+ if (task.kind === "object-property") {
662
+ tasks.push({
663
+ kind: "visit",
664
+ value: task.source[task.key],
665
+ ...task.target === void 0 ? {} : { destination: {
666
+ kind: "object",
667
+ target: task.target,
668
+ key: task.key
669
+ } }
670
+ });
671
+ continue;
672
+ }
673
+ const current = task.value;
674
+ if (current === null) {
675
+ assign(task.destination, null);
676
+ continue;
677
+ }
678
+ if (typeof current === "boolean" || typeof current === "string") {
679
+ assign(task.destination, current);
680
+ continue;
681
+ }
682
+ if (typeof current === "number") {
683
+ if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
684
+ assign(task.destination, current);
685
+ continue;
686
+ }
687
+ if (typeof current !== "object") return void 0;
688
+ if (ancestors.has(current)) return void 0;
689
+ if (Array.isArray(current)) {
690
+ if (!hasPlainArrayPrototype(current)) return void 0;
691
+ const length = current.length;
692
+ if (Reflect.ownKeys(current).length !== length + 1) return void 0;
693
+ const target = detach ? [] : void 0;
694
+ if (target !== void 0) assign(task.destination, target);
695
+ ancestors.add(current);
696
+ tasks.push({
697
+ kind: "leave",
698
+ source: current
699
+ });
700
+ for (let index = length - 1; index >= 0; index--) tasks.push({
701
+ kind: "array-item",
702
+ source: current,
703
+ index,
704
+ ...target === void 0 ? {} : { target }
705
+ });
706
+ continue;
707
+ }
708
+ if (!hasPlainObjectPrototype(current)) return void 0;
709
+ const keys = enumerableStringKeys(current);
710
+ if (keys === void 0) return void 0;
711
+ const target = detach ? {} : void 0;
712
+ if (target !== void 0) assign(task.destination, target);
713
+ ancestors.add(current);
714
+ tasks.push({
715
+ kind: "leave",
716
+ source: current
717
+ });
718
+ for (let index = keys.length - 1; index >= 0; index--) {
719
+ const key = keys[index];
720
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
721
+ if (key === void 0) return void 0;
722
+ tasks.push({
723
+ kind: "object-property",
724
+ source: current,
725
+ key,
726
+ ...target === void 0 ? {} : { target }
727
+ });
728
+ }
729
+ }
730
+ return detach ? root : true;
731
+ }
732
+ /**
733
+ * Validate and detach lossless JSON in one read per property.
734
+ * @param value - candidate value to validate and detach.
735
+ * @returns the detached snapshot, or `undefined` when the value is not losslessly JSON-serializable.
736
+ */
737
+ function snapshotJsonValue(value) {
738
+ return walkJsonValue(value, true);
739
+ }
740
+ /**
741
+ * Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
742
+ * @param value - value to freeze.
743
+ * @returns the same value after every reachable enumerable child is frozen.
744
+ */
745
+ function deepFreeze(value) {
746
+ const seen = /* @__PURE__ */ new WeakSet();
747
+ const pending = [{
748
+ kind: "visit",
749
+ node: value
750
+ }];
751
+ while (pending.length > 0) {
752
+ const task = pending.pop();
753
+ /* v8 ignore next -- the loop condition guarantees one pending task. */
754
+ if (task === void 0) continue;
755
+ if (task.kind === "property") {
756
+ pending.push({
757
+ kind: "visit",
758
+ node: task.source[task.key]
759
+ });
760
+ continue;
761
+ }
762
+ const node = task.node;
763
+ if (node === null || typeof node !== "object") continue;
764
+ if (node instanceof AbortSignal) continue;
765
+ if (seen.has(node)) continue;
766
+ seen.add(node);
767
+ Object.freeze(node);
768
+ const keys = Object.keys(node);
769
+ for (let index = keys.length - 1; index >= 0; index--) {
770
+ const key = keys[index];
771
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
772
+ if (key === void 0) continue;
773
+ pending.push({
774
+ kind: "property",
775
+ source: node,
776
+ key
777
+ });
778
+ }
779
+ }
780
+ return value;
781
+ }
782
+ //#endregion
546
783
  //#region lib/types/client/sessions/lineage.js
547
784
  /**
548
785
  * Summaries -> flat list with lineage indentation. Root and sibling order
549
786
  * follows the established input order; this projection never re-sorts a
550
787
  * hydrated list from mutable timestamps.
551
788
  * @param summaries - the host's session.list items.
552
- * @param completed - sessions with a pending completion reminder (manager-owned live fact; absent = false).
553
789
  * @returns display rows in render order.
554
790
  */
555
- function flattenLineage(summaries, completed) {
791
+ function flattenLineage(summaries) {
556
792
  const byId = /* @__PURE__ */ new Map();
557
793
  for (const s of summaries) byId.set(s.sessionId, s);
558
794
  const children = /* @__PURE__ */ new Map();
@@ -570,9 +806,9 @@ window.__ModuleLoader__.load({
570
806
  return;
571
807
  }
572
808
  visited.add(s.sessionId);
809
+ const { agentAvailable: _agentAvailable, ...row } = s;
573
810
  out.push({
574
- ...s,
575
- completed: completed?.has(s.sessionId) ?? false,
811
+ ...row,
576
812
  depth
577
813
  });
578
814
  const kids = children.get(s.sessionId);
@@ -675,10 +911,14 @@ window.__ModuleLoader__.load({
675
911
  //#region lib/types/client/sessions/projection-store.js
676
912
  /**
677
913
  * One session's projection values. Framework semantics, uniform across every
678
- * key: a baseline seeds rows at its cut, a push frame updates one row, and in
679
- * both paths a lower-or-equal seq loses — a replayed frame cannot regress a
680
- * value, a stale baseline cannot overwrite a newer frame. A key the store has
681
- * never seen reads `undefined` (capability absent). Faces are identity-stable
914
+ * key. Sequenced writes (a baseline seeds rows at its cut, a push frame
915
+ * updates one row) compare seqs among themselves: a lower-or-equal seq within
916
+ * the Host generation loses, so a replayed frame cannot regress a value and a
917
+ * stale baseline cannot overwrite a newer frame. Cached writes (the session
918
+ * list's zero-I/O block) only fill keys no sequenced row holds, and a baseline
919
+ * discards every cached row before it seeds, regardless of seq: the connected
920
+ * Session is the truth and a cached value never outranks it. A key the store
921
+ * has never seen reads `undefined` (capability absent). Faces are identity-stable
682
922
  * per key (create-on-demand, cached) so the React side binds each exactly
683
923
  * once; the store-level channel (`subscribeAny`) serves coarse consumers (the
684
924
  * manager's list projection reads the `title` key).
@@ -733,41 +973,60 @@ window.__ModuleLoader__.load({
733
973
  */
734
974
  apply(key, value, seq) {
735
975
  const row = this.rows.get(key);
736
- if (row !== void 0 && seq <= row.seq) return;
976
+ if (row?.kind === "sequenced" && seq <= row.seq) return;
737
977
  this.rows.set(key, {
978
+ kind: "sequenced",
738
979
  value,
739
980
  seq
740
981
  });
741
982
  this.changed(key);
742
983
  }
743
984
  /**
744
- * Seed from a history tail page's projections block: every carried key
745
- * lands under the same seq rule as frames; a key the block omits is
746
- * capability-absent as of the cut — its row clears unless a newer frame
747
- * already superseded the cut (a stale baseline can neither overwrite nor
748
- * clear newer values).
985
+ * Fill keys from a session-list block the Host labeled `cached`: a zero-I/O
986
+ * view of the persisted checkpoint. A cached value lands only where no
987
+ * sequenced row exists: a connected Session has already answered for such
988
+ * a key, and the list's view of the persisted checkpoint cannot be newer
989
+ * than it.
990
+ * @param values - whole values by key viewed from the persisted checkpoint.
991
+ */
992
+ applyCached(values) {
993
+ for (const key of Object.keys(values)) {
994
+ if (this.rows.get(key)?.kind === "sequenced") continue;
995
+ this.rows.set(key, {
996
+ kind: "cached",
997
+ value: values[key]
998
+ });
999
+ this.changed(key);
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Seed from a history tail page's projections block. Every cached row is
1004
+ * discarded first, regardless of seq: the block comes from the connected
1005
+ * Session, and a value viewed from the persisted checkpoint never outranks
1006
+ * it. Then every carried key lands under the same seq rule as frames, and a
1007
+ * key the block omits is capability-absent as of the cut — its row clears
1008
+ * unless a newer frame already superseded the cut (a stale baseline can
1009
+ * neither overwrite nor clear newer sequenced values).
749
1010
  * @param baseline - the response's projections block.
750
1011
  */
751
1012
  seed(baseline) {
1013
+ for (const [key, row] of this.rows) {
1014
+ if (row.kind !== "cached") continue;
1015
+ this.rows.delete(key);
1016
+ this.changed(key);
1017
+ }
752
1018
  const values = baseline.values;
753
1019
  for (const key of Object.keys(values)) this.apply(key, values[key], baseline.asOfSeq);
754
1020
  for (const [key, row] of this.rows) {
755
1021
  if (Object.hasOwn(values, key)) continue;
756
- if (row.seq > baseline.asOfSeq) continue;
1022
+ if (row.kind === "sequenced" && row.seq > baseline.asOfSeq) continue;
757
1023
  this.rows.delete(key);
758
1024
  this.changed(key);
759
1025
  }
760
1026
  }
761
- /**
762
- * Drop rows beyond a replacement control baseline. Such rows describe
763
- * process state the Host lost before persisting it and would otherwise
764
- * outrank recomputed lower-seq values forever. The caller seeds the new
765
- * baseline immediately afterward.
766
- * @param lastSeq - highest durable sequence reflected by the baseline.
767
- */
768
- truncate(lastSeq) {
769
- for (const [key, row] of this.rows) {
770
- if (row.seq <= lastSeq) continue;
1027
+ /** Discard one Host generation's values and watermarks while preserving subscribed faces. */
1028
+ clear() {
1029
+ for (const key of this.rows.keys()) {
771
1030
  this.rows.delete(key);
772
1031
  this.changed(key);
773
1032
  }
@@ -890,312 +1149,65 @@ window.__ModuleLoader__.load({
890
1149
  });
891
1150
  }
892
1151
  /**
893
- * Prepend one older contiguous page.
894
- * @param entries - newly loaded older entries.
895
- * @param hasMore - whether still older history remains.
896
- */
897
- prepend(entries, hasMore) {
898
- this.window = concat(leaf(entries), this.window);
899
- this.publish(hasMore, {
900
- kind: "prepend",
901
- entries
902
- });
903
- }
904
- /**
905
- * Append one contiguous live entry.
906
- * @param entry - live tail entry.
907
- */
908
- append(entry) {
909
- const entries = [entry];
910
- this.window = concat(this.window, leaf(entries));
911
- this.publish(this.snapshot.hasMore, {
912
- kind: "append",
913
- entries
914
- });
915
- }
916
- /**
917
- * Replace one attempt's transient rows with its committed durable settlement.
918
- * @param attemptId - process-local attempt whose live rows are now redundant.
919
- * @param entry - durable settlement committed for that attempt.
920
- */
921
- settleAssistant(attemptId, entry) {
922
- const entries = materialize(this.window).filter((candidate) => candidate.type !== "transient" || candidate.event.data.attemptId !== attemptId);
923
- if (entry !== void 0) {
924
- const index = entries.findIndex((candidate) => candidate.event.seq > entry.event.seq);
925
- if (index < 0) entries.push(entry);
926
- else entries.splice(index, 0, entry);
927
- }
928
- this.window = leaf(entries);
929
- this.publish(this.snapshot.hasMore, {
930
- kind: "settle-assistant",
931
- attemptId,
932
- ...entry === void 0 ? {} : { entry }
933
- });
934
- }
935
- publish(hasMore, change) {
936
- this.snapshot = windowSnapshot(this.window, hasMore, this.snapshot.revision + 1, change);
937
- (0, _deepseek_ai_dsh_client_store.notifySubscribers)(this.listeners, "[session-controller] event feed");
938
- }
939
- };
940
- //#endregion
941
- //#region lib/types/client/time-zone.js
942
- /** Browser-owned time-zone sampling for one prompt RPC. */
943
- /**
944
- * Resolve the current browser IANA zone for one outbound operation.
945
- * @returns The browser-provided canonical zone.
946
- * @throws when the runtime cannot provide a non-empty zone.
947
- */
948
- function resolvedClientTimeZone() {
949
- const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
950
- if (typeof timeZone !== "string" || timeZone.length === 0) throw new Error("browser time zone is unavailable");
951
- return timeZone;
952
- }
953
- //#endregion
954
- //#region lib/types/client/sessions/queue-mirror.js
955
- const QUEUE_PREVIEW_CHARS = 200;
956
- function previewOf(content) {
957
- const flat = content.filter((block) => block.type !== "image" && block.type !== "file").map((block) => block.type === "text" ? block.text : `[${block.type}]`).join(" ").replace(/\s+/g, " ").trim();
958
- const chars = Array.from(flat);
959
- return chars.length > QUEUE_PREVIEW_CHARS ? `${chars.slice(0, QUEUE_PREVIEW_CHARS).join("")}…` : flat;
960
- }
961
- function textOf(content) {
962
- if (!content.every((block) => block.type === "text")) return null;
963
- return content.map((block) => block.text).join("");
964
- }
965
- /** Authoritative transient queue projection and durable steering handoff. */
966
- var SessionQueueMirror = class {
967
- current = [];
968
- /**
969
- * Return the current immutable queue projection.
970
- * @returns current queue rows.
971
- */
972
- snapshot() {
973
- return this.current;
974
- }
975
- /**
976
- * Replace from one authoritative stream queue frame.
977
- * @param items - complete host queue snapshot.
978
- */
979
- replace(items) {
980
- this.current = items.map((item) => {
981
- const content = item.message.content;
982
- return {
983
- id: item.id,
984
- messageId: item.message.id,
985
- placement: item.placement,
986
- ...item.rpcId === void 0 ? {} : { rpcId: item.rpcId },
987
- content,
988
- preview: previewOf(content),
989
- text: textOf(content)
990
- };
991
- });
992
- }
993
- /**
994
- * Retire a transient steering row once its durable message enters the log.
995
- * @param event - newly contiguous durable Session event.
996
- * @returns whether the projection changed.
997
- */
998
- acceptDurable(event) {
999
- if (event.type !== "user/message") return false;
1000
- const messageId = event.data.id;
1001
- const index = this.current.findIndex((item) => item.placement === "steering" && item.messageId === messageId);
1002
- if (index < 0) return false;
1003
- this.current = this.current.filter((_item, candidate) => candidate !== index);
1004
- return true;
1005
- }
1006
- };
1007
- //#endregion
1008
- //#region ../../util/values/lib/index.js
1009
- /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
1010
- function hasIntrinsicConstructor(prototype, name) {
1011
- const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
1012
- if (typeof constructor !== "function") return false;
1013
- try {
1014
- return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
1015
- } catch {
1016
- return false;
1017
- }
1018
- }
1019
- /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
1020
- function isIntrinsicObjectPrototype(value) {
1021
- return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
1022
- }
1023
- /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
1024
- function hasPlainArrayPrototype(value) {
1025
- const prototype = Object.getPrototypeOf(value);
1026
- if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
1027
- const objectPrototype = Object.getPrototypeOf(prototype);
1028
- return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
1029
- }
1030
- /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
1031
- function hasPlainObjectPrototype(value) {
1032
- const prototype = Object.getPrototypeOf(value);
1033
- return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
1034
- }
1035
- /** Return every JSON-visible object key, or reject own data JSON would discard. */
1036
- function enumerableStringKeys(value) {
1037
- const keys = Reflect.ownKeys(value);
1038
- if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
1039
- return keys;
1040
- }
1041
- /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
1042
- function walkJsonValue(value, detach) {
1043
- const ancestors = /* @__PURE__ */ new Set();
1044
- let root;
1045
- const assign = (destination, item) => {
1046
- if (destination === void 0) return;
1047
- if (destination.kind === "root") root = item;
1048
- else if (destination.kind === "array") destination.target[destination.index] = item;
1049
- else Object.defineProperty(destination.target, destination.key, {
1050
- value: item,
1051
- enumerable: true,
1052
- configurable: true,
1053
- writable: true
1054
- });
1055
- };
1056
- const tasks = [{
1057
- kind: "visit",
1058
- value,
1059
- ...detach ? { destination: { kind: "root" } } : {}
1060
- }];
1061
- for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
1062
- if (task.kind === "leave") {
1063
- ancestors.delete(task.source);
1064
- continue;
1065
- }
1066
- if (task.kind === "array-item") {
1067
- if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
1068
- tasks.push({
1069
- kind: "visit",
1070
- value: task.source[task.index],
1071
- ...task.target === void 0 ? {} : { destination: {
1072
- kind: "array",
1073
- target: task.target,
1074
- index: task.index
1075
- } }
1076
- });
1077
- continue;
1078
- }
1079
- if (task.kind === "object-property") {
1080
- tasks.push({
1081
- kind: "visit",
1082
- value: task.source[task.key],
1083
- ...task.target === void 0 ? {} : { destination: {
1084
- kind: "object",
1085
- target: task.target,
1086
- key: task.key
1087
- } }
1088
- });
1089
- continue;
1090
- }
1091
- const current = task.value;
1092
- if (current === null) {
1093
- assign(task.destination, null);
1094
- continue;
1095
- }
1096
- if (typeof current === "boolean" || typeof current === "string") {
1097
- assign(task.destination, current);
1098
- continue;
1099
- }
1100
- if (typeof current === "number") {
1101
- if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
1102
- assign(task.destination, current);
1103
- continue;
1104
- }
1105
- if (typeof current !== "object") return void 0;
1106
- if (ancestors.has(current)) return void 0;
1107
- if (Array.isArray(current)) {
1108
- if (!hasPlainArrayPrototype(current)) return void 0;
1109
- const length = current.length;
1110
- if (Reflect.ownKeys(current).length !== length + 1) return void 0;
1111
- const target = detach ? [] : void 0;
1112
- if (target !== void 0) assign(task.destination, target);
1113
- ancestors.add(current);
1114
- tasks.push({
1115
- kind: "leave",
1116
- source: current
1117
- });
1118
- for (let index = length - 1; index >= 0; index--) tasks.push({
1119
- kind: "array-item",
1120
- source: current,
1121
- index,
1122
- ...target === void 0 ? {} : { target }
1123
- });
1124
- continue;
1125
- }
1126
- if (!hasPlainObjectPrototype(current)) return void 0;
1127
- const keys = enumerableStringKeys(current);
1128
- if (keys === void 0) return void 0;
1129
- const target = detach ? {} : void 0;
1130
- if (target !== void 0) assign(task.destination, target);
1131
- ancestors.add(current);
1132
- tasks.push({
1133
- kind: "leave",
1134
- source: current
1135
- });
1136
- for (let index = keys.length - 1; index >= 0; index--) {
1137
- const key = keys[index];
1138
- /* v8 ignore next -- the loop is bounded by the captured key count. */
1139
- if (key === void 0) return void 0;
1140
- tasks.push({
1141
- kind: "object-property",
1142
- source: current,
1143
- key,
1144
- ...target === void 0 ? {} : { target }
1145
- });
1146
- }
1147
- }
1148
- return detach ? root : true;
1149
- }
1150
- /**
1151
- * Validate and detach lossless JSON in one read per property.
1152
- * @param value - candidate value to validate and detach.
1153
- * @returns the detached snapshot, or `undefined` when the value is not losslessly JSON-serializable.
1154
- */
1155
- function snapshotJsonValue(value) {
1156
- return walkJsonValue(value, true);
1157
- }
1158
- /**
1159
- * Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
1160
- * @param value - value to freeze.
1161
- * @returns the same value after every reachable enumerable child is frozen.
1162
- */
1163
- function deepFreeze(value) {
1164
- const seen = /* @__PURE__ */ new WeakSet();
1165
- const pending = [{
1166
- kind: "visit",
1167
- node: value
1168
- }];
1169
- while (pending.length > 0) {
1170
- const task = pending.pop();
1171
- /* v8 ignore next -- the loop condition guarantees one pending task. */
1172
- if (task === void 0) continue;
1173
- if (task.kind === "property") {
1174
- pending.push({
1175
- kind: "visit",
1176
- node: task.source[task.key]
1177
- });
1178
- continue;
1179
- }
1180
- const node = task.node;
1181
- if (node === null || typeof node !== "object") continue;
1182
- if (node instanceof AbortSignal) continue;
1183
- if (seen.has(node)) continue;
1184
- seen.add(node);
1185
- Object.freeze(node);
1186
- const keys = Object.keys(node);
1187
- for (let index = keys.length - 1; index >= 0; index--) {
1188
- const key = keys[index];
1189
- /* v8 ignore next -- the loop is bounded by the captured key count. */
1190
- if (key === void 0) continue;
1191
- pending.push({
1192
- kind: "property",
1193
- source: node,
1194
- key
1195
- });
1152
+ * Prepend one older contiguous page.
1153
+ * @param entries - newly loaded older entries.
1154
+ * @param hasMore - whether still older history remains.
1155
+ */
1156
+ prepend(entries, hasMore) {
1157
+ this.window = concat(leaf(entries), this.window);
1158
+ this.publish(hasMore, {
1159
+ kind: "prepend",
1160
+ entries
1161
+ });
1162
+ }
1163
+ /**
1164
+ * Append one contiguous live entry.
1165
+ * @param entry - live tail entry.
1166
+ */
1167
+ append(entry) {
1168
+ const entries = [entry];
1169
+ this.window = concat(this.window, leaf(entries));
1170
+ this.publish(this.snapshot.hasMore, {
1171
+ kind: "append",
1172
+ entries
1173
+ });
1174
+ }
1175
+ /**
1176
+ * Replace one attempt's transient rows with its committed durable settlement.
1177
+ * @param attemptId - process-local attempt whose live rows are now redundant.
1178
+ * @param entry - durable settlement committed for that attempt.
1179
+ */
1180
+ settleAssistant(attemptId, entry) {
1181
+ const entries = materialize(this.window).filter((candidate) => candidate.type !== "transient" || candidate.event.data.attemptId !== attemptId);
1182
+ if (entry !== void 0) {
1183
+ const index = entries.findIndex((candidate) => candidate.event.seq > entry.event.seq);
1184
+ if (index < 0) entries.push(entry);
1185
+ else entries.splice(index, 0, entry);
1196
1186
  }
1187
+ this.window = leaf(entries);
1188
+ this.publish(this.snapshot.hasMore, {
1189
+ kind: "settle-assistant",
1190
+ attemptId,
1191
+ ...entry === void 0 ? {} : { entry }
1192
+ });
1197
1193
  }
1198
- return value;
1194
+ publish(hasMore, change) {
1195
+ this.snapshot = windowSnapshot(this.window, hasMore, this.snapshot.revision + 1, change);
1196
+ (0, _deepseek_ai_dsh_client_store.notifySubscribers)(this.listeners, "[session-controller] event feed");
1197
+ }
1198
+ };
1199
+ //#endregion
1200
+ //#region lib/types/client/time-zone.js
1201
+ /** Browser-owned time-zone sampling for one prompt RPC. */
1202
+ /**
1203
+ * Resolve the current browser IANA zone for one outbound operation.
1204
+ * @returns The browser-provided canonical zone.
1205
+ * @throws when the runtime cannot provide a non-empty zone.
1206
+ */
1207
+ function resolvedClientTimeZone() {
1208
+ const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
1209
+ if (typeof timeZone !== "string" || timeZone.length === 0) throw new Error("browser time zone is unavailable");
1210
+ return timeZone;
1199
1211
  }
1200
1212
  //#endregion
1201
1213
  //#region ../../llm/llm/lib/types/assistant-stream.js
@@ -1527,8 +1539,8 @@ window.__ModuleLoader__.load({
1527
1539
  jumpTargetSeq = null;
1528
1540
  /** The running jump loop's completion, shared by retargeting callers. */
1529
1541
  jumpPromise = null;
1530
- /** Authoritative stream-only inbox snapshot; pending work never hits history. */
1531
- queueMirror = new SessionQueueMirror();
1542
+ pendingHistory = null;
1543
+ stopObservingInbox;
1532
1544
  assistantStream = new ClientAssistantStream();
1533
1545
  running = false;
1534
1546
  address;
@@ -1541,7 +1553,7 @@ window.__ModuleLoader__.load({
1541
1553
  promptAttempted = false;
1542
1554
  /** A first accepted prompt stays in the engaging phase until its turn is observable. */
1543
1555
  firstPromptPendingTurn = false;
1544
- /** Empty-log mirror (see ConversationSnapshot.blank); unknown bare sessions begin conservatively blank. */
1556
+ /** New Session display state; unknown bare sessions begin conservatively blank. */
1545
1557
  blankBit = true;
1546
1558
  removed = false;
1547
1559
  promptError = null;
@@ -1549,7 +1561,7 @@ window.__ModuleLoader__.load({
1549
1561
  /** Local submission echoes, insertion-ordered (see SessionSnapshot.pendingSubmissions). */
1550
1562
  pendingSubmissions = [];
1551
1563
  /** Per-echo settlement state; `retiring` latches the first observation so a
1552
- * queue frame and its durable event cannot both retire one echo. */
1564
+ * Inbox projection and its durable event cannot both retire one echo. */
1553
1565
  submissionSettlements = /* @__PURE__ */ new Map();
1554
1566
  /** Owns the addressed page/follow lifecycle while this Session is open. */
1555
1567
  events;
@@ -1557,8 +1569,9 @@ window.__ModuleLoader__.load({
1557
1569
  * Per-session projection value store (push model; see the session-projection
1558
1570
  * subsystem page, docs/subsystems/session-projection.md): finished whole
1559
1571
  * values computed on the Host, seeded by the tail page's
1560
- * projections block and updated by Session Controller control frames under the
1561
- * one higher-seq-wins rule. Keys are read via `projections.faceOf(key)`
1572
+ * projections block and updated by Session Controller control frames;
1573
+ * Host-sequenced writes merge under higher-seq-wins and cached list blocks
1574
+ * yield to them (projection-store.ts). Keys are read via `projections.faceOf(key)`
1562
1575
  * (the useProjection resolution face); the conversation snapshot never
1563
1576
  * carries projection values, and no client-side domain folding exists.
1564
1577
  * Manager-owned when constructed through SessionManager (frames route and
@@ -1594,6 +1607,9 @@ window.__ModuleLoader__.load({
1594
1607
  this.snapshotCache = this.buildSnapshot();
1595
1608
  });
1596
1609
  this.snapshotCache = this.buildSnapshot();
1610
+ this.stopObservingInbox = this.projections.faceOf("inbox").subscribe(() => {
1611
+ this.observeSubmissionInbox();
1612
+ });
1597
1613
  }
1598
1614
  /**
1599
1615
  * Bind the Agent-scoped context minted by ClientSessions (single write;
@@ -1620,14 +1636,16 @@ window.__ModuleLoader__.load({
1620
1636
  */
1621
1637
  beginSubmission(input) {
1622
1638
  const requestId = randomUUID();
1639
+ const placement = this.running ? input.mode === "steer" ? "steering" : "queued" : "transcript";
1623
1640
  this.pendingSubmissions = [...this.pendingSubmissions, {
1624
1641
  requestId,
1625
- placement: this.running ? input.mode === "steer" ? "steering" : "queued" : "transcript",
1642
+ placement,
1626
1643
  time: Date.now(),
1627
1644
  text: input.text,
1628
1645
  attachments: input.attachments
1629
1646
  }];
1630
1647
  this.submissionSettlements.set(requestId, {
1648
+ placement,
1631
1649
  onRetire: input.onRetire,
1632
1650
  retiring: false
1633
1651
  });
@@ -1695,9 +1713,9 @@ window.__ModuleLoader__.load({
1695
1713
  }
1696
1714
  if (this.blankBit) {
1697
1715
  this.blankBit = false;
1698
- this.options.onEngaged?.(this);
1699
1716
  this.notifier.markDirty();
1700
1717
  }
1718
+ this.options.onEngaged?.(this);
1701
1719
  return result;
1702
1720
  }
1703
1721
  /**
@@ -1825,36 +1843,44 @@ window.__ModuleLoader__.load({
1825
1843
  return this.jumpPromise;
1826
1844
  }
1827
1845
  if (this.loadingOlder) return Promise.resolve();
1846
+ const events = this.events;
1847
+ if (events === void 0) return Promise.resolve();
1848
+ const pending = {
1849
+ beforeSeq: this.baseSeq,
1850
+ hasMore: this.hasMore,
1851
+ pages: []
1852
+ };
1853
+ this.pendingHistory = pending;
1828
1854
  this.jumpTargetSeq = seq;
1829
1855
  this.loadingOlder = true;
1830
1856
  this.notifier.markDirty();
1831
1857
  const generation = this.openGeneration;
1832
1858
  this.jumpPromise = (async () => {
1833
1859
  try {
1834
- while (this.hasMore && this.jumpTargetSeq !== null && this.baseSeq > this.jumpTargetSeq) {
1860
+ while (pending.hasMore && this.jumpTargetSeq !== null && pending.beforeSeq > this.jumpTargetSeq) {
1835
1861
  if (generation !== this.openGeneration) return;
1836
- const events = this.events;
1837
- if (events === void 0) return;
1838
- const before = this.baseSeq;
1862
+ const before = pending.beforeSeq;
1839
1863
  await events.prepend({
1840
- beforeSeq: this.baseSeq,
1864
+ beforeSeq: before,
1841
1865
  maxMessages: 200
1842
1866
  });
1843
- if (this.baseSeq >= before) return;
1867
+ if (pending.beforeSeq >= before) return;
1844
1868
  }
1845
1869
  } catch (error) {
1846
1870
  if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) console.error("[session-controller] loadThrough failed:", error);
1847
1871
  } finally {
1848
1872
  this.jumpTargetSeq = null;
1849
1873
  this.jumpPromise = null;
1874
+ this.pendingHistory = null;
1850
1875
  this.loadingOlder = false;
1876
+ if (generation === this.openGeneration && pending.pages.length > 0) this.prependWindow(pending.pages.reverse().flat(), pending.hasMore);
1851
1877
  this.notifier.markDirty();
1852
1878
  }
1853
1879
  })();
1854
1880
  return this.jumpPromise;
1855
1881
  }
1856
1882
  /** Rebuild an opened history source after address replacement.
1857
- * Invalidates any in-flight open first; queue state belongs to the independently
1883
+ * Invalidates any in-flight open first; projection state belongs to the independently
1858
1884
  * reconnecting control stream and remains untouched. */
1859
1885
  async resync() {
1860
1886
  if (this.openState === "cold") return;
@@ -1886,24 +1912,6 @@ window.__ModuleLoader__.load({
1886
1912
  return this.snapshotCache;
1887
1913
  }
1888
1914
  /**
1889
- * Replace every transient control value for this Session from one stream baseline.
1890
- * @param queue - complete pending queue for this Session.
1891
- */
1892
- replaceControl(queue) {
1893
- this.queueMirror.replace(queue);
1894
- this.observeSubmissionQueue(queue);
1895
- this.notifier.markDirty();
1896
- }
1897
- /**
1898
- * Apply one Session-addressed live control update.
1899
- * @param frame - queue replacement addressed to this Session.
1900
- */
1901
- handleControlFrame(frame) {
1902
- this.queueMirror.replace(frame.items);
1903
- this.observeSubmissionQueue(frame.items);
1904
- this.notifier.markDirty();
1905
- }
1906
- /**
1907
1915
  * Running-bit relay from the host stream (list entry and snapshot stay consistent).
1908
1916
  * @param running - the new running state.
1909
1917
  */
@@ -1940,13 +1948,15 @@ window.__ModuleLoader__.load({
1940
1948
  this.notifier.markDirty();
1941
1949
  }
1942
1950
  /**
1943
- * Blank-bit relay from the authoritative summary source (`session.list` and
1944
- * `api-session/added`). Monotone: once any signal (local first send,
1945
- * running flip, an earlier summary) cleared it, a stale true never
1946
- * re-blanks.
1947
- * @param blank - the summary's derived empty-log bit.
1951
+ * Apply the Manager's effective display blank, further reconciled with the
1952
+ * current `sessionListMetadata` projection. Local send attempts and current
1953
+ * running state prevent re-blanking; an earlier false summary alone does not.
1954
+ * The Manager retains acceptance and earlier running observations across
1955
+ * Session-object replacement.
1956
+ * @param blank - New Session display state after Manager reconciliation.
1948
1957
  */
1949
1958
  handleBlank(blank) {
1959
+ blank = blank && this.projections.values().sessionListMetadata?.blank !== false;
1950
1960
  if (blank === this.blankBit) return;
1951
1961
  if (blank && (this.promptAttempted || this.running)) return;
1952
1962
  this.blankBit = blank;
@@ -1970,6 +1980,7 @@ window.__ModuleLoader__.load({
1970
1980
  * @returns when the Remote iterator has completed teardown.
1971
1981
  */
1972
1982
  async dispose() {
1983
+ this.stopObservingInbox();
1973
1984
  for (const requestId of [...this.submissionSettlements.keys()]) this.retireFailedSubmission(requestId);
1974
1985
  this.openGeneration++;
1975
1986
  const events = this.events;
@@ -2025,10 +2036,22 @@ window.__ModuleLoader__.load({
2025
2036
  const visible = this.assistantStream.replace(entries, assistantStream);
2026
2037
  this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
2027
2038
  this.hasMore = hasMore;
2039
+ if (this.pendingHistory !== null) {
2040
+ this.pendingHistory.beforeSeq = this.baseSeq;
2041
+ this.pendingHistory.hasMore = hasMore;
2042
+ this.pendingHistory.pages.length = 0;
2043
+ }
2028
2044
  if (visible.some((entry) => entry.event.type === "turn/start")) this.firstPromptPendingTurn = false;
2029
2045
  if (projections !== void 0) this.projections.seed(projections);
2030
2046
  this.eventSource.replace(visible, hasMore);
2047
+ if (projections !== void 0) {
2048
+ for (const [requestId, { receipt }] of this.submissionSettlements) if (receipt !== void 0 && receipt.seq <= projections.asOfSeq) this.scheduleObservedRetirement(requestId, receipt.attachments);
2049
+ }
2031
2050
  for (const entry of visible) this.observeSubmissionEvent(entry.event);
2051
+ if (projections !== void 0) {
2052
+ const inbox = projections.values.inbox;
2053
+ this.observeSteeringInsertions(inbox?.["next-step"] ?? [], 0, projections.asOfSeq);
2054
+ }
2032
2055
  this.notifier.markDirty();
2033
2056
  }
2034
2057
  publishAssistantEntry(result) {
@@ -2058,6 +2081,13 @@ window.__ModuleLoader__.load({
2058
2081
  }
2059
2082
  /** Prepend one stream-validated history page. */
2060
2083
  prependWindow(entries, hasMore) {
2084
+ if (this.pendingHistory !== null) {
2085
+ const pending = this.pendingHistory;
2086
+ pending.beforeSeq = entries[0] === void 0 ? pending.beforeSeq : SessionLogOffset(entries[0].event.seq);
2087
+ pending.hasMore = hasMore;
2088
+ pending.pages.push(entries);
2089
+ return;
2090
+ }
2061
2091
  this.baseSeq = entries[0] === void 0 ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
2062
2092
  this.hasMore = hasMore;
2063
2093
  this.eventSource.prepend(entries, hasMore);
@@ -2067,23 +2097,63 @@ window.__ModuleLoader__.load({
2067
2097
  const event = entry.event;
2068
2098
  const awaitingFirstTurn = this.firstPromptPendingTurn;
2069
2099
  if (event.type === "turn/start") this.firstPromptPendingTurn = false;
2070
- const queueChanged = this.queueMirror.acceptDurable(event);
2071
2100
  this.eventSource.append(entry);
2072
2101
  this.observeSubmissionEvent(event);
2073
- return queueChanged || awaitingFirstTurn !== this.firstPromptPendingTurn;
2102
+ return awaitingFirstTurn !== this.firstPromptPendingTurn;
2074
2103
  }
2075
- /** Retire the matching echo when a durable browser-prompt `user/message` becomes visible. */
2104
+ /** Observe durable acceptance even when insertion and claim share one projection notification. */
2076
2105
  observeSubmissionEvent(event) {
2077
- if (this.submissionSettlements.size === 0 || event.type !== "user/message") return;
2078
- const data = event.data;
2079
- const source = data?.source;
2080
- if (source?.kind !== "user" || typeof source.rpcId !== "string") return;
2081
- this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(data?.content));
2082
- }
2083
- /** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
2084
- observeSubmissionQueue(items) {
2085
2106
  if (this.submissionSettlements.size === 0) return;
2086
- for (const item of items) if (item.rpcId !== void 0) this.scheduleObservedRetirement(item.rpcId, attachmentRefsIn(item.message.content));
2107
+ if (event.type === "agent/inbox/spliced") {
2108
+ const { target, start, removedCount = 0, inserted, outcome } = event.data;
2109
+ if (target === "next-step") {
2110
+ for (const [requestId, settlement] of this.submissionSettlements) {
2111
+ const receipt = settlement.receipt;
2112
+ if (receipt === void 0 || receipt.index === null || receipt.seq >= event.seq) continue;
2113
+ const removed = receipt.index >= start && receipt.index < start + removedCount;
2114
+ if (removed && outcome === "canceled") this.retireFailedSubmission(requestId);
2115
+ else settlement.receipt = {
2116
+ ...receipt,
2117
+ seq: event.seq,
2118
+ index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount
2119
+ };
2120
+ }
2121
+ this.observeSteeringInsertions(inserted, start, event.seq);
2122
+ }
2123
+ for (const message of inserted) this.observeSubmissionMessage(message, false);
2124
+ return;
2125
+ }
2126
+ if (event.type === "request/context" || event.type === "turn/end") {
2127
+ for (const [requestId, settlement] of this.submissionSettlements) if (settlement.receipt?.index === null && settlement.receipt.seq < event.seq) this.retireFailedSubmission(requestId);
2128
+ return;
2129
+ }
2130
+ if (event.type === "user/message") this.observeSubmissionMessage(event.data, true);
2131
+ }
2132
+ observeSteeringInsertions(messages, start, seq) {
2133
+ for (const [index, message] of messages.entries()) {
2134
+ const source = message.source;
2135
+ if (source.kind !== "user" || !("rpcId" in source)) continue;
2136
+ const settlement = this.submissionSettlements.get(source.rpcId);
2137
+ if (settlement?.placement !== "steering" || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq) continue;
2138
+ settlement.receipt = {
2139
+ seq,
2140
+ index: start + index,
2141
+ attachments: attachmentRefsIn(message.content)
2142
+ };
2143
+ }
2144
+ }
2145
+ observeSubmissionMessage(message, admitted) {
2146
+ const source = message.source;
2147
+ if (source.kind !== "user" || !("rpcId" in source)) return;
2148
+ if (!admitted && this.submissionSettlements.get(source.rpcId)?.placement === "steering") return;
2149
+ this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
2150
+ }
2151
+ /** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
2152
+ observeSubmissionInbox() {
2153
+ if (this.submissionSettlements.size === 0) return;
2154
+ const inbox = this.projections.get("inbox");
2155
+ if (inbox === void 0) return;
2156
+ for (const message of [...inbox["next-turn"], ...inbox["next-step"]]) this.observeSubmissionMessage(message, false);
2087
2157
  }
2088
2158
  /**
2089
2159
  * Latch one observed settlement and remove the echo an animation frame
@@ -2132,13 +2202,16 @@ window.__ModuleLoader__.load({
2132
2202
  this.notifier.markDirty();
2133
2203
  }
2134
2204
  buildSnapshot() {
2205
+ const identity = this.projections.values().subagent;
2135
2206
  return {
2136
2207
  sessionId: this.sessionId,
2137
- queue: this.queueMirror.snapshot(),
2138
2208
  pendingSubmissions: this.pendingSubmissions,
2139
2209
  running: this.running,
2140
2210
  subagent: this.address === void 0 ? null : {
2141
- address: this.address,
2211
+ address: this.address.mode === "unknown" && identity != null ? {
2212
+ ...this.address,
2213
+ mode: identity.mode
2214
+ } : this.address,
2142
2215
  ...this.parentAvailable === void 0 ? {} : { parentAvailable: this.parentAvailable }
2143
2216
  },
2144
2217
  removed: this.removed,
@@ -2183,28 +2256,25 @@ window.__ModuleLoader__.load({
2183
2256
  }
2184
2257
  //#endregion
2185
2258
  //#region lib/types/client/sessions/manager.js
2259
+ /** Host catalog, durable projection caches, and explicitly retained Client instances. */
2186
2260
  function sessionSeqCursor(value) {
2187
2261
  return value === -1 ? -1 : SessionSeq(value);
2188
2262
  }
2189
- function catalogAvailability(parentAvailable) {
2190
- return parentAvailable === void 0 ? {} : { parentAvailable };
2191
- }
2192
2263
  /** Instance cluster + frame entry + the session list. */
2193
2264
  var SessionManager = class {
2194
2265
  remote;
2195
2266
  sessions = /* @__PURE__ */ new Map();
2196
2267
  /** In-flight Session disposals remain here after instances leave `sessions`, so manager disposal can await quiescence. */
2197
2268
  sessionDisposals = /* @__PURE__ */ new Set();
2198
- /** Latest transient queues, retained independently of Session object materialization. */
2199
- queues = /* @__PURE__ */ new Map();
2200
2269
  /**
2201
- * Sessions that finished running while not selected — the sidebar's green
2202
- * "done" reminder (manager-owned, survives connection generations; cleared
2203
- * on select and session-removed, re-armed by the next completion).
2270
+ * Accepted/running presentation must survive a later empty-history list
2271
+ * response. Host-asserted running is recorded even before a row, instance, or
2272
+ * address holds the identity — the listing that would hold it may not have
2273
+ * landed yet — while the client-local acceptance callback requires a current
2274
+ * holder, because it can arrive from a replaced or already-dropped Session.
2204
2275
  */
2205
- completedNotifications = /* @__PURE__ */ new Set();
2206
- /** Last-observed running bits per session; the true→false edge here arms {@link completedNotifications}. */
2207
- prevRunning = /* @__PURE__ */ new Map();
2276
+ engagedSessions = /* @__PURE__ */ new Set();
2277
+ disposed = false;
2208
2278
  /** Per-session projection value stores, retained independently of instance arrival (the
2209
2279
  * title-snapshot precedent, generalized): push frames land here whether or not the Session
2210
2280
  * is instantiated (list rows read the 'title' key), and an instantiated Session adopts the
@@ -2216,22 +2286,11 @@ window.__ModuleLoader__.load({
2216
2286
  listPhase = "pending";
2217
2287
  listError = null;
2218
2288
  listInflight = null;
2219
- /** Mutations arriving after a list request starts are replayed over its response. */
2289
+ /** Active list request's mutation log; its identity also fences completion after reconnect. */
2220
2290
  listMutations = null;
2221
2291
  addresses = /* @__PURE__ */ new Map();
2222
- catalogs = /* @__PURE__ */ new Map();
2223
- catalogInflight = /* @__PURE__ */ new Map();
2224
- /** Catalog owners whose membership changed while a pull was in flight: one trailing refresh after it settles. */
2225
- catalogStale = /* @__PURE__ */ new Set();
2226
- openCatalogs = /* @__PURE__ */ new Set();
2227
- catalogDebounce = /* @__PURE__ */ new Map();
2228
- /**
2229
- * Background jobs per session, last-wins from Session Controller's control
2230
- * stream. An empty set is stored as an absent key, so absence and `[]` are
2231
- * one representation.
2232
- */
2233
- jobsBySession = /* @__PURE__ */ new Map();
2234
- selected;
2292
+ projectionLoads = /* @__PURE__ */ new Map();
2293
+ projectionInflight = /* @__PURE__ */ new Map();
2235
2294
  listSnapshotCache;
2236
2295
  /** Entry-identity cache (reference stability): list rebuilds reuse the previous entry
2237
2296
  * object when every field matches — wire refreshes mint all-new summary objects, so identity
@@ -2241,69 +2300,35 @@ window.__ModuleLoader__.load({
2241
2300
  notifier = new Notifier(() => {
2242
2301
  this.listSnapshotCache = this.buildListSnapshot();
2243
2302
  });
2244
- /**
2245
- * @param remote - generated Remote namespaces the Session cluster calls.
2246
- * @param restoredSelection - persisted real-Session selection candidate.
2247
- */
2248
- constructor(remote, restoredSelection, restoredAddress) {
2303
+ /** @param remote - generated Remote namespaces used by catalog and history readers. */
2304
+ constructor(remote) {
2249
2305
  this.remote = remote;
2250
- this.selected = restoredSelection;
2251
- if (restoredAddress !== void 0) this.addresses.set(restoredAddress.childSessionId, restoredAddress);
2252
2306
  this.listSnapshotCache = this.buildListSnapshot();
2253
2307
  }
2254
2308
  /**
2255
- * Select a listed Session or a retained catalog-addressed child.
2256
- * @param sessionId - listed or catalog-addressed Session id.
2257
- */
2258
- select(sessionId) {
2259
- const address = this.navigationAddress(sessionId);
2260
- if (!this.summaries.some((summary) => summary.sessionId === sessionId) && address === void 0) throw new Error(`sessions.select: unknown session ${sessionId}`);
2261
- if (address !== void 0) this.addresses.set(sessionId, address);
2262
- this.sessions.get(sessionId)?.configureSubagent(address, address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.parentAvailable);
2263
- this.selected = sessionId;
2264
- this.completedNotifications.delete(sessionId);
2265
- this.refreshSubagents(sessionId);
2266
- this.notifier.notifyNow();
2267
- }
2268
- /**
2269
- * Select a healthy child through its durable direct-parent address.
2270
- * @param address - catalog-derived parent and child ids.
2271
- */
2272
- selectSubagent(address) {
2273
- const catalog = this.catalogs.get(address.parentSessionId);
2274
- const entry = catalog?.entries.find((candidate) => candidate.id === address.childSessionId);
2275
- if (entry === void 0 || entry.kind !== "child" || entry.mode !== address.mode) throw new Error(`sessions.selectSubagent: ${address.childSessionId} is not a healthy catalog child`);
2276
- this.addresses.set(address.childSessionId, address);
2277
- this.sessions.get(address.childSessionId)?.configureSubagent(address, catalog?.parentAvailable);
2278
- this.selected = address.childSessionId;
2279
- this.completedNotifications.delete(address.childSessionId);
2280
- this.refreshSubagents(address.childSessionId);
2281
- this.notifier.notifyNow();
2282
- }
2283
- /** Clear the selection (the layout falls to the no-session view state). */
2284
- clearSelection() {
2285
- this.selected = void 0;
2286
- this.notifier.notifyNow();
2287
- }
2288
- /**
2289
- * Return the durable catalog address retained for one child.
2290
- * @param sessionId - possible addressed child id.
2291
- * @returns The direct-parent address, when navigation discovered one.
2309
+ * Resolve an acquisition target without materializing a Session.
2310
+ * @param target - known identity or durable direct-parent address.
2311
+ * @returns the resolved identity with its explicit or catalog-derived history route installed.
2292
2312
  */
2293
- subagentAddress(sessionId) {
2294
- return this.addresses.get(sessionId);
2313
+ resolveTarget(target) {
2314
+ const id = typeof target === "string" ? target : target.childSessionId;
2315
+ const address = typeof target === "string" ? this.subagentAddress(id) : target;
2316
+ if (typeof target === "string" && !this.sessions.has(id) && !this.summaries.some((summary) => summary.sessionId === id) && address === void 0) throw new Error(`sessions.retain: unknown session ${id}`);
2317
+ if (address !== void 0) this.addresses.set(id, address);
2318
+ this.sessions.get(id)?.configureSubagent(address, address === void 0 ? void 0 : this.agentAvailable(address.parentSessionId));
2319
+ return id;
2295
2320
  }
2296
2321
  /**
2297
2322
  * Resolve an address for breadcrumb navigation without retaining transport authority.
2298
2323
  * @param sessionId - possible child id in an already-loaded catalog.
2299
2324
  * @returns A retained or catalog-derived direct-parent address.
2300
2325
  */
2301
- navigationAddress(sessionId) {
2326
+ subagentAddress(sessionId) {
2302
2327
  const retained = this.addresses.get(sessionId);
2303
2328
  if (retained !== void 0) return retained;
2304
- for (const [parentSessionId, catalog] of this.catalogs) {
2305
- const child = catalog.entries.find((entry) => entry.kind === "child" && entry.id === sessionId);
2306
- if (child?.kind === "child") return {
2329
+ for (const parentSessionId of this.projectionStores.keys()) {
2330
+ const child = this.projectionStores.get(parentSessionId)?.values().subagentCatalog?.find((entry) => entry.id === sessionId);
2331
+ if (child !== void 0) return {
2307
2332
  parentSessionId,
2308
2333
  childSessionId: sessionId,
2309
2334
  mode: child.mode
@@ -2311,27 +2336,35 @@ window.__ModuleLoader__.load({
2311
2336
  }
2312
2337
  }
2313
2338
  /**
2314
- * Drop a session instance (scope-prune companion: instance
2315
- * and scope share one lifecycle). The host session log is the durable
2316
- * truth — a later get() lazily rebuilds and open() backfills history.
2317
- * @param sessionId - the session to drop.
2339
+ * Withdraw an exact Client instance before running its teardown callbacks.
2340
+ * @param sessionId - identity to withdraw.
2341
+ * @param expected - instance being released; a replacement is left untouched.
2342
+ * @returns completion of the detached instance's stream teardown.
2318
2343
  */
2319
- async drop(sessionId) {
2344
+ drop(sessionId, expected) {
2320
2345
  const session = this.sessions.get(sessionId);
2346
+ if (session !== expected) return Promise.resolve();
2321
2347
  this.sessions.delete(sessionId);
2322
- if (session !== void 0) await this.startSessionDisposal(session);
2348
+ this.addresses.delete(sessionId);
2349
+ this.pruneEngagement(sessionId, this.retainedIds(this.summaries));
2350
+ return this.startSessionDisposal(session);
2323
2351
  }
2324
2352
  /**
2325
- * Stop owned timers and every remaining Session instance.
2326
- * @returns when every Session Remote iterator has completed teardown.
2353
+ * Stop catalog requests and dispose every resident Session.
2354
+ * @returns once catalog requests and every Session stream have stopped.
2327
2355
  */
2328
2356
  async dispose() {
2329
- for (const timer of this.catalogDebounce.values()) clearTimeout(timer);
2330
- this.catalogDebounce.clear();
2331
- this.catalogStale.clear();
2332
- this.openCatalogs.clear();
2357
+ this.disposed = true;
2358
+ this.listMutations = null;
2359
+ this.listInflight = null;
2360
+ this.engagedSessions.clear();
2361
+ const reads = [...this.projectionInflight.values()];
2362
+ for (const { controller } of reads) controller.abort();
2363
+ this.projectionInflight.clear();
2364
+ await Promise.all(reads.map((read) => read.promise));
2333
2365
  const sessions = [...this.sessions.values()];
2334
2366
  this.sessions.clear();
2367
+ this.addresses.clear();
2335
2368
  for (const session of sessions) this.startSessionDisposal(session);
2336
2369
  await this.drainSessionDisposals();
2337
2370
  }
@@ -2350,7 +2383,8 @@ window.__ModuleLoader__.load({
2350
2383
  }
2351
2384
  /**
2352
2385
  * Lazy build: return the existing instance or construct one (no auto-open —
2353
- * open is triggered by the container's select callback).
2386
+ * the reference allocator opens history after binding the scope).
2387
+ * New instances reconcile retained metadata before returning.
2354
2388
  * @param sessionId - the session to get.
2355
2389
  * @returns the resident instance.
2356
2390
  */
@@ -2359,45 +2393,73 @@ window.__ModuleLoader__.load({
2359
2393
  if (session === void 0) {
2360
2394
  session = this.createSession(sessionId);
2361
2395
  this.sessions.set(sessionId, session);
2362
- session.replaceControl(this.queues.get(sessionId) ?? []);
2363
2396
  const summary = this.summaries.find((s) => s.sessionId === sessionId);
2364
2397
  if (summary !== void 0) {
2365
- session.handleBlank(summary.blank);
2398
+ session.handleBlank(this.effectiveBlank(summary));
2366
2399
  session.handleRunning(summary.running);
2367
2400
  } else {
2368
2401
  const address = this.addresses.get(sessionId);
2369
- const child = address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.entries.find((entry) => entry.kind === "child" && entry.id === sessionId);
2370
- if (child?.kind === "child") {
2402
+ if ((address === void 0 ? void 0 : this.projectionStores.get(address.parentSessionId)?.values().subagentCatalog?.find((entry) => entry.id === sessionId)) !== void 0) {
2371
2403
  session.handleBlank(false);
2372
- session.handleRunning(child.activity === "running");
2373
- }
2404
+ session.handleRunning(false);
2405
+ } else session.handleBlank(true);
2374
2406
  }
2375
2407
  }
2376
2408
  return session;
2377
2409
  }
2378
2410
  createSession(sessionId) {
2379
2411
  const address = this.addresses.get(sessionId);
2380
- const parentAvailable = address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.parentAvailable;
2412
+ const parentAvailable = address === void 0 ? void 0 : this.agentAvailable(address.parentSessionId);
2381
2413
  return new Session(sessionId, this.remote, {
2382
2414
  ...address === void 0 ? {} : {
2383
2415
  address,
2384
- ...catalogAvailability(parentAvailable)
2416
+ ...parentAvailable === void 0 ? {} : { parentAvailable }
2385
2417
  },
2386
2418
  onEngaged: (engaged) => {
2387
- this.recordMutation({
2388
- kind: "engaged",
2389
- sessionId: engaged.sessionId
2390
- });
2419
+ if (this.disposed || !this.retainedIds(this.summaries).has(engaged.sessionId)) return;
2420
+ if (!this.engagedSessions.has(engaged.sessionId)) {
2421
+ this.engagedSessions.add(engaged.sessionId);
2422
+ this.recordMutation({
2423
+ kind: "engaged",
2424
+ sessionId: engaged.sessionId
2425
+ });
2426
+ }
2427
+ this.sessions.get(engaged.sessionId)?.handleBlank(false);
2391
2428
  },
2392
2429
  projections: this.projectionStore(sessionId)
2393
2430
  });
2394
2431
  }
2432
+ effectiveBlank(summary) {
2433
+ return summary.blank && !this.engagedSessions.has(summary.sessionId);
2434
+ }
2435
+ /**
2436
+ * Identities an engagement may still belong to: the given list rows, resident
2437
+ * Session instances, and retained child addresses.
2438
+ * @param summaries - list rows of the caller's snapshot.
2439
+ * @returns the retained identity set.
2440
+ */
2441
+ retainedIds(summaries) {
2442
+ const retained = new Set(summaries.map((summary) => summary.sessionId));
2443
+ for (const sessionId of this.sessions.keys()) retained.add(sessionId);
2444
+ for (const sessionId of this.addresses.keys()) retained.add(sessionId);
2445
+ return retained;
2446
+ }
2447
+ /**
2448
+ * Forget one engagement that no retained identity holds.
2449
+ * @param sessionId - identity whose engagement may be dropped.
2450
+ * @param retained - identities from {@link retainedIds} for the caller's snapshot.
2451
+ */
2452
+ pruneEngagement(sessionId, retained) {
2453
+ if (!retained.has(sessionId)) this.engagedSessions.delete(sessionId);
2454
+ }
2395
2455
  /** Resident per-session projection store (create-on-demand; outlives instantiation). */
2396
2456
  projectionStore(sessionId) {
2397
2457
  let store = this.projectionStores.get(sessionId);
2398
2458
  if (store === void 0) {
2399
2459
  store = new ProjectionValueStore();
2460
+ const projections = store;
2400
2461
  store.subscribeAny(() => {
2462
+ if (projections.values().sessionListMetadata?.blank === false) this.sessions.get(sessionId)?.handleBlank(false);
2401
2463
  this.notifier.markDirty();
2402
2464
  });
2403
2465
  this.projectionStores.set(sessionId, store);
@@ -2405,85 +2467,70 @@ window.__ModuleLoader__.load({
2405
2467
  return store;
2406
2468
  }
2407
2469
  /**
2408
- * Refresh one direct-child catalog, reusing its in-flight request.
2409
- * @param parentSessionId - catalog owner.
2470
+ * Load a complete projection baseline once per connection; retry unsuccessful reads.
2471
+ * @param sessionId - Session to inspect without opening its conversation.
2472
+ * @returns completion of the current or newly started read.
2410
2473
  */
2411
- refreshSubagents(parentSessionId) {
2412
- const existing = this.catalogInflight.get(parentSessionId);
2474
+ refreshProjections(sessionId) {
2475
+ const existing = this.projectionInflight.get(sessionId);
2413
2476
  if (existing !== void 0) return existing.promise;
2414
- const previous = this.catalogs.get(parentSessionId);
2415
- const expandableRows = /* @__PURE__ */ new Set();
2416
- const activityRows = /* @__PURE__ */ new Map();
2417
- this.catalogs.set(parentSessionId, {
2418
- entries: previous?.entries ?? [],
2419
- ...previous?.parentAvailable === void 0 ? {} : { parentAvailable: previous.parentAvailable },
2477
+ if (this.projectionLoads.get(sessionId)?.state === "ready") return Promise.resolve();
2478
+ const controller = new AbortController();
2479
+ const store = this.projectionStore(sessionId);
2480
+ const initialValues = store.values();
2481
+ this.projectionLoads.set(sessionId, {
2420
2482
  state: "loading",
2421
2483
  error: null
2422
2484
  });
2423
2485
  this.notifier.markDirty();
2424
2486
  const operation = (async () => {
2425
2487
  try {
2426
- const result = await this.remote.subagents.list(parentSessionId);
2488
+ const result = await this.remote.session.projections({ sessionId }, controller.signal);
2489
+ if (controller.signal.aborted) return;
2427
2490
  if (result.ok) {
2428
- const parentAvailable = this.catalogInflight.get(parentSessionId)?.parentAvailableOverride ?? result.value.parentAvailable;
2429
- this.catalogs.set(parentSessionId, {
2491
+ if (result.value !== null) store.seed({
2430
2492
  ...result.value,
2431
- entries: this.withCatalogMutations(result.value.entries, expandableRows, activityRows),
2432
- parentAvailable,
2493
+ asOfSeq: sessionSeqCursor(result.value.asOfSeq)
2494
+ });
2495
+ else if (store.values() === initialValues) store.clear();
2496
+ this.projectionLoads.set(sessionId, {
2433
2497
  state: "ready",
2434
2498
  error: null
2435
2499
  });
2436
- for (const [childId, address] of this.addresses) {
2437
- if (address.parentSessionId !== parentSessionId) continue;
2438
- this.sessions.get(childId)?.handleSubagentParentAvailable(parentAvailable);
2439
- }
2440
- } else this.catalogs.set(parentSessionId, {
2441
- entries: this.withCatalogMutations(previous?.entries ?? [], expandableRows, activityRows),
2442
- ...catalogAvailability(this.catalogInflight.get(parentSessionId)?.parentAvailableOverride ?? previous?.parentAvailable),
2500
+ } else this.projectionLoads.set(sessionId, {
2443
2501
  state: "error",
2444
2502
  error: result.error
2445
2503
  });
2446
2504
  } catch (error) {
2505
+ if (controller.signal.aborted) return;
2447
2506
  if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) throw error;
2448
- this.catalogs.set(parentSessionId, {
2449
- entries: this.withCatalogMutations(previous?.entries ?? [], expandableRows, activityRows),
2450
- ...catalogAvailability(this.catalogInflight.get(parentSessionId)?.parentAvailableOverride ?? previous?.parentAvailable),
2507
+ this.projectionLoads.set(sessionId, {
2451
2508
  state: "error",
2452
2509
  error
2453
2510
  });
2454
2511
  } finally {
2455
- this.catalogInflight.delete(parentSessionId);
2456
- if (this.catalogStale.delete(parentSessionId)) this.refreshSubagents(parentSessionId);
2457
- this.notifier.markDirty();
2512
+ if (!controller.signal.aborted) {
2513
+ this.projectionInflight.delete(sessionId);
2514
+ this.notifier.markDirty();
2515
+ }
2458
2516
  }
2459
2517
  })();
2460
- this.catalogInflight.set(parentSessionId, {
2518
+ this.projectionInflight.set(sessionId, {
2461
2519
  promise: operation,
2462
- expandableRows,
2463
- activityRows,
2464
- parentAvailableOverride: void 0
2520
+ controller
2465
2521
  });
2466
2522
  return operation;
2467
2523
  }
2468
- /**
2469
- * Mark whether a catalog menu is consuming live membership updates.
2470
- * @param parentSessionId - catalog owner.
2471
- * @param open - current menu state.
2472
- */
2473
- setSubagentCatalogOpen(parentSessionId, open) {
2474
- if (open) {
2475
- this.openCatalogs.add(parentSessionId);
2476
- this.refreshSubagents(parentSessionId);
2477
- } else {
2478
- this.openCatalogs.delete(parentSessionId);
2479
- const timer = this.catalogDebounce.get(parentSessionId);
2480
- if (timer !== void 0) {
2481
- clearTimeout(timer);
2482
- this.catalogDebounce.delete(parentSessionId);
2483
- }
2524
+ agentAvailable(sessionId) {
2525
+ return this.summaries.find((summary) => summary.sessionId === sessionId)?.agentAvailable ?? (this.listPhase === "ready" ? false : void 0);
2526
+ }
2527
+ updateParentAvailability() {
2528
+ for (const [childId, address] of this.addresses) {
2529
+ const available = this.agentAvailable(address.parentSessionId);
2530
+ if (available !== void 0) this.sessions.get(childId)?.handleSubagentParentAvailable(available);
2484
2531
  }
2485
2532
  }
2486
- /** Full refresh via session.list (single-flight: an in-flight call is reused). */
2533
+ /** Full refresh via session.list (single-flight within one Host generation). */
2487
2534
  refreshList() {
2488
2535
  if (this.listInflight !== null) return this.listInflight;
2489
2536
  this.listState = "loading";
@@ -2495,44 +2542,41 @@ window.__ModuleLoader__.load({
2495
2542
  this.listInflight = (async () => {
2496
2543
  try {
2497
2544
  const result = await this.remote.session.list({});
2545
+ if (this.listMutations !== mutations) return;
2498
2546
  if (result.ok) {
2499
2547
  const baseline = this.listPhase === "pending" ? [...result.value.items] : mergeOrderedBaseline(established, result.value.items, (summary) => summary.sessionId);
2500
- for (const s of baseline) if (!this.prevRunning.has(s.sessionId)) this.prevRunning.set(s.sessionId, s.running);
2501
- let summaries = baseline;
2502
- for (const mutation of mutations) {
2503
- summaries = applyMutation(summaries, mutation);
2504
- this.summaries = summaries;
2505
- this.syncCompletedNotifications();
2506
- }
2548
+ const removedSincePull = /* @__PURE__ */ new Set();
2549
+ for (const mutation of mutations) if (mutation.kind === "remove") removedSincePull.add(mutation.sessionId);
2550
+ for (const s of baseline) if (s.running && !removedSincePull.has(s.sessionId)) this.engagedSessions.add(s.sessionId);
2551
+ const summaries = mutations.reduce(applyMutation, baseline);
2507
2552
  this.summaries = summaries;
2553
+ const retained = this.retainedIds(summaries);
2554
+ for (const sessionId of this.engagedSessions) this.pruneEngagement(sessionId, retained);
2508
2555
  this.listState = "idle";
2509
2556
  this.listPhase = "ready";
2510
- this.syncCompletedNotifications();
2557
+ this.updateParentAvailability();
2511
2558
  for (const s of this.summaries) {
2512
2559
  const session = this.sessions.get(s.sessionId);
2513
2560
  if (session === void 0) continue;
2514
- session.handleBlank(s.blank);
2561
+ session.handleBlank(this.effectiveBlank(s));
2515
2562
  session.handleRunning(s.running);
2516
2563
  }
2517
- for (const s of result.value.items) {
2518
- const block = s.projections;
2519
- if (block === void 0) continue;
2520
- const store = this.projectionStore(s.sessionId);
2521
- const values = block.values;
2522
- for (const key of Object.keys(values)) store.apply(key, values[key], sessionSeqCursor(block.asOfSeq));
2523
- }
2564
+ for (const s of result.value.items) if (s.projections !== void 0) this.applyListBlock(s.sessionId, s.projections);
2524
2565
  } else {
2525
2566
  this.listState = "error";
2526
2567
  this.listError = result.error;
2527
2568
  }
2528
2569
  } catch (error) {
2529
2570
  if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) throw error;
2571
+ if (this.listMutations !== mutations) return;
2530
2572
  this.listState = "error";
2531
2573
  this.listError = error;
2532
2574
  } finally {
2533
- this.listMutations = null;
2534
- this.listInflight = null;
2535
- this.notifier.markDirty();
2575
+ if (this.listMutations === mutations) {
2576
+ this.listMutations = null;
2577
+ this.listInflight = null;
2578
+ this.notifier.markDirty();
2579
+ }
2536
2580
  }
2537
2581
  })();
2538
2582
  return this.listInflight;
@@ -2573,8 +2617,9 @@ window.__ModuleLoader__.load({
2573
2617
  };
2574
2618
  const result = await this.remote.session.create(payload);
2575
2619
  if (result.ok) this.recordMutation({
2576
- kind: "upsert",
2620
+ kind: "placeholder",
2577
2621
  summary: {
2622
+ agentAvailable: true,
2578
2623
  sessionId: result.value.sessionId,
2579
2624
  updatedAt: Date.now(),
2580
2625
  running: false,
@@ -2585,8 +2630,9 @@ window.__ModuleLoader__.load({
2585
2630
  else {
2586
2631
  const publishedSessionId = workspaceAttachSessionId(result.error);
2587
2632
  if (publishedSessionId !== void 0) this.recordMutation({
2588
- kind: "upsert",
2633
+ kind: "placeholder",
2589
2634
  summary: {
2635
+ agentAvailable: true,
2590
2636
  sessionId: publishedSessionId,
2591
2637
  updatedAt: Date.now(),
2592
2638
  running: false,
@@ -2598,11 +2644,12 @@ window.__ModuleLoader__.load({
2598
2644
  }
2599
2645
  /**
2600
2646
  * Contract session.fork; on success merge the child into summaries
2601
- * immediately (same synchronous-addressability guarantee as create). The
2602
- * child carries the source's history, so it is never blank; lineage rides
2603
- * parentSessionId so the list nests it under its source. A child published
2604
- * before Workspace attachment fails is also reconciled into the list.
2605
- * @param opts - source session and the optional seq anchoring the cut.
2647
+ * immediately (same synchronous-addressability guarantee as create).
2648
+ * Blankness starts provisionally true so the authoritative Host summary can
2649
+ * preserve it or lower it after an exact cut before the first `turn/start`;
2650
+ * lineage rides parentSessionId. A child published before Workspace
2651
+ * attachment fails is also reconciled into the list.
2652
+ * @param opts - source session and the optional exact inclusive boundary seq.
2606
2653
  * @returns the fork result (the child session id).
2607
2654
  */
2608
2655
  async fork(opts) {
@@ -2613,12 +2660,13 @@ window.__ModuleLoader__.load({
2613
2660
  });
2614
2661
  const childId = result.ok ? result.value.sessionId : workspaceAttachSessionId(result.error);
2615
2662
  if (childId !== void 0) this.recordMutation({
2616
- kind: "upsert",
2663
+ kind: "placeholder",
2617
2664
  summary: {
2665
+ agentAvailable: true,
2618
2666
  sessionId: childId,
2619
2667
  updatedAt: Date.now(),
2620
2668
  running: false,
2621
- blank: false,
2669
+ blank: true,
2622
2670
  parentSessionId: opts.sessionId,
2623
2671
  ...source?.cwd !== void 0 ? { cwd: source.cwd } : {}
2624
2672
  }
@@ -2626,22 +2674,35 @@ window.__ModuleLoader__.load({
2626
2674
  return result;
2627
2675
  }
2628
2676
  /**
2629
- * Insert-or-enrich a locally synthesized summary: a new id prepends; an
2630
- * existing entry only gains fields it lacks (the session-added frame and the
2631
- * create() echo race — whichever lands second must fill the placeholder's
2632
- * missing cwd/parentSessionId, never overwrite list-refresh data).
2677
+ * Rename a Session and update its title projection without opening its history.
2678
+ * @param sessionId - Session to rename.
2679
+ * @param title - raw title text for Host normalization.
2680
+ * @returns the accepted title and event position, or the Remote failure.
2681
+ */
2682
+ async rename(sessionId, title) {
2683
+ const result = await this.remote.session.rename({
2684
+ sessionId,
2685
+ title
2686
+ });
2687
+ if (result.ok) this.projectionStore(sessionId).apply("title", result.value.title, SessionSeq(result.value.seq));
2688
+ return result;
2689
+ }
2690
+ /**
2691
+ * Merge a Host summary, replacing live state and filling missing metadata.
2692
+ * Local create/fork placeholders only fill metadata on an existing row.
2633
2693
  */
2634
2694
  mergeSummary(summary) {
2635
2695
  this.recordMutation({
2636
2696
  kind: "upsert",
2637
2697
  summary
2638
2698
  });
2699
+ this.updateParentAvailability();
2639
2700
  }
2640
2701
  /** Apply immediately and retain for replay when a list response is in flight. */
2641
2702
  recordMutation(mutation) {
2703
+ if (this.disposed) return;
2642
2704
  this.listMutations?.push(mutation);
2643
2705
  this.summaries = applyMutation(this.summaries, mutation);
2644
- this.syncCompletedNotifications();
2645
2706
  this.notifier.markDirty();
2646
2707
  }
2647
2708
  /**
@@ -2661,6 +2722,14 @@ window.__ModuleLoader__.load({
2661
2722
  return this.listSnapshotCache;
2662
2723
  }
2663
2724
  /**
2725
+ * Read cached projection values for a Session that may exist only in a loaded subagent catalog.
2726
+ * @param sessionId - Session whose control or history baseline supplied projections.
2727
+ * @returns current values, or undefined before any projection store exists.
2728
+ */
2729
+ projectionValues(sessionId) {
2730
+ return this.projectionStores.get(sessionId)?.values();
2731
+ }
2732
+ /**
2664
2733
  * Apply a complete control baseline or one later replacement frame.
2665
2734
  * @param frame - baseline or live control replacement from Session Controller.
2666
2735
  */
@@ -2669,35 +2738,18 @@ window.__ModuleLoader__.load({
2669
2738
  this.replaceControlBaseline(frame.value);
2670
2739
  return;
2671
2740
  }
2672
- if (frame.type === "projection") {
2673
- this.projectionStore(frame.sessionId).apply(frame.key, frame.value, SessionSeq(frame.seq));
2674
- this.notifier.markDirty();
2675
- return;
2676
- }
2677
- if (frame.type === "jobs") {
2678
- if (frame.jobs.length === 0) this.jobsBySession.delete(frame.sessionId);
2679
- else this.jobsBySession.set(frame.sessionId, frame.jobs);
2680
- this.notifier.markDirty();
2681
- return;
2682
- }
2683
- this.queues.set(frame.sessionId, frame.items);
2684
- this.sessions.get(frame.sessionId)?.handleControlFrame(frame);
2741
+ this.projectionStore(frame.sessionId).apply(frame.key, frame.value, SessionSeq(frame.seq));
2742
+ this.notifier.markDirty();
2685
2743
  }
2686
2744
  replaceControlBaseline(baseline) {
2687
- this.queues.clear();
2688
- for (const [sessionId, items] of Object.entries(baseline.queues)) this.queues.set(sessionId, items);
2689
- this.jobsBySession.clear();
2690
- for (const [sessionId, jobs] of Object.entries(baseline.jobs)) if (jobs.length > 0) this.jobsBySession.set(sessionId, jobs);
2691
2745
  for (const [sessionId, block] of Object.entries(baseline.projections)) {
2692
2746
  const store = this.projectionStore(sessionId);
2693
2747
  const asOfSeq = sessionSeqCursor(block.asOfSeq);
2694
- store.truncate(asOfSeq);
2695
2748
  store.seed({
2696
2749
  ...block,
2697
2750
  asOfSeq
2698
2751
  });
2699
2752
  }
2700
- for (const [sessionId, session] of this.sessions) session.replaceControl(this.queues.get(sessionId) ?? []);
2701
2753
  this.notifier.markDirty();
2702
2754
  }
2703
2755
  /**
@@ -2706,45 +2758,55 @@ window.__ModuleLoader__.load({
2706
2758
  */
2707
2759
  handleSessionAdded(summary) {
2708
2760
  this.mergeSummary(summary);
2709
- this.sessions.get(summary.sessionId)?.handleBlank(summary.blank);
2710
- const projections = summary.projections;
2711
- if (projections !== void 0) {
2712
- const store = this.projectionStore(summary.sessionId);
2713
- for (const [key, value] of Object.entries(projections.values)) store.apply(key, value, sessionSeqCursor(projections.asOfSeq));
2761
+ if (!this.disposed && summary.running) this.engagedSessions.add(summary.sessionId);
2762
+ this.sessions.get(summary.sessionId)?.handleBlank(this.effectiveBlank(summary));
2763
+ if (summary.projections !== void 0) this.applyListBlock(summary.sessionId, summary.projections);
2764
+ }
2765
+ /**
2766
+ * Merge one list-surface projection block by the sequence space it declares.
2767
+ * A `sequenced` block came from the Host's live registry for an attached
2768
+ * Session, so each key lands under higher-seq-wins against that Session's
2769
+ * baselines and frames. A `cached` block was viewed from the persisted
2770
+ * checkpoint by a header-only listing: its watermark is not comparable with
2771
+ * this connection's seqs, so it only fills keys no sequenced row holds.
2772
+ */
2773
+ applyListBlock(sessionId, block) {
2774
+ const store = this.projectionStore(sessionId);
2775
+ switch (block.kind) {
2776
+ case "sequenced": {
2777
+ const seq = sessionSeqCursor(block.asOfSeq);
2778
+ for (const [key, value] of Object.entries(block.values)) store.apply(key, value, seq);
2779
+ return;
2780
+ }
2781
+ case "cached":
2782
+ store.applyCached(block.values);
2783
+ return;
2784
+ default: assertNever(block.kind, "session list projection block kind");
2714
2785
  }
2715
- if (summary.origin === "subagent" && summary.parentSessionId !== void 0) this.markCatalogParentExpandable(summary.parentSessionId);
2716
- if (summary.parentSessionId !== void 0 && (this.selected === summary.parentSessionId || this.openCatalogs.has(summary.parentSessionId))) this.scheduleCatalogRefresh(summary.parentSessionId);
2717
2786
  }
2718
2787
  /**
2719
2788
  * Apply one Session removal forwarded through `ctx.remote.$on`.
2720
2789
  * @param sessionId - removed Session identity.
2721
2790
  */
2722
2791
  handleSessionRemoved(sessionId) {
2723
- const durableSubagent = this.summaries.find((candidate) => candidate.sessionId === sessionId)?.origin === "subagent" || this.addresses.has(sessionId);
2792
+ const durableSubagent = this.subagentAddress(sessionId) !== void 0 || this.summaries.some((summary) => summary.sessionId === sessionId && summary.origin === "subagent");
2724
2793
  this.recordMutation(durableSubagent ? {
2725
2794
  kind: "status",
2726
2795
  sessionId,
2727
- running: false
2796
+ running: false,
2797
+ agentAvailable: false
2728
2798
  } : {
2729
2799
  kind: "remove",
2730
2800
  sessionId
2731
2801
  });
2732
- this.updateCatalogActivity(sessionId, false);
2733
2802
  if (durableSubagent) this.sessions.get(sessionId)?.handleRunning(false);
2734
2803
  else this.sessions.get(sessionId)?.handleRemoved();
2735
- this.queues.delete(sessionId);
2736
- this.jobsBySession.delete(sessionId);
2737
- if (!durableSubagent) this.projectionStores.delete(sessionId);
2738
- const inflightCatalog = this.catalogInflight.get(sessionId);
2739
- if (inflightCatalog !== void 0) {
2740
- inflightCatalog.parentAvailableOverride = false;
2741
- this.catalogStale.add(sessionId);
2742
- }
2743
- const ownedCatalog = this.catalogs.get(sessionId);
2744
- if (ownedCatalog !== void 0 && ownedCatalog.parentAvailable) this.catalogs.set(sessionId, {
2745
- ...ownedCatalog,
2746
- parentAvailable: false
2747
- });
2804
+ const catalog = this.projectionStores.get(sessionId)?.values().subagentCatalog;
2805
+ if (!durableSubagent && (catalog === void 0 || catalog.length === 0)) this.projectionStores.delete(sessionId);
2806
+ this.pruneEngagement(sessionId, this.retainedIds(this.summaries));
2807
+ this.projectionInflight.get(sessionId)?.controller.abort();
2808
+ this.projectionInflight.delete(sessionId);
2809
+ this.projectionLoads.delete(sessionId);
2748
2810
  for (const [childId, address] of this.addresses) if (address.parentSessionId === sessionId) this.sessions.get(childId)?.handleSubagentParentAvailable(false);
2749
2811
  }
2750
2812
  /**
@@ -2753,13 +2815,15 @@ window.__ModuleLoader__.load({
2753
2815
  * @param running - current Agent running state.
2754
2816
  */
2755
2817
  handleSessionStatus(sessionId, running) {
2818
+ if (!this.disposed && running) this.engagedSessions.add(sessionId);
2756
2819
  this.recordMutation({
2757
2820
  kind: "status",
2758
2821
  sessionId,
2759
- running
2822
+ running,
2823
+ agentAvailable: true
2760
2824
  });
2825
+ this.updateParentAvailability();
2761
2826
  this.sessions.get(sessionId)?.handleRunning(running);
2762
- this.updateCatalogActivity(sessionId, running);
2763
2827
  }
2764
2828
  /**
2765
2829
  * Advance Session-list activity from one user-authored durable message.
@@ -2783,165 +2847,87 @@ window.__ModuleLoader__.load({
2783
2847
  }
2784
2848
  /**
2785
2849
  * Repair one re-established Host-event generation with queryable baselines.
2850
+ * Discard old projection cuts before new queries, including cold Sessions
2851
+ * absent from the process-local control baseline.
2786
2852
  * Opened Session follow streams resume independently through API Gateway.
2787
2853
  */
2788
2854
  handleConnected() {
2855
+ for (const store of this.projectionStores.values()) store.clear();
2856
+ this.listMutations = null;
2857
+ this.listInflight = null;
2789
2858
  this.refreshList();
2790
- const selectedAddress = this.selected === void 0 ? void 0 : this.addresses.get(this.selected);
2791
- if (selectedAddress !== void 0) this.refreshSubagents(selectedAddress.parentSessionId);
2792
- if (this.selected !== void 0) this.refreshSubagents(this.selected);
2793
- for (const parentSessionId of this.openCatalogs) this.refreshSubagents(parentSessionId);
2794
- }
2795
- /** Debounce membership refetches while one parent catalog is selected or open. */
2796
- scheduleCatalogRefresh(parentSessionId) {
2797
- if (this.catalogDebounce.has(parentSessionId)) return;
2798
- const timer = setTimeout(() => {
2799
- this.catalogDebounce.delete(parentSessionId);
2800
- if (this.catalogInflight.has(parentSessionId)) {
2801
- this.catalogStale.add(parentSessionId);
2802
- return;
2803
- }
2804
- this.refreshSubagents(parentSessionId);
2805
- }, 50);
2806
- this.catalogDebounce.set(parentSessionId, timer);
2807
- }
2808
- /** Apply one Agent-driver transition to loaded and in-flight catalogs. */
2809
- updateCatalogActivity(childSessionId, running) {
2810
- const activity = running ? "running" : "inactive";
2811
- for (const inflight of this.catalogInflight.values()) inflight.activityRows.set(childSessionId, activity);
2812
- let changed = false;
2813
- for (const [parentSessionId, catalog] of this.catalogs) {
2814
- if (!catalog.entries.some((entry) => entry.kind === "child" && entry.id === childSessionId && entry.activity !== activity)) continue;
2815
- const entries = catalog.entries.map((entry) => {
2816
- if (entry.kind !== "child" || entry.id !== childSessionId) return entry;
2817
- return {
2818
- ...entry,
2819
- activity
2820
- };
2821
- });
2822
- changed = true;
2823
- this.catalogs.set(parentSessionId, {
2824
- ...catalog,
2825
- entries
2826
- });
2827
- }
2828
- if (changed) this.notifier.markDirty();
2829
- }
2830
- /** Preserve and project a positive expandability hint after one direct subagent publishes. */
2831
- markCatalogParentExpandable(parentSessionId) {
2832
- this.applyCatalogParentExpandable(parentSessionId);
2833
- for (const inflight of this.catalogInflight.values()) inflight.expandableRows.add(parentSessionId);
2834
- }
2835
- /** Apply one positive expandability hint to every loaded catalog containing that unique row id. */
2836
- applyCatalogParentExpandable(parentSessionId) {
2837
- let changed = false;
2838
- for (const [catalogParentId, catalog] of this.catalogs) {
2839
- if (!catalog.entries.some((entry) => entry.kind === "child" && entry.id === parentSessionId && !entry.hasChildren)) continue;
2840
- const entries = catalog.entries.map((entry) => {
2841
- if (entry.kind !== "child" || entry.id !== parentSessionId || entry.hasChildren) return entry;
2842
- return {
2843
- ...entry,
2844
- hasChildren: true
2845
- };
2846
- });
2847
- changed = true;
2848
- this.catalogs.set(catalogParentId, {
2849
- ...catalog,
2850
- entries
2851
- });
2852
- }
2853
- if (changed) this.notifier.markDirty();
2854
- }
2855
- /** Fold request-local row mutations into one catalog result before publication. */
2856
- withCatalogMutations(entries, expandableRows, activityRows) {
2857
- return entries.map((entry) => {
2858
- if (entry.kind !== "child") return entry;
2859
- const activity = activityRows.get(entry.id);
2860
- if (!expandableRows.has(entry.id) && activity === void 0) return entry;
2861
- return {
2862
- ...entry,
2863
- ...expandableRows.has(entry.id) ? { hasChildren: true } : {},
2864
- ...activity === void 0 ? {} : { activity }
2865
- };
2866
- });
2867
- }
2868
- /**
2869
- * Reconcile completion reminders against the latest summaries, eagerly after
2870
- * every mutation and pull (a snapshot-build-time pass would collapse
2871
- * consecutive status frames into one observation). A running→idle edge of a
2872
- * non-selected session arms its reminder; running disarms it; removal drops
2873
- * it. First observation only records the running bit — sessions already
2874
- * idle at load get no reminder.
2875
- */
2876
- syncCompletedNotifications() {
2877
- const seen = /* @__PURE__ */ new Set();
2878
- for (const s of this.summaries) {
2879
- seen.add(s.sessionId);
2880
- const prev = this.prevRunning.get(s.sessionId);
2881
- if (prev === void 0) {
2882
- this.prevRunning.set(s.sessionId, s.running);
2883
- continue;
2884
- }
2885
- if (prev && !s.running) {
2886
- if (s.sessionId !== this.selected) this.completedNotifications.add(s.sessionId);
2887
- } else if (s.running) this.completedNotifications.delete(s.sessionId);
2888
- this.prevRunning.set(s.sessionId, s.running);
2859
+ const parents = new Set(this.projectionLoads.keys());
2860
+ for (const id of this.sessions.keys()) {
2861
+ const address = this.addresses.get(id);
2862
+ if (address !== void 0) parents.add(address.parentSessionId);
2889
2863
  }
2890
- for (const id of this.prevRunning.keys()) if (!seen.has(id)) this.prevRunning.delete(id);
2891
- for (const id of this.completedNotifications) if (!seen.has(id)) this.completedNotifications.delete(id);
2864
+ for (const { controller } of this.projectionInflight.values()) controller.abort();
2865
+ this.projectionInflight.clear();
2866
+ this.projectionLoads.clear();
2867
+ for (const parentSessionId of parents) this.refreshProjections(parentSessionId);
2892
2868
  }
2893
2869
  buildListSnapshot() {
2894
2870
  const items = flattenLineage(this.summaries.map((summary) => {
2895
2871
  const projectionStore = this.projectionStores.get(summary.sessionId);
2896
2872
  const title = projectionStore?.get("title");
2897
2873
  const projectionValues = projectionStore?.values();
2874
+ const metadata = projectionValues?.sessionListMetadata;
2898
2875
  return {
2899
2876
  ...summary,
2877
+ blank: this.effectiveBlank(summary) && metadata?.blank !== false,
2878
+ updatedAt: Math.max(summary.updatedAt, metadata?.lastPromptAt ?? 0),
2900
2879
  ...typeof title === "string" && title !== "" ? { title } : {},
2901
2880
  ...projectionValues === void 0 ? {} : { projectionValues }
2902
2881
  };
2903
- }), this.completedNotifications).map((entry) => {
2882
+ })).map((entry) => {
2904
2883
  const prev = this.entryCache.get(entry.sessionId);
2905
- if (prev !== void 0 && prev.updatedAt === entry.updatedAt && prev.running === entry.running && prev.blank === entry.blank && prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd && prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth && prev.projectionValues === entry.projectionValues && prev.completed === entry.completed) return prev;
2884
+ if (prev !== void 0 && prev.updatedAt === entry.updatedAt && prev.running === entry.running && prev.blank === entry.blank && prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd && prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth && prev.projectionValues === entry.projectionValues) return prev;
2906
2885
  this.entryCache.set(entry.sessionId, entry);
2907
2886
  return entry;
2908
2887
  });
2909
- for (const id of this.entryCache.keys()) if (!items.some((e) => e.sessionId === id)) this.entryCache.delete(id);
2888
+ const itemIds = new Set(items.map((entry) => entry.sessionId));
2889
+ for (const id of this.entryCache.keys()) if (!itemIds.has(id)) this.entryCache.delete(id);
2910
2890
  if (!(items.length === this.itemsCache.length && items.every((e, i) => e === this.itemsCache[i]))) this.itemsCache = items;
2911
- const selected = this.selected;
2912
- const current = selected !== void 0 && (items.some((item) => item.sessionId === selected) || this.addresses.has(selected)) ? selected : void 0;
2913
2891
  return {
2914
2892
  items: this.itemsCache,
2915
- current,
2916
2893
  state: this.listState,
2917
2894
  phase: this.listPhase,
2918
2895
  error: this.listError,
2919
- subagentsByParent: Object.fromEntries(this.catalogs),
2920
- jobsBySession: Object.fromEntries(this.jobsBySession),
2921
- currentAddress: current === void 0 ? void 0 : this.addresses.get(current)
2896
+ projectionsBySession: Object.fromEntries([...this.projectionStores].map(([sessionId, store]) => [sessionId, {
2897
+ values: store.values(),
2898
+ state: "idle",
2899
+ error: null,
2900
+ ...this.projectionLoads.get(sessionId)
2901
+ }]))
2922
2902
  };
2923
2903
  }
2924
2904
  };
2925
2905
  /** Apply one list mutation without deriving display order. */
2926
2906
  function applyMutation(summaries, mutation) {
2927
2907
  switch (mutation.kind) {
2928
- case "upsert": {
2908
+ case "upsert":
2909
+ case "placeholder": {
2929
2910
  const existing = summaries.find((summary) => summary.sessionId === mutation.summary.sessionId);
2930
2911
  if (existing === void 0) return [mutation.summary, ...summaries];
2931
2912
  const filled = {
2932
2913
  ...existing,
2933
2914
  blank: existing.blank && mutation.summary.blank,
2915
+ ...mutation.kind === "upsert" ? {
2916
+ agentAvailable: mutation.summary.agentAvailable,
2917
+ running: mutation.summary.running
2918
+ } : {},
2934
2919
  ...existing.cwd === void 0 && mutation.summary.cwd !== void 0 ? { cwd: mutation.summary.cwd } : {},
2935
2920
  ...existing.parentSessionId === void 0 && mutation.summary.parentSessionId !== void 0 ? { parentSessionId: mutation.summary.parentSessionId } : {},
2936
2921
  ...existing.origin === void 0 && mutation.summary.origin !== void 0 ? { origin: mutation.summary.origin } : {}
2937
2922
  };
2938
- if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId && filled.origin === existing.origin && filled.blank === existing.blank) return [...summaries];
2923
+ if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId && filled.origin === existing.origin && filled.blank === existing.blank && filled.agentAvailable === existing.agentAvailable && filled.running === existing.running) return [...summaries];
2939
2924
  return summaries.map((summary) => summary.sessionId === mutation.summary.sessionId ? filled : summary);
2940
2925
  }
2941
2926
  case "remove": return summaries.filter((summary) => summary.sessionId !== mutation.sessionId);
2942
- case "status": return summaries.map((summary) => summary.sessionId === mutation.sessionId && (summary.running !== mutation.running || mutation.running && summary.blank) ? {
2927
+ case "status": return summaries.map((summary) => summary.sessionId === mutation.sessionId && (summary.running !== mutation.running || summary.agentAvailable !== mutation.agentAvailable || mutation.running && summary.blank) ? {
2943
2928
  ...summary,
2944
2929
  running: mutation.running,
2930
+ agentAvailable: mutation.agentAvailable,
2945
2931
  blank: summary.blank && !mutation.running
2946
2932
  } : summary);
2947
2933
  case "activity": return summaries.map((summary) => summary.sessionId === mutation.sessionId && mutation.updatedAt > summary.updatedAt ? {
@@ -3015,7 +3001,74 @@ window.__ModuleLoader__.load({
3015
3001
  if (fullWidth?.[1] !== void 0 && fullWidth[2] !== void 0) return `${fullWidth[1]}(${BigInt(fullWidth[2]) + 1n})`;
3016
3002
  return `${title} (1)`;
3017
3003
  }
3018
- /** Root sessions service: list store, current selection, object-layer manager, scope tree, bindings, and breadcrumb routes. */
3004
+ /** Source labels are dictionary keys, including names also present on Object.prototype. */
3005
+ function freezeRetainedBy(counts) {
3006
+ Object.setPrototypeOf(counts, null);
3007
+ return Object.freeze(counts);
3008
+ }
3009
+ const EMPTY_RETAIN_INFO = Object.freeze({
3010
+ referenceCount: 0,
3011
+ retainedBy: freezeRetainedBy({})
3012
+ });
3013
+ /** A cancelled waiter releases only its own reference, not the shared opening. */
3014
+ async function waitForOpen(opening, signal) {
3015
+ if (signal === void 0) return opening;
3016
+ const aborted = Promise.withResolvers();
3017
+ const onAbort = () => {
3018
+ aborted.reject(signal.reason);
3019
+ };
3020
+ signal.addEventListener("abort", onAbort, { once: true });
3021
+ try {
3022
+ if (signal.aborted) onAbort();
3023
+ await Promise.race([opening, aborted.promise]);
3024
+ } finally {
3025
+ signal.removeEventListener("abort", onAbort);
3026
+ }
3027
+ }
3028
+ var ClientSessionReference = class {
3029
+ sessionId;
3030
+ record;
3031
+ releaseReference;
3032
+ released = new AbortController();
3033
+ readiness = Promise.withResolvers();
3034
+ ready = this.readiness.promise;
3035
+ constructor(sessionId, record, releaseReference) {
3036
+ this.sessionId = sessionId;
3037
+ this.record = record;
3038
+ this.releaseReference = releaseReference;
3039
+ this.ready.catch(() => {});
3040
+ }
3041
+ get binding() {
3042
+ if (this.record === void 0 || !this.record.live) throw new Error(`Session reference "${this.sessionId}" is released`);
3043
+ return this.record.binding;
3044
+ }
3045
+ attachOpening(opening, signal) {
3046
+ const waitSignal = signal === void 0 ? this.released.signal : AbortSignal.any([this.released.signal, signal]);
3047
+ waitForOpen(opening, waitSignal).then(() => {
3048
+ try {
3049
+ waitSignal.throwIfAborted();
3050
+ this.readiness.resolve(this.binding);
3051
+ } catch (error) {
3052
+ this.readiness.reject(error);
3053
+ }
3054
+ }, (error) => {
3055
+ this.readiness.reject(error);
3056
+ });
3057
+ }
3058
+ release() {
3059
+ const reason = /* @__PURE__ */ new Error(`Session reference "${this.sessionId}" is released`);
3060
+ const release = this.releaseReference;
3061
+ this.released.abort(reason);
3062
+ this.readiness.reject(reason);
3063
+ this.record = void 0;
3064
+ this.releaseReference = void 0;
3065
+ release?.();
3066
+ }
3067
+ [Symbol.dispose]() {
3068
+ this.release();
3069
+ }
3070
+ };
3071
+ /** Host catalog and local reference allocator; view selection remains outside the Controller. */
3019
3072
  var ClientSessions = class {
3020
3073
  rootCtx;
3021
3074
  /**
@@ -3025,115 +3078,109 @@ window.__ModuleLoader__.load({
3025
3078
  * reports the same number.
3026
3079
  */
3027
3080
  searchResultLimit = 20;
3028
- /** List snapshot store (list RPC + host stream increments; re-pulled on reconnect) — the useSessions standard feed, current included. */
3081
+ /** Catalog metadata and local reference-source projection. */
3029
3082
  list;
3030
3083
  /** The object-layer instance cluster and frame dispatch entry. */
3031
3084
  manager;
3032
- /**
3033
- * Persisted selection cell (the durable half of `list.current`). Private on
3034
- * purpose: reads go through the list snapshot; writes through {@link
3035
- * ClientSessions.open} / {@link ClientSessions.clear}. Projection
3036
- * validates it against the live list instead of destructively pruning, so a
3037
- * selection survives transient list states (reconnect re-pull) and
3038
- * resurfaces when its session returns.
3039
- */
3040
- selection;
3041
3085
  scopes = /* @__PURE__ */ new Map();
3042
- /** In-flight scope drops remain here after records leave `scopes`, so root disposal can await quiescence. */
3086
+ /** Stable per-id sources retained for the Client root lifetime, including across generation replacement. */
3087
+ retainObservers = /* @__PURE__ */ new Map();
3043
3088
  scopeDrops = /* @__PURE__ */ new Set();
3044
- /**
3045
- * The staged session id — follows `list.current` exactly, holding its last
3046
- * defined value across masked gaps (a transiently absent selection blanks
3047
- * `current` without moving the stage, so reconnect re-pulls and removals
3048
- * keep the staged scope's frozen view alive until the stage moves on).
3049
- */
3050
- watched;
3051
- /** Removed-while-staged sessions whose teardown waits for the stage to move away. */
3052
- deferredRemovals = /* @__PURE__ */ new Set();
3089
+ closed = false;
3053
3090
  /**
3054
3091
  * @param ctx - client root context (scope fibers mount under it).
3055
3092
  * @param remote - generated Remote namespaces shared with every Session.
3056
3093
  */
3057
3094
  constructor(rootCtx, remote) {
3058
3095
  this.rootCtx = rootCtx;
3059
- this.selection = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({}, { persist: { name: "dsh.sessions.current" } });
3060
- const restored = this.selection.getSnapshot();
3061
- this.manager = new SessionManager(remote, restored.sessionId, restored.subagentAddress);
3096
+ this.manager = new SessionManager(remote);
3062
3097
  this.list = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
3063
3098
  ids: [],
3064
3099
  byId: {},
3065
- current: void 0,
3066
3100
  phase: "pending",
3067
- subagentsByParent: {},
3068
- jobsBySession: {},
3069
- currentAddress: void 0
3101
+ projectionsBySession: {}
3070
3102
  });
3071
3103
  const disposeManagerProjection = this.manager.subscribe(() => {
3072
3104
  this.projectList();
3073
3105
  });
3074
- const disposeStageFollower = this.list.subscribe(() => {
3075
- this.followCurrent();
3076
- });
3077
3106
  rootCtx.effect(() => async () => {
3078
- disposeStageFollower();
3107
+ this.closed = true;
3079
3108
  disposeManagerProjection();
3080
3109
  const scopes = [...this.scopes];
3081
3110
  this.scopes.clear();
3082
- this.deferredRemovals.clear();
3083
- this.watched = void 0;
3084
- for (const [id, record] of scopes) this.startScopeDrop(id, record);
3111
+ for (const [, record] of scopes) {
3112
+ record.live = false;
3113
+ record.session.unbindScope();
3114
+ }
3115
+ const managerDisposal = this.manager.dispose();
3116
+ for (const [id, record] of scopes) {
3117
+ this.startScopeDrop(id, record);
3118
+ this.publishRetention(id);
3119
+ }
3085
3120
  await this.drainScopeDrops();
3086
- await this.manager.dispose();
3121
+ await managerDisposal;
3087
3122
  }, "session-controller.client.sessions");
3088
3123
  rootCtx.reflect.provide("sessions", this, void 0);
3089
3124
  }
3090
- /**
3091
- * Select a listed or retained catalog-addressed session as current.
3092
- * @param id - listed or addressed session id.
3093
- */
3094
- open(id) {
3095
- this.manager.select(id);
3125
+ retain(target, options) {
3126
+ const { source, signal } = options;
3127
+ signal?.throwIfAborted();
3128
+ if (this.closed) throw new Error("Session Controller is disposed");
3129
+ const id = this.manager.resolveTarget(target);
3130
+ const reference = this.retainScope(id, source);
3131
+ try {
3132
+ reference.attachOpening(this.manager.get(id).open(), signal);
3133
+ return reference;
3134
+ } catch (error) {
3135
+ reference.release();
3136
+ throw error;
3137
+ }
3096
3138
  }
3097
- /**
3098
- * Open a healthy catalog child through its direct-parent address.
3099
- * @param address - catalog-derived parent and child ids.
3100
- */
3101
- openSubagent(address) {
3102
- this.manager.selectSubagent(address);
3139
+ async using(target, options, operation) {
3140
+ const reference = this.retain(target, options);
3141
+ try {
3142
+ await reference.ready;
3143
+ return await operation(reference);
3144
+ } finally {
3145
+ reference.release();
3146
+ }
3147
+ }
3148
+ retainInfo(id) {
3149
+ let observer = this.retainObservers.get(id);
3150
+ if (observer === void 0) {
3151
+ const listeners = /* @__PURE__ */ new Set();
3152
+ observer = {
3153
+ listeners,
3154
+ published: this.retentionSnapshot(id),
3155
+ source: {
3156
+ getSnapshot: () => this.retentionSnapshot(id),
3157
+ subscribe: (listener) => {
3158
+ listeners.add(listener);
3159
+ return () => {
3160
+ listeners.delete(listener);
3161
+ };
3162
+ }
3163
+ }
3164
+ };
3165
+ this.retainObservers.set(id, observer);
3166
+ }
3167
+ return observer.source;
3103
3168
  }
3104
3169
  /**
3105
3170
  * Resolve an already discovered direct-parent address without opening it.
3106
3171
  * Feature plugins use this to avoid Agent-bound RPCs in persisted child views.
3107
3172
  * @param id - possible addressed child id.
3108
- * @returns The retained address, when present.
3173
+ * @returns A retained or loaded-catalog address, without retaining a new selection or scope.
3109
3174
  */
3110
3175
  subagentAddress(id) {
3111
3176
  return this.manager.subagentAddress(id);
3112
3177
  }
3113
3178
  /**
3114
- * Inform the Session Controller whether a catalog menu is consuming membership updates.
3115
- * @param parentSessionId - selected parent.
3116
- * @param open - menu state.
3117
- */
3118
- setSubagentCatalogOpen(parentSessionId, open) {
3119
- this.manager.setSubagentCatalogOpen(parentSessionId, open);
3120
- }
3121
- /**
3122
- * Refresh one direct-child catalog.
3123
- * @param parentSessionId - catalog owner.
3124
- */
3125
- refreshSubagents(parentSessionId) {
3126
- return this.manager.refreshSubagents(parentSessionId);
3127
- }
3128
- /**
3129
- * Clear the current selection so the layout shows the no-session empty
3130
- * state (new-session affordance and the workspace preselection flow).
3131
- * Wipes the persisted selection too — a reload stays on empty until the
3132
- * user opens or starts a session. The staged scope keeps its frozen view
3133
- * per the masked-gap contract until the next open() moves the stage.
3179
+ * Load all Session projections once per connection; retry an unsuccessful initial read.
3180
+ * @param sessionId - Session to inspect without opening its conversation.
3134
3181
  */
3135
- clear() {
3136
- this.manager.clearSelection();
3182
+ refreshProjections(sessionId) {
3183
+ return this.manager.refreshProjections(sessionId);
3137
3184
  }
3138
3185
  /**
3139
3186
  * Refresh the real Session baseline, reusing an in-flight pull.
@@ -3199,12 +3246,8 @@ window.__ModuleLoader__.load({
3199
3246
  this.manager.handleConnected();
3200
3247
  }
3201
3248
  /**
3202
- * Create a session on the host. Resolution guarantee: by the time the
3203
- * promise resolves, the created session is in the list store and
3204
- * {@link ClientSessions.binding} resolves it — callers (New Session
3205
- * draft hand-off) may address the scope synchronously, without waiting a
3206
- * notifier flush. The synchronous projection below makes this structural
3207
- * rather than an accident of microtask ordering.
3249
+ * Create a Host Session and publish its catalog row before resolving.
3250
+ * Callers retain the returned identity before borrowing its binding.
3208
3251
  * @param opts - target workspace or directory and an optional preallocated id.
3209
3252
  * @returns the new session id.
3210
3253
  * @throws {SessionCreateError} with the requested id.
@@ -3216,16 +3259,14 @@ window.__ModuleLoader__.load({
3216
3259
  return result.value.sessionId;
3217
3260
  }
3218
3261
  /**
3219
- * Fork a session from a completed-turn prefix of the source (same
3262
+ * Fork a session from an exact inclusive prefix of the source (same
3220
3263
  * synchronous-addressability guarantee as {@link ClientSessions.create}:
3221
- * on resolution the child is in the list store and open() can target it).
3222
- * @param opts - source session id, the optional event seq anchoring the
3223
- * cut (the boundary is the first turn/end at or after it; an in-log
3224
- * anchor in an open turn is unavailable rather than clipped backward),
3225
- * and whether to increment an inherited durable title before resolving.
3226
- * A fractional anchor floors to a real event seq: the frozen nodes of an
3227
- * interrupted turn carry flow-ordering seqs between two events, and the
3228
- * wire takes integers only.
3264
+ * on resolution the child is catalogued and may be explicitly retained).
3265
+ * @param opts - source session id, the optional exact inclusive boundary
3266
+ * seq (a real event seq the caller already knows; a cut inside an open
3267
+ * turn is balanced Host-side with synthetic closers, and omission selects
3268
+ * the latest completed-turn prefix), and whether to increment an
3269
+ * inherited durable title before resolving.
3229
3270
  * @returns the child session id.
3230
3271
  * @throws {SessionForkError} with the source id.
3231
3272
  * @throws {Error} when a requested child-title rename fails after creation.
@@ -3234,37 +3275,33 @@ window.__ModuleLoader__.load({
3234
3275
  const sourceTitle = opts.increaseTitle ? this.list.getSnapshot().byId[opts.sessionId]?.title : void 0;
3235
3276
  const result = await this.manager.fork({
3236
3277
  sessionId: opts.sessionId,
3237
- ...opts.atSeq === void 0 ? {} : { atSeq: SessionSeq(Math.floor(opts.atSeq)) }
3278
+ ...opts.atSeq === void 0 ? {} : { atSeq: SessionSeq(opts.atSeq) }
3238
3279
  });
3239
3280
  if (!result.ok) throw new SessionForkError(result.error, opts.sessionId);
3240
3281
  this.projectList();
3241
3282
  const childId = result.value.sessionId;
3242
3283
  if (sourceTitle !== void 0) {
3243
- const child = this.binding(childId)?.session;
3244
- if (child === void 0) throw new Error(`fork child "${childId}" is not locally addressable`);
3245
- const renamed = await child.rename(increasedForkTitle(sourceTitle));
3284
+ const renamed = await this.manager.rename(childId, increasedForkTitle(sourceTitle));
3246
3285
  if (!renamed.ok) throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`);
3247
3286
  }
3248
3287
  return childId;
3249
3288
  }
3250
3289
  /**
3251
- * Resolve an Agent-scoped context view (use-and-discard).
3290
+ * Borrow an already-retained Agent-scoped Context.
3252
3291
  * @param id - session id (the agent identity — 1:1 same axis).
3253
- * @returns scoped ctx, or undefined for a session neither listed nor already scoped.
3292
+ * @returns the scoped Context, or undefined without a retained generation.
3254
3293
  */
3255
3294
  scope(id) {
3256
- return this.resolve(id)?.ctx;
3295
+ return this.scopes.get(id)?.ctx;
3257
3296
  }
3258
3297
  /**
3259
- * Materialize the Agent scope named by a validated Host Remote Event.
3260
- * The first successful Session-list baseline becomes authoritative for its
3261
- * lifetime; until then, transport streams may address the scope in either
3262
- * arrival order.
3263
- * @param id - Host-projected Agent identity (the matching Session id).
3264
- * @returns the identity-stable Agent Context.
3298
+ * Retain a validated Gateway identity synchronously, without history or catalog I/O.
3299
+ * @param id - Host-projected Session identity, possibly not yet catalogued.
3300
+ * @returns a Gateway-source reference owned by the invocation.
3265
3301
  */
3266
- resolveAgentScope(id) {
3267
- return (this.scopes.get(id) ?? this.materializeScope(id)).ctx;
3302
+ retainAgentScope(id) {
3303
+ if (this.closed) throw new Error("Session Controller is disposed");
3304
+ return this.retainScope(id, "gateway");
3268
3305
  }
3269
3306
  /**
3270
3307
  * Read the Agent scope tag off a context. Service-method boundary: fetch
@@ -3284,55 +3321,83 @@ window.__ModuleLoader__.load({
3284
3321
  * `agent.session`). Same service-method boundary as
3285
3322
  * {@link ClientSessions.scopeOf}.
3286
3323
  * @param ctx - an Agent-scoped context.
3287
- * @returns the session face, or undefined when the ctx is untagged or its scope was pruned.
3324
+ * @returns the matching live Session, or undefined for an untagged or ended generation.
3288
3325
  */
3289
3326
  sessionOf(ctx) {
3290
3327
  const id = scopeOf(ctx);
3291
3328
  if (id === void 0) return void 0;
3292
- return this.scopes.get(id)?.binding.session;
3329
+ const record = this.scopes.get(id);
3330
+ return record !== void 0 && scopeIdentityOf(record.ctx) === scopeIdentityOf(ctx) ? record.binding.session : void 0;
3293
3331
  }
3294
3332
  /**
3295
- * Resolve the stable session binding (scope-addressed assembly feed). Pure
3296
- * resolution — no staging, no window side effects.
3297
- * @param id - session id.
3298
- * @returns binding, or undefined for a session neither listed nor already scoped.
3333
+ * Borrow an already-retained binding without extending its lifetime.
3334
+ * @param id - Session identity.
3335
+ * @returns the live binding, or undefined without a retained generation.
3299
3336
  */
3300
3337
  binding(id) {
3301
- return this.resolve(id)?.binding;
3302
- }
3303
- /**
3304
- * Move the stage to the list's current session: sweep teardowns deferred
3305
- * behind the previous occupant and pull the new occupant's history window.
3306
- * Staging IS the open signal — the window opens ⟺ the session is on stage
3307
- * — and open() is idempotent (an in-flight or completed open no-ops; a
3308
- * failed one retries the next time current is touched).
3309
- */
3310
- followCurrent() {
3311
- const snapshot = this.list.getSnapshot();
3312
- const current = snapshot.current;
3313
- if (current === void 0 || snapshot.byId[current] === void 0 || current === this.watched) return;
3314
- this.watched = current;
3315
- this.sweepDeferred();
3316
- const record = this.resolve(current);
3317
- /* v8 ignore next 3 -- defensive: current is always a listed id (open()
3318
- * validates and the projection masks absent selections), so resolve
3319
- * cannot miss; kept so a future current writer cannot crash the notify. */
3320
- if (record !== void 0) {
3321
- record.session.open();
3322
- this.manager.refreshSubagents(current);
3323
- }
3324
- }
3325
- /**
3326
- * Lazily mint the scope + binding for an eligible session. Eligibility and
3327
- * prune share one predicate: listed on the host or selected
3328
- * through a retained subagent address. Breadcrumb-only ancestors remain
3329
- * summary data and do not keep scopes alive.
3330
- */
3331
- resolve(id) {
3332
- const existing = this.scopes.get(id);
3333
- if (existing !== void 0) return existing;
3334
- if (!this.eligible(id)) return void 0;
3335
- return this.materializeScope(id);
3338
+ return this.scopes.get(id)?.binding;
3339
+ }
3340
+ retainScope(id, source) {
3341
+ const record = this.scopes.get(id) ?? this.materializeScope(id);
3342
+ const previous = record.retention;
3343
+ record.retention = Object.freeze({
3344
+ referenceCount: previous.referenceCount + 1,
3345
+ retainedBy: freezeRetainedBy({
3346
+ ...previous.retainedBy,
3347
+ [source]: (previous.retainedBy[source] ?? 0) + 1
3348
+ })
3349
+ });
3350
+ const reference = new ClientSessionReference(id, record, () => {
3351
+ if (!record.live) return;
3352
+ const count = record.retention.referenceCount - 1;
3353
+ const { [source]: sourceCount = 0, ...otherSources } = record.retention.retainedBy;
3354
+ const retainedBy = sourceCount > 1 ? {
3355
+ ...otherSources,
3356
+ [source]: sourceCount - 1
3357
+ } : otherSources;
3358
+ record.retention = count === 0 ? EMPTY_RETAIN_INFO : Object.freeze({
3359
+ referenceCount: count,
3360
+ retainedBy: freezeRetainedBy(retainedBy)
3361
+ });
3362
+ if (count === 0) this.retireScope(id, record);
3363
+ else this.publishRetention(id);
3364
+ });
3365
+ if (this.list.getSnapshot().byId[id] === void 0 && this.manager.subagentAddress(id) !== void 0) this.projectList();
3366
+ this.publishRetention(id);
3367
+ return reference;
3368
+ }
3369
+ retentionSnapshot(id) {
3370
+ return this.scopes.get(id)?.retention ?? EMPTY_RETAIN_INFO;
3371
+ }
3372
+ publishRetention(id) {
3373
+ const state = this.list.getSnapshot();
3374
+ const row = state.byId[id];
3375
+ const retainedBy = this.retentionSnapshot(id).retainedBy;
3376
+ if (row !== void 0 && row.retainedBy !== retainedBy) this.list.set({
3377
+ ...state,
3378
+ byId: {
3379
+ ...state.byId,
3380
+ [id]: {
3381
+ ...row,
3382
+ retainedBy
3383
+ }
3384
+ }
3385
+ });
3386
+ const observer = this.retainObservers.get(id);
3387
+ const snapshot = this.retentionSnapshot(id);
3388
+ if (observer === void 0 || observer.published === snapshot) return;
3389
+ observer.published = snapshot;
3390
+ (0, _deepseek_ai_dsh_client_store.notifySubscribers)(observer.listeners, "[session-controller] reference sources");
3391
+ }
3392
+ retireScope(id, record, disposeFiber = true) {
3393
+ if (!record.live) return;
3394
+ record.live = false;
3395
+ if (this.scopes.get(id) === record) this.scopes.delete(id);
3396
+ record.session.unbindScope();
3397
+ const sessionDisposal = this.manager.drop(id, record.session);
3398
+ this.projectList();
3399
+ this.publishRetention(id);
3400
+ this.startScopeDrop(id, record, disposeFiber, sessionDisposal);
3336
3401
  }
3337
3402
  /** Materialize one scope after its caller establishes that the id may be addressed. */
3338
3403
  materializeScope(id) {
@@ -3348,19 +3413,20 @@ window.__ModuleLoader__.load({
3348
3413
  eventSource: session.eventSource,
3349
3414
  ctx
3350
3415
  },
3351
- session
3416
+ session,
3417
+ retention: EMPTY_RETAIN_INFO,
3418
+ live: true
3352
3419
  };
3353
3420
  this.scopes.set(id, record);
3421
+ ctx.effect(() => () => {
3422
+ this.retireScope(id, record, false);
3423
+ }, "session-controller: exact generation");
3354
3424
  return record;
3355
3425
  }
3356
- /** The one aliveness predicate shared by scope mint and prune: host-listed or currently addressed. */
3357
- eligible(id) {
3358
- const { ids, current } = this.list.getSnapshot();
3359
- return current === id || ids.includes(id);
3360
- }
3361
3426
  /** Project the manager's list snapshot into the store (title derivation is display-only). */
3362
3427
  projectList() {
3363
- const { items, current, phase, subagentsByParent, jobsBySession, currentAddress } = this.manager.getListSnapshot();
3428
+ const previousById = this.list.getSnapshot().byId;
3429
+ const { items, phase, projectionsBySession } = this.manager.getListSnapshot();
3364
3430
  const ids = [];
3365
3431
  const byId = {};
3366
3432
  for (const entry of items) {
@@ -3369,7 +3435,7 @@ window.__ModuleLoader__.load({
3369
3435
  id: entry.sessionId,
3370
3436
  displayTitle: displayTitleOf(entry.title, entry.cwd, entry.sessionId),
3371
3437
  running: entry.running,
3372
- ...entry.completed ? { completed: true } : {},
3438
+ retainedBy: this.retentionSnapshot(entry.sessionId).retainedBy,
3373
3439
  blank: entry.blank,
3374
3440
  updatedAt: entry.updatedAt,
3375
3441
  ...entry.projectionValues === void 0 ? {} : { projectionValues: entry.projectionValues },
@@ -3379,68 +3445,67 @@ window.__ModuleLoader__.load({
3379
3445
  ...entry.origin !== void 0 ? { origin: entry.origin } : {}
3380
3446
  };
3381
3447
  }
3382
- if (current !== void 0 && currentAddress !== void 0) {
3383
- const seen = /* @__PURE__ */ new Set();
3384
- let address = currentAddress;
3385
- while (address !== void 0 && !seen.has(address.childSessionId)) {
3386
- const childId = address.childSessionId;
3387
- seen.add(childId);
3388
- const child = subagentsByParent[address.parentSessionId]?.entries.find((entry) => entry.kind === "child" && entry.id === childId);
3389
- if (child?.kind !== "child") break;
3390
- const displayTitle = child.label ?? childId;
3391
- const summary = byId[childId];
3392
- if (summary === void 0) byId[childId] = {
3393
- id: childId,
3394
- displayTitle,
3395
- parentId: address.parentSessionId,
3396
- origin: "subagent",
3397
- running: child.activity === "running",
3398
- blank: false,
3448
+ for (const [parentId, projection] of Object.entries(projectionsBySession)) for (const child of projection.values.subagentCatalog ?? []) {
3449
+ const childId = child.id;
3450
+ const summary = byId[childId];
3451
+ const projectionValues = summary?.projectionValues ?? this.manager.projectionValues(childId);
3452
+ const projectedTitle = projectionValues?.title;
3453
+ const title = typeof projectedTitle === "string" && projectedTitle !== "" ? projectedTitle : void 0;
3454
+ const displayTitle = title ?? child.label ?? childId;
3455
+ if (summary === void 0) byId[childId] = {
3456
+ id: childId,
3457
+ displayTitle,
3458
+ parentId,
3459
+ origin: "subagent",
3460
+ running: this.scopes.get(childId)?.session.getSnapshot().running ?? false,
3461
+ blank: false,
3462
+ updatedAt: 0,
3463
+ retainedBy: this.retentionSnapshot(childId).retainedBy,
3464
+ ...projectionValues === void 0 ? {} : { projectionValues },
3465
+ ...title === void 0 ? {} : { title }
3466
+ };
3467
+ else if (summary.displayTitle !== displayTitle || summary.projectionValues !== projectionValues) byId[childId] = {
3468
+ ...summary,
3469
+ displayTitle,
3470
+ ...projectionValues === void 0 ? {} : { projectionValues }
3471
+ };
3472
+ }
3473
+ for (const [id, record] of this.scopes) {
3474
+ if (byId[id] !== void 0) continue;
3475
+ const address = this.manager.subagentAddress(id);
3476
+ if (address === void 0) continue;
3477
+ const previous = previousById[id];
3478
+ const snapshot = record.session.getSnapshot();
3479
+ const projectionValues = this.manager.projectionValues(id);
3480
+ const projectedTitle = projectionValues?.title;
3481
+ const title = typeof projectedTitle === "string" && projectedTitle !== "" ? projectedTitle : previous?.title;
3482
+ byId[id] = {
3483
+ ...previous ?? {
3484
+ id,
3485
+ displayTitle: id,
3399
3486
  updatedAt: 0
3400
- };
3401
- else if (summary.displayTitle !== displayTitle) byId[childId] = {
3402
- ...summary,
3403
- displayTitle
3404
- };
3405
- const parent = byId[address.parentSessionId];
3406
- if (parent !== void 0 && parent.origin !== "subagent") break;
3407
- address = this.manager.navigationAddress(address.parentSessionId);
3408
- }
3487
+ },
3488
+ running: snapshot.running,
3489
+ retainedBy: record.retention.retainedBy,
3490
+ blank: snapshot.blank,
3491
+ parentId: address.parentSessionId,
3492
+ origin: "subagent",
3493
+ ...projectionValues === void 0 ? {} : { projectionValues },
3494
+ ...title === void 0 ? {} : {
3495
+ title,
3496
+ displayTitle: title
3497
+ }
3498
+ };
3409
3499
  }
3410
- const persisted = this.selection.getSnapshot().sessionId;
3411
- if (current === void 0) {
3412
- if (persisted !== void 0) this.selection.set({});
3413
- } else if (byId[current] !== void 0 && (persisted !== current || this.selection.getSnapshot().subagentAddress?.childSessionId !== currentAddress?.childSessionId || this.selection.getSnapshot().subagentAddress?.parentSessionId !== currentAddress?.parentSessionId || this.selection.getSnapshot().subagentAddress?.mode !== currentAddress?.mode)) this.selection.set({
3414
- sessionId: current,
3415
- ...currentAddress === void 0 ? {} : { subagentAddress: currentAddress }
3416
- });
3417
3500
  this.list.set({
3418
3501
  ids,
3419
3502
  byId,
3420
- current,
3421
3503
  phase,
3422
- subagentsByParent,
3423
- jobsBySession,
3424
- currentAddress
3504
+ projectionsBySession
3425
3505
  });
3426
- this.pruneScopes();
3427
- }
3428
- /** Tear down scope + instance for no-longer-eligible sessions off stage; the staged one defers until the stage moves. */
3429
- pruneScopes() {
3430
- if (this.list.getSnapshot().phase === "pending") return;
3431
- for (const [id, record] of this.scopes) {
3432
- if (this.eligible(id)) continue;
3433
- if (id === this.watched) {
3434
- this.deferredRemovals.add(id);
3435
- continue;
3436
- }
3437
- this.scopes.delete(id);
3438
- this.deferredRemovals.delete(id);
3439
- this.startScopeDrop(id, record);
3440
- }
3441
3506
  }
3442
- startScopeDrop(id, record) {
3443
- const drop = this.dropScope(id, record);
3507
+ startScopeDrop(id, record, disposeFiber = true, sessionDisposal = this.manager.drop(id, record.session)) {
3508
+ const drop = this.dropScope(record, disposeFiber, sessionDisposal);
3444
3509
  this.scopeDrops.add(drop);
3445
3510
  drop.then(() => {
3446
3511
  this.scopeDrops.delete(drop);
@@ -3451,38 +3516,9 @@ window.__ModuleLoader__.load({
3451
3516
  async drainScopeDrops() {
3452
3517
  while (this.scopeDrops.size > 0) await Promise.allSettled([...this.scopeDrops]);
3453
3518
  }
3454
- /**
3455
- * One teardown for the whole per-session axis: the scope
3456
- * fiber (cascading every actx-registered effect: input shell, slash
3457
- * controller, popup, plugin stores, listeners), the session-keyed slot
3458
- * registrations and the Session instance itself — the host session log is the
3459
- * durable truth, a reopen lazily rebuilds and backfills via open().
3460
- */
3461
- async dropScope(id, record) {
3462
- record.session.unbindScope();
3463
- await Promise.allSettled([record.fiber.dispose(), this.manager.drop(id)]);
3464
- }
3465
- /** Run deferred teardowns whose session is no longer staged (called when the stage moves). */
3466
- sweepDeferred() {
3467
- for (const id of [...this.deferredRemovals]) {
3468
- /* v8 ignore next -- defensive: only the staged id ever defers, and every
3469
- * stage move sweeps first, so the set cannot contain the id the stage just
3470
- * moved to; kept as a guard against future extra sweep call sites. */
3471
- if (id === this.watched) continue;
3472
- if (this.eligible(id)) {
3473
- this.deferredRemovals.delete(id);
3474
- continue;
3475
- }
3476
- const record = this.scopes.get(id);
3477
- this.deferredRemovals.delete(id);
3478
- /* v8 ignore next -- defensive: prune deletes a scope and its deferral
3479
- * together, so a deferred id always still owns its record; kept so a
3480
- * future teardown path cannot double-dispose. */
3481
- if (record !== void 0) {
3482
- this.scopes.delete(id);
3483
- this.startScopeDrop(id, record);
3484
- }
3485
- }
3519
+ /** Await the already-withdrawn Session and scoped cleanup to quiescence. */
3520
+ async dropScope(record, disposeFiber, sessionDisposal) {
3521
+ await Promise.allSettled([sessionDisposal, ...disposeFiber ? [record.fiber.dispose()] : []]);
3486
3522
  }
3487
3523
  };
3488
3524
  //#endregion
@@ -3504,6 +3540,7 @@ window.__ModuleLoader__.load({
3504
3540
  */
3505
3541
  function apply(ctx) {
3506
3542
  const remotes = ctx.remote;
3543
+ const connection = ctx.get("connection");
3507
3544
  const sessions = new ClientSessions(ctx, remotes);
3508
3545
  ctx.remote.$on("api-session/added", (summary) => {
3509
3546
  sessions.handleSessionAdded(summary);
@@ -3528,14 +3565,22 @@ window.__ModuleLoader__.load({
3528
3565
  console.error("[session-controller] control stream failed:", error);
3529
3566
  }
3530
3567
  });
3531
- control.start();
3532
- ctx.on("connection/reset", () => {
3568
+ const connected = () => {
3569
+ if (connection.generation.getSnapshot() === void 0) return;
3533
3570
  sessions.handleConnected();
3534
- });
3535
- if (ctx.remote.$host.home !== void 0) sessions.handleConnected();
3571
+ control.restart();
3572
+ control.start();
3573
+ };
3574
+ ctx.effect(() => connection.generation.subscribe(connected), "session-controller.client.generation");
3575
+ connected();
3536
3576
  ctx.typert.contexts.registerClient("agent", {
3537
- identity: (candidate) => sessions.scopeOf(candidate),
3538
- resolve: (sessionId) => sessions.resolveAgentScope(sessionId)
3577
+ identity: (candidate) => sessions.sessionOf(candidate)?.sessionId,
3578
+ resolve: (sessionId) => {
3579
+ const reference = sessions.retainAgentScope(sessionId);
3580
+ return typertOwnedValue(reference.binding.ctx, () => {
3581
+ reference.release();
3582
+ });
3583
+ }
3539
3584
  });
3540
3585
  ctx.effect(() => async () => {
3541
3586
  await control.dispose();