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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +31 -8
  3. package/README.zh.md +31 -8
  4. package/lib/client.js +603 -515
  5. package/lib/index.js +299 -137
  6. package/lib/typert.host.js +373 -183
  7. package/lib/typert.remote-client.d.ts +10 -5
  8. package/lib/typert.remote-client.js +216 -124
  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 +2 -0
  12. package/lib/types/client/contract/sessions.d.ts +11 -16
  13. package/lib/types/client/index.d.ts +1 -1
  14. package/lib/types/client/sessions/lineage.d.ts +4 -1
  15. package/lib/types/client/sessions/lineage.js +2 -1
  16. package/lib/types/client/sessions/manager.d.ts +66 -59
  17. package/lib/types/client/sessions/manager.js +249 -277
  18. package/lib/types/client/sessions/projection-store.d.ts +32 -13
  19. package/lib/types/client/sessions/projection-store.js +45 -13
  20. package/lib/types/client/sessions/remotes.d.ts +1 -2
  21. package/lib/types/client/sessions/service.d.ts +23 -35
  22. package/lib/types/client/sessions/service.js +35 -47
  23. package/lib/types/client/sessions/session.d.ts +18 -16
  24. package/lib/types/client/sessions/session.js +112 -50
  25. package/lib/types/commands.d.ts +4 -2
  26. package/lib/types/commands.js +72 -36
  27. package/lib/types/control.d.ts +2 -4
  28. package/lib/types/control.js +2 -44
  29. package/lib/types/history.js +1 -1
  30. package/lib/types/index.d.ts +32 -5
  31. package/lib/types/index.js +117 -13
  32. package/lib/types/list.js +24 -15
  33. package/lib/types/skill-catalog.js +60 -50
  34. package/lib/types/types.d.ts +37 -23
  35. package/lib/types/types.js +0 -1
  36. 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. */
