@agents24/react 0.2.13 → 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));
@@ -675,6 +909,19 @@ function reconcileModelTurnParts(parts, event) {
675
909
  return active ? upsertResponseBlockParts(next, [active]) : next;
676
910
  }
677
911
  function applyRuntimeEvent(state, event) {
912
+ if (state.activeRunId && event.run_id && event.run_id !== state.activeRunId) {
913
+ const previous = state.messages.find((message) => message.role === "assistant" && message.runId === event.run_id);
914
+ if (event.event !== "run.accepted") {
915
+ if (!previous) return state;
916
+ const reconciled = applyRuntimeEvent({
917
+ ...state,
918
+ activeRunId: event.run_id,
919
+ streamingMessageId: previous.id
920
+ }, event);
921
+ return { ...state, messages: reconciled.messages };
922
+ }
923
+ if (previous?.isFinal) return state;
924
+ }
678
925
  if (event.event === "run.accepted" && typeof event.payload?.message_id === "string") {
679
926
  const messageId2 = event.payload.message_id;
680
927
  let optimisticIndex = -1;
@@ -826,7 +1073,18 @@ function applyThreadSummaryEvent(threads, event) {
826
1073
  }
827
1074
 
828
1075
  // src/thread-message-queue.ts
829
- 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
830
1088
  async function retrySubmissionAdmission(admit) {
831
1089
  try {
832
1090
  return await admit();
@@ -836,23 +1094,34 @@ async function retrySubmissionAdmission(admit) {
836
1094
  return admit();
837
1095
  }
838
1096
  }
839
- function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic, onDiscard }) {
840
- const [pendingMessages, setPendingMessages] = React2.useState([]);
841
- const [messageQueue, setMessageQueue] = React2.useState(null);
842
- const pendingRef = React2.useRef(/* @__PURE__ */ new Map());
843
- const versionRef = React2.useRef(0);
844
- const cursorRef = React2.useRef(0);
845
- const promotedRef = React2.useRef(/* @__PURE__ */ new Set());
846
- const threadRef = React2.useRef(threadId);
847
- const transportRef = React2.useRef(transport);
848
- 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);
849
1109
  threadRef.current = threadId;
850
1110
  transportRef.current = transport;
851
1111
  onPromotedRef.current = onPromoted;
852
- const callbacksRef = React2.useRef({ onOptimistic, onDiscard });
853
- 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
+ };
854
1123
  const knownMessage = (id, submissionId) => pendingRef.current.get(id) || (submissionId ? [...pendingRef.current.values()].find((item) => item.client_submission_id === submissionId) : void 0);
