@ykurilov/agent-chat-core 0.80.1 → 0.81.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,84 @@
1
1
  # @ykurilov/agent-chat-core
2
2
 
3
+ ## 0.81.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 58204d2: **Ломает приложения со своим хранилищем мессенджеров или сценариев.** Хранилища из пакетов (память, SQLite,
8
+ Postgres) уже готовы — если вы передаёте их, менять ничего не нужно.
9
+
10
+ - `MessengerStore`: новый обязательный метод `saveLinkIfFree` и признак `guarded: true`. Привязка аккаунта по коду
11
+ идёт только через `saveLinkIfFree`: два кода разных людей на один аккаунт мессенджера больше не переписывают друг
12
+ друга.
13
+ - `FlowStore`: новый обязательный метод `swapDelivery` и признак `guarded: true`.
14
+ - Без `guarded: true` `createMessengers` и `createAgentFlow` сразу бросают `TypeError`. Признак — обещание, что
15
+ хранилище выполняет условия держателя и аренды (список — в шаге 3 ниже).
16
+
17
+ Новое для своей доставки сценария (`attach({ deliver })`):
18
+
19
+ - Исход `{ status: "await_repair" }` — канал сломан и ждёт починки (например, отозван токен бота). Ответ ждёт,
20
+ попытка не тратится, новых ходов и касаний нет. Починили — `flow.resumeDelivery({ ownerId, chatId, messageId })`:
21
+ тот же `messageId`, текст и части уходят ближайшим обходом таймеров. Ответ: `resumed`, `pending`, `busy`,
22
+ `not_found` или `dropped` (человек уже написал после ответа — ответ снят и помечен недоставленным). Ожидание
23
+ переживает перезапуск. В `reportDelivery` этот исход — `TypeError`.
24
+ - Пока идёт `deliver`, аренда чата продлевается: вторая копия сервера ту же попытку не начинает. Копия упала посреди
25
+ отправки — та же попытка повторяется через её паузу. Итог устаревшей копии не пишется.
26
+ - Перед самым `deliver`, уже под резервом, SDK ещё раз сверяет: разговор не закончен после хода (`flow.end`), ответ
27
+ есть в ветке, после починки человек не написал после него. Иначе `deliver` не зовётся, ответ помечен недоставленным.
28
+ - Повтор доставки прощания у законченного разговора теперь доходит — без нового хода и без открытия разговора.
29
+ - «Ровно один раз» снаружи SDK не обещает: после падения копии тот же ответ может уйти ещё раз с тем же `messageId`.
30
+ Двойную отправку отсекает приложение по `messageId`.
31
+
32
+ Остальные исправления аудита работают без правок в приложении:
33
+
34
+ - Ход группы мессенджера остаётся групповым и тогда, когда агент пишет первым, когда человек правит сообщение
35
+ группы, шлёт поправку к ходу или нажимает кнопку-подсказку: без памяти и личных блоков владельца, только
36
+ разрешённые группе инструменты, с пределом сообщений участника.
37
+ - Удаление чата ждёт хода на любой копии сервера: идёт ход — `ChatBusyError`, чат цел.
38
+ - Не удалось отметить вложения сообщения прикреплёнными — ошибка доходит до вызывающего, сообщение не записывается,
39
+ место в счёте сообщений отдаётся назад; повтор того же сообщения проходит.
40
+ - Сценарий, который не сходится с подтверждениями приложения (`confirmations`, `exitGuards`), проверяется до записи
41
+ версии и закрепления чата: чат идёт без сценария, перевод чатов на такую версию — отказ.
42
+ - Копия сервера, у которой забрали аренду таймера сценария или обновления мессенджера, больше не шлёт касание и не
43
+ переписывает срок и итог другой копии.
44
+
45
+ ### Как обновить приложение
46
+
47
+ Нужно только если приложение передаёт в `createMessengers` или `createAgentFlow` своё хранилище (не
48
+ `createMemoryMessengerStore`, `createSqliteMessengerStore`, `createPostgresMessengerStore`, `memoryFlowStore`,
49
+ `sqliteFlowStore`, `postgresFlowStore`).
50
+
51
+ 1. Найдите свою реализацию `MessengerStore` (поиск по `MessengerStore` или `saveLink(`).
52
+ 2. Добавьте метод — одной атомарной операцией базы:
53
+
54
+ ```ts
55
+ async saveLinkIfFree(link) {
56
+ // Аккаунт свободен или уже за link.userId — записать (имя и время обновить) и вернуть "saved".
57
+ // Аккаунт за другим пользователем — ничего не менять и вернуть "taken".
58
+ }
59
+ ```
60
+
61
+ В SQL это `insert … on conflict (messenger, bot_id, external_user_id) do update set … where user_id = $userId
62
+ returning …`: строка вернулась — `"saved"`, нет — `"taken"`. Отдельные «найти, потом записать» не годятся: между
63
+ ними успевает второй код.
64
+ 3. Добавьте признак `guarded: true` в объект хранилища. Он обещает:
65
+ - `finishUpdate(key, status, now, holder)`, `renewUpdates` и `releaseUpdate` пишут, только пока обновление за этим
66
+ держателем; `acquireLease` не отдаёт чужую живую аренду;
67
+ - `saveLinkIfFree` — одна атомарная операция.
68
+ 4. Своё `FlowStore` (если есть): метод `swapDelivery(chatId, { expected, next, due, claim, freeAt, endedAt })` одной
69
+ транзакцией. Заменить запись доставки, только если сейчас там `expected` (совпали `messageId`, `attempt` и `token`;
70
+ `null` — записи нет). С `claim` — только если аренда чата свободна, уже у `claim.owner` или у `claim.from`, и взять
71
+ её до `now + leaseMs`. С `freeAt` — только если живой аренды нет. С `endedAt` — только если конец разговора всё
72
+ ещё этот (`null` — не закончен). `due` записать вместе с записью. Вернуть `true`,
73
+ если заменил этот вызов. Плюс `guarded: true`, который обещает:
74
+ - `setDue(…, { leaseOwner })` пишет, только пока аренда у этой копии; `{ unlessDelivery: true }` — только если
75
+ доставки не ждёт;
76
+ - `leaseDue` не берёт живую аренду, а законченные разговоры берёт только со сроком `FLOW_DELIVERY_EXIT`;
77
+ - `endChat` при ждущей доставке не снимает срок и аренду.
78
+ Образец — `postgresFlowStore` и `sqliteFlowStore` в `@ykurilov/agent-chat-flows`.
79
+ 5. Проверка: `pnpm typecheck` проходит. Без признака или без `saveLinkIfFree` запуск сразу бросает `TypeError` с
80
+ подсказкой, без `swapDelivery` — `attach({ deliver })`.
81
+
3
82
  ## 0.80.1
