@ykurilov/agent-chat-react 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,15 @@
1
1
  # @ykurilov/agent-chat-react
2
2
 
3
+ ## 0.81.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 038b6e2: Исправления аудита клиентской зоны. `useAgentChat` без `key` при смене чата больше не переносит в новый чат дела прежнего: «Стоп», ответ на подтверждение или виджет, передачу поправок и сорвавшуюся голосовую замену; новый чат не остаётся «занятым», повторное открытие того же чата начинается с чистого состояния. Песочница не зовёт хуки приложения, которые пишут данные (`onStateChanged`, `onTurnFinished`, `onModelChosen`), ни в песочном чате, ни в копии чата человека; `newChatGreeting` и `modelMenu` решаются для настоящего админа. Вложения: удаление человеком и чистка брошенных больше не убирают файл, который отправляемое сообщение уже отметило; `confirmUserParts` отказывает `AttachmentRejectedError` (`attachment_not_found` — файла нет или отметка не записана, `attachment_unavailable` — сбой хранилища), а не отмечает молча.
8
+
9
+ ### Как обновить приложение
10
+
11
+ Обновите пакеты SDK одной версией. Со встроенными хранилищами вложений (`createDiskAttachmentStorage`, `createMemoryAttachmentStorage`) ничего менять не нужно. Своё хранилище (`AttachmentStorage`, например S3) должно получить метод `removeUnattached(id): Promise<boolean>` — удалить файл, только если он не прикреплён, атомарно с `markAttached` того же файла; `removeUnattachedOlderThan` должен следовать тому же правилу. Без метода `createAttachments` бросает ошибку при запуске. Проверьте хранилище обновлённым `describeAttachmentStorageContract`. Если песочница полагалась на `onStateChanged`, `onTurnFinished` или `onModelChosen` в песочных чатах — в песочнице они больше не зовутся. [Подробности](https://github.com/ykurilov/agent-chat-sdk/blob/main/docs/audit-client.md).
12
+
3
13
  ## 0.80.1
4
14
 
5
15
  No changes in this release.
@@ -1878,6 +1878,11 @@ function approvalOf(progress, controls) {
1878
1878
  // src/widgets/use-widget-answer.ts
1879
1879
  import { useCallback as useCallback2, useRef as useRef2, useState } from "react";
1880
1880
 
1881
+ // src/chat-lifetime.ts
1882
+ function ownedBy(owned, lifetime) {
1883
+ return owned !== null && owned.lifetime === lifetime && !lifetime.aborted ? owned.value : void 0;
1884
+ }
1885
+
1881
1886
  // src/widgets/widget-answer-keys.ts
1882
1887
  var CHOSEN_KEY = "\u0432\u044B\u0431\u0440\u0430\u043D\u043E";
1883
1888
  var RATING_KEY = "\u043E\u0446\u0435\u043D\u043A\u0430";
@@ -1943,14 +1948,15 @@ function hasPendingApproval(messages) {
1943
1948
  function useWidgetAnswer(deps) {
1944
1949
  const depsRef = useRef2(deps);
1945
1950
  depsRef.current = deps;
1946
- const answering = useRef2(false);
1951
+ const answering = useRef2(null);
1947
1952
  const [widgetPendingId, setWidgetPendingId] = useState(null);
1948
1953
  const [widgetFailure, setWidgetFailure] = useState(null);
1949
1954
  const answerWidget = useCallback2(async (toolCallId, answer, onAccepted) => {
1950
- if (answering.current) return false;
1951
- answering.current = true;
1952
1955
  const current = depsRef.current;
1953
1956
  const signal = current.chatSignal();
1957
+ if (ownedBy(answering.current, signal) !== void 0) return false;
1958
+ const pending = { lifetime: signal, value: toolCallId };
1959
+ answering.current = pending;
1954
1960
  const fail = (kind, text) => {
1955
1961
  if (!signal.aborted) setWidgetFailure({ id: toolCallId, kind, ...text !== void 0 && { text } });
1956
1962
  return false;
@@ -1970,6 +1976,7 @@ function useWidgetAnswer(deps) {
1970
1976
  return fail("connection");
1971
1977
  }
1972
1978
  const refusedByApp = response.ok ? null : await serverRefusalOf(response);
1979
+ if (signal.aborted) return false;
1973
1980
  if (refusedByApp?.kind) {
1974
1981
  current.announceAppRefusal(refusedByApp);
1975
1982
  return fail("refused", refusedByApp.text);
@@ -1993,8 +2000,9 @@ function useWidgetAnswer(deps) {
1993
2000
  }
1994
2001
  if (!response.ok) return fail("connection");
1995
2002
  const resumed = await resumedOf(response);
2003
+ if (signal.aborted) return true;
1996
2004
  current.setMessages((messages) => withWidgetAnswer(messages, toolCallId, answer, resumed));
1997
- if (!signal.aborted) onAccepted?.();
2005
+ onAccepted?.();
1998
2006
  if (!resumed) {
1999
2007
  current.expectLiveEventsOf(toolCallId);
2000
2008
  if (!signal.aborted) await current.rereadHistory(signal);
@@ -2010,12 +2018,16 @@ function useWidgetAnswer(deps) {
2010
2018
  }
2011
2019
  return true;
2012
2020
  } finally {
2013
- answering.current = false;
2021
+ if (answering.current === pending) answering.current = null;
2014
2022
  if (!signal.aborted) setWidgetPendingId(null);
2015
2023
  }
2016
2024
  }, []);
2017
- const answeringWidget = useCallback2(() => answering.current, []);
2018
- return { answerWidget, widgetPendingId, widgetFailure, answeringWidget };
2025
+ const answeringWidget = useCallback2(() => ownedBy(answering.current, depsRef.current.chatSignal()) !== void 0, []);
2026
+ const forgetChat = useCallback2(() => {
2027
+ setWidgetPendingId(null);
2028
+ setWidgetFailure(null);
2029
+ }, []);
2030
+ return { answerWidget, widgetPendingId, widgetFailure, answeringWidget, forgetChat };
2019
2031
  }
2020
2032
  async function resumedOf(response) {
2021
2033
  try {
@@ -2447,7 +2459,7 @@ function useAgentChat({
2447
2459
  const correctionsRef = useRef5(corrections);
2448
2460
  correctionsRef.current = corrections;
2449
2461
  const sendingCorrections = useRef5(/* @__PURE__ */ new Set());
2450
- const steerQueue = useRef5(Promise.resolve());
2462
+ const steerQueue = useRef5(null);
2451
2463
  const unconfirmedCorrection = useRef5(null);
2452
2464
  const [handingOff, setHandingOff] = useState3(false);
2453
2465
  const handOff = useRef5(null);
@@ -3039,7 +3051,7 @@ function useAgentChat({
3039
3051
  }, [chatId]);
3040
3052
  const [approvalPendingId, setApprovalPendingId] = useState3(null);
3041
3053
  const [approvalFailure, setApprovalFailure] = useState3(null);
3042
- const approving = useRef5(false);
3054
+ const approving = useRef5(null);
3043
3055
  const historyReads = useRef5({ started: 0, shown: 0 });
3044
3056
  const refreshFromServer = useCallback4(
3045
3057
  async (signal, options = {}) => {
@@ -3103,9 +3115,13 @@ function useAgentChat({
3103
3115
  useEffect3(() => setDetachedSince(null), [chatId]);
3104
3116
  const respondToApproval = useCallback4(
3105
3117
  async (approvalId, decision) => {
3106
- if (approving.current) return;
3107
- approving.current = true;
3108
3118
  const signal = chatLifetime.current.signal;
3119
+ if (ownedBy(approving.current, signal) !== void 0) return;
3120
+ const pending = { lifetime: signal, value: approvalId };
3121
+ approving.current = pending;
3122
+ const fail = (kind, text) => {
3123
+ if (!signal.aborted) setApprovalFailure({ id: approvalId, kind, ...text !== void 0 && { text } });
3124
+ };
3109
3125
  setApprovalFailure(null);
3110
3126
  setApprovalPendingId(approvalId);
3111
3127
  try {
@@ -3118,24 +3134,25 @@ function useAgentChat({
3118
3134
  { timeoutMs: settingsRef.current.requestTimeoutMs, wholeBody: true }
3119
3135
  );
3120
3136
  } catch {
3121
- setApprovalFailure({ id: approvalId, kind: "connection" });
3137
+ fail("connection");
3122
3138
  return;
3123
3139
  }
3124
3140
  const refusedByApp = response.ok ? null : await serverRefusalOf(response);
3141
+ if (signal.aborted) return;
3125
3142
  if (refusedByApp?.kind) {
3126
3143
  announceAppRefusal(refusedByApp);
3127
- setApprovalFailure({ id: approvalId, kind: "refused", text: refusedByApp.text });
3144
+ fail("refused", refusedByApp.text);
3128
3145
  return;
3129
3146
  }
3130
3147
  if (response.status === 429) {
3131
3148
  const refusal2 = await spendingRefusalOf(response);
3132
3149
  if (refusal2 && !signal.aborted) setSpending({ state: "exhausted", info: refusal2 });
3133
3150
  const tooManyTurns = !refusal2 && await errorKindOf(response) === "too_many_turns";
3134
- setApprovalFailure({ id: approvalId, kind: refusal2 ? "spending_limit" : tooManyTurns ? "too_many_turns" : "connection" });
3151
+ fail(refusal2 ? "spending_limit" : tooManyTurns ? "too_many_turns" : "connection");
3135
3152
  return;
3136
3153
  }
3137
3154
  if (response.status === 409 && await errorKindOf(response) === "busy") {
3138
- setApprovalFailure({ id: approvalId, kind: "busy" });
3155
+ fail("busy");
3139
3156
  return;
3140
3157
  }
3141
3158
  if (response.status === 409 || response.status === 404) {
@@ -3143,10 +3160,11 @@ function useAgentChat({
3143
3160
  return;
3144
3161
  }
3145
3162
  if (!response.ok) {
3146
- setApprovalFailure({ id: approvalId, kind: "connection" });
3163
+ fail("connection");
3147
3164
  return;
3148
3165
  }
3149
3166
  const { resumed } = await response.json();
3167
+ if (signal.aborted) return;
3150
3168
  setMessages((current) => withApprovalDecision(current, approvalId, decision, resumed));
3151
3169
  if (!resumed) return;
3152
3170
  setResuming(true);
@@ -3158,13 +3176,13 @@ function useAgentChat({
3158
3176
  if (!signal.aborted) setResuming(false);
3159
3177
  }
3160
3178
  } finally {
3161
- approving.current = false;
3179
+ if (approving.current === pending) approving.current = null;
3162
3180
  if (!signal.aborted) setApprovalPendingId(null);
3163
3181
  }
3164
3182
  },
3165
3183
  [announceAppRefusal, chatUrl2, request2, rereadHistory, resumeWithProbe, setMessages, startSeries]
3166
3184
  );
3167
- const { answerWidget, widgetPendingId, widgetFailure, answeringWidget } = useWidgetAnswer({
3185
+ const { answerWidget, widgetPendingId, widgetFailure, answeringWidget, forgetChat: forgetWidgetAnswer } = useWidgetAnswer({
3168
3186
  chatUrl: chatUrl2,
3169
3187
  request: request2,
3170
3188
  requestTimeoutMs: () => settingsRef.current.requestTimeoutMs,
@@ -3184,8 +3202,18 @@ function useAgentChat({
3184
3202
  showSpendingExhausted: (info) => setSpending({ state: "exhausted", info }),
3185
3203
  errorKindOf
3186
3204
  });
3205
+ useEffect3(
3206
+ () => () => {
3207
+ setResuming(false);
3208
+ setHandingOff(false);
3209
+ setApprovalPendingId(null);
3210
+ setApprovalFailure(null);
3211
+ forgetWidgetAnswer();
3212
+ },
3213
+ [chatId, forgetWidgetAnswer]
3214
+ );
3187
3215
  const ownTurnStarting = useCallback4(
3188
- () => busyRef.current || sending.current || approving.current || answeringWidget(),
3216
+ () => busyRef.current || sending.current || ownedBy(approving.current, chatLifetime.current.signal) !== void 0 || answeringWidget(),
3189
3217
  [answeringWidget]
3190
3218
  );
3191
3219
  const connectionRef = useRef5(connection);
@@ -3271,18 +3299,19 @@ function useAgentChat({
3271
3299
  [sendMessage, setMessages]
3272
3300
  );
3273
3301
  const handOffToCorrections = useCallback4(() => {
3274
- if (handOff.current) return handOff.current;
3275
3302
  const signal = chatLifetime.current.signal;
3303
+ const going = ownedBy(handOff.current, signal);
3304
+ if (going) return going;
3276
3305
  setHandingOff(true);
3277
3306
  const run = (async () => {
3278
- await steerQueue.current;
3279
- await stopping.current;
3307
+ await ownedBy(steerQueue.current, signal);
3308
+ await ownedBy(stopping.current, signal);
3280
3309
  if (!signal.aborted) await refreshFromServer(signal);
3281
3310
  })().finally(() => {
3282
- handOff.current = null;
3311
+ if (handOff.current?.value === run) handOff.current = null;
3283
3312
  if (!signal.aborted) setHandingOff(false);
3284
3313
  });
3285
- handOff.current = run;
3314
+ handOff.current = { lifetime: signal, value: run };
3286
3315
  return run;
3287
3316
  }, [refreshFromServer]);
3288
3317
  handOffToCorrectionsRef.current = handOffToCorrections;
@@ -3302,7 +3331,7 @@ function useAgentChat({
3302
3331
  setValue((current) => current.length === 0 ? text : current);
3303
3332
  onRejected?.(kind);
3304
3333
  };
3305
- const sent = steerQueue.current.then(async () => {
3334
+ const sent = (ownedBy(steerQueue.current, signal) ?? Promise.resolve()).then(async () => {
3306
3335
  let response;
3307
3336
  try {
3308
3337
  response = await fetchWithDeadline(
@@ -3322,15 +3351,17 @@ function useAgentChat({
3322
3351
  if (signal.aborted || chatUrlRef.current !== asked) return;
3323
3352
  if (!response.ok) {
3324
3353
  const spendingLimit = response.status === 429 ? await spendingRefusalOf(response) : null;
3325
- if (spendingLimit) setSpending({ state: "exhausted", info: spendingLimit });
3326
3354
  const refusedWithText = await serverRefusalOf(response);
3355
+ const kind = spendingLimit ? "spending_limit" : refusedWithText?.kind ?? await errorKindOf(response);
3356
+ if (signal.aborted) return;
3357
+ if (spendingLimit) setSpending({ state: "exhausted", info: spendingLimit });
3327
3358
  announceAppRefusal(refusedWithText);
3328
- refused(spendingLimit ? "spending_limit" : refusedWithText?.kind ?? await errorKindOf(response));
3359
+ refused(kind);
3329
3360
  return;
3330
3361
  }
3331
3362
  if (!answeringRef.current) void handOffToCorrectionsRef.current();
3332
3363
  });
3333
- steerQueue.current = sent.catch(() => void 0);
3364
+ steerQueue.current = { lifetime: signal, value: sent.catch(() => void 0) };
3334
3365
  },
3335
3366
  [announceAppRefusal, chatUrl2, request2]
3336
3367
  );
@@ -3430,24 +3461,31 @@ function useAgentChat({
3430
3461
  if (loadedChatState !== null) publishedState2.current?.update(loadedChatState);
3431
3462
  }, [loadedChatState]);
3432
3463
  const stopping = useRef5(null);
3433
- const stop = useCallback4(() => {
3434
- if (stopping.current) return stopping.current;
3435
- if (seriesRef.current) cancelSeries();
3436
- browserCallsLifetime.current?.abort();
3437
- browserCallsLifetime.current = null;
3438
- const stopped = (async () => {
3439
- await fetchWithDeadline(request2, `${chatUrl2}/stop`, { method: "POST" }, {
3440
- timeoutMs: settingsRef.current.requestTimeoutMs,
3441
- wholeBody: true
3442
- }).catch(() => void 0);
3443
- await stopChat();
3444
- setMessages((messages2) => markLastAnswerStopped(messages2));
3445
- })().finally(() => {
3446
- stopping.current = null;
3447
- });
3448
- stopping.current = stopped;
3449
- return stopped;
3450
- }, [cancelSeries, chatUrl2, request2, setMessages, stopChat]);
3464
+ const stopIn = useCallback4(
3465
+ (lifetime) => {
3466
+ const going = ownedBy(stopping.current, lifetime);
3467
+ if (going) return going;
3468
+ if (lifetime.aborted) return Promise.resolve();
3469
+ if (seriesRef.current) cancelSeries();
3470
+ browserCallsLifetime.current?.abort();
3471
+ browserCallsLifetime.current = null;
3472
+ const stopped = (async () => {
3473
+ await fetchWithDeadline(request2, `${chatUrl2}/stop`, { method: "POST" }, {
3474
+ timeoutMs: settingsRef.current.requestTimeoutMs,
3475
+ wholeBody: true
3476
+ }).catch(() => void 0);
3477
+ if (lifetime.aborted) return;
3478
+ await stopChat();
3479
+ if (!lifetime.aborted) setMessages((messages2) => markLastAnswerStopped(messages2));
3480
+ })().finally(() => {
3481
+ if (stopping.current?.value === stopped) stopping.current = null;
3482
+ });
3483
+ stopping.current = { lifetime, value: stopped };
3484
+ return stopped;
3485
+ },
3486
+ [cancelSeries, chatUrl2, request2, setMessages, stopChat]
3487
+ );
3488
+ const stop = useCallback4(() => stopIn(chatLifetime.current.signal), [stopIn]);
3451
3489
  const retry = useCallback4(() => {
3452
3490
  if (connection === "offline") {
3453
3491
  reconnectNow();
@@ -3532,11 +3570,12 @@ function useAgentChat({
3532
3570
  },
3533
3571
  [busy, canEdit, startEdit]
3534
3572
  );
3535
- const replacing = useRef5(Promise.resolve());
3573
+ const replacing = useRef5(null);
3536
3574
  const replaceLastVoiceMessage = useCallback4(
3537
3575
  (text, options) => {
3538
- const run = replacing.current.then(async () => {
3539
- const signal = chatLifetime.current.signal;
3576
+ const signal = chatLifetime.current.signal;
3577
+ const run = (ownedBy(replacing.current, signal) ?? Promise.resolve()).then(async () => {
3578
+ if (signal.aborted) return false;
3540
3579
  const messages2 = messagesRef.current;
3541
3580
  const index = messages2.findLastIndex((message) => message.role === "user");
3542
3581
  const target = messages2[index];
@@ -3544,24 +3583,25 @@ function useAgentChat({
3544
3583
  if (!messages2.slice(index + 1).every(isEmptyAnswer)) return false;
3545
3584
  const original = messages2;
3546
3585
  setMessages([...messages2.slice(0, index), { ...target, parts: [{ type: "text", text }, { type: VOICE_MODE_PART_TYPE, data: true }] }]);
3547
- await stop();
3548
- if (!await untilIdle(signal)) return restoreAfterFailure(original);
3549
- for (let attempt = 0; attempt < REPLACE_ATTEMPTS; attempt++) {
3586
+ await stopIn(signal);
3587
+ if (!await untilIdle(signal)) return restoreAfterFailure(original, signal);
3588
+ for (let attempt = 0; attempt < REPLACE_ATTEMPTS && !signal.aborted; attempt++) {
3550
3589
  const outcome = await startEdit(target.id, text, true, options?.sounds);
3551
3590
  if (outcome.status === "started") return true;
3552
3591
  if (!(outcome.status === "refused" && outcome.reason === "busy")) break;
3553
3592
  await pause2(REPLACE_RETRY_MS, signal);
3554
3593
  }
3555
- return restoreAfterFailure(original);
3594
+ return restoreAfterFailure(original, signal);
3556
3595
  });
3557
- replacing.current = run.catch(() => false);
3596
+ replacing.current = { lifetime: signal, value: run.catch(() => false) };
3558
3597
  return run;
3559
3598
  },
3560
- [exhausted, loaded, refreshFromServer, setMessages, startEdit, stop]
3599
+ [exhausted, loaded, refreshFromServer, setMessages, startEdit, stopIn]
3561
3600
  );
3562
- function restoreAfterFailure(original) {
3601
+ function restoreAfterFailure(original, signal) {
3602
+ if (signal.aborted) return false;
3563
3603
  setMessages(original);
3564
- void refreshFromServer(chatLifetime.current.signal);
3604
+ void refreshFromServer(signal);
3565
3605
  return false;
3566
3606
  }
3567
3607
  async function untilIdle(signal) {
@@ -7236,4 +7276,4 @@ export {
7236
7276
  activeChatTitle,
7237
7277
  useWorkspaceListSync
7238
7278
  };
7239
- //# sourceMappingURL=chunk-LF723Y52.js.map
7279
+ //# sourceMappingURL=chunk-PZZBB4Y6.js.map