@camstack/addon-ai 0.4.6 → 0.4.8
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/dist/addon.js +554 -68
- package/dist/addon.mjs +555 -73
- package/dist/{ensure-llama-server-COC6iveo.mjs → ensure-llama-server-D_uzfSep.mjs} +0 -0
- package/dist/index.js +4 -0
- package/dist/index.mjs +4 -4
- package/dist/{token-BDEeGrwn.mjs → token-rOc1oWJ0.mjs} +1 -1
- package/dist/{usage-store-RiVXP_ma.mjs → usage-store-D_bmdGGd.mjs} +1 -1
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import * as path$1 from "node:path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { connect, createServer } from "node:net";
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import { createReadStream, existsSync, statSync } from "node:fs";
|
|
@@ -41,7 +41,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
41
41
|
}) : target, mod));
|
|
42
42
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
43
43
|
//#endregion
|
|
44
|
-
//#region ../types/dist/event-category-
|
|
44
|
+
//#region ../types/dist/event-category-C0lyLd5U.mjs
|
|
45
45
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
46
46
|
EventCategory["SystemBoot"] = "system.boot";
|
|
47
47
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -58,9 +58,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
58
58
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
59
59
|
/**
|
|
60
60
|
* A newer addon or server-root package version was found by the
|
|
61
|
-
* authoritative registry check. Emitted once
|
|
62
|
-
* `(
|
|
63
|
-
*
|
|
61
|
+
* authoritative registry check. Emitted once when any observed
|
|
62
|
+
* `latestVersion` changes (or a package/node first appears behind);
|
|
63
|
+
* the payload carries the full currently-available list. Repeated
|
|
64
|
+
* polling of the same latests is silent.
|
|
64
65
|
*/
|
|
65
66
|
EventCategory["UpdateAvailable"] = "update.available";
|
|
66
67
|
/**
|
|
@@ -7780,6 +7781,10 @@ var RecordingBandSchema = object({
|
|
|
7780
7781
|
preBufferSec: number$1().min(0).optional(),
|
|
7781
7782
|
postBufferSec: number$1().min(0).optional()
|
|
7782
7783
|
});
|
|
7784
|
+
({
|
|
7785
|
+
preBufferSec: 10,
|
|
7786
|
+
postBufferSec: 30
|
|
7787
|
+
}).postBufferSec * 1e3;
|
|
7783
7788
|
/**
|
|
7784
7789
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7785
7790
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -14558,6 +14563,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14558
14563
|
"alarm-disarmed",
|
|
14559
14564
|
"alarm-arming",
|
|
14560
14565
|
"alarm-arm-refused",
|
|
14566
|
+
"addon-updated",
|
|
14567
|
+
"server-updated",
|
|
14568
|
+
"export-completed",
|
|
14561
14569
|
"camera-online",
|
|
14562
14570
|
"camera-offline",
|
|
14563
14571
|
"camera-disabled",
|
|
@@ -16451,13 +16459,19 @@ var RetrainStatusSchema = _enum([
|
|
|
16451
16459
|
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16452
16460
|
*
|
|
16453
16461
|
* `debug` does NOT pin; it is attention, not durability.
|
|
16462
|
+
*
|
|
16463
|
+
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
16464
|
+
* A favourited track is skipped by retention the same way `staging` is, but
|
|
16465
|
+
* it does not enter `none|staging|trained` and has no staging budget.
|
|
16454
16466
|
*/
|
|
16455
16467
|
var TrackFlagFields = {
|
|
16456
16468
|
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16457
16469
|
* `'staging'`. */
|
|
16458
16470
|
markForTrain: boolean().optional(),
|
|
16459
16471
|
/** Operator marked this track for diagnostic attention. */
|
|
16460
|
-
debug: boolean().optional()
|
|
16472
|
+
debug: boolean().optional(),
|
|
16473
|
+
/** Operator favourited this track. Pins it against pruning. */
|
|
16474
|
+
favourited: boolean().optional()
|
|
16461
16475
|
};
|
|
16462
16476
|
/**
|
|
16463
16477
|
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
@@ -16482,6 +16496,7 @@ var TrackFlagsSchema = object({
|
|
|
16482
16496
|
trackId: string(),
|
|
16483
16497
|
markForTrain: boolean(),
|
|
16484
16498
|
debug: boolean(),
|
|
16499
|
+
favourited: boolean(),
|
|
16485
16500
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16486
16501
|
* a track row) because this shape is only ever produced by the write body,
|
|
16487
16502
|
* which always knows it — and a surface that has just written needs to render
|
|
@@ -19436,7 +19451,14 @@ targets: array(object({
|
|
|
19436
19451
|
"sleeping",
|
|
19437
19452
|
"unreachable",
|
|
19438
19453
|
"waking"
|
|
19439
|
-
]).nullable()
|
|
19454
|
+
]).nullable(),
|
|
19455
|
+
/** A battery camera (whatever its current state). An AWAKE battery
|
|
19456
|
+
* camera is deliberately NOT recaptured on the poll cadence — every
|
|
19457
|
+
* capture is a camera hit that would keep it out of sleep — so its
|
|
19458
|
+
* cached frame legitimately ages past the currency ceiling while
|
|
19459
|
+
* nothing is streaming. A surface must keep painting it (a fresh
|
|
19460
|
+
* frame is captured at each wake), not blank to "unavailable". */
|
|
19461
|
+
battery: boolean()
|
|
19440
19462
|
})));
|
|
19441
19463
|
/**
|
|
19442
19464
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
@@ -20127,8 +20149,28 @@ var ClipSchema = object({
|
|
|
20127
20149
|
startMs: number$1(),
|
|
20128
20150
|
endMs: number$1()
|
|
20129
20151
|
}),
|
|
20130
|
-
/**
|
|
20131
|
-
thumbnail
|
|
20152
|
+
/**
|
|
20153
|
+
* Lazy thumbnail URL, never inlined.
|
|
20154
|
+
*
|
|
20155
|
+
* Recording-derived clips (events-mode keep-window, and the prepared
|
|
20156
|
+
* continuous event+fragment visit) MUST use the snapshot of the **main
|
|
20157
|
+
* event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
|
|
20158
|
+
* of the event that owns `kind` (object > motion > audio). Do not extract
|
|
20159
|
+
* a keyframe from the recorded segments. Other providers (onboard, HKSV)
|
|
20160
|
+
* mint their own stills.
|
|
20161
|
+
*/
|
|
20162
|
+
thumbnail: string().optional(),
|
|
20163
|
+
/** Analytics event ids that overlap this visit. Empty on footage-only clips.
|
|
20164
|
+
* The default provider's visit grain puts many motion heartbeats on one clip
|
|
20165
|
+
* instead of minting one clip per marker. */
|
|
20166
|
+
eventIds: array(string()).optional(),
|
|
20167
|
+
/** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
|
|
20168
|
+
* than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
|
|
20169
|
+
* bar keeps showing them via `recording.getAvailability`. */
|
|
20170
|
+
holes: array(object({
|
|
20171
|
+
startMs: number$1(),
|
|
20172
|
+
endMs: number$1()
|
|
20173
|
+
})).optional()
|
|
20132
20174
|
});
|
|
20133
20175
|
var ClipPlaybackSchema = object({
|
|
20134
20176
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -24726,7 +24768,9 @@ var ExportOptionsSchema = object({
|
|
|
24726
24768
|
includeAudio: boolean(),
|
|
24727
24769
|
maxLifeMs: number$1().int().positive(),
|
|
24728
24770
|
deleteAfterDownload: boolean(),
|
|
24729
|
-
title: string().max(200).optional()
|
|
24771
|
+
title: string().max(200).optional(),
|
|
24772
|
+
/** Notification-output target ids to ping when this export becomes ready. */
|
|
24773
|
+
notifyTargetIds: array(string().min(1)).max(20).optional()
|
|
24730
24774
|
}).superRefine((v, ctx) => {
|
|
24731
24775
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
24732
24776
|
code: ZodIssueCode.custom,
|
|
@@ -24785,10 +24829,18 @@ var ExportBytesSchema = object({
|
|
|
24785
24829
|
});
|
|
24786
24830
|
method(object({
|
|
24787
24831
|
deviceId: number$1(),
|
|
24788
|
-
|
|
24832
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
24833
|
+
profile: string().optional(),
|
|
24834
|
+
profiles: array(string()).min(1).optional(),
|
|
24789
24835
|
fromMs: number$1(),
|
|
24790
24836
|
toMs: number$1(),
|
|
24791
24837
|
options: ExportOptionsSchema
|
|
24838
|
+
}).superRefine((v, ctx) => {
|
|
24839
|
+
if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
|
|
24840
|
+
code: ZodIssueCode.custom,
|
|
24841
|
+
message: "pass profiles[] (min 1) or legacy profile",
|
|
24842
|
+
path: ["profiles"]
|
|
24843
|
+
});
|
|
24792
24844
|
}), ExportRecordSchema, {
|
|
24793
24845
|
kind: "mutation",
|
|
24794
24846
|
auth: "protected"
|
|
@@ -58975,7 +59027,7 @@ var require_get_vercel_oidc_token = /* @__PURE__ */ __commonJSMin(((exports, mod
|
|
|
58975
59027
|
err = error;
|
|
58976
59028
|
}
|
|
58977
59029
|
try {
|
|
58978
|
-
const [{ getTokenPayload, isExpired }, { refreshToken }] = await Promise.all([await Promise.resolve().then(() => /* @__PURE__ */ __toESM(require_token_util())), await import("./token-
|
|
59030
|
+
const [{ getTokenPayload, isExpired }, { refreshToken }] = await Promise.all([await Promise.resolve().then(() => /* @__PURE__ */ __toESM(require_token_util())), await import("./token-rOc1oWJ0.mjs").then((m) => /* @__PURE__ */ __toESM(m.default))]);
|
|
58979
59031
|
if (!token || isExpired(getTokenPayload(token), options?.expirationBufferMs)) {
|
|
58980
59032
|
await refreshToken(options);
|
|
58981
59033
|
token = getVercelOidcTokenSync();
|
|
@@ -69716,7 +69768,7 @@ async function* chunksFrom(stream) {
|
|
|
69716
69768
|
var DEFAULTS_COLLECTION = "ai:llm-defaults";
|
|
69717
69769
|
/** KV JSON-blob shape — a single `data` column routes through the settings
|
|
69718
69770
|
* backend's canonical key/value path (id TEXT PK, data TEXT). */
|
|
69719
|
-
var KV_BLOB_COLUMNS$
|
|
69771
|
+
var KV_BLOB_COLUMNS$2 = [{
|
|
69720
69772
|
name: "data",
|
|
69721
69773
|
type: "TEXT",
|
|
69722
69774
|
notNull: true
|
|
@@ -69732,7 +69784,7 @@ var DefaultsStore = class {
|
|
|
69732
69784
|
/** Declare the backing collection before any read/write — the SQLite
|
|
69733
69785
|
* settings backend rejects undeclared collections (fail-fast). */
|
|
69734
69786
|
async init() {
|
|
69735
|
-
await this.store.declareCollection(DEFAULTS_COLLECTION, KV_BLOB_COLUMNS$
|
|
69787
|
+
await this.store.declareCollection(DEFAULTS_COLLECTION, KV_BLOB_COLUMNS$2);
|
|
69736
69788
|
}
|
|
69737
69789
|
async list() {
|
|
69738
69790
|
const rows = await this.store.query(DEFAULTS_COLLECTION);
|
|
@@ -69784,7 +69836,7 @@ var SEED_PROFILE_ID = "seed-ollama-lan";
|
|
|
69784
69836
|
var SEED_MARKER_ID = "seededDefaults";
|
|
69785
69837
|
/** KV JSON-blob shape — a single `data` column routes the row through the
|
|
69786
69838
|
* settings backend's canonical key/value path (id TEXT PK, data TEXT). */
|
|
69787
|
-
var KV_BLOB_COLUMNS = [{
|
|
69839
|
+
var KV_BLOB_COLUMNS$1 = [{
|
|
69788
69840
|
name: "data",
|
|
69789
69841
|
type: "TEXT",
|
|
69790
69842
|
notNull: true
|
|
@@ -69809,8 +69861,8 @@ var ProfileStore = class {
|
|
|
69809
69861
|
* settings backend rejects undeclared collections (fail-fast), so this
|
|
69810
69862
|
* MUST run before `ensureSeeded`/`list`/`upsert` — mirrors UsageStore.init. */
|
|
69811
69863
|
async init() {
|
|
69812
|
-
await this.store.declareCollection(PROFILES_COLLECTION, KV_BLOB_COLUMNS);
|
|
69813
|
-
await this.store.declareCollection(META_COLLECTION, KV_BLOB_COLUMNS);
|
|
69864
|
+
await this.store.declareCollection(PROFILES_COLLECTION, KV_BLOB_COLUMNS$1);
|
|
69865
|
+
await this.store.declareCollection(META_COLLECTION, KV_BLOB_COLUMNS$1);
|
|
69814
69866
|
}
|
|
69815
69867
|
/** RAW rows (apiKey present); invalid rows dropped. */
|
|
69816
69868
|
async list() {
|
|
@@ -72371,6 +72423,178 @@ function createRuntimeClient(api) {
|
|
|
72371
72423
|
};
|
|
72372
72424
|
}
|
|
72373
72425
|
//#endregion
|
|
72426
|
+
//#region src/test-chat/chat-store.ts
|
|
72427
|
+
/**
|
|
72428
|
+
* Per-user persistent AI chats, over the same settings-store port as
|
|
72429
|
+
* profiles/usage.
|
|
72430
|
+
*
|
|
72431
|
+
* Collection `ai:chats`; row id `ai:chats/<userId>/<conversationId>` is
|
|
72432
|
+
* spelled as `${userId}/${conversationId}` (the collection already carries
|
|
72433
|
+
* the `ai:chats` prefix). Isolation is structural: every read/write takes
|
|
72434
|
+
* `userId` and refuses a row that belongs to someone else.
|
|
72435
|
+
*
|
|
72436
|
+
* @durable class=audit owner=ai
|
|
72437
|
+
* write="an operator sends a persistent test-chat turn — one row per conversation, messages[] replayed into the model each turn"
|
|
72438
|
+
* retention="operator knob: max-age days and/or max-count per user (addon global settings). Pruned on write and on the addon's existing 24h usage sweep."
|
|
72439
|
+
*/
|
|
72440
|
+
var CHATS_COLLECTION = "ai:chats";
|
|
72441
|
+
var TITLE_MAX_CHARS = 80;
|
|
72442
|
+
var KV_BLOB_COLUMNS = [{
|
|
72443
|
+
name: "data",
|
|
72444
|
+
type: "TEXT",
|
|
72445
|
+
notNull: true
|
|
72446
|
+
}];
|
|
72447
|
+
function chatsRowId(userId, conversationId) {
|
|
72448
|
+
return `${userId}/${conversationId}`;
|
|
72449
|
+
}
|
|
72450
|
+
function titleFromFirstUserMessage(content) {
|
|
72451
|
+
const collapsed = content.trim().replace(/\s+/g, " ");
|
|
72452
|
+
if (collapsed.length <= TITLE_MAX_CHARS) return collapsed;
|
|
72453
|
+
return collapsed.slice(0, TITLE_MAX_CHARS).trimEnd();
|
|
72454
|
+
}
|
|
72455
|
+
var AttachmentRefSchema = discriminatedUnion("kind", [object({
|
|
72456
|
+
kind: literal("snapshot"),
|
|
72457
|
+
deviceId: number$1().int().positive()
|
|
72458
|
+
}), object({
|
|
72459
|
+
kind: literal("track"),
|
|
72460
|
+
trackId: string().min(1),
|
|
72461
|
+
deviceId: number$1().int().positive()
|
|
72462
|
+
})]);
|
|
72463
|
+
var MediaSentSchema = object({
|
|
72464
|
+
kind: _enum(["snapshot", "track"]),
|
|
72465
|
+
mimeType: string(),
|
|
72466
|
+
sizeBytes: number$1().int().nonnegative(),
|
|
72467
|
+
mediaKind: string().optional(),
|
|
72468
|
+
capturedAt: number$1().optional(),
|
|
72469
|
+
thumbnailBase64: string().optional()
|
|
72470
|
+
});
|
|
72471
|
+
var StoredChatMessageSchema = object({
|
|
72472
|
+
role: _enum(["user", "assistant"]),
|
|
72473
|
+
content: string(),
|
|
72474
|
+
mediaSent: MediaSentSchema.nullable().optional(),
|
|
72475
|
+
thumbnailBase64: string().optional(),
|
|
72476
|
+
attachment: AttachmentRefSchema.optional()
|
|
72477
|
+
});
|
|
72478
|
+
var StoredConversationSchema = object({
|
|
72479
|
+
id: string(),
|
|
72480
|
+
userId: string(),
|
|
72481
|
+
title: string(),
|
|
72482
|
+
createdAt: number$1(),
|
|
72483
|
+
updatedAt: number$1(),
|
|
72484
|
+
messages: array(StoredChatMessageSchema)
|
|
72485
|
+
});
|
|
72486
|
+
function withoutNulls(data) {
|
|
72487
|
+
const out = {};
|
|
72488
|
+
for (const [key, value] of Object.entries(data)) if (value !== null) out[key] = value;
|
|
72489
|
+
return out;
|
|
72490
|
+
}
|
|
72491
|
+
function parseConversation(id, data) {
|
|
72492
|
+
const parsed = StoredConversationSchema.safeParse({
|
|
72493
|
+
...withoutNulls(data),
|
|
72494
|
+
id
|
|
72495
|
+
});
|
|
72496
|
+
return parsed.success ? parsed.data : null;
|
|
72497
|
+
}
|
|
72498
|
+
var ChatStore = class {
|
|
72499
|
+
store;
|
|
72500
|
+
now;
|
|
72501
|
+
constructor(store, now = Date.now) {
|
|
72502
|
+
this.store = store;
|
|
72503
|
+
this.now = now;
|
|
72504
|
+
}
|
|
72505
|
+
async init() {
|
|
72506
|
+
await this.store.declareCollection(CHATS_COLLECTION, KV_BLOB_COLUMNS);
|
|
72507
|
+
}
|
|
72508
|
+
async create(userId, messages, at = this.now()) {
|
|
72509
|
+
const id = randomUUID();
|
|
72510
|
+
const title = titleFromFirstUserMessage(messages.find((m) => m.role === "user")?.content ?? "");
|
|
72511
|
+
const conversation = {
|
|
72512
|
+
id,
|
|
72513
|
+
userId,
|
|
72514
|
+
title: title.length > 0 ? title : "New chat",
|
|
72515
|
+
createdAt: at,
|
|
72516
|
+
updatedAt: at,
|
|
72517
|
+
messages: [...messages]
|
|
72518
|
+
};
|
|
72519
|
+
await this.store.insert(CHATS_COLLECTION, {
|
|
72520
|
+
id: chatsRowId(userId, id),
|
|
72521
|
+
data: conversation
|
|
72522
|
+
});
|
|
72523
|
+
return conversation;
|
|
72524
|
+
}
|
|
72525
|
+
async list(userId) {
|
|
72526
|
+
return (await this.owned(userId)).map((c) => ({
|
|
72527
|
+
id: c.id,
|
|
72528
|
+
title: c.title,
|
|
72529
|
+
createdAt: c.createdAt,
|
|
72530
|
+
updatedAt: c.updatedAt
|
|
72531
|
+
})).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
72532
|
+
}
|
|
72533
|
+
async get(userId, conversationId) {
|
|
72534
|
+
const match = (await this.store.query(CHATS_COLLECTION)).find((r) => r.id === chatsRowId(userId, conversationId));
|
|
72535
|
+
if (match === void 0) return null;
|
|
72536
|
+
const parsed = parseConversation(conversationId, match.data);
|
|
72537
|
+
if (parsed === null || parsed.userId !== userId) return null;
|
|
72538
|
+
return parsed;
|
|
72539
|
+
}
|
|
72540
|
+
async append(userId, conversationId, messages, at = this.now()) {
|
|
72541
|
+
const current = await this.get(userId, conversationId);
|
|
72542
|
+
if (current === null) return null;
|
|
72543
|
+
const next = {
|
|
72544
|
+
...current,
|
|
72545
|
+
updatedAt: at,
|
|
72546
|
+
messages: [...current.messages, ...messages]
|
|
72547
|
+
};
|
|
72548
|
+
await this.store.update(CHATS_COLLECTION, chatsRowId(userId, conversationId), next);
|
|
72549
|
+
return next;
|
|
72550
|
+
}
|
|
72551
|
+
async prune(userId, retention, now = this.now()) {
|
|
72552
|
+
const owned = await this.owned(userId);
|
|
72553
|
+
return this.pruneRows(owned, retention, now);
|
|
72554
|
+
}
|
|
72555
|
+
async pruneAll(retention, now = this.now()) {
|
|
72556
|
+
const all = await this.allValid();
|
|
72557
|
+
return this.pruneRows(all, retention, now);
|
|
72558
|
+
}
|
|
72559
|
+
async pruneRows(rows, retention, now) {
|
|
72560
|
+
const doomed = /* @__PURE__ */ new Set();
|
|
72561
|
+
const maxAgeMs = retention.maxAgeMs;
|
|
72562
|
+
if (maxAgeMs !== void 0 && maxAgeMs > 0) {
|
|
72563
|
+
const cutoff = now - maxAgeMs;
|
|
72564
|
+
for (const row of rows) if (row.updatedAt < cutoff) doomed.add(chatsRowId(row.userId, row.id));
|
|
72565
|
+
}
|
|
72566
|
+
const maxCount = retention.maxCount;
|
|
72567
|
+
if (maxCount !== void 0 && maxCount > 0) {
|
|
72568
|
+
const byUser = /* @__PURE__ */ new Map();
|
|
72569
|
+
for (const row of rows) {
|
|
72570
|
+
if (doomed.has(chatsRowId(row.userId, row.id))) continue;
|
|
72571
|
+
const list = byUser.get(row.userId) ?? [];
|
|
72572
|
+
list.push(row);
|
|
72573
|
+
byUser.set(row.userId, list);
|
|
72574
|
+
}
|
|
72575
|
+
for (const group of byUser.values()) {
|
|
72576
|
+
const sorted = [...group].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
72577
|
+
for (const extra of sorted.slice(maxCount)) doomed.add(chatsRowId(extra.userId, extra.id));
|
|
72578
|
+
}
|
|
72579
|
+
}
|
|
72580
|
+
for (const id of doomed) await this.store.remove(CHATS_COLLECTION, id);
|
|
72581
|
+
return doomed.size;
|
|
72582
|
+
}
|
|
72583
|
+
async owned(userId) {
|
|
72584
|
+
return (await this.allValid()).filter((c) => c.userId === userId);
|
|
72585
|
+
}
|
|
72586
|
+
async allValid() {
|
|
72587
|
+
const rows = await this.store.query(CHATS_COLLECTION);
|
|
72588
|
+
const out = [];
|
|
72589
|
+
for (const row of rows) {
|
|
72590
|
+
const slash = row.id.indexOf("/");
|
|
72591
|
+
const parsed = parseConversation(slash >= 0 ? row.id.slice(slash + 1) : row.id, row.data);
|
|
72592
|
+
if (parsed !== null) out.push(parsed);
|
|
72593
|
+
}
|
|
72594
|
+
return out;
|
|
72595
|
+
}
|
|
72596
|
+
};
|
|
72597
|
+
//#endregion
|
|
72374
72598
|
//#region src/assembly.ts
|
|
72375
72599
|
/**
|
|
72376
72600
|
* Registration assembly (the hub/agent split, spec §1). Every node running
|
|
@@ -72398,13 +72622,15 @@ async function assembleAi(deps) {
|
|
|
72398
72622
|
registrations,
|
|
72399
72623
|
runtimeProvider
|
|
72400
72624
|
};
|
|
72401
|
-
const { UsageStore } = await import("./usage-store-
|
|
72625
|
+
const { UsageStore } = await import("./usage-store-D_bmdGGd.mjs").then((n) => n.i);
|
|
72402
72626
|
const store = new ProfileStore(deps.settingsPort);
|
|
72403
72627
|
const defaults = new DefaultsStore(deps.settingsPort);
|
|
72404
72628
|
const usage = new UsageStore(deps.settingsPort, Date.now, deps.logger.child("llm-usage"));
|
|
72629
|
+
const chats = new ChatStore(deps.settingsPort, Date.now);
|
|
72405
72630
|
await store.init();
|
|
72406
72631
|
await defaults.init();
|
|
72407
72632
|
await usage.init();
|
|
72633
|
+
await chats.init();
|
|
72408
72634
|
await store.ensureSeeded();
|
|
72409
72635
|
const llmProvider = createLlmProvider({
|
|
72410
72636
|
store,
|
|
@@ -72427,6 +72653,7 @@ async function assembleAi(deps) {
|
|
|
72427
72653
|
llmProvider,
|
|
72428
72654
|
store,
|
|
72429
72655
|
usage,
|
|
72656
|
+
chats,
|
|
72430
72657
|
prune: (retentionDays) => usage.prune(retentionDays)
|
|
72431
72658
|
};
|
|
72432
72659
|
}
|
|
@@ -72567,7 +72794,14 @@ var TestChatRequestSchema = object({
|
|
|
72567
72794
|
/** See the timeout block above. Applies to a VISION turn identically —
|
|
72568
72795
|
* vision inference is the slower one, so a bound tuned on text is a bound
|
|
72569
72796
|
* that only fails on the interesting case. */
|
|
72570
|
-
firstTokenTimeoutMs: number$1().int().positive().max(TEST_CHAT_MAX_FIRST_TOKEN_TIMEOUT_MS).default(TEST_CHAT_DEFAULT_FIRST_TOKEN_TIMEOUT_MS)
|
|
72797
|
+
firstTokenTimeoutMs: number$1().int().positive().max(TEST_CHAT_MAX_FIRST_TOKEN_TIMEOUT_MS).default(TEST_CHAT_DEFAULT_FIRST_TOKEN_TIMEOUT_MS),
|
|
72798
|
+
/**
|
|
72799
|
+
* Persist this turn into the caller's private chat history. Omitted or
|
|
72800
|
+
* false is throwaway — today's behaviour, nothing written.
|
|
72801
|
+
*/
|
|
72802
|
+
persist: boolean().optional(),
|
|
72803
|
+
/** Resume this conversation. Ignored unless `persist` is true. */
|
|
72804
|
+
conversationId: string().min(1).optional()
|
|
72571
72805
|
}).strict();
|
|
72572
72806
|
/**
|
|
72573
72807
|
* Below this, a turn that CARRIED an image almost certainly did not deliver it.
|
|
@@ -72598,7 +72832,9 @@ var TestChatAttachmentSchema = object({
|
|
|
72598
72832
|
/** Track attachments only: the `MediaFileKind` that won `TRACK_MEDIA_PREFERENCE`. */
|
|
72599
72833
|
mediaKind: string().optional(),
|
|
72600
72834
|
/** Epoch ms of the frame, when the source knows it. */
|
|
72601
|
-
capturedAt: number$1().optional()
|
|
72835
|
+
capturedAt: number$1().optional(),
|
|
72836
|
+
/** JPEG of what was actually sent — present when reopening history. */
|
|
72837
|
+
thumbnailBase64: string().optional()
|
|
72602
72838
|
});
|
|
72603
72839
|
/**
|
|
72604
72840
|
* What the run is doing right now — so a wait reads as progress, not as a hang.
|
|
@@ -72644,7 +72880,9 @@ var TestChatEventSchema = discriminatedUnion("kind", [
|
|
|
72644
72880
|
* arrives at once. Said out loud rather than faked. */
|
|
72645
72881
|
streamed: boolean(),
|
|
72646
72882
|
/** Echoed so the page states the bound it is really working under. */
|
|
72647
|
-
firstTokenTimeoutMs: number$1()
|
|
72883
|
+
firstTokenTimeoutMs: number$1(),
|
|
72884
|
+
/** Set when this turn was (or will be) persisted — so the page can resume. */
|
|
72885
|
+
conversationId: string().optional()
|
|
72648
72886
|
}),
|
|
72649
72887
|
object({
|
|
72650
72888
|
kind: literal("token"),
|
|
@@ -72655,7 +72893,8 @@ var TestChatEventSchema = discriminatedUnion("kind", [
|
|
|
72655
72893
|
inputTokens: number$1(),
|
|
72656
72894
|
outputTokens: number$1(),
|
|
72657
72895
|
latencyMs: number$1(),
|
|
72658
|
-
truncated: boolean()
|
|
72896
|
+
truncated: boolean(),
|
|
72897
|
+
conversationId: string().optional()
|
|
72659
72898
|
}),
|
|
72660
72899
|
object({
|
|
72661
72900
|
kind: literal("error"),
|
|
@@ -72668,6 +72907,59 @@ function encodeEvent(event) {
|
|
|
72668
72907
|
return `${JSON.stringify(event)}\n`;
|
|
72669
72908
|
}
|
|
72670
72909
|
//#endregion
|
|
72910
|
+
//#region src/test-chat/persist.ts
|
|
72911
|
+
/**
|
|
72912
|
+
* The messages SOURCE for a test-chat turn: in-page transcript (throwaway)
|
|
72913
|
+
* versus stored history (persistent). The NDJSON transport is unchanged.
|
|
72914
|
+
*/
|
|
72915
|
+
function asWireMessages(messages) {
|
|
72916
|
+
return messages.map((m) => ({
|
|
72917
|
+
role: m.role,
|
|
72918
|
+
content: m.content
|
|
72919
|
+
}));
|
|
72920
|
+
}
|
|
72921
|
+
function isPersistent(request) {
|
|
72922
|
+
return request.persist === true;
|
|
72923
|
+
}
|
|
72924
|
+
async function resolveStreamMessages(store, userId, request) {
|
|
72925
|
+
if (!isPersistent(request) || request.conversationId === void 0) return {
|
|
72926
|
+
ok: true,
|
|
72927
|
+
conversationId: request.conversationId ?? null,
|
|
72928
|
+
messages: request.messages
|
|
72929
|
+
};
|
|
72930
|
+
const existing = await store.get(userId, request.conversationId);
|
|
72931
|
+
if (existing === null) return {
|
|
72932
|
+
ok: false,
|
|
72933
|
+
status: 404,
|
|
72934
|
+
error: "conversation not found"
|
|
72935
|
+
};
|
|
72936
|
+
const combined = [...asWireMessages(existing.messages), ...request.messages];
|
|
72937
|
+
const messages = combined.length > 40 ? combined.slice(-40) : combined;
|
|
72938
|
+
return {
|
|
72939
|
+
ok: true,
|
|
72940
|
+
conversationId: existing.id,
|
|
72941
|
+
messages
|
|
72942
|
+
};
|
|
72943
|
+
}
|
|
72944
|
+
function userTurnsToStore(request, commit) {
|
|
72945
|
+
return [{
|
|
72946
|
+
role: "user",
|
|
72947
|
+
content: [...request.messages].reverse().find((m) => m.role === "user")?.content ?? "",
|
|
72948
|
+
...commit.mediaSent !== null ? { mediaSent: commit.mediaSent } : {},
|
|
72949
|
+
...commit.thumbnailBase64 !== void 0 ? { thumbnailBase64: commit.thumbnailBase64 } : {},
|
|
72950
|
+
...request.attachment !== void 0 ? { attachment: request.attachment } : {}
|
|
72951
|
+
}, {
|
|
72952
|
+
role: "assistant",
|
|
72953
|
+
content: commit.assistantContent
|
|
72954
|
+
}];
|
|
72955
|
+
}
|
|
72956
|
+
async function commitPersistentTurn(store, userId, request, commit) {
|
|
72957
|
+
if (!isPersistent(request)) return null;
|
|
72958
|
+
const turns = userTurnsToStore(request, commit);
|
|
72959
|
+
if (request.conversationId !== void 0) return store.append(userId, request.conversationId, turns);
|
|
72960
|
+
return store.create(userId, turns);
|
|
72961
|
+
}
|
|
72962
|
+
//#endregion
|
|
72671
72963
|
//#region src/test-chat/media.ts
|
|
72672
72964
|
/**
|
|
72673
72965
|
* Which stored media a track attachment is allowed to be, best first.
|
|
@@ -72920,7 +73212,10 @@ async function runTestChatStream(deps, request, emit, signal) {
|
|
|
72920
73212
|
await fail(outcome.failure, "ai test chat: attachment could not be resolved — vision turn refused", { attachmentKind: request.attachment.kind });
|
|
72921
73213
|
return;
|
|
72922
73214
|
}
|
|
72923
|
-
attachment =
|
|
73215
|
+
attachment = {
|
|
73216
|
+
...outcome.resolved.attachment,
|
|
73217
|
+
thumbnailBase64: Buffer.from(outcome.resolved.image.bytes).toString("base64")
|
|
73218
|
+
};
|
|
72924
73219
|
images = [outcome.resolved.image];
|
|
72925
73220
|
imageCount = 1;
|
|
72926
73221
|
}
|
|
@@ -73137,6 +73432,49 @@ async function runTestChatStream(deps, request, emit, signal) {
|
|
|
73137
73432
|
});
|
|
73138
73433
|
}
|
|
73139
73434
|
//#endregion
|
|
73435
|
+
//#region src/test-chat/user-from-request.ts
|
|
73436
|
+
var SESSION_COOKIE = "camstack_session";
|
|
73437
|
+
function cookieValue(header, name) {
|
|
73438
|
+
if (header === void 0 || header.length === 0) return null;
|
|
73439
|
+
for (const pair of header.split(";")) {
|
|
73440
|
+
const eq = pair.indexOf("=");
|
|
73441
|
+
if (eq === -1) continue;
|
|
73442
|
+
if (pair.slice(0, eq).trim() !== name) continue;
|
|
73443
|
+
const raw = pair.slice(eq + 1).trim();
|
|
73444
|
+
if (raw.length === 0) return null;
|
|
73445
|
+
try {
|
|
73446
|
+
return decodeURIComponent(raw);
|
|
73447
|
+
} catch {
|
|
73448
|
+
return raw;
|
|
73449
|
+
}
|
|
73450
|
+
}
|
|
73451
|
+
return null;
|
|
73452
|
+
}
|
|
73453
|
+
function userIdFromJwt(token) {
|
|
73454
|
+
const parts = token.split(".");
|
|
73455
|
+
if (parts.length < 2 || parts[1] === void 0) return null;
|
|
73456
|
+
try {
|
|
73457
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
73458
|
+
const payload = JSON.parse(json);
|
|
73459
|
+
return typeof payload.userId === "string" && payload.userId.length > 0 ? payload.userId : null;
|
|
73460
|
+
} catch {
|
|
73461
|
+
return null;
|
|
73462
|
+
}
|
|
73463
|
+
}
|
|
73464
|
+
function bearerToken(header) {
|
|
73465
|
+
const raw = Array.isArray(header) ? header[0] : header;
|
|
73466
|
+
if (raw === void 0 || !raw.startsWith("Bearer ")) return null;
|
|
73467
|
+
const token = raw.slice(7).trim();
|
|
73468
|
+
return token.length > 0 ? token : null;
|
|
73469
|
+
}
|
|
73470
|
+
function readUserIdFromRequest(req) {
|
|
73471
|
+
const fromAuth = bearerToken(req.headers.authorization);
|
|
73472
|
+
if (fromAuth !== null) return userIdFromJwt(fromAuth);
|
|
73473
|
+
const fromCookie = cookieValue(Array.isArray(req.headers.cookie) ? req.headers.cookie[0] : req.headers.cookie, SESSION_COOKIE);
|
|
73474
|
+
if (fromCookie === null) return null;
|
|
73475
|
+
return userIdFromJwt(fromCookie);
|
|
73476
|
+
}
|
|
73477
|
+
//#endregion
|
|
73140
73478
|
//#region src/test-chat/plane.ts
|
|
73141
73479
|
/** Bounded so a malformed or hostile body cannot buy memory. */
|
|
73142
73480
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
@@ -73160,61 +73498,164 @@ function sendJson(res, status, body) {
|
|
|
73160
73498
|
});
|
|
73161
73499
|
res.end(text);
|
|
73162
73500
|
}
|
|
73163
|
-
function
|
|
73164
|
-
return
|
|
73165
|
-
|
|
73166
|
-
|
|
73501
|
+
function pathParts(url) {
|
|
73502
|
+
return ((url ?? "/").split("?")[0] ?? "/").split("/").filter((p) => p.length > 0);
|
|
73503
|
+
}
|
|
73504
|
+
function requireUser(req, res) {
|
|
73505
|
+
const userId = readUserIdFromRequest(req);
|
|
73506
|
+
if (userId === null) {
|
|
73507
|
+
sendJson(res, 401, { error: "unauthenticated" });
|
|
73508
|
+
return null;
|
|
73509
|
+
}
|
|
73510
|
+
return userId;
|
|
73511
|
+
}
|
|
73512
|
+
async function handleList(deps, req, res) {
|
|
73513
|
+
if (deps.chats === void 0) {
|
|
73514
|
+
sendJson(res, 503, { error: "chat store unavailable" });
|
|
73515
|
+
return;
|
|
73516
|
+
}
|
|
73517
|
+
const userId = requireUser(req, res);
|
|
73518
|
+
if (userId === null) return;
|
|
73519
|
+
sendJson(res, 200, { chats: await deps.chats.list(userId) });
|
|
73520
|
+
}
|
|
73521
|
+
async function handleGet(deps, req, res, conversationId) {
|
|
73522
|
+
if (deps.chats === void 0) {
|
|
73523
|
+
sendJson(res, 503, { error: "chat store unavailable" });
|
|
73524
|
+
return;
|
|
73525
|
+
}
|
|
73526
|
+
const userId = requireUser(req, res);
|
|
73527
|
+
if (userId === null) return;
|
|
73528
|
+
const conversation = await deps.chats.get(userId, conversationId);
|
|
73529
|
+
if (conversation === null) {
|
|
73530
|
+
sendJson(res, 404, { error: "conversation not found" });
|
|
73531
|
+
return;
|
|
73532
|
+
}
|
|
73533
|
+
sendJson(res, 200, conversation);
|
|
73534
|
+
}
|
|
73535
|
+
async function handleStream(deps, req, res) {
|
|
73536
|
+
const raw = await readBody(req);
|
|
73537
|
+
if (raw === null) {
|
|
73538
|
+
sendJson(res, 413, { error: "request body too large" });
|
|
73539
|
+
return;
|
|
73540
|
+
}
|
|
73541
|
+
let parsedJson;
|
|
73542
|
+
try {
|
|
73543
|
+
parsedJson = JSON.parse(raw);
|
|
73544
|
+
} catch {
|
|
73545
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
73546
|
+
return;
|
|
73547
|
+
}
|
|
73548
|
+
const parsed = TestChatRequestSchema.safeParse(parsedJson);
|
|
73549
|
+
if (!parsed.success) {
|
|
73550
|
+
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
73551
|
+
deps.logger.warn("ai test chat: request rejected — turn never ran", { meta: { detail } });
|
|
73552
|
+
sendJson(res, 400, {
|
|
73553
|
+
error: "invalid request",
|
|
73554
|
+
detail
|
|
73555
|
+
});
|
|
73556
|
+
return;
|
|
73557
|
+
}
|
|
73558
|
+
let request = parsed.data;
|
|
73559
|
+
if (request.persist === true) {
|
|
73560
|
+
if (deps.chats === void 0) {
|
|
73561
|
+
sendJson(res, 503, { error: "chat store unavailable" });
|
|
73167
73562
|
return;
|
|
73168
73563
|
}
|
|
73169
|
-
const
|
|
73170
|
-
if (
|
|
73171
|
-
|
|
73564
|
+
const userId = requireUser(req, res);
|
|
73565
|
+
if (userId === null) return;
|
|
73566
|
+
const resolved = await resolveStreamMessages(deps.chats, userId, request);
|
|
73567
|
+
if (!resolved.ok) {
|
|
73568
|
+
sendJson(res, resolved.status, { error: resolved.error });
|
|
73172
73569
|
return;
|
|
73173
73570
|
}
|
|
73174
|
-
|
|
73175
|
-
|
|
73176
|
-
|
|
73177
|
-
}
|
|
73178
|
-
|
|
73571
|
+
request = {
|
|
73572
|
+
...request,
|
|
73573
|
+
messages: resolved.messages
|
|
73574
|
+
};
|
|
73575
|
+
}
|
|
73576
|
+
const controller = new AbortController();
|
|
73577
|
+
res.on("close", () => controller.abort());
|
|
73578
|
+
res.writeHead(200, {
|
|
73579
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
73580
|
+
"cache-control": "no-store, no-transform",
|
|
73581
|
+
"content-encoding": "identity",
|
|
73582
|
+
"x-no-compression": "1",
|
|
73583
|
+
connection: "keep-alive"
|
|
73584
|
+
});
|
|
73585
|
+
res.flushHeaders();
|
|
73586
|
+
let assistantText = "";
|
|
73587
|
+
let mediaSent = null;
|
|
73588
|
+
let sawDone = false;
|
|
73589
|
+
let pendingDone = null;
|
|
73590
|
+
const emit = (event) => {
|
|
73591
|
+
if (res.writableEnded) return;
|
|
73592
|
+
if (event.kind === "token") assistantText += event.text;
|
|
73593
|
+
if (event.kind === "meta") mediaSent = event.mediaSent;
|
|
73594
|
+
if (event.kind === "done") {
|
|
73595
|
+
sawDone = true;
|
|
73596
|
+
pendingDone = event;
|
|
73179
73597
|
return;
|
|
73180
73598
|
}
|
|
73181
|
-
|
|
73182
|
-
|
|
73183
|
-
|
|
73184
|
-
|
|
73185
|
-
|
|
73186
|
-
|
|
73187
|
-
|
|
73188
|
-
|
|
73599
|
+
res.write(encodeEvent(event));
|
|
73600
|
+
};
|
|
73601
|
+
try {
|
|
73602
|
+
await runTestChatStream(deps.stream, request, emit, controller.signal);
|
|
73603
|
+
let conversationId;
|
|
73604
|
+
if (sawDone && request.persist === true && deps.chats !== void 0) {
|
|
73605
|
+
const userId = readUserIdFromRequest(req);
|
|
73606
|
+
if (userId !== null) try {
|
|
73607
|
+
conversationId = (await commitPersistentTurn(deps.chats, userId, request, {
|
|
73608
|
+
assistantContent: assistantText,
|
|
73609
|
+
mediaSent,
|
|
73610
|
+
thumbnailBase64: thumbnailFrom(mediaSent)
|
|
73611
|
+
}))?.id;
|
|
73612
|
+
const retention = deps.retention?.();
|
|
73613
|
+
if (retention !== void 0) await deps.chats.prune(userId, retention).catch(() => void 0);
|
|
73614
|
+
} catch (cause) {
|
|
73615
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
73616
|
+
deps.logger.warn("ai test chat: persist failed — turn was not saved", { meta: { error: message } });
|
|
73617
|
+
}
|
|
73618
|
+
}
|
|
73619
|
+
if (pendingDone !== null) emitDone(res, pendingDone, conversationId);
|
|
73620
|
+
} catch (cause) {
|
|
73621
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
73622
|
+
deps.logger.error("ai test chat: run threw — stream closed with an error event", { meta: { error: message } });
|
|
73623
|
+
if (!res.writableEnded) res.write(encodeEvent({
|
|
73624
|
+
kind: "error",
|
|
73625
|
+
code: "adapter-error",
|
|
73626
|
+
message
|
|
73627
|
+
}));
|
|
73628
|
+
} finally {
|
|
73629
|
+
if (!res.writableEnded) res.end();
|
|
73630
|
+
}
|
|
73631
|
+
}
|
|
73632
|
+
function thumbnailFrom(sent) {
|
|
73633
|
+
return sent === null ? void 0 : sent.thumbnailBase64;
|
|
73634
|
+
}
|
|
73635
|
+
function emitDone(res, done, conversationId) {
|
|
73636
|
+
if (res.writableEnded) return;
|
|
73637
|
+
res.write(encodeEvent(conversationId !== void 0 ? {
|
|
73638
|
+
...done,
|
|
73639
|
+
conversationId
|
|
73640
|
+
} : done));
|
|
73641
|
+
}
|
|
73642
|
+
function createTestChatPlaneHandler(deps) {
|
|
73643
|
+
return async (req, res) => {
|
|
73644
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
73645
|
+
const parts = pathParts(req.url);
|
|
73646
|
+
if (method === "GET" && parts[0] === "chats" && parts.length === 1) {
|
|
73647
|
+
await handleList(deps, req, res);
|
|
73189
73648
|
return;
|
|
73190
73649
|
}
|
|
73191
|
-
|
|
73192
|
-
|
|
73193
|
-
|
|
73194
|
-
"content-type": "application/x-ndjson; charset=utf-8",
|
|
73195
|
-
"cache-control": "no-store, no-transform",
|
|
73196
|
-
"content-encoding": "identity",
|
|
73197
|
-
"x-no-compression": "1",
|
|
73198
|
-
connection: "keep-alive"
|
|
73199
|
-
});
|
|
73200
|
-
res.flushHeaders();
|
|
73201
|
-
const emit = (event) => {
|
|
73202
|
-
if (res.writableEnded) return;
|
|
73203
|
-
res.write(encodeEvent(event));
|
|
73204
|
-
};
|
|
73205
|
-
try {
|
|
73206
|
-
await runTestChatStream(deps.stream, parsed.data, emit, controller.signal);
|
|
73207
|
-
} catch (cause) {
|
|
73208
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
73209
|
-
deps.logger.error("ai test chat: run threw — stream closed with an error event", { meta: { error: message } });
|
|
73210
|
-
emit({
|
|
73211
|
-
kind: "error",
|
|
73212
|
-
code: "adapter-error",
|
|
73213
|
-
message
|
|
73214
|
-
});
|
|
73215
|
-
} finally {
|
|
73216
|
-
if (!res.writableEnded) res.end();
|
|
73650
|
+
if (method === "GET" && parts[0] === "chats" && parts.length === 2 && parts[1] !== void 0) {
|
|
73651
|
+
await handleGet(deps, req, res, parts[1]);
|
|
73652
|
+
return;
|
|
73217
73653
|
}
|
|
73654
|
+
if (method !== "POST" || parts.length > 0) {
|
|
73655
|
+
sendJson(res, 405, { error: "POST only" });
|
|
73656
|
+
return;
|
|
73657
|
+
}
|
|
73658
|
+
await handleStream(deps, req, res);
|
|
73218
73659
|
};
|
|
73219
73660
|
}
|
|
73220
73661
|
//#endregion
|
|
@@ -73229,13 +73670,40 @@ function createTestChatPlaneHandler(deps) {
|
|
|
73229
73670
|
var POST_BOOT_DELAY_MS = 15e3;
|
|
73230
73671
|
var PRUNE_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
73231
73672
|
var USAGE_RETENTION_DAYS = 90;
|
|
73673
|
+
var DAY_MS = 1440 * 60 * 1e3;
|
|
73232
73674
|
var AiAddon = class extends BaseAddon {
|
|
73233
73675
|
supervisor;
|
|
73234
73676
|
timers = [];
|
|
73235
73677
|
/** Hub only — see `serveTestChatPlane`. */
|
|
73236
73678
|
testChatPlane = null;
|
|
73237
73679
|
constructor() {
|
|
73238
|
-
super({
|
|
73680
|
+
super({
|
|
73681
|
+
chatRetentionMaxAgeDays: 30,
|
|
73682
|
+
chatRetentionMaxCount: 50
|
|
73683
|
+
});
|
|
73684
|
+
}
|
|
73685
|
+
globalSettingsSchema() {
|
|
73686
|
+
return this.schema({ sections: [{
|
|
73687
|
+
id: "chats",
|
|
73688
|
+
title: "Persistent chats",
|
|
73689
|
+
fields: [this.field({
|
|
73690
|
+
type: "number",
|
|
73691
|
+
key: "chatRetentionMaxAgeDays",
|
|
73692
|
+
label: "Chat retention (days)",
|
|
73693
|
+
description: "Drop persisted chats older than this. 0 keeps them forever.",
|
|
73694
|
+
min: 0,
|
|
73695
|
+
step: 1,
|
|
73696
|
+
default: 30
|
|
73697
|
+
}), this.field({
|
|
73698
|
+
type: "number",
|
|
73699
|
+
key: "chatRetentionMaxCount",
|
|
73700
|
+
label: "Max chats per user",
|
|
73701
|
+
description: "Keep only the newest N conversations per user. 0 means no cap.",
|
|
73702
|
+
min: 0,
|
|
73703
|
+
step: 1,
|
|
73704
|
+
default: 50
|
|
73705
|
+
})]
|
|
73706
|
+
}] });
|
|
73239
73707
|
}
|
|
73240
73708
|
async onInitialize() {
|
|
73241
73709
|
const ctx = this.ctx;
|
|
@@ -73250,7 +73718,7 @@ var AiAddon = class extends BaseAddon {
|
|
|
73250
73718
|
const settingsPort = api !== void 0 ? createApiSettingsStorePort(api) : createMemorySettingsStorePort();
|
|
73251
73719
|
if (api === void 0) logger.warn("addon-ai: no ctx.api — profiles are in-memory only");
|
|
73252
73720
|
const binDir = path$1.join(ctx.nodeDataDir, "bin");
|
|
73253
|
-
const { ensureLlamaServer } = await import("./ensure-llama-server-
|
|
73721
|
+
const { ensureLlamaServer } = await import("./ensure-llama-server-D_uzfSep.mjs").then((n) => n.r);
|
|
73254
73722
|
const assembly = await assembleAi({
|
|
73255
73723
|
nodeId: ownNodeId,
|
|
73256
73724
|
isHub,
|
|
@@ -73372,7 +73840,11 @@ var AiAddon = class extends BaseAddon {
|
|
|
73372
73840
|
access: "authenticated",
|
|
73373
73841
|
handler: createTestChatPlaneHandler({
|
|
73374
73842
|
stream,
|
|
73375
|
-
logger
|
|
73843
|
+
logger,
|
|
73844
|
+
...assembly.chats !== void 0 ? {
|
|
73845
|
+
chats: assembly.chats,
|
|
73846
|
+
retention: () => this.chatRetention()
|
|
73847
|
+
} : {}
|
|
73376
73848
|
})
|
|
73377
73849
|
}) ?? null;
|
|
73378
73850
|
this.ctx.logger.info("ai test-chat data-plane served", { meta: {
|
|
@@ -73380,11 +73852,21 @@ var AiAddon = class extends BaseAddon {
|
|
|
73380
73852
|
path: `/addon/${this.ctx.id}/${TEST_CHAT_PREFIX}`
|
|
73381
73853
|
} });
|
|
73382
73854
|
}
|
|
73855
|
+
chatRetention() {
|
|
73856
|
+
const days = this.config.chatRetentionMaxAgeDays;
|
|
73857
|
+
const count = this.config.chatRetentionMaxCount;
|
|
73858
|
+
return {
|
|
73859
|
+
...days > 0 ? { maxAgeMs: days * DAY_MS } : {},
|
|
73860
|
+
...count > 0 ? { maxCount: count } : {}
|
|
73861
|
+
};
|
|
73862
|
+
}
|
|
73383
73863
|
schedulePostBoot(assembly) {
|
|
73384
73864
|
const kick = setTimeout(() => {
|
|
73385
73865
|
assembly.prune?.(USAGE_RETENTION_DAYS).catch(() => void 0);
|
|
73866
|
+
assembly.chats?.pruneAll(this.chatRetention()).catch(() => void 0);
|
|
73386
73867
|
setInterval(() => {
|
|
73387
73868
|
assembly.prune?.(USAGE_RETENTION_DAYS).catch(() => void 0);
|
|
73869
|
+
assembly.chats?.pruneAll(this.chatRetention()).catch(() => void 0);
|
|
73388
73870
|
}, PRUNE_INTERVAL_MS).unref?.();
|
|
73389
73871
|
this.autoStartRuntimes(assembly);
|
|
73390
73872
|
}, POST_BOOT_DELAY_MS);
|
|
@@ -73412,4 +73894,4 @@ var AiAddon = class extends BaseAddon {
|
|
|
73412
73894
|
}
|
|
73413
73895
|
};
|
|
73414
73896
|
//#endregion
|
|
73415
|
-
export {
|
|
73897
|
+
export { entryForRef as A, AiAddon, AiAddon as default, boolean as B, createRuntimeClient as C, createDefaultModelOps as D, LlamaSupervisor as E, createLlmClient as F, __exportAll as G, object as H, require_token_util as I, require_token_error as L, createLlmProvider as M, CONSUMER_RETRY_POLICY as N, LLM_MODEL_CATALOG as O, resolveRetryPolicy as P, LlmErrorCodeSchema as R, titleFromFirstUserMessage as S, fileSha256 as T, string as U, number$1 as V, __commonJSMin as W, createApiSettingsStorePort as _, renderTranscript as a, ChatStore as b, TEST_CHAT_CONSUMER as c, TEST_CHAT_MAX_FIRST_TOKEN_TIMEOUT_MS as d, TEST_CHAT_MIN_VISION_INPUT_TOKENS as f, encodeEvent as g, TestChatRequestSchema as h, pickTrackMedia as i, AI_ADDON_ID as j, catalogById as k, TEST_CHAT_DEFAULT_FIRST_TOKEN_TIMEOUT_MS as l, TestChatEventSchema as m, runTestChatStream as n, resolveImage as o, TEST_CHAT_PREFIX as p, TRACK_MEDIA_PREFERENCE as r, TEST_CHAT_CONNECT_TIMEOUT_MS as s, createTestChatPlaneHandler as t, TEST_CHAT_IDLE_TIMEOUT_MS as u, createMemorySettingsStorePort as v, createLlmRuntimeProvider as w, chatsRowId as x, CHATS_COLLECTION as y, LlmProfileKindSchema as z };
|