@deepseek-ai/dsh-api-session-controller 0.1.5-rc.2 → 0.1.6-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 (39) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +20 -4
  3. package/README.zh.md +26 -10
  4. package/lib/client.js +432 -474
  5. package/lib/index.js +18 -49
  6. package/lib/typert.host.js +242 -200
  7. package/lib/typert.remote-client.js +195 -165
  8. package/lib/types/agent.d.ts +1 -1
  9. package/lib/types/agent.js +8 -1
  10. package/lib/types/client/contract/sessions.d.ts +55 -20
  11. package/lib/types/client/contract/snapshot.d.ts +0 -14
  12. package/lib/types/client/index.d.ts +11 -2
  13. package/lib/types/client/index.js +16 -5
  14. package/lib/types/client/scope.d.ts +13 -3
  15. package/lib/types/client/scope.js +15 -22
  16. package/lib/types/client/sessions/lineage.d.ts +1 -4
  17. package/lib/types/client/sessions/lineage.js +1 -3
  18. package/lib/types/client/sessions/manager.d.ts +28 -49
  19. package/lib/types/client/sessions/manager.js +75 -174
  20. package/lib/types/client/sessions/projection-store.d.ts +5 -11
  21. package/lib/types/client/sessions/projection-store.js +6 -14
  22. package/lib/types/client/sessions/service.d.ts +34 -119
  23. package/lib/types/client/sessions/service.js +259 -244
  24. package/lib/types/client/sessions/session.d.ts +7 -20
  25. package/lib/types/client/sessions/session.js +29 -33
  26. package/lib/types/client/time-zone.d.ts +1 -1
  27. package/lib/types/client/time-zone.js +1 -1
  28. package/lib/types/commands.d.ts +2 -2
  29. package/lib/types/commands.js +13 -8
  30. package/lib/types/control.d.ts +1 -1
  31. package/lib/types/control.js +1 -41
  32. package/lib/types/history.js +1 -0
  33. package/lib/types/index.d.ts +2 -2
  34. package/lib/types/index.js +2 -1
  35. package/lib/types/skill-catalog.js +1 -0
  36. package/lib/types/types.d.ts +5 -17
  37. package/package.json +72 -69
  38. package/lib/types/client/sessions/queue-mirror.d.ts +0 -26
  39. package/lib/types/client/sessions/queue-mirror.js +0 -61
package/lib/client.js CHANGED
@@ -34,6 +34,27 @@ window.__ModuleLoader__.load({
34
34
  this.name = "RemoteError";
35
35
  }
36
36
  };
37
+ /** Generic invocation-owned values returned by synchronous Client Context resolvers. */
38
+ /** Shared identity across independently bundled Context providers and Gateway. */
39
+ const TYPERT_OWNED_VALUE = Symbol.for("dsh.typert.owned-value");
40
+ /**
41
+ * Transfer cleanup ownership without adding another resource reference count.
42
+ * @param value - resolved payload passed to the invocation.
43
+ * @param release - non-throwing synchronous release, called at most once.
44
+ * @returns an owned payload disposed after invocation and reply settlement.
45
+ */
46
+ function typertOwnedValue(value, release) {
47
+ let active = true;
48
+ return {
49
+ [TYPERT_OWNED_VALUE]: true,
50
+ value,
51
+ [Symbol.dispose]() {
52
+ if (!active) return;
53
+ active = false;
54
+ release();
55
+ }
56
+ };
57
+ }
37
58
  //#endregion
38
59
  //#region lib/types/client/sessions/history-records.js
39
60
  /** Client range access and type narrowing for aligned Session history records. */
