@agents24/react 0.2.14 → 0.2.15

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/dist/index.js CHANGED
@@ -10,6 +10,235 @@ function isAbortLike(error) {
10
10
  return input.name === "AbortError" || input.kind === "aborted" || input.code === "ABORTED";
11
11
  }
12
12
 
13
+ // src/chat-observation.ts
14
+ function createChatObservation(input) {
15
+ const clock = globalThis;
16
+ let state = { threadId: null, running: false, waiting: false, pending: false, paused: false };
17
+ let visible = input.visibility?.isVisible() ?? true;
18
+ let disposed = false;
19
+ let generation = 0;
20
+ let cursor;
21
+ let connection;
22
+ let recovery;
23
+ let refreshing;
24
+ let timer;
25
+ let reconnectTimer;
26
+ let failures = 0;
27
+ let handoffAt;
28
+ let buffered;
29
+ const needed = () => !disposed && visible && Boolean(state.threadId) && !state.waiting && !state.paused && (state.running || state.pending || state.mode === "realtime");
30
+ const clearTimer = () => {
31
+ clock.clearTimeout(timer);
32
+ timer = void 0;
33
+ };
34
+ const stop = () => {
35
+ clearTimer();
36
+ clock.clearTimeout(reconnectTimer);
37
+ connection?.abort();
38
+ connection = void 0;
39
+ };
40
+ const schedule = (callback, ms) => {
41
+ clearTimer();
42
+ timer = clock.setTimeout(callback, ms);
43
+ };
44
+ function refresh() {
45
+ if (disposed || !visible || !state.threadId) return Promise.resolve();
46
+ if (refreshing) return refreshing;
47
+ const owned = generation;
48
+ const controller = input.createAbortController();
49
+ recovery = controller;
50
+ const timeout = clock.setTimeout(() => controller.abort("Observation refresh timed out."), 5e3);
51
+ let onAbort = () => {
52
+ };
53
+ const aborted = new Promise((_, reject) => {
54
+ onAbort = () => reject(Object.assign(new Error("Observation refresh detached."), { name: "AbortError" }));
55
+ controller.signal.addEventListener("abort", onAbort, { once: true });
56
+ });
57
+ const promise = Promise.race([input.refresh(controller.signal), aborted]).then(() => {
58
+ if (owned === generation && !controller.signal.aborted) input.onRecovered();
59
+ }).finally(() => {
60
+ clock.clearTimeout(timeout);
61
+ controller.signal.removeEventListener("abort", onAbort);
62
+ if (recovery === controller) recovery = void 0;
63
+ if (refreshing === promise) refreshing = void 0;
64
+ });
65
+ refreshing = promise;
66
+ return promise;
67
+ }
68
+ function recover() {
69
+ const owned = generation;
70
+ return refresh().then(() => {
71
+ if (owned === generation && !disposed && visible) {
72
+ failures = 0;
73
+ input.onResume();
74
+ }
75
+ }).catch((error) => {
76
+ if (owned !== generation || disposed || !visible) return;
77
+ failures += 1;
78
+ input.onError(error);
79
+ if (needed()) schedule(poll, 1e3);
80
+ });
81
+ }
82
+ function poll() {
83
+ if (!needed()) return;
84
+ void recover().finally(() => {
85
+ if (needed() && (state.pending && !state.running || state.degraded || failures > 0)) schedule(poll, 1e3);
86
+ });
87
+ }
88
+ function connect() {
89
+ if (!needed() || connection) return;
90
+ const owned = generation;
91
+ const controller = input.createAbortController();
92
+ connection = controller;
93
+ buffered = [];
94
+ let ready = false;
95
+ let leased = false;
96
+ let hydration;
97
+ const hydrate = () => hydration ??= recover().finally(() => {
98
+ if (owned !== generation || controller.signal.aborted) return;
99
+ const notices = buffered || [];
100
+ buffered = void 0;
101
+ for (const event of notices) input.onEvent(event);
102
+ if (notices.length) void recover();
103
+ });
104
+ void input.events({
105
+ threadId: state.threadId,
106
+ cursor,
107
+ leaseSeconds: 60,
108
+ signal: controller.signal,
109
+ onControl(control) {
110
+ if (control.type === "lease_expired") leased = true;
111
+ if (control.type === "ready") {
112
+ ready = true;
113
+ cursor ??= control.cursor;
114
+ void hydrate().catch(input.onError);
115
+ }
116
+ }
117
+ }, (event) => {
118
+ if (owned !== generation || controller.signal.aborted) return;
119
+ if (event.event === "snapshot_required") {
120
+ cursor = void 0;
121
+ void refresh().catch(input.onError);
122
+ return;
123
+ }
124
+ cursor = Math.max(cursor ?? 0, event.cursor);
125
+ failures = 0;
126
+ if (buffered) buffered.push(event);
127
+ else {
128
+ input.onEvent(event);
129
+ void recover();
130
+ }
131
+ if (!ready) {
132
+ ready = true;
133
+ void hydrate().catch(input.onError);
134
+ }
135
+ }).then(() => {
136
+ if (owned !== generation || controller.signal.aborted) return;
137
+ connection = void 0;
138
+ if (needed()) {
139
+ if (leased) connect();
140
+ else {
141
+ void refresh().catch(input.onError);
142
+ reconnectTimer = clock.setTimeout(connect, 1e3);
143
+ }
144
+ }
145
+ }).catch((error) => {
146
+ if (owned !== generation || controller.signal.aborted || isAbortLike(error)) return;
147
+ connection = void 0;
148
+ failures += 1;
149
+ input.onError(error);
150
+ poll();
151
+ reconnectTimer = clock.setTimeout(connect, Math.min(3e4, 1e3 * 2 ** Math.min(failures - 1, 5)));
152
+ });
153
+ }
154
+ function reconcile() {
155
+ if (!needed()) {
156
+ handoffAt = void 0;
157
+ stop();
158
+ return;
159
+ }
160
+ connect();
161
+ if (state.pending && !state.running) {
162
+ handoffAt ??= Date.now() + 2e3;
163
+ schedule(poll, Math.max(0, handoffAt - Date.now()));
164
+ } else {
165
+ handoffAt = void 0;
166
+ if (state.degraded) schedule(poll, 1e3);
167
+ else if (!failures) clearTimer();
168
+ }
169
+ }
170
+ const unsubscribe = input.visibility?.subscribe(() => {
171
+ const next = input.visibility.isVisible();
172
+ if (next === visible) return;
173
+ visible = next;
174
+ if (!visible) {
175
+ stop();
176
+ recovery?.abort();
177
+ refreshing = void 0;
178
+ input.onDetach();
179
+ } else {
180
+ reconcile();
181
+ if (!needed()) void refresh().then(input.onResume).catch(input.onError);
182
+ }
183
+ });
184
+ return {
185
+ refresh,
186
+ update(next) {
187
+ if (next.threadId !== state.threadId) {
188
+ generation += 1;
189
+ stop();
190
+ recovery?.abort();
191
+ refreshing = void 0;
192
+ cursor = void 0;
193
+ buffered = void 0;
194
+ failures = 0;
195
+ handoffAt = void 0;
196
+ }
197
+ state = next;
198
+ reconcile();
199
+ },
200
+ dispose() {
201
+ disposed = true;
202
+ generation += 1;
203
+ stop();
204
+ recovery?.abort();
205
+ unsubscribe?.();
206
+ }
207
+ };
208
+ }
209
+
210
+ // src/use-chat-observation.ts
211
+ import * as React from "react";
212
+ function useChatObservation(input) {
213
+ const latest = React.useRef(input);
214
+ latest.current = input;
215
+ const owner = React.useRef(null);
216
+ React.useEffect(() => {
217
+ const observation = createChatObservation({
218
+ createAbortController: input.createAbortController,
219
+ visibility: input.visibility,
220
+ events: (options, handler) => latest.current.events(options, handler),
221
+ refresh: (signal) => latest.current.refresh(signal),
222
+ onEvent: (event) => latest.current.onEvent(event),
223
+ onError: (error) => latest.current.onError(error),
224
+ onRecovered: () => latest.current.onRecovered(),
225
+ onDetach: () => latest.current.onDetach(),
226
+ onResume: () => latest.current.onResume()
227
+ });
228
+ owner.current = observation;
229
+ observation.update(latest.current.state);
230
+ return () => {
231
+ observation.dispose();
232
+ if (owner.current === observation) owner.current = null;
233
+ };
234
+ }, [input.createAbortController, input.visibility]);
235
+ const { threadId, running, waiting, pending, paused, degraded, mode } = input.state;
236
+ React.useEffect(() => {
237
+ owner.current?.update({ threadId, running, waiting, pending, paused, degraded, mode });
238
+ }, [threadId, running, waiting, pending, paused, degraded, mode]);
239
+ return React.useCallback(() => owner.current?.refresh() ?? Promise.resolve(), []);
240
+ }
241
+
13
242
  // src/model.ts
14
243
  import {
15
244
  compressionFromContextWindow,
@@ -389,8 +618,7 @@ function threadDetailToMessages(detail) {
389
618
  return output;
390
619
  });
391
620
  }
392
- function threadTimelineToMessages(items, detail) {
393
- const feedbackByRun = new Map((detail?.turns || []).flatMap((turn) => turn.run_id && turn.feedback ? [[turn.run_id, turn.feedback]] : []));
621
+ function threadTimelineToMessages(items) {
394
622
  return items.map((item) => ({
395
623
  id: item.id,
396
624
  clientSubmissionId: item.client_submission_id || void 0,
@@ -406,7 +634,7 @@ function threadTimelineToMessages(items, detail) {
406
634
  const descriptor = item.attachments?.find((attachment) => attachment.id === id);
407
635
  return { id, filename: descriptor?.filename || "Attachment", mediaType: descriptor?.mime_type || "application/octet-stream" };
408
636
  }) : void 0,
409
- feedback: item.role === "assistant" && item.run_id ? feedbackByRun.get(item.run_id) : void 0
637
+ feedback: item.role === "assistant" ? item.feedback ?? null : void 0
410
638
  }));
411
639
  }
