@ykurilov/agent-chat-flows 0.66.0 → 0.68.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 (41) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/agent-flow.schema.json +12 -0
  3. package/dist/{chunk-7VR4YNTJ.js → chunk-2LAVG3YR.js} +42 -9
  4. package/dist/chunk-2LAVG3YR.js.map +1 -0
  5. package/dist/{chunk-KRZMJJKG.js → chunk-DQQ2GCQ2.js} +22 -11
  6. package/dist/chunk-DQQ2GCQ2.js.map +1 -0
  7. package/dist/{chunk-L7XNC3RB.js → chunk-PLY57QT5.js} +2 -2
  8. package/dist/format.js +2 -2
  9. package/dist/index.js +63 -7
  10. package/dist/index.js.map +1 -1
  11. package/dist/postgres.js +7 -0
  12. package/dist/postgres.js.map +1 -1
  13. package/dist/react.js +51 -9
  14. package/dist/react.js.map +1 -1
  15. package/dist/sqlite.js +6 -0
  16. package/dist/sqlite.js.map +1 -1
  17. package/dist/types/engine/flow-run.d.ts +4 -2
  18. package/dist/types/engine/nudges.d.ts +1 -1
  19. package/dist/types/engine/run-updates.d.ts +5 -0
  20. package/dist/types/format/flow-format.d.ts +10 -0
  21. package/dist/types/format/flow-schema.d.ts +6 -0
  22. package/dist/types/labels/editor.d.ts +5 -1
  23. package/dist/types/react/editor/index.d.ts +1 -1
  24. package/dist/types/react/editor/schedule-model.d.ts +9 -1
  25. package/dist/types/react/flow-editor-skin.d.ts +3 -0
  26. package/dist/types/runtime/first-message-rules.d.ts +2 -0
  27. package/dist/types/runtime/flow-events.d.ts +6 -1
  28. package/dist/types/runtime/flow-types.d.ts +52 -0
  29. package/dist/types/store/flow-store.d.ts +10 -0
  30. package/dist/types/store/postgres-schema.d.ts +17 -0
  31. package/dist/types/store/sqlite-schema.d.ts +17 -0
  32. package/drizzle/postgres/0003_chat_reopen.sql +1 -0
  33. package/drizzle/postgres/meta/0003_snapshot.json +451 -0
  34. package/drizzle/postgres/meta/_journal.json +7 -0
  35. package/drizzle/sqlite/0003_chat_reopen.sql +1 -0
  36. package/drizzle/sqlite/meta/0003_snapshot.json +441 -0
  37. package/drizzle/sqlite/meta/_journal.json +7 -0
  38. package/package.json +4 -4
  39. package/dist/chunk-7VR4YNTJ.js.map +0 -1
  40. package/dist/chunk-KRZMJJKG.js.map +0 -1
  41. /package/dist/{chunk-L7XNC3RB.js.map → chunk-PLY57QT5.js.map} +0 -0
