@olares/lares-core 0.15.7 → 0.15.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 (99) hide show
  1. package/README.md +2 -1
  2. package/brand/identity.js +33 -3
  3. package/drive/cat.js +62 -0
  4. package/drive/download.js +3 -3
  5. package/drive/execute.js +8 -2
  6. package/drive/files-path.js +33 -0
  7. package/drive/flowstudio-files.js +87 -0
  8. package/drive/ls.js +3 -3
  9. package/drive/paths.js +23 -13
  10. package/drive/tools.js +4 -2
  11. package/entrance/surface.js +96 -0
  12. package/files/app-link.js +64 -0
  13. package/files/backend-client.js +266 -0
  14. package/files/deliverables.js +55 -1
  15. package/files/intake.js +0 -63
  16. package/files/mention.js +1 -51
  17. package/files/preview-groups.js +71 -3
  18. package/files/preview-local.js +69 -0
  19. package/files/preview-workspace.js +114 -13
  20. package/files/preview.js +171 -16
  21. package/files/produced-gate.js +197 -0
  22. package/files/produced-mentions.js +19 -0
  23. package/files/published-tools.js +69 -0
  24. package/files/published-turn-data.js +84 -0
  25. package/i18n/chat-model.js +5 -5
  26. package/i18n/mcp.js +83 -0
  27. package/i18n/mobile.js +96 -2
  28. package/i18n/preview.js +10 -0
  29. package/i18n/search.js +13 -7
  30. package/i18n/skills.js +31 -0
  31. package/i18n/voice.js +88 -19
  32. package/larepass/chat.js +9 -0
  33. package/larepass/host.js +7 -2
  34. package/larepass/mux.js +210 -2
  35. package/larepass/process-fold.js +239 -0
  36. package/larepass/queue.js +44 -0
  37. package/larepass/rpc.js +50 -3
  38. package/larepass/runtime.js +215 -83
  39. package/larepass/session-cache.js +6 -0
  40. package/larepass/settings.js +15 -7
  41. package/larepass/transcript.js +49 -11
  42. package/mcp/approval.js +152 -0
  43. package/mcp/client.js +38 -0
  44. package/mcp/config.js +139 -0
  45. package/media/catalog-row.js +244 -0
  46. package/media/generation.js +707 -0
  47. package/media/tool.js +92 -0
  48. package/olares/entrance.js +67 -15
  49. package/olares/identity.js +13 -136
  50. package/olares/session-identity.js +12 -12
  51. package/olares/trusted-host.js +0 -29
  52. package/package.json +48 -4
  53. package/router/catalog-cache.d.ts +1 -1
  54. package/router/catalog-cache.js +25 -14
  55. package/router/catalog-sync.js +8 -3
  56. package/router/catalog.d.ts +6 -1
  57. package/router/catalog.js +18 -2
  58. package/router/gateway.d.ts +9 -1
  59. package/router/gateway.js +40 -4
  60. package/router/gemini-schema.js +125 -0
  61. package/router/generation-guard.js +82 -0
  62. package/router/models.d.ts +6 -0
  63. package/router/models.js +22 -3
  64. package/router/search.js +10 -2
  65. package/router/session-model.js +53 -0
  66. package/router/shim.js +104 -3
  67. package/router/sref.js +126 -0
  68. package/router/stt.js +43 -12
  69. package/router/tts.js +118 -0
  70. package/search/client.js +5 -3
  71. package/search/config.js +53 -39
  72. package/search/menu.js +45 -10
  73. package/skills/client.js +42 -0
  74. package/skills/packs.js +40 -0
  75. package/skills/state.d.ts +22 -0
  76. package/skills/state.js +142 -0
  77. package/tools/crypto-uuid.js +3 -6
  78. package/tools/head-script.js +15 -0
  79. package/voice/agent.js +50 -0
  80. package/voice/capture.js +250 -0
  81. package/voice/client.js +126 -24
  82. package/voice/config.js +11 -1
  83. package/voice/duplex.js +44 -0
  84. package/voice/endpoint.js +57 -0
  85. package/voice/host/bluetooth-ready.py +1196 -0
  86. package/voice/host/install-host.sh +102 -0
  87. package/voice/input.js +4 -0
  88. package/voice/install.js +146 -0
  89. package/voice/languages.js +11 -1
  90. package/voice/link.js +86 -0
  91. package/voice/pcm.js +100 -0
  92. package/voice/playback.js +7 -0
  93. package/voice/problem.js +46 -0
  94. package/voice/recorder.js +79 -4
  95. package/voice/script.js +35 -0
  96. package/voice/service.js +253 -10
  97. package/voice/session.js +128 -0
  98. package/voice/speech.js +93 -0
  99. package/files/preview-cache.js +0 -100
