@opencode/client 2.0.7 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/chunks/pty-handoff-1tbj6z39.js +81 -0
  2. package/dist/chunks/pty-handoff-4tveyqeh.js +34 -0
  3. package/dist/chunks/pty-handoff-65kht65h.js +62 -0
  4. package/dist/chunks/pty-handoff-6mptkzbc.js +0 -0
  5. package/dist/chunks/pty-handoff-9rqn6x4v.js +16 -0
  6. package/dist/chunks/pty-handoff-atpwm825.js +1638 -0
  7. package/dist/chunks/pty-handoff-bssvn03b.js +594 -0
  8. package/dist/chunks/pty-handoff-frbwqjmf.js +145 -0
  9. package/dist/chunks/pty-handoff-m2eywc8w.js +0 -0
  10. package/dist/chunks/pty-handoff-n3c99vt5.js +11 -0
  11. package/dist/chunks/pty-handoff-nqd0dnpk.js +112 -0
  12. package/dist/chunks/pty-handoff-qav6mfdz.js +0 -0
  13. package/dist/chunks/pty-handoff-radw39tv.js +1402 -0
  14. package/dist/chunks/pty-handoff-sxt88q69.js +12 -0
  15. package/dist/chunks/pty-handoff-t1xa31vm.js +39 -0
  16. package/dist/chunks/pty-handoff-txckps9j.js +57 -0
  17. package/dist/chunks/pty-handoff-vgxtr9hm.js +30 -0
  18. package/dist/chunks/pty-handoff-vmytr4q8.js +4 -0
  19. package/dist/chunks/pty-handoff-vyas3vgt.js +0 -0
  20. package/dist/chunks/pty-handoff-xgnj2w4v.js +20 -0
  21. package/dist/chunks/pty-handoff-y2etg5bs.js +9 -0
  22. package/dist/chunks/pty-handoff-ydhz1zrh.js +238 -0
  23. package/dist/chunks/pty-handoff-yepdgb80.js +44 -0
  24. package/dist/chunks/pty-handoff-zadn5mwr.js +52 -0
  25. package/dist/contract.js +12 -1
  26. package/dist/effect/client.d.ts +4 -4
  27. package/dist/effect/client.js +15 -29
  28. package/dist/effect/generated/client-error.js +6 -5
  29. package/dist/effect/generated/client.d.ts +4 -4
  30. package/dist/effect/generated/client.js +8 -582
  31. package/dist/effect/generated/index.js +12 -2
  32. package/dist/effect/index.js +75 -30
  33. package/dist/effect/rpc.js +11 -49
  34. package/dist/effect/service.js +238 -245
  35. package/dist/promise/client.js +14 -13
  36. package/dist/promise/generated/client-error.js +6 -8
  37. package/dist/promise/generated/client.js +7 -1396
  38. package/dist/promise/generated/index.js +75 -3
  39. package/dist/promise/generated/types.d.ts +91 -89
  40. package/dist/promise/generated/types.js +66 -34
  41. package/dist/promise/index.js +79 -2
  42. package/dist/promise/rpc.js +7 -86
  43. package/dist/promise/service.js +230 -227
  44. package/dist/pty-handoff.js +14 -121
  45. package/dist/rpc-runtime.js +14 -32
  46. package/dist/service-contender.js +10 -56
  47. package/dist/service-timing.d.ts +0 -1
  48. package/dist/service-timing.js +9 -18
  49. package/dist/service-version.js +6 -9
  50. package/dist/service.js +1 -0
  51. package/dist/shared-events.js +8 -137
  52. package/dist/solid/connection.js +10 -246
  53. package/dist/solid/data.js +18 -1769
  54. package/dist/solid/index.js +32 -3
  55. package/dist/solid/pty.js +8 -41
  56. package/package.json +4 -4
