@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
@@ -1,1769 +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
- // An event can overtake the first read; queue a revalidation when that read is still active.
915
- if (!store.session.info[event.data.sessionID] && !sync.has(`session:${event.data.sessionID}`))
916
- return;
917
- result.session.invalidate(event.data.sessionID);
918
- refresh(() => result.session.sync(event.data.sessionID));
919
- return;
920
- case "session.viewed":
921
- if (!store.session.info[event.data.sessionID] && !sync.has(`session:${event.data.sessionID}`))
922
- return;
923
- result.session.invalidate(event.data.sessionID);
924
- refresh(() => result.session.sync(event.data.sessionID));
925
- return;
926
- case "session.revert.staged":
927
- if (store.session.info[event.data.sessionID])
928
- setStore("session", "info", event.data.sessionID, "revert", event.data.revert);
929
- return;
930
- case "session.revert.cleared":
931
- if (store.session.info[event.data.sessionID])
932
- setStore("session", "info", event.data.sessionID, "revert", undefined);
933
- return;
934
- case "session.revert.committed":
935
- if (store.session.info[event.data.sessionID]) {
936
- setStore("session", "info", event.data.sessionID, "revert", undefined);
937
- }
938
- // The projector also deletes inbox items enqueued at or after the boundary without a cancel event.
939
- setStore("session", "pending", event.data.sessionID, (store.session.pending[event.data.sessionID] ?? []).filter((item) => item.id < event.data.to));
940
- message.update(event.data.sessionID, (draft, index) => {
941
- const position = draft.findIndex((item) => item.id >= event.data.to);
942
- if (position === -1)
943
- return;
944
- for (const item of draft.splice(position))
945
- index.delete(item.id);
946
- });
947
- return;
948
- case "session.compaction.delta":
949
- message.update(event.data.sessionID, (draft) => {
950
- const current = message.compaction(draft);
951
- if (current?.status === "running")
952
- current.summary += event.data.text;
953
- });
954
- return;
955
- case "session.compaction.ended":
956
- message.update(event.data.sessionID, (draft, index) => {
957
- const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running");
958
- const current = draft[position];
959
- if (current?.type === "compaction") {
960
- Object.assign(current, {
961
- status: "completed",
962
- reason: event.data.reason,
963
- model: event.data.model,
964
- providerState: event.data.providerState,
965
- providerContext: event.data.providerContext,
966
- summary: event.data.text,
967
- recent: event.data.recent,
968
- cost: event.data.cost,
969
- tokens: event.data.tokens,
970
- });
971
- return;
972
- }
973
- message.append(draft, index, {
974
- id: messageIDFromEvent(event.id),
975
- type: "compaction",
976
- status: "completed",
977
- reason: event.data.reason,
978
- model: event.data.model,
979
- providerState: event.data.providerState,
980
- providerContext: event.data.providerContext,
981
- summary: event.data.text,
982
- recent: event.data.recent,
983
- cost: event.data.cost,
984
- tokens: event.data.tokens,
985
- time: { created: event.created },
986
- });
987
- });
988
- return;
989
- case "session.compaction.failed":
990
- if (event.data.inputID)
991
- removePending(event.data.sessionID, event.data.inputID);
992
- message.update(event.data.sessionID, (draft, index) => {
993
- const position = draft.findLastIndex((item) => item.type === "compaction" && item.status === "running");
994
- const current = draft[position];
995
- const failed = {
996
- id: current?.id ?? event.data.inputID ?? messageIDFromEvent(event.id),
997
- type: "compaction",
998
- status: "failed",
999
- reason: event.data.reason ?? "manual",
1000
- error: event.data.error ?? {
1001
- type: "compaction.failed",
1002
- message: "Compaction failed before recording an error",
1003
- },
1004
- metadata: current?.type === "compaction" ? current.metadata : event.metadata,
1005
- cost: event.data.cost,
1006
- tokens: event.data.tokens,
1007
- time: current?.type === "compaction" ? current.time : { created: event.created },
1008
- };
1009
- if (current?.type === "compaction") {
1010
- draft[position] = failed;
1011
- return;
1012
- }
1013
- message.append(draft, index, failed);
1014
- });
1015
- if (event.data.inputID)
1016
- compacting.get(event.data.sessionID)?.observed.add(event.data.inputID);
1017
- return;
1018
- case "permission.asked":
1019
- if (store.session.permission[event.data.sessionID]?.some((request) => request.id === event.data.id))
1020
- return;
1021
- setStore("session", "permission", event.data.sessionID, [
1022
- ...(store.session.permission[event.data.sessionID] ?? []),
1023
- event.data,
1024
- ]);
1025
- return;
1026
- case "permission.replied":
1027
- removePermission(event.data.sessionID, event.data.requestID);
1028
- return;
1029
- case "form.replied":
1030
- case "form.cancelled":
1031
- removeForm(event.data.sessionID, event.data.id, event.location);
1032
- return;
1033
- }
1034
- if (event.type === "credential.updated" || event.type === "credential.switched") {
1035
- Object.keys(store.location).forEach((key) => {
1036
- const ref = JSON.parse(key);
1037
- const location = { directory: ref[0], workspaceID: ref[1] ?? undefined };
1038
- if (event.type === "credential.updated") {
1039
- result.location.integration.invalidate(location);
1040
- refresh(() => result.location.integration.sync(location));
1041
- return;
1042
- }
1043
- setStore("location", key, (data) => ({
1044
- integration: data?.integration?.map((integration) => {
1045
- if (integration.id !== event.data.integrationID)
1046
- return integration;
1047
- const active = integration.connections.find((connection) => connection.type === "credential" && connection.id === event.data.credentialID);
1048
- if (!active)
1049
- return integration;
1050
- return {
1051
- ...integration,
1052
- connections: [active, ...integration.connections.filter((connection) => connection !== active)],
1053
- };
1054
- }),
1055
- }));
1056
- result.location.model.invalidate(location);
1057
- result.location.provider.invalidate(location);
1058
- refresh(() => Promise.all([result.location.model.sync(location), result.location.provider.sync(location)]));
1059
- });
1060
- return;
1061
- }
1062
- if (!event.location)
1063
- return;
1064
- const location = event.location;
1065
- switch (event.type) {
1066
- case "provider.updated":
1067
- result.location.provider.invalidate(location);
1068
- refresh(() => result.location.provider.sync(location));
1069
- break;
1070
- case "model.updated":
1071
- result.location.model.invalidate(location);
1072
- refresh(() => result.location.model.sync(location));
1073
- break;
1074
- case "agent.updated":
1075
- result.location.agent.invalidate(location);
1076
- refresh(() => result.location.agent.sync(location));
1077
- break;
1078
- case "command.updated":
1079
- result.location.command.invalidate(location);
1080
- refresh(() => result.location.command.sync(location));
1081
- break;
1082
- case "skill.updated":
1083
- result.location.skill.invalidate(location);
1084
- refresh(() => result.location.skill.sync(location));
1085
- break;
1086
- case "vcs.branch.updated":
1087
- setStore("location", locationKey(location), (data) => ({
1088
- vcs: {
1089
- branch: {
1090
- ...data?.vcs?.branch,
1091
- current: event.data.branch,
1092
- },
1093
- },
1094
- }));
1095
- break;
1096
- case "form.created":
1097
- if (store.session.form[event.data.form.sessionID]?.some((form) => form.id === event.data.form.id))
1098
- break;
1099
- setStore("session", "form", event.data.form.sessionID, [
1100
- ...(store.session.form[event.data.form.sessionID] ?? []),
1101
- event.data.form.sessionID === "global" ? { ...event.data.form, location } : event.data.form,
1102
- ]);
1103
- break;
1104
- case "shell.created":
1105
- setStore("location", locationKey(location), (data) => ({
1106
- shell: {
1107
- ...data?.shell,
1108
- [event.data.info.id]: { ...event.data.info, location },
1109
- },
1110
- }));
1111
- break;
1112
- case "shell.exited":
1113
- case "shell.deleted":
1114
- setStore("location", locationKey(location), (data) => ({
1115
- shell: Object.fromEntries(Object.entries(data?.shell ?? {}).filter(([id]) => id !== event.data.id)),
1116
- }));
1117
- break;
1118
- case "reference.updated":
1119
- result.location.reference.invalidate(location);
1120
- refresh(() => result.location.reference.sync(location));
1121
- break;
1122
- case "integration.updated":
1123
- result.location.integration.invalidate(location);
1124
- result.location.model.invalidate(location);
1125
- result.location.provider.invalidate(location);
1126
- refresh(() => Promise.all([
1127
- result.location.integration.sync(location),
1128
- result.location.model.sync(location),
1129
- result.location.provider.sync(location),
1130
- ]));
1131
- break;
1132
- case "config.updated":
1133
- result.location.config.invalidate(location);
1134
- if (result.location.config.list(location) !== undefined || sync.has(`location.config:${locationKey(location)}`))
1135
- refresh(() => result.location.config.sync(location));
1136
- refresh(() => result.location.websearch.refresh(location));
1137
- break;
1138
- case "websearch.updated":
1139
- refresh(() => result.location.websearch.refresh(location));
1140
- break;
1141
- // Authenticating an MCP integration reconnects its server, which emits mcp.status.changed,
1142
- // so the mcp list syncs here rather than off integration.updated. The server emits one event
1143
- // per MCP server as each settles, so a location booting nine servers emitted nine refetches.
1144
- case "mcp.status.changed":
1145
- result.location.mcp.server.invalidate(location);
1146
- settle(`mcp.status:${locationKey(location)}`, () => result.location.mcp.server.sync(location));
1147
- break;
1148
- case "mcp.resources.changed":
1149
- result.location.mcp.resource.invalidate(location);
1150
- refresh(() => result.location.mcp.resource.sync(location));
1151
- break;
1152
- }
1153
- }
1154
- // A cached per-location catalog. `sync` loads once per invalidation, keyed by the
1155
- // effective location, and publishes under the server's canonical location; `alias`
1156
- // also publishes under the requested key when the two differ.
1157
- function locationResource(field, load, options) {
1158
- const publish = (key, value) => setStore("location", key, { [field]: value });
1159
- return {
1160
- list: (ref) => store.location[locationKey(ref ?? defaultLocation())]?.[field],
1161
- sync: (ref) => {
1162
- const location = ref ?? defaultLocation();
1163
- const id = locationKey(location);
1164
- return sync.run(`location.${field}:${id}`, async () => {
1165
- const response = await load(locationQuery(location));
1166
- const key = locationKey(response.location);
1167
- publish(key, response.data);
1168
- if (options?.alias && key !== id)
1169
- publish(id, response.data);
1170
- });
1171
- },
1172
- invalidate: (ref) => sync.invalidate(`location.${field}:${locationKey(ref ?? defaultLocation())}`),
1173
- };
1174
- }
1175
- const vcs = locationResource("vcs", (location) => api().vcs.get({ location }));
1176
- const shells = locationResource("shell", async (location) => {
1177
- const response = await api().shell.list({ location });
1178
- const ref = { directory: response.location.directory };
1179
- return {
1180
- location: response.location,
1181
- data: Object.fromEntries(response.data.map((info) => [info.id, { ...info, location: ref }])),
1182
- };
1183
- });
1184
- const result = {
1185
- on: config.event.on,
1186
- listen: config.event.listen,
1187
- session: {
1188
- list() {
1189
- return sessions();
1190
- },
1191
- get(sessionID) {
1192
- return store.session.info[sessionID];
1193
- },
1194
- creating(sessionID) {
1195
- return creating.has(sessionID);
1196
- },
1197
- remember(info) {
1198
- batch(() => {
1199
- setStore("session", "info", info.id, reconcile(info));
1200
- sync.complete(`session:${info.id}`);
1201
- registerSession(info.id);
1202
- });
1203
- },
1204
- setStatus(sessionID, status) {
1205
- setSessionActive(sessionID, status);
1206
- },
1207
- root(sessionID) {
1208
- return resolveRoot(sessionID);
1209
- },
1210
- family(sessionID) {
1211
- return store.session.family[resolveRoot(sessionID)] ?? [];
1212
- },
1213
- /** Clear heavy cached data for the root and all known descendants. */
1214
- evict(sessionID) {
1215
- const root = resolveRoot(sessionID);
1216
- batch(() => {
1217
- for (const id of new Set([root, sessionID, ...(store.session.family[root] ?? [])]))
1218
- evictSession(id);
1219
- });
1220
- },
1221
- cost(sessionID) {
1222
- const session = store.session.info[sessionID];
1223
- if (!session)
1224
- return 0;
1225
- if (session.parentID)
1226
- return session.cost;
1227
- return (store.session.family[sessionID] ?? [sessionID]).reduce((total, id) => total + (store.session.info[id]?.cost ?? 0), 0);
1228
- },
1229
- status(sessionID) {
1230
- return store.session.active[sessionID] ?? "idle";
1231
- },
1232
- // Inputs are the pending user and synthetic items; compactions are control items.
1233
- input: {
1234
- list(sessionID) {
1235
- return (store.session.pending[sessionID] ?? []).flatMap((item) => item.type === "compaction" ? [] : [item.id]);
1236
- },
1237
- has(sessionID, inboxID) {
1238
- return (store.session.pending[sessionID]?.some((item) => item.id === inboxID && item.type !== "compaction") ?? false);
1239
- },
1240
- },
1241
- pending: {
1242
- list(sessionID) {
1243
- return store.session.pending[sessionID] ?? [];
1244
- },
1245
- sync(sessionID) {
1246
- return sync.run(`session.pending:${sessionID}`, async () => {
1247
- const updates = new Map();
1248
- pendingUpdates.set(sessionID, updates);
1249
- try {
1250
- const snapshot = await api().session.inbox.list({ sessionID });
1251
- // Events can overtake this HTTP response on a remote connection.
1252
- // Reconcile them before an older snapshot can resurrect delivered input.
1253
- const current = new Map(snapshot.map((item) => [item.id, item]));
1254
- updates.forEach((item, id) => {
1255
- if (item === undefined)
1256
- current.delete(id);
1257
- else if (typeof item === "string") {
1258
- const existing = current.get(id);
1259
- if (existing)
1260
- current.set(id, { ...existing, delivery: item });
1261
- }
1262
- else
1263
- current.set(id, item);
1264
- });
1265
- const pending = [...current.values()];
1266
- // A positive read acknowledges admission even when its SSE echo is delayed.
1267
- pending.forEach((item) => outbox.delete(item.id));
1268
- // Compactions also coalesce by Session, not just by the proposed ID.
1269
- if (pending.some((item) => item.type === "compaction"))
1270
- store.session.pending[sessionID]
1271
- ?.filter((item) => item.type === "compaction")
1272
- .forEach((item) => outbox.delete(item.id));
1273
- // Keep optimistic rows still awaiting their echo: this fetch may
1274
- // have raced ahead of an in-flight admission the server does not
1275
- // know about yet.
1276
- const inflight = (store.session.pending[sessionID] ?? []).filter((item) => outbox.has(item.id));
1277
- const merged = inflight.length === 0 ? pending : [...pending, ...inflight];
1278
- batch(() => {
1279
- setStore("session", "pending", sessionID, reconcile(merged));
1280
- merged.forEach(materializeInboxMessage);
1281
- });
1282
- }
1283
- finally {
1284
- if (pendingUpdates.get(sessionID) === updates)
1285
- pendingUpdates.delete(sessionID);
1286
- }
1287
- });
1288
- },
1289
- invalidate(sessionID) {
1290
- sync.invalidate(`session.pending:${sessionID}`);
1291
- },
1292
- },
1293
- // Optimistic session creation: admit a local record under a
1294
- // client-minted ID so a session view can mount immediately, then create
1295
- // the session on the server. The session.created echo re-syncs the
1296
- // record by ID, so the durable payload replaces the client's guess.
1297
- // Returns the ID synchronously along with the in-flight request:
1298
- // callers gate session-dependent sends on the request (prompt() gates
1299
- // itself on any in-flight create of its session automatically).
1300
- create(input) {
1301
- const { projectID, ...payload } = input;
1302
- const id = payload.id ?? SessionID.create();
1303
- const location = payload.location ?? defaultLocation();
1304
- const fresh = !store.session.info[id];
1305
- if (fresh) {
1306
- const now = Date.now();
1307
- sessionOutbox.add(id);
1308
- result.session.remember({
1309
- id,
1310
- projectID: projectID ?? store.location[locationKey(location)]?.info?.project.id ?? "",
1311
- agent: payload.agent,
1312
- model: payload.model,
1313
- cost: 0,
1314
- tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
1315
- time: { created: now, updated: now },
1316
- title: payload.title,
1317
- location,
1318
- });
1319
- // A mounted optimistic session must not fetch its empty collections
1320
- // before creation settles. The session.created echo re-syncs info.
1321
- sync.complete(`session.family:${id}`);
1322
- sync.complete(`session.pending:${id}`);
1323
- sync.complete(`session.message:${id}`);
1324
- }
1325
- // Wrapped so even a synchronous client failure reaches the rollback.
1326
- const request = Promise.resolve()
1327
- .then(() => api().session.create({ ...payload, id, location }))
1328
- .then((info) => {
1329
- sessionOutbox.delete(id);
1330
- result.session.remember(info);
1331
- return info;
1332
- })
1333
- .catch((error) => {
1334
- // Roll back only a record this call admitted and neither the echo
1335
- // nor the response has acknowledged: anything else is server state.
1336
- if (fresh && sessionOutbox.delete(id))
1337
- removeSession(id);
1338
- throw error;
1339
- });
1340
- if (fresh)
1341
- track(creating, id, request);
1342
- return { id, request };
1343
- },
1344
- compact(input) {
1345
- const active = compacting.get(input.sessionID);
1346
- if (active)
1347
- return active.request;
1348
- // A known pending control ID may be consumed while setup waits. Propose
1349
- // a fresh ID and let the server coalesce, without duplicating its row.
1350
- const id = SessionMessage.ID.create();
1351
- if (!store.session.pending[input.sessionID]?.some((item) => item.type === "compaction")) {
1352
- outbox.add(id);
1353
- admitLocal({
1354
- id,
1355
- sessionID: input.sessionID,
1356
- time: { created: Date.now() },
1357
- type: "compaction",
1358
- delivery: "steer",
1359
- payload: {},
1360
- });
1361
- }
1362
- // Compaction admission can coalesce onto a different ID. Retire the
1363
- // speculative row on an echo, and remember consumed IDs until the POST
1364
- // settles so its older response cannot resurrect a queued row.
1365
- const observed = new Set();
1366
- const request = sendAdmission(input.sessionID, async () => {
1367
- if (input.model)
1368
- await api().session.switchModel({ sessionID: input.sessionID, model: input.model });
1369
- return api().session.compact({ sessionID: input.sessionID, id });
1370
- })
1371
- .then((item) => {
1372
- batch(() => {
1373
- outbox.delete(id);
1374
- if (item.id !== id)
1375
- removePending(input.sessionID, id);
1376
- if (!observed.has(item.id) && !messageIndex.get(input.sessionID)?.has(item.id))
1377
- admitLocal(item);
1378
- });
1379
- return item;
1380
- })
1381
- .catch((error) => {
1382
- if (outbox.delete(id))
1383
- removePending(input.sessionID, id);
1384
- throw error;
1385
- })
1386
- .finally(() => {
1387
- if (compacting.get(input.sessionID)?.request === request)
1388
- compacting.delete(input.sessionID);
1389
- });
1390
- compacting.set(input.sessionID, { id, observed, request });
1391
- return request;
1392
- },
1393
- // Optimistic prompt admission: render the prompt immediately under a
1394
- // client-minted ID, send it, and let the durable inbox.enqueued echo
1395
- // upsert that same ID with the server's payload. Server admission is
1396
- // idempotent per ID, so retrying with the identical payload cannot
1397
- // double-admit.
1398
- prompt(input) {
1399
- const { gate, prepare, ...request } = input;
1400
- const id = request.id ?? SessionMessage.ID.create();
1401
- // A retry may reuse an ID that is already rendered — and possibly
1402
- // already durable. Admit optimistically only for new IDs so a failed
1403
- // retry cannot roll back acknowledged state.
1404
- const fresh = !messageIndex.get(request.sessionID)?.has(id) &&
1405
- !store.session.pending[request.sessionID]?.some((item) => item.id === id);
1406
- if (fresh) {
1407
- outbox.add(id);
1408
- admitLocal({
1409
- id,
1410
- sessionID: request.sessionID,
1411
- time: { created: Date.now() },
1412
- type: "user",
1413
- delivery: request.delivery ?? "steer",
1414
- // Files and skills stay off the optimistic row: their durable
1415
- // forms are server-loaded (content, mime, resolution), so they
1416
- // fill in when the echo upserts the row.
1417
- payload: {
1418
- text: request.text,
1419
- agents: request.agents?.map((agent) => ({ ...agent })),
1420
- metadata: request.metadata,
1421
- },
1422
- });
1423
- }
1424
- return sendAdmission(request.sessionID, async () => {
1425
- await prepare?.();
1426
- return api().session.prompt({ ...request, id });
1427
- }, gate).catch((error) => {
1428
- // Roll back only rows this call admitted and the server has not
1429
- // acknowledged: anything else is server state.
1430
- if (fresh && outbox.delete(id))
1431
- retractLocal(request.sessionID, id);
1432
- throw error;
1433
- });
1434
- },
1435
- sync(sessionID, options) {
1436
- return sync.run(options?.children ? `session.family:${sessionID}` : `session:${sessionID}`, async () => {
1437
- const [info, children] = await Promise.all([
1438
- api().session.get({ sessionID }),
1439
- options?.children
1440
- ? api()
1441
- .session.list({ parentID: sessionID, order: "desc" })
1442
- .then((response) => response.data)
1443
- : [],
1444
- ]);
1445
- const sessions = [info, ...children];
1446
- batch(() => {
1447
- setStore("session", "info", produce((draft) => {
1448
- for (const session of sessions)
1449
- draft[session.id] = session;
1450
- }));
1451
- for (const session of sessions) {
1452
- sync.complete(`session:${session.id}`);
1453
- registerSession(session.id);
1454
- }
1455
- });
1456
- });
1457
- },
1458
- invalidate(sessionID) {
1459
- sync.invalidate(`session:${sessionID}`);
1460
- },
1461
- message: {
1462
- list(sessionID) {
1463
- return store.session.message[sessionID] ?? [];
1464
- },
1465
- get(sessionID, messageID) {
1466
- const messages = store.session.message[sessionID];
1467
- const position = messageIndex.get(sessionID)?.get(messageID);
1468
- return position === undefined ? undefined : messages?.[position];
1469
- },
1470
- sync(sessionID) {
1471
- return sync.run(`session.message:${sessionID}`, async () => {
1472
- const response = await api().message.list({
1473
- sessionID,
1474
- limit: config.initialMessageLimit?.() ?? messagePageLimit,
1475
- order: "desc",
1476
- });
1477
- const fetched = response.data.toReversed();
1478
- // Same protection as the pending sync: a re-fetch racing an
1479
- // admission must not wipe its local transcript row.
1480
- const ids = new Set(fetched.map((item) => item.id));
1481
- const admitted = new Set((store.session.pending[sessionID] ?? []).flatMap((item) => item.type === "user" || item.type === "synthetic" ? [item.id] : []));
1482
- const local = (store.session.message[sessionID] ?? []).filter((item) => !ids.has(item.id) && (outbox.has(item.id) || admitted.has(item.id)));
1483
- const messages = local.length === 0 ? fetched : [...fetched, ...local];
1484
- batch(() => {
1485
- messageIndex.set(sessionID, new Map(messages.map((message, index) => [message.id, index])));
1486
- setStore("session", "message", sessionID, reconcile(messages));
1487
- setStore("session", "messageCursor", sessionID, response.cursor.next ?? undefined);
1488
- });
1489
- });
1490
- },
1491
- more(sessionID) {
1492
- return store.session.messageCursor[sessionID] !== undefined;
1493
- },
1494
- loading(sessionID) {
1495
- return store.session.messageLoading[sessionID] ?? false;
1496
- },
1497
- async loadMore(sessionID, options) {
1498
- const signal = options?.signal;
1499
- if (signal?.aborted)
1500
- return;
1501
- while (messageLoads.has(sessionID)) {
1502
- const published = await (() => {
1503
- const pending = messageLoads.get(sessionID);
1504
- if (!signal)
1505
- return pending;
1506
- const aborted = Promise.withResolvers();
1507
- const cancel = () => aborted.resolve();
1508
- signal.addEventListener("abort", cancel, { once: true });
1509
- return Promise.race([pending, aborted.promise])
1510
- .catch((error) => {
1511
- if (!signal.aborted)
1512
- throw error;
1513
- })
1514
- .finally(() => signal.removeEventListener("abort", cancel));
1515
- })();
1516
- if ((!options?.all && published) || signal?.aborted)
1517
- return;
1518
- }
1519
- const cursor = store.session.messageCursor[sessionID];
1520
- if (!cursor || signal?.aborted)
1521
- return;
1522
- setStore("session", "messageLoading", sessionID, true);
1523
- const request = (async () => {
1524
- const fetched = [];
1525
- let next = cursor;
1526
- do {
1527
- const response = await api().message.list({
1528
- sessionID,
1529
- limit: options?.all ? 200 : messagePageLimit,
1530
- cursor: next,
1531
- }, { signal });
1532
- if (signal?.aborted)
1533
- return;
1534
- fetched.push(...response.data);
1535
- next = response.cursor.next ?? undefined;
1536
- if (!options?.all)
1537
- break;
1538
- } while (next);
1539
- // A jump through history publishes once, not once per page of offscreen messages.
1540
- const existing = store.session.message[sessionID] ?? [];
1541
- const ids = new Set(existing.map((item) => item.id));
1542
- const messages = [...fetched.reverse().filter((item) => !ids.has(item.id)), ...existing];
1543
- batch(() => {
1544
- options?.beforePublish?.();
1545
- messageIndex.set(sessionID, new Map(messages.map((item, position) => [item.id, position])));
1546
- setStore("session", "message", sessionID, reconcile(messages));
1547
- setStore("session", "messageCursor", sessionID, next);
1548
- });
1549
- return true;
1550
- })()
1551
- .catch((error) => {
1552
- if (!signal?.aborted)
1553
- throw error;
1554
- })
1555
- .finally(() => setStore("session", "messageLoading", sessionID, false));
1556
- track(messageLoads, sessionID, request);
1557
- await request;
1558
- },
1559
- invalidate(sessionID) {
1560
- sync.invalidate(`session.message:${sessionID}`);
1561
- },
1562
- },
1563
- permission: {
1564
- list(sessionID) {
1565
- return store.session.permission[sessionID];
1566
- },
1567
- sync(sessionID) {
1568
- return sync.run(`session.permission:${sessionID}`, async () => {
1569
- setStore("session", "permission", sessionID, await api().permission.list({ sessionID }));
1570
- });
1571
- },
1572
- invalidate(sessionID) {
1573
- sync.invalidate(`session.permission:${sessionID}`);
1574
- },
1575
- async reply(input) {
1576
- await api()
1577
- .permission.reply(input)
1578
- .catch((error) => {
1579
- if (!isPermissionNotFoundError(error))
1580
- throw error;
1581
- });
1582
- removePermission(input.sessionID, input.requestID);
1583
- },
1584
- },
1585
- form: {
1586
- list(sessionID, ref) {
1587
- const forms = store.session.form[sessionID];
1588
- if (sessionID !== "global")
1589
- return forms;
1590
- if (!ref)
1591
- return;
1592
- const key = locationKey(ref);
1593
- return forms?.filter((form) => form.location && locationKey(form.location) === key);
1594
- },
1595
- sync(sessionID, ref) {
1596
- const key = `session.form:${sessionID}:${sessionID === "global" ? locationKey(ref ?? defaultLocation()) : ""}`;
1597
- return sync.run(key, async () => {
1598
- if (sessionID === "global") {
1599
- const response = await api().form.list({
1600
- location: locationQuery(ref ?? defaultLocation()),
1601
- });
1602
- const location = {
1603
- directory: response.location.directory,
1604
- };
1605
- const locationID = locationKey(location);
1606
- setStore("session", "form", sessionID, [
1607
- ...(store.session.form[sessionID] ?? []).filter((form) => form.location && locationKey(form.location) !== locationID),
1608
- ...response.data.filter((form) => form.sessionID === "global").map((form) => ({ ...form, location })),
1609
- ]);
1610
- return;
1611
- }
1612
- setStore("session", "form", sessionID, await api().session.form.list({ sessionID }));
1613
- });
1614
- },
1615
- invalidate(sessionID, ref) {
1616
- sync.invalidate(`session.form:${sessionID}:${sessionID === "global" ? locationKey(ref ?? defaultLocation()) : ""}`);
1617
- },
1618
- reply(input, ref) {
1619
- return settleForm(input, ref, api().session.form.reply(input, formRequestOptions(input.sessionID, ref)));
1620
- },
1621
- cancel(input, ref) {
1622
- return settleForm(input, ref, api().session.form.cancel(input, formRequestOptions(input.sessionID, ref)));
1623
- },
1624
- },
1625
- },
1626
- project: {
1627
- list() {
1628
- return Object.values(store.project.info).toSorted((a, b) => b.time.updated - a.time.updated);
1629
- },
1630
- get(projectID) {
1631
- return store.project.info[projectID];
1632
- },
1633
- sync() {
1634
- return sync.run("project", async () => {
1635
- const projects = await api().project.list();
1636
- setStore("project", "info", reconcile(Object.fromEntries(projects.map((project) => [project.id, project]))));
1637
- });
1638
- },
1639
- invalidate() {
1640
- sync.invalidate("project");
1641
- },
1642
- permission: {
1643
- list(projectID) {
1644
- return store.project.permission[projectID];
1645
- },
1646
- sync(projectID) {
1647
- return sync.run(`project.permission:${projectID}`, async () => {
1648
- setStore("project", "permission", projectID, await api().permission.saved.list({ projectID }));
1649
- });
1650
- },
1651
- invalidate(projectID) {
1652
- sync.invalidate(`project.permission:${projectID}`);
1653
- },
1654
- },
1655
- },
1656
- shell: {
1657
- list(location) {
1658
- return Object.values(shells.list(location) ?? {});
1659
- },
1660
- listBySession(sessionID) {
1661
- return Object.values(store.location)
1662
- .flatMap((data) => Object.values(data.shell ?? {}))
1663
- .filter((shell) => shell.metadata.sessionID === sessionID);
1664
- },
1665
- get(id) {
1666
- return Object.values(store.location)
1667
- .map((data) => data.shell?.[id])
1668
- .find((shell) => shell !== undefined);
1669
- },
1670
- sync: shells.sync,
1671
- invalidate: shells.invalidate,
1672
- },
1673
- location: {
1674
- info(ref) {
1675
- return store.location[locationKey(ref ?? defaultLocation())]?.info;
1676
- },
1677
- default() {
1678
- return defaultLocation();
1679
- },
1680
- syncInfo(ref) {
1681
- const current = ref ?? defaultLocation();
1682
- return sync.run(`location:${locationKey(current)}`, async () => {
1683
- const location = await api().location.get({ location: locationQuery(current) });
1684
- const key = locationKey(location);
1685
- if (!store.location[key])
1686
- setStore("location", key, {});
1687
- setStore("location", key, "info", location);
1688
- if (!ref) {
1689
- setDefaultLocation({ directory: location.directory });
1690
- }
1691
- });
1692
- },
1693
- async sync(ref) {
1694
- await result.location.syncInfo(ref);
1695
- const location = ref ?? defaultLocation();
1696
- await Promise.all([
1697
- result.location.vcs.sync(location),
1698
- result.location.agent.sync(location),
1699
- result.location.command.sync(location),
1700
- result.location.integration.sync(location),
1701
- result.location.mcp.server.sync(location),
1702
- result.location.mcp.resource.sync(location),
1703
- result.location.model.sync(location),
1704
- result.location.provider.sync(location),
1705
- result.location.reference.sync(location),
1706
- result.location.skill.sync(location),
1707
- result.shell.sync(location),
1708
- result.session.form.sync("global", location),
1709
- ]);
1710
- },
1711
- invalidate(ref) {
1712
- const location = ref ?? defaultLocation();
1713
- sync.invalidate(`location:${locationKey(location)}`);
1714
- result.location.vcs.invalidate(location);
1715
- result.location.agent.invalidate(location);
1716
- result.location.command.invalidate(location);
1717
- result.location.config.invalidate(location);
1718
- result.location.integration.invalidate(location);
1719
- result.location.mcp.server.invalidate(location);
1720
- result.location.mcp.resource.invalidate(location);
1721
- result.location.model.invalidate(location);
1722
- result.location.provider.invalidate(location);
1723
- result.location.reference.invalidate(location);
1724
- result.location.skill.invalidate(location);
1725
- result.shell.invalidate(location);
1726
- result.session.form.invalidate("global", location);
1727
- },
1728
- vcs: { info: vcs.list, sync: vcs.sync, invalidate: vcs.invalidate },
1729
- agent: locationResource("agent", (location) => api().agent.list({ location })),
1730
- command: locationResource("command", (location) => api().command.list({ location })),
1731
- config: locationResource("config", async (location) => ({
1732
- location: { directory: location.directory },
1733
- data: await api().config.get({ location }),
1734
- })),
1735
- integration: locationResource("integration", (location) => api().integration.list({ location })),
1736
- mcp: {
1737
- server: locationResource("mcpServer", (location) => api().mcp.list({ location })),
1738
- resource: locationResource("mcpResource", async (location) => {
1739
- const response = await api().mcp.resource.catalog({ location });
1740
- return { location: response.location, data: response.data.resources };
1741
- }),
1742
- },
1743
- model: locationResource("model", (location) => api().model.list({ location }), { alias: true }),
1744
- provider: locationResource("provider", (location) => api().provider.list({ location }), { alias: true }),
1745
- reference: locationResource("reference", (location) => api().reference.list({ location })),
1746
- websearch: {
1747
- list(location) {
1748
- return store.location[locationKey(location ?? defaultLocation())]?.websearch;
1749
- },
1750
- async refresh(ref) {
1751
- const input = { location: locationQuery(ref ?? defaultLocation()) };
1752
- const providers = await api().websearch.providers(input);
1753
- const key = locationKey(providers.location);
1754
- setStore("location", key, { websearch: providers.data });
1755
- },
1756
- },
1757
- skill: locationResource("skill", (location) => api().skill.list({ location })),
1758
- },
1759
- };
1760
- createEffect(() => {
1761
- if (config.connection?.status() === "connected")
1762
- return;
1763
- sync.invalidate();
1764
- });
1765
- onCleanup(config.event.listen(({ details }) => {
1766
- handleEvent(details);
1767
- }));
1768
- return result;
1769
- }
1
+ import {
2
+ settleMs2,
3
+ locationKey2,
4
+ createData2
5
+ } from "../chunks/pty-handoff-atpwm825.js";
6
+ import"../chunks/pty-handoff-m2eywc8w.js";
7
+ import"../chunks/pty-handoff-vgxtr9hm.js";
8
+ import"../chunks/pty-handoff-1tbj6z39.js";
9
+ import"../chunks/pty-handoff-vyas3vgt.js";
10
+ import"../chunks/pty-handoff-radw39tv.js";
11
+ import"../chunks/pty-handoff-4tveyqeh.js";
12
+ import"../chunks/pty-handoff-n3c99vt5.js";
13
+ import"../chunks/pty-handoff-frbwqjmf.js";
14
+ export {
15
+ createData2 as createData,
16
+ locationKey2 as locationKey,
17
+ settleMs2 as settleMs
18
+ };