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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +34 -9
  3. package/README.zh.md +34 -9
  4. package/lib/client.js +774 -641
  5. package/lib/index.js +318 -143
  6. package/lib/typert.host.js +384 -186
  7. package/lib/typert.remote-client.d.ts +10 -5
  8. package/lib/typert.remote-client.js +225 -125
  9. package/lib/types/archived-session-gate.d.ts +30 -0
  10. package/lib/types/archived-session-gate.js +52 -0
  11. package/lib/types/client/contract/session.d.ts +11 -4
  12. package/lib/types/client/contract/sessions.d.ts +11 -16
  13. package/lib/types/client/contract/snapshot.d.ts +1 -1
  14. package/lib/types/client/index.d.ts +1 -1
  15. package/lib/types/client/sessions/lineage.d.ts +4 -1
  16. package/lib/types/client/sessions/lineage.js +2 -1
  17. package/lib/types/client/sessions/manager.d.ts +66 -59
  18. package/lib/types/client/sessions/manager.js +249 -277
  19. package/lib/types/client/sessions/projection-store.d.ts +38 -13
  20. package/lib/types/client/sessions/projection-store.js +54 -13
  21. package/lib/types/client/sessions/remotes.d.ts +1 -2
  22. package/lib/types/client/sessions/service.d.ts +23 -35
  23. package/lib/types/client/sessions/service.js +35 -47
  24. package/lib/types/client/sessions/session.d.ts +23 -19
  25. package/lib/types/client/sessions/session.js +158 -59
  26. package/lib/types/client/transport.js +5 -2
  27. package/lib/types/commands.d.ts +4 -2
  28. package/lib/types/commands.js +72 -36
  29. package/lib/types/control.d.ts +2 -4
  30. package/lib/types/control.js +2 -44
  31. package/lib/types/history.js +25 -10
  32. package/lib/types/index.d.ts +32 -5
  33. package/lib/types/index.js +117 -13
  34. package/lib/types/list.js +24 -15
  35. package/lib/types/skill-catalog.js +60 -50
  36. package/lib/types/types.d.ts +45 -25
  37. package/lib/types/types.js +0 -1
  38. package/package.json +71 -76