412
640
  function latestThreadContext(detail) {
@@ -430,24 +658,25 @@ function attachmentResultToChatAttachment(result) {
430
658
  }
431
659
 
432
660
  // src/provider.tsx
433
- import * as React from "react";
661
+ import * as React2 from "react";
434
662
  import { jsx } from "react/jsx-runtime";
435
- var Agents24Context = React.createContext(null);
663
+ var Agents24Context = React2.createContext(null);
436
664
  function Agents24Provider({
437
665
  children,
438
666
  client,
439
667
  createAbortController,
440
668
  createId = createPortableId,
441
- storage
669
+ storage,
670
+ visibility
442
671
  }) {
443
- const value = React.useMemo(
444
- () => ({ client, createAbortController, createId, storage }),
445
- [client, createAbortController, createId, storage]
672
+ const value = React2.useMemo(
673
+ () => ({ client, createAbortController, createId, storage, visibility: visibility ?? client.visibility }),
674
+ [client, createAbortController, createId, storage, visibility]
446
675
  );
447
676
  return /* @__PURE__ */ jsx(Agents24Context.Provider, { value, children });
448
677
  }
449
678
  function useAgents24Runtime() {
450
- const value = React.useContext(Agents24Context);
679
+ const value = React2.useContext(Agents24Context);
451
680
  if (!value) throw new Error("Agents24 hooks must be rendered inside Agents24Provider.");
452
681
  return value;
453
682
  }
@@ -615,10 +844,15 @@ function reconcileThreadTimeline(current, canonical, activeRunId, streamingMessa
615
844
  const retained = current.filter((message) => !canonicalIds.has(message.id) && (message.isFinal || activeRunId && (!message.runId || message.runId === activeRunId)));
616
845
  const reconciled = mounted.map((message) => {
617
846
  const live = current.find((item) => item.id === message.id);
618
- return activeRunId && live?.role === "assistant" && live.runId === activeRunId && (live.id === streamingMessageId || !live.isFinal || live.responseCompletion) ? message.responseCompletion && !live.responseCompletion ? { ...message, id: live.id, durableId: message.durableId || live.durableId } : { ...message, ...live, durableId: message.durableId || live.durableId } : message;
847
+ return activeRunId && live?.role === "assistant" && live.runId === activeRunId && (live.id === streamingMessageId || !live.isFinal || live.responseCompletion) ? message.responseCompletion && !live.responseCompletion ? { ...message, id: live.id, durableId: message.durableId || live.durableId } : { ...message, ...live, feedback: message.feedback, durableId: message.durableId || live.durableId } : message;
619
848
  });
620
849
  return [...retained, ...reconciled];
621
850
  }
851
+ function prependThreadTimeline(current, older) {
852
+ const mounted = preserveMountedMessageIds(older, current);
853
+ const ids = new Set(current.map((message) => message.id));
854
+ return [...mounted.filter((message) => !ids.has(message.id)), ...current];
855
+ }
622
856
  function promoteThreadMessage(current, message, runId) {
623
857
  const promoted = { ...message, runId, isFinal: true };
624
858
  const index = current.findIndex((item) => item.id === message.id || item.durableId === message.id || Boolean(message.clientSubmissionId && item.clientSubmissionId === message.clientSubmissionId));
@@ -839,7 +1073,18 @@ function applyThreadSummaryEvent(threads, event) {
839
1073
  }
840
1074
 
841
1075
  // src/thread-message-queue.ts
842
- import * as React2 from "react";
1076
+ import * as React3 from "react";
1077
+
1078
+ // src/operation-ownership.ts
1079
+ function captureChatOperation(read, options = {}) {
1080
+ const owner = read();
1081
+ return () => {
1082
+ const current = read();
1083
+ return current.generation === owner.generation && (options.adoptThread || current.threadId === owner.threadId) && (!options.run || current.runId === owner.runId) && (!options.stream || current.stream === owner.stream);
1084
+ };
1085
+ }
1086
+
1087
+ // src/thread-message-queue.ts
843
1088
  async function retrySubmissionAdmission(admit) {
844
1089
  try {
845
1090
  return await admit();
@@ -849,23 +1094,34 @@ async function retrySubmissionAdmission(admit) {
849
1094
  return admit();
850
1095
  }
851
1096
  }
852
- function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic, onDiscard }) {
853
- const [pendingMessages, setPendingMessages] = React2.useState([]);
854
- const [messageQueue, setMessageQueue] = React2.useState(null);
855
- const pendingRef = React2.useRef(/* @__PURE__ */ new Map());
856
- const versionRef = React2.useRef(0);
857
- const cursorRef = React2.useRef(0);
858
- const promotedRef = React2.useRef(/* @__PURE__ */ new Set());
859
- const threadRef = React2.useRef(threadId);
860
- const transportRef = React2.useRef(transport);
861
- const onPromotedRef = React2.useRef(onPromoted);
1097
+ function useThreadMessageQueue({ threadId, transport, onPromoted, onUnknownPromotion, onOptimistic, onDiscard, ownOperation: ownSelection }) {
1098
+ const [pendingMessages, setPendingMessages] = React3.useState([]);
1099
+ const [messageQueue, setMessageQueue] = React3.useState(null);
1100
+ const pendingRef = React3.useRef(/* @__PURE__ */ new Map());
1101
+ const versionRef = React3.useRef(0);
1102
+ const cursorRef = React3.useRef(0);
1103
+ const promotedRef = React3.useRef(/* @__PURE__ */ new Set());
1104
+ const threadRef = React3.useRef(threadId);
1105
+ const selectionGenerationRef = React3.useRef(0);
1106
+ if (threadRef.current !== threadId) selectionGenerationRef.current += 1;
1107
+ const transportRef = React3.useRef(transport);
1108
+ const onPromotedRef = React3.useRef(onPromoted);
862
1109
  threadRef.current = threadId;
863
1110
  transportRef.current = transport;
864
1111
  onPromotedRef.current = onPromoted;
865
- const callbacksRef = React2.useRef({ onOptimistic, onDiscard });
866
- callbacksRef.current = { onOptimistic, onDiscard };
1112
+ const ownSelectionRef = React3.useRef(ownSelection);
1113
+ ownSelectionRef.current = ownSelection;
1114
+ const callbacksRef = React3.useRef({ onOptimistic, onDiscard, onUnknownPromotion });
1115
+ callbacksRef.current = { onOptimistic, onDiscard, onUnknownPromotion };
1116
+ const ownOperation = React3.useCallback(() => ownSelectionRef.current?.() ?? captureChatOperation(() => ({
1117
+ generation: selectionGenerationRef.current,
1118
+ threadId: threadRef.current
1119
+ })), []);
1120
+ const assertCurrent = (isCurrent) => {
1121
+ if (!isCurrent()) throw Object.assign(new Error("Thread selection changed."), { name: "AbortError" });
1122
+ };
867
1123
  const knownMessage = (id, submissionId) => pendingRef.current.get(id) || (submissionId ? [...pendingRef.current.values()].find((item) => item.client_submission_id === submissionId) : void 0);
868
- const hydrate = React2.useCallback((page, selectedThreadId) => {
1124
+ const hydrate = React3.useCallback((page, selectedThreadId) => {
869
1125
  if (threadRef.current !== selectedThreadId) return;
870
1126
  const pending = page.pending.filter((message) => !promotedRef.current.has(message.id)).map((message) => {
871
1127
  const known = knownMessage(message.id, message.client_submission_id);
@@ -877,11 +1133,12 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
877
1133
  setPendingMessages([...pending, ...optimistic]);
878
1134
  setMessageQueue(page.queue);
879
1135
  }, []);
880
- const refresh = React2.useCallback(async (selectedThreadId = threadRef.current) => {
1136
+ const refresh = React3.useCallback(async (selectedThreadId = threadRef.current, signal) => {
881
1137
  if (!selectedThreadId) return null;
1138
+ const isCurrent = ownOperation();
882
1139
  const version = ++versionRef.current;
883
- const page = await transportRef.current.list(selectedThreadId);
884
- if (version !== versionRef.current || threadRef.current !== selectedThreadId) return null;
1140
+ const page = await transportRef.current.list(selectedThreadId, signal);
1141
+ if (signal?.aborted || !isCurrent() || version !== versionRef.current || threadRef.current !== selectedThreadId) return null;
885
1142
  for (const item of page.items) {
886
1143
  if (item.role === "user" && item.status === "delivered" && item.run_id) {
887
1144
  const pending = knownMessage(item.id, item.client_submission_id);
@@ -899,23 +1156,24 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
899
1156
  }
900
1157
  hydrate(page, selectedThreadId);
901
1158
  return page;
902
- }, [hydrate]);
903
- const acceptNotice = React2.useCallback((notice) => {
1159
+ }, [hydrate, ownOperation]);
1160
+ const acceptNotice = React3.useCallback((notice) => {
904
1161
  if (notice.cursor && notice.cursor <= cursorRef.current) return false;
905
1162
  if (notice.cursor) cursorRef.current = notice.cursor;
906
1163
  const pending = knownMessage(notice.messageId, notice.clientSubmissionId);
907
- if (notice.status === "delivered" && notice.runId && pending && !promotedRef.current.has(notice.messageId)) {
1164
+ if (notice.status === "delivered" && notice.runId && !promotedRef.current.has(notice.messageId)) {
908
1165
  promotedRef.current.add(notice.messageId);
909
- pendingRef.current.delete(pending.id);
910
1166
  versionRef.current += 1;
911
- setPendingMessages((current) => current.filter((message) => message.id !== pending.id));
912
- onPromotedRef.current({ ...pending, id: notice.messageId, run_id: notice.runId, status: "delivered" }, notice.runId);
1167
+ if (pending) {
1168
+ pendingRef.current.delete(pending.id);
1169
+ setPendingMessages((current) => current.filter((message) => message.id !== pending.id));
1170
+ onPromotedRef.current({ ...pending, id: notice.messageId, run_id: notice.runId, status: "delivered" }, notice.runId);
1171
+ } else if (threadRef.current) callbacksRef.current.onUnknownPromotion?.(notice.runId, threadRef.current);
913
1172
  }
914
1173
  if (notice.status === "removed") pendingRef.current.delete(notice.messageId);
915
- void refresh().catch(() => void 0);
916
1174
  return true;
917
- }, [refresh]);
918
- const beginSubmission = React2.useCallback((input, queued = false) => {
1175
+ }, []);
1176
+ const beginSubmission = React3.useCallback((input, queued = false) => {
919
1177
  const now = (/* @__PURE__ */ new Date()).toISOString();
920
1178
  const message = {
921
1179
  id: input.clientSubmissionId,
@@ -942,16 +1200,17 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
942
1200
  else callbacksRef.current.onOptimistic?.(message, Boolean(input.uploading));
943
1201
  return message;
944
1202
  }, []);
945
- const discardSubmission = React2.useCallback((message) => {
1203
+ const discardSubmission = React3.useCallback((message) => {
946
1204
  if (!pendingRef.current.has(message.id)) return;
947
1205
  pendingRef.current.delete(message.id);
948
1206
  callbacksRef.current.onDiscard?.(message);
949
1207
  versionRef.current += 1;
950
1208
  setPendingMessages((current) => current.filter((item) => item.client_submission_id !== message.client_submission_id));
951
1209
  }, []);
952
- const submit = React2.useCallback(async (input) => {
1210
+ const submit = React3.useCallback(async (input) => {
953
1211
  const selectedThreadId = threadRef.current;
954
1212
  if (!selectedThreadId) throw new Error("No active Agent thread is available.");
1213
+ const isCurrent = ownOperation();
955
1214
  const { queued, prepare, attachments, uploading, ...submission } = input;
956
1215
  const optimistic = beginSubmission({
957
1216
  ...submission,
@@ -961,10 +1220,11 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
961
1220
  }, queued);
962
1221
  try {
963
1222
  const prepared = prepare ? await prepare() : {};
964
- if (threadRef.current === selectedThreadId) pendingRef.current.set(optimistic.id, { ...optimistic, optimisticQueued: queued, attachment_ids: prepared.attachmentIds || optimistic.attachment_ids, attachments: prepared.attachments || attachments });
1223
+ assertCurrent(isCurrent);
1224
+ pendingRef.current.set(optimistic.id, { ...optimistic, optimisticQueued: queued, attachment_ids: prepared.attachmentIds || optimistic.attachment_ids, attachments: prepared.attachments || attachments });
965
1225
  const request = { ...submission, ...prepared, clientSubmissionId: optimistic.client_submission_id, threadId: selectedThreadId };
966
1226
  const result = await retrySubmissionAdmission(() => transportRef.current.submit(request));
967
- if (threadRef.current !== selectedThreadId) return result;
1227
+ if (!isCurrent()) return result;
968
1228
  versionRef.current += 1;
969
1229
  const bound = {
970
1230
  ...optimistic,
@@ -991,36 +1251,44 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
991
1251
  void refresh().catch(() => void 0);
992
1252
  return result;
993
1253
  } catch (error) {
994
- if (threadRef.current === selectedThreadId) discardSubmission(optimistic);
1254
+ if (isCurrent()) discardSubmission(optimistic);
995
1255
  throw error;
996
1256
  }
997
- }, [beginSubmission, discardSubmission, refresh]);
998
- const edit = React2.useCallback(async (message, input) => {
1257
+ }, [beginSubmission, discardSubmission, refresh, ownOperation]);
1258
+ const edit = React3.useCallback(async (message, input) => {
1259
+ const isCurrent = ownOperation();
999
1260
  const updated = await transportRef.current.edit(message, input);
1261
+ assertCurrent(isCurrent);
1000
1262
  versionRef.current += 1;
1001
1263
  pendingRef.current.set(updated.id, updated);
1002
1264
  await refresh(message.thread_id);
1003
1265
  return updated;
1004
- }, [refresh]);
1005
- const remove = React2.useCallback(async (message) => {
1266
+ }, [refresh, ownOperation]);
1267
+ const remove = React3.useCallback(async (message) => {
1268
+ const isCurrent = ownOperation();
1006
1269
  await transportRef.current.remove(message);
1270
+ assertCurrent(isCurrent);
1007
1271
  versionRef.current += 1;
1008
1272
  pendingRef.current.delete(message.id);
1009
1273
  setPendingMessages((current) => current.filter((item) => item.id !== message.id));
1010
1274
  await refresh(message.thread_id);
1011
- }, [refresh]);
1012
- const retractForEdit = React2.useCallback(async (message) => {
1275
+ }, [refresh, ownOperation]);
1276
+ const retractForEdit = React3.useCallback(async (message) => {
1277
+ const isCurrent = ownOperation();
1013
1278
  await remove(message);
1279
+ assertCurrent(isCurrent);
1014
1280
  return { text: message.text, attachment_ids: message.attachment_ids, attachments: message.attachments };
1015
- }, [remove]);
1016
- const resume = React2.useCallback(async () => {
1281
+ }, [remove, ownOperation]);
1282
+ const resume = React3.useCallback(async () => {
1017
1283
  const selectedThreadId = threadRef.current;
1018
1284
  if (!selectedThreadId) return null;
1285
+ const isCurrent = ownOperation();
1019
1286
  const result = await transportRef.current.resume(selectedThreadId);
1287
+ if (!isCurrent()) return null;
1020
1288
  await refresh(selectedThreadId);
1021
1289
  return result;
1022
- }, [refresh]);
1023
- React2.useEffect(() => {
1290
+ }, [refresh, ownOperation]);
1291
+ React3.useEffect(() => {
1024
1292
  versionRef.current += 1;
1025
1293
  cursorRef.current = 0;
1026
1294
  promotedRef.current.clear();
@@ -1029,14 +1297,6 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
1029
1297
  setMessageQueue(null);
1030
1298
  if (threadId) void refresh(threadId).catch(() => void 0);
1031
1299
  }, [threadId, refresh]);
1032
- React2.useEffect(() => {
1033
- if (!threadId || !pendingMessages.length || messageQueue?.paused) return;
1034
- const clock = globalThis;
1035
- const timer = clock.setInterval(() => {
1036
- void refresh(threadId).catch(() => void 0);
1037
- }, 3e3);
1038
- return () => clock.clearInterval(timer);
1039
- }, [threadId, pendingMessages.length, messageQueue?.paused, refresh]);
1040
1300
  return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, beginSubmission, discardSubmission, submit, edit, remove, retractForEdit, resume };
1041
1301
  }
1042
1302
  function optimisticSubmissionTurn(message) {
@@ -1085,72 +1345,20 @@ function discardOptimisticSubmission(state, message) {
1085
1345
  }
1086
1346
 
1087
1347
  // src/use-agent-chat.ts
1088
- import * as React6 from "react";
1089
-
1090
- // src/thread-summary-subscription.ts
1091
- function waitForReconnect(signal, delay) {
1092
- return new Promise((resolve) => {
1093
- if (signal.aborted) return resolve();
1094
- const clock = globalThis;
1095
- const finish = () => {
1096
- clock.clearTimeout(timer);
1097
- signal.removeEventListener("abort", finish);
1098
- resolve();
1099
- };
1100
- const timer = clock.setTimeout(finish, delay);
1101
- signal.addEventListener("abort", finish, { once: true });
1102
- });
1103
- }
1104
- async function subscribeToThreadSummaries(input) {
1105
- let cursor;
1106
- let failures = 0;
1107
- while (!input.signal.aborted) {
1108
- let refreshedFromSnapshot = false;
1109
- try {
1110
- await input.client.threads.events({ cursor, signal: input.signal }, async (event) => {
1111
- if (event.event === "snapshot_required") {
1112
- await input.refresh();
1113
- cursor = void 0;
1114
- refreshedFromSnapshot = true;
1115
- return;
1116
- }
1117
- cursor = event.cursor;
1118
- failures = 0;
1119
- await input.onEvent(event);
1120
- });
1121
- if (input.signal.aborted) break;
1122
- if (!refreshedFromSnapshot) await input.refresh();
1123
- input.onRecovered();
1124
- failures = 0;
1125
- } catch (error) {
1126
- if (input.signal.aborted || isAbortLike(error)) break;
1127
- input.onError(error);
1128
- failures += 1;
1129
- try {
1130
- await input.refresh();
1131
- input.onRecovered();
1132
- } catch (refreshError) {
1133
- if (!input.signal.aborted && !isAbortLike(refreshError)) input.onError(refreshError);
1134
- }
1135
- }
1136
- if (!input.signal.aborted) {
1137
- await waitForReconnect(input.signal, Math.min(3e4, 1e3 * 2 ** Math.min(failures, 5)));
1138
- }
1139
- }
1140
- }
1348
+ import * as React7 from "react";
1141
1349
 
1142
1350
  // src/use-agent-chat-thread-list.ts
1143
- import * as React3 from "react";
1351
+ import * as React4 from "react";
1144
1352
  function summaryToStored(thread) {
1145
1353
  return { ...thread, id: String(thread.id), messages: [], isHydrated: false };
1146
1354
  }
1147
1355
  function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation }) {
1148
- const [isLoadingThreads, setIsLoadingThreads] = React3.useState(false);
1149
- const [isLoadingMoreThreads, setIsLoadingMoreThreads] = React3.useState(false);
1150
- const [threadTotal, setThreadTotal] = React3.useState();
1151
- const refreshPromiseRef = React3.useRef(null);
1152
- const eventsDuringRefreshRef = React3.useRef(null);
1153
- const refreshThreads = React3.useCallback(() => {
1356
+ const [isLoadingThreads, setIsLoadingThreads] = React4.useState(false);
1357
+ const [isLoadingMoreThreads, setIsLoadingMoreThreads] = React4.useState(false);
1358
+ const [threadTotal, setThreadTotal] = React4.useState();
1359
+ const refreshPromiseRef = React4.useRef(null);
1360
+ const eventsDuringRefreshRef = React4.useRef(null);
1361
+ const refreshThreads = React4.useCallback(() => {
1154
1362
  if (refreshPromiseRef.current) return refreshPromiseRef.current;
1155
1363
  clearOperationError();
1156
1364
  setIsLoadingThreads(true);
@@ -1185,7 +1393,7 @@ function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clea
1185
1393
  }).catch(() => void 0);
1186
1394
  return request;
1187
1395
  }, [clearOperationError, client, failOperation, threadPageSize, update]);
1188
- const loadMoreThreads = React3.useCallback(async () => {
1396
+ const loadMoreThreads = React4.useCallback(async () => {
1189
1397
  if (isLoadingMoreThreads) return;
1190
1398
  const skip = stateRef.current.threads.length;
1191
1399
  if (threadTotal !== void 0 && skip >= threadTotal) return;
@@ -1220,15 +1428,30 @@ function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clea
1220
1428
  }
1221
1429
 
1222
1430
  // src/use-agent-chat-feedback.ts
1223
- import * as React4 from "react";
1224
- function useAgentChatFeedback({ client, update, clearOperationError, failOperation }) {
1225
- return React4.useCallback(async (message, input) => {
1431
+ import * as React5 from "react";
1432
+ function useAgentChatFeedback({ client, update, clearOperationError, failOperation, ownOperation, refreshMessages }) {
1433
+ const mutations = React5.useRef(/* @__PURE__ */ new Map());
1434
+ const captureRead = React5.useCallback(() => {
1435
+ const revisions = new Map([...mutations.current].map(([run, mutation]) => [run, mutation.readRevision]));
1436
+ return (incoming, current) => incoming.map((message) => {
1437
+ const mutation = message.runId ? mutations.current.get(message.runId) : void 0;
1438
+ const preserve = mutation && (mutation.pending > 0 || mutation.readRevision !== revisions.get(message.runId));
1439
+ const live = preserve && current.find((item) => item.role === "assistant" && item.runId === message.runId);
1440
+ return live ? { ...message, feedback: live.feedback } : message;
1441
+ });
1442
+ }, []);
1443
+ const setFeedback = React5.useCallback(async (message, input) => {
1226
1444
  const runId = message.runId;
1227
1445
  if (!runId || message.role !== "assistant" || message.isFinal === false) {
1228
1446
  const error = new Error("Only persisted final responses can receive feedback.");
1229
1447
  failOperation("set_feedback", error);
1230
1448
  throw error;
1231
1449
  }
1450
+ const isCurrent = ownOperation();
1451
+ const revision = (mutations.current.get(runId)?.revision ?? 0) + 1;
1452
+ const pending = (mutations.current.get(runId)?.pending ?? 0) + 1;
1453
+ mutations.current.set(runId, { revision, pending, readRevision: (mutations.current.get(runId)?.readRevision ?? 0) + 1 });
1454
+ const isLatest = () => mutations.current.get(runId)?.revision === revision;
1232
1455
  const previous = message.feedback ?? null;
1233
1456
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1234
1457
  const optimistic = input.rating ? {
@@ -1240,26 +1463,34 @@ function useAgentChatFeedback({ client, update, clearOperationError, failOperati
1240
1463
  } : null;
1241
1464
  const replaceFeedback = (current, feedback) => ({
1242
1465
  ...current,
1243
- messages: current.messages.map((item) => item.id === message.id ? { ...item, feedback } : item),
1466
+ messages: current.messages.map((item) => item.role === "assistant" && item.runId === runId ? { ...item, feedback } : item),
1244
1467
  threads: current.threads.map((thread) => ({
1245
1468
  ...thread,
1246
- messages: thread.messages.map((item) => item.id === message.id ? { ...item, feedback } : item)
1469
+ messages: thread.messages.map((item) => item.role === "assistant" && item.runId === runId ? { ...item, feedback } : item)
1247
1470
  }))
1248
1471
  });
1249
1472
  clearOperationError();
1250
1473
  update((current) => replaceFeedback(current, optimistic));
1251
1474
  try {
1252
1475
  const result = await client.runs.setFeedback({ runId, ...input });
1253
- update((current) => replaceFeedback(current, result.feedback));
1476
+ if (isLatest() && isCurrent()) update((current) => replaceFeedback(current, result.feedback));
1254
1477
  } catch (error) {
1255
- update((current) => withAgentChatOperationError(replaceFeedback(current, previous), "set_feedback", error));
1478
+ if (isLatest() && isCurrent()) update((current) => {
1479
+ const restored = replaceFeedback(current, previous);
1480
+ return isCurrent() ? withAgentChatOperationError(restored, "set_feedback", error) : restored;
1481
+ });
1256
1482
  throw error;
1483
+ } finally {
1484
+ const mutation = mutations.current.get(runId);
1485
+ mutations.current.set(runId, { ...mutation, readRevision: mutation.readRevision + 1, pending: mutation.pending - 1 });
1486
+ if (mutation.pending === 1 && isCurrent()) await refreshMessages().catch(() => void 0);
1257
1487
  }
1258
- }, [clearOperationError, client, failOperation, update]);
1488
+ }, [clearOperationError, client, failOperation, update, ownOperation, refreshMessages]);
1489
+ return { setFeedback, captureRead };
1259
1490
  }
1260
1491
 
1261
1492
  // src/use-agent-chat-queue-actions.ts
1262
- import * as React5 from "react";
1493
+ import * as React6 from "react";
1263
1494
  function useAgentChatQueueActions({
1264
1495
  queue,
1265
1496
  stateRef,
@@ -1267,21 +1498,23 @@ function useAgentChatQueueActions({
1267
1498
  clearOperationError,
1268
1499
  failOperation,
1269
1500
  refreshMessages,
1270
- attachRun
1501
+ attachRun,
1502
+ ownOperation
1271
1503
  }) {
1272
- const steer = React5.useCallback(async (text2) => {
1504
+ const steer = React6.useCallback(async (text2) => {
1273
1505
  const threadId = stateRef.current.activeThreadId;
1274
1506
  if (!threadId || !text2.trim()) throw new Error("An active thread and text are required to steer.");
1507
+ const isCurrent = ownOperation();
1275
1508
  clearOperationError();
1276
1509
  try {
1277
1510
  await queue.submit({ mode: "steer", input: text2.trim(), idempotencyKey: createId() });
1278
- await refreshMessages(threadId);
1511
+ if (isCurrent()) await refreshMessages(threadId);
1279
1512
  } catch (error) {
1280
- failOperation("submit", error);
1513
+ if (isCurrent()) failOperation("submit", error);
1281
1514
  throw error;
1282
1515
  }
1283
- }, [clearOperationError, createId, failOperation, queue.submit, refreshMessages, stateRef]);
1284
- const editPendingMessage = React5.useCallback(async (message, input) => {
1516
+ }, [clearOperationError, createId, failOperation, queue.submit, refreshMessages, stateRef, ownOperation]);
1517
+ const editPendingMessage = React6.useCallback(async (message, input) => {
1285
1518
  await queue.edit(message, {
1286
1519
  text: input.text,
1287
1520
  attachmentIds: input.attachmentIds ?? message.attachment_ids,
@@ -1289,14 +1522,15 @@ function useAgentChatQueueActions({
1289
1522
  requestedModelId: input.requestedModelId
1290
1523
  });
1291
1524
  }, [queue.edit]);
1292
- const steerPendingMessage = React5.useCallback(async (message) => {
1525
+ const steerPendingMessage = React6.useCallback(async (message) => {
1293
1526
  await queue.edit(message, { text: message.text, attachmentIds: message.attachment_ids, mode: "steer" });
1294
1527
  }, [queue.edit]);
1295
- const resumeQueue = React5.useCallback(async () => {
1528
+ const resumeQueue = React6.useCallback(async () => {
1296
1529
  if (!stateRef.current.activeThreadId) return;
1530
+ const isCurrent = ownOperation();
1297
1531
  const result = await queue.resume();
1298
- if (result?.run_id) void attachRun(result.run_id).catch(() => void 0);
1299
- }, [attachRun, queue.resume, stateRef]);
1532
+ if (isCurrent() && result?.run_id && stateRef.current.activeRunId !== result.run_id) void attachRun(result.run_id).catch(() => void 0);
1533
+ }, [attachRun, queue.resume, stateRef, ownOperation]);
1300
1534
  return { steer, editPendingMessage, steerPendingMessage, removePendingMessage: queue.remove, resumeQueue };
1301
1535
  }
1302
1536
 
@@ -1307,9 +1541,6 @@ function resolveInitialActiveThreadId(options, storedThreadId) {
1307
1541
  function shouldHydrateSelectedThread(selectedThreadId, activeThreadId, activeThreadIsHydrated) {
1308
1542
  return Boolean(selectedThreadId) && (selectedThreadId !== activeThreadId || !activeThreadIsHydrated);
1309
1543
  }
1310
- function isCurrentThreadOpenRequest(input) {
1311
- return !input.signalAborted && input.requestGeneration === input.currentGeneration && input.requestedThreadId === input.activeThreadId;
1312
- }
1313
1544
 
1314
1545
  // src/use-agent-chat.ts
1315
1546
  function activeRunIdFromThread(thread) {
@@ -1322,7 +1553,7 @@ function useAgentChat(options = {}) {
1322
1553
  const client = options.client || runtime.client;
1323
1554
  const pageSize = options.pageSize ?? 30;
1324
1555
  const threadPageSize = options.threadPageSize ?? 50;
1325
- const [state, setState] = React6.useState(() => ({
1556
+ const [state, setState] = React7.useState(() => ({
1326
1557
  ...initialAgentChatState,
1327
1558
  activeThreadId: resolveInitialActiveThreadId(
1328
1559
  options,
@@ -1330,34 +1561,40 @@ function useAgentChat(options = {}) {
1330
1561
  ),
1331
1562
  threads: runtime.storage?.loadThreads() ?? []
1332
1563
  }));
1333
- const [isLoadingHistory, setIsLoadingHistory] = React6.useState(false);
1334
- const [isLoadingOlder, setIsLoadingOlder] = React6.useState(false);
1335
- const [isSubmitting, setIsSubmitting] = React6.useState(false);
1336
- const streamControllerRef = React6.useRef(null);
1337
- const threadOpenControllerRef = React6.useRef(null);
1338
- const threadOpenGenerationRef = React6.useRef(0);
1339
- const attachRunRef = React6.useRef(async () => void 0);
1340
- const messageRefreshVersionRef = React6.useRef(0);
1341
- const promotedRunHintRef = React6.useRef(null);
1342
- const promotedAttachRef = React6.useRef(null);
1343
- const firstSubmissionRef = React6.useRef(null);
1344
- const firstSubmissionAdmittedRef = React6.useRef(null);
1345
- const stateRef = React6.useRef(state);
1564
+ const [isLoadingHistory, setIsLoadingHistory] = React7.useState(false);
1565
+ const [isLoadingOlder, setIsLoadingOlder] = React7.useState(false);
1566
+ const [isSubmitting, setIsSubmitting] = React7.useState(false);
1567
+ const streamControllerRef = React7.useRef(null);
1568
+ const threadOpenControllerRef = React7.useRef(null);
1569
+ const threadOpenGenerationRef = React7.useRef(0);
1570
+ const attachRunRef = React7.useRef(async () => void 0);
1571
+ const messageRefreshVersionRef = React7.useRef(0);
1572
+ const promotedRunHintRef = React7.useRef(null);
1573
+ const promotedAttachRef = React7.useRef(null);
1574
+ const olderRequestRef = React7.useRef(null);
1575
+ const submissionGenerationRef = React7.useRef(0);
1576
+ const firstSubmissionRef = React7.useRef(null);
1577
+ const firstSubmissionAdmittedRef = React7.useRef(null);
1578
+ const stateRef = React7.useRef(state);
1346
1579
  stateRef.current = state;
1347
- const persist = React6.useCallback((next) => {
1580
+ const ownOperation = React7.useCallback((options2) => captureChatOperation(() => ({
1581
+ generation: threadOpenGenerationRef.current,
1582
+ threadId: stateRef.current.activeThreadId,
1583
+ runId: stateRef.current.activeRunId,
1584
+ stream: streamControllerRef.current
1585
+ }), options2), []);
1586
+ const persist = React7.useCallback((next) => {
1348
1587
  runtime.storage?.saveThreads(next.threads);
1349
1588
  runtime.storage?.saveActiveThreadId?.(next.activeThreadId);
1350
1589
  }, [runtime.storage]);
1351
- const update = React6.useCallback((updater) => {
1352
- setState((current) => {
1353
- const next = updater(current);
1354
- stateRef.current = next;
1355
- persist(next);
1356
- return next;
1357
- });
1590
+ const update = React7.useCallback((updater) => {
1591
+ const next = updater(stateRef.current);
1592
+ stateRef.current = next;
1593
+ persist(next);
1594
+ setState(next);
1358
1595
  }, [persist]);
1359
- const queueTransport = React6.useMemo(() => ({
1360
- list: (threadId) => client.threads.messages.list({ threadId, limit: pageSize }),
1596
+ const queueTransport = React7.useMemo(() => ({
1597
+ list: (threadId, signal) => client.threads.messages.list({ threadId, limit: pageSize, signal }),
1361
1598
  submit: (input) => client.threads.messages.submit(input),
1362
1599
  edit: (message, input) => client.threads.messages.edit({
1363
1600
  threadId: message.thread_id,
@@ -1374,40 +1611,46 @@ function useAgentChat(options = {}) {
1374
1611
  },
1375
1612
  resume: (threadId) => client.threads.resumeQueue({ threadId })
1376
1613
  }), [client, pageSize]);
1377
- const onQueuePromotion = React6.useCallback((message, runId) => {
1378
- if (stateRef.current.activeThreadId !== message.thread_id) return;
1614
+ const attachPromotion = React7.useCallback((runId, threadId) => {
1615
+ if (stateRef.current.activeThreadId !== threadId) return;
1379
1616
  promotedRunHintRef.current = runId;
1617
+ if (stateRef.current.activeRunId === runId || promotedAttachRef.current === runId) return;
1618
+ promotedAttachRef.current = runId;
1619
+ const isCurrent = ownOperation();
1620
+ void attachRunRef.current(runId, 0).catch(() => {
1621
+ if (isCurrent() && promotedAttachRef.current === runId) promotedAttachRef.current = null;
1622
+ });
1623
+ }, [ownOperation]);
1624
+ const onQueuePromotion = React7.useCallback((message, runId) => {
1625
+ if (stateRef.current.activeThreadId !== message.thread_id) return;
1380
1626
  const userMessage = threadTimelineToMessages([{ ...message, run_id: runId, status: "delivered" }])[0];
1381
1627
  update((current) => current.activeThreadId === message.thread_id ? { ...current, messages: promoteThreadMessage(current.messages, userMessage, runId) } : current);
1382
- if (stateRef.current.activeRunId !== runId && promotedAttachRef.current !== runId) {
1383
- promotedAttachRef.current = runId;
1384
- void attachRunRef.current(runId).catch(() => {
1385
- promotedAttachRef.current = null;
1386
- });
1387
- }
1388
- }, [update]);
1628
+ attachPromotion(runId, message.thread_id);
1629
+ }, [update, attachPromotion]);
1389
1630
  const queue = useThreadMessageQueue({
1390
1631
  threadId: state.activeThreadId,
1391
1632
  transport: queueTransport,
1633
+ ownOperation,
1392
1634
  onPromoted: onQueuePromotion,
1635
+ onUnknownPromotion: attachPromotion,
1393
1636
  onOptimistic: (message, uploading) => update((current) => optimisticSubmissionState(current, message, uploading)),
1394
1637
  onDiscard: (message) => update((current) => discardOptimisticSubmission(current, message))
1395
1638
  });
1396
1639
  const { pendingMessages, messageQueue } = queue;
1397
- const eventBuffer = React6.useMemo(
1640
+ const eventBuffer = React7.useMemo(
1398
1641
  () => createRuntimeEventFrameBuffer(
1399
1642
  (events) => update((current) => applyRuntimeEvents(current, events)),
1400
1643
  runtimeEventFrameScheduler()
1401
1644
  ),
1402
1645
  [update]
1403
1646
  );
1404
- const clearOperationError = React6.useCallback((operation) => {
1647
+ const clearOperationError = React7.useCallback((operation) => {
1405
1648
  update((current) => clearAgentChatOperationError(current, operation));
1406
1649
  }, [update]);
1407
- const clearBackgroundError = React6.useCallback(() => {
1650
+ const clearBackgroundError = React7.useCallback(() => {
1408
1651
  update(clearAgentChatBackgroundError);
1409
1652
  }, [update]);
1410
- const failOperation = React6.useCallback((operation, cause) => {
1653
+ const failOperation = React7.useCallback((operation, cause) => {
1411
1654
  update((current) => withAgentChatOperationError(current, operation, cause));
1412
1655
  }, [update]);
1413
1656
  const {
@@ -1418,60 +1661,67 @@ function useAgentChat(options = {}) {
1418
1661
  refreshThreads,
1419
1662
  loadMoreThreads
1420
1663
  } = useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation });
1421
- const invalidateThreadOpen = React6.useCallback(() => {
1664
+ const invalidateThreadOpen = React7.useCallback(() => {
1422
1665
  threadOpenGenerationRef.current += 1;
1423
1666
  threadOpenControllerRef.current?.abort("Thread selection changed.");
1424
1667
  threadOpenControllerRef.current = null;
1668
+ messageRefreshVersionRef.current += 1;
1669
+ promotedAttachRef.current = null;
1670
+ promotedRunHintRef.current = null;
1671
+ olderRequestRef.current = null;
1672
+ firstSubmissionRef.current = null;
1673
+ firstSubmissionAdmittedRef.current = null;
1674
+ setIsSubmitting(false);
1675
+ setIsLoadingOlder(false);
1425
1676
  setIsLoadingHistory(false);
1426
1677
  }, []);
1427
- const detach = React6.useCallback(() => {
1678
+ const detach = React7.useCallback(() => {
1428
1679
  eventBuffer.flush();
1429
1680
  streamControllerRef.current?.abort();
1430
1681
  streamControllerRef.current = null;
1431
1682
  update((current) => ({ ...current, streamingMessageId: null, runState: current.activeRunId ? "reconnecting" : "idle" }));
1432
1683
  }, [eventBuffer, update]);
1433
- const refreshMessages = React6.useCallback(async (threadId, promotedRunId) => {
1684
+ const refreshMessagesRef = React7.useRef(async () => void 0);
1685
+ const { setFeedback, captureRead: captureFeedbackRead } = useAgentChatFeedback({
1686
+ client,
1687
+ update,
1688
+ clearOperationError,
1689
+ failOperation,
1690
+ ownOperation,
1691
+ refreshMessages: React7.useCallback(() => refreshMessagesRef.current(), [])
1692
+ });
1693
+ const refreshMessages = React7.useCallback(async (threadId, promotedRunId, signal) => {
1434
1694
  const target = threadId || stateRef.current.activeThreadId;
1435
1695
  if (!target) return;
1696
+ const isCurrent = ownOperation();
1697
+ const reconcileFeedback = captureFeedbackRead();
1436
1698
  const version = ++messageRefreshVersionRef.current;
1437
- const page = await client.threads.messages.list({ threadId: target, limit: pageSize });
1438
- if (stateRef.current.activeThreadId !== target || version !== messageRefreshVersionRef.current) return;
1699
+ const page = await client.threads.messages.list({ threadId: target, limit: pageSize, ...signal ? { signal } : {} });
1700
+ if (!isCurrent() || stateRef.current.activeThreadId !== target || version !== messageRefreshVersionRef.current) return;
1439
1701
  queue.hydrate(page, target);
1440
1702
  const promoted = promotedRunId || promotedRunHintRef.current;
1441
- if (promoted && page.items.some((message) => message.role === "user" && message.run_id === promoted)) {
1442
- promotedRunHintRef.current = null;
1443
- update((current) => {
1444
- if (current.activeThreadId !== target) return current;
1445
- return {
1446
- ...current,
1447
- messages: reconcileThreadTimeline(current.messages, threadTimelineToMessages(page.items), current.activeRunId, current.streamingMessageId)
1448
- };
1449
- });
1450
- return;
1451
- }
1452
- if (stateRef.current.activeRunId) {
1453
- const delivered = threadTimelineToMessages(page.items.filter((item) => item.role === "user" && item.run_id === stateRef.current.activeRunId && (item.segment_index ?? 0) > 0));
1454
- if (delivered.length) update((current) => {
1455
- if (current.activeThreadId !== target) return current;
1456
- const additions = delivered.filter((item) => !current.messages.some((existing) => existing.id === item.id));
1457
- return additions.length ? { ...current, messages: [...current.messages, ...additions] } : current;
1458
- });
1459
- }
1460
- if (!stateRef.current.activeRunId && page.items.length) {
1461
- update((current) => current.activeThreadId === target && !current.activeRunId ? (() => {
1462
- const messages = reconcileThreadTimeline(current.messages, threadTimelineToMessages(page.items), null, null);
1463
- return { ...current, messages, threads: current.threads.map((thread) => thread.id === target ? { ...thread, messages } : thread) };
1464
- })() : current);
1465
- }
1466
- }, [client, pageSize, queue.hydrate, update]);
1467
- React6.useEffect(() => () => {
1703
+ if (promoted && page.items.some((message) => message.run_id === promoted)) promotedRunHintRef.current = null;
1704
+ update((current) => {
1705
+ if (!isCurrent() || current.activeThreadId !== target) return current;
1706
+ const messages = reconcileThreadTimeline(
1707
+ current.messages,
1708
+ reconcileFeedback(threadTimelineToMessages(page.items), current.messages),
1709
+ current.activeRunId,
1710
+ current.streamingMessageId
1711
+ );
1712
+ return { ...current, messages, threads: current.threads.map((thread) => thread.id === target ? { ...thread, messages } : thread) };
1713
+ });
1714
+ }, [client, ownOperation, pageSize, queue.hydrate, update, captureFeedbackRead]);
1715
+ refreshMessagesRef.current = refreshMessages;
1716
+ React7.useEffect(() => () => {
1717
+ threadOpenGenerationRef.current += 1;
1468
1718
  eventBuffer.dispose();
1469
1719
  streamControllerRef.current?.abort();
1470
1720
  threadOpenControllerRef.current?.abort();
1471
1721
  }, [eventBuffer]);
1472
- const openThread = React6.useCallback(async (threadId) => {
1722
+ const openThread = React7.useCallback(async (threadId) => {
1473
1723
  const previousSelection = captureAgentChatThreadSelection(stateRef.current);
1474
- threadOpenControllerRef.current?.abort();
1724
+ invalidateThreadOpen();
1475
1725
  const requestController = runtime.createAbortController();
1476
1726
  threadOpenControllerRef.current = requestController;
1477
1727
  const requestGeneration = threadOpenGenerationRef.current + 1;
@@ -1491,6 +1741,8 @@ function useAgentChat(options = {}) {
1491
1741
  runState: "idle",
1492
1742
  streamingMessageId: null
1493
1743
  }));
1744
+ const isCurrent = ownOperation();
1745
+ const reconcileFeedback = captureFeedbackRead();
1494
1746
  setIsLoadingHistory(true);
1495
1747
  try {
1496
1748
  const detail = await client.threads.get({
@@ -1498,39 +1750,21 @@ function useAgentChat(options = {}) {
1498
1750
  limit: pageSize,
1499
1751
  signal: requestController.signal
1500
1752
  });
1501
- if (!isCurrentThreadOpenRequest({
1502
- activeThreadId: stateRef.current.activeThreadId,
1503
- currentGeneration: threadOpenGenerationRef.current,
1504
- requestGeneration,
1505
- requestedThreadId: threadId,
1506
- signalAborted: requestController.signal.aborted
1507
- })) return;
1753
+ if (!isCurrent()) return;
1508
1754
  const timeline = await client.threads.messages.list({ threadId, limit: pageSize, signal: requestController.signal });
1509
- if (!isCurrentThreadOpenRequest({
1510
- activeThreadId: stateRef.current.activeThreadId,
1511
- currentGeneration: threadOpenGenerationRef.current,
1512
- requestGeneration,
1513
- requestedThreadId: threadId,
1514
- signalAborted: requestController.signal.aborted
1515
- })) return;
1755
+ if (!isCurrent()) return;
1516
1756
  queue.hydrate(timeline, threadId);
1517
- const messages = timeline.items.length ? threadTimelineToMessages(timeline.items, detail) : threadDetailToMessages(detail);
1757
+ const messages = reconcileFeedback(threadTimelineToMessages(timeline.items), stateRef.current.messages);
1518
1758
  const context = latestThreadContext(detail);
1519
1759
  const stored = {
1520
1760
  ...detail,
1521
1761
  messages,
1522
1762
  isHydrated: true,
1523
- hasOlderTurns: Boolean(detail.paging?.has_more),
1524
- nextBeforeTurnIndex: detail.paging?.next_before_turn_index ?? null,
1763
+ hasOlderTurns: Boolean(timeline.paging.has_more),
1764
+ nextBeforeTurnIndex: timeline.paging.next_before_turn_index,
1525
1765
  ...context
1526
1766
  };
1527
- update((current) => isCurrentThreadOpenRequest({
1528
- activeThreadId: current.activeThreadId,
1529
- currentGeneration: threadOpenGenerationRef.current,
1530
- requestGeneration,
1531
- requestedThreadId: threadId,
1532
- signalAborted: requestController.signal.aborted
1533
- }) ? {
1767
+ update((current) => isCurrent() ? {
1534
1768
  ...current,
1535
1769
  activeThreadId: threadId,
1536
1770
  messages,
@@ -1540,13 +1774,7 @@ function useAgentChat(options = {}) {
1540
1774
  threads: current.threads.some((thread) => thread.id === threadId) ? current.threads.map((thread) => thread.id === threadId ? stored : thread) : [stored, ...current.threads]
1541
1775
  } : current);
1542
1776
  const activeRunId = activeRunIdFromThread(stored);
1543
- if (activeRunId && isCurrentThreadOpenRequest({
1544
- activeThreadId: stateRef.current.activeThreadId,
1545
- currentGeneration: threadOpenGenerationRef.current,
1546
- requestGeneration,
1547
- requestedThreadId: threadId,
1548
- signalAborted: requestController.signal.aborted
1549
- })) await attachRunRef.current(activeRunId).catch(() => void 0);
1777
+ if (activeRunId && isCurrent()) void attachRunRef.current(activeRunId).catch(() => void 0);
1550
1778
  } catch (error) {
1551
1779
  if (!requestController.signal.aborted && !isAbortLike(error) && requestGeneration === threadOpenGenerationRef.current) {
1552
1780
  failOperation("open_thread", error);
@@ -1562,28 +1790,34 @@ function useAgentChat(options = {}) {
1562
1790
  setIsLoadingHistory(false);
1563
1791
  }
1564
1792
  }
1565
- }, [clearOperationError, client, detach, failOperation, pageSize, queue.hydrate, runtime, update]);
1566
- const consumeEvent = React6.useCallback((event) => {
1793
+ }, [clearOperationError, client, detach, failOperation, pageSize, queue.hydrate, runtime, update, invalidateThreadOpen, ownOperation, captureFeedbackRead]);
1794
+ const consumeEvent = React7.useCallback((event) => {
1567
1795
  const eventThreadId = typeof event.payload?.thread_id === "string" ? event.payload.thread_id.trim() : "";
1568
1796
  if (eventThreadId && !stateRef.current.activeThreadId) {
1569
1797
  update((current) => adoptStreamingThread(current, eventThreadId));
1798
+ setIsSubmitting(false);
1570
1799
  firstSubmissionAdmittedRef.current?.();
1571
1800
  }
1572
1801
  eventBuffer.push(event);
1573
1802
  }, [eventBuffer, update]);
1574
- const attachRun = React6.useCallback(async (runId, cursor) => {
1803
+ const attachRun = React7.useCallback(async (runId, cursor) => {
1804
+ eventBuffer.flush();
1575
1805
  streamControllerRef.current?.abort();
1576
1806
  const controller = runtime.createAbortController();
1577
1807
  streamControllerRef.current = controller;
1578
- clearOperationError();
1808
+ const isCurrent = ownOperation({ stream: true });
1809
+ clearOperationError("attach_run");
1579
1810
  update((current) => ({ ...current, activeRunId: runId, runState: "reconnecting", error: null }));
1580
1811
  try {
1581
- const result = await client.runs.attach({ runId, cursor, signal: controller.signal }, consumeEvent);
1812
+ const result = await client.runs.attach({ runId, cursor, leaseSeconds: 60, signal: controller.signal }, (event) => {
1813
+ if (isCurrent()) consumeEvent(event);
1814
+ });
1815
+ if (!isCurrent()) return;
1582
1816
  eventBuffer.flush();
1583
1817
  update((current) => ({ ...current, activeRunId: result.detached ? runId : current.activeRunId, cursor: result.cursor ?? current.cursor, runState: result.detached ? "reconnecting" : current.runState }));
1584
1818
  if (!result.detached && result.threadId) await refreshMessages(result.threadId);
1585
1819
  } catch (error) {
1586
- if (!isAbortLike(error)) {
1820
+ if (isCurrent() && !isAbortLike(error)) {
1587
1821
  update((current) => ({ ...current, runState: "failed", streamingMessageId: null }));
1588
1822
  failOperation("attach_run", error);
1589
1823
  throw error;
@@ -1591,15 +1825,17 @@ function useAgentChat(options = {}) {
1591
1825
  } finally {
1592
1826
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
1593
1827
  }
1594
- }, [clearOperationError, client, consumeEvent, eventBuffer, failOperation, refreshMessages, runtime, update]);
1828
+ }, [clearOperationError, client, consumeEvent, eventBuffer, failOperation, refreshMessages, runtime, update, ownOperation]);
1595
1829
  attachRunRef.current = attachRun;
1596
- const submit = React6.useCallback(async (input) => {
1830
+ const submit = React7.useCallback(async (input) => {
1597
1831
  if (!input.text.trim() && !input.attachmentIds?.length && !input.uploads?.length) return;
1598
1832
  if (firstSubmissionRef.current && !stateRef.current.activeThreadId) {
1599
1833
  await firstSubmissionRef.current;
1600
1834
  }
1601
1835
  const existingThreadId = stateRef.current.activeThreadId;
1602
1836
  if (existingThreadId) {
1837
+ const isCurrent = ownOperation();
1838
+ const submissionGeneration = ++submissionGenerationRef.current;
1603
1839
  clearOperationError();
1604
1840
  setIsSubmitting(true);
1605
1841
  try {
@@ -1644,21 +1880,23 @@ function useAgentChat(options = {}) {
1644
1880
  }
1645
1881
  });
1646
1882
  } catch (error) {
1647
- failOperation("submit", error);
1883
+ if (isCurrent()) failOperation("submit", error);
1648
1884
  throw error;
1649
1885
  } finally {
1650
- setIsSubmitting(false);
1886
+ if (isCurrent() && submissionGeneration === submissionGenerationRef.current) setIsSubmitting(false);
1651
1887
  }
1652
1888
  return;
1653
1889
  }
1890
+ invalidateThreadOpen();
1654
1891
  const admission = new Promise((resolve) => {
1655
1892
  firstSubmissionAdmittedRef.current = resolve;
1656
1893
  });
1657
1894
  const stream = (async () => {
1658
- invalidateThreadOpen();
1659
1895
  detach();
1660
1896
  const controller = runtime.createAbortController();
1661
1897
  streamControllerRef.current = controller;
1898
+ const isCurrent = ownOperation({ stream: true, adoptThread: true });
1899
+ const submissionGeneration = ++submissionGenerationRef.current;
1662
1900
  const ticket = queue.beginSubmission({
1663
1901
  input: input.text.trim(),
1664
1902
  clientSubmissionId: runtime.createId(),
@@ -1689,6 +1927,7 @@ function useAgentChat(options = {}) {
1689
1927
  idempotencyKey: runtime.createId(),
1690
1928
  signal: controller.signal
1691
1929
  }) : [];
1930
+ if (!isCurrent()) return;
1692
1931
  if (hasPendingUploads && uploaded.length !== input.uploads?.length) {
1693
1932
  throw new Error("Attachment upload returned an incomplete batch");
1694
1933
  }
@@ -1701,7 +1940,20 @@ function useAgentChat(options = {}) {
1701
1940
  runState: "streaming"
1702
1941
  }));
1703
1942
  }
1704
- const result = await client.chat.stream({ clientSubmissionId: ticket.client_submission_id, input: input.text.trim(), threadId: stateRef.current.activeThreadId || void 0, attachmentIds, requestedModelId: input.requestedModelId, requestedAgentId: input.requestedAgentId, idempotencyKey, signal: controller.signal }, consumeEvent);
1943
+ const result = await client.chat.stream({
1944
+ leaseSeconds: 60,
1945
+ clientSubmissionId: ticket.client_submission_id,
1946
+ input: input.text.trim(),
1947
+ threadId: stateRef.current.activeThreadId || void 0,
1948
+ attachmentIds,
1949
+ requestedModelId: input.requestedModelId,
1950
+ requestedAgentId: input.requestedAgentId,
1951
+ idempotencyKey,
1952
+ signal: controller.signal
1953
+ }, (event) => {
1954
+ if (isCurrent()) consumeEvent(event);
1955
+ });
1956
+ if (!isCurrent()) return;
1705
1957
  eventBuffer.flush();
1706
1958
  update((current) => {
1707
1959
  const threadId = result.threadId || current.activeThreadId;
@@ -1717,6 +1969,7 @@ function useAgentChat(options = {}) {
1717
1969
  if (result.threadId) await refreshThreads().catch(() => void 0);
1718
1970
  if (result.threadId && !result.detached) await refreshMessages(result.threadId);
1719
1971
  } catch (error) {
1972
+ if (!isCurrent()) return;
1720
1973
  const admitted = Boolean(stateRef.current.messages.find((message) => message.id === userId)?.runId);
1721
1974
  if (!admitted) queue.discardSubmission(ticket);
1722
1975
  if (!isAbortLike(error)) {
@@ -1729,7 +1982,7 @@ function useAgentChat(options = {}) {
1729
1982
  throw error;
1730
1983
  }
1731
1984
  } finally {
1732
- setIsSubmitting(false);
1985
+ if (isCurrent() && submissionGeneration === submissionGenerationRef.current) setIsSubmitting(false);
1733
1986
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
1734
1987
  }
1735
1988
  })();
@@ -1738,10 +1991,12 @@ function useAgentChat(options = {}) {
1738
1991
  try {
1739
1992
  await accepted;
1740
1993
  } finally {
1741
- if (firstSubmissionRef.current === accepted) firstSubmissionRef.current = null;
1742
- firstSubmissionAdmittedRef.current = null;
1994
+ if (firstSubmissionRef.current === accepted) {
1995
+ firstSubmissionRef.current = null;
1996
+ firstSubmissionAdmittedRef.current = null;
1997
+ }
1743
1998
  }
1744
- }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update]);
1999
+ }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update, ownOperation]);
1745
2000
  const { steer, editPendingMessage, steerPendingMessage, removePendingMessage, resumeQueue } = useAgentChatQueueActions({
1746
2001
  queue,
1747
2002
  stateRef,
@@ -1749,42 +2004,38 @@ function useAgentChat(options = {}) {
1749
2004
  clearOperationError,
1750
2005
  failOperation,
1751
2006
  refreshMessages,
1752
- attachRun
2007
+ attachRun,
2008
+ ownOperation
1753
2009
  });
1754
- const startNewThread = React6.useCallback(() => {
2010
+ const startNewThread = React7.useCallback(() => {
1755
2011
  invalidateThreadOpen();
1756
2012
  detach();
1757
2013
  update((current) => ({ ...initialAgentChatState, threads: current.threads }));
1758
2014
  }, [detach, invalidateThreadOpen, update]);
1759
- const deleteThread = React6.useCallback(async (threadId) => {
1760
- if (stateRef.current.activeThreadId === threadId) invalidateThreadOpen();
2015
+ const deleteThread = React7.useCallback(async (threadId) => {
2016
+ const isCurrent = ownOperation();
1761
2017
  clearOperationError();
1762
2018
  try {
1763
2019
  await client.threads.delete({ threadId, idempotencyKey: runtime.createId() });
1764
- update((current) => ({
1765
- ...current,
1766
- activeThreadId: current.activeThreadId === threadId ? null : current.activeThreadId,
1767
- messages: current.activeThreadId === threadId ? [] : current.messages,
1768
- contextWindow: current.activeThreadId === threadId ? null : current.contextWindow,
1769
- contextCompression: current.activeThreadId === threadId ? null : current.contextCompression,
1770
- contextCompressionRevision: current.activeThreadId === threadId ? 0 : current.contextCompressionRevision,
1771
- threads: current.threads.filter((thread) => thread.id !== threadId)
1772
- }));
2020
+ if (stateRef.current.activeThreadId === threadId) startNewThread();
2021
+ update((current) => ({ ...current, threads: current.threads.filter((thread) => thread.id !== threadId) }));
1773
2022
  } catch (error) {
1774
2023
  if (!isAbortLike(error)) {
1775
- failOperation("delete_thread", error);
2024
+ if (isCurrent()) failOperation("delete_thread", error);
1776
2025
  throw error;
1777
2026
  }
1778
2027
  }
1779
- }, [clearOperationError, client, failOperation, invalidateThreadOpen, runtime, update]);
1780
- const cancelRun = React6.useCallback(async () => {
2028
+ }, [clearOperationError, client, failOperation, ownOperation, runtime, startNewThread, update]);
2029
+ const cancelRun = React7.useCallback(async () => {
1781
2030
  const runId = stateRef.current.activeRunId;
1782
2031
  if (!runId || stateRef.current.runState === "cancelling") return;
2032
+ const isCurrent = ownOperation({ run: true, stream: true });
1783
2033
  const previousRunState = stateRef.current.runState;
1784
2034
  clearOperationError();
1785
2035
  update((current) => current.activeRunId === runId ? { ...current, runState: "cancelling" } : current);
1786
2036
  try {
1787
2037
  const result = await client.runs.cancel({ runId, idempotencyKey: runtime.createId() });
2038
+ if (!isCurrent()) return;
1788
2039
  if (result.status === "cancelled") {
1789
2040
  detach();
1790
2041
  update((current) => ({
@@ -1809,14 +2060,15 @@ function useAgentChat(options = {}) {
1809
2060
  }));
1810
2061
  }
1811
2062
  } catch (error) {
1812
- if (!isAbortLike(error)) {
2063
+ if (isCurrent() && !isAbortLike(error)) {
1813
2064
  update((current) => current.activeRunId === runId ? { ...current, runState: previousRunState } : current);
1814
2065
  failOperation("cancel_run", error);
1815
2066
  throw error;
1816
2067
  }
1817
2068
  }
1818
- }, [clearOperationError, client, detach, failOperation, runtime, update]);
1819
- const resumeHitl = React6.useCallback(async (part, response) => {
2069
+ }, [clearOperationError, client, detach, failOperation, runtime, update, ownOperation]);
2070
+ const resumeHitl = React7.useCallback(async (part, response) => {
2071
+ const isCurrent = ownOperation({ run: true, stream: true });
1820
2072
  const runId = stateRef.current.activeRunId || stateRef.current.messages.find((message) => message.parts.includes(part))?.runId;
1821
2073
  clearOperationError();
1822
2074
  if (!runId) {
@@ -1831,32 +2083,52 @@ function useAgentChat(options = {}) {
1831
2083
  ...response,
1832
2084
  idempotencyKey: runtime.createId()
1833
2085
  });
1834
- await attachRun(result.run_id || runId);
2086
+ if (isCurrent()) await attachRun(result.run_id || runId);
1835
2087
  } catch (error) {
1836
- if (!isAbortLike(error)) failOperation("resume_hitl", error);
2088
+ if (isCurrent() && !isAbortLike(error)) failOperation("resume_hitl", error);
1837
2089
  throw error;
1838
2090
  }
1839
- }, [attachRun, clearOperationError, client, failOperation, runtime]);
1840
- const setFeedback = useAgentChatFeedback({ client, update, clearOperationError, failOperation });
1841
- const loadOlder = React6.useCallback(async () => {
2091
+ }, [attachRun, clearOperationError, client, failOperation, runtime, ownOperation]);
2092
+ const loadOlder = React7.useCallback(() => {
1842
2093
  const thread = stateRef.current.threads.find((item) => item.id === stateRef.current.activeThreadId);
1843
- if (!thread?.nextBeforeTurnIndex) return;
2094
+ if (thread?.nextBeforeTurnIndex == null) return Promise.resolve();
2095
+ const cursor = thread.nextBeforeTurnIndex;
2096
+ const reconcileFeedback = captureFeedbackRead();
2097
+ const key = `${threadOpenGenerationRef.current}:${thread.id}:${cursor}`;
2098
+ if (olderRequestRef.current?.key === key) return olderRequestRef.current.promise;
2099
+ const isCurrent = ownOperation();
1844
2100
  clearOperationError();
1845
2101
  setIsLoadingOlder(true);
1846
- try {
1847
- const detail = await client.threads.get({ threadId: thread.id, limit: pageSize, beforeTurnIndex: thread.nextBeforeTurnIndex });
1848
- const older = threadDetailToMessages(detail);
1849
- update((current) => ({ ...current, messages: [...older, ...current.messages.filter((message) => !older.some((item) => item.id === message.id))], threads: current.threads.map((item) => item.id === thread.id ? { ...item, hasOlderTurns: Boolean(detail.paging?.has_more), nextBeforeTurnIndex: detail.paging?.next_before_turn_index ?? null } : item) }));
1850
- } catch (error) {
1851
- if (!isAbortLike(error)) {
1852
- failOperation("load_older_turns", error);
1853
- throw error;
2102
+ const promise = (async () => {
2103
+ try {
2104
+ const page = await client.threads.messages.list({ threadId: thread.id, limit: pageSize, before: cursor });
2105
+ if (!isCurrent()) return;
2106
+ const older = reconcileFeedback(threadTimelineToMessages(page.items), stateRef.current.messages);
2107
+ update((current) => ({
2108
+ ...current,
2109
+ messages: prependThreadTimeline(current.messages, older),
2110
+ threads: current.threads.map((item) => item.id === thread.id ? {
2111
+ ...item,
2112
+ hasOlderTurns: page.paging.has_more,
2113
+ nextBeforeTurnIndex: page.paging.next_before_turn_index
2114
+ } : item)
2115
+ }));
2116
+ } catch (error) {
2117
+ if (isCurrent() && !isAbortLike(error)) {
2118
+ failOperation("load_older_turns", error);
2119
+ throw error;
2120
+ }
2121
+ } finally {
2122
+ if (olderRequestRef.current?.key === key) {
2123
+ olderRequestRef.current = null;
2124
+ setIsLoadingOlder(false);
2125
+ }
1854
2126
  }
1855
- } finally {
1856
- setIsLoadingOlder(false);
1857
- }
1858
- }, [clearOperationError, client, failOperation, pageSize, update]);
1859
- const regenerate = React6.useCallback(async (message) => {
2127
+ })();
2128
+ olderRequestRef.current = { key, promise };
2129
+ return promise;
2130
+ }, [clearOperationError, client, failOperation, ownOperation, pageSize, update, captureFeedbackRead]);
2131
+ const regenerate = React7.useCallback(async (message) => {
1860
2132
  const index = stateRef.current.messages.findIndex((item) => item.id === message.id);
1861
2133
  const user = stateRef.current.messages.slice(0, index).reverse().find((item) => item.role === "user");
1862
2134
  if (user) await submit({
@@ -1865,51 +2137,68 @@ function useAgentChat(options = {}) {
1865
2137
  attachments: user.attachments
1866
2138
  });
1867
2139
  }, [submit]);
1868
- React6.useEffect(() => {
1869
- const controller = runtime.createAbortController();
1870
- clearBackgroundError();
1871
- void subscribeToThreadSummaries({
1872
- client,
1873
- signal: controller.signal,
1874
- refresh: refreshThreads,
1875
- onEvent(event) {
1876
- threadEventsDuringRefreshRef.current?.push(event);
1877
- update((current) => ({
1878
- ...clearAgentChatBackgroundError(current),
1879
- threads: applyThreadSummaryEvent(current.threads, event)
1880
- }));
1881
- if (event.event === "thread.message.updated" && event.thread_id === stateRef.current.activeThreadId) {
1882
- const wasQueued = queue.acceptNotice({
1883
- messageId: event.message_id,
1884
- status: event.status,
1885
- runId: event.run_id,
1886
- cursor: event.cursor,
1887
- clientSubmissionId: event.client_submission_id
1888
- });
1889
- if (wasQueued) void refreshMessages(event.thread_id, event.run_id || void 0).catch(() => void 0);
1890
- }
1891
- if (event.event === "thread.summary.updated" && event.thread.id === stateRef.current.activeThreadId) {
1892
- if (streamControllerRef.current && stateRef.current.messages.some((message) => message.role === "user" && !message.runId)) return;
1893
- const promotedRunId = activeRunIdFromThread(summaryToStored(event.thread));
1894
- void (async () => {
1895
- await refreshMessages(String(event.thread.id), promotedRunId || void 0);
1896
- if (promotedRunId && stateRef.current.activeRunId !== promotedRunId && stateRef.current.activeThreadId === String(event.thread.id) && promotedAttachRef.current !== promotedRunId) {
1897
- promotedAttachRef.current = promotedRunId;
1898
- await attachRunRef.current(promotedRunId);
1899
- }
1900
- })().catch(() => void 0);
1901
- }
1902
- },
1903
- onError(error) {
1904
- update((current) => withAgentChatBackgroundError(current, error));
1905
- },
1906
- onRecovered() {
1907
- clearBackgroundError();
2140
+ useChatObservation({
2141
+ state: {
2142
+ threadId: state.activeThreadId,
2143
+ running: Boolean(state.activeRunId) && !["idle", "completed", "failed", "cancelled"].includes(state.runState),
2144
+ waiting: ["paused", "waiting", "interrupted"].includes(state.runState),
2145
+ pending: pendingMessages.length > 0,
2146
+ paused: Boolean(messageQueue?.paused),
2147
+ degraded: state.runState === "reconnecting",
2148
+ mode: options.observation
2149
+ },
2150
+ visibility: runtime.visibility,
2151
+ createAbortController: runtime.createAbortController,
2152
+ events: (input, handler) => client.threads.events(input, handler),
2153
+ async refresh(signal) {
2154
+ const target = stateRef.current.activeThreadId;
2155
+ if (!target) return;
2156
+ const isCurrent = ownOperation();
2157
+ const detail = await client.threads.get({ threadId: target, limit: 1, signal });
2158
+ if (!isCurrent() || signal.aborted) return;
2159
+ const summary = summaryToStored(detail);
2160
+ update((current) => ({ ...current, threads: applyThreadSummaryEvent(current.threads, {
2161
+ schema_version: "agents24.thread_summary_event.v1",
2162
+ event: "thread.summary.updated",
2163
+ cursor: 0,
2164
+ thread: detail
2165
+ }) }));
2166
+ const runId = activeRunIdFromThread(summary);
2167
+ if (runId && !streamControllerRef.current) {
2168
+ void attachRunRef.current(runId, runId === stateRef.current.activeRunId ? stateRef.current.cursor ?? void 0 : 0).catch(() => void 0);
1908
2169
  }
1909
- });
1910
- return () => controller.abort();
1911
- }, [clearBackgroundError, client, queue.acceptNotice, refreshMessages, refreshThreads, runtime.createAbortController, update]);
1912
- React6.useEffect(() => {
2170
+ await refreshMessages(target, runId || void 0, signal);
2171
+ },
2172
+ onEvent(event) {
2173
+ if (event.event === "thread.deleted" && event.thread_id === stateRef.current.activeThreadId) startNewThread();
2174
+ threadEventsDuringRefreshRef.current?.push(event);
2175
+ update((current) => ({
2176
+ ...clearAgentChatBackgroundError(current),
2177
+ threads: applyThreadSummaryEvent(current.threads, event)
2178
+ }));
2179
+ if (event.event === "thread.message.updated" && event.thread_id === stateRef.current.activeThreadId) {
2180
+ queue.acceptNotice({
2181
+ messageId: event.message_id,
2182
+ status: event.status,
2183
+ runId: event.run_id,
2184
+ cursor: event.cursor,
2185
+ clientSubmissionId: event.client_submission_id
2186
+ });
2187
+ }
2188
+ },
2189
+ onError(error) {
2190
+ if (!isAbortLike(error)) update((current) => withAgentChatBackgroundError(current, error));
2191
+ },
2192
+ onRecovered: clearBackgroundError,
2193
+ onDetach: detach,
2194
+ onResume() {
2195
+ const current = stateRef.current;
2196
+ if (current.activeRunId && !streamControllerRef.current && !["paused", "waiting", "interrupted", "completed", "idle"].includes(current.runState)) {
2197
+ void attachRunRef.current(current.activeRunId, current.cursor ?? void 0).catch(() => void 0);
2198
+ }
2199
+ }
2200
+ });
2201
+ React7.useEffect(() => {
1913
2202
  const threadId = options.activeThreadId;
1914
2203
  const activeThread2 = stateRef.current.threads.find((thread) => thread.id === stateRef.current.activeThreadId);
1915
2204
  if (shouldHydrateSelectedThread(threadId, stateRef.current.activeThreadId, Boolean(activeThread2?.isHydrated))) {
@@ -1956,15 +2245,15 @@ function useAgentChat(options = {}) {
1956
2245
  }
1957
2246
 
1958
2247
  // src/use-agent-resources.ts
1959
- import * as React7 from "react";
2248
+ import * as React8 from "react";
1960
2249
  function useAgentThreads(options = {}) {
1961
2250
  const { client, createAbortController } = useAgents24Runtime();
1962
- const [threads, setThreads] = React7.useState([]);
1963
- const [total, setTotal] = React7.useState();
1964
- const [isLoading, setIsLoading] = React7.useState(true);
1965
- const [error, setError] = React7.useState(null);
1966
- const controllerRef = React7.useRef(null);
1967
- const load = React7.useCallback(async () => {
2251
+ const [threads, setThreads] = React8.useState([]);
2252
+ const [total, setTotal] = React8.useState();
2253
+ const [isLoading, setIsLoading] = React8.useState(true);
2254
+ const [error, setError] = React8.useState(null);
2255
+ const controllerRef = React8.useRef(null);
2256
+ const load = React8.useCallback(async () => {
1968
2257
  controllerRef.current?.abort();
1969
2258
  const controller = createAbortController();
1970
2259
  controllerRef.current = controller;
@@ -1983,7 +2272,7 @@ function useAgentThreads(options = {}) {
1983
2272
  }
1984
2273
  }
1985
2274
  }, [client, createAbortController, options.limit, options.skip]);
1986
- React7.useEffect(() => {
2275
+ React8.useEffect(() => {
1987
2276
  void load();
1988
2277
  return () => controllerRef.current?.abort();
1989
2278
  }, [load]);
@@ -1991,11 +2280,11 @@ function useAgentThreads(options = {}) {
1991
2280
  }
1992
2281
  function useAgentThread(threadId, options = {}) {
1993
2282
  const { client, createAbortController } = useAgents24Runtime();
1994
- const [thread, setThread] = React7.useState(null);
1995
- const [isLoading, setIsLoading] = React7.useState(Boolean(threadId));
1996
- const [error, setError] = React7.useState(null);
1997
- const controllerRef = React7.useRef(null);
1998
- const load = React7.useCallback(async () => {
2283
+ const [thread, setThread] = React8.useState(null);
2284
+ const [isLoading, setIsLoading] = React8.useState(Boolean(threadId));
2285
+ const [error, setError] = React8.useState(null);
2286
+ const controllerRef = React8.useRef(null);
2287
+ const load = React8.useCallback(async () => {
1999
2288
  if (!threadId) {
2000
2289
  controllerRef.current?.abort();
2001
2290
  controllerRef.current = null;
@@ -2022,7 +2311,7 @@ function useAgentThread(threadId, options = {}) {
2022
2311
  }
2023
2312
  }
2024
2313
  }, [client, createAbortController, options.beforeTurnIndex, options.limit, threadId]);
2025
- React7.useEffect(() => {
2314
+ React8.useEffect(() => {
2026
2315
  void load();
2027
2316
  return () => controllerRef.current?.abort();
2028
2317
  }, [load]);
@@ -2030,11 +2319,11 @@ function useAgentThread(threadId, options = {}) {
2030
2319
  }
2031
2320
  function useThreadComputer(threadId) {
2032
2321
  const { client, createAbortController } = useAgents24Runtime();
2033
- const [computer, setComputer] = React7.useState(null);
2034
- const [isLoading, setIsLoading] = React7.useState(Boolean(threadId));
2035
- const [error, setError] = React7.useState(null);
2036
- const controllerRef = React7.useRef(null);
2037
- const refresh = React7.useCallback(async () => {
2322
+ const [computer, setComputer] = React8.useState(null);
2323
+ const [isLoading, setIsLoading] = React8.useState(Boolean(threadId));
2324
+ const [error, setError] = React8.useState(null);
2325
+ const controllerRef = React8.useRef(null);
2326
+ const refresh = React8.useCallback(async () => {
2038
2327
  controllerRef.current?.abort();
2039
2328
  if (!threadId) {
2040
2329
  setComputer(null);
@@ -2059,19 +2348,19 @@ function useThreadComputer(threadId) {
2059
2348
  }
2060
2349
  }
2061
2350
  }, [client, createAbortController, threadId]);
2062
- React7.useEffect(() => {
2351
+ React8.useEffect(() => {
2063
2352
  void refresh();
2064
2353
  return () => controllerRef.current?.abort();
2065
2354
  }, [refresh]);
2066
- const list = React7.useCallback((input) => {
2355
+ const list = React8.useCallback((input) => {
2067
2356
  if (!threadId) throw new Error("A thread is required to list computer files");
2068
2357
  return client.threads.computer.list({ threadId, ...input });
2069
2358
  }, [client, threadId]);
2070
- const read = React7.useCallback((path) => {
2359
+ const read = React8.useCallback((path) => {
2071
2360
  if (!threadId) throw new Error("A thread is required to read a computer file");
2072
2361
  return client.threads.computer.read({ threadId, path });
2073
2362
  }, [client, threadId]);
2074
- const createContentAccess = React7.useCallback((path, download = false) => {
2363
+ const createContentAccess = React8.useCallback((path, download = false) => {
2075
2364
  if (!threadId) throw new Error("A thread is required to access a computer file");
2076
2365
  return client.threads.computer.createContentAccess({ threadId, path, download });
2077
2366
  }, [client, threadId]);
@@ -2079,16 +2368,16 @@ function useThreadComputer(threadId) {
2079
2368
  }
2080
2369
  function useAgentRun() {
2081
2370
  const { client, createAbortController, createId } = useAgents24Runtime();
2082
- const controllerRef = React7.useRef(null);
2083
- const [isAttached, setIsAttached] = React7.useState(false);
2084
- const [error, setError] = React7.useState(null);
2085
- React7.useEffect(() => () => controllerRef.current?.abort(), []);
2086
- const detach = React7.useCallback(() => {
2371
+ const controllerRef = React8.useRef(null);
2372
+ const [isAttached, setIsAttached] = React8.useState(false);
2373
+ const [error, setError] = React8.useState(null);
2374
+ React8.useEffect(() => () => controllerRef.current?.abort(), []);
2375
+ const detach = React8.useCallback(() => {
2087
2376
  controllerRef.current?.abort();
2088
2377
  controllerRef.current = null;
2089
2378
  setIsAttached(false);
2090
2379
  }, []);
2091
- const attach = React7.useCallback(async (runId, onEvent, cursor) => {
2380
+ const attach = React8.useCallback(async (runId, onEvent, cursor) => {
2092
2381
  detach();
2093
2382
  const controller = createAbortController();
2094
2383
  controllerRef.current = controller;
@@ -2104,7 +2393,7 @@ function useAgentRun() {
2104
2393
  setIsAttached(false);
2105
2394
  }
2106
2395
  }, [client, createAbortController, detach]);
2107
- const cancel = React7.useCallback(async (runId) => {
2396
+ const cancel = React8.useCallback(async (runId) => {
2108
2397
  const result = await client.runs.cancel({ runId, idempotencyKey: createId() });
2109
2398
  detach();
2110
2399
  return result;
@@ -2113,9 +2402,9 @@ function useAgentRun() {
2113
2402
  }
2114
2403
  function useAgentAttachments() {
2115
2404
  const { client, createId } = useAgents24Runtime();
2116
- const [uploads, setUploads] = React7.useState({});
2117
- const [isUploading, setIsUploading] = React7.useState(false);
2118
- const uploadBatch = React7.useCallback(async (input) => {
2405
+ const [uploads, setUploads] = React8.useState({});
2406
+ const [isUploading, setIsUploading] = React8.useState(false);
2407
+ const uploadBatch = React8.useCallback(async (input) => {
2119
2408
  setIsUploading(true);
2120
2409
  try {
2121
2410
  const results = await client.attachments.uploadBatch({ uploads: input.uploads, threadId: input.threadId, requestedAgentId: input.requestedAgentId, idempotencyKey: createId(), signal: input.signal });
@@ -2128,7 +2417,7 @@ function useAgentAttachments() {
2128
2417
  setIsUploading(false);
2129
2418
  }
2130
2419
  }, [client, createId]);
2131
- const createContentAccess = React7.useCallback(
2420
+ const createContentAccess = React8.useCallback(
2132
2421
  (input) => client.attachments.createContentAccess(input),
2133
2422
  [client]
2134
2423
  );
@@ -2136,9 +2425,9 @@ function useAgentAttachments() {
2136
2425
  }
2137
2426
  function useAgentHitl() {
2138
2427
  const { client, createId } = useAgents24Runtime();
2139
- const [isResolving, setIsResolving] = React7.useState(false);
2140
- const [lastResult, setLastResult] = React7.useState(null);
2141
- const resume = React7.useCallback(async (input) => {
2428
+ const [isResolving, setIsResolving] = React8.useState(false);
2429
+ const [lastResult, setLastResult] = React8.useState(null);
2430
+ const resume = React8.useCallback(async (input) => {
2142
2431
  setIsResolving(true);
2143
2432
  try {
2144
2433
  const result = await client.hitl.resume({ ...input, idempotencyKey: createId() });
@@ -2152,8 +2441,8 @@ function useAgentHitl() {
2152
2441
  }
2153
2442
  function useAgentMcp() {
2154
2443
  const { client, createId } = useAgents24Runtime();
2155
- const [isAuthorizing, setIsAuthorizing] = React7.useState(false);
2156
- const startAuthorization = React7.useCallback(async (input) => {
2444
+ const [isAuthorizing, setIsAuthorizing] = React8.useState(false);
2445
+ const startAuthorization = React8.useCallback(async (input) => {
2157
2446
  setIsAuthorizing(true);
2158
2447
  try {
2159
2448
  return await client.mcp.startAuthorization({ ...input, idempotencyKey: createId() });
@@ -2161,7 +2450,7 @@ function useAgentMcp() {
2161
2450
  setIsAuthorizing(false);
2162
2451
  }
2163
2452
  }, [client, createId]);
2164
- const redeemCallback = React7.useCallback(async (input) => client.mcp.redeemCallback({ ...input, idempotencyKey: createId() }), [client, createId]);
2453
+ const redeemCallback = React8.useCallback(async (input) => client.mcp.redeemCallback({ ...input, idempotencyKey: createId() }), [client, createId]);
2165
2454
  return { isAuthorizing, redeemCallback, startAuthorization };
2166
2455
  }
2167
2456
  export {
@@ -2174,11 +2463,13 @@ export {
2174
2463
  assistantProjectionFromEvent,
2175
2464
  attachmentResultToChatAttachment,
2176
2465
  captureAgentChatThreadSelection,
2466
+ captureChatOperation,
2177
2467
  chatAttachmentIds,
2178
2468
  clearAgentChatBackgroundError,
2179
2469
  clearAgentChatOperationError,
2180
2470
  computerResourceTargets,
2181
2471
  contextCompressionPartFromEvent,
2472
+ createChatObservation,
2182
2473
  createPortableId,
2183
2474
  createRuntimeEventFrameBuffer,
2184
2475
  discardOptimisticSubmission,
@@ -2191,6 +2482,7 @@ export {
2191
2482
  optimisticSubmissionTurn,
2192
2483
  partsFromResponseBlocks,
2193
2484
  preferNewestThreadSummary,
2485
+ prependThreadTimeline,
2194
2486
  preserveMountedMessageIds,
2195
2487
  promoteThreadMessage,
2196
2488
  reconcileRefreshedThreads,
@@ -2203,6 +2495,7 @@ export {
2203
2495
  upsertStableAssistantMessage,
2204
2496
  useAgentAttachments,
2205
2497
  useAgentChat,
2498
+ useAgentChatFeedback,
2206
2499
  useAgentClient,
2207
2500
  useAgentHitl,
2208
2501
  useAgentMcp,
@@ -2210,6 +2503,7 @@ export {
2210
2503
  useAgentThread,
2211
2504
  useAgentThreads,
2212
2505
  useAgents24Runtime,
2506
+ useChatObservation,
2213
2507
  useThreadComputer,
2214
2508
  useThreadMessageQueue,
2215
2509
  withAgentChatBackgroundError,