4
83
 
5
84
  ### Patch Changes
@@ -1700,9 +1700,13 @@ function channelOfMessage(message) {
1700
1700
  }
1701
1701
  var isNoticeMessage = (message) => message.parts.some((part) => part.type === AGENT_NOTICE_PART_TYPE);
1702
1702
  var isHumanMessage = (message) => message.role === "user" && !isNoticeMessage(message) && !isStateMessage(message);
1703
+ var hasChannelPart = (message) => message.parts.some((part) => part.type === CHANNEL_PART_TYPE);
1703
1704
  function turnChannelOf(branch) {
1704
- const human = branch.findLast(isHumanMessage);
1705
- return human ? channelOfMessage(human) : { channel: "web" };
1705
+ const source = branch.findLast((message) => isHumanMessage(message) || message.role === "user" && isNoticeMessage(message) && hasChannelPart(message));
1706
+ return source ? channelOfMessage(source) : { channel: "web" };
1707
+ }
1708
+ function inheritedGroupPart(scope) {
1709
+ return scope.group ? channelPart(scope.channel, scope.group) : void 0;
1706
1710
  }
1707
1711
  function turnOriginOf(branch) {
1708
1712
  const parent = branch.at(-2);
@@ -8639,7 +8643,8 @@ function shortOutput(value) {
8639
8643
  var MAX_SERVER_TURNS_IN_ROW = 5;
8640
8644
  function noticeMessageParts(input) {
8641
8645
  const data = { notices: input.notices, deliveryKey: input.deliveryKey, wakeAgent: input.wakeAgent };
8642
- return [{ type: AGENT_NOTICE_PART_TYPE, data }];
8646
+ const channel = channelPart(input.channel ?? "web", input.group);
8647
+ return [{ type: AGENT_NOTICE_PART_TYPE, data }, ...channel ? [channel] : []];
8643
8648
  }
8644
8649
  function noticeDataOf(message) {
8645
8650
  if (message.role !== "user") return void 0;
@@ -13188,9 +13193,9 @@ function createChatService(input) {
13188
13193
  throw error;
13189
13194
  }
13190
13195
  }
13191
- async function confirmFiles(messageId, fileParts) {
13196
+ async function confirmFiles(fileParts) {
13192
13197
  if (fileParts.length === 0) return;
13193
- await options2.attachments.confirmUserParts(fileParts).catch((error) => logError(`\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043C\u0435\u0442\u0438\u0442\u044C \u0432\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F ${logValue(messageId)}: ${String(error)}`));
13198
+ await options2.attachments.confirmUserParts(fileParts);
13194
13199
  }
13195
13200
  async function ownChat(userId, chatId) {
13196
13201
  const chat = await storage.getChat(chatId);
@@ -13907,7 +13912,7 @@ function createChatService(input) {
13907
13912
  // Окно тишины пачки считается от этого времени: по часам службы.
13908
13913
  ...burst && { createdAt: writtenAt.toISOString() }
13909
13914
  });
13910
- await confirmFiles(question3.id, fileParts);
13915
+ await confirmFiles(fileParts);
13911
13916
  await storage.insertMessage(question3);
13912
13917
  return question3;
13913
13918
  });
@@ -14143,14 +14148,20 @@ function createChatService(input) {
14143
14148
  return await writingCountedMessage(userId, chatId, async () => {
14144
14149
  const queued = await steering.accept(chatId, async (parentId) => {
14145
14150
  const fileParts = await acceptFiles(userId, chatId, files);
14151
+ const groupPart = inheritedGroupPart(turnChannelOf(await currentBranch(chatId)));
14146
14152
  const correction = newStoredMessage({
14147
14153
  id: message.id,
14148
14154
  chatId,
14149
14155
  parentId,
14150
14156
  role: "user",
14151
- parts: [...await filePartsWithNumbers(chatId, fileParts), ...text.trim().length > 0 ? [{ type: "text", text }] : [], steerPart()]
14157
+ parts: [
14158
+ ...await filePartsWithNumbers(chatId, fileParts),
14159
+ ...text.trim().length > 0 ? [{ type: "text", text }] : [],
14160
+ steerPart(),
14161
+ ...groupPart ? [groupPart] : []
14162
+ ]
14152
14163
  });
14153
- await confirmFiles(correction.id, fileParts);
14164
+ await confirmFiles(fileParts);
14154
14165
  await storage.insertMessage(correction);
14155
14166
  return correction;
14156
14167
  }, page);
@@ -14438,7 +14449,7 @@ function createChatService(input) {
14438
14449
  await compaction.cancel(chatId);
14439
14450
  await options2.attachments?.removeChatFiles?.(chatId);
14440
14451
  await storage.deleteChat(chatId);
14441
- }).finally(() => deleting.delete(chatId));
14452
+ }, { lease: "required" }).finally(() => deleting.delete(chatId));
14442
14453
  await options2.background?.chatDeleted(userId, chatId).catch((error) => logError(`\u0424\u043E\u043D: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0443\u0431\u0440\u0430\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0438 \u0443\u0434\u0430\u043B\u0451\u043D\u043D\u043E\u0433\u043E \u0447\u0430\u0442\u0430 ${chatId}: ${String(error)}`));
14443
14454
  await options2.limits?.chatDeleted?.({ userId, chatId }).catch((error) => logError(`\u041B\u0438\u043C\u0438\u0442\u044B: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u0442\u0435\u0440\u0435\u0442\u044C \u043E\u043A\u043D\u0430 \u0443\u0434\u0430\u043B\u0451\u043D\u043D\u043E\u0433\u043E \u0447\u0430\u0442\u0430 ${chatId}: ${String(error)}`));
14444
14455
  },