package/dist/sqlite.js CHANGED
@@ -38,6 +38,7 @@ var flowChats = sqliteTable(
38
38
  updatedAt: integer("updated_at").notNull(),
39
39
  endedAt: integer("ended_at"),
40
40
  endReason: text("end_reason").$type(),
41
+ reopenedAt: integer("reopened_at"),
41
42
  pausedUntil: integer("paused_until"),
42
43
  windowClosed: integer("window_closed", { mode: "boolean" }).notNull().default(false),
43
44
  dayClosedOn: text("day_closed_on"),
@@ -117,6 +118,7 @@ function toRecord(row) {
117
118
  updatedAt: toIso(row.updatedAt),
118
119
  endedAt: row.endedAt === null ? null : toIso(row.endedAt),
119
120
  endReason: row.endReason,
121
+ reopenedAt: row.reopenedAt === null ? null : toIso(row.reopenedAt),
120
122
  pausedUntil: row.pausedUntil === null ? null : toIso(row.pausedUntil),
121
123
  windowClosed: row.windowClosed,
122
124
  dayClosedOn: row.dayClosedOn,
@@ -277,6 +279,10 @@ function createSqliteFlowStore(filePath, migrationsFolder) {
277
279
  const row = getChat(chatId);
278
280
  return row ? toRecord(row) : null;
279
281
  },
282
+ async reopenChat(chatId, { endedAt, at }) {
283
+ const row = db.update(flowChats).set({ endedAt: null, endReason: null, reopenedAt: toMs(at) }).where(and(eq(flowChats.chatId, chatId), eq(flowChats.endedAt, toMs(endedAt)))).returning().get();
284
+ return row ? toRecord(row) : null;
285
+ },
280
286
  async setPause(chatId, until) {
281
287
  const row = db.update(flowChats).set({ pausedUntil: until === null ? null : toMs(until) }).where(eq(flowChats.chatId, chatId)).returning().get();
282
288
  return row ? toRecord(row) : null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sqlite.ts","../src/store/sqlite.ts","../src/store/sqlite-schema.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { createSqliteFlowStore, type SqliteFlowStore } from \"./store/sqlite\";\n\nexport type { SqliteFlowStore } from \"./store/sqlite\";\nexport { FLOWS_MIGRATIONS_TABLE } from \"./store/migration-settings\";\nexport { flowChats, flowClientCards, flowFirstMessages, flowVersions } from \"./store/sqlite-schema\";\n\n/** Миграции лежат в пакете рядом с `dist`: `drizzle/sqlite`. */\nconst MIGRATIONS_FOLDER = fileURLToPath(new URL(\"../drizzle/sqlite\", import.meta.url));\n\n/**\n * Сценарии в файле SQLite; файл может быть общим с хранилищем чатов (`createSqliteChatStorage`).\n * Таблицы приводятся к последней миграции при открытии.\n */\nexport function sqliteFlowStore(filePath: string): SqliteFlowStore {\n return createSqliteFlowStore(filePath, MIGRATIONS_FOLDER);\n}\n","import Database from \"better-sqlite3\";\nimport { and, asc, eq, inArray, isNull, lt, lte, ne, or, sql, type SQL } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { migrate } from \"drizzle-orm/better-sqlite3/migrator\";\nimport type { StateMap } from \"../engine/migrate-run\";\nimport type { FlowChatRecord, FlowStore, PendingFlowChange, SideTaskRecord } from \"./flow-store\";\nimport { appliedInTime, byVersionAndState, compareText, migratePending, pendingAdded } from \"./flow-store-shared\";\nimport { FLOWS_MIGRATIONS_TABLE } from \"./migration-settings\";\nimport { flowChats, flowClientCards, flowFirstMessages, flowPeople, flowSideTasks, flowVersions } from \"./sqlite-schema\";\n\n/** Хранилище в файле SQLite: `close` обязателен — файл закрывает приложение при остановке. */\nexport type SqliteFlowStore = FlowStore & Required<Pick<FlowStore, \"close\">>;\n\ntype ChatRow = typeof flowChats.$inferSelect;\n\nconst toMs = (iso: string) => Date.parse(iso);\nconst toIso = (ms: number) => new Date(ms).toISOString();\n\nfunction toRecord(row: ChatRow): FlowChatRecord {\n return {\n chatId: row.chatId,\n ownerId: row.ownerId,\n flowId: row.flowId,\n versionId: row.versionId,\n stateId: row.stateId,\n pending: row.pending,\n dueAt: row.dueAt === null ? null : toIso(row.dueAt),\n dueExitId: row.dueExitId,\n dueBranchHead: row.dueBranchHead,\n timeZone: row.timeZone,\n updatedAt: toIso(row.updatedAt),\n endedAt: row.endedAt === null ? null : toIso(row.endedAt),\n endReason: row.endReason,\n pausedUntil: row.pausedUntil === null ? null : toIso(row.pausedUntil),\n windowClosed: row.windowClosed,\n dayClosedOn: row.dayClosedOn,\n delivery: row.delivery,\n placement: row.placement,\n };\n}\n\ntype SideTaskRow = typeof flowSideTasks.$inferSelect;\nconst toSideTask = (row: SideTaskRow): SideTaskRecord => ({\n taskId: row.taskId,\n chatId: row.chatId === \"\" ? null : row.chatId,\n asks: row.asks,\n doneAt: row.doneAt === null ? null : toIso(row.doneAt),\n});\n\n/** Дописать изменение в конец `pending`, если такого ключа там ещё нет. Одно выражение — одна запись базы. */\nconst pendingWith = (change: PendingFlowChange): SQL =>\n sql`case when exists (select 1 from json_each(${flowChats.pending}) where json_extract(value, '$.key') = ${change.key})\n then ${flowChats.pending} else json_insert(${flowChats.pending}, '$[#]', json(${JSON.stringify(change)})) end`;\n\n/** Состояние по карте перевода: удалённые — в выбранные, остальные как были. */\nfunction mappedStateSql(map: StateMap): SQL {\n const entries = Object.entries(map);\n if (entries.length === 0) return sql`${flowChats.stateId}`;\n const cases = entries.map(([from, to]) => sql`when ${from} then ${to}`);\n return sql`case ${flowChats.stateId} ${sql.join(cases, sql` `)} else ${flowChats.stateId} end`;\n}\n\n/** Таймер наступил, никто его не держит, разговор идёт и не на паузе. Аренда до `lease_until` включительно ещё живая. */\nconst dueAndFree = (now: number) =>\n and(\n lte(flowChats.dueAt, now),\n or(isNull(flowChats.leaseUntil), lt(flowChats.leaseUntil, now)),\n isNull(flowChats.endedAt),\n or(isNull(flowChats.pausedUntil), lte(flowChats.pausedUntil, now)),\n );\n\n/**\n * Сценарии в файле SQLite. Схема приводится к последней миграции при открытии; файл может быть общим с хранилищем\n * чатов. Несколько копий сервера на одном файле делят таймеры арендой: каждое изменение — одно условное `update`\n * или короткая транзакция с захватом записи (`immediate`).\n */\nexport function createSqliteFlowStore(filePath: string, migrationsFolder: string): SqliteFlowStore {\n const sqlite = new Database(filePath);\n sqlite.pragma(\"journal_mode = WAL\");\n sqlite.pragma(\"busy_timeout = 5000\");\n const db = drizzle(sqlite);\n migrate(db, { migrationsFolder, migrationsTable: FLOWS_MIGRATIONS_TABLE });\n\n const getChat = (chatId: string) => db.select().from(flowChats).where(eq(flowChats.chatId, chatId)).get();\n\n return {\n async pinChat(input) {\n db.insert(flowChats)\n .values({\n chatId: input.chatId,\n ownerId: input.ownerId,\n flowId: input.flowId,\n versionId: input.versionId,\n stateId: input.stateId,\n pending: [],\n timeZone: input.timeZone,\n ...(input.placement && { placement: input.placement }),\n updatedAt: toMs(input.at),\n })\n .onConflictDoNothing()\n .run();\n return toRecord(getChat(input.chatId)!);\n },\n async getChat(chatId) {\n const row = getChat(chatId);\n return row ? toRecord(row) : null;\n },\n async updateChat(chatId, patch, at) {\n const row = db\n .update(flowChats)\n .set({ versionId: patch.versionId, stateId: patch.stateId, timeZone: patch.timeZone, placement: patch.placement, updatedAt: toMs(at) })\n .where(eq(flowChats.chatId, chatId))\n .returning()\n .get();\n return row ? toRecord(row) : null;\n },\n async deleteChat(chatId) {\n db.delete(flowChats).where(eq(flowChats.chatId, chatId)).run();\n },\n\n async addPending(chatId, change) {\n // Прочитать и дописать под захватом записи: две копии с одним ключом не дают двух записей.\n const add = sqlite.transaction(() => {\n const row = db.select({ pending: flowChats.pending }).from(flowChats).where(eq(flowChats.chatId, chatId)).get();\n const next = row && pendingAdded(row.pending, change);\n if (next) db.update(flowChats).set({ pending: next }).where(eq(flowChats.chatId, chatId)).run();\n });\n add.immediate();\n },\n async takePending(chatId) {\n // Прочитать и очистить под захватом записи: вторая копия ждёт и видит уже пустой список (кроме применённых в срок).\n const take = sqlite.transaction(() => {\n const row = db.select({ pending: flowChats.pending }).from(flowChats).where(eq(flowChats.chatId, chatId)).get();\n if (!row || row.pending.length === 0) return [];\n db.update(flowChats).set({ pending: row.pending.filter(appliedInTime) }).where(eq(flowChats.chatId, chatId)).run();\n return row.pending;\n });\n return take.immediate();\n },\n\n async setDue(chatId, due, options) {\n db.update(flowChats)\n .set({\n dueAt: due ? toMs(due.at) : null,\n dueExitId: due?.exitId ?? null,\n dueBranchHead: due?.branchHead ?? null,\n leaseUntil: null,\n leaseOwner: null,\n })\n .where(and(eq(flowChats.chatId, chatId), options?.unlessDelivery ? isNull(flowChats.delivery) : undefined))\n .run();\n },\n async leaseDue({ now, owner, leaseMs, limit }) {\n const nowMs = toMs(now);\n // Выбор и аренда — одно `update`: из двух копий, пришедших одновременно, строку получит только первая.\n const due = db\n .select({ chatId: flowChats.chatId })\n .from(flowChats)\n .where(dueAndFree(nowMs))\n .orderBy(asc(flowChats.dueAt), asc(flowChats.chatId))\n .limit(limit);\n const rows = db\n .update(flowChats)\n .set({ leaseOwner: owner, leaseUntil: nowMs + leaseMs })\n .where(and(inArray(flowChats.chatId, due), dueAndFree(nowMs)))\n .returning()\n .all();\n return rows.map(toRecord).sort((left, right) => compareText(left.dueAt!, right.dueAt!) || compareText(left.chatId, right.chatId));\n },\n async releaseLease(chatId, owner) {\n db.update(flowChats)\n .set({ leaseOwner: null, leaseUntil: null })\n .where(and(eq(flowChats.chatId, chatId), eq(flowChats.leaseOwner, owner)))\n .run();\n },\n async renewLease({ chatId, owner, now, leaseMs }) {\n const renewed = db\n .update(flowChats)\n .set({ leaseUntil: toMs(now) + leaseMs })\n .where(and(eq(flowChats.chatId, chatId), eq(flowChats.leaseOwner, owner)))\n .returning({ chatId: flowChats.chatId })\n .all();\n return renewed.length > 0;\n },\n\n async saveVersion(input) {\n const inserted = db\n .insert(flowVersions)\n .values({ flowId: input.flowId, versionId: input.versionId, hash: input.hash, flow: input.flow, createdAt: toMs(input.at) })\n .onConflictDoNothing()\n .returning({ hash: flowVersions.hash })\n .all();\n if (inserted.length > 0) return \"new\";\n const saved = db\n .select({ hash: flowVersions.hash })\n .from(flowVersions)\n .where(and(eq(flowVersions.flowId, input.flowId), eq(flowVersions.versionId, input.versionId)))\n .get();\n return saved?.hash === input.hash ? \"same\" : \"conflict\";\n },\n async getVersion(flowId, versionId) {\n const row = db\n .select()\n .from(flowVersions)\n .where(and(eq(flowVersions.flowId, flowId), eq(flowVersions.versionId, versionId)))\n .get();\n return row ? { ...row, createdAt: toIso(row.createdAt) } : null;\n },\n\n async countChatsByState({ flowId }) {\n const rows = db\n .select({ versionId: flowChats.versionId, stateId: flowChats.stateId, chats: sql<number>`count(*)` })\n .from(flowChats)\n .where(eq(flowChats.flowId, flowId))\n .groupBy(flowChats.versionId, flowChats.stateId)\n .all();\n return rows.map((row) => ({ ...row, chats: Number(row.chats) })).sort(byVersionAndState);\n },\n async listFlowChats({ flowId, exceptVersionId, chatIds }) {\n if (chatIds?.length === 0) return [];\n const rows = db\n .select({\n chatId: flowChats.chatId,\n ownerId: flowChats.ownerId,\n flowId: flowChats.flowId,\n versionId: flowChats.versionId,\n stateId: flowChats.stateId,\n endedAt: flowChats.endedAt,\n endReason: flowChats.endReason,\n })\n .from(flowChats)\n .where(\n and(\n eq(flowChats.flowId, flowId),\n exceptVersionId === undefined ? undefined : ne(flowChats.versionId, exceptVersionId),\n chatIds === undefined ? undefined : inArray(flowChats.chatId, chatIds),\n ),\n )\n .orderBy(asc(flowChats.chatId))\n .all();\n return rows.map((row) => ({ ...row, endedAt: row.endedAt === null ? null : toIso(row.endedAt) }));\n },\n async migrateBatch(input) {\n const change = migratePending(input);\n if (input.chatIds?.length === 0) return { moved: [] };\n const notYetMoved = and(\n eq(flowChats.flowId, input.flowId),\n ne(flowChats.versionId, input.toVersionId),\n input.chatIds === undefined ? undefined : inArray(flowChats.chatId, input.chatIds),\n );\n const batch = db.select({ chatId: flowChats.chatId }).from(flowChats).where(notYetMoved).orderBy(asc(flowChats.chatId)).limit(input.limit);\n const moved = db\n .update(flowChats)\n .set({ versionId: input.toVersionId, stateId: mappedStateSql(input.map), pending: pendingWith(change), updatedAt: toMs(input.at) })\n .where(and(inArray(flowChats.chatId, batch), notYetMoved))\n .returning({ chatId: flowChats.chatId })\n .all();\n return { moved: moved.map((row) => row.chatId) };\n },\n\n async readCard(ownerId) {\n const rows = db.select().from(flowClientCards).where(eq(flowClientCards.ownerId, ownerId)).all();\n return rows\n .map((row) => ({ fieldId: row.fieldId, value: row.value, updatedAt: toIso(row.updatedAt), chatId: row.chatId, flowId: row.flowId }))\n .sort((left, right) => compareText(left.fieldId, right.fieldId));\n },\n async writeCardField(input) {\n const entry = { value: input.value, updatedAt: toMs(input.at), chatId: input.chatId, flowId: input.flowId };\n db.insert(flowClientCards)\n .values({ ownerId: input.ownerId, fieldId: input.fieldId, ...entry })\n .onConflictDoUpdate({ target: [flowClientCards.ownerId, flowClientCards.fieldId], set: entry })\n .run();\n },\n async forgetCard(ownerId, fieldId) {\n db.delete(flowClientCards)\n .where(and(eq(flowClientCards.ownerId, ownerId), fieldId === \"all\" ? undefined : eq(flowClientCards.fieldId, fieldId)))\n .run();\n },\n\n async endChat(chatId, { reason, at }) {\n db.update(flowChats)\n .set({ endedAt: toMs(at), endReason: reason, dueAt: null, dueExitId: null, dueBranchHead: null, leaseUntil: null, leaseOwner: null })\n .where(and(eq(flowChats.chatId, chatId), isNull(flowChats.endedAt)))\n .run();\n const row = getChat(chatId);\n return row ? toRecord(row) : null;\n },\n async setPause(chatId, until) {\n const row = db\n .update(flowChats)\n .set({ pausedUntil: until === null ? null : toMs(until) })\n .where(eq(flowChats.chatId, chatId))\n .returning()\n .get();\n return row ? toRecord(row) : null;\n },\n async setWindowClosed(chatId, closed) {\n db.update(flowChats).set({ windowClosed: closed }).where(eq(flowChats.chatId, chatId)).run();\n },\n async setDayClosed(chatId, day) {\n db.update(flowChats).set({ dayClosedOn: day }).where(eq(flowChats.chatId, chatId)).run();\n },\n async setDelivery(chatId, delivery) {\n const clean = delivery === null ? null : { ...delivery, retryAt: toIso(toMs(delivery.retryAt)) };\n db.update(flowChats).set({ delivery: clean }).where(eq(flowChats.chatId, chatId)).run();\n },\n\n async readTimeZone(ownerId) {\n return db.select({ timeZone: flowPeople.timeZone }).from(flowPeople).where(eq(flowPeople.ownerId, ownerId)).get()?.timeZone ?? null;\n },\n async writeTimeZone(ownerId, timeZone, at) {\n db.insert(flowPeople)\n .values({ ownerId, timeZone, updatedAt: toMs(at) })\n .onConflictDoUpdate({ target: flowPeople.ownerId, set: { timeZone, updatedAt: toMs(at) } })\n .run();\n },\n\n async readSideTasks(ownerId, chatId) {\n const rows = db\n .select()\n .from(flowSideTasks)\n .where(and(eq(flowSideTasks.ownerId, ownerId), or(eq(flowSideTasks.chatId, \"\"), eq(flowSideTasks.chatId, chatId))))\n .all();\n return rows.map(toSideTask).sort((left, right) => compareText(left.taskId, right.taskId));\n },\n async forgetSideTasks(ownerId) {\n db.delete(flowSideTasks).where(eq(flowSideTasks.ownerId, ownerId)).run();\n },\n async noteSideTask(input) {\n const asked = input.status === \"asked\";\n const doneAt = asked ? null : toMs(input.at);\n const row = db\n .insert(flowSideTasks)\n .values({ ownerId: input.ownerId, taskId: input.taskId, chatId: input.chatId ?? \"\", asks: asked ? 1 : 0, doneAt })\n .onConflictDoUpdate({\n target: [flowSideTasks.ownerId, flowSideTasks.taskId, flowSideTasks.chatId],\n set: asked ? { asks: sql`${flowSideTasks.asks} + 1` } : { doneAt: sql`coalesce(${flowSideTasks.doneAt}, ${doneAt})` },\n })\n .returning()\n .get();\n return toSideTask(row!);\n },\n\n async countFirstMessages(ownerId, day) {\n const row = db\n .select({ count: flowFirstMessages.count })\n .from(flowFirstMessages)\n .where(and(eq(flowFirstMessages.ownerId, ownerId), eq(flowFirstMessages.day, day)))\n .get();\n return row?.count ?? 0;\n },\n async addFirstMessage(ownerId, day) {\n const row = db\n .insert(flowFirstMessages)\n .values({ ownerId, day, count: 1 })\n .onConflictDoUpdate({ target: [flowFirstMessages.ownerId, flowFirstMessages.day], set: { count: sql`${flowFirstMessages.count} + 1` } })\n .returning({ count: flowFirstMessages.count })\n .get();\n return row!.count;\n },\n\n async close() {\n if (sqlite.open) sqlite.close();\n },\n };\n}\n","import type { AgentFlow } from \"../format/flow-format\";\nimport type { FlowEndReason } from \"../engine/flow-run\";\nimport type { FlowChatPlacement, FlowPendingDelivery, PendingFlowChange } from \"./flow-store\";\nimport { index, integer, primaryKey, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\n// Времена — миллисекунды с 1970 года, JSON — текстом.\n\n/** Чаты, которые идут по сценарию. */\nexport const flowChats = sqliteTable(\n \"flow_chats\",\n {\n chatId: text(\"chat_id\").primaryKey(),\n ownerId: text(\"owner_id\").notNull(),\n flowId: text(\"flow_id\").notNull(),\n versionId: text(\"version_id\").notNull(),\n stateId: text(\"state_id\").notNull(),\n pending: text(\"pending\", { mode: \"json\" }).$type<PendingFlowChange[]>().notNull().default([]),\n dueAt: integer(\"due_at\"),\n dueExitId: text(\"due_exit_id\"),\n dueBranchHead: text(\"due_branch_head\"),\n timeZone: text(\"time_zone\").notNull(),\n leaseUntil: integer(\"lease_until\"),\n leaseOwner: text(\"lease_owner\"),\n updatedAt: integer(\"updated_at\").notNull(),\n endedAt: integer(\"ended_at\"),\n endReason: text(\"end_reason\").$type<FlowEndReason>(),\n pausedUntil: integer(\"paused_until\"),\n windowClosed: integer(\"window_closed\", { mode: \"boolean\" }).notNull().default(false),\n dayClosedOn: text(\"day_closed_on\"),\n delivery: text(\"delivery\", { mode: \"json\" }).$type<FlowPendingDelivery>(),\n placement: text(\"placement\", { mode: \"json\" }).$type<FlowChatPlacement>(),\n },\n (table) => [\n index(\"flow_chats_due_idx\").on(table.dueAt),\n index(\"flow_chats_flow_state_idx\").on(table.flowId, table.versionId, table.stateId),\n index(\"flow_chats_owner_idx\").on(table.ownerId),\n ],\n);\n\n/** Сохранённые версии сценариев: чат идёт по своей версии, пока его не перевели. */\nexport const flowVersions = sqliteTable(\n \"flow_versions\",\n {\n flowId: text(\"flow_id\").notNull(),\n versionId: text(\"version_id\").notNull(),\n hash: text(\"hash\").notNull(),\n flow: text(\"flow\", { mode: \"json\" }).$type<AgentFlow>().notNull(),\n createdAt: integer(\"created_at\").notNull(),\n },\n (table) => [primaryKey({ columns: [table.flowId, table.versionId] })],\n);\n\n/** Карточка клиента: поле за полем, переживает чаты. */\nexport const flowClientCards = sqliteTable(\n \"flow_client_cards\",\n {\n ownerId: text(\"owner_id\").notNull(),\n fieldId: text(\"field_id\").notNull(),\n value: text(\"value\", { mode: \"json\" }).notNull(),\n updatedAt: integer(\"updated_at\").notNull(),\n chatId: text(\"chat_id\").notNull(),\n flowId: text(\"flow_id\").notNull(),\n },\n (table) => [primaryKey({ columns: [table.ownerId, table.fieldId] })],\n);\n\n/** Сколько сообщений агент написал первым за сутки человека. */\nexport const flowFirstMessages = sqliteTable(\n \"flow_first_messages\",\n {\n ownerId: text(\"owner_id\").notNull(),\n day: text(\"day\").notNull(),\n count: integer(\"count\").notNull().default(0),\n },\n (table) => [primaryKey({ columns: [table.ownerId, table.day] })],\n);\n\n/** Пояс человека: общий для всех его чатов. */\nexport const flowPeople = sqliteTable(\"flow_people\", {\n ownerId: text(\"owner_id\").primaryKey(),\n timeZone: text(\"time_zone\").notNull(),\n updatedAt: integer(\"updated_at\").notNull(),\n});\n\n/** Попытки попутных задач. `chat_id` пустой — попытки на человека, иначе — на разговор. */\nexport const flowSideTasks = sqliteTable(\n \"flow_side_tasks\",\n {\n ownerId: text(\"owner_id\").notNull(),\n taskId: text(\"task_id\").notNull(),\n chatId: text(\"chat_id\").notNull().default(\"\"),\n asks: integer(\"asks\").notNull().default(0),\n doneAt: integer(\"done_at\"),\n },\n (table) => [primaryKey({ columns: [table.ownerId, table.taskId, table.chatId] })],\n);\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sqlite.ts","../src/store/sqlite.ts","../src/store/sqlite-schema.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { createSqliteFlowStore, type SqliteFlowStore } from \"./store/sqlite\";\n\nexport type { SqliteFlowStore } from \"./store/sqlite\";\nexport { FLOWS_MIGRATIONS_TABLE } from \"./store/migration-settings\";\nexport { flowChats, flowClientCards, flowFirstMessages, flowVersions } from \"./store/sqlite-schema\";\n\n/** Миграции лежат в пакете рядом с `dist`: `drizzle/sqlite`. */\nconst MIGRATIONS_FOLDER = fileURLToPath(new URL(\"../drizzle/sqlite\", import.meta.url));\n\n/**\n * Сценарии в файле SQLite; файл может быть общим с хранилищем чатов (`createSqliteChatStorage`).\n * Таблицы приводятся к последней миграции при открытии.\n */\nexport function sqliteFlowStore(filePath: string): SqliteFlowStore {\n return createSqliteFlowStore(filePath, MIGRATIONS_FOLDER);\n}\n","import Database from \"better-sqlite3\";\nimport { and, asc, eq, inArray, isNull, lt, lte, ne, or, sql, type SQL } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/better-sqlite3\";\nimport { migrate } from \"drizzle-orm/better-sqlite3/migrator\";\nimport type { StateMap } from \"../engine/migrate-run\";\nimport type { FlowChatRecord, FlowStore, PendingFlowChange, SideTaskRecord } from \"./flow-store\";\nimport { appliedInTime, byVersionAndState, compareText, migratePending, pendingAdded } from \"./flow-store-shared\";\nimport { FLOWS_MIGRATIONS_TABLE } from \"./migration-settings\";\nimport { flowChats, flowClientCards, flowFirstMessages, flowPeople, flowSideTasks, flowVersions } from \"./sqlite-schema\";\n\n/** Хранилище в файле SQLite: `close` обязателен — файл закрывает приложение при остановке. */\nexport type SqliteFlowStore = FlowStore & Required<Pick<FlowStore, \"close\">>;\n\ntype ChatRow = typeof flowChats.$inferSelect;\n\nconst toMs = (iso: string) => Date.parse(iso);\nconst toIso = (ms: number) => new Date(ms).toISOString();\n\nfunction toRecord(row: ChatRow): FlowChatRecord {\n return {\n chatId: row.chatId,\n ownerId: row.ownerId,\n flowId: row.flowId,\n versionId: row.versionId,\n stateId: row.stateId,\n pending: row.pending,\n dueAt: row.dueAt === null ? null : toIso(row.dueAt),\n dueExitId: row.dueExitId,\n dueBranchHead: row.dueBranchHead,\n timeZone: row.timeZone,\n updatedAt: toIso(row.updatedAt),\n endedAt: row.endedAt === null ? null : toIso(row.endedAt),\n endReason: row.endReason,\n reopenedAt: row.reopenedAt === null ? null : toIso(row.reopenedAt),\n pausedUntil: row.pausedUntil === null ? null : toIso(row.pausedUntil),\n windowClosed: row.windowClosed,\n dayClosedOn: row.dayClosedOn,\n delivery: row.delivery,\n placement: row.placement,\n };\n}\n\ntype SideTaskRow = typeof flowSideTasks.$inferSelect;\nconst toSideTask = (row: SideTaskRow): SideTaskRecord => ({\n taskId: row.taskId,\n chatId: row.chatId === \"\" ? null : row.chatId,\n asks: row.asks,\n doneAt: row.doneAt === null ? null : toIso(row.doneAt),\n});\n\n/** Дописать изменение в конец `pending`, если такого ключа там ещё нет. Одно выражение — одна запись базы. */\nconst pendingWith = (change: PendingFlowChange): SQL =>\n sql`case when exists (select 1 from json_each(${flowChats.pending}) where json_extract(value, '$.key') = ${change.key})\n then ${flowChats.pending} else json_insert(${flowChats.pending}, '$[#]', json(${JSON.stringify(change)})) end`;\n\n/** Состояние по карте перевода: удалённые — в выбранные, остальные как были. */\nfunction mappedStateSql(map: StateMap): SQL {\n const entries = Object.entries(map);\n if (entries.length === 0) return sql`${flowChats.stateId}`;\n const cases = entries.map(([from, to]) => sql`when ${from} then ${to}`);\n return sql`case ${flowChats.stateId} ${sql.join(cases, sql` `)} else ${flowChats.stateId} end`;\n}\n\n/** Таймер наступил, никто его не держит, разговор идёт и не на паузе. Аренда до `lease_until` включительно ещё живая. */\nconst dueAndFree = (now: number) =>\n and(\n lte(flowChats.dueAt, now),\n or(isNull(flowChats.leaseUntil), lt(flowChats.leaseUntil, now)),\n isNull(flowChats.endedAt),\n or(isNull(flowChats.pausedUntil), lte(flowChats.pausedUntil, now)),\n );\n\n/**\n * Сценарии в файле SQLite. Схема приводится к последней миграции при открытии; файл может быть общим с хранилищем\n * чатов. Несколько копий сервера на одном файле делят таймеры арендой: каждое изменение — одно условное `update`\n * или короткая транзакция с захватом записи (`immediate`).\n */\nexport function createSqliteFlowStore(filePath: string, migrationsFolder: string): SqliteFlowStore {\n const sqlite = new Database(filePath);\n sqlite.pragma(\"journal_mode = WAL\");\n sqlite.pragma(\"busy_timeout = 5000\");\n const db = drizzle(sqlite);\n migrate(db, { migrationsFolder, migrationsTable: FLOWS_MIGRATIONS_TABLE });\n\n const getChat = (chatId: string) => db.select().from(flowChats).where(eq(flowChats.chatId, chatId)).get();\n\n return {\n async pinChat(input) {\n db.insert(flowChats)\n .values({\n chatId: input.chatId,\n ownerId: input.ownerId,\n flowId: input.flowId,\n versionId: input.versionId,\n stateId: input.stateId,\n pending: [],\n timeZone: input.timeZone,\n ...(input.placement && { placement: input.placement }),\n updatedAt: toMs(input.at),\n })\n .onConflictDoNothing()\n .run();\n return toRecord(getChat(input.chatId)!);\n },\n async getChat(chatId) {\n const row = getChat(chatId);\n return row ? toRecord(row) : null;\n },\n async updateChat(chatId, patch, at) {\n const row = db\n .update(flowChats)\n .set({ versionId: patch.versionId, stateId: patch.stateId, timeZone: patch.timeZone, placement: patch.placement, updatedAt: toMs(at) })\n .where(eq(flowChats.chatId, chatId))\n .returning()\n .get();\n return row ? toRecord(row) : null;\n },\n async deleteChat(chatId) {\n db.delete(flowChats).where(eq(flowChats.chatId, chatId)).run();\n },\n\n async addPending(chatId, change) {\n // Прочитать и дописать под захватом записи: две копии с одним ключом не дают двух записей.\n const add = sqlite.transaction(() => {\n const row = db.select({ pending: flowChats.pending }).from(flowChats).where(eq(flowChats.chatId, chatId)).get();\n const next = row && pendingAdded(row.pending, change);\n if (next) db.update(flowChats).set({ pending: next }).where(eq(flowChats.chatId, chatId)).run();\n });\n add.immediate();\n },\n async takePending(chatId) {\n // Прочитать и очистить под захватом записи: вторая копия ждёт и видит уже пустой список (кроме применённых в срок).\n const take = sqlite.transaction(() => {\n const row = db.select({ pending: flowChats.pending }).from(flowChats).where(eq(flowChats.chatId, chatId)).get();\n if (!row || row.pending.length === 0) return [];\n db.update(flowChats).set({ pending: row.pending.filter(appliedInTime) }).where(eq(flowChats.chatId, chatId)).run();\n return row.pending;\n });\n return take.immediate();\n },\n\n async setDue(chatId, due, options) {\n db.update(flowChats)\n .set({\n dueAt: due ? toMs(due.at) : null,\n dueExitId: due?.exitId ?? null,\n dueBranchHead: due?.branchHead ?? null,\n leaseUntil: null,\n leaseOwner: null,\n })\n .where(and(eq(flowChats.chatId, chatId), options?.unlessDelivery ? isNull(flowChats.delivery) : undefined))\n .run();\n },\n async leaseDue({ now, owner, leaseMs, limit }) {\n const nowMs = toMs(now);\n // Выбор и аренда — одно `update`: из двух копий, пришедших одновременно, строку получит только первая.\n const due = db\n .select({ chatId: flowChats.chatId })\n .from(flowChats)\n .where(dueAndFree(nowMs))\n .orderBy(asc(flowChats.dueAt), asc(flowChats.chatId))\n .limit(limit);\n const rows = db\n .update(flowChats)\n .set({ leaseOwner: owner, leaseUntil: nowMs + leaseMs })\n .where(and(inArray(flowChats.chatId, due), dueAndFree(nowMs)))\n .returning()\n .all();\n return rows.map(toRecord).sort((left, right) => compareText(left.dueAt!, right.dueAt!) || compareText(left.chatId, right.chatId));\n },\n async releaseLease(chatId, owner) {\n db.update(flowChats)\n .set({ leaseOwner: null, leaseUntil: null })\n .where(and(eq(flowChats.chatId, chatId), eq(flowChats.leaseOwner, owner)))\n .run();\n },\n async renewLease({ chatId, owner, now, leaseMs }) {\n const renewed = db\n .update(flowChats)\n .set({ leaseUntil: toMs(now) + leaseMs })\n .where(and(eq(flowChats.chatId, chatId), eq(flowChats.leaseOwner, owner)))\n .returning({ chatId: flowChats.chatId })\n .all();\n return renewed.length > 0;\n },\n\n async saveVersion(input) {\n const inserted = db\n .insert(flowVersions)\n .values({ flowId: input.flowId, versionId: input.versionId, hash: input.hash, flow: input.flow, createdAt: toMs(input.at) })\n .onConflictDoNothing()\n .returning({ hash: flowVersions.hash })\n .all();\n if (inserted.length > 0) return \"new\";\n const saved = db\n .select({ hash: flowVersions.hash })\n .from(flowVersions)\n .where(and(eq(flowVersions.flowId, input.flowId), eq(flowVersions.versionId, input.versionId)))\n .get();\n return saved?.hash === input.hash ? \"same\" : \"conflict\";\n },\n async getVersion(flowId, versionId) {\n const row = db\n .select()\n .from(flowVersions)\n .where(and(eq(flowVersions.flowId, flowId), eq(flowVersions.versionId, versionId)))\n .get();\n return row ? { ...row, createdAt: toIso(row.createdAt) } : null;\n },\n\n async countChatsByState({ flowId }) {\n const rows = db\n .select({ versionId: flowChats.versionId, stateId: flowChats.stateId, chats: sql<number>`count(*)` })\n .from(flowChats)\n .where(eq(flowChats.flowId, flowId))\n .groupBy(flowChats.versionId, flowChats.stateId)\n .all();\n return rows.map((row) => ({ ...row, chats: Number(row.chats) })).sort(byVersionAndState);\n },\n async listFlowChats({ flowId, exceptVersionId, chatIds }) {\n if (chatIds?.length === 0) return [];\n const rows = db\n .select({\n chatId: flowChats.chatId,\n ownerId: flowChats.ownerId,\n flowId: flowChats.flowId,\n versionId: flowChats.versionId,\n stateId: flowChats.stateId,\n endedAt: flowChats.endedAt,\n endReason: flowChats.endReason,\n })\n .from(flowChats)\n .where(\n and(\n eq(flowChats.flowId, flowId),\n exceptVersionId === undefined ? undefined : ne(flowChats.versionId, exceptVersionId),\n chatIds === undefined ? undefined : inArray(flowChats.chatId, chatIds),\n ),\n )\n .orderBy(asc(flowChats.chatId))\n .all();\n return rows.map((row) => ({ ...row, endedAt: row.endedAt === null ? null : toIso(row.endedAt) }));\n },\n async migrateBatch(input) {\n const change = migratePending(input);\n if (input.chatIds?.length === 0) return { moved: [] };\n const notYetMoved = and(\n eq(flowChats.flowId, input.flowId),\n ne(flowChats.versionId, input.toVersionId),\n input.chatIds === undefined ? undefined : inArray(flowChats.chatId, input.chatIds),\n );\n const batch = db.select({ chatId: flowChats.chatId }).from(flowChats).where(notYetMoved).orderBy(asc(flowChats.chatId)).limit(input.limit);\n const moved = db\n .update(flowChats)\n .set({ versionId: input.toVersionId, stateId: mappedStateSql(input.map), pending: pendingWith(change), updatedAt: toMs(input.at) })\n .where(and(inArray(flowChats.chatId, batch), notYetMoved))\n .returning({ chatId: flowChats.chatId })\n .all();\n return { moved: moved.map((row) => row.chatId) };\n },\n\n async readCard(ownerId) {\n const rows = db.select().from(flowClientCards).where(eq(flowClientCards.ownerId, ownerId)).all();\n return rows\n .map((row) => ({ fieldId: row.fieldId, value: row.value, updatedAt: toIso(row.updatedAt), chatId: row.chatId, flowId: row.flowId }))\n .sort((left, right) => compareText(left.fieldId, right.fieldId));\n },\n async writeCardField(input) {\n const entry = { value: input.value, updatedAt: toMs(input.at), chatId: input.chatId, flowId: input.flowId };\n db.insert(flowClientCards)\n .values({ ownerId: input.ownerId, fieldId: input.fieldId, ...entry })\n .onConflictDoUpdate({ target: [flowClientCards.ownerId, flowClientCards.fieldId], set: entry })\n .run();\n },\n async forgetCard(ownerId, fieldId) {\n db.delete(flowClientCards)\n .where(and(eq(flowClientCards.ownerId, ownerId), fieldId === \"all\" ? undefined : eq(flowClientCards.fieldId, fieldId)))\n .run();\n },\n\n async endChat(chatId, { reason, at }) {\n db.update(flowChats)\n .set({ endedAt: toMs(at), endReason: reason, dueAt: null, dueExitId: null, dueBranchHead: null, leaseUntil: null, leaseOwner: null })\n .where(and(eq(flowChats.chatId, chatId), isNull(flowChats.endedAt)))\n .run();\n const row = getChat(chatId);\n return row ? toRecord(row) : null;\n },\n async reopenChat(chatId, { endedAt, at }) {\n const row = db\n .update(flowChats)\n .set({ endedAt: null, endReason: null, reopenedAt: toMs(at) })\n .where(and(eq(flowChats.chatId, chatId), eq(flowChats.endedAt, toMs(endedAt))))\n .returning()\n .get();\n return row ? toRecord(row) : null;\n },\n async setPause(chatId, until) {\n const row = db\n .update(flowChats)\n .set({ pausedUntil: until === null ? null : toMs(until) })\n .where(eq(flowChats.chatId, chatId))\n .returning()\n .get();\n return row ? toRecord(row) : null;\n },\n async setWindowClosed(chatId, closed) {\n db.update(flowChats).set({ windowClosed: closed }).where(eq(flowChats.chatId, chatId)).run();\n },\n async setDayClosed(chatId, day) {\n db.update(flowChats).set({ dayClosedOn: day }).where(eq(flowChats.chatId, chatId)).run();\n },\n async setDelivery(chatId, delivery) {\n const clean = delivery === null ? null : { ...delivery, retryAt: toIso(toMs(delivery.retryAt)) };\n db.update(flowChats).set({ delivery: clean }).where(eq(flowChats.chatId, chatId)).run();\n },\n\n async readTimeZone(ownerId) {\n return db.select({ timeZone: flowPeople.timeZone }).from(flowPeople).where(eq(flowPeople.ownerId, ownerId)).get()?.timeZone ?? null;\n },\n async writeTimeZone(ownerId, timeZone, at) {\n db.insert(flowPeople)\n .values({ ownerId, timeZone, updatedAt: toMs(at) })\n .onConflictDoUpdate({ target: flowPeople.ownerId, set: { timeZone, updatedAt: toMs(at) } })\n .run();\n },\n\n async readSideTasks(ownerId, chatId) {\n const rows = db\n .select()\n .from(flowSideTasks)\n .where(and(eq(flowSideTasks.ownerId, ownerId), or(eq(flowSideTasks.chatId, \"\"), eq(flowSideTasks.chatId, chatId))))\n .all();\n return rows.map(toSideTask).sort((left, right) => compareText(left.taskId, right.taskId));\n },\n async forgetSideTasks(ownerId) {\n db.delete(flowSideTasks).where(eq(flowSideTasks.ownerId, ownerId)).run();\n },\n async noteSideTask(input) {\n const asked = input.status === \"asked\";\n const doneAt = asked ? null : toMs(input.at);\n const row = db\n .insert(flowSideTasks)\n .values({ ownerId: input.ownerId, taskId: input.taskId, chatId: input.chatId ?? \"\", asks: asked ? 1 : 0, doneAt })\n .onConflictDoUpdate({\n target: [flowSideTasks.ownerId, flowSideTasks.taskId, flowSideTasks.chatId],\n set: asked ? { asks: sql`${flowSideTasks.asks} + 1` } : { doneAt: sql`coalesce(${flowSideTasks.doneAt}, ${doneAt})` },\n })\n .returning()\n .get();\n return toSideTask(row!);\n },\n\n async countFirstMessages(ownerId, day) {\n const row = db\n .select({ count: flowFirstMessages.count })\n .from(flowFirstMessages)\n .where(and(eq(flowFirstMessages.ownerId, ownerId), eq(flowFirstMessages.day, day)))\n .get();\n return row?.count ?? 0;\n },\n async addFirstMessage(ownerId, day) {\n const row = db\n .insert(flowFirstMessages)\n .values({ ownerId, day, count: 1 })\n .onConflictDoUpdate({ target: [flowFirstMessages.ownerId, flowFirstMessages.day], set: { count: sql`${flowFirstMessages.count} + 1` } })\n .returning({ count: flowFirstMessages.count })\n .get();\n return row!.count;\n },\n\n async close() {\n if (sqlite.open) sqlite.close();\n },\n };\n}\n","import type { AgentFlow } from \"../format/flow-format\";\nimport type { FlowEndReason } from \"../engine/flow-run\";\nimport type { FlowChatPlacement, FlowPendingDelivery, PendingFlowChange } from \"./flow-store\";\nimport { index, integer, primaryKey, sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n\n// Времена — миллисекунды с 1970 года, JSON — текстом.\n\n/** Чаты, которые идут по сценарию. */\nexport const flowChats = sqliteTable(\n \"flow_chats\",\n {\n chatId: text(\"chat_id\").primaryKey(),\n ownerId: text(\"owner_id\").notNull(),\n flowId: text(\"flow_id\").notNull(),\n versionId: text(\"version_id\").notNull(),\n stateId: text(\"state_id\").notNull(),\n pending: text(\"pending\", { mode: \"json\" }).$type<PendingFlowChange[]>().notNull().default([]),\n dueAt: integer(\"due_at\"),\n dueExitId: text(\"due_exit_id\"),\n dueBranchHead: text(\"due_branch_head\"),\n timeZone: text(\"time_zone\").notNull(),\n leaseUntil: integer(\"lease_until\"),\n leaseOwner: text(\"lease_owner\"),\n updatedAt: integer(\"updated_at\").notNull(),\n endedAt: integer(\"ended_at\"),\n endReason: text(\"end_reason\").$type<FlowEndReason>(),\n reopenedAt: integer(\"reopened_at\"),\n pausedUntil: integer(\"paused_until\"),\n windowClosed: integer(\"window_closed\", { mode: \"boolean\" }).notNull().default(false),\n dayClosedOn: text(\"day_closed_on\"),\n delivery: text(\"delivery\", { mode: \"json\" }).$type<FlowPendingDelivery>(),\n placement: text(\"placement\", { mode: \"json\" }).$type<FlowChatPlacement>(),\n },\n (table) => [\n index(\"flow_chats_due_idx\").on(table.dueAt),\n index(\"flow_chats_flow_state_idx\").on(table.flowId, table.versionId, table.stateId),\n index(\"flow_chats_owner_idx\").on(table.ownerId),\n ],\n);\n\n/** Сохранённые версии сценариев: чат идёт по своей версии, пока его не перевели. */\nexport const flowVersions = sqliteTable(\n \"flow_versions\",\n {\n flowId: text(\"flow_id\").notNull(),\n versionId: text(\"version_id\").notNull(),\n hash: text(\"hash\").notNull(),\n flow: text(\"flow\", { mode: \"json\" }).$type<AgentFlow>().notNull(),\n createdAt: integer(\"created_at\").notNull(),\n },\n (table) => [primaryKey({ columns: [table.flowId, table.versionId] })],\n);\n\n/** Карточка клиента: поле за полем, переживает чаты. */\nexport const flowClientCards = sqliteTable(\n \"flow_client_cards\",\n {\n ownerId: text(\"owner_id\").notNull(),\n fieldId: text(\"field_id\").notNull(),\n value: text(\"value\", { mode: \"json\" }).notNull(),\n updatedAt: integer(\"updated_at\").notNull(),\n chatId: text(\"chat_id\").notNull(),\n flowId: text(\"flow_id\").notNull(),\n },\n (table) => [primaryKey({ columns: [table.ownerId, table.fieldId] })],\n);\n\n/** Сколько сообщений агент написал первым за сутки человека. */\nexport const flowFirstMessages = sqliteTable(\n \"flow_first_messages\",\n {\n ownerId: text(\"owner_id\").notNull(),\n day: text(\"day\").notNull(),\n count: integer(\"count\").notNull().default(0),\n },\n (table) => [primaryKey({ columns: [table.ownerId, table.day] })],\n);\n\n/** Пояс человека: общий для всех его чатов. */\nexport const flowPeople = sqliteTable(\"flow_people\", {\n ownerId: text(\"owner_id\").primaryKey(),\n timeZone: text(\"time_zone\").notNull(),\n updatedAt: integer(\"updated_at\").notNull(),\n});\n\n/** Попытки попутных задач. `chat_id` пустой — попытки на человека, иначе — на разговор. */\nexport const flowSideTasks = sqliteTable(\n \"flow_side_tasks\",\n {\n ownerId: text(\"owner_id\").notNull(),\n taskId: text(\"task_id\").notNull(),\n chatId: text(\"chat_id\").notNull().default(\"\"),\n asks: integer(\"asks\").notNull().default(0),\n doneAt: integer(\"done_at\"),\n },\n (table) => [primaryKey({ columns: [table.ownerId, table.taskId, table.chatId] })],\n);\n"],"mappings":"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","names":[]}
@@ -13,8 +13,8 @@ export interface TransitionReason {
13
13
  }
14
14
  /** Почему разговор закончился: конечная нода, срок вышел, человек пропал, канал заблокирован, своя причина приложения (`flow.end`). */
15
15
  export type FlowEndReason = "final" | "deadline" | "abandoned" | "blocked" | (string & {});
16
- /** Как чат попал в состояние: старт сценария, переход или перевод на новую версию. */
17
- export type FlowPathCause = TransitionDecider | "start" | "migrate";
16
+ /** Как чат попал в состояние: старт сценария, переход, перевод на новую версию или открытие заново (`flow.reopen`, с 0.68). */
17
+ export type FlowPathCause = TransitionDecider | "start" | "migrate" | "reopen";
18
18
  export interface FlowPathEntry {
19
19
  state: string;
20
20
  /** Время входа, мс с 1970 года. */
@@ -79,6 +79,8 @@ export interface FlowRun {
79
79
  /** Когда разговор закончился. После конца таймеров и касаний нет. */
80
80
  endedAt?: number;
81
81
  endReason?: FlowEndReason;
82
+ /** Разговор открыт заново (`flow.reopen`): тишина, утреннее и «не завершил» считаются не раньше этого момента. С 0.68. */
83
+ reopenedAt?: number;
82
84
  }
83
85
  /** Ход «пишет первым» и что было до него. Ответ хода помечен «не доставлено» — счёт возвращается к этому. */
84
86
  export interface FlowTouch {
@@ -36,7 +36,7 @@ export interface NudgeDue {
36
36
  reason: FlowFirstReason | {
37
37
  kind: "give_up";
38
38
  };
39
- /** Касание своего ряда ноды на N дней (`nudges.days`): его шаг не идёт в счёт общего ряда. */
39
+ /** Касание своего ряда ноды, ограниченного днями (`nudges.days`, `nudges.untilProgramDay`): его шаг не идёт в счёт общего ряда. */
40
40
  own?: boolean;
41
41
  }
42
42
  /** Касания ноды: свои, «off», общие из «Дней и времени», общие сценария. Нет ни тех, ни других — `null`. */
@@ -9,6 +9,11 @@ export declare function appendPath(path: FlowPathEntry[], entry: FlowPathEntry):
9
9
  * запоминает, сколько там пробыли; «назад» продолжает отсчёт с этого места, обычный вход — с нуля.
10
10
  */
11
11
  export declare function applyTransition(flow: AgentFlow, run: FlowRun, transition: Transition): FlowRun;
12
+ /**
13
+ * Разговор открыт заново в `at` (`flow.reopen`): конца нет, нода та же, ряд касаний — с первого шага от `at`, счёт ходов
14
+ * сервера и переходов подряд — заново; в пути — запись «открыт заново». Дни программы — от исходного старта.
15
+ */
16
+ export declare function reopenRun<T extends FlowRun>(flow: AgentFlow, run: T, at: number): T;
12
17
  /**
13
18
  * Пришло сообщение человека: тишина прервана, счёт переходов подряд — заново, ряд касаний — с первого шага.
14
19
  * `dayKey` — местная дата дня сценария («ГГГГ-ММ-ДД»), только у сценариев с днями: по нему считаются рамки дня.
@@ -147,6 +147,11 @@ export interface FlowStateNudges extends FlowNudges {
147
147
  * (`schedule.nudges`, затем `nudges` сценария), их ряд с начала. Нет — всё время в ноде. С 0.55.
148
148
  */
149
149
  days?: number;
150
+ /**
151
+ * Свой ряд действует, пока идёт день программы не позже этого (1 — только первый день программы). Человек дошёл до
152
+ * ноды позже — своего ряда нет, сразу общие касания. Вместе с `days` — что наступит раньше. С 0.67.
153
+ */
154
+ untilProgramDay?: number;
150
155
  }
151
156
  /** Порог позднего старта по умолчанию: до ночи меньше 3 ч — первым днём считается завтра. */
152
157
  export declare const DEFAULT_LATE_START_HOURS = 3;
@@ -169,6 +174,11 @@ export interface FlowSchedule {
169
174
  firstMessages?: {
170
175
  /** Нет — из настроек приложения. */
171
176
  maxPerDay?: number;
177
+ /**
178
+ * Потолок по дням программы: `[3, 2]` — в день 1 не больше трёх, со дня 2 — не больше двух (последнее значение — на
179
+ * все следующие дни). Главнее `maxPerDay`. Счёт идёт по дням программы, ответ человека его не обнуляет. С 0.67.
180
+ */
181
+ maxPerDayByDay?: number[];
172
182
  /** Писать в начале дня; нет — да, если задан `lengthDays`. */
173
183
  morning?: boolean;
174
184
  /** Утреннее пропускается, если человек писал за столько минут до него; нет — 180. */
@@ -178,6 +178,7 @@ export declare const agentFlowSchema: z.ZodPipe<z.ZodObject<{
178
178
  }>;
179
179
  }, z.core.$strict>>;
180
180
  days: z.ZodOptional<z.ZodNumber>;
181
+ untilProgramDay: z.ZodOptional<z.ZodNumber>;
181
182
  }, z.core.$strict>, z.ZodLiteral<"off">]>>;
182
183
  }, z.core.$strict>>;
