@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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +74 -0
- package/README.zh.md +74 -0
- package/lib/client.js +2724 -0
- package/lib/index.js +2826 -0
- package/lib/invariant.js +13 -0
- package/lib/typert.host.d.ts +3 -0
- package/lib/typert.host.js +2574 -0
- package/lib/typert.remote-client.d.ts +67 -0
- package/lib/typert.remote-client.js +1119 -0
- package/lib/types/agent.d.ts +156 -0
- package/lib/types/agent.js +537 -0
- package/lib/types/catalog.d.ts +11 -0
- package/lib/types/catalog.js +58 -0
- package/lib/types/client/contract/events.d.ts +71 -0
- package/lib/types/client/contract/events.js +91 -0
- package/lib/types/client/contract/session.d.ts +150 -0
- package/lib/types/client/contract/session.js +2 -0
- package/lib/types/client/contract/sessions.d.ts +125 -0
- package/lib/types/client/contract/sessions.js +2 -0
- package/lib/types/client/contract/snapshot.d.ts +82 -0
- package/lib/types/client/contract/snapshot.js +2 -0
- package/lib/types/client/index.d.ts +30 -0
- package/lib/types/client/index.js +48 -0
- package/lib/types/client/ordered-baseline.d.ts +12 -0
- package/lib/types/client/ordered-baseline.js +41 -0
- package/lib/types/client/scope.d.ts +36 -0
- package/lib/types/client/scope.js +54 -0
- package/lib/types/client/sessions/history-records.d.ts +22 -0
- package/lib/types/client/sessions/history-records.js +31 -0
- package/lib/types/client/sessions/lineage.d.ts +38 -0
- package/lib/types/client/sessions/lineage.js +56 -0
- package/lib/types/client/sessions/manager.d.ts +280 -0
- package/lib/types/client/sessions/manager.js +894 -0
- package/lib/types/client/sessions/notifier.d.ts +39 -0
- package/lib/types/client/sessions/notifier.js +98 -0
- package/lib/types/client/sessions/projection-store.d.ts +108 -0
- package/lib/types/client/sessions/projection-store.js +129 -0
- package/lib/types/client/sessions/queue-mirror.d.ts +26 -0
- package/lib/types/client/sessions/queue-mirror.js +61 -0
- package/lib/types/client/sessions/remotes.d.ts +30 -0
- package/lib/types/client/sessions/remotes.js +8 -0
- package/lib/types/client/sessions/service.d.ts +349 -0
- package/lib/types/client/sessions/service.js +574 -0
- package/lib/types/client/sessions/session.d.ts +294 -0
- package/lib/types/client/sessions/session.js +711 -0
- package/lib/types/client/time-zone.d.ts +8 -0
- package/lib/types/client/time-zone.js +14 -0
- package/lib/types/client/transport.d.ts +73 -0
- package/lib/types/client/transport.js +106 -0
- package/lib/types/commands.d.ts +69 -0
- package/lib/types/commands.js +544 -0
- package/lib/types/control.d.ts +23 -0
- package/lib/types/control.js +192 -0
- package/lib/types/file-references.d.ts +27 -0
- package/lib/types/file-references.js +69 -0
- package/lib/types/history.d.ts +31 -0
- package/lib/types/history.js +376 -0
- package/lib/types/index.d.ts +171 -0
- package/lib/types/index.js +424 -0
- package/lib/types/invariant.d.ts +9 -0
- package/lib/types/invariant.js +12 -0
- package/lib/types/list.d.ts +53 -0
- package/lib/types/list.js +405 -0
- package/lib/types/model-selection-projection.d.ts +8 -0
- package/lib/types/model-selection-projection.js +66 -0
- package/lib/types/remote-events.d.ts +8 -0
- package/lib/types/remote-events.js +2 -0
- package/lib/types/skill-catalog.d.ts +28 -0
- package/lib/types/skill-catalog.js +192 -0
- package/lib/types/types.d.ts +505 -0
- package/lib/types/types.js +6 -0
- 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
|