package/larepass/queue.js CHANGED
@@ -31,3 +31,47 @@ export function normalizeQueueItems(items) {
31
31
  }];
32
32
  });
33
33
  }
34
+
35
+ /**
36
+ * dsh `inbox` projection per session: the latest baseline (`projections` on a
37
+ * follow snapshot or history page) with later `agent/inbox/spliced` events
38
+ * replayed on top. Its `next-turn` list is the queue.
39
+ */
40
+ export function createInboxTracker() {
41
+ const inboxes = new Map();
42
+
43
+ return {
44
+ seed(sessionId, projections) {
45
+ const inbox = projections?.values?.inbox;
46
+ const asOfSeq = projections?.asOfSeq;
47
+ if (!sessionId || !Array.isArray(inbox?.["next-turn"]) || !Number.isInteger(asOfSeq)) return false;
48
+ const current = inboxes.get(sessionId);
49
+ if (current && current.seq > asOfSeq) return false;
50
+ inboxes.set(sessionId, { seq: asOfSeq, nextTurn: inbox["next-turn"] });
51
+ return true;
52
+ },
53
+ apply(sessionId, events) {
54
+ let changed = false;
55
+ for (const event of Array.isArray(events) ? events : []) {
56
+ if (event?.type !== "agent/inbox/spliced" || event.data?.target !== "next-turn") continue;
57
+ const current = inboxes.get(sessionId) ?? { seq: -1, nextTurn: [] };
58
+ if (typeof event.seq !== "number" || event.seq <= current.seq) continue;
59
+ const { start = 0, removedCount = 0, inserted = [] } = event.data;
60
+ inboxes.set(sessionId, {
61
+ seq: event.seq,
62
+ nextTurn: current.nextTurn.toSpliced(start, removedCount, ...inserted),
63
+ });
64
+ changed = true;
65
+ }
66
+ return changed;
67
+ },
68
+ queue(sessionId) {
69
+ const nextTurn = inboxes.get(sessionId)?.nextTurn ?? [];
70
+ return normalizeQueueItems(nextTurn.map((message) => ({
71
+ id: message?.id,
72
+ content: message?.content,
73
+ placement: "queued",
74
+ })));
75
+ },
76
+ };
77
+ }
package/larepass/rpc.js CHANGED
@@ -11,8 +11,54 @@ export function mintRpcId() {
11
11
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
12
12
  }
13
13
 
14
+ /**
15
+ * Call sites use `namespace.method`. The Host gateway claims `namespace/method`
16
+ * and 404s a single path segment such as `session.list`.
17
+ */
18
+ export function hostEndpoint(method) {
19
+ const name = String(method ?? "");
20
+ if (name.includes("/")) return name;
21
+ const dot = name.indexOf(".");
22
+ if (dot <= 0 || dot === name.length - 1) return name;
23
+ return `${name.slice(0, dot)}/${name.slice(dot + 1)}`;
24
+ }
25
+
26
+ /**
27
+ * Wire body for one call. `session.history` is `session/page`. `session/list`
28
+ * names its argument `_request`; every other claimed method uses `request`.
29
+ * Prompt requires a `requestId` the call site does not carry.
30
+ */
31
+ export function hostCall(method, payload = {}) {
32
+ let endpoint = hostEndpoint(method);
33
+ const body = payload && typeof payload === "object" ? payload : {};
34
+ if (endpoint === "session/history") {
35
+ endpoint = "session/page";
36
+ const throughSeq = Number.isInteger(body.throughSeq) ? body.throughSeq : -1;
37
+ return {
38
+ endpoint,
39
+ args: {
40
+ request: {
41
+ address: { kind: "session", sessionId: body.sessionId },
42
+ throughSeq,
43
+ },
44
+ },
45
+ };
46
+ }
47
+ if (endpoint === "session/list") return { endpoint, args: { _request: body } };
48
+ // modelCatalog takes no parameter; a `request` object fails the strict codec.
49
+ if (endpoint === "session/modelCatalog") return { endpoint, args: {} };
50
+ if (endpoint === "$events/result") return { endpoint, args: body };
51
+ if (endpoint === "session/prompt") {
52
+ return {
53
+ endpoint,
54
+ args: { request: { requestId: body.requestId || mintRpcId(), ...body } },
55
+ };
56
+ }
57
+ return { endpoint, args: { request: body } };
58
+ }
59
+
14
60
  export function rpcPath(method) {
15
- return `/api/${method}`;
61
+ return `/api/${hostCall(method).endpoint}`;
16
62
  }