183
184
  layout: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
@@ -194,6 +195,7 @@ export declare const agentFlowSchema: z.ZodPipe<z.ZodObject<{
194
195
  farewellText: z.ZodOptional<z.ZodString>;
195
196
  firstMessages: z.ZodOptional<z.ZodObject<{
196
197
  maxPerDay: z.ZodOptional<z.ZodInt>;
198
+ maxPerDayByDay: z.ZodOptional<z.ZodArray<z.ZodInt>>;
197
199
  morning: z.ZodOptional<z.ZodBoolean>;
198
200
  skipMorningIfHumanWithinMin: z.ZodOptional<z.ZodInt>;
199
201
  }, z.core.$strict>>;
@@ -371,6 +373,7 @@ export declare const agentFlowSchema: z.ZodPipe<z.ZodObject<{
371
373
  unit: "d" | "h" | "min";
372
374
  } | undefined;
373
375
  days?: number | undefined;
376
+ untilProgramDay?: number | undefined;
374
377
  } | undefined;
375
378
  }[];
376
379
  layout: Record<string, {
@@ -387,6 +390,7 @@ export declare const agentFlowSchema: z.ZodPipe<z.ZodObject<{
387
390
  farewellText?: string | undefined;
388
391
  firstMessages?: {
389
392
  maxPerDay?: number | undefined;
393
+ maxPerDayByDay?: number[] | undefined;
390
394
  morning?: boolean | undefined;
391
395
  skipMorningIfHumanWithinMin?: number | undefined;
392
396
  } | undefined;
@@ -546,6 +550,7 @@ export declare const agentFlowSchema: z.ZodPipe<z.ZodObject<{
546
550
  unit: "d" | "h" | "min";
547
551
  } | undefined;
548
552
  days?: number | undefined;
553
+ untilProgramDay?: number | undefined;
549
554
  } | undefined;
550
555
  }[];