@@ -14620,11 +14631,13 @@ function createChatService(input) {
14620
14631
  )
14621
14632
  ] : [],
14622
14633
  // Правленое сообщение пачки остаётся в своей пачке: отметка с прежним временем записи.
14623
- ...edited2.parts.filter((part) => part.type === BURST_PART_TYPE)
14634
+ ...edited2.parts.filter((part) => part.type === BURST_PART_TYPE),
14635
+ // Правленое сообщение группы остаётся в границах группы: отметка — из записанного сообщения, не из запроса.
14636
+ ...[inheritedGroupPart(channelOfMessage(edited2))].filter((part) => part !== void 0)
14624
14637
  ],
14625
14638
  createdAt: nextSiblingTime(messages, selections, edited2.parentId)
14626
14639
  });
14627
- await confirmFiles(question3.id, fileParts);
14640
+ await confirmFiles(fileParts);
14628
14641
  await storage.insertMessage(question3);
14629
14642
  return { branch: branch2, index: index2, edited: edited2, question: question3 };
14630
14643
  });
@@ -15228,6 +15241,7 @@ export {
15228
15241
  THOUGHT_TITLE_METADATA_KEY,
15229
15242
  thoughtTitlesOfParts,
15230
15243
  CHANNEL_PART_TYPE,
15244
+ isHumanMessage,
15231
15245
  turnOriginOf,
15232
15246
  GREETING_LEAD,
15233
15247
  STEER_PART_TYPE,
@@ -15352,4 +15366,4 @@ export {
15352
15366
  InvalidWidgetAnswerError,
15353
15367
  createChatService
15354
15368
  };
15355
- //# sourceMappingURL=chunk-PORXDIWX.js.map
15369
+ //# sourceMappingURL=chunk-YGHGURNF.js.map