17
63
 
18
64
  export const RESPOND_PATH = "/api/respond";
@@ -26,11 +72,12 @@ export function wrapClientResponse(rpcId, result) {
26
72
  }
27
73
 
28
74
  export function wrapClientRequest(method, payload = {}) {
75
+ const call = hostCall(method, payload);
29
76
  return {
30
77
  type: "client-request",
31
78
  rpcId: mintRpcId(),
32
- method,
33
- payload,
79
+ method: call.endpoint,
80
+ payload: { args: call.args },
34
81
  };
35
82
  }
36
83
 
@@ -1,10 +1,17 @@
1
1
  import { isAuthFailure } from "./host.js";
2
2
  import { listNavigableSessions, summarizeSession } from "./chat.js";
3
- import { consumeMuxInbox } from "./mux.js";
3
+ import {
4
+ classifyRemoteEvent,
5
+ consumeMuxInbox,
6
+ createSessionFollow,
7
+ remoteEventsOpen,
8
+ sessionFollowOpen,
9
+ } from "./mux.js";
4
10
  import { permissionLine, permissionSelect } from "./permissions.js";
5
- import { wrapClientResponse } from "./rpc.js";
11
+ import { mintRpcId } from "./rpc.js";
6
12
  import { sessionReferenceCandidates } from "./references.js";
7
13
  import { createSessionCache } from "./session-cache.js";
14
+ import { createInboxTracker } from "./queue.js";
8
15
  import { foldTranscript } from "./transcript.js";
9
16
  import { normalizeWorkspaceList, workspaceForSession } from "./workspace.js";
10
17
 
@@ -25,10 +32,11 @@ function composerProjections(source) {
25
32
  contextBreakdown: values.contextBreakdown,
26
33
  goal: values.goal,
27
34
  todos: values.todos,
35
+ modelSelection: values.modelSelection,
28
36
  };
29
37
  }
30
38
 
