@opencode/client 2.0.8 → 2.0.10

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/service-contender-1tbj6z39.js +81 -0
  2. package/dist/chunks/service-contender-4tveyqeh.js +34 -0
  3. package/dist/chunks/service-contender-6mptkzbc.js +0 -0
  4. package/dist/chunks/service-contender-7p1wryha.js +30 -0
  5. package/dist/chunks/service-contender-9rqn6x4v.js +16 -0
  6. package/dist/chunks/service-contender-atpwm825.js +1638 -0
  7. package/dist/chunks/service-contender-baxqtw51.js +52 -0
  8. package/dist/chunks/service-contender-frbwqjmf.js +145 -0
  9. package/dist/chunks/service-contender-hv5bnxzg.js +594 -0
  10. package/dist/chunks/service-contender-m2eywc8w.js +0 -0
  11. package/dist/chunks/service-contender-n3c99vt5.js +11 -0
  12. package/dist/chunks/service-contender-pgjv2t62.js +112 -0
  13. package/dist/chunks/service-contender-qav6mfdz.js +0 -0
  14. package/dist/chunks/service-contender-radw39tv.js +1402 -0
  15. package/dist/chunks/service-contender-sxt88q69.js +12 -0
  16. package/dist/chunks/service-contender-t1xa31vm.js +39 -0
  17. package/dist/chunks/service-contender-txckps9j.js +57 -0
  18. package/dist/chunks/service-contender-vmytr4q8.js +4 -0
  19. package/dist/chunks/service-contender-vyas3vgt.js +0 -0
  20. package/dist/chunks/service-contender-xgnj2w4v.js +20 -0
  21. package/dist/chunks/service-contender-y2etg5bs.js +9 -0
  22. package/dist/chunks/service-contender-ydhz1zrh.js +238 -0
  23. package/dist/chunks/service-contender-yepdgb80.js +44 -0
  24. package/dist/chunks/service-contender-z6qdwcsf.js +62 -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 +90 -88
  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 -1774
  54. package/dist/solid/index.js +32 -3
  55. package/dist/solid/pty.js +8 -41
  56. package/package.json +4 -4