@@ -558,6 +576,210 @@ window.__ModuleLoader__.load({
558
576
  return merged;
559
577
  }
560
578
  //#endregion
579
+ //#region ../../util/values/lib/index.js
580
+ /** Duplicate-install-safe JSON and immutable-value helpers. @module @deepseek-ai/dsh-util-values */
581
+ /**
582
+ * Mark an unreachable closed-union branch.
583
+ * @param value - impossible value; an unhandled typed variant fails at the call site.
584
+ * @param context - optional switch-site label included in the failure message.
585
+ * @returns never; a runtime value that escaped its type always throws.
586
+ */
587
+ function assertNever(value, context) {
588
+ const rendered = JSON.stringify(value) ?? String(value);
589
+ throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
590
+ }
591
+ /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
592
+ function hasIntrinsicConstructor(prototype, name) {
593
+ const constructor = Object.getOwnPropertyDescriptor(prototype, "constructor")?.value;
594
+ if (typeof constructor !== "function") return false;
595
+ try {
596
+ return constructor.name === name && constructor.prototype === prototype && Function.prototype.toString.call(constructor) === `function ${name}() { [native code] }`;
597
+ } catch {
598
+ return false;
599
+ }
600
+ }
601
+ /** Whether a candidate is one realm's intrinsic `Object.prototype`. */
602
+ function isIntrinsicObjectPrototype(value) {
603
+ return Object.getPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
604
+ }
605
+ /** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
606
+ function hasPlainArrayPrototype(value) {
607
+ const prototype = Object.getPrototypeOf(value);
608
+ if (!Array.isArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
609
+ const objectPrototype = Object.getPrototypeOf(prototype);
610
+ return typeof objectPrototype === "object" && objectPrototype !== null && isIntrinsicObjectPrototype(objectPrototype);
611
+ }
612
+ /** Whether an object is a plain or null-prototype record from any JavaScript realm. */
613
+ function hasPlainObjectPrototype(value) {
614
+ const prototype = Object.getPrototypeOf(value);
615
+ return prototype === null || typeof prototype === "object" && isIntrinsicObjectPrototype(prototype);
616
+ }
617
+ /** Return every JSON-visible object key, or reject own data JSON would discard. */
618
+ function enumerableStringKeys(value) {
619
+ const keys = Reflect.ownKeys(value);
620
+ if (keys.some((key) => typeof key !== "string" || !Object.prototype.propertyIsEnumerable.call(value, key))) return void 0;
621
+ return keys;
622
+ }
623
+ /** Validate lossless JSON iteratively, optionally materializing a detached snapshot. */
624
+ function walkJsonValue(value, detach) {
625
+ const ancestors = /* @__PURE__ */ new Set();
626
+ let root;
627
+ const assign = (destination, item) => {
628
+ if (destination === void 0) return;
629
+ if (destination.kind === "root") root = item;
630
+ else if (destination.kind === "array") destination.target[destination.index] = item;
631
+ else Object.defineProperty(destination.target, destination.key, {
632
+ value: item,
633
+ enumerable: true,
634
+ configurable: true,
635
+ writable: true
636
+ });
637
+ };
638
+ const tasks = [{
639
+ kind: "visit",
640
+ value,
641
+ ...detach ? { destination: { kind: "root" } } : {}
642
+ }];
643
+ for (let task = tasks.pop(); task !== void 0; task = tasks.pop()) {
644
+ if (task.kind === "leave") {
645
+ ancestors.delete(task.source);
646
+ continue;
647
+ }
648
+ if (task.kind === "array-item") {
649
+ if (!Object.prototype.hasOwnProperty.call(task.source, task.index)) return void 0;
650
+ tasks.push({
651
+ kind: "visit",
652
+ value: task.source[task.index],
653
+ ...task.target === void 0 ? {} : { destination: {
654
+ kind: "array",
655
+ target: task.target,
656
+ index: task.index
657
+ } }
658
+ });
659
+ continue;
660
+ }
661
+ if (task.kind === "object-property") {
662
+ tasks.push({
663
+ kind: "visit",
664
+ value: task.source[task.key],
665
+ ...task.target === void 0 ? {} : { destination: {
666
+ kind: "object",
667
+ target: task.target,
668
+ key: task.key
669
+ } }
670
+ });
671
+ continue;
672
+ }
673
+ const current = task.value;
674
+ if (current === null) {
675
+ assign(task.destination, null);
676
+ continue;
677
+ }
678
+ if (typeof current === "boolean" || typeof current === "string") {
679
+ assign(task.destination, current);
680
+ continue;
681
+ }
682
+ if (typeof current === "number") {
683
+ if (!Number.isFinite(current) || Object.is(current, -0)) return void 0;
684
+ assign(task.destination, current);
685
+ continue;
686
+ }
687
+ if (typeof current !== "object") return void 0;
688
+ if (ancestors.has(current)) return void 0;
689
+ if (Array.isArray(current)) {
690
+ if (!hasPlainArrayPrototype(current)) return void 0;
691
+ const length = current.length;
692
+ if (Reflect.ownKeys(current).length !== length + 1) return void 0;
693
+ const target = detach ? [] : void 0;
694
+ if (target !== void 0) assign(task.destination, target);
695
+ ancestors.add(current);
696
+ tasks.push({
697
+ kind: "leave",
698
+ source: current
699
+ });
700
+ for (let index = length - 1; index >= 0; index--) tasks.push({
701
+ kind: "array-item",
702
+ source: current,
703
+ index,
704
+ ...target === void 0 ? {} : { target }
705
+ });
706
+ continue;
707
+ }
708
+ if (!hasPlainObjectPrototype(current)) return void 0;
709
+ const keys = enumerableStringKeys(current);
710
+ if (keys === void 0) return void 0;
711
+ const target = detach ? {} : void 0;
712
+ if (target !== void 0) assign(task.destination, target);
713
+ ancestors.add(current);
714
+ tasks.push({
715
+ kind: "leave",
716
+ source: current
717
+ });
718
+ for (let index = keys.length - 1; index >= 0; index--) {
719
+ const key = keys[index];
720
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
721
+ if (key === void 0) return void 0;
722
+ tasks.push({
723
+ kind: "object-property",
724
+ source: current,
725
+ key,
726
+ ...target === void 0 ? {} : { target }
727
+ });
728
+ }
729
+ }
730
+ return detach ? root : true;
731
+ }
732
+ /**
733
+ * Validate and detach lossless JSON in one read per property.
734
+ * @param value - candidate value to validate and detach.
735
+ * @returns the detached snapshot, or `undefined` when the value is not losslessly JSON-serializable.
736
+ */
737
+ function snapshotJsonValue(value) {
738
+ return walkJsonValue(value, true);
739
+ }
740
+ /**
741
+ * Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
742
+ * @param value - value to freeze.
743
+ * @returns the same value after every reachable enumerable child is frozen.
744
+ */
745
+ function deepFreeze(value) {
746
+ const seen = /* @__PURE__ */ new WeakSet();
747
+ const pending = [{
748
+ kind: "visit",
749
+ node: value
750
+ }];
751
+ while (pending.length > 0) {
752
+ const task = pending.pop();
753
+ /* v8 ignore next -- the loop condition guarantees one pending task. */
754
+ if (task === void 0) continue;
755
+ if (task.kind === "property") {
756
+ pending.push({
757
+ kind: "visit",
758
+ node: task.source[task.key]
759
+ });
760
+ continue;
761
+ }
762
+ const node = task.node;
763
+ if (node === null || typeof node !== "object") continue;
764
+ if (node instanceof AbortSignal) continue;
765
+ if (seen.has(node)) continue;
766
+ seen.add(node);
767
+ Object.freeze(node);
768
+ const keys = Object.keys(node);
769
+ for (let index = keys.length - 1; index >= 0; index--) {
770
+ const key = keys[index];
771
+ /* v8 ignore next -- the loop is bounded by the captured key count. */
772
+ if (key === void 0) continue;
773
+ pending.push({
774
+ kind: "property",
775
+ source: node,
776
+ key
777
+ });
778
+ }
779
+ }
780
+ return value;
781
+ }
782
+ //#endregion
561
783
  //#region lib/types/client/sessions/lineage.js
562
784
  /**
563
785
  * Summaries -> flat list with lineage indentation. Root and sibling order
@@ -584,8 +806,9 @@ window.__ModuleLoader__.load({
584
806
  return;
585
807
  }
586
808
  visited.add(s.sessionId);
809
+ const { agentAvailable: _agentAvailable, ...row } = s;
587
810
  out.push({
588
- ...s,
811
+ ...row,
589
812
  depth
590
813
  });
591
814
  const kids = children.get(s.sessionId);
@@ -688,10 +911,14 @@ window.__ModuleLoader__.load({
688
911
  //#region lib/types/client/sessions/projection-store.js
689
912
  /**
690
913
  * 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
914
+ * key. Sequenced writes (a baseline seeds rows at its cut, a push frame
915
+ * updates one row) compare seqs among themselves: a lower-or-equal seq within
916
+ * the Host generation loses, so a replayed frame cannot regress a value and a
917
+ * stale baseline cannot overwrite a newer frame. Cached writes (the session
918
+ * list's zero-I/O block) only fill keys no sequenced row holds, and a baseline
919
+ * discards every cached row before it seeds, regardless of seq: the connected
920
+ * Session is the truth and a cached value never outranks it. A key the store
921
+ * has never seen reads `undefined` (capability absent). Faces are identity-stable
695
922
  * per key (create-on-demand, cached) so the React side binds each exactly
696
923
  * once; the store-level channel (`subscribeAny`) serves coarse consumers (the
697
924
  * manager's list projection reads the `title` key).
@@ -746,27 +973,53 @@ window.__ModuleLoader__.load({
746
973
  */
747
974
  apply(key, value, seq) {
748
975
  const row = this.rows.get(key);
749
- if (row !== void 0 && seq <= row.seq) return;
976
+ if (row?.kind === "sequenced" && seq <= row.seq) return;
750
977
  this.rows.set(key, {
978
+ kind: "sequenced",
751
979
  value,
752
980
  seq
753
981
  });
754
982
  this.changed(key);
755
983
  }
756
984
  /**
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).
985
+ * Fill keys from a session-list block the Host labeled `cached`: a zero-I/O
986
+ * view of the persisted checkpoint. A cached value lands only where no
987
+ * sequenced row exists: a connected Session has already answered for such
988
+ * a key, and the list's view of the persisted checkpoint cannot be newer
989
+ * than it.
990
+ * @param values - whole values by key viewed from the persisted checkpoint.
991
+ */
992
+ applyCached(values) {
993
+ for (const key of Object.keys(values)) {
994
+ if (this.rows.get(key)?.kind === "sequenced") continue;
995
+ this.rows.set(key, {
996
+ kind: "cached",
997
+ value: values[key]
998
+ });
999
+ this.changed(key);
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Seed from a history tail page's projections block. Every cached row is
1004
+ * discarded first, regardless of seq: the block comes from the connected
1005
+ * Session, and a value viewed from the persisted checkpoint never outranks
1006
+ * it. Then every carried key lands under the same seq rule as frames, and a
1007
+ * key the block omits is capability-absent as of the cut — its row clears
1008
+ * unless a newer frame already superseded the cut (a stale baseline can
1009
+ * neither overwrite nor clear newer sequenced values).
762
1010
  * @param baseline - the response's projections block.
763
1011
  */
764
1012
  seed(baseline) {
1013
+ for (const [key, row] of this.rows) {
1014
+ if (row.kind !== "cached") continue;
1015
+ this.rows.delete(key);
1016
+ this.changed(key);
1017
+ }
765
1018
  const values = baseline.values;
766
1019
  for (const key of Object.keys(values)) this.apply(key, values[key], baseline.asOfSeq);
767
1020
  for (const [key, row] of this.rows) {
768
1021
  if (Object.hasOwn(values, key)) continue;
769
- if (row.seq > baseline.asOfSeq) continue;
1022
+ if (row.kind === "sequenced" && row.seq > baseline.asOfSeq) continue;
770
1023
  this.rows.delete(key);
771
1024
  this.changed(key);
772
1025
  }
@@ -957,199 +1210,6 @@ window.__ModuleLoader__.load({
957
1210
  return timeZone;
958
1211
  }
959
1212
  //#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
- }];
1121
- 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
- });
1130
- continue;
1131
- }
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
- });
1148
- }
1149
- }
1150
- return value;
1151
- }
1152
- //#endregion
1153
1213
  //#region ../../llm/llm/lib/types/assistant-stream.js
1154
1214
  /**
1155
1215
  * Lossless compact representation of one model-stream attempt, plus record-level
@@ -1479,6 +1539,7 @@ window.__ModuleLoader__.load({
1479
1539
  jumpTargetSeq = null;
1480
1540
  /** The running jump loop's completion, shared by retargeting callers. */
1481
1541
  jumpPromise = null;
1542
+ pendingHistory = null;
1482
1543
  stopObservingInbox;
1483
1544
  assistantStream = new ClientAssistantStream();
1484
1545
  running = false;
