@qearlyao/familiar 0.2.2 → 0.2.4
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 +6 -14
- package/config.example.toml +1 -1
- package/dist/added-models.js +6 -15
- package/dist/agent-events.js +1 -3
- package/dist/agent.js +3 -4
- package/dist/browser-tools.js +15 -11
- package/dist/chat-log.js +3 -2
- package/dist/cli.js +2 -2
- package/dist/config-overrides.js +5 -14
- package/dist/config-registry.js +1 -4
- package/dist/config.js +45 -113
- package/dist/contact-note.js +2 -12
- package/dist/data-retention.js +1 -3
- package/dist/discord.js +72 -19
- package/dist/generated-media.js +3 -2
- package/dist/hot-reload.js +1 -3
- package/dist/image-gen.js +12 -51
- package/dist/inbound-attachments.js +64 -23
- package/dist/memory/diary/ambient-injector.js +1 -3
- package/dist/memory/diary/ambient.js +1 -3
- package/dist/memory/diary/chunks.js +1 -3
- package/dist/memory/diary/indexer.js +1 -3
- package/dist/memory/doctor.js +3 -8
- package/dist/memory/index/chunk-indexer.js +27 -6
- package/dist/memory/index/retrieval.js +1 -3
- package/dist/memory/index/store.js +47 -19
- package/dist/memory/lcm/backfill.js +19 -16
- package/dist/memory/lcm/context-transformer.js +17 -29
- package/dist/memory/lcm/context.js +10 -4
- package/dist/memory/lcm/eviction-score.js +25 -13
- package/dist/memory/lcm/indexer.js +1 -5
- package/dist/memory/lcm/normalize.js +22 -1
- package/dist/memory/lcm/store.js +27 -24
- package/dist/memory/operator.js +3 -31
- package/dist/memory/service.js +1 -3
- package/dist/memory/tools.js +0 -4
- package/dist/memory/util.js +6 -0
- package/dist/models.js +3 -0
- package/dist/persona.js +3 -15
- package/dist/runtime.js +12 -23
- package/dist/scheduler.js +15 -49
- package/dist/service.js +39 -27
- package/dist/settings.js +7 -32
- package/dist/silent-marker.js +64 -0
- package/dist/tts.js +0 -6
- package/dist/util/fs.js +41 -0
- package/dist/util/guards.js +8 -0
- package/dist/util/image-mime.js +31 -0
- package/dist/util/time.js +29 -0
- package/dist/web-auth.js +4 -1
- package/dist/web-static.js +36 -1
- package/dist/web-tools.js +8 -5
- package/dist/web.js +253 -69
- package/npm-shrinkwrap.json +5139 -0
- package/package.json +5 -4
- package/web/dist/assets/index-B23WT77N.js +63 -0
- package/web/dist/assets/index-D3MotFzN.css +2 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BPZQbZh5.js +0 -61
- package/web/dist/assets/index-CcQ13VAY.css +0 -2
package/dist/web.js
CHANGED
|
@@ -12,6 +12,7 @@ import { publicAttachmentPath } from "./generated-media.js";
|
|
|
12
12
|
import { materializeInboundAttachments } from "./inbound-attachments.js";
|
|
13
13
|
import { PROVIDER_DEFAULTS, parseModelRef, supportedThinkingLevels } from "./models.js";
|
|
14
14
|
import { loadPersona, parsePersonaName } from "./persona.js";
|
|
15
|
+
import { consumeSilentDelta, createSilentFilterState, finalizeSilentFilter, parseAgentReply } from "./silent-marker.js";
|
|
15
16
|
import { createAuth, sessionCookie, verifyTotp } from "./web-auth.js";
|
|
16
17
|
import { acceptWebSocket, decodeFrames, encodeFrame, replayEvents } from "./web-events.js";
|
|
17
18
|
import { isObject, readJsonBody, sendJson, sendText } from "./web-http.js";
|
|
@@ -147,6 +148,8 @@ function webAttachments(config, attachments) {
|
|
|
147
148
|
}));
|
|
148
149
|
}
|
|
149
150
|
function attachmentDerivedText(attachment) {
|
|
151
|
+
if (attachment.derived?.text?.label === "preview")
|
|
152
|
+
return undefined;
|
|
150
153
|
return attachment.derived?.text?.text;
|
|
151
154
|
}
|
|
152
155
|
function toolError(result) {
|
|
@@ -215,23 +218,79 @@ function mergeToolEvent(existing, patch) {
|
|
|
215
218
|
startedAt: existing?.startedAt ?? patch.startedAt,
|
|
216
219
|
};
|
|
217
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
|
+
}
|
|
218
268
|
function applyStoredAgentEventToMessage(message, record, options) {
|
|
219
269
|
const event = record.event;
|
|
220
270
|
const ts = toUnixMs(record.ts);
|
|
271
|
+
message.steps ??= [];
|
|
272
|
+
const steps = message.steps;
|
|
221
273
|
if (event.type === "message_update") {
|
|
222
274
|
const assistantEvent = event.assistantMessageEvent;
|
|
223
275
|
if (assistantEvent.type === "text_delta") {
|
|
276
|
+
appendDeltaStep(steps, message.id, "text", assistantEvent.delta, ts);
|
|
224
277
|
if (options.applyTextDeltas)
|
|
225
278
|
message.text += assistantEvent.delta;
|
|
226
279
|
}
|
|
227
|
-
if (assistantEvent.type === "thinking_delta"
|
|
228
|
-
|
|
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}`;
|
|
229
284
|
}
|
|
230
285
|
}
|
|
231
|
-
if (event.type === "message_end"
|
|
232
|
-
|
|
286
|
+
if (event.type === "message_end") {
|
|
287
|
+
closeOpenContentSteps(steps, ts);
|
|
288
|
+
if (event.usage)
|
|
289
|
+
message.usage = event.usage;
|
|
290
|
+
}
|
|
233
291
|
const tool = toolFromStoredAgentEvent(event, ts);
|
|
234
292
|
if (tool) {
|
|
293
|
+
upsertToolStep(steps, tool, ts);
|
|
235
294
|
const tools = message.tools ?? [];
|
|
236
295
|
const index = tools.findIndex((candidate) => candidate.id === tool.id);
|
|
237
296
|
if (index >= 0) {
|
|
@@ -243,6 +302,29 @@ function applyStoredAgentEventToMessage(message, record, options) {
|
|
|
243
302
|
message.tools = tools;
|
|
244
303
|
}
|
|
245
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
|
+
}
|
|
246
328
|
function webMessagesFromRecords(config, records, assistantName) {
|
|
247
329
|
const messages = [];
|
|
248
330
|
const messagesById = new Map();
|
|
@@ -255,7 +337,7 @@ function webMessagesFromRecords(config, records, assistantName) {
|
|
|
255
337
|
const pending = pendingAgentEvents.get(message.id) ?? [];
|
|
256
338
|
for (const pendingRecord of pending) {
|
|
257
339
|
applyStoredAgentEventToMessage(message, pendingRecord, {
|
|
258
|
-
applyTextDeltas: !message.text,
|
|
340
|
+
applyTextDeltas: !message.text && !message.silent,
|
|
259
341
|
applyThinkingDeltas: !message.thinking,
|
|
260
342
|
});
|
|
261
343
|
}
|
|
@@ -265,7 +347,7 @@ function webMessagesFromRecords(config, records, assistantName) {
|
|
|
265
347
|
const existing = messagesById.get(record.messageId);
|
|
266
348
|
if (existing) {
|
|
267
349
|
applyStoredAgentEventToMessage(existing, record, {
|
|
268
|
-
applyTextDeltas:
|
|
350
|
+
applyTextDeltas: !existing.silent,
|
|
269
351
|
applyThinkingDeltas: true,
|
|
270
352
|
});
|
|
271
353
|
}
|
|
@@ -276,8 +358,17 @@ function webMessagesFromRecords(config, records, assistantName) {
|
|
|
276
358
|
}
|
|
277
359
|
}
|
|
278
360
|
}
|
|
361
|
+
for (const message of messages)
|
|
362
|
+
ensureFallbackSteps(message);
|
|
279
363
|
return messages;
|
|
280
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
|
+
}
|
|
281
372
|
function webMessageFromRecord(config, record, assistantName) {
|
|
282
373
|
if (!isUserVisibleRuntimeRecord(record))
|
|
283
374
|
return undefined;
|
|
@@ -304,6 +395,7 @@ function webMessageFromRecord(config, record, assistantName) {
|
|
|
304
395
|
attachments: webAttachments(config, record.attachments),
|
|
305
396
|
thinking: record.thinking,
|
|
306
397
|
thinkingMs: record.thinkingMs,
|
|
398
|
+
silent: record.silent || undefined,
|
|
307
399
|
ts: toUnixMs(record.ts),
|
|
308
400
|
};
|
|
309
401
|
}
|
|
@@ -363,7 +455,32 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
363
455
|
const clients = new Set();
|
|
364
456
|
const eventsByChannel = new Map();
|
|
365
457
|
const runtimeSubscriptions = new Map();
|
|
366
|
-
const
|
|
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?.();
|
|
367
484
|
const publish = (event) => {
|
|
368
485
|
const fullEvent = { ...event, eventId: eventId(), ts: event.ts ?? Date.now() };
|
|
369
486
|
const events = eventsByChannel.get(fullEvent.channelKey ?? "") ?? [];
|
|
@@ -388,32 +505,83 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
388
505
|
};
|
|
389
506
|
const publishDelta = (channelKey, messageIdValue, part, text, ts) => publish({ type: "delta", channelKey, messageId: messageIdValue, part, content: text, text, ts });
|
|
390
507
|
const publishStoredAgentEvent = (channelKey, messageIdValue, storedEvent, ts) => {
|
|
508
|
+
touchInFlight(messageIdValue);
|
|
391
509
|
if (storedEvent.type === "message_start" && storedEvent.role === "assistant") {
|
|
392
|
-
|
|
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;
|
|
393
523
|
publish({
|
|
394
524
|
type: "message_started",
|
|
395
525
|
channelKey,
|
|
396
526
|
messageId: messageIdValue,
|
|
397
527
|
role: "assistant",
|
|
398
528
|
who: personaName,
|
|
399
|
-
ts,
|
|
529
|
+
ts: startTs,
|
|
400
530
|
});
|
|
401
|
-
|
|
531
|
+
return true;
|
|
532
|
+
};
|
|
402
533
|
if (storedEvent.type === "message_update") {
|
|
403
534
|
const assistantEvent = storedEvent.assistantMessageEvent;
|
|
404
535
|
if (assistantEvent.type === "thinking_delta") {
|
|
536
|
+
startedSilentMessage();
|
|
405
537
|
publishDelta(channelKey, messageIdValue, "thinking", assistantEvent.delta, ts);
|
|
406
538
|
}
|
|
407
539
|
if (assistantEvent.type === "text_delta") {
|
|
408
|
-
|
|
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
|
+
}
|
|
409
552
|
}
|
|
410
553
|
}
|
|
554
|
+
if (storedEvent.type === "tool_execution_start") {
|
|
555
|
+
startedSilentMessage();
|
|
556
|
+
}
|
|
411
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
|
+
}
|
|
412
579
|
publish({
|
|
413
580
|
type: "message_completed",
|
|
414
581
|
channelKey,
|
|
415
582
|
messageId: messageIdValue,
|
|
416
583
|
usage: storedEvent.usage,
|
|
584
|
+
silent: silent || undefined,
|
|
417
585
|
ts,
|
|
418
586
|
});
|
|
419
587
|
}
|
|
@@ -439,6 +607,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
439
607
|
type: "message_completed",
|
|
440
608
|
channelKey: runtime.channelKey,
|
|
441
609
|
messageId: record.messageId,
|
|
610
|
+
attachments: webAttachments(config, record.attachments),
|
|
442
611
|
ts: toUnixMs(record.ts),
|
|
443
612
|
});
|
|
444
613
|
}
|
|
@@ -450,24 +619,28 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
450
619
|
messageId: outboundId,
|
|
451
620
|
thinkingMs: record.thinkingMs,
|
|
452
621
|
attachments: webAttachments(config, record.attachments),
|
|
622
|
+
silent: record.silent || undefined,
|
|
453
623
|
ts: toUnixMs(record.ts),
|
|
454
624
|
};
|
|
455
|
-
if (
|
|
625
|
+
if (inFlightMessages.get(outboundId)?.locallyStreamed) {
|
|
626
|
+
inFlightMessages.delete(outboundId);
|
|
456
627
|
publish(completion);
|
|
457
628
|
return;
|
|
458
629
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
+
}
|
|
471
644
|
publish(completion);
|
|
472
645
|
}
|
|
473
646
|
});
|
|
@@ -564,7 +737,9 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
564
737
|
finally {
|
|
565
738
|
await recorder.flush();
|
|
566
739
|
}
|
|
567
|
-
|
|
740
|
+
const parsed = parseAgentReply(reply.text);
|
|
741
|
+
const finalText = parsed.silent ? "" : reply.text;
|
|
742
|
+
if (!started && !parsed.silent) {
|
|
568
743
|
publish({
|
|
569
744
|
type: "message_started",
|
|
570
745
|
channelKey: runtime.channelKey,
|
|
@@ -572,7 +747,9 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
572
747
|
role: "assistant",
|
|
573
748
|
who: personaName,
|
|
574
749
|
});
|
|
575
|
-
|
|
750
|
+
if (finalText) {
|
|
751
|
+
publishDelta(runtime.channelKey, assistantMessageId, "text", finalText);
|
|
752
|
+
}
|
|
576
753
|
}
|
|
577
754
|
const thinkingMs = thinkingDurationMs(summary);
|
|
578
755
|
publish({
|
|
@@ -581,14 +758,17 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
581
758
|
messageId: assistantMessageId,
|
|
582
759
|
thinkingMs,
|
|
583
760
|
attachments: webAttachments(config, reply.attachments),
|
|
761
|
+
silent: parsed.silent || undefined,
|
|
584
762
|
});
|
|
585
|
-
|
|
763
|
+
const entry = getOrCreateInFlight(assistantMessageId);
|
|
764
|
+
entry.locallyStreamed = true;
|
|
586
765
|
return {
|
|
587
|
-
text:
|
|
766
|
+
text: finalText,
|
|
588
767
|
messageId: assistantMessageId,
|
|
589
768
|
thinking: summary.thinking,
|
|
590
769
|
thinkingMs,
|
|
591
770
|
attachments: reply.attachments,
|
|
771
|
+
silent: parsed.silent,
|
|
592
772
|
};
|
|
593
773
|
};
|
|
594
774
|
const drainJobs = async (runtime) => {
|
|
@@ -611,6 +791,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
611
791
|
attachments: reply.attachments,
|
|
612
792
|
thinking: reply.thinking,
|
|
613
793
|
thinkingMs: reply.thinkingMs,
|
|
794
|
+
silent: reply.silent,
|
|
614
795
|
replyToMessageId: dispatch.triggerMessageId,
|
|
615
796
|
});
|
|
616
797
|
}
|
|
@@ -672,7 +853,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
672
853
|
}
|
|
673
854
|
try {
|
|
674
855
|
if (request.method === "GET" && url.pathname.startsWith("/api/web/attachments/")) {
|
|
675
|
-
return serveAttachment(config, response, url.pathname);
|
|
856
|
+
return serveAttachment(config, response, url.pathname, request.headers.range);
|
|
676
857
|
}
|
|
677
858
|
if (request.method === "GET" && url.pathname === "/api/web/auth/mode") {
|
|
678
859
|
sendJson(response, 200, { mode: config.web.authMode, personaName });
|
|
@@ -686,12 +867,8 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
686
867
|
if (request.method === "GET" && url.pathname === "/api/web/history") {
|
|
687
868
|
const runtime = await getRuntime(getChannelKeyFromRequest(url));
|
|
688
869
|
const limit = Math.min(Math.max(Number(url.searchParams.get("limit") ?? 50) || 50, 1), 200);
|
|
689
|
-
const before = url.searchParams.get("before");
|
|
690
|
-
|
|
691
|
-
const end = before ? messages.findIndex((message) => message.id === before) : messages.length;
|
|
692
|
-
const safeEnd = end >= 0 ? end : messages.length;
|
|
693
|
-
const page = messages.slice(Math.max(0, safeEnd - limit), safeEnd);
|
|
694
|
-
sendJson(response, 200, { messages: page, hasMore: safeEnd - limit > 0, channelKey: runtime.channelKey });
|
|
870
|
+
const before = url.searchParams.get("before") ?? undefined;
|
|
871
|
+
sendJson(response, 200, webHistoryPayload(config, runtime.getRecords(), personaName, runtime.channelKey, { limit, before }));
|
|
695
872
|
return true;
|
|
696
873
|
}
|
|
697
874
|
if (request.method === "GET" && url.pathname === "/api/web/agent/settings") {
|
|
@@ -952,49 +1129,53 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
952
1129
|
netSocket.destroy();
|
|
953
1130
|
return;
|
|
954
1131
|
}
|
|
955
|
-
if (!acceptWebSocket(request, netSocket))
|
|
956
|
-
return;
|
|
957
|
-
netSocket.setNoDelay(true);
|
|
958
1132
|
const requestedChannelKey = url.searchParams.get("channelKey") || undefined;
|
|
959
|
-
const client = { socket: netSocket, channelKey: requestedChannelKey, authed: false };
|
|
960
|
-
clients.add(client);
|
|
961
|
-
let frameBuffer = Buffer.alloc(0);
|
|
962
1133
|
void getRuntime(requestedChannelKey)
|
|
963
1134
|
.then((runtime) => {
|
|
964
|
-
|
|
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));
|
|
965
1171
|
})
|
|
966
1172
|
.catch((error) => {
|
|
967
1173
|
console.error("WebSocket runtime lookup failed", error);
|
|
968
|
-
netSocket.
|
|
969
|
-
|
|
970
|
-
netSocket.on("data", (chunk) => {
|
|
971
|
-
try {
|
|
972
|
-
frameBuffer = Buffer.concat([frameBuffer, chunk]);
|
|
973
|
-
const decoded = decodeFrames(frameBuffer);
|
|
974
|
-
frameBuffer = decoded.remaining;
|
|
975
|
-
if (decoded.close)
|
|
976
|
-
netSocket.destroy();
|
|
977
|
-
for (const raw of decoded.messages) {
|
|
978
|
-
const message = JSON.parse(raw);
|
|
979
|
-
if (isObject(message) && message.type === "hello") {
|
|
980
|
-
if (!client.channelKey)
|
|
981
|
-
continue;
|
|
982
|
-
replay(client, client.channelKey, typeof message.lastEventId === "string" ? message.lastEventId : null);
|
|
983
|
-
}
|
|
984
|
-
if (isObject(message) && message.type === "abort") {
|
|
985
|
-
void getRuntime(client.channelKey).then(async (runtime) => {
|
|
986
|
-
familiarAgent.requestSoftStop(runtime.channelKey);
|
|
987
|
-
});
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
catch (error) {
|
|
992
|
-
console.error("WebSocket frame handling failed", error);
|
|
1174
|
+
if (!netSocket.destroyed) {
|
|
1175
|
+
netSocket.write("HTTP/1.1 503 Service Unavailable\r\n\r\n");
|
|
993
1176
|
netSocket.destroy();
|
|
994
1177
|
}
|
|
995
1178
|
});
|
|
996
|
-
netSocket.on("close", () => clients.delete(client));
|
|
997
|
-
netSocket.on("error", () => clients.delete(client));
|
|
998
1179
|
});
|
|
999
1180
|
await new Promise((resolveListen, rejectListen) => {
|
|
1000
1181
|
server.once("error", rejectListen);
|
|
@@ -1007,6 +1188,7 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
1007
1188
|
return {
|
|
1008
1189
|
server,
|
|
1009
1190
|
async stop() {
|
|
1191
|
+
clearInterval(inFlightGcTimer);
|
|
1010
1192
|
for (const client of clients)
|
|
1011
1193
|
client.socket.destroy();
|
|
1012
1194
|
clients.clear();
|
|
@@ -1022,4 +1204,6 @@ export async function startWebDaemon(config, familiarAgent, discordDaemon, optio
|
|
|
1022
1204
|
export const __webTest = {
|
|
1023
1205
|
memeCatalogPath,
|
|
1024
1206
|
parseMemeCatalog,
|
|
1207
|
+
webHistoryPayload,
|
|
1208
|
+
webMessagesFromRecords,
|
|
1025
1209
|
};
|