@ykurilov/agent-chat-core 0.56.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +2 -0
- package/dist/access.js +2 -2
- package/dist/{chunk-7M7M7DAN.js → chunk-BS2HDKZB.js} +502 -28
- package/dist/chunk-BS2HDKZB.js.map +1 -0
- package/dist/{chunk-SNPF4BPB.js → chunk-FBULM4V6.js} +2 -2
- package/dist/{chunk-PWGI6NH6.js → chunk-FOAIG4DY.js} +1 -1
- package/dist/chunk-FOAIG4DY.js.map +1 -0
- package/dist/index.js +29 -3
- package/dist/index.js.map +1 -1
- package/dist/storage-contract.js +87 -5
- package/dist/storage-contract.js.map +1 -1
- package/dist/types/agent-run/garbled-tail.d.ts +69 -0
- package/dist/types/agent-run/reply-checks.d.ts +3 -1
- package/dist/types/agent-run/staff-replies.d.ts +7 -0
- package/dist/types/bursts/message-bursts.d.ts +12 -0
- package/dist/types/chat-events/chat-events.d.ts +7 -0
- package/dist/types/chat-service.d.ts +18 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/storage/chat-storage.d.ts +47 -0
- package/dist/types/storage/storage-contract.d.ts +5 -0
- package/dist/types/turn-hooks/turn-gate.d.ts +2 -0
- package/dist/types/turn-hooks/turn-hooks.d.ts +7 -0
- package/package.json +1 -1
- package/dist/chunk-7M7M7DAN.js.map +0 -1
- package/dist/chunk-PWGI6NH6.js.map +0 -1
- /package/dist/{chunk-SNPF4BPB.js.map → chunk-FBULM4V6.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeTurnRefusal
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-FOAIG4DY.js";
|
|
4
4
|
|
|
5
5
|
// src/access/resolved-user.ts
|
|
6
6
|
function allowedUser(resolved) {
|
|
@@ -17,4 +17,4 @@ export {
|
|
|
17
17
|
allowedUser,
|
|
18
18
|
accessRefusedResponse
|
|
19
19
|
};
|
|
20
|
-
//# sourceMappingURL=chunk-
|
|
20
|
+
//# sourceMappingURL=chunk-FBULM4V6.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/turn-hooks/turn-hooks.ts"],"sourcesContent":["import type { AgentChannel, Chat, ChatErrorKind, ChatSubject, FinalAnswerStatus, StoredMessage, TokenUsage } from \"../conversation/types\";\n\n/** Что человек делает с чатом, когда приложение решает, пускать ли ход. */\nexport type TurnAction = \"send\" | \"edit\" | \"retry\" | \"regenerate\" | \"approval\" | \"widget\" | \"notice\";\n\n/** Отказ приложения в ходе: код для программы, текст для человека. */\nexport interface TurnRefusal {\n /** Латиница в нижнем регистре, цифры и `.`, `_`, `-`; от 1 до 64 знаков. */\n code: string;\n /** До 300 знаков; длиннее — обрезается. */\n message: string;\n}\n\n/**\n * Сотрудник, который действует в чужом чате (копия чата человека в песочнице). Пока он есть у хода (`model.staff`),\n * инструменты приложения данные `ownerId` только читают: любая запись в них запрещена — это обязанность инструментов.\n */\nexport interface StaffActor {\n id: string;\n}\n\n/** Событие приложения в ответе агента: `type` — 1–100 знаков, `data` — JSON до 16 КБ. */\nexport interface AppEvent {\n type: string;\n data?: unknown;\n}\n\n/** Записать сообщение человека без хода: агент промолчит (рамки дня, пауза сценария). */\nexport interface TurnHold {\n hold: true;\n}\n\n/** Ход с готовым текстом, без модели: путь `reply` из `deliverNotices`. */\nexport interface TurnReply {\n reply: { text: string };\n}\n\n/** Что `beforeTurn` может решить, кроме «пускать»: отказ, «записать без хода» или «ответить готовым текстом». */\nexport type BeforeTurnDecision = TurnRefusal | TurnHold | TurnReply;\n\n/**\n * Проверка приложения перед ходом: вернуть отказ — ход не начнётся и ничего не запишется. `{ hold: true }` и\n * `{ reply }` — с 0.49 (сценарии): сообщение записывается, хода модели нет.\n */\nexport type BeforeTurnHandler = (input: {\n userId: string;\n chat: Chat;\n action: TurnAction;\n /** Текст сообщения человека (`send`, `edit`). */\n text?: string;\n /** Id сообщения человека (`send`, `edit`): по нему приложение свяжет свой резерв с ходом. */\n messageId?: string;\n /**\n * Ход на пачку сообщений человека (`sendBurstMessage`): id всех её сообщений по порядку, последнее — `messageId`, а\n * `text` — их тексты через пустую строку. Одно решение на всю пачку; счёт сообщений — по длине списка.\n */\n burstMessageIds?: string[];\n /**\n * Ход на пачку начал сам сервер после перезапуска или за упавшую копию (`answerAfterRestart`): вызова, который ждёт\n * ответ или отказ, нет. Отказ дойдёт только до журнала — сообщить человеку может само приложение.\n */\n unattended?: true;\n} & AbandonedAnswers) => Promise<BeforeTurnDecision | void> | BeforeTurnDecision | void;\n\n/**\n * Ответы агента, которые уйдут с текущей ветки, если ход начнётся (`regenerate`, `edit`, `retry`): по ним приложение откатывает\n * побочные дела брошенных ответов (правки формы и т. п.). Ход не начался (`turnNotStarted`) — ветка прежняя, ничего не\n * ушло: тот хук получает те же поля. Посчитаны по ветке на миг проверки; сообщения нет на ветке — полей нет.\n */\nexport interface AbandonedAnswers {\n /** Ответы агента, которые уходят с ветки, сверху вниз; у `edit` без ответов ниже — пустой список. */\n abandonedAnswerIds?: string[];\n /**\n * Ответ, который заменит новый: переделываемый (`regenerate`), ответ на правленый вопрос (`edit`), если был, или\n * оборванный ответ, который «Повторить» (`retry`) заменяет соседом. У «Повторить» вопроса без ответа полей нет.\n */\n previousAnswerId?: string;\n}\n\n/** Почему ход не начался после того, как `beforeTurn` его пропустил. */\nexport type TurnNotStartedReason =\n /** В чате уже идёт ответ, или служба останавливается (`flush`). */\n | \"busy\"\n /** У человека уже идёт предел одновременных ходов. */\n | \"too_many_turns\"\n /** Лимит расходов исчерпан. */\n | \"spending_limit\"\n /** Сообщение с таким id уже есть (повтор запроса). */\n | \"duplicate_message\"\n /** Запрос неверен: сообщение, файл, решение, ответ на виджет. */\n | \"invalid\"\n /** Чат закрыли, пока ход запускался (`closeChat` между проверкой и началом хода). */\n | \"chat_closed\"\n /** Хода и не будет: проверка без хода (`assertTurnAllowed`), ответ ждёт ещё решений, служебное сообщение без ответа. */\n | \"no_turn\"\n /** Сбой (база, сеть): ход не начат. */\n | \"error\";\n\n/**\n * Ход не начался, хотя `beforeTurn` его пропустил: приложение снимает свой резерв. Каждый пропуск `beforeTurn`\n * закрывается ровно одним: ходом (`afterTurn`) или этим хуком. Ошибка хода наружу прежняя; бросок хука — в журнал.\n */\nexport type TurnNotStartedHandler = (input: {\n userId: string;\n chat: Chat;\n action: TurnAction;\n messageId?: string;\n text?: string;\n /** Пачка сообщений, как в `beforeTurn`. */\n burstMessageIds?: string[];\n /** Ход без вызова, как в `beforeTurn`: об отказе (например, `spending_limit`) человеку может сказать только приложение. */\n unattended?: true;\n reason: TurnNotStartedReason;\n} & AbandonedAnswers) => Promise<void> | void;\n\n/** Кто остановил ход: человек («Стоп»), общий срок хода (`agent.turnTimeoutMs`) или остановка службы (`flush`). */\nexport type TurnStopReason = \"user\" | \"timeout\" | \"shutdown\";\n\n/** Итог законченного хода для приложения. */\nexport interface TurnResult {\n ownerId: string;\n /** Кто сделал ход: владелец чата, а в копии у сотрудника — сотрудник. */\n actorId: string;\n chatId: string;\n messageId: string;\n status: FinalAnswerStatus;\n /** Кто остановил ход; только у `status: \"stopped\"` живого хода (у доделанного после перезапуска нет). */\n stopReason?: TurnStopReason;\n /** Ответ упёрся в предел токенов (`finishReason: \"length\"`, см. `agent.maxOutputTokens`) и оборван. */\n lengthCut?: true;\n /**\n * Ход дошёл до предела шагов (`agent.maxSteps`): последний шаг был итоговым, без инструментов. Это «дошли до потолка»,\n * а не обязательно «недоделали»: модель могла закончить дело как раз на последнем шаге. Статус обычный (`complete`).\n * В журнале SDK — строка `[предел шагов]`.\n */\n stepLimit?: true;\n /** Текстовые части ответа подряд. */\n text: string;\n /** Части ответа, как они записаны, кроме служебной части с блоками контекста (`data-frozen-context`). */\n parts: StoredMessage[\"parts\"];\n usage: TokenUsage | null;\n /** Цена хода в долларах; `null` — у модели нет цены. */\n costUsd: number | null;\n error?: { kind: ChatErrorKind };\n /**\n * Каким действием начат ход (см. `TurnAction`). `null` — итог доделан после перезапуска (`recovered`), а ядро не\n * помнит действие; ход по служебному сообщению и тогда — `notice`.\n */\n trigger: TurnAction | null;\n channel: AgentChannel;\n /**\n * Ход сделал сотрудник в копии чата человека (песочница): `ownerId` и `subject` — человека и его исходного чата.\n * При `staff` в данные человека не писать.\n */\n staff?: StaffActor;\n /** Объект чата (урок, заказ…): ключ и данные чата объекта. Нет — обычный чат. */\n subject?: ChatSubject;\n /** Данные чата (`Chat.data`) на момент итога; нет данных — поля нет. */\n chatData?: Record<string, unknown>;\n /** Итог доделан после перезапуска сервера, а не сразу после хода. */\n recovered: boolean;\n}\n\n/**\n * Хук итога хода: зовётся после записи итога и расхода, до `finish` клиенту; пока он идёт, чат занят. Получает любой\n * итог: законченный, остановленный, с ошибкой, прерванный (в том числе восстановлением после перезапуска) и ждущий\n * человека. Ждущий ответ (`awaiting_approval`) после решения человека продолжается тем же сообщением — хук позовут\n * второй раз, с новым статусом. Хук может быть вызван повторно (повтор после сбоя, доделка после перезапуска):\n * ключ повтора — `messageId` + `status`. Возвращённые `events` ложатся в конец ответа частями `data-app-event`.\n */\nexport type AfterTurnHandler = (\n result: TurnResult,\n) => Promise<{ events?: AppEvent[] } | void> | { events?: AppEvent[] } | void;\n\nexport const TURN_REFUSAL_MESSAGE_MAX_LENGTH = 300;\nexport const APP_EVENT_TYPE_MAX_LENGTH = 100;\nexport const APP_EVENT_DATA_MAX_BYTES = 16 * 1024;\n\nconst TURN_REFUSAL_CODE = /^[a-z0-9_.-]{1,64}$/;\n\nexport function isTurnRefusalCode(code: unknown): code is string {\n return typeof code === \"string\" && TURN_REFUSAL_CODE.test(code);\n}\n\n/** Отказ по правилам: код проверен (иначе `TypeError`), текст обрезан до 300 знаков. */\nexport function normalizeTurnRefusal(refusal: TurnRefusal): TurnRefusal {\n if (!isTurnRefusalCode(refusal?.code)) throw new TypeError(\"Код отказа — [a-z0-9_.-], от 1 до 64 знаков\");\n if (typeof refusal.message !== \"string\") throw new TypeError(\"Текст отказа — строка\");\n // По знакам, а не по половинкам: эмодзи на границе не рвётся.\n const message = Array.from(refusal.message).slice(0, TURN_REFUSAL_MESSAGE_MAX_LENGTH).join(\"\");\n return { code: refusal.code, message };\n}\n\n/** Приложение отказало в ходе: ход не начат, ничего не записано. */\nexport class TurnRefusedError extends Error {\n readonly code: string;\n\n constructor(refusal: TurnRefusal) {\n const normalized = normalizeTurnRefusal(refusal);\n super(normalized.message);\n this.name = \"TurnRefusedError\";\n this.code = normalized.code;\n }\n\n get refusal(): TurnRefusal {\n return { code: this.code, message: this.message };\n }\n}\n\n/** Событие по правилам или `TypeError`. Отдаёт копию: данные прошли через JSON. */\nexport function validateAppEvent(event: unknown): AppEvent {\n if (event === null || typeof event !== \"object\") throw new TypeError(\"Событие приложения — объект { type, data? }\");\n const { type, data } = event as { type?: unknown; data?: unknown };\n if (typeof type !== \"string\" || type.length < 1 || type.length > APP_EVENT_TYPE_MAX_LENGTH) {\n throw new TypeError(`Тип события приложения — строка от 1 до ${APP_EVENT_TYPE_MAX_LENGTH} знаков`);\n }\n if (data === undefined) return { type };\n let json: string | undefined;\n try {\n json = JSON.stringify(data);\n } catch {\n json = undefined;\n }\n if (json === undefined) throw new TypeError(\"Данные события приложения — значение JSON\");\n if (new TextEncoder().encode(json).length > APP_EVENT_DATA_MAX_BYTES) {\n throw new TypeError(`Данные события приложения больше ${APP_EVENT_DATA_MAX_BYTES} байт`);\n }\n return { type, data: JSON.parse(json) };\n}\n"],"mappings":";AA8KO,IAAM,kCAAkC;AACxC,IAAM,4BAA4B;AAClC,IAAM,2BAA2B,KAAK;AAE7C,IAAM,oBAAoB;AAEnB,SAAS,kBAAkB,MAA+B;AAC/D,SAAO,OAAO,SAAS,YAAY,kBAAkB,KAAK,IAAI;AAChE;AAGO,SAAS,qBAAqB,SAAmC;AACtE,MAAI,CAAC,kBAAkB,SAAS,IAAI,EAAG,OAAM,IAAI,UAAU,iJAA6C;AACxG,MAAI,OAAO,QAAQ,YAAY,SAAU,OAAM,IAAI,UAAU,iHAAuB;AAEpF,QAAM,UAAU,MAAM,KAAK,QAAQ,OAAO,EAAE,MAAM,GAAG,+BAA+B,EAAE,KAAK,EAAE;AAC7F,SAAO,EAAE,MAAM,QAAQ,MAAM,QAAQ;AACvC;AAGO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EACjC;AAAA,EAET,YAAY,SAAsB;AAChC,UAAM,aAAa,qBAAqB,OAAO;AAC/C,UAAM,WAAW,OAAO;AACxB,SAAK,OAAO;AACZ,SAAK,OAAO,WAAW;AAAA,EACzB;AAAA,EAEA,IAAI,UAAuB;AACzB,WAAO,EAAE,MAAM,KAAK,MAAM,SAAS,KAAK,QAAQ;AAAA,EAClD;AACF;AAGO,SAAS,iBAAiB,OAA0B;AACzD,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,OAAM,IAAI,UAAU,qKAA6C;AAClH,QAAM,EAAE,MAAM,KAAK,IAAI;AACvB,MAAI,OAAO,SAAS,YAAY,KAAK,SAAS,KAAK,KAAK,SAAS,2BAA2B;AAC1F,UAAM,IAAI,UAAU,sMAA2C,yBAAyB,uCAAS;AAAA,EACnG;AACA,MAAI,SAAS,OAAW,QAAO,EAAE,KAAK;AACtC,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,UAAU,IAAI;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,SAAS,OAAW,OAAM,IAAI,UAAU,2MAA2C;AACvF,MAAI,IAAI,YAAY,EAAE,OAAO,IAAI,EAAE,SAAS,0BAA0B;AACpE,UAAM,IAAI,UAAU,qLAAoC,wBAAwB,2BAAO;AAAA,EACzF;AACA,SAAO,EAAE,MAAM,MAAM,KAAK,MAAM,IAAI,EAAE;AACxC;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
accessRefusedResponse,
|
|
3
3
|
allowedUser
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FBULM4V6.js";
|
|
5
5
|
import {
|
|
6
6
|
AGENT_NOTICE_PART_TYPE,
|
|
7
7
|
ANSWERED_BY_MESSAGE,
|
|
@@ -19,7 +19,9 @@ import {
|
|
|
19
19
|
BASIC_WIDGET_NAMES,
|
|
20
20
|
BROWSER_CALL_PART_TYPE,
|
|
21
21
|
BUILT_IN_USAGE_SOURCES,
|
|
22
|
+
BURST_HOLD_MS,
|
|
22
23
|
BURST_PART_TYPE,
|
|
24
|
+
BURST_SWEEP_LIMIT,
|
|
23
25
|
BrowserCallNotPendingError,
|
|
24
26
|
CHANNEL_PART_TYPE,
|
|
25
27
|
CHAT_CAPABILITIES,
|
|
@@ -166,7 +168,7 @@ import {
|
|
|
166
168
|
withoutFileParts,
|
|
167
169
|
withoutGlueFragments,
|
|
168
170
|
withoutResetSteps
|
|
169
|
-
} from "./chunk-
|
|
171
|
+
} from "./chunk-BS2HDKZB.js";
|
|
170
172
|
import {
|
|
171
173
|
APP_EVENT_DATA_MAX_BYTES,
|
|
172
174
|
APP_EVENT_TYPE_MAX_LENGTH,
|
|
@@ -175,7 +177,7 @@ import {
|
|
|
175
177
|
isTurnRefusalCode,
|
|
176
178
|
normalizeTurnRefusal,
|
|
177
179
|
validateAppEvent
|
|
178
|
-
} from "./chunk-
|
|
180
|
+
} from "./chunk-FOAIG4DY.js";
|
|
179
181
|
|
|
180
182
|
// src/memory/memory-optimization.ts
|
|
181
183
|
function pauseAfterFailure(rules, failuresInRow) {
|
|
@@ -323,6 +325,7 @@ function createInMemoryChatStorage() {
|
|
|
323
325
|
const messages = /* @__PURE__ */ new Map();
|
|
324
326
|
const aliveAt = /* @__PURE__ */ new Map();
|
|
325
327
|
const turnLeases = /* @__PURE__ */ new Map();
|
|
328
|
+
const pendingBursts = /* @__PURE__ */ new Map();
|
|
326
329
|
const compactions = [];
|
|
327
330
|
const branchSelections = [];
|
|
328
331
|
const usageRecords = [];
|
|
@@ -502,6 +505,7 @@ function createInMemoryChatStorage() {
|
|
|
502
505
|
async deleteChat(chatId) {
|
|
503
506
|
if (!chats.delete(chatId)) return;
|
|
504
507
|
turnLeases.delete(chatId);
|
|
508
|
+
pendingBursts.delete(chatId);
|
|
505
509
|
for (const message of [...messages.values()]) {
|
|
506
510
|
if (message.chatId !== chatId) continue;
|
|
507
511
|
messages.delete(message.id);
|
|
@@ -592,6 +596,26 @@ function createInMemoryChatStorage() {
|
|
|
592
596
|
async clearChatTurnLeases() {
|
|
593
597
|
turnLeases.clear();
|
|
594
598
|
},
|
|
599
|
+
async savePendingBurst(burst) {
|
|
600
|
+
pendingBursts.set(burst.chatId, structuredClone(burst));
|
|
601
|
+
},
|
|
602
|
+
async takeAbandonedBursts({ abandonedBefore, holder, heldUntil, limit, exceptHolder }) {
|
|
603
|
+
const abandoned = [...pendingBursts.values()].filter((burst) => Date.parse(burst.heldUntil) <= Date.parse(abandonedBefore) && burst.holder !== exceptHolder).sort((first, second) => Date.parse(first.dueAt) - Date.parse(second.dueAt)).slice(0, limit);
|
|
604
|
+
return abandoned.map((burst) => {
|
|
605
|
+
const taken = { ...burst, holder, heldUntil };
|
|
606
|
+
pendingBursts.set(burst.chatId, taken);
|
|
607
|
+
return structuredClone(taken);
|
|
608
|
+
});
|
|
609
|
+
},
|
|
610
|
+
async renewPendingBurst({ chatId, messageId, holder, heldUntil }) {
|
|
611
|
+
const burst = pendingBursts.get(chatId);
|
|
612
|
+
if (burst?.messageId !== messageId || burst.holder !== holder) return false;
|
|
613
|
+
pendingBursts.set(chatId, { ...burst, heldUntil });
|
|
614
|
+
return true;
|
|
615
|
+
},
|
|
616
|
+
async removePendingBurst(chatId, messageId) {
|
|
617
|
+
if (pendingBursts.get(chatId)?.messageId === messageId) pendingBursts.delete(chatId);
|
|
618
|
+
},
|
|
595
619
|
async insertCompaction(record) {
|
|
596
620
|
if (compactions.some((existing) => existing.id === record.id)) throw new Error(`\u0417\u0430\u043F\u0438\u0441\u044C \u0441\u0436\u0430\u0442\u0438\u044F ${record.id} \u0443\u0436\u0435 \u0435\u0441\u0442\u044C`);
|
|
597
621
|
compactions.push(structuredClone(record));
|
|
@@ -811,7 +835,9 @@ export {
|
|
|
811
835
|
BASIC_WIDGET_NAMES,
|
|
812
836
|
BROWSER_CALL_PART_TYPE,
|
|
813
837
|
BUILT_IN_USAGE_SOURCES,
|
|
838
|
+
BURST_HOLD_MS,
|
|
814
839
|
BURST_PART_TYPE,
|
|
840
|
+
BURST_SWEEP_LIMIT,
|
|
815
841
|
BrowserCallNotPendingError,
|
|
816
842
|
CHANNEL_PART_TYPE,
|
|
817
843
|
CHAT_CAPABILITIES,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/memory/memory-optimization.ts","../src/request-origin/request-origin.ts","../src/storage/in-memory-storage.ts"],"sourcesContent":["import type { MemoryError, MemoryNote, MemoryNoteInput } from \"./long-term-memory\";\n\n/**\n * Состояние запуска оптимизации дублей: идёт → слито / нечего сливать / не удалось; слитое можно вернуть (`reverted`).\n * Запуск `running` у человека не больше одного.\n */\nexport type MemoryOptimizationStatus = \"running\" | \"applied\" | \"no_changes\" | \"failed\" | \"reverted\";\n\n/** Одна склейка: записи `originalIds` скрыты и подчинены новой записи `resultId`. */\nexport interface MemoryMergeGroup {\n resultId: string;\n originalIds: string[];\n /** Версия результата на момент слияния: пока она та же, результат не менялся и слияние можно вернуть. */\n resultVersion: number;\n}\n\n/** Запуск оптимизации. Владелец (`ownerId`) — свойство хранилища и наружу не выдаётся. */\nexport interface MemoryOptimizationRun {\n id: string;\n /** Время в формате ISO 8601. Часы даёт вызывающий: от них считаются срок между запусками и пауза после сбоя. */\n startedAt: string;\n finishedAt: string | null;\n status: MemoryOptimizationStatus;\n /** Склейки запуска; у не слитых запусков пусто. */\n groups: MemoryMergeGroup[];\n /** Причина сбоя одним словом-кодом; у остальных `null`. */\n error: string | null;\n /** Запись расхода модели, если вызывающий её знает. */\n usageRecordId: string | null;\n}\n\n/** Правила запуска: их проверяет хранилище в той же операции, что и берёт запуск, чтобы два процесса не начали его вдвоём. */\nexport interface MemoryOptimizationRules {\n /** Сколько должно пройти после законченного запуска (слито, нечего сливать, отменено) до следующего. */\n minIntervalMs: number;\n /**\n * Сколько должно пройти после неудачного запуска до повтора. Число — одна пауза на все неудачи; список — паузы после\n * 1-й, 2-й, … неудачи подряд (последняя держится дальше). Успешный, пустой или отменённый запуск начинает счёт заново.\n */\n retryAfterFailureMs: number | readonly number[];\n /** Сколько запуск `running` считается живым; после этого срока он становится неудачным (`failed`, ошибка `lease_expired`). */\n leaseMs: number;\n}\n\n/** Чем закончился запуск без слияния. */\nexport interface MemoryOptimizationOutcome {\n status: \"no_changes\" | \"failed\";\n finishedAt: string;\n error?: string | null;\n usageRecordId?: string | null;\n}\n\n/** Что слить: новая запись-результат вместо оригиналов; версии оригиналов — те, что видел планировщик. */\nexport interface MemoryMergeInput {\n runId: string;\n finishedAt: string;\n usageRecordId?: string | null;\n merges: { result: MemoryNoteInput; originals: { id: string; version: number }[] }[];\n}\n\n/** Скрытая запись слияния: оригинал, подчинённый результату. Список, блок памяти и поиск таких записей не видят. */\nexport interface MergedMemoryNote extends MemoryNote {\n mergedInto: string;\n}\n\nexport type MemoryMergeApplyResult = MemoryOptimizationRun | \"stale_version\" | \"run_not_active\";\n/** `limit_reached` и `duplicate` — возврат вышел бы за пределы памяти или вернул бы повтор записи, сделанной за это время. */\nexport type MemoryMergeRevertResult = \"ok\" | \"not_found\" | \"conflict\" | \"limit_reached\" | \"duplicate\";\n\n/** Запуск в том виде, что нужен для решения «можно ли начать новый»; время — миллисекунды. */\nexport interface OptimizationRunTiming {\n status: MemoryOptimizationStatus;\n startedAtMs: number;\n /** Конец запуска; у идущего его нет. */\n endedAtMs: number | null;\n}\n\n/** Пауза после неудачи с номером `failuresInRow` (с 1) подряд. */\nexport function pauseAfterFailure(rules: Pick<MemoryOptimizationRules, \"retryAfterFailureMs\">, failuresInRow: number): number {\n const { retryAfterFailureMs } = rules;\n if (typeof retryAfterFailureMs === \"number\") return retryAfterFailureMs;\n return retryAfterFailureMs[Math.min(failuresInRow, retryAfterFailureMs.length) - 1] ?? 0;\n}\n\n/**\n * Мешает ли история запусков начать новый: идёт другой, или не вышла пауза после последнего. Пауза после неудачи\n * растёт с числом неудач подряд. Все три хранилища решают этим одним правилом.\n */\nexport function isOptimizationBlocked(runs: readonly OptimizationRunTiming[], rules: MemoryOptimizationRules, nowMs: number): boolean {\n const ordered = [...runs].sort((left, right) => left.startedAtMs - right.startedAtMs);\n let failuresInRow = 0;\n for (const run of ordered) {\n if (run.status === \"running\") return true;\n failuresInRow = run.status === \"failed\" ? failuresInRow + 1 : 0;\n const pause = run.status === \"failed\" ? pauseAfterFailure(rules, failuresInRow) : rules.minIntervalMs;\n if (nowMs - (run.endedAtMs ?? run.startedAtMs) < pause) return true;\n }\n return false;\n}\n\n/** Ответ отката слияния на ошибку проверки: повтор записи — `duplicate`, всё остальное (число, объём) — `limit_reached`. */\nexport function revertRefusal(error: MemoryError): \"limit_reached\" | \"duplicate\" {\n return error.kind === \"duplicate\" ? \"duplicate\" : \"limit_reached\";\n}\n","/**\n * Защита от запросов с чужих страниц. Браузер сам прикладывает к запросу куки человека, даже если запрос\n * отправила чужая страница (обычная HTML-форма, WebSocket). Отличить свою страницу от чужой помогают заголовки,\n * которые ставит только браузер: `Sec-Fetch-Site` и `Origin`. Порядок проверки как в Go 1.25 (CrossOriginProtection).\n */\n\nexport interface RequestOriginCheckOptions {\n /**\n * Чужие страницы, которым можно слать запросы с куками человека, например `[\"https://school.getcourse.ru\"]`\n * (схема, хост, порт). Шаблон `https://*.example.com` пускает все поддомены `example.com`, но не сам домен\n * и не `evil-example.com`. Свой сайт в список вносить не нужно.\n */\n allowedOrigins?: readonly string[] | undefined;\n /** Отказ: вызывается один раз на каждый новый адрес страницы, чтобы журнал не забивался. */\n onRefused?: (refusal: OriginRefusal) => void;\n}\n\n/**\n * Почему запрос отклонён. `cross_site` — браузер сам сказал, что страница чужая. `host_mismatch` — браузер старый\n * или адрес не https, и Origin не совпал ни с одним именем сервера (`hosts`): так бывает за прокси, который подменил Host.\n */\nexport type OriginRefusal = { origin: string; reason: \"cross_site\" } | { origin: string; reason: \"host_mismatch\"; hosts: string[] };\n\n/** Подсказка для журнала: что поправить, если отклонена своя страница. */\nexport function originRefusalHint(refusal: OriginRefusal): string {\n if (refusal.reason === \"host_mismatch\") {\n return (\n `отклонён запрос со страницы ${refusal.origin}: Origin не совпал с Host (${refusal.hosts.join(\", \")}). ` +\n \"Если сервер за прокси, за прокси передайте Host/X-Forwarded-Host (nginx: proxy_set_header Host $host; \" +\n \"proxy_set_header X-Forwarded-Host $host;). Если это чужая для сервера, но ваша страница, добавьте её адрес в allowedOrigins.\"\n );\n }\n return `отклонён запрос с чужой страницы ${refusal.origin}. Если это ваша страница, добавьте её адрес в allowedOrigins.`;\n}\n\n/** `true` — запрос со своей страницы, со страницы из списка или не из браузера. Метод запроса не смотрит. */\nexport type RequestOriginCheck = (request: Request) => boolean;\n\n/** Чтения (GET, HEAD, OPTIONS) не меняют данных: их ответ браузер чужой странице всё равно не покажет. */\nexport function isSafeMethod(method: string): boolean {\n const upper = method.toUpperCase();\n return upper === \"GET\" || upper === \"HEAD\" || upper === \"OPTIONS\";\n}\n\n/** Сколько разных отклонённых адресов запоминается для журнала. */\nconst MAX_REPORTED_ORIGINS = 100;\nconst MAX_REPORTED_LENGTH = 200;\n\nexport function createRequestOriginCheck(options: RequestOriginCheckOptions = {}): RequestOriginCheck {\n const isAllowed = allowedOriginsMatcher(options.allowedOrigins ?? []);\n const reported = new Set<string>();\n const refuse = (refusal: OriginRefusal): false => {\n const shown = refusal.origin.slice(0, MAX_REPORTED_LENGTH);\n if (options.onRefused && !reported.has(shown) && reported.size < MAX_REPORTED_ORIGINS) {\n reported.add(shown);\n options.onRefused({ ...refusal, origin: shown });\n }\n return false;\n };\n return (request) => {\n const origin = request.headers.get(\"origin\");\n if (origin !== null && isAllowed(origin)) return true;\n const site = request.headers.get(\"sec-fetch-site\");\n if (site !== null) {\n if (site === \"same-origin\" || site === \"none\") return true;\n return refuse({ origin: origin ?? `(без Origin, Sec-Fetch-Site: ${site})`, reason: \"cross_site\" });\n }\n // Нет ни того ни другого — не браузер (сервер, curl): куки человека чужая страница так не приложит.\n if (origin === null) return true;\n // Старый браузер без Sec-Fetch-Site: Origin должен совпасть с адресом самого сервера.\n const host = originHost(origin);\n const hosts = hostsOf(request);\n if (host !== null && hosts.has(host)) return true;\n return refuse({ origin, reason: \"host_mismatch\", hosts: [...hosts] });\n };\n}\n\n/** Метка вместо `*`: адрес с ней разбирает URL, а в настоящем имени хоста она не встречается. */\nconst WILDCARD_LABEL = \"wildcard-label-0\";\n\n/** Точные адреса и шаблоны поддоменов из `allowedOrigins`; неверная запись — ошибка сразу. */\nfunction allowedOriginsMatcher(entries: readonly string[]): (origin: string) => boolean {\n const exact = new Set<string>();\n const subdomainsOf: Array<{ protocol: string; port: string; suffix: string }> = [];\n for (const entry of entries) {\n const wildcard = /^(https?:\\/\\/)\\*\\.([^*]+)$/.exec(entry);\n const url = parsePageAddress(wildcard ? `${wildcard[1]}${WILDCARD_LABEL}.${wildcard[2]}` : entry);\n if (url === null || (!wildcard && entry.includes(\"*\"))) throw wrongEntry(entry);\n if (!wildcard) {\n exact.add(url.origin);\n continue;\n }\n const suffix = url.hostname.slice(WILDCARD_LABEL.length);\n // `*.com` пустил бы весь интернет: после звёздочки нужен домен хотя бы из двух частей (или localhost).\n // Точка в конце (`*.com.`) — тот же `*.com`. Общие суффиксы вроде `*.co.uk` так не отличить: вписывают свой домен.\n const tooBroad = suffix.endsWith(\".\") || (!suffix.slice(1).includes(\".\") && suffix !== \".localhost\");\n if (!url.hostname.startsWith(`${WILDCARD_LABEL}.`) || tooBroad) throw wrongEntry(entry);\n subdomainsOf.push({ protocol: url.protocol, port: url.port, suffix });\n }\n return (origin) => {\n const url = parseOrigin(origin);\n if (url === null) return false;\n if (exact.has(url.origin)) return true;\n return subdomainsOf.some(\n (pattern) =>\n url.protocol === pattern.protocol && url.port === pattern.port && url.hostname.endsWith(pattern.suffix) && url.hostname.length > pattern.suffix.length,\n );\n };\n}\n\n/** Адрес страницы: http(s), без пути, запроса, якоря и имени пользователя. Иначе `null`. */\nfunction parsePageAddress(entry: string): URL | null {\n try {\n const url = new URL(entry);\n const bare = url.pathname === \"/\" && !url.search && !url.hash && !url.username;\n return (url.protocol === \"http:\" || url.protocol === \"https:\") && bare ? url : null;\n } catch {\n return null;\n }\n}\n\nfunction wrongEntry(entry: string): Error {\n return new Error(\n `allowedOrigins: «${entry}» не адрес страницы; нужен вид https://app.example, http://localhost:5173 или https://*.example.com ` +\n \"(в шаблоне — свой домен: общие суффиксы вроде *.co.uk проверка не распознаёт)\",\n );\n}\n\nfunction parseOrigin(origin: string): URL | null {\n try {\n return new URL(origin);\n } catch {\n return null;\n }\n}\n\n/** Хост с портом из Origin; `null` и не адрес — `null`. */\nfunction originHost(origin: string): string | null {\n try {\n const url = new URL(origin);\n return url.protocol === \"http:\" || url.protocol === \"https:\" ? url.host : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Под какими именами сервер знает себя: `Host` и `X-Forwarded-Host` от прокси. Адрес запроса — только без `Host`:\n * при подъёме WebSocket в Node (`@hono/node-ws`) он собран как `http://localhost/…` и порта в нём нет.\n */\nfunction hostsOf(request: Request): Set<string> {\n const host = request.headers.get(\"host\");\n const hosts = new Set<string>([host ? host.trim().toLowerCase() : new URL(request.url).host]);\n for (const forwarded of request.headers.get(\"x-forwarded-host\")?.split(\",\") ?? []) {\n if (forwarded.trim()) hosts.add(forwarded.trim().toLowerCase());\n }\n return hosts;\n}\n","import type { ModelChoice } from \"../models/model-choice\";\nimport type { MemoryNote, MemoryNoteInput } from \"../memory/long-term-memory\";\nimport { isOptimizationBlocked, revertRefusal } from \"../memory/memory-optimization\";\nimport type { MemoryMergeGroup, MemoryOptimizationRun, MergedMemoryNote } from \"../memory/memory-optimization\";\nimport type { Chat, StoredBranchSelection, StoredCompaction, StoredMessage, UsageRecord, UsageSum } from \"../conversation/types\";\nimport {\n aliveAtByLocalClock,\n assertKnownFormatVersion,\n ChatAlreadyExistsError,\n DuplicateChatSubjectError,\n DuplicateMessageError,\n MessageAlreadyFinishedError,\n MessageNotAwaitingError,\n sameMessageParts,\n silenceBoundaryByLocalClock,\n} from \"./chat-storage\";\nimport { assertChatDataFilter, chatDataFields, chatDataMatches, chatDataOfInput, mergeChatData } from \"./chat-data\";\nimport type { ChatListOptions, ChatStorage, ChatSubjectFilter, SpendingLimitStateRecord } from \"./chat-storage\";\n\n/** Хранилище в памяти процесса — для тестов. Копирует данные на входе и выходе, как база. */\ntype StoredNote = MemoryNote & { mergedInto?: string };\n\n/** Сумма записей расхода по ключу (`group` — поля ключа: пара «модель × провайдер», у суммы по чатам — ещё чат). */\nfunction sumRecords<K extends Pick<UsageSum, \"modelId\" | \"providerId\">>(records: readonly UsageRecord[], group: (record: UsageRecord) => K): Array<K & UsageSum> {\n const sums = new Map<string, K & UsageSum>();\n for (const record of records) {\n const fields = group(record);\n const key = Object.values(fields).join(\"\\u0000\");\n const sum = sums.get(key) ?? { ...fields, inputTokens: 0, cachedInputTokens: 0, cacheWriteTokens: 0, outputTokens: 0, reasoningTokens: 0, costUsd: 0 };\n sum.inputTokens += record.inputTokens;\n sum.cachedInputTokens += record.cachedInputTokens;\n sum.cacheWriteTokens += record.cacheWriteTokens;\n sum.outputTokens += record.outputTokens;\n sum.reasoningTokens += record.reasoningTokens;\n sum.costUsd += record.costUsd;\n sums.set(key, sum);\n }\n return [...sums.values()];\n}\n\nexport function createInMemoryChatStorage(): ChatStorage {\n const chats = new Map<string, Chat>();\n const messages = new Map<string, StoredMessage>();\n /** Время последнего знака жизни идущих ответов по id: при чтении не отдаётся. */\n const aliveAt = new Map<string, string>();\n /** Аренды хода по чатам: кто держит и до какого времени (мс по часам этой машины). */\n const turnLeases = new Map<string, { holder: string; until: number }>();\n const compactions: StoredCompaction[] = [];\n const branchSelections: StoredBranchSelection[] = [];\n const usageRecords: UsageRecord[] = [];\n const userModelChoices = new Map<string, ModelChoice>();\n const voiceChoices = new Map<string, string>();\n const spendingLimitStates = new Map<string, SpendingLimitStateRecord>();\n /** Долгая память: у каждого человека свои записи; у скрытых оригиналов слияния есть `mergedInto`. */\n const memoryNotes = new Map<string, Map<string, StoredNote>>();\n const optimizationRuns = new Map<string, MemoryOptimizationRun[]>();\n\n function streamingMessage(messageId: string): StoredMessage {\n const message = messages.get(messageId);\n if (!message) throw new Error(`Сообщения ${messageId} нет`);\n if (message.status !== \"streaming\") throw new MessageAlreadyFinishedError(messageId);\n return message;\n }\n\n /** Сообщение для чтения: копия, `voiceCut` — `null`, если не задан. */\n function readable(message: StoredMessage): StoredMessage {\n const copy = structuredClone(message);\n return { ...copy, voiceCut: copy.voiceCut ?? null };\n }\n\n function ownerNotes(ownerId: string): Map<string, StoredNote> {\n let notes = memoryNotes.get(ownerId);\n if (!notes) {\n notes = new Map();\n memoryNotes.set(ownerId, notes);\n }\n return notes;\n }\n\n function ownerRuns(ownerId: string): MemoryOptimizationRun[] {\n let runs = optimizationRuns.get(ownerId);\n if (!runs) {\n runs = [];\n optimizationRuns.set(ownerId, runs);\n }\n return runs;\n }\n\n /** Порядок списка по договору: `updatedAt`, затем `createdAt`, затем `id` — новее и больше первыми. */\n function newestFirst<T extends MemoryNote>(notes: T[]): T[] {\n return notes.sort(\n (left, right) =>\n right.updatedAt.localeCompare(left.updatedAt) ||\n right.createdAt.localeCompare(left.createdAt) ||\n (right.id > left.id ? 1 : right.id < left.id ? -1 : 0),\n );\n }\n\n /** Обычная запись без служебного признака скрытия. */\n function publicNote({ mergedInto: _hidden, ...note }: StoredNote): MemoryNote {\n return note;\n }\n\n /** Как база с NOT NULL: разделы — список строк, текст — строка. */\n function assertWellFormedNote(note: MemoryNoteInput): void {\n if (!Array.isArray(note?.categories) || !note.categories.every((category) => typeof category === \"string\")) {\n throw new TypeError(\"Разделы записи памяти — список строк\");\n }\n if (typeof note.content !== \"string\") throw new TypeError(\"Текст записи памяти — строка\");\n }\n\n /** Записи, которые видит человек: без скрытых оригиналов, копиями. */\n function visibleNotes(ownerId: string): MemoryNote[] {\n const visible = [...ownerNotes(ownerId).values()].filter((note) => !note.mergedInto);\n return structuredClone(newestFirst(visible).map(publicNote));\n }\n\n function insertNotes(ownerId: string, inputs: MemoryNoteInput[]): MemoryNote[] {\n // Сначала проверяем весь пакет: негодная запись не оставляет в памяти и остальных.\n inputs.forEach(assertWellFormedNote);\n const now = new Date().toISOString();\n const created = inputs.map(\n ({ categories, content }): MemoryNote => ({\n id: crypto.randomUUID(),\n categories: [...categories],\n content,\n version: 1,\n createdAt: now,\n updatedAt: now,\n }),\n );\n const notes = ownerNotes(ownerId);\n for (const note of created) notes.set(note.id, note);\n return structuredClone(created);\n }\n\n function updateNote(ownerId: string, id: string, expectedVersion: number, input: MemoryNoteInput): MemoryNote | \"not_found\" | \"stale_version\" {\n assertWellFormedNote(input);\n const notes = ownerNotes(ownerId);\n const note = notes.get(id);\n if (!note || note.mergedInto) return \"not_found\";\n if (note.version !== expectedVersion) return \"stale_version\";\n const updated: MemoryNote = {\n ...note,\n categories: [...input.categories],\n content: input.content,\n version: note.version + 1,\n updatedAt: new Date().toISOString(),\n };\n notes.set(id, updated);\n return structuredClone(updated);\n }\n\n /** Как уникальный индекс (владелец, ключ): чат без ключа не мешает никому. */\n function assertSubjectFree(chat: Pick<Chat, \"ownerId\" | \"subjectKey\">): void {\n if (chat.subjectKey === undefined) return;\n for (const existing of chats.values()) {\n if (existing.ownerId === chat.ownerId && existing.subjectKey === chat.subjectKey) {\n throw new DuplicateChatSubjectError(chat.ownerId, chat.subjectKey);\n }\n }\n }\n\n function sortedMessages(chatId: string): StoredMessage[] {\n return [...messages.values()]\n .filter((message) => message.chatId === chatId)\n .sort((left, right) => left.createdAt.localeCompare(right.createdAt));\n }\n\n return {\n async createChat({ createdAt, ...input }) {\n const at = createdAt ?? new Date().toISOString();\n const chat: Chat = withChatData(withoutUndefined({ ...structuredClone(input), createdAt: at, updatedAt: at }));\n assertSubjectFree(chat);\n chats.set(chat.id, chat);\n return structuredClone(chat);\n },\n\n async findChatBySubject(ownerId, subjectKey) {\n const chat = [...chats.values()].find((existing) => existing.ownerId === ownerId && existing.subjectKey === subjectKey);\n return chat ? structuredClone(chat) : null;\n },\n\n async importChat(input, imported) {\n // Сначала все проверки: при ошибке ничего не записано.\n if (chats.has(input.id)) throw new ChatAlreadyExistsError(input.id);\n const chat: Chat = withChatData(withoutUndefined(structuredClone(input)));\n assertSubjectFree(chat);\n const ids = new Set<string>();\n for (const message of imported) {\n if (messages.has(message.id) || ids.has(message.id)) throw new DuplicateMessageError(message.id);\n ids.add(message.id);\n }\n chats.set(chat.id, chat);\n for (const message of imported) {\n const { alive: _alive, ...stored } = message;\n messages.set(message.id, structuredClone(stored));\n }\n },\n\n async setAfterTurnState(messageId, state, options) {\n const message = messages.get(messageId);\n if (!message || (state !== \"pending\" && message.afterTurn !== \"pending\")) return false;\n const parts = options?.appendParts ? [...message.parts, ...structuredClone(options.appendParts)] : message.parts;\n messages.set(messageId, { ...message, parts, afterTurn: state });\n // Отметка ожидания хука — в той же колонке, что знак жизни: у законченного ответа она больше ни для чего.\n if (state === \"pending\") aliveAt.set(messageId, aliveAtByLocalClock(\"now\"));\n return true;\n },\n\n async listPendingAfterTurn(filter) {\n const boundary = filter?.pendingForMs === undefined ? null : silenceBoundaryByLocalClock(filter.pendingForMs);\n return [...messages.values()]\n .filter((message) => message.afterTurn === \"pending\")\n .filter((message) => boundary === null || (aliveAt.get(message.id) ?? message.createdAt) < boundary)\n .map((message) => message.id);\n },\n\n async getChat(chatId) {\n const chat = chats.get(chatId);\n return chat ? structuredClone(chat) : null;\n },\n\n async updateChatModel(chatId, model) {\n const chat = chats.get(chatId);\n if (!chat) return;\n const { reasoning: _dropped, ...rest } = chat;\n chats.set(chatId, { ...rest, ...model });\n },\n\n async listChats(ownerId, options?: ChatListOptions) {\n if (options?.data) assertChatDataFilter(options.data);\n const owned = [...chats.values()].filter(\n (chat) => chat.ownerId === ownerId && matchesSubjects(chat, options?.subjects) && (!options?.data || chatDataMatches(chat.data, options.data)),\n );\n owned.sort(\n (left, right) =>\n right.updatedAt.localeCompare(left.updatedAt) ||\n right.createdAt.localeCompare(left.createdAt) ||\n (right.id > left.id ? 1 : right.id < left.id ? -1 : 0),\n );\n return structuredClone(owned);\n },\n\n async updateChatData(chatId, patch, { maxBytes }) {\n const chat = chats.get(chatId);\n if (!chat) return null;\n // Слияние и проверка размера до записи: при ошибке данные прежние.\n const merged = mergeChatData(chat.data, patch, maxBytes);\n const { data: _data, subjectData: _subjectData, ...rest } = chat;\n const updated: Chat = { ...rest, ...chatDataFields(merged) };\n chats.set(chatId, updated);\n return structuredClone(updated);\n },\n\n async renameChat(chatId, title) {\n const chat = chats.get(chatId);\n if (!chat) return;\n const { title: _dropped, ...rest } = chat;\n chats.set(chatId, title === null ? rest : { ...rest, title });\n },\n\n async setChatClosed(chatId, closed) {\n const chat = chats.get(chatId);\n if (!chat) return;\n const { closed: _dropped, ...rest } = chat;\n chats.set(chatId, closed === null ? rest : { ...rest, closed: { code: closed.code, message: closed.message } });\n },\n\n async deleteChat(chatId) {\n if (!chats.delete(chatId)) return;\n turnLeases.delete(chatId);\n for (const message of [...messages.values()]) {\n if (message.chatId !== chatId) continue;\n messages.delete(message.id);\n aliveAt.delete(message.id);\n }\n // Записи сжатия — на месте, с обратным ходом, чтобы индексы не сдвигались.\n for (let index = compactions.length - 1; index >= 0; index -= 1) {\n if (compactions[index]!.chatId === chatId) compactions.splice(index, 1);\n }\n for (let index = branchSelections.length - 1; index >= 0; index -= 1) {\n if (branchSelections[index]!.chatId === chatId) branchSelections.splice(index, 1);\n }\n },\n\n async getMessage(messageId) {\n const message = messages.get(messageId);\n if (!message) return null;\n assertKnownFormatVersion(message);\n return readable(message);\n },\n\n async listMessages(chatId) {\n const found = sortedMessages(chatId);\n found.forEach(assertKnownFormatVersion);\n return found.map(readable);\n },\n\n async listMessageLinks(chatId) {\n return sortedMessages(chatId).map(({ id, parentId, createdAt }) => ({ id, parentId, createdAt }));\n },\n\n async listMessagePartsOfTypes(chatId, types) {\n return sortedMessages(chatId).map(({ id, parentId, createdAt, parts }) => ({\n id,\n parentId,\n createdAt,\n parts: structuredClone(parts.filter((part) => types.includes(part.type))),\n }));\n },\n\n async insertMessage(message) {\n if (messages.has(message.id)) throw new DuplicateMessageError(message.id);\n // Как внешний ключ в базе: сообщение без чата не пишется.\n const chat = chats.get(message.chatId);\n if (!chat) throw new Error(`Чата ${message.chatId} нет`);\n const { alive, ...stored } = message;\n messages.set(message.id, structuredClone(stored));\n if (alive) aliveAt.set(message.id, aliveAtByLocalClock(alive));\n chats.set(chat.id, { ...chat, updatedAt: message.createdAt });\n },\n\n async updateStreamingMessage(messageId, patch) {\n const message = streamingMessage(messageId);\n const { alive, ...rest } = structuredClone(patch);\n messages.set(messageId, { ...message, ...rest });\n if (alive) aliveAt.set(messageId, aliveAtByLocalClock(alive));\n },\n\n async markAnswerAlive(messageId) {\n streamingMessage(messageId);\n aliveAt.set(messageId, aliveAtByLocalClock(\"now\"));\n },\n\n async finishMessage(messageId, final) {\n const message = streamingMessage(messageId);\n messages.set(messageId, { ...message, ...structuredClone(final) });\n if (final.afterTurn === \"pending\") aliveAt.set(messageId, aliveAtByLocalClock(\"now\"));\n },\n\n async updateAwaitingMessage(messageId, patch) {\n const message = messages.get(messageId);\n if (!message) throw new Error(`Сообщения ${messageId} нет`);\n if (message.status !== \"awaiting_approval\") throw new MessageNotAwaitingError(messageId);\n if (patch.expectedParts && !sameMessageParts(message.parts, patch.expectedParts)) throw new MessageNotAwaitingError(messageId);\n messages.set(messageId, {\n ...message,\n parts: structuredClone(patch.parts),\n ...(patch.status && { status: patch.status }),\n });\n if (patch.alive) aliveAt.set(messageId, aliveAtByLocalClock(patch.alive));\n },\n\n async listStreamingMessageIds() {\n return [...messages.values()].filter((message) => message.status === \"streaming\").map((message) => message.id);\n },\n\n async listSilentAnswerIds({ silentForMs, chatId }) {\n const boundary = silenceBoundaryByLocalClock(silentForMs);\n return [...messages.values()]\n .filter((message) => message.status === \"streaming\" && (chatId === undefined || message.chatId === chatId))\n .filter((message) => (aliveAt.get(message.id) ?? message.createdAt) < boundary)\n .map((message) => message.id);\n },\n\n async claimChatTurn(chatId, holder, leaseMs) {\n const now = Date.now();\n const lease = turnLeases.get(chatId);\n if (lease && lease.holder !== holder && lease.until > now) return false;\n turnLeases.set(chatId, { holder, until: now + leaseMs });\n return true;\n },\n\n async releaseChatTurn(chatId, holder) {\n if (turnLeases.get(chatId)?.holder === holder) turnLeases.delete(chatId);\n },\n\n async clearChatTurnLeases() {\n turnLeases.clear();\n },\n\n async insertCompaction(record) {\n if (compactions.some((existing) => existing.id === record.id)) throw new Error(`Запись сжатия ${record.id} уже есть`);\n compactions.push(structuredClone(record));\n },\n\n async listCompactions(chatId) {\n return structuredClone(compactions.filter((record) => record.chatId === chatId));\n },\n\n async insertBranchSelection(record) {\n if (branchSelections.some((existing) => existing.id === record.id)) throw new Error(`Выбор ветки ${record.id} уже есть`);\n branchSelections.push(structuredClone(record));\n },\n\n async listBranchSelections(chatId) {\n return structuredClone(branchSelections.filter((record) => record.chatId === chatId));\n },\n\n async insertUsageRecord(record) {\n if (usageRecords.some((existing) => existing.id === record.id)) throw new Error(`Запись расхода ${record.id} уже есть`);\n usageRecords.push(structuredClone(record));\n },\n\n async hasUsageRecord(id) {\n return usageRecords.some((record) => record.id === id);\n },\n\n async sumUsage({ ownerId, chatId, messageId, source, since }) {\n return sumRecords(\n usageRecords.filter(\n (record) =>\n (ownerId === undefined || record.ownerId === ownerId) &&\n (chatId === undefined || record.chatId === chatId) &&\n (messageId === undefined || record.messageId === messageId) &&\n (source === undefined || record.source === source) &&\n (since === undefined || record.createdAt >= since),\n ),\n (record) => ({ modelId: record.modelId, providerId: record.providerId }),\n );\n },\n\n async sumUsageByChat({ chatIds, ownerId, source, since }) {\n const wanted = new Set(chatIds);\n return sumRecords(\n usageRecords.filter(\n (record) =>\n wanted.has(record.chatId) &&\n (ownerId === undefined || record.ownerId === ownerId) &&\n (source === undefined || record.source === source) &&\n (since === undefined || record.createdAt >= since),\n ),\n (record) => ({ chatId: record.chatId, modelId: record.modelId, providerId: record.providerId }),\n );\n },\n\n async setVoiceCut({ ownerId, chatId, messageId, voiceCut }) {\n const message = messages.get(messageId);\n if (!message || message.chatId !== chatId || chats.get(chatId)?.ownerId !== ownerId) return;\n messages.set(messageId, { ...message, voiceCut: structuredClone(voiceCut) });\n },\n\n async getSpendingLimitState(key) {\n const record = spendingLimitStates.get(key);\n return record ? { ...record } : null;\n },\n\n async saveSpendingLimitState(key, value, expectedVersion) {\n const current = spendingLimitStates.get(key);\n if ((current?.version ?? null) !== expectedVersion) return false;\n spendingLimitStates.set(key, { value, version: (current?.version ?? 0) + 1 });\n return true;\n },\n\n async deleteSpendingLimitStates(keys) {\n for (const key of keys) spendingLimitStates.delete(key);\n },\n\n async getVoiceChoice(ownerId) {\n return voiceChoices.get(ownerId) ?? null;\n },\n\n async setVoiceChoice(ownerId, voiceId) {\n voiceChoices.set(ownerId, voiceId);\n },\n\n async getUserModelChoice(ownerId) {\n const choice = userModelChoices.get(ownerId);\n return choice ? structuredClone(choice) : null;\n },\n\n async saveUserModelChoice(ownerId, choice) {\n userModelChoices.set(ownerId, structuredClone(choice));\n },\n\n async listMemoryNotes(ownerId) {\n return visibleNotes(ownerId);\n },\n\n async insertMemoryNotes(ownerId, inputs) {\n return insertNotes(ownerId, inputs);\n },\n\n // Всё выполняется без ожидания, поэтому между проверкой и вставкой никто не успеет вклиниться.\n async insertMemoryNotesChecked(ownerId, inputs, guard) {\n const error = guard(visibleNotes(ownerId));\n return error ?? insertNotes(ownerId, inputs);\n },\n\n async updateMemoryNote(ownerId, id, expectedVersion, input) {\n return updateNote(ownerId, id, expectedVersion, input);\n },\n\n async updateMemoryNoteChecked(ownerId, id, expectedVersion, input, guard) {\n return guard(visibleNotes(ownerId)) ?? updateNote(ownerId, id, expectedVersion, input);\n },\n\n async deleteMemoryNote(ownerId, id, expectedVersion) {\n const notes = ownerNotes(ownerId);\n const note = notes.get(id);\n if (!note || note.mergedInto) return \"not_found\";\n if (note.version !== expectedVersion) return \"stale_version\";\n notes.delete(id);\n for (const [hiddenId, hidden] of notes) if (hidden.mergedInto === id) notes.delete(hiddenId);\n return \"ok\";\n },\n\n async clearMemoryNotes(ownerId) {\n const count = [...ownerNotes(ownerId).values()].filter((note) => !note.mergedInto).length;\n memoryNotes.delete(ownerId);\n optimizationRuns.delete(ownerId);\n return count;\n },\n\n async claimMemoryOptimizationRun(ownerId, rules, now) {\n const runs = ownerRuns(ownerId);\n const nowMs = Date.parse(now);\n for (const run of runs) {\n const leaseEnd = Date.parse(run.startedAt) + rules.leaseMs;\n if (run.status === \"running\" && nowMs >= leaseEnd) Object.assign(run, { status: \"failed\", error: \"lease_expired\", finishedAt: new Date(leaseEnd).toISOString() });\n }\n const blocked = isOptimizationBlocked(\n runs.map((run) => ({ status: run.status, startedAtMs: Date.parse(run.startedAt), endedAtMs: run.finishedAt ? Date.parse(run.finishedAt) : null })),\n rules,\n nowMs,\n );\n if (blocked) return null;\n const run: MemoryOptimizationRun = { id: crypto.randomUUID(), startedAt: now, finishedAt: null, status: \"running\", groups: [], error: null, usageRecordId: null };\n runs.push(run);\n return structuredClone(run);\n },\n\n async finishMemoryOptimizationRun(ownerId, runId, outcome) {\n const run = ownerRuns(ownerId).find((candidate) => candidate.id === runId);\n if (run?.status !== \"running\") return \"not_found\";\n Object.assign(run, { status: outcome.status, finishedAt: outcome.finishedAt, error: outcome.error ?? null, usageRecordId: outcome.usageRecordId ?? null });\n return \"ok\";\n },\n\n async applyMemoryMerge(ownerId, merge) {\n const { merges } = merge;\n if (merges.length === 0) throw new TypeError(\"Слияние без склеек\");\n const seen = new Set<string>();\n for (const { result, originals } of merges) {\n assertWellFormedNote(result);\n if (originals.length === 0) throw new TypeError(\"Склейка без оригиналов\");\n for (const { id } of originals) {\n if (seen.has(id)) throw new TypeError(`Запись ${id} названа в двух склейках`);\n seen.add(id);\n }\n }\n const run = ownerRuns(ownerId).find((candidate) => candidate.id === merge.runId);\n if (run?.status !== \"running\") return \"run_not_active\";\n const notes = ownerNotes(ownerId);\n for (const { originals } of merges) {\n for (const { id, version } of originals) {\n const original = notes.get(id);\n if (!original || original.mergedInto || original.version !== version) return \"stale_version\";\n }\n }\n const now = new Date().toISOString();\n const groups: MemoryMergeGroup[] = merges.map(({ result, originals }) => {\n const created: StoredNote = { id: crypto.randomUUID(), categories: [...result.categories], content: result.content, version: 1, createdAt: now, updatedAt: now };\n notes.set(created.id, created);\n for (const { id } of originals) notes.get(id)!.mergedInto = created.id;\n return { resultId: created.id, originalIds: originals.map(({ id }) => id), resultVersion: 1 };\n });\n Object.assign(run, { status: \"applied\", finishedAt: merge.finishedAt, groups, usageRecordId: merge.usageRecordId ?? null });\n return structuredClone(run);\n },\n\n async revertMemoryMerge(ownerId, runId, guard) {\n const run = ownerRuns(ownerId).find((candidate) => candidate.id === runId);\n if (run?.status !== \"applied\") return \"not_found\";\n const notes = ownerNotes(ownerId);\n const intact = run.groups.every(\n ({ resultId, originalIds, resultVersion }) =>\n notes.get(resultId)?.version === resultVersion && !notes.get(resultId)?.mergedInto && originalIds.every((id) => notes.get(id)?.mergedInto === resultId),\n );\n if (!intact) return \"conflict\";\n if (guard) {\n const resultIds = new Set(run.groups.map(({ resultId }) => resultId));\n const restoredIds = new Set(run.groups.flatMap(({ originalIds }) => originalIds));\n const inPlay = [...notes.values()];\n const remaining = newestFirst(inPlay.filter((note) => !note.mergedInto && !resultIds.has(note.id))).map(publicNote);\n const restored = newestFirst(inPlay.filter((note) => restoredIds.has(note.id))).map(publicNote);\n const error = guard(structuredClone(remaining), structuredClone(restored));\n if (error) return revertRefusal(error);\n }\n for (const { resultId, originalIds } of run.groups) {\n notes.delete(resultId);\n for (const id of originalIds) delete notes.get(id)!.mergedInto;\n }\n run.status = \"reverted\";\n return \"ok\";\n },\n\n async listMemoryOptimizationRuns(ownerId, filter) {\n const runs = ownerRuns(ownerId)\n .map((run, index) => ({ run, index }))\n .filter(({ run }) => !filter?.status || filter.status.includes(run.status))\n .sort((left, right) => right.run.startedAt.localeCompare(left.run.startedAt) || right.index - left.index)\n .map(({ run }) => run);\n return structuredClone(filter?.limit === undefined ? runs : runs.slice(0, filter.limit));\n },\n\n async listMergedMemoryNotes(ownerId) {\n const hidden = [...ownerNotes(ownerId).values()].filter((note): note is StoredNote & { mergedInto: string } => Boolean(note.mergedInto));\n return structuredClone(newestFirst(hidden) as MergedMemoryNote[]);\n },\n };\n}\n\n/** Подходит ли чат под фильтр `listChats` по ключу объекта. */\nfunction matchesSubjects(chat: Chat, subjects: ChatSubjectFilter = \"exclude\"): boolean {\n if (subjects === \"include\") return true;\n return subjects === \"only\" ? chat.subjectKey !== undefined : chat.subjectKey === undefined;\n}\n\n/** `data` и `subjectData` — один объект; данных нет — полей нет. */\nfunction withChatData<T extends Pick<Chat, \"data\" | \"subjectData\">>(chat: T): T {\n const { data: _data, subjectData: _subjectData, ...rest } = chat;\n return { ...rest, ...chatDataFields(chatDataOfInput(chat)) } as T;\n}\n\n/** Как база: поле без значения не хранится. */\nfunction withoutUndefined<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as T;\n}\n"],"mappings":"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","names":["run"]}
|
|
1
|
+
{"version":3,"sources":["../src/memory/memory-optimization.ts","../src/request-origin/request-origin.ts","../src/storage/in-memory-storage.ts"],"sourcesContent":["import type { MemoryError, MemoryNote, MemoryNoteInput } from \"./long-term-memory\";\n\n/**\n * Состояние запуска оптимизации дублей: идёт → слито / нечего сливать / не удалось; слитое можно вернуть (`reverted`).\n * Запуск `running` у человека не больше одного.\n */\nexport type MemoryOptimizationStatus = \"running\" | \"applied\" | \"no_changes\" | \"failed\" | \"reverted\";\n\n/** Одна склейка: записи `originalIds` скрыты и подчинены новой записи `resultId`. */\nexport interface MemoryMergeGroup {\n resultId: string;\n originalIds: string[];\n /** Версия результата на момент слияния: пока она та же, результат не менялся и слияние можно вернуть. */\n resultVersion: number;\n}\n\n/** Запуск оптимизации. Владелец (`ownerId`) — свойство хранилища и наружу не выдаётся. */\nexport interface MemoryOptimizationRun {\n id: string;\n /** Время в формате ISO 8601. Часы даёт вызывающий: от них считаются срок между запусками и пауза после сбоя. */\n startedAt: string;\n finishedAt: string | null;\n status: MemoryOptimizationStatus;\n /** Склейки запуска; у не слитых запусков пусто. */\n groups: MemoryMergeGroup[];\n /** Причина сбоя одним словом-кодом; у остальных `null`. */\n error: string | null;\n /** Запись расхода модели, если вызывающий её знает. */\n usageRecordId: string | null;\n}\n\n/** Правила запуска: их проверяет хранилище в той же операции, что и берёт запуск, чтобы два процесса не начали его вдвоём. */\nexport interface MemoryOptimizationRules {\n /** Сколько должно пройти после законченного запуска (слито, нечего сливать, отменено) до следующего. */\n minIntervalMs: number;\n /**\n * Сколько должно пройти после неудачного запуска до повтора. Число — одна пауза на все неудачи; список — паузы после\n * 1-й, 2-й, … неудачи подряд (последняя держится дальше). Успешный, пустой или отменённый запуск начинает счёт заново.\n */\n retryAfterFailureMs: number | readonly number[];\n /** Сколько запуск `running` считается живым; после этого срока он становится неудачным (`failed`, ошибка `lease_expired`). */\n leaseMs: number;\n}\n\n/** Чем закончился запуск без слияния. */\nexport interface MemoryOptimizationOutcome {\n status: \"no_changes\" | \"failed\";\n finishedAt: string;\n error?: string | null;\n usageRecordId?: string | null;\n}\n\n/** Что слить: новая запись-результат вместо оригиналов; версии оригиналов — те, что видел планировщик. */\nexport interface MemoryMergeInput {\n runId: string;\n finishedAt: string;\n usageRecordId?: string | null;\n merges: { result: MemoryNoteInput; originals: { id: string; version: number }[] }[];\n}\n\n/** Скрытая запись слияния: оригинал, подчинённый результату. Список, блок памяти и поиск таких записей не видят. */\nexport interface MergedMemoryNote extends MemoryNote {\n mergedInto: string;\n}\n\nexport type MemoryMergeApplyResult = MemoryOptimizationRun | \"stale_version\" | \"run_not_active\";\n/** `limit_reached` и `duplicate` — возврат вышел бы за пределы памяти или вернул бы повтор записи, сделанной за это время. */\nexport type MemoryMergeRevertResult = \"ok\" | \"not_found\" | \"conflict\" | \"limit_reached\" | \"duplicate\";\n\n/** Запуск в том виде, что нужен для решения «можно ли начать новый»; время — миллисекунды. */\nexport interface OptimizationRunTiming {\n status: MemoryOptimizationStatus;\n startedAtMs: number;\n /** Конец запуска; у идущего его нет. */\n endedAtMs: number | null;\n}\n\n/** Пауза после неудачи с номером `failuresInRow` (с 1) подряд. */\nexport function pauseAfterFailure(rules: Pick<MemoryOptimizationRules, \"retryAfterFailureMs\">, failuresInRow: number): number {\n const { retryAfterFailureMs } = rules;\n if (typeof retryAfterFailureMs === \"number\") return retryAfterFailureMs;\n return retryAfterFailureMs[Math.min(failuresInRow, retryAfterFailureMs.length) - 1] ?? 0;\n}\n\n/**\n * Мешает ли история запусков начать новый: идёт другой, или не вышла пауза после последнего. Пауза после неудачи\n * растёт с числом неудач подряд. Все три хранилища решают этим одним правилом.\n */\nexport function isOptimizationBlocked(runs: readonly OptimizationRunTiming[], rules: MemoryOptimizationRules, nowMs: number): boolean {\n const ordered = [...runs].sort((left, right) => left.startedAtMs - right.startedAtMs);\n let failuresInRow = 0;\n for (const run of ordered) {\n if (run.status === \"running\") return true;\n failuresInRow = run.status === \"failed\" ? failuresInRow + 1 : 0;\n const pause = run.status === \"failed\" ? pauseAfterFailure(rules, failuresInRow) : rules.minIntervalMs;\n if (nowMs - (run.endedAtMs ?? run.startedAtMs) < pause) return true;\n }\n return false;\n}\n\n/** Ответ отката слияния на ошибку проверки: повтор записи — `duplicate`, всё остальное (число, объём) — `limit_reached`. */\nexport function revertRefusal(error: MemoryError): \"limit_reached\" | \"duplicate\" {\n return error.kind === \"duplicate\" ? \"duplicate\" : \"limit_reached\";\n}\n","/**\n * Защита от запросов с чужих страниц. Браузер сам прикладывает к запросу куки человека, даже если запрос\n * отправила чужая страница (обычная HTML-форма, WebSocket). Отличить свою страницу от чужой помогают заголовки,\n * которые ставит только браузер: `Sec-Fetch-Site` и `Origin`. Порядок проверки как в Go 1.25 (CrossOriginProtection).\n */\n\nexport interface RequestOriginCheckOptions {\n /**\n * Чужие страницы, которым можно слать запросы с куками человека, например `[\"https://school.getcourse.ru\"]`\n * (схема, хост, порт). Шаблон `https://*.example.com` пускает все поддомены `example.com`, но не сам домен\n * и не `evil-example.com`. Свой сайт в список вносить не нужно.\n */\n allowedOrigins?: readonly string[] | undefined;\n /** Отказ: вызывается один раз на каждый новый адрес страницы, чтобы журнал не забивался. */\n onRefused?: (refusal: OriginRefusal) => void;\n}\n\n/**\n * Почему запрос отклонён. `cross_site` — браузер сам сказал, что страница чужая. `host_mismatch` — браузер старый\n * или адрес не https, и Origin не совпал ни с одним именем сервера (`hosts`): так бывает за прокси, который подменил Host.\n */\nexport type OriginRefusal = { origin: string; reason: \"cross_site\" } | { origin: string; reason: \"host_mismatch\"; hosts: string[] };\n\n/** Подсказка для журнала: что поправить, если отклонена своя страница. */\nexport function originRefusalHint(refusal: OriginRefusal): string {\n if (refusal.reason === \"host_mismatch\") {\n return (\n `отклонён запрос со страницы ${refusal.origin}: Origin не совпал с Host (${refusal.hosts.join(\", \")}). ` +\n \"Если сервер за прокси, за прокси передайте Host/X-Forwarded-Host (nginx: proxy_set_header Host $host; \" +\n \"proxy_set_header X-Forwarded-Host $host;). Если это чужая для сервера, но ваша страница, добавьте её адрес в allowedOrigins.\"\n );\n }\n return `отклонён запрос с чужой страницы ${refusal.origin}. Если это ваша страница, добавьте её адрес в allowedOrigins.`;\n}\n\n/** `true` — запрос со своей страницы, со страницы из списка или не из браузера. Метод запроса не смотрит. */\nexport type RequestOriginCheck = (request: Request) => boolean;\n\n/** Чтения (GET, HEAD, OPTIONS) не меняют данных: их ответ браузер чужой странице всё равно не покажет. */\nexport function isSafeMethod(method: string): boolean {\n const upper = method.toUpperCase();\n return upper === \"GET\" || upper === \"HEAD\" || upper === \"OPTIONS\";\n}\n\n/** Сколько разных отклонённых адресов запоминается для журнала. */\nconst MAX_REPORTED_ORIGINS = 100;\nconst MAX_REPORTED_LENGTH = 200;\n\nexport function createRequestOriginCheck(options: RequestOriginCheckOptions = {}): RequestOriginCheck {\n const isAllowed = allowedOriginsMatcher(options.allowedOrigins ?? []);\n const reported = new Set<string>();\n const refuse = (refusal: OriginRefusal): false => {\n const shown = refusal.origin.slice(0, MAX_REPORTED_LENGTH);\n if (options.onRefused && !reported.has(shown) && reported.size < MAX_REPORTED_ORIGINS) {\n reported.add(shown);\n options.onRefused({ ...refusal, origin: shown });\n }\n return false;\n };\n return (request) => {\n const origin = request.headers.get(\"origin\");\n if (origin !== null && isAllowed(origin)) return true;\n const site = request.headers.get(\"sec-fetch-site\");\n if (site !== null) {\n if (site === \"same-origin\" || site === \"none\") return true;\n return refuse({ origin: origin ?? `(без Origin, Sec-Fetch-Site: ${site})`, reason: \"cross_site\" });\n }\n // Нет ни того ни другого — не браузер (сервер, curl): куки человека чужая страница так не приложит.\n if (origin === null) return true;\n // Старый браузер без Sec-Fetch-Site: Origin должен совпасть с адресом самого сервера.\n const host = originHost(origin);\n const hosts = hostsOf(request);\n if (host !== null && hosts.has(host)) return true;\n return refuse({ origin, reason: \"host_mismatch\", hosts: [...hosts] });\n };\n}\n\n/** Метка вместо `*`: адрес с ней разбирает URL, а в настоящем имени хоста она не встречается. */\nconst WILDCARD_LABEL = \"wildcard-label-0\";\n\n/** Точные адреса и шаблоны поддоменов из `allowedOrigins`; неверная запись — ошибка сразу. */\nfunction allowedOriginsMatcher(entries: readonly string[]): (origin: string) => boolean {\n const exact = new Set<string>();\n const subdomainsOf: Array<{ protocol: string; port: string; suffix: string }> = [];\n for (const entry of entries) {\n const wildcard = /^(https?:\\/\\/)\\*\\.([^*]+)$/.exec(entry);\n const url = parsePageAddress(wildcard ? `${wildcard[1]}${WILDCARD_LABEL}.${wildcard[2]}` : entry);\n if (url === null || (!wildcard && entry.includes(\"*\"))) throw wrongEntry(entry);\n if (!wildcard) {\n exact.add(url.origin);\n continue;\n }\n const suffix = url.hostname.slice(WILDCARD_LABEL.length);\n // `*.com` пустил бы весь интернет: после звёздочки нужен домен хотя бы из двух частей (или localhost).\n // Точка в конце (`*.com.`) — тот же `*.com`. Общие суффиксы вроде `*.co.uk` так не отличить: вписывают свой домен.\n const tooBroad = suffix.endsWith(\".\") || (!suffix.slice(1).includes(\".\") && suffix !== \".localhost\");\n if (!url.hostname.startsWith(`${WILDCARD_LABEL}.`) || tooBroad) throw wrongEntry(entry);\n subdomainsOf.push({ protocol: url.protocol, port: url.port, suffix });\n }\n return (origin) => {\n const url = parseOrigin(origin);\n if (url === null) return false;\n if (exact.has(url.origin)) return true;\n return subdomainsOf.some(\n (pattern) =>\n url.protocol === pattern.protocol && url.port === pattern.port && url.hostname.endsWith(pattern.suffix) && url.hostname.length > pattern.suffix.length,\n );\n };\n}\n\n/** Адрес страницы: http(s), без пути, запроса, якоря и имени пользователя. Иначе `null`. */\nfunction parsePageAddress(entry: string): URL | null {\n try {\n const url = new URL(entry);\n const bare = url.pathname === \"/\" && !url.search && !url.hash && !url.username;\n return (url.protocol === \"http:\" || url.protocol === \"https:\") && bare ? url : null;\n } catch {\n return null;\n }\n}\n\nfunction wrongEntry(entry: string): Error {\n return new Error(\n `allowedOrigins: «${entry}» не адрес страницы; нужен вид https://app.example, http://localhost:5173 или https://*.example.com ` +\n \"(в шаблоне — свой домен: общие суффиксы вроде *.co.uk проверка не распознаёт)\",\n );\n}\n\nfunction parseOrigin(origin: string): URL | null {\n try {\n return new URL(origin);\n } catch {\n return null;\n }\n}\n\n/** Хост с портом из Origin; `null` и не адрес — `null`. */\nfunction originHost(origin: string): string | null {\n try {\n const url = new URL(origin);\n return url.protocol === \"http:\" || url.protocol === \"https:\" ? url.host : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Под какими именами сервер знает себя: `Host` и `X-Forwarded-Host` от прокси. Адрес запроса — только без `Host`:\n * при подъёме WebSocket в Node (`@hono/node-ws`) он собран как `http://localhost/…` и порта в нём нет.\n */\nfunction hostsOf(request: Request): Set<string> {\n const host = request.headers.get(\"host\");\n const hosts = new Set<string>([host ? host.trim().toLowerCase() : new URL(request.url).host]);\n for (const forwarded of request.headers.get(\"x-forwarded-host\")?.split(\",\") ?? []) {\n if (forwarded.trim()) hosts.add(forwarded.trim().toLowerCase());\n }\n return hosts;\n}\n","import type { ModelChoice } from \"../models/model-choice\";\nimport type { MemoryNote, MemoryNoteInput } from \"../memory/long-term-memory\";\nimport { isOptimizationBlocked, revertRefusal } from \"../memory/memory-optimization\";\nimport type { MemoryMergeGroup, MemoryOptimizationRun, MergedMemoryNote } from \"../memory/memory-optimization\";\nimport type { Chat, StoredBranchSelection, StoredCompaction, StoredMessage, UsageRecord, UsageSum } from \"../conversation/types\";\nimport {\n aliveAtByLocalClock,\n assertKnownFormatVersion,\n ChatAlreadyExistsError,\n DuplicateChatSubjectError,\n DuplicateMessageError,\n MessageAlreadyFinishedError,\n MessageNotAwaitingError,\n sameMessageParts,\n silenceBoundaryByLocalClock,\n} from \"./chat-storage\";\nimport { assertChatDataFilter, chatDataFields, chatDataMatches, chatDataOfInput, mergeChatData } from \"./chat-data\";\nimport type { ChatListOptions, ChatStorage, ChatSubjectFilter, SpendingLimitStateRecord, StoredPendingBurst } from \"./chat-storage\";\n\n/** Хранилище в памяти процесса — для тестов. Копирует данные на входе и выходе, как база. */\ntype StoredNote = MemoryNote & { mergedInto?: string };\n\n/** Сумма записей расхода по ключу (`group` — поля ключа: пара «модель × провайдер», у суммы по чатам — ещё чат). */\nfunction sumRecords<K extends Pick<UsageSum, \"modelId\" | \"providerId\">>(records: readonly UsageRecord[], group: (record: UsageRecord) => K): Array<K & UsageSum> {\n const sums = new Map<string, K & UsageSum>();\n for (const record of records) {\n const fields = group(record);\n const key = Object.values(fields).join(\"\\u0000\");\n const sum = sums.get(key) ?? { ...fields, inputTokens: 0, cachedInputTokens: 0, cacheWriteTokens: 0, outputTokens: 0, reasoningTokens: 0, costUsd: 0 };\n sum.inputTokens += record.inputTokens;\n sum.cachedInputTokens += record.cachedInputTokens;\n sum.cacheWriteTokens += record.cacheWriteTokens;\n sum.outputTokens += record.outputTokens;\n sum.reasoningTokens += record.reasoningTokens;\n sum.costUsd += record.costUsd;\n sums.set(key, sum);\n }\n return [...sums.values()];\n}\n\nexport function createInMemoryChatStorage(): ChatStorage {\n const chats = new Map<string, Chat>();\n const messages = new Map<string, StoredMessage>();\n /** Время последнего знака жизни идущих ответов по id: при чтении не отдаётся. */\n const aliveAt = new Map<string, string>();\n /** Аренды хода по чатам: кто держит и до какого времени (мс по часам этой машины). */\n const turnLeases = new Map<string, { holder: string; until: number }>();\n /** Ждущие пачки по чатам. */\n const pendingBursts = new Map<string, StoredPendingBurst>();\n const compactions: StoredCompaction[] = [];\n const branchSelections: StoredBranchSelection[] = [];\n const usageRecords: UsageRecord[] = [];\n const userModelChoices = new Map<string, ModelChoice>();\n const voiceChoices = new Map<string, string>();\n const spendingLimitStates = new Map<string, SpendingLimitStateRecord>();\n /** Долгая память: у каждого человека свои записи; у скрытых оригиналов слияния есть `mergedInto`. */\n const memoryNotes = new Map<string, Map<string, StoredNote>>();\n const optimizationRuns = new Map<string, MemoryOptimizationRun[]>();\n\n function streamingMessage(messageId: string): StoredMessage {\n const message = messages.get(messageId);\n if (!message) throw new Error(`Сообщения ${messageId} нет`);\n if (message.status !== \"streaming\") throw new MessageAlreadyFinishedError(messageId);\n return message;\n }\n\n /** Сообщение для чтения: копия, `voiceCut` — `null`, если не задан. */\n function readable(message: StoredMessage): StoredMessage {\n const copy = structuredClone(message);\n return { ...copy, voiceCut: copy.voiceCut ?? null };\n }\n\n function ownerNotes(ownerId: string): Map<string, StoredNote> {\n let notes = memoryNotes.get(ownerId);\n if (!notes) {\n notes = new Map();\n memoryNotes.set(ownerId, notes);\n }\n return notes;\n }\n\n function ownerRuns(ownerId: string): MemoryOptimizationRun[] {\n let runs = optimizationRuns.get(ownerId);\n if (!runs) {\n runs = [];\n optimizationRuns.set(ownerId, runs);\n }\n return runs;\n }\n\n /** Порядок списка по договору: `updatedAt`, затем `createdAt`, затем `id` — новее и больше первыми. */\n function newestFirst<T extends MemoryNote>(notes: T[]): T[] {\n return notes.sort(\n (left, right) =>\n right.updatedAt.localeCompare(left.updatedAt) ||\n right.createdAt.localeCompare(left.createdAt) ||\n (right.id > left.id ? 1 : right.id < left.id ? -1 : 0),\n );\n }\n\n /** Обычная запись без служебного признака скрытия. */\n function publicNote({ mergedInto: _hidden, ...note }: StoredNote): MemoryNote {\n return note;\n }\n\n /** Как база с NOT NULL: разделы — список строк, текст — строка. */\n function assertWellFormedNote(note: MemoryNoteInput): void {\n if (!Array.isArray(note?.categories) || !note.categories.every((category) => typeof category === \"string\")) {\n throw new TypeError(\"Разделы записи памяти — список строк\");\n }\n if (typeof note.content !== \"string\") throw new TypeError(\"Текст записи памяти — строка\");\n }\n\n /** Записи, которые видит человек: без скрытых оригиналов, копиями. */\n function visibleNotes(ownerId: string): MemoryNote[] {\n const visible = [...ownerNotes(ownerId).values()].filter((note) => !note.mergedInto);\n return structuredClone(newestFirst(visible).map(publicNote));\n }\n\n function insertNotes(ownerId: string, inputs: MemoryNoteInput[]): MemoryNote[] {\n // Сначала проверяем весь пакет: негодная запись не оставляет в памяти и остальных.\n inputs.forEach(assertWellFormedNote);\n const now = new Date().toISOString();\n const created = inputs.map(\n ({ categories, content }): MemoryNote => ({\n id: crypto.randomUUID(),\n categories: [...categories],\n content,\n version: 1,\n createdAt: now,\n updatedAt: now,\n }),\n );\n const notes = ownerNotes(ownerId);\n for (const note of created) notes.set(note.id, note);\n return structuredClone(created);\n }\n\n function updateNote(ownerId: string, id: string, expectedVersion: number, input: MemoryNoteInput): MemoryNote | \"not_found\" | \"stale_version\" {\n assertWellFormedNote(input);\n const notes = ownerNotes(ownerId);\n const note = notes.get(id);\n if (!note || note.mergedInto) return \"not_found\";\n if (note.version !== expectedVersion) return \"stale_version\";\n const updated: MemoryNote = {\n ...note,\n categories: [...input.categories],\n content: input.content,\n version: note.version + 1,\n updatedAt: new Date().toISOString(),\n };\n notes.set(id, updated);\n return structuredClone(updated);\n }\n\n /** Как уникальный индекс (владелец, ключ): чат без ключа не мешает никому. */\n function assertSubjectFree(chat: Pick<Chat, \"ownerId\" | \"subjectKey\">): void {\n if (chat.subjectKey === undefined) return;\n for (const existing of chats.values()) {\n if (existing.ownerId === chat.ownerId && existing.subjectKey === chat.subjectKey) {\n throw new DuplicateChatSubjectError(chat.ownerId, chat.subjectKey);\n }\n }\n }\n\n function sortedMessages(chatId: string): StoredMessage[] {\n return [...messages.values()]\n .filter((message) => message.chatId === chatId)\n .sort((left, right) => left.createdAt.localeCompare(right.createdAt));\n }\n\n return {\n async createChat({ createdAt, ...input }) {\n const at = createdAt ?? new Date().toISOString();\n const chat: Chat = withChatData(withoutUndefined({ ...structuredClone(input), createdAt: at, updatedAt: at }));\n assertSubjectFree(chat);\n chats.set(chat.id, chat);\n return structuredClone(chat);\n },\n\n async findChatBySubject(ownerId, subjectKey) {\n const chat = [...chats.values()].find((existing) => existing.ownerId === ownerId && existing.subjectKey === subjectKey);\n return chat ? structuredClone(chat) : null;\n },\n\n async importChat(input, imported) {\n // Сначала все проверки: при ошибке ничего не записано.\n if (chats.has(input.id)) throw new ChatAlreadyExistsError(input.id);\n const chat: Chat = withChatData(withoutUndefined(structuredClone(input)));\n assertSubjectFree(chat);\n const ids = new Set<string>();\n for (const message of imported) {\n if (messages.has(message.id) || ids.has(message.id)) throw new DuplicateMessageError(message.id);\n ids.add(message.id);\n }\n chats.set(chat.id, chat);\n for (const message of imported) {\n const { alive: _alive, ...stored } = message;\n messages.set(message.id, structuredClone(stored));\n }\n },\n\n async setAfterTurnState(messageId, state, options) {\n const message = messages.get(messageId);\n if (!message || (state !== \"pending\" && message.afterTurn !== \"pending\")) return false;\n const parts = options?.appendParts ? [...message.parts, ...structuredClone(options.appendParts)] : message.parts;\n messages.set(messageId, { ...message, parts, afterTurn: state });\n // Отметка ожидания хука — в той же колонке, что знак жизни: у законченного ответа она больше ни для чего.\n if (state === \"pending\") aliveAt.set(messageId, aliveAtByLocalClock(\"now\"));\n return true;\n },\n\n async listPendingAfterTurn(filter) {\n const boundary = filter?.pendingForMs === undefined ? null : silenceBoundaryByLocalClock(filter.pendingForMs);\n return [...messages.values()]\n .filter((message) => message.afterTurn === \"pending\")\n .filter((message) => boundary === null || (aliveAt.get(message.id) ?? message.createdAt) < boundary)\n .map((message) => message.id);\n },\n\n async getChat(chatId) {\n const chat = chats.get(chatId);\n return chat ? structuredClone(chat) : null;\n },\n\n async updateChatModel(chatId, model) {\n const chat = chats.get(chatId);\n if (!chat) return;\n const { reasoning: _dropped, ...rest } = chat;\n chats.set(chatId, { ...rest, ...model });\n },\n\n async listChats(ownerId, options?: ChatListOptions) {\n if (options?.data) assertChatDataFilter(options.data);\n const owned = [...chats.values()].filter(\n (chat) => chat.ownerId === ownerId && matchesSubjects(chat, options?.subjects) && (!options?.data || chatDataMatches(chat.data, options.data)),\n );\n owned.sort(\n (left, right) =>\n right.updatedAt.localeCompare(left.updatedAt) ||\n right.createdAt.localeCompare(left.createdAt) ||\n (right.id > left.id ? 1 : right.id < left.id ? -1 : 0),\n );\n return structuredClone(owned);\n },\n\n async updateChatData(chatId, patch, { maxBytes }) {\n const chat = chats.get(chatId);\n if (!chat) return null;\n // Слияние и проверка размера до записи: при ошибке данные прежние.\n const merged = mergeChatData(chat.data, patch, maxBytes);\n const { data: _data, subjectData: _subjectData, ...rest } = chat;\n const updated: Chat = { ...rest, ...chatDataFields(merged) };\n chats.set(chatId, updated);\n return structuredClone(updated);\n },\n\n async renameChat(chatId, title) {\n const chat = chats.get(chatId);\n if (!chat) return;\n const { title: _dropped, ...rest } = chat;\n chats.set(chatId, title === null ? rest : { ...rest, title });\n },\n\n async setChatClosed(chatId, closed) {\n const chat = chats.get(chatId);\n if (!chat) return;\n const { closed: _dropped, ...rest } = chat;\n chats.set(chatId, closed === null ? rest : { ...rest, closed: { code: closed.code, message: closed.message } });\n },\n\n async deleteChat(chatId) {\n if (!chats.delete(chatId)) return;\n turnLeases.delete(chatId);\n pendingBursts.delete(chatId);\n for (const message of [...messages.values()]) {\n if (message.chatId !== chatId) continue;\n messages.delete(message.id);\n aliveAt.delete(message.id);\n }\n // Записи сжатия — на месте, с обратным ходом, чтобы индексы не сдвигались.\n for (let index = compactions.length - 1; index >= 0; index -= 1) {\n if (compactions[index]!.chatId === chatId) compactions.splice(index, 1);\n }\n for (let index = branchSelections.length - 1; index >= 0; index -= 1) {\n if (branchSelections[index]!.chatId === chatId) branchSelections.splice(index, 1);\n }\n },\n\n async getMessage(messageId) {\n const message = messages.get(messageId);\n if (!message) return null;\n assertKnownFormatVersion(message);\n return readable(message);\n },\n\n async listMessages(chatId) {\n const found = sortedMessages(chatId);\n found.forEach(assertKnownFormatVersion);\n return found.map(readable);\n },\n\n async listMessageLinks(chatId) {\n return sortedMessages(chatId).map(({ id, parentId, createdAt }) => ({ id, parentId, createdAt }));\n },\n\n async listMessagePartsOfTypes(chatId, types) {\n return sortedMessages(chatId).map(({ id, parentId, createdAt, parts }) => ({\n id,\n parentId,\n createdAt,\n parts: structuredClone(parts.filter((part) => types.includes(part.type))),\n }));\n },\n\n async insertMessage(message) {\n if (messages.has(message.id)) throw new DuplicateMessageError(message.id);\n // Как внешний ключ в базе: сообщение без чата не пишется.\n const chat = chats.get(message.chatId);\n if (!chat) throw new Error(`Чата ${message.chatId} нет`);\n const { alive, ...stored } = message;\n messages.set(message.id, structuredClone(stored));\n if (alive) aliveAt.set(message.id, aliveAtByLocalClock(alive));\n chats.set(chat.id, { ...chat, updatedAt: message.createdAt });\n },\n\n async updateStreamingMessage(messageId, patch) {\n const message = streamingMessage(messageId);\n const { alive, ...rest } = structuredClone(patch);\n messages.set(messageId, { ...message, ...rest });\n if (alive) aliveAt.set(messageId, aliveAtByLocalClock(alive));\n },\n\n async markAnswerAlive(messageId) {\n streamingMessage(messageId);\n aliveAt.set(messageId, aliveAtByLocalClock(\"now\"));\n },\n\n async finishMessage(messageId, final) {\n const message = streamingMessage(messageId);\n messages.set(messageId, { ...message, ...structuredClone(final) });\n if (final.afterTurn === \"pending\") aliveAt.set(messageId, aliveAtByLocalClock(\"now\"));\n },\n\n async updateAwaitingMessage(messageId, patch) {\n const message = messages.get(messageId);\n if (!message) throw new Error(`Сообщения ${messageId} нет`);\n if (message.status !== \"awaiting_approval\") throw new MessageNotAwaitingError(messageId);\n if (patch.expectedParts && !sameMessageParts(message.parts, patch.expectedParts)) throw new MessageNotAwaitingError(messageId);\n messages.set(messageId, {\n ...message,\n parts: structuredClone(patch.parts),\n ...(patch.status && { status: patch.status }),\n });\n if (patch.alive) aliveAt.set(messageId, aliveAtByLocalClock(patch.alive));\n },\n\n async listStreamingMessageIds() {\n return [...messages.values()].filter((message) => message.status === \"streaming\").map((message) => message.id);\n },\n\n async listSilentAnswerIds({ silentForMs, chatId }) {\n const boundary = silenceBoundaryByLocalClock(silentForMs);\n return [...messages.values()]\n .filter((message) => message.status === \"streaming\" && (chatId === undefined || message.chatId === chatId))\n .filter((message) => (aliveAt.get(message.id) ?? message.createdAt) < boundary)\n .map((message) => message.id);\n },\n\n async claimChatTurn(chatId, holder, leaseMs) {\n const now = Date.now();\n const lease = turnLeases.get(chatId);\n if (lease && lease.holder !== holder && lease.until > now) return false;\n turnLeases.set(chatId, { holder, until: now + leaseMs });\n return true;\n },\n\n async releaseChatTurn(chatId, holder) {\n if (turnLeases.get(chatId)?.holder === holder) turnLeases.delete(chatId);\n },\n\n async clearChatTurnLeases() {\n turnLeases.clear();\n },\n\n async savePendingBurst(burst) {\n pendingBursts.set(burst.chatId, structuredClone(burst));\n },\n\n async takeAbandonedBursts({ abandonedBefore, holder, heldUntil, limit, exceptHolder }) {\n const abandoned = [...pendingBursts.values()]\n .filter((burst) => Date.parse(burst.heldUntil) <= Date.parse(abandonedBefore) && burst.holder !== exceptHolder)\n .sort((first, second) => Date.parse(first.dueAt) - Date.parse(second.dueAt))\n .slice(0, limit);\n return abandoned.map((burst) => {\n const taken = { ...burst, holder, heldUntil };\n pendingBursts.set(burst.chatId, taken);\n return structuredClone(taken);\n });\n },\n\n async renewPendingBurst({ chatId, messageId, holder, heldUntil }) {\n const burst = pendingBursts.get(chatId);\n if (burst?.messageId !== messageId || burst.holder !== holder) return false;\n pendingBursts.set(chatId, { ...burst, heldUntil });\n return true;\n },\n\n async removePendingBurst(chatId, messageId) {\n if (pendingBursts.get(chatId)?.messageId === messageId) pendingBursts.delete(chatId);\n },\n\n async insertCompaction(record) {\n if (compactions.some((existing) => existing.id === record.id)) throw new Error(`Запись сжатия ${record.id} уже есть`);\n compactions.push(structuredClone(record));\n },\n\n async listCompactions(chatId) {\n return structuredClone(compactions.filter((record) => record.chatId === chatId));\n },\n\n async insertBranchSelection(record) {\n if (branchSelections.some((existing) => existing.id === record.id)) throw new Error(`Выбор ветки ${record.id} уже есть`);\n branchSelections.push(structuredClone(record));\n },\n\n async listBranchSelections(chatId) {\n return structuredClone(branchSelections.filter((record) => record.chatId === chatId));\n },\n\n async insertUsageRecord(record) {\n if (usageRecords.some((existing) => existing.id === record.id)) throw new Error(`Запись расхода ${record.id} уже есть`);\n usageRecords.push(structuredClone(record));\n },\n\n async hasUsageRecord(id) {\n return usageRecords.some((record) => record.id === id);\n },\n\n async sumUsage({ ownerId, chatId, messageId, source, since }) {\n return sumRecords(\n usageRecords.filter(\n (record) =>\n (ownerId === undefined || record.ownerId === ownerId) &&\n (chatId === undefined || record.chatId === chatId) &&\n (messageId === undefined || record.messageId === messageId) &&\n (source === undefined || record.source === source) &&\n (since === undefined || record.createdAt >= since),\n ),\n (record) => ({ modelId: record.modelId, providerId: record.providerId }),\n );\n },\n\n async sumUsageByChat({ chatIds, ownerId, source, since }) {\n const wanted = new Set(chatIds);\n return sumRecords(\n usageRecords.filter(\n (record) =>\n wanted.has(record.chatId) &&\n (ownerId === undefined || record.ownerId === ownerId) &&\n (source === undefined || record.source === source) &&\n (since === undefined || record.createdAt >= since),\n ),\n (record) => ({ chatId: record.chatId, modelId: record.modelId, providerId: record.providerId }),\n );\n },\n\n async setVoiceCut({ ownerId, chatId, messageId, voiceCut }) {\n const message = messages.get(messageId);\n if (!message || message.chatId !== chatId || chats.get(chatId)?.ownerId !== ownerId) return;\n messages.set(messageId, { ...message, voiceCut: structuredClone(voiceCut) });\n },\n\n async getSpendingLimitState(key) {\n const record = spendingLimitStates.get(key);\n return record ? { ...record } : null;\n },\n\n async saveSpendingLimitState(key, value, expectedVersion) {\n const current = spendingLimitStates.get(key);\n if ((current?.version ?? null) !== expectedVersion) return false;\n spendingLimitStates.set(key, { value, version: (current?.version ?? 0) + 1 });\n return true;\n },\n\n async deleteSpendingLimitStates(keys) {\n for (const key of keys) spendingLimitStates.delete(key);\n },\n\n async getVoiceChoice(ownerId) {\n return voiceChoices.get(ownerId) ?? null;\n },\n\n async setVoiceChoice(ownerId, voiceId) {\n voiceChoices.set(ownerId, voiceId);\n },\n\n async getUserModelChoice(ownerId) {\n const choice = userModelChoices.get(ownerId);\n return choice ? structuredClone(choice) : null;\n },\n\n async saveUserModelChoice(ownerId, choice) {\n userModelChoices.set(ownerId, structuredClone(choice));\n },\n\n async listMemoryNotes(ownerId) {\n return visibleNotes(ownerId);\n },\n\n async insertMemoryNotes(ownerId, inputs) {\n return insertNotes(ownerId, inputs);\n },\n\n // Всё выполняется без ожидания, поэтому между проверкой и вставкой никто не успеет вклиниться.\n async insertMemoryNotesChecked(ownerId, inputs, guard) {\n const error = guard(visibleNotes(ownerId));\n return error ?? insertNotes(ownerId, inputs);\n },\n\n async updateMemoryNote(ownerId, id, expectedVersion, input) {\n return updateNote(ownerId, id, expectedVersion, input);\n },\n\n async updateMemoryNoteChecked(ownerId, id, expectedVersion, input, guard) {\n return guard(visibleNotes(ownerId)) ?? updateNote(ownerId, id, expectedVersion, input);\n },\n\n async deleteMemoryNote(ownerId, id, expectedVersion) {\n const notes = ownerNotes(ownerId);\n const note = notes.get(id);\n if (!note || note.mergedInto) return \"not_found\";\n if (note.version !== expectedVersion) return \"stale_version\";\n notes.delete(id);\n for (const [hiddenId, hidden] of notes) if (hidden.mergedInto === id) notes.delete(hiddenId);\n return \"ok\";\n },\n\n async clearMemoryNotes(ownerId) {\n const count = [...ownerNotes(ownerId).values()].filter((note) => !note.mergedInto).length;\n memoryNotes.delete(ownerId);\n optimizationRuns.delete(ownerId);\n return count;\n },\n\n async claimMemoryOptimizationRun(ownerId, rules, now) {\n const runs = ownerRuns(ownerId);\n const nowMs = Date.parse(now);\n for (const run of runs) {\n const leaseEnd = Date.parse(run.startedAt) + rules.leaseMs;\n if (run.status === \"running\" && nowMs >= leaseEnd) Object.assign(run, { status: \"failed\", error: \"lease_expired\", finishedAt: new Date(leaseEnd).toISOString() });\n }\n const blocked = isOptimizationBlocked(\n runs.map((run) => ({ status: run.status, startedAtMs: Date.parse(run.startedAt), endedAtMs: run.finishedAt ? Date.parse(run.finishedAt) : null })),\n rules,\n nowMs,\n );\n if (blocked) return null;\n const run: MemoryOptimizationRun = { id: crypto.randomUUID(), startedAt: now, finishedAt: null, status: \"running\", groups: [], error: null, usageRecordId: null };\n runs.push(run);\n return structuredClone(run);\n },\n\n async finishMemoryOptimizationRun(ownerId, runId, outcome) {\n const run = ownerRuns(ownerId).find((candidate) => candidate.id === runId);\n if (run?.status !== \"running\") return \"not_found\";\n Object.assign(run, { status: outcome.status, finishedAt: outcome.finishedAt, error: outcome.error ?? null, usageRecordId: outcome.usageRecordId ?? null });\n return \"ok\";\n },\n\n async applyMemoryMerge(ownerId, merge) {\n const { merges } = merge;\n if (merges.length === 0) throw new TypeError(\"Слияние без склеек\");\n const seen = new Set<string>();\n for (const { result, originals } of merges) {\n assertWellFormedNote(result);\n if (originals.length === 0) throw new TypeError(\"Склейка без оригиналов\");\n for (const { id } of originals) {\n if (seen.has(id)) throw new TypeError(`Запись ${id} названа в двух склейках`);\n seen.add(id);\n }\n }\n const run = ownerRuns(ownerId).find((candidate) => candidate.id === merge.runId);\n if (run?.status !== \"running\") return \"run_not_active\";\n const notes = ownerNotes(ownerId);\n for (const { originals } of merges) {\n for (const { id, version } of originals) {\n const original = notes.get(id);\n if (!original || original.mergedInto || original.version !== version) return \"stale_version\";\n }\n }\n const now = new Date().toISOString();\n const groups: MemoryMergeGroup[] = merges.map(({ result, originals }) => {\n const created: StoredNote = { id: crypto.randomUUID(), categories: [...result.categories], content: result.content, version: 1, createdAt: now, updatedAt: now };\n notes.set(created.id, created);\n for (const { id } of originals) notes.get(id)!.mergedInto = created.id;\n return { resultId: created.id, originalIds: originals.map(({ id }) => id), resultVersion: 1 };\n });\n Object.assign(run, { status: \"applied\", finishedAt: merge.finishedAt, groups, usageRecordId: merge.usageRecordId ?? null });\n return structuredClone(run);\n },\n\n async revertMemoryMerge(ownerId, runId, guard) {\n const run = ownerRuns(ownerId).find((candidate) => candidate.id === runId);\n if (run?.status !== \"applied\") return \"not_found\";\n const notes = ownerNotes(ownerId);\n const intact = run.groups.every(\n ({ resultId, originalIds, resultVersion }) =>\n notes.get(resultId)?.version === resultVersion && !notes.get(resultId)?.mergedInto && originalIds.every((id) => notes.get(id)?.mergedInto === resultId),\n );\n if (!intact) return \"conflict\";\n if (guard) {\n const resultIds = new Set(run.groups.map(({ resultId }) => resultId));\n const restoredIds = new Set(run.groups.flatMap(({ originalIds }) => originalIds));\n const inPlay = [...notes.values()];\n const remaining = newestFirst(inPlay.filter((note) => !note.mergedInto && !resultIds.has(note.id))).map(publicNote);\n const restored = newestFirst(inPlay.filter((note) => restoredIds.has(note.id))).map(publicNote);\n const error = guard(structuredClone(remaining), structuredClone(restored));\n if (error) return revertRefusal(error);\n }\n for (const { resultId, originalIds } of run.groups) {\n notes.delete(resultId);\n for (const id of originalIds) delete notes.get(id)!.mergedInto;\n }\n run.status = \"reverted\";\n return \"ok\";\n },\n\n async listMemoryOptimizationRuns(ownerId, filter) {\n const runs = ownerRuns(ownerId)\n .map((run, index) => ({ run, index }))\n .filter(({ run }) => !filter?.status || filter.status.includes(run.status))\n .sort((left, right) => right.run.startedAt.localeCompare(left.run.startedAt) || right.index - left.index)\n .map(({ run }) => run);\n return structuredClone(filter?.limit === undefined ? runs : runs.slice(0, filter.limit));\n },\n\n async listMergedMemoryNotes(ownerId) {\n const hidden = [...ownerNotes(ownerId).values()].filter((note): note is StoredNote & { mergedInto: string } => Boolean(note.mergedInto));\n return structuredClone(newestFirst(hidden) as MergedMemoryNote[]);\n },\n };\n}\n\n/** Подходит ли чат под фильтр `listChats` по ключу объекта. */\nfunction matchesSubjects(chat: Chat, subjects: ChatSubjectFilter = \"exclude\"): boolean {\n if (subjects === \"include\") return true;\n return subjects === \"only\" ? chat.subjectKey !== undefined : chat.subjectKey === undefined;\n}\n\n/** `data` и `subjectData` — один объект; данных нет — полей нет. */\nfunction withChatData<T extends Pick<Chat, \"data\" | \"subjectData\">>(chat: T): T {\n const { data: _data, subjectData: _subjectData, ...rest } = chat;\n return { ...rest, ...chatDataFields(chatDataOfInput(chat)) } as T;\n}\n\n/** Как база: поле без значения не хранится. */\nfunction withoutUndefined<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)) as T;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8EO,SAAS,kBAAkB,OAA6D,eAA+B;AAC5H,QAAM,EAAE,oBAAoB,IAAI;AAChC,MAAI,OAAO,wBAAwB,SAAU,QAAO;AACpD,SAAO,oBAAoB,KAAK,IAAI,eAAe,oBAAoB,MAAM,IAAI,CAAC,KAAK;AACzF;AAMO,SAAS,sBAAsB,MAAwC,OAAgC,OAAwB;AACpI,QAAM,UAAU,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,cAAc,MAAM,WAAW;AACpF,MAAI,gBAAgB;AACpB,aAAW,OAAO,SAAS;AACzB,QAAI,IAAI,WAAW,UAAW,QAAO;AACrC,oBAAgB,IAAI,WAAW,WAAW,gBAAgB,IAAI;AAC9D,UAAM,QAAQ,IAAI,WAAW,WAAW,kBAAkB,OAAO,aAAa,IAAI,MAAM;AACxF,QAAI,SAAS,IAAI,aAAa,IAAI,eAAe,MAAO,QAAO;AAAA,EACjE;AACA,SAAO;AACT;AAGO,SAAS,cAAc,OAAmD;AAC/E,SAAO,MAAM,SAAS,cAAc,cAAc;AACpD;;;AC/EO,SAAS,kBAAkB,SAAgC;AAChE,MAAI,QAAQ,WAAW,iBAAiB;AACtC,WACE,uJAA+B,QAAQ,MAAM,2EAA8B,QAAQ,MAAM,KAAK,IAAI,CAAC;AAAA,EAIvG;AACA,SAAO,gLAAoC,QAAQ,MAAM;AAC3D;AAMO,SAAS,aAAa,QAAyB;AACpD,QAAM,QAAQ,OAAO,YAAY;AACjC,SAAO,UAAU,SAAS,UAAU,UAAU,UAAU;AAC1D;AAGA,IAAM,uBAAuB;AAC7B,IAAM,sBAAsB;AAErB,SAAS,yBAAyB,UAAqC,CAAC,GAAuB;AACpG,QAAM,YAAY,sBAAsB,QAAQ,kBAAkB,CAAC,CAAC;AACpE,QAAM,WAAW,oBAAI,IAAY;AACjC,QAAM,SAAS,CAAC,YAAkC;AAChD,UAAM,QAAQ,QAAQ,OAAO,MAAM,GAAG,mBAAmB;AACzD,QAAI,QAAQ,aAAa,CAAC,SAAS,IAAI,KAAK,KAAK,SAAS,OAAO,sBAAsB;AACrF,eAAS,IAAI,KAAK;AAClB,cAAQ,UAAU,EAAE,GAAG,SAAS,QAAQ,MAAM,CAAC;AAAA,IACjD;AACA,WAAO;AAAA,EACT;AACA,SAAO,CAAC,YAAY;AAClB,UAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAC3C,QAAI,WAAW,QAAQ,UAAU,MAAM,EAAG,QAAO;AACjD,UAAM,OAAO,QAAQ,QAAQ,IAAI,gBAAgB;AACjD,QAAI,SAAS,MAAM;AACjB,UAAI,SAAS,iBAAiB,SAAS,OAAQ,QAAO;AACtD,aAAO,OAAO,EAAE,QAAQ,UAAU,+CAAgC,IAAI,KAAK,QAAQ,aAAa,CAAC;AAAA,IACnG;AAEA,QAAI,WAAW,KAAM,QAAO;AAE5B,UAAM,OAAO,WAAW,MAAM;AAC9B,UAAM,QAAQ,QAAQ,OAAO;AAC7B,QAAI,SAAS,QAAQ,MAAM,IAAI,IAAI,EAAG,QAAO;AAC7C,WAAO,OAAO,EAAE,QAAQ,QAAQ,iBAAiB,OAAO,CAAC,GAAG,KAAK,EAAE,CAAC;AAAA,EACtE;AACF;AAGA,IAAM,iBAAiB;AAGvB,SAAS,sBAAsB,SAAyD;AACtF,QAAM,QAAQ,oBAAI,IAAY;AAC9B,QAAM,eAA0E,CAAC;AACjF,aAAW,SAAS,SAAS;AAC3B,UAAM,WAAW,6BAA6B,KAAK,KAAK;AACxD,UAAM,MAAM,iBAAiB,WAAW,GAAG,SAAS,CAAC,CAAC,GAAG,cAAc,IAAI,SAAS,CAAC,CAAC,KAAK,KAAK;AAChG,QAAI,QAAQ,QAAS,CAAC,YAAY,MAAM,SAAS,GAAG,EAAI,OAAM,WAAW,KAAK;AAC9E,QAAI,CAAC,UAAU;AACb,YAAM,IAAI,IAAI,MAAM;AACpB;AAAA,IACF;AACA,UAAM,SAAS,IAAI,SAAS,MAAM,eAAe,MAAM;AAGvD,UAAM,WAAW,OAAO,SAAS,GAAG,KAAM,CAAC,OAAO,MAAM,CAAC,EAAE,SAAS,GAAG,KAAK,WAAW;AACvF,QAAI,CAAC,IAAI,SAAS,WAAW,GAAG,cAAc,GAAG,KAAK,SAAU,OAAM,WAAW,KAAK;AACtF,iBAAa,KAAK,EAAE,UAAU,IAAI,UAAU,MAAM,IAAI,MAAM,OAAO,CAAC;AAAA,EACtE;AACA,SAAO,CAAC,WAAW;AACjB,UAAM,MAAM,YAAY,MAAM;AAC9B,QAAI,QAAQ,KAAM,QAAO;AACzB,QAAI,MAAM,IAAI,IAAI,MAAM,EAAG,QAAO;AAClC,WAAO,aAAa;AAAA,MAClB,CAAC,YACC,IAAI,aAAa,QAAQ,YAAY,IAAI,SAAS,QAAQ,QAAQ,IAAI,SAAS,SAAS,QAAQ,MAAM,KAAK,IAAI,SAAS,SAAS,QAAQ,OAAO;AAAA,IACpJ;AAAA,EACF;AACF;AAGA,SAAS,iBAAiB,OAA2B;AACnD,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK;AACzB,UAAM,OAAO,IAAI,aAAa,OAAO,CAAC,IAAI,UAAU,CAAC,IAAI,QAAQ,CAAC,IAAI;AACtE,YAAQ,IAAI,aAAa,WAAW,IAAI,aAAa,aAAa,OAAO,MAAM;AAAA,EACjF,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,WAAW,OAAsB;AACxC,SAAO,IAAI;AAAA,IACT,uBAAoB,KAAK;AAAA,EAE3B;AACF;AAEA,SAAS,YAAY,QAA4B;AAC/C,MAAI;AACF,WAAO,IAAI,IAAI,MAAM;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,SAAS,WAAW,QAA+B;AACjD,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,MAAM;AAC1B,WAAO,IAAI,aAAa,WAAW,IAAI,aAAa,WAAW,IAAI,OAAO;AAAA,EAC5E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,SAAS,QAAQ,SAA+B;AAC9C,QAAM,OAAO,QAAQ,QAAQ,IAAI,MAAM;AACvC,QAAM,QAAQ,oBAAI,IAAY,CAAC,OAAO,KAAK,KAAK,EAAE,YAAY,IAAI,IAAI,IAAI,QAAQ,GAAG,EAAE,IAAI,CAAC;AAC5F,aAAW,aAAa,QAAQ,QAAQ,IAAI,kBAAkB,GAAG,MAAM,GAAG,KAAK,CAAC,GAAG;AACjF,QAAI,UAAU,KAAK,EAAG,OAAM,IAAI,UAAU,KAAK,EAAE,YAAY,CAAC;AAAA,EAChE;AACA,SAAO;AACT;;;ACtIA,SAAS,WAA+D,SAAiC,OAAwD;AAC/J,QAAM,OAAO,oBAAI,IAA0B;AAC3C,aAAW,UAAU,SAAS;AAC5B,UAAM,SAAS,MAAM,MAAM;AAC3B,UAAM,MAAM,OAAO,OAAO,MAAM,EAAE,KAAK,IAAQ;AAC/C,UAAM,MAAM,KAAK,IAAI,GAAG,KAAK,EAAE,GAAG,QAAQ,aAAa,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,cAAc,GAAG,iBAAiB,GAAG,SAAS,EAAE;AACrJ,QAAI,eAAe,OAAO;AAC1B,QAAI,qBAAqB,OAAO;AAChC,QAAI,oBAAoB,OAAO;AAC/B,QAAI,gBAAgB,OAAO;AAC3B,QAAI,mBAAmB,OAAO;AAC9B,QAAI,WAAW,OAAO;AACtB,SAAK,IAAI,KAAK,GAAG;AAAA,EACnB;AACA,SAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B;AAEO,SAAS,4BAAyC;AACvD,QAAM,QAAQ,oBAAI,IAAkB;AACpC,QAAM,WAAW,oBAAI,IAA2B;AAEhD,QAAM,UAAU,oBAAI,IAAoB;AAExC,QAAM,aAAa,oBAAI,IAA+C;AAEtE,QAAM,gBAAgB,oBAAI,IAAgC;AAC1D,QAAM,cAAkC,CAAC;AACzC,QAAM,mBAA4C,CAAC;AACnD,QAAM,eAA8B,CAAC;AACrC,QAAM,mBAAmB,oBAAI,IAAyB;AACtD,QAAM,eAAe,oBAAI,IAAoB;AAC7C,QAAM,sBAAsB,oBAAI,IAAsC;AAEtE,QAAM,cAAc,oBAAI,IAAqC;AAC7D,QAAM,mBAAmB,oBAAI,IAAqC;AAElE,WAAS,iBAAiB,WAAkC;AAC1D,UAAM,UAAU,SAAS,IAAI,SAAS;AACtC,QAAI,CAAC,QAAS,OAAM,IAAI,MAAM,0DAAa,SAAS,qBAAM;AAC1D,QAAI,QAAQ,WAAW,YAAa,OAAM,IAAI,4BAA4B,SAAS;AACnF,WAAO;AAAA,EACT;AAGA,WAAS,SAAS,SAAuC;AACvD,UAAM,OAAO,gBAAgB,OAAO;AACpC,WAAO,EAAE,GAAG,MAAM,UAAU,KAAK,YAAY,KAAK;AAAA,EACpD;AAEA,WAAS,WAAW,SAA0C;AAC5D,QAAI,QAAQ,YAAY,IAAI,OAAO;AACnC,QAAI,CAAC,OAAO;AACV,cAAQ,oBAAI,IAAI;AAChB,kBAAY,IAAI,SAAS,KAAK;AAAA,IAChC;AACA,WAAO;AAAA,EACT;AAEA,WAAS,UAAU,SAA0C;AAC3D,QAAI,OAAO,iBAAiB,IAAI,OAAO;AACvC,QAAI,CAAC,MAAM;AACT,aAAO,CAAC;AACR,uBAAiB,IAAI,SAAS,IAAI;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAGA,WAAS,YAAkC,OAAiB;AAC1D,WAAO,MAAM;AAAA,MACX,CAAC,MAAM,UACL,MAAM,UAAU,cAAc,KAAK,SAAS,KAC5C,MAAM,UAAU,cAAc,KAAK,SAAS,MAC3C,MAAM,KAAK,KAAK,KAAK,IAAI,MAAM,KAAK,KAAK,KAAK,KAAK;AAAA,IACxD;AAAA,EACF;AAGA,WAAS,WAAW,EAAE,YAAY,SAAS,GAAG,KAAK,GAA2B;AAC5E,WAAO;AAAA,EACT;AAGA,WAAS,qBAAqB,MAA6B;AACzD,QAAI,CAAC,MAAM,QAAQ,MAAM,UAAU,KAAK,CAAC,KAAK,WAAW,MAAM,CAAC,aAAa,OAAO,aAAa,QAAQ,GAAG;AAC1G,YAAM,IAAI,UAAU,iMAAsC;AAAA,IAC5D;AACA,QAAI,OAAO,KAAK,YAAY,SAAU,OAAM,IAAI,UAAU,sJAA8B;AAAA,EAC1F;AAGA,WAAS,aAAa,SAA+B;AACnD,UAAM,UAAU,CAAC,GAAG,WAAW,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,KAAK,UAAU;AACnF,WAAO,gBAAgB,YAAY,OAAO,EAAE,IAAI,UAAU,CAAC;AAAA,EAC7D;AAEA,WAAS,YAAY,SAAiB,QAAyC;AAE7E,WAAO,QAAQ,oBAAoB;AACnC,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,UAAU,OAAO;AAAA,MACrB,CAAC,EAAE,YAAY,QAAQ,OAAmB;AAAA,QACxC,IAAI,OAAO,WAAW;AAAA,QACtB,YAAY,CAAC,GAAG,UAAU;AAAA,QAC1B;AAAA,QACA,SAAS;AAAA,QACT,WAAW;AAAA,QACX,WAAW;AAAA,MACb;AAAA,IACF;AACA,UAAM,QAAQ,WAAW,OAAO;AAChC,eAAW,QAAQ,QAAS,OAAM,IAAI,KAAK,IAAI,IAAI;AACnD,WAAO,gBAAgB,OAAO;AAAA,EAChC;AAEA,WAAS,WAAW,SAAiB,IAAY,iBAAyB,OAAoE;AAC5I,yBAAqB,KAAK;AAC1B,UAAM,QAAQ,WAAW,OAAO;AAChC,UAAM,OAAO,MAAM,IAAI,EAAE;AACzB,QAAI,CAAC,QAAQ,KAAK,WAAY,QAAO;AACrC,QAAI,KAAK,YAAY,gBAAiB,QAAO;AAC7C,UAAM,UAAsB;AAAA,MAC1B,GAAG;AAAA,MACH,YAAY,CAAC,GAAG,MAAM,UAAU;AAAA,MAChC,SAAS,MAAM;AAAA,MACf,SAAS,KAAK,UAAU;AAAA,MACxB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IACpC;AACA,UAAM,IAAI,IAAI,OAAO;AACrB,WAAO,gBAAgB,OAAO;AAAA,EAChC;AAGA,WAAS,kBAAkB,MAAkD;AAC3E,QAAI,KAAK,eAAe,OAAW;AACnC,eAAW,YAAY,MAAM,OAAO,GAAG;AACrC,UAAI,SAAS,YAAY,KAAK,WAAW,SAAS,eAAe,KAAK,YAAY;AAChF,cAAM,IAAI,0BAA0B,KAAK,SAAS,KAAK,UAAU;AAAA,MACnE;AAAA,IACF;AAAA,EACF;AAEA,WAAS,eAAe,QAAiC;AACvD,WAAO,CAAC,GAAG,SAAS,OAAO,CAAC,EACzB,OAAO,CAAC,YAAY,QAAQ,WAAW,MAAM,EAC7C,KAAK,CAAC,MAAM,UAAU,KAAK,UAAU,cAAc,MAAM,SAAS,CAAC;AAAA,EACxE;AAEA,SAAO;AAAA,IACL,MAAM,WAAW,EAAE,WAAW,GAAG,MAAM,GAAG;AACxC,YAAM,KAAK,cAAa,oBAAI,KAAK,GAAE,YAAY;AAC/C,YAAM,OAAa,aAAa,iBAAiB,EAAE,GAAG,gBAAgB,KAAK,GAAG,WAAW,IAAI,WAAW,GAAG,CAAC,CAAC;AAC7G,wBAAkB,IAAI;AACtB,YAAM,IAAI,KAAK,IAAI,IAAI;AACvB,aAAO,gBAAgB,IAAI;AAAA,IAC7B;AAAA,IAEA,MAAM,kBAAkB,SAAS,YAAY;AAC3C,YAAM,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,KAAK,CAAC,aAAa,SAAS,YAAY,WAAW,SAAS,eAAe,UAAU;AACtH,aAAO,OAAO,gBAAgB,IAAI,IAAI;AAAA,IACxC;AAAA,IAEA,MAAM,WAAW,OAAO,UAAU;AAEhC,UAAI,MAAM,IAAI,MAAM,EAAE,EAAG,OAAM,IAAI,uBAAuB,MAAM,EAAE;AAClE,YAAM,OAAa,aAAa,iBAAiB,gBAAgB,KAAK,CAAC,CAAC;AACxE,wBAAkB,IAAI;AACtB,YAAM,MAAM,oBAAI,IAAY;AAC5B,iBAAW,WAAW,UAAU;AAC9B,YAAI,SAAS,IAAI,QAAQ,EAAE,KAAK,IAAI,IAAI,QAAQ,EAAE,EAAG,OAAM,IAAI,sBAAsB,QAAQ,EAAE;AAC/F,YAAI,IAAI,QAAQ,EAAE;AAAA,MACpB;AACA,YAAM,IAAI,KAAK,IAAI,IAAI;AACvB,iBAAW,WAAW,UAAU;AAC9B,cAAM,EAAE,OAAO,QAAQ,GAAG,OAAO,IAAI;AACrC,iBAAS,IAAI,QAAQ,IAAI,gBAAgB,MAAM,CAAC;AAAA,MAClD;AAAA,IACF;AAAA,IAEA,MAAM,kBAAkB,WAAW,OAAO,SAAS;AACjD,YAAM,UAAU,SAAS,IAAI,SAAS;AACtC,UAAI,CAAC,WAAY,UAAU,aAAa,QAAQ,cAAc,UAAY,QAAO;AACjF,YAAM,QAAQ,SAAS,cAAc,CAAC,GAAG,QAAQ,OAAO,GAAG,gBAAgB,QAAQ,WAAW,CAAC,IAAI,QAAQ;AAC3G,eAAS,IAAI,WAAW,EAAE,GAAG,SAAS,OAAO,WAAW,MAAM,CAAC;AAE/D,UAAI,UAAU,UAAW,SAAQ,IAAI,WAAW,oBAAoB,KAAK,CAAC;AAC1E,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,qBAAqB,QAAQ;AACjC,YAAM,WAAW,QAAQ,iBAAiB,SAAY,OAAO,4BAA4B,OAAO,YAAY;AAC5G,aAAO,CAAC,GAAG,SAAS,OAAO,CAAC,EACzB,OAAO,CAAC,YAAY,QAAQ,cAAc,SAAS,EACnD,OAAO,CAAC,YAAY,aAAa,SAAS,QAAQ,IAAI,QAAQ,EAAE,KAAK,QAAQ,aAAa,QAAQ,EAClG,IAAI,CAAC,YAAY,QAAQ,EAAE;AAAA,IAChC;AAAA,IAEA,MAAM,QAAQ,QAAQ;AACpB,YAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,aAAO,OAAO,gBAAgB,IAAI,IAAI;AAAA,IACxC;AAAA,IAEA,MAAM,gBAAgB,QAAQ,OAAO;AACnC,YAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,UAAI,CAAC,KAAM;AACX,YAAM,EAAE,WAAW,UAAU,GAAG,KAAK,IAAI;AACzC,YAAM,IAAI,QAAQ,EAAE,GAAG,MAAM,GAAG,MAAM,CAAC;AAAA,IACzC;AAAA,IAEA,MAAM,UAAU,SAAS,SAA2B;AAClD,UAAI,SAAS,KAAM,sBAAqB,QAAQ,IAAI;AACpD,YAAM,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE;AAAA,QAChC,CAAC,SAAS,KAAK,YAAY,WAAW,gBAAgB,MAAM,SAAS,QAAQ,MAAM,CAAC,SAAS,QAAQ,gBAAgB,KAAK,MAAM,QAAQ,IAAI;AAAA,MAC9I;AACA,YAAM;AAAA,QACJ,CAAC,MAAM,UACL,MAAM,UAAU,cAAc,KAAK,SAAS,KAC5C,MAAM,UAAU,cAAc,KAAK,SAAS,MAC3C,MAAM,KAAK,KAAK,KAAK,IAAI,MAAM,KAAK,KAAK,KAAK,KAAK;AAAA,MACxD;AACA,aAAO,gBAAgB,KAAK;AAAA,IAC9B;AAAA,IAEA,MAAM,eAAe,QAAQ,OAAO,EAAE,SAAS,GAAG;AAChD,YAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,UAAI,CAAC,KAAM,QAAO;AAElB,YAAM,SAAS,cAAc,KAAK,MAAM,OAAO,QAAQ;AACvD,YAAM,EAAE,MAAM,OAAO,aAAa,cAAc,GAAG,KAAK,IAAI;AAC5D,YAAM,UAAgB,EAAE,GAAG,MAAM,GAAG,eAAe,MAAM,EAAE;AAC3D,YAAM,IAAI,QAAQ,OAAO;AACzB,aAAO,gBAAgB,OAAO;AAAA,IAChC;AAAA,IAEA,MAAM,WAAW,QAAQ,OAAO;AAC9B,YAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,UAAI,CAAC,KAAM;AACX,YAAM,EAAE,OAAO,UAAU,GAAG,KAAK,IAAI;AACrC,YAAM,IAAI,QAAQ,UAAU,OAAO,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC;AAAA,IAC9D;AAAA,IAEA,MAAM,cAAc,QAAQ,QAAQ;AAClC,YAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,UAAI,CAAC,KAAM;AACX,YAAM,EAAE,QAAQ,UAAU,GAAG,KAAK,IAAI;AACtC,YAAM,IAAI,QAAQ,WAAW,OAAO,OAAO,EAAE,GAAG,MAAM,QAAQ,EAAE,MAAM,OAAO,MAAM,SAAS,OAAO,QAAQ,EAAE,CAAC;AAAA,IAChH;AAAA,IAEA,MAAM,WAAW,QAAQ;AACvB,UAAI,CAAC,MAAM,OAAO,MAAM,EAAG;AAC3B,iBAAW,OAAO,MAAM;AACxB,oBAAc,OAAO,MAAM;AAC3B,iBAAW,WAAW,CAAC,GAAG,SAAS,OAAO,CAAC,GAAG;AAC5C,YAAI,QAAQ,WAAW,OAAQ;AAC/B,iBAAS,OAAO,QAAQ,EAAE;AAC1B,gBAAQ,OAAO,QAAQ,EAAE;AAAA,MAC3B;AAEA,eAAS,QAAQ,YAAY,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;AAC/D,YAAI,YAAY,KAAK,EAAG,WAAW,OAAQ,aAAY,OAAO,OAAO,CAAC;AAAA,MACxE;AACA,eAAS,QAAQ,iBAAiB,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;AACpE,YAAI,iBAAiB,KAAK,EAAG,WAAW,OAAQ,kBAAiB,OAAO,OAAO,CAAC;AAAA,MAClF;AAAA,IACF;AAAA,IAEA,MAAM,WAAW,WAAW;AAC1B,YAAM,UAAU,SAAS,IAAI,SAAS;AACtC,UAAI,CAAC,QAAS,QAAO;AACrB,+BAAyB,OAAO;AAChC,aAAO,SAAS,OAAO;AAAA,IACzB;AAAA,IAEA,MAAM,aAAa,QAAQ;AACzB,YAAM,QAAQ,eAAe,MAAM;AACnC,YAAM,QAAQ,wBAAwB;AACtC,aAAO,MAAM,IAAI,QAAQ;AAAA,IAC3B;AAAA,IAEA,MAAM,iBAAiB,QAAQ;AAC7B,aAAO,eAAe,MAAM,EAAE,IAAI,CAAC,EAAE,IAAI,UAAU,UAAU,OAAO,EAAE,IAAI,UAAU,UAAU,EAAE;AAAA,IAClG;AAAA,IAEA,MAAM,wBAAwB,QAAQ,OAAO;AAC3C,aAAO,eAAe,MAAM,EAAE,IAAI,CAAC,EAAE,IAAI,UAAU,WAAW,MAAM,OAAO;AAAA,QACzE;AAAA,QACA;AAAA,QACA;AAAA,QACA,OAAO,gBAAgB,MAAM,OAAO,CAAC,SAAS,MAAM,SAAS,KAAK,IAAI,CAAC,CAAC;AAAA,MAC1E,EAAE;AAAA,IACJ;AAAA,IAEA,MAAM,cAAc,SAAS;AAC3B,UAAI,SAAS,IAAI,QAAQ,EAAE,EAAG,OAAM,IAAI,sBAAsB,QAAQ,EAAE;AAExE,YAAM,OAAO,MAAM,IAAI,QAAQ,MAAM;AACrC,UAAI,CAAC,KAAM,OAAM,IAAI,MAAM,4BAAQ,QAAQ,MAAM,qBAAM;AACvD,YAAM,EAAE,OAAO,GAAG,OAAO,IAAI;AAC7B,eAAS,IAAI,QAAQ,IAAI,gBAAgB,MAAM,CAAC;AAChD,UAAI,MAAO,SAAQ,IAAI,QAAQ,IAAI,oBAAoB,KAAK,CAAC;AAC7D,YAAM,IAAI,KAAK,IAAI,EAAE,GAAG,MAAM,WAAW,QAAQ,UAAU,CAAC;AAAA,IAC9D;AAAA,IAEA,MAAM,uBAAuB,WAAW,OAAO;AAC7C,YAAM,UAAU,iBAAiB,SAAS;AAC1C,YAAM,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,KAAK;AAChD,eAAS,IAAI,WAAW,EAAE,GAAG,SAAS,GAAG,KAAK,CAAC;AAC/C,UAAI,MAAO,SAAQ,IAAI,WAAW,oBAAoB,KAAK,CAAC;AAAA,IAC9D;AAAA,IAEA,MAAM,gBAAgB,WAAW;AAC/B,uBAAiB,SAAS;AAC1B,cAAQ,IAAI,WAAW,oBAAoB,KAAK,CAAC;AAAA,IACnD;AAAA,IAEA,MAAM,cAAc,WAAW,OAAO;AACpC,YAAM,UAAU,iBAAiB,SAAS;AAC1C,eAAS,IAAI,WAAW,EAAE,GAAG,SAAS,GAAG,gBAAgB,KAAK,EAAE,CAAC;AACjE,UAAI,MAAM,cAAc,UAAW,SAAQ,IAAI,WAAW,oBAAoB,KAAK,CAAC;AAAA,IACtF;AAAA,IAEA,MAAM,sBAAsB,WAAW,OAAO;AAC5C,YAAM,UAAU,SAAS,IAAI,SAAS;AACtC,UAAI,CAAC,QAAS,OAAM,IAAI,MAAM,0DAAa,SAAS,qBAAM;AAC1D,UAAI,QAAQ,WAAW,oBAAqB,OAAM,IAAI,wBAAwB,SAAS;AACvF,UAAI,MAAM,iBAAiB,CAAC,iBAAiB,QAAQ,OAAO,MAAM,aAAa,EAAG,OAAM,IAAI,wBAAwB,SAAS;AAC7H,eAAS,IAAI,WAAW;AAAA,QACtB,GAAG;AAAA,QACH,OAAO,gBAAgB,MAAM,KAAK;AAAA,QAClC,GAAI,MAAM,UAAU,EAAE,QAAQ,MAAM,OAAO;AAAA,MAC7C,CAAC;AACD,UAAI,MAAM,MAAO,SAAQ,IAAI,WAAW,oBAAoB,MAAM,KAAK,CAAC;AAAA,IAC1E;AAAA,IAEA,MAAM,0BAA0B;AAC9B,aAAO,CAAC,GAAG,SAAS,OAAO,CAAC,EAAE,OAAO,CAAC,YAAY,QAAQ,WAAW,WAAW,EAAE,IAAI,CAAC,YAAY,QAAQ,EAAE;AAAA,IAC/G;AAAA,IAEA,MAAM,oBAAoB,EAAE,aAAa,OAAO,GAAG;AACjD,YAAM,WAAW,4BAA4B,WAAW;AACxD,aAAO,CAAC,GAAG,SAAS,OAAO,CAAC,EACzB,OAAO,CAAC,YAAY,QAAQ,WAAW,gBAAgB,WAAW,UAAa,QAAQ,WAAW,OAAO,EACzG,OAAO,CAAC,aAAa,QAAQ,IAAI,QAAQ,EAAE,KAAK,QAAQ,aAAa,QAAQ,EAC7E,IAAI,CAAC,YAAY,QAAQ,EAAE;AAAA,IAChC;AAAA,IAEA,MAAM,cAAc,QAAQ,QAAQ,SAAS;AAC3C,YAAM,MAAM,KAAK,IAAI;AACrB,YAAM,QAAQ,WAAW,IAAI,MAAM;AACnC,UAAI,SAAS,MAAM,WAAW,UAAU,MAAM,QAAQ,IAAK,QAAO;AAClE,iBAAW,IAAI,QAAQ,EAAE,QAAQ,OAAO,MAAM,QAAQ,CAAC;AACvD,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,gBAAgB,QAAQ,QAAQ;AACpC,UAAI,WAAW,IAAI,MAAM,GAAG,WAAW,OAAQ,YAAW,OAAO,MAAM;AAAA,IACzE;AAAA,IAEA,MAAM,sBAAsB;AAC1B,iBAAW,MAAM;AAAA,IACnB;AAAA,IAEA,MAAM,iBAAiB,OAAO;AAC5B,oBAAc,IAAI,MAAM,QAAQ,gBAAgB,KAAK,CAAC;AAAA,IACxD;AAAA,IAEA,MAAM,oBAAoB,EAAE,iBAAiB,QAAQ,WAAW,OAAO,aAAa,GAAG;AACrF,YAAM,YAAY,CAAC,GAAG,cAAc,OAAO,CAAC,EACzC,OAAO,CAAC,UAAU,KAAK,MAAM,MAAM,SAAS,KAAK,KAAK,MAAM,eAAe,KAAK,MAAM,WAAW,YAAY,EAC7G,KAAK,CAAC,OAAO,WAAW,KAAK,MAAM,MAAM,KAAK,IAAI,KAAK,MAAM,OAAO,KAAK,CAAC,EAC1E,MAAM,GAAG,KAAK;AACjB,aAAO,UAAU,IAAI,CAAC,UAAU;AAC9B,cAAM,QAAQ,EAAE,GAAG,OAAO,QAAQ,UAAU;AAC5C,sBAAc,IAAI,MAAM,QAAQ,KAAK;AACrC,eAAO,gBAAgB,KAAK;AAAA,MAC9B,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,kBAAkB,EAAE,QAAQ,WAAW,QAAQ,UAAU,GAAG;AAChE,YAAM,QAAQ,cAAc,IAAI,MAAM;AACtC,UAAI,OAAO,cAAc,aAAa,MAAM,WAAW,OAAQ,QAAO;AACtE,oBAAc,IAAI,QAAQ,EAAE,GAAG,OAAO,UAAU,CAAC;AACjD,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,mBAAmB,QAAQ,WAAW;AAC1C,UAAI,cAAc,IAAI,MAAM,GAAG,cAAc,UAAW,eAAc,OAAO,MAAM;AAAA,IACrF;AAAA,IAEA,MAAM,iBAAiB,QAAQ;AAC7B,UAAI,YAAY,KAAK,CAAC,aAAa,SAAS,OAAO,OAAO,EAAE,EAAG,OAAM,IAAI,MAAM,6EAAiB,OAAO,EAAE,8CAAW;AACpH,kBAAY,KAAK,gBAAgB,MAAM,CAAC;AAAA,IAC1C;AAAA,IAEA,MAAM,gBAAgB,QAAQ;AAC5B,aAAO,gBAAgB,YAAY,OAAO,CAAC,WAAW,OAAO,WAAW,MAAM,CAAC;AAAA,IACjF;AAAA,IAEA,MAAM,sBAAsB,QAAQ;AAClC,UAAI,iBAAiB,KAAK,CAAC,aAAa,SAAS,OAAO,OAAO,EAAE,EAAG,OAAM,IAAI,MAAM,iEAAe,OAAO,EAAE,8CAAW;AACvH,uBAAiB,KAAK,gBAAgB,MAAM,CAAC;AAAA,IAC/C;AAAA,IAEA,MAAM,qBAAqB,QAAQ;AACjC,aAAO,gBAAgB,iBAAiB,OAAO,CAAC,WAAW,OAAO,WAAW,MAAM,CAAC;AAAA,IACtF;AAAA,IAEA,MAAM,kBAAkB,QAAQ;AAC9B,UAAI,aAAa,KAAK,CAAC,aAAa,SAAS,OAAO,OAAO,EAAE,EAAG,OAAM,IAAI,MAAM,mFAAkB,OAAO,EAAE,8CAAW;AACtH,mBAAa,KAAK,gBAAgB,MAAM,CAAC;AAAA,IAC3C;AAAA,IAEA,MAAM,eAAe,IAAI;AACvB,aAAO,aAAa,KAAK,CAAC,WAAW,OAAO,OAAO,EAAE;AAAA,IACvD;AAAA,IAEA,MAAM,SAAS,EAAE,SAAS,QAAQ,WAAW,QAAQ,MAAM,GAAG;AAC5D,aAAO;AAAA,QACL,aAAa;AAAA,UACX,CAAC,YACE,YAAY,UAAa,OAAO,YAAY,aAC5C,WAAW,UAAa,OAAO,WAAW,YAC1C,cAAc,UAAa,OAAO,cAAc,eAChD,WAAW,UAAa,OAAO,WAAW,YAC1C,UAAU,UAAa,OAAO,aAAa;AAAA,QAChD;AAAA,QACA,CAAC,YAAY,EAAE,SAAS,OAAO,SAAS,YAAY,OAAO,WAAW;AAAA,MACxE;AAAA,IACF;AAAA,IAEA,MAAM,eAAe,EAAE,SAAS,SAAS,QAAQ,MAAM,GAAG;AACxD,YAAM,SAAS,IAAI,IAAI,OAAO;AAC9B,aAAO;AAAA,QACL,aAAa;AAAA,UACX,CAAC,WACC,OAAO,IAAI,OAAO,MAAM,MACvB,YAAY,UAAa,OAAO,YAAY,aAC5C,WAAW,UAAa,OAAO,WAAW,YAC1C,UAAU,UAAa,OAAO,aAAa;AAAA,QAChD;AAAA,QACA,CAAC,YAAY,EAAE,QAAQ,OAAO,QAAQ,SAAS,OAAO,SAAS,YAAY,OAAO,WAAW;AAAA,MAC/F;AAAA,IACF;AAAA,IAEA,MAAM,YAAY,EAAE,SAAS,QAAQ,WAAW,SAAS,GAAG;AAC1D,YAAM,UAAU,SAAS,IAAI,SAAS;AACtC,UAAI,CAAC,WAAW,QAAQ,WAAW,UAAU,MAAM,IAAI,MAAM,GAAG,YAAY,QAAS;AACrF,eAAS,IAAI,WAAW,EAAE,GAAG,SAAS,UAAU,gBAAgB,QAAQ,EAAE,CAAC;AAAA,IAC7E;AAAA,IAEA,MAAM,sBAAsB,KAAK;AAC/B,YAAM,SAAS,oBAAoB,IAAI,GAAG;AAC1C,aAAO,SAAS,EAAE,GAAG,OAAO,IAAI;AAAA,IAClC;AAAA,IAEA,MAAM,uBAAuB,KAAK,OAAO,iBAAiB;AACxD,YAAM,UAAU,oBAAoB,IAAI,GAAG;AAC3C,WAAK,SAAS,WAAW,UAAU,gBAAiB,QAAO;AAC3D,0BAAoB,IAAI,KAAK,EAAE,OAAO,UAAU,SAAS,WAAW,KAAK,EAAE,CAAC;AAC5E,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,0BAA0B,MAAM;AACpC,iBAAW,OAAO,KAAM,qBAAoB,OAAO,GAAG;AAAA,IACxD;AAAA,IAEA,MAAM,eAAe,SAAS;AAC5B,aAAO,aAAa,IAAI,OAAO,KAAK;AAAA,IACtC;AAAA,IAEA,MAAM,eAAe,SAAS,SAAS;AACrC,mBAAa,IAAI,SAAS,OAAO;AAAA,IACnC;AAAA,IAEA,MAAM,mBAAmB,SAAS;AAChC,YAAM,SAAS,iBAAiB,IAAI,OAAO;AAC3C,aAAO,SAAS,gBAAgB,MAAM,IAAI;AAAA,IAC5C;AAAA,IAEA,MAAM,oBAAoB,SAAS,QAAQ;AACzC,uBAAiB,IAAI,SAAS,gBAAgB,MAAM,CAAC;AAAA,IACvD;AAAA,IAEA,MAAM,gBAAgB,SAAS;AAC7B,aAAO,aAAa,OAAO;AAAA,IAC7B;AAAA,IAEA,MAAM,kBAAkB,SAAS,QAAQ;AACvC,aAAO,YAAY,SAAS,MAAM;AAAA,IACpC;AAAA;AAAA,IAGA,MAAM,yBAAyB,SAAS,QAAQ,OAAO;AACrD,YAAM,QAAQ,MAAM,aAAa,OAAO,CAAC;AACzC,aAAO,SAAS,YAAY,SAAS,MAAM;AAAA,IAC7C;AAAA,IAEA,MAAM,iBAAiB,SAAS,IAAI,iBAAiB,OAAO;AAC1D,aAAO,WAAW,SAAS,IAAI,iBAAiB,KAAK;AAAA,IACvD;AAAA,IAEA,MAAM,wBAAwB,SAAS,IAAI,iBAAiB,OAAO,OAAO;AACxE,aAAO,MAAM,aAAa,OAAO,CAAC,KAAK,WAAW,SAAS,IAAI,iBAAiB,KAAK;AAAA,IACvF;AAAA,IAEA,MAAM,iBAAiB,SAAS,IAAI,iBAAiB;AACnD,YAAM,QAAQ,WAAW,OAAO;AAChC,YAAM,OAAO,MAAM,IAAI,EAAE;AACzB,UAAI,CAAC,QAAQ,KAAK,WAAY,QAAO;AACrC,UAAI,KAAK,YAAY,gBAAiB,QAAO;AAC7C,YAAM,OAAO,EAAE;AACf,iBAAW,CAAC,UAAU,MAAM,KAAK,MAAO,KAAI,OAAO,eAAe,GAAI,OAAM,OAAO,QAAQ;AAC3F,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,iBAAiB,SAAS;AAC9B,YAAM,QAAQ,CAAC,GAAG,WAAW,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,SAAS,CAAC,KAAK,UAAU,EAAE;AACnF,kBAAY,OAAO,OAAO;AAC1B,uBAAiB,OAAO,OAAO;AAC/B,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,2BAA2B,SAAS,OAAO,KAAK;AACpD,YAAM,OAAO,UAAU,OAAO;AAC9B,YAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,iBAAWA,QAAO,MAAM;AACtB,cAAM,WAAW,KAAK,MAAMA,KAAI,SAAS,IAAI,MAAM;AACnD,YAAIA,KAAI,WAAW,aAAa,SAAS,SAAU,QAAO,OAAOA,MAAK,EAAE,QAAQ,UAAU,OAAO,iBAAiB,YAAY,IAAI,KAAK,QAAQ,EAAE,YAAY,EAAE,CAAC;AAAA,MAClK;AACA,YAAM,UAAU;AAAA,QACd,KAAK,IAAI,CAACA,UAAS,EAAE,QAAQA,KAAI,QAAQ,aAAa,KAAK,MAAMA,KAAI,SAAS,GAAG,WAAWA,KAAI,aAAa,KAAK,MAAMA,KAAI,UAAU,IAAI,KAAK,EAAE;AAAA,QACjJ;AAAA,QACA;AAAA,MACF;AACA,UAAI,QAAS,QAAO;AACpB,YAAM,MAA6B,EAAE,IAAI,OAAO,WAAW,GAAG,WAAW,KAAK,YAAY,MAAM,QAAQ,WAAW,QAAQ,CAAC,GAAG,OAAO,MAAM,eAAe,KAAK;AAChK,WAAK,KAAK,GAAG;AACb,aAAO,gBAAgB,GAAG;AAAA,IAC5B;AAAA,IAEA,MAAM,4BAA4B,SAAS,OAAO,SAAS;AACzD,YAAM,MAAM,UAAU,OAAO,EAAE,KAAK,CAAC,cAAc,UAAU,OAAO,KAAK;AACzE,UAAI,KAAK,WAAW,UAAW,QAAO;AACtC,aAAO,OAAO,KAAK,EAAE,QAAQ,QAAQ,QAAQ,YAAY,QAAQ,YAAY,OAAO,QAAQ,SAAS,MAAM,eAAe,QAAQ,iBAAiB,KAAK,CAAC;AACzJ,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,iBAAiB,SAAS,OAAO;AACrC,YAAM,EAAE,OAAO,IAAI;AACnB,UAAI,OAAO,WAAW,EAAG,OAAM,IAAI,UAAU,oGAAoB;AACjE,YAAM,OAAO,oBAAI,IAAY;AAC7B,iBAAW,EAAE,QAAQ,UAAU,KAAK,QAAQ;AAC1C,6BAAqB,MAAM;AAC3B,YAAI,UAAU,WAAW,EAAG,OAAM,IAAI,UAAU,4HAAwB;AACxE,mBAAW,EAAE,GAAG,KAAK,WAAW;AAC9B,cAAI,KAAK,IAAI,EAAE,EAAG,OAAM,IAAI,UAAU,wCAAU,EAAE,8HAA0B;AAC5E,eAAK,IAAI,EAAE;AAAA,QACb;AAAA,MACF;AACA,YAAM,MAAM,UAAU,OAAO,EAAE,KAAK,CAAC,cAAc,UAAU,OAAO,MAAM,KAAK;AAC/E,UAAI,KAAK,WAAW,UAAW,QAAO;AACtC,YAAM,QAAQ,WAAW,OAAO;AAChC,iBAAW,EAAE,UAAU,KAAK,QAAQ;AAClC,mBAAW,EAAE,IAAI,QAAQ,KAAK,WAAW;AACvC,gBAAM,WAAW,MAAM,IAAI,EAAE;AAC7B,cAAI,CAAC,YAAY,SAAS,cAAc,SAAS,YAAY,QAAS,QAAO;AAAA,QAC/E;AAAA,MACF;AACA,YAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,YAAM,SAA6B,OAAO,IAAI,CAAC,EAAE,QAAQ,UAAU,MAAM;AACvE,cAAM,UAAsB,EAAE,IAAI,OAAO,WAAW,GAAG,YAAY,CAAC,GAAG,OAAO,UAAU,GAAG,SAAS,OAAO,SAAS,SAAS,GAAG,WAAW,KAAK,WAAW,IAAI;AAC/J,cAAM,IAAI,QAAQ,IAAI,OAAO;AAC7B,mBAAW,EAAE,GAAG,KAAK,UAAW,OAAM,IAAI,EAAE,EAAG,aAAa,QAAQ;AACpE,eAAO,EAAE,UAAU,QAAQ,IAAI,aAAa,UAAU,IAAI,CAAC,EAAE,GAAG,MAAM,EAAE,GAAG,eAAe,EAAE;AAAA,MAC9F,CAAC;AACD,aAAO,OAAO,KAAK,EAAE,QAAQ,WAAW,YAAY,MAAM,YAAY,QAAQ,eAAe,MAAM,iBAAiB,KAAK,CAAC;AAC1H,aAAO,gBAAgB,GAAG;AAAA,IAC5B;AAAA,IAEA,MAAM,kBAAkB,SAAS,OAAO,OAAO;AAC7C,YAAM,MAAM,UAAU,OAAO,EAAE,KAAK,CAAC,cAAc,UAAU,OAAO,KAAK;AACzE,UAAI,KAAK,WAAW,UAAW,QAAO;AACtC,YAAM,QAAQ,WAAW,OAAO;AAChC,YAAM,SAAS,IAAI,OAAO;AAAA,QACxB,CAAC,EAAE,UAAU,aAAa,cAAc,MACtC,MAAM,IAAI,QAAQ,GAAG,YAAY,iBAAiB,CAAC,MAAM,IAAI,QAAQ,GAAG,cAAc,YAAY,MAAM,CAAC,OAAO,MAAM,IAAI,EAAE,GAAG,eAAe,QAAQ;AAAA,MAC1J;AACA,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,OAAO;AACT,cAAM,YAAY,IAAI,IAAI,IAAI,OAAO,IAAI,CAAC,EAAE,SAAS,MAAM,QAAQ,CAAC;AACpE,cAAM,cAAc,IAAI,IAAI,IAAI,OAAO,QAAQ,CAAC,EAAE,YAAY,MAAM,WAAW,CAAC;AAChF,cAAM,SAAS,CAAC,GAAG,MAAM,OAAO,CAAC;AACjC,cAAM,YAAY,YAAY,OAAO,OAAO,CAAC,SAAS,CAAC,KAAK,cAAc,CAAC,UAAU,IAAI,KAAK,EAAE,CAAC,CAAC,EAAE,IAAI,UAAU;AAClH,cAAM,WAAW,YAAY,OAAO,OAAO,CAAC,SAAS,YAAY,IAAI,KAAK,EAAE,CAAC,CAAC,EAAE,IAAI,UAAU;AAC9F,cAAM,QAAQ,MAAM,gBAAgB,SAAS,GAAG,gBAAgB,QAAQ,CAAC;AACzE,YAAI,MAAO,QAAO,cAAc,KAAK;AAAA,MACvC;AACA,iBAAW,EAAE,UAAU,YAAY,KAAK,IAAI,QAAQ;AAClD,cAAM,OAAO,QAAQ;AACrB,mBAAW,MAAM,YAAa,QAAO,MAAM,IAAI,EAAE,EAAG;AAAA,MACtD;AACA,UAAI,SAAS;AACb,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,2BAA2B,SAAS,QAAQ;AAChD,YAAM,OAAO,UAAU,OAAO,EAC3B,IAAI,CAAC,KAAK,WAAW,EAAE,KAAK,MAAM,EAAE,EACpC,OAAO,CAAC,EAAE,IAAI,MAAM,CAAC,QAAQ,UAAU,OAAO,OAAO,SAAS,IAAI,MAAM,CAAC,EACzE,KAAK,CAAC,MAAM,UAAU,MAAM,IAAI,UAAU,cAAc,KAAK,IAAI,SAAS,KAAK,MAAM,QAAQ,KAAK,KAAK,EACvG,IAAI,CAAC,EAAE,IAAI,MAAM,GAAG;AACvB,aAAO,gBAAgB,QAAQ,UAAU,SAAY,OAAO,KAAK,MAAM,GAAG,OAAO,KAAK,CAAC;AAAA,IACzF;AAAA,IAEA,MAAM,sBAAsB,SAAS;AACnC,YAAM,SAAS,CAAC,GAAG,WAAW,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,SAAsD,QAAQ,KAAK,UAAU,CAAC;AACvI,aAAO,gBAAgB,YAAY,MAAM,CAAuB;AAAA,IAClE;AAAA,EACF;AACF;AAGA,SAAS,gBAAgB,MAAY,WAA8B,WAAoB;AACrF,MAAI,aAAa,UAAW,QAAO;AACnC,SAAO,aAAa,SAAS,KAAK,eAAe,SAAY,KAAK,eAAe;AACnF;AAGA,SAAS,aAA2D,MAAY;AAC9E,QAAM,EAAE,MAAM,OAAO,aAAa,cAAc,GAAG,KAAK,IAAI;AAC5D,SAAO,EAAE,GAAG,MAAM,GAAG,eAAe,gBAAgB,IAAI,CAAC,EAAE;AAC7D;AAGA,SAAS,iBAAmC,OAAa;AACvD,SAAO,OAAO,YAAY,OAAO,QAAQ,KAAK,EAAE,OAAO,CAAC,CAAC,EAAE,IAAI,MAAM,SAAS,MAAS,CAAC;AAC1F;","names":["run"]}
|
package/dist/storage-contract.js
CHANGED
|
@@ -13,12 +13,12 @@ import {
|
|
|
13
13
|
extendCatalog,
|
|
14
14
|
newStoredMessage,
|
|
15
15
|
standardCatalog
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import "./chunk-
|
|
16
|
+
} from "./chunk-BS2HDKZB.js";
|
|
17
|
+
import "./chunk-FOAIG4DY.js";
|
|
18
18
|
|
|
19
19
|
// src/storage/storage-contract.ts
|
|
20
20
|
import { jsonSchema } from "ai";
|
|
21
|
-
import { describe, expect, it } from "vitest";
|
|
21
|
+
import { describe, expect, it, vi } from "vitest";
|
|
22
22
|
function describeChatStorageContract(createStorage, options = {}) {
|
|
23
23
|
const chatInput = { id: "chat-1", ownerId: "anna", modelId: "deepseek-4.1-flash", providerId: "deepseek" };
|
|
24
24
|
function userMessage(id, createdAt, parentId = null) {
|
|
@@ -335,9 +335,10 @@ function describeChatStorageContract(createStorage, options = {}) {
|
|
|
335
335
|
const accepted = results.flatMap((result, index) => result.status === "fulfilled" ? [answers[index]] : []);
|
|
336
336
|
expect(accepted).toHaveLength(1);
|
|
337
337
|
expect(results.find((result) => result.status === "rejected")).toMatchObject({ reason: expect.any(WidgetNotPendingError) });
|
|
338
|
-
|
|
338
|
+
const stream = await accepted[0].service.subscribe("anna", chat.id);
|
|
339
|
+
if (stream) await drain(stream);
|
|
340
|
+
await vi.waitFor(async () => expect((await storage.listMessages(chat.id)).at(-1).status).toBe("complete"));
|
|
339
341
|
const answer = (await storage.listMessages(chat.id)).at(-1);
|
|
340
|
-
expect(answer.status).toBe("complete");
|
|
341
342
|
expect(answer.parts.find((part) => part.toolCallId === "w1")).toMatchObject({ output: { \u0432\u044B\u0431\u0440\u0430\u043D\u043E: accepted[0].choice } });
|
|
342
343
|
}
|
|
343
344
|
}, 3e4);
|
|
@@ -1522,6 +1523,87 @@ function describeChatStorageContract(createStorage, options = {}) {
|
|
|
1522
1523
|
expect(await storage.claimChatTurn("chat-2", "copy-c", minute)).toBe(true);
|
|
1523
1524
|
});
|
|
1524
1525
|
});
|
|
1526
|
+
describe("\u0436\u0434\u0443\u0449\u0438\u0435 \u043F\u0430\u0447\u043A\u0438 (savePendingBurst, takeAbandonedBursts, renewPendingBurst, removePendingBurst; \u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0435)", () => {
|
|
1527
|
+
async function burstStorage(context) {
|
|
1528
|
+
const storage = await createStorage();
|
|
1529
|
+
if (!storage.savePendingBurst || !storage.takeAbandonedBursts || !storage.renewPendingBurst || !storage.removePendingBurst) {
|
|
1530
|
+
if (options.pendingBursts === "required") throw new Error("\u0423 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430 \u043D\u0435\u0442 \u0436\u0434\u0443\u0449\u0438\u0445 \u043F\u0430\u0447\u0435\u043A");
|
|
1531
|
+
context.skip();
|
|
1532
|
+
}
|
|
1533
|
+
await storage.createChat(chatInput);
|
|
1534
|
+
return storage;
|
|
1535
|
+
}
|
|
1536
|
+
const at = (seconds) => new Date(Date.UTC(2026, 9, 15, 12, 0, seconds)).toISOString();
|
|
1537
|
+
const burst = (fields = {}) => ({
|
|
1538
|
+
chatId: chatInput.id,
|
|
1539
|
+
ownerId: chatInput.ownerId,
|
|
1540
|
+
messageId: "m1",
|
|
1541
|
+
dueAt: at(3),
|
|
1542
|
+
holder: "copy-a",
|
|
1543
|
+
heldUntil: at(33),
|
|
1544
|
+
...fields
|
|
1545
|
+
});
|
|
1546
|
+
const take = (storage, abandonedBefore, holder = "copy-b", limit = 10) => storage.takeAbandonedBursts({ abandonedBefore, holder, heldUntil: at(50), limit });
|
|
1547
|
+
it("\u0436\u0438\u0432\u043E\u0435 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u043D\u0435 \u0431\u0435\u0440\u0451\u0442\u0441\u044F; \u0431\u0440\u043E\u0448\u0435\u043D\u043D\u043E\u0435 \u2014 \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u043E\u0434\u0438\u043D \u0440\u0430\u0437 \u0438 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0438\u0442 \u043A \u0432\u0437\u044F\u0432\u0448\u0435\u043C\u0443", async (context) => {
|
|
1548
|
+
const storage = await burstStorage(context);
|
|
1549
|
+
const page = { tabId: "tab-1", timeZone: "Europe/Moscow" };
|
|
1550
|
+
await storage.savePendingBurst(burst({ page }));
|
|
1551
|
+
expect(await take(storage, at(32))).toEqual([]);
|
|
1552
|
+
expect(await take(storage, at(33))).toEqual([burst({ page, holder: "copy-b", heldUntil: at(50) })]);
|
|
1553
|
+
expect(await take(storage, at(40), "copy-c")).toEqual([]);
|
|
1554
|
+
expect(await take(storage, at(50), "copy-c")).toEqual([expect.objectContaining({ holder: "copy-c" })]);
|
|
1555
|
+
});
|
|
1556
|
+
it("\u043D\u043E\u0432\u0430\u044F \u0437\u0430\u043F\u0438\u0441\u044C \u0447\u0430\u0442\u0430 \u0437\u0430\u043C\u0435\u043D\u044F\u0435\u0442 \u043F\u0440\u0435\u0436\u043D\u044E\u044E; \u0441\u043D\u044F\u0442\u0438\u0435 \u2014 \u0442\u043E\u043B\u044C\u043A\u043E \u0441\u0432\u043E\u0435\u0433\u043E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F", async (context) => {
|
|
1557
|
+
const storage = await burstStorage(context);
|
|
1558
|
+
await storage.savePendingBurst(burst());
|
|
1559
|
+
await storage.savePendingBurst(burst({ messageId: "m2", dueAt: at(5), heldUntil: at(35) }));
|
|
1560
|
+
await storage.removePendingBurst(chatInput.id, "m1");
|
|
1561
|
+
expect(await take(storage, at(59))).toEqual([expect.objectContaining({ messageId: "m2", dueAt: at(5) })]);
|
|
1562
|
+
await storage.removePendingBurst(chatInput.id, "m2");
|
|
1563
|
+
expect(await take(storage, "2100-01-01T00:00:00.000Z")).toEqual([]);
|
|
1564
|
+
});
|
|
1565
|
+
it("limit \u0438 \u043F\u043E\u0440\u044F\u0434\u043E\u043A: \u0440\u0430\u043D\u043D\u0438\u0435 \u0441\u0440\u043E\u043A\u0438 \u043F\u0435\u0440\u0432\u044B\u043C\u0438; \u0443\u0434\u0430\u043B\u0451\u043D\u043D\u044B\u0439 \u0447\u0430\u0442 \u2014 \u0431\u0435\u0437 \u0437\u0430\u043F\u0438\u0441\u0438", async (context) => {
|
|
1566
|
+
const storage = await burstStorage(context);
|
|
1567
|
+
await storage.createChat({ ...chatInput, id: "chat-2" });
|
|
1568
|
+
await storage.createChat({ ...chatInput, id: "chat-3" });
|
|
1569
|
+
await storage.savePendingBurst(burst({ chatId: "chat-2", dueAt: at(9), heldUntil: at(10) }));
|
|
1570
|
+
await storage.savePendingBurst(burst({ dueAt: at(1), heldUntil: at(10) }));
|
|
1571
|
+
await storage.savePendingBurst(burst({ chatId: "chat-3", dueAt: at(5), heldUntil: at(10) }));
|
|
1572
|
+
await storage.deleteChat("chat-3");
|
|
1573
|
+
expect((await take(storage, at(20), "copy-b", 1)).map((taken) => taken.chatId)).toEqual([chatInput.id]);
|
|
1574
|
+
expect((await take(storage, at(20), "copy-b", 10)).map((taken) => taken.chatId)).toEqual(["chat-2"]);
|
|
1575
|
+
});
|
|
1576
|
+
it("\u043F\u0440\u043E\u0434\u043B\u0435\u043D\u0438\u0435: \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u0435\u0440\u0436\u0430\u0442\u0435\u043B\u044C \u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0442\u043E\u0439 \u0436\u0435 \u043F\u0430\u0447\u043A\u0438; \u043F\u0440\u043E\u0434\u043B\u0451\u043D\u043D\u0430\u044F \u043D\u0435 \u0431\u0435\u0440\u0451\u0442\u0441\u044F \u0434\u043E \u043D\u043E\u0432\u043E\u0433\u043E \u0441\u0440\u043E\u043A\u0430", async (context) => {
|
|
1577
|
+
const storage = await burstStorage(context);
|
|
1578
|
+
await storage.savePendingBurst(burst());
|
|
1579
|
+
expect(await storage.renewPendingBurst({ chatId: chatInput.id, messageId: "m1", holder: "copy-b", heldUntil: at(59) })).toBe(false);
|
|
1580
|
+
expect(await storage.renewPendingBurst({ chatId: chatInput.id, messageId: "m0", holder: "copy-a", heldUntil: at(59) })).toBe(false);
|
|
1581
|
+
expect(await storage.renewPendingBurst({ chatId: "chat-none", messageId: "m1", holder: "copy-a", heldUntil: at(59) })).toBe(false);
|
|
1582
|
+
expect(await storage.renewPendingBurst({ chatId: chatInput.id, messageId: "m1", holder: "copy-a", heldUntil: at(45) })).toBe(true);
|
|
1583
|
+
expect(await take(storage, at(44))).toEqual([]);
|
|
1584
|
+
expect(await take(storage, at(45))).toEqual([burst({ holder: "copy-b", heldUntil: at(50) })]);
|
|
1585
|
+
expect(await storage.renewPendingBurst({ chatId: chatInput.id, messageId: "m1", holder: "copy-a", heldUntil: at(59) })).toBe(false);
|
|
1586
|
+
});
|
|
1587
|
+
it("exceptHolder: \u0441\u0432\u043E\u0438 \u0437\u0430\u043F\u0438\u0441\u0438 \u043D\u0435 \u0431\u0435\u0440\u0443\u0442\u0441\u044F, \u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0431\u044B \u0440\u0430\u0437 \u043D\u0438 \u0437\u0432\u0430\u043B\u0438; \u0447\u0443\u0436\u0438\u0435 \u2014 \u0431\u0435\u0440\u0443\u0442\u0441\u044F", async (context) => {
|
|
1588
|
+
const storage = await burstStorage(context);
|
|
1589
|
+
await storage.createChat({ ...chatInput, id: "chat-2" });
|
|
1590
|
+
await storage.savePendingBurst(burst());
|
|
1591
|
+
await storage.savePendingBurst(burst({ chatId: "chat-2", holder: "copy-b" }));
|
|
1592
|
+
const far = "2100-01-01T00:00:00.000Z";
|
|
1593
|
+
const takeExcept = () => storage.takeAbandonedBursts({ abandonedBefore: far, holder: "copy-b", heldUntil: at(50), limit: 10, exceptHolder: "copy-b" });
|
|
1594
|
+
expect((await takeExcept()).map((taken) => taken.chatId)).toEqual([chatInput.id]);
|
|
1595
|
+
expect(await takeExcept()).toEqual([]);
|
|
1596
|
+
});
|
|
1597
|
+
it("\u0433\u043E\u043D\u043A\u0430: \u0434\u0435\u0441\u044F\u0442\u044C \u043A\u043E\u043F\u0438\u0439 \u0431\u0435\u0440\u0443\u0442 \u0440\u0430\u0437\u043E\u043C \u2014 \u043A\u0430\u0436\u0434\u0443\u044E \u043F\u0430\u0447\u043A\u0443 \u043F\u043E\u043B\u0443\u0447\u0430\u0435\u0442 \u0440\u043E\u0432\u043D\u043E \u043E\u0434\u043D\u0430", async (context) => {
|
|
1598
|
+
const storage = await burstStorage(context);
|
|
1599
|
+
await storage.createChat({ ...chatInput, id: "chat-2" });
|
|
1600
|
+
await storage.savePendingBurst(burst());
|
|
1601
|
+
await storage.savePendingBurst(burst({ chatId: "chat-2" }));
|
|
1602
|
+
const copies = Array.from({ length: 10 }, (_, index) => `copy-${index}`);
|
|
1603
|
+
const taken = (await Promise.all(copies.map((holder) => take(storage, at(40), holder)))).flat();
|
|
1604
|
+
expect(taken.map((one) => one.chatId).sort()).toEqual([chatInput.id, "chat-2"].sort());
|
|
1605
|
+
});
|
|
1606
|
+
});
|
|
1525
1607
|
describe("\u0437\u0430\u043A\u0440\u044B\u0442\u0438\u0435 \u0447\u0430\u0442\u0430 (\u043D\u0435\u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u0439 \u043C\u0435\u0442\u043E\u0434)", () => {
|
|
1526
1608
|
async function closureStorage(context) {
|
|
1527
1609
|
const storage = await createStorage();
|