31
- function viewOf(state, cache, pendingQuestions, pendingApprovals, queues) {
39
+ function viewOf(state, cache, pendingQuestions, pendingApprovals, inbox) {
32
40
  const page = cache.peek(state.sessionId);
33
41
  const events = page?.events ?? [];
34
42
  const snap = foldTranscript(events);
@@ -38,6 +46,8 @@ function viewOf(state, cache, pendingQuestions, pendingApprovals, queues) {
38
46
  items: snap.items,
39
47
  messages: snap.messages,
40
48
  running: snap.running,
49
+ runningSince: snap.runningSince,
50
+ reply: snap.reply,
41
51
  error: state.error || snap.error,
42
52
  failed: state.failed,
43
53
  phase: state.phase,
@@ -50,7 +60,7 @@ function viewOf(state, cache, pendingQuestions, pendingApprovals, queues) {
50
60
  historyLoading: Boolean(state.sessionId) && !page?.ready && events.length === 0,
51
61
  question: pendingQuestions.get(state.sessionId) ?? null,
52
62
  approval: pendingApprovals.get(state.sessionId) ?? null,
53
- queue: queues.get(state.sessionId) ?? [],
63
+ queue: inbox.queue(state.sessionId),
54
64
  permissions: permissionSelect(session),
55
65
  plan: session?.plan,
56
66
  contextPressure: session?.contextPressure,
@@ -75,7 +85,7 @@ export function createChatRuntime(client) {
75
85
  const cache = createSessionCache();
76
86
  const pendingQuestions = new Map();
77
87
  const pendingApprovals = new Map();
78
- const queues = new Map();
88
+ const inbox = createInboxTracker();
79
89
  const projectionSeqs = new Map();
80
90
  const state = {
81
91
  sessionId: "",
@@ -93,12 +103,19 @@ export function createChatRuntime(client) {
93
103
  let abort = null;
94
104
  let dead = false;
95
105
  let muxLoop = null;
106
+ let liveSocket = null;
107
+ let followStreamId = "";
108
+ let followedSessionId = "";
109
+ let followMisses = 0;
110
+ let eventsStreamId = "";
111
+ let eventsClientId = "";
112
+ const follow = createSessionFollow();
96
113
  let listing = null;
97
114
  let workspaceListing = null;
98
115
  let creating = null;
99
116
 
100
117
  const emit = () => {
101
- const view = viewOf(state, cache, pendingQuestions, pendingApprovals, queues);
118
+ const view = viewOf(state, cache, pendingQuestions, pendingApprovals, inbox);
102
119
  for (const listener of listeners) listener(view);
103
120
  };
104
121
 
@@ -156,10 +173,11 @@ export function createChatRuntime(client) {
156
173
  }
157
174
 
158
175
  function ingest(sessionId, extra) {
176
+ const queued = inbox.apply(sessionId, extra);
159
177
  const changed = cache.merge(sessionId, extra);
160
178
  const filled = extra?.length ? noteFilled(sessionId) : false;
161
179
  const moved = extra?.length ? noteActivity(sessionId) : false;
162
- if (filled || moved || (changed && sessionId === state.sessionId)) emit();
180
+ if (filled || moved || ((changed || queued) && sessionId === state.sessionId)) emit();
163
181
  }
164
182
 
165
183
  function rememberListed(row) {
@@ -171,7 +189,11 @@ export function createChatRuntime(client) {
171
189
  function pullHistory(sessionId) {
172
190
  if (!sessionId || dead || isDraft(sessionId)) return Promise.resolve();
173
191
  return cache.load(sessionId, async () => {
174
- const history = await client.rpc("session.history", { sessionId });
192
+ const row = state.sessions.find((item) => item.sessionId === sessionId);
193
+ const history = await client.rpc("session.history", {
194
+ sessionId,
195
+ ...(Number.isInteger(row?.asOfSeq) ? { throughSeq: row.asOfSeq } : {}),
196
+ });
175
197
  if (dead) return;
176
198
  if (!history.ok) {
177
199
  if (sessionId !== state.sessionId) return;
@@ -181,7 +203,9 @@ export function createChatRuntime(client) {
181
203
  return;
182
204
  }
183
205
  const events = history.value?.events;
184
- const changed = cache.rememberHistory(sessionId, events);
206
+ const seeded = inbox.seed(sessionId, history.value?.projections);
207
+ const queued = inbox.apply(sessionId, events) || seeded;
208
+ const changed = cache.rememberHistory(sessionId, events) || queued;
185
209
  const filled = events?.length ? noteFilled(sessionId) : false;
186
210
  // The history tail carries the same projections a list row does, and it
187
211
  // is the fresher of the two for the session being opened.
@@ -206,16 +230,40 @@ export function createChatRuntime(client) {
206
230
  }
207
231
  state.failed = "";
208
232
  emit();
233
+ liveSocket = typeof opened.body?.send === "function" ? opened.body : null;
234
+ followMisses = 0;
235
+ followStreamId = "";
236
+ followedSessionId = "";
237
+ sendFollow(state.sessionId);
238
+ openEvents();
209
239
  try {
210
240
  for await (const item of consumeMuxInbox(opened.body)) {
211
241
  if (dead) return "stop";
212
- if (item.kind === "event") {
213
- ingest(item.sessionId, [item.event]);
242
+ if (item.kind === "stream" && item.streamId === followStreamId) {
243
+ const step = follow.push(item.value);
244
+ if (step?.ready) followMisses = 0;
245
+ applyFollow(followedSessionId, step);
214
246
  continue;
215
247
  }
216
- if (item.kind === "queue") {
217
- queues.set(item.sessionId, item.items);
218
- if (item.sessionId === state.sessionId) emit();
248
+ if ((item.kind === "stream-end" || item.kind === "stream-error") && item.streamId === followStreamId) {
249
+ followStreamId = "";
250
+ followedSessionId = "";
251
+ followMisses += 1;
252
+ void pullHistory(state.sessionId);
253
+ sendFollow(state.sessionId);
254
+ continue;
255
+ }
256
+ if (item.kind === "stream" && item.streamId === eventsStreamId) {
257
+ onRemoteEvent(classifyRemoteEvent(item.value));
258
+ continue;
259
+ }
260
+ if ((item.kind === "stream-end" || item.kind === "stream-error") && item.streamId === eventsStreamId) {
261
+ dropRemoteWaits();
262
+ openEvents();
263
+ continue;
264
+ }
265
+ if (item.kind === "event") {
266
+ ingest(item.sessionId, [item.event]);
219
267
  continue;
220
268
  }
221
269
  if (item.kind === "projection") {
@@ -229,40 +277,18 @@ export function createChatRuntime(client) {
229
277
  if (item.sessionId === state.sessionId) emit();
230
278
  continue;
231
279
  }
232
- if (item.kind === "question") {
233
- pendingQuestions.set(item.sessionId, {
234
- rpcId: item.rpcId,
235
- questions: item.questions,
236
- });
237
- emit();
238
- continue;
239
- }
240
- if (item.kind === "approval") {
241
- pendingApprovals.set(item.sessionId, item);
242
- if (item.sessionId === state.sessionId) emit();
243
- continue;
244
- }
245
- if (item.kind === "approval-resolved") {
246
- const current = pendingApprovals.get(item.sessionId);
247
- if (!current || current.approvalId === item.approvalId) {
248
- pendingApprovals.delete(item.sessionId);
249
- if (item.sessionId === state.sessionId) emit();
250
- }
251
- continue;
252
- }
253
- if (item.kind === "question-resolved") {
254
- const current = pendingQuestions.get(item.sessionId);
255
- if (!current || !item.rpcId || current.rpcId === item.rpcId) {
256
- pendingQuestions.delete(item.sessionId);
257
- emit();
258
- }
259
- }
260
280
  }
261
281
  } catch (err) {
262
282
  if (dead || err?.name === "AbortError") return "stop";
263
283
  state.failed = err instanceof Error ? err.message : String(err);
264
284
  emit();
265
285
  return "error";
286
+ } finally {
287
+ if (liveSocket === opened.body) {
288
+ liveSocket = null;
289
+ eventsStreamId = "";
290
+ dropRemoteWaits();
291
+ }
266
292
  }
267
293
  return "end";
268
294
  }
@@ -329,12 +355,118 @@ export function createChatRuntime(client) {
329
355
  return workspaceListing;
330
356
  }
331
357
 
358
+ function sendFollow(sessionId) {
359
+ const socket = liveSocket;
360
+ if (!socket || socket.readyState !== 1 || !sessionId || isDraft(sessionId)) return;
361
+ if (followedSessionId === sessionId && followStreamId) return;
362
+ if (followMisses > 2) return;
363
+ if (followStreamId) {
364
+ try {
365
+ socket.send(JSON.stringify({ type: "cancel", streamId: followStreamId }));
366
+ } catch {
367
+ // the socket is already closing
368
+ }
369
+ }
370
+ follow.reset();
371
+ followStreamId = mintRpcId();
372
+ followedSessionId = sessionId;
373
+ try {
374
+ socket.send(JSON.stringify(sessionFollowOpen(followStreamId, sessionId)));
375
+ } catch {
376
+ followStreamId = "";
377
+ followedSessionId = "";
378
+ }
379
+ }
380
+
381
+ function openEvents() {
382
+ const socket = liveSocket;
383
+ eventsStreamId = "";
384
+ eventsClientId = "";
385
+ if (!socket || socket.readyState !== 1) return;
386
+ eventsStreamId = mintRpcId();
387
+ try {
388
+ socket.send(JSON.stringify(remoteEventsOpen(eventsStreamId)));
389
+ } catch {
390
+ eventsStreamId = "";
391
+ }
392
+ }
393
+
394
+ /** A Host delivery dies with the `$events` generation that received it. */
395
+ function dropRemoteWaits() {
396
+ const shown = pendingQuestions.has(state.sessionId) || pendingApprovals.has(state.sessionId);
397
+ pendingQuestions.clear();
398
+ pendingApprovals.clear();
399
+ if (shown) emit();
400
+ }
401
+
402
+ function replyRemote(eventId, outcome) {
403
+ if (!eventsClientId) return Promise.resolve({ ok: false, error: { message: "no event stream" } });
404
+ return client.rpc("$events/result", { clientId: eventsClientId, eventId, outcome });
405
+ }
406
+
407
+ function onRemoteEvent(item) {
408
+ if (!item) return;
409
+ if (item.kind === "ready") {
410
+ eventsClientId = item.clientId;
411
+ return;
412
+ }
413
+ if (item.kind === "pass") {
414
+ void replyRemote(item.eventId, { kind: "next" });
415
+ return;
416
+ }
417
+ if (item.kind === "question" || item.kind === "approval") {
418
+ (item.kind === "question" ? pendingQuestions : pendingApprovals).set(item.sessionId, item);
419
+ if (item.sessionId === state.sessionId) emit();
420
+ return;
421
+ }
422
+ if (item.kind === "cancel") {
423
+ for (const pending of [pendingQuestions, pendingApprovals]) {
424
+ for (const [sessionId, wait] of pending) {
425
+ if (wait.eventId !== item.eventId) continue;
426
+ pending.delete(sessionId);
427
+ if (sessionId === state.sessionId) emit();
428
+ }
429
+ }
430
+ }
431
+ }
432
+
433
+ async function settleRemote(pending, sessionId, value) {
434
+ const wait = pending.get(sessionId);
435
+ if (!wait?.eventId) return { ok: false, error: { message: "not-pending" } };
436
+ const sent = await replyRemote(wait.eventId, { kind: "result", value });
437
+ if (!sent.ok) return { ok: false, error: sent.error };
438
+ if (pending.get(sessionId) === wait) {
439
+ pending.delete(sessionId);
440
+ if (sessionId === state.sessionId) emit();
441
+ }
442
+ return { ok: true };
443
+ }
444
+
445
+ function applyFollow(sessionId, step) {
446
+ if (!sessionId || !step) return;
447
+ const dropped = step.dropLive ? cache.dropLive(sessionId) : false;
448
+ const events = step.events ?? [];
449
+ const seeded = inbox.seed(sessionId, step.projections);
450
+ const queued = inbox.apply(sessionId, events) || seeded;
451
+ const changed = step.ready
452
+ ? cache.rememberHistory(sessionId, events)
453
+ : (events.length ? cache.merge(sessionId, events) : false);
454
+ const filled = events.length ? noteFilled(sessionId) : false;
455
+ const projected = step.projections
456
+ ? noteComposerProjections(sessionId, { projections: step.projections })
457
+ : false;
458
+ if (projected || filled || dropped || ((changed || queued) && sessionId === state.sessionId)) emit();
459
+ if (step.resync) void pullHistory(sessionId);
460
+ }
461
+
332
462
  function occupy(sessionId) {
333
463
  state.sessionId = sessionId;
334
464
  state.error = "";
335
465
  state.failed = "";
336
466
  liveScroll = cache.scroll(sessionId);
337
467
  emit();
468
+ followMisses = 0;
469
+ sendFollow(sessionId);
338
470
  }
339
471
 
340
472
  function currentIsEmpty(workspaceId, scoped) {
@@ -429,10 +561,10 @@ export function createChatRuntime(client) {
429
561
  }
430
562
 
431
563
  return {
432
- snapshot: () => viewOf(state, cache, pendingQuestions, pendingApprovals, queues),
564
+ snapshot: () => viewOf(state, cache, pendingQuestions, pendingApprovals, inbox),
433
565
  subscribe(listener) {
434
566
  listeners.add(listener);
435
- listener(viewOf(state, cache, pendingQuestions, pendingApprovals, queues));
567
+ listener(viewOf(state, cache, pendingQuestions, pendingApprovals, inbox));
436
568
  return () => listeners.delete(listener);
437
569
  },
438
570
  async start(options = {}) {
@@ -599,18 +731,32 @@ export function createChatRuntime(client) {
599
731
  emit();
600
732
  return archived;
601
733
  },
602
- sessionModels(sessionId = state.sessionId) {
603
- if (!sessionId || isDraft(sessionId)) {
604
- return Promise.resolve({ ok: false, error: { code: "no_session", message: "no session" } });
605
- }
606
- return client.rpc("session.models", { sessionId });
734
+ modelCatalog() {
735
+ return client.rpc("session.modelCatalog");
607
736
  },
608
737
  selectModel(selection) {
609
738
  const sessionId = state.sessionId;
610
739
  if (!sessionId || isDraft(sessionId)) {
611
740
  return Promise.resolve({ ok: false, error: { code: "no_session", message: "no session" } });
612
741
  }
613
- return client.rpc("session.selectModel", { sessionId, ...selection });
742
+ return client.rpc("session.selectModel", { sessionId, ...selection }).then((result) => {
743
+ const selected = result.ok ? result.value?.selected : null;
744
+ if (selected) {
745
+ const row = state.sessions.find((item) => item.sessionId === sessionId);
746
+ noteComposerProjections(sessionId, {
747
+ projections: {
748
+ values: {
749
+ modelSelection: {
750
+ lastUsed: row?.modelSelection?.lastUsed ?? null,
751
+ next: selected,
752
+ },
753
+ },
754
+ },
755
+ });
756
+ emit();
757
+ }
758
+ return result;
759
+ });
614
760
  },
615
761
  cancel() {
616
762
  const sessionId = state.sessionId;
@@ -691,37 +837,11 @@ export function createChatRuntime(client) {
691
837
  }
692
838
  return client.prompt(state.sessionId, content, mode);
693
839
  },
694
- async answerQuestion(answers) {
695
- const pending = pendingQuestions.get(state.sessionId);
696
- if (!pending?.rpcId || typeof client.respond !== "function") {
697
- return { ok: false, error: { message: "no question" } };
698
- }
699
- const receipt = await client.respond(wrapClientResponse(pending.rpcId, {
700
- ok: true,
701
- value: { sessionId: state.sessionId, answer: { answers } },
702
- }));
703
- if (!receipt?.accepted) {
704
- return { ok: false, error: { message: receipt?.reason || "not-pending" } };
705
- }
706
- return { ok: true };
707
- },
708
- async answerApproval(outcome) {
709
- const pending = pendingApprovals.get(state.sessionId);
710
- if (!pending?.rpcId || !pending.approvalId || typeof client.respond !== "function") {
711
- return { ok: false, error: { message: "no approval" } };
712
- }
713
- const receipt = await client.respond(wrapClientResponse(pending.rpcId, {
714
- ok: true,
715
- value: {
716
- sessionId: state.sessionId,
717
- approvalId: pending.approvalId,
718
- outcome: outcome === "allowed-once" ? "allowed-once" : "rejected",
719
- },
720
- }));
721
- if (!receipt?.accepted) {
722
- return { ok: false, error: { message: receipt?.reason || "not-pending" } };
723
- }
724
- return { ok: true };
840
+ answerQuestion(answers) {
841
+ return settleRemote(pendingQuestions, state.sessionId, { answers });
842
+ },
843
+ answerApproval(outcome) {
844
+ return settleRemote(pendingApprovals, state.sessionId, outcome === "allowed-once" ? "allowed-once" : "rejected");
725
845
  },
726
846
  preview(path) {
727
847
  if (!state.sessionId || typeof client.preview !== "function") {
@@ -729,8 +849,8 @@ export function createChatRuntime(client) {
729
849
  }
730
850
  return client.preview(state.sessionId, path);
731
851
  },
732
- mediaUrl(path, modifiedAt) {
733
- return client.mediaUrl?.(state.sessionId, path, modifiedAt) ?? "";
852
+ mediaUrl(path, modifiedAt, size) {
853
+ return client.mediaUrl?.(state.sessionId, path, modifiedAt, size) ?? "";
734
854
  },
735
855
  downloadUrl(path) {
736
856
  return client.downloadUrl?.(state.sessionId, path) ?? "";
@@ -748,6 +868,18 @@ export function createChatRuntime(client) {
748
868
  }
749
869
  return client.transcribe(blob, language, signal);
750
870
  },
871
+ reportLog(scope, event, fields) {
872
+ if (typeof client.reportLog !== "function") return Promise.resolve(false);
873
+ return client.reportLog(scope, event, fields);
874
+ },
875
+ speak(text) {
876
+ if (typeof client.speak !== "function") return Promise.resolve(null);
877
+ return client.speak(text);
878
+ },
879
+ interruptSpeech() {
880
+ if (typeof client.interruptSpeech !== "function") return Promise.resolve(false);
881
+ return client.interruptSpeech();
882
+ },
751
883
  settings: client.settings,
752
884
  rememberScroll(top, height, view) {
753
885
  if (view <= 0) return;
@@ -82,6 +82,12 @@ export function createSessionCache() {
82
82
  pages.delete(sessionId);
83
83
  loads.delete(sessionId);
84
84
  },
85
+ dropLive(sessionId) {
86
+ const page = peek(sessionId);
87
+ if (!page?.events?.some((event) => event?.live)) return false;
88
+ page.events = page.events.filter((event) => !event?.live);
89
+ return true;
90
+ },
85
91
  setScroll(sessionId, scroll) {
86
92
  if (!sessionId || !scroll) return;
87
93
  ensure(sessionId).scroll = scroll;
@@ -27,7 +27,9 @@ export function subscribeConversationSettings(listener) {
27
27
 
28
28
  function fail(res, path) {
29
29
  const err = res?.body?.error;
30
- throw new Error(err?.message || err?.code || `${path} ${res?.status ?? ""}`.trim());
30
+ const code = typeof err?.code === "string" ? err.code : "";
31
+ const message = err?.message || code || `${path} ${res?.status ?? ""}`.trim();
32
+ throw Object.assign(new Error(message), code ? { code } : {});
31
33
  }
32
34
 
33
35
  function payload(res, path) {
@@ -40,8 +42,8 @@ function rpcValue(result, method) {
40
42
  throw new Error(result?.error?.message || result?.error?.code || method);
41
43
  }
42
44
 
43
- function sttModels(body) {
44
- const list = Array.isArray(body?.stt) ? body.stt : [];
45
+ function modelIds(body, key) {
46
+ const list = Array.isArray(body?.[key]) ? body[key] : [];
45
47
  return list.map((item) => (typeof item === "string" ? item : item?.id)).filter(Boolean);
46
48
  }
47
49
 
@@ -93,10 +95,12 @@ export function createHostSettings(request) {
93
95
  request(`/api/lares/voice/status${query}`),
94
96
  request(`/api/lares/voice/models${query}`),
95
97
  ]);
98
+ const listed = payload(models, "/api/lares/voice/models");
96
99
  return {
97
100
  config: payload(config, "/api/lares/voice/config"),
98
101
  status: payload(status, "/api/lares/voice/status"),
99
- sttModels: sttModels(payload(models, "/api/lares/voice/models")),
102
+ sttModels: modelIds(listed, "stt"),
103
+ ttsModels: modelIds(listed, "tts"),
100
104
  };
101
105
  }, { force: Boolean(force) });
102
106
  },
@@ -108,17 +112,21 @@ export function createHostSettings(request) {
108
112
  return this.voice(true);
109
113
  },
110
114
  async search(options = {}) {
115
+ const query = options.force ? "?refresh=1" : "";
111
116
  return searchStore.load(
112
- async () => payload(await request("/api/lares/web-search/config"), "/api/lares/web-search/config"),
117
+ async () => payload(
118
+ await request(`/api/lares/web-search/config${query}`),
119
+ "/api/lares/web-search/config",
120
+ ),
113
121
  options,
114
122
  );
115
123
  },
116
- async setSearchDefault(id) {
124
+ async setSearchDefault(selection) {
117
125
  return searchStore.remember(
118
126
  payload(
119
127
  await request("/api/lares/web-search/config/default", {
120
128
  method: "POST",
121
- body: { defaultSearchModel: id },
129
+ body: selection,
122
130
  }),
123
131
  "/api/lares/web-search/config/default",
124
132
  ),