@@ -1492,7 +1553,7 @@ window.__ModuleLoader__.load({
1492
1553
  promptAttempted = false;
1493
1554
  /** A first accepted prompt stays in the engaging phase until its turn is observable. */
1494
1555
  firstPromptPendingTurn = false;
1495
- /** Empty-log mirror (see ConversationSnapshot.blank); unknown bare sessions begin conservatively blank. */
1556
+ /** New Session display state; unknown bare sessions begin conservatively blank. */
1496
1557
  blankBit = true;
1497
1558
  removed = false;
1498
1559
  promptError = null;
@@ -1508,8 +1569,9 @@ window.__ModuleLoader__.load({
1508
1569
  * Per-session projection value store (push model; see the session-projection
1509
1570
  * subsystem page, docs/subsystems/session-projection.md): finished whole
1510
1571
  * 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)`
1572
+ * projections block and updated by Session Controller control frames;
1573
+ * Host-sequenced writes merge under higher-seq-wins and cached list blocks
1574
+ * yield to them (projection-store.ts). Keys are read via `projections.faceOf(key)`
1513
1575
  * (the useProjection resolution face); the conversation snapshot never
1514
1576
  * carries projection values, and no client-side domain folding exists.
1515
1577
  * Manager-owned when constructed through SessionManager (frames route and
@@ -1574,14 +1636,16 @@ window.__ModuleLoader__.load({
1574
1636
  */
1575
1637
  beginSubmission(input) {
1576
1638
  const requestId = randomUUID();
1639
+ const placement = this.running ? input.mode === "steer" ? "steering" : "queued" : "transcript";
1577
1640
  this.pendingSubmissions = [...this.pendingSubmissions, {
1578
1641
  requestId,
1579
- placement: this.running ? input.mode === "steer" ? "steering" : "queued" : "transcript",
1642
+ placement,
1580
1643
  time: Date.now(),
1581
1644
  text: input.text,
1582
1645
  attachments: input.attachments
1583
1646
  }];
1584
1647
  this.submissionSettlements.set(requestId, {
1648
+ placement,
1585
1649
  onRetire: input.onRetire,
1586
1650
  retiring: false
1587
1651
  });
@@ -1649,9 +1713,9 @@ window.__ModuleLoader__.load({
1649
1713
  }
1650
1714
  if (this.blankBit) {
1651
1715
  this.blankBit = false;
1652
- this.options.onEngaged?.(this);
1653
1716
  this.notifier.markDirty();
1654
1717
  }
1718
+ this.options.onEngaged?.(this);
1655
1719
  return result;
1656
1720
  }
1657
1721
  /**
@@ -1779,29 +1843,37 @@ window.__ModuleLoader__.load({
1779
1843
  return this.jumpPromise;
1780
1844
  }
1781
1845
  if (this.loadingOlder) return Promise.resolve();
1846
+ const events = this.events;
1847
+ if (events === void 0) return Promise.resolve();
1848
+ const pending = {
1849
+ beforeSeq: this.baseSeq,
1850
+ hasMore: this.hasMore,
1851
+ pages: []
1852
+ };
1853
+ this.pendingHistory = pending;
1782
1854
  this.jumpTargetSeq = seq;
1783
1855
  this.loadingOlder = true;
1784
1856
  this.notifier.markDirty();
1785
1857
  const generation = this.openGeneration;
1786
1858
  this.jumpPromise = (async () => {
1787
1859
  try {
1788
- while (this.hasMore && this.jumpTargetSeq !== null && this.baseSeq > this.jumpTargetSeq) {
1860
+ while (pending.hasMore && this.jumpTargetSeq !== null && pending.beforeSeq > this.jumpTargetSeq) {
1789
1861
  if (generation !== this.openGeneration) return;
1790
- const events = this.events;
1791
- if (events === void 0) return;
1792
- const before = this.baseSeq;
1862
+ const before = pending.beforeSeq;
1793
1863
  await events.prepend({
1794
- beforeSeq: this.baseSeq,
1864
+ beforeSeq: before,
1795
1865
  maxMessages: 200
1796
1866
  });
1797
- if (this.baseSeq >= before) return;
1867
+ if (pending.beforeSeq >= before) return;
1798
1868
  }
1799
1869
  } catch (error) {
1800
1870
  if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) console.error("[session-controller] loadThrough failed:", error);
1801
1871
  } finally {
1802
1872
  this.jumpTargetSeq = null;
1803
1873
  this.jumpPromise = null;
1874
+ this.pendingHistory = null;
1804
1875
  this.loadingOlder = false;
1876
+ if (generation === this.openGeneration && pending.pages.length > 0) this.prependWindow(pending.pages.reverse().flat(), pending.hasMore);
1805
1877
  this.notifier.markDirty();
1806
1878
  }
1807
1879
  })();
@@ -1876,13 +1948,15 @@ window.__ModuleLoader__.load({
1876
1948
  this.notifier.markDirty();
1877
1949
  }
1878
1950
  /**
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.
1951
+ * Apply the Manager's effective display blank, further reconciled with the
1952
+ * current `sessionListMetadata` projection. Local send attempts and current
1953
+ * running state prevent re-blanking; an earlier false summary alone does not.
1954
+ * The Manager retains acceptance and earlier running observations across
1955
+ * Session-object replacement.
1956
+ * @param blank - New Session display state after Manager reconciliation.
1884
1957
  */
1885
1958
  handleBlank(blank) {
1959
+ blank = blank && this.projections.values().sessionListMetadata?.blank !== false;
1886
1960
  if (blank === this.blankBit) return;
1887
1961
  if (blank && (this.promptAttempted || this.running)) return;
1888
1962
  this.blankBit = blank;
@@ -1962,10 +2036,22 @@ window.__ModuleLoader__.load({
1962
2036
  const visible = this.assistantStream.replace(entries, assistantStream);
1963
2037
  this.baseSeq = SessionLogOffset(entries[0]?.event.seq ?? 0);
1964
2038
  this.hasMore = hasMore;
2039
+ if (this.pendingHistory !== null) {
2040
+ this.pendingHistory.beforeSeq = this.baseSeq;
2041
+ this.pendingHistory.hasMore = hasMore;
2042
+ this.pendingHistory.pages.length = 0;
2043
+ }
1965
2044
  if (visible.some((entry) => entry.event.type === "turn/start")) this.firstPromptPendingTurn = false;
1966
2045
  if (projections !== void 0) this.projections.seed(projections);
1967
2046
  this.eventSource.replace(visible, hasMore);
2047
+ if (projections !== void 0) {
2048
+ for (const [requestId, { receipt }] of this.submissionSettlements) if (receipt !== void 0 && receipt.seq <= projections.asOfSeq) this.scheduleObservedRetirement(requestId, receipt.attachments);
2049
+ }
1968
2050
  for (const entry of visible) this.observeSubmissionEvent(entry.event);
2051
+ if (projections !== void 0) {
2052
+ const inbox = projections.values.inbox;
2053
+ this.observeSteeringInsertions(inbox?.["next-step"] ?? [], 0, projections.asOfSeq);
2054
+ }
1969
2055
  this.notifier.markDirty();
1970
2056
  }
1971
2057
  publishAssistantEntry(result) {
@@ -1995,6 +2081,13 @@ window.__ModuleLoader__.load({
1995
2081
  }
1996
2082
  /** Prepend one stream-validated history page. */
1997
2083
  prependWindow(entries, hasMore) {
2084
+ if (this.pendingHistory !== null) {
2085
+ const pending = this.pendingHistory;
2086
+ pending.beforeSeq = entries[0] === void 0 ? pending.beforeSeq : SessionLogOffset(entries[0].event.seq);
2087
+ pending.hasMore = hasMore;
2088
+ pending.pages.push(entries);
2089
+ return;
2090
+ }
1998
2091
  this.baseSeq = entries[0] === void 0 ? this.baseSeq : SessionLogOffset(entries[0].event.seq);
1999
2092
  this.hasMore = hasMore;
2000
2093
  this.eventSource.prepend(entries, hasMore);
@@ -2012,28 +2105,55 @@ window.__ModuleLoader__.load({
2012
2105
  observeSubmissionEvent(event) {
2013
2106
  if (this.submissionSettlements.size === 0) return;
2014
2107
  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
- });
2108
+ const { target, start, removedCount = 0, inserted, outcome } = event.data;
2109
+ if (target === "next-step") {
2110
+ for (const [requestId, settlement] of this.submissionSettlements) {
2111
+ const receipt = settlement.receipt;
2112
+ if (receipt === void 0 || receipt.index === null || receipt.seq >= event.seq) continue;
2113
+ const removed = receipt.index >= start && receipt.index < start + removedCount;
2114
+ if (removed && outcome === "canceled") this.retireFailedSubmission(requestId);
2115
+ else settlement.receipt = {
2116
+ ...receipt,
2117
+ seq: event.seq,
2118
+ index: removed ? null : receipt.index < start ? receipt.index : receipt.index + inserted.length - removedCount
2119
+ };
2120
+ }
2121
+ this.observeSteeringInsertions(inserted, start, event.seq);
2122
+ }
2123
+ for (const message of inserted) this.observeSubmissionMessage(message, false);
2020
2124
  return;
2021
2125
  }
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));
2126
+ if (event.type === "request/context" || event.type === "turn/end") {
2127
+ for (const [requestId, settlement] of this.submissionSettlements) if (settlement.receipt?.index === null && settlement.receipt.seq < event.seq) this.retireFailedSubmission(requestId);
2128
+ return;
2129
+ }
2130
+ if (event.type === "user/message") this.observeSubmissionMessage(event.data, true);
2131
+ }
2132
+ observeSteeringInsertions(messages, start, seq) {
2133
+ for (const [index, message] of messages.entries()) {
2134
+ const source = message.source;
2135
+ if (source.kind !== "user" || !("rpcId" in source)) continue;
2136
+ const settlement = this.submissionSettlements.get(source.rpcId);
2137
+ if (settlement?.placement !== "steering" || settlement.retiring || (settlement.receipt?.seq ?? -1) > seq) continue;
2138
+ settlement.receipt = {
2139
+ seq,
2140
+ index: start + index,
2141
+ attachments: attachmentRefsIn(message.content)
2142
+ };
2143
+ }
2027
2144
  }
2028
- /** Retire local echoes when their accepted messages appear in the durable Inbox projection. */
2145
+ observeSubmissionMessage(message, admitted) {
2146
+ const source = message.source;
2147
+ if (source.kind !== "user" || !("rpcId" in source)) return;
2148
+ if (!admitted && this.submissionSettlements.get(source.rpcId)?.placement === "steering") return;
2149
+ this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(message.content));
2150
+ }
2151
+ /** Retire non-steering echoes when the Inbox accepts their queue occurrences. */
2029
2152
  observeSubmissionInbox() {
2030
2153
  if (this.submissionSettlements.size === 0) return;
2031
2154
  const inbox = this.projections.get("inbox");
2032
2155
  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));
2036
- }
2156
+ for (const message of [...inbox["next-turn"], ...inbox["next-step"]]) this.observeSubmissionMessage(message, false);
2037
2157
  }
2038
2158
  /**
2039
2159
  * Latch one observed settlement and remove the echo an animation frame
@@ -2082,12 +2202,16 @@ window.__ModuleLoader__.load({
2082
2202
  this.notifier.markDirty();
2083
2203
  }
2084
2204
  buildSnapshot() {
2205
+ const identity = this.projections.values().subagent;
2085
2206
  return {
2086
2207
  sessionId: this.sessionId,
2087
2208
  pendingSubmissions: this.pendingSubmissions,
2088
2209
  running: this.running,
2089
2210
  subagent: this.address === void 0 ? null : {
2090
- address: this.address,
2211
+ address: this.address.mode === "unknown" && identity != null ? {
2212
+ ...this.address,
2213
+ mode: identity.mode
2214
+ } : this.address,
2091
2215
  ...this.parentAvailable === void 0 ? {} : { parentAvailable: this.parentAvailable }
2092
2216
  },
2093
2217
  removed: this.removed,
@@ -2136,15 +2260,21 @@ window.__ModuleLoader__.load({
2136
2260
  function sessionSeqCursor(value) {
2137
2261
  return value === -1 ? -1 : SessionSeq(value);
2138
2262
  }
2139
- function catalogAvailability(parentAvailable) {
2140
- return parentAvailable === void 0 ? {} : { parentAvailable };
2141
- }
2142
2263
  /** Instance cluster + frame entry + the session list. */
2143
2264
  var SessionManager = class {
2144
2265
  remote;
2145
2266
  sessions = /* @__PURE__ */ new Map();
2146
2267
  /** In-flight Session disposals remain here after instances leave `sessions`, so manager disposal can await quiescence. */
2147
2268
  sessionDisposals = /* @__PURE__ */ new Set();
2269
+ /**
2270
+ * Accepted/running presentation must survive a later empty-history list
2271
+ * response. Host-asserted running is recorded even before a row, instance, or
2272
+ * address holds the identity — the listing that would hold it may not have
2273
+ * landed yet — while the client-local acceptance callback requires a current
2274
+ * holder, because it can arrive from a replaced or already-dropped Session.
2275
+ */
2276
+ engagedSessions = /* @__PURE__ */ new Set();
2277
+ disposed = false;
2148
2278
  /** Per-session projection value stores, retained independently of instance arrival (the
2149
2279
  * title-snapshot precedent, generalized): push frames land here whether or not the Session
2150
2280
  * is instantiated (list rows read the 'title' key), and an instantiated Session adopts the
@@ -2159,18 +2289,8 @@ window.__ModuleLoader__.load({
2159
2289
  /** Active list request's mutation log; its identity also fences completion after reconnect. */
2160
2290
  listMutations = null;
2161
2291
  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();
2292
+ projectionLoads = /* @__PURE__ */ new Map();
2293
+ projectionInflight = /* @__PURE__ */ new Map();
2174
2294
  listSnapshotCache;
2175
2295
  /** Entry-identity cache (reference stability): list rebuilds reuse the previous entry
2176
2296
  * object when every field matches — wire refreshes mint all-new summary objects, so identity
@@ -2192,31 +2312,23 @@ window.__ModuleLoader__.load({
2192
2312
  */
2193
2313
  resolveTarget(target) {
2194
2314
  const id = typeof target === "string" ? target : target.childSessionId;
2195
- const address = typeof target === "string" ? this.navigationAddress(id) : target;
2315
+ const address = typeof target === "string" ? this.subagentAddress(id) : target;
2196
2316
  if (typeof target === "string" && !this.sessions.has(id) && !this.summaries.some((summary) => summary.sessionId === id) && address === void 0) throw new Error(`sessions.retain: unknown session ${id}`);
2197
2317
  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);
2318
+ this.sessions.get(id)?.configureSubagent(address, address === void 0 ? void 0 : this.agentAvailable(address.parentSessionId));
2199
2319
  return id;
2200
2320
  }
2201
2321
  /**
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
2322
  * Resolve an address for breadcrumb navigation without retaining transport authority.
2211
2323
  * @param sessionId - possible child id in an already-loaded catalog.
2212
2324
  * @returns A retained or catalog-derived direct-parent address.
2213
2325
  */
2214
- navigationAddress(sessionId) {
2326
+ subagentAddress(sessionId) {
2215
2327
  const retained = this.addresses.get(sessionId);
2216
2328
  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 {
2329
+ for (const parentSessionId of this.projectionStores.keys()) {
2330
+ const child = this.projectionStores.get(parentSessionId)?.values().subagentCatalog?.find((entry) => entry.id === sessionId);
2331
+ if (child !== void 0) return {
2220
2332
  parentSessionId,
2221
2333
  childSessionId: sessionId,
2222
2334
  mode: child.mode
@@ -2234,6 +2346,7 @@ window.__ModuleLoader__.load({
2234
2346
  if (session !== expected) return Promise.resolve();
2235
2347
  this.sessions.delete(sessionId);
2236
2348
  this.addresses.delete(sessionId);
2349
+ this.pruneEngagement(sessionId, this.retainedIds(this.summaries));
2237
2350
  return this.startSessionDisposal(session);
2238
2351
  }
2239
2352
  /**
@@ -2241,10 +2354,14 @@ window.__ModuleLoader__.load({
2241
2354
  * @returns once catalog requests and every Session stream have stopped.
2242
2355
  */
2243
2356
  async dispose() {
2244
- for (const timer of this.catalogDebounce.values()) clearTimeout(timer);
2245
- this.catalogDebounce.clear();
2246
- this.catalogStale.clear();
2247
- this.openCatalogs.clear();
2357
+ this.disposed = true;
2358
+ this.listMutations = null;
2359
+ this.listInflight = null;
2360
+ this.engagedSessions.clear();
2361
+ const reads = [...this.projectionInflight.values()];
2362
+ for (const { controller } of reads) controller.abort();
2363
+ this.projectionInflight.clear();
2364
+ await Promise.all(reads.map((read) => read.promise));
2248
2365
  const sessions = [...this.sessions.values()];
2249
2366
  this.sessions.clear();
2250
2367
  this.addresses.clear();
@@ -2267,6 +2384,7 @@ window.__ModuleLoader__.load({
2267
2384
  /**
2268
2385
  * Lazy build: return the existing instance or construct one (no auto-open —
2269
2386
  * the reference allocator opens history after binding the scope).
2387
+ * New instances reconcile retained metadata before returning.
2270
2388
  * @param sessionId - the session to get.
2271
2389
  * @returns the resident instance.
2272
2390
  */
@@ -2277,42 +2395,71 @@ window.__ModuleLoader__.load({
2277
2395
  this.sessions.set(sessionId, session);
2278
2396
  const summary = this.summaries.find((s) => s.sessionId === sessionId);
2279
2397
  if (summary !== void 0) {
2280
- session.handleBlank(summary.blank);
2398
+ session.handleBlank(this.effectiveBlank(summary));
2281
2399
  session.handleRunning(summary.running);
2282
2400
  } else {
2283
2401
  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") {
2402
+ if ((address === void 0 ? void 0 : this.projectionStores.get(address.parentSessionId)?.values().subagentCatalog?.find((entry) => entry.id === sessionId)) !== void 0) {
2286
2403
  session.handleBlank(false);
2287
- session.handleRunning(child.activity === "running");
2288
- }
2404
+ session.handleRunning(false);
2405
+ } else session.handleBlank(true);
2289
2406
  }
2290
2407
  }
2291
2408
  return session;
2292
2409
  }
2293
2410
  createSession(sessionId) {
2294
2411
  const address = this.addresses.get(sessionId);
2295
- const parentAvailable = address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.parentAvailable;
2412
+ const parentAvailable = address === void 0 ? void 0 : this.agentAvailable(address.parentSessionId);
2296
2413
  return new Session(sessionId, this.remote, {
2297
2414
  ...address === void 0 ? {} : {
2298
2415
  address,
2299
- ...catalogAvailability(parentAvailable)
2416
+ ...parentAvailable === void 0 ? {} : { parentAvailable }
2300
2417
  },
2301
2418
  onEngaged: (engaged) => {
2302
- this.recordMutation({
2303
- kind: "engaged",
2304
- sessionId: engaged.sessionId
2305
- });
2419
+ if (this.disposed || !this.retainedIds(this.summaries).has(engaged.sessionId)) return;
2420
+ if (!this.engagedSessions.has(engaged.sessionId)) {
2421
+ this.engagedSessions.add(engaged.sessionId);
2422
+ this.recordMutation({
2423
+ kind: "engaged",
2424
+ sessionId: engaged.sessionId
2425
+ });
2426
+ }
2427
+ this.sessions.get(engaged.sessionId)?.handleBlank(false);
2306
2428
  },
2307
2429
  projections: this.projectionStore(sessionId)
2308
2430
  });
2309
2431
  }
2432
+ effectiveBlank(summary) {
2433
+ return summary.blank && !this.engagedSessions.has(summary.sessionId);
2434
+ }
2435
+ /**
2436
+ * Identities an engagement may still belong to: the given list rows, resident
2437
+ * Session instances, and retained child addresses.
2438
+ * @param summaries - list rows of the caller's snapshot.
2439
+ * @returns the retained identity set.
2440
+ */
2441
+ retainedIds(summaries) {
2442
+ const retained = new Set(summaries.map((summary) => summary.sessionId));
2443
+ for (const sessionId of this.sessions.keys()) retained.add(sessionId);
2444
+ for (const sessionId of this.addresses.keys()) retained.add(sessionId);
2445
+ return retained;
2446
+ }
2447
+ /**
2448
+ * Forget one engagement that no retained identity holds.
2449
+ * @param sessionId - identity whose engagement may be dropped.
2450
+ * @param retained - identities from {@link retainedIds} for the caller's snapshot.
2451
+ */
2452
+ pruneEngagement(sessionId, retained) {
2453
+ if (!retained.has(sessionId)) this.engagedSessions.delete(sessionId);
2454
+ }
2310
2455
  /** Resident per-session projection store (create-on-demand; outlives instantiation). */
2311
2456
  projectionStore(sessionId) {
2312
2457
  let store = this.projectionStores.get(sessionId);
2313
2458
  if (store === void 0) {
2314
2459
  store = new ProjectionValueStore();
2460
+ const projections = store;
2315
2461
  store.subscribeAny(() => {
2462
+ if (projections.values().sessionListMetadata?.blank === false) this.sessions.get(sessionId)?.handleBlank(false);
2316
2463
  this.notifier.markDirty();
2317
2464
  });
2318
2465
  this.projectionStores.set(sessionId, store);
@@ -2320,82 +2467,67 @@ window.__ModuleLoader__.load({
2320
2467
  return store;
2321
2468
  }
2322
2469
  /**
2323
- * Refresh one direct-child catalog, reusing its in-flight request.
2324
- * @param parentSessionId - catalog owner.
2470
+ * Load a complete projection baseline once per connection; retry unsuccessful reads.
2471
+ * @param sessionId - Session to inspect without opening its conversation.
2472
+ * @returns completion of the current or newly started read.
2325
2473
  */
2326
- refreshSubagents(parentSessionId) {
2327
- const existing = this.catalogInflight.get(parentSessionId);
2474
+ refreshProjections(sessionId) {
2475
+ const existing = this.projectionInflight.get(sessionId);
2328
2476
  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 },
2477
+ if (this.projectionLoads.get(sessionId)?.state === "ready") return Promise.resolve();
2478
+ const controller = new AbortController();
2479
+ const store = this.projectionStore(sessionId);
2480
+ const initialValues = store.values();
2481
+ this.projectionLoads.set(sessionId, {
2335
2482
  state: "loading",
2336
2483
  error: null
2337
2484
  });
2338
2485
  this.notifier.markDirty();
2339
2486
  const operation = (async () => {
2340
2487
  try {
2341
- const result = await this.remote.subagents.list(parentSessionId);
2488
+ const result = await this.remote.session.projections({ sessionId }, controller.signal);
2489
+ if (controller.signal.aborted) return;
2342
2490
  if (result.ok) {
2343
- const parentAvailable = this.catalogInflight.get(parentSessionId)?.parentAvailableOverride ?? result.value.parentAvailable;
2344
- this.catalogs.set(parentSessionId, {
2491
+ if (result.value !== null) store.seed({
2345
2492
  ...result.value,
2346
- entries: this.withCatalogMutations(result.value.entries, expandableRows, activityRows),
2347
- parentAvailable,
2493
+ asOfSeq: sessionSeqCursor(result.value.asOfSeq)
2494
+ });
2495
+ else if (store.values() === initialValues) store.clear();
2496
+ this.projectionLoads.set(sessionId, {
2348
2497
  state: "ready",
2349
2498
  error: null
2350
2499
  });
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),
2500
+ } else this.projectionLoads.set(sessionId, {
2358
2501
  state: "error",
2359
2502
  error: result.error
2360
2503
  });
2361
2504
  } catch (error) {
2505
+ if (controller.signal.aborted) return;
2362
2506
  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),
2507
+ this.projectionLoads.set(sessionId, {
2366
2508
  state: "error",
2367
2509
  error
2368
2510
  });
2369
2511
  } finally {
2370
- this.catalogInflight.delete(parentSessionId);
2371
- if (this.catalogStale.delete(parentSessionId)) this.refreshSubagents(parentSessionId);
2372
- this.notifier.markDirty();
2512
+ if (!controller.signal.aborted) {
2513
+ this.projectionInflight.delete(sessionId);
2514
+ this.notifier.markDirty();
2515
+ }
2373
2516
  }
2374
2517
  })();
2375
- this.catalogInflight.set(parentSessionId, {
2518
+ this.projectionInflight.set(sessionId, {
2376
2519
  promise: operation,
2377
- expandableRows,
2378
- activityRows,
2379
- parentAvailableOverride: void 0
2520
+ controller
2380
2521
  });
2381
2522
  return operation;
2382
2523
  }
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
- }
2524
+ agentAvailable(sessionId) {
2525
+ return this.summaries.find((summary) => summary.sessionId === sessionId)?.agentAvailable ?? (this.listPhase === "ready" ? false : void 0);
2526
+ }
2527
+ updateParentAvailability() {
2528
+ for (const [childId, address] of this.addresses) {
2529
+ const available = this.agentAvailable(address.parentSessionId);
2530
+ if (available !== void 0) this.sessions.get(childId)?.handleSubagentParentAvailable(available);
2399
2531
  }
2400
2532
  }
2401
2533
  /** Full refresh via session.list (single-flight within one Host generation). */
@@ -2413,22 +2545,23 @@ window.__ModuleLoader__.load({
2413
2545
  if (this.listMutations !== mutations) return;
2414
2546
  if (result.ok) {
2415
2547
  const baseline = this.listPhase === "pending" ? [...result.value.items] : mergeOrderedBaseline(established, result.value.items, (summary) => summary.sessionId);
2416
- this.summaries = mutations.reduce(applyMutation, baseline);
2548
+ const removedSincePull = /* @__PURE__ */ new Set();
2549
+ for (const mutation of mutations) if (mutation.kind === "remove") removedSincePull.add(mutation.sessionId);
2550
+ for (const s of baseline) if (s.running && !removedSincePull.has(s.sessionId)) this.engagedSessions.add(s.sessionId);
2551
+ const summaries = mutations.reduce(applyMutation, baseline);
2552
+ this.summaries = summaries;
2553
+ const retained = this.retainedIds(summaries);
2554
+ for (const sessionId of this.engagedSessions) this.pruneEngagement(sessionId, retained);
2417
2555
  this.listState = "idle";
2418
2556
  this.listPhase = "ready";
2557
+ this.updateParentAvailability();
2419
2558
  for (const s of this.summaries) {
2420
2559
  const session = this.sessions.get(s.sessionId);
2421
2560
  if (session === void 0) continue;
2422
- session.handleBlank(s.blank);
2561
+ session.handleBlank(this.effectiveBlank(s));
2423
2562
  session.handleRunning(s.running);
2424
2563
  }
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
- }
2564
+ for (const s of result.value.items) if (s.projections !== void 0) this.applyListBlock(s.sessionId, s.projections);
2432
2565
  } else {
2433
2566
  this.listState = "error";
2434
2567
  this.listError = result.error;
@@ -2484,8 +2617,9 @@ window.__ModuleLoader__.load({
2484
2617
  };
2485
2618
  const result = await this.remote.session.create(payload);
2486
2619
  if (result.ok) this.recordMutation({
2487
- kind: "upsert",
2620
+ kind: "placeholder",
2488
2621
  summary: {
2622
+ agentAvailable: true,
2489
2623
  sessionId: result.value.sessionId,
2490
2624
  updatedAt: Date.now(),
2491
2625
  running: false,
@@ -2496,8 +2630,9 @@ window.__ModuleLoader__.load({
2496
2630
  else {
2497
2631
  const publishedSessionId = workspaceAttachSessionId(result.error);
2498
2632
  if (publishedSessionId !== void 0) this.recordMutation({
2499
- kind: "upsert",
2633
+ kind: "placeholder",
2500
2634
  summary: {
2635
+ agentAvailable: true,
2501
2636
  sessionId: publishedSessionId,
2502
2637
  updatedAt: Date.now(),
2503
2638
  running: false,
@@ -2509,11 +2644,12 @@ window.__ModuleLoader__.load({
2509
2644
  }
2510
2645
  /**
2511
2646
  * 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.
2647
+ * immediately (same synchronous-addressability guarantee as create).
2648
+ * Blankness starts provisionally true so the authoritative Host summary can
2649
+ * preserve it or lower it after an exact cut before the first `turn/start`;
2650
+ * lineage rides parentSessionId. A child published before Workspace
2651
+ * attachment fails is also reconciled into the list.
2652
+ * @param opts - source session and the optional exact inclusive boundary seq.
2517
2653
  * @returns the fork result (the child session id).
2518
2654
  */
2519
2655
  async fork(opts) {
@@ -2524,12 +2660,13 @@ window.__ModuleLoader__.load({
2524
2660
  });
2525
2661
  const childId = result.ok ? result.value.sessionId : workspaceAttachSessionId(result.error);
2526
2662
  if (childId !== void 0) this.recordMutation({
2527
- kind: "upsert",
2663
+ kind: "placeholder",
2528
2664
  summary: {
2665
+ agentAvailable: true,
2529
2666
  sessionId: childId,
2530
2667
  updatedAt: Date.now(),
2531
2668
  running: false,
2532
- blank: false,
2669
+ blank: true,
2533
2670
  parentSessionId: opts.sessionId,
2534
2671
  ...source?.cwd !== void 0 ? { cwd: source.cwd } : {}
2535
2672
  }
@@ -2537,19 +2674,33 @@ window.__ModuleLoader__.load({
2537
2674
  return result;
2538
2675
  }
2539
2676
  /**
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).
2677
+ * Rename a Session and update its title projection without opening its history.
2678
+ * @param sessionId - Session to rename.
2679
+ * @param title - raw title text for Host normalization.
2680
+ * @returns the accepted title and event position, or the Remote failure.
2681
+ */
2682
+ async rename(sessionId, title) {
2683
+ const result = await this.remote.session.rename({
2684
+ sessionId,
2685
+ title
2686
+ });
2687
+ if (result.ok) this.projectionStore(sessionId).apply("title", result.value.title, SessionSeq(result.value.seq));
2688
+ return result;
2689
+ }
2690
+ /**
2691
+ * Merge a Host summary, replacing live state and filling missing metadata.
2692
+ * Local create/fork placeholders only fill metadata on an existing row.
2544
2693
  */
2545
2694
  mergeSummary(summary) {
2546
2695
  this.recordMutation({
2547
2696
  kind: "upsert",
2548
2697
  summary
2549
2698
  });
2699
+ this.updateParentAvailability();
2550
2700
  }
2551
2701
  /** Apply immediately and retain for replay when a list response is in flight. */
2552
2702
  recordMutation(mutation) {
2703
+ if (this.disposed) return;
2553
2704
  this.listMutations?.push(mutation);
2554
2705
  this.summaries = applyMutation(this.summaries, mutation);
2555
2706
  this.notifier.markDirty();
@@ -2587,18 +2738,10 @@ window.__ModuleLoader__.load({
2587
2738
  this.replaceControlBaseline(frame.value);
2588
2739
  return;
2589
2740
  }
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);
2741
+ this.projectionStore(frame.sessionId).apply(frame.key, frame.value, SessionSeq(frame.seq));
2597
2742
  this.notifier.markDirty();
2598
2743
  }
2599
2744
  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
2745
  for (const [sessionId, block] of Object.entries(baseline.projections)) {
2603
2746
  const store = this.projectionStore(sessionId);
2604
2747
  const asOfSeq = sessionSeqCursor(block.asOfSeq);
@@ -2615,44 +2758,55 @@ window.__ModuleLoader__.load({
2615
2758
  */
2616
2759
  handleSessionAdded(summary) {
2617
2760
  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));
2761
+ if (!this.disposed && summary.running) this.engagedSessions.add(summary.sessionId);
2762
+ this.sessions.get(summary.sessionId)?.handleBlank(this.effectiveBlank(summary));
2763
+ if (summary.projections !== void 0) this.applyListBlock(summary.sessionId, summary.projections);
2764
+ }
2765
+ /**
2766
+ * Merge one list-surface projection block by the sequence space it declares.
2767
+ * A `sequenced` block came from the Host's live registry for an attached
2768
+ * Session, so each key lands under higher-seq-wins against that Session's
2769
+ * baselines and frames. A `cached` block was viewed from the persisted
2770
+ * checkpoint by a header-only listing: its watermark is not comparable with
2771
+ * this connection's seqs, so it only fills keys no sequenced row holds.
2772
+ */
2773
+ applyListBlock(sessionId, block) {
2774
+ const store = this.projectionStore(sessionId);
2775
+ switch (block.kind) {
2776
+ case "sequenced": {
2777
+ const seq = sessionSeqCursor(block.asOfSeq);
2778
+ for (const [key, value] of Object.entries(block.values)) store.apply(key, value, seq);
2779
+ return;
2780
+ }
2781
+ case "cached":
2782
+ store.applyCached(block.values);
2783
+ return;
2784
+ default: assertNever(block.kind, "session list projection block kind");
2623
2785
  }
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
2786
  }
2627
2787
  /**
2628
2788
  * Apply one Session removal forwarded through `ctx.remote.$on`.
2629
2789
  * @param sessionId - removed Session identity.
2630
2790
  */
2631
2791
  handleSessionRemoved(sessionId) {
2632
- const durableSubagent = this.summaries.find((candidate) => candidate.sessionId === sessionId)?.origin === "subagent" || this.addresses.has(sessionId);
2792
+ const durableSubagent = this.subagentAddress(sessionId) !== void 0 || this.summaries.some((summary) => summary.sessionId === sessionId && summary.origin === "subagent");
2633
2793
  this.recordMutation(durableSubagent ? {
2634
2794
  kind: "status",
2635
2795
  sessionId,
2636
- running: false
2796
+ running: false,
2797
+ agentAvailable: false
2637
2798
  } : {
2638
2799
  kind: "remove",
2639
2800
  sessionId
2640
2801
  });
2641
- this.updateCatalogActivity(sessionId, false);
2642
2802
  if (durableSubagent) this.sessions.get(sessionId)?.handleRunning(false);
2643
2803
  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
- });
2804
+ const catalog = this.projectionStores.get(sessionId)?.values().subagentCatalog;
2805
+ if (!durableSubagent && (catalog === void 0 || catalog.length === 0)) this.projectionStores.delete(sessionId);
2806
+ this.pruneEngagement(sessionId, this.retainedIds(this.summaries));
2807
+ this.projectionInflight.get(sessionId)?.controller.abort();
2808
+ this.projectionInflight.delete(sessionId);
2809
+ this.projectionLoads.delete(sessionId);
2656
2810
  for (const [childId, address] of this.addresses) if (address.parentSessionId === sessionId) this.sessions.get(childId)?.handleSubagentParentAvailable(false);
2657
2811
  }
2658
2812
  /**
@@ -2661,13 +2815,15 @@ window.__ModuleLoader__.load({
2661
2815
  * @param running - current Agent running state.
2662
2816
  */
2663
2817
  handleSessionStatus(sessionId, running) {
2818
+ if (!this.disposed && running) this.engagedSessions.add(sessionId);
2664
2819
  this.recordMutation({
2665
2820
  kind: "status",
2666
2821
  sessionId,
2667
- running
2822
+ running,
2823
+ agentAvailable: true
2668
2824
  });
2825
+ this.updateParentAvailability();
2669
2826
  this.sessions.get(sessionId)?.handleRunning(running);
2670
- this.updateCatalogActivity(sessionId, running);
2671
2827
  }
2672
2828
  /**
2673
2829
  * Advance Session-list activity from one user-authored durable message.
@@ -2700,93 +2856,26 @@ window.__ModuleLoader__.load({
2700
2856
  this.listMutations = null;
2701
2857
  this.listInflight = null;
2702
2858
  this.refreshList();
2703
- const parents = new Set(this.openCatalogs);
2859
+ const parents = new Set(this.projectionLoads.keys());
2704
2860
  for (const id of this.sessions.keys()) {
2705
2861
  const address = this.addresses.get(id);
2706
2862
  if (address !== void 0) parents.add(address.parentSessionId);
2707
2863
  }
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
- });
2864
+ for (const { controller } of this.projectionInflight.values()) controller.abort();
2865
+ this.projectionInflight.clear();
2866
+ this.projectionLoads.clear();
2867
+ for (const parentSessionId of parents) this.refreshProjections(parentSessionId);
2782
2868
  }
2783
2869
  buildListSnapshot() {
2784
2870
  const items = flattenLineage(this.summaries.map((summary) => {
2785
2871
  const projectionStore = this.projectionStores.get(summary.sessionId);
2786
2872
  const title = projectionStore?.get("title");
2787
2873
  const projectionValues = projectionStore?.values();
2874
+ const metadata = projectionValues?.sessionListMetadata;
2788
2875
  return {
2789
2876
  ...summary,
2877
+ blank: this.effectiveBlank(summary) && metadata?.blank !== false,
2878
+ updatedAt: Math.max(summary.updatedAt, metadata?.lastPromptAt ?? 0),
2790
2879
  ...typeof title === "string" && title !== "" ? { title } : {},
2791
2880
  ...projectionValues === void 0 ? {} : { projectionValues }
2792
2881
  };
@@ -2804,31 +2893,41 @@ window.__ModuleLoader__.load({
2804
2893
  state: this.listState,
2805
2894
  phase: this.listPhase,
2806
2895
  error: this.listError,
2807
- subagentsByParent: Object.fromEntries(this.catalogs),
2808
- jobsBySession: Object.fromEntries(this.jobsBySession)
2896
+ projectionsBySession: Object.fromEntries([...this.projectionStores].map(([sessionId, store]) => [sessionId, {
2897
+ values: store.values(),
2898
+ state: "idle",
2899
+ error: null,
2900
+ ...this.projectionLoads.get(sessionId)
2901
+ }]))
2809
2902
  };
2810
2903
  }
2811
2904
  };
2812
2905
  /** Apply one list mutation without deriving display order. */
2813
2906
  function applyMutation(summaries, mutation) {
2814
2907
  switch (mutation.kind) {
2815
- case "upsert": {
2908
+ case "upsert":
2909
+ case "placeholder": {
2816
2910
  const existing = summaries.find((summary) => summary.sessionId === mutation.summary.sessionId);
2817
2911
  if (existing === void 0) return [mutation.summary, ...summaries];
2818
2912
  const filled = {
2819
2913
  ...existing,
2820
2914
  blank: existing.blank && mutation.summary.blank,
2915
+ ...mutation.kind === "upsert" ? {
2916
+ agentAvailable: mutation.summary.agentAvailable,
2917
+ running: mutation.summary.running
2918
+ } : {},
2821
2919
  ...existing.cwd === void 0 && mutation.summary.cwd !== void 0 ? { cwd: mutation.summary.cwd } : {},
2822
2920
  ...existing.parentSessionId === void 0 && mutation.summary.parentSessionId !== void 0 ? { parentSessionId: mutation.summary.parentSessionId } : {},
2823
2921
  ...existing.origin === void 0 && mutation.summary.origin !== void 0 ? { origin: mutation.summary.origin } : {}
2824
2922
  };
2825
- if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId && filled.origin === existing.origin && filled.blank === existing.blank) return [...summaries];
2923
+ if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId && filled.origin === existing.origin && filled.blank === existing.blank && filled.agentAvailable === existing.agentAvailable && filled.running === existing.running) return [...summaries];
2826
2924
  return summaries.map((summary) => summary.sessionId === mutation.summary.sessionId ? filled : summary);
2827
2925
  }
2828
2926
  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) ? {
2927
+ case "status": return summaries.map((summary) => summary.sessionId === mutation.sessionId && (summary.running !== mutation.running || summary.agentAvailable !== mutation.agentAvailable || mutation.running && summary.blank) ? {
2830
2928
  ...summary,
2831
2929
  running: mutation.running,
2930
+ agentAvailable: mutation.agentAvailable,
2832
2931
  blank: summary.blank && !mutation.running
2833
2932
  } : summary);
2834
2933
  case "activity": return summaries.map((summary) => summary.sessionId === mutation.sessionId && mutation.updatedAt > summary.updatedAt ? {
@@ -2999,8 +3098,7 @@ window.__ModuleLoader__.load({
2999
3098
  ids: [],
3000
3099
  byId: {},
3001
3100
  phase: "pending",
3002
- subagentsByParent: {},
3003
- jobsBySession: {}
3101
+ projectionsBySession: {}
3004
3102
  });
3005
3103
  const disposeManagerProjection = this.manager.subscribe(() => {
3006
3104
  this.projectList();
@@ -3072,25 +3170,17 @@ window.__ModuleLoader__.load({
3072
3170
  * Resolve an already discovered direct-parent address without opening it.
3073
3171
  * Feature plugins use this to avoid Agent-bound RPCs in persisted child views.
3074
3172
  * @param id - possible addressed child id.
3075
- * @returns The retained address, when present.
3173
+ * @returns A retained or loaded-catalog address, without retaining a new selection or scope.
3076
3174
  */
3077
3175
  subagentAddress(id) {
3078
3176
  return this.manager.subagentAddress(id);
3079
3177
  }
3080
3178
  /**
3081
- * Inform the Session Controller whether a catalog menu is consuming membership updates.
3082
- * @param parentSessionId - selected parent.
3083
- * @param open - menu state.
3084
- */
3085
- setSubagentCatalogOpen(parentSessionId, open) {
3086
- this.manager.setSubagentCatalogOpen(parentSessionId, open);
3087
- }
3088
- /**
3089
- * Refresh one direct-child catalog.
3090
- * @param parentSessionId - catalog owner.
3179
+ * Load all Session projections once per connection; retry an unsuccessful initial read.
3180
+ * @param sessionId - Session to inspect without opening its conversation.
3091
3181
  */
3092
- refreshSubagents(parentSessionId) {
3093
- return this.manager.refreshSubagents(parentSessionId);
3182
+ refreshProjections(sessionId) {
3183
+ return this.manager.refreshProjections(sessionId);
3094
3184
  }
3095
3185
  /**
3096
3186
  * Refresh the real Session baseline, reusing an in-flight pull.
@@ -3169,15 +3259,14 @@ window.__ModuleLoader__.load({
3169
3259
  return result.value.sessionId;
3170
3260
  }
3171
3261
  /**
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.
3262
+ * Fork a session from an exact inclusive prefix of the source (same
3263
+ * synchronous-addressability guarantee as {@link ClientSessions.create}:
3264
+ * on resolution the child is catalogued and may be explicitly retained).
3265
+ * @param opts - source session id, the optional exact inclusive boundary
3266
+ * seq (a real event seq the caller already knows; a cut inside an open
3267
+ * turn is balanced Host-side with synthetic closers, and omission selects
3268
+ * the latest completed-turn prefix), and whether to increment an
3269
+ * inherited durable title before resolving.
3181
3270
  * @returns the child session id.
3182
3271
  * @throws {SessionForkError} with the source id.
3183
3272
  * @throws {Error} when a requested child-title rename fails after creation.
@@ -3186,20 +3275,14 @@ window.__ModuleLoader__.load({
3186
3275
  const sourceTitle = opts.increaseTitle ? this.list.getSnapshot().byId[opts.sessionId]?.title : void 0;
3187
3276
  const result = await this.manager.fork({
3188
3277
  sessionId: opts.sessionId,
3189
- ...opts.atSeq === void 0 ? {} : { atSeq: SessionSeq(Math.floor(opts.atSeq)) }
3278
+ ...opts.atSeq === void 0 ? {} : { atSeq: SessionSeq(opts.atSeq) }
3190
3279
  });
3191
3280
  if (!result.ok) throw new SessionForkError(result.error, opts.sessionId);
3192
3281
  this.projectList();
3193
3282
  const childId = result.value.sessionId;
3194
3283
  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
- }
3284
+ const renamed = await this.manager.rename(childId, increasedForkTitle(sourceTitle));
3285
+ if (!renamed.ok) throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`);
3203
3286
  }
3204
3287
  return childId;
3205
3288
  }