@@ -134,6 +155,7 @@ window.__ModuleLoader__.load({
134
155
  "goal/change",
135
156
  "hook/invoked",
136
157
  "hook/result",
158
+ "image/offload",
137
159
  "llm/retry",
138
160
  "llm/retry-started",
139
161
  "model/selection",
@@ -169,7 +191,8 @@ window.__ModuleLoader__.load({
169
191
  "turn/end",
170
192
  "turn/start",
171
193
  "user/message",
172
- "web/deepseek-search-llm-request"
194
+ "web/deepseek-search-llm-request",
195
+ "workspace/changes"
173
196
  ]);
174
197
  //#endregion
175
198
  //#region ../../core/session/lib/types/surface.js
@@ -244,7 +267,7 @@ window.__ModuleLoader__.load({
244
267
  return op;
245
268
  }
246
269
  /** Validate cited source-event seqs against prior log entries and the replacement range. */
247
- function assertProvenance(event, shadowedSeqs) {
270
+ function assertSourceEventReferences(event, shadowedSeqs) {
248
271
  const raw = event.sourceEventSeqs;
249
272
  if (event.type === "assistant/message" && raw !== void 0) throw new Error("assistant/message embeds its source stream and cannot carry sourceEventSeqs");
250
273
  const sources = /* @__PURE__ */ new Set();
@@ -273,7 +296,7 @@ window.__ModuleLoader__.load({
273
296
  function validateSurfaceMetadata(event) {
274
297
  const op = surfaceOpOf(event);
275
298
  if (op !== void 0 && op !== "append" && (op.startSeq >= event.seq || op.endSeq >= event.seq)) throw new Error(`surface replace at seq ${event.seq}: startSeq and endSeq must reference earlier events`);
276
- if (op !== void 0) assertProvenance(event, []);
299
+ if (op !== void 0) assertSourceEventReferences(event, []);
277
300
  return op;
278
301
  }
279
302
  //#endregion
@@ -453,23 +476,7 @@ window.__ModuleLoader__.load({
453
476
  }
454
477
  //#endregion
455
478
  //#region lib/types/client/scope.js
456
- /**
457
- * Client Agent-scope primitive: mint a Cordis context tagged with the owning
458
- * Agent's identity. The mechanism mirrors the host `dsh-scope` architecture
459
- * (no-op plugin fiber + context tag + `Context.filter` routing predicate);
460
- * the shape deliberately diverges: the filter lives on the actx itself
461
- * instead of a separate carrier object, so scoped dispatch is plain cordis —
462
- * `actx.bail(actx, event, payload)` / `actx.emit(actx, ...)` — with no
463
- * wrapper. The host needs a detached carrier because its dispatch subject is
464
- * the business Agent object; client scope events carry only ids, so the
465
- * actx is the natural subject. The second divergence stands: the scope key
466
- * is the branded `SessionId` (value compared), not an object identity — the
467
- * agent and its session share one id (1:1, same axis; no separate AgentId
468
- * brand), and a client scope's identity IS that wire id. Third divergence,
469
- * deliberate: the client scopes the Agent IDENTITY, not a live Agent object
470
- * — a cold session's host Agent is already disposed while its client actx
471
- * stays alive for history viewing.
472
- */
479
+ /** Client scope generations route local events independently of Host Agent residency. */
473
480
  /** Context tag written by {@link createScope}. */
474
481
  const kScope = Symbol("dsh.client.scope");
475
482
  /** Shared no-op plugin backing each Agent scope fiber. */
@@ -477,21 +484,22 @@ window.__ModuleLoader__.load({
477
484
  /**
478
485
  * Mint an Agent scope under `ctx`: a no-op plugin fiber whose context
479
486
  * carries the agent tag and the dispatch filter — untagged listeners are
480
- * admitted globally, tagged listeners only for a matching agent.
487
+ * admitted globally, tagged listeners only for the same Client generation.
481
488
  * Registrations through the returned ctx dispose with the fiber.
482
489
  * @param ctx - client root context the scope fiber mounts under.
483
- * @param key - owning agent identity (the routing tag; agent id === session id).
490
+ * @param key - durable Session identity carried by this generation.
484
491
  * @returns the tagged context and its backing fiber.
485
492
  */
486
493
  function createScope(ctx, key) {
487
494
  const fiber = ctx.plugin(agentScope);
495
+ const identity = { sessionId: key };
488
496
  return {
489
497
  fiber,
490
498
  ctx: fiber.ctx.extend({
491
- [kScope]: key,
499
+ [kScope]: identity,
492
500
  [_deepseek_ai_cordis.Context.filter](listenerCtx) {
493
- const tag = scopeOf(listenerCtx);
494
- return tag === void 0 || tag === key;
501
+ const tag = scopeIdentityOf(listenerCtx);
502
+ return tag === void 0 || tag === identity;
495
503
  }
496
504
  })
497
505
  };
@@ -502,6 +510,14 @@ window.__ModuleLoader__.load({
502
510
  * @returns its agent identity (the session id), or undefined for root contexts.
503
511
  */
504
512
  function scopeOf(ctx) {
513
+ return scopeIdentityOf(ctx)?.sessionId;
514
+ }
515
+ /**
516
+ * Read the exact generation identity inherited by a Client Context.
517
+ * @param ctx - scoped or root Client Context.
518
+ * @returns the generation identity, or undefined for an unscoped Context.
519
+ */
520
+ function scopeIdentityOf(ctx) {
505
521
  return ctx[kScope];
506
522
  }
507
523
  //#endregion
@@ -548,10 +564,9 @@ window.__ModuleLoader__.load({
548
564
  * follows the established input order; this projection never re-sorts a
549
565
  * hydrated list from mutable timestamps.
550
566
  * @param summaries - the host's session.list items.
551
- * @param completed - sessions with a pending completion reminder (manager-owned live fact; absent = false).
552
567
  * @returns display rows in render order.
553
568
  */
554
- function flattenLineage(summaries, completed) {
569
+ function flattenLineage(summaries) {
555
570
  const byId = /* @__PURE__ */ new Map();
556
571
  for (const s of summaries) byId.set(s.sessionId, s);
557
572
  const children = /* @__PURE__ */ new Map();
@@ -571,7 +586,6 @@ window.__ModuleLoader__.load({
571
586
  visited.add(s.sessionId);
572
587
  out.push({
573
588
  ...s,
574
- completed: completed?.has(s.sessionId) ?? false,
575
589
  depth
576
590
  });
577
591
  const kids = children.get(s.sessionId);
@@ -675,9 +689,9 @@ window.__ModuleLoader__.load({
675
689
  /**
676
690
  * One session's projection values. Framework semantics, uniform across every
677
691
  * key: a baseline seeds rows at its cut, a push frame updates one row, and in
678
- * both paths a lower-or-equal seq loses — a replayed frame cannot regress a
679
- * value, a stale baseline cannot overwrite a newer frame. A key the store has
680
- * never seen reads `undefined` (capability absent). Faces are identity-stable
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
681
695
  * per key (create-on-demand, cached) so the React side binds each exactly
682
696
  * once; the store-level channel (`subscribeAny`) serves coarse consumers (the
683
697
  * manager's list projection reads the `title` key).
@@ -757,16 +771,9 @@ window.__ModuleLoader__.load({
757
771
  this.changed(key);
758
772
  }
759
773
  }
760
- /**
761
- * Drop rows beyond a replacement control baseline. Such rows describe
762
- * process state the Host lost before persisting it and would otherwise
763
- * outrank recomputed lower-seq values forever. The caller seeds the new
764
- * baseline immediately afterward.
765
- * @param lastSeq - highest durable sequence reflected by the baseline.
766
- */
767
- truncate(lastSeq) {
768
- for (const [key, row] of this.rows) {
769
- if (row.seq <= lastSeq) continue;
774
+ /** Discard one Host generation's values and watermarks while preserving subscribed faces. */
775
+ clear() {
776
+ for (const key of this.rows.keys()) {
770
777
  this.rows.delete(key);
771
778
  this.changed(key);
772
779
  }
@@ -938,7 +945,7 @@ window.__ModuleLoader__.load({
938
945
  };
939
946
  //#endregion
940
947
  //#region lib/types/client/time-zone.js
941
- /** Browser-owned time-zone sampling for prompt RPC provenance. */
948
+ /** Browser-owned time-zone sampling for one prompt RPC. */
942
949
  /**
943
950
  * Resolve the current browser IANA zone for one outbound operation.
944
951
  * @returns The browser-provided canonical zone.
@@ -950,60 +957,6 @@ window.__ModuleLoader__.load({
950
957
  return timeZone;
951
958
  }
952
959
  //#endregion
953
- //#region lib/types/client/sessions/queue-mirror.js
954
- const QUEUE_PREVIEW_CHARS = 200;
955
- function previewOf(content) {
956
- const flat = content.filter((block) => block.type !== "image" && block.type !== "file").map((block) => block.type === "text" ? block.text : `[${block.type}]`).join(" ").replace(/\s+/g, " ").trim();
957
- const chars = Array.from(flat);
958
- return chars.length > QUEUE_PREVIEW_CHARS ? `${chars.slice(0, QUEUE_PREVIEW_CHARS).join("")}…` : flat;
959
- }
960
- function textOf(content) {
961
- if (!content.every((block) => block.type === "text")) return null;
962
- return content.map((block) => block.text).join("");
963
- }
964
- /** Authoritative transient queue projection and durable steering handoff. */
965
- var SessionQueueMirror = class {
966
- current = [];
967
- /**
968
- * Return the current immutable queue projection.
969
- * @returns current queue rows.
970
- */
971
- snapshot() {
972
- return this.current;
973
- }
974
- /**
975
- * Replace from one authoritative stream queue frame.
976
- * @param items - complete host queue snapshot.
977
- */
978
- replace(items) {
979
- this.current = items.map((item) => {
980
- const content = item.message.content;
981
- return {
982
- id: item.id,
983
- messageId: item.message.id,
984
- placement: item.placement,
985
- ...item.rpcId === void 0 ? {} : { rpcId: item.rpcId },
986
- content,
987
- preview: previewOf(content),
988
- text: textOf(content)
989
- };
990
- });
991
- }
992
- /**
993
- * Retire a transient steering row once its durable message enters the log.
994
- * @param event - newly contiguous durable Session event.
995
- * @returns whether the projection changed.
996
- */
997
- acceptDurable(event) {
998
- if (event.type !== "user/message") return false;
999
- const messageId = event.data.id;
1000
- const index = this.current.findIndex((item) => item.placement === "steering" && item.messageId === messageId);
1001
- if (index < 0) return false;
1002
- this.current = this.current.filter((_item, candidate) => candidate !== index);
1003
- return true;
1004
- }
1005
- };
1006
- //#endregion
1007
960
  //#region ../../util/values/lib/index.js
1008
961
  /** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
1009
962
  function hasIntrinsicConstructor(prototype, name) {
@@ -1526,8 +1479,7 @@ window.__ModuleLoader__.load({
1526
1479
  jumpTargetSeq = null;
1527
1480
  /** The running jump loop's completion, shared by retargeting callers. */
1528
1481
  jumpPromise = null;
1529
- /** Authoritative stream-only inbox snapshot; pending work never hits history. */
1530
- queueMirror = new SessionQueueMirror();
1482
+ stopObservingInbox;
1531
1483
  assistantStream = new ClientAssistantStream();
1532
1484
  running = false;
1533
1485
  address;
@@ -1548,7 +1500,7 @@ window.__ModuleLoader__.load({
1548
1500
  /** Local submission echoes, insertion-ordered (see SessionSnapshot.pendingSubmissions). */
1549
1501
  pendingSubmissions = [];
1550
1502
  /** Per-echo settlement state; `retiring` latches the first observation so a
1551
- * queue frame and its durable event cannot both retire one echo. */
1503
+ * Inbox projection and its durable event cannot both retire one echo. */
1552
1504
  submissionSettlements = /* @__PURE__ */ new Map();
1553
1505
  /** Owns the addressed page/follow lifecycle while this Session is open. */
1554
1506
  events;
@@ -1593,6 +1545,9 @@ window.__ModuleLoader__.load({
1593
1545
  this.snapshotCache = this.buildSnapshot();
1594
1546
  });
1595
1547
  this.snapshotCache = this.buildSnapshot();
1548
+ this.stopObservingInbox = this.projections.faceOf("inbox").subscribe(() => {
1549
+ this.observeSubmissionInbox();
1550
+ });
1596
1551
  }
1597
1552
  /**
1598
1553
  * Bind the Agent-scoped context minted by ClientSessions (single write;
@@ -1853,7 +1808,7 @@ window.__ModuleLoader__.load({
1853
1808
  return this.jumpPromise;
1854
1809
  }
1855
1810
  /** Rebuild an opened history source after address replacement.
1856
- * Invalidates any in-flight open first; queue state belongs to the independently
1811
+ * Invalidates any in-flight open first; projection state belongs to the independently
1857
1812
  * reconnecting control stream and remains untouched. */
1858
1813
  async resync() {
1859
1814
  if (this.openState === "cold") return;
@@ -1885,24 +1840,6 @@ window.__ModuleLoader__.load({
1885
1840
  return this.snapshotCache;
1886
1841
  }
1887
1842
  /**
1888
- * Replace every transient control value for this Session from one stream baseline.
1889
- * @param queue - complete pending queue for this Session.
1890
- */
1891
- replaceControl(queue) {
1892
- this.queueMirror.replace(queue);
1893
- this.observeSubmissionQueue(queue);
1894
- this.notifier.markDirty();
1895
- }
1896
- /**
1897
- * Apply one Session-addressed live control update.
1898
- * @param frame - queue replacement addressed to this Session.
1899
- */
1900
- handleControlFrame(frame) {
1901
- this.queueMirror.replace(frame.items);
1902
- this.observeSubmissionQueue(frame.items);
1903
- this.notifier.markDirty();
1904
- }
1905
- /**
1906
1843
  * Running-bit relay from the host stream (list entry and snapshot stay consistent).
1907
1844
  * @param running - the new running state.
1908
1845
  */
@@ -1969,6 +1906,7 @@ window.__ModuleLoader__.load({
1969
1906
  * @returns when the Remote iterator has completed teardown.
1970
1907
  */
1971
1908
  async dispose() {
1909
+ this.stopObservingInbox();
1972
1910
  for (const requestId of [...this.submissionSettlements.keys()]) this.retireFailedSubmission(requestId);
1973
1911
  this.openGeneration++;
1974
1912
  const events = this.events;
@@ -2066,23 +2004,36 @@ window.__ModuleLoader__.load({
2066
2004
  const event = entry.event;
2067
2005
  const awaitingFirstTurn = this.firstPromptPendingTurn;
2068
2006
  if (event.type === "turn/start") this.firstPromptPendingTurn = false;
2069
- const queueChanged = this.queueMirror.acceptDurable(event);
2070
2007
  this.eventSource.append(entry);
2071
2008
  this.observeSubmissionEvent(event);
2072
- return queueChanged || awaitingFirstTurn !== this.firstPromptPendingTurn;
2009
+ return awaitingFirstTurn !== this.firstPromptPendingTurn;
2073
2010
  }
2074
- /** Retire the matching echo when a durable browser-prompt `user/message` becomes visible. */
2011
+ /** Observe durable acceptance even when insertion and claim share one projection notification. */
2075
2012
  observeSubmissionEvent(event) {
2076
- if (this.submissionSettlements.size === 0 || event.type !== "user/message") return;
2013
+ if (this.submissionSettlements.size === 0) return;
2014
+ 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
+ });
2020
+ return;
2021
+ }
2022
+ if (event.type !== "user/message") return;
2077
2023
  const data = event.data;
2078
2024
  const source = data?.source;
2079
2025
  if (source?.kind !== "user" || typeof source.rpcId !== "string") return;
2080
2026
  this.scheduleObservedRetirement(source.rpcId, attachmentRefsIn(data?.content));
2081
2027
  }
2082
- /** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
2083
- observeSubmissionQueue(items) {
2028
+ /** Retire local echoes when their accepted messages appear in the durable Inbox projection. */
2029
+ observeSubmissionInbox() {
2084
2030
  if (this.submissionSettlements.size === 0) return;
2085
- for (const item of items) if (item.rpcId !== void 0) this.scheduleObservedRetirement(item.rpcId, attachmentRefsIn(item.message.content));
2031
+ const inbox = this.projections.get("inbox");
2032
+ 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
+ }
2086
2037
  }
2087
2038
  /**
2088
2039
  * Latch one observed settlement and remove the echo an animation frame
@@ -2133,7 +2084,6 @@ window.__ModuleLoader__.load({
2133
2084
  buildSnapshot() {
2134
2085
  return {
2135
2086
  sessionId: this.sessionId,
2136
- queue: this.queueMirror.snapshot(),
2137
2087
  pendingSubmissions: this.pendingSubmissions,
2138
2088
  running: this.running,
2139
2089
  subagent: this.address === void 0 ? null : {
@@ -2182,6 +2132,7 @@ window.__ModuleLoader__.load({
2182
2132
  }
2183
2133
  //#endregion
2184
2134
  //#region lib/types/client/sessions/manager.js
2135
+ /** Host catalog, durable projection caches, and explicitly retained Client instances. */
2185
2136
  function sessionSeqCursor(value) {
2186
2137
  return value === -1 ? -1 : SessionSeq(value);
2187
2138
  }
@@ -2194,16 +2145,6 @@ window.__ModuleLoader__.load({
2194
2145
  sessions = /* @__PURE__ */ new Map();
2195
2146
  /** In-flight Session disposals remain here after instances leave `sessions`, so manager disposal can await quiescence. */
2196
2147
  sessionDisposals = /* @__PURE__ */ new Set();
2197
- /** Latest transient queues, retained independently of Session object materialization. */
2198
- queues = /* @__PURE__ */ new Map();
2199
- /**
2200
- * Sessions that finished running while not selected — the sidebar's green
2201
- * "done" reminder (manager-owned, survives connection generations; cleared
2202
- * on select and session-removed, re-armed by the next completion).
2203
- */
2204
- completedNotifications = /* @__PURE__ */ new Set();
2205
- /** Last-observed running bits per session; the true→false edge here arms {@link completedNotifications}. */
2206
- prevRunning = /* @__PURE__ */ new Map();
2207
2148
  /** Per-session projection value stores, retained independently of instance arrival (the
2208
2149
  * title-snapshot precedent, generalized): push frames land here whether or not the Session
2209
2150
  * is instantiated (list rows read the 'title' key), and an instantiated Session adopts the
@@ -2215,7 +2156,7 @@ window.__ModuleLoader__.load({
2215
2156
  listPhase = "pending";
2216
2157
  listError = null;
2217
2158
  listInflight = null;
2218
- /** Mutations arriving after a list request starts are replayed over its response. */
2159
+ /** Active list request's mutation log; its identity also fences completion after reconnect. */
2219
2160
  listMutations = null;
2220
2161
  addresses = /* @__PURE__ */ new Map();
2221
2162
  catalogs = /* @__PURE__ */ new Map();
@@ -2230,7 +2171,6 @@ window.__ModuleLoader__.load({
2230
2171
  * one representation.
2231
2172
  */
2232
2173
  jobsBySession = /* @__PURE__ */ new Map();
2233
- selected;
2234
2174
  listSnapshotCache;
2235
2175
  /** Entry-identity cache (reference stability): list rebuilds reuse the previous entry
2236
2176
  * object when every field matches — wire refreshes mint all-new summary objects, so identity
@@ -2240,49 +2180,23 @@ window.__ModuleLoader__.load({
2240
2180
  notifier = new Notifier(() => {
2241
2181
  this.listSnapshotCache = this.buildListSnapshot();
2242
2182
  });
2243
- /**
2244
- * @param remote - generated Remote namespaces the Session cluster calls.
2245
- * @param restoredSelection - persisted real-Session selection candidate.
2246
- */
2247
- constructor(remote, restoredSelection, restoredAddress) {
2183
+ /** @param remote - generated Remote namespaces used by catalog and history readers. */
2184
+ constructor(remote) {
2248
2185
  this.remote = remote;
2249
- this.selected = restoredSelection;
2250
- if (restoredAddress !== void 0) this.addresses.set(restoredAddress.childSessionId, restoredAddress);
2251
2186
  this.listSnapshotCache = this.buildListSnapshot();
2252
2187
  }
2253
2188
  /**
2254
- * Select a listed Session or a retained catalog-addressed child.
2255
- * @param sessionId - listed or catalog-addressed Session id.
2256
- */
2257
- select(sessionId) {
2258
- const address = this.navigationAddress(sessionId);
2259
- if (!this.summaries.some((summary) => summary.sessionId === sessionId) && address === void 0) throw new Error(`sessions.select: unknown session ${sessionId}`);
2260
- if (address !== void 0) this.addresses.set(sessionId, address);
2261
- this.sessions.get(sessionId)?.configureSubagent(address, address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.parentAvailable);
2262
- this.selected = sessionId;
2263
- this.completedNotifications.delete(sessionId);
2264
- this.refreshSubagents(sessionId);
2265
- this.notifier.notifyNow();
2266
- }
2267
- /**
2268
- * Select a healthy child through its durable direct-parent address.
2269
- * @param address - catalog-derived parent and child ids.
2189
+ * Resolve an acquisition target without materializing a Session.
2190
+ * @param target - known identity or durable direct-parent address.
2191
+ * @returns the resolved identity with its explicit or catalog-derived history route installed.
2270
2192
  */
2271
- selectSubagent(address) {
2272
- const catalog = this.catalogs.get(address.parentSessionId);
2273
- const entry = catalog?.entries.find((candidate) => candidate.id === address.childSessionId);
2274
- if (entry === void 0 || entry.kind !== "child" || entry.mode !== address.mode) throw new Error(`sessions.selectSubagent: ${address.childSessionId} is not a healthy catalog child`);
2275
- this.addresses.set(address.childSessionId, address);
2276
- this.sessions.get(address.childSessionId)?.configureSubagent(address, catalog?.parentAvailable);
2277
- this.selected = address.childSessionId;
2278
- this.completedNotifications.delete(address.childSessionId);
2279
- this.refreshSubagents(address.childSessionId);
2280
- this.notifier.notifyNow();
2281
- }
2282
- /** Clear the selection (the layout falls to the no-session view state). */
2283
- clearSelection() {
2284
- this.selected = void 0;
2285
- this.notifier.notifyNow();
2193
+ resolveTarget(target) {
2194
+ const id = typeof target === "string" ? target : target.childSessionId;
2195
+ const address = typeof target === "string" ? this.navigationAddress(id) : target;
2196
+ 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
+ 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);
2199
+ return id;
2286
2200
  }
2287
2201
  /**
2288
2202
  * Return the durable catalog address retained for one child.
@@ -2290,7 +2204,7 @@ window.__ModuleLoader__.load({
2290
2204
  * @returns The direct-parent address, when navigation discovered one.
2291
2205
  */
2292
2206
  subagentAddress(sessionId) {
2293
- return this.addresses.get(sessionId);
2207
+ return this.navigationAddress(sessionId);
2294
2208
  }
2295
2209
  /**
2296
2210
  * Resolve an address for breadcrumb navigation without retaining transport authority.
@@ -2310,19 +2224,21 @@ window.__ModuleLoader__.load({
2310
2224
  }
2311
2225
  }
2312
2226
  /**
2313
- * Drop a session instance (scope-prune companion: instance
2314
- * and scope share one lifecycle). The host session log is the durable
2315
- * truth — a later get() lazily rebuilds and open() backfills history.
2316
- * @param sessionId - the session to drop.
2227
+ * Withdraw an exact Client instance before running its teardown callbacks.
2228
+ * @param sessionId - identity to withdraw.
2229
+ * @param expected - instance being released; a replacement is left untouched.
2230
+ * @returns completion of the detached instance's stream teardown.
2317
2231
  */
2318
- async drop(sessionId) {
2232
+ drop(sessionId, expected) {
2319
2233
  const session = this.sessions.get(sessionId);
2234
+ if (session !== expected) return Promise.resolve();
2320
2235
  this.sessions.delete(sessionId);
2321
- if (session !== void 0) await this.startSessionDisposal(session);
2236
+ this.addresses.delete(sessionId);
2237
+ return this.startSessionDisposal(session);
2322
2238
  }
2323
2239
  /**
2324
- * Stop owned timers and every remaining Session instance.
2325
- * @returns when every Session Remote iterator has completed teardown.
2240
+ * Stop catalog requests and dispose every resident Session.
2241
+ * @returns once catalog requests and every Session stream have stopped.
2326
2242
  */
2327
2243
  async dispose() {
2328
2244
  for (const timer of this.catalogDebounce.values()) clearTimeout(timer);
@@ -2331,6 +2247,7 @@ window.__ModuleLoader__.load({
2331
2247
  this.openCatalogs.clear();
2332
2248
  const sessions = [...this.sessions.values()];
2333
2249
  this.sessions.clear();
2250
+ this.addresses.clear();
2334
2251
  for (const session of sessions) this.startSessionDisposal(session);
2335
2252
  await this.drainSessionDisposals();
2336
2253
  }
@@ -2349,7 +2266,7 @@ window.__ModuleLoader__.load({
2349
2266
  }
2350
2267
  /**
2351
2268
  * Lazy build: return the existing instance or construct one (no auto-open —
2352
- * open is triggered by the container's select callback).
2269
+ * the reference allocator opens history after binding the scope).
2353
2270
  * @param sessionId - the session to get.
2354
2271
  * @returns the resident instance.
2355
2272
  */
@@ -2358,7 +2275,6 @@ window.__ModuleLoader__.load({
2358
2275
  if (session === void 0) {
2359
2276
  session = this.createSession(sessionId);
2360
2277
  this.sessions.set(sessionId, session);
2361
- session.replaceControl(this.queues.get(sessionId) ?? []);
2362
2278
  const summary = this.summaries.find((s) => s.sessionId === sessionId);
2363
2279
  if (summary !== void 0) {
2364
2280
  session.handleBlank(summary.blank);
@@ -2482,7 +2398,7 @@ window.__ModuleLoader__.load({
2482
2398
  }
2483
2399
  }
2484
2400
  }
2485
- /** Full refresh via session.list (single-flight: an in-flight call is reused). */
2401
+ /** Full refresh via session.list (single-flight within one Host generation). */
2486
2402
  refreshList() {
2487
2403
  if (this.listInflight !== null) return this.listInflight;
2488
2404
  this.listState = "loading";
@@ -2494,19 +2410,12 @@ window.__ModuleLoader__.load({
2494
2410
  this.listInflight = (async () => {
2495
2411
  try {
2496
2412
  const result = await this.remote.session.list({});
2413
+ if (this.listMutations !== mutations) return;
2497
2414
  if (result.ok) {
2498
2415
  const baseline = this.listPhase === "pending" ? [...result.value.items] : mergeOrderedBaseline(established, result.value.items, (summary) => summary.sessionId);
2499
- for (const s of baseline) if (!this.prevRunning.has(s.sessionId)) this.prevRunning.set(s.sessionId, s.running);
2500
- let summaries = baseline;
2501
- for (const mutation of mutations) {
2502
- summaries = applyMutation(summaries, mutation);
2503
- this.summaries = summaries;
2504
- this.syncCompletedNotifications();
2505
- }
2506
- this.summaries = summaries;
2416
+ this.summaries = mutations.reduce(applyMutation, baseline);
2507
2417
  this.listState = "idle";
2508
2418
  this.listPhase = "ready";
2509
- this.syncCompletedNotifications();
2510
2419
  for (const s of this.summaries) {
2511
2420
  const session = this.sessions.get(s.sessionId);
2512
2421
  if (session === void 0) continue;
@@ -2526,12 +2435,15 @@ window.__ModuleLoader__.load({
2526
2435
  }
2527
2436
  } catch (error) {
2528
2437
  if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) throw error;
2438
+ if (this.listMutations !== mutations) return;
2529
2439
  this.listState = "error";
2530
2440
  this.listError = error;
2531
2441
  } finally {
2532
- this.listMutations = null;
2533
- this.listInflight = null;
2534
- this.notifier.markDirty();
2442
+ if (this.listMutations === mutations) {
2443
+ this.listMutations = null;
2444
+ this.listInflight = null;
2445
+ this.notifier.markDirty();
2446
+ }
2535
2447
  }
2536
2448
  })();
2537
2449
  return this.listInflight;
@@ -2640,7 +2552,6 @@ window.__ModuleLoader__.load({
2640
2552
  recordMutation(mutation) {
2641
2553
  this.listMutations?.push(mutation);
2642
2554
  this.summaries = applyMutation(this.summaries, mutation);
2643
- this.syncCompletedNotifications();
2644
2555
  this.notifier.markDirty();
2645
2556
  }
2646
2557
  /**
@@ -2660,6 +2571,14 @@ window.__ModuleLoader__.load({
2660
2571
  return this.listSnapshotCache;
2661
2572
  }
2662
2573
  /**
2574
+ * Read cached projection values for a Session that may exist only in a loaded subagent catalog.
2575
+ * @param sessionId - Session whose control or history baseline supplied projections.
2576
+ * @returns current values, or undefined before any projection store exists.
2577
+ */
2578
+ projectionValues(sessionId) {
2579
+ return this.projectionStores.get(sessionId)?.values();
2580
+ }
2581
+ /**
2663
2582
  * Apply a complete control baseline or one later replacement frame.
2664
2583
  * @param frame - baseline or live control replacement from Session Controller.
2665
2584
  */
@@ -2673,30 +2592,21 @@ window.__ModuleLoader__.load({
2673
2592
  this.notifier.markDirty();
2674
2593
  return;
2675
2594
  }
2676
- if (frame.type === "jobs") {
2677
- if (frame.jobs.length === 0) this.jobsBySession.delete(frame.sessionId);
2678
- else this.jobsBySession.set(frame.sessionId, frame.jobs);
2679
- this.notifier.markDirty();
2680
- return;
2681
- }
2682
- this.queues.set(frame.sessionId, frame.items);
2683
- this.sessions.get(frame.sessionId)?.handleControlFrame(frame);
2595
+ if (frame.jobs.length === 0) this.jobsBySession.delete(frame.sessionId);
2596
+ else this.jobsBySession.set(frame.sessionId, frame.jobs);
2597
+ this.notifier.markDirty();
2684
2598
  }
2685
2599
  replaceControlBaseline(baseline) {
2686
- this.queues.clear();
2687
- for (const [sessionId, items] of Object.entries(baseline.queues)) this.queues.set(sessionId, items);
2688
2600
  this.jobsBySession.clear();
2689
2601
  for (const [sessionId, jobs] of Object.entries(baseline.jobs)) if (jobs.length > 0) this.jobsBySession.set(sessionId, jobs);
2690
2602
  for (const [sessionId, block] of Object.entries(baseline.projections)) {
2691
2603
  const store = this.projectionStore(sessionId);
2692
2604
  const asOfSeq = sessionSeqCursor(block.asOfSeq);
2693
- store.truncate(asOfSeq);
2694
2605
  store.seed({
2695
2606
  ...block,
2696
2607
  asOfSeq
2697
2608
  });
2698
2609
  }
2699
- for (const [sessionId, session] of this.sessions) session.replaceControl(this.queues.get(sessionId) ?? []);
2700
2610
  this.notifier.markDirty();
2701
2611
  }
2702
2612
  /**
@@ -2712,7 +2622,7 @@ window.__ModuleLoader__.load({
2712
2622
  for (const [key, value] of Object.entries(projections.values)) store.apply(key, value, sessionSeqCursor(projections.asOfSeq));
2713
2623
  }
2714
2624
  if (summary.origin === "subagent" && summary.parentSessionId !== void 0) this.markCatalogParentExpandable(summary.parentSessionId);
2715
- if (summary.parentSessionId !== void 0 && (this.selected === summary.parentSessionId || this.openCatalogs.has(summary.parentSessionId))) this.scheduleCatalogRefresh(summary.parentSessionId);
2625
+ if (summary.parentSessionId !== void 0 && this.openCatalogs.has(summary.parentSessionId)) this.scheduleCatalogRefresh(summary.parentSessionId);
2716
2626
  }
2717
2627
  /**
2718
2628
  * Apply one Session removal forwarded through `ctx.remote.$on`.
@@ -2731,7 +2641,6 @@ window.__ModuleLoader__.load({
2731
2641
  this.updateCatalogActivity(sessionId, false);
2732
2642
  if (durableSubagent) this.sessions.get(sessionId)?.handleRunning(false);
2733
2643
  else this.sessions.get(sessionId)?.handleRemoved();
2734
- this.queues.delete(sessionId);
2735
2644
  this.jobsBySession.delete(sessionId);
2736
2645
  if (!durableSubagent) this.projectionStores.delete(sessionId);
2737
2646
  const inflightCatalog = this.catalogInflight.get(sessionId);
@@ -2782,16 +2691,23 @@ window.__ModuleLoader__.load({
2782
2691
  }
2783
2692
  /**
2784
2693
  * Repair one re-established Host-event generation with queryable baselines.
2694
+ * Discard old projection cuts before new queries, including cold Sessions
2695
+ * absent from the process-local control baseline.
2785
2696
  * Opened Session follow streams resume independently through API Gateway.
2786
2697
  */
2787
2698
  handleConnected() {
2699
+ for (const store of this.projectionStores.values()) store.clear();
2700
+ this.listMutations = null;
2701
+ this.listInflight = null;
2788
2702
  this.refreshList();
2789
- const selectedAddress = this.selected === void 0 ? void 0 : this.addresses.get(this.selected);
2790
- if (selectedAddress !== void 0) this.refreshSubagents(selectedAddress.parentSessionId);
2791
- if (this.selected !== void 0) this.refreshSubagents(this.selected);
2792
- for (const parentSessionId of this.openCatalogs) this.refreshSubagents(parentSessionId);
2703
+ const parents = new Set(this.openCatalogs);
2704
+ for (const id of this.sessions.keys()) {
2705
+ const address = this.addresses.get(id);
2706
+ if (address !== void 0) parents.add(address.parentSessionId);
2707
+ }
2708
+ for (const parentSessionId of parents) this.refreshSubagents(parentSessionId);
2793
2709
  }
2794
- /** Debounce membership refetches while one parent catalog is selected or open. */
2710
+ /** Debounce membership refetches for an explicitly consumed catalog. */
2795
2711
  scheduleCatalogRefresh(parentSessionId) {
2796
2712
  if (this.catalogDebounce.has(parentSessionId)) return;
2797
2713
  const timer = setTimeout(() => {
@@ -2864,31 +2780,6 @@ window.__ModuleLoader__.load({
2864
2780
  };
2865
2781
  });
2866
2782
  }
2867
- /**
2868
- * Reconcile completion reminders against the latest summaries, eagerly after
2869
- * every mutation and pull (a snapshot-build-time pass would collapse
2870
- * consecutive status frames into one observation). A running→idle edge of a
2871
- * non-selected session arms its reminder; running disarms it; removal drops
2872
- * it. First observation only records the running bit — sessions already
2873
- * idle at load get no reminder.
2874
- */
2875
- syncCompletedNotifications() {
2876
- const seen = /* @__PURE__ */ new Set();
2877
- for (const s of this.summaries) {
2878
- seen.add(s.sessionId);
2879
- const prev = this.prevRunning.get(s.sessionId);
2880
- if (prev === void 0) {
2881
- this.prevRunning.set(s.sessionId, s.running);
2882
- continue;
2883
- }
2884
- if (prev && !s.running) {
2885
- if (s.sessionId !== this.selected) this.completedNotifications.add(s.sessionId);
2886
- } else if (s.running) this.completedNotifications.delete(s.sessionId);
2887
- this.prevRunning.set(s.sessionId, s.running);
2888
- }
2889
- for (const id of this.prevRunning.keys()) if (!seen.has(id)) this.prevRunning.delete(id);
2890
- for (const id of this.completedNotifications) if (!seen.has(id)) this.completedNotifications.delete(id);
2891
- }
2892
2783
  buildListSnapshot() {
2893
2784
  const items = flattenLineage(this.summaries.map((summary) => {
2894
2785
  const projectionStore = this.projectionStores.get(summary.sessionId);
@@ -2899,25 +2790,22 @@ window.__ModuleLoader__.load({
2899
2790
  ...typeof title === "string" && title !== "" ? { title } : {},
2900
2791
  ...projectionValues === void 0 ? {} : { projectionValues }
2901
2792
  };
2902
- }), this.completedNotifications).map((entry) => {
2793
+ })).map((entry) => {
2903
2794
  const prev = this.entryCache.get(entry.sessionId);
2904
- if (prev !== void 0 && prev.updatedAt === entry.updatedAt && prev.running === entry.running && prev.blank === entry.blank && prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd && prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth && prev.projectionValues === entry.projectionValues && prev.completed === entry.completed) return prev;
2795
+ if (prev !== void 0 && prev.updatedAt === entry.updatedAt && prev.running === entry.running && prev.blank === entry.blank && prev.parentSessionId === entry.parentSessionId && prev.cwd === entry.cwd && prev.origin === entry.origin && prev.title === entry.title && prev.depth === entry.depth && prev.projectionValues === entry.projectionValues) return prev;
2905
2796
  this.entryCache.set(entry.sessionId, entry);
2906
2797
  return entry;
2907
2798
  });
2908
- for (const id of this.entryCache.keys()) if (!items.some((e) => e.sessionId === id)) this.entryCache.delete(id);
2799
+ const itemIds = new Set(items.map((entry) => entry.sessionId));
2800
+ for (const id of this.entryCache.keys()) if (!itemIds.has(id)) this.entryCache.delete(id);
2909
2801
  if (!(items.length === this.itemsCache.length && items.every((e, i) => e === this.itemsCache[i]))) this.itemsCache = items;
2910
- const selected = this.selected;
2911
- const current = selected !== void 0 && (items.some((item) => item.sessionId === selected) || this.addresses.has(selected)) ? selected : void 0;
2912
2802
  return {
2913
2803
  items: this.itemsCache,
2914
- current,
2915
2804
  state: this.listState,
2916
2805
  phase: this.listPhase,
2917
2806
  error: this.listError,
2918
2807
  subagentsByParent: Object.fromEntries(this.catalogs),
2919
- jobsBySession: Object.fromEntries(this.jobsBySession),
2920
- currentAddress: current === void 0 ? void 0 : this.addresses.get(current)
2808
+ jobsBySession: Object.fromEntries(this.jobsBySession)
2921
2809
  };
2922
2810
  }
2923
2811
  };
@@ -3014,7 +2902,74 @@ window.__ModuleLoader__.load({
3014
2902
  if (fullWidth?.[1] !== void 0 && fullWidth[2] !== void 0) return `${fullWidth[1]}(${BigInt(fullWidth[2]) + 1n})`;
3015
2903
  return `${title} (1)`;
3016
2904
  }
3017
- /** Root sessions service: list store, current selection, object-layer manager, scope tree, bindings, and breadcrumb routes. */
2905
+ /** Source labels are dictionary keys, including names also present on Object.prototype. */
2906
+ function freezeRetainedBy(counts) {
2907
+ Object.setPrototypeOf(counts, null);
2908
+ return Object.freeze(counts);
2909
+ }
2910
+ const EMPTY_RETAIN_INFO = Object.freeze({
2911
+ referenceCount: 0,
2912
+ retainedBy: freezeRetainedBy({})
2913
+ });
2914
+ /** A cancelled waiter releases only its own reference, not the shared opening. */
2915
+ async function waitForOpen(opening, signal) {
2916
+ if (signal === void 0) return opening;
2917
+ const aborted = Promise.withResolvers();
2918
+ const onAbort = () => {
2919
+ aborted.reject(signal.reason);
2920
+ };
2921
+ signal.addEventListener("abort", onAbort, { once: true });
2922
+ try {
2923
+ if (signal.aborted) onAbort();
2924
+ await Promise.race([opening, aborted.promise]);
2925
+ } finally {
2926
+ signal.removeEventListener("abort", onAbort);
2927
+ }
2928
+ }
2929
+ var ClientSessionReference = class {
2930
+ sessionId;
2931
+ record;
2932
+ releaseReference;
2933
+ released = new AbortController();
2934
+ readiness = Promise.withResolvers();
2935
+ ready = this.readiness.promise;
2936
+ constructor(sessionId, record, releaseReference) {
2937
+ this.sessionId = sessionId;
2938
+ this.record = record;
2939
+ this.releaseReference = releaseReference;
2940
+ this.ready.catch(() => {});
2941
+ }
2942
+ get binding() {
2943
+ if (this.record === void 0 || !this.record.live) throw new Error(`Session reference "${this.sessionId}" is released`);
2944
+ return this.record.binding;
2945
+ }
2946
+ attachOpening(opening, signal) {
2947
+ const waitSignal = signal === void 0 ? this.released.signal : AbortSignal.any([this.released.signal, signal]);
2948
+ waitForOpen(opening, waitSignal).then(() => {
2949
+ try {
2950
+ waitSignal.throwIfAborted();
2951
+ this.readiness.resolve(this.binding);
2952
+ } catch (error) {
2953
+ this.readiness.reject(error);
2954
+ }
2955
+ }, (error) => {
2956
+ this.readiness.reject(error);
2957
+ });
2958
+ }
2959
+ release() {
2960
+ const reason = /* @__PURE__ */ new Error(`Session reference "${this.sessionId}" is released`);
2961
+ const release = this.releaseReference;
2962
+ this.released.abort(reason);
2963
+ this.readiness.reject(reason);
2964
+ this.record = void 0;
2965
+ this.releaseReference = void 0;
2966
+ release?.();
2967
+ }
2968
+ [Symbol.dispose]() {
2969
+ this.release();
2970
+ }
2971
+ };
2972
+ /** Host catalog and local reference allocator; view selection remains outside the Controller. */
3018
2973
  var ClientSessions = class {
3019
2974
  rootCtx;
3020
2975
  /**
@@ -3024,81 +2979,94 @@ window.__ModuleLoader__.load({
3024
2979
  * reports the same number.
3025
2980
  */
3026
2981
  searchResultLimit = 20;
3027
- /** List snapshot store (list RPC + host stream increments; re-pulled on reconnect) — the useSessions standard feed, current included. */
2982
+ /** Catalog metadata and local reference-source projection. */
3028
2983
  list;
3029
2984
  /** The object-layer instance cluster and frame dispatch entry. */
3030
2985
  manager;
3031
- /**
3032
- * Persisted selection cell (the durable half of `list.current`). Private on
3033
- * purpose: reads go through the list snapshot; writes through {@link
3034
- * ClientSessions.open} / {@link ClientSessions.clear}. Projection
3035
- * validates it against the live list instead of destructively pruning, so a
3036
- * selection survives transient list states (reconnect re-pull) and
3037
- * resurfaces when its session returns.
3038
- */
3039
- selection;
3040
2986
  scopes = /* @__PURE__ */ new Map();
3041
- /** In-flight scope drops remain here after records leave `scopes`, so root disposal can await quiescence. */
2987
+ /** Stable per-id sources retained for the Client root lifetime, including across generation replacement. */
2988
+ retainObservers = /* @__PURE__ */ new Map();
3042
2989
  scopeDrops = /* @__PURE__ */ new Set();
3043
- /**
3044
- * The staged session id — follows `list.current` exactly, holding its last
3045
- * defined value across masked gaps (a transiently absent selection blanks
3046
- * `current` without moving the stage, so reconnect re-pulls and removals
3047
- * keep the staged scope's frozen view alive until the stage moves on).
3048
- */
3049
- watched;
3050
- /** Removed-while-staged sessions whose teardown waits for the stage to move away. */
3051
- deferredRemovals = /* @__PURE__ */ new Set();
2990
+ closed = false;
3052
2991
  /**
3053
2992
  * @param ctx - client root context (scope fibers mount under it).
3054
2993
  * @param remote - generated Remote namespaces shared with every Session.
3055
2994
  */
3056
2995
  constructor(rootCtx, remote) {
3057
2996
  this.rootCtx = rootCtx;
3058
- this.selection = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({}, { persist: { name: "dsh.sessions.current" } });
3059
- const restored = this.selection.getSnapshot();
3060
- this.manager = new SessionManager(remote, restored.sessionId, restored.subagentAddress);
2997
+ this.manager = new SessionManager(remote);
3061
2998
  this.list = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
3062
2999
  ids: [],
3063
3000
  byId: {},
3064
- current: void 0,
3065
3001
  phase: "pending",
3066
3002
  subagentsByParent: {},
3067
- jobsBySession: {},
3068
- currentAddress: void 0
3003
+ jobsBySession: {}
3069
3004
  });
3070
3005
  const disposeManagerProjection = this.manager.subscribe(() => {
3071
3006
  this.projectList();
3072
3007
  });
3073
- const disposeStageFollower = this.list.subscribe(() => {
3074
- this.followCurrent();
3075
- });
3076
3008
  rootCtx.effect(() => async () => {
3077
- disposeStageFollower();
3009
+ this.closed = true;
3078
3010
  disposeManagerProjection();
3079
3011
  const scopes = [...this.scopes];
3080
3012
  this.scopes.clear();
3081
- this.deferredRemovals.clear();
3082
- this.watched = void 0;
3083
- for (const [id, record] of scopes) this.startScopeDrop(id, record);
3013
+ for (const [, record] of scopes) {
3014
+ record.live = false;
3015
+ record.session.unbindScope();
3016
+ }
3017
+ const managerDisposal = this.manager.dispose();
3018
+ for (const [id, record] of scopes) {
3019
+ this.startScopeDrop(id, record);
3020
+ this.publishRetention(id);
3021
+ }
3084
3022
  await this.drainScopeDrops();
3085
- await this.manager.dispose();
3023
+ await managerDisposal;
3086
3024
  }, "session-controller.client.sessions");
3087
3025
  rootCtx.reflect.provide("sessions", this, void 0);
3088
3026
  }
3089
- /**
3090
- * Select a listed or retained catalog-addressed session as current.
3091
- * @param id - listed or addressed session id.
3092
- */
3093
- open(id) {
3094
- this.manager.select(id);
3027
+ retain(target, options) {
3028
+ const { source, signal } = options;
3029
+ signal?.throwIfAborted();
3030
+ if (this.closed) throw new Error("Session Controller is disposed");
3031
+ const id = this.manager.resolveTarget(target);
3032
+ const reference = this.retainScope(id, source);
3033
+ try {
3034
+ reference.attachOpening(this.manager.get(id).open(), signal);
3035
+ return reference;
3036
+ } catch (error) {
3037
+ reference.release();
3038
+ throw error;
3039
+ }
3095
3040
  }
3096
- /**
3097
- * Open a healthy catalog child through its direct-parent address.
3098
- * @param address - catalog-derived parent and child ids.
3099
- */
3100
- openSubagent(address) {
3101
- this.manager.selectSubagent(address);
3041
+ async using(target, options, operation) {
3042
+ const reference = this.retain(target, options);
3043
+ try {
3044
+ await reference.ready;
3045
+ return await operation(reference);
3046
+ } finally {
3047
+ reference.release();
3048
+ }
3049
+ }
3050
+ retainInfo(id) {
3051
+ let observer = this.retainObservers.get(id);
3052
+ if (observer === void 0) {
3053
+ const listeners = /* @__PURE__ */ new Set();
3054
+ observer = {
3055
+ listeners,
3056
+ published: this.retentionSnapshot(id),
3057
+ source: {
3058
+ getSnapshot: () => this.retentionSnapshot(id),
3059
+ subscribe: (listener) => {
3060
+ listeners.add(listener);
3061
+ return () => {
3062
+ listeners.delete(listener);
3063
+ };
3064
+ }
3065
+ }
3066
+ };
3067
+ this.retainObservers.set(id, observer);
3068
+ }
3069
+ return observer.source;
3102
3070
  }
3103
3071
  /**
3104
3072
  * Resolve an already discovered direct-parent address without opening it.
@@ -3125,16 +3093,6 @@ window.__ModuleLoader__.load({
3125
3093
  return this.manager.refreshSubagents(parentSessionId);
3126
3094
  }
3127
3095
  /**
3128
- * Clear the current selection so the layout shows the no-session empty
3129
- * state (new-session affordance and the workspace preselection flow).
3130
- * Wipes the persisted selection too — a reload stays on empty until the
3131
- * user opens or starts a session. The staged scope keeps its frozen view
3132
- * per the masked-gap contract until the next open() moves the stage.
3133
- */
3134
- clear() {
3135
- this.manager.clearSelection();
3136
- }
3137
- /**
3138
3096
  * Refresh the real Session baseline, reusing an in-flight pull.
3139
3097
  * @returns completion of the current or newly started baseline pull.
3140
3098
  */
@@ -3198,12 +3156,8 @@ window.__ModuleLoader__.load({
3198
3156
  this.manager.handleConnected();
3199
3157
  }
3200
3158
  /**
3201
- * Create a session on the host. Resolution guarantee: by the time the
3202
- * promise resolves, the created session is in the list store and
3203
- * {@link ClientSessions.binding} resolves it — callers (New Session
3204
- * draft hand-off) may address the scope synchronously, without waiting a
3205
- * notifier flush. The synchronous projection below makes this structural
3206
- * rather than an accident of microtask ordering.
3159
+ * Create a Host Session and publish its catalog row before resolving.
3160
+ * Callers retain the returned identity before borrowing its binding.
3207
3161
  * @param opts - target workspace or directory and an optional preallocated id.
3208
3162
  * @returns the new session id.
3209
3163
  * @throws {SessionCreateError} with the requested id.
@@ -3215,9 +3169,8 @@ window.__ModuleLoader__.load({
3215
3169
  return result.value.sessionId;
3216
3170
  }
3217
3171
  /**
3218
- * Fork a session from a completed-turn prefix of the source (same
3219
- * synchronous-addressability guarantee as {@link ClientSessions.create}:
3220
- * on resolution the child is in the list store and open() can target it).
3172
+ * Fork a Session from a completed-turn prefix of the source and publish
3173
+ * the child in the catalog before resolving.
3221
3174
  * @param opts - source session id, the optional event seq anchoring the
3222
3175
  * cut (the boundary is the first turn/end at or after it; an in-log
3223
3176
  * anchor in an open turn is unavailable rather than clipped backward),
@@ -3239,31 +3192,33 @@ window.__ModuleLoader__.load({
3239
3192
  this.projectList();
3240
3193
  const childId = result.value.sessionId;
3241
3194
  if (sourceTitle !== void 0) {
3242
- const child = this.binding(childId)?.session;
3243
- if (child === void 0) throw new Error(`fork child "${childId}" is not locally addressable`);
3244
- const renamed = await child.rename(increasedForkTitle(sourceTitle));
3245
- if (!renamed.ok) throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`);
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
+ }
3246
3203
  }
3247
3204
  return childId;
3248
3205
  }
3249
3206
  /**
3250
- * Resolve an Agent-scoped context view (use-and-discard).
3207
+ * Borrow an already-retained Agent-scoped Context.
3251
3208
  * @param id - session id (the agent identity — 1:1 same axis).
3252
- * @returns scoped ctx, or undefined for a session neither listed nor already scoped.
3209
+ * @returns the scoped Context, or undefined without a retained generation.
3253
3210
  */
3254
3211
  scope(id) {
3255
- return this.resolve(id)?.ctx;
3212
+ return this.scopes.get(id)?.ctx;
3256
3213
  }
3257
3214
  /**
3258
- * Materialize the Agent scope named by a validated Host Remote Event.
3259
- * The first successful Session-list baseline becomes authoritative for its
3260
- * lifetime; until then, transport streams may address the scope in either
3261
- * arrival order.
3262
- * @param id - Host-projected Agent identity (the matching Session id).
3263
- * @returns the identity-stable Agent Context.
3215
+ * Retain a validated Gateway identity synchronously, without history or catalog I/O.
3216
+ * @param id - Host-projected Session identity, possibly not yet catalogued.
3217
+ * @returns a Gateway-source reference owned by the invocation.
3264
3218
  */
3265
- resolveAgentScope(id) {
3266
- return (this.scopes.get(id) ?? this.materializeScope(id)).ctx;
3219
+ retainAgentScope(id) {
3220
+ if (this.closed) throw new Error("Session Controller is disposed");
3221
+ return this.retainScope(id, "gateway");
3267
3222
  }
3268
3223
  /**
3269
3224
  * Read the Agent scope tag off a context. Service-method boundary: fetch
@@ -3283,55 +3238,83 @@ window.__ModuleLoader__.load({
3283
3238
  * `agent.session`). Same service-method boundary as
3284
3239
  * {@link ClientSessions.scopeOf}.
3285
3240
  * @param ctx - an Agent-scoped context.
3286
- * @returns the session face, or undefined when the ctx is untagged or its scope was pruned.
3241
+ * @returns the matching live Session, or undefined for an untagged or ended generation.
3287
3242
  */
3288
3243
  sessionOf(ctx) {
3289
3244
  const id = scopeOf(ctx);
3290
3245
  if (id === void 0) return void 0;
3291
- return this.scopes.get(id)?.binding.session;
3246
+ const record = this.scopes.get(id);
3247
+ return record !== void 0 && scopeIdentityOf(record.ctx) === scopeIdentityOf(ctx) ? record.binding.session : void 0;
3292
3248
  }
3293
3249
  /**
3294
- * Resolve the stable session binding (scope-addressed assembly feed). Pure
3295
- * resolution — no staging, no window side effects.
3296
- * @param id - session id.
3297
- * @returns binding, or undefined for a session neither listed nor already scoped.
3250
+ * Borrow an already-retained binding without extending its lifetime.
3251
+ * @param id - Session identity.
3252
+ * @returns the live binding, or undefined without a retained generation.
3298
3253
  */
3299
3254
  binding(id) {
3300
- return this.resolve(id)?.binding;
3301
- }
3302
- /**
3303
- * Move the stage to the list's current session: sweep teardowns deferred
3304
- * behind the previous occupant and pull the new occupant's history window.
3305
- * Staging IS the open signal — the window opens ⟺ the session is on stage
3306
- * — and open() is idempotent (an in-flight or completed open no-ops; a
3307
- * failed one retries the next time current is touched).
3308
- */
3309
- followCurrent() {
3310
- const snapshot = this.list.getSnapshot();
3311
- const current = snapshot.current;
3312
- if (current === void 0 || snapshot.byId[current] === void 0 || current === this.watched) return;
3313
- this.watched = current;
3314
- this.sweepDeferred();
3315
- const record = this.resolve(current);
3316
- /* v8 ignore next 3 -- defensive: current is always a listed id (open()
3317
- * validates and the projection masks absent selections), so resolve
3318
- * cannot miss; kept so a future current writer cannot crash the notify. */
3319
- if (record !== void 0) {
3320
- record.session.open();
3321
- this.manager.refreshSubagents(current);
3322
- }
3323
- }
3324
- /**
3325
- * Lazily mint the scope + binding for an eligible session. Eligibility and
3326
- * prune share one predicate: listed on the host or selected
3327
- * through a retained subagent address. Breadcrumb-only ancestors remain
3328
- * summary data and do not keep scopes alive.
3329
- */
3330
- resolve(id) {
3331
- const existing = this.scopes.get(id);
3332
- if (existing !== void 0) return existing;
3333
- if (!this.eligible(id)) return void 0;
3334
- return this.materializeScope(id);
3255
+ return this.scopes.get(id)?.binding;
3256
+ }
3257
+ retainScope(id, source) {
3258
+ const record = this.scopes.get(id) ?? this.materializeScope(id);
3259
+ const previous = record.retention;
3260
+ record.retention = Object.freeze({
3261
+ referenceCount: previous.referenceCount + 1,
3262
+ retainedBy: freezeRetainedBy({
3263
+ ...previous.retainedBy,
3264
+ [source]: (previous.retainedBy[source] ?? 0) + 1
3265
+ })
3266
+ });
3267
+ const reference = new ClientSessionReference(id, record, () => {
3268
+ if (!record.live) return;
3269
+ const count = record.retention.referenceCount - 1;
3270
+ const { [source]: sourceCount = 0, ...otherSources } = record.retention.retainedBy;
3271
+ const retainedBy = sourceCount > 1 ? {
3272
+ ...otherSources,
3273
+ [source]: sourceCount - 1
3274
+ } : otherSources;
3275
+ record.retention = count === 0 ? EMPTY_RETAIN_INFO : Object.freeze({
3276
+ referenceCount: count,
3277
+ retainedBy: freezeRetainedBy(retainedBy)
3278
+ });
3279
+ if (count === 0) this.retireScope(id, record);
3280
+ else this.publishRetention(id);
3281
+ });
3282
+ if (this.list.getSnapshot().byId[id] === void 0) this.projectList();
3283
+ this.publishRetention(id);
3284
+ return reference;
3285
+ }
3286
+ retentionSnapshot(id) {
3287
+ return this.scopes.get(id)?.retention ?? EMPTY_RETAIN_INFO;
3288
+ }
3289
+ publishRetention(id) {
3290
+ const state = this.list.getSnapshot();
3291
+ const row = state.byId[id];
3292
+ const retainedBy = this.retentionSnapshot(id).retainedBy;
3293
+ if (row !== void 0 && row.retainedBy !== retainedBy) this.list.set({
3294
+ ...state,
3295
+ byId: {
3296
+ ...state.byId,
3297
+ [id]: {
3298
+ ...row,
3299
+ retainedBy
3300
+ }
3301
+ }
3302
+ });
3303
+ const observer = this.retainObservers.get(id);
3304
+ const snapshot = this.retentionSnapshot(id);
3305
+ if (observer === void 0 || observer.published === snapshot) return;
3306
+ observer.published = snapshot;
3307
+ (0, _deepseek_ai_dsh_client_store.notifySubscribers)(observer.listeners, "[session-controller] reference sources");
3308
+ }
3309
+ retireScope(id, record, disposeFiber = true) {
3310
+ if (!record.live) return;
3311
+ record.live = false;
3312
+ if (this.scopes.get(id) === record) this.scopes.delete(id);
3313
+ record.session.unbindScope();
3314
+ const sessionDisposal = this.manager.drop(id, record.session);
3315
+ this.projectList();
3316
+ this.publishRetention(id);
3317
+ this.startScopeDrop(id, record, disposeFiber, sessionDisposal);
3335
3318
  }
3336
3319
  /** Materialize one scope after its caller establishes that the id may be addressed. */
3337
3320
  materializeScope(id) {
@@ -3347,19 +3330,20 @@ window.__ModuleLoader__.load({
3347
3330
  eventSource: session.eventSource,
3348
3331
  ctx
3349
3332
  },
3350
- session
3333
+ session,
3334
+ retention: EMPTY_RETAIN_INFO,
3335
+ live: true
3351
3336
  };
3352
3337
  this.scopes.set(id, record);
3338
+ ctx.effect(() => () => {
3339
+ this.retireScope(id, record, false);
3340
+ }, "session-controller: exact generation");
3353
3341
  return record;
3354
3342
  }
3355
- /** The one aliveness predicate shared by scope mint and prune: host-listed or currently addressed. */
3356
- eligible(id) {
3357
- const { ids, current } = this.list.getSnapshot();
3358
- return current === id || ids.includes(id);
3359
- }
3360
3343
  /** Project the manager's list snapshot into the store (title derivation is display-only). */
3361
3344
  projectList() {
3362
- const { items, current, phase, subagentsByParent, jobsBySession, currentAddress } = this.manager.getListSnapshot();
3345
+ const previousById = this.list.getSnapshot().byId;
3346
+ const { items, phase, subagentsByParent, jobsBySession } = this.manager.getListSnapshot();
3363
3347
  const ids = [];
3364
3348
  const byId = {};
3365
3349
  for (const entry of items) {
@@ -3368,7 +3352,7 @@ window.__ModuleLoader__.load({
3368
3352
  id: entry.sessionId,
3369
3353
  displayTitle: displayTitleOf(entry.title, entry.cwd, entry.sessionId),
3370
3354
  running: entry.running,
3371
- ...entry.completed ? { completed: true } : {},
3355
+ retainedBy: this.retentionSnapshot(entry.sessionId).retainedBy,
3372
3356
  blank: entry.blank,
3373
3357
  updatedAt: entry.updatedAt,
3374
3358
  ...entry.projectionValues === void 0 ? {} : { projectionValues: entry.projectionValues },
@@ -3378,68 +3362,62 @@ window.__ModuleLoader__.load({
3378
3362
  ...entry.origin !== void 0 ? { origin: entry.origin } : {}
3379
3363
  };
3380
3364
  }
3381
- if (current !== void 0 && currentAddress !== void 0) {
3382
- const seen = /* @__PURE__ */ new Set();
3383
- let address = currentAddress;
3384
- while (address !== void 0 && !seen.has(address.childSessionId)) {
3385
- const childId = address.childSessionId;
3386
- seen.add(childId);
3387
- const child = subagentsByParent[address.parentSessionId]?.entries.find((entry) => entry.kind === "child" && entry.id === childId);
3388
- if (child?.kind !== "child") break;
3389
- const displayTitle = child.label ?? childId;
3390
- const summary = byId[childId];
3391
- if (summary === void 0) byId[childId] = {
3392
- id: childId,
3393
- displayTitle,
3394
- parentId: address.parentSessionId,
3395
- origin: "subagent",
3396
- running: child.activity === "running",
3397
- blank: false,
3365
+ for (const [parentId, catalog] of Object.entries(subagentsByParent)) for (const child of catalog.entries) {
3366
+ if (child.kind !== "child") continue;
3367
+ const childId = child.id;
3368
+ const summary = byId[childId];
3369
+ const projectionValues = summary?.projectionValues ?? this.manager.projectionValues(childId);
3370
+ const projectedTitle = projectionValues?.title;
3371
+ const title = typeof projectedTitle === "string" && projectedTitle !== "" ? projectedTitle : void 0;
3372
+ const displayTitle = title ?? child.label ?? childId;
3373
+ if (summary === void 0) byId[childId] = {
3374
+ id: childId,
3375
+ displayTitle,
3376
+ parentId,
3377
+ origin: "subagent",
3378
+ running: child.activity === "running",
3379
+ blank: false,
3380
+ updatedAt: 0,
3381
+ retainedBy: this.retentionSnapshot(childId).retainedBy,
3382
+ ...projectionValues === void 0 ? {} : { projectionValues },
3383
+ ...title === void 0 ? {} : { title }
3384
+ };
3385
+ else if (summary.displayTitle !== displayTitle || summary.projectionValues !== projectionValues) byId[childId] = {
3386
+ ...summary,
3387
+ displayTitle,
3388
+ ...projectionValues === void 0 ? {} : { projectionValues }
3389
+ };
3390
+ }
3391
+ for (const [id, record] of this.scopes) {
3392
+ if (byId[id] !== void 0) continue;
3393
+ const previous = previousById[id];
3394
+ const snapshot = record.session.getSnapshot();
3395
+ const address = this.manager.subagentAddress(id);
3396
+ byId[id] = {
3397
+ ...previous ?? {
3398
+ id,
3399
+ displayTitle: id,
3398
3400
  updatedAt: 0
3399
- };
3400
- else if (summary.displayTitle !== displayTitle) byId[childId] = {
3401
- ...summary,
3402
- displayTitle
3403
- };
3404
- const parent = byId[address.parentSessionId];
3405
- if (parent !== void 0 && parent.origin !== "subagent") break;
3406
- address = this.manager.navigationAddress(address.parentSessionId);
3407
- }
3401
+ },
3402
+ running: snapshot.running,
3403
+ retainedBy: record.retention.retainedBy,
3404
+ blank: snapshot.blank,
3405
+ ...address === void 0 ? {} : {
3406
+ parentId: address.parentSessionId,
3407
+ origin: "subagent"
3408
+ }
3409
+ };
3408
3410
  }
3409
- const persisted = this.selection.getSnapshot().sessionId;
3410
- if (current === void 0) {
3411
- if (persisted !== void 0) this.selection.set({});
3412
- } else if (byId[current] !== void 0 && (persisted !== current || this.selection.getSnapshot().subagentAddress?.childSessionId !== currentAddress?.childSessionId || this.selection.getSnapshot().subagentAddress?.parentSessionId !== currentAddress?.parentSessionId || this.selection.getSnapshot().subagentAddress?.mode !== currentAddress?.mode)) this.selection.set({
3413
- sessionId: current,
3414
- ...currentAddress === void 0 ? {} : { subagentAddress: currentAddress }
3415
- });
3416
3411
  this.list.set({
3417
3412
  ids,
3418
3413
  byId,
3419
- current,
3420
3414
  phase,
3421
3415
  subagentsByParent,
3422
- jobsBySession,
3423
- currentAddress
3416
+ jobsBySession
3424
3417
  });
3425
- this.pruneScopes();
3426
- }
3427
- /** Tear down scope + instance for no-longer-eligible sessions off stage; the staged one defers until the stage moves. */
3428
- pruneScopes() {
3429
- if (this.list.getSnapshot().phase === "pending") return;
3430
- for (const [id, record] of this.scopes) {
3431
- if (this.eligible(id)) continue;
3432
- if (id === this.watched) {
3433
- this.deferredRemovals.add(id);
3434
- continue;
3435
- }
3436
- this.scopes.delete(id);
3437
- this.deferredRemovals.delete(id);
3438
- this.startScopeDrop(id, record);
3439
- }
3440
3418
  }
3441
- startScopeDrop(id, record) {
3442
- const drop = this.dropScope(id, record);
3419
+ startScopeDrop(id, record, disposeFiber = true, sessionDisposal = this.manager.drop(id, record.session)) {
3420
+ const drop = this.dropScope(record, disposeFiber, sessionDisposal);
3443
3421
  this.scopeDrops.add(drop);
3444
3422
  drop.then(() => {
3445
3423
  this.scopeDrops.delete(drop);
@@ -3450,38 +3428,9 @@ window.__ModuleLoader__.load({
3450
3428
  async drainScopeDrops() {
3451
3429
  while (this.scopeDrops.size > 0) await Promise.allSettled([...this.scopeDrops]);
3452
3430
  }
3453
- /**
3454
- * One teardown for the whole per-session axis: the scope
3455
- * fiber (cascading every actx-registered effect: input shell, slash
3456
- * controller, popup, plugin stores, listeners), the session-keyed slot
3457
- * registrations and the Session instance itself — the host session log is the
3458
- * durable truth, a reopen lazily rebuilds and backfills via open().
3459
- */
3460
- async dropScope(id, record) {
3461
- record.session.unbindScope();
3462
- await Promise.allSettled([record.fiber.dispose(), this.manager.drop(id)]);
3463
- }
3464
- /** Run deferred teardowns whose session is no longer staged (called when the stage moves). */
3465
- sweepDeferred() {
3466
- for (const id of [...this.deferredRemovals]) {
3467
- /* v8 ignore next -- defensive: only the staged id ever defers, and every
3468
- * stage move sweeps first, so the set cannot contain the id the stage just
3469
- * moved to; kept as a guard against future extra sweep call sites. */
3470
- if (id === this.watched) continue;
3471
- if (this.eligible(id)) {
3472
- this.deferredRemovals.delete(id);
3473
- continue;
3474
- }
3475
- const record = this.scopes.get(id);
3476
- this.deferredRemovals.delete(id);
3477
- /* v8 ignore next -- defensive: prune deletes a scope and its deferral
3478
- * together, so a deferred id always still owns its record; kept so a
3479
- * future teardown path cannot double-dispose. */
3480
- if (record !== void 0) {
3481
- this.scopes.delete(id);
3482
- this.startScopeDrop(id, record);
3483
- }
3484
- }
3431
+ /** Await the already-withdrawn Session and scoped cleanup to quiescence. */
3432
+ async dropScope(record, disposeFiber, sessionDisposal) {
3433
+ await Promise.allSettled([sessionDisposal, ...disposeFiber ? [record.fiber.dispose()] : []]);
3485
3434
  }
3486
3435
  };
3487
3436
  //#endregion
@@ -3503,6 +3452,7 @@ window.__ModuleLoader__.load({
3503
3452
  */
3504
3453
  function apply(ctx) {
3505
3454
  const remotes = ctx.remote;
3455
+ const connection = ctx.get("connection");
3506
3456
  const sessions = new ClientSessions(ctx, remotes);
3507
3457
  ctx.remote.$on("api-session/added", (summary) => {
3508
3458
  sessions.handleSessionAdded(summary);
@@ -3527,14 +3477,22 @@ window.__ModuleLoader__.load({
3527
3477
  console.error("[session-controller] control stream failed:", error);
3528
3478
  }
3529
3479
  });
3530
- control.start();
3531
- ctx.on("connection/reset", () => {
3480
+ const connected = () => {
3481
+ if (connection.generation.getSnapshot() === void 0) return;
3532
3482
  sessions.handleConnected();
3533
- });
3534
- if (ctx.remote.$host.home !== void 0) sessions.handleConnected();
3483
+ control.restart();
3484
+ control.start();
3485
+ };
3486
+ ctx.effect(() => connection.generation.subscribe(connected), "session-controller.client.generation");
3487
+ connected();
3535
3488
  ctx.typert.contexts.registerClient("agent", {
3536
- identity: (candidate) => sessions.scopeOf(candidate),
3537
- resolve: (sessionId) => sessions.resolveAgentScope(sessionId)
3489
+ identity: (candidate) => sessions.sessionOf(candidate)?.sessionId,
3490
+ resolve: (sessionId) => {
3491
+ const reference = sessions.retainAgentScope(sessionId);
3492
+ return typertOwnedValue(reference.binding.ctx, () => {
3493
+ reference.release();
3494
+ });
3495
+ }
3538
3496
  });
3539
3497
  ctx.effect(() => async () => {
3540
3498
  await control.dispose();