855
- const hydrate = React2.useCallback((page, selectedThreadId) => {
1124
+ const hydrate = React3.useCallback((page, selectedThreadId) => {
856
1125
  if (threadRef.current !== selectedThreadId) return;
857
1126
  const pending = page.pending.filter((message) => !promotedRef.current.has(message.id)).map((message) => {
858
1127
  const known = knownMessage(message.id, message.client_submission_id);
@@ -864,11 +1133,12 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
864
1133
  setPendingMessages([...pending, ...optimistic]);
865
1134
  setMessageQueue(page.queue);
866
1135
  }, []);
867
- const refresh = React2.useCallback(async (selectedThreadId = threadRef.current) => {
1136
+ const refresh = React3.useCallback(async (selectedThreadId = threadRef.current, signal) => {
868
1137
  if (!selectedThreadId) return null;
1138
+ const isCurrent = ownOperation();
869
1139
  const version = ++versionRef.current;
870
- const page = await transportRef.current.list(selectedThreadId);
871
- 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;
872
1142
  for (const item of page.items) {
873
1143
  if (item.role === "user" && item.status === "delivered" && item.run_id) {
874
1144
  const pending = knownMessage(item.id, item.client_submission_id);
@@ -886,23 +1156,24 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
886
1156
  }
887
1157
  hydrate(page, selectedThreadId);
888
1158
  return page;
889
- }, [hydrate]);
890
- const acceptNotice = React2.useCallback((notice) => {
1159
+ }, [hydrate, ownOperation]);
1160
+ const acceptNotice = React3.useCallback((notice) => {
891
1161
  if (notice.cursor && notice.cursor <= cursorRef.current) return false;
892
1162
  if (notice.cursor) cursorRef.current = notice.cursor;
893
1163
  const pending = knownMessage(notice.messageId, notice.clientSubmissionId);
894
- if (notice.status === "delivered" && notice.runId && pending && !promotedRef.current.has(notice.messageId)) {
1164
+ if (notice.status === "delivered" && notice.runId && !promotedRef.current.has(notice.messageId)) {
895
1165
  promotedRef.current.add(notice.messageId);
896
- pendingRef.current.delete(pending.id);
897
1166
  versionRef.current += 1;
898
- setPendingMessages((current) => current.filter((message) => message.id !== pending.id));
899
- 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);
900
1172
  }
901
1173
  if (notice.status === "removed") pendingRef.current.delete(notice.messageId);
902
- void refresh().catch(() => void 0);
903
1174
  return true;
904
- }, [refresh]);
905
- const beginSubmission = React2.useCallback((input, queued = false) => {
1175
+ }, []);
1176
+ const beginSubmission = React3.useCallback((input, queued = false) => {
906
1177
  const now = (/* @__PURE__ */ new Date()).toISOString();
907
1178
  const message = {
908
1179
  id: input.clientSubmissionId,
@@ -929,16 +1200,17 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
929
1200
  else callbacksRef.current.onOptimistic?.(message, Boolean(input.uploading));
930
1201
  return message;
931
1202
  }, []);
932
- const discardSubmission = React2.useCallback((message) => {
1203
+ const discardSubmission = React3.useCallback((message) => {
933
1204
  if (!pendingRef.current.has(message.id)) return;
934
1205
  pendingRef.current.delete(message.id);
935
1206
  callbacksRef.current.onDiscard?.(message);
936
1207
  versionRef.current += 1;
937
1208
  setPendingMessages((current) => current.filter((item) => item.client_submission_id !== message.client_submission_id));
938
1209
  }, []);
939
- const submit = React2.useCallback(async (input) => {
1210
+ const submit = React3.useCallback(async (input) => {
940
1211
  const selectedThreadId = threadRef.current;
941
1212
  if (!selectedThreadId) throw new Error("No active Agent thread is available.");
1213
+ const isCurrent = ownOperation();
942
1214
  const { queued, prepare, attachments, uploading, ...submission } = input;
943
1215
  const optimistic = beginSubmission({
944
1216
  ...submission,
@@ -948,10 +1220,11 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
948
1220
  }, queued);
949
1221
  try {
950
1222
  const prepared = prepare ? await prepare() : {};
951
- 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 });
952
1225
  const request = { ...submission, ...prepared, clientSubmissionId: optimistic.client_submission_id, threadId: selectedThreadId };
953
1226
  const result = await retrySubmissionAdmission(() => transportRef.current.submit(request));
954
- if (threadRef.current !== selectedThreadId) return result;
1227
+ if (!isCurrent()) return result;
955
1228
  versionRef.current += 1;
956
1229
  const bound = {
957
1230
  ...optimistic,
@@ -978,36 +1251,44 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
978
1251
  void refresh().catch(() => void 0);
979
1252
  return result;
980
1253
  } catch (error) {
981
- if (threadRef.current === selectedThreadId) discardSubmission(optimistic);
1254
+ if (isCurrent()) discardSubmission(optimistic);
982
1255
  throw error;
983
1256
  }
984
- }, [beginSubmission, discardSubmission, refresh]);
985
- const edit = React2.useCallback(async (message, input) => {
1257
+ }, [beginSubmission, discardSubmission, refresh, ownOperation]);
1258
+ const edit = React3.useCallback(async (message, input) => {
1259
+ const isCurrent = ownOperation();
986
1260
  const updated = await transportRef.current.edit(message, input);
1261
+ assertCurrent(isCurrent);
987
1262
  versionRef.current += 1;
988
1263
  pendingRef.current.set(updated.id, updated);
989
1264
  await refresh(message.thread_id);
990
1265
  return updated;
991
- }, [refresh]);
992
- const remove = React2.useCallback(async (message) => {
1266
+ }, [refresh, ownOperation]);
1267
+ const remove = React3.useCallback(async (message) => {
1268
+ const isCurrent = ownOperation();
993
1269
  await transportRef.current.remove(message);
1270
+ assertCurrent(isCurrent);
994
1271
  versionRef.current += 1;
995
1272
  pendingRef.current.delete(message.id);
996
1273
  setPendingMessages((current) => current.filter((item) => item.id !== message.id));
997
1274
  await refresh(message.thread_id);
998
- }, [refresh]);
999
- const retractForEdit = React2.useCallback(async (message) => {
1275
+ }, [refresh, ownOperation]);
1276
+ const retractForEdit = React3.useCallback(async (message) => {
1277
+ const isCurrent = ownOperation();
1000
1278
  await remove(message);
1279
+ assertCurrent(isCurrent);
1001
1280
  return { text: message.text, attachment_ids: message.attachment_ids, attachments: message.attachments };
1002
- }, [remove]);
1003
- const resume = React2.useCallback(async () => {
1281
+ }, [remove, ownOperation]);
1282
+ const resume = React3.useCallback(async () => {
1004
1283
  const selectedThreadId = threadRef.current;
1005
1284
  if (!selectedThreadId) return null;
1285
+ const isCurrent = ownOperation();
1006
1286
  const result = await transportRef.current.resume(selectedThreadId);
1287
+ if (!isCurrent()) return null;
1007
1288
  await refresh(selectedThreadId);
1008
1289
  return result;
1009
- }, [refresh]);
1010
- React2.useEffect(() => {
1290
+ }, [refresh, ownOperation]);
1291
+ React3.useEffect(() => {
1011
1292
  versionRef.current += 1;
1012
1293
  cursorRef.current = 0;
1013
1294
  promotedRef.current.clear();
@@ -1016,14 +1297,6 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
1016
1297
  setMessageQueue(null);
1017
1298
  if (threadId) void refresh(threadId).catch(() => void 0);
1018
1299
  }, [threadId, refresh]);
1019
- React2.useEffect(() => {
1020
- if (!threadId || !pendingMessages.length || messageQueue?.paused) return;
1021
- const clock = globalThis;
1022
- const timer = clock.setInterval(() => {
1023
- void refresh(threadId).catch(() => void 0);
1024
- }, 3e3);
1025
- return () => clock.clearInterval(timer);
1026
- }, [threadId, pendingMessages.length, messageQueue?.paused, refresh]);
1027
1300
  return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, beginSubmission, discardSubmission, submit, edit, remove, retractForEdit, resume };
1028
1301
  }
1029
1302
  function optimisticSubmissionTurn(message) {
@@ -1072,72 +1345,20 @@ function discardOptimisticSubmission(state, message) {
1072
1345
  }
1073
1346
 
1074
1347
  // src/use-agent-chat.ts
1075
- import * as React6 from "react";
1076
-
1077
- // src/thread-summary-subscription.ts
1078
- function waitForReconnect(signal, delay) {
1079
- return new Promise((resolve) => {
1080
- if (signal.aborted) return resolve();
1081
- const clock = globalThis;
1082
- const finish = () => {
1083
- clock.clearTimeout(timer);
1084
- signal.removeEventListener("abort", finish);
1085
- resolve();
1086
- };
1087
- const timer = clock.setTimeout(finish, delay);
1088
- signal.addEventListener("abort", finish, { once: true });
1089
- });
1090
- }
1091
- async function subscribeToThreadSummaries(input) {
1092
- let cursor;
1093
- let failures = 0;
1094
- while (!input.signal.aborted) {
1095
- let refreshedFromSnapshot = false;
1096
- try {
1097
- await input.client.threads.events({ cursor, signal: input.signal }, async (event) => {
1098
- if (event.event === "snapshot_required") {
1099
- await input.refresh();
1100
- cursor = void 0;
1101
- refreshedFromSnapshot = true;
1102
- return;
1103
- }
1104
- cursor = event.cursor;
1105
- failures = 0;
1106
- await input.onEvent(event);
1107
- });
1108
- if (input.signal.aborted) break;
1109
- if (!refreshedFromSnapshot) await input.refresh();
1110
- input.onRecovered();
1111
- failures = 0;
1112
- } catch (error) {
1113
- if (input.signal.aborted || isAbortLike(error)) break;
1114
- input.onError(error);
1115
- failures += 1;
1116
- try {
1117
- await input.refresh();
1118
- input.onRecovered();
1119
- } catch (refreshError) {
1120
- if (!input.signal.aborted && !isAbortLike(refreshError)) input.onError(refreshError);
1121
- }
1122
- }
1123
- if (!input.signal.aborted) {
1124
- await waitForReconnect(input.signal, Math.min(3e4, 1e3 * 2 ** Math.min(failures, 5)));
1125
- }
1126
- }
1127
- }
1348
+ import * as React7 from "react";
1128
1349
 
1129
1350
  // src/use-agent-chat-thread-list.ts
1130
- import * as React3 from "react";
1351
+ import * as React4 from "react";
1131
1352
  function summaryToStored(thread) {
1132
1353
  return { ...thread, id: String(thread.id), messages: [], isHydrated: false };
1133
1354
  }
1134
1355
  function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation }) {
1135
- const [isLoadingThreads, setIsLoadingThreads] = React3.useState(false);
1136
- const [isLoadingMoreThreads, setIsLoadingMoreThreads] = React3.useState(false);
1137
- const [threadTotal, setThreadTotal] = React3.useState();
1138
- const refreshPromiseRef = React3.useRef(null);
1139
- const eventsDuringRefreshRef = React3.useRef(null);
1140
- 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(() => {
1141
1362
  if (refreshPromiseRef.current) return refreshPromiseRef.current;
1142
1363
  clearOperationError();
1143
1364
  setIsLoadingThreads(true);
@@ -1172,7 +1393,7 @@ function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clea
1172
1393
  }).catch(() => void 0);
1173
1394
  return request;
1174
1395
  }, [clearOperationError, client, failOperation, threadPageSize, update]);
1175
- const loadMoreThreads = React3.useCallback(async () => {
1396
+ const loadMoreThreads = React4.useCallback(async () => {
1176
1397
  if (isLoadingMoreThreads) return;
1177
1398
  const skip = stateRef.current.threads.length;
1178
1399
  if (threadTotal !== void 0 && skip >= threadTotal) return;
@@ -1207,15 +1428,30 @@ function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clea
1207
1428
  }
1208
1429
 
1209
1430
  // src/use-agent-chat-feedback.ts
1210
- import * as React4 from "react";
1211
- function useAgentChatFeedback({ client, update, clearOperationError, failOperation }) {
1212
- 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) => {
1213
1444
  const runId = message.runId;
1214
1445
  if (!runId || message.role !== "assistant" || message.isFinal === false) {
1215
1446
  const error = new Error("Only persisted final responses can receive feedback.");
1216
1447
  failOperation("set_feedback", error);
1217
1448
  throw error;
1218
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;
1219
1455
  const previous = message.feedback ?? null;
1220
1456
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1221
1457
  const optimistic = input.rating ? {
@@ -1227,26 +1463,34 @@ function useAgentChatFeedback({ client, update, clearOperationError, failOperati
1227
1463
  } : null;
1228
1464
  const replaceFeedback = (current, feedback) => ({
1229
1465
  ...current,
1230
- 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),
1231
1467
  threads: current.threads.map((thread) => ({
1232
1468
  ...thread,
1233
- 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)
1234
1470
  }))
1235
1471
  });
1236
1472
  clearOperationError();
1237
1473
  update((current) => replaceFeedback(current, optimistic));
1238
1474
  try {
1239
1475
  const result = await client.runs.setFeedback({ runId, ...input });
1240
- update((current) => replaceFeedback(current, result.feedback));
1476
+ if (isLatest() && isCurrent()) update((current) => replaceFeedback(current, result.feedback));
1241
1477
  } catch (error) {
1242
- 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
+ });
1243
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);
1244
1487
  }
1245
- }, [clearOperationError, client, failOperation, update]);
1488
+ }, [clearOperationError, client, failOperation, update, ownOperation, refreshMessages]);
1489
+ return { setFeedback, captureRead };
1246
1490
  }
1247
1491
 
1248
1492
  // src/use-agent-chat-queue-actions.ts
1249
- import * as React5 from "react";
1493
+ import * as React6 from "react";
1250
1494
  function useAgentChatQueueActions({
1251
1495
  queue,
1252
1496
  stateRef,
@@ -1254,21 +1498,23 @@ function useAgentChatQueueActions({
1254
1498
  clearOperationError,
1255
1499
  failOperation,
1256
1500
  refreshMessages,
1257
- attachRun
1501
+ attachRun,
1502
+ ownOperation
1258
1503
  }) {
1259
- const steer = React5.useCallback(async (text2) => {
1504
+ const steer = React6.useCallback(async (text2) => {
1260
1505
  const threadId = stateRef.current.activeThreadId;
1261
1506
  if (!threadId || !text2.trim()) throw new Error("An active thread and text are required to steer.");
1507
+ const isCurrent = ownOperation();
1262
1508
  clearOperationError();
1263
1509
  try {
1264
1510
  await queue.submit({ mode: "steer", input: text2.trim(), idempotencyKey: createId() });
1265
- await refreshMessages(threadId);
1511
+ if (isCurrent()) await refreshMessages(threadId);
1266
1512
  } catch (error) {
1267
- failOperation("submit", error);
1513
+ if (isCurrent()) failOperation("submit", error);
1268
1514
  throw error;
1269
1515
  }
1270
- }, [clearOperationError, createId, failOperation, queue.submit, refreshMessages, stateRef]);
1271
- const editPendingMessage = React5.useCallback(async (message, input) => {
1516
+ }, [clearOperationError, createId, failOperation, queue.submit, refreshMessages, stateRef, ownOperation]);
1517
+ const editPendingMessage = React6.useCallback(async (message, input) => {
1272
1518
  await queue.edit(message, {
1273
1519
  text: input.text,
1274
1520
  attachmentIds: input.attachmentIds ?? message.attachment_ids,
@@ -1276,14 +1522,15 @@ function useAgentChatQueueActions({
1276
1522
  requestedModelId: input.requestedModelId
1277
1523
  });
1278
1524
  }, [queue.edit]);
1279
- const steerPendingMessage = React5.useCallback(async (message) => {
1525
+ const steerPendingMessage = React6.useCallback(async (message) => {
1280
1526
  await queue.edit(message, { text: message.text, attachmentIds: message.attachment_ids, mode: "steer" });
1281
1527
  }, [queue.edit]);
1282
- const resumeQueue = React5.useCallback(async () => {
1528
+ const resumeQueue = React6.useCallback(async () => {
1283
1529
  if (!stateRef.current.activeThreadId) return;
1530
+ const isCurrent = ownOperation();
1284
1531
  const result = await queue.resume();
1285
- if (result?.run_id) void attachRun(result.run_id).catch(() => void 0);
1286
- }, [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]);
1287
1534
  return { steer, editPendingMessage, steerPendingMessage, removePendingMessage: queue.remove, resumeQueue };
1288
1535
  }
1289
1536
 
@@ -1294,9 +1541,6 @@ function resolveInitialActiveThreadId(options, storedThreadId) {
1294
1541
  function shouldHydrateSelectedThread(selectedThreadId, activeThreadId, activeThreadIsHydrated) {
1295
1542
  return Boolean(selectedThreadId) && (selectedThreadId !== activeThreadId || !activeThreadIsHydrated);
1296
1543
  }
1297
- function isCurrentThreadOpenRequest(input) {
1298
- return !input.signalAborted && input.requestGeneration === input.currentGeneration && input.requestedThreadId === input.activeThreadId;
1299
- }
1300
1544
 
1301
1545
  // src/use-agent-chat.ts
1302
1546
  function activeRunIdFromThread(thread) {
@@ -1309,7 +1553,7 @@ function useAgentChat(options = {}) {
1309
1553
  const client = options.client || runtime.client;
1310
1554
  const pageSize = options.pageSize ?? 30;
1311
1555
  const threadPageSize = options.threadPageSize ?? 50;
1312
- const [state, setState] = React6.useState(() => ({
1556
+ const [state, setState] = React7.useState(() => ({
1313
1557
  ...initialAgentChatState,
1314
1558
  activeThreadId: resolveInitialActiveThreadId(
1315
1559
  options,
@@ -1317,34 +1561,40 @@ function useAgentChat(options = {}) {
1317
1561
  ),
1318
1562
  threads: runtime.storage?.loadThreads() ?? []
1319
1563
  }));
1320
- const [isLoadingHistory, setIsLoadingHistory] = React6.useState(false);
1321
- const [isLoadingOlder, setIsLoadingOlder] = React6.useState(false);
1322
- const [isSubmitting, setIsSubmitting] = React6.useState(false);
1323
- const streamControllerRef = React6.useRef(null);
1324
- const threadOpenControllerRef = React6.useRef(null);
1325
- const threadOpenGenerationRef = React6.useRef(0);
1326
- const attachRunRef = React6.useRef(async () => void 0);
1327
- const messageRefreshVersionRef = React6.useRef(0);
1328
- const promotedRunHintRef = React6.useRef(null);
1329
- const promotedAttachRef = React6.useRef(null);
1330
- const firstSubmissionRef = React6.useRef(null);
1331
- const firstSubmissionAdmittedRef = React6.useRef(null);
1332
- 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);
1333
1579
  stateRef.current = state;
1334
- 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) => {
1335
1587
  runtime.storage?.saveThreads(next.threads);
1336
1588
  runtime.storage?.saveActiveThreadId?.(next.activeThreadId);
1337
1589
  }, [runtime.storage]);
1338
- const update = React6.useCallback((updater) => {
1339
- setState((current) => {
1340
- const next = updater(current);
1341
- stateRef.current = next;
1342
- persist(next);
1343
- return next;
1344
- });
1590
+ const update = React7.useCallback((updater) => {
1591
+ const next = updater(stateRef.current);
1592
+ stateRef.current = next;
1593
+ persist(next);
1594
+ setState(next);
1345
1595
  }, [persist]);
1346
- const queueTransport = React6.useMemo(() => ({
1347
- 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 }),
1348
1598
  submit: (input) => client.threads.messages.submit(input),
1349
1599
  edit: (message, input) => client.threads.messages.edit({
1350
1600
  threadId: message.thread_id,
@@ -1361,40 +1611,46 @@ function useAgentChat(options = {}) {
1361
1611
  },
1362
1612
  resume: (threadId) => client.threads.resumeQueue({ threadId })
1363
1613
  }), [client, pageSize]);
1364
- const onQueuePromotion = React6.useCallback((message, runId) => {
1365
- if (stateRef.current.activeThreadId !== message.thread_id) return;
1614
+ const attachPromotion = React7.useCallback((runId, threadId) => {
1615
+ if (stateRef.current.activeThreadId !== threadId) return;
1366
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;
1367
1626
  const userMessage = threadTimelineToMessages([{ ...message, run_id: runId, status: "delivered" }])[0];
1368
1627
  update((current) => current.activeThreadId === message.thread_id ? { ...current, messages: promoteThreadMessage(current.messages, userMessage, runId) } : current);
1369
- if (stateRef.current.activeRunId !== runId && promotedAttachRef.current !== runId) {
1370
- promotedAttachRef.current = runId;
1371
- void attachRunRef.current(runId).catch(() => {
1372
- promotedAttachRef.current = null;
1373
- });
1374
- }
1375
- }, [update]);
1628
+ attachPromotion(runId, message.thread_id);
1629
+ }, [update, attachPromotion]);
1376
1630
  const queue = useThreadMessageQueue({
1377
1631
  threadId: state.activeThreadId,
1378
1632
  transport: queueTransport,
1633
+ ownOperation,
1379
1634
  onPromoted: onQueuePromotion,
1635
+ onUnknownPromotion: attachPromotion,
1380
1636
  onOptimistic: (message, uploading) => update((current) => optimisticSubmissionState(current, message, uploading)),
1381
1637
  onDiscard: (message) => update((current) => discardOptimisticSubmission(current, message))
1382
1638
  });
1383
1639
  const { pendingMessages, messageQueue } = queue;
1384
- const eventBuffer = React6.useMemo(
1640
+ const eventBuffer = React7.useMemo(
1385
1641
  () => createRuntimeEventFrameBuffer(
1386
1642
  (events) => update((current) => applyRuntimeEvents(current, events)),
1387
1643
  runtimeEventFrameScheduler()
1388
1644
  ),
1389
1645
  [update]
1390
1646
  );
1391
- const clearOperationError = React6.useCallback((operation) => {
1647
+ const clearOperationError = React7.useCallback((operation) => {
1392
1648
  update((current) => clearAgentChatOperationError(current, operation));
1393
1649
  }, [update]);
1394
- const clearBackgroundError = React6.useCallback(() => {
1650
+ const clearBackgroundError = React7.useCallback(() => {
1395
1651
  update(clearAgentChatBackgroundError);
1396
1652
  }, [update]);
1397
- const failOperation = React6.useCallback((operation, cause) => {
1653
+ const failOperation = React7.useCallback((operation, cause) => {
1398
1654
  update((current) => withAgentChatOperationError(current, operation, cause));
1399
1655
  }, [update]);
1400
1656
  const {
@@ -1405,60 +1661,67 @@ function useAgentChat(options = {}) {
1405
1661
  refreshThreads,
1406
1662
  loadMoreThreads
1407
1663
  } = useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation });
1408
- const invalidateThreadOpen = React6.useCallback(() => {
1664
+ const invalidateThreadOpen = React7.useCallback(() => {
1409
1665
  threadOpenGenerationRef.current += 1;
1410
1666
  threadOpenControllerRef.current?.abort("Thread selection changed.");
1411
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);
1412
1676
  setIsLoadingHistory(false);
1413
1677
  }, []);
1414
- const detach = React6.useCallback(() => {
1678
+ const detach = React7.useCallback(() => {
1415
1679
  eventBuffer.flush();
1416
1680
  streamControllerRef.current?.abort();
1417
1681
  streamControllerRef.current = null;
1418
1682
  update((current) => ({ ...current, streamingMessageId: null, runState: current.activeRunId ? "reconnecting" : "idle" }));
1419
1683
  }, [eventBuffer, update]);
1420
- 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) => {
1421
1694
  const target = threadId || stateRef.current.activeThreadId;
1422
1695
  if (!target) return;
1696
+ const isCurrent = ownOperation();
1697
+ const reconcileFeedback = captureFeedbackRead();
1423
1698
  const version = ++messageRefreshVersionRef.current;
1424
- const page = await client.threads.messages.list({ threadId: target, limit: pageSize });
1425
- 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;
1426
1701
  queue.hydrate(page, target);
1427
1702
  const promoted = promotedRunId || promotedRunHintRef.current;
1428
- if (promoted && page.items.some((message) => message.role === "user" && message.run_id === promoted)) {
1429
- promotedRunHintRef.current = null;
1430
- update((current) => {
1431
- if (current.activeThreadId !== target) return current;
1432
- return {
1433
- ...current,
1434
- messages: reconcileThreadTimeline(current.messages, threadTimelineToMessages(page.items), current.activeRunId, current.streamingMessageId)
1435
- };
1436
- });
1437
- return;
1438
- }
1439
- if (stateRef.current.activeRunId) {
1440
- const delivered = threadTimelineToMessages(page.items.filter((item) => item.role === "user" && item.run_id === stateRef.current.activeRunId && (item.segment_index ?? 0) > 0));
1441
- if (delivered.length) update((current) => {
1442
- if (current.activeThreadId !== target) return current;
1443
- const additions = delivered.filter((item) => !current.messages.some((existing) => existing.id === item.id));
1444
- return additions.length ? { ...current, messages: [...current.messages, ...additions] } : current;
1445
- });
1446
- }
1447
- if (!stateRef.current.activeRunId && page.items.length) {
1448
- update((current) => current.activeThreadId === target && !current.activeRunId ? (() => {
1449
- const messages = reconcileThreadTimeline(current.messages, threadTimelineToMessages(page.items), null, null);
1450
- return { ...current, messages, threads: current.threads.map((thread) => thread.id === target ? { ...thread, messages } : thread) };
1451
- })() : current);
1452
- }
1453
- }, [client, pageSize, queue.hydrate, update]);
1454
- 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;
1455
1718
  eventBuffer.dispose();
1456
1719
  streamControllerRef.current?.abort();
1457
1720
  threadOpenControllerRef.current?.abort();
1458
1721
  }, [eventBuffer]);
1459
- const openThread = React6.useCallback(async (threadId) => {
1722
+ const openThread = React7.useCallback(async (threadId) => {
1460
1723
  const previousSelection = captureAgentChatThreadSelection(stateRef.current);
1461
- threadOpenControllerRef.current?.abort();
1724
+ invalidateThreadOpen();
1462
1725
  const requestController = runtime.createAbortController();
1463
1726
  threadOpenControllerRef.current = requestController;
1464
1727
  const requestGeneration = threadOpenGenerationRef.current + 1;
@@ -1478,6 +1741,8 @@ function useAgentChat(options = {}) {
1478
1741
  runState: "idle",
1479
1742
  streamingMessageId: null
1480
1743
  }));
1744
+ const isCurrent = ownOperation();
1745
+ const reconcileFeedback = captureFeedbackRead();
1481
1746
  setIsLoadingHistory(true);
1482
1747
  try {
1483
1748
  const detail = await client.threads.get({
@@ -1485,39 +1750,21 @@ function useAgentChat(options = {}) {
1485
1750
  limit: pageSize,
1486
1751
  signal: requestController.signal
1487
1752
  });
1488
- if (!isCurrentThreadOpenRequest({
1489
- activeThreadId: stateRef.current.activeThreadId,
1490
- currentGeneration: threadOpenGenerationRef.current,
1491
- requestGeneration,
1492
- requestedThreadId: threadId,
1493
- signalAborted: requestController.signal.aborted
1494
- })) return;
1753
+ if (!isCurrent()) return;
1495
1754
  const timeline = await client.threads.messages.list({ threadId, limit: pageSize, signal: requestController.signal });
1496
- if (!isCurrentThreadOpenRequest({
1497
- activeThreadId: stateRef.current.activeThreadId,
1498
- currentGeneration: threadOpenGenerationRef.current,
1499
- requestGeneration,
1500
- requestedThreadId: threadId,
1501
- signalAborted: requestController.signal.aborted
1502
- })) return;
1755
+ if (!isCurrent()) return;
1503
1756
  queue.hydrate(timeline, threadId);
1504
- const messages = timeline.items.length ? threadTimelineToMessages(timeline.items, detail) : threadDetailToMessages(detail);
1757
+ const messages = reconcileFeedback(threadTimelineToMessages(timeline.items), stateRef.current.messages);
1505
1758
  const context = latestThreadContext(detail);
1506
1759
  const stored = {
1507
1760
  ...detail,
1508
1761
  messages,
1509
1762
  isHydrated: true,
1510
- hasOlderTurns: Boolean(detail.paging?.has_more),
1511
- nextBeforeTurnIndex: detail.paging?.next_before_turn_index ?? null,
1763
+ hasOlderTurns: Boolean(timeline.paging.has_more),
1764
+ nextBeforeTurnIndex: timeline.paging.next_before_turn_index,
1512
1765
  ...context
1513
1766
  };
1514
- update((current) => isCurrentThreadOpenRequest({
1515
- activeThreadId: current.activeThreadId,
1516
- currentGeneration: threadOpenGenerationRef.current,
1517
- requestGeneration,
1518
- requestedThreadId: threadId,
1519
- signalAborted: requestController.signal.aborted
1520
- }) ? {
1767
+ update((current) => isCurrent() ? {
1521
1768
  ...current,
1522
1769
  activeThreadId: threadId,
1523
1770
  messages,
@@ -1527,13 +1774,7 @@ function useAgentChat(options = {}) {
1527
1774
  threads: current.threads.some((thread) => thread.id === threadId) ? current.threads.map((thread) => thread.id === threadId ? stored : thread) : [stored, ...current.threads]
1528
1775
  } : current);
1529
1776
  const activeRunId = activeRunIdFromThread(stored);
1530
- if (activeRunId && isCurrentThreadOpenRequest({
1531
- activeThreadId: stateRef.current.activeThreadId,
1532
- currentGeneration: threadOpenGenerationRef.current,
1533
- requestGeneration,
1534
- requestedThreadId: threadId,
1535
- signalAborted: requestController.signal.aborted
1536
- })) await attachRunRef.current(activeRunId).catch(() => void 0);
1777
+ if (activeRunId && isCurrent()) void attachRunRef.current(activeRunId).catch(() => void 0);
1537
1778
  } catch (error) {
1538
1779
  if (!requestController.signal.aborted && !isAbortLike(error) && requestGeneration === threadOpenGenerationRef.current) {
1539
1780
  failOperation("open_thread", error);
@@ -1549,28 +1790,34 @@ function useAgentChat(options = {}) {
1549
1790
  setIsLoadingHistory(false);
1550
1791
  }
1551
1792
  }
1552
- }, [clearOperationError, client, detach, failOperation, pageSize, queue.hydrate, runtime, update]);
1553
- const consumeEvent = React6.useCallback((event) => {
1793
+ }, [clearOperationError, client, detach, failOperation, pageSize, queue.hydrate, runtime, update, invalidateThreadOpen, ownOperation, captureFeedbackRead]);
1794
+ const consumeEvent = React7.useCallback((event) => {
1554
1795
  const eventThreadId = typeof event.payload?.thread_id === "string" ? event.payload.thread_id.trim() : "";
1555
1796
  if (eventThreadId && !stateRef.current.activeThreadId) {
1556
1797
  update((current) => adoptStreamingThread(current, eventThreadId));
1798
+ setIsSubmitting(false);
1557
1799
  firstSubmissionAdmittedRef.current?.();
1558
1800
  }
1559
1801
  eventBuffer.push(event);
1560
1802
  }, [eventBuffer, update]);
1561
- const attachRun = React6.useCallback(async (runId, cursor) => {
1803
+ const attachRun = React7.useCallback(async (runId, cursor) => {
1804
+ eventBuffer.flush();
1562
1805
  streamControllerRef.current?.abort();
1563
1806
  const controller = runtime.createAbortController();
1564
1807
  streamControllerRef.current = controller;
1565
- clearOperationError();
1808
+ const isCurrent = ownOperation({ stream: true });
1809
+ clearOperationError("attach_run");
1566
1810
  update((current) => ({ ...current, activeRunId: runId, runState: "reconnecting", error: null }));
1567
1811
  try {
1568
- 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;
1569
1816
  eventBuffer.flush();
1570
1817
  update((current) => ({ ...current, activeRunId: result.detached ? runId : current.activeRunId, cursor: result.cursor ?? current.cursor, runState: result.detached ? "reconnecting" : current.runState }));
1571
1818
  if (!result.detached && result.threadId) await refreshMessages(result.threadId);
1572
1819
  } catch (error) {
1573
- if (!isAbortLike(error)) {
1820
+ if (isCurrent() && !isAbortLike(error)) {
1574
1821
  update((current) => ({ ...current, runState: "failed", streamingMessageId: null }));
1575
1822
  failOperation("attach_run", error);
1576
1823
  throw error;
@@ -1578,15 +1825,17 @@ function useAgentChat(options = {}) {
1578
1825
  } finally {
1579
1826
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
1580
1827
  }
1581
- }, [clearOperationError, client, consumeEvent, eventBuffer, failOperation, refreshMessages, runtime, update]);
1828
+ }, [clearOperationError, client, consumeEvent, eventBuffer, failOperation, refreshMessages, runtime, update, ownOperation]);
1582
1829
  attachRunRef.current = attachRun;
1583
- const submit = React6.useCallback(async (input) => {
1830
+ const submit = React7.useCallback(async (input) => {
1584
1831
  if (!input.text.trim() && !input.attachmentIds?.length && !input.uploads?.length) return;
1585
1832
  if (firstSubmissionRef.current && !stateRef.current.activeThreadId) {
1586
1833
  await firstSubmissionRef.current;
1587
1834
  }
1588
1835
  const existingThreadId = stateRef.current.activeThreadId;
1589
1836
  if (existingThreadId) {
1837
+ const isCurrent = ownOperation();
1838
+ const submissionGeneration = ++submissionGenerationRef.current;
1590
1839
  clearOperationError();
1591
1840
  setIsSubmitting(true);
1592
1841
  try {
@@ -1631,21 +1880,23 @@ function useAgentChat(options = {}) {
1631
1880
  }
1632
1881
  });
1633
1882
  } catch (error) {
1634
- failOperation("submit", error);
1883
+ if (isCurrent()) failOperation("submit", error);
1635
1884
  throw error;
1636
1885
  } finally {
1637
- setIsSubmitting(false);
1886
+ if (isCurrent() && submissionGeneration === submissionGenerationRef.current) setIsSubmitting(false);
1638
1887
  }
1639
1888
  return;
1640
1889
  }
1890
+ invalidateThreadOpen();
1641
1891
  const admission = new Promise((resolve) => {
1642
1892
  firstSubmissionAdmittedRef.current = resolve;
1643
1893
  });
1644
1894
  const stream = (async () => {
1645
- invalidateThreadOpen();
1646
1895
  detach();
1647
1896
  const controller = runtime.createAbortController();
1648
1897
  streamControllerRef.current = controller;
1898
+ const isCurrent = ownOperation({ stream: true, adoptThread: true });
1899
+ const submissionGeneration = ++submissionGenerationRef.current;
1649
1900
  const ticket = queue.beginSubmission({
1650
1901
  input: input.text.trim(),
1651
1902
  clientSubmissionId: runtime.createId(),
@@ -1676,6 +1927,7 @@ function useAgentChat(options = {}) {
1676
1927
  idempotencyKey: runtime.createId(),
1677
1928
  signal: controller.signal
1678
1929
  }) : [];
1930
+ if (!isCurrent()) return;
1679
1931
  if (hasPendingUploads && uploaded.length !== input.uploads?.length) {
1680
1932
  throw new Error("Attachment upload returned an incomplete batch");
1681
1933
  }
@@ -1688,7 +1940,20 @@ function useAgentChat(options = {}) {
1688
1940
  runState: "streaming"
1689
1941
  }));
1690
1942
  }
1691
- 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;
1692
1957
  eventBuffer.flush();
1693
1958
  update((current) => {
1694
1959
  const threadId = result.threadId || current.activeThreadId;
@@ -1704,6 +1969,7 @@ function useAgentChat(options = {}) {
1704
1969
  if (result.threadId) await refreshThreads().catch(() => void 0);
1705
1970
  if (result.threadId && !result.detached) await refreshMessages(result.threadId);
1706
1971
  } catch (error) {
1972
+ if (!isCurrent()) return;
1707
1973
  const admitted = Boolean(stateRef.current.messages.find((message) => message.id === userId)?.runId);
1708
1974
  if (!admitted) queue.discardSubmission(ticket);
1709
1975
  if (!isAbortLike(error)) {
@@ -1716,7 +1982,7 @@ function useAgentChat(options = {}) {
1716
1982
  throw error;
1717
1983
  }
1718
1984
  } finally {
1719
- setIsSubmitting(false);
1985
+ if (isCurrent() && submissionGeneration === submissionGenerationRef.current) setIsSubmitting(false);
1720
1986
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
1721
1987
  }
1722
1988
  })();
@@ -1725,10 +1991,12 @@ function useAgentChat(options = {}) {
1725
1991
  try {
1726
1992
  await accepted;
1727
1993
  } finally {
1728
- if (firstSubmissionRef.current === accepted) firstSubmissionRef.current = null;
1729
- firstSubmissionAdmittedRef.current = null;
1994
+ if (firstSubmissionRef.current === accepted) {
1995
+ firstSubmissionRef.current = null;
1996
+ firstSubmissionAdmittedRef.current = null;
1997
+ }
1730
1998
  }
1731
- }, [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]);
1732
2000
  const { steer, editPendingMessage, steerPendingMessage, removePendingMessage, resumeQueue } = useAgentChatQueueActions({
1733
2001
  queue,
1734
2002
  stateRef,
@@ -1736,42 +2004,38 @@ function useAgentChat(options = {}) {
1736
2004
  clearOperationError,
1737
2005
  failOperation,
1738
2006
  refreshMessages,
1739
- attachRun
2007
+ attachRun,
2008
+ ownOperation
1740
2009
  });
1741
- const startNewThread = React6.useCallback(() => {
2010
+ const startNewThread = React7.useCallback(() => {
1742
2011
  invalidateThreadOpen();
1743
2012
  detach();
1744
2013
  update((current) => ({ ...initialAgentChatState, threads: current.threads }));
1745
2014
  }, [detach, invalidateThreadOpen, update]);
1746
- const deleteThread = React6.useCallback(async (threadId) => {
1747
- if (stateRef.current.activeThreadId === threadId) invalidateThreadOpen();
2015
+ const deleteThread = React7.useCallback(async (threadId) => {
2016
+ const isCurrent = ownOperation();
1748
2017
  clearOperationError();
1749
2018
  try {
1750
2019
  await client.threads.delete({ threadId, idempotencyKey: runtime.createId() });
1751
- update((current) => ({
1752
- ...current,
1753
- activeThreadId: current.activeThreadId === threadId ? null : current.activeThreadId,
1754
- messages: current.activeThreadId === threadId ? [] : current.messages,
1755
- contextWindow: current.activeThreadId === threadId ? null : current.contextWindow,
1756
- contextCompression: current.activeThreadId === threadId ? null : current.contextCompression,
1757
- contextCompressionRevision: current.activeThreadId === threadId ? 0 : current.contextCompressionRevision,
1758
- threads: current.threads.filter((thread) => thread.id !== threadId)
1759
- }));
2020
+ if (stateRef.current.activeThreadId === threadId) startNewThread();
2021
+ update((current) => ({ ...current, threads: current.threads.filter((thread) => thread.id !== threadId) }));
1760
2022
  } catch (error) {
1761
2023
  if (!isAbortLike(error)) {
1762
- failOperation("delete_thread", error);
2024
+ if (isCurrent()) failOperation("delete_thread", error);
1763
2025
  throw error;
1764
2026
  }
1765
2027
  }
1766
- }, [clearOperationError, client, failOperation, invalidateThreadOpen, runtime, update]);
1767
- const cancelRun = React6.useCallback(async () => {
2028
+ }, [clearOperationError, client, failOperation, ownOperation, runtime, startNewThread, update]);
2029
+ const cancelRun = React7.useCallback(async () => {
1768
2030
  const runId = stateRef.current.activeRunId;
1769
2031
  if (!runId || stateRef.current.runState === "cancelling") return;
2032
+ const isCurrent = ownOperation({ run: true, stream: true });
1770
2033
  const previousRunState = stateRef.current.runState;
1771
2034
  clearOperationError();
1772
2035
  update((current) => current.activeRunId === runId ? { ...current, runState: "cancelling" } : current);
1773
2036
  try {
1774
2037
  const result = await client.runs.cancel({ runId, idempotencyKey: runtime.createId() });
2038
+ if (!isCurrent()) return;
1775
2039
  if (result.status === "cancelled") {
1776
2040
  detach();
1777
2041
  update((current) => ({
@@ -1796,14 +2060,15 @@ function useAgentChat(options = {}) {
1796
2060
  }));
1797
2061
  }
1798
2062
  } catch (error) {
1799
- if (!isAbortLike(error)) {
2063
+ if (isCurrent() && !isAbortLike(error)) {
1800
2064
  update((current) => current.activeRunId === runId ? { ...current, runState: previousRunState } : current);
1801
2065
  failOperation("cancel_run", error);
1802
2066
  throw error;
1803
2067
  }
1804
2068
  }
1805
- }, [clearOperationError, client, detach, failOperation, runtime, update]);
1806
- 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 });
1807
2072
  const runId = stateRef.current.activeRunId || stateRef.current.messages.find((message) => message.parts.includes(part))?.runId;
1808
2073
  clearOperationError();
1809
2074
  if (!runId) {
@@ -1818,32 +2083,52 @@ function useAgentChat(options = {}) {
1818
2083
  ...response,
1819
2084
  idempotencyKey: runtime.createId()
1820
2085
  });
1821
- await attachRun(result.run_id || runId);
2086
+ if (isCurrent()) await attachRun(result.run_id || runId);
1822
2087
  } catch (error) {
1823
- if (!isAbortLike(error)) failOperation("resume_hitl", error);
2088
+ if (isCurrent() && !isAbortLike(error)) failOperation("resume_hitl", error);
1824
2089
  throw error;
1825
2090
  }
1826
- }, [attachRun, clearOperationError, client, failOperation, runtime]);
1827
- const setFeedback = useAgentChatFeedback({ client, update, clearOperationError, failOperation });
1828
- const loadOlder = React6.useCallback(async () => {
2091
+ }, [attachRun, clearOperationError, client, failOperation, runtime, ownOperation]);
2092
+ const loadOlder = React7.useCallback(() => {
1829
2093
  const thread = stateRef.current.threads.find((item) => item.id === stateRef.current.activeThreadId);
1830
- 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();
1831
2100
  clearOperationError();
1832
2101
  setIsLoadingOlder(true);
1833
- try {
1834
- const detail = await client.threads.get({ threadId: thread.id, limit: pageSize, beforeTurnIndex: thread.nextBeforeTurnIndex });
1835
- const older = threadDetailToMessages(detail);
1836
- 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) }));
1837
- } catch (error) {
1838
- if (!isAbortLike(error)) {
1839
- failOperation("load_older_turns", error);
1840
- 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
+ }
1841
2126
  }
1842
- } finally {
1843
- setIsLoadingOlder(false);
1844
- }
1845
- }, [clearOperationError, client, failOperation, pageSize, update]);
1846
- 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) => {
1847
2132
  const index = stateRef.current.messages.findIndex((item) => item.id === message.id);
1848
2133
  const user = stateRef.current.messages.slice(0, index).reverse().find((item) => item.role === "user");
1849
2134
  if (user) await submit({
@@ -1852,51 +2137,68 @@ function useAgentChat(options = {}) {
1852
2137
  attachments: user.attachments
1853
2138
  });
1854
2139
  }, [submit]);
1855
- React6.useEffect(() => {
1856
- const controller = runtime.createAbortController();
1857
- clearBackgroundError();
1858
- void subscribeToThreadSummaries({
1859
- client,
1860
- signal: controller.signal,
1861
- refresh: refreshThreads,
1862
- onEvent(event) {
1863
- threadEventsDuringRefreshRef.current?.push(event);
1864
- update((current) => ({
1865
- ...clearAgentChatBackgroundError(current),
1866
- threads: applyThreadSummaryEvent(current.threads, event)
1867
- }));
1868
- if (event.event === "thread.message.updated" && event.thread_id === stateRef.current.activeThreadId) {
1869
- const wasQueued = queue.acceptNotice({
1870
- messageId: event.message_id,
1871
- status: event.status,
1872
- runId: event.run_id,
1873
- cursor: event.cursor,
1874
- clientSubmissionId: event.client_submission_id
1875
- });
1876
- if (wasQueued) void refreshMessages(event.thread_id, event.run_id || void 0).catch(() => void 0);
1877
- }
1878
- if (event.event === "thread.summary.updated" && event.thread.id === stateRef.current.activeThreadId) {
1879
- if (streamControllerRef.current && stateRef.current.messages.some((message) => message.role === "user" && !message.runId)) return;
1880
- const promotedRunId = activeRunIdFromThread(summaryToStored(event.thread));
1881
- void (async () => {
1882
- await refreshMessages(String(event.thread.id), promotedRunId || void 0);
1883
- if (promotedRunId && stateRef.current.activeRunId !== promotedRunId && stateRef.current.activeThreadId === String(event.thread.id) && promotedAttachRef.current !== promotedRunId) {
1884
- promotedAttachRef.current = promotedRunId;
1885
- await attachRunRef.current(promotedRunId);
1886
- }
1887
- })().catch(() => void 0);
1888
- }
1889
- },
1890
- onError(error) {
1891
- update((current) => withAgentChatBackgroundError(current, error));
1892
- },
1893
- onRecovered() {
1894
- 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);
1895
2169
  }
1896
- });
1897
- return () => controller.abort();
1898
- }, [clearBackgroundError, client, queue.acceptNotice, refreshMessages, refreshThreads, runtime.createAbortController, update]);
1899
- 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(() => {
1900
2202
  const threadId = options.activeThreadId;
1901
2203
  const activeThread2 = stateRef.current.threads.find((thread) => thread.id === stateRef.current.activeThreadId);
1902
2204
  if (shouldHydrateSelectedThread(threadId, stateRef.current.activeThreadId, Boolean(activeThread2?.isHydrated))) {
@@ -1943,15 +2245,15 @@ function useAgentChat(options = {}) {
1943
2245
  }
1944
2246
 
1945
2247
  // src/use-agent-resources.ts
1946
- import * as React7 from "react";
2248
+ import * as React8 from "react";
1947
2249
  function useAgentThreads(options = {}) {
1948
2250
  const { client, createAbortController } = useAgents24Runtime();
1949
- const [threads, setThreads] = React7.useState([]);
1950
- const [total, setTotal] = React7.useState();
1951
- const [isLoading, setIsLoading] = React7.useState(true);
1952
- const [error, setError] = React7.useState(null);
1953
- const controllerRef = React7.useRef(null);
1954
- 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 () => {
1955
2257
  controllerRef.current?.abort();
1956
2258
  const controller = createAbortController();
1957
2259
  controllerRef.current = controller;
@@ -1970,7 +2272,7 @@ function useAgentThreads(options = {}) {
1970
2272
  }
1971
2273
  }
1972
2274
  }, [client, createAbortController, options.limit, options.skip]);
1973
- React7.useEffect(() => {
2275
+ React8.useEffect(() => {
1974
2276
  void load();
1975
2277
  return () => controllerRef.current?.abort();
1976
2278
  }, [load]);
@@ -1978,11 +2280,11 @@ function useAgentThreads(options = {}) {
1978
2280
  }
1979
2281
  function useAgentThread(threadId, options = {}) {
1980
2282
  const { client, createAbortController } = useAgents24Runtime();
1981
- const [thread, setThread] = React7.useState(null);
1982
- const [isLoading, setIsLoading] = React7.useState(Boolean(threadId));
1983
- const [error, setError] = React7.useState(null);
1984
- const controllerRef = React7.useRef(null);
1985
- 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 () => {
1986
2288
  if (!threadId) {
1987
2289
  controllerRef.current?.abort();
1988
2290
  controllerRef.current = null;
@@ -2009,7 +2311,7 @@ function useAgentThread(threadId, options = {}) {
2009
2311
  }
2010
2312
  }
2011
2313
  }, [client, createAbortController, options.beforeTurnIndex, options.limit, threadId]);
2012
- React7.useEffect(() => {
2314
+ React8.useEffect(() => {
2013
2315
  void load();
2014
2316
  return () => controllerRef.current?.abort();
2015
2317
  }, [load]);
@@ -2017,11 +2319,11 @@ function useAgentThread(threadId, options = {}) {
2017
2319
  }
2018
2320
  function useThreadComputer(threadId) {
2019
2321
  const { client, createAbortController } = useAgents24Runtime();
2020
- const [computer, setComputer] = React7.useState(null);
2021
- const [isLoading, setIsLoading] = React7.useState(Boolean(threadId));
2022
- const [error, setError] = React7.useState(null);
2023
- const controllerRef = React7.useRef(null);
2024
- 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 () => {
2025
2327
  controllerRef.current?.abort();
2026
2328
  if (!threadId) {
2027
2329
  setComputer(null);
@@ -2046,19 +2348,19 @@ function useThreadComputer(threadId) {
2046
2348
  }
2047
2349
  }
2048
2350
  }, [client, createAbortController, threadId]);
2049
- React7.useEffect(() => {
2351
+ React8.useEffect(() => {
2050
2352
  void refresh();
2051
2353
  return () => controllerRef.current?.abort();
2052
2354
  }, [refresh]);
2053
- const list = React7.useCallback((input) => {
2355
+ const list = React8.useCallback((input) => {
2054
2356
  if (!threadId) throw new Error("A thread is required to list computer files");
2055
2357
  return client.threads.computer.list({ threadId, ...input });
2056
2358
  }, [client, threadId]);
2057
- const read = React7.useCallback((path) => {
2359
+ const read = React8.useCallback((path) => {
2058
2360
  if (!threadId) throw new Error("A thread is required to read a computer file");
2059
2361
  return client.threads.computer.read({ threadId, path });
2060
2362
  }, [client, threadId]);
2061
- const createContentAccess = React7.useCallback((path, download = false) => {
2363
+ const createContentAccess = React8.useCallback((path, download = false) => {
2062
2364
  if (!threadId) throw new Error("A thread is required to access a computer file");
2063
2365
  return client.threads.computer.createContentAccess({ threadId, path, download });
2064
2366
  }, [client, threadId]);
@@ -2066,16 +2368,16 @@ function useThreadComputer(threadId) {
2066
2368
  }
2067
2369
  function useAgentRun() {
2068
2370
  const { client, createAbortController, createId } = useAgents24Runtime();
2069
- const controllerRef = React7.useRef(null);
2070
- const [isAttached, setIsAttached] = React7.useState(false);
2071
- const [error, setError] = React7.useState(null);
2072
- React7.useEffect(() => () => controllerRef.current?.abort(), []);
2073
- 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(() => {
2074
2376
  controllerRef.current?.abort();
2075
2377
  controllerRef.current = null;
2076
2378
  setIsAttached(false);
2077
2379
  }, []);
2078
- const attach = React7.useCallback(async (runId, onEvent, cursor) => {
2380
+ const attach = React8.useCallback(async (runId, onEvent, cursor) => {
2079
2381
  detach();
2080
2382
  const controller = createAbortController();
2081
2383
  controllerRef.current = controller;
@@ -2091,7 +2393,7 @@ function useAgentRun() {
2091
2393
  setIsAttached(false);
2092
2394
  }
2093
2395
  }, [client, createAbortController, detach]);
2094
- const cancel = React7.useCallback(async (runId) => {
2396
+ const cancel = React8.useCallback(async (runId) => {
2095
2397
  const result = await client.runs.cancel({ runId, idempotencyKey: createId() });
2096
2398
  detach();
2097
2399
  return result;
@@ -2100,9 +2402,9 @@ function useAgentRun() {
2100
2402
  }
2101
2403
  function useAgentAttachments() {
2102
2404
  const { client, createId } = useAgents24Runtime();
2103
- const [uploads, setUploads] = React7.useState({});
2104
- const [isUploading, setIsUploading] = React7.useState(false);
2105
- 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) => {
2106
2408
  setIsUploading(true);
2107
2409
  try {
2108
2410
  const results = await client.attachments.uploadBatch({ uploads: input.uploads, threadId: input.threadId, requestedAgentId: input.requestedAgentId, idempotencyKey: createId(), signal: input.signal });
@@ -2115,7 +2417,7 @@ function useAgentAttachments() {
2115
2417
  setIsUploading(false);
2116
2418
  }
2117
2419
  }, [client, createId]);
2118
- const createContentAccess = React7.useCallback(
2420
+ const createContentAccess = React8.useCallback(
2119
2421
  (input) => client.attachments.createContentAccess(input),
2120
2422
  [client]
2121
2423
  );
@@ -2123,9 +2425,9 @@ function useAgentAttachments() {
2123
2425
  }
2124
2426
  function useAgentHitl() {
2125
2427
  const { client, createId } = useAgents24Runtime();
2126
- const [isResolving, setIsResolving] = React7.useState(false);
2127
- const [lastResult, setLastResult] = React7.useState(null);
2128
- 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) => {
2129
2431
  setIsResolving(true);
2130
2432
  try {
2131
2433
  const result = await client.hitl.resume({ ...input, idempotencyKey: createId() });
@@ -2139,8 +2441,8 @@ function useAgentHitl() {
2139
2441
  }
2140
2442
  function useAgentMcp() {
2141
2443
  const { client, createId } = useAgents24Runtime();
2142
- const [isAuthorizing, setIsAuthorizing] = React7.useState(false);
2143
- const startAuthorization = React7.useCallback(async (input) => {
2444
+ const [isAuthorizing, setIsAuthorizing] = React8.useState(false);
2445
+ const startAuthorization = React8.useCallback(async (input) => {
2144
2446
  setIsAuthorizing(true);
2145
2447
  try {
2146
2448
  return await client.mcp.startAuthorization({ ...input, idempotencyKey: createId() });
@@ -2148,7 +2450,7 @@ function useAgentMcp() {
2148
2450
  setIsAuthorizing(false);
2149
2451
  }
2150
2452
  }, [client, createId]);
2151
- 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]);
2152
2454
  return { isAuthorizing, redeemCallback, startAuthorization };
2153
2455
  }
2154
2456
  export {
@@ -2161,11 +2463,13 @@ export {
2161
2463
  assistantProjectionFromEvent,
2162
2464
  attachmentResultToChatAttachment,
2163
2465
  captureAgentChatThreadSelection,
2466
+ captureChatOperation,
2164
2467
  chatAttachmentIds,
2165
2468
  clearAgentChatBackgroundError,
2166
2469
  clearAgentChatOperationError,
2167
2470
  computerResourceTargets,
2168
2471
  contextCompressionPartFromEvent,
2472
+ createChatObservation,
2169
2473
  createPortableId,
2170
2474
  createRuntimeEventFrameBuffer,
2171
2475
  discardOptimisticSubmission,
@@ -2178,6 +2482,7 @@ export {
2178
2482
  optimisticSubmissionTurn,
2179
2483
  partsFromResponseBlocks,
2180
2484
  preferNewestThreadSummary,
2485
+ prependThreadTimeline,
2181
2486
  preserveMountedMessageIds,
2182
2487
  promoteThreadMessage,
2183
2488
  reconcileRefreshedThreads,
@@ -2190,6 +2495,7 @@ export {
2190
2495
  upsertStableAssistantMessage,
2191
2496
  useAgentAttachments,
2192
2497
  useAgentChat,
2498
+ useAgentChatFeedback,
2193
2499
  useAgentClient,
2194
2500
  useAgentHitl,
2195
2501
  useAgentMcp,
@@ -2197,6 +2503,7 @@ export {
2197
2503
  useAgentThread,
2198
2504
  useAgentThreads,
2199
2505
  useAgents24Runtime,
2506
+ useChatObservation,
2200
2507
  useThreadComputer,
2201
2508
  useThreadMessageQueue,
2202
2509
  withAgentChatBackgroundError,