@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.js
CHANGED
|
@@ -15,7 +15,7 @@ let node_zlib = require("node:zlib");
|
|
|
15
15
|
let node_fs_promises = require("node:fs/promises");
|
|
16
16
|
node_fs_promises = require_chunk.__toESM(node_fs_promises);
|
|
17
17
|
let node_child_process = require("node:child_process");
|
|
18
|
-
//#region ../types/dist/event-category-
|
|
18
|
+
//#region ../types/dist/event-category-C0lyLd5U.mjs
|
|
19
19
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
20
20
|
EventCategory["SystemBoot"] = "system.boot";
|
|
21
21
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -32,9 +32,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
32
32
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
33
33
|
/**
|
|
34
34
|
* A newer addon or server-root package version was found by the
|
|
35
|
-
* authoritative registry check. Emitted once
|
|
36
|
-
* `(
|
|
37
|
-
*
|
|
35
|
+
* authoritative registry check. Emitted once when any observed
|
|
36
|
+
* `latestVersion` changes (or a package/node first appears behind);
|
|
37
|
+
* the payload carries the full currently-available list. Repeated
|
|
38
|
+
* polling of the same latests is silent.
|
|
38
39
|
*/
|
|
39
40
|
EventCategory["UpdateAvailable"] = "update.available";
|
|
40
41
|
/**
|
|
@@ -7754,6 +7755,10 @@ var RecordingBandSchema = object({
|
|
|
7754
7755
|
preBufferSec: number$1().min(0).optional(),
|
|
7755
7756
|
postBufferSec: number$1().min(0).optional()
|
|
7756
7757
|
});
|
|
7758
|
+
({
|
|
7759
|
+
preBufferSec: 10,
|
|
7760
|
+
postBufferSec: 30
|
|
7761
|
+
}).postBufferSec * 1e3;
|
|
7757
7762
|
/**
|
|
7758
7763
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7759
7764
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -14532,6 +14537,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14532
14537
|
"alarm-disarmed",
|
|
14533
14538
|
"alarm-arming",
|
|
14534
14539
|
"alarm-arm-refused",
|
|
14540
|
+
"addon-updated",
|
|
14541
|
+
"server-updated",
|
|
14542
|
+
"export-completed",
|
|
14535
14543
|
"camera-online",
|
|
14536
14544
|
"camera-offline",
|
|
14537
14545
|
"camera-disabled",
|
|
@@ -16425,13 +16433,19 @@ var RetrainStatusSchema = _enum([
|
|
|
16425
16433
|
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16426
16434
|
*
|
|
16427
16435
|
* `debug` does NOT pin; it is attention, not durability.
|
|
16436
|
+
*
|
|
16437
|
+
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
16438
|
+
* A favourited track is skipped by retention the same way `staging` is, but
|
|
16439
|
+
* it does not enter `none|staging|trained` and has no staging budget.
|
|
16428
16440
|
*/
|
|
16429
16441
|
var TrackFlagFields = {
|
|
16430
16442
|
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16431
16443
|
* `'staging'`. */
|
|
16432
16444
|
markForTrain: boolean().optional(),
|
|
16433
16445
|
/** Operator marked this track for diagnostic attention. */
|
|
16434
|
-
debug: boolean().optional()
|
|
16446
|
+
debug: boolean().optional(),
|
|
16447
|
+
/** Operator favourited this track. Pins it against pruning. */
|
|
16448
|
+
favourited: boolean().optional()
|
|
16435
16449
|
};
|
|
16436
16450
|
/**
|
|
16437
16451
|
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
@@ -16456,6 +16470,7 @@ var TrackFlagsSchema = object({
|
|
|
16456
16470
|
trackId: string(),
|
|
16457
16471
|
markForTrain: boolean(),
|
|
16458
16472
|
debug: boolean(),
|
|
16473
|
+
favourited: boolean(),
|
|
16459
16474
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16460
16475
|
* a track row) because this shape is only ever produced by the write body,
|
|
16461
16476
|
* which always knows it — and a surface that has just written needs to render
|
|
@@ -19410,7 +19425,14 @@ targets: array(object({
|
|
|
19410
19425
|
"sleeping",
|
|
19411
19426
|
"unreachable",
|
|
19412
19427
|
"waking"
|
|
19413
|
-
]).nullable()
|
|
19428
|
+
]).nullable(),
|
|
19429
|
+
/** A battery camera (whatever its current state). An AWAKE battery
|
|
19430
|
+
* camera is deliberately NOT recaptured on the poll cadence — every
|
|
19431
|
+
* capture is a camera hit that would keep it out of sleep — so its
|
|
19432
|
+
* cached frame legitimately ages past the currency ceiling while
|
|
19433
|
+
* nothing is streaming. A surface must keep painting it (a fresh
|
|
19434
|
+
* frame is captured at each wake), not blank to "unavailable". */
|
|
19435
|
+
battery: boolean()
|
|
19414
19436
|
})));
|
|
19415
19437
|
/**
|
|
19416
19438
|
* `sso-bridge` — internal hub-only cap that lets SSO-style auth
|
|
@@ -20101,8 +20123,28 @@ var ClipSchema = object({
|
|
|
20101
20123
|
startMs: number$1(),
|
|
20102
20124
|
endMs: number$1()
|
|
20103
20125
|
}),
|
|
20104
|
-
/**
|
|
20105
|
-
thumbnail
|
|
20126
|
+
/**
|
|
20127
|
+
* Lazy thumbnail URL, never inlined.
|
|
20128
|
+
*
|
|
20129
|
+
* Recording-derived clips (events-mode keep-window, and the prepared
|
|
20130
|
+
* continuous event+fragment visit) MUST use the snapshot of the **main
|
|
20131
|
+
* event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
|
|
20132
|
+
* of the event that owns `kind` (object > motion > audio). Do not extract
|
|
20133
|
+
* a keyframe from the recorded segments. Other providers (onboard, HKSV)
|
|
20134
|
+
* mint their own stills.
|
|
20135
|
+
*/
|
|
20136
|
+
thumbnail: string().optional(),
|
|
20137
|
+
/** Analytics event ids that overlap this visit. Empty on footage-only clips.
|
|
20138
|
+
* The default provider's visit grain puts many motion heartbeats on one clip
|
|
20139
|
+
* instead of minting one clip per marker. */
|
|
20140
|
+
eventIds: array(string()).optional(),
|
|
20141
|
+
/** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
|
|
20142
|
+
* than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
|
|
20143
|
+
* bar keeps showing them via `recording.getAvailability`. */
|
|
20144
|
+
holes: array(object({
|
|
20145
|
+
startMs: number$1(),
|
|
20146
|
+
endMs: number$1()
|
|
20147
|
+
})).optional()
|
|
20106
20148
|
});
|
|
20107
20149
|
var ClipPlaybackSchema = object({
|
|
20108
20150
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -24700,7 +24742,9 @@ var ExportOptionsSchema = object({
|
|
|
24700
24742
|
includeAudio: boolean(),
|
|
24701
24743
|
maxLifeMs: number$1().int().positive(),
|
|
24702
24744
|
deleteAfterDownload: boolean(),
|
|
24703
|
-
title: string().max(200).optional()
|
|
24745
|
+
title: string().max(200).optional(),
|
|
24746
|
+
/** Notification-output target ids to ping when this export becomes ready. */
|
|
24747
|
+
notifyTargetIds: array(string().min(1)).max(20).optional()
|
|
24704
24748
|
}).superRefine((v, ctx) => {
|
|
24705
24749
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
24706
24750
|
code: ZodIssueCode.custom,
|
|
@@ -24759,10 +24803,18 @@ var ExportBytesSchema = object({
|
|
|
24759
24803
|
});
|
|
24760
24804
|
method(object({
|
|
24761
24805
|
deviceId: number$1(),
|
|
24762
|
-
|
|
24806
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
24807
|
+
profile: string().optional(),
|
|
24808
|
+
profiles: array(string()).min(1).optional(),
|
|
24763
24809
|
fromMs: number$1(),
|
|
24764
24810
|
toMs: number$1(),
|
|
24765
24811
|
options: ExportOptionsSchema
|
|
24812
|
+
}).superRefine((v, ctx) => {
|
|
24813
|
+
if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
|
|
24814
|
+
code: ZodIssueCode.custom,
|
|
24815
|
+
message: "pass profiles[] (min 1) or legacy profile",
|
|
24816
|
+
path: ["profiles"]
|
|
24817
|
+
});
|
|
24766
24818
|
}), ExportRecordSchema, {
|
|
24767
24819
|
kind: "mutation",
|
|
24768
24820
|
auth: "protected"
|
|
@@ -69690,7 +69742,7 @@ async function* chunksFrom(stream) {
|
|
|
69690
69742
|
var DEFAULTS_COLLECTION = "ai:llm-defaults";
|
|
69691
69743
|
/** KV JSON-blob shape — a single `data` column routes through the settings
|
|
69692
69744
|
* backend's canonical key/value path (id TEXT PK, data TEXT). */
|
|
69693
|
-
var KV_BLOB_COLUMNS$
|
|
69745
|
+
var KV_BLOB_COLUMNS$2 = [{
|
|
69694
69746
|
name: "data",
|
|
69695
69747
|
type: "TEXT",
|
|
69696
69748
|
notNull: true
|
|
@@ -69706,7 +69758,7 @@ var DefaultsStore = class {
|
|
|
69706
69758
|
/** Declare the backing collection before any read/write — the SQLite
|
|
69707
69759
|
* settings backend rejects undeclared collections (fail-fast). */
|
|
69708
69760
|
async init() {
|
|
69709
|
-
await this.store.declareCollection(DEFAULTS_COLLECTION, KV_BLOB_COLUMNS$
|
|
69761
|
+
await this.store.declareCollection(DEFAULTS_COLLECTION, KV_BLOB_COLUMNS$2);
|
|
69710
69762
|
}
|
|
69711
69763
|
async list() {
|
|
69712
69764
|
const rows = await this.store.query(DEFAULTS_COLLECTION);
|
|
@@ -69758,7 +69810,7 @@ var SEED_PROFILE_ID = "seed-ollama-lan";
|
|
|
69758
69810
|
var SEED_MARKER_ID = "seededDefaults";
|
|
69759
69811
|
/** KV JSON-blob shape — a single `data` column routes the row through the
|
|
69760
69812
|
* settings backend's canonical key/value path (id TEXT PK, data TEXT). */
|
|
69761
|
-
var KV_BLOB_COLUMNS = [{
|
|
69813
|
+
var KV_BLOB_COLUMNS$1 = [{
|
|
69762
69814
|
name: "data",
|
|
69763
69815
|
type: "TEXT",
|
|
69764
69816
|
notNull: true
|
|
@@ -69783,8 +69835,8 @@ var ProfileStore = class {
|
|
|
69783
69835
|
* settings backend rejects undeclared collections (fail-fast), so this
|
|
69784
69836
|
* MUST run before `ensureSeeded`/`list`/`upsert` — mirrors UsageStore.init. */
|
|
69785
69837
|
async init() {
|
|
69786
|
-
await this.store.declareCollection(PROFILES_COLLECTION, KV_BLOB_COLUMNS);
|
|
69787
|
-
await this.store.declareCollection(META_COLLECTION, KV_BLOB_COLUMNS);
|
|
69838
|
+
await this.store.declareCollection(PROFILES_COLLECTION, KV_BLOB_COLUMNS$1);
|
|
69839
|
+
await this.store.declareCollection(META_COLLECTION, KV_BLOB_COLUMNS$1);
|
|
69788
69840
|
}
|
|
69789
69841
|
/** RAW rows (apiKey present); invalid rows dropped. */
|
|
69790
69842
|
async list() {
|
|
@@ -72345,6 +72397,178 @@ function createRuntimeClient(api) {
|
|
|
72345
72397
|
};
|
|
72346
72398
|
}
|
|
72347
72399
|
//#endregion
|
|
72400
|
+
//#region src/test-chat/chat-store.ts
|
|
72401
|
+
/**
|
|
72402
|
+
* Per-user persistent AI chats, over the same settings-store port as
|
|
72403
|
+
* profiles/usage.
|
|
72404
|
+
*
|
|
72405
|
+
* Collection `ai:chats`; row id `ai:chats/<userId>/<conversationId>` is
|
|
72406
|
+
* spelled as `${userId}/${conversationId}` (the collection already carries
|
|
72407
|
+
* the `ai:chats` prefix). Isolation is structural: every read/write takes
|
|
72408
|
+
* `userId` and refuses a row that belongs to someone else.
|
|
72409
|
+
*
|
|
72410
|
+
* @durable class=audit owner=ai
|
|
72411
|
+
* write="an operator sends a persistent test-chat turn — one row per conversation, messages[] replayed into the model each turn"
|
|
72412
|
+
* 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."
|
|
72413
|
+
*/
|
|
72414
|
+
var CHATS_COLLECTION = "ai:chats";
|
|
72415
|
+
var TITLE_MAX_CHARS = 80;
|
|
72416
|
+
var KV_BLOB_COLUMNS = [{
|
|
72417
|
+
name: "data",
|
|
72418
|
+
type: "TEXT",
|
|
72419
|
+
notNull: true
|
|
72420
|
+
}];
|
|
72421
|
+
function chatsRowId(userId, conversationId) {
|
|
72422
|
+
return `${userId}/${conversationId}`;
|
|
72423
|
+
}
|
|
72424
|
+
function titleFromFirstUserMessage(content) {
|
|
72425
|
+
const collapsed = content.trim().replace(/\s+/g, " ");
|
|
72426
|
+
if (collapsed.length <= TITLE_MAX_CHARS) return collapsed;
|
|
72427
|
+
return collapsed.slice(0, TITLE_MAX_CHARS).trimEnd();
|
|
72428
|
+
}
|
|
72429
|
+
var AttachmentRefSchema = discriminatedUnion("kind", [object({
|
|
72430
|
+
kind: literal("snapshot"),
|
|
72431
|
+
deviceId: number$1().int().positive()
|
|
72432
|
+
}), object({
|
|
72433
|
+
kind: literal("track"),
|
|
72434
|
+
trackId: string().min(1),
|
|
72435
|
+
deviceId: number$1().int().positive()
|
|
72436
|
+
})]);
|
|
72437
|
+
var MediaSentSchema = object({
|
|
72438
|
+
kind: _enum(["snapshot", "track"]),
|
|
72439
|
+
mimeType: string(),
|
|
72440
|
+
sizeBytes: number$1().int().nonnegative(),
|
|
72441
|
+
mediaKind: string().optional(),
|
|
72442
|
+
capturedAt: number$1().optional(),
|
|
72443
|
+
thumbnailBase64: string().optional()
|
|
72444
|
+
});
|
|
72445
|
+
var StoredChatMessageSchema = object({
|
|
72446
|
+
role: _enum(["user", "assistant"]),
|
|
72447
|
+
content: string(),
|
|
72448
|
+
mediaSent: MediaSentSchema.nullable().optional(),
|
|
72449
|
+
thumbnailBase64: string().optional(),
|
|
72450
|
+
attachment: AttachmentRefSchema.optional()
|
|
72451
|
+
});
|
|
72452
|
+
var StoredConversationSchema = object({
|
|
72453
|
+
id: string(),
|
|
72454
|
+
userId: string(),
|
|
72455
|
+
title: string(),
|
|
72456
|
+
createdAt: number$1(),
|
|
72457
|
+
updatedAt: number$1(),
|
|
72458
|
+
messages: array(StoredChatMessageSchema)
|
|
72459
|
+
});
|
|
72460
|
+
function withoutNulls(data) {
|
|
72461
|
+
const out = {};
|
|
72462
|
+
for (const [key, value] of Object.entries(data)) if (value !== null) out[key] = value;
|
|
72463
|
+
return out;
|
|
72464
|
+
}
|
|
72465
|
+
function parseConversation(id, data) {
|
|
72466
|
+
const parsed = StoredConversationSchema.safeParse({
|
|
72467
|
+
...withoutNulls(data),
|
|
72468
|
+
id
|
|
72469
|
+
});
|
|
72470
|
+
return parsed.success ? parsed.data : null;
|
|
72471
|
+
}
|
|
72472
|
+
var ChatStore = class {
|
|
72473
|
+
store;
|
|
72474
|
+
now;
|
|
72475
|
+
constructor(store, now = Date.now) {
|
|
72476
|
+
this.store = store;
|
|
72477
|
+
this.now = now;
|
|
72478
|
+
}
|
|
72479
|
+
async init() {
|
|
72480
|
+
await this.store.declareCollection(CHATS_COLLECTION, KV_BLOB_COLUMNS);
|
|
72481
|
+
}
|
|
72482
|
+
async create(userId, messages, at = this.now()) {
|
|
72483
|
+
const id = (0, node_crypto.randomUUID)();
|
|
72484
|
+
const title = titleFromFirstUserMessage(messages.find((m) => m.role === "user")?.content ?? "");
|
|
72485
|
+
const conversation = {
|
|
72486
|
+
id,
|
|
72487
|
+
userId,
|
|
72488
|
+
title: title.length > 0 ? title : "New chat",
|
|
72489
|
+
createdAt: at,
|
|
72490
|
+
updatedAt: at,
|
|
72491
|
+
messages: [...messages]
|
|
72492
|
+
};
|
|
72493
|
+
await this.store.insert(CHATS_COLLECTION, {
|
|
72494
|
+
id: chatsRowId(userId, id),
|
|
72495
|
+
data: conversation
|
|
72496
|
+
});
|
|
72497
|
+
return conversation;
|
|
72498
|
+
}
|
|
72499
|
+
async list(userId) {
|
|
72500
|
+
return (await this.owned(userId)).map((c) => ({
|
|
72501
|
+
id: c.id,
|
|
72502
|
+
title: c.title,
|
|
72503
|
+
createdAt: c.createdAt,
|
|
72504
|
+
updatedAt: c.updatedAt
|
|
72505
|
+
})).sort((a, b) => b.updatedAt - a.updatedAt);
|
|
72506
|
+
}
|
|
72507
|
+
async get(userId, conversationId) {
|
|
72508
|
+
const match = (await this.store.query(CHATS_COLLECTION)).find((r) => r.id === chatsRowId(userId, conversationId));
|
|
72509
|
+
if (match === void 0) return null;
|
|
72510
|
+
const parsed = parseConversation(conversationId, match.data);
|
|
72511
|
+
if (parsed === null || parsed.userId !== userId) return null;
|
|
72512
|
+
return parsed;
|
|
72513
|
+
}
|
|
72514
|
+
async append(userId, conversationId, messages, at = this.now()) {
|
|
72515
|
+
const current = await this.get(userId, conversationId);
|
|
72516
|
+
if (current === null) return null;
|
|
72517
|
+
const next = {
|
|
72518
|
+
...current,
|
|
72519
|
+
updatedAt: at,
|
|
72520
|
+
messages: [...current.messages, ...messages]
|
|
72521
|
+
};
|
|
72522
|
+
await this.store.update(CHATS_COLLECTION, chatsRowId(userId, conversationId), next);
|
|
72523
|
+
return next;
|
|
72524
|
+
}
|
|
72525
|
+
async prune(userId, retention, now = this.now()) {
|
|
72526
|
+
const owned = await this.owned(userId);
|
|
72527
|
+
return this.pruneRows(owned, retention, now);
|
|
72528
|
+
}
|
|
72529
|
+
async pruneAll(retention, now = this.now()) {
|
|
72530
|
+
const all = await this.allValid();
|
|
72531
|
+
return this.pruneRows(all, retention, now);
|
|
72532
|
+
}
|
|
72533
|
+
async pruneRows(rows, retention, now) {
|
|
72534
|
+
const doomed = /* @__PURE__ */ new Set();
|
|
72535
|
+
const maxAgeMs = retention.maxAgeMs;
|
|
72536
|
+
if (maxAgeMs !== void 0 && maxAgeMs > 0) {
|
|
72537
|
+
const cutoff = now - maxAgeMs;
|
|
72538
|
+
for (const row of rows) if (row.updatedAt < cutoff) doomed.add(chatsRowId(row.userId, row.id));
|
|
72539
|
+
}
|
|
72540
|
+
const maxCount = retention.maxCount;
|
|
72541
|
+
if (maxCount !== void 0 && maxCount > 0) {
|
|
72542
|
+
const byUser = /* @__PURE__ */ new Map();
|
|
72543
|
+
for (const row of rows) {
|
|
72544
|
+
if (doomed.has(chatsRowId(row.userId, row.id))) continue;
|
|
72545
|
+
const list = byUser.get(row.userId) ?? [];
|
|
72546
|
+
list.push(row);
|
|
72547
|
+
byUser.set(row.userId, list);
|
|
72548
|
+
}
|
|
72549
|
+
for (const group of byUser.values()) {
|
|
72550
|
+
const sorted = [...group].sort((a, b) => b.updatedAt - a.updatedAt);
|
|
72551
|
+
for (const extra of sorted.slice(maxCount)) doomed.add(chatsRowId(extra.userId, extra.id));
|
|
72552
|
+
}
|
|
72553
|
+
}
|
|
72554
|
+
for (const id of doomed) await this.store.remove(CHATS_COLLECTION, id);
|
|
72555
|
+
return doomed.size;
|
|
72556
|
+
}
|
|
72557
|
+
async owned(userId) {
|
|
72558
|
+
return (await this.allValid()).filter((c) => c.userId === userId);
|
|
72559
|
+
}
|
|
72560
|
+
async allValid() {
|
|
72561
|
+
const rows = await this.store.query(CHATS_COLLECTION);
|
|
72562
|
+
const out = [];
|
|
72563
|
+
for (const row of rows) {
|
|
72564
|
+
const slash = row.id.indexOf("/");
|
|
72565
|
+
const parsed = parseConversation(slash >= 0 ? row.id.slice(slash + 1) : row.id, row.data);
|
|
72566
|
+
if (parsed !== null) out.push(parsed);
|
|
72567
|
+
}
|
|
72568
|
+
return out;
|
|
72569
|
+
}
|
|
72570
|
+
};
|
|
72571
|
+
//#endregion
|
|
72348
72572
|
//#region src/assembly.ts
|
|
72349
72573
|
/**
|
|
72350
72574
|
* Registration assembly (the hub/agent split, spec §1). Every node running
|
|
@@ -72376,9 +72600,11 @@ async function assembleAi(deps) {
|
|
|
72376
72600
|
const store = new ProfileStore(deps.settingsPort);
|
|
72377
72601
|
const defaults = new DefaultsStore(deps.settingsPort);
|
|
72378
72602
|
const usage = new UsageStore(deps.settingsPort, Date.now, deps.logger.child("llm-usage"));
|
|
72603
|
+
const chats = new ChatStore(deps.settingsPort, Date.now);
|
|
72379
72604
|
await store.init();
|
|
72380
72605
|
await defaults.init();
|
|
72381
72606
|
await usage.init();
|
|
72607
|
+
await chats.init();
|
|
72382
72608
|
await store.ensureSeeded();
|
|
72383
72609
|
const llmProvider = createLlmProvider({
|
|
72384
72610
|
store,
|
|
@@ -72401,6 +72627,7 @@ async function assembleAi(deps) {
|
|
|
72401
72627
|
llmProvider,
|
|
72402
72628
|
store,
|
|
72403
72629
|
usage,
|
|
72630
|
+
chats,
|
|
72404
72631
|
prune: (retentionDays) => usage.prune(retentionDays)
|
|
72405
72632
|
};
|
|
72406
72633
|
}
|
|
@@ -72541,7 +72768,14 @@ var TestChatRequestSchema = object({
|
|
|
72541
72768
|
/** See the timeout block above. Applies to a VISION turn identically —
|
|
72542
72769
|
* vision inference is the slower one, so a bound tuned on text is a bound
|
|
72543
72770
|
* that only fails on the interesting case. */
|
|
72544
|
-
firstTokenTimeoutMs: number$1().int().positive().max(TEST_CHAT_MAX_FIRST_TOKEN_TIMEOUT_MS).default(TEST_CHAT_DEFAULT_FIRST_TOKEN_TIMEOUT_MS)
|
|
72771
|
+
firstTokenTimeoutMs: number$1().int().positive().max(TEST_CHAT_MAX_FIRST_TOKEN_TIMEOUT_MS).default(TEST_CHAT_DEFAULT_FIRST_TOKEN_TIMEOUT_MS),
|
|
72772
|
+
/**
|
|
72773
|
+
* Persist this turn into the caller's private chat history. Omitted or
|
|
72774
|
+
* false is throwaway — today's behaviour, nothing written.
|
|
72775
|
+
*/
|
|
72776
|
+
persist: boolean().optional(),
|
|
72777
|
+
/** Resume this conversation. Ignored unless `persist` is true. */
|
|
72778
|
+
conversationId: string().min(1).optional()
|
|
72545
72779
|
}).strict();
|
|
72546
72780
|
/**
|
|
72547
72781
|
* Below this, a turn that CARRIED an image almost certainly did not deliver it.
|
|
@@ -72572,7 +72806,9 @@ var TestChatAttachmentSchema = object({
|
|
|
72572
72806
|
/** Track attachments only: the `MediaFileKind` that won `TRACK_MEDIA_PREFERENCE`. */
|
|
72573
72807
|
mediaKind: string().optional(),
|
|
72574
72808
|
/** Epoch ms of the frame, when the source knows it. */
|
|
72575
|
-
capturedAt: number$1().optional()
|
|
72809
|
+
capturedAt: number$1().optional(),
|
|
72810
|
+
/** JPEG of what was actually sent — present when reopening history. */
|
|
72811
|
+
thumbnailBase64: string().optional()
|
|
72576
72812
|
});
|
|
72577
72813
|
/**
|
|
72578
72814
|
* What the run is doing right now — so a wait reads as progress, not as a hang.
|
|
@@ -72618,7 +72854,9 @@ var TestChatEventSchema = discriminatedUnion("kind", [
|
|
|
72618
72854
|
* arrives at once. Said out loud rather than faked. */
|
|
72619
72855
|
streamed: boolean(),
|
|
72620
72856
|
/** Echoed so the page states the bound it is really working under. */
|
|
72621
|
-
firstTokenTimeoutMs: number$1()
|
|
72857
|
+
firstTokenTimeoutMs: number$1(),
|
|
72858
|
+
/** Set when this turn was (or will be) persisted — so the page can resume. */
|
|
72859
|
+
conversationId: string().optional()
|
|
72622
72860
|
}),
|
|
72623
72861
|
object({
|
|
72624
72862
|
kind: literal("token"),
|
|
@@ -72629,7 +72867,8 @@ var TestChatEventSchema = discriminatedUnion("kind", [
|
|
|
72629
72867
|
inputTokens: number$1(),
|
|
72630
72868
|
outputTokens: number$1(),
|
|
72631
72869
|
latencyMs: number$1(),
|
|
72632
|
-
truncated: boolean()
|
|
72870
|
+
truncated: boolean(),
|
|
72871
|
+
conversationId: string().optional()
|
|
72633
72872
|
}),
|
|
72634
72873
|
object({
|
|
72635
72874
|
kind: literal("error"),
|
|
@@ -72642,6 +72881,59 @@ function encodeEvent(event) {
|
|
|
72642
72881
|
return `${JSON.stringify(event)}\n`;
|
|
72643
72882
|
}
|
|
72644
72883
|
//#endregion
|
|
72884
|
+
//#region src/test-chat/persist.ts
|
|
72885
|
+
/**
|
|
72886
|
+
* The messages SOURCE for a test-chat turn: in-page transcript (throwaway)
|
|
72887
|
+
* versus stored history (persistent). The NDJSON transport is unchanged.
|
|
72888
|
+
*/
|
|
72889
|
+
function asWireMessages(messages) {
|
|
72890
|
+
return messages.map((m) => ({
|
|
72891
|
+
role: m.role,
|
|
72892
|
+
content: m.content
|
|
72893
|
+
}));
|
|
72894
|
+
}
|
|
72895
|
+
function isPersistent(request) {
|
|
72896
|
+
return request.persist === true;
|
|
72897
|
+
}
|
|
72898
|
+
async function resolveStreamMessages(store, userId, request) {
|
|
72899
|
+
if (!isPersistent(request) || request.conversationId === void 0) return {
|
|
72900
|
+
ok: true,
|
|
72901
|
+
conversationId: request.conversationId ?? null,
|
|
72902
|
+
messages: request.messages
|
|
72903
|
+
};
|
|
72904
|
+
const existing = await store.get(userId, request.conversationId);
|
|
72905
|
+
if (existing === null) return {
|
|
72906
|
+
ok: false,
|
|
72907
|
+
status: 404,
|
|
72908
|
+
error: "conversation not found"
|
|
72909
|
+
};
|
|
72910
|
+
const combined = [...asWireMessages(existing.messages), ...request.messages];
|
|
72911
|
+
const messages = combined.length > 40 ? combined.slice(-40) : combined;
|
|
72912
|
+
return {
|
|
72913
|
+
ok: true,
|
|
72914
|
+
conversationId: existing.id,
|
|
72915
|
+
messages
|
|
72916
|
+
};
|
|
72917
|
+
}
|
|
72918
|
+
function userTurnsToStore(request, commit) {
|
|
72919
|
+
return [{
|
|
72920
|
+
role: "user",
|
|
72921
|
+
content: [...request.messages].reverse().find((m) => m.role === "user")?.content ?? "",
|
|
72922
|
+
...commit.mediaSent !== null ? { mediaSent: commit.mediaSent } : {},
|
|
72923
|
+
...commit.thumbnailBase64 !== void 0 ? { thumbnailBase64: commit.thumbnailBase64 } : {},
|
|
72924
|
+
...request.attachment !== void 0 ? { attachment: request.attachment } : {}
|
|
72925
|
+
}, {
|
|
72926
|
+
role: "assistant",
|
|
72927
|
+
content: commit.assistantContent
|
|
72928
|
+
}];
|
|
72929
|
+
}
|
|
72930
|
+
async function commitPersistentTurn(store, userId, request, commit) {
|
|
72931
|
+
if (!isPersistent(request)) return null;
|
|
72932
|
+
const turns = userTurnsToStore(request, commit);
|
|
72933
|
+
if (request.conversationId !== void 0) return store.append(userId, request.conversationId, turns);
|
|
72934
|
+
return store.create(userId, turns);
|
|
72935
|
+
}
|
|
72936
|
+
//#endregion
|
|
72645
72937
|
//#region src/test-chat/media.ts
|
|
72646
72938
|
/**
|
|
72647
72939
|
* Which stored media a track attachment is allowed to be, best first.
|
|
@@ -72894,7 +73186,10 @@ async function runTestChatStream(deps, request, emit, signal) {
|
|
|
72894
73186
|
await fail(outcome.failure, "ai test chat: attachment could not be resolved — vision turn refused", { attachmentKind: request.attachment.kind });
|
|
72895
73187
|
return;
|
|
72896
73188
|
}
|
|
72897
|
-
attachment =
|
|
73189
|
+
attachment = {
|
|
73190
|
+
...outcome.resolved.attachment,
|
|
73191
|
+
thumbnailBase64: Buffer.from(outcome.resolved.image.bytes).toString("base64")
|
|
73192
|
+
};
|
|
72898
73193
|
images = [outcome.resolved.image];
|
|
72899
73194
|
imageCount = 1;
|
|
72900
73195
|
}
|
|
@@ -73111,6 +73406,49 @@ async function runTestChatStream(deps, request, emit, signal) {
|
|
|
73111
73406
|
});
|
|
73112
73407
|
}
|
|
73113
73408
|
//#endregion
|
|
73409
|
+
//#region src/test-chat/user-from-request.ts
|
|
73410
|
+
var SESSION_COOKIE = "camstack_session";
|
|
73411
|
+
function cookieValue(header, name) {
|
|
73412
|
+
if (header === void 0 || header.length === 0) return null;
|
|
73413
|
+
for (const pair of header.split(";")) {
|
|
73414
|
+
const eq = pair.indexOf("=");
|
|
73415
|
+
if (eq === -1) continue;
|
|
73416
|
+
if (pair.slice(0, eq).trim() !== name) continue;
|
|
73417
|
+
const raw = pair.slice(eq + 1).trim();
|
|
73418
|
+
if (raw.length === 0) return null;
|
|
73419
|
+
try {
|
|
73420
|
+
return decodeURIComponent(raw);
|
|
73421
|
+
} catch {
|
|
73422
|
+
return raw;
|
|
73423
|
+
}
|
|
73424
|
+
}
|
|
73425
|
+
return null;
|
|
73426
|
+
}
|
|
73427
|
+
function userIdFromJwt(token) {
|
|
73428
|
+
const parts = token.split(".");
|
|
73429
|
+
if (parts.length < 2 || parts[1] === void 0) return null;
|
|
73430
|
+
try {
|
|
73431
|
+
const json = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
73432
|
+
const payload = JSON.parse(json);
|
|
73433
|
+
return typeof payload.userId === "string" && payload.userId.length > 0 ? payload.userId : null;
|
|
73434
|
+
} catch {
|
|
73435
|
+
return null;
|
|
73436
|
+
}
|
|
73437
|
+
}
|
|
73438
|
+
function bearerToken(header) {
|
|
73439
|
+
const raw = Array.isArray(header) ? header[0] : header;
|
|
73440
|
+
if (raw === void 0 || !raw.startsWith("Bearer ")) return null;
|
|
73441
|
+
const token = raw.slice(7).trim();
|
|
73442
|
+
return token.length > 0 ? token : null;
|
|
73443
|
+
}
|
|
73444
|
+
function readUserIdFromRequest(req) {
|
|
73445
|
+
const fromAuth = bearerToken(req.headers.authorization);
|
|
73446
|
+
if (fromAuth !== null) return userIdFromJwt(fromAuth);
|
|
73447
|
+
const fromCookie = cookieValue(Array.isArray(req.headers.cookie) ? req.headers.cookie[0] : req.headers.cookie, SESSION_COOKIE);
|
|
73448
|
+
if (fromCookie === null) return null;
|
|
73449
|
+
return userIdFromJwt(fromCookie);
|
|
73450
|
+
}
|
|
73451
|
+
//#endregion
|
|
73114
73452
|
//#region src/test-chat/plane.ts
|
|
73115
73453
|
/** Bounded so a malformed or hostile body cannot buy memory. */
|
|
73116
73454
|
var MAX_BODY_BYTES = 256 * 1024;
|
|
@@ -73134,61 +73472,164 @@ function sendJson(res, status, body) {
|
|
|
73134
73472
|
});
|
|
73135
73473
|
res.end(text);
|
|
73136
73474
|
}
|
|
73137
|
-
function
|
|
73138
|
-
return
|
|
73139
|
-
|
|
73140
|
-
|
|
73475
|
+
function pathParts(url) {
|
|
73476
|
+
return ((url ?? "/").split("?")[0] ?? "/").split("/").filter((p) => p.length > 0);
|
|
73477
|
+
}
|
|
73478
|
+
function requireUser(req, res) {
|
|
73479
|
+
const userId = readUserIdFromRequest(req);
|
|
73480
|
+
if (userId === null) {
|
|
73481
|
+
sendJson(res, 401, { error: "unauthenticated" });
|
|
73482
|
+
return null;
|
|
73483
|
+
}
|
|
73484
|
+
return userId;
|
|
73485
|
+
}
|
|
73486
|
+
async function handleList(deps, req, res) {
|
|
73487
|
+
if (deps.chats === void 0) {
|
|
73488
|
+
sendJson(res, 503, { error: "chat store unavailable" });
|
|
73489
|
+
return;
|
|
73490
|
+
}
|
|
73491
|
+
const userId = requireUser(req, res);
|
|
73492
|
+
if (userId === null) return;
|
|
73493
|
+
sendJson(res, 200, { chats: await deps.chats.list(userId) });
|
|
73494
|
+
}
|
|
73495
|
+
async function handleGet(deps, req, res, conversationId) {
|
|
73496
|
+
if (deps.chats === void 0) {
|
|
73497
|
+
sendJson(res, 503, { error: "chat store unavailable" });
|
|
73498
|
+
return;
|
|
73499
|
+
}
|
|
73500
|
+
const userId = requireUser(req, res);
|
|
73501
|
+
if (userId === null) return;
|
|
73502
|
+
const conversation = await deps.chats.get(userId, conversationId);
|
|
73503
|
+
if (conversation === null) {
|
|
73504
|
+
sendJson(res, 404, { error: "conversation not found" });
|
|
73505
|
+
return;
|
|
73506
|
+
}
|
|
73507
|
+
sendJson(res, 200, conversation);
|
|
73508
|
+
}
|
|
73509
|
+
async function handleStream(deps, req, res) {
|
|
73510
|
+
const raw = await readBody(req);
|
|
73511
|
+
if (raw === null) {
|
|
73512
|
+
sendJson(res, 413, { error: "request body too large" });
|
|
73513
|
+
return;
|
|
73514
|
+
}
|
|
73515
|
+
let parsedJson;
|
|
73516
|
+
try {
|
|
73517
|
+
parsedJson = JSON.parse(raw);
|
|
73518
|
+
} catch {
|
|
73519
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
73520
|
+
return;
|
|
73521
|
+
}
|
|
73522
|
+
const parsed = TestChatRequestSchema.safeParse(parsedJson);
|
|
73523
|
+
if (!parsed.success) {
|
|
73524
|
+
const detail = parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ");
|
|
73525
|
+
deps.logger.warn("ai test chat: request rejected — turn never ran", { meta: { detail } });
|
|
73526
|
+
sendJson(res, 400, {
|
|
73527
|
+
error: "invalid request",
|
|
73528
|
+
detail
|
|
73529
|
+
});
|
|
73530
|
+
return;
|
|
73531
|
+
}
|
|
73532
|
+
let request = parsed.data;
|
|
73533
|
+
if (request.persist === true) {
|
|
73534
|
+
if (deps.chats === void 0) {
|
|
73535
|
+
sendJson(res, 503, { error: "chat store unavailable" });
|
|
73141
73536
|
return;
|
|
73142
73537
|
}
|
|
73143
|
-
const
|
|
73144
|
-
if (
|
|
73145
|
-
|
|
73538
|
+
const userId = requireUser(req, res);
|
|
73539
|
+
if (userId === null) return;
|
|
73540
|
+
const resolved = await resolveStreamMessages(deps.chats, userId, request);
|
|
73541
|
+
if (!resolved.ok) {
|
|
73542
|
+
sendJson(res, resolved.status, { error: resolved.error });
|
|
73146
73543
|
return;
|
|
73147
73544
|
}
|
|
73148
|
-
|
|
73149
|
-
|
|
73150
|
-
|
|
73151
|
-
}
|
|
73152
|
-
|
|
73545
|
+
request = {
|
|
73546
|
+
...request,
|
|
73547
|
+
messages: resolved.messages
|
|
73548
|
+
};
|
|
73549
|
+
}
|
|
73550
|
+
const controller = new AbortController();
|
|
73551
|
+
res.on("close", () => controller.abort());
|
|
73552
|
+
res.writeHead(200, {
|
|
73553
|
+
"content-type": "application/x-ndjson; charset=utf-8",
|
|
73554
|
+
"cache-control": "no-store, no-transform",
|
|
73555
|
+
"content-encoding": "identity",
|
|
73556
|
+
"x-no-compression": "1",
|
|
73557
|
+
connection: "keep-alive"
|
|
73558
|
+
});
|
|
73559
|
+
res.flushHeaders();
|
|
73560
|
+
let assistantText = "";
|
|
73561
|
+
let mediaSent = null;
|
|
73562
|
+
let sawDone = false;
|
|
73563
|
+
let pendingDone = null;
|
|
73564
|
+
const emit = (event) => {
|
|
73565
|
+
if (res.writableEnded) return;
|
|
73566
|
+
if (event.kind === "token") assistantText += event.text;
|
|
73567
|
+
if (event.kind === "meta") mediaSent = event.mediaSent;
|
|
73568
|
+
if (event.kind === "done") {
|
|
73569
|
+
sawDone = true;
|
|
73570
|
+
pendingDone = event;
|
|
73153
73571
|
return;
|
|
73154
73572
|
}
|
|
73155
|
-
|
|
73156
|
-
|
|
73157
|
-
|
|
73158
|
-
|
|
73159
|
-
|
|
73160
|
-
|
|
73161
|
-
|
|
73162
|
-
|
|
73573
|
+
res.write(encodeEvent(event));
|
|
73574
|
+
};
|
|
73575
|
+
try {
|
|
73576
|
+
await runTestChatStream(deps.stream, request, emit, controller.signal);
|
|
73577
|
+
let conversationId;
|
|
73578
|
+
if (sawDone && request.persist === true && deps.chats !== void 0) {
|
|
73579
|
+
const userId = readUserIdFromRequest(req);
|
|
73580
|
+
if (userId !== null) try {
|
|
73581
|
+
conversationId = (await commitPersistentTurn(deps.chats, userId, request, {
|
|
73582
|
+
assistantContent: assistantText,
|
|
73583
|
+
mediaSent,
|
|
73584
|
+
thumbnailBase64: thumbnailFrom(mediaSent)
|
|
73585
|
+
}))?.id;
|
|
73586
|
+
const retention = deps.retention?.();
|
|
73587
|
+
if (retention !== void 0) await deps.chats.prune(userId, retention).catch(() => void 0);
|
|
73588
|
+
} catch (cause) {
|
|
73589
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
73590
|
+
deps.logger.warn("ai test chat: persist failed — turn was not saved", { meta: { error: message } });
|
|
73591
|
+
}
|
|
73592
|
+
}
|
|
73593
|
+
if (pendingDone !== null) emitDone(res, pendingDone, conversationId);
|
|
73594
|
+
} catch (cause) {
|
|
73595
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
73596
|
+
deps.logger.error("ai test chat: run threw — stream closed with an error event", { meta: { error: message } });
|
|
73597
|
+
if (!res.writableEnded) res.write(encodeEvent({
|
|
73598
|
+
kind: "error",
|
|
73599
|
+
code: "adapter-error",
|
|
73600
|
+
message
|
|
73601
|
+
}));
|
|
73602
|
+
} finally {
|
|
73603
|
+
if (!res.writableEnded) res.end();
|
|
73604
|
+
}
|
|
73605
|
+
}
|
|
73606
|
+
function thumbnailFrom(sent) {
|
|
73607
|
+
return sent === null ? void 0 : sent.thumbnailBase64;
|
|
73608
|
+
}
|
|
73609
|
+
function emitDone(res, done, conversationId) {
|
|
73610
|
+
if (res.writableEnded) return;
|
|
73611
|
+
res.write(encodeEvent(conversationId !== void 0 ? {
|
|
73612
|
+
...done,
|
|
73613
|
+
conversationId
|
|
73614
|
+
} : done));
|
|
73615
|
+
}
|
|
73616
|
+
function createTestChatPlaneHandler(deps) {
|
|
73617
|
+
return async (req, res) => {
|
|
73618
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
73619
|
+
const parts = pathParts(req.url);
|
|
73620
|
+
if (method === "GET" && parts[0] === "chats" && parts.length === 1) {
|
|
73621
|
+
await handleList(deps, req, res);
|
|
73163
73622
|
return;
|
|
73164
73623
|
}
|
|
73165
|
-
|
|
73166
|
-
|
|
73167
|
-
|
|
73168
|
-
"content-type": "application/x-ndjson; charset=utf-8",
|
|
73169
|
-
"cache-control": "no-store, no-transform",
|
|
73170
|
-
"content-encoding": "identity",
|
|
73171
|
-
"x-no-compression": "1",
|
|
73172
|
-
connection: "keep-alive"
|
|
73173
|
-
});
|
|
73174
|
-
res.flushHeaders();
|
|
73175
|
-
const emit = (event) => {
|
|
73176
|
-
if (res.writableEnded) return;
|
|
73177
|
-
res.write(encodeEvent(event));
|
|
73178
|
-
};
|
|
73179
|
-
try {
|
|
73180
|
-
await runTestChatStream(deps.stream, parsed.data, emit, controller.signal);
|
|
73181
|
-
} catch (cause) {
|
|
73182
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
73183
|
-
deps.logger.error("ai test chat: run threw — stream closed with an error event", { meta: { error: message } });
|
|
73184
|
-
emit({
|
|
73185
|
-
kind: "error",
|
|
73186
|
-
code: "adapter-error",
|
|
73187
|
-
message
|
|
73188
|
-
});
|
|
73189
|
-
} finally {
|
|
73190
|
-
if (!res.writableEnded) res.end();
|
|
73624
|
+
if (method === "GET" && parts[0] === "chats" && parts.length === 2 && parts[1] !== void 0) {
|
|
73625
|
+
await handleGet(deps, req, res, parts[1]);
|
|
73626
|
+
return;
|
|
73191
73627
|
}
|
|
73628
|
+
if (method !== "POST" || parts.length > 0) {
|
|
73629
|
+
sendJson(res, 405, { error: "POST only" });
|
|
73630
|
+
return;
|
|
73631
|
+
}
|
|
73632
|
+
await handleStream(deps, req, res);
|
|
73192
73633
|
};
|
|
73193
73634
|
}
|
|
73194
73635
|
//#endregion
|
|
@@ -73203,13 +73644,40 @@ function createTestChatPlaneHandler(deps) {
|
|
|
73203
73644
|
var POST_BOOT_DELAY_MS = 15e3;
|
|
73204
73645
|
var PRUNE_INTERVAL_MS = 1440 * 60 * 1e3;
|
|
73205
73646
|
var USAGE_RETENTION_DAYS = 90;
|
|
73647
|
+
var DAY_MS = 1440 * 60 * 1e3;
|
|
73206
73648
|
var AiAddon = class extends BaseAddon {
|
|
73207
73649
|
supervisor;
|
|
73208
73650
|
timers = [];
|
|
73209
73651
|
/** Hub only — see `serveTestChatPlane`. */
|
|
73210
73652
|
testChatPlane = null;
|
|
73211
73653
|
constructor() {
|
|
73212
|
-
super({
|
|
73654
|
+
super({
|
|
73655
|
+
chatRetentionMaxAgeDays: 30,
|
|
73656
|
+
chatRetentionMaxCount: 50
|
|
73657
|
+
});
|
|
73658
|
+
}
|
|
73659
|
+
globalSettingsSchema() {
|
|
73660
|
+
return this.schema({ sections: [{
|
|
73661
|
+
id: "chats",
|
|
73662
|
+
title: "Persistent chats",
|
|
73663
|
+
fields: [this.field({
|
|
73664
|
+
type: "number",
|
|
73665
|
+
key: "chatRetentionMaxAgeDays",
|
|
73666
|
+
label: "Chat retention (days)",
|
|
73667
|
+
description: "Drop persisted chats older than this. 0 keeps them forever.",
|
|
73668
|
+
min: 0,
|
|
73669
|
+
step: 1,
|
|
73670
|
+
default: 30
|
|
73671
|
+
}), this.field({
|
|
73672
|
+
type: "number",
|
|
73673
|
+
key: "chatRetentionMaxCount",
|
|
73674
|
+
label: "Max chats per user",
|
|
73675
|
+
description: "Keep only the newest N conversations per user. 0 means no cap.",
|
|
73676
|
+
min: 0,
|
|
73677
|
+
step: 1,
|
|
73678
|
+
default: 50
|
|
73679
|
+
})]
|
|
73680
|
+
}] });
|
|
73213
73681
|
}
|
|
73214
73682
|
async onInitialize() {
|
|
73215
73683
|
const ctx = this.ctx;
|
|
@@ -73346,7 +73814,11 @@ var AiAddon = class extends BaseAddon {
|
|
|
73346
73814
|
access: "authenticated",
|
|
73347
73815
|
handler: createTestChatPlaneHandler({
|
|
73348
73816
|
stream,
|
|
73349
|
-
logger
|
|
73817
|
+
logger,
|
|
73818
|
+
...assembly.chats !== void 0 ? {
|
|
73819
|
+
chats: assembly.chats,
|
|
73820
|
+
retention: () => this.chatRetention()
|
|
73821
|
+
} : {}
|
|
73350
73822
|
})
|
|
73351
73823
|
}) ?? null;
|
|
73352
73824
|
this.ctx.logger.info("ai test-chat data-plane served", { meta: {
|
|
@@ -73354,11 +73826,21 @@ var AiAddon = class extends BaseAddon {
|
|
|
73354
73826
|
path: `/addon/${this.ctx.id}/${TEST_CHAT_PREFIX}`
|
|
73355
73827
|
} });
|
|
73356
73828
|
}
|
|
73829
|
+
chatRetention() {
|
|
73830
|
+
const days = this.config.chatRetentionMaxAgeDays;
|
|
73831
|
+
const count = this.config.chatRetentionMaxCount;
|
|
73832
|
+
return {
|
|
73833
|
+
...days > 0 ? { maxAgeMs: days * DAY_MS } : {},
|
|
73834
|
+
...count > 0 ? { maxCount: count } : {}
|
|
73835
|
+
};
|
|
73836
|
+
}
|
|
73357
73837
|
schedulePostBoot(assembly) {
|
|
73358
73838
|
const kick = setTimeout(() => {
|
|
73359
73839
|
assembly.prune?.(USAGE_RETENTION_DAYS).catch(() => void 0);
|
|
73840
|
+
assembly.chats?.pruneAll(this.chatRetention()).catch(() => void 0);
|
|
73360
73841
|
setInterval(() => {
|
|
73361
73842
|
assembly.prune?.(USAGE_RETENTION_DAYS).catch(() => void 0);
|
|
73843
|
+
assembly.chats?.pruneAll(this.chatRetention()).catch(() => void 0);
|
|
73362
73844
|
}, PRUNE_INTERVAL_MS).unref?.();
|
|
73363
73845
|
this.autoStartRuntimes(assembly);
|
|
73364
73846
|
}, POST_BOOT_DELAY_MS);
|
|
@@ -73389,7 +73871,9 @@ var AiAddon = class extends BaseAddon {
|
|
|
73389
73871
|
exports.AI_ADDON_ID = AI_ADDON_ID;
|
|
73390
73872
|
exports.AiAddon = AiAddon;
|
|
73391
73873
|
exports.default = AiAddon;
|
|
73874
|
+
exports.CHATS_COLLECTION = CHATS_COLLECTION;
|
|
73392
73875
|
exports.CONSUMER_RETRY_POLICY = CONSUMER_RETRY_POLICY;
|
|
73876
|
+
exports.ChatStore = ChatStore;
|
|
73393
73877
|
exports.LLM_MODEL_CATALOG = LLM_MODEL_CATALOG;
|
|
73394
73878
|
exports.LlamaSupervisor = LlamaSupervisor;
|
|
73395
73879
|
exports.LlmErrorCodeSchema = LlmErrorCodeSchema;
|
|
@@ -73406,6 +73890,7 @@ exports.TestChatEventSchema = TestChatEventSchema;
|
|
|
73406
73890
|
exports.TestChatRequestSchema = TestChatRequestSchema;
|
|
73407
73891
|
exports.boolean = boolean;
|
|
73408
73892
|
exports.catalogById = catalogById;
|
|
73893
|
+
exports.chatsRowId = chatsRowId;
|
|
73409
73894
|
exports.createApiSettingsStorePort = createApiSettingsStorePort;
|
|
73410
73895
|
exports.createDefaultModelOps = createDefaultModelOps;
|
|
73411
73896
|
exports.createLlmClient = createLlmClient;
|
|
@@ -73437,3 +73922,4 @@ exports.resolveImage = resolveImage;
|
|
|
73437
73922
|
exports.resolveRetryPolicy = resolveRetryPolicy;
|
|
73438
73923
|
exports.runTestChatStream = runTestChatStream;
|
|
73439
73924
|
exports.string = string;
|
|
73925
|
+
exports.titleFromFirstUserMessage = titleFromFirstUserMessage;
|