@@ -0,0 +1,1638 @@
1
+ import {
2
+ isFormNotFoundError2,
3
+ isFormAlreadySettledError2,
4
+ isPermissionNotFoundError2
5
+ } from "./pty-handoff-4tveyqeh.js";
6
+
7
+ // src/solid/data.ts
8
+ import { Worktree } from "@opencode/schema/worktree";
9
+ import { SessionID } from "@opencode/schema/session-id";
10
+ import { SessionMessage } from "@opencode/schema/session-message";
11
+ import { createStore, produce, reconcile } from "solid-js/store";
12
+ import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
13
+ var messageIDFromEvent = (eventID) => eventID.replace(/^evt_/, "msg_");
14
+ var messagePageLimit = 20;
15
+ var settleMs2 = 150;
16
+ function locationKey2(location) {
17
+ return JSON.stringify([location.directory, location.workspaceID]);
18
+ }
19
+ function locationQuery(ref) {
20
+ return { directory: ref.directory };
21
+ }
22
+ function formRequestOptions(sessionID, ref) {
23
+ if (sessionID !== "global" || !ref)
24
+ return;
25
+ return {
26
+ headers: {
27
+ "x-opencode-directory": encodeURIComponent(ref.directory)
28
+ }
29
+ };
30
+ }
31
+ function createSync() {
32
+ const state = new Map;
33
+ const start = (key, load, wait) => {
34
+ const entry = { promise: Promise.resolve(), invalidated: false, started: !wait };
35
+ state.set(key, entry);
36
+ const run = () => {
37
+ entry.started = true;
38
+ return load();
39
+ };
40
+ entry.promise = (wait ? wait.catch(() => {
41
+ return;
42
+ }).then(run) : run()).then(() => {
43
+ if (state.get(key) === entry && !entry.invalidated)
44
+ state.set(key, true);
45
+ }).finally(() => {
46
+ if (state.get(key) === entry)
47
+ state.delete(key);
48
+ });
49
+ return entry.promise;
50
+ };
51
+ return {
52
+ run(key, load) {
53
+ const active = state.get(key);
54
+ if (active === true)
55
+ return Promise.resolve();
56
+ if (!active)
57
+ return start(key, load);
58
+ if (!active.invalidated)
59
+ return active.promise;
60
+ return start(key, load, active.promise);
61
+ },
62
+ complete(key) {
63
+ if (state.has(key))
64
+ return;
65
+ state.set(key, true);
66
+ },
67
+ has(key) {
68
+ return state.has(key);
69
+ },
70
+ pending(key) {
71
+ const active = state.get(key);
72
+ return active !== undefined && active !== true;
73
+ },
74
+ invalidate(key) {
75
+ if (key) {
76
+ const active = state.get(key);
77
+ if (active === true)
78
+ state.delete(key);
79
+ if (active !== undefined && active !== true && active.started)
80
+ active.invalidated = true;
81
+ return;
82
+ }
83
+ state.forEach((active, current) => {
84
+ if (active === true)
85
+ state.delete(current);
86
+ if (active !== true && active.started)
87
+ active.invalidated = true;
88
+ });
89
+ }
90
+ };
91
+ }
92
+ function createData2(config) {
93
+ const api = config.api;
94
+ let disposed = false;
95
+ onCleanup(() => disposed = true);
96
+ function refresh(load) {
97
+ if (disposed || config.connection && config.connection.status() !== "connected")
98
+ return;
99
+ load().catch((error) => {
100
+ if (disposed || config.connection && config.connection.status() !== "connected")
101
+ return;
102
+ if (config.onError)
103
+ return config.onError(error);
104
+ console.error("Failed to refresh client data", error);
105
+ });
106
+ }
107
+ const settling = new Map;
108
+ onCleanup(() => settling.forEach((timer) => clearTimeout(timer)));
109
+ function settle(key, load) {
110
+ clearTimeout(settling.get(key));
111
+ settling.set(key, setTimeout(() => {
112
+ settling.delete(key);
113
+ refresh(load);
114
+ }, settleMs2));
115
+ }
116
+ const [store, setStore] = createStore({
117
+ session: {
118
+ info: {},
119
+ family: {},
120
+ active: {},
121
+ message: {},
122
+ messageCursor: {},
123
+ messageLoading: {},
124
+ pending: {},
125
+ permission: {},
126
+ form: {}
127
+ },
128
+ project: {
129
+ info: {},
130
+ permission: {}
131
+ },
132
+ location: {}
133
+ });
134
+ const [defaultLocation, setDefaultLocation] = createSignal({ directory: config.directory });
135
+ const sessions = createMemo(() => Object.values(store.session.info).toSorted((a, b) => b.time.updated - a.time.updated));
136
+ const messageIndex = new Map;
137
+ const sync = createSync();
138
+ let activeUpdates;
139
+ const pendingUpdates = new Map;
140
+ function setSessionActive(sessionID, status) {
141
+ activeUpdates?.set(sessionID, status);
142
+ setStore("session", "active", sessionID, status);
143
+ }
144
+ function removePending(sessionID, inboxID) {
145
+ if (!inboxID)
146
+ return;
147
+ pendingUpdates.get(sessionID)?.set(inboxID, undefined);
148
+ if (store.session.pending[sessionID]?.some((item) => item.id === inboxID))
149
+ setStore("session", "pending", sessionID, (store.session.pending[sessionID] ?? []).filter((item) => item.id !== inboxID));
150
+ }
151
+ function removePermission(sessionID, requestID) {
152
+ const requests = store.session.permission[sessionID];
153
+ if (!requests?.some((request) => request.id === requestID))
154
+ return;
155
+ setStore("session", "permission", sessionID, requests.filter((request) => request.id !== requestID));
156
+ }
157
+ function removeForm(sessionID, formID, ref) {
158
+ const forms = store.session.form[sessionID];
159
+ if (!forms)
160
+ return false;
161
+ const location = ref && locationKey2(ref);
162
+ const next = forms.filter((form) => {
163
+ if (form.id !== formID)
164
+ return true;
165
+ if (sessionID !== "global" || !location)
166
+ return false;
167
+ return !form.location || locationKey2(form.location) !== location;
168
+ });
169
+ if (next.length === forms.length)
170
+ return false;
171
+ setStore("session", "form", sessionID, next);
172
+ return true;
173
+ }
174
+ function settleForm(input, ref, request) {
175
+ return request.catch((error) => {
176
+ if (!isFormNotFoundError2(error) && !isFormAlreadySettledError2(error) || error.id !== input.formID)
177
+ throw error;
178
+ }).then(() => {
179
+ if (!removeForm(input.sessionID, input.formID, ref))
180
+ return;
181
+ result.session.form.invalidate(input.sessionID, ref);
182
+ result.session.form.sync(input.sessionID, ref).catch(() => {
183
+ return;
184
+ });
185
+ });
186
+ }
187
+ function updatePending(sessionID, inboxID, delivery) {
188
+ const index = store.session.pending[sessionID]?.findIndex((item) => item.id === inboxID) ?? -1;
189
+ const item = store.session.pending[sessionID]?.[index];
190
+ pendingUpdates.get(sessionID)?.set(inboxID, item ? { ...item, delivery } : delivery);
191
+ if (index < 0 || !item || item.delivery === delivery)
192
+ return;
193
+ setStore("session", "pending", sessionID, index, { ...item, delivery });
194
+ }
195
+ const outbox = new Set;
196
+ const sessionOutbox = new Set;
197
+ const creating = new Map;
198
+ const sending = new Map;
199
+ const messageLoads = new Map;
200
+ const compacting = new Map;
201
+ onCleanup(() => compacting.clear());
202
+ function track(map, key, promise) {
203
+ map.set(key, promise);
204
+ const settle = () => {
205
+ if (map.get(key) === promise)
206
+ map.delete(key);
207
+ };
208
+ promise.then(settle, settle);
209
+ }
210
+ function sendAdmission(sessionID, send, gate) {
211
+ const created = creating.get(sessionID);
212
+ const previous = sending.get(sessionID);
213
+ const request = Promise.resolve().then(() => Promise.all([gate, created, previous])).then(send);
214
+ track(sending, sessionID, request.catch(() => {
215
+ return;
216
+ }));
217
+ return request;
218
+ }
219
+ function admitLocal(item) {
220
+ batch(() => {
221
+ const pending = store.session.pending[item.sessionID] ?? [];
222
+ const at = pending.findIndex((entry) => entry.id === item.id);
223
+ setStore("session", "pending", item.sessionID, at < 0 ? [...pending, item] : pending.map((entry, index) => index === at ? item : entry));
224
+ if (item.type === "compaction")
225
+ return;
226
+ materializeInboxMessage(item);
227
+ });
228
+ }
229
+ function materializeInboxMessage(item) {
230
+ if (item.type !== "user" && item.type !== "synthetic")
231
+ return;
232
+ message.update(item.sessionID, (draft, index) => {
233
+ const row = item.type === "user" ? { id: item.id, type: "user", ...item.payload, time: { created: item.time.created } } : { id: item.id, type: "synthetic", ...item.payload, time: { created: item.time.created } };
234
+ const position = index.get(item.id);
235
+ if (position === undefined)
236
+ return message.append(draft, index, row);
237
+ draft[position] = row;
238
+ });
239
+ }
240
+ function retractLocal(sessionID, inboxID) {
241
+ batch(() => {
242
+ removePending(sessionID, inboxID);
243
+ if (!messageIndex.get(sessionID)?.has(inboxID))
244
+ return;
245
+ message.update(sessionID, (draft, index) => {
246
+ const position = index.get(inboxID);
247
+ if (position === undefined)
248
+ return;
249
+ draft.splice(position, 1);
250
+ index.delete(inboxID);
251
+ message.reindex(draft, index, position);
252
+ });
253
+ });
254
+ }
255
+ const message = {
256
+ update(sessionID, fn) {
257
+ setStore("session", "message", produce((draft) => {
258
+ fn(draft[sessionID] ??= [], index(sessionID));
259
+ }));
260
+ },
261
+ append(messages, index, item) {
262
+ if (index.has(item.id))
263
+ return;
264
+ index.set(item.id, messages.length);
265
+ messages.push(item);
266
+ },
267
+ insert(sessionID, item) {
268
+ message.update(sessionID, (draft, index) => message.append(draft, index, item));
269
+ },
270
+ editAssistant(sessionID, messageID, fn) {
271
+ message.update(sessionID, (draft, index) => {
272
+ const position = index.get(messageID);
273
+ const item = position === undefined ? undefined : draft[position];
274
+ if (item?.type === "assistant")
275
+ fn(item);
276
+ });
277
+ },
278
+ editTool(sessionID, messageID, toolID, fn) {
279
+ message.editAssistant(sessionID, messageID, (assistant) => {
280
+ const tool = assistant.content.findLast((item) => item.type === "tool" && item.id === toolID);
281
+ if (tool)
282
+ fn(tool);
283
+ });
284
+ },
285
+ editText(sessionID, messageID, fn) {
286
+ message.editAssistant(sessionID, messageID, (assistant) => {
287
+ const text = assistant.content.findLast((item) => item.type === "text");
288
+ if (text)
289
+ fn(text);
290
+ });
291
+ },
292
+ editReasoning(sessionID, messageID, fn) {
293
+ message.editAssistant(sessionID, messageID, (assistant) => {
294
+ const reasoning = assistant.content.findLast((item) => item.type === "reasoning" && !item.time?.completed);
295
+ if (reasoning)
296
+ fn(reasoning);
297
+ });
298
+ },
299
+ activeAssistant(messages) {
300
+ const item = messages.findLast((item) => item.type === "assistant" && !item.time.completed);
301
+ return item?.type === "assistant" ? item : undefined;
302
+ },
303
+ shell(messages, shellID) {
304
+ const item = messages.findLast((item) => item.type === "shell" && item.shellID === shellID);
305
+ return item?.type === "shell" ? item : undefined;
306
+ },
307
+ compaction(messages) {
308
+ const item = messages.findLast((item) => item.type === "compaction" && item.status === "running");
309
+ return item?.type === "compaction" ? item : undefined;
310
+ },
311
+ reindex(messages, index, start) {
312
+ for (let position = start;position < messages.length; position++) {
313
+ const item = messages[position];
314
+ if (item)
315
+ index.set(item.id, position);
316
+ }
317
+ }
318
+ };
319
+ function index(sessionID) {
320
+ const existing = messageIndex.get(sessionID);
321
+ if (existing)
322
+ return existing;
323
+ const created = new Map;
324
+ messageIndex.set(sessionID, created);
325
+ return created;
326
+ }
327
+ function resolveRoot(sessionID) {
328
+ let current = sessionID;
329
+ let parentID = store.session.info[sessionID]?.parentID;
330
+ const seen = new Set([sessionID]);
331
+ while (parentID) {
332
+ if (seen.has(parentID))
333
+ break;
334
+ seen.add(parentID);
335
+ current = parentID;
336
+ parentID = store.session.info[parentID]?.parentID;
337
+ }
338
+ return current;
339
+ }
340
+ function registerSession(sessionID) {
341
+ const info = store.session.info[sessionID];
342
+ if (!info)
343
+ return;
344
+ const rootID = resolveRoot(sessionID);
345
+ setStore("session", "family", produce((draft) => {
346
+ if (sessionID !== rootID && draft[sessionID]) {
347
+ const members = draft[rootID] ??= [];
348
+ for (const id of draft[sessionID]) {
349
+ if (!members.includes(id))
350
+ members.push(id);
351
+ }
352
+ delete draft[sessionID];
353
+ }
354
+ const family = draft[rootID] ??= [];
355
+ if (!family.includes(sessionID))
356
+ family.push(sessionID);
357
+ }));
358
+ }
359
+ function evictSession(sessionID) {
360
+ if (sessionOutbox.has(sessionID))
361
+ return;
362
+ sync.invalidate(`session.pending:${sessionID}`);
363
+ sync.invalidate(`session.message:${sessionID}`);
364
+ messageLoads.delete(sessionID);
365
+ const pending = store.session.pending[sessionID]?.filter((item) => outbox.has(item.id)) ?? [];
366
+ const messages = store.session.message[sessionID]?.filter((item) => outbox.has(item.id)) ?? [];
367
+ messageIndex.delete(sessionID);
368
+ if (messages.length)
369
+ messageIndex.set(sessionID, new Map(messages.map((item, index) => [item.id, index])));
370
+ setStore("session", produce((draft) => {
371
+ delete draft.message[sessionID];
372
+ delete draft.messageCursor[sessionID];
373
+ delete draft.messageLoading[sessionID];
374
+ delete draft.pending[sessionID];
375
+ if (messages.length)
376
+ draft.message[sessionID] = messages;
377
+ if (pending.length)
378
+ draft.pending[sessionID] = pending;
379
+ }));
380
+ }
381
+ function removeSession(sessionID) {
382
+ activeUpdates?.set(sessionID, undefined);
383
+ store.session.pending[sessionID]?.forEach((item) => outbox.delete(item.id));
384
+ messageIndex.delete(sessionID);
385
+ sync.invalidate(`session:${sessionID}`);
386
+ sync.invalidate(`session.family:${sessionID}`);
387
+ sync.invalidate(`session.pending:${sessionID}`);
388
+ sync.invalidate(`session.message:${sessionID}`);
389
+ sync.invalidate(`session.permission:${sessionID}`);
390
+ sync.invalidate(`session.form:${sessionID}:`);
391
+ setStore("session", produce((draft) => {
392
+ delete draft.info[sessionID];
393
+ delete draft.active[sessionID];
394
+ delete draft.message[sessionID];
395
+ delete draft.messageCursor[sessionID];
396
+ delete draft.messageLoading[sessionID];
397
+ delete draft.pending[sessionID];
398
+ delete draft.permission[sessionID];
399
+ delete draft.form[sessionID];
400
+ for (const [rootID, family] of Object.entries(draft.family)) {
401
+ const next = family.filter((id) => id !== sessionID);
402
+ if (next.length === 0)
403
+ delete draft.family[rootID];
404
+ else
405
+ draft.family[rootID] = next;
406
+ }
407
+ }));
408
+ }
409
+ function handleEvent(event) {
410
+ switch (event.type) {
411
+ case "server.connected": {
412
+ const updates = new Map;
413
+ activeUpdates = updates;
414
+ refresh(() => api().session.active().then((active) => {
415
+ if (activeUpdates !== updates)
416
+ return;
417
+ const snapshot = new Map(Object.keys(active).map((id) => [id, "running"]));
418
+ updates.forEach((status, id) => {
419
+ if (status === undefined)
420
+ return snapshot.delete(id);
421
+ snapshot.set(id, status);
422
+ });
423
+ activeUpdates = undefined;
424
+ setStore("session", "active", reconcile(Object.fromEntries(snapshot)));
425
+ }).catch(() => {
426
+ if (activeUpdates === updates)
427
+ activeUpdates = undefined;
428
+ }));
429
+ refresh(() => api().location.get({ location: locationQuery(defaultLocation()) }).then((location) => {
430
+ const key = locationKey2(location);
431
+ setStore("location", key, { info: location });
432
+ }));
433
+ refresh(() => result.location.vcs.sync());
434
+ refresh(() => result.project.sync());
435
+ return;
436
+ }
437
+ case "project.updated":
438
+ setStore("project", "info", event.data.id, reconcile(event.data));
439
+ return;
440
+ case "session.created":
441
+ sessionOutbox.delete(event.data.sessionID);
442
+ result.session.invalidate(event.data.sessionID);
443
+ refresh(() => result.session.sync(event.data.sessionID));
444
+ sync.complete(`session.pending:${event.data.sessionID}`);
445
+ sync.complete(`session.message:${event.data.sessionID}`);
446
+ return;
447
+ case "session.deleted":
448
+ removeSession(event.data.sessionID);
449
+ return;
450
+ case "session.usage.updated":
451
+ if (store.session.info[event.data.sessionID])
452
+ setStore("session", "info", event.data.sessionID, {
453
+ cost: event.data.cost,
454
+ tokens: event.data.tokens
455
+ });
456
+ return;
457
+ case "session.agent.selected": {
458
+ const previous = store.session.info[event.data.sessionID]?.agent;
459
+ if (store.session.info[event.data.sessionID])
460
+ setStore("session", "info", event.data.sessionID, "agent", event.data.agent);
461
+ message.insert(event.data.sessionID, {
462
+ id: messageIDFromEvent(event.id),
463
+ type: "agent-switched",
464
+ agent: event.data.agent,
465
+ previous,
466
+ time: { created: event.created }
467
+ });
468
+ return;
469
+ }
470
+ case "session.model.selected":
471
+ if (store.session.info[event.data.sessionID])
472
+ setStore("session", "info", event.data.sessionID, "model", event.data.model);
473
+ if (!store.session.message[event.data.sessionID])
474
+ return;
475
+ message.insert(event.data.sessionID, {
476
+ id: messageIDFromEvent(event.id),
477
+ type: "model-switched",
478
+ model: event.data.model,
479
+ time: { created: event.created }
480
+ });
481
+ refresh(() => api().session.message.get({ sessionID: event.data.sessionID, messageID: messageIDFromEvent(event.id) }).then((item) => {
482
+ message.update(event.data.sessionID, (draft, index) => {
483
+ const position = index.get(item.id);
484
+ if (position === undefined)
485
+ return message.append(draft, index, item);
486
+ draft[position] = item;
487
+ });
488
+ }));
489
+ return;
490
+ case "session.renamed": {
491
+ refresh(() => {
492
+ const family = sync.pending(`session.family:${event.data.sessionID}`) ? result.session.sync(event.data.sessionID, { children: true }) : Promise.resolve();
493
+ return Promise.all([result.session.sync(event.data.sessionID), family]).then(() => {
494
+ if (store.session.info[event.data.sessionID])
495
+ setStore("session", "info", event.data.sessionID, "title", event.data.title);
496
+ });
497
+ });
498
+ return;
499
+ }
500
+ case "session.permissions":
501
+ if (store.session.info[event.data.sessionID])
502
+ setStore("session", "info", event.data.sessionID, "permissions", event.data.permissions);
503
+ return;
504
+ case "session.moved": {
505
+ const current = store.session.info[event.data.sessionID];
506
+ if (current) {
507
+ const previous = {
508
+ location: { ...current.location },
509
+ projectID: current.projectID,
510
+ subpath: current.subpath
511
+ };
512
+ setStore("session", "info", event.data.sessionID, "location", event.data.location);
513
+ if (event.data.projectID)
514
+ setStore("session", "info", event.data.sessionID, "projectID", event.data.projectID);
515
+ setStore("session", "info", event.data.sessionID, "subpath", event.data.subpath);
516
+ message.insert(event.data.sessionID, {
517
+ id: messageIDFromEvent(event.id),
518
+ type: "location-switched",
519
+ location: event.data.location,
520
+ projectID: event.data.projectID,
521
+ subpath: event.data.subpath,
522
+ previous,
523
+ time: { created: event.created }
524
+ });
525
+ }
526
+ return;
527
+ }
528
+ case "worktree.resolved": {
529
+ for (const [sessionID, info] of Object.entries(store.session.info)) {
530
+ const explicit = event.data.adopted?.includes(info.projectID);
531
+ const directory = explicit ? store.project.info[info.projectID]?.canonical : info.location.directory;
532
+ if (!directory) {
533
+ result.session.invalidate(sessionID);
534
+ refresh(() => result.session.sync(sessionID));
535
+ continue;
536
+ }
537
+ const adopted = Worktree.adopt({
538
+ projectID: info.projectID,
539
+ directory
540
+ }, event.data);
541
+ if (!adopted)
542
+ continue;
543
+ setStore("session", "info", sessionID, "projectID", adopted.projectID);
544
+ setStore("session", "info", sessionID, "subpath", adopted.subpath);
545
+ }
546
+ return;
547
+ }
548
+ case "session.inbox.delivered": {
549
+ const admitted = result.session.input.has(event.data.sessionID, event.data.inboxID);
550
+ removePending(event.data.sessionID, event.data.inboxID);
551
+ message.update(event.data.sessionID, (draft, index) => {
552
+ const position = index.get(event.data.inboxID);
553
+ if (position === undefined)
554
+ return;
555
+ const existing = draft[position];
556
+ if (!existing || !admitted)
557
+ return;
558
+ existing.time.created = event.created;
559
+ draft.splice(position, 1);
560
+ draft.push(existing);
561
+ message.reindex(draft, index, position);
562
+ });
563
+ compacting.get(event.data.sessionID)?.observed.add(event.data.inboxID);
564
+ return;
565
+ }
566
+ case "session.inbox.delivery.changed":
567
+ updatePending(event.data.sessionID, event.data.inboxID, event.data.delivery);
568
+ return;
569
+ case "session.inbox.cancelled": {
570
+ retractLocal(event.data.sessionID, event.data.inboxID);
571
+ compacting.get(event.data.sessionID)?.observed.add(event.data.inboxID);
572
+ return;
573
+ }
574
+ case "session.inbox.enqueued": {
575
+ outbox.delete(event.data.inboxID);
576
+ const item = {
577
+ id: event.data.inboxID,
578
+ sessionID: event.data.sessionID,
579
+ time: { created: event.created },
580
+ ...event.data.item
581
+ };
582
+ pendingUpdates.get(item.sessionID)?.set(item.id, item);
583
+ admitLocal(item);
584
+ if (event.data.item.type === "compaction") {
585
+ const active = compacting.get(event.data.sessionID);
586
+ active?.observed.add(event.data.inboxID);
587
+ if (active && active.id !== event.data.inboxID && outbox.delete(active.id))
588
+ removePending(event.data.sessionID, active.id);
589
+ }
590
+ return;
591
+ }
592
+ case "session.instructions.updated":
593
+ const updateText = event.data.text;
594
+ if (updateText === undefined)
595
+ return;
596
+ message.insert(event.data.sessionID, {
597
+ id: messageIDFromEvent(event.id),
598
+ type: "system",
599
+ text: updateText,
600
+ description: `Instructions updated: ${Object.keys(event.data.delta).join(", ")}`,
601
+ metadata: event.metadata,
602
+ time: { created: event.created }
603
+ });
604
+ return;
605
+ case "session.synthetic":
606
+ message.insert(event.data.sessionID, {
607
+ id: messageIDFromEvent(event.id),
608
+ type: "synthetic",
609
+ text: event.data.text,
610
+ description: event.data.description,
611
+ metadata: event.data.metadata,
612
+ time: { created: event.created }
613
+ });
614
+ return;
615
+ case "session.shell.started":
616
+ message.insert(event.data.sessionID, {
617
+ id: messageIDFromEvent(event.id),
618
+ type: "shell",
619
+ shellID: event.data.shell.id,
620
+ command: event.data.shell.command,
621
+ status: event.data.shell.status,
622
+ exit: event.data.shell.exit,
623
+ metadata: event.data.shell.metadata.background === true ? { ...event.metadata, background: true } : event.metadata,
624
+ time: { created: event.created }
625
+ });
626
+ return;
627
+ case "session.shell.ended":
628
+ message.update(event.data.sessionID, (draft) => {
629
+ const match = message.shell(draft, event.data.shell.id);
630
+ if (!match)
631
+ return;
632
+ match.status = event.data.shell.status;
633
+ match.exit = event.data.shell.exit;
634
+ match.output = event.data.output;
635
+ match.time.completed = event.created;
636
+ });
637
+ return;
638
+ case "session.step.started":
639
+ message.update(event.data.sessionID, (draft, index) => {
640
+ const position = index.get(event.data.assistantMessageID);
641
+ const existing = position === undefined ? undefined : draft[position];
642
+ if (existing?.type === "assistant") {
643
+ existing.agent = event.data.agent;
644
+ existing.model = event.data.model;
645
+ existing.retry = undefined;
646
+ existing.error = undefined;
647
+ existing.finish = undefined;
648
+ existing.rawFinish = undefined;
649
+ existing.providerState = undefined;
650
+ existing.time.created = event.data.started;
651
+ existing.time.streamed = undefined;
652
+ existing.time.completed = undefined;
653
+ if (event.data.snapshot)
654
+ existing.snapshot = { ...existing.snapshot, start: event.data.snapshot };
655
+ return;
656
+ }
657
+ const currentAssistant = message.activeAssistant(draft);
658
+ if (currentAssistant) {
659
+ currentAssistant.retry = undefined;
660
+ currentAssistant.time.completed = event.created;
661
+ }
662
+ message.append(draft, index, {
663
+ id: event.data.assistantMessageID,
664
+ type: "assistant",
665
+ agent: event.data.agent,
666
+ model: event.data.model,
667
+ metadata: event.metadata,
668
+ content: [],
669
+ snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
670
+ time: { created: event.data.started }
671
+ });
672
+ });
673
+ return;
674
+ case "session.step.streamed":
675
+ message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
676
+ assistant.time.streamed = event.created;
677
+ });
678
+ return;
679
+ case "session.step.ended": {
680
+ message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
681
+ assistant.time.completed = event.created;
682
+ assistant.finish = event.data.finish;
683
+ assistant.rawFinish = event.data.rawFinish;
684
+ assistant.providerState = event.data.providerState;
685
+ assistant.cost = event.data.cost;
686
+ assistant.tokens = event.data.tokens;
687
+ if (event.data.snapshot)
688
+ assistant.snapshot = { ...assistant.snapshot, end: event.data.snapshot };
689
+ });
690
+ return;
691
+ }
692
+ case "session.step.failed":
693
+ message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
694
+ assistant.time.completed = event.created;
695
+ assistant.finish = event.data.finish ?? "error";
696
+ assistant.rawFinish = event.data.rawFinish;
697
+ assistant.providerState = event.data.providerState;
698
+ assistant.error = event.data.error;
699
+ assistant.retry = undefined;
700
+ if (event.data.cost !== undefined && event.data.tokens !== undefined) {
701
+ assistant.cost = event.data.cost;
702
+ assistant.tokens = event.data.tokens;
703
+ }
704
+ });
705
+ return;
706
+ case "session.text.started":
707
+ message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
708
+ assistant.content.push({ type: "text", text: "" });
709
+ });
710
+ return;
711
+ case "session.text.delta":
712
+ message.editText(event.data.sessionID, event.data.assistantMessageID, (text) => {
713
+ text.text += event.data.delta;
714
+ });
715
+ return;
716
+ case "session.text.ended":
717
+ message.editText(event.data.sessionID, event.data.assistantMessageID, (text) => {
718
+ text.text = event.data.text;
719
+ });
720
+ return;
721
+ case "session.tool.input.started":
722
+ message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
723
+ assistant.content.push({
724
+ type: "tool",
725
+ id: event.data.id,
726
+ name: event.data.name,
727
+ time: { created: event.created },
728
+ state: { status: "streaming", input: "" }
729
+ });
730
+ });
731
+ return;
732
+ case "session.tool.input.delta":
733
+ message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
734
+ if (tool.state.status === "streaming")
735
+ tool.state.input += event.data.delta;
736
+ });
737
+ return;
738
+ case "session.tool.input.ended":
739
+ message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
740
+ if (tool.state.status === "streaming")
741
+ tool.state.input = event.data.text;
742
+ });
743
+ return;
744
+ case "session.tool.called":
745
+ message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
746
+ tool.time.ran = event.created;
747
+ tool.executed = event.data.executed;
748
+ tool.providerState = event.data.state;
749
+ tool.state = { status: "running", input: event.data.input, metadata: {} };
750
+ });
751
+ return;
752
+ case "session.tool.progress":
753
+ message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
754
+ if (tool.state.status === "running")
755
+ tool.state.metadata = event.data.metadata;
756
+ });
757
+ return;
758
+ case "session.tool.success":
759
+ message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
760
+ if (tool.state.status !== "running")
761
+ return;
762
+ tool.state = {
763
+ status: "completed",
764
+ input: tool.state.input,
765
+ metadata: event.data.metadata,
766
+ content: [...event.data.content]
767
+ };
768
+ tool.executed = event.data.executed || tool.executed === true;
769
+ tool.providerResultState = event.data.resultState;
770
+ tool.time.completed = event.created;
771
+ });
772
+ return;
773
+ case "session.tool.failed":
774
+ message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
775
+ if (tool.state.status !== "streaming" && tool.state.status !== "running")
776
+ return;
777
+ tool.state = {
778
+ status: "error",
779
+ error: event.data.error,
780
+ input: typeof tool.state.input === "string" ? {} : tool.state.input,
781
+ metadata: event.data.metadata,
782
+ content: event.data.content
783
+ };
784
+ tool.executed = event.data.executed || tool.executed === true;
785
+ tool.providerResultState = event.data.resultState;
786
+ tool.time.completed = event.created;
787
+ });
788
+ return;
789
+ case "session.reasoning.started":
790
+ message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
791
+ assistant.content.push({
792
+ type: "reasoning",
793
+ text: "",
794
+ state: event.data.state,
795
+ time: { created: event.created }
796
+ });
797
+ });
798
+ return;
799
+ case "session.reasoning.delta":
800
+ message.editReasoning(event.data.sessionID, event.data.assistantMessageID, (reasoning) => {
801
+ reasoning.text += event.data.delta;
802
+ });
803
+ return;
804
+ case "session.reasoning.ended":
805
+ message.editReasoning(event.data.sessionID, event.data.assistantMessageID, (reasoning) => {
806
+ reasoning.text = event.data.text;
807
+ reasoning.time = { created: reasoning.time?.created ?? event.created, completed: event.created };
808
+ if (event.data.state !== undefined)
809
+ reasoning.state = event.data.state;
810
+ });
811
+ return;
812
+ case "session.retry.scheduled":
813
+ message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
814
+ assistant.retry = { attempt: event.data.attempt, at: event.data.at, error: event.data.error };
815
+ });
816
+ return;
817
+ case "session.execution.started":
818
+ setSessionActive(event.data.sessionID, "running");
819
+ return;
820
+ case "session.compaction.started":
821
+ if (event.data.inputID)
822
+ removePending(event.data.sessionID, event.data.inputID);
823
+ message.insert(event.data.sessionID, {
824
+ id: event.data.inputID ?? messageIDFromEvent(event.id),
825
+ type: "compaction",
826
+ status: "running",
827
+ reason: event.data.reason,
828
+ summary: "",
829
+ recent: event.data.recent ?? "",
830
+ time: { created: event.created }
831
+ });
832
+ if (event.data.inputID)
833
+ compacting.get(event.data.sessionID)?.observed.add(event.data.inputID);
834
+ return;
835
+ case "session.execution.succeeded":
836
+ case "session.execution.failed":
837
+ case "session.execution.interrupted":
838
+ setSessionActive(event.data.sessionID, "idle");
839
+ message.update(event.data.sessionID, (draft) => {
840
+ const currentAssistant = message.activeAssistant(draft);
841
+ if (currentAssistant)
842
+ currentAssistant.retry = undefined;
843
+ });
844
+ if (event.type === "session.execution.interrupted" && event.data.reason === "shutdown")
845
+ return;
846
+ message.insert(event.data.sessionID, {
847
+ id: messageIDFromEvent(event.id),
848
+ type: "idle",
849
+ outcome: event.type === "session.execution.succeeded" ? "succeeded" : event.type === "session.execution.failed" ? "failed" : "interrupted",
850
+ time: { created: event.created }
851
+ });
852
+ if (store.session.message[event.data.sessionID]?.some((item) => item.type === "assistant" && item.content.some((part) => part.type === "tool" && (part.state.status === "streaming" || part.state.status === "running")))) {
853
+ sync.invalidate(`session.message:${event.data.sessionID}`);
854
+ refresh(() => result.session.message.sync(event.data.sessionID));
855
+ }
856
+ if (!store.session.info[event.data.sessionID] && !sync.has(`session:${event.data.sessionID}`))
857
+ return;
858
+ result.session.invalidate(event.data.sessionID);
859
+ refresh(() => result.session.sync(event.data.sessionID));
860
+ return;
861
+ case "session.viewed":
862
+ if (!store.session.info[event.data.sessionID] && !sync.has(`session:${event.data.sessionID}`))
863
+ return;
864
+ result.session.invalidate(event.data.sessionID);
865
+ refresh(() => result.session.sync(event.data.sessionID));
866
+ return;
867
+ case "session.revert.staged":
868
+ if (store.session.info[event.data.sessionID])
869
+ setStore("session", "info", event.data.sessionID, "revert", event.data.revert);
870
+ return;
871
+ case "session.revert.cleared":
872
+ if (store.session.info[event.data.sessionID])
873
+ setStore("session", "info", event.data.sessionID, "revert", undefined);
874
+ return;
875
+ case "session.revert.committed":
876
+ if (store.session.info[event.data.sessionID]) {
877
+ setStore("session", "info", event.data.sessionID, "revert", undefined);
878
+ }
879
+ setStore("session", "pending", event.data.sessionID, (store.session.pending[event.data.sessionID] ?? []).filter((item) => item.id < event.data.to));
880
+ message.update(event.data.sessionID, (draft, index) => {
881
+ const position = draft.findIndex((item) => item.id >= event.data.to);
882
+ if (position === -1)
883
+ return;
884
+ for (const item of draft.splice(position))
885
+ index.delete(item.id);
886
+ });
887
+ return;
888
+ case "session.compaction.delta":
889
+ message.update(event.data.sessionID, (draft) => {
890
+ const current = message.compaction(draft);
891
+ if (current?.status === "running")
892
+ current.summary += event.data.text;
893
+ });
894
+ return;
895
+ case "session.compaction.ended":
896
+ message.update(event.data.sessionID, (draft, index) => {
897
+ const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running");
898
+ const current = draft[position];
899
+ if (current?.type === "compaction") {
900
+ Object.assign(current, {
901
+ status: "completed",
902
+ reason: event.data.reason,
903
+ model: event.data.model,
904
+ providerState: event.data.providerState,
905
+ providerContext: event.data.providerContext,
906
+ summary: event.data.text,
907
+ recent: event.data.recent,
908
+ cost: event.data.cost,
909
+ tokens: event.data.tokens
910
+ });
911
+ return;
912
+ }
913
+ message.append(draft, index, {
914
+ id: messageIDFromEvent(event.id),
915
+ type: "compaction",
916
+ status: "completed",
917
+ reason: event.data.reason,
918
+ model: event.data.model,
919
+ providerState: event.data.providerState,
920
+ providerContext: event.data.providerContext,
921
+ summary: event.data.text,
922
+ recent: event.data.recent,
923
+ cost: event.data.cost,
924
+ tokens: event.data.tokens,
925
+ time: { created: event.created }
926
+ });
927
+ });
928
+ return;
929
+ case "session.compaction.failed":
930
+ if (event.data.inputID)
931
+ removePending(event.data.sessionID, event.data.inputID);
932
+ message.update(event.data.sessionID, (draft, index) => {
933
+ const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running");
934
+ const current = draft[position];
935
+ const failed = {
936
+ id: current?.id ?? event.data.inputID ?? messageIDFromEvent(event.id),
937
+ type: "compaction",
938
+ status: "failed",
939
+ reason: event.data.reason ?? "manual",
940
+ error: event.data.error ?? {
941
+ type: "compaction.failed",
942
+ message: "Compaction failed before recording an error"
943
+ },
944
+ metadata: current?.type === "compaction" ? current.metadata : event.metadata,
945
+ cost: event.data.cost,
946
+ tokens: event.data.tokens,
947
+ time: current?.type === "compaction" ? current.time : { created: event.created }
948
+ };
949
+ if (current?.type === "compaction") {
950
+ draft[position] = failed;
951
+ return;
952
+ }
953
+ message.append(draft, index, failed);
954
+ });
955
+ if (event.data.inputID)
956
+ compacting.get(event.data.sessionID)?.observed.add(event.data.inputID);
957
+ return;
958
+ case "permission.asked":
959
+ if (store.session.permission[event.data.sessionID]?.some((request) => request.id === event.data.id))
960
+ return;
961
+ setStore("session", "permission", event.data.sessionID, [
962
+ ...store.session.permission[event.data.sessionID] ?? [],
963
+ event.data
964
+ ]);
965
+ return;
966
+ case "permission.replied":
967
+ removePermission(event.data.sessionID, event.data.requestID);
968
+ return;
969
+ case "form.replied":
970
+ case "form.cancelled":
971
+ removeForm(event.data.sessionID, event.data.id, event.location);
972
+ return;
973
+ }
974
+ if (event.type === "credential.updated" || event.type === "credential.switched") {
975
+ Object.keys(store.location).forEach((key) => {
976
+ const ref = JSON.parse(key);
977
+ const location = { directory: ref[0], workspaceID: ref[1] ?? undefined };
978
+ if (event.type === "credential.updated") {
979
+ result.location.integration.invalidate(location);
980
+ refresh(() => result.location.integration.sync(location));
981
+ return;
982
+ }
983
+ setStore("location", key, (data) => ({
984
+ integration: data?.integration?.map((integration) => {
985
+ if (integration.id !== event.data.integrationID)
986
+ return integration;
987
+ const active = integration.connections.find((connection) => connection.type === "credential" && connection.id === event.data.credentialID);
988
+ if (!active)
989
+ return integration;
990
+ return {
991
+ ...integration,
992
+ connections: [active, ...integration.connections.filter((connection) => connection !== active)]
993
+ };
994
+ })
995
+ }));
996
+ result.location.model.invalidate(location);
997
+ result.location.provider.invalidate(location);
998
+ refresh(() => Promise.all([result.location.model.sync(location), result.location.provider.sync(location)]));
999
+ });
1000
+ return;
1001
+ }
1002
+ if (!event.location)
1003
+ return;
1004
+ const location = event.location;
1005
+ switch (event.type) {
1006
+ case "provider.updated":
1007
+ result.location.provider.invalidate(location);
1008
+ refresh(() => result.location.provider.sync(location));
1009
+ break;
1010
+ case "model.updated":
1011
+ result.location.model.invalidate(location);
1012
+ refresh(() => result.location.model.sync(location));
1013
+ break;
1014
+ case "agent.updated":
1015
+ result.location.agent.invalidate(location);
1016
+ refresh(() => result.location.agent.sync(location));
1017
+ break;
1018
+ case "command.updated":
1019
+ result.location.command.invalidate(location);
1020
+ refresh(() => result.location.command.sync(location));
1021
+ break;
1022
+ case "skill.updated":
1023
+ result.location.skill.invalidate(location);
1024
+ refresh(() => result.location.skill.sync(location));
1025
+ break;
1026
+ case "vcs.branch.updated":
1027
+ setStore("location", locationKey2(location), (data) => ({
1028
+ vcs: {
1029
+ branch: {
1030
+ ...data?.vcs?.branch,
1031
+ current: event.data.branch
1032
+ }
1033
+ }
1034
+ }));
1035
+ break;
1036
+ case "form.created":
1037
+ if (store.session.form[event.data.form.sessionID]?.some((form) => form.id === event.data.form.id))
1038
+ break;
1039
+ setStore("session", "form", event.data.form.sessionID, [
1040
+ ...store.session.form[event.data.form.sessionID] ?? [],
1041
+ event.data.form.sessionID === "global" ? { ...event.data.form, location } : event.data.form
1042
+ ]);
1043
+ break;
1044
+ case "shell.created":
1045
+ setStore("location", locationKey2(location), (data) => ({
1046
+ shell: {
1047
+ ...data?.shell,
1048
+ [event.data.info.id]: { ...event.data.info, location }
1049
+ }
1050
+ }));
1051
+ break;
1052
+ case "shell.exited":
1053
+ case "shell.deleted":
1054
+ setStore("location", locationKey2(location), (data) => ({
1055
+ shell: Object.fromEntries(Object.entries(data?.shell ?? {}).filter(([id]) => id !== event.data.id))
1056
+ }));
1057
+ break;
1058
+ case "reference.updated":
1059
+ result.location.reference.invalidate(location);
1060
+ refresh(() => result.location.reference.sync(location));
1061
+ break;
1062
+ case "integration.updated":
1063
+ result.location.integration.invalidate(location);
1064
+ result.location.model.invalidate(location);
1065
+ result.location.provider.invalidate(location);
1066
+ refresh(() => Promise.all([
1067
+ result.location.integration.sync(location),
1068
+ result.location.model.sync(location),
1069
+ result.location.provider.sync(location)
1070
+ ]));
1071
+ break;
1072
+ case "config.updated":
1073
+ result.location.config.invalidate(location);
1074
+ if (result.location.config.list(location) !== undefined || sync.has(`location.config:${locationKey2(location)}`))
1075
+ refresh(() => result.location.config.sync(location));
1076
+ refresh(() => result.location.websearch.refresh(location));
1077
+ break;
1078
+ case "websearch.updated":
1079
+ refresh(() => result.location.websearch.refresh(location));
1080
+ break;
1081
+ case "mcp.status.changed":
1082
+ result.location.mcp.server.invalidate(location);
1083
+ settle(`mcp.status:${locationKey2(location)}`, () => result.location.mcp.server.sync(location));
1084
+ break;
1085
+ case "mcp.resources.changed":
1086
+ result.location.mcp.resource.invalidate(location);
1087
+ refresh(() => result.location.mcp.resource.sync(location));
1088
+ break;
1089
+ }
1090
+ }
1091
+ function locationResource(field, load, options) {
1092
+ const publish = (key, value) => setStore("location", key, { [field]: value });
1093
+ return {
1094
+ list: (ref) => store.location[locationKey2(ref ?? defaultLocation())]?.[field],
1095
+ sync: (ref) => {
1096
+ const location = ref ?? defaultLocation();
1097
+ const id = locationKey2(location);
1098
+ return sync.run(`location.${field}:${id}`, async () => {
1099
+ const response = await load(locationQuery(location));
1100
+ const key = locationKey2(response.location);
1101
+ publish(key, response.data);
1102
+ if (options?.alias && key !== id)
1103
+ publish(id, response.data);
1104
+ });
1105
+ },
1106
+ invalidate: (ref) => sync.invalidate(`location.${field}:${locationKey2(ref ?? defaultLocation())}`)
1107
+ };
1108
+ }
1109
+ const vcs = locationResource("vcs", (location) => api().vcs.get({ location }));
1110
+ const shells = locationResource("shell", async (location) => {
1111
+ const response = await api().shell.list({ location });
1112
+ const ref = { directory: response.location.directory };
1113
+ return {
1114
+ location: response.location,
1115
+ data: Object.fromEntries(response.data.map((info) => [info.id, { ...info, location: ref }]))
1116
+ };
1117
+ });
1118
+ const result = {
1119
+ on: config.event.on,
1120
+ listen: config.event.listen,
1121
+ session: {
1122
+ list() {
1123
+ return sessions();
1124
+ },
1125
+ get(sessionID) {
1126
+ return store.session.info[sessionID];
1127
+ },
1128
+ creating(sessionID) {
1129
+ return creating.has(sessionID);
1130
+ },
1131
+ remember(info) {
1132
+ batch(() => {
1133
+ setStore("session", "info", info.id, reconcile(info));
1134
+ sync.complete(`session:${info.id}`);
1135
+ registerSession(info.id);
1136
+ });
1137
+ },
1138
+ setStatus(sessionID, status) {
1139
+ setSessionActive(sessionID, status);
1140
+ },
1141
+ root(sessionID) {
1142
+ return resolveRoot(sessionID);
1143
+ },
1144
+ family(sessionID) {
1145
+ return store.session.family[resolveRoot(sessionID)] ?? [];
1146
+ },
1147
+ evict(sessionID) {
1148
+ const root = resolveRoot(sessionID);
1149
+ batch(() => {
1150
+ for (const id of new Set([root, sessionID, ...store.session.family[root] ?? []]))
1151
+ evictSession(id);
1152
+ });
1153
+ },
1154
+ cost(sessionID) {
1155
+ const session = store.session.info[sessionID];
1156
+ if (!session)
1157
+ return 0;
1158
+ if (session.parentID)
1159
+ return session.cost;
1160
+ return (store.session.family[sessionID] ?? [sessionID]).reduce((total, id) => total + (store.session.info[id]?.cost ?? 0), 0);
1161
+ },
1162
+ status(sessionID) {
1163
+ return store.session.active[sessionID] ?? "idle";
1164
+ },
1165
+ input: {
1166
+ list(sessionID) {
1167
+ return (store.session.pending[sessionID] ?? []).flatMap((item) => item.type === "compaction" ? [] : [item.id]);
1168
+ },
1169
+ has(sessionID, inboxID) {
1170
+ return store.session.pending[sessionID]?.some((item) => item.id === inboxID && item.type !== "compaction") ?? false;
1171
+ }
1172
+ },
1173
+ pending: {
1174
+ list(sessionID) {
1175
+ return store.session.pending[sessionID] ?? [];
1176
+ },
1177
+ sync(sessionID) {
1178
+ return sync.run(`session.pending:${sessionID}`, async () => {
1179
+ const updates = new Map;
1180
+ pendingUpdates.set(sessionID, updates);
1181
+ try {
1182
+ const snapshot = await api().session.inbox.list({ sessionID });
1183
+ const current = new Map(snapshot.map((item) => [item.id, item]));
1184
+ updates.forEach((item, id) => {
1185
+ if (item === undefined)
1186
+ current.delete(id);
1187
+ else if (typeof item === "string") {
1188
+ const existing = current.get(id);
1189
+ if (existing)
1190
+ current.set(id, { ...existing, delivery: item });
1191
+ } else
1192
+ current.set(id, item);
1193
+ });
1194
+ const pending = [...current.values()];
1195
+ pending.forEach((item) => outbox.delete(item.id));
1196
+ if (pending.some((item) => item.type === "compaction"))
1197
+ store.session.pending[sessionID]?.filter((item) => item.type === "compaction").forEach((item) => outbox.delete(item.id));
1198
+ const inflight = (store.session.pending[sessionID] ?? []).filter((item) => outbox.has(item.id));
1199
+ const merged = inflight.length === 0 ? pending : [...pending, ...inflight];
1200
+ batch(() => {
1201
+ setStore("session", "pending", sessionID, reconcile(merged));
1202
+ merged.forEach(materializeInboxMessage);
1203
+ });
1204
+ } finally {
1205
+ if (pendingUpdates.get(sessionID) === updates)
1206
+ pendingUpdates.delete(sessionID);
1207
+ }
1208
+ });
1209
+ },
1210
+ invalidate(sessionID) {
1211
+ sync.invalidate(`session.pending:${sessionID}`);
1212
+ }
1213
+ },
1214
+ create(input) {
1215
+ const { projectID, ...payload } = input;
1216
+ const id = payload.id ?? SessionID.create();
1217
+ const location = payload.location ?? defaultLocation();
1218
+ const fresh = !store.session.info[id];
1219
+ if (fresh) {
1220
+ const now = Date.now();
1221
+ sessionOutbox.add(id);
1222
+ result.session.remember({
1223
+ id,
1224
+ projectID: projectID ?? store.location[locationKey2(location)]?.info?.project.id ?? "",
1225
+ agent: payload.agent,
1226
+ model: payload.model,
1227
+ cost: 0,
1228
+ tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
1229
+ time: { created: now, updated: now },
1230
+ title: payload.title,
1231
+ location
1232
+ });
1233
+ sync.complete(`session.family:${id}`);
1234
+ sync.complete(`session.pending:${id}`);
1235
+ sync.complete(`session.message:${id}`);
1236
+ }
1237
+ const request = Promise.resolve().then(() => api().session.create({ ...payload, id, location })).then((info) => {
1238
+ sessionOutbox.delete(id);
1239
+ result.session.remember(info);
1240
+ return info;
1241
+ }).catch((error) => {
1242
+ if (fresh && sessionOutbox.delete(id))
1243
+ removeSession(id);
1244
+ throw error;
1245
+ });
1246
+ if (fresh)
1247
+ track(creating, id, request);
1248
+ return { id, request };
1249
+ },
1250
+ compact(input) {
1251
+ const active = compacting.get(input.sessionID);
1252
+ if (active)
1253
+ return active.request;
1254
+ const id = SessionMessage.ID.create();
1255
+ if (!store.session.pending[input.sessionID]?.some((item) => item.type === "compaction")) {
1256
+ outbox.add(id);
1257
+ admitLocal({
1258
+ id,
1259
+ sessionID: input.sessionID,
1260
+ time: { created: Date.now() },
1261
+ type: "compaction",
1262
+ delivery: "steer",
1263
+ payload: {}
1264
+ });
1265
+ }
1266
+ const observed = new Set;
1267
+ const request = sendAdmission(input.sessionID, async () => {
1268
+ if (input.model)
1269
+ await api().session.switchModel({ sessionID: input.sessionID, model: input.model });
1270
+ return api().session.compact({ sessionID: input.sessionID, id });
1271
+ }).then((item) => {
1272
+ batch(() => {
1273
+ outbox.delete(id);
1274
+ if (item.id !== id)
1275
+ removePending(input.sessionID, id);
1276
+ if (!observed.has(item.id) && !messageIndex.get(input.sessionID)?.has(item.id))
1277
+ admitLocal(item);
1278
+ });
1279
+ return item;
1280
+ }).catch((error) => {
1281
+ if (outbox.delete(id))
1282
+ removePending(input.sessionID, id);
1283
+ throw error;
1284
+ }).finally(() => {
1285
+ if (compacting.get(input.sessionID)?.request === request)
1286
+ compacting.delete(input.sessionID);
1287
+ });
1288
+ compacting.set(input.sessionID, { id, observed, request });
1289
+ return request;
1290
+ },
1291
+ prompt(input) {
1292
+ const { gate, prepare, ...request } = input;
1293
+ const id = request.id ?? SessionMessage.ID.create();
1294
+ const fresh = !messageIndex.get(request.sessionID)?.has(id) && !store.session.pending[request.sessionID]?.some((item) => item.id === id);
1295
+ if (fresh) {
1296
+ outbox.add(id);
1297
+ admitLocal({
1298
+ id,
1299
+ sessionID: request.sessionID,
1300
+ time: { created: Date.now() },
1301
+ type: "user",
1302
+ delivery: request.delivery ?? "steer",
1303
+ payload: {
1304
+ text: request.text,
1305
+ agents: request.agents?.map((agent) => ({ ...agent })),
1306
+ metadata: request.metadata
1307
+ }
1308
+ });
1309
+ }
1310
+ return sendAdmission(request.sessionID, async () => {
1311
+ await prepare?.();
1312
+ return api().session.prompt({ ...request, id });
1313
+ }, gate).catch((error) => {
1314
+ if (fresh && outbox.delete(id))
1315
+ retractLocal(request.sessionID, id);
1316
+ throw error;
1317
+ });
1318
+ },
1319
+ sync(sessionID, options) {
1320
+ return sync.run(options?.children ? `session.family:${sessionID}` : `session:${sessionID}`, async () => {
1321
+ const [info, children] = await Promise.all([
1322
+ api().session.get({ sessionID }),
1323
+ options?.children ? api().session.list({ parentID: sessionID, order: "desc" }).then((response) => response.data) : []
1324
+ ]);
1325
+ const sessions = [info, ...children];
1326
+ batch(() => {
1327
+ setStore("session", "info", produce((draft) => {
1328
+ for (const session of sessions)
1329
+ draft[session.id] = session;
1330
+ }));
1331
+ for (const session of sessions) {
1332
+ sync.complete(`session:${session.id}`);
1333
+ registerSession(session.id);
1334
+ }
1335
+ });
1336
+ });
1337
+ },
1338
+ invalidate(sessionID) {
1339
+ sync.invalidate(`session:${sessionID}`);
1340
+ },
1341
+ message: {
1342
+ list(sessionID) {
1343
+ return store.session.message[sessionID] ?? [];
1344
+ },
1345
+ get(sessionID, messageID) {
1346
+ const messages = store.session.message[sessionID];
1347
+ const position = messageIndex.get(sessionID)?.get(messageID);
1348
+ return position === undefined ? undefined : messages?.[position];
1349
+ },
1350
+ sync(sessionID) {
1351
+ return sync.run(`session.message:${sessionID}`, async () => {
1352
+ const response = await api().message.list({
1353
+ sessionID,
1354
+ limit: config.initialMessageLimit?.() ?? messagePageLimit,
1355
+ order: "desc"
1356
+ });
1357
+ const fetched = response.data.toReversed();
1358
+ const ids = new Set(fetched.map((item) => item.id));
1359
+ const admitted = new Set((store.session.pending[sessionID] ?? []).flatMap((item) => item.type === "user" || item.type === "synthetic" ? [item.id] : []));
1360
+ const local = (store.session.message[sessionID] ?? []).filter((item) => !ids.has(item.id) && (outbox.has(item.id) || admitted.has(item.id)));
1361
+ const messages = local.length === 0 ? fetched : [...fetched, ...local];
1362
+ batch(() => {
1363
+ messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])));
1364
+ setStore("session", "message", sessionID, reconcile(messages));
1365
+ setStore("session", "messageCursor", sessionID, response.cursor.next ?? undefined);
1366
+ });
1367
+ });
1368
+ },
1369
+ more(sessionID) {
1370
+ return store.session.messageCursor[sessionID] !== undefined;
1371
+ },
1372
+ loading(sessionID) {
1373
+ return store.session.messageLoading[sessionID] ?? false;
1374
+ },
1375
+ async loadMore(sessionID, options) {
1376
+ const signal = options?.signal;
1377
+ if (signal?.aborted)
1378
+ return;
1379
+ while (messageLoads.has(sessionID)) {
1380
+ const published = await (() => {
1381
+ const pending = messageLoads.get(sessionID);
1382
+ if (!signal)
1383
+ return pending;
1384
+ const aborted = Promise.withResolvers();
1385
+ const cancel = () => aborted.resolve();
1386
+ signal.addEventListener("abort", cancel, { once: true });
1387
+ return Promise.race([pending, aborted.promise]).catch((error) => {
1388
+ if (!signal.aborted)
1389
+ throw error;
1390
+ }).finally(() => signal.removeEventListener("abort", cancel));
1391
+ })();
1392
+ if (!options?.all && published || signal?.aborted)
1393
+ return;
1394
+ }
1395
+ const cursor = store.session.messageCursor[sessionID];
1396
+ if (!cursor || signal?.aborted)
1397
+ return;
1398
+ setStore("session", "messageLoading", sessionID, true);
1399
+ const request = (async () => {
1400
+ const fetched = [];
1401
+ let next = cursor;
1402
+ do {
1403
+ const response = await api().message.list({
1404
+ sessionID,
1405
+ limit: options?.all ? 200 : messagePageLimit,
1406
+ cursor: next
1407
+ }, { signal });
1408
+ if (signal?.aborted)
1409
+ return;
1410
+ fetched.push(...response.data);
1411
+ next = response.cursor.next ?? undefined;
1412
+ if (!options?.all)
1413
+ break;
1414
+ } while (next);
1415
+ const existing = store.session.message[sessionID] ?? [];
1416
+ const ids = new Set(existing.map((item) => item.id));
1417
+ const messages = [...fetched.reverse().filter((item) => !ids.has(item.id)), ...existing];
1418
+ batch(() => {
1419
+ options?.beforePublish?.();
1420
+ messageIndex.set(sessionID, new Map(messages.map((item, position) => [item.id, position])));
1421
+ setStore("session", "message", sessionID, reconcile(messages));
1422
+ setStore("session", "messageCursor", sessionID, next);
1423
+ });
1424
+ return true;
1425
+ })().catch((error) => {
1426
+ if (!signal?.aborted)
1427
+ throw error;
1428
+ }).finally(() => setStore("session", "messageLoading", sessionID, false));
1429
+ track(messageLoads, sessionID, request);
1430
+ await request;
1431
+ },
1432
+ invalidate(sessionID) {
1433
+ sync.invalidate(`session.message:${sessionID}`);
1434
+ }
1435
+ },
1436
+ permission: {
1437
+ list(sessionID) {
1438
+ return store.session.permission[sessionID];
1439
+ },
1440
+ sync(sessionID) {
1441
+ return sync.run(`session.permission:${sessionID}`, async () => {
1442
+ setStore("session", "permission", sessionID, await api().permission.list({ sessionID }));
1443
+ });
1444
+ },
1445
+ invalidate(sessionID) {
1446
+ sync.invalidate(`session.permission:${sessionID}`);
1447
+ },
1448
+ async reply(input) {
1449
+ await api().permission.reply(input).catch((error) => {
1450
+ if (!isPermissionNotFoundError2(error))
1451
+ throw error;
1452
+ });
1453
+ removePermission(input.sessionID, input.requestID);
1454
+ }
1455
+ },
1456
+ form: {
1457
+ list(sessionID, ref) {
1458
+ const forms = store.session.form[sessionID];
1459
+ if (sessionID !== "global")
1460
+ return forms;
1461
+ if (!ref)
1462
+ return;
1463
+ const key = locationKey2(ref);
1464
+ return forms?.filter((form) => form.location && locationKey2(form.location) === key);
1465
+ },
1466
+ sync(sessionID, ref) {
1467
+ const key = `session.form:${sessionID}:${sessionID === "global" ? locationKey2(ref ?? defaultLocation()) : ""}`;
1468
+ return sync.run(key, async () => {
1469
+ if (sessionID === "global") {
1470
+ const response = await api().form.list({
1471
+ location: locationQuery(ref ?? defaultLocation())
1472
+ });
1473
+ const location = {
1474
+ directory: response.location.directory
1475
+ };
1476
+ const locationID = locationKey2(location);
1477
+ setStore("session", "form", sessionID, [
1478
+ ...(store.session.form[sessionID] ?? []).filter((form) => form.location && locationKey2(form.location) !== locationID),
1479
+ ...response.data.filter((form) => form.sessionID === "global").map((form) => ({ ...form, location }))
1480
+ ]);
1481
+ return;
1482
+ }
1483
+ setStore("session", "form", sessionID, await api().session.form.list({ sessionID }));
1484
+ });
1485
+ },
1486
+ invalidate(sessionID, ref) {
1487
+ sync.invalidate(`session.form:${sessionID}:${sessionID === "global" ? locationKey2(ref ?? defaultLocation()) : ""}`);
1488
+ },
1489
+ reply(input, ref) {
1490
+ return settleForm(input, ref, api().session.form.reply(input, formRequestOptions(input.sessionID, ref)));
1491
+ },
1492
+ cancel(input, ref) {
1493
+ return settleForm(input, ref, api().session.form.cancel(input, formRequestOptions(input.sessionID, ref)));
1494
+ }
1495
+ }
1496
+ },
1497
+ project: {
1498
+ list() {
1499
+ return Object.values(store.project.info).toSorted((a, b) => b.time.updated - a.time.updated);
1500
+ },
1501
+ get(projectID) {
1502
+ return store.project.info[projectID];
1503
+ },
1504
+ sync() {
1505
+ return sync.run("project", async () => {
1506
+ const projects = await api().project.list();
1507
+ setStore("project", "info", reconcile(Object.fromEntries(projects.map((project) => [project.id, project]))));
1508
+ });
1509
+ },
1510
+ invalidate() {
1511
+ sync.invalidate("project");
1512
+ },
1513
+ permission: {
1514
+ list(projectID) {
1515
+ return store.project.permission[projectID];
1516
+ },
1517
+ sync(projectID) {
1518
+ return sync.run(`project.permission:${projectID}`, async () => {
1519
+ setStore("project", "permission", projectID, await api().permission.saved.list({ projectID }));
1520
+ });
1521
+ },
1522
+ invalidate(projectID) {
1523
+ sync.invalidate(`project.permission:${projectID}`);
1524
+ }
1525
+ }
1526
+ },
1527
+ shell: {
1528
+ list(location) {
1529
+ return Object.values(shells.list(location) ?? {});
1530
+ },
1531
+ listBySession(sessionID) {
1532
+ return Object.values(store.location).flatMap((data) => Object.values(data.shell ?? {})).filter((shell) => shell.metadata.sessionID === sessionID);
1533
+ },
1534
+ get(id) {
1535
+ return Object.values(store.location).map((data) => data.shell?.[id]).find((shell) => shell !== undefined);
1536
+ },
1537
+ sync: shells.sync,
1538
+ invalidate: shells.invalidate
1539
+ },
1540
+ location: {
1541
+ info(ref) {
1542
+ return store.location[locationKey2(ref ?? defaultLocation())]?.info;
1543
+ },
1544
+ default() {
1545
+ return defaultLocation();
1546
+ },
1547
+ syncInfo(ref) {
1548
+ const current = ref ?? defaultLocation();
1549
+ return sync.run(`location:${locationKey2(current)}`, async () => {
1550
+ const location = await api().location.get({ location: locationQuery(current) });
1551
+ const key = locationKey2(location);
1552
+ if (!store.location[key])
1553
+ setStore("location", key, {});
1554
+ setStore("location", key, "info", location);
1555
+ if (!ref) {
1556
+ setDefaultLocation({ directory: location.directory });
1557
+ }
1558
+ });
1559
+ },
1560
+ async sync(ref) {
1561
+ await result.location.syncInfo(ref);
1562
+ const location = ref ?? defaultLocation();
1563
+ await Promise.all([
1564
+ result.location.vcs.sync(location),
1565
+ result.location.agent.sync(location),
1566
+ result.location.command.sync(location),
1567
+ result.location.integration.sync(location),
1568
+ result.location.mcp.server.sync(location),
1569
+ result.location.mcp.resource.sync(location),
1570
+ result.location.model.sync(location),
1571
+ result.location.provider.sync(location),
1572
+ result.location.reference.sync(location),
1573
+ result.location.skill.sync(location),
1574
+ result.shell.sync(location),
1575
+ result.session.form.sync("global", location)
1576
+ ]);
1577
+ },
1578
+ invalidate(ref) {
1579
+ const location = ref ?? defaultLocation();
1580
+ sync.invalidate(`location:${locationKey2(location)}`);
1581
+ result.location.vcs.invalidate(location);
1582
+ result.location.agent.invalidate(location);
1583
+ result.location.command.invalidate(location);
1584
+ result.location.config.invalidate(location);
1585
+ result.location.integration.invalidate(location);
1586
+ result.location.mcp.server.invalidate(location);
1587
+ result.location.mcp.resource.invalidate(location);
1588
+ result.location.model.invalidate(location);
1589
+ result.location.provider.invalidate(location);
1590
+ result.location.reference.invalidate(location);
1591
+ result.location.skill.invalidate(location);
1592
+ result.shell.invalidate(location);
1593
+ result.session.form.invalidate("global", location);
1594
+ },
1595
+ vcs: { info: vcs.list, sync: vcs.sync, invalidate: vcs.invalidate },
1596
+ agent: locationResource("agent", (location) => api().agent.list({ location })),
1597
+ command: locationResource("command", (location) => api().command.list({ location })),
1598
+ config: locationResource("config", async (location) => ({
1599
+ location: { directory: location.directory },
1600
+ data: await api().config.get({ location })
1601
+ })),
1602
+ integration: locationResource("integration", (location) => api().integration.list({ location })),
1603
+ mcp: {
1604
+ server: locationResource("mcpServer", (location) => api().mcp.list({ location })),
1605
+ resource: locationResource("mcpResource", async (location) => {
1606
+ const response = await api().mcp.resource.catalog({ location });
1607
+ return { location: response.location, data: response.data.resources };
1608
+ })
1609
+ },
1610
+ model: locationResource("model", (location) => api().model.list({ location }), { alias: true }),
1611
+ provider: locationResource("provider", (location) => api().provider.list({ location }), { alias: true }),
1612
+ reference: locationResource("reference", (location) => api().reference.list({ location })),
1613
+ websearch: {
1614
+ list(location) {
1615
+ return store.location[locationKey2(location ?? defaultLocation())]?.websearch;
1616
+ },
1617
+ async refresh(ref) {
1618
+ const input = { location: locationQuery(ref ?? defaultLocation()) };
1619
+ const providers = await api().websearch.providers(input);
1620
+ const key = locationKey2(providers.location);
1621
+ setStore("location", key, { websearch: providers.data });
1622
+ }
1623
+ },
1624
+ skill: locationResource("skill", (location) => api().skill.list({ location }))
1625
+ }
1626
+ };
1627
+ createEffect(() => {
1628
+ if (config.connection?.status() === "connected")
1629
+ return;
1630
+ sync.invalidate();
1631
+ });
1632
+ onCleanup(config.event.listen(({ details }) => {
1633
+ handleEvent(details);
1634
+ }));
1635
+ return result;
1636
+ }
1637
+
1638
+ export { settleMs2, locationKey2, createData2 };