@olares/lares-core 0.15.6 → 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.
- package/README.md +2 -1
- package/brand/identity.js +33 -3
- package/drive/cat.js +62 -0
- package/drive/download.js +3 -3
- package/drive/execute.js +8 -2
- package/drive/files-path.js +33 -0
- package/drive/flowstudio-files.js +87 -0
- package/drive/ls.js +3 -3
- package/drive/paths.js +23 -13
- package/drive/tools.js +4 -2
- package/entrance/surface.js +96 -0
- package/files/app-link.js +64 -0
- package/files/backend-client.js +266 -0
- package/files/deliverables.js +55 -1
- package/files/intake.js +0 -63
- package/files/mention.js +1 -51
- package/files/preview-groups.js +71 -3
- package/files/preview-local.js +69 -0
- package/files/preview-workspace.js +114 -13
- package/files/preview.js +171 -16
- package/files/produced-gate.js +197 -0
- package/files/produced-mentions.js +19 -0
- package/files/published-tools.js +69 -0
- package/files/published-turn-data.js +84 -0
- package/i18n/chat-model.js +5 -5
- package/i18n/mcp.js +83 -0
- package/i18n/mobile.js +100 -2
- package/i18n/preview.js +10 -0
- package/i18n/search.js +13 -7
- package/i18n/skills.js +31 -0
- package/i18n/voice.js +88 -19
- package/larepass/chat.js +9 -0
- package/larepass/host.js +7 -2
- package/larepass/mux.js +210 -2
- package/larepass/process-fold.js +239 -0
- package/larepass/queue.js +44 -0
- package/larepass/rpc.js +50 -3
- package/larepass/runtime.js +215 -83
- package/larepass/session-cache.js +6 -0
- package/larepass/settings.js +15 -7
- package/larepass/transcript.js +49 -11
- package/mcp/approval.js +152 -0
- package/mcp/client.js +38 -0
- package/mcp/config.js +139 -0
- package/media/catalog-row.js +244 -0
- package/media/generation.js +707 -0
- package/media/tool.js +92 -0
- package/olares/entrance.js +67 -15
- package/olares/identity.js +13 -136
- package/olares/session-identity.js +12 -12
- package/olares/trusted-host.js +0 -29
- package/package.json +48 -4
- package/router/catalog-cache.d.ts +1 -1
- package/router/catalog-cache.js +25 -14
- package/router/catalog-sync.js +8 -3
- package/router/catalog.d.ts +6 -1
- package/router/catalog.js +18 -2
- package/router/gateway.d.ts +9 -1
- package/router/gateway.js +40 -4
- package/router/gemini-schema.js +125 -0
- package/router/generation-guard.js +82 -0
- package/router/models.d.ts +6 -0
- package/router/models.js +22 -3
- package/router/search.js +10 -2
- package/router/session-model.js +53 -0
- package/router/shim.js +104 -3
- package/router/sref.js +126 -0
- package/router/stt.js +43 -12
- package/router/tts.js +118 -0
- package/search/client.js +5 -3
- package/search/config.js +53 -39
- package/search/menu.js +45 -10
- package/skills/client.js +42 -0
- package/skills/packs.js +40 -0
- package/skills/state.d.ts +22 -0
- package/skills/state.js +142 -0
- package/tools/crypto-uuid.js +3 -6
- package/tools/head-script.js +15 -0
- package/voice/agent.js +50 -0
- package/voice/capture.js +250 -0
- package/voice/client.js +126 -24
- package/voice/config.js +11 -1
- package/voice/duplex.js +44 -0
- package/voice/endpoint.js +57 -0
- package/voice/host/bluetooth-ready.py +1196 -0
- package/voice/host/install-host.sh +102 -0
- package/voice/input.js +4 -0
- package/voice/install.js +146 -0
- package/voice/languages.js +11 -1
- package/voice/link.js +86 -0
- package/voice/pcm.js +100 -0
- package/voice/playback.js +7 -0
- package/voice/problem.js +46 -0
- package/voice/recorder.js +79 -4
- package/voice/script.js +35 -0
- package/voice/service.js +253 -10
- package/voice/session.js +128 -0
- package/voice/speech.js +93 -0
- 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
|
|
package/larepass/runtime.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import { isAuthFailure } from "./host.js";
|
|
2
2
|
import { listNavigableSessions, summarizeSession } from "./chat.js";
|
|
3
|
-
import {
|
|
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 {
|
|
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,
|
|
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:
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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 === "
|
|
213
|
-
|
|
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 === "
|
|
217
|
-
|
|
218
|
-
|
|
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,
|
|
564
|
+
snapshot: () => viewOf(state, cache, pendingQuestions, pendingApprovals, inbox),
|
|
433
565
|
subscribe(listener) {
|
|
434
566
|
listeners.add(listener);
|
|
435
|
-
listener(viewOf(state, cache, pendingQuestions, pendingApprovals,
|
|
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
|
-
|
|
603
|
-
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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;
|
package/larepass/settings.js
CHANGED
|
@@ -27,7 +27,9 @@ export function subscribeConversationSettings(listener) {
|
|
|
27
27
|
|
|
28
28
|
function fail(res, path) {
|
|
29
29
|
const err = res?.body?.error;
|
|
30
|
-
|
|
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
|
|
44
|
-
const list = Array.isArray(body?.
|
|
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:
|
|
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(
|
|
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(
|
|
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:
|
|
129
|
+
body: selection,
|
|
122
130
|
}),
|
|
123
131
|
"/api/lares/web-search/config/default",
|
|
124
132
|
),
|