package/lib/client.js CHANGED
@@ -149,6 +149,7 @@ window.__ModuleLoader__.load({
149
149
  "compaction/start",
150
150
  "compaction/summary",
151
151
  "deliverables/presented",
152
+ "developer/message",
152
153
  "feedback/message-delete",
153
154
  "feedback/message-put",
154
155
  "feedback/record",
@@ -199,6 +200,7 @@ window.__ModuleLoader__.load({
199
200
  /** Runtime counterpart of the message-producing event union. */
200
201
  const SURFACE_EVENT_TYPES = new Set([
201
202
  "system/message",
203
+ "developer/message",
202
204
  "user/message",
203
205
  "assistant/message",
204
206
  "tool/result"
@@ -206,7 +208,7 @@ window.__ModuleLoader__.load({
206
208
  /**
207
209
  * Whether an event type can join the model-visible surface.
208
210
  * @param type - event type to test.
209
- * @returns true for one of the four message-producing event types.
211
+ * @returns true for one of the message-producing event types.
210
212
  */
211
213
  function isSurfaceEligibleType(type) {
212
214
  return SURFACE_EVENT_TYPES.has(type);
@@ -216,14 +218,33 @@ window.__ModuleLoader__.load({
216
218
  return typeof value === "object" && value !== null && !Array.isArray(value);
217
219
  }
218
220
  /**
219
- * Reject noncanonical request-header fields and contradictory tool failure metadata.
221
+ * Reject noncanonical request-header fields, developer roles/content, and contradictory tool failure metadata.
220
222
  * This does not validate complete event payloads or embedded provider streams.
221
223
  * @param event - event whose locally related payload fields are inspected.
222
224
  * @param subject - event location to include in validation errors.
223
- * @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.
224
226
  */
225
227
  function validateSessionEventData(event, subject) {
226
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
+ }
227
248
  if (event.type === "request/header") {
228
249
  if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
229
250
  const header = data["header"];
@@ -236,9 +257,7 @@ window.__ModuleLoader__.load({
236
257
  if (!isRecord(data)) throw new Error(`${subject} data must be an object`);
237
258
  if (data["error"] === void 0) return;
238
259
  const message = data["message"];
239
- const content = isRecord(message) ? message["content"] : void 0;
240
- const block = Array.isArray(content) ? content[0] : void 0;
241
- 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`);
242
261
  }
243
262
  }
244
263
  /** Whether a runtime value is a non-negative safe event sequence. */
@@ -331,7 +350,6 @@ window.__ModuleLoader__.load({
331
350
  }
332
351
  //#endregion
333
352
  //#region lib/types/types.js
334
- /** Browser-safe request, result, and lifecycle vocabulary for the Session Remote service. */
335
353
  /** Maximum number of Sessions returned by one search. */
336
354
  const SESSION_SEARCH_RESULT_LIMIT = 20;
337
355
  /** Maximum search snippet length in Unicode code points. */
@@ -410,7 +428,7 @@ window.__ModuleLoader__.load({
410
428
  for await (const frame of this.remote.session.follow({
411
429
  address: this.address,
412
430
  assistantStream: true,
413
- ...request.maxMessages === void 0 ? {} : { maxMessages: request.maxMessages }
431
+ ...this.repairRequest(request)
414
432
  }, signal)) {
415
433
  if (frame.type === "snapshot") {
416
434
  for (const record of frame.records) assertSessionWireEvent(record.event);
@@ -458,7 +476,10 @@ window.__ModuleLoader__.load({
458
476
  }
459
477
  /** @inheritdoc */
460
478
  repairRequest(request) {
461
- return request.maxMessages === void 0 ? {} : { maxMessages: request.maxMessages };
479
+ return {
480
+ ...request.maxMessages === void 0 ? {} : { maxMessages: request.maxMessages },
481
+ ...request.turnWindow === void 0 ? {} : { turnWindow: request.turnWindow }
482
+ };
462
483
  }
463
484
  };
464
485
  //#endregion
@@ -558,6 +579,210 @@ window.__ModuleLoader__.load({
558
579
  return merged;
559
580
  }
560
581
  //#endregion
582
+ //#region ../../util/values/lib/index.js
583
+ /** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
584
+ /**
585
+ * Mark an unreachable closed-union branch.
586
+ * @param value - impossible value; an unhandled typed variant fails at the call site.
587
+ * @param context - optional switch-site label included in the failure message.
588
+ * @returns never; a runtime value that escaped its type always throws.
589
+ */
590
+ function assertNever(value, context) {
591
+ const rendered = JSON.stringify(value) ?? String(value);
592
+ throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
593
+ }
594
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
595
+ function hasIntrinsicConstructor(prototype, name) {
596
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
597
+ if (typeof constructor !== "function") return false;
598
+ try {
599
+ return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
600
+ } catch {
601
+ return false;
602
+ }
603
+ }
604
+ /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
605
+ function isIntrinsicObjectPrototype(value) {
606
+ return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
607
+ }
608
+ /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
609
+ function hasPlainArrayPrototype(value) {
610
+ const prototype = Object.getPrototypeOf(value);
611
+ if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
612
+ const objectPrototype = Object.getPrototypeOf(prototype);
613
+ return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
614
+ }
615
+ /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
616
+ function hasPlainObjectPrototype(value) {
617
+ const prototype = Object.getPrototypeOf(value);
618
+ return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
619
+ }
620
+ /** Return every JSON-visible object key, or reject own data JSON would discard. */
621
+ function enumerableStringKeys(value) {
622
+ const keys = Reflect.ownKeys(value);
623
+ if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
624
+ return keys;
625
+ }
626
+ /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
627
+ function walkJsonValue(value, detach) {
628
+ const ancestors = /* @__PURE__ */ new Set();
629
+ let root;
630
+ const assign = (destination, item) => {
631
+ if (destination === void 0) return;
632
+ if (destination.kind === "root") root = item;
633
+ else if (destination.kind === "array") destination.target[destination.index] = item;
634
+ else Object.defineProperty(destination.target, destination.key, {
635
+ value: item,
636
+ enumerable: true,
637
+ configurable: true,
638
+ writable: true
639
+ });
640
+ };
641
+ const tasks = [{
642
+ kind: "visit",
643
+ value,
644
+ ...detach ? { destination: { kind: "root" } } : {}
645
+ }];
646
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
647
+ if (task.kind === "leave") {
648
+ ancestors.delete(task.source);
649
+ continue;
650
+ }
651
+ if (task.kind === "array-item") {
652
+ if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
653
+ tasks.push({
654
+ kind: "visit",
655
+ value: task.source[task.index],
656
+ ...task.target === void 0 ? {} : { destination: {
657
+ kind: "array",
658
+ target: task.target,
659
+ index: task.index
660
+ } }
661
+ });
662
+ continue;
663
+ }
664
+ if (task.kind === "object-property") {
665
+ tasks.push({
666
+ kind: "visit",
667
+ value: task.source[task.key],
668
+ ...task.target === void 0 ? {} : { destination: {
669
+ kind: "object",
670
+ target: task.target,
671
+ key: task.key
672
+ } }
673
+ });
674
+ continue;
675
+ }
676
+ const current = task.value;
677
+ if (current === null) {
678
+ assign(task.destination, null);
679
+ continue;
680
+ }
681
+ if (typeof current === "boolean" || typeof current === "string") {
682
+ assign(task.destination, current);
683
+ continue;
684
+ }
685
+ if (typeof current === "number") {
686
+ if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
687
+ assign(task.destination, current);
688
+ continue;
689
+ }
690
+ if (typeof current !== "object") return void 0;
691
+ if (ancestors.has(current)) return void 0;
692
+ if (Array.isArray(current)) {
693
+ if (!hasPlainArrayPrototype(current)) return void 0;
694
+ const length = current.length;
695
+ if (Reflect.ownKeys(current).length !== length + 1) return void 0;
696
+ const target = detach ? [] : void 0;
697
+ if (target !== void 0) assign(task.destination, target);
698
+ ancestors.add(current);
699
+ tasks.push({
700
+ kind: "leave",
701
+ source: current
702
+ });
703
+ for (let index = length - 1; index >= 0; index--) tasks.push({
704
+ kind: "array-item",
705
+ source: current,
706
+ index,
707
+ ...target === void 0 ? {} : { target }
708
+ });
709
+ continue;
710
+ }
711
+ if (!hasPlainObjectPrototype(current)) return void 0;
712
+ const keys = enumerableStringKeys(current);
713
+ if (keys === void 0) return void 0;
714
+ const target = detach ? {} : void 0;
715
+ if (target !== void 0) assign(task.destination, target);
716
+ ancestors.add(current);
717
+ tasks.push({
718
+ kind: "leave",
719
+ source: current
720
+ });
721
+ for (let index = keys.length - 1; index >= 0; index--) {
722
+ const key = keys[index];
723
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
724
+ if (key === void 0) return void 0;
725
+ tasks.push({
726
+ kind: "object-property",
727
+ source: current,
728
+ key,
729
+ ...target === void 0 ? {} : { target }
730
+ });
731
+ }
732
+ }
733
+ return detach ? root : true;
734
+ }
735
+ /**
736
+ * Validate and detach lossless JSON in one read per property.
737
+ * @param value - candidate value to validate and detach.
738
+ * @returns the detached snapshot, or `undefined` when the value is not losslessly JSON-serializable.
739
+ */
740
+ function snapshotJsonValue(value) {
741
+ return walkJsonValue(value, true);
742
+ }
743
+ /**
744
+ * Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
745
+ * @param value - value to freeze.
746
+ * @returns the same value after every reachable enumerable child is frozen.
747
+ */
748
+ function deepFreeze(value) {
749
+ const seen = /* @__PURE__ */ new WeakSet();
750
+ const pending = [{
751
+ kind: "visit",
752
+ node: value
753
+ }];
754
+ while (pending.length > 0) {
755
+ const task = pending.pop();
756
+ /* v8 ignore next -- the loop condition guarantees one pending task. */
757
+ if (task === void 0) continue;
758
+ if (task.kind === "property") {
759
+ pending.push({
760
+ kind: "visit",
761
+ node: task.source[task.key]
762
+ });
763
+ continue;
764
+ }
765
+ const node = task.node;
766
+ if (node === null || typeof node !== "object") continue;
767
+ if (node instanceof AbortSignal) continue;
768
+ if (seen.has(node)) continue;
769
+ seen.add(node);
770
+ Object.freeze(node);
771
+ const keys = Object.keys(node);
772
+ for (let index = keys.length - 1; index >= 0; index--) {
773
+ const key = keys[index];
774
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
775
+ if (key === void 0) continue;
776
+ pending.push({
777
+ kind: "property",
778
+ source: node,
779
+ key
780
+ });
781
+ }
782
+ }
783
+ return value;
784
+ }
785
+ //#endregion
561
786
  //#region lib/types/client/sessions/lineage.js
562
787
  /**
563
788
  * Summaries -> flat list with lineage indentation. Root and sibling order
@@ -584,8 +809,9 @@ window.__ModuleLoader__.load({
584
809
  return;
585
810
  }
586
811
  visited.add(s.sessionId);
812
+ const { agentAvailable: _agentAvailable, ...row } = s;
587
813
  out.push({
588
- ...s,
814
+ ...row,
589
815
  depth
590
816
  });
591
817
  const kids = children.get(s.sessionId);
@@ -688,10 +914,14 @@ window.__ModuleLoader__.load({
688
914
  //#region lib/types/client/sessions/projection-store.js
689
915
  /**
690
916
  * One session's projection values. Framework semantics, uniform across every
691
- * key: a baseline seeds rows at its cut, a push frame updates one row, and in
692
- * both paths a lower-or-equal seq within the Host generation loses. A replayed
693
- * frame cannot regress a value; a stale baseline cannot overwrite a newer
694
- * frame. A key the store has never seen reads `undefined` (capability absent). Faces are identity-stable
917
+ * key. Sequenced writes (a baseline seeds rows at its cut, a push frame
918
+ * updates one row) compare seqs among themselves: a lower-or-equal seq within
919
+ * the Host generation loses, so a replayed frame cannot regress a value and a
920
+ * stale baseline cannot overwrite a newer frame. Cached writes (the session
921
+ * list's zero-I/O block) only fill keys no sequenced row holds, and a baseline
922
+ * discards every cached row before it seeds, regardless of seq: the connected
923
+ * Session is the truth and a cached value never outranks it. A key the store
924
+ * has never seen reads `undefined` (capability absent). Faces are identity-stable
695
925
  * per key (create-on-demand, cached) so the React side binds each exactly
696
926
  * once; the store-level channel (`subscribeAny`) serves coarse consumers (the
697
927
  * manager's list projection reads the `title` key).
@@ -722,6 +952,15 @@ window.__ModuleLoader__.load({
722
952
  return this.rows.get(key)?.value;
723
953
  }
724
954
  /**
955
+ * Read the accepted Host watermark without subscribing or copying a value.
956
+ * @param key - projection key.
957
+ * @returns the current sequence, or undefined for absent and cached values.
958
+ */
959
+ seqOf(key) {
960
+ const row = this.rows.get(key);
961
+ return row?.kind === "sequenced" ? row.seq : void 0;
962
+ }
963
+ /**
725
964
  * Read every current projection value as one reference-stable snapshot.
726
965
  * @returns The same frozen value map until a row changes.
727
966
  */
@@ -746,27 +985,53 @@ window.__ModuleLoader__.load({
746
985
  */
747
986
  apply(key, value, seq) {
748
987
  const row = this.rows.get(key);
749
- if (row !== void 0 && seq <= row.seq) return;
988
+ if (row?.kind === "sequenced" && seq <= row.seq) return;
750
989
  this.rows.set(key, {
990
+ kind: "sequenced",
751
991
  value,
752
992
  seq
753
993
  });
754
994
  this.changed(key);
755
995
  }
756
996
  /**
757
- * Seed from a history tail page's projections block: every carried key
758
- * lands under the same seq rule as frames; a key the block omits is
759
- * capability-absent as of the cut — its row clears unless a newer frame
760
- * already superseded the cut (a stale baseline can neither overwrite nor
761
- * clear newer values).
997
+ * Fill keys from a session-list block the Host labeled `cached`: a zero-I/O
998
+ * view of the persisted checkpoint. A cached value lands only where no
999
+ * sequenced row exists: a connected Session has already answered for such
1000
+ * a key, and the list's view of the persisted checkpoint cannot be newer
1001
+ * than it.
1002
+ * @param values - whole values by key viewed from the persisted checkpoint.
1003
+ */
1004
+ applyCached(values) {
1005
+ for (const key of Object.keys(values)) {
1006
+ if (this.rows.get(key)?.kind === "sequenced") continue;
1007
+ this.rows.set(key, {
1008
+ kind: "cached",
1009
+ value: values[key]
1010
+ });
1011
+ this.changed(key);
1012
+ }
1013
+ }
1014
+ /**
1015
+ * Seed from a history tail page's projections block. Every cached row is
1016
+ * discarded first, regardless of seq: the block comes from the connected
1017
+ * Session, and a value viewed from the persisted checkpoint never outranks
1018
+ * it. Then every carried key lands under the same seq rule as frames, and a
1019
+ * key the block omits is capability-absent as of the cut — its row clears
1020
+ * unless a newer frame already superseded the cut (a stale baseline can
1021
+ * neither overwrite nor clear newer sequenced values).
762
1022
  * @param baseline - the response's projections block.
763
1023
  */
764
1024
  seed(baseline) {
1025
+ for (const [key, row] of this.rows) {
1026
+ if (row.kind !== "cached") continue;
1027
+ this.rows.delete(key);
1028
+ this.changed(key);
1029
+ }
765
1030
  const values = baseline.values;
766
1031
  for (const key of Object.keys(values)) this.apply(key, values[key], baseline.asOfSeq);
767
1032
  for (const [key, row] of this.rows) {
768
1033
  if (Object.hasOwn(values, key)) continue;
769
- if (row.seq > baseline.asOfSeq) continue;
1034
+ if (row.kind === "sequenced" && row.seq > baseline.asOfSeq) continue;
770
1035
  this.rows.delete(key);
771
1036
  this.changed(key);
772
1037
  }
@@ -831,323 +1096,130 @@ window.__ModuleLoader__.load({
831
1096
  };
832
1097
  }
833
1098
  function materialize(node) {
834
- if (node.kind === "leaf") return node.entries;
835
- const entries = new Array(node.length);
836
- const pending = [node];
837
- let index = 0;
838
- while (pending.length > 0) {
839
- const current = pending.pop();
840
- if (current.kind === "concat") {
841
- pending.push(current.right, current.left);
842
- continue;
843
- }
844
- for (const entry of current.entries) {
845
- entries[index] = entry;
846
- index += 1;
847
- }
848
- }
849
- return entries;
850
- }
851
- function windowSnapshot(node, hasMore, revision, change) {
852
- let entries;
853
- return {
854
- get entries() {
855
- entries ??= materialize(node);
856
- return entries;
857
- },
858
- hasMore,
859
- revision,
860
- change
861
- };
862
- }
863
- /** Session-owned event feed; every accepted window mutation publishes synchronously. */
864
- var MutableSessionEventSource = class {
865
- listeners = /* @__PURE__ */ new Set();
866
- window = leaf([]);
867
- snapshot = windowSnapshot(this.window, false, 0, {
868
- kind: "replace",
869
- entries: []
870
- });
871
- /** @returns the cached event-window snapshot. */
872
- getSnapshot() {
873
- return this.snapshot;
874
- }
875
- /**
876
- * Subscribe to synchronous window publication.
877
- * @param listener - invalidation callback.
878
- * @returns unsubscribe function.
879
- */
880
- subscribe(listener) {
881
- this.listeners.add(listener);
882
- return () => {
883
- this.listeners.delete(listener);
884
- };
885
- }
886
- /**
887
- * Replace the complete contiguous window.
888
- * @param entries - complete window.
889
- * @param hasMore - whether older history remains.
890
- */
891
- replace(entries, hasMore) {
892
- this.window = leaf(entries);
893
- this.publish(hasMore, {
894
- kind: "replace",
895
- entries
896
- });
897
- }
898
- /**
899
- * Prepend one older contiguous page.
900
- * @param entries - newly loaded older entries.
901
- * @param hasMore - whether still older history remains.
902
- */
903
- prepend(entries, hasMore) {
904
- this.window = concat(leaf(entries), this.window);
905
- this.publish(hasMore, {
906
- kind: "prepend",
907
- entries
908
- });
909
- }
910
- /**
911
- * Append one contiguous live entry.
912
- * @param entry - live tail entry.
913
- */
914
- append(entry) {
915
- const entries = [entry];
916
- this.window = concat(this.window, leaf(entries));
917
- this.publish(this.snapshot.hasMore, {
918
- kind: "append",
919
- entries
920
- });
921
- }
922
- /**
923
- * Replace one attempt's transient rows with its committed durable settlement.
924
- * @param attemptId - process-local attempt whose live rows are now redundant.
925
- * @param entry - durable settlement committed for that attempt.
926
- */
927
- settleAssistant(attemptId, entry) {
928
- const entries = materialize(this.window).filter((candidate) => candidate.type !== "transient" || candidate.event.data.attemptId !== attemptId);
929
- if (entry !== void 0) {
930
- const index = entries.findIndex((candidate) => candidate.event.seq > entry.event.seq);
931
- if (index < 0) entries.push(entry);
932
- else entries.splice(index, 0, entry);
933
- }
934
- this.window = leaf(entries);
935
- this.publish(this.snapshot.hasMore, {
936
- kind: "settle-assistant",
937
- attemptId,
938
- ...entry === void 0 ? {} : { entry }
939
- });
940
- }
941
- publish(hasMore, change) {
942
- this.snapshot = windowSnapshot(this.window, hasMore, this.snapshot.revision + 1, change);
943
- (0, _deepseek_ai_dsh_client_store.notifySubscribers)(this.listeners, "[session-controller] event feed");
944
- }
945
- };
946
- //#endregion
947
- //#region lib/types/client/time-zone.js
948
- /** Browser-owned time-zone sampling for one prompt RPC. */
949
- /**
950
- * Resolve the current browser IANA zone for one outbound operation.
951
- * @returns The browser-provided canonical zone.
952
- * @throws when the runtime cannot provide a non-empty zone.
953
- */
954
- function resolvedClientTimeZone() {
955
- const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
956
- if (typeof timeZone !== "string" || timeZone.length === 0) throw new Error("browser time zone is unavailable");
957
- return timeZone;
958
- }
959
- //#endregion
960
- //#region ../../util/values/lib/index.js
961
- /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
962
- function hasIntrinsicConstructor(prototype, name) {
963
- const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
964
- if (typeof constructor !== "function") return false;
965
- try {
966
- return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
967
- } catch {
968
- return false;
969
- }
970
- }
971
- /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
972
- function isIntrinsicObjectPrototype(value) {
973
- return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
974
- }
975
- /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
976
- function hasPlainArrayPrototype(value) {
977
- const prototype = Object.getPrototypeOf(value);
978
- if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
979
- const objectPrototype = Object.getPrototypeOf(prototype);
980
- return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
981
- }
982
- /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
983
- function hasPlainObjectPrototype(value) {
984
- const prototype = Object.getPrototypeOf(value);
985
- return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
986
- }
987
- /** Return every JSON-visible object key, or reject own data JSON would discard. */
988
- function enumerableStringKeys(value) {
989
- const keys = Reflect.ownKeys(value);
990
- if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
991
- return keys;
992
- }
993
- /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
994
- function walkJsonValue(value, detach) {
995
- const ancestors = /* @__PURE__ */ new Set();
996
- let root;
997
- const assign = (destination, item) => {
998
- if (destination === void 0) return;
999
- if (destination.kind === "root") root = item;
1000
- else if (destination.kind === "array") destination.target[destination.index] = item;
1001
- else Object.defineProperty(destination.target, destination.key, {
1002
- value: item,
1003
- enumerable: true,
1004
- configurable: true,
1005
- writable: true
1006
- });
1007
- };
1008
- const tasks = [{
1009
- kind: "visit",
1010
- value,
1011
- ...detach ? { destination: { kind: "root" } } : {}
1012
- }];
1013
- for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
1014
- if (task.kind === "leave") {
1015
- ancestors.delete(task.source);
1016
- continue;
1017
- }
1018
- if (task.kind === "array-item") {
1019
- if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
1020
- tasks.push({
1021
- kind: "visit",
1022
- value: task.source[task.index],
1023
- ...task.target === void 0 ? {} : { destination: {
1024
- kind: "array",
1025
- target: task.target,
1026
- index: task.index
1027
- } }
1028
- });
1029
- continue;
1030
- }
1031
- if (task.kind === "object-property") {
1032
- tasks.push({
1033
- kind: "visit",
1034
- value: task.source[task.key],
1035
- ...task.target === void 0 ? {} : { destination: {
1036
- kind: "object",
1037
- target: task.target,
1038
- key: task.key
1039
- } }
1040
- });
1041
- continue;
1042
- }
1043
- const current = task.value;
1044
- if (current === null) {
1045
- assign(task.destination, null);
1046
- continue;
1047
- }
1048
- if (typeof current === "boolean" || typeof current === "string") {
1049
- assign(task.destination, current);
1050
- continue;
1051
- }
1052
- if (typeof current === "number") {
1053
- if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
1054
- assign(task.destination, current);
1055
- continue;
1056
- }
1057
- if (typeof current !== "object") return void 0;
1058
- if (ancestors.has(current)) return void 0;
1059
- if (Array.isArray(current)) {
1060
- if (!hasPlainArrayPrototype(current)) return void 0;
1061
- const length = current.length;
1062
- if (Reflect.ownKeys(current).length !== length + 1) return void 0;
1063
- const target = detach ? [] : void 0;
1064
- if (target !== void 0) assign(task.destination, target);
1065
- ancestors.add(current);
1066
- tasks.push({
1067
- kind: "leave",
1068
- source: current
1069
- });
1070
- for (let index = length - 1; index >= 0; index--) tasks.push({
1071
- kind: "array-item",
1072
- source: current,
1073
- index,
1074
- ...target === void 0 ? {} : { target }
1075
- });
1076
- continue;
1077
- }
1078
- if (!hasPlainObjectPrototype(current)) return void 0;
1079
- const keys = enumerableStringKeys(current);
1080
- if (keys === void 0) return void 0;
1081
- const target = detach ? {} : void 0;
1082
- if (target !== void 0) assign(task.destination, target);
1083
- ancestors.add(current);
1084
- tasks.push({
1085
- kind: "leave",
1086
- source: current
1087
- });
1088
- for (let index = keys.length - 1; index >= 0; index--) {
1089
- const key = keys[index];
1090
- /* v8 ignore next -- the loop is bounded by the captured key count. */
1091
- if (key === void 0) return void 0;
1092
- tasks.push({
1093
- kind: "object-property",
1094
- source: current,
1095
- key,
1096
- ...target === void 0 ? {} : { target }
1097
- });
1098
- }
1099
- }
1100
- return detach ? root : true;
1101
- }
1102
- /**
1103
- * Validate and detach lossless JSON in one read per property.
1104
- * @param value - candidate value to validate and detach.
1105
- * @returns the detached snapshot, or `undefined` when the value is not losslessly JSON-serializable.
1106
- */
1107
- function snapshotJsonValue(value) {
1108
- return walkJsonValue(value, true);
1109
- }
1110
- /**
1111
- * Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
1112
- * @param value - value to freeze.
1113
- * @returns the same value after every reachable enumerable child is frozen.
1114
- */
1115
- function deepFreeze(value) {
1116
- const seen = /* @__PURE__ */ new WeakSet();
1117
- const pending = [{
1118
- kind: "visit",
1119
- node: value
1120
- }];
1099
+ if (node.kind === "leaf") return node.entries;
1100
+ const entries = new Array(node.length);
1101
+ const pending = [node];
1102
+ let index = 0;
1121
1103
  while (pending.length > 0) {
1122
- const task = pending.pop();
1123
- /* v8 ignore next -- the loop condition guarantees one pending task. */
1124
- if (task === void 0) continue;
1125
- if (task.kind === "property") {
1126
- pending.push({
1127
- kind: "visit",
1128
- node: task.source[task.key]
1129
- });
1104
+ const current = pending.pop();
1105
+ if (current.kind === "concat") {
1106
+ pending.push(current.right, current.left);
1130
1107
  continue;
1131
1108
  }
1132
- const node = task.node;
1133
- if (node === null || typeof node !== "object") continue;
1134
- if (node instanceof AbortSignal) continue;
1135
- if (seen.has(node)) continue;
1136
- seen.add(node);
1137
- Object.freeze(node);
1138
- const keys = Object.keys(node);
1139
- for (let index = keys.length - 1; index >= 0; index--) {
1140
- const key = keys[index];
1141
- /* v8 ignore next -- the loop is bounded by the captured key count. */
1142
- if (key === void 0) continue;
1143
- pending.push({
1144
- kind: "property",
1145
- source: node,
1146
- key
1147
- });
1109
+ for (const entry of current.entries) {
1110
+ entries[index] = entry;
1111
+ index += 1;
1148
1112
  }
1149
1113
  }
1150
- return value;
1114
+ return entries;
1115
+ }
1116
+ function windowSnapshot(node, hasMore, revision, change) {
1117
+ let entries;
1118
+ return {
1119
+ get entries() {
1120
+ entries ??= materialize(node);
1121
+ return entries;
1122
+ },
1123
+ hasMore,
1124
+ revision,
1125
+ change
1126
+ };
1127
+ }
1128
+ /** Session-owned event feed; every accepted window mutation publishes synchronously. */
1129
+ var MutableSessionEventSource = class {
1130
+ listeners = /* @__PURE__ */ new Set();
1131
+ window = leaf([]);
1132
+ snapshot = windowSnapshot(this.window, false, 0, {
1133
+ kind: "replace",
1134
+ entries: []
1135
+ });
1136
+ /** @returns the cached event-window snapshot. */
1137
+ getSnapshot() {
1138
+ return this.snapshot;
1139
+ }
1140
+ /**
1141
+ * Subscribe to synchronous window publication.
1142
+ * @param listener - invalidation callback.
1143
+ * @returns unsubscribe function.
1144
+ */
1145
+ subscribe(listener) {
1146
+ this.listeners.add(listener);
1147
+ return () => {
1148
+ this.listeners.delete(listener);
1149
+ };
1150
+ }
1151
+ /**
1152
+ * Replace the complete contiguous window.
1153
+ * @param entries - complete window.
1154
+ * @param hasMore - whether older history remains.
1155
+ */
1156
+ replace(entries, hasMore) {
1157
+ this.window = leaf(entries);
1158
+ this.publish(hasMore, {
1159
+ kind: "replace",
1160
+ entries
1161
+ });
1162
+ }
1163
+ /**
1164
+ * Prepend one older contiguous page.
1165
+ * @param entries - newly loaded older entries.
1166
+ * @param hasMore - whether still older history remains.
1167
+ */
1168
+ prepend(entries, hasMore) {
1169
+ this.window = concat(leaf(entries), this.window);
1170
+ this.publish(hasMore, {
1171
+ kind: "prepend",
1172
+ entries
1173
+ });
1174
+ }
1175
+ /**
1176
+ * Append one contiguous live entry.
1177
+ * @param entry - live tail entry.
1178
+ */
1179
+ append(entry) {
1180
+ const entries = [entry];
1181
+ this.window = concat(this.window, leaf(entries));
1182
+ this.publish(this.snapshot.hasMore, {
1183
+ kind: "append",
1184
+ entries
1185
+ });
1186
+ }
1187
+ /**
1188
+ * Replace one attempt's transient rows with its committed durable settlement.
1189
+ * @param attemptId - process-local attempt whose live rows are now redundant.
1190
+ * @param entry - durable settlement committed for that attempt.
1191
+ */
1192
+ settleAssistant(attemptId, entry) {
1193
+ const entries = materialize(this.window).filter((candidate) => candidate.type !== "transient" || candidate.event.data.attemptId !== attemptId);
1194
+ if (entry !== void 0) {
1195
+ const index = entries.findIndex((candidate) => candidate.event.seq > entry.event.seq);
1196
+ if (index < 0) entries.push(entry);
1197
+ else entries.splice(index, 0, entry);
1198
+ }
1199
+ this.window = leaf(entries);
1200
+ this.publish(this.snapshot.hasMore, {
1201
+ kind: "settle-assistant",
1202
+ attemptId,
1203
+ ...entry === void 0 ? {} : { entry }
1204
+ });
1205
+ }
1206
+ publish(hasMore, change) {
1207
+ this.snapshot = windowSnapshot(this.window, hasMore, this.snapshot.revision + 1, change);
1208
+ (0, _deepseek_ai_dsh_client_store.notifySubscribers)(this.listeners, "[session-controller] event feed");
1209
+ }
1210
+ };
1211
+ //#endregion
1212
+ //#region lib/types/client/time-zone.js
1213
+ /** Browser-owned time-zone sampling for one prompt RPC. */
1214
+ /**
1215
+ * Resolve the current browser IANA zone for one outbound operation.
1216
+ * @returns The browser-provided canonical zone.
1217
+ * @throws when the runtime cannot provide a non-empty zone.
1218
+ */
1219
+ function resolvedClientTimeZone() {
1220
+ const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
1221
+ if (typeof timeZone !== "string" || timeZone.length === 0) throw new Error("browser time zone is unavailable");
1222
+ return timeZone;
1151
1223
  }
1152
1224
  //#endregion
1153
1225
  //#region ../../llm/llm/lib/types/assistant-stream.js
@@ -1456,6 +1528,20 @@ window.__ModuleLoader__.load({
1456
1528
  asOfSeq: value.asOfSeq === -1 ? -1 : SessionSeq(value.asOfSeq)
1457
1529
  };
1458
1530
  }
1531
+ const HISTORY_PAGE_OPTIONS = {
1532
+ maxMessages: 500,
1533
+ turnWindow: {
1534
+ minMessages: 50,
1535
+ minTurns: 2
1536
+ }
1537
+ };
1538
+ const JUMP_PAGE_OPTIONS = {
1539
+ ...HISTORY_PAGE_OPTIONS,
1540
+ turnWindow: {
1541
+ ...HISTORY_PAGE_OPTIONS.turnWindow,
1542
+ minMessages: 200
1543
+ }
1544
+ };
1459
1545
  /**
1460
1546
  * Owns a session's event window, lifecycle state, and observable
1461
1547
  * snapshot. React bindings remain outside this data layer. Features see only
@@ -1479,6 +1565,7 @@ window.__ModuleLoader__.load({
1479
1565
  jumpTargetSeq = null;
1480
1566
  /** The running jump loop's completion, shared by retargeting callers. */
1481
1567
  jumpPromise = null;
1568
+ pendingHistory = null;
1482
1569
  stopObservingInbox;
1483
1570
  assistantStream = new ClientAssistantStream();
1484
1571
  running = false;
@@ -1492,7 +1579,7 @@ window.__ModuleLoader__.load({
1492
1579
  promptAttempted = false;
1493
1580
  /** A first accepted prompt stays in the engaging phase until its turn is observable. */
1494
1581
  firstPromptPendingTurn = false;
1495
- /** Empty-log mirror (see ConversationSnapshot.blank); unknown bare sessions begin conservatively blank. */
1582
+ /** New Session display state; unknown bare sessions begin conservatively blank. */
1496
1583
  blankBit = true;
1497
1584
  removed = false;
1498
1585
  promptError = null;
@@ -1508,8 +1595,9 @@ window.__ModuleLoader__.load({
1508
1595
  * Per-session projection value store (push model; see the session-projection
1509
1596
  * subsystem page, docs/subsystems/session-projection.md): finished whole
1510
1597
  * values computed on the Host, seeded by the tail page's
1511
- * projections block and updated by Session Controller control frames under the
1512
- * one higher-seq-wins rule. Keys are read via `projections.faceOf(key)`
1598
+ * projections block and updated by Session Controller control frames;
1599
+ * Host-sequenced writes merge under higher-seq-wins and cached list blocks
1600
+ * yield to them (projection-store.ts). Keys are read via `projections.faceOf(key)`
1513
1601
  * (the useProjection resolution face); the conversation snapshot never
1514
1602
  * carries projection values, and no client-side domain folding exists.
1515
1603
  * Manager-owned when constructed through SessionManager (frames route and
@@ -1574,14 +1662,16 @@ window.__ModuleLoader__.load({
1574
1662
  */
1575
1663
  beginSubmission(input) {
1576
1664
  const requestId = randomUUID();
1665
+ const placement = this.running ? input.mode === "steer" ? "steering" : "queued" : "transcript";
1577
1666
  this.pendingSubmissions = [...this.pendingSubmissions, {
1578
1667
  requestId,
1579
- placement: this.running ? input.mode === "steer" ? "steering" : "queued" : "transcript",
1668
+ placement,
1580
1669
  time: Date.now(),
1581
1670
  text: input.text,
1582
1671
  attachments: input.attachments
1583
1672
  }];
1584
1673
  this.submissionSettlements.set(requestId, {
1674
+ placement,
1585
1675
  onRetire: input.onRetire,
1586
1676
  retiring: false
1587
1677
  });
@@ -1649,9 +1739,9 @@ window.__ModuleLoader__.load({
1649
1739
  }
1650
1740
  if (this.blankBit) {
1651
1741
  this.blankBit = false;
1652
- this.options.onEngaged?.(this);
1653
1742
  this.notifier.markDirty();
1654
1743
  }
1744
+ this.options.onEngaged?.(this);
1655
1745
  return result;
1656
1746
  }
1657
1747
  /**
@@ -1752,7 +1842,7 @@ window.__ModuleLoader__.load({
1752
1842
  this.openPromise = promise;
1753
1843
  return promise;
1754
1844
  }
1755
- /** Page up: pull one earlier page with the window's first seq as beforeSeq and prepend. */
1845
+ /** Prepend one Turn-aligned page: at least 50 messages and two Turn starts, capped at 500 messages. */
1756
1846
  async loadOlder() {
1757
1847
  if (this.openState !== "open" || !this.hasMore || this.loadingOlder) return;
1758
1848
  const events = this.events;
@@ -1762,7 +1852,7 @@ window.__ModuleLoader__.load({
1762
1852
  try {
1763
1853
  await events.prepend({
1764
1854
  beforeSeq: this.baseSeq,
1765
- maxMessages: 50
1855
+ ...HISTORY_PAGE_OPTIONS
1766
1856
  });
1767
1857
  } catch (error) {
1768
1858
  if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) console.error("[session-controller] loadOlder failed:", error);
@@ -1779,29 +1869,37 @@ window.__ModuleLoader__.load({
1779
1869
  return this.jumpPromise;
1780
1870
  }
1781
1871
  if (this.loadingOlder) return Promise.resolve();
1872
+ const events = this.events;
1873
+ if (events === void 0) return Promise.resolve();
1874
+ const pending = {
1875
+ beforeSeq: this.baseSeq,
1876
+ hasMore: this.hasMore,
1877
+ pages: []
1878
+ };
1879
+ this.pendingHistory = pending;
1782
1880
  this.jumpTargetSeq = seq;
1783
1881
  this.loadingOlder = true;
1784
1882
  this.notifier.markDirty();
1785
1883
  const generation = this.openGeneration;
1786
1884
  this.jumpPromise = (async () => {
1787
1885
  try {
1788
- while (this.hasMore && this.jumpTargetSeq !== null && this.baseSeq > this.jumpTargetSeq) {
1886
+ while (pending.hasMore && this.jumpTargetSeq !== null && pending.beforeSeq > this.jumpTargetSeq) {
1789
1887
  if (generation !== this.openGeneration) return;
1790
- const events = this.events;
1791
- if (events === void 0) return;
1792
- const before = this.baseSeq;
1888
+ const before = pending.beforeSeq;
1793
1889
  await events.prepend({
1794
- beforeSeq: this.baseSeq,
1795
- maxMessages: 200
1890
+ beforeSeq: before,
1891
+ ...JUMP_PAGE_OPTIONS
1796
1892
  });
1797
- if (this.baseSeq >= before) return;
1893
+ if (pending.beforeSeq >= before) return;
1798
1894
  }
1799
1895
  } catch (error) {
1800
1896
  if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) console.error("[session-controller] loadThrough failed:", error);
1801
1897
  } finally {
1802
1898
  this.jumpTargetSeq = null;
1803
1899
  this.jumpPromise = null;
1900
+ this.pendingHistory = null;
1804
1901
  this.loadingOlder = false;
1902
+ if (generation === this.openGeneration && pending.pages.length > 0) this.prependWindow(pending.pages.reverse().flat(), pending.hasMore);
1805
1903
  this.notifier.markDirty();
1806
1904
  }
1807
1905
  })();
@@ -1876,13 +1974,15 @@ window.__ModuleLoader__.load({
1876
1974
  this.notifier.markDirty();
1877
1975
  }
1878
1976
  /**
1879
- * Blank-bit relay from the authoritative summary source (`session.list` and
1880
- * `api-session/added`). Monotone: once any signal (local first send,
1881
- * running flip, an earlier summary) cleared it, a stale true never
1882
- * re-blanks.
1883
- * @param blank - the summary's derived empty-log bit.
1977
+ * Apply the Manager's effective display blank, further reconciled with the
1978
+ * current `sessionListMetadata` projection. Local send attempts and current
1979
+ * running state prevent re-blanking; an earlier false summary alone does not.
1980
+ * The Manager retains acceptance and earlier running observations across
1981
+ * Session-object replacement.
1982
+ * @param blank - New Session display state after Manager reconciliation.
1884
1983
  */
1885
1984
  handleBlank(blank) {
1985
+ blank = blank && this.projections.values().sessionListMetadata?.blank !== false;
1886
1986
  if (blank === this.blankBit) return;
1887
1987
  if (blank && (this.promptAttempted || this.running)) return;
1888
1988
  this.blankBit = blank;
@@ -1907,7 +2007,8 @@ window.__ModuleLoader__.load({
1907
2007
  */
1908
2008
  async dispose() {
1909
2009
  this.stopObservingInbox();
1910
- for (const requestId of [...this.submissionSettlements.keys()]) this.retireFailedSubmission(requestId);
2010
+ for (const [requestId, settlement] of [...this.submissionSettlements]) if (settlement.admitted !== void 0) this.scheduleObservedRetirement(requestId, settlement.admitted);
2011
+ else this.retireFailedSubmission(requestId);
1911
2012
  this.openGeneration++;
1912
2013
  const events = this.events;
1913
2014
  this.events = void 0;
@@ -1929,7 +2030,7 @@ window.__ModuleLoader__.load({
1929
2030
  });
1930
2031
  this.events = events;
1931
2032
  try {
1932
- await events.open({ maxMessages: 50 });
2033
+ await events.open(HISTORY_PAGE_OPTIONS);
1933
2034
  if (generation !== this.openGeneration || this.events !== events) return;
1934
2035
  this.openState = "open";
1935
2036
  } catch (error) {
@@ -1962,10 +2063,22 @@ window.__ModuleLoader__.load({
1962
2063
  const visible = this.assistantStream.replace(entries, assistantStream);
1963
2064
  this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
1964
2065
  this.hasMore = hasMore;
2066
+ if (this.pendingHistory !== null) {
2067
+ this.pendingHistory.beforeSeq = this.baseSeq;
2068
+ this.pendingHistory.hasMore = hasMore;
2069
+ this.pendingHistory.pages.length = 0;
2070
+ }
1965
2071
  if (visible.some((entry) => entry.event.type === "turn/start")) this.firstPromptPendingTurn = false;
1966
2072
  if (projections !== void 0) this.projections.seed(projections);
1967
2073
  this.eventSource.replace(visible, hasMore);
2074
+ if (projections !== void 0) {
2075
+ for (const [requestId, { receipt }] of this.submissionSettlements) if (receipt !== void 0 && receipt.seq <= projections.asOfSeq) this.scheduleObservedRetirement(requestId, receipt.attachments);
2076
+ }
1968
2077
  for (const entry of visible) this.observeSubmissionEvent(entry.event);
2078
+ if (projections !== void 0) {
2079
+ const inbox = projections.values.inbox;
2080
+ for (const target of ["next-turn", "next-step"]) this.observeSubmissionInsertions(target, inbox?.[target] ?? [], 0, projections.asOfSeq);
2081
+ }
1969
2082
  this.notifier.markDirty();
1970
2083
  }
1971
2084
  publishAssistantEntry(result) {
@@ -1995,6 +2108,13 @@ window.__ModuleLoader__.load({
1995
2108
  }
1996
2109
  /** Prepend one stream-validated history page. */
1997
2110
  prependWindow(entries, hasMore) {
2111
+ if (this.pendingHistory !== null) {
2112
+ const pending = this.pendingHistory;
2113
+ pending.beforeSeq = entries[0] === void 0 ? pending.beforeSeq : SessionLogOffset(entries[0].event.seq);
2114
+ pending.hasMore = hasMore;
2115
+ pending.pages.push(entries);
2116
+ return;
2117
+ }
1998
2118
  this.baseSeq = entries[0] === void 0 ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
1999
2119
  this.hasMore = hasMore;
2000
2120
  this.eventSource.prepend(entries, hasMore);
@@ -2012,28 +2132,73 @@ window.__ModuleLoader__.load({
2012
2132
  observeSubmissionEvent(event) {
2013
2133
  if (this.submissionSettlements.size === 0) return;
2014
2134
  if (event.type === "agent/inbox/spliced") {
2015
- const splice = event.data;
2016
- if (Array.isArray(splice?.inserted)) for (const message of splice.inserted) this.observeSubmissionEvent({
2017
- type: "user/message",
2018
- data: message
2019
- });
2135
+ const { target, start, removedCount = 0, inserted, outcome } = event.data;
2136
+ for (const [requestId, settlement] of this.submissionSettlements) {
2137
+ const receipt = settlement.receipt;
2138
+ if (receipt?.target !== target || receipt.index === null || receipt.seq >= event.seq) continue;
2139
+ const removed = receipt.index >= start && receipt.index < start + removedCount;
2140
+ if (removed && outcome === "canceled") this.retireFailedSubmission(requestId);
2141
+ else settlement.receipt = {
2142
+ ...receipt,
2143
+ seq: event.seq,
2144
+ index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount
2145
+ };
2146
+ }
2147
+ this.observeSubmissionInsertions(target, inserted, start, event.seq);
2148
+ for (const message of inserted) this.observeSubmissionMessage(message, false);
2149
+ return;
2150
+ }
2151
+ if (event.type === "request/context" || event.type === "turn/end") {
2152
+ for (const [requestId, settlement] of this.submissionSettlements) if (settlement.admitted === void 0 && settlement.receipt?.index === null && settlement.receipt.seq < event.seq) this.retireFailedSubmission(requestId);
2153
+ return;
2154
+ }
2155
+ if (event.type === "user/message") this.observeSubmissionMessage(event.data, true);
2156
+ }
2157
+ observeSubmissionInsertions(target, messages, start, seq) {
2158
+ for (const [index, message] of messages.entries()) {
2159
+ const source = message.source;
2160
+ if (source.kind !== "user" || !("rpcId" in source)) continue;
2161
+ const settlement = this.submissionSettlements.get(source.rpcId);
2162
+ if (settlement === void 0 || settlement.placement === "queued" || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq) continue;
2163
+ settlement.receipt = {
2164
+ target,
2165
+ seq,
2166
+ index: start + index,
2167
+ attachments: attachmentRefsIn(message.content)
2168
+ };
2169
+ }
2170
+ }
2171
+ observeSubmissionMessage(message, admitted) {
2172
+ const source = message.source;
2173
+ if (source.kind !== "user" || !("rpcId" in source)) return;
2174
+ const settlement = this.submissionSettlements.get(source.rpcId);
2175
+ if (settlement === void 0 || settlement.retiring) return;
2176
+ if (!admitted) {
2177
+ if (settlement.placement === "queued") this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
2020
2178
  return;
2021
2179
  }
2022
- if (event.type !== "user/message") return;
2023
- const data = event.data;
2024
- const source = data?.source;
2025
- if (source?.kind !== "user" || typeof source.rpcId !== "string") return;
2026
- this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(data?.content));
2180
+ settlement.admitted = attachmentRefsIn(message.content);
2181
+ this.retireAdmittedSubmission(source.rpcId);
2182
+ }
2183
+ /** Retire admitted Chat identities only after stale Inbox rows can no longer reappear. */
2184
+ retireAdmittedSubmission(requestId) {
2185
+ const settlement = this.submissionSettlements.get(requestId);
2186
+ if (settlement?.admitted === void 0) return;
2187
+ const receipt = settlement.receipt;
2188
+ if (receipt?.index === null && (this.projections.seqOf("inbox") ?? -1) < receipt.seq) return;
2189
+ this.scheduleObservedRetirement(requestId, settlement.admitted);
2027
2190
  }
2028
- /** Retire local echoes when their accepted messages appear in the durable Inbox projection. */
2191
+ /** Inbox acceptance retires queued echoes; its watermark completes admitted Chat handoffs. */
2029
2192
  observeSubmissionInbox() {
2030
2193
  if (this.submissionSettlements.size === 0) return;
2031
2194
  const inbox = this.projections.get("inbox");
2032
2195
  if (inbox === void 0) return;
2033
- for (const message of [...inbox["next-turn"], ...inbox["next-step"]]) {
2034
- const source = message.source;
2035
- if (source.kind === "user" && "rpcId" in source) this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
2196
+ const seq = this.projections.seqOf("inbox");
2197
+ for (const target of ["next-turn", "next-step"]) {
2198
+ if (seq !== void 0) this.observeSubmissionInsertions(target, inbox[target], 0, seq);
2199
+ for (const message of inbox[target]) this.observeSubmissionMessage(message, false);
2036
2200
  }
2201
+ for (const requestId of this.submissionSettlements.keys()) this.retireAdmittedSubmission(requestId);
2037
2202
  }
2038
2203
  /**
2039
2204
  * Latch one observed settlement and remove the echo an animation frame
@@ -2055,7 +2220,7 @@ window.__ModuleLoader__.load({
2055
2220
  /** Remove one unsettled echo immediately (prompt rejection, abort, or disposal). */
2056
2221
  retireFailedSubmission(requestId) {
2057
2222
  const settlement = this.submissionSettlements.get(requestId);
2058
- if (settlement === void 0 || settlement.retiring) return;
2223
+ if (settlement === void 0 || settlement.retiring || settlement.admitted !== void 0) return;
2059
2224
  settlement.retiring = true;
2060
2225
  this.finishSubmission(requestId, { reason: "failed" });
2061
2226
  }
@@ -2082,12 +2247,16 @@ window.__ModuleLoader__.load({
2082
2247
  this.notifier.markDirty();
2083
2248
  }
2084
2249
  buildSnapshot() {
2250
+ const identity = this.projections.values().subagent;
2085
2251
  return {
2086
2252
  sessionId: this.sessionId,
2087
2253
  pendingSubmissions: this.pendingSubmissions,
2088
2254
  running: this.running,
2089
2255
  subagent: this.address === void 0 ? null : {
2090
- address: this.address,
2256
+ address: this.address.mode === "unknown" && identity != null ? {
2257
+ ...this.address,
2258
+ mode: identity.mode
2259
+ } : this.address,
2091
2260
  ...this.parentAvailable === void 0 ? {} : { parentAvailable: this.parentAvailable }
2092
2261
  },
2093
2262
  removed: this.removed,
@@ -2136,15 +2305,21 @@ window.__ModuleLoader__.load({
2136
2305
  function sessionSeqCursor(value) {
2137
2306
  return value === -1 ? -1 : SessionSeq(value);
2138
2307
  }
2139
- function catalogAvailability(parentAvailable) {
2140
- return parentAvailable === void 0 ? {} : { parentAvailable };
2141
- }
2142
2308
  /** Instance cluster + frame entry + the session list. */
2143
2309
  var SessionManager = class {
2144
2310
  remote;
2145
2311
  sessions = /* @__PURE__ */ new Map();
2146
2312
  /** In-flight Session disposals remain here after instances leave `sessions`, so manager disposal can await quiescence. */
2147
2313
  sessionDisposals = /* @__PURE__ */ new Set();
2314
+ /**
2315
+ * Accepted/running presentation must survive a later empty-history list
2316
+ * response. Host-asserted running is recorded even before a row, instance, or
2317
+ * address holds the identity — the listing that would hold it may not have
2318
+ * landed yet — while the client-local acceptance callback requires a current
2319
+ * holder, because it can arrive from a replaced or already-dropped Session.
2320
+ */
2321
+ engagedSessions = /* @__PURE__ */ new Set();
2322
+ disposed = false;
2148
2323
  /** Per-session projection value stores, retained independently of instance arrival (the
2149
2324
  * title-snapshot precedent, generalized): push frames land here whether or not the Session
2150
2325
  * is instantiated (list rows read the 'title' key), and an instantiated Session adopts the
@@ -2159,18 +2334,8 @@ window.__ModuleLoader__.load({
2159
2334
  /** Active list request's mutation log; its identity also fences completion after reconnect. */
2160
2335
  listMutations = null;
2161
2336
  addresses = /* @__PURE__ */ new Map();
2162
- catalogs = /* @__PURE__ */ new Map();
2163
- catalogInflight = /* @__PURE__ */ new Map();
2164
- /** Catalog owners whose membership changed while a pull was in flight: one trailing refresh after it settles. */
2165
- catalogStale = /* @__PURE__ */ new Set();
2166
- openCatalogs = /* @__PURE__ */ new Set();
2167
- catalogDebounce = /* @__PURE__ */ new Map();
2168
- /**
2169
- * Background jobs per session, last-wins from Session Controller's control
2170
- * stream. An empty set is stored as an absent key, so absence and `[]` are
2171
- * one representation.
2172
- */
2173
- jobsBySession = /* @__PURE__ */ new Map();
2337
+ projectionLoads = /* @__PURE__ */ new Map();
2338
+ projectionInflight = /* @__PURE__ */ new Map();
2174
2339
  listSnapshotCache;
2175
2340
  /** Entry-identity cache (reference stability): list rebuilds reuse the previous entry
2176
2341
  * object when every field matches — wire refreshes mint all-new summary objects, so identity
@@ -2192,31 +2357,23 @@ window.__ModuleLoader__.load({
2192
2357
  */
2193
2358
  resolveTarget(target) {
2194
2359
  const id = typeof target === "string" ? target : target.childSessionId;
2195
- const address = typeof target === "string" ? this.navigationAddress(id) : target;
2360
+ const address = typeof target === "string" ? this.subagentAddress(id) : target;
2196
2361
  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}`);
2197
2362
  if (address !== void 0) this.addresses.set(id, address);
2198
- this.sessions.get(id)?.configureSubagent(address, address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.parentAvailable);
2363
+ this.sessions.get(id)?.configureSubagent(address, address === void 0 ? void 0 : this.agentAvailable(address.parentSessionId));
2199
2364
  return id;
2200
2365
  }
2201
2366
  /**
2202
- * Return the durable catalog address retained for one child.
2203
- * @param sessionId - possible addressed child id.
2204
- * @returns The direct-parent address, when navigation discovered one.
2205
- */
2206
- subagentAddress(sessionId) {
2207
- return this.navigationAddress(sessionId);
2208
- }
2209
- /**
2210
2367
  * Resolve an address for breadcrumb navigation without retaining transport authority.
2211
2368
  * @param sessionId - possible child id in an already-loaded catalog.
2212
2369
  * @returns A retained or catalog-derived direct-parent address.
2213
2370
  */
2214
- navigationAddress(sessionId) {
2371
+ subagentAddress(sessionId) {
2215
2372
  const retained = this.addresses.get(sessionId);
2216
2373
  if (retained !== void 0) return retained;
2217
- for (const [parentSessionId, catalog] of this.catalogs) {
2218
- const child = catalog.entries.find((entry) => entry.kind === "child" && entry.id === sessionId);
2219
- if (child?.kind === "child") return {
2374
+ for (const parentSessionId of this.projectionStores.keys()) {
2375
+ const child = this.projectionStores.get(parentSessionId)?.values().subagentCatalog?.find((entry) => entry.id === sessionId);
2376
+ if (child !== void 0) return {
2220
2377
  parentSessionId,
2221
2378
  childSessionId: sessionId,
2222
2379
  mode: child.mode
@@ -2234,6 +2391,7 @@ window.__ModuleLoader__.load({
2234
2391
  if (session !== expected) return Promise.resolve();
2235
2392
  this.sessions.delete(sessionId);
2236
2393
  this.addresses.delete(sessionId);
2394
+ this.pruneEngagement(sessionId, this.retainedIds(this.summaries));
2237
2395
  return this.startSessionDisposal(session);
2238
2396
  }
2239
2397
  /**
@@ -2241,10 +2399,14 @@ window.__ModuleLoader__.load({
2241
2399
  * @returns once catalog requests and every Session stream have stopped.
2242
2400
  */
2243
2401
  async dispose() {
2244
- for (const timer of this.catalogDebounce.values()) clearTimeout(timer);
2245
- this.catalogDebounce.clear();
2246
- this.catalogStale.clear();
2247
- this.openCatalogs.clear();
2402
+ this.disposed = true;
2403
+ this.listMutations = null;
2404
+ this.listInflight = null;
2405
+ this.engagedSessions.clear();
2406
+ const reads = [...this.projectionInflight.values()];
2407
+ for (const { controller } of reads) controller.abort();
2408
+ this.projectionInflight.clear();
2409
+ await Promise.all(reads.map((read) => read.promise));
2248
2410
  const sessions = [...this.sessions.values()];
2249
2411
  this.sessions.clear();
2250
2412
  this.addresses.clear();
@@ -2267,6 +2429,7 @@ window.__ModuleLoader__.load({
2267
2429
  /**
2268
2430
  * Lazy build: return the existing instance or construct one (no auto-open —
2269
2431
  * the reference allocator opens history after binding the scope).
2432
+ * New instances reconcile retained metadata before returning.
2270
2433
  * @param sessionId - the session to get.
2271
2434
  * @returns the resident instance.
2272
2435
  */
@@ -2277,42 +2440,71 @@ window.__ModuleLoader__.load({
2277
2440
  this.sessions.set(sessionId, session);
2278
2441
  const summary = this.summaries.find((s) => s.sessionId === sessionId);
2279
2442
  if (summary !== void 0) {
2280
- session.handleBlank(summary.blank);
2443
+ session.handleBlank(this.effectiveBlank(summary));
2281
2444
  session.handleRunning(summary.running);
2282
2445
  } else {
2283
2446
  const address = this.addresses.get(sessionId);
2284
- const child = address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.entries.find((entry) => entry.kind === "child" && entry.id === sessionId);
2285
- if (child?.kind === "child") {
2447
+ if ((address === void 0 ? void 0 : this.projectionStores.get(address.parentSessionId)?.values().subagentCatalog?.find((entry) => entry.id === sessionId)) !== void 0) {
2286
2448
  session.handleBlank(false);
2287
- session.handleRunning(child.activity === "running");
2288
- }
2449
+ session.handleRunning(false);
2450
+ } else session.handleBlank(true);
2289
2451
  }
2290
2452
  }
2291
2453
  return session;
2292
2454
  }
2293
2455
  createSession(sessionId) {
2294
2456
  const address = this.addresses.get(sessionId);
2295
- const parentAvailable = address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.parentAvailable;
2457
+ const parentAvailable = address === void 0 ? void 0 : this.agentAvailable(address.parentSessionId);
2296
2458
  return new Session(sessionId, this.remote, {
2297
2459
  ...address === void 0 ? {} : {
2298
2460
  address,
2299
- ...catalogAvailability(parentAvailable)
2461
+ ...parentAvailable === void 0 ? {} : { parentAvailable }
2300
2462
  },
2301
2463
  onEngaged: (engaged) => {
2302
- this.recordMutation({
2303
- kind: "engaged",
2304
- sessionId: engaged.sessionId
2305
- });
2464
+ if (this.disposed || !this.retainedIds(this.summaries).has(engaged.sessionId)) return;
2465
+ if (!this.engagedSessions.has(engaged.sessionId)) {
2466
+ this.engagedSessions.add(engaged.sessionId);
2467
+ this.recordMutation({
2468
+ kind: "engaged",
2469
+ sessionId: engaged.sessionId
2470
+ });
2471
+ }
2472
+ this.sessions.get(engaged.sessionId)?.handleBlank(false);
2306
2473
  },
2307
2474
  projections: this.projectionStore(sessionId)
2308
2475
  });
2309
2476
  }
2477
+ effectiveBlank(summary) {
2478
+ return summary.blank && !this.engagedSessions.has(summary.sessionId);
2479
+ }
2480
+ /**
2481
+ * Identities an engagement may still belong to: the given list rows, resident
2482
+ * Session instances, and retained child addresses.
2483
+ * @param summaries - list rows of the caller's snapshot.
2484
+ * @returns the retained identity set.
2485
+ */
2486
+ retainedIds(summaries) {
2487
+ const retained = new Set(summaries.map((summary) => summary.sessionId));
2488
+ for (const sessionId of this.sessions.keys()) retained.add(sessionId);
2489
+ for (const sessionId of this.addresses.keys()) retained.add(sessionId);
2490
+ return retained;
2491
+ }
2492
+ /**
2493
+ * Forget one engagement that no retained identity holds.
2494
+ * @param sessionId - identity whose engagement may be dropped.
2495
+ * @param retained - identities from {@link retainedIds} for the caller's snapshot.
2496
+ */
2497
+ pruneEngagement(sessionId, retained) {
2498
+ if (!retained.has(sessionId)) this.engagedSessions.delete(sessionId);
2499
+ }
2310
2500
  /** Resident per-session projection store (create-on-demand; outlives instantiation). */
2311
2501
  projectionStore(sessionId) {
2312
2502
  let store = this.projectionStores.get(sessionId);
2313
2503
  if (store === void 0) {
2314
2504
  store = new ProjectionValueStore();
2505
+ const projections = store;
2315
2506
  store.subscribeAny(() => {
2507
+ if (projections.values().sessionListMetadata?.blank === false) this.sessions.get(sessionId)?.handleBlank(false);
2316
2508
  this.notifier.markDirty();
2317
2509
  });
2318
2510
  this.projectionStores.set(sessionId, store);
@@ -2320,82 +2512,67 @@ window.__ModuleLoader__.load({
2320
2512
  return store;
2321
2513
  }
2322
2514
  /**
2323
- * Refresh one direct-child catalog, reusing its in-flight request.
2324
- * @param parentSessionId - catalog owner.
2515
+ * Load a complete projection baseline once per connection; retry unsuccessful reads.
2516
+ * @param sessionId - Session to inspect without opening its conversation.
2517
+ * @returns completion of the current or newly started read.
2325
2518
  */
2326
- refreshSubagents(parentSessionId) {
2327
- const existing = this.catalogInflight.get(parentSessionId);
2519
+ refreshProjections(sessionId) {
2520
+ const existing = this.projectionInflight.get(sessionId);
2328
2521
  if (existing !== void 0) return existing.promise;
2329
- const previous = this.catalogs.get(parentSessionId);
2330
- const expandableRows = /* @__PURE__ */ new Set();
2331
- const activityRows = /* @__PURE__ */ new Map();
2332
- this.catalogs.set(parentSessionId, {
2333
- entries: previous?.entries ?? [],
2334
- ...previous?.parentAvailable === void 0 ? {} : { parentAvailable: previous.parentAvailable },
2522
+ if (this.projectionLoads.get(sessionId)?.state === "ready") return Promise.resolve();
2523
+ const controller = new AbortController();
2524
+ const store = this.projectionStore(sessionId);
2525
+ const initialValues = store.values();
2526
+ this.projectionLoads.set(sessionId, {
2335
2527
  state: "loading",
2336
2528
  error: null
2337
2529
  });
2338
2530
  this.notifier.markDirty();
2339
2531
  const operation = (async () => {
2340
2532
  try {
2341
- const result = await this.remote.subagents.list(parentSessionId);
2533
+ const result = await this.remote.session.projections({ sessionId }, controller.signal);
2534
+ if (controller.signal.aborted) return;
2342
2535
  if (result.ok) {
2343
- const parentAvailable = this.catalogInflight.get(parentSessionId)?.parentAvailableOverride ?? result.value.parentAvailable;
2344
- this.catalogs.set(parentSessionId, {
2536
+ if (result.value !== null) store.seed({
2345
2537
  ...result.value,
2346
- entries: this.withCatalogMutations(result.value.entries, expandableRows, activityRows),
2347
- parentAvailable,
2538
+ asOfSeq: sessionSeqCursor(result.value.asOfSeq)
2539
+ });
2540
+ else if (store.values() === initialValues) store.clear();
2541
+ this.projectionLoads.set(sessionId, {
2348
2542
  state: "ready",
2349
2543
  error: null
2350
2544
  });
2351
- for (const [childId, address] of this.addresses) {
2352
- if (address.parentSessionId !== parentSessionId) continue;
2353
- this.sessions.get(childId)?.handleSubagentParentAvailable(parentAvailable);
2354
- }
2355
- } else this.catalogs.set(parentSessionId, {
2356
- entries: this.withCatalogMutations(previous?.entries ?? [], expandableRows, activityRows),
2357
- ...catalogAvailability(this.catalogInflight.get(parentSessionId)?.parentAvailableOverride ?? previous?.parentAvailable),
2545
+ } else this.projectionLoads.set(sessionId, {
2358
2546
  state: "error",
2359
2547
  error: result.error
2360
2548
  });
2361
2549
  } catch (error) {
2550
+ if (controller.signal.aborted) return;
2362
2551
  if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) throw error;
2363
- this.catalogs.set(parentSessionId, {
2364
- entries: this.withCatalogMutations(previous?.entries ?? [], expandableRows, activityRows),
2365
- ...catalogAvailability(this.catalogInflight.get(parentSessionId)?.parentAvailableOverride ?? previous?.parentAvailable),
2552
+ this.projectionLoads.set(sessionId, {
2366
2553
  state: "error",
2367
2554
  error
2368
2555
  });
2369
2556
  } finally {
2370
- this.catalogInflight.delete(parentSessionId);
2371
- if (this.catalogStale.delete(parentSessionId)) this.refreshSubagents(parentSessionId);
2372
- this.notifier.markDirty();
2557
+ if (!controller.signal.aborted) {
2558
+ this.projectionInflight.delete(sessionId);
2559
+ this.notifier.markDirty();
2560
+ }
2373
2561
  }
2374
2562
  })();
2375
- this.catalogInflight.set(parentSessionId, {
2563
+ this.projectionInflight.set(sessionId, {
2376
2564
  promise: operation,
2377
- expandableRows,
2378
- activityRows,
2379
- parentAvailableOverride: void 0
2565
+ controller
2380
2566
  });
2381
2567
  return operation;
2382
2568
  }
2383
- /**
2384
- * Mark whether a catalog menu is consuming live membership updates.
2385
- * @param parentSessionId - catalog owner.
2386
- * @param open - current menu state.
2387
- */
2388
- setSubagentCatalogOpen(parentSessionId, open) {
2389
- if (open) {
2390
- this.openCatalogs.add(parentSessionId);
2391
- this.refreshSubagents(parentSessionId);
2392
- } else {
2393
- this.openCatalogs.delete(parentSessionId);
2394
- const timer = this.catalogDebounce.get(parentSessionId);
2395
- if (timer !== void 0) {
2396
- clearTimeout(timer);
2397
- this.catalogDebounce.delete(parentSessionId);
2398
- }
2569
+ agentAvailable(sessionId) {
2570
+ return this.summaries.find((summary) => summary.sessionId === sessionId)?.agentAvailable ?? (this.listPhase === "ready" ? false : void 0);
2571
+ }
2572
+ updateParentAvailability() {
2573
+ for (const [childId, address] of this.addresses) {
2574
+ const available = this.agentAvailable(address.parentSessionId);
2575
+ if (available !== void 0) this.sessions.get(childId)?.handleSubagentParentAvailable(available);
2399
2576
  }
2400
2577
  }
2401
2578
  /** Full refresh via session.list (single-flight within one Host generation). */
@@ -2413,22 +2590,23 @@ window.__ModuleLoader__.load({
2413
2590
  if (this.listMutations !== mutations) return;
2414
2591
  if (result.ok) {
2415
2592
  const baseline = this.listPhase === "pending" ? [...result.value.items] : mergeOrderedBaseline(established, result.value.items, (summary) => summary.sessionId);
2416
- this.summaries = mutations.reduce(applyMutation, baseline);
2593
+ const removedSincePull = /* @__PURE__ */ new Set();
2594
+ for (const mutation of mutations) if (mutation.kind === "remove") removedSincePull.add(mutation.sessionId);
2595
+ for (const s of baseline) if (s.running && !removedSincePull.has(s.sessionId)) this.engagedSessions.add(s.sessionId);
2596
+ const summaries = mutations.reduce(applyMutation, baseline);
2597
+ this.summaries = summaries;
2598
+ const retained = this.retainedIds(summaries);
2599
+ for (const sessionId of this.engagedSessions) this.pruneEngagement(sessionId, retained);
2417
2600
  this.listState = "idle";
2418
2601
  this.listPhase = "ready";
2602
+ this.updateParentAvailability();
2419
2603
  for (const s of this.summaries) {
2420
2604
  const session = this.sessions.get(s.sessionId);
2421
2605
  if (session === void 0) continue;
2422
- session.handleBlank(s.blank);
2606
+ session.handleBlank(this.effectiveBlank(s));
2423
2607
  session.handleRunning(s.running);
2424
2608
  }
2425
- for (const s of result.value.items) {
2426
- const block = s.projections;
2427
- if (block === void 0) continue;
2428
- const store = this.projectionStore(s.sessionId);
2429
- const values = block.values;
2430
- for (const key of Object.keys(values)) store.apply(key, values[key], sessionSeqCursor(block.asOfSeq));
2431
- }
2609
+ for (const s of result.value.items) if (s.projections !== void 0) this.applyListBlock(s.sessionId, s.projections);
2432
2610
  } else {
2433
2611
  this.listState = "error";
2434
2612
  this.listError = result.error;
@@ -2484,8 +2662,9 @@ window.__ModuleLoader__.load({
2484
2662
  };
2485
2663
  const result = await this.remote.session.create(payload);
2486
2664
  if (result.ok) this.recordMutation({
2487
- kind: "upsert",
2665
+ kind: "placeholder",
2488
2666
  summary: {
2667
+ agentAvailable: true,
2489
2668
  sessionId: result.value.sessionId,
2490
2669
  updatedAt: Date.now(),
2491
2670
  running: false,
@@ -2496,8 +2675,9 @@ window.__ModuleLoader__.load({
2496
2675
  else {
2497
2676
  const publishedSessionId = workspaceAttachSessionId(result.error);
2498
2677
  if (publishedSessionId !== void 0) this.recordMutation({
2499
- kind: "upsert",
2678
+ kind: "placeholder",
2500
2679
  summary: {
2680
+ agentAvailable: true,
2501
2681
  sessionId: publishedSessionId,
2502
2682
  updatedAt: Date.now(),
2503
2683
  running: false,
@@ -2509,11 +2689,12 @@ window.__ModuleLoader__.load({
2509
2689
  }
2510
2690
  /**
2511
2691
  * Contract session.fork; on success merge the child into summaries
2512
- * immediately (same synchronous-addressability guarantee as create). The
2513
- * child carries the source's history, so it is never blank; lineage rides
2514
- * parentSessionId so the list nests it under its source. A child published
2515
- * before Workspace attachment fails is also reconciled into the list.
2516
- * @param opts - source session and the optional seq anchoring the cut.
2692
+ * immediately (same synchronous-addressability guarantee as create).
2693
+ * Blankness starts provisionally true so the authoritative Host summary can
2694
+ * preserve it or lower it after an exact cut before the first `turn/start`;
2695
+ * lineage rides parentSessionId. A child published before Workspace
2696
+ * attachment fails is also reconciled into the list.
2697
+ * @param opts - source session and the optional exact inclusive boundary seq.
2517
2698
  * @returns the fork result (the child session id).
2518
2699
  */
2519
2700
  async fork(opts) {
@@ -2524,12 +2705,13 @@ window.__ModuleLoader__.load({
2524
2705
  });
2525
2706
  const childId = result.ok ? result.value.sessionId : workspaceAttachSessionId(result.error);
2526
2707
  if (childId !== void 0) this.recordMutation({
2527
- kind: "upsert",
2708
+ kind: "placeholder",
2528
2709
  summary: {
2710
+ agentAvailable: true,
2529
2711
  sessionId: childId,
2530
2712
  updatedAt: Date.now(),
2531
2713
  running: false,
2532
- blank: false,
2714
+ blank: true,
2533
2715
  parentSessionId: opts.sessionId,
2534
2716
  ...source?.cwd !== void 0 ? { cwd: source.cwd } : {}
2535
2717
  }
@@ -2537,19 +2719,33 @@ window.__ModuleLoader__.load({
2537
2719
  return result;
2538
2720
  }
2539
2721
  /**
2540
- * Insert-or-enrich a locally synthesized summary: a new id prepends; an
2541
- * existing entry only gains fields it lacks (the session-added frame and the
2542
- * create() echo race — whichever lands second must fill the placeholder's
2543
- * missing cwd/parentSessionId, never overwrite list-refresh data).
2722
+ * Rename a Session and update its title projection without opening its history.
2723
+ * @param sessionId - Session to rename.
2724
+ * @param title - raw title text for Host normalization.
2725
+ * @returns the accepted title and event position, or the Remote failure.
2726
+ */
2727
+ async rename(sessionId, title) {
2728
+ const result = await this.remote.session.rename({
2729
+ sessionId,
2730
+ title
2731
+ });
2732
+ if (result.ok) this.projectionStore(sessionId).apply("title", result.value.title, SessionSeq(result.value.seq));
2733
+ return result;
2734
+ }
2735
+ /**
2736
+ * Merge a Host summary, replacing live state and filling missing metadata.
2737
+ * Local create/fork placeholders only fill metadata on an existing row.
2544
2738
  */
2545
2739
  mergeSummary(summary) {
2546
2740
  this.recordMutation({
2547
2741
  kind: "upsert",
2548
2742
  summary
2549
2743
  });
2744
+ this.updateParentAvailability();
2550
2745
  }
2551
2746
  /** Apply immediately and retain for replay when a list response is in flight. */
2552
2747
  recordMutation(mutation) {
2748
+ if (this.disposed) return;
2553
2749
  this.listMutations?.push(mutation);
2554
2750
  this.summaries = applyMutation(this.summaries, mutation);
2555
2751
  this.notifier.markDirty();
@@ -2587,18 +2783,10 @@ window.__ModuleLoader__.load({
2587
2783
  this.replaceControlBaseline(frame.value);
2588
2784
  return;
2589
2785
  }
2590
- if (frame.type === "projection") {
2591
- this.projectionStore(frame.sessionId).apply(frame.key, frame.value, SessionSeq(frame.seq));
2592
- this.notifier.markDirty();
2593
- return;
2594
- }
2595
- if (frame.jobs.length === 0) this.jobsBySession.delete(frame.sessionId);
2596
- else this.jobsBySession.set(frame.sessionId, frame.jobs);
2786
+ this.projectionStore(frame.sessionId).apply(frame.key, frame.value, SessionSeq(frame.seq));
2597
2787
  this.notifier.markDirty();
2598
2788
  }
2599
2789
  replaceControlBaseline(baseline) {
2600
- this.jobsBySession.clear();
2601
- for (const [sessionId, jobs] of Object.entries(baseline.jobs)) if (jobs.length > 0) this.jobsBySession.set(sessionId, jobs);
2602
2790
  for (const [sessionId, block] of Object.entries(baseline.projections)) {
2603
2791
  const store = this.projectionStore(sessionId);
2604
2792
  const asOfSeq = sessionSeqCursor(block.asOfSeq);
@@ -2615,44 +2803,55 @@ window.__ModuleLoader__.load({
2615
2803
  */
2616
2804
  handleSessionAdded(summary) {
2617
2805
  this.mergeSummary(summary);
2618
- this.sessions.get(summary.sessionId)?.handleBlank(summary.blank);
2619
- const projections = summary.projections;
2620
- if (projections !== void 0) {
2621
- const store = this.projectionStore(summary.sessionId);
2622
- for (const [key, value] of Object.entries(projections.values)) store.apply(key, value, sessionSeqCursor(projections.asOfSeq));
2806
+ if (!this.disposed && summary.running) this.engagedSessions.add(summary.sessionId);
2807
+ this.sessions.get(summary.sessionId)?.handleBlank(this.effectiveBlank(summary));
2808
+ if (summary.projections !== void 0) this.applyListBlock(summary.sessionId, summary.projections);
2809
+ }
2810
+ /**
2811
+ * Merge one list-surface projection block by the sequence space it declares.
2812
+ * A `sequenced` block came from the Host's live registry for an attached
2813
+ * Session, so each key lands under higher-seq-wins against that Session's
2814
+ * baselines and frames. A `cached` block was viewed from the persisted
2815
+ * checkpoint by a header-only listing: its watermark is not comparable with
2816
+ * this connection's seqs, so it only fills keys no sequenced row holds.
2817
+ */
2818
+ applyListBlock(sessionId, block) {
2819
+ const store = this.projectionStore(sessionId);
2820
+ switch (block.kind) {
2821
+ case "sequenced": {
2822
+ const seq = sessionSeqCursor(block.asOfSeq);
2823
+ for (const [key, value] of Object.entries(block.values)) store.apply(key, value, seq);
2824
+ return;
2825
+ }
2826
+ case "cached":
2827
+ store.applyCached(block.values);
2828
+ return;
2829
+ default: assertNever(block.kind, "session list projection block kind");
2623
2830
  }
2624
- if (summary.origin === "subagent" && summary.parentSessionId !== void 0) this.markCatalogParentExpandable(summary.parentSessionId);
2625
- if (summary.parentSessionId !== void 0 && this.openCatalogs.has(summary.parentSessionId)) this.scheduleCatalogRefresh(summary.parentSessionId);
2626
2831
  }
2627
2832
  /**
2628
2833
  * Apply one Session removal forwarded through `ctx.remote.$on`.
2629
2834
  * @param sessionId - removed Session identity.
2630
2835
  */
2631
2836
  handleSessionRemoved(sessionId) {
2632
- const durableSubagent = this.summaries.find((candidate) => candidate.sessionId === sessionId)?.origin === "subagent" || this.addresses.has(sessionId);
2837
+ const durableSubagent = this.subagentAddress(sessionId) !== void 0 || this.summaries.some((summary) => summary.sessionId === sessionId && summary.origin === "subagent");
2633
2838
  this.recordMutation(durableSubagent ? {
2634
2839
  kind: "status",
2635
2840
  sessionId,
2636
- running: false
2841
+ running: false,
2842
+ agentAvailable: false
2637
2843
  } : {
2638
2844
  kind: "remove",
2639
2845
  sessionId
2640
2846
  });
2641
- this.updateCatalogActivity(sessionId, false);
2642
2847
  if (durableSubagent) this.sessions.get(sessionId)?.handleRunning(false);
2643
2848
  else this.sessions.get(sessionId)?.handleRemoved();
2644
- this.jobsBySession.delete(sessionId);
2645
- if (!durableSubagent) this.projectionStores.delete(sessionId);
2646
- const inflightCatalog = this.catalogInflight.get(sessionId);
2647
- if (inflightCatalog !== void 0) {
2648
- inflightCatalog.parentAvailableOverride = false;
2649
- this.catalogStale.add(sessionId);
2650
- }
2651
- const ownedCatalog = this.catalogs.get(sessionId);
2652
- if (ownedCatalog !== void 0 && ownedCatalog.parentAvailable) this.catalogs.set(sessionId, {
2653
- ...ownedCatalog,
2654
- parentAvailable: false
2655
- });
2849
+ const catalog = this.projectionStores.get(sessionId)?.values().subagentCatalog;
2850
+ if (!durableSubagent && (catalog === void 0 || catalog.length === 0)) this.projectionStores.delete(sessionId);
2851
+ this.pruneEngagement(sessionId, this.retainedIds(this.summaries));
2852
+ this.projectionInflight.get(sessionId)?.controller.abort();
2853
+ this.projectionInflight.delete(sessionId);
2854
+ this.projectionLoads.delete(sessionId);
2656
2855
  for (const [childId, address] of this.addresses) if (address.parentSessionId === sessionId) this.sessions.get(childId)?.handleSubagentParentAvailable(false);
2657
2856
  }
2658
2857
  /**
@@ -2661,13 +2860,15 @@ window.__ModuleLoader__.load({
2661
2860
  * @param running - current Agent running state.
2662
2861
  */
2663
2862
  handleSessionStatus(sessionId, running) {
2863
+ if (!this.disposed && running) this.engagedSessions.add(sessionId);
2664
2864
  this.recordMutation({
2665
2865
  kind: "status",
2666
2866
  sessionId,
2667
- running
2867
+ running,
2868
+ agentAvailable: true
2668
2869
  });
2870
+ this.updateParentAvailability();
2669
2871
  this.sessions.get(sessionId)?.handleRunning(running);
2670
- this.updateCatalogActivity(sessionId, running);
2671
2872
  }
2672
2873
  /**
2673
2874
  * Advance Session-list activity from one user-authored durable message.
@@ -2700,93 +2901,26 @@ window.__ModuleLoader__.load({
2700
2901
  this.listMutations = null;
2701
2902
  this.listInflight = null;
2702
2903
  this.refreshList();
2703
- const parents = new Set(this.openCatalogs);
2904
+ const parents = new Set(this.projectionLoads.keys());
2704
2905
  for (const id of this.sessions.keys()) {
2705
2906
  const address = this.addresses.get(id);
2706
2907
  if (address !== void 0) parents.add(address.parentSessionId);
2707
2908
  }
2708
- for (const parentSessionId of parents) this.refreshSubagents(parentSessionId);
2709
- }
2710
- /** Debounce membership refetches for an explicitly consumed catalog. */
2711
- scheduleCatalogRefresh(parentSessionId) {
2712
- if (this.catalogDebounce.has(parentSessionId)) return;
2713
- const timer = setTimeout(() => {
2714
- this.catalogDebounce.delete(parentSessionId);
2715
- if (this.catalogInflight.has(parentSessionId)) {
2716
- this.catalogStale.add(parentSessionId);
2717
- return;
2718
- }
2719
- this.refreshSubagents(parentSessionId);
2720
- }, 50);
2721
- this.catalogDebounce.set(parentSessionId, timer);
2722
- }
2723
- /** Apply one Agent-driver transition to loaded and in-flight catalogs. */
2724
- updateCatalogActivity(childSessionId, running) {
2725
- const activity = running ? "running" : "inactive";
2726
- for (const inflight of this.catalogInflight.values()) inflight.activityRows.set(childSessionId, activity);
2727
- let changed = false;
2728
- for (const [parentSessionId, catalog] of this.catalogs) {
2729
- if (!catalog.entries.some((entry) => entry.kind === "child" && entry.id === childSessionId && entry.activity !== activity)) continue;
2730
- const entries = catalog.entries.map((entry) => {
2731
- if (entry.kind !== "child" || entry.id !== childSessionId) return entry;
2732
- return {
2733
- ...entry,
2734
- activity
2735
- };
2736
- });
2737
- changed = true;
2738
- this.catalogs.set(parentSessionId, {
2739
- ...catalog,
2740
- entries
2741
- });
2742
- }
2743
- if (changed) this.notifier.markDirty();
2744
- }
2745
- /** Preserve and project a positive expandability hint after one direct subagent publishes. */
2746
- markCatalogParentExpandable(parentSessionId) {
2747
- this.applyCatalogParentExpandable(parentSessionId);
2748
- for (const inflight of this.catalogInflight.values()) inflight.expandableRows.add(parentSessionId);
2749
- }
2750
- /** Apply one positive expandability hint to every loaded catalog containing that unique row id. */
2751
- applyCatalogParentExpandable(parentSessionId) {
2752
- let changed = false;
2753
- for (const [catalogParentId, catalog] of this.catalogs) {
2754
- if (!catalog.entries.some((entry) => entry.kind === "child" && entry.id === parentSessionId && !entry.hasChildren)) continue;
2755
- const entries = catalog.entries.map((entry) => {
2756
- if (entry.kind !== "child" || entry.id !== parentSessionId || entry.hasChildren) return entry;
2757
- return {
2758
- ...entry,
2759
- hasChildren: true
2760
- };
2761
- });
2762
- changed = true;
2763
- this.catalogs.set(catalogParentId, {
2764
- ...catalog,
2765
- entries
2766
- });
2767
- }
2768
- if (changed) this.notifier.markDirty();
2769
- }
2770
- /** Fold request-local row mutations into one catalog result before publication. */
2771
- withCatalogMutations(entries, expandableRows, activityRows) {
2772
- return entries.map((entry) => {
2773
- if (entry.kind !== "child") return entry;
2774
- const activity = activityRows.get(entry.id);
2775
- if (!expandableRows.has(entry.id) && activity === void 0) return entry;
2776
- return {
2777
- ...entry,
2778
- ...expandableRows.has(entry.id) ? { hasChildren: true } : {},
2779
- ...activity === void 0 ? {} : { activity }
2780
- };
2781
- });
2909
+ for (const { controller } of this.projectionInflight.values()) controller.abort();
2910
+ this.projectionInflight.clear();
2911
+ this.projectionLoads.clear();
2912
+ for (const parentSessionId of parents) this.refreshProjections(parentSessionId);
2782
2913
  }
2783
2914
  buildListSnapshot() {
2784
2915
  const items = flattenLineage(this.summaries.map((summary) => {
2785
2916
  const projectionStore = this.projectionStores.get(summary.sessionId);
2786
2917
  const title = projectionStore?.get("title");
2787
2918
  const projectionValues = projectionStore?.values();
2919
+ const metadata = projectionValues?.sessionListMetadata;
2788
2920
  return {
2789
2921
  ...summary,
2922
+ blank: this.effectiveBlank(summary) && metadata?.blank !== false,
2923
+ updatedAt: Math.max(summary.updatedAt, metadata?.lastPromptAt ?? 0),
2790
2924
  ...typeof title === "string" && title !== "" ? { title } : {},
2791
2925
  ...projectionValues === void 0 ? {} : { projectionValues }
2792
2926
  };
@@ -2804,31 +2938,41 @@ window.__ModuleLoader__.load({
2804
2938
  state: this.listState,
2805
2939
  phase: this.listPhase,
2806
2940
  error: this.listError,
2807
- subagentsByParent: Object.fromEntries(this.catalogs),
2808
- jobsBySession: Object.fromEntries(this.jobsBySession)
2941
+ projectionsBySession: Object.fromEntries([...this.projectionStores].map(([sessionId, store]) => [sessionId, {
2942
+ values: store.values(),
2943
+ state: "idle",
2944
+ error: null,
2945
+ ...this.projectionLoads.get(sessionId)
2946
+ }]))
2809
2947
  };
2810
2948
  }
2811
2949
  };
2812
2950
  /** Apply one list mutation without deriving display order. */
2813
2951
  function applyMutation(summaries, mutation) {
2814
2952
  switch (mutation.kind) {
2815
- case "upsert": {
2953
+ case "upsert":
2954
+ case "placeholder": {
2816
2955
  const existing = summaries.find((summary) => summary.sessionId === mutation.summary.sessionId);
2817
2956
  if (existing === void 0) return [mutation.summary, ...summaries];
2818
2957
  const filled = {
2819
2958
  ...existing,
2820
2959
  blank: existing.blank && mutation.summary.blank,
2960
+ ...mutation.kind === "upsert" ? {
2961
+ agentAvailable: mutation.summary.agentAvailable,
2962
+ running: mutation.summary.running
2963
+ } : {},
2821
2964
  ...existing.cwd === void 0 && mutation.summary.cwd !== void 0 ? { cwd: mutation.summary.cwd } : {},
2822
2965
  ...existing.parentSessionId === void 0 && mutation.summary.parentSessionId !== void 0 ? { parentSessionId: mutation.summary.parentSessionId } : {},
2823
2966
  ...existing.origin === void 0 && mutation.summary.origin !== void 0 ? { origin: mutation.summary.origin } : {}
2824
2967
  };
2825
- if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId && filled.origin === existing.origin && filled.blank === existing.blank) return [...summaries];
2968
+ 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];
2826
2969
  return summaries.map((summary) => summary.sessionId === mutation.summary.sessionId ? filled : summary);
2827
2970
  }
2828
2971
  case "remove": return summaries.filter((summary) => summary.sessionId !== mutation.sessionId);
2829
- case "status": return summaries.map((summary) => summary.sessionId === mutation.sessionId && (summary.running !== mutation.running || mutation.running && summary.blank) ? {
2972
+ case "status": return summaries.map((summary) => summary.sessionId === mutation.sessionId && (summary.running !== mutation.running || summary.agentAvailable !== mutation.agentAvailable || mutation.running && summary.blank) ? {
2830
2973
  ...summary,
2831
2974
  running: mutation.running,
2975
+ agentAvailable: mutation.agentAvailable,
2832
2976
  blank: summary.blank && !mutation.running
2833
2977
  } : summary);
2834
2978
  case "activity": return summaries.map((summary) => summary.sessionId === mutation.sessionId && mutation.updatedAt > summary.updatedAt ? {
@@ -2999,8 +3143,7 @@ window.__ModuleLoader__.load({
2999
3143
  ids: [],
3000
3144
  byId: {},
3001
3145
  phase: "pending",
3002
- subagentsByParent: {},
3003
- jobsBySession: {}
3146
+ projectionsBySession: {}
3004
3147
  });
3005
3148
  const disposeManagerProjection = this.manager.subscribe(() => {
3006
3149
  this.projectList();
@@ -3072,25 +3215,17 @@ window.__ModuleLoader__.load({
3072
3215
  * Resolve an already discovered direct-parent address without opening it.
3073
3216
  * Feature plugins use this to avoid Agent-bound RPCs in persisted child views.
3074
3217
  * @param id - possible addressed child id.
3075
- * @returns The retained address, when present.
3218
+ * @returns A retained or loaded-catalog address, without retaining a new selection or scope.
3076
3219
  */
3077
3220
  subagentAddress(id) {
3078
3221
  return this.manager.subagentAddress(id);
3079
3222
  }
3080
3223
  /**
3081
- * Inform the Session Controller whether a catalog menu is consuming membership updates.
3082
- * @param parentSessionId - selected parent.
3083
- * @param open - menu state.
3224
+ * Load all Session projections once per connection; retry an unsuccessful initial read.
3225
+ * @param sessionId - Session to inspect without opening its conversation.
3084
3226
  */
3085
- setSubagentCatalogOpen(parentSessionId, open) {
3086
- this.manager.setSubagentCatalogOpen(parentSessionId, open);
3087
- }
3088
- /**
3089
- * Refresh one direct-child catalog.
3090
- * @param parentSessionId - catalog owner.
3091
- */
3092
- refreshSubagents(parentSessionId) {
3093
- return this.manager.refreshSubagents(parentSessionId);
3227
+ refreshProjections(sessionId) {
3228
+ return this.manager.refreshProjections(sessionId);
3094
3229
  }
3095
3230
  /**
3096
3231
  * Refresh the real Session baseline, reusing an in-flight pull.
@@ -3169,15 +3304,14 @@ window.__ModuleLoader__.load({
3169
3304
  return result.value.sessionId;
3170
3305
  }
3171
3306
  /**
3172
- * Fork a Session from a completed-turn prefix of the source and publish
3173
- * the child in the catalog before resolving.
3174
- * @param opts - source session id, the optional event seq anchoring the
3175
- * cut (the boundary is the first turn/end at or after it; an in-log
3176
- * anchor in an open turn is unavailable rather than clipped backward),
3177
- * and whether to increment an inherited durable title before resolving.
3178
- * A fractional anchor floors to a real event seq: the frozen nodes of an
3179
- * interrupted turn carry flow-ordering seqs between two events, and the
3180
- * wire takes integers only.
3307
+ * Fork a session from an exact inclusive prefix of the source (same
3308
+ * synchronous-addressability guarantee as {@link ClientSessions.create}:
3309
+ * on resolution the child is catalogued and may be explicitly retained).
3310
+ * @param opts - source session id, the optional exact inclusive boundary
3311
+ * seq (a real event seq the caller already knows; a cut inside an open
3312
+ * turn is balanced Host-side with synthetic closers, and omission selects
3313
+ * the latest completed-turn prefix), and whether to increment an
3314
+ * inherited durable title before resolving.
3181
3315
  * @returns the child session id.
3182
3316
  * @throws {SessionForkError} with the source id.
3183
3317
  * @throws {Error} when a requested child-title rename fails after creation.
@@ -3186,20 +3320,14 @@ window.__ModuleLoader__.load({
3186
3320
  const sourceTitle = opts.increaseTitle ? this.list.getSnapshot().byId[opts.sessionId]?.title : void 0;
3187
3321
  const result = await this.manager.fork({
3188
3322
  sessionId: opts.sessionId,
3189
- ...opts.atSeq === void 0 ? {} : { atSeq: SessionSeq(Math.floor(opts.atSeq)) }
3323
+ ...opts.atSeq === void 0 ? {} : { atSeq: SessionSeq(opts.atSeq) }
3190
3324
  });
3191
3325
  if (!result.ok) throw new SessionForkError(result.error, opts.sessionId);
3192
3326
  this.projectList();
3193
3327
  const childId = result.value.sessionId;
3194
3328
  if (sourceTitle !== void 0) {
3195
- const reference = this.retain(childId, { source: "controllerOperation" });
3196
- try {
3197
- await reference.ready;
3198
- const renamed = await reference.binding.session.rename(increasedForkTitle(sourceTitle));
3199
- if (!renamed.ok) throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`);
3200
- } finally {
3201
- reference.release();
3202
- }
3329
+ const renamed = await this.manager.rename(childId, increasedForkTitle(sourceTitle));
3330
+ if (!renamed.ok) throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`);
3203
3331
  }
3204
3332
  return childId;
3205
3333
  }
@@ -3279,7 +3407,7 @@ window.__ModuleLoader__.load({
3279
3407
  if (count === 0) this.retireScope(id, record);
3280
3408
  else this.publishRetention(id);
3281
3409
  });
3282
- if (this.list.getSnapshot().byId[id] === void 0) this.projectList();
3410
+ if (this.list.getSnapshot().byId[id] === void 0 && this.manager.subagentAddress(id) !== void 0) this.projectList();
3283
3411
  this.publishRetention(id);
3284
3412
  return reference;
3285
3413
  }
@@ -3343,7 +3471,7 @@ window.__ModuleLoader__.load({
3343
3471
  /** Project the manager's list snapshot into the store (title derivation is display-only). */
3344
3472
  projectList() {
3345
3473
  const previousById = this.list.getSnapshot().byId;
3346
- const { items, phase, subagentsByParent, jobsBySession } = this.manager.getListSnapshot();
3474
+ const { items, phase, projectionsBySession } = this.manager.getListSnapshot();
3347
3475
  const ids = [];
3348
3476
  const byId = {};
3349
3477
  for (const entry of items) {
@@ -3362,8 +3490,7 @@ window.__ModuleLoader__.load({
3362
3490
  ...entry.origin !== void 0 ? { origin: entry.origin } : {}
3363
3491
  };
3364
3492
  }
3365
- for (const [parentId, catalog] of Object.entries(subagentsByParent)) for (const child of catalog.entries) {
3366
- if (child.kind !== "child") continue;
3493
+ for (const [parentId, projection] of Object.entries(projectionsBySession)) for (const child of projection.values.subagentCatalog ?? []) {
3367
3494
  const childId = child.id;
3368
3495
  const summary = byId[childId];
3369
3496
  const projectionValues = summary?.projectionValues ?? this.manager.projectionValues(childId);
@@ -3375,7 +3502,7 @@ window.__ModuleLoader__.load({
3375
3502
  displayTitle,
3376
3503
  parentId,
3377
3504
  origin: "subagent",
3378
- running: child.activity === "running",
3505
+ running: this.scopes.get(childId)?.session.getSnapshot().running ?? false,
3379
3506
  blank: false,
3380
3507
  updatedAt: 0,
3381
3508
  retainedBy: this.retentionSnapshot(childId).retainedBy,
@@ -3390,9 +3517,13 @@ window.__ModuleLoader__.load({
3390
3517
  }
3391
3518
  for (const [id, record] of this.scopes) {
3392
3519
  if (byId[id] !== void 0) continue;
3520
+ const address = this.manager.subagentAddress(id);
3521
+ if (address === void 0) continue;
3393
3522
  const previous = previousById[id];
3394
3523
  const snapshot = record.session.getSnapshot();
3395
- const address = this.manager.subagentAddress(id);
3524
+ const projectionValues = this.manager.projectionValues(id);
3525
+ const projectedTitle = projectionValues?.title;
3526
+ const title = typeof projectedTitle === "string" && projectedTitle !== "" ? projectedTitle : previous?.title;
3396
3527
  byId[id] = {
3397
3528
  ...previous ?? {
3398
3529
  id,
@@ -3402,9 +3533,12 @@ window.__ModuleLoader__.load({
3402
3533
  running: snapshot.running,
3403
3534
  retainedBy: record.retention.retainedBy,
3404
3535
  blank: snapshot.blank,
3405
- ...address === void 0 ? {} : {
3406
- parentId: address.parentSessionId,
3407
- origin: "subagent"
3536
+ parentId: address.parentSessionId,
3537
+ origin: "subagent",
3538
+ ...projectionValues === void 0 ? {} : { projectionValues },
3539
+ ...title === void 0 ? {} : {
3540
+ title,
3541
+ displayTitle: title
3408
3542
  }
3409
3543
  };
3410
3544
  }
@@ -3412,8 +3546,7 @@ window.__ModuleLoader__.load({
3412
3546
  ids,
3413
3547
  byId,
3414
3548
  phase,
3415
- subagentsByParent,
3416
- jobsBySession
3549
+ projectionsBySession
3417
3550
  });
3418
3551
  }
3419
3552
  startScopeDrop(id, record, disposeFiber = true, sessionDisposal = this.manager.drop(id, record.session)) {