@qearlyao/familiar 0.2.5 → 0.4.0
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/HEARTBEAT.md +1 -1
- package/README.md +33 -0
- package/config.example.toml +4 -2
- package/dist/{agent.js → agent/factory.js} +97 -328
- package/dist/agent/payload-normalizers.js +52 -0
- package/dist/agent/session-helpers.js +86 -0
- package/dist/agent/tool-descriptions.js +4 -0
- package/dist/agent/tools.js +30 -0
- package/dist/agent/transcript-log.js +93 -0
- package/dist/cli.js +45 -15
- package/dist/config/enums.js +35 -0
- package/dist/{config.js → config/index.js} +9 -272
- package/dist/config/interpolate.js +15 -0
- package/dist/config/model-refs.js +11 -0
- package/dist/{config-overrides.js → config/overrides.js} +1 -1
- package/dist/config/readers.js +116 -0
- package/dist/{config-registry.js → config/registry.js} +27 -8
- package/dist/config/sections.js +113 -0
- package/dist/{settings.js → config/settings.js} +5 -2
- package/dist/config/types.js +1 -0
- package/dist/{chat-log.js → conversation/chat-log.js} +16 -4
- package/dist/{contact-note.js → conversation/contact-note.js} +1 -1
- package/dist/conversation/ids.js +11 -0
- package/dist/conversation/owner-identity.js +29 -0
- package/dist/discord/channel.js +32 -0
- package/dist/discord/chunking.js +163 -0
- package/dist/discord/client.js +44 -0
- package/dist/discord/commands.js +181 -0
- package/dist/discord/daemon.js +379 -0
- package/dist/discord/inbound.js +44 -0
- package/dist/discord/send.js +115 -0
- package/dist/discord/turn.js +55 -0
- package/dist/index.js +12 -11
- package/dist/lifecycle/control.js +1 -0
- package/dist/{data-retention.js → lifecycle/data-retention.js} +1 -1
- package/dist/{hot-reload.js → lifecycle/hot-reload.js} +2 -2
- package/dist/{service.js → lifecycle/service.js} +1 -0
- package/dist/media/attachment-limits.js +3 -0
- package/dist/{generated-media.js → media/generated-media.js} +1 -1
- package/dist/{image-gen.js → media/image-gen.js} +2 -2
- package/dist/{inbound-attachments.js → media/inbound-attachments.js} +47 -43
- package/dist/media/media-understanding.js +215 -0
- package/dist/memory/index/store.js +21 -17
- package/dist/memory/index/vector-codec.js +2 -2
- package/dist/memory/lcm/context-transformer.js +6 -2
- package/dist/memory/lcm/segment-manager.js +6 -2
- package/dist/memory/lcm/store/index-ids.js +6 -0
- package/dist/memory/lcm/store/inserts.js +31 -0
- package/dist/memory/lcm/store/normalizers.js +91 -0
- package/dist/memory/lcm/store/row-mappers.js +114 -0
- package/dist/memory/lcm/store/row-types.js +1 -0
- package/dist/memory/lcm/store/serialization.js +37 -0
- package/dist/memory/lcm/store/snapshots.js +73 -0
- package/dist/memory/lcm/store.js +20 -360
- package/dist/memory/lcm/summarizer.js +1 -1
- package/dist/{added-models.js → models/added-models.js} +1 -1
- package/dist/{persona.js → prompting/persona.js} +1 -1
- package/dist/runtime/agent-core.js +82 -0
- package/dist/{agent-events.js → runtime/agent-events.js} +1 -1
- package/dist/runtime/agent-work-queue.js +55 -0
- package/dist/{runtime.js → runtime/conversation-runtime.js} +91 -43
- package/dist/runtime/runtime-manager.js +51 -0
- package/dist/runtime/scheduler-runner.js +243 -0
- package/dist/{scheduler.js → runtime/scheduler.js} +4 -4
- package/dist/{browser-tools.js → tools/browser-tools.js} +24 -34
- package/dist/util/fs.js +2 -1
- package/dist/web/agent-routes.js +104 -0
- package/dist/web/auth-routes.js +39 -0
- package/dist/web/auth.js +205 -0
- package/dist/web/config-routes.js +55 -0
- package/dist/web/conversation-routes.js +122 -0
- package/dist/web/daemon.js +108 -0
- package/dist/web/diary-routes.js +88 -0
- package/dist/web/errors.js +3 -0
- package/dist/web/event-hub.js +246 -0
- package/dist/{web-http.js → web/http.js} +19 -5
- package/dist/web/memes.js +25 -0
- package/dist/web/messages.js +348 -0
- package/dist/web/multipart.js +86 -0
- package/dist/web/payloads.js +34 -0
- package/dist/web/request-context.js +25 -0
- package/dist/web/route-helpers.js +9 -0
- package/dist/web/routes.js +37 -0
- package/dist/web/runtime-actions.js +231 -0
- package/dist/web/session-store.js +161 -0
- package/dist/{web-static.js → web/static.js} +19 -14
- package/dist/web/stream.js +78 -0
- package/dist/web-tools/cache.js +42 -0
- package/dist/web-tools/config.js +16 -0
- package/dist/web-tools/fetch-providers.js +119 -0
- package/dist/web-tools/format.js +88 -0
- package/dist/web-tools/http.js +81 -0
- package/dist/web-tools/index.js +152 -0
- package/dist/web-tools/routing.js +29 -0
- package/dist/web-tools/safety.js +73 -0
- package/dist/web-tools/search-providers.js +277 -0
- package/dist/web-tools/types.js +54 -0
- package/dist/web-tools/util.js +23 -0
- package/npm-shrinkwrap.json +319 -201
- package/package.json +6 -4
- package/web/dist/assets/index-C-k4O5Dz.js +6 -0
- package/web/dist/assets/index-Dj-L9nX4.css +2 -0
- package/web/dist/assets/markdown-kaIeGxdv.js +14 -0
- package/web/dist/assets/react-Bi_azaFt.js +9 -0
- package/web/dist/assets/rolldown-runtime-S-ySWqyJ.js +1 -0
- package/web/dist/assets/ui-C12-nN_X.js +51 -0
- package/web/dist/assets/vendor-D1QXMhXm.js +16 -0
- package/web/dist/index.html +11 -3
- package/dist/discord.js +0 -1299
- package/dist/media-understanding.js +0 -120
- package/dist/web-auth.js +0 -111
- package/dist/web-tools.js +0 -941
- package/dist/web.js +0 -1209
- package/web/dist/assets/index-B23WT77N.js +0 -63
- package/web/dist/assets/index-D3MotFzN.css +0 -2
- /package/dist/{control.js → agent/types.js} +0 -0
- /package/dist/{image-derivatives.js → media/image-derivatives.js} +0 -0
- /package/dist/{tts.js → media/tts.js} +0 -0
- /package/dist/{models.js → models/index.js} +0 -0
- /package/dist/{skills.js → prompting/skills.js} +0 -0
- /package/dist/{silent-marker.js → runtime/silent-marker.js} +0 -0
- /package/dist/{web-events.js → web/events.js} +0 -0
- /package/dist/{web-types.js → web/types.js} +0 -0
package/dist/web.js
DELETED
|
@@ -1,1209 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { createServer } from "node:http";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { getProviders } from "@earendil-works/pi-ai";
|
|
6
|
-
import { addModel, loadAddedModels, removeModel, setAddedModelsPath } from "./added-models.js";
|
|
7
|
-
import { createAgentEventRecorder, storedAgentEventFromAgentEvent, thinkingDurationMs, updateAgentEventSummary, } from "./agent-events.js";
|
|
8
|
-
import { clearConfigOverride, loadConfigOverrides, setConfigOverride } from "./config-overrides.js";
|
|
9
|
-
import { CONFIG_KEYS, CONFIG_REGISTRY, getConfigDefault, isConfigKey } from "./config-registry.js";
|
|
10
|
-
import { getContactNickname, refreshContactNote, setContactNotePath } from "./contact-note.js";
|
|
11
|
-
import { publicAttachmentPath } from "./generated-media.js";
|
|
12
|
-
import { materializeInboundAttachments } from "./inbound-attachments.js";
|
|
13
|
-
import { PROVIDER_DEFAULTS, parseModelRef, supportedThinkingLevels } from "./models.js";
|
|
14
|
-
import { loadPersona, parsePersonaName } from "./persona.js";
|
|
15
|
-
import { consumeSilentDelta, createSilentFilterState, finalizeSilentFilter, parseAgentReply } from "./silent-marker.js";
|
|
16
|
-
import { createAuth, sessionCookie, verifyTotp } from "./web-auth.js";
|
|
17
|
-
import { acceptWebSocket, decodeFrames, encodeFrame, replayEvents } from "./web-events.js";
|
|
18
|
-
import { isObject, readJsonBody, sendJson, sendText } from "./web-http.js";
|
|
19
|
-
import { serveAttachment, serveStatic } from "./web-static.js";
|
|
20
|
-
import { EVENT_REPLAY_LIMIT, WEB_USER_NAME, } from "./web-types.js";
|
|
21
|
-
function toUnixMs(ts) {
|
|
22
|
-
const parsed = ts ? Date.parse(ts) : NaN;
|
|
23
|
-
return Number.isFinite(parsed) ? parsed : Date.now();
|
|
24
|
-
}
|
|
25
|
-
function eventId() {
|
|
26
|
-
return `evt_${randomUUID()}`;
|
|
27
|
-
}
|
|
28
|
-
function messageId(prefix = "msg") {
|
|
29
|
-
return `${prefix}_${randomUUID()}`;
|
|
30
|
-
}
|
|
31
|
-
function isUserVisibleRuntimeRecord(record) {
|
|
32
|
-
return record.type !== "runtime" || !["armed", "reset", "stopped"].includes(record.event);
|
|
33
|
-
}
|
|
34
|
-
function parseMemeCatalog(markdown) {
|
|
35
|
-
const families = [];
|
|
36
|
-
let currentFamily;
|
|
37
|
-
for (const line of markdown.split(/\r?\n/)) {
|
|
38
|
-
const familyMatch = line.match(/^## (.+)$/);
|
|
39
|
-
if (familyMatch) {
|
|
40
|
-
currentFamily = { name: familyMatch[1]?.trim() ?? "", memes: [] };
|
|
41
|
-
families.push(currentFamily);
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (!currentFamily || !line.startsWith("- ") || !line.includes(" — "))
|
|
45
|
-
continue;
|
|
46
|
-
const separator = line.indexOf(" — ");
|
|
47
|
-
const name = line.slice(2, separator).trim();
|
|
48
|
-
const suffix = line.slice(separator + " — ".length).trim();
|
|
49
|
-
if (!name || !suffix)
|
|
50
|
-
continue;
|
|
51
|
-
currentFamily.memes.push({ name, url: `https://files.catbox.moe/${suffix}` });
|
|
52
|
-
}
|
|
53
|
-
return families;
|
|
54
|
-
}
|
|
55
|
-
function memeCatalogPath(config) {
|
|
56
|
-
return join(config.workspacePath, "skills", "memes", "SKILL.md");
|
|
57
|
-
}
|
|
58
|
-
function isWebUploadAttachment(value) {
|
|
59
|
-
return !!value && typeof value === "object" && Buffer.isBuffer(value.buffer);
|
|
60
|
-
}
|
|
61
|
-
async function readRawBody(request, maxBytes) {
|
|
62
|
-
const chunks = [];
|
|
63
|
-
let total = 0;
|
|
64
|
-
for await (const chunk of request) {
|
|
65
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
66
|
-
total += buffer.length;
|
|
67
|
-
if (total > maxBytes)
|
|
68
|
-
throw new Error("Request body too large");
|
|
69
|
-
chunks.push(buffer);
|
|
70
|
-
}
|
|
71
|
-
return Buffer.concat(chunks);
|
|
72
|
-
}
|
|
73
|
-
function multipartBoundary(contentType) {
|
|
74
|
-
const header = Array.isArray(contentType) ? contentType.find((value) => value.includes("boundary=")) : contentType;
|
|
75
|
-
const match = header?.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
|
|
76
|
-
if (!match?.[1] && !match?.[2])
|
|
77
|
-
throw new Error("Missing multipart boundary");
|
|
78
|
-
return match[1] ?? match[2] ?? "";
|
|
79
|
-
}
|
|
80
|
-
function parseContentDisposition(header) {
|
|
81
|
-
const parts = header.split(";").map((part) => part.trim());
|
|
82
|
-
const values = {};
|
|
83
|
-
for (const part of parts.slice(1)) {
|
|
84
|
-
const [key, rawValue] = part.split("=");
|
|
85
|
-
if (!key || rawValue === undefined)
|
|
86
|
-
continue;
|
|
87
|
-
values[key.toLowerCase()] = rawValue.replace(/^"|"$/g, "");
|
|
88
|
-
}
|
|
89
|
-
return values;
|
|
90
|
-
}
|
|
91
|
-
async function readMultipartBody(request, contentType) {
|
|
92
|
-
const boundary = multipartBoundary(contentType);
|
|
93
|
-
const raw = await readRawBody(request, 32 * 1024 * 1024);
|
|
94
|
-
const binary = raw.toString("binary");
|
|
95
|
-
const marker = `--${boundary}`;
|
|
96
|
-
const attachments = [];
|
|
97
|
-
const body = { text: "" };
|
|
98
|
-
for (const section of binary.split(marker).slice(1)) {
|
|
99
|
-
if (!section || section === "--\r\n" || section === "--")
|
|
100
|
-
continue;
|
|
101
|
-
const trimmed = section.replace(/^\r\n/, "").replace(/\r\n--$/, "");
|
|
102
|
-
const headerEnd = trimmed.indexOf("\r\n\r\n");
|
|
103
|
-
if (headerEnd < 0)
|
|
104
|
-
continue;
|
|
105
|
-
const headerText = trimmed.slice(0, headerEnd);
|
|
106
|
-
let contentBinary = trimmed.slice(headerEnd + 4);
|
|
107
|
-
if (contentBinary.endsWith("\r\n"))
|
|
108
|
-
contentBinary = contentBinary.slice(0, -2);
|
|
109
|
-
const headers = Object.fromEntries(headerText.split("\r\n").map((line) => {
|
|
110
|
-
const colon = line.indexOf(":");
|
|
111
|
-
return colon >= 0
|
|
112
|
-
? [line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim()]
|
|
113
|
-
: [line.toLowerCase(), ""];
|
|
114
|
-
}));
|
|
115
|
-
const disposition = parseContentDisposition(headers["content-disposition"] ?? "");
|
|
116
|
-
const name = disposition.name;
|
|
117
|
-
if (!name)
|
|
118
|
-
continue;
|
|
119
|
-
if (name === "text" || name === "channelKey" || name === "clientId") {
|
|
120
|
-
body[name] = Buffer.from(contentBinary, "binary").toString("utf8");
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (name !== "attachments")
|
|
124
|
-
continue;
|
|
125
|
-
const buffer = Buffer.from(contentBinary, "binary");
|
|
126
|
-
if (buffer.length === 0)
|
|
127
|
-
continue;
|
|
128
|
-
attachments.push({
|
|
129
|
-
name: disposition.filename,
|
|
130
|
-
mimeType: headers["content-type"],
|
|
131
|
-
size: buffer.length,
|
|
132
|
-
buffer,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
body.attachments = attachments;
|
|
136
|
-
return body;
|
|
137
|
-
}
|
|
138
|
-
function webAttachments(config, attachments) {
|
|
139
|
-
if (!attachments?.length)
|
|
140
|
-
return undefined;
|
|
141
|
-
return attachments.map((attachment) => ({
|
|
142
|
-
id: attachment.id,
|
|
143
|
-
name: attachment.name,
|
|
144
|
-
kind: attachment.kind,
|
|
145
|
-
mimeType: attachment.mimeType,
|
|
146
|
-
size: attachment.size,
|
|
147
|
-
url: attachment.localPath ? publicAttachmentPath(config, attachment.localPath) : attachment.remoteUrl,
|
|
148
|
-
}));
|
|
149
|
-
}
|
|
150
|
-
function attachmentDerivedText(attachment) {
|
|
151
|
-
if (attachment.derived?.text?.label === "preview")
|
|
152
|
-
return undefined;
|
|
153
|
-
return attachment.derived?.text?.text;
|
|
154
|
-
}
|
|
155
|
-
function toolError(result) {
|
|
156
|
-
if (typeof result === "string")
|
|
157
|
-
return result;
|
|
158
|
-
if (!isObject(result))
|
|
159
|
-
return undefined;
|
|
160
|
-
if (typeof result.error === "string")
|
|
161
|
-
return result.error;
|
|
162
|
-
if (typeof result.message === "string")
|
|
163
|
-
return result.message;
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
|
-
function toolFromStoredAgentEvent(event, ts) {
|
|
167
|
-
if (event.type === "tool_execution_start") {
|
|
168
|
-
return {
|
|
169
|
-
id: event.toolCallId,
|
|
170
|
-
name: event.toolName,
|
|
171
|
-
status: "running",
|
|
172
|
-
args: event.args,
|
|
173
|
-
startedAt: ts,
|
|
174
|
-
updatedAt: ts,
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
if (event.type === "tool_execution_update") {
|
|
178
|
-
return {
|
|
179
|
-
id: event.toolCallId,
|
|
180
|
-
name: event.toolName,
|
|
181
|
-
status: "running",
|
|
182
|
-
args: event.args,
|
|
183
|
-
partialResult: event.partialResult,
|
|
184
|
-
updatedAt: ts,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
if (event.type === "tool_execution_end") {
|
|
188
|
-
return {
|
|
189
|
-
id: event.toolCallId,
|
|
190
|
-
name: event.toolName,
|
|
191
|
-
status: event.isError ? "error" : "completed",
|
|
192
|
-
result: event.result,
|
|
193
|
-
error: event.isError ? toolError(event.result) : undefined,
|
|
194
|
-
completedAt: ts,
|
|
195
|
-
updatedAt: ts,
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
if (event.type === "message_update" && event.assistantMessageEvent.type === "toolcall_end") {
|
|
199
|
-
return {
|
|
200
|
-
id: event.assistantMessageEvent.toolCall.id,
|
|
201
|
-
name: event.assistantMessageEvent.toolCall.name,
|
|
202
|
-
status: "pending",
|
|
203
|
-
args: event.assistantMessageEvent.toolCall.arguments,
|
|
204
|
-
updatedAt: ts,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
return undefined;
|
|
208
|
-
}
|
|
209
|
-
function mergeToolEvent(existing, patch) {
|
|
210
|
-
const terminal = patch.status === "completed" || patch.status === "error";
|
|
211
|
-
return {
|
|
212
|
-
...existing,
|
|
213
|
-
...patch,
|
|
214
|
-
args: patch.args ?? existing?.args,
|
|
215
|
-
partialResult: terminal ? undefined : (patch.partialResult ?? existing?.partialResult),
|
|
216
|
-
result: patch.result ?? existing?.result,
|
|
217
|
-
error: patch.error ?? existing?.error,
|
|
218
|
-
startedAt: existing?.startedAt ?? patch.startedAt,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
function stepId(messageId, kind, index) {
|
|
222
|
-
return `${messageId}-${kind}-${index}`;
|
|
223
|
-
}
|
|
224
|
-
function closeOpenContentSteps(steps, now) {
|
|
225
|
-
for (const step of steps) {
|
|
226
|
-
if (step.kind === "thinking" && !step.complete) {
|
|
227
|
-
step.complete = true;
|
|
228
|
-
step.endedAt ??= now;
|
|
229
|
-
}
|
|
230
|
-
if (step.kind === "text" && !step.complete)
|
|
231
|
-
step.complete = true;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
function appendDeltaStep(steps, messageId, part, content, now) {
|
|
235
|
-
const last = steps.at(-1);
|
|
236
|
-
if (part === "thinking") {
|
|
237
|
-
if (last?.kind === "thinking" && !last.complete) {
|
|
238
|
-
last.text += content;
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
closeOpenContentSteps(steps, now);
|
|
242
|
-
steps.push({
|
|
243
|
-
kind: "thinking",
|
|
244
|
-
id: stepId(messageId, "thinking", steps.length),
|
|
245
|
-
text: content,
|
|
246
|
-
startedAt: now,
|
|
247
|
-
});
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
if (last?.kind === "text" && !last.complete) {
|
|
251
|
-
last.text += content;
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
closeOpenContentSteps(steps, now);
|
|
255
|
-
steps.push({ kind: "text", id: stepId(messageId, "text", steps.length), text: content });
|
|
256
|
-
}
|
|
257
|
-
function upsertToolStep(steps, tool, now) {
|
|
258
|
-
const index = steps.findIndex((step) => step.kind === "tool" && step.tool.id === tool.id);
|
|
259
|
-
if (index >= 0) {
|
|
260
|
-
const existing = steps[index];
|
|
261
|
-
if (existing?.kind === "tool")
|
|
262
|
-
existing.tool = mergeToolEvent(existing.tool, tool);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
closeOpenContentSteps(steps, now);
|
|
266
|
-
steps.push({ kind: "tool", id: tool.id, tool });
|
|
267
|
-
}
|
|
268
|
-
function applyStoredAgentEventToMessage(message, record, options) {
|
|
269
|
-
const event = record.event;
|
|
270
|
-
const ts = toUnixMs(record.ts);
|
|
271
|
-
message.steps ??= [];
|
|
272
|
-
const steps = message.steps;
|
|
273
|
-
if (event.type === "message_update") {
|
|
274
|
-
const assistantEvent = event.assistantMessageEvent;
|
|
275
|
-
if (assistantEvent.type === "text_delta") {
|
|
276
|
-
appendDeltaStep(steps, message.id, "text", assistantEvent.delta, ts);
|
|
277
|
-
if (options.applyTextDeltas)
|
|
278
|
-
message.text += assistantEvent.delta;
|
|
279
|
-
}
|
|
280
|
-
if (assistantEvent.type === "thinking_delta") {
|
|
281
|
-
appendDeltaStep(steps, message.id, "thinking", assistantEvent.delta, ts);
|
|
282
|
-
if (options.applyThinkingDeltas)
|
|
283
|
-
message.thinking = `${message.thinking ?? ""}${assistantEvent.delta}`;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
if (event.type === "message_end") {
|
|
287
|
-
closeOpenContentSteps(steps, ts);
|
|
288
|
-
if (event.usage)
|
|
289
|
-
message.usage = event.usage;
|
|
290
|
-
}
|
|
291
|
-
const tool = toolFromStoredAgentEvent(event, ts);
|
|
292
|
-
if (tool) {
|
|
293
|
-
upsertToolStep(steps, tool, ts);
|
|
294
|
-
const tools = message.tools ?? [];
|
|
295
|
-
const index = tools.findIndex((candidate) => candidate.id === tool.id);
|
|
296
|
-
if (index >= 0) {
|
|
297
|
-
tools[index] = mergeToolEvent(tools[index], tool);
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
tools.push(tool);
|
|
301
|
-
}
|
|
302
|
-
message.tools = tools;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
function ensureFallbackSteps(message) {
|
|
306
|
-
if (message.steps?.length)
|
|
307
|
-
return;
|
|
308
|
-
const steps = [];
|
|
309
|
-
if (message.thinking || message.thinkingMs != null) {
|
|
310
|
-
const endedAt = message.ts;
|
|
311
|
-
steps.push({
|
|
312
|
-
kind: "thinking",
|
|
313
|
-
id: stepId(message.id, "thinking", steps.length),
|
|
314
|
-
text: message.thinking ?? "",
|
|
315
|
-
startedAt: endedAt - (message.thinkingMs ?? 0),
|
|
316
|
-
endedAt,
|
|
317
|
-
complete: true,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
for (const tool of message.tools ?? [])
|
|
321
|
-
steps.push({ kind: "tool", id: tool.id, tool });
|
|
322
|
-
if (message.text) {
|
|
323
|
-
steps.push({ kind: "text", id: stepId(message.id, "text", steps.length), text: message.text, complete: true });
|
|
324
|
-
}
|
|
325
|
-
if (steps.length)
|
|
326
|
-
message.steps = steps;
|
|
327
|
-
}
|
|
328
|
-
function webMessagesFromRecords(config, records, assistantName) {
|
|
329
|
-
const messages = [];
|
|
330
|
-
const messagesById = new Map();
|
|
331
|
-
const pendingAgentEvents = new Map();
|
|
332
|
-
for (const record of records) {
|
|
333
|
-
const message = webMessageFromRecord(config, record, assistantName);
|
|
334
|
-
if (message) {
|
|
335
|
-
messages.push(message);
|
|
336
|
-
messagesById.set(message.id, message);
|
|
337
|
-
const pending = pendingAgentEvents.get(message.id) ?? [];
|
|
338
|
-
for (const pendingRecord of pending) {
|
|
339
|
-
applyStoredAgentEventToMessage(message, pendingRecord, {
|
|
340
|
-
applyTextDeltas: !message.text && !message.silent,
|
|
341
|
-
applyThinkingDeltas: !message.thinking,
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
pendingAgentEvents.delete(message.id);
|
|
345
|
-
}
|
|
346
|
-
if (record.type === "agent_event") {
|
|
347
|
-
const existing = messagesById.get(record.messageId);
|
|
348
|
-
if (existing) {
|
|
349
|
-
applyStoredAgentEventToMessage(existing, record, {
|
|
350
|
-
applyTextDeltas: !existing.silent,
|
|
351
|
-
applyThinkingDeltas: true,
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
const pending = pendingAgentEvents.get(record.messageId) ?? [];
|
|
356
|
-
pending.push(record);
|
|
357
|
-
pendingAgentEvents.set(record.messageId, pending);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
for (const message of messages)
|
|
362
|
-
ensureFallbackSteps(message);
|
|
363
|
-
return messages;
|
|
364
|
-
}
|
|
365
|
-
function webHistoryPayload(config, records, assistantName, channelKey, options) {
|
|
366
|
-
const messages = webMessagesFromRecords(config, records, assistantName);
|
|
367
|
-
const end = options.before ? messages.findIndex((message) => message.id === options.before) : messages.length;
|
|
368
|
-
const safeEnd = end >= 0 ? end : messages.length;
|
|
369
|
-
const page = messages.slice(Math.max(0, safeEnd - options.limit), safeEnd);
|
|
370
|
-
return { messages: page, hasMore: safeEnd - options.limit > 0, channelKey };
|
|
371
|
-
}
|
|
372
|
-
function webMessageFromRecord(config, record, assistantName) {
|
|
373
|
-
if (!isUserVisibleRuntimeRecord(record))
|
|
374
|
-
return undefined;
|
|
375
|
-
if (record.type === "inbound") {
|
|
376
|
-
const attachmentText = record.attachments
|
|
377
|
-
.map((attachment) => attachmentDerivedText(attachment))
|
|
378
|
-
.filter((text) => !!text)
|
|
379
|
-
.join("\n");
|
|
380
|
-
return {
|
|
381
|
-
id: record.messageId,
|
|
382
|
-
role: "user",
|
|
383
|
-
who: record.authorName || getContactNickname(WEB_USER_NAME),
|
|
384
|
-
text: [record.text, attachmentText].filter(Boolean).join("\n"),
|
|
385
|
-
attachments: webAttachments(config, record.attachments),
|
|
386
|
-
ts: toUnixMs(record.ts),
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
if (record.type === "outbound" && !record.control) {
|
|
390
|
-
return {
|
|
391
|
-
id: record.webMessageId || record.messageIds[0] || `out_${record.recordId}`,
|
|
392
|
-
role: "assistant",
|
|
393
|
-
who: assistantName,
|
|
394
|
-
text: record.text,
|
|
395
|
-
attachments: webAttachments(config, record.attachments),
|
|
396
|
-
thinking: record.thinking,
|
|
397
|
-
thinkingMs: record.thinkingMs,
|
|
398
|
-
silent: record.silent || undefined,
|
|
399
|
-
ts: toUnixMs(record.ts),
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
if (record.type === "runtime" || record.type === "error") {
|
|
403
|
-
return {
|
|
404
|
-
id: `sys_${record.recordId}`,
|
|
405
|
-
role: "system",
|
|
406
|
-
who: "system",
|
|
407
|
-
text: record.type === "runtime" ? record.detail || record.event : record.message,
|
|
408
|
-
ts: toUnixMs(record.ts),
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
return undefined;
|
|
412
|
-
}
|
|
413
|
-
function commandArgs(command, args) {
|
|
414
|
-
if (!isObject(args))
|
|
415
|
-
return "";
|
|
416
|
-
if (command === "model")
|
|
417
|
-
return typeof args.model === "string" ? args.model : "";
|
|
418
|
-
if (command === "thinking")
|
|
419
|
-
return typeof args.level === "string" ? args.level : "";
|
|
420
|
-
if (command === "channel-trigger")
|
|
421
|
-
return typeof args.trigger === "string" ? args.trigger : "";
|
|
422
|
-
return "";
|
|
423
|
-
}
|
|
424
|
-
function formatSetting(setting) {
|
|
425
|
-
return `${setting.value} (${setting.source})`;
|
|
426
|
-
}
|
|
427
|
-
function agentSettingsPayload(familiarAgent, channelKey, personaName) {
|
|
428
|
-
const { model } = familiarAgent.resolveChannelModel(channelKey);
|
|
429
|
-
return {
|
|
430
|
-
model: familiarAgent.getModel(channelKey),
|
|
431
|
-
thinking: familiarAgent.getThinkingLevel(channelKey),
|
|
432
|
-
supportedThinking: supportedThinkingLevels(model),
|
|
433
|
-
persona: { name: personaName },
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
function sessionDto(session) {
|
|
437
|
-
return {
|
|
438
|
-
key: session.key,
|
|
439
|
-
label: session.label,
|
|
440
|
-
service: session.channel.service,
|
|
441
|
-
scope: session.channel.scope,
|
|
442
|
-
channelId: session.channel.channelId,
|
|
443
|
-
channelName: session.channel.channelName,
|
|
444
|
-
threadId: session.channel.threadId,
|
|
445
|
-
isDefault: session.isDefault,
|
|
446
|
-
};
|
|
447
|
-
}
|
|
448
|
-
export async function startWebDaemon(config, familiarAgent, discordDaemon, options = {}) {
|
|
449
|
-
setAddedModelsPath(config.workspace.dataDir);
|
|
450
|
-
setContactNotePath(config.persona.contact);
|
|
451
|
-
await refreshContactNote();
|
|
452
|
-
const persona = await loadPersona(config);
|
|
453
|
-
const personaName = parsePersonaName(persona.soul);
|
|
454
|
-
const auth = createAuth(config);
|
|
455
|
-
const clients = new Set();
|
|
456
|
-
const eventsByChannel = new Map();
|
|
457
|
-
const runtimeSubscriptions = new Map();
|
|
458
|
-
const IN_FLIGHT_TTL_MS = 10 * 60 * 1000;
|
|
459
|
-
const inFlightMessages = new Map();
|
|
460
|
-
const getOrCreateInFlight = (messageIdValue) => {
|
|
461
|
-
let entry = inFlightMessages.get(messageIdValue);
|
|
462
|
-
if (!entry) {
|
|
463
|
-
entry = { locallyStreamed: false, startedSilent: false, lastActiveAt: Date.now() };
|
|
464
|
-
inFlightMessages.set(messageIdValue, entry);
|
|
465
|
-
}
|
|
466
|
-
else {
|
|
467
|
-
entry.lastActiveAt = Date.now();
|
|
468
|
-
}
|
|
469
|
-
return entry;
|
|
470
|
-
};
|
|
471
|
-
const touchInFlight = (messageIdValue) => {
|
|
472
|
-
const entry = inFlightMessages.get(messageIdValue);
|
|
473
|
-
if (entry)
|
|
474
|
-
entry.lastActiveAt = Date.now();
|
|
475
|
-
};
|
|
476
|
-
const inFlightGcTimer = setInterval(() => {
|
|
477
|
-
const cutoff = Date.now() - IN_FLIGHT_TTL_MS;
|
|
478
|
-
for (const [id, entry] of inFlightMessages) {
|
|
479
|
-
if (entry.lastActiveAt < cutoff)
|
|
480
|
-
inFlightMessages.delete(id);
|
|
481
|
-
}
|
|
482
|
-
}, 60 * 1000);
|
|
483
|
-
inFlightGcTimer.unref?.();
|
|
484
|
-
const publish = (event) => {
|
|
485
|
-
const fullEvent = { ...event, eventId: eventId(), ts: event.ts ?? Date.now() };
|
|
486
|
-
const events = eventsByChannel.get(fullEvent.channelKey ?? "") ?? [];
|
|
487
|
-
events.push(fullEvent);
|
|
488
|
-
if (events.length > EVENT_REPLAY_LIMIT)
|
|
489
|
-
events.shift();
|
|
490
|
-
eventsByChannel.set(fullEvent.channelKey ?? "", events);
|
|
491
|
-
const frame = encodeFrame(JSON.stringify(fullEvent));
|
|
492
|
-
for (const client of clients) {
|
|
493
|
-
if (client.channelKey === fullEvent.channelKey && !client.socket.destroyed) {
|
|
494
|
-
if (client.authed) {
|
|
495
|
-
client.socket.write(frame);
|
|
496
|
-
}
|
|
497
|
-
else {
|
|
498
|
-
const pendingEvents = client.pendingEvents ?? [];
|
|
499
|
-
pendingEvents.push(fullEvent);
|
|
500
|
-
client.pendingEvents = pendingEvents;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
return fullEvent;
|
|
505
|
-
};
|
|
506
|
-
const publishDelta = (channelKey, messageIdValue, part, text, ts) => publish({ type: "delta", channelKey, messageId: messageIdValue, part, content: text, text, ts });
|
|
507
|
-
const publishStoredAgentEvent = (channelKey, messageIdValue, storedEvent, ts) => {
|
|
508
|
-
touchInFlight(messageIdValue);
|
|
509
|
-
if (storedEvent.type === "message_start" && storedEvent.role === "assistant") {
|
|
510
|
-
const entry = getOrCreateInFlight(messageIdValue);
|
|
511
|
-
entry.locallyStreamed = true;
|
|
512
|
-
entry.silentFilter = createSilentFilterState();
|
|
513
|
-
entry.pendingStartTs = ts;
|
|
514
|
-
entry.startedSilent = false;
|
|
515
|
-
}
|
|
516
|
-
const startedSilentMessage = () => {
|
|
517
|
-
const entry = inFlightMessages.get(messageIdValue);
|
|
518
|
-
if (!entry || entry.startedSilent)
|
|
519
|
-
return false;
|
|
520
|
-
const startTs = entry.pendingStartTs;
|
|
521
|
-
entry.pendingStartTs = undefined;
|
|
522
|
-
entry.startedSilent = true;
|
|
523
|
-
publish({
|
|
524
|
-
type: "message_started",
|
|
525
|
-
channelKey,
|
|
526
|
-
messageId: messageIdValue,
|
|
527
|
-
role: "assistant",
|
|
528
|
-
who: personaName,
|
|
529
|
-
ts: startTs,
|
|
530
|
-
});
|
|
531
|
-
return true;
|
|
532
|
-
};
|
|
533
|
-
if (storedEvent.type === "message_update") {
|
|
534
|
-
const assistantEvent = storedEvent.assistantMessageEvent;
|
|
535
|
-
if (assistantEvent.type === "thinking_delta") {
|
|
536
|
-
startedSilentMessage();
|
|
537
|
-
publishDelta(channelKey, messageIdValue, "thinking", assistantEvent.delta, ts);
|
|
538
|
-
}
|
|
539
|
-
if (assistantEvent.type === "text_delta") {
|
|
540
|
-
const filter = inFlightMessages.get(messageIdValue)?.silentFilter;
|
|
541
|
-
if (!filter) {
|
|
542
|
-
startedSilentMessage();
|
|
543
|
-
publishDelta(channelKey, messageIdValue, "text", assistantEvent.delta, ts);
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
const result = consumeSilentDelta(filter, assistantEvent.delta);
|
|
547
|
-
if (result.kind === "emit" && result.text) {
|
|
548
|
-
startedSilentMessage();
|
|
549
|
-
publishDelta(channelKey, messageIdValue, "text", result.text, ts);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
if (storedEvent.type === "tool_execution_start") {
|
|
555
|
-
startedSilentMessage();
|
|
556
|
-
}
|
|
557
|
-
if (storedEvent.type === "message_end" && storedEvent.role === "assistant") {
|
|
558
|
-
const entry = inFlightMessages.get(messageIdValue);
|
|
559
|
-
const filter = entry?.silentFilter;
|
|
560
|
-
let silent = false;
|
|
561
|
-
if (filter && entry) {
|
|
562
|
-
const final = finalizeSilentFilter(filter);
|
|
563
|
-
silent = final.silent;
|
|
564
|
-
if (!silent) {
|
|
565
|
-
startedSilentMessage();
|
|
566
|
-
if (final.flush) {
|
|
567
|
-
publishDelta(channelKey, messageIdValue, "text", final.flush, ts);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
else {
|
|
571
|
-
entry.startedSilent = true;
|
|
572
|
-
entry.pendingStartTs = undefined;
|
|
573
|
-
}
|
|
574
|
-
entry.silentFilter = undefined;
|
|
575
|
-
}
|
|
576
|
-
else {
|
|
577
|
-
startedSilentMessage();
|
|
578
|
-
}
|
|
579
|
-
publish({
|
|
580
|
-
type: "message_completed",
|
|
581
|
-
channelKey,
|
|
582
|
-
messageId: messageIdValue,
|
|
583
|
-
usage: storedEvent.usage,
|
|
584
|
-
silent: silent || undefined,
|
|
585
|
-
ts,
|
|
586
|
-
});
|
|
587
|
-
}
|
|
588
|
-
const tool = toolFromStoredAgentEvent(storedEvent, ts ?? Date.now());
|
|
589
|
-
if (tool)
|
|
590
|
-
publish({ type: "tool_event", channelKey, messageId: messageIdValue, tool, ts });
|
|
591
|
-
};
|
|
592
|
-
const subscribeRuntime = (runtime) => {
|
|
593
|
-
if (runtimeSubscriptions.has(runtime.channelKey))
|
|
594
|
-
return;
|
|
595
|
-
const unsubscribeRecords = runtime.subscribe((record) => {
|
|
596
|
-
if (record.type === "inbound") {
|
|
597
|
-
publish({
|
|
598
|
-
type: "message_started",
|
|
599
|
-
channelKey: runtime.channelKey,
|
|
600
|
-
messageId: record.messageId,
|
|
601
|
-
role: "user",
|
|
602
|
-
who: record.authorName || getContactNickname(WEB_USER_NAME),
|
|
603
|
-
ts: toUnixMs(record.ts),
|
|
604
|
-
});
|
|
605
|
-
publishDelta(runtime.channelKey, record.messageId, "text", record.text, toUnixMs(record.ts));
|
|
606
|
-
publish({
|
|
607
|
-
type: "message_completed",
|
|
608
|
-
channelKey: runtime.channelKey,
|
|
609
|
-
messageId: record.messageId,
|
|
610
|
-
attachments: webAttachments(config, record.attachments),
|
|
611
|
-
ts: toUnixMs(record.ts),
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
if (record.type === "outbound" && !record.control) {
|
|
615
|
-
const outboundId = record.webMessageId || record.messageIds[0] || `out_${record.recordId}`;
|
|
616
|
-
const completion = {
|
|
617
|
-
type: "message_completed",
|
|
618
|
-
channelKey: runtime.channelKey,
|
|
619
|
-
messageId: outboundId,
|
|
620
|
-
thinkingMs: record.thinkingMs,
|
|
621
|
-
attachments: webAttachments(config, record.attachments),
|
|
622
|
-
silent: record.silent || undefined,
|
|
623
|
-
ts: toUnixMs(record.ts),
|
|
624
|
-
};
|
|
625
|
-
if (inFlightMessages.get(outboundId)?.locallyStreamed) {
|
|
626
|
-
inFlightMessages.delete(outboundId);
|
|
627
|
-
publish(completion);
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
if (!record.silent) {
|
|
631
|
-
publish({
|
|
632
|
-
type: "message_started",
|
|
633
|
-
channelKey: runtime.channelKey,
|
|
634
|
-
messageId: outboundId,
|
|
635
|
-
role: "assistant",
|
|
636
|
-
who: personaName,
|
|
637
|
-
ts: toUnixMs(record.ts),
|
|
638
|
-
});
|
|
639
|
-
if (record.thinking)
|
|
640
|
-
publishDelta(runtime.channelKey, outboundId, "thinking", record.thinking, toUnixMs(record.ts));
|
|
641
|
-
if (record.text)
|
|
642
|
-
publishDelta(runtime.channelKey, outboundId, "text", record.text, toUnixMs(record.ts));
|
|
643
|
-
}
|
|
644
|
-
publish(completion);
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
const unsubscribeAgentEvents = runtime.subscribeAgentEvents((agentEvent) => {
|
|
648
|
-
publishStoredAgentEvent(runtime.channelKey, agentEvent.messageId, agentEvent.event, agentEvent.ts);
|
|
649
|
-
});
|
|
650
|
-
runtimeSubscriptions.set(runtime.channelKey, () => {
|
|
651
|
-
unsubscribeRecords();
|
|
652
|
-
unsubscribeAgentEvents();
|
|
653
|
-
});
|
|
654
|
-
};
|
|
655
|
-
const getRuntime = async (channelKey) => {
|
|
656
|
-
const runtime = await discordDaemon.getRuntimeForWebChannel(channelKey);
|
|
657
|
-
subscribeRuntime(runtime);
|
|
658
|
-
return runtime;
|
|
659
|
-
};
|
|
660
|
-
const subscribeKnownRuntimes = async () => {
|
|
661
|
-
const sessions = await discordDaemon.getWebSessions();
|
|
662
|
-
await Promise.all(sessions.map(async (session) => {
|
|
663
|
-
const runtime = await discordDaemon.getRuntimeForWebChannel(session.key);
|
|
664
|
-
subscribeRuntime(runtime);
|
|
665
|
-
}));
|
|
666
|
-
};
|
|
667
|
-
const getChannelKeyFromRequest = (url, body) => {
|
|
668
|
-
const queryKey = url.searchParams.get("channelKey");
|
|
669
|
-
if (queryKey)
|
|
670
|
-
return queryKey;
|
|
671
|
-
if (isObject(body) && typeof body.channelKey === "string")
|
|
672
|
-
return body.channelKey;
|
|
673
|
-
return undefined;
|
|
674
|
-
};
|
|
675
|
-
const getAgentModelsPayload = () => {
|
|
676
|
-
const models = [];
|
|
677
|
-
const added = [];
|
|
678
|
-
const seen = new Set();
|
|
679
|
-
for (const model of config.models.allow) {
|
|
680
|
-
if (seen.has(model))
|
|
681
|
-
continue;
|
|
682
|
-
seen.add(model);
|
|
683
|
-
models.push(model);
|
|
684
|
-
}
|
|
685
|
-
for (const model of loadAddedModels()) {
|
|
686
|
-
if (seen.has(model))
|
|
687
|
-
continue;
|
|
688
|
-
seen.add(model);
|
|
689
|
-
models.push(model);
|
|
690
|
-
added.push(model);
|
|
691
|
-
}
|
|
692
|
-
return { models, added };
|
|
693
|
-
};
|
|
694
|
-
const getConfigPayload = () => {
|
|
695
|
-
const overrides = loadConfigOverrides();
|
|
696
|
-
const values = {};
|
|
697
|
-
for (const key of CONFIG_KEYS) {
|
|
698
|
-
const entry = CONFIG_REGISTRY[key];
|
|
699
|
-
values[key] = {
|
|
700
|
-
value: entry.read(config),
|
|
701
|
-
source: key in overrides ? "override" : "config",
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
return { values };
|
|
705
|
-
};
|
|
706
|
-
const parseRequestedModel = (value) => {
|
|
707
|
-
if (typeof value !== "string")
|
|
708
|
-
return { ok: false, error: "format must be provider/model-id" };
|
|
709
|
-
const ref = parseModelRef(value);
|
|
710
|
-
if (!ref)
|
|
711
|
-
return { ok: false, error: "format must be provider/model-id" };
|
|
712
|
-
return { ok: true, model: ref.key, ref };
|
|
713
|
-
};
|
|
714
|
-
const replay = (client, channelKey, lastEventId) => {
|
|
715
|
-
const events = eventsByChannel.get(channelKey) ?? [];
|
|
716
|
-
replayEvents(client, events, lastEventId, () => publish({ type: "replay_window_lost", channelKey }));
|
|
717
|
-
};
|
|
718
|
-
const promptForRuntime = async (runtime, jobId, prompt, attachments = [], onTurnEnd) => {
|
|
719
|
-
const assistantMessageId = messageId();
|
|
720
|
-
const summary = { thinking: "" };
|
|
721
|
-
const recorder = createAgentEventRecorder((storedEvent) => runtime.noteAgentEvent(jobId, assistantMessageId, storedEvent, { notify: false }));
|
|
722
|
-
let started = false;
|
|
723
|
-
let reply;
|
|
724
|
-
try {
|
|
725
|
-
reply = await discordDaemon.runPromptForWeb(runtime, jobId, prompt, attachments, async (event) => {
|
|
726
|
-
if (event.type === "message_start" && event.message.role === "assistant" && !started) {
|
|
727
|
-
started = true;
|
|
728
|
-
}
|
|
729
|
-
updateAgentEventSummary(summary, event);
|
|
730
|
-
const storedEvent = storedAgentEventFromAgentEvent(event);
|
|
731
|
-
if (storedEvent) {
|
|
732
|
-
runtime.publishAgentEvent(jobId, assistantMessageId, storedEvent);
|
|
733
|
-
await recorder.record(storedEvent);
|
|
734
|
-
}
|
|
735
|
-
}, onTurnEnd);
|
|
736
|
-
}
|
|
737
|
-
finally {
|
|
738
|
-
await recorder.flush();
|
|
739
|
-
}
|
|
740
|
-
const parsed = parseAgentReply(reply.text);
|
|
741
|
-
const finalText = parsed.silent ? "" : reply.text;
|
|
742
|
-
if (!started && !parsed.silent) {
|
|
743
|
-
publish({
|
|
744
|
-
type: "message_started",
|
|
745
|
-
channelKey: runtime.channelKey,
|
|
746
|
-
messageId: assistantMessageId,
|
|
747
|
-
role: "assistant",
|
|
748
|
-
who: personaName,
|
|
749
|
-
});
|
|
750
|
-
if (finalText) {
|
|
751
|
-
publishDelta(runtime.channelKey, assistantMessageId, "text", finalText);
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
const thinkingMs = thinkingDurationMs(summary);
|
|
755
|
-
publish({
|
|
756
|
-
type: "message_completed",
|
|
757
|
-
channelKey: runtime.channelKey,
|
|
758
|
-
messageId: assistantMessageId,
|
|
759
|
-
thinkingMs,
|
|
760
|
-
attachments: webAttachments(config, reply.attachments),
|
|
761
|
-
silent: parsed.silent || undefined,
|
|
762
|
-
});
|
|
763
|
-
const entry = getOrCreateInFlight(assistantMessageId);
|
|
764
|
-
entry.locallyStreamed = true;
|
|
765
|
-
return {
|
|
766
|
-
text: finalText,
|
|
767
|
-
messageId: assistantMessageId,
|
|
768
|
-
thinking: summary.thinking,
|
|
769
|
-
thinkingMs,
|
|
770
|
-
attachments: reply.attachments,
|
|
771
|
-
silent: parsed.silent,
|
|
772
|
-
};
|
|
773
|
-
};
|
|
774
|
-
const drainJobs = async (runtime) => {
|
|
775
|
-
for (;;) {
|
|
776
|
-
const dispatch = runtime.beginNextJob();
|
|
777
|
-
if (!dispatch)
|
|
778
|
-
return;
|
|
779
|
-
try {
|
|
780
|
-
const reply = await promptForRuntime(runtime, dispatch.job.jobId, dispatch.prompt, dispatch.attachments, () => {
|
|
781
|
-
publish({
|
|
782
|
-
type: "status",
|
|
783
|
-
channelKey: runtime.channelKey,
|
|
784
|
-
kind: "idle",
|
|
785
|
-
});
|
|
786
|
-
});
|
|
787
|
-
await runtime.completeActiveJob({
|
|
788
|
-
text: reply.text,
|
|
789
|
-
messageIds: [reply.messageId],
|
|
790
|
-
webMessageId: reply.messageId,
|
|
791
|
-
attachments: reply.attachments,
|
|
792
|
-
thinking: reply.thinking,
|
|
793
|
-
thinkingMs: reply.thinkingMs,
|
|
794
|
-
silent: reply.silent,
|
|
795
|
-
replyToMessageId: dispatch.triggerMessageId,
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
catch (error) {
|
|
799
|
-
if (!runtime.hasActiveJob(dispatch.job.jobId))
|
|
800
|
-
return;
|
|
801
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
802
|
-
await runtime.failActiveJob(message);
|
|
803
|
-
await runtime.appendError(message);
|
|
804
|
-
publish({ type: "error", channelKey: runtime.channelKey, code: "unknown", message });
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
};
|
|
808
|
-
const applyControlCommand = async (runtime, control) => {
|
|
809
|
-
if (control.command === "stop") {
|
|
810
|
-
familiarAgent.requestSoftStop(runtime.channelKey);
|
|
811
|
-
return "Stopped after current step. Conversation preserved.";
|
|
812
|
-
}
|
|
813
|
-
if (control.command === "new") {
|
|
814
|
-
await familiarAgent.reset(runtime.channelKey);
|
|
815
|
-
await runtime.resetConversation("new conversation requested");
|
|
816
|
-
return "Started a fresh agent transcript for this channel.";
|
|
817
|
-
}
|
|
818
|
-
if (control.command === "reload") {
|
|
819
|
-
return familiarAgent.reload();
|
|
820
|
-
}
|
|
821
|
-
if (control.command === "restart") {
|
|
822
|
-
return options.restart
|
|
823
|
-
? await options.restart()
|
|
824
|
-
: "Restart requested, but no restart handler is configured. Please restart the Familiar process manually.";
|
|
825
|
-
}
|
|
826
|
-
if (control.command === "model") {
|
|
827
|
-
return control.args
|
|
828
|
-
? await familiarAgent.setModel(runtime.channelKey, control.args)
|
|
829
|
-
: `Current model: ${formatSetting(familiarAgent.getModel(runtime.channelKey))}`;
|
|
830
|
-
}
|
|
831
|
-
if (control.command === "thinking") {
|
|
832
|
-
return control.args
|
|
833
|
-
? await familiarAgent.setThinkingLevel(runtime.channelKey, control.args)
|
|
834
|
-
: `Current thinking: ${formatSetting(familiarAgent.getThinkingLevel(runtime.channelKey))}`;
|
|
835
|
-
}
|
|
836
|
-
if (control.command === "channel-trigger")
|
|
837
|
-
return "Use Discord /familiar channel-trigger in the channel for now.";
|
|
838
|
-
if (control.command === "status") {
|
|
839
|
-
return [
|
|
840
|
-
runtime.formatStatus(),
|
|
841
|
-
`model: ${formatSetting(familiarAgent.getModel(runtime.channelKey))}`,
|
|
842
|
-
`thinking: ${formatSetting(familiarAgent.getThinkingLevel(runtime.channelKey))}`,
|
|
843
|
-
].join("\n");
|
|
844
|
-
}
|
|
845
|
-
return "Compact is not wired for this runtime yet. I logged the command, but I won't run lossy compaction here.";
|
|
846
|
-
};
|
|
847
|
-
const handleApi = async (request, response, url) => {
|
|
848
|
-
if (!url.pathname.startsWith("/api/web/"))
|
|
849
|
-
return false;
|
|
850
|
-
if (!auth.authorize(request, url.pathname)) {
|
|
851
|
-
sendJson(response, 401, { error: "unauthorized" });
|
|
852
|
-
return true;
|
|
853
|
-
}
|
|
854
|
-
try {
|
|
855
|
-
if (request.method === "GET" && url.pathname.startsWith("/api/web/attachments/")) {
|
|
856
|
-
return serveAttachment(config, response, url.pathname, request.headers.range);
|
|
857
|
-
}
|
|
858
|
-
if (request.method === "GET" && url.pathname === "/api/web/auth/mode") {
|
|
859
|
-
sendJson(response, 200, { mode: config.web.authMode, personaName });
|
|
860
|
-
return true;
|
|
861
|
-
}
|
|
862
|
-
if (request.method === "GET" && url.pathname === "/api/web/sessions") {
|
|
863
|
-
const sessions = await discordDaemon.getWebSessions();
|
|
864
|
-
sendJson(response, 200, { sessions: sessions.map(sessionDto) });
|
|
865
|
-
return true;
|
|
866
|
-
}
|
|
867
|
-
if (request.method === "GET" && url.pathname === "/api/web/history") {
|
|
868
|
-
const runtime = await getRuntime(getChannelKeyFromRequest(url));
|
|
869
|
-
const limit = Math.min(Math.max(Number(url.searchParams.get("limit") ?? 50) || 50, 1), 200);
|
|
870
|
-
const before = url.searchParams.get("before") ?? undefined;
|
|
871
|
-
sendJson(response, 200, webHistoryPayload(config, runtime.getRecords(), personaName, runtime.channelKey, { limit, before }));
|
|
872
|
-
return true;
|
|
873
|
-
}
|
|
874
|
-
if (request.method === "GET" && url.pathname === "/api/web/agent/settings") {
|
|
875
|
-
const runtime = await getRuntime(getChannelKeyFromRequest(url));
|
|
876
|
-
sendJson(response, 200, agentSettingsPayload(familiarAgent, runtime.channelKey, personaName));
|
|
877
|
-
return true;
|
|
878
|
-
}
|
|
879
|
-
if (request.method === "GET" && url.pathname === "/api/web/agent/models") {
|
|
880
|
-
sendJson(response, 200, getAgentModelsPayload());
|
|
881
|
-
return true;
|
|
882
|
-
}
|
|
883
|
-
if (request.method === "POST" && url.pathname === "/api/web/agent/models") {
|
|
884
|
-
const body = await readJsonBody(request);
|
|
885
|
-
if (!isObject(body)) {
|
|
886
|
-
sendJson(response, 400, { error: "body is required" });
|
|
887
|
-
return true;
|
|
888
|
-
}
|
|
889
|
-
const parsed = parseRequestedModel(body.model);
|
|
890
|
-
if (!parsed.ok) {
|
|
891
|
-
sendJson(response, 400, { error: parsed.error });
|
|
892
|
-
return true;
|
|
893
|
-
}
|
|
894
|
-
if (!Object.hasOwn(PROVIDER_DEFAULTS, parsed.ref.provider) &&
|
|
895
|
-
!getProviders().includes(parsed.ref.provider)) {
|
|
896
|
-
sendJson(response, 400, { error: `unsupported provider: ${parsed.ref.provider}` });
|
|
897
|
-
return true;
|
|
898
|
-
}
|
|
899
|
-
if (config.models.allow.includes(parsed.model) || loadAddedModels().includes(parsed.model)) {
|
|
900
|
-
sendJson(response, 200, getAgentModelsPayload());
|
|
901
|
-
return true;
|
|
902
|
-
}
|
|
903
|
-
await addModel(parsed.model);
|
|
904
|
-
sendJson(response, 200, getAgentModelsPayload());
|
|
905
|
-
return true;
|
|
906
|
-
}
|
|
907
|
-
if (request.method === "DELETE" && url.pathname === "/api/web/agent/models") {
|
|
908
|
-
const body = await readJsonBody(request);
|
|
909
|
-
if (!isObject(body)) {
|
|
910
|
-
sendJson(response, 400, { error: "body is required" });
|
|
911
|
-
return true;
|
|
912
|
-
}
|
|
913
|
-
const parsed = parseRequestedModel(body.model);
|
|
914
|
-
if (!parsed.ok) {
|
|
915
|
-
sendJson(response, 400, { error: parsed.error });
|
|
916
|
-
return true;
|
|
917
|
-
}
|
|
918
|
-
if (!loadAddedModels().includes(parsed.model)) {
|
|
919
|
-
sendJson(response, 400, { error: "model is not user-added" });
|
|
920
|
-
return true;
|
|
921
|
-
}
|
|
922
|
-
await removeModel(parsed.model);
|
|
923
|
-
sendJson(response, 200, getAgentModelsPayload());
|
|
924
|
-
return true;
|
|
925
|
-
}
|
|
926
|
-
if (request.method === "GET" && url.pathname === "/api/web/config") {
|
|
927
|
-
sendJson(response, 200, getConfigPayload());
|
|
928
|
-
return true;
|
|
929
|
-
}
|
|
930
|
-
if (request.method === "POST" && url.pathname === "/api/web/config") {
|
|
931
|
-
const body = await readJsonBody(request);
|
|
932
|
-
if (!isObject(body) || typeof body.key !== "string") {
|
|
933
|
-
sendJson(response, 400, { error: "key is required" });
|
|
934
|
-
return true;
|
|
935
|
-
}
|
|
936
|
-
if (!isConfigKey(body.key)) {
|
|
937
|
-
sendJson(response, 400, { error: `unknown config key: ${body.key}` });
|
|
938
|
-
return true;
|
|
939
|
-
}
|
|
940
|
-
const entry = CONFIG_REGISTRY[body.key];
|
|
941
|
-
try {
|
|
942
|
-
const validated = entry.validate(body.value, config);
|
|
943
|
-
entry.write(config, validated);
|
|
944
|
-
await setConfigOverride(body.key, validated);
|
|
945
|
-
await entry.apply?.({ config, discordDaemon });
|
|
946
|
-
}
|
|
947
|
-
catch (error) {
|
|
948
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
949
|
-
sendJson(response, 400, { error: message });
|
|
950
|
-
return true;
|
|
951
|
-
}
|
|
952
|
-
sendJson(response, 200, getConfigPayload());
|
|
953
|
-
return true;
|
|
954
|
-
}
|
|
955
|
-
if (request.method === "DELETE" && url.pathname === "/api/web/config") {
|
|
956
|
-
const body = await readJsonBody(request);
|
|
957
|
-
if (!isObject(body) || typeof body.key !== "string") {
|
|
958
|
-
sendJson(response, 400, { error: "key is required" });
|
|
959
|
-
return true;
|
|
960
|
-
}
|
|
961
|
-
if (!isConfigKey(body.key)) {
|
|
962
|
-
sendJson(response, 400, { error: `unknown config key: ${body.key}` });
|
|
963
|
-
return true;
|
|
964
|
-
}
|
|
965
|
-
const entry = CONFIG_REGISTRY[body.key];
|
|
966
|
-
try {
|
|
967
|
-
const fallback = getConfigDefault(body.key);
|
|
968
|
-
entry.write(config, fallback);
|
|
969
|
-
await clearConfigOverride(body.key);
|
|
970
|
-
await entry.apply?.({ config, discordDaemon });
|
|
971
|
-
}
|
|
972
|
-
catch (error) {
|
|
973
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
974
|
-
sendJson(response, 400, { error: message });
|
|
975
|
-
return true;
|
|
976
|
-
}
|
|
977
|
-
sendJson(response, 200, getConfigPayload());
|
|
978
|
-
return true;
|
|
979
|
-
}
|
|
980
|
-
if (request.method === "GET" && url.pathname === "/api/web/memes") {
|
|
981
|
-
try {
|
|
982
|
-
const markdown = await readFile(memeCatalogPath(config), "utf8");
|
|
983
|
-
sendJson(response, 200, { families: parseMemeCatalog(markdown) });
|
|
984
|
-
}
|
|
985
|
-
catch {
|
|
986
|
-
sendJson(response, 500, { error: "memes catalog unavailable" });
|
|
987
|
-
}
|
|
988
|
-
return true;
|
|
989
|
-
}
|
|
990
|
-
if (request.method === "POST" && url.pathname === "/api/web/send") {
|
|
991
|
-
const contentType = request.headers["content-type"] ?? "";
|
|
992
|
-
const isMultipart = Array.isArray(contentType)
|
|
993
|
-
? contentType.some((value) => value.includes("multipart/form-data"))
|
|
994
|
-
: contentType.includes("multipart/form-data");
|
|
995
|
-
const body = isMultipart ? await readMultipartBody(request, contentType) : await readJsonBody(request);
|
|
996
|
-
const runtime = await getRuntime(getChannelKeyFromRequest(url, body));
|
|
997
|
-
if (!isObject(body) || typeof body.text !== "string") {
|
|
998
|
-
sendJson(response, 400, { error: "text is required" });
|
|
999
|
-
return true;
|
|
1000
|
-
}
|
|
1001
|
-
if (!isMultipart && isObject(body) && Array.isArray(body.attachments) && body.attachments.length > 0) {
|
|
1002
|
-
sendJson(response, 400, { error: "attachments require multipart form data" });
|
|
1003
|
-
return true;
|
|
1004
|
-
}
|
|
1005
|
-
const rawAttachments = Array.isArray(body.attachments) ? body.attachments : [];
|
|
1006
|
-
const attachments = await materializeInboundAttachments(config, rawAttachments
|
|
1007
|
-
.filter((attachment) => isWebUploadAttachment(attachment))
|
|
1008
|
-
.map((attachment) => ({ ...attachment, source: "web" })));
|
|
1009
|
-
if (!body.text.trim() && attachments.length === 0) {
|
|
1010
|
-
sendJson(response, 400, { error: "text or attachment is required" });
|
|
1011
|
-
return true;
|
|
1012
|
-
}
|
|
1013
|
-
const id = messageId("user");
|
|
1014
|
-
const ts = Date.now();
|
|
1015
|
-
const input = {
|
|
1016
|
-
messageId: id,
|
|
1017
|
-
authorId: config.discord.ownerId,
|
|
1018
|
-
authorName: getContactNickname(WEB_USER_NAME),
|
|
1019
|
-
text: body.text,
|
|
1020
|
-
isBot: false,
|
|
1021
|
-
mentionedBot: true,
|
|
1022
|
-
remoteTimestamp: new Date(ts).toISOString(),
|
|
1023
|
-
checkpoint: { messageId: id },
|
|
1024
|
-
attachments,
|
|
1025
|
-
};
|
|
1026
|
-
await runtime.ingestInbound(input, { mode: "queue" });
|
|
1027
|
-
void drainJobs(runtime).catch((error) => console.error("Web job drain failed", error));
|
|
1028
|
-
sendJson(response, 200, { id, ts, channelKey: runtime.channelKey });
|
|
1029
|
-
return true;
|
|
1030
|
-
}
|
|
1031
|
-
if (request.method === "POST" && url.pathname === "/api/web/agent/settings") {
|
|
1032
|
-
const body = await readJsonBody(request);
|
|
1033
|
-
const runtime = await getRuntime(getChannelKeyFromRequest(url, body));
|
|
1034
|
-
if (!isObject(body)) {
|
|
1035
|
-
sendJson(response, 400, { error: "body is required" });
|
|
1036
|
-
return true;
|
|
1037
|
-
}
|
|
1038
|
-
try {
|
|
1039
|
-
if (typeof body.model === "string")
|
|
1040
|
-
await familiarAgent.setModel(runtime.channelKey, body.model);
|
|
1041
|
-
if (typeof body.thinking === "string")
|
|
1042
|
-
await familiarAgent.setThinkingLevel(runtime.channelKey, body.thinking);
|
|
1043
|
-
}
|
|
1044
|
-
catch (error) {
|
|
1045
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1046
|
-
sendJson(response, 400, { error: message });
|
|
1047
|
-
return true;
|
|
1048
|
-
}
|
|
1049
|
-
sendJson(response, 200, agentSettingsPayload(familiarAgent, runtime.channelKey, personaName));
|
|
1050
|
-
return true;
|
|
1051
|
-
}
|
|
1052
|
-
if (request.method === "POST" && url.pathname === "/api/web/agent/new") {
|
|
1053
|
-
const body = await readJsonBody(request);
|
|
1054
|
-
const runtime = await getRuntime(getChannelKeyFromRequest(url, body));
|
|
1055
|
-
await familiarAgent.reset(runtime.channelKey);
|
|
1056
|
-
await runtime.resetConversation("new conversation requested from web");
|
|
1057
|
-
publish({
|
|
1058
|
-
type: "status",
|
|
1059
|
-
channelKey: runtime.channelKey,
|
|
1060
|
-
kind: "idle",
|
|
1061
|
-
detail: "started fresh from web",
|
|
1062
|
-
});
|
|
1063
|
-
sendJson(response, 200, { ok: true });
|
|
1064
|
-
return true;
|
|
1065
|
-
}
|
|
1066
|
-
if (request.method === "POST" && url.pathname === "/api/web/control") {
|
|
1067
|
-
const body = await readJsonBody(request);
|
|
1068
|
-
const runtime = await getRuntime(getChannelKeyFromRequest(url, body));
|
|
1069
|
-
if (!isObject(body) || typeof body.command !== "string") {
|
|
1070
|
-
sendJson(response, 400, { error: "command is required" });
|
|
1071
|
-
return true;
|
|
1072
|
-
}
|
|
1073
|
-
if (config.web.authMode === "public-2fa" && body.command === "login") {
|
|
1074
|
-
const token = isObject(body.args) && typeof body.args.token === "string" ? body.args.token : "";
|
|
1075
|
-
if (!config.web.totpSecret || !verifyTotp(config.web.totpSecret, token)) {
|
|
1076
|
-
sendJson(response, 401, { ok: false, message: "Invalid TOTP token." });
|
|
1077
|
-
return true;
|
|
1078
|
-
}
|
|
1079
|
-
const sessionId = auth.createSession();
|
|
1080
|
-
sendJson(response, 200, { ok: true, message: "Authenticated." }, { "set-cookie": sessionCookie(sessionId) });
|
|
1081
|
-
return true;
|
|
1082
|
-
}
|
|
1083
|
-
const args = commandArgs(body.command, body.args);
|
|
1084
|
-
const input = {
|
|
1085
|
-
messageId: messageId("control"),
|
|
1086
|
-
authorId: config.discord.ownerId,
|
|
1087
|
-
authorName: getContactNickname(WEB_USER_NAME),
|
|
1088
|
-
text: `/${body.command}${args ? ` ${args}` : ""}`,
|
|
1089
|
-
isBot: false,
|
|
1090
|
-
mentionedBot: true,
|
|
1091
|
-
remoteTimestamp: new Date().toISOString(),
|
|
1092
|
-
};
|
|
1093
|
-
const control = runtime.parseControlCommand(input);
|
|
1094
|
-
if (!control) {
|
|
1095
|
-
sendJson(response, 400, { ok: false, message: "Unsupported command." });
|
|
1096
|
-
return true;
|
|
1097
|
-
}
|
|
1098
|
-
await runtime.noteControlCommand(input, control);
|
|
1099
|
-
const message = await applyControlCommand(runtime, control);
|
|
1100
|
-
await runtime.noteOutbound({ text: message, messageIds: [], control: control.command });
|
|
1101
|
-
sendJson(response, 200, { ok: true, message, channelKey: runtime.channelKey });
|
|
1102
|
-
return true;
|
|
1103
|
-
}
|
|
1104
|
-
sendJson(response, 404, { error: "not found" });
|
|
1105
|
-
return true;
|
|
1106
|
-
}
|
|
1107
|
-
catch (error) {
|
|
1108
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1109
|
-
sendJson(response, 500, { error: message });
|
|
1110
|
-
return true;
|
|
1111
|
-
}
|
|
1112
|
-
};
|
|
1113
|
-
await subscribeKnownRuntimes();
|
|
1114
|
-
const server = createServer((request, response) => {
|
|
1115
|
-
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
1116
|
-
void handleApi(request, response, url).then(async (handled) => {
|
|
1117
|
-
if (handled)
|
|
1118
|
-
return;
|
|
1119
|
-
if (await serveStatic(response, url.pathname))
|
|
1120
|
-
return;
|
|
1121
|
-
sendText(response, 404, "Not found");
|
|
1122
|
-
});
|
|
1123
|
-
});
|
|
1124
|
-
server.on("upgrade", (request, socket) => {
|
|
1125
|
-
const netSocket = socket;
|
|
1126
|
-
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "localhost"}`);
|
|
1127
|
-
if (url.pathname !== "/api/web/stream" || !auth.authorize(request, url.pathname)) {
|
|
1128
|
-
netSocket.write("HTTP/1.1 401 Unauthorized\r\n\r\n");
|
|
1129
|
-
netSocket.destroy();
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
const requestedChannelKey = url.searchParams.get("channelKey") || undefined;
|
|
1133
|
-
void getRuntime(requestedChannelKey)
|
|
1134
|
-
.then((runtime) => {
|
|
1135
|
-
if (netSocket.destroyed)
|
|
1136
|
-
return;
|
|
1137
|
-
if (!acceptWebSocket(request, netSocket))
|
|
1138
|
-
return;
|
|
1139
|
-
netSocket.setNoDelay(true);
|
|
1140
|
-
const client = { socket: netSocket, channelKey: runtime.channelKey, authed: false };
|
|
1141
|
-
clients.add(client);
|
|
1142
|
-
let frameBuffer = Buffer.alloc(0);
|
|
1143
|
-
netSocket.on("data", (chunk) => {
|
|
1144
|
-
try {
|
|
1145
|
-
frameBuffer = Buffer.concat([frameBuffer, chunk]);
|
|
1146
|
-
const decoded = decodeFrames(frameBuffer);
|
|
1147
|
-
frameBuffer = decoded.remaining;
|
|
1148
|
-
if (decoded.close)
|
|
1149
|
-
netSocket.destroy();
|
|
1150
|
-
for (const raw of decoded.messages) {
|
|
1151
|
-
const message = JSON.parse(raw);
|
|
1152
|
-
if (isObject(message) && message.type === "hello") {
|
|
1153
|
-
if (!client.channelKey)
|
|
1154
|
-
continue;
|
|
1155
|
-
replay(client, client.channelKey, typeof message.lastEventId === "string" ? message.lastEventId : null);
|
|
1156
|
-
}
|
|
1157
|
-
if (isObject(message) && message.type === "abort") {
|
|
1158
|
-
void getRuntime(client.channelKey).then(async (runtime) => {
|
|
1159
|
-
familiarAgent.requestSoftStop(runtime.channelKey);
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
catch (error) {
|
|
1165
|
-
console.error("WebSocket frame handling failed", error);
|
|
1166
|
-
netSocket.destroy();
|
|
1167
|
-
}
|
|
1168
|
-
});
|
|
1169
|
-
netSocket.on("close", () => clients.delete(client));
|
|
1170
|
-
netSocket.on("error", () => clients.delete(client));
|
|
1171
|
-
})
|
|
1172
|
-
.catch((error) => {
|
|
1173
|
-
console.error("WebSocket runtime lookup failed", error);
|
|
1174
|
-
if (!netSocket.destroyed) {
|
|
1175
|
-
netSocket.write("HTTP/1.1 503 Service Unavailable\r\n\r\n");
|
|
1176
|
-
netSocket.destroy();
|
|
1177
|
-
}
|
|
1178
|
-
});
|
|
1179
|
-
});
|
|
1180
|
-
await new Promise((resolveListen, rejectListen) => {
|
|
1181
|
-
server.once("error", rejectListen);
|
|
1182
|
-
server.listen(config.web.port, config.web.bindAddress, () => {
|
|
1183
|
-
server.off("error", rejectListen);
|
|
1184
|
-
resolveListen();
|
|
1185
|
-
});
|
|
1186
|
-
});
|
|
1187
|
-
console.log(`Web side-door listening on http://${config.web.bindAddress}:${config.web.port}`);
|
|
1188
|
-
return {
|
|
1189
|
-
server,
|
|
1190
|
-
async stop() {
|
|
1191
|
-
clearInterval(inFlightGcTimer);
|
|
1192
|
-
for (const client of clients)
|
|
1193
|
-
client.socket.destroy();
|
|
1194
|
-
clients.clear();
|
|
1195
|
-
for (const unsubscribe of runtimeSubscriptions.values())
|
|
1196
|
-
unsubscribe();
|
|
1197
|
-
runtimeSubscriptions.clear();
|
|
1198
|
-
await new Promise((resolveClose, rejectClose) => {
|
|
1199
|
-
server.close((error) => (error ? rejectClose(error) : resolveClose()));
|
|
1200
|
-
});
|
|
1201
|
-
},
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
export const __webTest = {
|
|
1205
|
-
memeCatalogPath,
|
|
1206
|
-
parseMemeCatalog,
|
|
1207
|
-
webHistoryPayload,
|
|
1208
|
-
webMessagesFromRecords,
|
|
1209
|
-
};
|