@@ -3279,7 +3362,7 @@ window.__ModuleLoader__.load({
3279
3362
  if (count === 0) this.retireScope(id, record);
3280
3363
  else this.publishRetention(id);
3281
3364
  });
3282
- if (this.list.getSnapshot().byId[id] === void 0) this.projectList();
3365
+ if (this.list.getSnapshot().byId[id] === void 0 && this.manager.subagentAddress(id) !== void 0) this.projectList();
3283
3366
  this.publishRetention(id);
3284
3367
  return reference;
3285
3368
  }
@@ -3343,7 +3426,7 @@ window.__ModuleLoader__.load({
3343
3426
  /** Project the manager's list snapshot into the store (title derivation is display-only). */
3344
3427
  projectList() {
3345
3428
  const previousById = this.list.getSnapshot().byId;
3346
- const { items, phase, subagentsByParent, jobsBySession } = this.manager.getListSnapshot();
3429
+ const { items, phase, projectionsBySession } = this.manager.getListSnapshot();
3347
3430
  const ids = [];
3348
3431
  const byId = {};
3349
3432
  for (const entry of items) {
@@ -3362,8 +3445,7 @@ window.__ModuleLoader__.load({
3362
3445
  ...entry.origin !== void 0 ? { origin: entry.origin } : {}
3363
3446
  };
3364
3447
  }
3365
- for (const [parentId, catalog] of Object.entries(subagentsByParent)) for (const child of catalog.entries) {
3366
- if (child.kind !== "child") continue;
3448
+ for (const [parentId, projection] of Object.entries(projectionsBySession)) for (const child of projection.values.subagentCatalog ?? []) {
3367
3449
  const childId = child.id;
3368
3450
  const summary = byId[childId];
3369
3451
  const projectionValues = summary?.projectionValues ?? this.manager.projectionValues(childId);
@@ -3375,7 +3457,7 @@ window.__ModuleLoader__.load({
3375
3457
  displayTitle,
3376
3458
  parentId,
3377
3459
  origin: "subagent",
3378
- running: child.activity === "running",
3460
+ running: this.scopes.get(childId)?.session.getSnapshot().running ?? false,
3379
3461
  blank: false,
3380
3462
  updatedAt: 0,
3381
3463
  retainedBy: this.retentionSnapshot(childId).retainedBy,
@@ -3390,9 +3472,13 @@ window.__ModuleLoader__.load({
3390
3472
  }
3391
3473
  for (const [id, record] of this.scopes) {
3392
3474
  if (byId[id] !== void 0) continue;
3475
+ const address = this.manager.subagentAddress(id);
3476
+ if (address === void 0) continue;
3393
3477
  const previous = previousById[id];
3394
3478
  const snapshot = record.session.getSnapshot();
3395
- const address = this.manager.subagentAddress(id);
3479
+ const projectionValues = this.manager.projectionValues(id);
3480
+ const projectedTitle = projectionValues?.title;
3481
+ const title = typeof projectedTitle === "string" && projectedTitle !== "" ? projectedTitle : previous?.title;
3396
3482
  byId[id] = {
3397
3483
  ...previous ?? {
3398
3484
  id,
@@ -3402,9 +3488,12 @@ window.__ModuleLoader__.load({
3402
3488
  running: snapshot.running,
3403
3489
  retainedBy: record.retention.retainedBy,
3404
3490
  blank: snapshot.blank,
3405
- ...address === void 0 ? {} : {
3406
- parentId: address.parentSessionId,
3407
- origin: "subagent"
3491
+ parentId: address.parentSessionId,
3492
+ origin: "subagent",
3493
+ ...projectionValues === void 0 ? {} : { projectionValues },
3494
+ ...title === void 0 ? {} : {
3495
+ title,
3496
+ displayTitle: title
3408
3497
  }
3409
3498
  };
3410
3499
  }
@@ -3412,8 +3501,7 @@ window.__ModuleLoader__.load({
3412
3501
  ids,
3413
3502
  byId,
3414
3503
  phase,
3415
- subagentsByParent,
3416
- jobsBySession
3504
+ projectionsBySession
3417
3505
  });
3418
3506
  }
3419
3507
  startScopeDrop(id, record, disposeFiber = true, sessionDisposal = this.manager.drop(id, record.session)) {