551
556
  layout: Record<string, {
@@ -562,6 +567,7 @@ export declare const agentFlowSchema: z.ZodPipe<z.ZodObject<{
562
567
  farewellText?: string | undefined;
563
568
  firstMessages?: {
564
569
  maxPerDay?: number | undefined;
570
+ maxPerDayByDay?: number[] | undefined;
565
571
  morning?: boolean | undefined;
566
572
  skipMorningIfHumanWithinMin?: number | undefined;
567
573
  } | undefined;
@@ -58,6 +58,7 @@ export interface FlowEditorLabels {
58
58
  noNudges: string;
59
59
  /** Срок своих касаний на карточке: «в день входа», «первые 2 дн.». */
60
60
  nudgesDays: (days: number) => string;
61
+ nudgesUntilProgramDay: (day: number) => string;
61
62
  /** Кнопки места на доске: легенда, мини-карта, панель «Сценарий», полный экран. */
62
63
  space: {
63
64
  legend: string;
@@ -106,6 +107,8 @@ export interface FlowEditorLabels {
106
107
  firstMessages: string;
107
108
  maxPerDay: string;
108
109
  maxPerDayHint: string;
110
+ maxPerDayByDay: string;
111
+ maxPerDayByDayHint: string;
109
112
  morning: string;
110
113
  skipMorning: string;
111
114
  nudges: string;
@@ -172,8 +175,9 @@ export interface FlowEditorLabels {
172
175
  nudgesHint: string;
173
176
  /** Сколько действуют свои касания: всё время в ноде или N дней с дня входа. */
174
177
  nudgesDays: string;
175
- nudgesDaysModes: Record<"always" | "days", string>;
178
+ nudgesDaysModes: Record<"always" | "days" | "untilProgramDay", string>;
176
179
  nudgesDaysHint: string;
180
+ nudgesUntilProgramDayHint: string;
177
181
  };
178
182
  exitCard: {
179
183
  when: string;
@@ -2,6 +2,6 @@ export { FlowEditor, type FlowEditorProps } from "./flow-editor.js";
2
2
  export { useFlowEditor, type FlowDialog, type FlowEditorController, type FlowMigrateController, type FlowMigrateRow, type FlowMigration, type FlowSaveResult, type FlowSelection, type UseFlowEditorOptions, } from "./use-flow-editor.js";
3
3
  export { conditionText } from "./exit-text.js";
4
4
  export { chooseExit, chooseNothing, nextQuestionField, setKnownValue, simulationChoices, startSimulation, stepBack, type Simulation, type SimulationChoice, type SimulationStep, } from "./flow-simulation.js";
5
- export { DEFAULT_NUDGES, DEFAULT_SCHEDULE } from "./schedule-model.js";
5
+ export { capsByDayText, DEFAULT_NUDGES, DEFAULT_SCHEDULE, parseCapsByDay } from "./schedule-model.js";
6
6
  export type { FlowBoardSpaceProps, FlowHeaderMode, FlowInspectorMode, FlowLegendMode, FlowMinimapMode } from "./board-space.js";
7
7
  export { sameFlow } from "./editor-model.js";
@@ -33,12 +33,20 @@ export type NudgesMode = "inherit" | "own" | "off";
33
33
  export declare function nudgesMode(state: FlowState): NudgesMode;
34
34
  /** Касания ноды: как у сценария (поля нет), свои (образец — общие или 15 мин · 1 ч · 4 ч), выкл. */
35
35
  export declare function setStateNudges(flow: AgentFlow, stateId: string, mode: NudgesMode): AgentFlow;
36
- /** Своё расписание касаний ноды: пустые части убираются, срок в днях остаётся. */
36
+ /** Своё расписание касаний ноды: пустые части убираются, сроки в днях остаются. */
37
37
  export declare function updateStateNudges(flow: AgentFlow, stateId: string, nudges: FlowNudges): AgentFlow;
38
38
  /** Сколько дней действуют свои касания ноды, считая день входа; `null` — всё время в ноде. */
39
39
  export declare function setStateNudgesDays(flow: AgentFlow, stateId: string, days: number | null): AgentFlow;
40
40
  /** Срок своих касаний ноды в днях; всё время в ноде или касания не свои — `null`. */
41
41
  export declare function stateNudgesDays(state: FlowState): number | null;
42
+ /** Свои касания ноды действуют до этого дня программы включительно; `null` — без такого предела. С 0.67. */
43
+ export declare function setStateNudgesUntilProgramDay(flow: AgentFlow, stateId: string, day: number | null): AgentFlow;
44
+ /** До какого дня программы действуют свои касания ноды; предела нет или касания не свои — `null`. */
45
+ export declare function stateNudgesUntilProgramDay(state: FlowState): number | null;
46
+ /** Потолок «пишет первым» по дням программы текстом: `[3, 2]` → «3, 2». */
47
+ export declare function capsByDayText(caps: number[] | undefined): string;
48
+ /** «3, 2» → `[3, 2]`; пусто — `undefined` (потолок без дней); не числа — `null`. */
49
+ export declare function parseCapsByDay(text: string): number[] | undefined | null;
42
50
  /** Касания по умолчанию в «Днях и времени»; `null` — без общих касаний. */
43
51
  export declare function updateScheduleNudges(flow: AgentFlow, nudges: FlowNudges | null): AgentFlow;
44
52
  /** «15 мин», «4 ч», «3 дн.». */
@@ -203,6 +203,9 @@ export interface FlowStateTimingProps {
203
203
  /** Свои касания действуют столько дней, считая день входа; `null` — всё время в ноде. С 0.55. */
204
204
  nudgesDays: number | null;
205
205
  onNudgesDaysChange(days: number | null): void;
206
+ /** Свои касания действуют до этого дня программы включительно; `null` — без такого предела. С 0.67. */
207
+ nudgesUntilProgramDay: number | null;
208
+ onNudgesUntilProgramDayChange(day: number | null): void;
206
209
  }
207
210
  /** Цель перехода в списке «Куда»: состояние или «назад». */
208
211
  export interface FlowTargetOption {
@@ -11,6 +11,8 @@ export type FlowChatWriteState = Pick<FlowChatRecord, "dayClosedOn" | "pausedUnt
11
11
  export declare function chatWriteBlock(chat: FlowChatWriteState, at: number, timeZone: string, schedule: FlowSchedule | undefined): {
12
12
  retryAt: number | null;
13
13
  } | null;
14
+ /** Потолок дня программы из `maxPerDayByDay`: вечер до первого дня — как день 1, после списка — его последнее значение. */
15
+ export declare function capOfProgramDay(byDay: number[] | undefined, programDay: number | undefined): number | undefined;
14
16
  /** Правила «писать первым» на хранилище счётчиков; `false` в настройках — `null`, агент пишет первым без ограничений. */
15
17
  export declare function createFirstMessageRules(setting: FirstMessagesSetting | undefined, store: FlowStore): FirstMessageRules | null;
16
18
  /** Пояс чата ещё не известен: страница его не присылала. */
@@ -50,6 +50,11 @@ export interface FlowRuntime {
50
50
  export declare function reportTransition(handler: ((event: FlowTransitionEvent) => void | Promise<void>) | undefined, event: FlowTransitionEvent, logError: (message: string) => void): Promise<void>;
51
51
  /** `FlowRun` из состояния чата; нет или не то — `null`. */
52
52
  export declare function storedRun(value: unknown): ChatFlowRun | null;
53
+ /**
54
+ * Разговор открыт заново (`flow.reopen`), а место в состоянии чата ещё «до»: оно догоняет отметку записи — конец снят,
55
+ * ряд касаний с первого шага от открытия. Уже догнало, не открывали или снова закончен — как есть.
56
+ */
57
+ export declare function withReopen<T extends FlowRun>(flow: AgentFlow, run: T, record: Partial<Pick<FlowChatRecord, "reopenedAt" | "endedAt">> | null | undefined): T;
53
58
  /** Где чат сейчас: из состояния ветки, а на пустой ветке — старт сценария. */
54
59
  export declare function currentRun(flow: AgentFlow, values: Record<string, unknown> | undefined, now: number): ChatFlowRun;
55
60
  /**
@@ -80,7 +85,7 @@ export declare function recordPlacement(record: Pick<FlowChatRecord, "stateId" |
80
85
  * Где чат сейчас вне хода: из состояния ветки; до первого хода перенесённого чата — из записи переноса; иначе старт.
81
86
  * Так таймеры, `admit` и `readRun` видят перенесённое место, не дожидаясь хода человека.
82
87
  */
83
- export declare function chatRun(flow: AgentFlow, values: Record<string, unknown> | undefined, record: Pick<FlowChatRecord, "stateId" | "placement"> | null | undefined, now: number, timeZone?: string): ChatFlowRun;
88
+ export declare function chatRun(flow: AgentFlow, values: Record<string, unknown> | undefined, record: (Pick<FlowChatRecord, "stateId" | "placement"> & Partial<Pick<FlowChatRecord, "reopenedAt" | "endedAt">>) | null | undefined, now: number, timeZone?: string): ChatFlowRun;
84
89
  /**
85
90
  * Пояс человека для шага движка вне подготовки хода, как в `runOutsideTurn`: пояса модуля, без них — из записи чата;
86
91
  * неизвестен — `defaultTimeZone`, иначе сервера.
@@ -58,6 +58,47 @@ export interface FlowEndInput {
58
58
  /** Итог словами приложения: придёт в `onFinish.outcome`. */
59
59
  outcome?: string;
60
60
  }
61
+ /** Открыть законченный разговор заново (`flow.reopen`). С 0.68. */
62
+ export interface FlowReopenInput {
63
+ ownerId: string;
64
+ chatId: string;
65
+ /** Почему открыт, словами приложения: придёт в `onReopen.reason`. */
66
+ reason?: string;
67
+ /** Открыть, только если разговор закончился не раньше этого момента; раньше — `ended_too_long_ago`. Политика давности — приложения. */
68
+ endedAfter?: Date;
69
+ }
70
+ /**
71
+ * Почему разговор не открыт: не по сценарию (`not_flow`), не закончен или уже открыт другим вызовом (`not_ended`), конец,
72
+ * который не открывается (`final`, `deadline`, `blocked`), срок программы уже вышел (`deadline_passed`), закончился раньше
73
+ * `endedAfter` (`ended_too_long_ago`).
74
+ */
75
+ export type FlowReopenRefusal = "not_flow" | "not_ended" | "final" | "deadline" | "blocked" | "deadline_passed" | "ended_too_long_ago";
76
+ export type FlowReopenResult = {
77
+ reopened: true;
78
+ } | {
79
+ reopened: false;
80
+ reason: FlowReopenRefusal;
81
+ };
82
+ /** Разговор открыт заново: приложение возвращает статус «идёт». */
83
+ export interface FlowReopen {
84
+ ownerId: string;
85
+ chatId: string;
86
+ flowId: string;
87
+ versionId: string;
88
+ /** Чем разговор кончился: `abandoned` или своя причина приложения. */
89
+ endReason: FlowEndReason;
90
+ endedAt: Date;
91
+ /** Причина открытия из `flow.reopen`. */
92
+ reason?: string;
93
+ /** День программы сейчас, от исходного старта; без «Дней и времени» — `null`. */
94
+ day: number | null;
95
+ /** Нода, где разговор продолжается: та, где кончился. */
96
+ state: string;
97
+ tags: string[];
98
+ /** Место после открытия: путь с записью «открыт заново», заметки, счётчики. */
99
+ run: FlowRun;
100
+ at: Date;
101
+ }
61
102
  /** Перенести идущий чат сразу, без хода: сценарий и место — как у `loadFlow`. */
62
103
  export type FlowAdoptInput = LoadedFlow & {
63
104
  ownerId: string;
@@ -214,6 +255,8 @@ export interface FirstMessageRules {
214
255
  timeZone: string;
215
256
  /** «Дни и время» сценария: его ночь и потолок главнее настроек приложения, закрытый день — до следующего. */
216
257
  schedule?: FlowSchedule;
258
+ /** День программы этого чата (как `flowDay`): по нему потолок `maxPerDayByDay`. */
259
+ programDay?: number;
217
260
  /** Запись чата: окно канала, пауза сотрудника, закрытый день — запрет до правил приложения. */
218
261
  chat?: Pick<FlowChatRecord, "dayClosedOn" | "pausedUntil" | "windowClosed">;
219
262
  }): Promise<{
@@ -550,6 +593,8 @@ export interface AgentFlowOptions {
550
593
  onTransition?: (event: FlowTransitionEvent) => void | Promise<void>;
551
594
  /** Разговор закончился (`final`, `deadline`, `abandoned`, `blocked`). Ошибка приложения не ломает сценарий. */
552
595
  onFinish?: (event: FlowFinish) => void | Promise<void>;
596
+ /** Разговор открыт заново (`flow.reopen`). Ошибка приложения не ломает сценарий. С 0.68. */
597
+ onReopen?: (event: FlowReopen) => void | Promise<void>;
553
598
  /** Карточка клиента в хранилище приложения. Нет — таблица модуля. */
554
599
  clientCard?: ClientCardStore;
555
600
  /**
@@ -603,6 +648,13 @@ export interface AgentFlowHandle {
603
648
  end(input: FlowEndInput): Promise<{
604
649
  ended: boolean;
605
650
  }>;
651
+ /**
652
+ * Открыть законченный разговор заново: «не завершил» (`abandoned`) или своя причина приложения (`flow.end`). Нода,
653
+ * история, память и `chatId` те же; ряд касаний — с первого шага от открытия; дни и срок программы — от исходного
654
+ * старта. Событие `onReopen`, запись «reopen» в пути, сроки поставлены. Не открывается — `reopened: false` и причина,
655
+ * ничего не меняется. Повтор и гонка копий безопасны: открывает один вызов. С 0.68.
656
+ */
657
+ reopen(input: FlowReopenInput): Promise<FlowReopenResult>;
606
658
  /** Сообщение человека в канале приложения: отвечать, ответить готовой фразой, промолчать. Звать до хода. */
607
659
  admit(input: FlowAdmitInput): Promise<FlowAdmitDecision>;
608
660
  /** Отвечает живой сотрудник: агент молчит, таймеры и касания стоят. */
@@ -45,6 +45,8 @@ export interface FlowChatRecord {
45
45
  /** Разговор закончился: таймеров и касаний больше нет. */
46
46
  endedAt: string | null;
47
47
  endReason: FlowEndReason | null;
48
+ /** Разговор открыт заново (`flow.reopen`) в этот момент, по часам чата; место догоняет отметку на ближайшем чтении. С 0.68. */
49
+ reopenedAt: string | null;
48
50
  /** Пауза «отвечает живой сотрудник» до этого момента; таймеры не берутся. */
49
51
  pausedUntil: string | null;
50
52
  /** Канал приложения закрыл окно: первым не писать до сообщения человека. */
@@ -234,6 +236,14 @@ export interface FlowStore {
234
236
  reason: FlowEndReason;
235
237
  at: string;
236
238
  }): Promise<FlowChatRecord | null>;
239
+ /**
240
+ * Открыть законченный разговор заново: только если он всё ещё закончен в `endedAt` (что прочитал вызывающий). Снимает
241
+ * конец, ставит `reopenedAt`. Не тот конец, не закончен или чата нет — `null`: из двух копий открывает одна. С 0.68.
242
+ */
243
+ reopenChat(chatId: string, input: {
244
+ endedAt: string;
245
+ at: string;
246
+ }): Promise<FlowChatRecord | null>;
237
247
  /** `null` — снять паузу. */
238
248
  setPause(chatId: string, until: string | null): Promise<FlowChatRecord | null>;
239
249
  setWindowClosed(chatId: string, closed: boolean): Promise<void>;
@@ -265,6 +265,23 @@ export declare const flowChats: import("drizzle-orm/pg-core").PgTableWithColumns
265
265
  }, {}, {
266
266
  $type: FlowEndReason;
267
267
  }>;
268
+ reopenedAt: import("drizzle-orm/pg-core").PgColumn<{
269
+ name: string;
270
+ tableName: "flow_chats";
271
+ dataType: "date";
272
+ columnType: "PgTimestamp";
273
+ data: Date;
274
+ driverParam: string;
275
+ notNull: false;
276
+ hasDefault: false;
277
+ isPrimaryKey: false;
278
+ isAutoincrement: false;
279
+ hasRuntimeDefault: false;
280
+ enumValues: undefined;
281
+ baseColumn: never;
282
+ identity: undefined;
283
+ generated: undefined;
284
+ }, {}, {}>;
268
285
  pausedUntil: import("drizzle-orm/pg-core").PgColumn<{
269
286
  name: string;
270
287
  tableName: "flow_chats";
@@ -284,6 +284,23 @@ export declare const flowChats: import("drizzle-orm/sqlite-core").SQLiteTableWit
284
284
  length: number | undefined;
285
285
  $type: FlowEndReason;
286
286
  }>;
287
+ reopenedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
288
+ name: "reopened_at";
289
+ tableName: "flow_chats";
290
+ dataType: "number";
291
+ columnType: "SQLiteInteger";
292
+ data: number;
293
+ driverParam: number;
294
+ notNull: false;
295
+ hasDefault: false;
296
+ isPrimaryKey: false;
297
+ isAutoincrement: false;
298
+ hasRuntimeDefault: false;
299
+ enumValues: undefined;
300
+ baseColumn: never;
301
+ identity: undefined;
302
+ generated: undefined;
303
+ }, {}, {}>;
287
304
  pausedUntil: import("drizzle-orm/sqlite-core").SQLiteColumn<{
288
305
  name: "paused_until";
289
306
  tableName: "flow_chats";
@@ -0,0 +1 @@
1
+ ALTER TABLE "agent_chat"."flow_chats" ADD COLUMN "reopened_at" timestamp (3) with time zone;