@crazx/dsh-api-session-controller 0.1.2-alpha.3.zw.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 (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +74 -0
  4. package/README.zh.md +74 -0
  5. package/lib/client.js +2724 -0
  6. package/lib/index.js +2826 -0
  7. package/lib/invariant.js +13 -0
  8. package/lib/typert.host.d.ts +3 -0
  9. package/lib/typert.host.js +2574 -0
  10. package/lib/typert.remote-client.d.ts +67 -0
  11. package/lib/typert.remote-client.js +1119 -0
  12. package/lib/types/agent.d.ts +156 -0
  13. package/lib/types/agent.js +537 -0
  14. package/lib/types/catalog.d.ts +11 -0
  15. package/lib/types/catalog.js +58 -0
  16. package/lib/types/client/contract/events.d.ts +71 -0
  17. package/lib/types/client/contract/events.js +91 -0
  18. package/lib/types/client/contract/session.d.ts +150 -0
  19. package/lib/types/client/contract/session.js +2 -0
  20. package/lib/types/client/contract/sessions.d.ts +125 -0
  21. package/lib/types/client/contract/sessions.js +2 -0
  22. package/lib/types/client/contract/snapshot.d.ts +82 -0
  23. package/lib/types/client/contract/snapshot.js +2 -0
  24. package/lib/types/client/index.d.ts +30 -0
  25. package/lib/types/client/index.js +48 -0
  26. package/lib/types/client/ordered-baseline.d.ts +12 -0
  27. package/lib/types/client/ordered-baseline.js +41 -0
  28. package/lib/types/client/scope.d.ts +36 -0
  29. package/lib/types/client/scope.js +54 -0
  30. package/lib/types/client/sessions/history-records.d.ts +22 -0
  31. package/lib/types/client/sessions/history-records.js +31 -0
  32. package/lib/types/client/sessions/lineage.d.ts +38 -0
  33. package/lib/types/client/sessions/lineage.js +56 -0
  34. package/lib/types/client/sessions/manager.d.ts +280 -0
  35. package/lib/types/client/sessions/manager.js +894 -0
  36. package/lib/types/client/sessions/notifier.d.ts +39 -0
  37. package/lib/types/client/sessions/notifier.js +98 -0
  38. package/lib/types/client/sessions/projection-store.d.ts +108 -0
  39. package/lib/types/client/sessions/projection-store.js +129 -0
  40. package/lib/types/client/sessions/queue-mirror.d.ts +26 -0
  41. package/lib/types/client/sessions/queue-mirror.js +61 -0
  42. package/lib/types/client/sessions/remotes.d.ts +30 -0
  43. package/lib/types/client/sessions/remotes.js +8 -0
  44. package/lib/types/client/sessions/service.d.ts +349 -0
  45. package/lib/types/client/sessions/service.js +574 -0
  46. package/lib/types/client/sessions/session.d.ts +294 -0
  47. package/lib/types/client/sessions/session.js +711 -0
  48. package/lib/types/client/time-zone.d.ts +8 -0
  49. package/lib/types/client/time-zone.js +14 -0
  50. package/lib/types/client/transport.d.ts +73 -0
  51. package/lib/types/client/transport.js +106 -0
  52. package/lib/types/commands.d.ts +69 -0
  53. package/lib/types/commands.js +544 -0
  54. package/lib/types/control.d.ts +23 -0
  55. package/lib/types/control.js +192 -0
  56. package/lib/types/file-references.d.ts +27 -0
  57. package/lib/types/file-references.js +69 -0
  58. package/lib/types/history.d.ts +31 -0
  59. package/lib/types/history.js +376 -0
  60. package/lib/types/index.d.ts +171 -0
  61. package/lib/types/index.js +424 -0
  62. package/lib/types/invariant.d.ts +9 -0
  63. package/lib/types/invariant.js +12 -0
  64. package/lib/types/list.d.ts +53 -0
  65. package/lib/types/list.js +405 -0
  66. package/lib/types/model-selection-projection.d.ts +8 -0
  67. package/lib/types/model-selection-projection.js +66 -0
  68. package/lib/types/remote-events.d.ts +8 -0
  69. package/lib/types/remote-events.js +2 -0
  70. package/lib/types/skill-catalog.d.ts +28 -0
  71. package/lib/types/skill-catalog.js +192 -0
  72. package/lib/types/types.d.ts +505 -0
  73. package/lib/types/types.js +6 -0
  74. package/package.json +154 -0
package/lib/client.js ADDED
@@ -0,0 +1,2724 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@deepseek-ai/dsh-api-session-controller",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
8
+ let _deepseek_ai_dsh_api_gateway_client = require("@deepseek-ai/dsh-api-gateway/client");
9
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
10
+ //#region ../../typert/protocol/lib/index.js
11
+ /** The one Remote failure class shared by owners, the Gateway, and consumers. */
12
+ /**
13
+ * One Remote call failure: a real Error carrying its stable code and typed
14
+ * details. Owners throw it at the failure point; the Host Gateway encodes it
15
+ * onto the wire unchanged; the Client face rebuilds an instance for the
16
+ * `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
17
+ * Discrimination is always by `code`, never by instanceof.
18
+ */
19
+ var RemoteError = class extends Error {
20
+ code;
21
+ details;
22
+ /** Structural marker: cross-realm/bundle identification never uses instanceof. */
23
+ isDSHRemoteError = true;
24
+ /**
25
+ * @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
26
+ * @param message - human diagnostic carried across the wire.
27
+ * @param details - structured payload typed by the code.
28
+ * @param options - standard Error options (`cause` survives in-process only).
29
+ */
30
+ constructor(code, message, details, options) {
31
+ super(message, options);
32
+ this.code = code;
33
+ this.details = details;
34
+ this.name = "RemoteError";
35
+ }
36
+ };
37
+ //#endregion
38
+ //#region lib/types/client/sessions/history-records.js
39
+ /** Client range access and type narrowing for aligned Session history records. */
40
+ /**
41
+ * Narrow aligned wire records to their Client event types without allocation.
42
+ * @param records - validated history transport records.
43
+ * @returns the same record array with typed inner events.
44
+ */
45
+ function historyEntries(records) {
46
+ return records;
47
+ }
48
+ /**
49
+ * Read the first logical sequence represented by one wire record.
50
+ * @param record - validated scalar event or packed Assistant delta run.
51
+ * @returns inclusive first Session sequence.
52
+ */
53
+ function historyRecordFirstSeq(record) {
54
+ return record.event.seq;
55
+ }
56
+ /**
57
+ * Read the final logical sequence represented by one wire record.
58
+ * @param record - validated scalar event or packed Assistant delta run.
59
+ * @returns inclusive final Session sequence.
60
+ */
61
+ function historyRecordLastSeq(record) {
62
+ if (record.type === "event") return record.event.seq;
63
+ const length = record.event.type === "chunkrow/tool-call-chunks" ? record.event.data.args.length : record.event.data.texts.length;
64
+ return record.event.seq + length - 1;
65
+ }
66
+ //#endregion
67
+ //#region lib/types/types.js
68
+ /** Browser-safe request, result, and lifecycle vocabulary for the Session Remote service. */
69
+ /** Maximum number of Sessions returned by one search. */
70
+ const SESSION_SEARCH_RESULT_LIMIT = 20;
71
+ /** Maximum search snippet length in Unicode code points. */
72
+ const SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS = 240;
73
+ //#endregion
74
+ //#region lib/types/client/transport.js
75
+ /** Session-specific adapters for Gateway-owned Remote stream lifecycles. */
76
+ function toSessionJournalChange(change) {
77
+ switch (change.type) {
78
+ case "replace":
79
+ case "prepend": return {
80
+ ...change,
81
+ entries: historyEntries(change.entries)
82
+ };
83
+ case "append":
84
+ if (change.entry.type !== "event") throw new RemoteError("gateway/internal", "session live stream emitted a packed history record", {});
85
+ return {
86
+ type: "append",
87
+ entry: change.entry
88
+ };
89
+ }
90
+ }
91
+ /**
92
+ * Create the Host-wide Session control snapshot stream.
93
+ * @param remote - generated Session namespace and Gateway stream factory.
94
+ * @param options - Session state destinations.
95
+ * @returns an unstarted stream owned by the Client Session runtime.
96
+ */
97
+ function createSessionControlStream(remote, options) {
98
+ return new _deepseek_ai_dsh_api_gateway_client.RemoteSnapshotStream(remote.$stream({
99
+ name: "session control stream",
100
+ open: (signal) => remote.session.control(signal),
101
+ ended: (accepted) => accepted ? new _deepseek_ai_dsh_api_gateway_client.RemoteStreamCarrierError("session control stream ended without a terminal result") : /* @__PURE__ */ new Error("session control stream ended before its opening snapshot"),
102
+ ...options.carrierFailed === void 0 ? {} : { carrierFailed: options.carrierFailed }
103
+ }), {
104
+ name: "session control stream",
105
+ isSnapshot: (frame) => frame.type === "baseline",
106
+ replace: options.accept,
107
+ update: options.accept,
108
+ failed: options.failed
109
+ });
110
+ }
111
+ /** Gateway-owned event journal bound to one ordinary or direct-subagent Session address. */
112
+ var SessionEventStream = class extends _deepseek_ai_dsh_api_gateway_client.RemoteJournalStream {
113
+ remote;
114
+ address;
115
+ /**
116
+ * @param remote - generated Session namespace and Gateway stream factory.
117
+ * @param address - durable ordinary-Session or direct-subagent address.
118
+ * @param options - Session event-window destinations.
119
+ */
120
+ constructor(remote, address, options) {
121
+ super(remote, {
122
+ name: "session event stream",
123
+ emptyCursor: -1,
124
+ entries: (page) => page.records,
125
+ hasMore: (page) => page.hasMore,
126
+ first: historyRecordFirstSeq,
127
+ last: historyRecordLastSeq,
128
+ compare: (left, right) => left - right,
129
+ follows: (left, right) => right === left + 1,
130
+ publish: (change) => {
131
+ options.publish(toSessionJournalChange(change));
132
+ },
133
+ ...options.carrierFailed === void 0 ? {} : { carrierFailed: options.carrierFailed },
134
+ failed: options.failed
135
+ });
136
+ this.remote = remote;
137
+ this.address = address;
138
+ }
139
+ /** @inheritdoc */
140
+ async *follow(request, signal) {
141
+ for await (const frame of this.remote.session.follow({
142
+ address: this.address,
143
+ ...request.maxMessages === void 0 ? {} : { maxMessages: request.maxMessages }
144
+ }, signal)) {
145
+ if (frame.type === "snapshot") {
146
+ yield {
147
+ type: "opened",
148
+ cursor: frame.cursor,
149
+ page: {
150
+ records: frame.records,
151
+ hasMore: frame.hasMore,
152
+ projections: frame.projections
153
+ }
154
+ };
155
+ continue;
156
+ }
157
+ yield {
158
+ type: "entry",
159
+ entry: frame
160
+ };
161
+ }
162
+ }
163
+ /** @inheritdoc */
164
+ async readPage(request, throughSeq, signal) {
165
+ const result = await this.remote.session.page({
166
+ address: this.address,
167
+ throughSeq,
168
+ ...request
169
+ }, signal);
170
+ if (!result.ok) throw result.error;
171
+ return result.value;
172
+ }
173
+ /** @inheritdoc */
174
+ repairRequest(request) {
175
+ return request.maxMessages === void 0 ? {} : { maxMessages: request.maxMessages };
176
+ }
177
+ };
178
+ //#endregion
179
+ //#region ../../util/workspace-path/lib/index.js
180
+ /**
181
+ * Read the final non-empty segment of a Workspace path for display.
182
+ * Workspace-label surfaces use this helper instead of deriving another basename.
183
+ * @param path - Workspace directory path using POSIX or Windows separators.
184
+ * @returns the final segment, or an empty string for a separator-only path.
185
+ */
186
+ function workspaceTitleOf(path) {
187
+ const trimmed = path.replace(/[/\\]+$/, "");
188
+ const separator = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
189
+ return trimmed.slice(separator + 1);
190
+ }
191
+ //#endregion
192
+ //#region lib/types/client/scope.js
193
+ /**
194
+ * Client Agent-scope primitive: mint a Cordis context tagged with the owning
195
+ * Agent's identity. The mechanism mirrors the host `dsh-scope` architecture
196
+ * (no-op plugin fiber + context tag + `Context.filter` routing predicate);
197
+ * the shape deliberately diverges: the filter lives on the actx itself
198
+ * instead of a separate carrier object, so scoped dispatch is plain cordis —
199
+ * `actx.bail(actx, event, payload)` / `actx.emit(actx, ...)` — with no
200
+ * wrapper. The host needs a detached carrier because its dispatch subject is
201
+ * the business Agent object; client scope events carry only ids, so the
202
+ * actx is the natural subject. The second divergence stands: the scope key
203
+ * is the branded `SessionId` (value compared), not an object identity — the
204
+ * agent and its session share one id (1:1, same axis; no separate AgentId
205
+ * brand), and a client scope's identity IS that wire id. Third divergence,
206
+ * deliberate: the client scopes the Agent IDENTITY, not a live Agent object
207
+ * — a cold session's host Agent is already disposed while its client actx
208
+ * stays alive for history viewing.
209
+ */
210
+ /** Context tag written by {@link createScope}. */
211
+ const kScope = Symbol("dsh.client.scope");
212
+ /** Shared no-op plugin backing each Agent scope fiber. */
213
+ function agentScope() {}
214
+ /**
215
+ * Mint an Agent scope under `ctx`: a no-op plugin fiber whose context
216
+ * carries the agent tag and the dispatch filter — untagged listeners are
217
+ * admitted globally, tagged listeners only for a matching agent.
218
+ * Registrations through the returned ctx dispose with the fiber.
219
+ * @param ctx - client root context the scope fiber mounts under.
220
+ * @param key - owning agent identity (the routing tag; agent id === session id).
221
+ * @returns the tagged context and its backing fiber.
222
+ */
223
+ function createScope(ctx, key) {
224
+ const fiber = ctx.plugin(agentScope);
225
+ return {
226
+ fiber,
227
+ ctx: fiber.ctx.extend({
228
+ [kScope]: key,
229
+ [_deepseek_ai_cordis.Context.filter](listenerCtx) {
230
+ const tag = scopeOf(listenerCtx);
231
+ return tag === void 0 || tag === key;
232
+ }
233
+ })
234
+ };
235
+ }
236
+ /**
237
+ * Read the nearest agent tag inherited by a context.
238
+ * @param ctx - any client context.
239
+ * @returns its agent identity (the session id), or undefined for root contexts.
240
+ */
241
+ function scopeOf(ctx) {
242
+ return ctx[kScope];
243
+ }
244
+ //#endregion
245
+ //#region lib/types/client/ordered-baseline.js
246
+ /**
247
+ * Merge an authoritative baseline without moving identities already visible to
248
+ * the client. Baseline-only identities are inserted relative to the nearest
249
+ * following known identity; identities absent from the baseline are removed.
250
+ *
251
+ * @param current - the established client order.
252
+ * @param baseline - the latest authoritative rows.
253
+ * @param keyOf - stable identity selector.
254
+ * @returns baseline-valued rows with the established relative order retained.
255
+ */
256
+ function mergeOrderedBaseline(current, baseline, keyOf) {
257
+ const baselineByKey = /* @__PURE__ */ new Map();
258
+ for (const value of baseline) baselineByKey.set(keyOf(value), value);
259
+ const merged = current.map((value) => baselineByKey.get(keyOf(value))).filter((value) => value !== void 0);
260
+ const mergedKeys = new Set(merged.map(keyOf));
261
+ for (let index = 0; index < baseline.length; index++) {
262
+ const value = baseline[index];
263
+ /* v8 ignore next -- dense-array guard: index is bounded by baseline.length. */
264
+ if (value === void 0 || mergedKeys.has(keyOf(value))) continue;
265
+ let insertion = merged.length;
266
+ for (let following = index + 1; following < baseline.length; following++) {
267
+ const candidate = baseline[following];
268
+ /* v8 ignore next -- dense-array guard: following is bounded by baseline.length. */
269
+ if (candidate === void 0) continue;
270
+ const known = merged.findIndex((item) => keyOf(item) === keyOf(candidate));
271
+ if (known !== -1) {
272
+ insertion = known;
273
+ break;
274
+ }
275
+ }
276
+ merged.splice(insertion, 0, value);
277
+ mergedKeys.add(keyOf(value));
278
+ }
279
+ return merged;
280
+ }
281
+ //#endregion
282
+ //#region lib/types/client/sessions/lineage.js
283
+ /**
284
+ * Summaries -> flat list with lineage indentation. Root and sibling order
285
+ * follows the established input order; this projection never re-sorts a
286
+ * hydrated list from mutable timestamps.
287
+ * @param summaries - the host's session.list items.
288
+ * @param completed - sessions with a pending completion reminder (manager-owned live fact; absent = false).
289
+ * @returns display rows in render order.
290
+ */
291
+ function flattenLineage(summaries, completed) {
292
+ const byId = /* @__PURE__ */ new Map();
293
+ for (const s of summaries) byId.set(s.sessionId, s);
294
+ const children = /* @__PURE__ */ new Map();
295
+ const roots = [];
296
+ for (const s of summaries) if (s.parentSessionId !== void 0 && byId.has(s.parentSessionId)) {
297
+ const list = children.get(s.parentSessionId) ?? [];
298
+ list.push(s);
299
+ children.set(s.parentSessionId, list);
300
+ } else roots.push(s);
301
+ const out = [];
302
+ const visited = /* @__PURE__ */ new Set();
303
+ const walk = (s, depth) => {
304
+ if (visited.has(s.sessionId)) {
305
+ console.warn(`[session-controller] lineage cycle at ${s.sessionId}; emitting as root`);
306
+ return;
307
+ }
308
+ visited.add(s.sessionId);
309
+ out.push({
310
+ ...s,
311
+ completed: completed?.has(s.sessionId) ?? false,
312
+ depth
313
+ });
314
+ const kids = children.get(s.sessionId);
315
+ if (kids === void 0) return;
316
+ for (const kid of kids) walk(kid, depth + 1);
317
+ };
318
+ for (const root of roots) walk(root, 0);
319
+ for (const s of summaries) if (!visited.has(s.sessionId)) walk(s, 0);
320
+ return out;
321
+ }
322
+ //#endregion
323
+ //#region lib/types/client/sessions/notifier.js
324
+ /**
325
+ * Batches structural updates in microtasks and stream updates by animation
326
+ * frame. Reads may rebuild a dirty snapshot without consuming the pending
327
+ * subscriber notification.
328
+ */
329
+ var Notifier = class {
330
+ rebuild;
331
+ listeners = /* @__PURE__ */ new Set();
332
+ dirty = false;
333
+ notifyPending = false;
334
+ scheduled = "none";
335
+ scheduleGeneration = 0;
336
+ /** @param rebuild - snapshot rebuild function injected by the owner (writes the owner's snapshotCache). */
337
+ constructor(rebuild) {
338
+ this.rebuild = rebuild;
339
+ }
340
+ /**
341
+ * uSES subscription entry.
342
+ * @param listener - change callback.
343
+ * @returns the unsubscribe function.
344
+ */
345
+ subscribe(listener) {
346
+ this.listeners.add(listener);
347
+ return () => {
348
+ this.listeners.delete(listener);
349
+ };
350
+ }
351
+ /** Mark the snapshot dirty and notify in a microtask. */
352
+ markDirty() {
353
+ this.dirty = true;
354
+ this.notifyPending = true;
355
+ if (this.scheduled === "microtask") return;
356
+ this.schedule("microtask");
357
+ }
358
+ /** Mark the snapshot dirty and publish cumulative state at most once per frame. */
359
+ markFrameDirty() {
360
+ this.dirty = true;
361
+ this.notifyPending = true;
362
+ if (this.scheduled !== "none") return;
363
+ this.schedule(typeof globalThis.requestAnimationFrame === "function" ? "frame" : "microtask");
364
+ }
365
+ /**
366
+ * Synchronous flush: controlled-input writes must notify in the same tick as
367
+ * onChange, or React rolls the DOM back to the stale value and the caret jumps to the end.
368
+ */
369
+ notifyNow() {
370
+ this.dirty = true;
371
+ this.notifyPending = true;
372
+ this.invalidateSchedule();
373
+ this.flush();
374
+ }
375
+ /**
376
+ * Pre-getSnapshot check: rebuild synchronously when dirty (read path
377
+ * before first subscribe / while unobserved). Notification stays pending.
378
+ */
379
+ ensureFresh() {
380
+ if (!this.dirty) return;
381
+ this.dirty = false;
382
+ this.rebuild();
383
+ }
384
+ schedule(kind) {
385
+ const generation = ++this.scheduleGeneration;
386
+ this.scheduled = kind;
387
+ const publish = () => {
388
+ if (generation !== this.scheduleGeneration) return;
389
+ this.scheduled = "none";
390
+ this.flush();
391
+ };
392
+ if (kind === "frame") globalThis.requestAnimationFrame(publish);
393
+ else queueMicrotask(publish);
394
+ }
395
+ invalidateSchedule() {
396
+ this.scheduleGeneration++;
397
+ this.scheduled = "none";
398
+ }
399
+ flush() {
400
+ if (!this.notifyPending) return;
401
+ if (this.listeners.size === 0) return;
402
+ this.notifyPending = false;
403
+ if (this.dirty) {
404
+ this.dirty = false;
405
+ this.rebuild();
406
+ }
407
+ (0, _deepseek_ai_dsh_client_store.notifySubscribers)(this.listeners, "[session-controller]");
408
+ }
409
+ };
410
+ //#endregion
411
+ //#region lib/types/client/sessions/projection-store.js
412
+ /**
413
+ * One session's projection values. Framework semantics, uniform across every
414
+ * key: a baseline seeds rows at its cut, a push frame updates one row, and in
415
+ * both paths a lower-or-equal seq loses — a replayed frame cannot regress a
416
+ * value, a stale baseline cannot overwrite a newer frame. A key the store has
417
+ * never seen reads `undefined` (capability absent). Faces are identity-stable
418
+ * per key (create-on-demand, cached) so the React side binds each exactly
419
+ * once; the store-level channel (`subscribeAny`) serves coarse consumers (the
420
+ * manager's list projection reads the `title` key).
421
+ */
422
+ var ProjectionValueStore = class {
423
+ rows = /* @__PURE__ */ new Map();
424
+ channels = /* @__PURE__ */ new Map();
425
+ valuesCache;
426
+ /** Coarse any-key channel (no snapshot cache to rebuild: reads hit rows directly). */
427
+ anyNotifier = new Notifier(() => {});
428
+ /**
429
+ * Key-addressed bare observable face (the useProjection resolution path).
430
+ * Always defined — absence is an `undefined` snapshot, never a missing
431
+ * face, so a component may subscribe before the key ever carries a value.
432
+ * @param key - projection key.
433
+ * @returns the identity-stable face for this key.
434
+ */
435
+ faceOf(key) {
436
+ return this.channel(key).face;
437
+ }
438
+ /**
439
+ * Current whole value for a key (erased framework read; typed reads go
440
+ * through `useProjection`'s map lookup).
441
+ * @param key - projection key.
442
+ * @returns the value, or undefined while the key is absent.
443
+ */
444
+ get(key) {
445
+ return this.rows.get(key)?.value;
446
+ }
447
+ /**
448
+ * Read every current projection value as one reference-stable snapshot.
449
+ * @returns The same frozen value map until a row changes.
450
+ */
451
+ values() {
452
+ if (this.valuesCache === void 0) this.valuesCache = Object.freeze(Object.fromEntries([...this.rows].map(([key, row]) => [key, row.value])));
453
+ return this.valuesCache;
454
+ }
455
+ /**
456
+ * Subscribe to any-key changes (microtask-batched) — the manager's list
457
+ * rebuild channel.
458
+ * @param listener - change callback.
459
+ * @returns the unsubscribe function.
460
+ */
461
+ subscribeAny(listener) {
462
+ return this.anyNotifier.subscribe(listener);
463
+ }
464
+ /**
465
+ * Apply one finished value from the Session control stream.
466
+ * @param key - projection key.
467
+ * @param value - whole value computed by the host unit.
468
+ * @param seq - the unit's watermark at emission.
469
+ */
470
+ apply(key, value, seq) {
471
+ const row = this.rows.get(key);
472
+ if (row !== void 0 && seq <= row.seq) return;
473
+ this.rows.set(key, {
474
+ value,
475
+ seq
476
+ });
477
+ this.changed(key);
478
+ }
479
+ /**
480
+ * Seed from a history tail page's projections block: every carried key
481
+ * lands under the same seq rule as frames; a key the block omits is
482
+ * capability-absent as of the cut — its row clears unless a newer frame
483
+ * already superseded the cut (a stale baseline can neither overwrite nor
484
+ * clear newer values).
485
+ * @param baseline - the response's projections block.
486
+ */
487
+ seed(baseline) {
488
+ const values = baseline.values;
489
+ for (const key of Object.keys(values)) this.apply(key, values[key], baseline.asOfSeq);
490
+ for (const [key, row] of this.rows) {
491
+ if (Object.hasOwn(values, key)) continue;
492
+ if (row.seq > baseline.asOfSeq) continue;
493
+ this.rows.delete(key);
494
+ this.changed(key);
495
+ }
496
+ }
497
+ /**
498
+ * Drop rows beyond a replacement control baseline. Such rows describe
499
+ * process state the Host lost before persisting it and would otherwise
500
+ * outrank recomputed lower-seq values forever. The caller seeds the new
501
+ * baseline immediately afterward.
502
+ * @param lastSeq - highest durable sequence reflected by the baseline.
503
+ */
504
+ truncate(lastSeq) {
505
+ for (const [key, row] of this.rows) {
506
+ if (row.seq <= lastSeq) continue;
507
+ this.rows.delete(key);
508
+ this.changed(key);
509
+ }
510
+ }
511
+ changed(key) {
512
+ this.valuesCache = void 0;
513
+ this.channels.get(key)?.notifier.markDirty();
514
+ this.anyNotifier.markDirty();
515
+ }
516
+ channel(key) {
517
+ let channel = this.channels.get(key);
518
+ if (channel === void 0) {
519
+ const notifier = new Notifier(() => {});
520
+ channel = {
521
+ notifier,
522
+ face: {
523
+ getSnapshot: () => this.rows.get(key)?.value,
524
+ subscribe: (listener) => notifier.subscribe(listener)
525
+ }
526
+ };
527
+ this.channels.set(key, channel);
528
+ }
529
+ return channel;
530
+ }
531
+ };
532
+ //#endregion
533
+ //#region ../../util/crypto/lib/index.js
534
+ /**
535
+ * Random v4 UUID, minted from `crypto.getRandomValues`.
536
+ * @returns the UUID string.
537
+ */
538
+ function randomUUID() {
539
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
540
+ const hex = Array.from(bytes, (byte, index) => {
541
+ return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
542
+ }).join("");
543
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
544
+ }
545
+ //#endregion
546
+ //#region lib/types/client/contract/events.js
547
+ /** Observable contiguous Session event window consumed by domain assemblers. */
548
+ function leaf(entries) {
549
+ return {
550
+ kind: "leaf",
551
+ entries,
552
+ length: entries.length
553
+ };
554
+ }
555
+ function concat(left, right) {
556
+ return {
557
+ kind: "concat",
558
+ left,
559
+ right,
560
+ length: left.length + right.length
561
+ };
562
+ }
563
+ function materialize(node) {
564
+ if (node.kind === "leaf") return node.entries;
565
+ const entries = new Array(node.length);
566
+ const pending = [node];
567
+ let index = 0;
568
+ while (pending.length > 0) {
569
+ const current = pending.pop();
570
+ if (current.kind === "concat") {
571
+ pending.push(current.right, current.left);
572
+ continue;
573
+ }
574
+ for (const entry of current.entries) {
575
+ entries[index] = entry;
576
+ index += 1;
577
+ }
578
+ }
579
+ return entries;
580
+ }
581
+ function windowSnapshot(node, hasMore, revision, change) {
582
+ let entries;
583
+ return {
584
+ get entries() {
585
+ entries ??= materialize(node);
586
+ return entries;
587
+ },
588
+ hasMore,
589
+ revision,
590
+ change
591
+ };
592
+ }
593
+ /** Session-owned event feed; every accepted window mutation publishes synchronously. */
594
+ var MutableSessionEventSource = class {
595
+ listeners = /* @__PURE__ */ new Set();
596
+ window = leaf([]);
597
+ snapshot = windowSnapshot(this.window, false, 0, {
598
+ kind: "replace",
599
+ entries: []
600
+ });
601
+ /** @returns the cached event-window snapshot. */
602
+ getSnapshot() {
603
+ return this.snapshot;
604
+ }
605
+ /**
606
+ * Subscribe to synchronous window publication.
607
+ * @param listener - invalidation callback.
608
+ * @returns unsubscribe function.
609
+ */
610
+ subscribe(listener) {
611
+ this.listeners.add(listener);
612
+ return () => {
613
+ this.listeners.delete(listener);
614
+ };
615
+ }
616
+ /**
617
+ * Replace the complete contiguous window.
618
+ * @param entries - complete window.
619
+ * @param hasMore - whether older history remains.
620
+ */
621
+ replace(entries, hasMore) {
622
+ this.window = leaf(entries);
623
+ this.publish(hasMore, {
624
+ kind: "replace",
625
+ entries
626
+ });
627
+ }
628
+ /**
629
+ * Prepend one older contiguous page.
630
+ * @param entries - newly loaded older entries.
631
+ * @param hasMore - whether still older history remains.
632
+ */
633
+ prepend(entries, hasMore) {
634
+ this.window = concat(leaf(entries), this.window);
635
+ this.publish(hasMore, {
636
+ kind: "prepend",
637
+ entries
638
+ });
639
+ }
640
+ /**
641
+ * Append one contiguous live entry.
642
+ * @param entry - live tail entry.
643
+ */
644
+ append(entry) {
645
+ const entries = [entry];
646
+ this.window = concat(this.window, leaf(entries));
647
+ this.publish(this.snapshot.hasMore, {
648
+ kind: "append",
649
+ entries
650
+ });
651
+ }
652
+ publish(hasMore, change) {
653
+ this.snapshot = windowSnapshot(this.window, hasMore, this.snapshot.revision + 1, change);
654
+ (0, _deepseek_ai_dsh_client_store.notifySubscribers)(this.listeners, "[session-controller] event feed");
655
+ }
656
+ };
657
+ //#endregion
658
+ //#region lib/types/client/time-zone.js
659
+ /** Browser-owned time-zone sampling for prompt RPC provenance. */
660
+ /**
661
+ * Resolve the current browser IANA zone for one outbound operation.
662
+ * @returns The browser-provided canonical zone.
663
+ * @throws when the runtime cannot provide a non-empty zone.
664
+ */
665
+ function resolvedClientTimeZone() {
666
+ const timeZone = new Intl.DateTimeFormat().resolvedOptions().timeZone;
667
+ if (typeof timeZone !== "string" || timeZone.length === 0) throw new Error("browser time zone is unavailable");
668
+ return timeZone;
669
+ }
670
+ //#endregion
671
+ //#region lib/types/client/sessions/queue-mirror.js
672
+ const QUEUE_PREVIEW_CHARS = 200;
673
+ function previewOf(content) {
674
+ const flat = content.filter((block) => block.type !== "image").map((block) => block.type === "text" ? block.text : `[${block.type}]`).join(" ").replace(/\s+/g, " ").trim();
675
+ const chars = Array.from(flat);
676
+ return chars.length > QUEUE_PREVIEW_CHARS ? `${chars.slice(0, QUEUE_PREVIEW_CHARS).join("")}…` : flat;
677
+ }
678
+ function textOf(content) {
679
+ if (!content.every((block) => block.type === "text")) return null;
680
+ return content.map((block) => block.text).join("");
681
+ }
682
+ /** Authoritative transient queue projection and durable steering handoff. */
683
+ var SessionQueueMirror = class {
684
+ current = [];
685
+ /**
686
+ * Return the current immutable queue projection.
687
+ * @returns current queue rows.
688
+ */
689
+ snapshot() {
690
+ return this.current;
691
+ }
692
+ /**
693
+ * Replace from one authoritative stream queue frame.
694
+ * @param items - complete host queue snapshot.
695
+ */
696
+ replace(items) {
697
+ this.current = items.map((item) => {
698
+ const content = item.message.content;
699
+ return {
700
+ id: item.id,
701
+ messageId: item.message.id,
702
+ placement: item.placement,
703
+ ...item.rpcId === void 0 ? {} : { rpcId: item.rpcId },
704
+ content,
705
+ preview: previewOf(content),
706
+ text: textOf(content)
707
+ };
708
+ });
709
+ }
710
+ /**
711
+ * Retire a transient steering row once its durable message enters the log.
712
+ * @param event - newly contiguous durable Session event.
713
+ * @returns whether the projection changed.
714
+ */
715
+ acceptDurable(event) {
716
+ if (event.type !== "user/message") return false;
717
+ const messageId = event.data.id;
718
+ const index = this.current.findIndex((item) => item.placement === "steering" && item.messageId === messageId);
719
+ if (index < 0) return false;
720
+ this.current = this.current.filter((_item, candidate) => candidate !== index);
721
+ return true;
722
+ }
723
+ };
724
+ /**
725
+ * Owns a session's event window, lifecycle state, and observable
726
+ * snapshot. React bindings remain outside this data layer. Features see only
727
+ * the {@link SessionFace} slice (ISession verbs + the snapshot source); the
728
+ * remaining public members are Session Controller internals.
729
+ */
730
+ var Session = class {
731
+ sessionId;
732
+ remote;
733
+ options;
734
+ baseSeq = 0;
735
+ hasMore = false;
736
+ openState = "cold";
737
+ openError = null;
738
+ openPromise = null;
739
+ /** Bumped by stream replacement to invalidate an in-flight doOpen. Stale
740
+ * passes drop all writes once the generation moves on. */
741
+ openGeneration = 0;
742
+ loadingOlder = false;
743
+ /** Shared low-water target of the running jump loop; null when no jump is paging. */
744
+ jumpTargetSeq = null;
745
+ /** The running jump loop's completion, shared by retargeting callers. */
746
+ jumpPromise = null;
747
+ /** Authoritative stream-only inbox snapshot; pending work never hits history. */
748
+ queueMirror = new SessionQueueMirror();
749
+ running = false;
750
+ address;
751
+ parentAvailable;
752
+ /**
753
+ * Sticky send marker, private input of the composerPhase derivation: set
754
+ * synchronously before prompt()'s first await, never reset — the blank →
755
+ * engaging edge of the phase machine (see ComposerPhase).
756
+ */
757
+ promptAttempted = false;
758
+ /** A first accepted prompt stays in the engaging phase until its turn is observable. */
759
+ firstPromptPendingTurn = false;
760
+ /** Empty-log mirror (see ConversationSnapshot.blank); unknown bare sessions begin conservatively blank. */
761
+ blankBit = true;
762
+ removed = false;
763
+ promptError = null;
764
+ lastAgentError = null;
765
+ /** Local submission echoes, insertion-ordered (see SessionSnapshot.pendingSubmissions). */
766
+ pendingSubmissions = [];
767
+ /** Per-echo settlement state; `retiring` latches the first observation so a
768
+ * queue frame and its durable event cannot both retire one echo. */
769
+ submissionSettlements = /* @__PURE__ */ new Map();
770
+ /** Owns the addressed page/follow lifecycle while this Session is open. */
771
+ events;
772
+ /**
773
+ * Per-session projection value store (push model; see the session-projection
774
+ * subsystem page, docs/subsystems/session-projection.md): finished whole
775
+ * values computed on the Host, seeded by the tail page's
776
+ * projections block and updated by Session Controller control frames under the
777
+ * one higher-seq-wins rule. Keys are read via `projections.faceOf(key)`
778
+ * (the useProjection resolution face); the conversation snapshot never
779
+ * carries projection values, and no client-side domain folding exists.
780
+ * Manager-owned when constructed through SessionManager (frames route and
781
+ * the store outlives instantiation, the title-snapshot precedent); a bare
782
+ * construction gets a private store.
783
+ */
784
+ projections;
785
+ /** Contiguous history and live tail consumed by Conversation assembly. */
786
+ eventSource = new MutableSessionEventSource();
787
+ snapshotCache;
788
+ notifier;
789
+ /**
790
+ * Agent-scoped cordis context, bound once by ClientSessions when it
791
+ * mints the scope (the client mirror of the host Agent's loopCtx). The
792
+ * Session dispatches its own scoped events through it; undefined means
793
+ * unbound (bare object-layer construction) or already pruned — both skip
794
+ * dispatch-dependent behavior rather than fail.
795
+ */
796
+ actx;
797
+ /**
798
+ * @param sessionId - Host session identity (client sessions are always Host-born).
799
+ * @param remote - generated Remote namespaces this session calls.
800
+ * @param options - optional manager-owned state observers.
801
+ */
802
+ constructor(sessionId, remote, options = {}) {
803
+ this.sessionId = sessionId;
804
+ this.remote = remote;
805
+ this.options = options;
806
+ this.projections = options.projections ?? new ProjectionValueStore();
807
+ this.address = options.address;
808
+ this.parentAvailable = options.parentAvailable;
809
+ this.notifier = new Notifier(() => {
810
+ this.snapshotCache = this.buildSnapshot();
811
+ });
812
+ this.snapshotCache = this.buildSnapshot();
813
+ }
814
+ /**
815
+ * Bind the Agent-scoped context minted by ClientSessions (single write;
816
+ * a second bind is a wiring error and throws). Direction stays one-way at
817
+ * this binding boundary: consumers still reach the Session via `sessions.sessionOf`,
818
+ * while the Session holds its own dispatch point (host Agent.loopCtx
819
+ * mirror).
820
+ * @param actx - the agent's scoped context.
821
+ */
822
+ bindScope(actx) {
823
+ if (this.actx !== void 0) throw new Error(`session ${this.sessionId} already has a bound scope`);
824
+ this.actx = actx;
825
+ }
826
+ /** Release the bound scope at prune time (a later rebind accompanies a freshly minted scope). */
827
+ unbindScope() {
828
+ this.actx = void 0;
829
+ }
830
+ /**
831
+ * Register one local submission echo (see the ISession declaration).
832
+ * Synchronous through markDirty: the echo is in the very next snapshot, so
833
+ * the conversation can paint it before the caller starts serializing.
834
+ * @param input - echo content and the optional settlement callback.
835
+ * @returns the minted identity for {@link prompt} plus the pre-prompt abandon path.
836
+ */
837
+ beginSubmission(input) {
838
+ const requestId = randomUUID();
839
+ this.pendingSubmissions = [...this.pendingSubmissions, {
840
+ requestId,
841
+ placement: this.running ? input.mode === "steer" ? "steering" : "queued" : "transcript",
842
+ time: Date.now(),
843
+ text: input.text,
844
+ images: input.images
845
+ }];
846
+ this.submissionSettlements.set(requestId, {
847
+ onRetire: input.onRetire,
848
+ retiring: false
849
+ });
850
+ this.promptAttempted = true;
851
+ this.notifier.markDirty();
852
+ return {
853
+ requestId,
854
+ abandon: () => {
855
+ this.retireFailedSubmission(requestId);
856
+ }
857
+ };
858
+ }
859
+ /**
860
+ * Send (queue/steer passed through 1:1); failures land in the snapshot's promptError.
861
+ * @param content - text plus browser-owned temporary image uploads.
862
+ * @param mode - queue appends after the current turn; steer interrupts it.
863
+ * @param signal - optional caller cancellation for the complete admission round-trip.
864
+ * @param requestId - identity from {@link beginSubmission}; a failed identified prompt retires its echo.
865
+ * @returns the prompt result (also mirrored into promptError on failure).
866
+ */
867
+ async prompt(content, mode, signal, requestId) {
868
+ this.promptError = null;
869
+ this.lastAgentError = null;
870
+ this.promptAttempted = true;
871
+ if (this.blankBit) this.firstPromptPendingTurn = true;
872
+ this.notifier.markDirty();
873
+ let result;
874
+ if (this.address === void 0) {
875
+ const clientTimeZone = resolvedClientTimeZone();
876
+ result = await this.remote.session.prompt({
877
+ requestId: requestId ?? randomUUID(),
878
+ sessionId: this.sessionId,
879
+ mode,
880
+ content,
881
+ clientTimeZone
882
+ }, signal);
883
+ } else {
884
+ const routed = await this.remote.subagents.prompt({
885
+ requestId: randomUUID(),
886
+ parentSessionId: this.address.parentSessionId,
887
+ childSessionId: this.address.childSessionId,
888
+ mode: "continuable",
889
+ content,
890
+ clientTimeZone: resolvedClientTimeZone()
891
+ }, signal);
892
+ result = routed.ok ? {
893
+ ok: true,
894
+ value: { accepted: true }
895
+ } : routed;
896
+ }
897
+ if (!result.ok) {
898
+ if (requestId !== void 0) this.retireFailedSubmission(requestId);
899
+ this.promptError = {
900
+ op: "send",
901
+ error: result.error
902
+ };
903
+ this.notifier.markDirty();
904
+ return result;
905
+ }
906
+ if (this.blankBit) {
907
+ this.blankBit = false;
908
+ this.options.onEngaged?.(this);
909
+ this.notifier.markDirty();
910
+ }
911
+ return result;
912
+ }
913
+ /**
914
+ * Resolve one image referenced by this session into browser-consumable bytes.
915
+ * @param attachmentId - opaque id found in the folded session log.
916
+ * @returns the authenticated reference and decoded bytes.
917
+ */
918
+ async readAttachment(attachmentId) {
919
+ const result = await this.remote.session.attachment({
920
+ sessionId: this.sessionId,
921
+ attachmentId
922
+ });
923
+ if (!result.ok) return result;
924
+ const binary = atob(result.value.data);
925
+ const data = Uint8Array.from(binary, (char) => char.charCodeAt(0));
926
+ return {
927
+ ok: true,
928
+ value: {
929
+ attachment: result.value.attachment,
930
+ data
931
+ }
932
+ };
933
+ }
934
+ /** Apply one operation to a still-pending queue occurrence. */
935
+ async updateQueue(itemId, action) {
936
+ return this.remote.session.updateQueue({
937
+ sessionId: this.sessionId,
938
+ itemId,
939
+ action
940
+ });
941
+ }
942
+ /**
943
+ * Stop the active turn while the Host preserves pending inbox work; failures
944
+ * land in promptError (same error-strip display slot). A subagent address
945
+ * routes through `subagents.interruptByParent`, whose durable parent-address
946
+ * authority works without a live parent Agent.
947
+ * @returns the cancel result.
948
+ */
949
+ async cancel() {
950
+ const address = this.address;
951
+ const result = address !== void 0 ? await this.remote.subagents.interruptByParent(address.childSessionId, address.parentSessionId, "continuable") : await this.remote.session.cancel({ sessionId: this.sessionId });
952
+ if (!result.ok) {
953
+ this.promptError = {
954
+ op: "stop",
955
+ error: result.error
956
+ };
957
+ this.notifier.markDirty();
958
+ }
959
+ return result;
960
+ }
961
+ /**
962
+ * Rename: contract session.rename 1:1. On success settle the 'title'
963
+ * projection cell from the response's `{title, seq}` under the store's
964
+ * higher-seq-wins rule (the push frame arriving later is a no-op replay),
965
+ * so the list row and any useProjection('title') reader update without
966
+ * waiting for the control-stream projection update.
967
+ * @param title - raw title text (the host normalizes acceptance).
968
+ * @returns the rename result (normalized accepted title + title event seq).
969
+ */
970
+ async rename(title) {
971
+ const result = await this.remote.session.rename({
972
+ sessionId: this.sessionId,
973
+ title
974
+ });
975
+ if (result.ok) this.projections.apply("title", result.value.title, result.value.seq);
976
+ return result;
977
+ }
978
+ /**
979
+ * Execute one slash-command line against this session's agent — pure
980
+ * admission semantics (the host executor durably logs the lifecycle;
981
+ * outcomes render as flow nodes, never as a response echo).
982
+ * @param line - the full command line, leading slash included.
983
+ * @returns the admission result.
984
+ */
985
+ async command(line) {
986
+ const result = await this.remote.commands.execute(this.sessionId, line, []);
987
+ if (!result.ok) return result;
988
+ return {
989
+ ok: true,
990
+ value: { matched: result.value !== void 0 }
991
+ };
992
+ }
993
+ /** First open: pull the tail page (idempotent — in-flight/already-open returns the existing promise). */
994
+ open() {
995
+ if (this.openState === "open") return Promise.resolve();
996
+ if (this.openPromise !== null) return this.openPromise;
997
+ const promise = this.doOpen(this.openGeneration).finally(() => {
998
+ if (this.openPromise === promise) this.openPromise = null;
999
+ });
1000
+ this.openPromise = promise;
1001
+ return promise;
1002
+ }
1003
+ /** Page up: pull one earlier page with the window's first seq as beforeSeq and prepend. */
1004
+ async loadOlder() {
1005
+ if (this.openState !== "open" || !this.hasMore || this.loadingOlder) return;
1006
+ const events = this.events;
1007
+ if (events === void 0) return;
1008
+ this.loadingOlder = true;
1009
+ this.notifier.markDirty();
1010
+ try {
1011
+ await events.prepend({
1012
+ beforeSeq: this.baseSeq,
1013
+ maxMessages: 50
1014
+ });
1015
+ } catch (error) {
1016
+ if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) console.error("[session-controller] loadOlder failed:", error);
1017
+ } finally {
1018
+ this.loadingOlder = false;
1019
+ this.notifier.markDirty();
1020
+ }
1021
+ }
1022
+ /** Jump loader: page backwards until the window covers seq (see ISession.loadThrough). */
1023
+ loadThrough(seq) {
1024
+ if (this.openState !== "open" || !this.hasMore || this.baseSeq <= seq) return Promise.resolve();
1025
+ if (this.jumpPromise !== null) {
1026
+ this.jumpTargetSeq = Math.min(this.jumpTargetSeq ?? seq, seq);
1027
+ return this.jumpPromise;
1028
+ }
1029
+ if (this.loadingOlder) return Promise.resolve();
1030
+ this.jumpTargetSeq = seq;
1031
+ this.loadingOlder = true;
1032
+ this.notifier.markDirty();
1033
+ const generation = this.openGeneration;
1034
+ this.jumpPromise = (async () => {
1035
+ try {
1036
+ while (this.hasMore && this.jumpTargetSeq !== null && this.baseSeq > this.jumpTargetSeq) {
1037
+ if (generation !== this.openGeneration) return;
1038
+ const events = this.events;
1039
+ if (events === void 0) return;
1040
+ const before = this.baseSeq;
1041
+ await events.prepend({
1042
+ beforeSeq: this.baseSeq,
1043
+ maxMessages: 200
1044
+ });
1045
+ if (this.baseSeq >= before) return;
1046
+ }
1047
+ } catch (error) {
1048
+ if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) console.error("[session-controller] loadThrough failed:", error);
1049
+ } finally {
1050
+ this.jumpTargetSeq = null;
1051
+ this.jumpPromise = null;
1052
+ this.loadingOlder = false;
1053
+ this.notifier.markDirty();
1054
+ }
1055
+ })();
1056
+ return this.jumpPromise;
1057
+ }
1058
+ /** Rebuild an opened history source after address replacement.
1059
+ * Invalidates any in-flight open first; queue state belongs to the independently
1060
+ * reconnecting control stream and remains untouched. */
1061
+ async resync() {
1062
+ if (this.openState === "cold") return;
1063
+ this.openGeneration++;
1064
+ const events = this.events;
1065
+ this.events = void 0;
1066
+ await events?.dispose();
1067
+ this.openPromise = null;
1068
+ this.openState = "cold";
1069
+ this.openError = null;
1070
+ this.baseSeq = 0;
1071
+ this.notifier.markDirty();
1072
+ await this.open();
1073
+ }
1074
+ /**
1075
+ * uSES subscription entry.
1076
+ * @param listener - change callback.
1077
+ * @returns the unsubscribe function.
1078
+ */
1079
+ subscribe(listener) {
1080
+ return this.notifier.subscribe(listener);
1081
+ }
1082
+ /**
1083
+ * Cached Session snapshot (rebuilt lazily when dirty with no listeners).
1084
+ * @returns the cached reference (stable until the next flush).
1085
+ */
1086
+ getSnapshot() {
1087
+ this.notifier.ensureFresh();
1088
+ return this.snapshotCache;
1089
+ }
1090
+ /**
1091
+ * Replace every transient control value for this Session from one stream baseline.
1092
+ * @param queue - complete pending queue for this Session.
1093
+ */
1094
+ replaceControl(queue) {
1095
+ this.queueMirror.replace(queue);
1096
+ this.observeSubmissionQueue(queue);
1097
+ this.notifier.markDirty();
1098
+ }
1099
+ /**
1100
+ * Apply one Session-addressed live control update.
1101
+ * @param frame - queue replacement addressed to this Session.
1102
+ */
1103
+ handleControlFrame(frame) {
1104
+ this.queueMirror.replace(frame.items);
1105
+ this.observeSubmissionQueue(frame.items);
1106
+ this.notifier.markDirty();
1107
+ }
1108
+ /**
1109
+ * Running-bit relay from the host stream (list entry and snapshot stay consistent).
1110
+ * @param running - the new running state.
1111
+ */
1112
+ handleRunning(running) {
1113
+ if (running && this.blankBit) {
1114
+ this.blankBit = false;
1115
+ this.notifier.markDirty();
1116
+ }
1117
+ if (running) this.firstPromptPendingTurn = false;
1118
+ if (this.running === running) return;
1119
+ this.running = running;
1120
+ this.notifier.markDirty();
1121
+ }
1122
+ /**
1123
+ * Install or clear the catalog-discovered transport address. A changed
1124
+ * address rebuilds an already-open window through its new history route.
1125
+ * @param address - direct parent/child address, or undefined for ordinary transport.
1126
+ * @param parentAvailable - latest exact-parent availability hint, or undefined before a catalog read.
1127
+ */
1128
+ configureSubagent(address, parentAvailable) {
1129
+ const same = this.address?.parentSessionId === address?.parentSessionId && this.address?.childSessionId === address?.childSessionId && this.address?.mode === address?.mode;
1130
+ this.address = address;
1131
+ this.parentAvailable = parentAvailable;
1132
+ if (!same && this.openState !== "cold") this.resync();
1133
+ else this.notifier.markDirty();
1134
+ }
1135
+ /**
1136
+ * Update only the parent availability hint from a catalog refresh.
1137
+ * @param available - whether the exact direct parent is live.
1138
+ */
1139
+ handleSubagentParentAvailable(available) {
1140
+ if (this.parentAvailable === available) return;
1141
+ this.parentAvailable = available;
1142
+ this.notifier.markDirty();
1143
+ }
1144
+ /**
1145
+ * Blank-bit relay from the authoritative summary source (`session.list` and
1146
+ * `api-session/added`). Monotone: once any signal (local first send,
1147
+ * running flip, an earlier summary) cleared it, a stale true never
1148
+ * re-blanks.
1149
+ * @param blank - the summary's derived empty-log bit.
1150
+ */
1151
+ handleBlank(blank) {
1152
+ if (blank === this.blankBit) return;
1153
+ if (blank && (this.promptAttempted || this.running)) return;
1154
+ this.blankBit = blank;
1155
+ this.notifier.markDirty();
1156
+ }
1157
+ /** `api-session/removed` relay: flag the snapshot while retaining the resident instance. */
1158
+ handleRemoved() {
1159
+ this.removed = true;
1160
+ this.notifier.markDirty();
1161
+ }
1162
+ /**
1163
+ * `api-session/error` relay: the outlet for live failures with no turn position.
1164
+ * @param message - the stringified error.
1165
+ */
1166
+ handleAgentError(message) {
1167
+ this.lastAgentError = message;
1168
+ this.notifier.markDirty();
1169
+ }
1170
+ /**
1171
+ * Stop the Session's live Remote source.
1172
+ * @returns when the Remote iterator has completed teardown.
1173
+ */
1174
+ async dispose() {
1175
+ for (const requestId of [...this.submissionSettlements.keys()]) this.retireFailedSubmission(requestId);
1176
+ this.openGeneration++;
1177
+ const events = this.events;
1178
+ this.events = void 0;
1179
+ await events?.dispose();
1180
+ }
1181
+ /** @param generation - openGeneration at launch; stale passes cannot publish after replacement. */
1182
+ async doOpen(generation) {
1183
+ this.openState = "loading";
1184
+ this.openError = null;
1185
+ this.notifier.markDirty();
1186
+ const events = new SessionEventStream(this.remote, this.sessionAddress(), {
1187
+ publish: (change) => {
1188
+ if (generation !== this.openGeneration || this.events !== events) return;
1189
+ this.acceptEventChange(change);
1190
+ },
1191
+ failed: (error) => {
1192
+ this.failEventStream(events, generation, error);
1193
+ }
1194
+ });
1195
+ this.events = events;
1196
+ try {
1197
+ await events.open({ maxMessages: 50 });
1198
+ if (generation !== this.openGeneration || this.events !== events) return;
1199
+ this.openState = "open";
1200
+ } catch (error) {
1201
+ if (generation !== this.openGeneration || this.events !== events) return;
1202
+ if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) throw error;
1203
+ this.events = void 0;
1204
+ this.openState = "error";
1205
+ this.openError = error;
1206
+ } finally {
1207
+ if (generation === this.openGeneration) this.notifier.markDirty();
1208
+ }
1209
+ }
1210
+ /** Apply one contiguous journal update already reconciled by the Remote stream. */
1211
+ acceptEventChange(change) {
1212
+ switch (change.type) {
1213
+ case "replace":
1214
+ this.installWindow(change.entries, change.hasMore, change.page.projections);
1215
+ return;
1216
+ case "prepend":
1217
+ this.prependWindow(change.entries, change.hasMore);
1218
+ return;
1219
+ case "append": if (this.appendLive(change.entry)) this.notifier.markDirty();
1220
+ }
1221
+ }
1222
+ /** Replace the complete contiguous window and apply page-owned projection metadata. */
1223
+ installWindow(entries, hasMore, projections) {
1224
+ this.baseSeq = entries[0]?.event.seq ?? 0;
1225
+ this.hasMore = hasMore;
1226
+ if (entries.some((entry) => entry.event.type === "turn/start")) this.firstPromptPendingTurn = false;
1227
+ if (projections !== void 0) this.projections.seed(projections);
1228
+ this.eventSource.replace(entries, hasMore);
1229
+ for (const entry of entries) this.observeSubmissionEvent(entry.event);
1230
+ this.notifier.markDirty();
1231
+ }
1232
+ /** Prepend one stream-validated history page. */
1233
+ prependWindow(entries, hasMore) {
1234
+ this.baseSeq = entries[0]?.event.seq ?? this.baseSeq;
1235
+ this.hasMore = hasMore;
1236
+ this.eventSource.prepend(entries, hasMore);
1237
+ }
1238
+ /** Append one stream-validated live event. */
1239
+ appendLive(entry) {
1240
+ const event = entry.event;
1241
+ const awaitingFirstTurn = this.firstPromptPendingTurn;
1242
+ if (event.type === "turn/start") this.firstPromptPendingTurn = false;
1243
+ const queueChanged = this.queueMirror.acceptDurable(event);
1244
+ this.eventSource.append(entry);
1245
+ this.observeSubmissionEvent(event);
1246
+ return queueChanged || awaitingFirstTurn !== this.firstPromptPendingTurn;
1247
+ }
1248
+ /** Retire the matching echo when a durable browser-prompt `user/message` becomes visible. */
1249
+ observeSubmissionEvent(event) {
1250
+ if (this.submissionSettlements.size === 0 || event.type !== "user/message") return;
1251
+ const data = event.data;
1252
+ const source = data?.source;
1253
+ if (source?.kind !== "user" || typeof source.rpcId !== "string") return;
1254
+ this.scheduleObservedRetirement(source.rpcId, imageRefsIn(data?.content));
1255
+ }
1256
+ /** Retire echoes whose prompts landed in the host inbox instead of the log (running-turn submissions). */
1257
+ observeSubmissionQueue(items) {
1258
+ if (this.submissionSettlements.size === 0) return;
1259
+ for (const item of items) if (item.rpcId !== void 0) this.scheduleObservedRetirement(item.rpcId, imageRefsIn(item.message.content));
1260
+ }
1261
+ /**
1262
+ * Latch one observed settlement and remove the echo an animation frame
1263
+ * later. The delay keeps the echo in the snapshot until the frame in which
1264
+ * the durable node (whose assembly frame was registered first) is
1265
+ * renderable; the render-time rpcId dedupe hides the one-frame overlap.
1266
+ */
1267
+ scheduleObservedRetirement(requestId, attachments) {
1268
+ const settlement = this.submissionSettlements.get(requestId);
1269
+ if (settlement === void 0 || settlement.retiring) return;
1270
+ settlement.retiring = true;
1271
+ scheduleFrame(() => {
1272
+ this.finishSubmission(requestId, {
1273
+ reason: "observed",
1274
+ attachments
1275
+ });
1276
+ });
1277
+ }
1278
+ /** Remove one unsettled echo immediately (prompt rejection, abort, or disposal). */
1279
+ retireFailedSubmission(requestId) {
1280
+ const settlement = this.submissionSettlements.get(requestId);
1281
+ if (settlement === void 0 || settlement.retiring) return;
1282
+ settlement.retiring = true;
1283
+ this.finishSubmission(requestId, { reason: "failed" });
1284
+ }
1285
+ /** Single removal point: drop the echo, publish, then notify the owner. */
1286
+ finishSubmission(requestId, retirement) {
1287
+ const settlement = this.submissionSettlements.get(requestId);
1288
+ /* v8 ignore next -- retiring latches before every schedule, so one settlement never finishes twice. */
1289
+ if (settlement === void 0) return;
1290
+ this.submissionSettlements.delete(requestId);
1291
+ this.pendingSubmissions = this.pendingSubmissions.filter((echo) => echo.requestId !== requestId);
1292
+ this.notifier.markDirty();
1293
+ settlement.onRetire?.(retirement);
1294
+ }
1295
+ /** Publish a terminal background failure only while this stream still owns the Session. */
1296
+ failEventStream(events, generation, error) {
1297
+ if (generation !== this.openGeneration || this.events !== events) return;
1298
+ if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) throw error;
1299
+ this.openGeneration++;
1300
+ this.events = void 0;
1301
+ this.openPromise = null;
1302
+ this.openState = "error";
1303
+ this.openError = error;
1304
+ events.dispose();
1305
+ this.notifier.markDirty();
1306
+ }
1307
+ buildSnapshot() {
1308
+ return {
1309
+ sessionId: this.sessionId,
1310
+ queue: this.queueMirror.snapshot(),
1311
+ pendingSubmissions: this.pendingSubmissions,
1312
+ running: this.running,
1313
+ subagent: this.address === void 0 ? null : {
1314
+ address: this.address,
1315
+ ...this.parentAvailable === void 0 ? {} : { parentAvailable: this.parentAvailable }
1316
+ },
1317
+ removed: this.removed,
1318
+ openState: this.openState,
1319
+ openError: this.openError,
1320
+ hasMore: this.hasMore,
1321
+ loadingOlder: this.loadingOlder,
1322
+ promptError: this.promptError,
1323
+ blank: this.blankBit,
1324
+ lastAgentError: this.lastAgentError,
1325
+ promptAttempted: this.promptAttempted,
1326
+ awaitingFirstTurn: this.firstPromptPendingTurn
1327
+ };
1328
+ }
1329
+ sessionAddress() {
1330
+ return this.address === void 0 ? {
1331
+ kind: "session",
1332
+ sessionId: this.sessionId
1333
+ } : {
1334
+ kind: "subagent",
1335
+ ...this.address
1336
+ };
1337
+ }
1338
+ };
1339
+ /** Run one callback on the next animation frame, or a macrotask where no frame clock exists. */
1340
+ function scheduleFrame(fn) {
1341
+ if (typeof requestAnimationFrame === "function") requestAnimationFrame(() => {
1342
+ fn();
1343
+ });
1344
+ else setTimeout(fn, 0);
1345
+ }
1346
+ /** Image attachment references in one structurally-read content block list, in block order. */
1347
+ function imageRefsIn(content) {
1348
+ if (!Array.isArray(content)) return [];
1349
+ const refs = [];
1350
+ for (const block of content) {
1351
+ if (typeof block !== "object" || block === null) continue;
1352
+ const candidate = block;
1353
+ if (candidate.type === "image" && typeof candidate.attachment === "object" && candidate.attachment !== null) refs.push(candidate.attachment);
1354
+ }
1355
+ return refs;
1356
+ }
1357
+ //#endregion
1358
+ //#region lib/types/client/sessions/manager.js
1359
+ function catalogAvailability(parentAvailable) {
1360
+ return parentAvailable === void 0 ? {} : { parentAvailable };
1361
+ }
1362
+ /** Instance cluster + frame entry + the session list. */
1363
+ var SessionManager = class {
1364
+ remote;
1365
+ sessions = /* @__PURE__ */ new Map();
1366
+ /** In-flight Session disposals remain here after instances leave `sessions`, so manager disposal can await quiescence. */
1367
+ sessionDisposals = /* @__PURE__ */ new Set();
1368
+ /** Latest transient queues, retained independently of Session object materialization. */
1369
+ queues = /* @__PURE__ */ new Map();
1370
+ /**
1371
+ * Sessions that finished running while not selected — the sidebar's green
1372
+ * "done" reminder (manager-owned, survives connection generations; cleared
1373
+ * on select and session-removed, re-armed by the next completion).
1374
+ */
1375
+ completedNotifications = /* @__PURE__ */ new Set();
1376
+ /** Last-observed running bits per session; the true→false edge here arms {@link completedNotifications}. */
1377
+ prevRunning = /* @__PURE__ */ new Map();
1378
+ /** Per-session projection value stores, retained independently of instance arrival (the
1379
+ * title-snapshot precedent, generalized): push frames land here whether or not the Session
1380
+ * is instantiated (list rows read the 'title' key), and an instantiated Session adopts the
1381
+ * same store so history-baseline seeding and frames converge on one row set. */
1382
+ projectionStores = /* @__PURE__ */ new Map();
1383
+ summaries = [];
1384
+ listState = "idle";
1385
+ /** Arrival phase; the pending → ready edge fires on the first successful pull (see SessionListPhase). */
1386
+ listPhase = "pending";
1387
+ listError = null;
1388
+ listInflight = null;
1389
+ /** Mutations arriving after a list request starts are replayed over its response. */
1390
+ listMutations = null;
1391
+ addresses = /* @__PURE__ */ new Map();
1392
+ catalogs = /* @__PURE__ */ new Map();
1393
+ catalogInflight = /* @__PURE__ */ new Map();
1394
+ /** Catalog owners whose membership changed while a pull was in flight: one trailing refresh after it settles. */
1395
+ catalogStale = /* @__PURE__ */ new Set();
1396
+ openCatalogs = /* @__PURE__ */ new Set();
1397
+ catalogDebounce = /* @__PURE__ */ new Map();
1398
+ /**
1399
+ * Background jobs per session, last-wins from Session Controller's control
1400
+ * stream. An empty set is stored as an absent key, so absence and `[]` are
1401
+ * one representation.
1402
+ */
1403
+ jobsBySession = /* @__PURE__ */ new Map();
1404
+ selected;
1405
+ listSnapshotCache;
1406
+ /** Entry-identity cache (reference stability): list rebuilds reuse the previous entry
1407
+ * object when every field matches — wire refreshes mint all-new summary objects, so identity
1408
+ * must be recovered by value or every SessionListItem memo misses on every refresh. */
1409
+ entryCache = /* @__PURE__ */ new Map();
1410
+ itemsCache = [];
1411
+ notifier = new Notifier(() => {
1412
+ this.listSnapshotCache = this.buildListSnapshot();
1413
+ });
1414
+ /**
1415
+ * @param remote - generated Remote namespaces the Session cluster calls.
1416
+ * @param restoredSelection - persisted real-Session selection candidate.
1417
+ */
1418
+ constructor(remote, restoredSelection, restoredAddress) {
1419
+ this.remote = remote;
1420
+ this.selected = restoredSelection;
1421
+ if (restoredAddress !== void 0) this.addresses.set(restoredAddress.childSessionId, restoredAddress);
1422
+ this.listSnapshotCache = this.buildListSnapshot();
1423
+ }
1424
+ /**
1425
+ * Select a listed Session or a retained catalog-addressed child.
1426
+ * @param sessionId - listed or catalog-addressed Session id.
1427
+ */
1428
+ select(sessionId) {
1429
+ const address = this.navigationAddress(sessionId);
1430
+ if (!this.summaries.some((summary) => summary.sessionId === sessionId) && address === void 0) throw new Error(`sessions.select: unknown session ${sessionId}`);
1431
+ if (address !== void 0) this.addresses.set(sessionId, address);
1432
+ this.sessions.get(sessionId)?.configureSubagent(address, address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.parentAvailable);
1433
+ this.selected = sessionId;
1434
+ this.completedNotifications.delete(sessionId);
1435
+ this.refreshSubagents(sessionId);
1436
+ this.notifier.notifyNow();
1437
+ }
1438
+ /**
1439
+ * Select a healthy child through its durable direct-parent address.
1440
+ * @param address - catalog-derived parent and child ids.
1441
+ */
1442
+ selectSubagent(address) {
1443
+ const catalog = this.catalogs.get(address.parentSessionId);
1444
+ const entry = catalog?.entries.find((candidate) => candidate.id === address.childSessionId);
1445
+ if (entry === void 0 || entry.kind !== "child" || entry.mode !== address.mode) throw new Error(`sessions.selectSubagent: ${address.childSessionId} is not a healthy catalog child`);
1446
+ this.addresses.set(address.childSessionId, address);
1447
+ this.sessions.get(address.childSessionId)?.configureSubagent(address, catalog?.parentAvailable);
1448
+ this.selected = address.childSessionId;
1449
+ this.completedNotifications.delete(address.childSessionId);
1450
+ this.refreshSubagents(address.childSessionId);
1451
+ this.notifier.notifyNow();
1452
+ }
1453
+ /** Clear the selection (the layout falls to the no-session view state). */
1454
+ clearSelection() {
1455
+ this.selected = void 0;
1456
+ this.notifier.notifyNow();
1457
+ }
1458
+ /**
1459
+ * Return the durable catalog address retained for one child.
1460
+ * @param sessionId - possible addressed child id.
1461
+ * @returns The direct-parent address, when navigation discovered one.
1462
+ */
1463
+ subagentAddress(sessionId) {
1464
+ return this.addresses.get(sessionId);
1465
+ }
1466
+ /**
1467
+ * Resolve an address for breadcrumb navigation without retaining transport authority.
1468
+ * @param sessionId - possible child id in an already-loaded catalog.
1469
+ * @returns A retained or catalog-derived direct-parent address.
1470
+ */
1471
+ navigationAddress(sessionId) {
1472
+ const retained = this.addresses.get(sessionId);
1473
+ if (retained !== void 0) return retained;
1474
+ for (const [parentSessionId, catalog] of this.catalogs) {
1475
+ const child = catalog.entries.find((entry) => entry.kind === "child" && entry.id === sessionId);
1476
+ if (child?.kind === "child") return {
1477
+ parentSessionId,
1478
+ childSessionId: sessionId,
1479
+ mode: child.mode
1480
+ };
1481
+ }
1482
+ }
1483
+ /**
1484
+ * Drop a session instance (scope-prune companion: instance
1485
+ * and scope share one lifecycle). The host session log is the durable
1486
+ * truth — a later get() lazily rebuilds and open() backfills history.
1487
+ * @param sessionId - the session to drop.
1488
+ */
1489
+ async drop(sessionId) {
1490
+ const session = this.sessions.get(sessionId);
1491
+ this.sessions.delete(sessionId);
1492
+ if (session !== void 0) await this.startSessionDisposal(session);
1493
+ }
1494
+ /**
1495
+ * Stop owned timers and every remaining Session instance.
1496
+ * @returns when every Session Remote iterator has completed teardown.
1497
+ */
1498
+ async dispose() {
1499
+ for (const timer of this.catalogDebounce.values()) clearTimeout(timer);
1500
+ this.catalogDebounce.clear();
1501
+ this.catalogStale.clear();
1502
+ this.openCatalogs.clear();
1503
+ const sessions = [...this.sessions.values()];
1504
+ this.sessions.clear();
1505
+ for (const session of sessions) this.startSessionDisposal(session);
1506
+ await this.drainSessionDisposals();
1507
+ }
1508
+ startSessionDisposal(session) {
1509
+ const disposal = session.dispose();
1510
+ this.sessionDisposals.add(disposal);
1511
+ disposal.then(() => {
1512
+ this.sessionDisposals.delete(disposal);
1513
+ }, () => {
1514
+ this.sessionDisposals.delete(disposal);
1515
+ });
1516
+ return disposal;
1517
+ }
1518
+ async drainSessionDisposals() {
1519
+ while (this.sessionDisposals.size > 0) await Promise.allSettled([...this.sessionDisposals]);
1520
+ }
1521
+ /**
1522
+ * Lazy build: return the existing instance or construct one (no auto-open —
1523
+ * open is triggered by the container's select callback).
1524
+ * @param sessionId - the session to get.
1525
+ * @returns the resident instance.
1526
+ */
1527
+ get(sessionId) {
1528
+ let session = this.sessions.get(sessionId);
1529
+ if (session === void 0) {
1530
+ session = this.createSession(sessionId);
1531
+ this.sessions.set(sessionId, session);
1532
+ session.replaceControl(this.queues.get(sessionId) ?? []);
1533
+ const summary = this.summaries.find((s) => s.sessionId === sessionId);
1534
+ if (summary !== void 0) {
1535
+ session.handleBlank(summary.blank);
1536
+ session.handleRunning(summary.running);
1537
+ } else {
1538
+ const address = this.addresses.get(sessionId);
1539
+ const child = address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.entries.find((entry) => entry.kind === "child" && entry.id === sessionId);
1540
+ if (child?.kind === "child") {
1541
+ session.handleBlank(false);
1542
+ session.handleRunning(child.activity === "running");
1543
+ }
1544
+ }
1545
+ }
1546
+ return session;
1547
+ }
1548
+ createSession(sessionId) {
1549
+ const address = this.addresses.get(sessionId);
1550
+ const parentAvailable = address === void 0 ? void 0 : this.catalogs.get(address.parentSessionId)?.parentAvailable;
1551
+ return new Session(sessionId, this.remote, {
1552
+ ...address === void 0 ? {} : {
1553
+ address,
1554
+ ...catalogAvailability(parentAvailable)
1555
+ },
1556
+ onEngaged: (engaged) => {
1557
+ this.recordMutation({
1558
+ kind: "engaged",
1559
+ sessionId: engaged.sessionId
1560
+ });
1561
+ },
1562
+ projections: this.projectionStore(sessionId)
1563
+ });
1564
+ }
1565
+ /** Resident per-session projection store (create-on-demand; outlives instantiation). */
1566
+ projectionStore(sessionId) {
1567
+ let store = this.projectionStores.get(sessionId);
1568
+ if (store === void 0) {
1569
+ store = new ProjectionValueStore();
1570
+ store.subscribeAny(() => {
1571
+ this.notifier.markDirty();
1572
+ });
1573
+ this.projectionStores.set(sessionId, store);
1574
+ }
1575
+ return store;
1576
+ }
1577
+ /**
1578
+ * Refresh one direct-child catalog, reusing its in-flight request.
1579
+ * @param parentSessionId - catalog owner.
1580
+ */
1581
+ refreshSubagents(parentSessionId) {
1582
+ const existing = this.catalogInflight.get(parentSessionId);
1583
+ if (existing !== void 0) return existing.promise;
1584
+ const previous = this.catalogs.get(parentSessionId);
1585
+ const expandableRows = /* @__PURE__ */ new Set();
1586
+ const activityRows = /* @__PURE__ */ new Map();
1587
+ this.catalogs.set(parentSessionId, {
1588
+ entries: previous?.entries ?? [],
1589
+ ...previous?.parentAvailable === void 0 ? {} : { parentAvailable: previous.parentAvailable },
1590
+ state: "loading",
1591
+ error: null
1592
+ });
1593
+ this.notifier.markDirty();
1594
+ const operation = (async () => {
1595
+ try {
1596
+ const result = await this.remote.subagents.list(parentSessionId);
1597
+ if (result.ok) {
1598
+ const parentAvailable = this.catalogInflight.get(parentSessionId)?.parentAvailableOverride ?? result.value.parentAvailable;
1599
+ this.catalogs.set(parentSessionId, {
1600
+ ...result.value,
1601
+ entries: this.withCatalogMutations(result.value.entries, expandableRows, activityRows),
1602
+ parentAvailable,
1603
+ state: "ready",
1604
+ error: null
1605
+ });
1606
+ for (const [childId, address] of this.addresses) {
1607
+ if (address.parentSessionId !== parentSessionId) continue;
1608
+ this.sessions.get(childId)?.handleSubagentParentAvailable(parentAvailable);
1609
+ }
1610
+ } else this.catalogs.set(parentSessionId, {
1611
+ entries: this.withCatalogMutations(previous?.entries ?? [], expandableRows, activityRows),
1612
+ ...catalogAvailability(this.catalogInflight.get(parentSessionId)?.parentAvailableOverride ?? previous?.parentAvailable),
1613
+ state: "error",
1614
+ error: result.error
1615
+ });
1616
+ } catch (error) {
1617
+ if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) throw error;
1618
+ this.catalogs.set(parentSessionId, {
1619
+ entries: this.withCatalogMutations(previous?.entries ?? [], expandableRows, activityRows),
1620
+ ...catalogAvailability(this.catalogInflight.get(parentSessionId)?.parentAvailableOverride ?? previous?.parentAvailable),
1621
+ state: "error",
1622
+ error
1623
+ });
1624
+ } finally {
1625
+ this.catalogInflight.delete(parentSessionId);
1626
+ if (this.catalogStale.delete(parentSessionId)) this.refreshSubagents(parentSessionId);
1627
+ this.notifier.markDirty();
1628
+ }
1629
+ })();
1630
+ this.catalogInflight.set(parentSessionId, {
1631
+ promise: operation,
1632
+ expandableRows,
1633
+ activityRows,
1634
+ parentAvailableOverride: void 0
1635
+ });
1636
+ return operation;
1637
+ }
1638
+ /**
1639
+ * Mark whether a catalog menu is consuming live membership updates.
1640
+ * @param parentSessionId - catalog owner.
1641
+ * @param open - current menu state.
1642
+ */
1643
+ setSubagentCatalogOpen(parentSessionId, open) {
1644
+ if (open) {
1645
+ this.openCatalogs.add(parentSessionId);
1646
+ this.refreshSubagents(parentSessionId);
1647
+ } else {
1648
+ this.openCatalogs.delete(parentSessionId);
1649
+ const timer = this.catalogDebounce.get(parentSessionId);
1650
+ if (timer !== void 0) {
1651
+ clearTimeout(timer);
1652
+ this.catalogDebounce.delete(parentSessionId);
1653
+ }
1654
+ }
1655
+ }
1656
+ /** Full refresh via session.list (single-flight: an in-flight call is reused). */
1657
+ refreshList() {
1658
+ if (this.listInflight !== null) return this.listInflight;
1659
+ this.listState = "loading";
1660
+ this.listError = null;
1661
+ const established = this.summaries;
1662
+ const mutations = [];
1663
+ this.listMutations = mutations;
1664
+ this.notifier.markDirty();
1665
+ this.listInflight = (async () => {
1666
+ try {
1667
+ const result = await this.remote.session.list({});
1668
+ if (result.ok) {
1669
+ const baseline = this.listPhase === "pending" ? [...result.value.items] : mergeOrderedBaseline(established, result.value.items, (summary) => summary.sessionId);
1670
+ for (const s of baseline) if (!this.prevRunning.has(s.sessionId)) this.prevRunning.set(s.sessionId, s.running);
1671
+ let summaries = baseline;
1672
+ for (const mutation of mutations) {
1673
+ summaries = applyMutation(summaries, mutation);
1674
+ this.summaries = summaries;
1675
+ this.syncCompletedNotifications();
1676
+ }
1677
+ this.summaries = summaries;
1678
+ this.listState = "idle";
1679
+ this.listPhase = "ready";
1680
+ this.syncCompletedNotifications();
1681
+ for (const s of this.summaries) {
1682
+ const session = this.sessions.get(s.sessionId);
1683
+ if (session === void 0) continue;
1684
+ session.handleBlank(s.blank);
1685
+ session.handleRunning(s.running);
1686
+ }
1687
+ for (const s of result.value.items) {
1688
+ const block = s.projections;
1689
+ if (block === void 0) continue;
1690
+ const store = this.projectionStore(s.sessionId);
1691
+ const values = block.values;
1692
+ for (const key of Object.keys(values)) store.apply(key, values[key], block.asOfSeq);
1693
+ }
1694
+ } else {
1695
+ this.listState = "error";
1696
+ this.listError = result.error;
1697
+ }
1698
+ } catch (error) {
1699
+ if (!(0, _deepseek_ai_dsh_api_gateway_client.isRemoteFailure)(error)) throw error;
1700
+ this.listState = "error";
1701
+ this.listError = error;
1702
+ } finally {
1703
+ this.listMutations = null;
1704
+ this.listInflight = null;
1705
+ this.notifier.markDirty();
1706
+ }
1707
+ })();
1708
+ return this.listInflight;
1709
+ }
1710
+ /**
1711
+ * Search visible session message content without adding transient query
1712
+ * state to the list snapshot.
1713
+ * @param query - non-blank literal phrase.
1714
+ * @param signal - cancellation for superseded UI queries.
1715
+ * @returns the Host result or a folded transport error.
1716
+ */
1717
+ async search(query, signal) {
1718
+ const result = await this.remote.session.search({ query }, signal);
1719
+ if (!result.ok) return result;
1720
+ return {
1721
+ ok: true,
1722
+ value: {
1723
+ items: [...result.value.items],
1724
+ hasMore: result.value.hasMore
1725
+ }
1726
+ };
1727
+ }
1728
+ /**
1729
+ * Contract session.create; on success merge into summaries immediately (no
1730
+ * wait for the next refresh). A created session is blank by definition
1731
+ * (entity birth precedes the first message).
1732
+ * @param opts - target workspace or working directory, plus an optional caller-owned id.
1733
+ * @returns the create result.
1734
+ */
1735
+ async create(opts = {}) {
1736
+ const shared = opts.sessionId === void 0 ? {} : { sessionId: opts.sessionId };
1737
+ const payload = opts.workspaceId !== void 0 ? {
1738
+ workspaceId: opts.workspaceId,
1739
+ ...shared
1740
+ } : {
1741
+ ...opts.cwd === void 0 ? {} : { cwd: opts.cwd },
1742
+ ...shared
1743
+ };
1744
+ const result = await this.remote.session.create(payload);
1745
+ if (result.ok) this.recordMutation({
1746
+ kind: "upsert",
1747
+ summary: {
1748
+ sessionId: result.value.sessionId,
1749
+ updatedAt: Date.now(),
1750
+ running: false,
1751
+ blank: true,
1752
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {}
1753
+ }
1754
+ });
1755
+ else {
1756
+ const publishedSessionId = workspaceAttachSessionId(result.error);
1757
+ if (publishedSessionId !== void 0) this.recordMutation({
1758
+ kind: "upsert",
1759
+ summary: {
1760
+ sessionId: publishedSessionId,
1761
+ updatedAt: Date.now(),
1762
+ running: false,
1763
+ blank: true
1764
+ }
1765
+ });
1766
+ }
1767
+ return result;
1768
+ }
1769
+ /**
1770
+ * Contract session.fork; on success merge the child into summaries
1771
+ * immediately (same synchronous-addressability guarantee as create). The
1772
+ * child carries the source's history, so it is never blank; lineage rides
1773
+ * parentSessionId so the list nests it under its source. A child published
1774
+ * before Workspace attachment fails is also reconciled into the list.
1775
+ * @param opts - source session and the optional seq anchoring the cut.
1776
+ * @returns the fork result (the child session id).
1777
+ */
1778
+ async fork(opts) {
1779
+ const source = this.summaries.find((s) => s.sessionId === opts.sessionId);
1780
+ const result = await this.remote.session.fork({
1781
+ sessionId: opts.sessionId,
1782
+ ...opts.atSeq === void 0 ? {} : { atSeq: opts.atSeq }
1783
+ });
1784
+ const childId = result.ok ? result.value.sessionId : workspaceAttachSessionId(result.error);
1785
+ if (childId !== void 0) this.recordMutation({
1786
+ kind: "upsert",
1787
+ summary: {
1788
+ sessionId: childId,
1789
+ updatedAt: Date.now(),
1790
+ running: false,
1791
+ blank: false,
1792
+ parentSessionId: opts.sessionId,
1793
+ ...source?.cwd !== void 0 ? { cwd: source.cwd } : {}
1794
+ }
1795
+ });
1796
+ return result;
1797
+ }
1798
+ /**
1799
+ * Insert-or-enrich a locally synthesized summary: a new id prepends; an
1800
+ * existing entry only gains fields it lacks (the session-added frame and the
1801
+ * create() echo race — whichever lands second must fill the placeholder's
1802
+ * missing cwd/parentSessionId, never overwrite list-refresh data).
1803
+ */
1804
+ mergeSummary(summary) {
1805
+ this.recordMutation({
1806
+ kind: "upsert",
1807
+ summary
1808
+ });
1809
+ }
1810
+ /** Apply immediately and retain for replay when a list response is in flight. */
1811
+ recordMutation(mutation) {
1812
+ this.listMutations?.push(mutation);
1813
+ this.summaries = applyMutation(this.summaries, mutation);
1814
+ this.syncCompletedNotifications();
1815
+ this.notifier.markDirty();
1816
+ }
1817
+ /**
1818
+ * uSES subscription entry for useSessionList.
1819
+ * @param listener - change callback.
1820
+ * @returns the unsubscribe function.
1821
+ */
1822
+ subscribe(listener) {
1823
+ return this.notifier.subscribe(listener);
1824
+ }
1825
+ /**
1826
+ * Cached list snapshot (rebuilt lazily when dirty with no listeners).
1827
+ * @returns the cached reference (stable until the next flush).
1828
+ */
1829
+ getListSnapshot() {
1830
+ this.notifier.ensureFresh();
1831
+ return this.listSnapshotCache;
1832
+ }
1833
+ /**
1834
+ * Apply a complete control baseline or one later replacement frame.
1835
+ * @param frame - baseline or live control replacement from Session Controller.
1836
+ */
1837
+ handleControlFrame(frame) {
1838
+ if (frame.type === "baseline") {
1839
+ this.replaceControlBaseline(frame.value);
1840
+ return;
1841
+ }
1842
+ if (frame.type === "projection") {
1843
+ this.projectionStore(frame.sessionId).apply(frame.key, frame.value, frame.seq);
1844
+ this.notifier.markDirty();
1845
+ return;
1846
+ }
1847
+ if (frame.type === "jobs") {
1848
+ if (frame.jobs.length === 0) this.jobsBySession.delete(frame.sessionId);
1849
+ else this.jobsBySession.set(frame.sessionId, frame.jobs);
1850
+ this.notifier.markDirty();
1851
+ return;
1852
+ }
1853
+ this.queues.set(frame.sessionId, frame.items);
1854
+ this.sessions.get(frame.sessionId)?.handleControlFrame(frame);
1855
+ }
1856
+ replaceControlBaseline(baseline) {
1857
+ this.queues.clear();
1858
+ for (const [sessionId, items] of Object.entries(baseline.queues)) this.queues.set(sessionId, items);
1859
+ this.jobsBySession.clear();
1860
+ for (const [sessionId, jobs] of Object.entries(baseline.jobs)) if (jobs.length > 0) this.jobsBySession.set(sessionId, jobs);
1861
+ for (const [sessionId, block] of Object.entries(baseline.projections)) {
1862
+ const store = this.projectionStore(sessionId);
1863
+ store.truncate(block.asOfSeq);
1864
+ store.seed(block);
1865
+ }
1866
+ for (const [sessionId, session] of this.sessions) session.replaceControl(this.queues.get(sessionId) ?? []);
1867
+ this.notifier.markDirty();
1868
+ }
1869
+ /**
1870
+ * Apply one Session-list addition forwarded through `ctx.remote.$on`.
1871
+ * @param summary - current Host summary for the added Session.
1872
+ */
1873
+ handleSessionAdded(summary) {
1874
+ this.mergeSummary(summary);
1875
+ this.sessions.get(summary.sessionId)?.handleBlank(summary.blank);
1876
+ const projections = summary.projections;
1877
+ if (projections !== void 0) {
1878
+ const store = this.projectionStore(summary.sessionId);
1879
+ for (const [key, value] of Object.entries(projections.values)) store.apply(key, value, projections.asOfSeq);
1880
+ }
1881
+ if (summary.origin === "subagent" && summary.parentSessionId !== void 0) this.markCatalogParentExpandable(summary.parentSessionId);
1882
+ if (summary.parentSessionId !== void 0 && (this.selected === summary.parentSessionId || this.openCatalogs.has(summary.parentSessionId))) this.scheduleCatalogRefresh(summary.parentSessionId);
1883
+ }
1884
+ /**
1885
+ * Apply one Session removal forwarded through `ctx.remote.$on`.
1886
+ * @param sessionId - removed Session identity.
1887
+ */
1888
+ handleSessionRemoved(sessionId) {
1889
+ const durableSubagent = this.summaries.find((candidate) => candidate.sessionId === sessionId)?.origin === "subagent" || this.addresses.has(sessionId);
1890
+ this.recordMutation(durableSubagent ? {
1891
+ kind: "status",
1892
+ sessionId,
1893
+ running: false
1894
+ } : {
1895
+ kind: "remove",
1896
+ sessionId
1897
+ });
1898
+ this.updateCatalogActivity(sessionId, false);
1899
+ if (durableSubagent) this.sessions.get(sessionId)?.handleRunning(false);
1900
+ else this.sessions.get(sessionId)?.handleRemoved();
1901
+ this.queues.delete(sessionId);
1902
+ this.jobsBySession.delete(sessionId);
1903
+ if (!durableSubagent) this.projectionStores.delete(sessionId);
1904
+ const inflightCatalog = this.catalogInflight.get(sessionId);
1905
+ if (inflightCatalog !== void 0) {
1906
+ inflightCatalog.parentAvailableOverride = false;
1907
+ this.catalogStale.add(sessionId);
1908
+ }
1909
+ const ownedCatalog = this.catalogs.get(sessionId);
1910
+ if (ownedCatalog !== void 0 && ownedCatalog.parentAvailable) this.catalogs.set(sessionId, {
1911
+ ...ownedCatalog,
1912
+ parentAvailable: false
1913
+ });
1914
+ for (const [childId, address] of this.addresses) if (address.parentSessionId === sessionId) this.sessions.get(childId)?.handleSubagentParentAvailable(false);
1915
+ }
1916
+ /**
1917
+ * Apply one live Agent running-state change.
1918
+ * @param sessionId - Session whose Agent state changed.
1919
+ * @param running - current Agent running state.
1920
+ */
1921
+ handleSessionStatus(sessionId, running) {
1922
+ this.recordMutation({
1923
+ kind: "status",
1924
+ sessionId,
1925
+ running
1926
+ });
1927
+ this.sessions.get(sessionId)?.handleRunning(running);
1928
+ this.updateCatalogActivity(sessionId, running);
1929
+ }
1930
+ /**
1931
+ * Advance Session-list activity from one user-authored durable message.
1932
+ * @param sessionId - Session whose activity changed.
1933
+ * @param updatedAt - durable message timestamp.
1934
+ */
1935
+ handleSessionActivity(sessionId, updatedAt) {
1936
+ this.recordMutation({
1937
+ kind: "activity",
1938
+ sessionId,
1939
+ updatedAt
1940
+ });
1941
+ }
1942
+ /**
1943
+ * Surface one live Agent failure on an already-materialized Session.
1944
+ * @param sessionId - Session whose Agent failed.
1945
+ * @param message - caller-visible failure description.
1946
+ */
1947
+ handleSessionError(sessionId, message) {
1948
+ this.sessions.get(sessionId)?.handleAgentError(message);
1949
+ }
1950
+ /**
1951
+ * Repair one re-established Host-event generation with queryable baselines.
1952
+ * Opened Session follow streams resume independently through API Gateway.
1953
+ */
1954
+ handleConnected() {
1955
+ this.refreshList();
1956
+ const selectedAddress = this.selected === void 0 ? void 0 : this.addresses.get(this.selected);
1957
+ if (selectedAddress !== void 0) this.refreshSubagents(selectedAddress.parentSessionId);
1958
+ if (this.selected !== void 0) this.refreshSubagents(this.selected);
1959
+ for (const parentSessionId of this.openCatalogs) this.refreshSubagents(parentSessionId);
1960
+ }
1961
+ /** Debounce membership refetches while one parent catalog is selected or open. */
1962
+ scheduleCatalogRefresh(parentSessionId) {
1963
+ if (this.catalogDebounce.has(parentSessionId)) return;
1964
+ const timer = setTimeout(() => {
1965
+ this.catalogDebounce.delete(parentSessionId);
1966
+ if (this.catalogInflight.has(parentSessionId)) {
1967
+ this.catalogStale.add(parentSessionId);
1968
+ return;
1969
+ }
1970
+ this.refreshSubagents(parentSessionId);
1971
+ }, 50);
1972
+ this.catalogDebounce.set(parentSessionId, timer);
1973
+ }
1974
+ /** Apply one Agent-driver transition to loaded and in-flight catalogs. */
1975
+ updateCatalogActivity(childSessionId, running) {
1976
+ const activity = running ? "running" : "inactive";
1977
+ for (const inflight of this.catalogInflight.values()) inflight.activityRows.set(childSessionId, activity);
1978
+ let changed = false;
1979
+ for (const [parentSessionId, catalog] of this.catalogs) {
1980
+ if (!catalog.entries.some((entry) => entry.kind === "child" && entry.id === childSessionId && entry.activity !== activity)) continue;
1981
+ const entries = catalog.entries.map((entry) => {
1982
+ if (entry.kind !== "child" || entry.id !== childSessionId) return entry;
1983
+ return {
1984
+ ...entry,
1985
+ activity
1986
+ };
1987
+ });
1988
+ changed = true;
1989
+ this.catalogs.set(parentSessionId, {
1990
+ ...catalog,
1991
+ entries
1992
+ });
1993
+ }
1994
+ if (changed) this.notifier.markDirty();
1995
+ }
1996
+ /** Preserve and project a positive expandability hint after one direct subagent publishes. */
1997
+ markCatalogParentExpandable(parentSessionId) {
1998
+ this.applyCatalogParentExpandable(parentSessionId);
1999
+ for (const inflight of this.catalogInflight.values()) inflight.expandableRows.add(parentSessionId);
2000
+ }
2001
+ /** Apply one positive expandability hint to every loaded catalog containing that unique row id. */
2002
+ applyCatalogParentExpandable(parentSessionId) {
2003
+ let changed = false;
2004
+ for (const [catalogParentId, catalog] of this.catalogs) {
2005
+ if (!catalog.entries.some((entry) => entry.kind === "child" && entry.id === parentSessionId && !entry.hasChildren)) continue;
2006
+ const entries = catalog.entries.map((entry) => {
2007
+ if (entry.kind !== "child" || entry.id !== parentSessionId || entry.hasChildren) return entry;
2008
+ return {
2009
+ ...entry,
2010
+ hasChildren: true
2011
+ };
2012
+ });
2013
+ changed = true;
2014
+ this.catalogs.set(catalogParentId, {
2015
+ ...catalog,
2016
+ entries
2017
+ });
2018
+ }
2019
+ if (changed) this.notifier.markDirty();
2020
+ }
2021
+ /** Fold request-local row mutations into one catalog result before publication. */
2022
+ withCatalogMutations(entries, expandableRows, activityRows) {
2023
+ return entries.map((entry) => {
2024
+ if (entry.kind !== "child") return entry;
2025
+ const activity = activityRows.get(entry.id);
2026
+ if (!expandableRows.has(entry.id) && activity === void 0) return entry;
2027
+ return {
2028
+ ...entry,
2029
+ ...expandableRows.has(entry.id) ? { hasChildren: true } : {},
2030
+ ...activity === void 0 ? {} : { activity }
2031
+ };
2032
+ });
2033
+ }
2034
+ /**
2035
+ * Reconcile completion reminders against the latest summaries, eagerly after
2036
+ * every mutation and pull (a snapshot-build-time pass would collapse
2037
+ * consecutive status frames into one observation). A running→idle edge of a
2038
+ * non-selected session arms its reminder; running disarms it; removal drops
2039
+ * it. First observation only records the running bit — sessions already
2040
+ * idle at load get no reminder.
2041
+ */
2042
+ syncCompletedNotifications() {
2043
+ const seen = /* @__PURE__ */ new Set();
2044
+ for (const s of this.summaries) {
2045
+ seen.add(s.sessionId);
2046
+ const prev = this.prevRunning.get(s.sessionId);
2047
+ if (prev === void 0) {
2048
+ this.prevRunning.set(s.sessionId, s.running);
2049
+ continue;
2050
+ }
2051
+ if (prev && !s.running) {
2052
+ if (s.sessionId !== this.selected) this.completedNotifications.add(s.sessionId);
2053
+ } else if (s.running) this.completedNotifications.delete(s.sessionId);
2054
+ this.prevRunning.set(s.sessionId, s.running);
2055
+ }
2056
+ for (const id of this.prevRunning.keys()) if (!seen.has(id)) this.prevRunning.delete(id);
2057
+ for (const id of this.completedNotifications) if (!seen.has(id)) this.completedNotifications.delete(id);
2058
+ }
2059
+ buildListSnapshot() {
2060
+ const items = flattenLineage(this.summaries.map((summary) => {
2061
+ const projectionStore = this.projectionStores.get(summary.sessionId);
2062
+ const title = projectionStore?.get("title");
2063
+ const projectionValues = projectionStore?.values();
2064
+ return {
2065
+ ...summary,
2066
+ ...typeof title === "string" && title !== "" ? { title } : {},
2067
+ ...projectionValues === void 0 ? {} : { projectionValues }
2068
+ };
2069
+ }), this.completedNotifications).map((entry) => {
2070
+ const prev = this.entryCache.get(entry.sessionId);
2071
+ 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;
2072
+ this.entryCache.set(entry.sessionId, entry);
2073
+ return entry;
2074
+ });
2075
+ for (const id of this.entryCache.keys()) if (!items.some((e) => e.sessionId === id)) this.entryCache.delete(id);
2076
+ if (!(items.length === this.itemsCache.length && items.every((e, i) => e === this.itemsCache[i]))) this.itemsCache = items;
2077
+ const selected = this.selected;
2078
+ const current = selected !== void 0 && (items.some((item) => item.sessionId === selected) || this.addresses.has(selected)) ? selected : void 0;
2079
+ return {
2080
+ items: this.itemsCache,
2081
+ current,
2082
+ state: this.listState,
2083
+ phase: this.listPhase,
2084
+ error: this.listError,
2085
+ subagentsByParent: Object.fromEntries(this.catalogs),
2086
+ jobsBySession: Object.fromEntries(this.jobsBySession),
2087
+ currentAddress: current === void 0 ? void 0 : this.addresses.get(current)
2088
+ };
2089
+ }
2090
+ };
2091
+ /** Apply one list mutation without deriving display order. */
2092
+ function applyMutation(summaries, mutation) {
2093
+ switch (mutation.kind) {
2094
+ case "upsert": {
2095
+ const existing = summaries.find((summary) => summary.sessionId === mutation.summary.sessionId);
2096
+ if (existing === void 0) return [mutation.summary, ...summaries];
2097
+ const filled = {
2098
+ ...existing,
2099
+ blank: existing.blank && mutation.summary.blank,
2100
+ ...existing.cwd === void 0 && mutation.summary.cwd !== void 0 ? { cwd: mutation.summary.cwd } : {},
2101
+ ...existing.parentSessionId === void 0 && mutation.summary.parentSessionId !== void 0 ? { parentSessionId: mutation.summary.parentSessionId } : {},
2102
+ ...existing.origin === void 0 && mutation.summary.origin !== void 0 ? { origin: mutation.summary.origin } : {}
2103
+ };
2104
+ if (filled.cwd === existing.cwd && filled.parentSessionId === existing.parentSessionId && filled.origin === existing.origin && filled.blank === existing.blank) return [...summaries];
2105
+ return summaries.map((summary) => summary.sessionId === mutation.summary.sessionId ? filled : summary);
2106
+ }
2107
+ case "remove": return summaries.filter((summary) => summary.sessionId !== mutation.sessionId);
2108
+ case "status": return summaries.map((summary) => summary.sessionId === mutation.sessionId && (summary.running !== mutation.running || mutation.running && summary.blank) ? {
2109
+ ...summary,
2110
+ running: mutation.running,
2111
+ blank: summary.blank && !mutation.running
2112
+ } : summary);
2113
+ case "activity": return summaries.map((summary) => summary.sessionId === mutation.sessionId && mutation.updatedAt > summary.updatedAt ? {
2114
+ ...summary,
2115
+ updatedAt: mutation.updatedAt
2116
+ } : summary);
2117
+ case "engaged": return summaries.map((summary) => summary.sessionId === mutation.sessionId && summary.blank ? {
2118
+ ...summary,
2119
+ blank: false
2120
+ } : summary);
2121
+ }
2122
+ }
2123
+ /** Temporary source-plane bridge while the Host contract and client project build independently. */
2124
+ function workspaceAttachSessionId(error) {
2125
+ return error.code === "session/workspace-attach-failed" ? error.details.sessionId : void 0;
2126
+ }
2127
+ //#endregion
2128
+ //#region lib/types/client/sessions/service.js
2129
+ /** Structured session-create failure. */
2130
+ var SessionCreateError = class extends Error {
2131
+ rpcError;
2132
+ requestedSessionId;
2133
+ name = "SessionCreateError";
2134
+ /**
2135
+ * @param rpcError - Host business or folded transport error.
2136
+ * @param requestedSessionId - caller-preallocated id used for later stream/list reconciliation.
2137
+ */
2138
+ constructor(rpcError, requestedSessionId) {
2139
+ super(`session create failed: ${rpcError.code}: ${rpcError.message}`);
2140
+ this.rpcError = rpcError;
2141
+ this.requestedSessionId = requestedSessionId;
2142
+ }
2143
+ };
2144
+ /** Structured session-fork failure. */
2145
+ var SessionForkError = class extends Error {
2146
+ rpcError;
2147
+ sourceSessionId;
2148
+ name = "SessionForkError";
2149
+ /**
2150
+ * @param rpcError - Host business or folded transport error.
2151
+ * @param sourceSessionId - the session the fork was cut from.
2152
+ */
2153
+ constructor(rpcError, sourceSessionId) {
2154
+ super(`session fork failed: ${rpcError.code}: ${rpcError.message}`);
2155
+ this.rpcError = rpcError;
2156
+ this.sourceSessionId = sourceSessionId;
2157
+ }
2158
+ };
2159
+ /**
2160
+ * Display title projection: durable title, project directory basename, then
2161
+ * the raw id.
2162
+ */
2163
+ function displayTitleOf(title, cwd, id) {
2164
+ if (title !== void 0) return title;
2165
+ if (cwd !== void 0 && cwd !== "") {
2166
+ const base = workspaceTitleOf(cwd);
2167
+ if (base !== "") return base;
2168
+ }
2169
+ return id;
2170
+ }
2171
+ /**
2172
+ * Increment a trailing fork number while preserving its half-width or
2173
+ * full-width parentheses; an unnumbered title starts with ` (1)`.
2174
+ * @param title - source session's durable title.
2175
+ * @returns the title assigned to the fork child.
2176
+ */
2177
+ function increasedForkTitle(title) {
2178
+ const ascii = /^(.*?)\((\d+)\)$/u.exec(title);
2179
+ if (ascii?.[1] !== void 0 && ascii[2] !== void 0) return `${ascii[1]}(${BigInt(ascii[2]) + 1n})`;
2180
+ const fullWidth = /^(.*?)((\d+))$/u.exec(title);
2181
+ if (fullWidth?.[1] !== void 0 && fullWidth[2] !== void 0) return `${fullWidth[1]}(${BigInt(fullWidth[2]) + 1n})`;
2182
+ return `${title} (1)`;
2183
+ }
2184
+ /** Root sessions service: list store, current selection, object-layer manager, scope tree, bindings, and breadcrumb routes. */
2185
+ var ClientSessions = class {
2186
+ rootCtx;
2187
+ /**
2188
+ * The wire schema's own result bound, re-exposed for presentation plugins as
2189
+ * injected data. Not per-connection state: the `session.search` response
2190
+ * schema caps `items` at this constant, so every transport (fixture included)
2191
+ * reports the same number.
2192
+ */
2193
+ searchResultLimit = 20;
2194
+ /** List snapshot store (list RPC + host stream increments; re-pulled on reconnect) — the useSessions standard feed, current included. */
2195
+ list;
2196
+ /** The object-layer instance cluster and frame dispatch entry. */
2197
+ manager;
2198
+ /**
2199
+ * Persisted selection cell (the durable half of `list.current`). Private on
2200
+ * purpose: reads go through the list snapshot; writes through {@link
2201
+ * ClientSessions.open} / {@link ClientSessions.clear}. Projection
2202
+ * validates it against the live list instead of destructively pruning, so a
2203
+ * selection survives transient list states (reconnect re-pull) and
2204
+ * resurfaces when its session returns.
2205
+ */
2206
+ selection;
2207
+ scopes = /* @__PURE__ */ new Map();
2208
+ /** In-flight scope drops remain here after records leave `scopes`, so root disposal can await quiescence. */
2209
+ scopeDrops = /* @__PURE__ */ new Set();
2210
+ /**
2211
+ * The staged session id — follows `list.current` exactly, holding its last
2212
+ * defined value across masked gaps (a transiently absent selection blanks
2213
+ * `current` without moving the stage, so reconnect re-pulls and removals
2214
+ * keep the staged scope's frozen view alive until the stage moves on).
2215
+ */
2216
+ watched;
2217
+ /** Removed-while-staged sessions whose teardown waits for the stage to move away. */
2218
+ deferredRemovals = /* @__PURE__ */ new Set();
2219
+ /**
2220
+ * @param ctx - client root context (scope fibers mount under it).
2221
+ * @param remote - generated Remote namespaces shared with every Session.
2222
+ */
2223
+ constructor(rootCtx, remote) {
2224
+ this.rootCtx = rootCtx;
2225
+ this.selection = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({}, { persist: { name: "dsh.sessions.current" } });
2226
+ const restored = this.selection.getSnapshot();
2227
+ this.manager = new SessionManager(remote, restored.sessionId, restored.subagentAddress);
2228
+ this.list = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
2229
+ ids: [],
2230
+ byId: {},
2231
+ current: void 0,
2232
+ phase: "pending",
2233
+ subagentsByParent: {},
2234
+ jobsBySession: {},
2235
+ currentAddress: void 0
2236
+ });
2237
+ const disposeManagerProjection = this.manager.subscribe(() => {
2238
+ this.projectList();
2239
+ });
2240
+ const disposeStageFollower = this.list.subscribe(() => {
2241
+ this.followCurrent();
2242
+ });
2243
+ rootCtx.effect(() => async () => {
2244
+ disposeStageFollower();
2245
+ disposeManagerProjection();
2246
+ const scopes = [...this.scopes];
2247
+ this.scopes.clear();
2248
+ this.deferredRemovals.clear();
2249
+ this.watched = void 0;
2250
+ for (const [id, record] of scopes) this.startScopeDrop(id, record);
2251
+ await this.drainScopeDrops();
2252
+ await this.manager.dispose();
2253
+ }, "session-controller.client.sessions");
2254
+ rootCtx.reflect.provide("sessions", this, void 0);
2255
+ }
2256
+ /**
2257
+ * Select a listed or retained catalog-addressed session as current.
2258
+ * @param id - listed or addressed session id.
2259
+ */
2260
+ open(id) {
2261
+ this.manager.select(id);
2262
+ }
2263
+ /**
2264
+ * Open a healthy catalog child through its direct-parent address.
2265
+ * @param address - catalog-derived parent and child ids.
2266
+ */
2267
+ openSubagent(address) {
2268
+ this.manager.selectSubagent(address);
2269
+ }
2270
+ /**
2271
+ * Resolve an already discovered direct-parent address without opening it.
2272
+ * Feature plugins use this to avoid Agent-bound RPCs in persisted child views.
2273
+ * @param id - possible addressed child id.
2274
+ * @returns The retained address, when present.
2275
+ */
2276
+ subagentAddress(id) {
2277
+ return this.manager.subagentAddress(id);
2278
+ }
2279
+ /**
2280
+ * Inform the Session Controller whether a catalog menu is consuming membership updates.
2281
+ * @param parentSessionId - selected parent.
2282
+ * @param open - menu state.
2283
+ */
2284
+ setSubagentCatalogOpen(parentSessionId, open) {
2285
+ this.manager.setSubagentCatalogOpen(parentSessionId, open);
2286
+ }
2287
+ /**
2288
+ * Refresh one direct-child catalog.
2289
+ * @param parentSessionId - catalog owner.
2290
+ */
2291
+ refreshSubagents(parentSessionId) {
2292
+ return this.manager.refreshSubagents(parentSessionId);
2293
+ }
2294
+ /**
2295
+ * Clear the current selection so the layout shows the no-session empty
2296
+ * state (new-session affordance and the workspace preselection flow).
2297
+ * Wipes the persisted selection too — a reload stays on empty until the
2298
+ * user opens or starts a session. The staged scope keeps its frozen view
2299
+ * per the masked-gap contract until the next open() moves the stage.
2300
+ */
2301
+ clear() {
2302
+ this.manager.clearSelection();
2303
+ }
2304
+ /**
2305
+ * Refresh the real Session baseline, reusing an in-flight pull.
2306
+ * @returns completion of the current or newly started baseline pull.
2307
+ */
2308
+ refresh() {
2309
+ return this.manager.refreshList();
2310
+ }
2311
+ /**
2312
+ * Search the Host's visible message-content index. Results stay
2313
+ * request-local; the list snapshot remains the metadata authority.
2314
+ * @param query - non-blank literal phrase.
2315
+ * @param signal - cancellation for a superseded search.
2316
+ * @returns bounded results or a business/transport error.
2317
+ */
2318
+ search(query, signal) {
2319
+ return this.manager.search(query, signal);
2320
+ }
2321
+ /**
2322
+ * Apply one Session Controller live-control frame.
2323
+ * @param frame - baseline or live control replacement.
2324
+ */
2325
+ handleControlFrame(frame) {
2326
+ this.manager.handleControlFrame(frame);
2327
+ }
2328
+ /**
2329
+ * Apply one remotely forwarded Session-list addition.
2330
+ * @param summary - current Host summary for the added Session.
2331
+ */
2332
+ handleSessionAdded(summary) {
2333
+ this.manager.handleSessionAdded(summary);
2334
+ }
2335
+ /**
2336
+ * Apply one remotely forwarded Session removal.
2337
+ * @param sessionId - removed Session identity.
2338
+ */
2339
+ handleSessionRemoved(sessionId) {
2340
+ this.manager.handleSessionRemoved(sessionId);
2341
+ }
2342
+ /**
2343
+ * Apply one remotely forwarded running-state change.
2344
+ * @param args - Session identity and current Agent running state.
2345
+ */
2346
+ handleSessionStatus(...args) {
2347
+ this.manager.handleSessionStatus(...args);
2348
+ }
2349
+ /**
2350
+ * Apply one remotely forwarded list-activity change.
2351
+ * @param args - Session identity and durable activity timestamp.
2352
+ */
2353
+ handleSessionActivity(...args) {
2354
+ this.manager.handleSessionActivity(...args);
2355
+ }
2356
+ /**
2357
+ * Apply one remotely forwarded Agent failure.
2358
+ * @param args - Session identity and caller-visible failure description.
2359
+ */
2360
+ handleSessionError(...args) {
2361
+ this.manager.handleSessionError(...args);
2362
+ }
2363
+ /** Rebuild the Session baseline and every opened window after connection. */
2364
+ handleConnected() {
2365
+ this.manager.handleConnected();
2366
+ }
2367
+ /**
2368
+ * Create a session on the host. Resolution guarantee: by the time the
2369
+ * promise resolves, the created session is in the list store and
2370
+ * {@link ClientSessions.binding} resolves it — callers (New Session
2371
+ * draft hand-off) may address the scope synchronously, without waiting a
2372
+ * notifier flush. The synchronous projection below makes this structural
2373
+ * rather than an accident of microtask ordering.
2374
+ * @param opts - target workspace or directory and an optional preallocated id.
2375
+ * @returns the new session id.
2376
+ * @throws {SessionCreateError} with the requested id.
2377
+ */
2378
+ async create(opts = {}) {
2379
+ const result = await this.manager.create(opts);
2380
+ if (!result.ok) throw new SessionCreateError(result.error, opts.sessionId);
2381
+ this.projectList();
2382
+ return result.value.sessionId;
2383
+ }
2384
+ /**
2385
+ * Fork a session from a completed-turn prefix of the source (same
2386
+ * synchronous-addressability guarantee as {@link ClientSessions.create}:
2387
+ * on resolution the child is in the list store and open() can target it).
2388
+ * @param opts - source session id, the optional event seq anchoring the
2389
+ * cut (the boundary is the first turn/end at or after it; an in-log
2390
+ * anchor in an open turn is unavailable rather than clipped backward),
2391
+ * and whether to increment an inherited durable title before resolving.
2392
+ * A fractional anchor floors to a real event seq: the frozen nodes of an
2393
+ * interrupted turn carry flow-ordering seqs between two events, and the
2394
+ * wire takes integers only.
2395
+ * @returns the child session id.
2396
+ * @throws {SessionForkError} with the source id.
2397
+ * @throws {Error} when a requested child-title rename fails after creation.
2398
+ */
2399
+ async fork(opts) {
2400
+ const sourceTitle = opts.increaseTitle ? this.list.getSnapshot().byId[opts.sessionId]?.title : void 0;
2401
+ const result = await this.manager.fork({
2402
+ sessionId: opts.sessionId,
2403
+ ...opts.atSeq === void 0 ? {} : { atSeq: Math.floor(opts.atSeq) }
2404
+ });
2405
+ if (!result.ok) throw new SessionForkError(result.error, opts.sessionId);
2406
+ this.projectList();
2407
+ const childId = result.value.sessionId;
2408
+ if (sourceTitle !== void 0) {
2409
+ const child = this.binding(childId)?.session;
2410
+ if (child === void 0) throw new Error(`fork child "${childId}" is not locally addressable`);
2411
+ const renamed = await child.rename(increasedForkTitle(sourceTitle));
2412
+ if (!renamed.ok) throw new Error(`fork child rename failed: ${renamed.error.code}: ${renamed.error.message}`);
2413
+ }
2414
+ return childId;
2415
+ }
2416
+ /**
2417
+ * Resolve an Agent-scoped context view (use-and-discard).
2418
+ * @param id - session id (the agent identity — 1:1 same axis).
2419
+ * @returns scoped ctx, or undefined for a session neither listed nor already scoped.
2420
+ */
2421
+ scope(id) {
2422
+ return this.resolve(id)?.ctx;
2423
+ }
2424
+ /**
2425
+ * Materialize the Agent scope named by a validated Host Remote Event.
2426
+ * The first successful Session-list baseline becomes authoritative for its
2427
+ * lifetime; until then, transport streams may address the scope in either
2428
+ * arrival order.
2429
+ * @param id - Host-projected Agent identity (the matching Session id).
2430
+ * @returns the identity-stable Agent Context.
2431
+ */
2432
+ resolveAgentScope(id) {
2433
+ return (this.scopes.get(id) ?? this.materializeScope(id)).ctx;
2434
+ }
2435
+ /**
2436
+ * Read the Agent scope tag off a context. Service-method boundary: fetch
2437
+ * bundles must reach scope resolution through ctx.sessions — a cross-bundle
2438
+ * value import of the standalone helper would inline a second module
2439
+ * instance whose private tag Symbol never matches.
2440
+ * @param ctx - any client context.
2441
+ * @returns the session id, or undefined on root contexts.
2442
+ */
2443
+ scopeOf(ctx) {
2444
+ return scopeOf(ctx);
2445
+ }
2446
+ /**
2447
+ * Resolve the business Session behind an Agent-scoped context — the one
2448
+ * hop every scoped consumer (event listeners, per-session controllers)
2449
+ * takes from ctx-space into object-space (the client mirror of host
2450
+ * `agent.session`). Same service-method boundary as
2451
+ * {@link ClientSessions.scopeOf}.
2452
+ * @param ctx - an Agent-scoped context.
2453
+ * @returns the session face, or undefined when the ctx is untagged or its scope was pruned.
2454
+ */
2455
+ sessionOf(ctx) {
2456
+ const id = scopeOf(ctx);
2457
+ if (id === void 0) return void 0;
2458
+ return this.scopes.get(id)?.binding.session;
2459
+ }
2460
+ /**
2461
+ * Resolve the stable session binding (scope-addressed assembly feed). Pure
2462
+ * resolution — no staging, no window side effects.
2463
+ * @param id - session id.
2464
+ * @returns binding, or undefined for a session neither listed nor already scoped.
2465
+ */
2466
+ binding(id) {
2467
+ return this.resolve(id)?.binding;
2468
+ }
2469
+ /**
2470
+ * Move the stage to the list's current session: sweep teardowns deferred
2471
+ * behind the previous occupant and pull the new occupant's history window.
2472
+ * Staging IS the open signal — the window opens ⟺ the session is on stage
2473
+ * — and open() is idempotent (an in-flight or completed open no-ops; a
2474
+ * failed one retries the next time current is touched).
2475
+ */
2476
+ followCurrent() {
2477
+ const snapshot = this.list.getSnapshot();
2478
+ const current = snapshot.current;
2479
+ if (current === void 0 || snapshot.byId[current] === void 0 || current === this.watched) return;
2480
+ this.watched = current;
2481
+ this.sweepDeferred();
2482
+ const record = this.resolve(current);
2483
+ /* v8 ignore next 3 -- defensive: current is always a listed id (open()
2484
+ * validates and the projection masks absent selections), so resolve
2485
+ * cannot miss; kept so a future current writer cannot crash the notify. */
2486
+ if (record !== void 0) {
2487
+ record.session.open();
2488
+ this.manager.refreshSubagents(current);
2489
+ }
2490
+ }
2491
+ /**
2492
+ * Lazily mint the scope + binding for an eligible session. Eligibility and
2493
+ * prune share one predicate: listed on the host or selected
2494
+ * through a retained subagent address. Breadcrumb-only ancestors remain
2495
+ * summary data and do not keep scopes alive.
2496
+ */
2497
+ resolve(id) {
2498
+ const existing = this.scopes.get(id);
2499
+ if (existing !== void 0) return existing;
2500
+ if (!this.eligible(id)) return void 0;
2501
+ return this.materializeScope(id);
2502
+ }
2503
+ /** Materialize one scope after its caller establishes that the id may be addressed. */
2504
+ materializeScope(id) {
2505
+ const { fiber, ctx } = createScope(this.rootCtx, id);
2506
+ const session = this.manager.get(id);
2507
+ session.bindScope(ctx);
2508
+ const record = {
2509
+ fiber,
2510
+ ctx,
2511
+ binding: {
2512
+ sessionId: id,
2513
+ session,
2514
+ eventSource: session.eventSource,
2515
+ ctx
2516
+ },
2517
+ session
2518
+ };
2519
+ this.scopes.set(id, record);
2520
+ return record;
2521
+ }
2522
+ /** The one aliveness predicate shared by scope mint and prune: host-listed or currently addressed. */
2523
+ eligible(id) {
2524
+ const { ids, current } = this.list.getSnapshot();
2525
+ return current === id || ids.includes(id);
2526
+ }
2527
+ /** Project the manager's list snapshot into the store (title derivation is display-only). */
2528
+ projectList() {
2529
+ const { items, current, phase, subagentsByParent, jobsBySession, currentAddress } = this.manager.getListSnapshot();
2530
+ const ids = [];
2531
+ const byId = {};
2532
+ for (const entry of items) {
2533
+ ids.push(entry.sessionId);
2534
+ byId[entry.sessionId] = {
2535
+ id: entry.sessionId,
2536
+ displayTitle: displayTitleOf(entry.title, entry.cwd, entry.sessionId),
2537
+ running: entry.running,
2538
+ ...entry.completed ? { completed: true } : {},
2539
+ blank: entry.blank,
2540
+ updatedAt: entry.updatedAt,
2541
+ ...entry.projectionValues === void 0 ? {} : { projectionValues: entry.projectionValues },
2542
+ ...entry.title !== void 0 ? { title: entry.title } : {},
2543
+ ...entry.cwd !== void 0 ? { cwd: entry.cwd } : {},
2544
+ ...entry.parentSessionId !== void 0 ? { parentId: entry.parentSessionId } : {},
2545
+ ...entry.origin !== void 0 ? { origin: entry.origin } : {}
2546
+ };
2547
+ }
2548
+ if (current !== void 0 && currentAddress !== void 0) {
2549
+ const seen = /* @__PURE__ */ new Set();
2550
+ let address = currentAddress;
2551
+ while (address !== void 0 && !seen.has(address.childSessionId)) {
2552
+ const childId = address.childSessionId;
2553
+ seen.add(childId);
2554
+ const child = subagentsByParent[address.parentSessionId]?.entries.find((entry) => entry.kind === "child" && entry.id === childId);
2555
+ if (child?.kind !== "child") break;
2556
+ const displayTitle = child.label ?? childId;
2557
+ const summary = byId[childId];
2558
+ if (summary === void 0) byId[childId] = {
2559
+ id: childId,
2560
+ displayTitle,
2561
+ parentId: address.parentSessionId,
2562
+ origin: "subagent",
2563
+ running: child.activity === "running",
2564
+ blank: false,
2565
+ updatedAt: 0
2566
+ };
2567
+ else if (summary.displayTitle !== displayTitle) byId[childId] = {
2568
+ ...summary,
2569
+ displayTitle
2570
+ };
2571
+ const parent = byId[address.parentSessionId];
2572
+ if (parent !== void 0 && parent.origin !== "subagent") break;
2573
+ address = this.manager.navigationAddress(address.parentSessionId);
2574
+ }
2575
+ }
2576
+ const persisted = this.selection.getSnapshot().sessionId;
2577
+ if (current === void 0) {
2578
+ if (persisted !== void 0) this.selection.set({});
2579
+ } 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({
2580
+ sessionId: current,
2581
+ ...currentAddress === void 0 ? {} : { subagentAddress: currentAddress }
2582
+ });
2583
+ this.list.set({
2584
+ ids,
2585
+ byId,
2586
+ current,
2587
+ phase,
2588
+ subagentsByParent,
2589
+ jobsBySession,
2590
+ currentAddress
2591
+ });
2592
+ this.pruneScopes();
2593
+ }
2594
+ /** Tear down scope + instance for no-longer-eligible sessions off stage; the staged one defers until the stage moves. */
2595
+ pruneScopes() {
2596
+ if (this.list.getSnapshot().phase === "pending") return;
2597
+ for (const [id, record] of this.scopes) {
2598
+ if (this.eligible(id)) continue;
2599
+ if (id === this.watched) {
2600
+ this.deferredRemovals.add(id);
2601
+ continue;
2602
+ }
2603
+ this.scopes.delete(id);
2604
+ this.deferredRemovals.delete(id);
2605
+ this.startScopeDrop(id, record);
2606
+ }
2607
+ }
2608
+ startScopeDrop(id, record) {
2609
+ const drop = this.dropScope(id, record);
2610
+ this.scopeDrops.add(drop);
2611
+ drop.then(() => {
2612
+ this.scopeDrops.delete(drop);
2613
+ }, () => {
2614
+ this.scopeDrops.delete(drop);
2615
+ });
2616
+ }
2617
+ async drainScopeDrops() {
2618
+ while (this.scopeDrops.size > 0) await Promise.allSettled([...this.scopeDrops]);
2619
+ }
2620
+ /**
2621
+ * One teardown for the whole per-session axis: the scope
2622
+ * fiber (cascading every actx-registered effect: input shell, slash
2623
+ * controller, popup, plugin stores, listeners), the session-keyed slot
2624
+ * registrations and the Session instance itself — the host session log is the
2625
+ * durable truth, a reopen lazily rebuilds and backfills via open().
2626
+ */
2627
+ async dropScope(id, record) {
2628
+ record.session.unbindScope();
2629
+ await Promise.allSettled([record.fiber.dispose(), this.manager.drop(id)]);
2630
+ }
2631
+ /** Run deferred teardowns whose session is no longer staged (called when the stage moves). */
2632
+ sweepDeferred() {
2633
+ for (const id of [...this.deferredRemovals]) {
2634
+ /* v8 ignore next -- defensive: only the staged id ever defers, and every
2635
+ * stage move sweeps first, so the set cannot contain the id the stage just
2636
+ * moved to; kept as a guard against future extra sweep call sites. */
2637
+ if (id === this.watched) continue;
2638
+ if (this.eligible(id)) {
2639
+ this.deferredRemovals.delete(id);
2640
+ continue;
2641
+ }
2642
+ const record = this.scopes.get(id);
2643
+ this.deferredRemovals.delete(id);
2644
+ /* v8 ignore next -- defensive: prune deletes a scope and its deferral
2645
+ * together, so a deferred id always still owns its record; kept so a
2646
+ * future teardown path cannot double-dispose. */
2647
+ if (record !== void 0) {
2648
+ this.scopes.delete(id);
2649
+ this.startScopeDrop(id, record);
2650
+ }
2651
+ }
2652
+ }
2653
+ };
2654
+ //#endregion
2655
+ //#region lib/types/client/index.js
2656
+ /** Client Session object layer, Agent scopes, and Remote lifecycle wiring. */
2657
+ /** Required Remote and Context projection services. */
2658
+ const inject = [
2659
+ "typert",
2660
+ "remote",
2661
+ "remote.commands",
2662
+ "remote.session",
2663
+ "remote.subagents"
2664
+ ];
2665
+ /**
2666
+ * Install Client Session state and its reconnecting control stream.
2667
+ * @param ctx - Client Cordis context.
2668
+ */
2669
+ function apply(ctx) {
2670
+ const remotes = ctx.remote;
2671
+ const sessions = new ClientSessions(ctx, remotes);
2672
+ ctx.remote.$on("api-session/added", (summary) => {
2673
+ sessions.handleSessionAdded(summary);
2674
+ });
2675
+ ctx.remote.$on("api-session/removed", (sessionId) => {
2676
+ sessions.handleSessionRemoved(sessionId);
2677
+ });
2678
+ ctx.remote.$on("api-session/status", (sessionId, running) => {
2679
+ sessions.handleSessionStatus(sessionId, running);
2680
+ });
2681
+ ctx.remote.$on("api-session/activity", (sessionId, updatedAt) => {
2682
+ sessions.handleSessionActivity(sessionId, updatedAt);
2683
+ });
2684
+ ctx.remote.$on("api-session/error", (sessionId, message) => {
2685
+ sessions.handleSessionError(sessionId, message);
2686
+ });
2687
+ const control = createSessionControlStream(remotes, {
2688
+ accept: (frame) => {
2689
+ sessions.handleControlFrame(frame);
2690
+ },
2691
+ failed: (error) => {
2692
+ console.error("[session-controller] control stream failed:", error);
2693
+ }
2694
+ });
2695
+ control.start();
2696
+ ctx.on("connection/reset", () => {
2697
+ sessions.handleConnected();
2698
+ });
2699
+ if (ctx.remote.$host.home !== void 0) sessions.handleConnected();
2700
+ ctx.typert.contexts.registerClient("agent", {
2701
+ identity: (candidate) => sessions.scopeOf(candidate),
2702
+ resolve: (sessionId) => sessions.resolveAgentScope(sessionId)
2703
+ });
2704
+ ctx.effect(() => async () => {
2705
+ await control.dispose();
2706
+ }, "session-controller.client.control");
2707
+ }
2708
+ //#endregion
2709
+ exports.MutableSessionEventSource = MutableSessionEventSource;
2710
+ exports.SESSION_SEARCH_RESULT_LIMIT = SESSION_SEARCH_RESULT_LIMIT;
2711
+ exports.SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS = SESSION_SEARCH_SNIPPET_MAX_CODE_POINTS;
2712
+ exports.SessionCreateError = SessionCreateError;
2713
+ exports.SessionEventStream = SessionEventStream;
2714
+ exports.SessionForkError = SessionForkError;
2715
+ exports.apply = apply;
2716
+ exports.createScope = createScope;
2717
+ exports.createSessionControlStream = createSessionControlStream;
2718
+ exports.inject = inject;
2719
+ exports.scopeOf = scopeOf;
2720
+ return module.exports;
2721
+ }
2722
+ });
2723
+
2724
+ //# sourceMappingURL=client.js.map