@ykurilov/agent-chat-storage-postgres 0.0.0-stage → 0.55.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +1213 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -2
  4. package/dist/index.js +801 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/types/drizzle-chat-storage.d.ts +1604 -0
  7. package/dist/types/index.d.ts +2 -0
  8. package/dist/types/migrate.d.ts +6 -0
  9. package/dist/types/postgres-storage.d.ts +18 -0
  10. package/dist/types/schema.d.ts +1678 -0
  11. package/drizzle/0000_init.sql +32 -0
  12. package/drizzle/0001_chat_reasoning.sql +1 -0
  13. package/drizzle/0002_compactions.sql +16 -0
  14. package/drizzle/0003_branch_selections.sql +10 -0
  15. package/drizzle/0004_chat_title.sql +22 -0
  16. package/drizzle/0005_usage_records.sql +19 -0
  17. package/drizzle/0006_user_model_choices.sql +7 -0
  18. package/drizzle/0007_voice_dialog.sql +7 -0
  19. package/drizzle/0008_memory_notes.sql +11 -0
  20. package/drizzle/0009_memory_optimization.sql +15 -0
  21. package/drizzle/0010_usage_estimated.sql +1 -0
  22. package/drizzle/0011_message_alive_at.sql +1 -0
  23. package/drizzle/0012_spending_limit_states.sql +6 -0
  24. package/drizzle/0013_chat_origin.sql +1 -0
  25. package/drizzle/0014_chat_subject_after_turn.sql +5 -0
  26. package/drizzle/0015_chat_closed.sql +1 -0
  27. package/drizzle/0016_chat_turn_leases.sql +5 -0
  28. package/drizzle/meta/0000_snapshot.json +259 -0
  29. package/drizzle/meta/0001_snapshot.json +265 -0
  30. package/drizzle/meta/0002_snapshot.json +392 -0
  31. package/drizzle/meta/0003_snapshot.json +483 -0
  32. package/drizzle/meta/0004_snapshot.json +489 -0
  33. package/drizzle/meta/0005_snapshot.json +629 -0
  34. package/drizzle/meta/0006_snapshot.json +672 -0
  35. package/drizzle/meta/0007_snapshot.json +709 -0
  36. package/drizzle/meta/0008_snapshot.json +787 -0
  37. package/drizzle/meta/0009_snapshot.json +908 -0
  38. package/drizzle/meta/0010_snapshot.json +915 -0
  39. package/drizzle/meta/0011_snapshot.json +921 -0
  40. package/drizzle/meta/0012_snapshot.json +958 -0
  41. package/drizzle/meta/0013_snapshot.json +964 -0
  42. package/drizzle/meta/0014_snapshot.json +1019 -0
  43. package/drizzle/meta/0015_snapshot.json +1025 -0
  44. package/drizzle/meta/0016_snapshot.json +1056 -0
  45. package/drizzle/meta/_journal.json +125 -0
  46. package/package.json +46 -3