@@ -1,1774 +1,18 @@
1
- // Client data layer: apply server events and cache API reads into a Solid store.
2
- // Prefer straightforward projection. Invalidated reads revalidate serially so an older
3
- // response cannot commit after its replacement. Reconnect invalidates cached reads;
4
- // active UI owners decide what to sync again.
5
- import { Worktree } from "@opencode/schema/worktree";
6
- import { SessionID } from "@opencode/schema/session-id";
7
- import { SessionMessage } from "@opencode/schema/session-message";
8
- import { isFormAlreadySettledError, isFormNotFoundError, isPermissionNotFoundError, } from "../promise";
9
- import { createStore, produce, reconcile } from "solid-js/store";
10
- import { batch, createEffect, createMemo, createSignal, onCleanup } from "solid-js";
11
- const messageIDFromEvent = (eventID) => eventID.replace(/^evt_/, "msg_");
12
- const messagePageLimit = 20;
13
- // Trailing window for event bursts that each ask for the same refetch.
14
- export const settleMs = 150;
15
- export function locationKey(location) {
16
- return JSON.stringify([location.directory, location.workspaceID]);
17
- }
18
- function locationQuery(ref) {
19
- return { directory: ref.directory };
20
- }
21
- function formRequestOptions(sessionID, ref) {
22
- if (sessionID !== "global" || !ref)
23
- return undefined;
24
- return {
25
- headers: {
26
- "x-opencode-directory": encodeURIComponent(ref.directory),
27
- },
28
- };
29
- }
30
- function createSync() {
31
- const state = new Map();
32
- const start = (key, load, wait) => {
33
- const entry = { promise: Promise.resolve(), invalidated: false, started: !wait };
34
- state.set(key, entry);
35
- const run = () => {
36
- entry.started = true;
37
- return load();
38
- };
39
- entry.promise = (wait ? wait.catch(() => undefined).then(run) : run())
40
- .then(() => {
41
- if (state.get(key) === entry && !entry.invalidated)
42
- state.set(key, true);
43
- })
44
- .finally(() => {
45
- if (state.get(key) === entry)
46
- state.delete(key);
47
- });
48
- return entry.promise;
49
- };
50
- return {
51
- run(key, load) {
52
- const active = state.get(key);
53
- if (active === true)
54
- return Promise.resolve();
55
- if (!active)
56
- return start(key, load);
57
- if (!active.invalidated)
58
- return active.promise;
59
- return start(key, load, active.promise);
60
- },
61
- complete(key) {
62
- if (state.has(key))
63
- return;
64
- state.set(key, true);
65
- },
66
- has(key) {
67
- return state.has(key);
68
- },
69
- pending(key) {
70
- const active = state.get(key);
71
- return active !== undefined && active !== true;
72
- },
73
- invalidate(key) {
74
- if (key) {
75
- const active = state.get(key);
76
- if (active === true)
77
- state.delete(key);
78
- if (active !== undefined && active !== true && active.started)
79
- active.invalidated = true;
80
- return;
81
- }
82
- state.forEach((active, current) => {
83
- if (active === true)
84
- state.delete(current);
85
- if (active !== true && active.started)
86
- active.invalidated = true;
87
- });
88
- },
89
- };
90
- }
91
- export function createData(config) {
92
- const api = config.api;
93
- let disposed = false;
94
- onCleanup(() => (disposed = true));
95
- function refresh(load) {
96
- if (disposed || (config.connection && config.connection.status() !== "connected"))
97
- return;
98
- void load().catch((error) => {
99
- if (disposed || (config.connection && config.connection.status() !== "connected"))
100
- return;
101
- if (config.onError)
102
- return config.onError(error);
103
- console.error("Failed to refresh client data", error);
104
- });
105
- }
106
- // Runs `load` once a burst of same-key events goes quiet, so N events cost one refetch.
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
- }, settleMs));
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 && locationKey(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 || locationKey(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
176
- .catch((error) => {
177
- if ((!isFormNotFoundError(error) && !isFormAlreadySettledError(error)) || error.id !== input.formID)
178
- throw error;
179
- })
180
- .then(() => {
181
- if (!removeForm(input.sessionID, input.formID, ref))
182
- return;
183
- result.session.form.invalidate(input.sessionID, ref);
184
- void result.session.form.sync(input.sessionID, ref).catch(() => undefined);
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
- // Inbox IDs of optimistic admissions awaiting acknowledgement, so rejection
196
- // only rolls back unacknowledged rows and a pending re-fetch cannot wipe a
197
- // row the server does not know about yet. Prompts clear on their durable
198
- // echo, positive pending read, or rollback; compactions also reconcile the
199
- // POST's canonical ID.
200
- const outbox = new Set();
201
- // Session IDs of optimistic create admissions still awaiting acknowledgement
202
- // (the session.created echo or the create response itself). A failed create
203
- // only rolls back a session the server never acknowledged. Unlike
204
- // `creating`, this clears on the echo rather than request settlement.
205
- const sessionOutbox = new Set();
206
- // In-flight optimistic creates by session ID. prompt() gates its POST on
207
- // this so a prompt sent to a still-creating session waits for the session
208
- // to exist server-side instead of failing with "not found".
209
- const creating = new Map();
210
- // Per-session send chain: prompts and compactions must be admitted in
211
- // submission order. Each waits for the previous POST to settle, so one
212
- // failure does not block the next.
213
- const sending = new Map();
214
- const messageLoads = new Map();
215
- const compacting = new Map();
216
- onCleanup(() => compacting.clear());
217
- // Register `promise` under `key` until it settles. A later registration
218
- // replaces an earlier one; settlement only clears its own entry.
219
- function track(map, key, promise) {
220
- map.set(key, promise);
221
- const settle = () => {
222
- if (map.get(key) === promise)
223
- map.delete(key);
224
- };
225
- void promise.then(settle, settle);
226
- }
227
- // Capture creation before settlement clears its entry, so dependent RPCs still see a failed create.
228
- function sendAdmission(sessionID, send, gate) {
229
- const created = creating.get(sessionID);
230
- const previous = sending.get(sessionID);
231
- const request = Promise.resolve()
232
- .then(() => Promise.all([gate, created, previous]))
233
- .then(send);
234
- track(sending, sessionID, request.catch(() => undefined));
235
- return request;
236
- }
237
- // Upsert an admitted inbox item into pending and (for user and synthetic
238
- // items) the visible transcript. Used by the inbox.enqueued
239
- // handler and by optimistic admission; the upsert is what reconciles
240
- // the durable echo with an optimistic placeholder — the durable payload and
241
- // times replace the client's guess.
242
- function admitLocal(item) {
243
- batch(() => {
244
- const pending = store.session.pending[item.sessionID] ?? [];
245
- const at = pending.findIndex((entry) => entry.id === item.id);
246
- setStore("session", "pending", item.sessionID, at < 0 ? [...pending, item] : pending.map((entry, index) => (index === at ? item : entry)));
247
- if (item.type === "compaction")
248
- return;
249
- materializeInboxMessage(item);
250
- });
251
- }
252
- function materializeInboxMessage(item) {
253
- if (item.type !== "user" && item.type !== "synthetic")
254
- return;
255
- message.update(item.sessionID, (draft, index) => {
256
- const row = item.type === "user"
257
- ? { id: item.id, type: "user", ...item.payload, time: { created: item.time.created } }
258
- : { id: item.id, type: "synthetic", ...item.payload, time: { created: item.time.created } };
259
- const position = index.get(item.id);
260
- if (position === undefined)
261
- return message.append(draft, index, row);
262
- draft[position] = row;
263
- });
264
- }
265
- // Remove an inbox item from pending, input, and the visible transcript.
266
- // Used by the inbox.cancelled handler and by optimistic rollback.
267
- function retractLocal(sessionID, inboxID) {
268
- batch(() => {
269
- removePending(sessionID, inboxID);
270
- if (!messageIndex.get(sessionID)?.has(inboxID))
271
- return;
272
- message.update(sessionID, (draft, index) => {
273
- const position = index.get(inboxID);
274
- if (position === undefined)
275
- return;
276
- draft.splice(position, 1);
277
- index.delete(inboxID);
278
- message.reindex(draft, index, position);
279
- });
280
- });
281
- }
282
- const message = {
283
- update(sessionID, fn) {
284
- setStore("session", "message", produce((draft) => {
285
- fn((draft[sessionID] ??= []), index(sessionID));
286
- }));
287
- },
288
- append(messages, index, item) {
289
- if (index.has(item.id))
290
- return;
291
- index.set(item.id, messages.length);
292
- messages.push(item);
293
- },
294
- insert(sessionID, item) {
295
- message.update(sessionID, (draft, index) => message.append(draft, index, item));
296
- },
297
- // Streaming events target one assistant message and, within it, the latest part of a kind.
298
- // A missing target means the row was never loaded or was evicted; the event is dropped.
299
- editAssistant(sessionID, messageID, fn) {
300
- message.update(sessionID, (draft, index) => {
301
- const position = index.get(messageID);
302
- const item = position === undefined ? undefined : draft[position];
303
- if (item?.type === "assistant")
304
- fn(item);
305
- });
306
- },
307
- editTool(sessionID, messageID, toolID, fn) {
308
- message.editAssistant(sessionID, messageID, (assistant) => {
309
- const tool = assistant.content.findLast((item) => item.type === "tool" && item.id === toolID);
310
- if (tool)
311
- fn(tool);
312
- });
313
- },
314
- editText(sessionID, messageID, fn) {
315
- message.editAssistant(sessionID, messageID, (assistant) => {
316
- const text = assistant.content.findLast((item) => item.type === "text");
317
- if (text)
318
- fn(text);
319
- });
320
- },
321
- editReasoning(sessionID, messageID, fn) {
322
- message.editAssistant(sessionID, messageID, (assistant) => {
323
- const reasoning = assistant.content.findLast((item) => item.type === "reasoning" && !item.time?.completed);
324
- if (reasoning)
325
- fn(reasoning);
326
- });
327
- },
328
- activeAssistant(messages) {
329
- const item = messages.findLast((item) => item.type === "assistant" && !item.time.completed);
330
- return item?.type === "assistant" ? item : undefined;
331
- },
332
- shell(messages, shellID) {
333
- const item = messages.findLast((item) => item.type === "shell" && item.shellID === shellID);
334
- return item?.type === "shell" ? item : undefined;
335
- },
336
- compaction(messages) {
337
- const item = messages.findLast((item) => item.type === "compaction" && item.status === "running");
338
- return item?.type === "compaction" ? item : undefined;
339
- },
340
- reindex(messages, index, start) {
341
- for (let position = start; position < messages.length; position++) {
342
- const item = messages[position];
343
- if (item)
344
- index.set(item.id, position);
345
- }
346
- },
347
- };
348
- function index(sessionID) {
349
- const existing = messageIndex.get(sessionID);
350
- if (existing)
351
- return existing;
352
- const created = new Map();
353
- messageIndex.set(sessionID, created);
354
- return created;
355
- }
356
- // Walk parentID upward through loaded session info to the family root. When a
357
- // parent's info is missing, that missing ID is the furthest-known ancestor and
358
- // is returned so orphan subtrees group under it until the parent arrives. A
359
- // seen set guards against parent cycles, stopping at the last non-repeating
360
- // ancestor.
361
- function resolveRoot(sessionID) {
362
- let current = sessionID;
363
- let parentID = store.session.info[sessionID]?.parentID;
364
- const seen = new Set([sessionID]);
365
- while (parentID) {
366
- if (seen.has(parentID))
367
- break;
368
- seen.add(parentID);
369
- current = parentID;
370
- parentID = store.session.info[parentID]?.parentID;
371
- }
372
- return current;
373
- }
374
- // Register one session into the family index. Idempotent: syncing an
375
- // existing session never duplicates its ID. When a tentative family keyed by
376
- // sessionID exists (descendants arrived while sessionID's own info was
377
- // absent) but sessionID turns out to have a parent, fold the orphan subtree
378
- // into the resolved root's family and drop the tentative entry.
379
- function registerSession(sessionID) {
380
- const info = store.session.info[sessionID];
381
- if (!info)
382
- return;
383
- const rootID = resolveRoot(sessionID);
384
- setStore("session", "family", produce((draft) => {
385
- if (sessionID !== rootID && draft[sessionID]) {
386
- const members = (draft[rootID] ??= []);
387
- for (const id of draft[sessionID]) {
388
- if (!members.includes(id))
389
- members.push(id);
390
- }
391
- delete draft[sessionID];
392
- }
393
- const family = (draft[rootID] ??= []);
394
- if (!family.includes(sessionID))
395
- family.push(sessionID);
396
- }));
397
- }
398
- function evictSession(sessionID) {
399
- if (sessionOutbox.has(sessionID))
400
- return;
401
- sync.invalidate(`session.pending:${sessionID}`);
402
- sync.invalidate(`session.message:${sessionID}`);
403
- messageLoads.delete(sessionID);
404
- // Keep unacknowledged submissions until their echo or rollback settles them.
405
- const pending = store.session.pending[sessionID]?.filter((item) => outbox.has(item.id)) ?? [];
406
- const messages = store.session.message[sessionID]?.filter((item) => outbox.has(item.id)) ?? [];
407
- messageIndex.delete(sessionID);
408
- if (messages.length)
409
- messageIndex.set(sessionID, new Map(messages.map((item, index) => [item.id, index])));
410
- setStore("session", produce((draft) => {
411
- delete draft.message[sessionID];
412
- delete draft.messageCursor[sessionID];
413
- delete draft.messageLoading[sessionID];
414
- delete draft.pending[sessionID];
415
- if (messages.length)
416
- draft.message[sessionID] = messages;
417
- if (pending.length)
418
- draft.pending[sessionID] = pending;
419
- }));
420
- }
421
- function removeSession(sessionID) {
422
- activeUpdates?.set(sessionID, undefined);
423
- store.session.pending[sessionID]?.forEach((item) => outbox.delete(item.id));
424
- messageIndex.delete(sessionID);
425
- sync.invalidate(`session:${sessionID}`);
426
- sync.invalidate(`session.family:${sessionID}`);
427
- sync.invalidate(`session.pending:${sessionID}`);
428
- sync.invalidate(`session.message:${sessionID}`);
429
- sync.invalidate(`session.permission:${sessionID}`);
430
- sync.invalidate(`session.form:${sessionID}:`);
431
- setStore("session", produce((draft) => {
432
- delete draft.info[sessionID];
433
- delete draft.active[sessionID];
434
- delete draft.message[sessionID];
435
- delete draft.messageCursor[sessionID];
436
- delete draft.messageLoading[sessionID];
437
- delete draft.pending[sessionID];
438
- delete draft.permission[sessionID];
439
- delete draft.form[sessionID];
440
- for (const [rootID, family] of Object.entries(draft.family)) {
441
- const next = family.filter((id) => id !== sessionID);
442
- if (next.length === 0)
443
- delete draft.family[rootID];
444
- else
445
- draft.family[rootID] = next;
446
- }
447
- }));
448
- }
449
- function handleEvent(event) {
450
- switch (event.type) {
451
- case "server.connected": {
452
- const updates = new Map();
453
- activeUpdates = updates;
454
- refresh(() => api()
455
- .session.active()
456
- .then((active) => {
457
- if (activeUpdates !== updates)
458
- return;
459
- // Lifecycle events received during hydration supersede the snapshot.
460
- const snapshot = new Map(Object.keys(active).map((id) => [id, "running"]));
461
- updates.forEach((status, id) => {
462
- if (status === undefined)
463
- return snapshot.delete(id);
464
- snapshot.set(id, status);
465
- });
466
- activeUpdates = undefined;
467
- setStore("session", "active", reconcile(Object.fromEntries(snapshot)));
468
- })
469
- .catch(() => {
470
- if (activeUpdates === updates)
471
- activeUpdates = undefined;
472
- }));
473
- refresh(() => api()
474
- .location.get({ location: locationQuery(defaultLocation()) })
475
- .then((location) => {
476
- const key = locationKey(location);
477
- setStore("location", key, { info: location });
478
- }));
479
- refresh(() => result.location.vcs.sync());
480
- refresh(() => result.project.sync());
481
- return;
482
- }
483
- case "project.updated":
484
- setStore("project", "info", event.data.id, reconcile(event.data));
485
- return;
486
- case "session.created":
487
- sessionOutbox.delete(event.data.sessionID);
488
- result.session.invalidate(event.data.sessionID);
489
- refresh(() => result.session.sync(event.data.sessionID));
490
- // Band-aid: a newly created session starts empty, so live events can be its source of truth.
491
- // Fetching pending inputs and projected messages separately lets promotion move an input between snapshots,
492
- // causing both requests to miss it and overwrite event-built state. Skip those racy initial reads until
493
- // hydration can load pending and projected messages atomically.
494
- sync.complete(`session.pending:${event.data.sessionID}`);
495
- sync.complete(`session.message:${event.data.sessionID}`);
496
- return;
497
- case "session.deleted":
498
- removeSession(event.data.sessionID);
499
- return;
500
- case "session.usage.updated":
501
- if (store.session.info[event.data.sessionID])
502
- setStore("session", "info", event.data.sessionID, {
503
- cost: event.data.cost,
504
- tokens: event.data.tokens,
505
- });
506
- return;
507
- case "session.agent.selected": {
508
- const previous = store.session.info[event.data.sessionID]?.agent;
509
- if (store.session.info[event.data.sessionID])
510
- setStore("session", "info", event.data.sessionID, "agent", event.data.agent);
511
- message.insert(event.data.sessionID, {
512
- id: messageIDFromEvent(event.id),
513
- type: "agent-switched",
514
- agent: event.data.agent,
515
- previous,
516
- time: { created: event.created },
517
- });
518
- return;
519
- }
520
- case "session.model.selected":
521
- if (store.session.info[event.data.sessionID])
522
- setStore("session", "info", event.data.sessionID, "model", event.data.model);
523
- if (!store.session.message[event.data.sessionID])
524
- return;
525
- message.insert(event.data.sessionID, {
526
- id: messageIDFromEvent(event.id),
527
- type: "model-switched",
528
- model: event.data.model,
529
- time: { created: event.created },
530
- });
531
- refresh(() => api()
532
- .session.message.get({ sessionID: event.data.sessionID, messageID: messageIDFromEvent(event.id) })
533
- .then((item) => {
534
- message.update(event.data.sessionID, (draft, index) => {
535
- const position = index.get(item.id);
536
- if (position === undefined)
537
- return message.append(draft, index, item);
538
- draft[position] = item;
539
- });
540
- }));
541
- return;
542
- case "session.renamed": {
543
- // Preserve the live title when it races the session's initial read.
544
- refresh(() => {
545
- const family = sync.pending(`session.family:${event.data.sessionID}`)
546
- ? result.session.sync(event.data.sessionID, { children: true })
547
- : Promise.resolve();
548
- return Promise.all([result.session.sync(event.data.sessionID), family]).then(() => {
549
- if (store.session.info[event.data.sessionID])
550
- setStore("session", "info", event.data.sessionID, "title", event.data.title);
551
- });
552
- });
553
- return;
554
- }
555
- case "session.permissions":
556
- if (store.session.info[event.data.sessionID])
557
- setStore("session", "info", event.data.sessionID, "permissions", event.data.permissions);
558
- return;
559
- case "session.moved": {
560
- const current = store.session.info[event.data.sessionID];
561
- if (current) {
562
- const previous = {
563
- location: { ...current.location },
564
- projectID: current.projectID,
565
- subpath: current.subpath,
566
- };
567
- setStore("session", "info", event.data.sessionID, "location", event.data.location);
568
- if (event.data.projectID)
569
- setStore("session", "info", event.data.sessionID, "projectID", event.data.projectID);
570
- setStore("session", "info", event.data.sessionID, "subpath", event.data.subpath);
571
- message.insert(event.data.sessionID, {
572
- id: messageIDFromEvent(event.id),
573
- type: "location-switched",
574
- location: event.data.location,
575
- projectID: event.data.projectID,
576
- subpath: event.data.subpath,
577
- previous,
578
- time: { created: event.created },
579
- });
580
- }
581
- return;
582
- }
583
- case "worktree.resolved": {
584
- for (const [sessionID, info] of Object.entries(store.session.info)) {
585
- const explicit = event.data.adopted?.includes(info.projectID);
586
- const directory = explicit ? store.project.info[info.projectID]?.canonical : info.location.directory;
587
- if (!directory) {
588
- result.session.invalidate(sessionID);
589
- refresh(() => result.session.sync(sessionID));
590
- continue;
591
- }
592
- const adopted = Worktree.adopt({
593
- projectID: info.projectID,
594
- directory,
595
- }, event.data);
596
- if (!adopted)
597
- continue;
598
- setStore("session", "info", sessionID, "projectID", adopted.projectID);
599
- setStore("session", "info", sessionID, "subpath", adopted.subpath);
600
- }
601
- return;
602
- }
603
- case "session.inbox.delivered": {
604
- const admitted = result.session.input.has(event.data.sessionID, event.data.inboxID);
605
- removePending(event.data.sessionID, event.data.inboxID);
606
- message.update(event.data.sessionID, (draft, index) => {
607
- const position = index.get(event.data.inboxID);
608
- if (position === undefined)
609
- return;
610
- const existing = draft[position];
611
- if (!existing || !admitted)
612
- return;
613
- existing.time.created = event.created;
614
- draft.splice(position, 1);
615
- draft.push(existing);
616
- message.reindex(draft, index, position);
617
- });
618
- compacting.get(event.data.sessionID)?.observed.add(event.data.inboxID);
619
- return;
620
- }
621
- case "session.inbox.delivery.changed":
622
- updatePending(event.data.sessionID, event.data.inboxID, event.data.delivery);
623
- return;
624
- case "session.inbox.cancelled": {
625
- retractLocal(event.data.sessionID, event.data.inboxID);
626
- compacting.get(event.data.sessionID)?.observed.add(event.data.inboxID);
627
- return;
628
- }
629
- case "session.inbox.enqueued": {
630
- outbox.delete(event.data.inboxID);
631
- const item = {
632
- id: event.data.inboxID,
633
- sessionID: event.data.sessionID,
634
- time: { created: event.created },
635
- ...event.data.item,
636
- };
637
- pendingUpdates.get(item.sessionID)?.set(item.id, item);
638
- admitLocal(item);
639
- if (event.data.item.type === "compaction") {
640
- const active = compacting.get(event.data.sessionID);
641
- active?.observed.add(event.data.inboxID);
642
- if (active && active.id !== event.data.inboxID && outbox.delete(active.id))
643
- removePending(event.data.sessionID, active.id);
644
- }
645
- return;
646
- }
647
- case "session.instructions.updated":
648
- // Mirror the projector: the initial baseline and empty-rendering deltas carry no text
649
- // and produce no transcript message.
650
- const updateText = event.data.text;
651
- if (updateText === undefined)
652
- return;
653
- message.insert(event.data.sessionID, {
654
- id: messageIDFromEvent(event.id),
655
- type: "system",
656
- text: updateText,
657
- description: `Instructions updated: ${Object.keys(event.data.delta).join(", ")}`,
658
- metadata: event.metadata,
659
- time: { created: event.created },
660
- });
661
- return;
662
- case "session.synthetic":
663
- message.insert(event.data.sessionID, {
664
- id: messageIDFromEvent(event.id),
665
- type: "synthetic",
666
- text: event.data.text,
667
- description: event.data.description,
668
- metadata: event.data.metadata,
669
- time: { created: event.created },
670
- });
671
- return;
672
- case "session.shell.started":
673
- message.insert(event.data.sessionID, {
674
- id: messageIDFromEvent(event.id),
675
- type: "shell",
676
- shellID: event.data.shell.id,
677
- command: event.data.shell.command,
678
- status: event.data.shell.status,
679
- exit: event.data.shell.exit,
680
- metadata: event.data.shell.metadata.background === true ? { ...event.metadata, background: true } : event.metadata,
681
- time: { created: event.created },
682
- });
683
- return;
684
- case "session.shell.ended":
685
- message.update(event.data.sessionID, (draft) => {
686
- const match = message.shell(draft, event.data.shell.id);
687
- if (!match)
688
- return;
689
- match.status = event.data.shell.status;
690
- match.exit = event.data.shell.exit;
691
- match.output = event.data.output;
692
- match.time.completed = event.created;
693
- });
694
- return;
695
- case "session.step.started":
696
- message.update(event.data.sessionID, (draft, index) => {
697
- const position = index.get(event.data.assistantMessageID);
698
- const existing = position === undefined ? undefined : draft[position];
699
- if (existing?.type === "assistant") {
700
- existing.agent = event.data.agent;
701
- existing.model = event.data.model;
702
- existing.retry = undefined;
703
- existing.error = undefined;
704
- existing.finish = undefined;
705
- existing.rawFinish = undefined;
706
- existing.providerState = undefined;
707
- existing.time.created = event.data.started;
708
- existing.time.streamed = undefined;
709
- existing.time.completed = undefined;
710
- if (event.data.snapshot)
711
- existing.snapshot = { ...existing.snapshot, start: event.data.snapshot };
712
- return;
713
- }
714
- const currentAssistant = message.activeAssistant(draft);
715
- if (currentAssistant) {
716
- currentAssistant.retry = undefined;
717
- currentAssistant.time.completed = event.created;
718
- }
719
- message.append(draft, index, {
720
- id: event.data.assistantMessageID,
721
- type: "assistant",
722
- agent: event.data.agent,
723
- model: event.data.model,
724
- metadata: event.metadata,
725
- content: [],
726
- snapshot: event.data.snapshot ? { start: event.data.snapshot } : undefined,
727
- time: { created: event.data.started },
728
- });
729
- });
730
- return;
731
- case "session.step.streamed":
732
- message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
733
- assistant.time.streamed = event.created;
734
- });
735
- return;
736
- case "session.step.ended": {
737
- message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
738
- assistant.time.completed = event.created;
739
- assistant.finish = event.data.finish;
740
- assistant.rawFinish = event.data.rawFinish;
741
- assistant.providerState = event.data.providerState;
742
- assistant.cost = event.data.cost;
743
- assistant.tokens = event.data.tokens;
744
- if (event.data.snapshot)
745
- assistant.snapshot = { ...assistant.snapshot, end: event.data.snapshot };
746
- });
747
- return;
748
- }
749
- case "session.step.failed":
750
- message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
751
- assistant.time.completed = event.created;
752
- assistant.finish = event.data.finish ?? "error";
753
- assistant.rawFinish = event.data.rawFinish;
754
- assistant.providerState = event.data.providerState;
755
- assistant.error = event.data.error;
756
- assistant.retry = undefined;
757
- if (event.data.cost !== undefined && event.data.tokens !== undefined) {
758
- assistant.cost = event.data.cost;
759
- assistant.tokens = event.data.tokens;
760
- }
761
- });
762
- return;
763
- case "session.text.started":
764
- message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
765
- assistant.content.push({ type: "text", text: "" });
766
- });
767
- return;
768
- case "session.text.delta":
769
- message.editText(event.data.sessionID, event.data.assistantMessageID, (text) => {
770
- text.text += event.data.delta;
771
- });
772
- return;
773
- case "session.text.ended":
774
- message.editText(event.data.sessionID, event.data.assistantMessageID, (text) => {
775
- text.text = event.data.text;
776
- });
777
- return;
778
- case "session.tool.input.started":
779
- message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
780
- assistant.content.push({
781
- type: "tool",
782
- id: event.data.id,
783
- name: event.data.name,
784
- time: { created: event.created },
785
- state: { status: "streaming", input: "" },
786
- });
787
- });
788
- return;
789
- case "session.tool.input.delta":
790
- message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
791
- if (tool.state.status === "streaming")
792
- tool.state.input += event.data.delta;
793
- });
794
- return;
795
- case "session.tool.input.ended":
796
- message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
797
- if (tool.state.status === "streaming")
798
- tool.state.input = event.data.text;
799
- });
800
- return;
801
- case "session.tool.called":
802
- message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
803
- tool.time.ran = event.created;
804
- tool.executed = event.data.executed;
805
- tool.providerState = event.data.state;
806
- tool.state = { status: "running", input: event.data.input, metadata: {} };
807
- });
808
- return;
809
- case "session.tool.progress":
810
- message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
811
- if (tool.state.status === "running")
812
- tool.state.metadata = event.data.metadata;
813
- });
814
- return;
815
- case "session.tool.success":
816
- message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
817
- if (tool.state.status !== "running")
818
- return;
819
- tool.state = {
820
- status: "completed",
821
- input: tool.state.input,
822
- metadata: event.data.metadata,
823
- content: [...event.data.content],
824
- };
825
- tool.executed = event.data.executed || tool.executed === true;
826
- tool.providerResultState = event.data.resultState;
827
- tool.time.completed = event.created;
828
- });
829
- return;
830
- case "session.tool.failed":
831
- message.editTool(event.data.sessionID, event.data.assistantMessageID, event.data.id, (tool) => {
832
- if (tool.state.status !== "streaming" && tool.state.status !== "running")
833
- return;
834
- tool.state = {
835
- status: "error",
836
- error: event.data.error,
837
- input: typeof tool.state.input === "string" ? {} : tool.state.input,
838
- metadata: event.data.metadata,
839
- content: event.data.content,
840
- };
841
- tool.executed = event.data.executed || tool.executed === true;
842
- tool.providerResultState = event.data.resultState;
843
- tool.time.completed = event.created;
844
- });
845
- return;
846
- case "session.reasoning.started":
847
- message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
848
- assistant.content.push({
849
- type: "reasoning",
850
- text: "",
851
- state: event.data.state,
852
- time: { created: event.created },
853
- });
854
- });
855
- return;
856
- case "session.reasoning.delta":
857
- message.editReasoning(event.data.sessionID, event.data.assistantMessageID, (reasoning) => {
858
- reasoning.text += event.data.delta;
859
- });
860
- return;
861
- case "session.reasoning.ended":
862
- message.editReasoning(event.data.sessionID, event.data.assistantMessageID, (reasoning) => {
863
- reasoning.text = event.data.text;
864
- reasoning.time = { created: reasoning.time?.created ?? event.created, completed: event.created };
865
- if (event.data.state !== undefined)
866
- reasoning.state = event.data.state;
867
- });
868
- return;
869
- case "session.retry.scheduled":
870
- message.editAssistant(event.data.sessionID, event.data.assistantMessageID, (assistant) => {
871
- assistant.retry = { attempt: event.data.attempt, at: event.data.at, error: event.data.error };
872
- });
873
- return;
874
- case "session.execution.started":
875
- setSessionActive(event.data.sessionID, "running");
876
- return;
877
- case "session.compaction.started":
878
- if (event.data.inputID)
879
- removePending(event.data.sessionID, event.data.inputID);
880
- message.insert(event.data.sessionID, {
881
- id: event.data.inputID ?? messageIDFromEvent(event.id),
882
- type: "compaction",
883
- status: "running",
884
- reason: event.data.reason,
885
- summary: "",
886
- recent: event.data.recent ?? "",
887
- time: { created: event.created },
888
- });
889
- if (event.data.inputID)
890
- compacting.get(event.data.sessionID)?.observed.add(event.data.inputID);
891
- return;
892
- case "session.execution.succeeded":
893
- case "session.execution.failed":
894
- case "session.execution.interrupted":
895
- setSessionActive(event.data.sessionID, "idle");
896
- message.update(event.data.sessionID, (draft) => {
897
- const currentAssistant = message.activeAssistant(draft);
898
- if (currentAssistant)
899
- currentAssistant.retry = undefined;
900
- });
901
- if (event.type === "session.execution.interrupted" && event.data.reason === "shutdown")
902
- return;
903
- // Mirror the projected idle marker so turn boundaries match before the next message read.
904
- message.insert(event.data.sessionID, {
905
- id: messageIDFromEvent(event.id),
906
- type: "idle",
907
- outcome: event.type === "session.execution.succeeded"
908
- ? "succeeded"
909
- : event.type === "session.execution.failed"
910
- ? "failed"
911
- : "interrupted",
912
- time: { created: event.created },
913
- });
914
- if (store.session.message[event.data.sessionID]?.some((item) => item.type === "assistant" &&
915
- item.content.some((part) => part.type === "tool" && (part.state.status === "streaming" || part.state.status === "running")))) {
916
- sync.invalidate(`session.message:${event.data.sessionID}`);
917
- refresh(() => result.session.message.sync(event.data.sessionID));
918
- }
919
- // An event can overtake the first read; queue a revalidation when that read is still active.
920
- if (!store.session.info[event.data.sessionID] && !sync.has(`session:${event.data.sessionID}`))
921
- return;
922
- result.session.invalidate(event.data.sessionID);
923
- refresh(() => result.session.sync(event.data.sessionID));
924
- return;
925
- case "session.viewed":
926
- if (!store.session.info[event.data.sessionID] && !sync.has(`session:${event.data.sessionID}`))
927
- return;
928
- result.session.invalidate(event.data.sessionID);
929
- refresh(() => result.session.sync(event.data.sessionID));
930
- return;
931
- case "session.revert.staged":
932
- if (store.session.info[event.data.sessionID])
933
- setStore("session", "info", event.data.sessionID, "revert", event.data.revert);
934
- return;
935
- case "session.revert.cleared":
936
- if (store.session.info[event.data.sessionID])
937
- setStore("session", "info", event.data.sessionID, "revert", undefined);
938
- return;
939
- case "session.revert.committed":
940
- if (store.session.info[event.data.sessionID]) {
941
- setStore("session", "info", event.data.sessionID, "revert", undefined);
942
- }
943
- // The projector also deletes inbox items enqueued at or after the boundary without a cancel event.
944
- setStore("session", "pending", event.data.sessionID, (store.session.pending[event.data.sessionID] ?? []).filter((item) => item.id < event.data.to));
945
- message.update(event.data.sessionID, (draft, index) => {
946
- const position = draft.findIndex((item) => item.id >= event.data.to);
947
- if (position === -1)
948
- return;
949
- for (const item of draft.splice(position))
950
- index.delete(item.id);
951
- });
952
- return;
953
- case "session.compaction.delta":
954
- message.update(event.data.sessionID, (draft) => {
955
- const current = message.compaction(draft);
956
- if (current?.status === "running")
957
- current.summary += event.data.text;
958
- });
959
- return;
960
- case "session.compaction.ended":
961
- message.update(event.data.sessionID, (draft, index) => {
962
- const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running");
963
- const current = draft[position];
964
- if (current?.type === "compaction") {
965
- Object.assign(current, {
966
- status: "completed",
967
- reason: event.data.reason,
968
- model: event.data.model,
969
- providerState: event.data.providerState,
970
- providerContext: event.data.providerContext,
971
- summary: event.data.text,
972
- recent: event.data.recent,
973
- cost: event.data.cost,
974
- tokens: event.data.tokens,
975
- });
976
- return;
977
- }
978
- message.append(draft, index, {
979
- id: messageIDFromEvent(event.id),
980
- type: "compaction",
981
- status: "completed",
982
- reason: event.data.reason,
983
- model: event.data.model,
984
- providerState: event.data.providerState,
985
- providerContext: event.data.providerContext,
986
- summary: event.data.text,
987
- recent: event.data.recent,
988
- cost: event.data.cost,
989
- tokens: event.data.tokens,
990
- time: { created: event.created },
991
- });
992
- });
993
- return;
994
- case "session.compaction.failed":
995
- if (event.data.inputID)
996
- removePending(event.data.sessionID, event.data.inputID);
997
- message.update(event.data.sessionID, (draft, index) => {
998
- const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running");
999
- const current = draft[position];
1000
- const failed = {
1001
- id: current?.id ?? event.data.inputID ?? messageIDFromEvent(event.id),
1002
- type: "compaction",
1003
- status: "failed",
1004
- reason: event.data.reason ?? "manual",
1005
- error: event.data.error ?? {
1006
- type: "compaction.failed",
1007
- message: "Compaction failed before recording an error",
1008
- },
1009
- metadata: current?.type === "compaction" ? current.metadata : event.metadata,
1010
- cost: event.data.cost,
1011
- tokens: event.data.tokens,
1012
- time: current?.type === "compaction" ? current.time : { created: event.created },
1013
- };
1014
- if (current?.type === "compaction") {
1015
- draft[position] = failed;
1016
- return;
1017
- }
1018
- message.append(draft, index, failed);
1019
- });
1020
- if (event.data.inputID)
1021
- compacting.get(event.data.sessionID)?.observed.add(event.data.inputID);
1022
- return;
1023
- case "permission.asked":
1024
- if (store.session.permission[event.data.sessionID]?.some((request) => request.id === event.data.id))
1025
- return;
1026
- setStore("session", "permission", event.data.sessionID, [
1027
- ...(store.session.permission[event.data.sessionID] ?? []),
1028
- event.data,
1029
- ]);
1030
- return;
1031
- case "permission.replied":
1032
- removePermission(event.data.sessionID, event.data.requestID);
1033
- return;
1034
- case "form.replied":
1035
- case "form.cancelled":
1036
- removeForm(event.data.sessionID, event.data.id, event.location);
1037
- return;
1038
- }
1039
- if (event.type === "credential.updated" || event.type === "credential.switched") {
1040
- Object.keys(store.location).forEach((key) => {
1041
- const ref = JSON.parse(key);
1042
- const location = { directory: ref[0], workspaceID: ref[1] ?? undefined };
1043
- if (event.type === "credential.updated") {
1044
- result.location.integration.invalidate(location);
1045
- refresh(() => result.location.integration.sync(location));
1046
- return;
1047
- }
1048
- setStore("location", key, (data) => ({
1049
- integration: data?.integration?.map((integration) => {
1050
- if (integration.id !== event.data.integrationID)
1051
- return integration;
1052
- const active = integration.connections.find((connection) => connection.type === "credential" && connection.id === event.data.credentialID);
1053
- if (!active)
1054
- return integration;
1055
- return {
1056
- ...integration,
1057
- connections: [active, ...integration.connections.filter((connection) => connection !== active)],
1058
- };
1059
- }),
1060
- }));
1061
- result.location.model.invalidate(location);
1062
- result.location.provider.invalidate(location);
1063
- refresh(() => Promise.all([result.location.model.sync(location), result.location.provider.sync(location)]));
1064
- });
1065
- return;
1066
- }
1067
- if (!event.location)
1068
- return;
1069
- const location = event.location;
1070
- switch (event.type) {
1071
- case "provider.updated":
1072
- result.location.provider.invalidate(location);
1073
- refresh(() => result.location.provider.sync(location));
1074
- break;
1075
- case "model.updated":
1076
- result.location.model.invalidate(location);
1077
- refresh(() => result.location.model.sync(location));
1078
- break;
1079
- case "agent.updated":
1080
- result.location.agent.invalidate(location);
1081
- refresh(() => result.location.agent.sync(location));
1082
- break;
1083
- case "command.updated":
1084
- result.location.command.invalidate(location);
1085
- refresh(() => result.location.command.sync(location));
1086
- break;
1087
- case "skill.updated":
1088
- result.location.skill.invalidate(location);
1089
- refresh(() => result.location.skill.sync(location));
1090
- break;
1091
- case "vcs.branch.updated":
1092
- setStore("location", locationKey(location), (data) => ({
1093
- vcs: {
1094
- branch: {
1095
- ...data?.vcs?.branch,
1096
- current: event.data.branch,
1097
- },
1098
- },
1099
- }));
1100
- break;
1101
- case "form.created":
1102
- if (store.session.form[event.data.form.sessionID]?.some((form) => form.id === event.data.form.id))
1103
- break;
1104
- setStore("session", "form", event.data.form.sessionID, [
1105
- ...(store.session.form[event.data.form.sessionID] ?? []),
1106
- event.data.form.sessionID === "global" ? { ...event.data.form, location } : event.data.form,
1107
- ]);
1108
- break;
1109
- case "shell.created":
1110
- setStore("location", locationKey(location), (data) => ({
1111
- shell: {
1112
- ...data?.shell,
1113
- [event.data.info.id]: { ...event.data.info, location },
1114
- },
1115
- }));
1116
- break;
1117
- case "shell.exited":
1118
- case "shell.deleted":
1119
- setStore("location", locationKey(location), (data) => ({
1120
- shell: Object.fromEntries(Object.entries(data?.shell ?? {}).filter(([id]) => id !== event.data.id)),
1121
- }));
1122
- break;
1123
- case "reference.updated":
1124
- result.location.reference.invalidate(location);
1125
- refresh(() => result.location.reference.sync(location));
1126
- break;
1127
- case "integration.updated":
1128
- result.location.integration.invalidate(location);
1129
- result.location.model.invalidate(location);
1130
- result.location.provider.invalidate(location);
1131
- refresh(() => Promise.all([
1132
- result.location.integration.sync(location),
1133
- result.location.model.sync(location),
1134
- result.location.provider.sync(location),
1135
- ]));
1136
- break;
1137
- case "config.updated":
1138
- result.location.config.invalidate(location);
1139
- if (result.location.config.list(location) !== undefined || sync.has(`location.config:${locationKey(location)}`))
1140
- refresh(() => result.location.config.sync(location));
1141
- refresh(() => result.location.websearch.refresh(location));
1142
- break;
1143
- case "websearch.updated":
1144
- refresh(() => result.location.websearch.refresh(location));
1145
- break;
1146
- // Authenticating an MCP integration reconnects its server, which emits mcp.status.changed,
1147
- // so the mcp list syncs here rather than off integration.updated. The server emits one event
1148
- // per MCP server as each settles, so a location booting nine servers emitted nine refetches.
1149
- case "mcp.status.changed":
1150
- result.location.mcp.server.invalidate(location);
1151
- settle(`mcp.status:${locationKey(location)}`, () => result.location.mcp.server.sync(location));
1152
- break;
1153
- case "mcp.resources.changed":
1154
- result.location.mcp.resource.invalidate(location);
1155
- refresh(() => result.location.mcp.resource.sync(location));
1156
- break;
1157
- }
1158
- }
1159
- // A cached per-location catalog. `sync` loads once per invalidation, keyed by the
1160
- // effective location, and publishes under the server's canonical location; `alias`
1161
- // also publishes under the requested key when the two differ.
1162
- function locationResource(field, load, options) {
1163
- const publish = (key, value) => setStore("location", key, { [field]: value });
1164
- return {
1165
- list: (ref) => store.location[locationKey(ref ?? defaultLocation())]?.[field],
1166
- sync: (ref) => {
1167
- const location = ref ?? defaultLocation();
1168
- const id = locationKey(location);
1169
- return sync.run(`location.${field}:${id}`, async () => {
1170
- const response = await load(locationQuery(location));
1171
- const key = locationKey(response.location);
1172
- publish(key, response.data);
1173
- if (options?.alias && key !== id)
1174
- publish(id, response.data);
1175
- });
1176
- },
1177
- invalidate: (ref) => sync.invalidate(`location.${field}:${locationKey(ref ?? defaultLocation())}`),
1178
- };
1179
- }
1180
- const vcs = locationResource("vcs", (location) => api().vcs.get({ location }));
1181
- const shells = locationResource("shell", async (location) => {
1182
- const response = await api().shell.list({ location });
1183
- const ref = { directory: response.location.directory };
1184
- return {
1185
- location: response.location,
1186
- data: Object.fromEntries(response.data.map((info) => [info.id, { ...info, location: ref }])),
1187
- };
1188
- });
1189
- const result = {
1190
- on: config.event.on,
1191
- listen: config.event.listen,
1192
- session: {
1193
- list() {
1194
- return sessions();
1195
- },
1196
- get(sessionID) {
1197
- return store.session.info[sessionID];
1198
- },
1199
- creating(sessionID) {
1200
- return creating.has(sessionID);
1201
- },
1202
- remember(info) {
1203
- batch(() => {
1204
- setStore("session", "info", info.id, reconcile(info));
1205
- sync.complete(`session:${info.id}`);
1206
- registerSession(info.id);
1207
- });
1208
- },
1209
- setStatus(sessionID, status) {
1210
- setSessionActive(sessionID, status);
1211
- },
1212
- root(sessionID) {
1213
- return resolveRoot(sessionID);
1214
- },
1215
- family(sessionID) {
1216
- return store.session.family[resolveRoot(sessionID)] ?? [];
1217
- },
1218
- /** Clear heavy cached data for the root and all known descendants. */
1219
- evict(sessionID) {
1220
- const root = resolveRoot(sessionID);
1221
- batch(() => {
1222
- for (const id of new Set([root, sessionID, ...(store.session.family[root] ?? [])]))
1223
- evictSession(id);
1224
- });
1225
- },
1226
- cost(sessionID) {
1227
- const session = store.session.info[sessionID];
1228
- if (!session)
1229
- return 0;
1230
- if (session.parentID)
1231
- return session.cost;
1232
- return (store.session.family[sessionID] ?? [sessionID]).reduce((total, id) => total + (store.session.info[id]?.cost ?? 0), 0);
1233
- },
1234
- status(sessionID) {
1235
- return store.session.active[sessionID] ?? "idle";
1236
- },
1237
- // Inputs are the pending user and synthetic items; compactions are control items.
1238
- input: {
1239
- list(sessionID) {
1240
- return (store.session.pending[sessionID] ?? []).flatMap((item) => item.type === "compaction" ? [] : [item.id]);
1241
- },
1242
- has(sessionID, inboxID) {
1243
- return (store.session.pending[sessionID]?.some((item) => item.id === inboxID && item.type !== "compaction") ?? false);
1244
- },
1245
- },
1246
- pending: {
1247
- list(sessionID) {
1248
- return store.session.pending[sessionID] ?? [];
1249
- },
1250
- sync(sessionID) {
1251
- return sync.run(`session.pending:${sessionID}`, async () => {
1252
- const updates = new Map();
1253
- pendingUpdates.set(sessionID, updates);
1254
- try {
1255
- const snapshot = await api().session.inbox.list({ sessionID });
1256
- // Events can overtake this HTTP response on a remote connection.
1257
- // Reconcile them before an older snapshot can resurrect delivered input.
1258
- const current = new Map(snapshot.map((item) => [item.id, item]));
1259
- updates.forEach((item, id) => {
1260
- if (item === undefined)
1261
- current.delete(id);
1262
- else if (typeof item === "string") {
1263
- const existing = current.get(id);
1264
- if (existing)
1265
- current.set(id, { ...existing, delivery: item });
1266
- }
1267
- else
1268
- current.set(id, item);
1269
- });
1270
- const pending = [...current.values()];
1271
- // A positive read acknowledges admission even when its SSE echo is delayed.
1272
- pending.forEach((item) => outbox.delete(item.id));
1273
- // Compactions also coalesce by Session, not just by the proposed ID.
1274
- if (pending.some((item) => item.type === "compaction"))
1275
- store.session.pending[sessionID]
1276
- ?.filter((item) => item.type === "compaction")
1277
- .forEach((item) => outbox.delete(item.id));
1278
- // Keep optimistic rows still awaiting their echo: this fetch may
1279
- // have raced ahead of an in-flight admission the server does not
1280
- // know about yet.
1281
- const inflight = (store.session.pending[sessionID] ?? []).filter((item) => outbox.has(item.id));
1282
- const merged = inflight.length === 0 ? pending : [...pending, ...inflight];
1283
- batch(() => {
1284
- setStore("session", "pending", sessionID, reconcile(merged));
1285
- merged.forEach(materializeInboxMessage);
1286
- });
1287
- }
1288
- finally {
1289
- if (pendingUpdates.get(sessionID) === updates)
1290
- pendingUpdates.delete(sessionID);
1291
- }
1292
- });
1293
- },
1294
- invalidate(sessionID) {
1295
- sync.invalidate(`session.pending:${sessionID}`);
1296
- },
1297
- },
1298
- // Optimistic session creation: admit a local record under a
1299
- // client-minted ID so a session view can mount immediately, then create
1300
- // the session on the server. The session.created echo re-syncs the
1301
- // record by ID, so the durable payload replaces the client's guess.
1302
- // Returns the ID synchronously along with the in-flight request:
1303
- // callers gate session-dependent sends on the request (prompt() gates
1304
- // itself on any in-flight create of its session automatically).
1305
- create(input) {
1306
- const { projectID, ...payload } = input;
1307
- const id = payload.id ?? SessionID.create();
1308
- const location = payload.location ?? defaultLocation();
1309
- const fresh = !store.session.info[id];
1310
- if (fresh) {
1311
- const now = Date.now();
1312
- sessionOutbox.add(id);
1313
- result.session.remember({
1314
- id,
1315
- projectID: projectID ?? store.location[locationKey(location)]?.info?.project.id ?? "",
1316
- agent: payload.agent,
1317
- model: payload.model,
1318
- cost: 0,
1319
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
1320
- time: { created: now, updated: now },
1321
- title: payload.title,
1322
- location,
1323
- });
1324
- // A mounted optimistic session must not fetch its empty collections
1325
- // before creation settles. The session.created echo re-syncs info.
1326
- sync.complete(`session.family:${id}`);
1327
- sync.complete(`session.pending:${id}`);
1328
- sync.complete(`session.message:${id}`);
1329
- }
1330
- // Wrapped so even a synchronous client failure reaches the rollback.
1331
- const request = Promise.resolve()
1332
- .then(() => api().session.create({ ...payload, id, location }))
1333
- .then((info) => {
1334
- sessionOutbox.delete(id);
1335
- result.session.remember(info);
1336
- return info;
1337
- })
1338
- .catch((error) => {
1339
- // Roll back only a record this call admitted and neither the echo
1340
- // nor the response has acknowledged: anything else is server state.
1341
- if (fresh && sessionOutbox.delete(id))
1342
- removeSession(id);
1343
- throw error;
1344
- });
1345
- if (fresh)
1346
- track(creating, id, request);
1347
- return { id, request };
1348
- },
1349
- compact(input) {
1350
- const active = compacting.get(input.sessionID);
1351
- if (active)
1352
- return active.request;
1353
- // A known pending control ID may be consumed while setup waits. Propose
1354
- // a fresh ID and let the server coalesce, without duplicating its row.
1355
- const id = SessionMessage.ID.create();
1356
- if (!store.session.pending[input.sessionID]?.some((item) => item.type === "compaction")) {
1357
- outbox.add(id);
1358
- admitLocal({
1359
- id,
1360
- sessionID: input.sessionID,
1361
- time: { created: Date.now() },
1362
- type: "compaction",
1363
- delivery: "steer",
1364
- payload: {},
1365
- });
1366
- }
1367
- // Compaction admission can coalesce onto a different ID. Retire the
1368
- // speculative row on an echo, and remember consumed IDs until the POST
1369
- // settles so its older response cannot resurrect a queued row.
1370
- const observed = new Set();
1371
- const request = sendAdmission(input.sessionID, async () => {
1372
- if (input.model)
1373
- await api().session.switchModel({ sessionID: input.sessionID, model: input.model });
1374
- return api().session.compact({ sessionID: input.sessionID, id });
1375
- })
1376
- .then((item) => {
1377
- batch(() => {
1378
- outbox.delete(id);
1379
- if (item.id !== id)
1380
- removePending(input.sessionID, id);
1381
- if (!observed.has(item.id) && !messageIndex.get(input.sessionID)?.has(item.id))
1382
- admitLocal(item);
1383
- });
1384
- return item;
1385
- })
1386
- .catch((error) => {
1387
- if (outbox.delete(id))
1388
- removePending(input.sessionID, id);
1389
- throw error;
1390
- })
1391
- .finally(() => {
1392
- if (compacting.get(input.sessionID)?.request === request)
1393
- compacting.delete(input.sessionID);
1394
- });
1395
- compacting.set(input.sessionID, { id, observed, request });
1396
- return request;
1397
- },
1398
- // Optimistic prompt admission: render the prompt immediately under a
1399
- // client-minted ID, send it, and let the durable inbox.enqueued echo
1400
- // upsert that same ID with the server's payload. Server admission is
1401
- // idempotent per ID, so retrying with the identical payload cannot
1402
- // double-admit.
1403
- prompt(input) {
1404
- const { gate, prepare, ...request } = input;
1405
- const id = request.id ?? SessionMessage.ID.create();
1406
- // A retry may reuse an ID that is already rendered — and possibly
1407
- // already durable. Admit optimistically only for new IDs so a failed
1408
- // retry cannot roll back acknowledged state.
1409
- const fresh = !messageIndex.get(request.sessionID)?.has(id) &&
1410
- !store.session.pending[request.sessionID]?.some((item) => item.id === id);
1411
- if (fresh) {
1412
- outbox.add(id);
1413
- admitLocal({
1414
- id,
1415
- sessionID: request.sessionID,
1416
- time: { created: Date.now() },
1417
- type: "user",
1418
- delivery: request.delivery ?? "steer",
1419
- // Files and skills stay off the optimistic row: their durable
1420
- // forms are server-loaded (content, mime, resolution), so they
1421
- // fill in when the echo upserts the row.
1422
- payload: {
1423
- text: request.text,
1424
- agents: request.agents?.map((agent) => ({ ...agent })),
1425
- metadata: request.metadata,
1426
- },
1427
- });
1428
- }
1429
- return sendAdmission(request.sessionID, async () => {
1430
- await prepare?.();
1431
- return api().session.prompt({ ...request, id });
1432
- }, gate).catch((error) => {
1433
- // Roll back only rows this call admitted and the server has not
1434
- // acknowledged: anything else is server state.
1435
- if (fresh && outbox.delete(id))
1436
- retractLocal(request.sessionID, id);
1437
- throw error;
1438
- });
1439
- },
1440
- sync(sessionID, options) {
1441
- return sync.run(options?.children ? `session.family:${sessionID}` : `session:${sessionID}`, async () => {
1442
- const [info, children] = await Promise.all([
1443
- api().session.get({ sessionID }),
1444
- options?.children
1445
- ? api()
1446
- .session.list({ parentID: sessionID, order: "desc" })
1447
- .then((response) => response.data)
1448
- : [],
1449
- ]);
1450
- const sessions = [info, ...children];
1451
- batch(() => {
1452
- setStore("session", "info", produce((draft) => {
1453
- for (const session of sessions)
1454
- draft[session.id] = session;
1455
- }));
1456
- for (const session of sessions) {
1457
- sync.complete(`session:${session.id}`);
1458
- registerSession(session.id);
1459
- }
1460
- });
1461
- });
1462
- },
1463
- invalidate(sessionID) {
1464
- sync.invalidate(`session:${sessionID}`);
1465
- },
1466
- message: {
1467
- list(sessionID) {
1468
- return store.session.message[sessionID] ?? [];
1469
- },
1470
- get(sessionID, messageID) {
1471
- const messages = store.session.message[sessionID];
1472
- const position = messageIndex.get(sessionID)?.get(messageID);
1473
- return position === undefined ? undefined : messages?.[position];
1474
- },
1475
- sync(sessionID) {
1476
- return sync.run(`session.message:${sessionID}`, async () => {
1477
- const response = await api().message.list({
1478
- sessionID,
1479
- limit: config.initialMessageLimit?.() ?? messagePageLimit,
1480
- order: "desc",
1481
- });
1482
- const fetched = response.data.toReversed();
1483
- // Same protection as the pending sync: a re-fetch racing an
1484
- // admission must not wipe its local transcript row.
1485
- const ids = new Set(fetched.map((item) => item.id));
1486
- const admitted = new Set((store.session.pending[sessionID] ?? []).flatMap((item) => item.type === "user" || item.type === "synthetic" ? [item.id] : []));
1487
- const local = (store.session.message[sessionID] ?? []).filter((item) => !ids.has(item.id) && (outbox.has(item.id) || admitted.has(item.id)));
1488
- const messages = local.length === 0 ? fetched : [...fetched, ...local];
1489
- batch(() => {
1490
- messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])));
1491
- setStore("session", "message", sessionID, reconcile(messages));
1492
- setStore("session", "messageCursor", sessionID, response.cursor.next ?? undefined);
1493
- });
1494
- });
1495
- },
1496
- more(sessionID) {
1497
- return store.session.messageCursor[sessionID] !== undefined;
1498
- },
1499
- loading(sessionID) {
1500
- return store.session.messageLoading[sessionID] ?? false;
1501
- },
1502
- async loadMore(sessionID, options) {
1503
- const signal = options?.signal;
1504
- if (signal?.aborted)
1505
- return;
1506
- while (messageLoads.has(sessionID)) {
1507
- const published = await (() => {
1508
- const pending = messageLoads.get(sessionID);
1509
- if (!signal)
1510
- return pending;
1511
- const aborted = Promise.withResolvers();
1512
- const cancel = () => aborted.resolve();
1513
- signal.addEventListener("abort", cancel, { once: true });
1514
- return Promise.race([pending, aborted.promise])
1515
- .catch((error) => {
1516
- if (!signal.aborted)
1517
- throw error;
1518
- })
1519
- .finally(() => signal.removeEventListener("abort", cancel));
1520
- })();
1521
- if ((!options?.all && published) || signal?.aborted)
1522
- return;
1523
- }
1524
- const cursor = store.session.messageCursor[sessionID];
1525
- if (!cursor || signal?.aborted)
1526
- return;
1527
- setStore("session", "messageLoading", sessionID, true);
1528
- const request = (async () => {
1529
- const fetched = [];
1530
- let next = cursor;
1531
- do {
1532
- const response = await api().message.list({
1533
- sessionID,
1534
- limit: options?.all ? 200 : messagePageLimit,
1535
- cursor: next,
1536
- }, { signal });
1537
- if (signal?.aborted)
1538
- return;
1539
- fetched.push(...response.data);
1540
- next = response.cursor.next ?? undefined;
1541
- if (!options?.all)
1542
- break;
1543
- } while (next);
1544
- // A jump through history publishes once, not once per page of offscreen messages.
1545
- const existing = store.session.message[sessionID] ?? [];
1546
- const ids = new Set(existing.map((item) => item.id));
1547
- const messages = [...fetched.reverse().filter((item) => !ids.has(item.id)), ...existing];
1548
- batch(() => {
1549
- options?.beforePublish?.();
1550
- messageIndex.set(sessionID, new Map(messages.map((item, position) => [item.id, position])));
1551
- setStore("session", "message", sessionID, reconcile(messages));
1552
- setStore("session", "messageCursor", sessionID, next);
1553
- });
1554
- return true;
1555
- })()
1556
- .catch((error) => {
1557
- if (!signal?.aborted)
1558
- throw error;
1559
- })
1560
- .finally(() => setStore("session", "messageLoading", sessionID, false));
1561
- track(messageLoads, sessionID, request);
1562
- await request;
1563
- },
1564
- invalidate(sessionID) {
1565
- sync.invalidate(`session.message:${sessionID}`);
1566
- },
1567
- },
1568
- permission: {
1569
- list(sessionID) {
1570
- return store.session.permission[sessionID];
1571
- },
1572
- sync(sessionID) {
1573
- return sync.run(`session.permission:${sessionID}`, async () => {
1574
- setStore("session", "permission", sessionID, await api().permission.list({ sessionID }));
1575
- });
1576
- },
1577
- invalidate(sessionID) {
1578
- sync.invalidate(`session.permission:${sessionID}`);
1579
- },
1580
- async reply(input) {
1581
- await api()
1582
- .permission.reply(input)
1583
- .catch((error) => {
1584
- if (!isPermissionNotFoundError(error))
1585
- throw error;
1586
- });
1587
- removePermission(input.sessionID, input.requestID);
1588
- },
1589
- },
1590
- form: {
1591
- list(sessionID, ref) {
1592
- const forms = store.session.form[sessionID];
1593
- if (sessionID !== "global")
1594
- return forms;
1595
- if (!ref)
1596
- return;
1597
- const key = locationKey(ref);
1598
- return forms?.filter((form) => form.location && locationKey(form.location) === key);
1599
- },
1600
- sync(sessionID, ref) {
1601
- const key = `session.form:${sessionID}:${sessionID === "global" ? locationKey(ref ?? defaultLocation()) : ""}`;
1602
- return sync.run(key, async () => {
1603
- if (sessionID === "global") {
1604
- const response = await api().form.list({
1605
- location: locationQuery(ref ?? defaultLocation()),
1606
- });
1607
- const location = {
1608
- directory: response.location.directory,
1609
- };
1610
- const locationID = locationKey(location);
1611
- setStore("session", "form", sessionID, [
1612
- ...(store.session.form[sessionID] ?? []).filter((form) => form.location && locationKey(form.location) !== locationID),
1613
- ...response.data.filter((form) => form.sessionID === "global").map((form) => ({ ...form, location })),
1614
- ]);
1615
- return;
1616
- }
1617
- setStore("session", "form", sessionID, await api().session.form.list({ sessionID }));
1618
- });
1619
- },
1620
- invalidate(sessionID, ref) {
1621
- sync.invalidate(`session.form:${sessionID}:${sessionID === "global" ? locationKey(ref ?? defaultLocation()) : ""}`);
1622
- },
1623
- reply(input, ref) {
1624
- return settleForm(input, ref, api().session.form.reply(input, formRequestOptions(input.sessionID, ref)));
1625
- },
1626
- cancel(input, ref) {
1627
- return settleForm(input, ref, api().session.form.cancel(input, formRequestOptions(input.sessionID, ref)));
1628
- },
1629
- },
1630
- },
1631
- project: {
1632
- list() {
1633
- return Object.values(store.project.info).toSorted((a, b) => b.time.updated - a.time.updated);
1634
- },
1635
- get(projectID) {
1636
- return store.project.info[projectID];
1637
- },
1638
- sync() {
1639
- return sync.run("project", async () => {
1640
- const projects = await api().project.list();
1641
- setStore("project", "info", reconcile(Object.fromEntries(projects.map((project) => [project.id, project]))));
1642
- });
1643
- },
1644
- invalidate() {
1645
- sync.invalidate("project");
1646
- },
1647
- permission: {
1648
- list(projectID) {
1649
- return store.project.permission[projectID];
1650
- },
1651
- sync(projectID) {
1652
- return sync.run(`project.permission:${projectID}`, async () => {
1653
- setStore("project", "permission", projectID, await api().permission.saved.list({ projectID }));
1654
- });
1655
- },
1656
- invalidate(projectID) {
1657
- sync.invalidate(`project.permission:${projectID}`);
1658
- },
1659
- },
1660
- },
1661
- shell: {
1662
- list(location) {
1663
- return Object.values(shells.list(location) ?? {});
1664
- },
1665
- listBySession(sessionID) {
1666
- return Object.values(store.location)
1667
- .flatMap((data) => Object.values(data.shell ?? {}))
1668
- .filter((shell) => shell.metadata.sessionID === sessionID);
1669
- },
1670
- get(id) {
1671
- return Object.values(store.location)
1672
- .map((data) => data.shell?.[id])
1673
- .find((shell) => shell !== undefined);
1674
- },
1675
- sync: shells.sync,
1676
- invalidate: shells.invalidate,
1677
- },
1678
- location: {
1679
- info(ref) {
1680
- return store.location[locationKey(ref ?? defaultLocation())]?.info;
1681
- },
1682
- default() {
1683
- return defaultLocation();
1684
- },
1685
- syncInfo(ref) {
1686
- const current = ref ?? defaultLocation();
1687
- return sync.run(`location:${locationKey(current)}`, async () => {
1688
- const location = await api().location.get({ location: locationQuery(current) });
1689
- const key = locationKey(location);
1690
- if (!store.location[key])
1691
- setStore("location", key, {});
1692
- setStore("location", key, "info", location);
1693
- if (!ref) {
1694
- setDefaultLocation({ directory: location.directory });
1695
- }
1696
- });
1697
- },
1698
- async sync(ref) {
1699
- await result.location.syncInfo(ref);
1700
- const location = ref ?? defaultLocation();
1701
- await Promise.all([
1702
- result.location.vcs.sync(location),
1703
- result.location.agent.sync(location),
1704
- result.location.command.sync(location),
1705
- result.location.integration.sync(location),
1706
- result.location.mcp.server.sync(location),
1707
- result.location.mcp.resource.sync(location),
1708
- result.location.model.sync(location),
1709
- result.location.provider.sync(location),
1710
- result.location.reference.sync(location),
1711
- result.location.skill.sync(location),
1712
- result.shell.sync(location),
1713
- result.session.form.sync("global", location),
1714
- ]);
1715
- },
1716
- invalidate(ref) {
1717
- const location = ref ?? defaultLocation();
1718
- sync.invalidate(`location:${locationKey(location)}`);
1719
- result.location.vcs.invalidate(location);
1720
- result.location.agent.invalidate(location);
1721
- result.location.command.invalidate(location);
1722
- result.location.config.invalidate(location);
1723
- result.location.integration.invalidate(location);
1724
- result.location.mcp.server.invalidate(location);
1725
- result.location.mcp.resource.invalidate(location);
1726
- result.location.model.invalidate(location);
1727
- result.location.provider.invalidate(location);
1728
- result.location.reference.invalidate(location);
1729
- result.location.skill.invalidate(location);
1730
- result.shell.invalidate(location);
1731
- result.session.form.invalidate("global", location);
1732
- },
1733
- vcs: { info: vcs.list, sync: vcs.sync, invalidate: vcs.invalidate },
1734
- agent: locationResource("agent", (location) => api().agent.list({ location })),
1735
- command: locationResource("command", (location) => api().command.list({ location })),
1736
- config: locationResource("config", async (location) => ({
1737
- location: { directory: location.directory },
1738
- data: await api().config.get({ location }),
1739
- })),
1740
- integration: locationResource("integration", (location) => api().integration.list({ location })),
1741
- mcp: {
1742
- server: locationResource("mcpServer", (location) => api().mcp.list({ location })),
1743
- resource: locationResource("mcpResource", async (location) => {
1744
- const response = await api().mcp.resource.catalog({ location });
1745
- return { location: response.location, data: response.data.resources };
1746
- }),
1747
- },
1748
- model: locationResource("model", (location) => api().model.list({ location }), { alias: true }),
1749
- provider: locationResource("provider", (location) => api().provider.list({ location }), { alias: true }),
1750
- reference: locationResource("reference", (location) => api().reference.list({ location })),
1751
- websearch: {
1752
- list(location) {
1753
- return store.location[locationKey(location ?? defaultLocation())]?.websearch;
1754
- },
1755
- async refresh(ref) {
1756
- const input = { location: locationQuery(ref ?? defaultLocation()) };
1757
- const providers = await api().websearch.providers(input);
1758
- const key = locationKey(providers.location);
1759
- setStore("location", key, { websearch: providers.data });
1760
- },
1761
- },
1762
- skill: locationResource("skill", (location) => api().skill.list({ location })),
1763
- },
1764
- };
1765
- createEffect(() => {
1766
- if (config.connection?.status() === "connected")
1767
- return;
1768
- sync.invalidate();
1769
- });
1770
- onCleanup(config.event.listen(({ details }) => {
1771
- handleEvent(details);
1772
- }));
1773
- return result;
1774
- }
1
+ import {
2
+ settleMs2,
3
+ locationKey2,
4
+ createData2
5
+ } from "../chunks/service-contender-atpwm825.js";
6
+ import"../chunks/service-contender-m2eywc8w.js";
7
+ import"../chunks/service-contender-7p1wryha.js";
8
+ import"../chunks/service-contender-vyas3vgt.js";
9
+ import"../chunks/service-contender-radw39tv.js";
10
+ import"../chunks/service-contender-n3c99vt5.js";
11
+ import"../chunks/service-contender-1tbj6z39.js";
12
+ import"../chunks/service-contender-4tveyqeh.js";
13
+ import"../chunks/service-contender-frbwqjmf.js";
14
+ export {
15
+ createData2 as createData,
16
+ locationKey2 as locationKey,
17
+ settleMs2 as settleMs
18
+ };