package/dist/index.js ADDED
@@ -0,0 +1,801 @@
1
+ // src/postgres-storage.ts
2
+ import { drizzle as drizzle2 } from "drizzle-orm/node-postgres";
3
+ import pg from "pg";
4
+
5
+ // src/drizzle-chat-storage.ts
6
+ import {
7
+ assertChatDataFilter,
8
+ assertKnownFormatVersion,
9
+ ChatAlreadyExistsError,
10
+ chatDataFields,
11
+ chatDataOfInput,
12
+ DuplicateChatSubjectError,
13
+ DuplicateMessageError,
14
+ isOptimizationBlocked,
15
+ mergeChatData,
16
+ MessageAlreadyFinishedError,
17
+ MessageNotAwaitingError,
18
+ RELEASED_ANSWER_ALIVE_AT,
19
+ revertRefusal,
20
+ sameMessageParts
21
+ } from "@ykurilov/agent-chat-core";
22
+ import { and, asc, desc, eq, gte, inArray, isNotNull, isNull, sql as sql2 } from "drizzle-orm";
23
+
24
+ // src/schema.ts
25
+ import { sql } from "drizzle-orm";
26
+ import { bigint, boolean, doublePrecision, index, integer, json, jsonb, pgSchema, text, timestamp, uniqueIndex } from "drizzle-orm/pg-core";
27
+ var agentChatSchema = pgSchema("agent_chat");
28
+ var chats = agentChatSchema.table(
29
+ "chats",
30
+ {
31
+ id: text("id").primaryKey(),
32
+ ownerId: text("owner_id").notNull(),
33
+ modelId: text("model_id").notNull(),
34
+ providerId: text("provider_id").notNull(),
35
+ /** Уровень рассуждений выбранной модели; пусто — у модели нет уровней. */
36
+ reasoning: text("reasoning"),
37
+ /** Название для списка чатов; пусто — чат без названия. */
38
+ title: text("title"),
39
+ /** Мессенджер, из которого создан чат; пусто — чат создан в вебе. */
40
+ origin: text("origin", { enum: ["telegram", "vk", "max"] }),
41
+ /** Ключ объекта приложения; пусто — обычный чат. У владельца ключ не повторяется. */
42
+ subjectKey: text("subject_key"),
43
+ /** Данные объекта (JSON-объект); пусто — данных нет. */
44
+ subjectData: json("subject_data").$type(),
45
+ /** Закрытие чата (`{ code, message }`); пусто — чат открыт. */
46
+ closed: json("closed").$type(),
47
+ createdAt: text("created_at").notNull(),
48
+ updatedAt: text("updated_at").notNull()
49
+ },
50
+ (table) => [
51
+ index("chats_owner_updated_idx").on(table.ownerId, table.updatedAt),
52
+ uniqueIndex("chats_owner_subject_idx").on(table.ownerId, table.subjectKey)
53
+ ]
54
+ );
55
+ var messages = agentChatSchema.table(
56
+ "messages",
57
+ {
58
+ /** Порядок записи: сообщения с одинаковым временем создания отдаются по нему. */
59
+ seq: bigint("seq", { mode: "number" }).generatedAlwaysAsIdentity().notNull(),
60
+ id: text("id").primaryKey(),
61
+ chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
62
+ parentId: text("parent_id"),
63
+ role: text("role", { enum: ["user", "assistant"] }).notNull(),
64
+ parts: json("parts").$type().notNull(),
65
+ formatVersion: integer("format_version").notNull(),
66
+ status: text("status").$type().notNull(),
67
+ modelId: text("model_id"),
68
+ providerId: text("provider_id"),
69
+ usage: json("usage").$type(),
70
+ reasoningDurationsMs: json("reasoning_durations_ms").$type().notNull(),
71
+ error: json("error").$type(),
72
+ /** Отметка обрыва озвучки (голосовой режим); пусто — озвучку не обрывали. */
73
+ voiceCut: json("voice_cut").$type(),
74
+ /** Состояние хука итога хода; пусто — хук не зовётся. */
75
+ afterTurn: text("after_turn", { enum: ["pending", "done", "failed"] }),
76
+ createdAt: text("created_at").notNull(),
77
+ /** Последний знак жизни идущего ответа (ISO); пусто — время создания. Только для восстановления, при чтении не отдаётся. */
78
+ aliveAt: text("alive_at")
79
+ },
80
+ (table) => [
81
+ index("messages_chat_created_idx").on(table.chatId, table.createdAt, table.seq),
82
+ index("messages_streaming_idx").on(table.status).where(sql`${table.status} = 'streaming'`),
83
+ index("messages_after_turn_pending_idx").on(table.afterTurn).where(sql`${table.afterTurn} = 'pending'`)
84
+ ]
85
+ );
86
+ var compactions = agentChatSchema.table(
87
+ "compactions",
88
+ {
89
+ seq: bigint("seq", { mode: "number" }).generatedAlwaysAsIdentity().notNull(),
90
+ id: text("id").primaryKey(),
91
+ chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
92
+ fromMessageId: text("from_message_id").notNull(),
93
+ throughMessageId: text("through_message_id").notNull(),
94
+ summary: text("summary").notNull(),
95
+ summaryTokens: integer("summary_tokens").notNull(),
96
+ modelId: text("model_id").notNull(),
97
+ providerId: text("provider_id").notNull(),
98
+ usage: json("usage").$type(),
99
+ createdAt: text("created_at").notNull()
100
+ },
101
+ (table) => [index("compactions_chat_seq_idx").on(table.chatId, table.seq)]
102
+ );
103
+ var branchSelections = agentChatSchema.table(
104
+ "branch_selections",
105
+ {
106
+ seq: bigint("seq", { mode: "number" }).generatedAlwaysAsIdentity().notNull(),
107
+ id: text("id").primaryKey(),
108
+ chatId: text("chat_id").notNull().references(() => chats.id, { onDelete: "cascade" }),
109
+ messageId: text("message_id").notNull(),
110
+ selectedAt: text("selected_at").notNull()
111
+ },
112
+ (table) => [index("branch_selections_chat_seq_idx").on(table.chatId, table.seq)]
113
+ );
114
+ var usageRecords = agentChatSchema.table(
115
+ "usage_records",
116
+ {
117
+ id: text("id").primaryKey(),
118
+ ownerId: text("owner_id").notNull(),
119
+ chatId: text("chat_id").notNull(),
120
+ messageId: text("message_id"),
121
+ source: text("source").$type().notNull(),
122
+ modelId: text("model_id").notNull(),
123
+ providerId: text("provider_id").notNull(),
124
+ inputTokens: bigint("input_tokens", { mode: "number" }).notNull(),
125
+ cachedInputTokens: bigint("cached_input_tokens", { mode: "number" }).notNull(),
126
+ cacheWriteTokens: bigint("cache_write_tokens", { mode: "number" }).notNull(),
127
+ outputTokens: bigint("output_tokens", { mode: "number" }).notNull(),
128
+ reasoningTokens: bigint("reasoning_tokens", { mode: "number" }).notNull(),
129
+ costUsd: doublePrecision("cost_usd").notNull(),
130
+ createdAt: text("created_at").notNull(),
131
+ /** Числа «примерно»: прикинуты по длине текста, а не сообщены провайдером. */
132
+ estimated: boolean("estimated").notNull().default(false)
133
+ },
134
+ (table) => [
135
+ index("usage_records_owner_created_idx").on(table.ownerId, table.createdAt),
136
+ index("usage_records_chat_created_idx").on(table.chatId, table.createdAt)
137
+ ]
138
+ );
139
+ var userModelChoices = agentChatSchema.table("user_model_choices", {
140
+ ownerId: text("owner_id").primaryKey(),
141
+ modelId: text("model_id").notNull(),
142
+ providerId: text("provider_id").notNull(),
143
+ /** Пусто — у модели нет уровней рассуждений. */
144
+ reasoning: text("reasoning"),
145
+ updatedAt: text("updated_at").notNull()
146
+ });
147
+ var voiceChoices = agentChatSchema.table("voice_choices", {
148
+ ownerId: text("owner_id").primaryKey(),
149
+ voiceId: text("voice_id").notNull(),
150
+ updatedAt: text("updated_at").notNull()
151
+ });
152
+ var spendingLimitStates = agentChatSchema.table("spending_limit_states", {
153
+ key: text("key").primaryKey(),
154
+ value: text("value").notNull(),
155
+ version: integer("version").notNull(),
156
+ updatedAt: text("updated_at").notNull()
157
+ });
158
+ var chatTurnLeases = agentChatSchema.table("chat_turn_leases", {
159
+ chatId: text("chat_id").primaryKey(),
160
+ holder: text("holder").notNull(),
161
+ until: timestamp("until", { withTimezone: true, mode: "string" }).notNull()
162
+ });
163
+ var memoryNotes = agentChatSchema.table(
164
+ "memory_notes",
165
+ {
166
+ id: text("id").primaryKey(),
167
+ ownerId: text("owner_id").notNull(),
168
+ categories: jsonb("categories").$type().notNull(),
169
+ content: text("content").notNull(),
170
+ version: integer("version").notNull().default(1),
171
+ createdAt: timestamp("created_at", { withTimezone: true, mode: "date" }).notNull(),
172
+ updatedAt: timestamp("updated_at", { withTimezone: true, mode: "date" }).notNull(),
173
+ /** Оптимизация дублей: id записи-результата у скрытого оригинала слияния; у обычных записей пусто. */
174
+ mergedInto: text("merged_into")
175
+ },
176
+ (table) => [
177
+ index("memory_notes_owner_updated_idx").on(table.ownerId, table.updatedAt),
178
+ index("memory_notes_merged_into_idx").on(table.mergedInto)
179
+ ]
180
+ );
181
+ var memoryOptimizationRuns = agentChatSchema.table(
182
+ "memory_optimization_runs",
183
+ {
184
+ id: text("id").primaryKey(),
185
+ ownerId: text("owner_id").notNull(),
186
+ startedAt: timestamp("started_at", { withTimezone: true, mode: "date" }).notNull(),
187
+ finishedAt: timestamp("finished_at", { withTimezone: true, mode: "date" }),
188
+ status: text("status").$type().notNull(),
189
+ groups: jsonb("groups").$type().notNull().default(sql`'[]'::jsonb`),
190
+ error: text("error"),
191
+ usageRecordId: text("usage_record_id")
192
+ },
193
+ (table) => [
194
+ index("memory_optimization_runs_owner_started_idx").on(table.ownerId, table.startedAt),
195
+ uniqueIndex("memory_optimization_runs_one_running_idx").on(table.ownerId).where(sql`${table.status} = 'running'`)
196
+ ]
197
+ );
198
+
199
+ // src/drizzle-chat-storage.ts
200
+ var storageSchema = { chats, messages, compactions, branchSelections, usageRecords, userModelChoices, voiceChoices, spendingLimitStates, memoryNotes, memoryOptimizationRuns };
201
+ var StaleMerge = class extends Error {
202
+ };
203
+ function isoAtDatabase(moment) {
204
+ return sql2`to_char((${moment}) at time zone 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`;
205
+ }
206
+ function aliveAtColumn(alive) {
207
+ if (!alive) return {};
208
+ return { aliveAt: alive === "released" ? RELEASED_ANSWER_ALIVE_AT : isoAtDatabase(sql2`now()`) };
209
+ }
210
+ function createDrizzleChatStorage(db) {
211
+ async function updateStreaming(messageId, { alive, ...values }) {
212
+ const updated = await db.update(messages).set({ ...values, ...aliveAtColumn(alive) }).where(and(eq(messages.id, messageId), eq(messages.status, "streaming"))).returning({ id: messages.id });
213
+ if (updated.length > 0) return;
214
+ const [existing] = await db.select({ id: messages.id }).from(messages).where(eq(messages.id, messageId));
215
+ if (!existing) throw new Error(`\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F ${messageId} \u043D\u0435\u0442`);
216
+ throw new MessageAlreadyFinishedError(messageId);
217
+ }
218
+ async function lockMemoryNotes(tx, ownerId) {
219
+ await tx.execute(sql2`select pg_advisory_xact_lock(hashtextextended(${`agent_chat.memory_notes:${ownerId}`}, 0))`);
220
+ }
221
+ async function visibleNotes(exec, ownerId) {
222
+ const rows = await exec.select().from(memoryNotes).where(and(eq(memoryNotes.ownerId, ownerId), isNull(memoryNotes.mergedInto))).orderBy(desc(memoryNotes.updatedAt), desc(memoryNotes.createdAt), sql2`${memoryNotes.id} collate "C" desc`);
223
+ return rows.map(toMemoryNote);
224
+ }
225
+ async function insertNotes(exec, ownerId, inputs) {
226
+ inputs.forEach(assertWellFormedNote);
227
+ if (inputs.length === 0) return [];
228
+ const now = /* @__PURE__ */ new Date();
229
+ const rows = await exec.insert(memoryNotes).values(
230
+ inputs.map(({ categories, content }) => ({
231
+ id: crypto.randomUUID(),
232
+ ownerId,
233
+ categories: [...categories],
234
+ content,
235
+ version: 1,
236
+ createdAt: now,
237
+ updatedAt: now
238
+ }))
239
+ ).returning();
240
+ return rows.map(toMemoryNote);
241
+ }
242
+ async function updateNote(exec, ownerId, id, expectedVersion, input) {
243
+ assertWellFormedNote(input);
244
+ const updated = await exec.update(memoryNotes).set({ categories: [...input.categories], content: input.content, version: sql2`${memoryNotes.version} + 1`, updatedAt: /* @__PURE__ */ new Date() }).where(and(eq(memoryNotes.ownerId, ownerId), eq(memoryNotes.id, id), eq(memoryNotes.version, expectedVersion), isNull(memoryNotes.mergedInto))).returning();
245
+ if (updated[0]) return toMemoryNote(updated[0]);
246
+ return await memoryNoteExists(exec, ownerId, id) ? "stale_version" : "not_found";
247
+ }
248
+ async function memoryNoteExists(exec, ownerId, id) {
249
+ const [existing] = await exec.select({ id: memoryNotes.id }).from(memoryNotes).where(and(eq(memoryNotes.ownerId, ownerId), eq(memoryNotes.id, id), isNull(memoryNotes.mergedInto)));
250
+ return existing !== void 0;
251
+ }
252
+ return {
253
+ async createChat({ createdAt, ...input }) {
254
+ const at = createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
255
+ const row = chatRow({ ...input, createdAt: at, updatedAt: at });
256
+ try {
257
+ await db.insert(chats).values(row);
258
+ } catch (error) {
259
+ if (isConflictOn(error, "chats_owner_subject_idx")) throw new DuplicateChatSubjectError(input.ownerId, input.subjectKey);
260
+ throw error;
261
+ }
262
+ return toChat(row);
263
+ },
264
+ async findChatBySubject(ownerId, subjectKey) {
265
+ const [row] = await db.select().from(chats).where(and(eq(chats.ownerId, ownerId), eq(chats.subjectKey, subjectKey)));
266
+ return row ? toChat(row) : null;
267
+ },
268
+ async importChat(chat, imported) {
269
+ await db.transaction(async (tx) => {
270
+ try {
271
+ await tx.insert(chats).values(chatRow(chat));
272
+ } catch (error) {
273
+ if (isConflictOn(error, "chats_owner_subject_idx")) throw new DuplicateChatSubjectError(chat.ownerId, chat.subjectKey);
274
+ if (isConflictOn(error, "chats_pkey")) throw new ChatAlreadyExistsError(chat.id);
275
+ throw error;
276
+ }
277
+ for (const message of imported) {
278
+ const { alive: _alive, ...stored } = message;
279
+ try {
280
+ await tx.insert(messages).values(stored);
281
+ } catch (error) {
282
+ if (isPrimaryKeyConflict(error)) throw new DuplicateMessageError(message.id);
283
+ throw error;
284
+ }
285
+ }
286
+ });
287
+ },
288
+ async setAfterTurnState(messageId, state, options) {
289
+ return db.transaction(async (tx) => {
290
+ const [existing] = await tx.select({ parts: messages.parts, afterTurn: messages.afterTurn }).from(messages).where(eq(messages.id, messageId)).for("update");
291
+ if (!existing || state !== "pending" && existing.afterTurn !== "pending") return false;
292
+ await tx.update(messages).set({
293
+ afterTurn: state,
294
+ ...options?.appendParts && { parts: [...existing.parts, ...options.appendParts] },
295
+ // Отметка ожидания хука — в колонке знака жизни, по часам базы: у законченного ответа она больше ни для чего.
296
+ ...state === "pending" && aliveAtColumn("now")
297
+ }).where(eq(messages.id, messageId));
298
+ return true;
299
+ });
300
+ },
301
+ async listPendingAfterTurn(filter) {
302
+ const pending = eq(messages.afterTurn, "pending");
303
+ const rows = await db.select({ id: messages.id }).from(messages).where(
304
+ filter?.pendingForMs === void 0 ? pending : and(
305
+ pending,
306
+ sql2`coalesce(${messages.aliveAt}, ${messages.createdAt}) < ${isoAtDatabase(sql2`now() - ${filter.pendingForMs} * interval '1 millisecond'`)}`
307
+ )
308
+ );
309
+ return rows.map((row) => row.id);
310
+ },
311
+ async getChat(chatId) {
312
+ const [chat] = await db.select().from(chats).where(eq(chats.id, chatId));
313
+ return chat ? toChat(chat) : null;
314
+ },
315
+ async updateChatModel(chatId, model) {
316
+ await db.update(chats).set({ modelId: model.modelId, providerId: model.providerId, reasoning: model.reasoning ?? null }).where(eq(chats.id, chatId));
317
+ },
318
+ async updateChatData(chatId, patch, { maxBytes }) {
319
+ return db.transaction(async (tx) => {
320
+ const [row] = await tx.select().from(chats).where(eq(chats.id, chatId)).for("update");
321
+ if (!row) return null;
322
+ const merged = mergeChatData(row.subjectData ?? void 0, patch, maxBytes);
323
+ await tx.update(chats).set({ subjectData: merged ?? null }).where(eq(chats.id, chatId));
324
+ return toChat({ ...row, subjectData: merged ?? null });
325
+ });
326
+ },
327
+ async listChats(ownerId, options) {
328
+ if (options?.data) assertChatDataFilter(options.data);
329
+ const rows = await db.select().from(chats).where(and(eq(chats.ownerId, ownerId), subjectCondition(options?.subjects), dataCondition(options?.data))).orderBy(desc(chats.updatedAt), desc(chats.createdAt), desc(chats.id));
330
+ return rows.map(toChat);
331
+ },
332
+ async renameChat(chatId, title) {
333
+ await db.update(chats).set({ title }).where(eq(chats.id, chatId));
334
+ },
335
+ async setChatClosed(chatId, closed) {
336
+ await db.update(chats).set({ closed: closed && { code: closed.code, message: closed.message } }).where(eq(chats.id, chatId));
337
+ },
338
+ async deleteChat(chatId) {
339
+ await db.transaction(async (tx) => {
340
+ await tx.delete(compactions).where(eq(compactions.chatId, chatId));
341
+ await tx.delete(branchSelections).where(eq(branchSelections.chatId, chatId));
342
+ await tx.delete(messages).where(eq(messages.chatId, chatId));
343
+ await tx.delete(chats).where(eq(chats.id, chatId));
344
+ await tx.delete(chatTurnLeases).where(eq(chatTurnLeases.chatId, chatId));
345
+ });
346
+ },
347
+ async claimChatTurn(chatId, holder, leaseMs) {
348
+ const until = sql2`now() + ${leaseMs} * interval '1 millisecond'`;
349
+ const claimed = await db.insert(chatTurnLeases).values({ chatId, holder, until }).onConflictDoUpdate({
350
+ target: chatTurnLeases.chatId,
351
+ set: { holder, until },
352
+ setWhere: sql2`${chatTurnLeases.holder} = ${holder} or ${chatTurnLeases.until} <= now()`
353
+ }).returning({ chatId: chatTurnLeases.chatId });
354
+ return claimed.length > 0;
355
+ },
356
+ async releaseChatTurn(chatId, holder) {
357
+ await db.delete(chatTurnLeases).where(and(eq(chatTurnLeases.chatId, chatId), eq(chatTurnLeases.holder, holder)));
358
+ },
359
+ async clearChatTurnLeases() {
360
+ await db.delete(chatTurnLeases);
361
+ },
362
+ async getMessage(messageId) {
363
+ const [row] = await db.select().from(messages).where(eq(messages.id, messageId));
364
+ if (!row) return null;
365
+ const message = toMessage(row);
366
+ assertKnownFormatVersion(message);
367
+ return message;
368
+ },
369
+ async listMessages(chatId) {
370
+ const rows = await db.select().from(messages).where(eq(messages.chatId, chatId)).orderBy(asc(messages.createdAt), asc(messages.seq));
371
+ const found = rows.map(toMessage);
372
+ found.forEach(assertKnownFormatVersion);
373
+ return found;
374
+ },
375
+ async listMessageLinks(chatId) {
376
+ return db.select({ id: messages.id, parentId: messages.parentId, createdAt: messages.createdAt }).from(messages).where(eq(messages.chatId, chatId)).orderBy(asc(messages.createdAt), asc(messages.seq));
377
+ },
378
+ // Части нужных типов выбираются внутри базы: тексты и выводы инструментов в процесс не идут.
379
+ async listMessagePartsOfTypes(chatId, types) {
380
+ const wanted = JSON.stringify([...types]);
381
+ const rows = await db.select({
382
+ id: messages.id,
383
+ parentId: messages.parentId,
384
+ createdAt: messages.createdAt,
385
+ parts: sql2`(select coalesce(json_agg(part.value order by part.position), '[]'::json) from json_array_elements(${messages.parts}) with ordinality as part(value, position) where part.value->>'type' in (select json_array_elements_text(${wanted}::json)))`
386
+ }).from(messages).where(eq(messages.chatId, chatId)).orderBy(asc(messages.createdAt), asc(messages.seq));
387
+ return rows.map((row) => ({ ...row, parts: typeof row.parts === "string" ? JSON.parse(row.parts) : row.parts }));
388
+ },
389
+ async insertMessage(message) {
390
+ try {
391
+ await db.transaction(async (tx) => {
392
+ const { alive, ...stored } = message;
393
+ await tx.insert(messages).values({ ...stored, ...aliveAtColumn(alive) });
394
+ await tx.update(chats).set({ updatedAt: message.createdAt }).where(eq(chats.id, message.chatId));
395
+ });
396
+ } catch (error) {
397
+ if (isPrimaryKeyConflict(error)) throw new DuplicateMessageError(message.id);
398
+ throw error;
399
+ }
400
+ },
401
+ updateStreamingMessage: (messageId, patch) => updateStreaming(messageId, patch),
402
+ markAnswerAlive: (messageId) => updateStreaming(messageId, { alive: "now" }),
403
+ finishMessage: (messageId, final) => updateStreaming(messageId, { ...final, ...final.afterTurn === "pending" && { alive: "now" } }),
404
+ async updateAwaitingMessage(messageId, patch) {
405
+ await db.transaction(async (tx) => {
406
+ const [existing] = await tx.select({ status: messages.status, parts: messages.parts }).from(messages).where(eq(messages.id, messageId)).for("update");
407
+ if (!existing) throw new Error(`\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F ${messageId} \u043D\u0435\u0442`);
408
+ if (existing.status !== "awaiting_approval") throw new MessageNotAwaitingError(messageId);
409
+ if (patch.expectedParts && !sameMessageParts(existing.parts, patch.expectedParts)) throw new MessageNotAwaitingError(messageId);
410
+ await tx.update(messages).set({ parts: patch.parts, ...patch.status && { status: patch.status }, ...aliveAtColumn(patch.alive) }).where(eq(messages.id, messageId));
411
+ });
412
+ },
413
+ async listStreamingMessageIds() {
414
+ const rows = await db.select({ id: messages.id }).from(messages).where(eq(messages.status, "streaming")).orderBy(asc(messages.seq));
415
+ return rows.map((row) => row.id);
416
+ },
417
+ async listSilentAnswerIds({ silentForMs, chatId }) {
418
+ const silent = sql2`coalesce(${messages.aliveAt}, ${messages.createdAt}) < ${isoAtDatabase(sql2`now() - ${silentForMs} * interval '1 millisecond'`)}`;
419
+ const rows = await db.select({ id: messages.id }).from(messages).where(and(eq(messages.status, "streaming"), silent, chatId === void 0 ? void 0 : eq(messages.chatId, chatId))).orderBy(asc(messages.seq));
420
+ return rows.map((row) => row.id);
421
+ },
422
+ async insertCompaction(record) {
423
+ await db.insert(compactions).values(record);
424
+ },
425
+ async listCompactions(chatId) {
426
+ const rows = await db.select().from(compactions).where(eq(compactions.chatId, chatId)).orderBy(asc(compactions.seq));
427
+ return rows.map(({ seq: _seq, ...record }) => record);
428
+ },
429
+ async insertBranchSelection(record) {
430
+ await db.insert(branchSelections).values(record);
431
+ },
432
+ async listBranchSelections(chatId) {
433
+ const rows = await db.select().from(branchSelections).where(eq(branchSelections.chatId, chatId)).orderBy(asc(branchSelections.seq));
434
+ return rows.map(({ seq: _seq, ...record }) => record);
435
+ },
436
+ async insertUsageRecord(record) {
437
+ await db.insert(usageRecords).values(record);
438
+ },
439
+ async hasUsageRecord(id) {
440
+ const rows = await db.select({ id: usageRecords.id }).from(usageRecords).where(eq(usageRecords.id, id)).limit(1);
441
+ return rows.length > 0;
442
+ },
443
+ async sumUsage({ ownerId, chatId, messageId, source, since }) {
444
+ const filters = [
445
+ ...ownerId !== void 0 ? [eq(usageRecords.ownerId, ownerId)] : [],
446
+ ...chatId !== void 0 ? [eq(usageRecords.chatId, chatId)] : [],
447
+ ...messageId !== void 0 ? [eq(usageRecords.messageId, messageId)] : [],
448
+ ...source !== void 0 ? [eq(usageRecords.source, source)] : [],
449
+ ...since !== void 0 ? [gte(usageRecords.createdAt, since)] : []
450
+ ];
451
+ return db.select({
452
+ modelId: usageRecords.modelId,
453
+ providerId: usageRecords.providerId,
454
+ inputTokens: sql2`sum(${usageRecords.inputTokens})`.mapWith(Number),
455
+ cachedInputTokens: sql2`sum(${usageRecords.cachedInputTokens})`.mapWith(Number),
456
+ cacheWriteTokens: sql2`sum(${usageRecords.cacheWriteTokens})`.mapWith(Number),
457
+ outputTokens: sql2`sum(${usageRecords.outputTokens})`.mapWith(Number),
458
+ reasoningTokens: sql2`sum(${usageRecords.reasoningTokens})`.mapWith(Number),
459
+ costUsd: sql2`sum(${usageRecords.costUsd})`.mapWith(Number)
460
+ }).from(usageRecords).where(and(...filters)).groupBy(usageRecords.modelId, usageRecords.providerId);
461
+ },
462
+ async sumUsageByChat({ chatIds, ownerId, source, since }) {
463
+ if (chatIds.length === 0) return [];
464
+ const filters = [
465
+ inArray(usageRecords.chatId, [...chatIds]),
466
+ ...ownerId !== void 0 ? [eq(usageRecords.ownerId, ownerId)] : [],
467
+ ...source !== void 0 ? [eq(usageRecords.source, source)] : [],
468
+ ...since !== void 0 ? [gte(usageRecords.createdAt, since)] : []
469
+ ];
470
+ return db.select({
471
+ chatId: usageRecords.chatId,
472
+ modelId: usageRecords.modelId,
473
+ providerId: usageRecords.providerId,
474
+ inputTokens: sql2`sum(${usageRecords.inputTokens})`.mapWith(Number),
475
+ cachedInputTokens: sql2`sum(${usageRecords.cachedInputTokens})`.mapWith(Number),
476
+ cacheWriteTokens: sql2`sum(${usageRecords.cacheWriteTokens})`.mapWith(Number),
477
+ outputTokens: sql2`sum(${usageRecords.outputTokens})`.mapWith(Number),
478
+ reasoningTokens: sql2`sum(${usageRecords.reasoningTokens})`.mapWith(Number),
479
+ costUsd: sql2`sum(${usageRecords.costUsd})`.mapWith(Number)
480
+ }).from(usageRecords).where(and(...filters)).groupBy(usageRecords.chatId, usageRecords.modelId, usageRecords.providerId);
481
+ },
482
+ async getUserModelChoice(ownerId) {
483
+ const [row] = await db.select().from(userModelChoices).where(eq(userModelChoices.ownerId, ownerId));
484
+ if (!row) return null;
485
+ return { modelId: row.modelId, providerId: row.providerId, ...row.reasoning !== null && { reasoning: row.reasoning } };
486
+ },
487
+ async saveUserModelChoice(ownerId, choice) {
488
+ const values = { modelId: choice.modelId, providerId: choice.providerId, reasoning: choice.reasoning ?? null, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
489
+ await db.insert(userModelChoices).values({ ownerId, ...values }).onConflictDoUpdate({ target: userModelChoices.ownerId, set: values });
490
+ },
491
+ async setVoiceCut({ ownerId, chatId, messageId, voiceCut }) {
492
+ await db.update(messages).set({ voiceCut }).where(
493
+ and(
494
+ eq(messages.id, messageId),
495
+ eq(messages.chatId, chatId),
496
+ sql2`exists (select 1 from ${chats} where ${chats.id} = ${chatId} and ${chats.ownerId} = ${ownerId})`
497
+ )
498
+ );
499
+ },
500
+ async getSpendingLimitState(key) {
501
+ const [row] = await db.select().from(spendingLimitStates).where(eq(spendingLimitStates.key, key));
502
+ return row ? { value: row.value, version: row.version } : null;
503
+ },
504
+ // Одна команда без чтения: вставка или правка срабатывает только при совпавшей версии, из двух одновременных проходит одна.
505
+ async saveSpendingLimitState(key, value, expectedVersion) {
506
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
507
+ if (expectedVersion === null) {
508
+ const inserted = await db.insert(spendingLimitStates).values({ key, value, version: 1, updatedAt }).onConflictDoNothing().returning({ key: spendingLimitStates.key });
509
+ return inserted.length === 1;
510
+ }
511
+ const updated = await db.update(spendingLimitStates).set({ value, version: expectedVersion + 1, updatedAt }).where(and(eq(spendingLimitStates.key, key), eq(spendingLimitStates.version, expectedVersion))).returning({ key: spendingLimitStates.key });
512
+ return updated.length === 1;
513
+ },
514
+ async deleteSpendingLimitStates(keys) {
515
+ if (keys.length > 0) await db.delete(spendingLimitStates).where(inArray(spendingLimitStates.key, keys));
516
+ },
517
+ async getVoiceChoice(ownerId) {
518
+ const [row] = await db.select().from(voiceChoices).where(eq(voiceChoices.ownerId, ownerId));
519
+ return row?.voiceId ?? null;
520
+ },
521
+ async setVoiceChoice(ownerId, voiceId) {
522
+ const values = { voiceId, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
523
+ await db.insert(voiceChoices).values({ ownerId, ...values }).onConflictDoUpdate({ target: voiceChoices.ownerId, set: values });
524
+ },
525
+ async listMemoryNotes(ownerId) {
526
+ return visibleNotes(db, ownerId);
527
+ },
528
+ async insertMemoryNotes(ownerId, inputs) {
529
+ return insertNotes(db, ownerId, inputs);
530
+ },
531
+ async insertMemoryNotesChecked(ownerId, inputs, guard) {
532
+ return db.transaction(async (tx) => {
533
+ await lockMemoryNotes(tx, ownerId);
534
+ const error = guard(await visibleNotes(tx, ownerId));
535
+ return error ?? await insertNotes(tx, ownerId, inputs);
536
+ });
537
+ },
538
+ async updateMemoryNote(ownerId, id, expectedVersion, input) {
539
+ return updateNote(db, ownerId, id, expectedVersion, input);
540
+ },
541
+ async updateMemoryNoteChecked(ownerId, id, expectedVersion, input, guard) {
542
+ return db.transaction(async (tx) => {
543
+ await lockMemoryNotes(tx, ownerId);
544
+ const error = guard(await visibleNotes(tx, ownerId));
545
+ return error ?? await updateNote(tx, ownerId, id, expectedVersion, input);
546
+ });
547
+ },
548
+ async deleteMemoryNote(ownerId, id, expectedVersion) {
549
+ const deleted = await db.transaction(async (tx) => {
550
+ const rows = await tx.delete(memoryNotes).where(and(eq(memoryNotes.ownerId, ownerId), eq(memoryNotes.id, id), eq(memoryNotes.version, expectedVersion), isNull(memoryNotes.mergedInto))).returning({ id: memoryNotes.id });
551
+ if (rows.length > 0) await tx.delete(memoryNotes).where(and(eq(memoryNotes.ownerId, ownerId), eq(memoryNotes.mergedInto, id)));
552
+ return rows.length > 0;
553
+ });
554
+ if (deleted) return "ok";
555
+ return await memoryNoteExists(db, ownerId, id) ? "stale_version" : "not_found";
556
+ },
557
+ async clearMemoryNotes(ownerId) {
558
+ return db.transaction(async (tx) => {
559
+ const deleted = await tx.delete(memoryNotes).where(eq(memoryNotes.ownerId, ownerId)).returning({ mergedInto: memoryNotes.mergedInto });
560
+ await tx.delete(memoryOptimizationRuns).where(eq(memoryOptimizationRuns.ownerId, ownerId));
561
+ return deleted.filter((row) => row.mergedInto === null).length;
562
+ });
563
+ },
564
+ // Взятие запуска — одна транзакция под замком человека: проверка правил и вставка не разрываются между процессами.
565
+ async claimMemoryOptimizationRun(ownerId, rules, now) {
566
+ const nowMs = Date.parse(now);
567
+ return db.transaction(async (tx) => {
568
+ await tx.execute(sql2`select pg_advisory_xact_lock(hashtextextended(${`agent_chat.memory_optimization:${ownerId}`}, 0))`);
569
+ const runs = await tx.select().from(memoryOptimizationRuns).where(eq(memoryOptimizationRuns.ownerId, ownerId));
570
+ const state = runs.map((run) => {
571
+ const leaseEnd = run.startedAt.getTime() + rules.leaseMs;
572
+ const expired = run.status === "running" && nowMs >= leaseEnd;
573
+ return { status: expired ? "failed" : run.status, endedAt: expired ? leaseEnd : (run.finishedAt ?? run.startedAt).getTime(), expired, id: run.id };
574
+ });
575
+ for (const run of state.filter((item) => item.expired)) {
576
+ await tx.update(memoryOptimizationRuns).set({ status: "failed", error: "lease_expired", finishedAt: new Date(run.endedAt) }).where(eq(memoryOptimizationRuns.id, run.id));
577
+ }
578
+ const blocked = isOptimizationBlocked(
579
+ runs.map((run, index2) => ({ status: state[index2].status, startedAtMs: run.startedAt.getTime(), endedAtMs: state[index2].endedAt })),
580
+ rules,
581
+ nowMs
582
+ );
583
+ if (blocked) return null;
584
+ const [created] = await tx.insert(memoryOptimizationRuns).values({ id: crypto.randomUUID(), ownerId, startedAt: new Date(nowMs), status: "running" }).returning();
585
+ return toOptimizationRun(created);
586
+ });
587
+ },
588
+ async finishMemoryOptimizationRun(ownerId, runId, outcome) {
589
+ const finished = await db.update(memoryOptimizationRuns).set({ status: outcome.status, finishedAt: new Date(outcome.finishedAt), error: outcome.error ?? null, usageRecordId: outcome.usageRecordId ?? null }).where(and(eq(memoryOptimizationRuns.id, runId), eq(memoryOptimizationRuns.ownerId, ownerId), eq(memoryOptimizationRuns.status, "running"))).returning({ id: memoryOptimizationRuns.id });
590
+ return finished.length > 0 ? "ok" : "not_found";
591
+ },
592
+ // Слияние — одна транзакция: несошедшийся оригинал бросает StaleMerge, и откатывается всё, включая запуск.
593
+ async applyMemoryMerge(ownerId, merge) {
594
+ const { merges } = merge;
595
+ if (merges.length === 0) throw new TypeError("\u0421\u043B\u0438\u044F\u043D\u0438\u0435 \u0431\u0435\u0437 \u0441\u043A\u043B\u0435\u0435\u043A");
596
+ const seen = /* @__PURE__ */ new Set();
597
+ for (const { result, originals } of merges) {
598
+ assertWellFormedNote(result);
599
+ if (originals.length === 0) throw new TypeError("\u0421\u043A\u043B\u0435\u0439\u043A\u0430 \u0431\u0435\u0437 \u043E\u0440\u0438\u0433\u0438\u043D\u0430\u043B\u043E\u0432");
600
+ for (const { id } of originals) {
601
+ if (seen.has(id)) throw new TypeError(`\u0417\u0430\u043F\u0438\u0441\u044C ${id} \u043D\u0430\u0437\u0432\u0430\u043D\u0430 \u0432 \u0434\u0432\u0443\u0445 \u0441\u043A\u043B\u0435\u0439\u043A\u0430\u0445`);
602
+ seen.add(id);
603
+ }
604
+ }
605
+ try {
606
+ return await db.transaction(async (tx) => {
607
+ const [run] = await tx.select().from(memoryOptimizationRuns).where(and(eq(memoryOptimizationRuns.id, merge.runId), eq(memoryOptimizationRuns.ownerId, ownerId))).for("update");
608
+ if (run?.status !== "running") return "run_not_active";
609
+ const now = /* @__PURE__ */ new Date();
610
+ const groups = [];
611
+ for (const { result, originals } of merges) {
612
+ const created = { id: crypto.randomUUID(), ownerId, categories: [...result.categories], content: result.content, version: 1, createdAt: now, updatedAt: now };
613
+ await tx.insert(memoryNotes).values(created);
614
+ for (const { id, version } of originals) {
615
+ const hidden = await tx.update(memoryNotes).set({ mergedInto: created.id }).where(and(eq(memoryNotes.ownerId, ownerId), eq(memoryNotes.id, id), eq(memoryNotes.version, version), isNull(memoryNotes.mergedInto))).returning({ id: memoryNotes.id });
616
+ if (hidden.length === 0) throw new StaleMerge();
617
+ }
618
+ groups.push({ resultId: created.id, originalIds: originals.map(({ id }) => id), resultVersion: 1 });
619
+ }
620
+ const [finished] = await tx.update(memoryOptimizationRuns).set({ status: "applied", finishedAt: new Date(merge.finishedAt), groups, usageRecordId: merge.usageRecordId ?? null }).where(eq(memoryOptimizationRuns.id, run.id)).returning();
621
+ return toOptimizationRun(finished);
622
+ });
623
+ } catch (error) {
624
+ if (error instanceof StaleMerge) return "stale_version";
625
+ throw error;
626
+ }
627
+ },
628
+ async revertMemoryMerge(ownerId, runId, guard) {
629
+ return db.transaction(async (tx) => {
630
+ await lockMemoryNotes(tx, ownerId);
631
+ const [run] = await tx.select().from(memoryOptimizationRuns).where(and(eq(memoryOptimizationRuns.id, runId), eq(memoryOptimizationRuns.ownerId, ownerId))).for("update");
632
+ if (run?.status !== "applied") return "not_found";
633
+ for (const { resultId, originalIds, resultVersion } of run.groups) {
634
+ const [result] = await tx.select({ version: memoryNotes.version }).from(memoryNotes).where(and(eq(memoryNotes.ownerId, ownerId), eq(memoryNotes.id, resultId), isNull(memoryNotes.mergedInto))).for("update");
635
+ if (result?.version !== resultVersion) return "conflict";
636
+ const hidden = await tx.select({ id: memoryNotes.id }).from(memoryNotes).where(and(eq(memoryNotes.ownerId, ownerId), eq(memoryNotes.mergedInto, resultId), inArray(memoryNotes.id, originalIds))).for("update");
637
+ if (hidden.length !== originalIds.length) return "conflict";
638
+ }
639
+ if (guard) {
640
+ const resultIds = run.groups.map(({ resultId }) => resultId);
641
+ const remaining = (await visibleNotes(tx, ownerId)).filter((note) => !resultIds.includes(note.id));
642
+ const restoredRows = await tx.select().from(memoryNotes).where(and(eq(memoryNotes.ownerId, ownerId), inArray(memoryNotes.mergedInto, resultIds))).orderBy(desc(memoryNotes.updatedAt), desc(memoryNotes.createdAt), sql2`${memoryNotes.id} collate "C" desc`);
643
+ const error = guard(remaining, restoredRows.map(toMemoryNote));
644
+ if (error) return revertRefusal(error);
645
+ }
646
+ for (const { resultId } of run.groups) {
647
+ await tx.update(memoryNotes).set({ mergedInto: null }).where(and(eq(memoryNotes.ownerId, ownerId), eq(memoryNotes.mergedInto, resultId)));
648
+ await tx.delete(memoryNotes).where(and(eq(memoryNotes.ownerId, ownerId), eq(memoryNotes.id, resultId)));
649
+ }
650
+ await tx.update(memoryOptimizationRuns).set({ status: "reverted" }).where(eq(memoryOptimizationRuns.id, run.id));
651
+ return "ok";
652
+ });
653
+ },
654
+ async listMemoryOptimizationRuns(ownerId, filter) {
655
+ const query = db.select().from(memoryOptimizationRuns).where(and(eq(memoryOptimizationRuns.ownerId, ownerId), ...filter?.status ? [inArray(memoryOptimizationRuns.status, filter.status)] : [])).orderBy(desc(memoryOptimizationRuns.startedAt), sql2`${memoryOptimizationRuns.id} collate "C" desc`);
656
+ const rows = await (filter?.limit === void 0 ? query : query.limit(filter.limit));
657
+ return rows.map(toOptimizationRun);
658
+ },
659
+ async listMergedMemoryNotes(ownerId) {
660
+ const rows = await db.select().from(memoryNotes).where(and(eq(memoryNotes.ownerId, ownerId), isNotNull(memoryNotes.mergedInto))).orderBy(desc(memoryNotes.updatedAt), desc(memoryNotes.createdAt), sql2`${memoryNotes.id} collate "C" desc`);
661
+ return rows.map((row) => ({ ...toMemoryNote(row), mergedInto: row.mergedInto }));
662
+ }
663
+ };
664
+ }
665
+ function isPrimaryKeyConflict(error) {
666
+ return isConflictOn(error, "messages_pkey");
667
+ }
668
+ function isConflictOn(error, constraintName) {
669
+ const { code, constraint, cause } = error ?? {};
670
+ return [code, cause?.code].includes("23505") && [constraint, cause?.constraint].includes(constraintName);
671
+ }
672
+ function chatRow(chat) {
673
+ return {
674
+ id: chat.id,
675
+ ownerId: chat.ownerId,
676
+ modelId: chat.modelId,
677
+ providerId: chat.providerId,
678
+ reasoning: chat.reasoning ?? null,
679
+ title: chat.title ?? null,
680
+ origin: chat.origin ?? null,
681
+ subjectKey: chat.subjectKey ?? null,
682
+ subjectData: chatDataOfInput(chat) ?? null,
683
+ closed: chat.closed ?? null,
684
+ createdAt: chat.createdAt,
685
+ updatedAt: chat.updatedAt
686
+ };
687
+ }
688
+ function toChat(row) {
689
+ const { reasoning, title, origin, subjectKey, subjectData, closed, ...chat } = row;
690
+ return {
691
+ ...chat,
692
+ ...reasoning !== null && { reasoning },
693
+ ...title !== null && { title },
694
+ ...origin !== null && { origin },
695
+ ...subjectKey !== null && { subjectKey },
696
+ ...chatDataFields(subjectData),
697
+ ...closed !== null && { closed }
698
+ };
699
+ }
700
+ function subjectCondition(subjects = "exclude") {
701
+ if (subjects === "include") return void 0;
702
+ return subjects === "only" ? isNotNull(chats.subjectKey) : isNull(chats.subjectKey);
703
+ }
704
+ function dataCondition(filter) {
705
+ if (!filter || Object.keys(filter).length === 0) return void 0;
706
+ return sql2`${chats.subjectData}::jsonb @> ${JSON.stringify(filter)}::jsonb`;
707
+ }
708
+ function toMessage(row) {
709
+ const { seq: _seq, aliveAt: _aliveAt, afterTurn, ...message } = row;
710
+ return { ...message, ...afterTurn !== null && { afterTurn } };
711
+ }
712
+ function toMemoryNote(row) {
713
+ return {
714
+ id: row.id,
715
+ categories: [...row.categories],
716
+ content: row.content,
717
+ version: row.version,
718
+ createdAt: row.createdAt.toISOString(),
719
+ updatedAt: row.updatedAt.toISOString()
720
+ };
721
+ }
722
+ function toOptimizationRun(row) {
723
+ return {
724
+ id: row.id,
725
+ startedAt: row.startedAt.toISOString(),
726
+ finishedAt: row.finishedAt ? row.finishedAt.toISOString() : null,
727
+ status: row.status,
728
+ groups: row.groups.map((group) => ({ ...group, originalIds: [...group.originalIds] })),
729
+ error: row.error,
730
+ usageRecordId: row.usageRecordId
731
+ };
732
+ }
733
+ function assertWellFormedNote(note) {
734
+ if (!Array.isArray(note?.categories) || !note.categories.every((category) => typeof category === "string")) {
735
+ throw new TypeError("\u0420\u0430\u0437\u0434\u0435\u043B\u044B \u0437\u0430\u043F\u0438\u0441\u0438 \u043F\u0430\u043C\u044F\u0442\u0438 \u2014 \u0441\u043F\u0438\u0441\u043E\u043A \u0441\u0442\u0440\u043E\u043A");
736
+ }
737
+ if (typeof note.content !== "string") throw new TypeError("\u0422\u0435\u043A\u0441\u0442 \u0437\u0430\u043F\u0438\u0441\u0438 \u043F\u0430\u043C\u044F\u0442\u0438 \u2014 \u0441\u0442\u0440\u043E\u043A\u0430");
738
+ }
739
+
740
+ // src/migrate.ts
741
+ import { fileURLToPath } from "url";
742
+ import { drizzle } from "drizzle-orm/node-postgres";
743
+ import { migrate } from "drizzle-orm/node-postgres/migrator";
744
+ var migrationsFolder = fileURLToPath(new URL("../drizzle", import.meta.url));
745
+ var MIGRATION_LOCK_KEY = 7311900442;
746
+ async function migrateToLatest(pool) {
747
+ const client = await pool.connect();
748
+ try {
749
+ await client.query("select pg_advisory_lock($1)", [MIGRATION_LOCK_KEY]);
750
+ try {
751
+ await migrate(drizzle(client), { migrationsFolder, migrationsSchema: "agent_chat" });
752
+ } finally {
753
+ await client.query("select pg_advisory_unlock($1)", [MIGRATION_LOCK_KEY]);
754
+ }
755
+ } finally {
756
+ client.release();
757
+ }
758
+ }
759
+
760
+ // src/postgres-storage.ts
761
+ var DEFAULT_POOL_SIZE = 10;
762
+ async function createPostgresChatStorage(options) {
763
+ const logError = options.logError ?? ((message) => console.error(message));
764
+ const pool = new pg.Pool({ connectionString: options.connectionString, max: options.poolSize ?? DEFAULT_POOL_SIZE });
765
+ pool.on("error", (error) => logError(`PostgreSQL: \u0441\u043E\u0435\u0434\u0438\u043D\u0435\u043D\u0438\u0435 \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u043E\u0441\u044C: ${error.message}`));
766
+ try {
767
+ await pool.query("select 1");
768
+ } catch (error) {
769
+ await pool.end().catch(() => {
770
+ });
771
+ throw failure("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C\u0441\u044F \u043A PostgreSQL", options.connectionString, error);
772
+ }
773
+ try {
774
+ await migrateToLatest(pool);
775
+ } catch (error) {
776
+ await pool.end().catch(() => {
777
+ });
778
+ throw failure("\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u043C\u0438\u0433\u0440\u0430\u0446\u0438\u0438 PostgreSQL", options.connectionString, error);
779
+ }
780
+ return {
781
+ ...createDrizzleChatStorage(drizzle2(pool, { schema: storageSchema })),
782
+ async close() {
783
+ await pool.end();
784
+ }
785
+ };
786
+ }
787
+ function failure(summary, connectionString, cause) {
788
+ const reason = cause instanceof Error ? cause.message : String(cause);
789
+ let place = "\u0430\u0434\u0440\u0435\u0441 \u043D\u0435 \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u043D";
790
+ try {
791
+ const address = new URL(connectionString);
792
+ place = `${address.hostname}:${address.port || "5432"}${address.pathname}`;
793
+ } catch {
794
+ }
795
+ const safeReason = reason.split(connectionString).join("<\u0441\u0442\u0440\u043E\u043A\u0430 \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F>");
796
+ return new Error(`${summary} (${place}): ${safeReason}`);
797
+ }
798
+ export {
799
+ createPostgresChatStorage
800
+ };
801
+ //# sourceMappingURL=index.js.map