@agents24/react 0.2.14 → 0.2.16

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,42 @@ 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 admissionRef = React3.useRef(null);
1102
+ const initialAdmissionRef = React3.useRef(null);
1103
+ const versionRef = React3.useRef(0);
1104
+ const cursorRef = React3.useRef(0);
1105
+ const promotedRef = React3.useRef(/* @__PURE__ */ new Set());
1106
+ const threadRef = React3.useRef(threadId);
1107
+ const selectionGenerationRef = React3.useRef(0);
1108
+ if (threadRef.current !== threadId && initialAdmissionRef.current?.threadId !== threadId) selectionGenerationRef.current += 1;
1109
+ const transportRef = React3.useRef(transport);
1110
+ const onPromotedRef = React3.useRef(onPromoted);
862
1111
  threadRef.current = threadId;
863
1112
  transportRef.current = transport;
864
1113
  onPromotedRef.current = onPromoted;
865
- const callbacksRef = React2.useRef({ onOptimistic, onDiscard });
866
- callbacksRef.current = { onOptimistic, onDiscard };
1114
+ const ownSelectionRef = React3.useRef(ownSelection);
1115
+ ownSelectionRef.current = ownSelection;
1116
+ const callbacksRef = React3.useRef({ onOptimistic, onDiscard, onUnknownPromotion });
1117
+ callbacksRef.current = { onOptimistic, onDiscard, onUnknownPromotion };
1118
+ const ownOperation = React3.useCallback((options) => ownSelectionRef.current?.(options) ?? captureChatOperation(() => ({
1119
+ generation: selectionGenerationRef.current,
1120
+ threadId: threadRef.current
1121
+ }), options), []);
1122
+ const confirmThreadAdmission = React3.useCallback((admittedThreadId) => {
1123
+ const initial = initialAdmissionRef.current;
1124
+ if (!initial || !initial.isCurrent()) return;
1125
+ initial.threadId = admittedThreadId;
1126
+ initial.resolve(admittedThreadId);
1127
+ }, []);
1128
+ const assertCurrent = (isCurrent) => {
1129
+ if (!isCurrent()) throw Object.assign(new Error("Thread selection changed."), { name: "AbortError" });
1130
+ };
867
1131
  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) => {
1132
+ const hydrate = React3.useCallback((page, selectedThreadId) => {
869
1133
  if (threadRef.current !== selectedThreadId) return;
870
1134
  const pending = page.pending.filter((message) => !promotedRef.current.has(message.id)).map((message) => {
871
1135
  const known = knownMessage(message.id, message.client_submission_id);
@@ -877,11 +1141,12 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
877
1141
  setPendingMessages([...pending, ...optimistic]);
878
1142
  setMessageQueue(page.queue);
879
1143
  }, []);
880
- const refresh = React2.useCallback(async (selectedThreadId = threadRef.current) => {
1144
+ const refresh = React3.useCallback(async (selectedThreadId = threadRef.current, signal) => {
881
1145
  if (!selectedThreadId) return null;
1146
+ const isCurrent = ownOperation();
882
1147
  const version = ++versionRef.current;
883
- const page = await transportRef.current.list(selectedThreadId);
884
- if (version !== versionRef.current || threadRef.current !== selectedThreadId) return null;
1148
+ const page = await transportRef.current.list(selectedThreadId, signal);
1149
+ if (signal?.aborted || !isCurrent() || version !== versionRef.current || threadRef.current !== selectedThreadId) return null;
885
1150
  for (const item of page.items) {
886
1151
  if (item.role === "user" && item.status === "delivered" && item.run_id) {
887
1152
  const pending = knownMessage(item.id, item.client_submission_id);
@@ -899,23 +1164,24 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
899
1164
  }
900
1165
  hydrate(page, selectedThreadId);
901
1166
  return page;
902
- }, [hydrate]);
903
- const acceptNotice = React2.useCallback((notice) => {
1167
+ }, [hydrate, ownOperation]);
1168
+ const acceptNotice = React3.useCallback((notice) => {
904
1169
  if (notice.cursor && notice.cursor <= cursorRef.current) return false;
905
1170
  if (notice.cursor) cursorRef.current = notice.cursor;
906
1171
  const pending = knownMessage(notice.messageId, notice.clientSubmissionId);
907
- if (notice.status === "delivered" && notice.runId && pending && !promotedRef.current.has(notice.messageId)) {
1172
+ if (notice.status === "delivered" && notice.runId && !promotedRef.current.has(notice.messageId)) {
908
1173
  promotedRef.current.add(notice.messageId);
909
- pendingRef.current.delete(pending.id);
910
1174
  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);
1175
+ if (pending) {
1176
+ pendingRef.current.delete(pending.id);
1177
+ setPendingMessages((current) => current.filter((message) => message.id !== pending.id));
1178
+ onPromotedRef.current({ ...pending, id: notice.messageId, run_id: notice.runId, status: "delivered" }, notice.runId);
1179
+ } else if (threadRef.current) callbacksRef.current.onUnknownPromotion?.(notice.runId, threadRef.current);
913
1180
  }
914
1181
  if (notice.status === "removed") pendingRef.current.delete(notice.messageId);
915
- void refresh().catch(() => void 0);
916
1182
  return true;
917
- }, [refresh]);
918
- const beginSubmission = React2.useCallback((input, queued = false) => {
1183
+ }, []);
1184
+ const beginSubmission = React3.useCallback((input, queued = false) => {
919
1185
  const now = (/* @__PURE__ */ new Date()).toISOString();
920
1186
  const message = {
921
1187
  id: input.clientSubmissionId,
@@ -937,34 +1203,60 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
937
1203
  updated_at: now
938
1204
  };
939
1205
  pendingRef.current.set(message.id, { ...message, optimisticQueued: queued });
1206
+ if (!queued && !threadRef.current && !initialAdmissionRef.current) {
1207
+ let resolve;
1208
+ const done = new Promise((callback) => {
1209
+ resolve = callback;
1210
+ });
1211
+ initialAdmissionRef.current = { id: message.id, done, resolve, isCurrent: ownOperation({ adoptThread: true }) };
1212
+ }
940
1213
  versionRef.current += 1;
941
1214
  if (queued) setPendingMessages((current) => [...current, message]);
942
1215
  else callbacksRef.current.onOptimistic?.(message, Boolean(input.uploading));
943
1216
  return message;
944
- }, []);
945
- const discardSubmission = React2.useCallback((message) => {
1217
+ }, [ownOperation]);
1218
+ const discardSubmission = React3.useCallback((message, failure) => {
1219
+ if (initialAdmissionRef.current?.id === message.id) {
1220
+ initialAdmissionRef.current.failure = failure;
1221
+ initialAdmissionRef.current.resolve(null);
1222
+ initialAdmissionRef.current = null;
1223
+ }
946
1224
  if (!pendingRef.current.has(message.id)) return;
947
1225
  pendingRef.current.delete(message.id);
948
1226
  callbacksRef.current.onDiscard?.(message);
949
1227
  versionRef.current += 1;
950
1228
  setPendingMessages((current) => current.filter((item) => item.client_submission_id !== message.client_submission_id));
951
1229
  }, []);
952
- const submit = React2.useCallback(async (input) => {
953
- const selectedThreadId = threadRef.current;
954
- if (!selectedThreadId) throw new Error("No active Agent thread is available.");
955
- const { queued, prepare, attachments, uploading, ...submission } = input;
1230
+ const submit = React3.useCallback(async (input) => {
1231
+ const initial = !threadRef.current ? initialAdmissionRef.current : null;
1232
+ let selectedThreadId = threadRef.current;
1233
+ if (!selectedThreadId && !initial) throw new Error("No active Agent thread is available.");
1234
+ const isCurrent = ownOperation({ adoptThread: Boolean(initial) });
1235
+ const { queued: requestedQueued, prepare, attachments, uploading, ...submission } = input;
1236
+ const priorAdmission = admissionRef.current?.threadId === (selectedThreadId || "") ? admissionRef.current.done : void 0;
1237
+ const queued = Boolean(requestedQueued || priorAdmission || initial);
956
1238
  const optimistic = beginSubmission({
957
1239
  ...submission,
958
1240
  attachments,
959
1241
  uploading,
960
1242
  clientSubmissionId: submission.clientSubmissionId || submission.idempotencyKey
961
1243
  }, queued);
1244
+ let releaseAdmission;
1245
+ const admission = { threadId: selectedThreadId || "", done: new Promise((resolve) => {
1246
+ releaseAdmission = resolve;
1247
+ }) };
1248
+ admissionRef.current = admission;
962
1249
  try {
963
1250
  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 });
1251
+ if (initial) selectedThreadId = await initial.done;
1252
+ if (priorAdmission) await priorAdmission;
1253
+ assertCurrent(isCurrent);
1254
+ if (!selectedThreadId) throw Object.assign(new Error("Initial message admission did not complete."), { name: "AbortError" });
1255
+ admission.threadId = selectedThreadId;
1256
+ pendingRef.current.set(optimistic.id, { ...optimistic, optimisticQueued: queued, attachment_ids: prepared.attachmentIds || optimistic.attachment_ids, attachments: prepared.attachments || attachments });
965
1257
  const request = { ...submission, ...prepared, clientSubmissionId: optimistic.client_submission_id, threadId: selectedThreadId };
966
1258
  const result = await retrySubmissionAdmission(() => transportRef.current.submit(request));
967
- if (threadRef.current !== selectedThreadId) return result;
1259
+ if (!isCurrent()) return result;
968
1260
  versionRef.current += 1;
969
1261
  const bound = {
970
1262
  ...optimistic,
@@ -991,53 +1283,105 @@ function useThreadMessageQueue({ threadId, transport, onPromoted, onOptimistic,
991
1283
  void refresh().catch(() => void 0);
992
1284
  return result;
993
1285
  } catch (error) {
994
- if (threadRef.current === selectedThreadId) discardSubmission(optimistic);
1286
+ if (isCurrent()) discardSubmission(optimistic);
995
1287
  throw error;
1288
+ } finally {
1289
+ void Promise.resolve(priorAdmission).then(() => {
1290
+ releaseAdmission();
1291
+ if (admissionRef.current === admission) admissionRef.current = null;
1292
+ });
996
1293
  }
997
- }, [beginSubmission, discardSubmission, refresh]);
998
- const edit = React2.useCallback(async (message, input) => {
1294
+ }, [beginSubmission, discardSubmission, refresh, ownOperation]);
1295
+ const edit = React3.useCallback(async (message, input) => {
1296
+ const isCurrent = ownOperation();
999
1297
  const updated = await transportRef.current.edit(message, input);
1298
+ assertCurrent(isCurrent);
1000
1299
  versionRef.current += 1;
1001
1300
  pendingRef.current.set(updated.id, updated);
1002
1301
  await refresh(message.thread_id);
1003
1302
  return updated;
1004
- }, [refresh]);
1005
- const remove = React2.useCallback(async (message) => {
1303
+ }, [refresh, ownOperation]);
1304
+ const remove = React3.useCallback(async (message) => {
1305
+ const isCurrent = ownOperation();
1006
1306
  await transportRef.current.remove(message);
1307
+ assertCurrent(isCurrent);
1007
1308
  versionRef.current += 1;
1008
1309
  pendingRef.current.delete(message.id);
1009
1310
  setPendingMessages((current) => current.filter((item) => item.id !== message.id));
1010
1311
  await refresh(message.thread_id);
1011
- }, [refresh]);
1012
- const retractForEdit = React2.useCallback(async (message) => {
1312
+ }, [refresh, ownOperation]);
1313
+ const retractForEdit = React3.useCallback(async (message) => {
1314
+ const isCurrent = ownOperation();
1013
1315
  await remove(message);
1316
+ assertCurrent(isCurrent);
1014
1317
  return { text: message.text, attachment_ids: message.attachment_ids, attachments: message.attachments };
1015
- }, [remove]);
1016
- const resume = React2.useCallback(async () => {
1318
+ }, [remove, ownOperation]);
1319
+ const resume = React3.useCallback(async () => {
1017
1320
  const selectedThreadId = threadRef.current;
1018
1321
  if (!selectedThreadId) return null;
1322
+ const isCurrent = ownOperation();
1019
1323
  const result = await transportRef.current.resume(selectedThreadId);
1324
+ if (!isCurrent()) return null;
1020
1325
  await refresh(selectedThreadId);
1021
1326
  return result;
1022
- }, [refresh]);
1023
- React2.useEffect(() => {
1327
+ }, [refresh, ownOperation]);
1328
+ const reset = React3.useCallback(() => {
1329
+ initialAdmissionRef.current?.resolve(null);
1330
+ initialAdmissionRef.current = null;
1331
+ admissionRef.current = null;
1332
+ selectionGenerationRef.current += 1;
1024
1333
  versionRef.current += 1;
1025
1334
  cursorRef.current = 0;
1026
1335
  promotedRef.current.clear();
1027
1336
  pendingRef.current.clear();
1028
1337
  setPendingMessages([]);
1029
1338
  setMessageQueue(null);
1339
+ }, []);
1340
+ React3.useEffect(() => {
1341
+ const initial = initialAdmissionRef.current;
1342
+ const adopting = Boolean(initial?.threadId && initial.threadId === threadId && initial.isCurrent());
1343
+ if (adopting) {
1344
+ for (const [id, message] of pendingRef.current) pendingRef.current.set(id, { ...message, thread_id: threadId });
1345
+ initialAdmissionRef.current = null;
1346
+ } else {
1347
+ initial?.resolve(null);
1348
+ initialAdmissionRef.current = null;
1349
+ pendingRef.current.clear();
1350
+ setPendingMessages([]);
1351
+ }
1352
+ versionRef.current += 1;
1353
+ cursorRef.current = 0;
1354
+ promotedRef.current.clear();
1355
+ setMessageQueue(null);
1030
1356
  if (threadId) void refresh(threadId).catch(() => void 0);
1357
+ return () => {
1358
+ initialAdmissionRef.current?.resolve(null);
1359
+ };
1031
1360
  }, [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
- return { pendingMessages, messageQueue, refresh, hydrate, acceptNotice, beginSubmission, discardSubmission, submit, edit, remove, retractForEdit, resume };
1361
+ return {
1362
+ pendingMessages,
1363
+ messageQueue,
1364
+ refresh,
1365
+ hydrate,
1366
+ acceptNotice,
1367
+ beginSubmission,
1368
+ discardSubmission,
1369
+ reset,
1370
+ confirmThreadAdmission,
1371
+ hasPendingAdmission: () => Boolean(initialAdmissionRef.current?.isCurrent()),
1372
+ waitForInitialAdmission: () => {
1373
+ const initial = initialAdmissionRef.current;
1374
+ return initial ? initial.done.then((threadId2) => {
1375
+ if (!threadId2) throw initial.failure || Object.assign(new Error("Initial message admission detached."), { name: "AbortError" });
1376
+ return threadId2;
1377
+ }) : Promise.resolve(threadRef.current);
1378
+ },
1379
+ submit,
1380
+ edit,
1381
+ remove,
1382
+ retractForEdit,
1383
+ resume
1384
+ };
1041
1385
  }
1042
1386
  function optimisticSubmissionTurn(message) {
1043
1387
  const user = {
@@ -1085,72 +1429,20 @@ function discardOptimisticSubmission(state, message) {
1085
1429
  }
1086
1430
 
1087
1431
  // 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
- }
1432
+ import * as React7 from "react";
1141
1433
 
1142
1434
  // src/use-agent-chat-thread-list.ts
1143
- import * as React3 from "react";
1435
+ import * as React4 from "react";
1144
1436
  function summaryToStored(thread) {
1145
1437
  return { ...thread, id: String(thread.id), messages: [], isHydrated: false };
1146
1438
  }
1147
1439
  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(() => {
1440
+ const [isLoadingThreads, setIsLoadingThreads] = React4.useState(false);
1441
+ const [isLoadingMoreThreads, setIsLoadingMoreThreads] = React4.useState(false);
1442
+ const [threadTotal, setThreadTotal] = React4.useState();
1443
+ const refreshPromiseRef = React4.useRef(null);
1444
+ const eventsDuringRefreshRef = React4.useRef(null);
1445
+ const refreshThreads = React4.useCallback(() => {
1154
1446
  if (refreshPromiseRef.current) return refreshPromiseRef.current;
1155
1447
  clearOperationError();
1156
1448
  setIsLoadingThreads(true);
@@ -1185,7 +1477,7 @@ function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clea
1185
1477
  }).catch(() => void 0);
1186
1478
  return request;
1187
1479
  }, [clearOperationError, client, failOperation, threadPageSize, update]);
1188
- const loadMoreThreads = React3.useCallback(async () => {
1480
+ const loadMoreThreads = React4.useCallback(async () => {
1189
1481
  if (isLoadingMoreThreads) return;
1190
1482
  const skip = stateRef.current.threads.length;
1191
1483
  if (threadTotal !== void 0 && skip >= threadTotal) return;
@@ -1220,15 +1512,30 @@ function useAgentChatThreadList({ client, threadPageSize, stateRef, update, clea
1220
1512
  }
1221
1513
 
1222
1514
  // 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) => {
1515
+ import * as React5 from "react";
1516
+ function useAgentChatFeedback({ client, update, clearOperationError, failOperation, ownOperation, refreshMessages }) {
1517
+ const mutations = React5.useRef(/* @__PURE__ */ new Map());
1518
+ const captureRead = React5.useCallback(() => {
1519
+ const revisions = new Map([...mutations.current].map(([run, mutation]) => [run, mutation.readRevision]));
1520
+ return (incoming, current) => incoming.map((message) => {
1521
+ const mutation = message.runId ? mutations.current.get(message.runId) : void 0;
1522
+ const preserve = mutation && (mutation.pending > 0 || mutation.readRevision !== revisions.get(message.runId));
1523
+ const live = preserve && current.find((item) => item.role === "assistant" && item.runId === message.runId);
1524
+ return live ? { ...message, feedback: live.feedback } : message;
1525
+ });
1526
+ }, []);
1527
+ const setFeedback = React5.useCallback(async (message, input) => {
1226
1528
  const runId = message.runId;
1227
1529
  if (!runId || message.role !== "assistant" || message.isFinal === false) {
1228
1530
  const error = new Error("Only persisted final responses can receive feedback.");
1229
1531
  failOperation("set_feedback", error);
1230
1532
  throw error;
1231
1533
  }
1534
+ const isCurrent = ownOperation();
1535
+ const revision = (mutations.current.get(runId)?.revision ?? 0) + 1;
1536
+ const pending = (mutations.current.get(runId)?.pending ?? 0) + 1;
1537
+ mutations.current.set(runId, { revision, pending, readRevision: (mutations.current.get(runId)?.readRevision ?? 0) + 1 });
1538
+ const isLatest = () => mutations.current.get(runId)?.revision === revision;
1232
1539
  const previous = message.feedback ?? null;
1233
1540
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1234
1541
  const optimistic = input.rating ? {
@@ -1240,26 +1547,34 @@ function useAgentChatFeedback({ client, update, clearOperationError, failOperati
1240
1547
  } : null;
1241
1548
  const replaceFeedback = (current, feedback) => ({
1242
1549
  ...current,
1243
- messages: current.messages.map((item) => item.id === message.id ? { ...item, feedback } : item),
1550
+ messages: current.messages.map((item) => item.role === "assistant" && item.runId === runId ? { ...item, feedback } : item),
1244
1551
  threads: current.threads.map((thread) => ({
1245
1552
  ...thread,
1246
- messages: thread.messages.map((item) => item.id === message.id ? { ...item, feedback } : item)
1553
+ messages: thread.messages.map((item) => item.role === "assistant" && item.runId === runId ? { ...item, feedback } : item)
1247
1554
  }))
1248
1555
  });
1249
1556
  clearOperationError();
1250
1557
  update((current) => replaceFeedback(current, optimistic));
1251
1558
  try {
1252
1559
  const result = await client.runs.setFeedback({ runId, ...input });
1253
- update((current) => replaceFeedback(current, result.feedback));
1560
+ if (isLatest() && isCurrent()) update((current) => replaceFeedback(current, result.feedback));
1254
1561
  } catch (error) {
1255
- update((current) => withAgentChatOperationError(replaceFeedback(current, previous), "set_feedback", error));
1562
+ if (isLatest() && isCurrent()) update((current) => {
1563
+ const restored = replaceFeedback(current, previous);
1564
+ return isCurrent() ? withAgentChatOperationError(restored, "set_feedback", error) : restored;
1565
+ });
1256
1566
  throw error;
1567
+ } finally {
1568
+ const mutation = mutations.current.get(runId);
1569
+ mutations.current.set(runId, { ...mutation, readRevision: mutation.readRevision + 1, pending: mutation.pending - 1 });
1570
+ if (mutation.pending === 1 && isCurrent()) await refreshMessages().catch(() => void 0);
1257
1571
  }
1258
- }, [clearOperationError, client, failOperation, update]);
1572
+ }, [clearOperationError, client, failOperation, update, ownOperation, refreshMessages]);
1573
+ return { setFeedback, captureRead };
1259
1574
  }
1260
1575
 
1261
1576
  // src/use-agent-chat-queue-actions.ts
1262
- import * as React5 from "react";
1577
+ import * as React6 from "react";
1263
1578
  function useAgentChatQueueActions({
1264
1579
  queue,
1265
1580
  stateRef,
@@ -1267,21 +1582,23 @@ function useAgentChatQueueActions({
1267
1582
  clearOperationError,
1268
1583
  failOperation,
1269
1584
  refreshMessages,
1270
- attachRun
1585
+ attachRun,
1586
+ ownOperation
1271
1587
  }) {
1272
- const steer = React5.useCallback(async (text2) => {
1588
+ const steer = React6.useCallback(async (text2) => {
1273
1589
  const threadId = stateRef.current.activeThreadId;
1274
1590
  if (!threadId || !text2.trim()) throw new Error("An active thread and text are required to steer.");
1591
+ const isCurrent = ownOperation();
1275
1592
  clearOperationError();
1276
1593
  try {
1277
1594
  await queue.submit({ mode: "steer", input: text2.trim(), idempotencyKey: createId() });
1278
- await refreshMessages(threadId);
1595
+ if (isCurrent()) await refreshMessages(threadId);
1279
1596
  } catch (error) {
1280
- failOperation("submit", error);
1597
+ if (isCurrent()) failOperation("submit", error);
1281
1598
  throw error;
1282
1599
  }
1283
- }, [clearOperationError, createId, failOperation, queue.submit, refreshMessages, stateRef]);
1284
- const editPendingMessage = React5.useCallback(async (message, input) => {
1600
+ }, [clearOperationError, createId, failOperation, queue.submit, refreshMessages, stateRef, ownOperation]);
1601
+ const editPendingMessage = React6.useCallback(async (message, input) => {
1285
1602
  await queue.edit(message, {
1286
1603
  text: input.text,
1287
1604
  attachmentIds: input.attachmentIds ?? message.attachment_ids,
@@ -1289,14 +1606,15 @@ function useAgentChatQueueActions({
1289
1606
  requestedModelId: input.requestedModelId
1290
1607
  });
1291
1608
  }, [queue.edit]);
1292
- const steerPendingMessage = React5.useCallback(async (message) => {
1609
+ const steerPendingMessage = React6.useCallback(async (message) => {
1293
1610
  await queue.edit(message, { text: message.text, attachmentIds: message.attachment_ids, mode: "steer" });
1294
1611
  }, [queue.edit]);
1295
- const resumeQueue = React5.useCallback(async () => {
1612
+ const resumeQueue = React6.useCallback(async () => {
1296
1613
  if (!stateRef.current.activeThreadId) return;
1614
+ const isCurrent = ownOperation();
1297
1615
  const result = await queue.resume();
1298
- if (result?.run_id) void attachRun(result.run_id).catch(() => void 0);
1299
- }, [attachRun, queue.resume, stateRef]);
1616
+ if (isCurrent() && result?.run_id && stateRef.current.activeRunId !== result.run_id) void attachRun(result.run_id).catch(() => void 0);
1617
+ }, [attachRun, queue.resume, stateRef, ownOperation]);
1300
1618
  return { steer, editPendingMessage, steerPendingMessage, removePendingMessage: queue.remove, resumeQueue };
1301
1619
  }
1302
1620
 
@@ -1307,9 +1625,6 @@ function resolveInitialActiveThreadId(options, storedThreadId) {
1307
1625
  function shouldHydrateSelectedThread(selectedThreadId, activeThreadId, activeThreadIsHydrated) {
1308
1626
  return Boolean(selectedThreadId) && (selectedThreadId !== activeThreadId || !activeThreadIsHydrated);
1309
1627
  }
1310
- function isCurrentThreadOpenRequest(input) {
1311
- return !input.signalAborted && input.requestGeneration === input.currentGeneration && input.requestedThreadId === input.activeThreadId;
1312
- }
1313
1628
 
1314
1629
  // src/use-agent-chat.ts
1315
1630
  function activeRunIdFromThread(thread) {
@@ -1322,7 +1637,7 @@ function useAgentChat(options = {}) {
1322
1637
  const client = options.client || runtime.client;
1323
1638
  const pageSize = options.pageSize ?? 30;
1324
1639
  const threadPageSize = options.threadPageSize ?? 50;
1325
- const [state, setState] = React6.useState(() => ({
1640
+ const [state, setState] = React7.useState(() => ({
1326
1641
  ...initialAgentChatState,
1327
1642
  activeThreadId: resolveInitialActiveThreadId(
1328
1643
  options,
@@ -1330,34 +1645,38 @@ function useAgentChat(options = {}) {
1330
1645
  ),
1331
1646
  threads: runtime.storage?.loadThreads() ?? []
1332
1647
  }));
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);
1648
+ const [isLoadingHistory, setIsLoadingHistory] = React7.useState(false);
1649
+ const [isLoadingOlder, setIsLoadingOlder] = React7.useState(false);
1650
+ const [isSubmitting, setIsSubmitting] = React7.useState(false);
1651
+ const streamControllerRef = React7.useRef(null);
1652
+ const threadOpenControllerRef = React7.useRef(null);
1653
+ const threadOpenGenerationRef = React7.useRef(0);
1654
+ const attachRunRef = React7.useRef(async () => void 0);
1655
+ const messageRefreshVersionRef = React7.useRef(0);
1656
+ const promotedRunHintRef = React7.useRef(null);
1657
+ const promotedAttachRef = React7.useRef(null);
1658
+ const olderRequestRef = React7.useRef(null);
1659
+ const submissionGenerationRef = React7.useRef(0);
1660
+ const stateRef = React7.useRef(state);
1346
1661
  stateRef.current = state;
1347
- const persist = React6.useCallback((next) => {
1662
+ const ownOperation = React7.useCallback((options2) => captureChatOperation(() => ({
1663
+ generation: threadOpenGenerationRef.current,
1664
+ threadId: stateRef.current.activeThreadId,
1665
+ runId: stateRef.current.activeRunId,
1666
+ stream: streamControllerRef.current
1667
+ }), options2), []);
1668
+ const persist = React7.useCallback((next) => {
1348
1669
  runtime.storage?.saveThreads(next.threads);
1349
1670
  runtime.storage?.saveActiveThreadId?.(next.activeThreadId);
1350
1671
  }, [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
- });
1672
+ const update = React7.useCallback((updater) => {
1673
+ const next = updater(stateRef.current);
1674
+ stateRef.current = next;
1675
+ persist(next);
1676
+ setState(next);
1358
1677
  }, [persist]);
1359
- const queueTransport = React6.useMemo(() => ({
1360
- list: (threadId) => client.threads.messages.list({ threadId, limit: pageSize }),
1678
+ const queueTransport = React7.useMemo(() => ({
1679
+ list: (threadId, signal) => client.threads.messages.list({ threadId, limit: pageSize, signal }),
1361
1680
  submit: (input) => client.threads.messages.submit(input),
1362
1681
  edit: (message, input) => client.threads.messages.edit({
1363
1682
  threadId: message.thread_id,
@@ -1374,40 +1693,46 @@ function useAgentChat(options = {}) {
1374
1693
  },
1375
1694
  resume: (threadId) => client.threads.resumeQueue({ threadId })
1376
1695
  }), [client, pageSize]);
1377
- const onQueuePromotion = React6.useCallback((message, runId) => {
1378
- if (stateRef.current.activeThreadId !== message.thread_id) return;
1696
+ const attachPromotion = React7.useCallback((runId, threadId) => {
1697
+ if (stateRef.current.activeThreadId !== threadId) return;
1379
1698
  promotedRunHintRef.current = runId;
1699
+ if (stateRef.current.activeRunId === runId || promotedAttachRef.current === runId) return;
1700
+ promotedAttachRef.current = runId;
1701
+ const isCurrent = ownOperation();
1702
+ void attachRunRef.current(runId, 0).catch(() => {
1703
+ if (isCurrent() && promotedAttachRef.current === runId) promotedAttachRef.current = null;
1704
+ });
1705
+ }, [ownOperation]);
1706
+ const onQueuePromotion = React7.useCallback((message, runId) => {
1707
+ if (stateRef.current.activeThreadId !== message.thread_id) return;
1380
1708
  const userMessage = threadTimelineToMessages([{ ...message, run_id: runId, status: "delivered" }])[0];
1381
1709
  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]);
1710
+ attachPromotion(runId, message.thread_id);
1711
+ }, [update, attachPromotion]);
1389
1712
  const queue = useThreadMessageQueue({
1390
1713
  threadId: state.activeThreadId,
1391
1714
  transport: queueTransport,
1715
+ ownOperation,
1392
1716
  onPromoted: onQueuePromotion,
1717
+ onUnknownPromotion: attachPromotion,
1393
1718
  onOptimistic: (message, uploading) => update((current) => optimisticSubmissionState(current, message, uploading)),
1394
1719
  onDiscard: (message) => update((current) => discardOptimisticSubmission(current, message))
1395
1720
  });
1396
1721
  const { pendingMessages, messageQueue } = queue;
1397
- const eventBuffer = React6.useMemo(
1722
+ const eventBuffer = React7.useMemo(
1398
1723
  () => createRuntimeEventFrameBuffer(
1399
1724
  (events) => update((current) => applyRuntimeEvents(current, events)),
1400
1725
  runtimeEventFrameScheduler()
1401
1726
  ),
1402
1727
  [update]
1403
1728
  );
1404
- const clearOperationError = React6.useCallback((operation) => {
1729
+ const clearOperationError = React7.useCallback((operation) => {
1405
1730
  update((current) => clearAgentChatOperationError(current, operation));
1406
1731
  }, [update]);
1407
- const clearBackgroundError = React6.useCallback(() => {
1732
+ const clearBackgroundError = React7.useCallback(() => {
1408
1733
  update(clearAgentChatBackgroundError);
1409
1734
  }, [update]);
1410
- const failOperation = React6.useCallback((operation, cause) => {
1735
+ const failOperation = React7.useCallback((operation, cause) => {
1411
1736
  update((current) => withAgentChatOperationError(current, operation, cause));
1412
1737
  }, [update]);
1413
1738
  const {
@@ -1418,60 +1743,66 @@ function useAgentChat(options = {}) {
1418
1743
  refreshThreads,
1419
1744
  loadMoreThreads
1420
1745
  } = useAgentChatThreadList({ client, threadPageSize, stateRef, update, clearOperationError, failOperation });
1421
- const invalidateThreadOpen = React6.useCallback(() => {
1746
+ const invalidateThreadOpen = React7.useCallback(() => {
1422
1747
  threadOpenGenerationRef.current += 1;
1423
1748
  threadOpenControllerRef.current?.abort("Thread selection changed.");
1424
1749
  threadOpenControllerRef.current = null;
1750
+ messageRefreshVersionRef.current += 1;
1751
+ promotedAttachRef.current = null;
1752
+ promotedRunHintRef.current = null;
1753
+ olderRequestRef.current = null;
1754
+ queue.reset();
1755
+ setIsSubmitting(false);
1756
+ setIsLoadingOlder(false);
1425
1757
  setIsLoadingHistory(false);
1426
- }, []);
1427
- const detach = React6.useCallback(() => {
1758
+ }, [queue.reset]);
1759
+ const detach = React7.useCallback(() => {
1428
1760
  eventBuffer.flush();
1429
1761
  streamControllerRef.current?.abort();
1430
1762
  streamControllerRef.current = null;
1431
1763
  update((current) => ({ ...current, streamingMessageId: null, runState: current.activeRunId ? "reconnecting" : "idle" }));
1432
1764
  }, [eventBuffer, update]);
1433
- const refreshMessages = React6.useCallback(async (threadId, promotedRunId) => {
1765
+ const refreshMessagesRef = React7.useRef(async () => void 0);
1766
+ const { setFeedback, captureRead: captureFeedbackRead } = useAgentChatFeedback({
1767
+ client,
1768
+ update,
1769
+ clearOperationError,
1770
+ failOperation,
1771
+ ownOperation,
1772
+ refreshMessages: React7.useCallback(() => refreshMessagesRef.current(), [])
1773
+ });
1774
+ const refreshMessages = React7.useCallback(async (threadId, promotedRunId, signal) => {
1434
1775
  const target = threadId || stateRef.current.activeThreadId;
1435
1776
  if (!target) return;
1777
+ const isCurrent = ownOperation();
1778
+ const reconcileFeedback = captureFeedbackRead();
1436
1779
  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;
1780
+ const page = await client.threads.messages.list({ threadId: target, limit: pageSize, ...signal ? { signal } : {} });
1781
+ if (!isCurrent() || stateRef.current.activeThreadId !== target || version !== messageRefreshVersionRef.current) return;
1439
1782
  queue.hydrate(page, target);
1440
1783
  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(() => () => {
1784
+ if (promoted && page.items.some((message) => message.run_id === promoted)) promotedRunHintRef.current = null;
1785
+ update((current) => {
1786
+ if (!isCurrent() || current.activeThreadId !== target) return current;
1787
+ const messages = reconcileThreadTimeline(
1788
+ current.messages,
1789
+ reconcileFeedback(threadTimelineToMessages(page.items), current.messages),
1790
+ current.activeRunId,
1791
+ current.streamingMessageId
1792
+ );
1793
+ return { ...current, messages, threads: current.threads.map((thread) => thread.id === target ? { ...thread, messages } : thread) };
1794
+ });
1795
+ }, [client, ownOperation, pageSize, queue.hydrate, update, captureFeedbackRead]);
1796
+ refreshMessagesRef.current = refreshMessages;
1797
+ React7.useEffect(() => () => {
1798
+ threadOpenGenerationRef.current += 1;
1468
1799
  eventBuffer.dispose();
1469
1800
  streamControllerRef.current?.abort();
1470
1801
  threadOpenControllerRef.current?.abort();
1471
1802
  }, [eventBuffer]);
1472
- const openThread = React6.useCallback(async (threadId) => {
1803
+ const openThread = React7.useCallback(async (threadId) => {
1473
1804
  const previousSelection = captureAgentChatThreadSelection(stateRef.current);
1474
- threadOpenControllerRef.current?.abort();
1805
+ invalidateThreadOpen();
1475
1806
  const requestController = runtime.createAbortController();
1476
1807
  threadOpenControllerRef.current = requestController;
1477
1808
  const requestGeneration = threadOpenGenerationRef.current + 1;
@@ -1491,6 +1822,8 @@ function useAgentChat(options = {}) {
1491
1822
  runState: "idle",
1492
1823
  streamingMessageId: null
1493
1824
  }));
1825
+ const isCurrent = ownOperation();
1826
+ const reconcileFeedback = captureFeedbackRead();
1494
1827
  setIsLoadingHistory(true);
1495
1828
  try {
1496
1829
  const detail = await client.threads.get({
@@ -1498,39 +1831,21 @@ function useAgentChat(options = {}) {
1498
1831
  limit: pageSize,
1499
1832
  signal: requestController.signal
1500
1833
  });
1501
- if (!isCurrentThreadOpenRequest({
1502
- activeThreadId: stateRef.current.activeThreadId,
1503
- currentGeneration: threadOpenGenerationRef.current,
1504
- requestGeneration,
1505
- requestedThreadId: threadId,
1506
- signalAborted: requestController.signal.aborted
1507
- })) return;
1834
+ if (!isCurrent()) return;
1508
1835
  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;
1836
+ if (!isCurrent()) return;
1516
1837
  queue.hydrate(timeline, threadId);
1517
- const messages = timeline.items.length ? threadTimelineToMessages(timeline.items, detail) : threadDetailToMessages(detail);
1838
+ const messages = reconcileFeedback(threadTimelineToMessages(timeline.items), stateRef.current.messages);
1518
1839
  const context = latestThreadContext(detail);
1519
1840
  const stored = {
1520
1841
  ...detail,
1521
1842
  messages,
1522
1843
  isHydrated: true,
1523
- hasOlderTurns: Boolean(detail.paging?.has_more),
1524
- nextBeforeTurnIndex: detail.paging?.next_before_turn_index ?? null,
1844
+ hasOlderTurns: Boolean(timeline.paging.has_more),
1845
+ nextBeforeTurnIndex: timeline.paging.next_before_turn_index,
1525
1846
  ...context
1526
1847
  };
1527
- update((current) => isCurrentThreadOpenRequest({
1528
- activeThreadId: current.activeThreadId,
1529
- currentGeneration: threadOpenGenerationRef.current,
1530
- requestGeneration,
1531
- requestedThreadId: threadId,
1532
- signalAborted: requestController.signal.aborted
1533
- }) ? {
1848
+ update((current) => isCurrent() ? {
1534
1849
  ...current,
1535
1850
  activeThreadId: threadId,
1536
1851
  messages,
@@ -1540,13 +1855,7 @@ function useAgentChat(options = {}) {
1540
1855
  threads: current.threads.some((thread) => thread.id === threadId) ? current.threads.map((thread) => thread.id === threadId ? stored : thread) : [stored, ...current.threads]
1541
1856
  } : current);
1542
1857
  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);
1858
+ if (activeRunId && isCurrent()) void attachRunRef.current(activeRunId).catch(() => void 0);
1550
1859
  } catch (error) {
1551
1860
  if (!requestController.signal.aborted && !isAbortLike(error) && requestGeneration === threadOpenGenerationRef.current) {
1552
1861
  failOperation("open_thread", error);
@@ -1562,28 +1871,34 @@ function useAgentChat(options = {}) {
1562
1871
  setIsLoadingHistory(false);
1563
1872
  }
1564
1873
  }
1565
- }, [clearOperationError, client, detach, failOperation, pageSize, queue.hydrate, runtime, update]);
1566
- const consumeEvent = React6.useCallback((event) => {
1874
+ }, [clearOperationError, client, detach, failOperation, pageSize, queue.hydrate, runtime, update, invalidateThreadOpen, ownOperation, captureFeedbackRead]);
1875
+ const consumeEvent = React7.useCallback((event) => {
1567
1876
  const eventThreadId = typeof event.payload?.thread_id === "string" ? event.payload.thread_id.trim() : "";
1568
1877
  if (eventThreadId && !stateRef.current.activeThreadId) {
1878
+ queue.confirmThreadAdmission(eventThreadId);
1569
1879
  update((current) => adoptStreamingThread(current, eventThreadId));
1570
- firstSubmissionAdmittedRef.current?.();
1880
+ setIsSubmitting(false);
1571
1881
  }
1572
1882
  eventBuffer.push(event);
1573
- }, [eventBuffer, update]);
1574
- const attachRun = React6.useCallback(async (runId, cursor) => {
1883
+ }, [eventBuffer, update, queue.confirmThreadAdmission]);
1884
+ const attachRun = React7.useCallback(async (runId, cursor) => {
1885
+ eventBuffer.flush();
1575
1886
  streamControllerRef.current?.abort();
1576
1887
  const controller = runtime.createAbortController();
1577
1888
  streamControllerRef.current = controller;
1578
- clearOperationError();
1889
+ const isCurrent = ownOperation({ stream: true });
1890
+ clearOperationError("attach_run");
1579
1891
  update((current) => ({ ...current, activeRunId: runId, runState: "reconnecting", error: null }));
1580
1892
  try {
1581
- const result = await client.runs.attach({ runId, cursor, signal: controller.signal }, consumeEvent);
1893
+ const result = await client.runs.attach({ runId, cursor, leaseSeconds: 60, signal: controller.signal }, (event) => {
1894
+ if (isCurrent()) consumeEvent(event);
1895
+ });
1896
+ if (!isCurrent()) return;
1582
1897
  eventBuffer.flush();
1583
1898
  update((current) => ({ ...current, activeRunId: result.detached ? runId : current.activeRunId, cursor: result.cursor ?? current.cursor, runState: result.detached ? "reconnecting" : current.runState }));
1584
1899
  if (!result.detached && result.threadId) await refreshMessages(result.threadId);
1585
1900
  } catch (error) {
1586
- if (!isAbortLike(error)) {
1901
+ if (isCurrent() && !isAbortLike(error)) {
1587
1902
  update((current) => ({ ...current, runState: "failed", streamingMessageId: null }));
1588
1903
  failOperation("attach_run", error);
1589
1904
  throw error;
@@ -1591,15 +1906,14 @@ function useAgentChat(options = {}) {
1591
1906
  } finally {
1592
1907
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
1593
1908
  }
1594
- }, [clearOperationError, client, consumeEvent, eventBuffer, failOperation, refreshMessages, runtime, update]);
1909
+ }, [clearOperationError, client, consumeEvent, eventBuffer, failOperation, refreshMessages, runtime, update, ownOperation]);
1595
1910
  attachRunRef.current = attachRun;
1596
- const submit = React6.useCallback(async (input) => {
1911
+ const submit = React7.useCallback(async (input) => {
1597
1912
  if (!input.text.trim() && !input.attachmentIds?.length && !input.uploads?.length) return;
1598
- if (firstSubmissionRef.current && !stateRef.current.activeThreadId) {
1599
- await firstSubmissionRef.current;
1600
- }
1601
1913
  const existingThreadId = stateRef.current.activeThreadId;
1602
- if (existingThreadId) {
1914
+ if (existingThreadId || queue.hasPendingAdmission()) {
1915
+ const isCurrent = ownOperation({ adoptThread: !existingThreadId });
1916
+ const submissionGeneration = ++submissionGenerationRef.current;
1603
1917
  clearOperationError();
1604
1918
  setIsSubmitting(true);
1605
1919
  try {
@@ -1627,7 +1941,7 @@ function useAgentChat(options = {}) {
1627
1941
  prepare: async () => {
1628
1942
  const uploaded = input.uploads?.length ? await client.attachments.uploadBatch({
1629
1943
  uploads: input.uploads,
1630
- threadId: existingThreadId,
1944
+ threadId: existingThreadId || void 0,
1631
1945
  requestedAgentId: input.requestedAgentId,
1632
1946
  idempotencyKey: runtime.createId()
1633
1947
  }) : [];
@@ -1644,21 +1958,21 @@ function useAgentChat(options = {}) {
1644
1958
  }
1645
1959
  });
1646
1960
  } catch (error) {
1647
- failOperation("submit", error);
1961
+ if (isCurrent()) failOperation("submit", error);
1648
1962
  throw error;
1649
1963
  } finally {
1650
- setIsSubmitting(false);
1964
+ if (isCurrent() && submissionGeneration === submissionGenerationRef.current) setIsSubmitting(false);
1651
1965
  }
1652
1966
  return;
1653
1967
  }
1654
- const admission = new Promise((resolve) => {
1655
- firstSubmissionAdmittedRef.current = resolve;
1656
- });
1968
+ invalidateThreadOpen();
1969
+ const submissionOwner = ownOperation({ adoptThread: true });
1657
1970
  const stream = (async () => {
1658
- invalidateThreadOpen();
1659
1971
  detach();
1660
1972
  const controller = runtime.createAbortController();
1661
1973
  streamControllerRef.current = controller;
1974
+ const isCurrent = ownOperation({ stream: true, adoptThread: true });
1975
+ const submissionGeneration = ++submissionGenerationRef.current;
1662
1976
  const ticket = queue.beginSubmission({
1663
1977
  input: input.text.trim(),
1664
1978
  clientSubmissionId: runtime.createId(),
@@ -1689,6 +2003,7 @@ function useAgentChat(options = {}) {
1689
2003
  idempotencyKey: runtime.createId(),
1690
2004
  signal: controller.signal
1691
2005
  }) : [];
2006
+ if (!isCurrent()) return;
1692
2007
  if (hasPendingUploads && uploaded.length !== input.uploads?.length) {
1693
2008
  throw new Error("Attachment upload returned an incomplete batch");
1694
2009
  }
@@ -1701,7 +2016,20 @@ function useAgentChat(options = {}) {
1701
2016
  runState: "streaming"
1702
2017
  }));
1703
2018
  }
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);
2019
+ const result = await client.chat.stream({
2020
+ leaseSeconds: 60,
2021
+ clientSubmissionId: ticket.client_submission_id,
2022
+ input: input.text.trim(),
2023
+ threadId: stateRef.current.activeThreadId || void 0,
2024
+ attachmentIds,
2025
+ requestedModelId: input.requestedModelId,
2026
+ requestedAgentId: input.requestedAgentId,
2027
+ idempotencyKey,
2028
+ signal: controller.signal
2029
+ }, (event) => {
2030
+ if (isCurrent()) consumeEvent(event);
2031
+ });
2032
+ if (!isCurrent()) return;
1705
2033
  eventBuffer.flush();
1706
2034
  update((current) => {
1707
2035
  const threadId = result.threadId || current.activeThreadId;
@@ -1717,8 +2045,9 @@ function useAgentChat(options = {}) {
1717
2045
  if (result.threadId) await refreshThreads().catch(() => void 0);
1718
2046
  if (result.threadId && !result.detached) await refreshMessages(result.threadId);
1719
2047
  } catch (error) {
2048
+ if (!isCurrent()) return;
1720
2049
  const admitted = Boolean(stateRef.current.messages.find((message) => message.id === userId)?.runId);
1721
- if (!admitted) queue.discardSubmission(ticket);
2050
+ if (!admitted) queue.discardSubmission(ticket, error);
1722
2051
  if (!isAbortLike(error)) {
1723
2052
  update((current) => ({
1724
2053
  ...current,
@@ -1729,19 +2058,17 @@ function useAgentChat(options = {}) {
1729
2058
  throw error;
1730
2059
  }
1731
2060
  } finally {
1732
- setIsSubmitting(false);
2061
+ if (!stateRef.current.messages.some((message) => (message.id === ticket.id || message.clientSubmissionId === ticket.client_submission_id) && message.runId)) queue.discardSubmission(ticket);
2062
+ if (isCurrent() && submissionGeneration === submissionGenerationRef.current) setIsSubmitting(false);
1733
2063
  if (streamControllerRef.current === controller) streamControllerRef.current = null;
1734
2064
  }
1735
2065
  })();
1736
- const accepted = Promise.race([stream, admission]);
1737
- firstSubmissionRef.current = accepted;
1738
2066
  try {
1739
- await accepted;
1740
- } finally {
1741
- if (firstSubmissionRef.current === accepted) firstSubmissionRef.current = null;
1742
- firstSubmissionAdmittedRef.current = null;
2067
+ await Promise.race([stream, queue.waitForInitialAdmission()]);
2068
+ } catch (error) {
2069
+ if (submissionOwner() || !isAbortLike(error)) throw error;
1743
2070
  }
1744
- }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update]);
2071
+ }, [clearOperationError, client, consumeEvent, detach, eventBuffer, failOperation, invalidateThreadOpen, queue.submit, refreshMessages, refreshThreads, runtime, update, ownOperation]);
1745
2072
  const { steer, editPendingMessage, steerPendingMessage, removePendingMessage, resumeQueue } = useAgentChatQueueActions({
1746
2073
  queue,
1747
2074
  stateRef,
@@ -1749,42 +2076,38 @@ function useAgentChat(options = {}) {
1749
2076
  clearOperationError,
1750
2077
  failOperation,
1751
2078
  refreshMessages,
1752
- attachRun
2079
+ attachRun,
2080
+ ownOperation
1753
2081
  });
1754
- const startNewThread = React6.useCallback(() => {
2082
+ const startNewThread = React7.useCallback(() => {
1755
2083
  invalidateThreadOpen();
1756
2084
  detach();
1757
2085
  update((current) => ({ ...initialAgentChatState, threads: current.threads }));
1758
2086
  }, [detach, invalidateThreadOpen, update]);
1759
- const deleteThread = React6.useCallback(async (threadId) => {
1760
- if (stateRef.current.activeThreadId === threadId) invalidateThreadOpen();
2087
+ const deleteThread = React7.useCallback(async (threadId) => {
2088
+ const isCurrent = ownOperation();
1761
2089
  clearOperationError();
1762
2090
  try {
1763
2091
  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
- }));
2092
+ if (stateRef.current.activeThreadId === threadId) startNewThread();
2093
+ update((current) => ({ ...current, threads: current.threads.filter((thread) => thread.id !== threadId) }));
1773
2094
  } catch (error) {
1774
2095
  if (!isAbortLike(error)) {
1775
- failOperation("delete_thread", error);
2096
+ if (isCurrent()) failOperation("delete_thread", error);
1776
2097
  throw error;
1777
2098
  }
1778
2099
  }
1779
- }, [clearOperationError, client, failOperation, invalidateThreadOpen, runtime, update]);
1780
- const cancelRun = React6.useCallback(async () => {
2100
+ }, [clearOperationError, client, failOperation, ownOperation, runtime, startNewThread, update]);
2101
+ const cancelRun = React7.useCallback(async () => {
1781
2102
  const runId = stateRef.current.activeRunId;
1782
2103
  if (!runId || stateRef.current.runState === "cancelling") return;
2104
+ const isCurrent = ownOperation({ run: true, stream: true });
1783
2105
  const previousRunState = stateRef.current.runState;
1784
2106
  clearOperationError();
1785
2107
  update((current) => current.activeRunId === runId ? { ...current, runState: "cancelling" } : current);
1786
2108
  try {
1787
2109
  const result = await client.runs.cancel({ runId, idempotencyKey: runtime.createId() });
2110
+ if (!isCurrent()) return;
1788
2111
  if (result.status === "cancelled") {
1789
2112
  detach();
1790
2113
  update((current) => ({
@@ -1809,14 +2132,15 @@ function useAgentChat(options = {}) {
1809
2132
  }));
1810
2133
  }
1811
2134
  } catch (error) {
1812
- if (!isAbortLike(error)) {
2135
+ if (isCurrent() && !isAbortLike(error)) {
1813
2136
  update((current) => current.activeRunId === runId ? { ...current, runState: previousRunState } : current);
1814
2137
  failOperation("cancel_run", error);
1815
2138
  throw error;
1816
2139
  }
1817
2140
  }
1818
- }, [clearOperationError, client, detach, failOperation, runtime, update]);
1819
- const resumeHitl = React6.useCallback(async (part, response) => {
2141
+ }, [clearOperationError, client, detach, failOperation, runtime, update, ownOperation]);
2142
+ const resumeHitl = React7.useCallback(async (part, response) => {
2143
+ const isCurrent = ownOperation({ run: true, stream: true });
1820
2144
  const runId = stateRef.current.activeRunId || stateRef.current.messages.find((message) => message.parts.includes(part))?.runId;
1821
2145
  clearOperationError();
1822
2146
  if (!runId) {
@@ -1831,32 +2155,52 @@ function useAgentChat(options = {}) {
1831
2155
  ...response,
1832
2156
  idempotencyKey: runtime.createId()
1833
2157
  });
1834
- await attachRun(result.run_id || runId);
2158
+ if (isCurrent()) await attachRun(result.run_id || runId);
1835
2159
  } catch (error) {
1836
- if (!isAbortLike(error)) failOperation("resume_hitl", error);
2160
+ if (isCurrent() && !isAbortLike(error)) failOperation("resume_hitl", error);
1837
2161
  throw error;
1838
2162
  }
1839
- }, [attachRun, clearOperationError, client, failOperation, runtime]);
1840
- const setFeedback = useAgentChatFeedback({ client, update, clearOperationError, failOperation });
1841
- const loadOlder = React6.useCallback(async () => {
2163
+ }, [attachRun, clearOperationError, client, failOperation, runtime, ownOperation]);
2164
+ const loadOlder = React7.useCallback(() => {
1842
2165
  const thread = stateRef.current.threads.find((item) => item.id === stateRef.current.activeThreadId);
1843
- if (!thread?.nextBeforeTurnIndex) return;
2166
+ if (thread?.nextBeforeTurnIndex == null) return Promise.resolve();
2167
+ const cursor = thread.nextBeforeTurnIndex;
2168
+ const reconcileFeedback = captureFeedbackRead();
2169
+ const key = `${threadOpenGenerationRef.current}:${thread.id}:${cursor}`;
2170
+ if (olderRequestRef.current?.key === key) return olderRequestRef.current.promise;
2171
+ const isCurrent = ownOperation();
1844
2172
  clearOperationError();
1845
2173
  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;
2174
+ const promise = (async () => {
2175
+ try {
2176
+ const page = await client.threads.messages.list({ threadId: thread.id, limit: pageSize, before: cursor });
2177
+ if (!isCurrent()) return;
2178
+ const older = reconcileFeedback(threadTimelineToMessages(page.items), stateRef.current.messages);
2179
+ update((current) => ({
2180
+ ...current,
2181
+ messages: prependThreadTimeline(current.messages, older),
2182
+ threads: current.threads.map((item) => item.id === thread.id ? {
2183
+ ...item,
2184
+ hasOlderTurns: page.paging.has_more,
2185
+ nextBeforeTurnIndex: page.paging.next_before_turn_index
2186
+ } : item)
2187
+ }));
2188
+ } catch (error) {
2189
+ if (isCurrent() && !isAbortLike(error)) {
2190
+ failOperation("load_older_turns", error);
2191
+ throw error;
2192
+ }
2193
+ } finally {
2194
+ if (olderRequestRef.current?.key === key) {
2195
+ olderRequestRef.current = null;
2196
+ setIsLoadingOlder(false);
2197
+ }
1854
2198
  }
1855
- } finally {
1856
- setIsLoadingOlder(false);
1857
- }
1858
- }, [clearOperationError, client, failOperation, pageSize, update]);
1859
- const regenerate = React6.useCallback(async (message) => {
2199
+ })();
2200
+ olderRequestRef.current = { key, promise };
2201
+ return promise;
2202
+ }, [clearOperationError, client, failOperation, ownOperation, pageSize, update, captureFeedbackRead]);
2203
+ const regenerate = React7.useCallback(async (message) => {
1860
2204
  const index = stateRef.current.messages.findIndex((item) => item.id === message.id);
1861
2205
  const user = stateRef.current.messages.slice(0, index).reverse().find((item) => item.role === "user");
1862
2206
  if (user) await submit({
@@ -1865,51 +2209,68 @@ function useAgentChat(options = {}) {
1865
2209
  attachments: user.attachments
1866
2210
  });
1867
2211
  }, [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();
2212
+ useChatObservation({
2213
+ state: {
2214
+ threadId: state.activeThreadId,
2215
+ running: Boolean(state.activeRunId) && !["idle", "completed", "failed", "cancelled"].includes(state.runState),
2216
+ waiting: ["paused", "waiting", "interrupted"].includes(state.runState),
2217
+ pending: pendingMessages.length > 0,
2218
+ paused: Boolean(messageQueue?.paused),
2219
+ degraded: state.runState === "reconnecting",
2220
+ mode: options.observation
2221
+ },
2222
+ visibility: runtime.visibility,
2223
+ createAbortController: runtime.createAbortController,
2224
+ events: (input, handler) => client.threads.events(input, handler),
2225
+ async refresh(signal) {
2226
+ const target = stateRef.current.activeThreadId;
2227
+ if (!target) return;
2228
+ const isCurrent = ownOperation();
2229
+ const detail = await client.threads.get({ threadId: target, limit: 1, signal });
2230
+ if (!isCurrent() || signal.aborted) return;
2231
+ const summary = summaryToStored(detail);
2232
+ update((current) => ({ ...current, threads: applyThreadSummaryEvent(current.threads, {
2233
+ schema_version: "agents24.thread_summary_event.v1",
2234
+ event: "thread.summary.updated",
2235
+ cursor: 0,
2236
+ thread: detail
2237
+ }) }));
2238
+ const runId = activeRunIdFromThread(summary);
2239
+ if (runId && !streamControllerRef.current) {
2240
+ void attachRunRef.current(runId, runId === stateRef.current.activeRunId ? stateRef.current.cursor ?? void 0 : 0).catch(() => void 0);
1908
2241
  }
1909
- });
1910
- return () => controller.abort();
1911
- }, [clearBackgroundError, client, queue.acceptNotice, refreshMessages, refreshThreads, runtime.createAbortController, update]);
1912
- React6.useEffect(() => {
2242
+ await refreshMessages(target, runId || void 0, signal);
2243
+ },
2244
+ onEvent(event) {
2245
+ if (event.event === "thread.deleted" && event.thread_id === stateRef.current.activeThreadId) startNewThread();
2246
+ threadEventsDuringRefreshRef.current?.push(event);
2247
+ update((current) => ({
2248
+ ...clearAgentChatBackgroundError(current),
2249
+ threads: applyThreadSummaryEvent(current.threads, event)
2250
+ }));
2251
+ if (event.event === "thread.message.updated" && event.thread_id === stateRef.current.activeThreadId) {
2252
+ queue.acceptNotice({
2253
+ messageId: event.message_id,
2254
+ status: event.status,
2255
+ runId: event.run_id,
2256
+ cursor: event.cursor,
2257
+ clientSubmissionId: event.client_submission_id
2258
+ });
2259
+ }
2260
+ },
2261
+ onError(error) {
2262
+ if (!isAbortLike(error)) update((current) => withAgentChatBackgroundError(current, error));
2263
+ },
2264
+ onRecovered: clearBackgroundError,
2265
+ onDetach: detach,
2266
+ onResume() {
2267
+ const current = stateRef.current;
2268
+ if (current.activeRunId && !streamControllerRef.current && !["paused", "waiting", "interrupted", "completed", "idle"].includes(current.runState)) {
2269
+ void attachRunRef.current(current.activeRunId, current.cursor ?? void 0).catch(() => void 0);
2270
+ }
2271
+ }
2272
+ });
2273
+ React7.useEffect(() => {
1913
2274
  const threadId = options.activeThreadId;
1914
2275
  const activeThread2 = stateRef.current.threads.find((thread) => thread.id === stateRef.current.activeThreadId);
1915
2276
  if (shouldHydrateSelectedThread(threadId, stateRef.current.activeThreadId, Boolean(activeThread2?.isHydrated))) {
@@ -1956,15 +2317,15 @@ function useAgentChat(options = {}) {
1956
2317
  }
1957
2318
 
1958
2319
  // src/use-agent-resources.ts
1959
- import * as React7 from "react";
2320
+ import * as React8 from "react";
1960
2321
  function useAgentThreads(options = {}) {
1961
2322
  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 () => {
2323
+ const [threads, setThreads] = React8.useState([]);
2324
+ const [total, setTotal] = React8.useState();
2325
+ const [isLoading, setIsLoading] = React8.useState(true);
2326
+ const [error, setError] = React8.useState(null);
2327
+ const controllerRef = React8.useRef(null);
2328
+ const load = React8.useCallback(async () => {
1968
2329
  controllerRef.current?.abort();
1969
2330
  const controller = createAbortController();
1970
2331
  controllerRef.current = controller;
@@ -1983,7 +2344,7 @@ function useAgentThreads(options = {}) {
1983
2344
  }
1984
2345
  }
1985
2346
  }, [client, createAbortController, options.limit, options.skip]);
1986
- React7.useEffect(() => {
2347
+ React8.useEffect(() => {
1987
2348
  void load();
1988
2349
  return () => controllerRef.current?.abort();
1989
2350
  }, [load]);
@@ -1991,11 +2352,11 @@ function useAgentThreads(options = {}) {
1991
2352
  }
1992
2353
  function useAgentThread(threadId, options = {}) {
1993
2354
  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 () => {
2355
+ const [thread, setThread] = React8.useState(null);
2356
+ const [isLoading, setIsLoading] = React8.useState(Boolean(threadId));
2357
+ const [error, setError] = React8.useState(null);
2358
+ const controllerRef = React8.useRef(null);
2359
+ const load = React8.useCallback(async () => {
1999
2360
  if (!threadId) {
2000
2361
  controllerRef.current?.abort();
2001
2362
  controllerRef.current = null;
@@ -2022,7 +2383,7 @@ function useAgentThread(threadId, options = {}) {
2022
2383
  }
2023
2384
  }
2024
2385
  }, [client, createAbortController, options.beforeTurnIndex, options.limit, threadId]);
2025
- React7.useEffect(() => {
2386
+ React8.useEffect(() => {
2026
2387
  void load();
2027
2388
  return () => controllerRef.current?.abort();
2028
2389
  }, [load]);
@@ -2030,11 +2391,11 @@ function useAgentThread(threadId, options = {}) {
2030
2391
  }
2031
2392
  function useThreadComputer(threadId) {
2032
2393
  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 () => {
2394
+ const [computer, setComputer] = React8.useState(null);
2395
+ const [isLoading, setIsLoading] = React8.useState(Boolean(threadId));
2396
+ const [error, setError] = React8.useState(null);
2397
+ const controllerRef = React8.useRef(null);
2398
+ const refresh = React8.useCallback(async () => {
2038
2399
  controllerRef.current?.abort();
2039
2400
  if (!threadId) {
2040
2401
  setComputer(null);
@@ -2059,19 +2420,19 @@ function useThreadComputer(threadId) {
2059
2420
  }
2060
2421
  }
2061
2422
  }, [client, createAbortController, threadId]);
2062
- React7.useEffect(() => {
2423
+ React8.useEffect(() => {
2063
2424
  void refresh();
2064
2425
  return () => controllerRef.current?.abort();
2065
2426
  }, [refresh]);
2066
- const list = React7.useCallback((input) => {
2427
+ const list = React8.useCallback((input) => {
2067
2428
  if (!threadId) throw new Error("A thread is required to list computer files");
2068
2429
  return client.threads.computer.list({ threadId, ...input });
2069
2430
  }, [client, threadId]);
2070
- const read = React7.useCallback((path) => {
2431
+ const read = React8.useCallback((path) => {
2071
2432
  if (!threadId) throw new Error("A thread is required to read a computer file");
2072
2433
  return client.threads.computer.read({ threadId, path });
2073
2434
  }, [client, threadId]);
2074
- const createContentAccess = React7.useCallback((path, download = false) => {
2435
+ const createContentAccess = React8.useCallback((path, download = false) => {
2075
2436
  if (!threadId) throw new Error("A thread is required to access a computer file");
2076
2437
  return client.threads.computer.createContentAccess({ threadId, path, download });
2077
2438
  }, [client, threadId]);
@@ -2079,16 +2440,16 @@ function useThreadComputer(threadId) {
2079
2440
  }
2080
2441
  function useAgentRun() {
2081
2442
  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(() => {
2443
+ const controllerRef = React8.useRef(null);
2444
+ const [isAttached, setIsAttached] = React8.useState(false);
2445
+ const [error, setError] = React8.useState(null);
2446
+ React8.useEffect(() => () => controllerRef.current?.abort(), []);
2447
+ const detach = React8.useCallback(() => {
2087
2448
  controllerRef.current?.abort();
2088
2449
  controllerRef.current = null;
2089
2450
  setIsAttached(false);
2090
2451
  }, []);
2091
- const attach = React7.useCallback(async (runId, onEvent, cursor) => {
2452
+ const attach = React8.useCallback(async (runId, onEvent, cursor) => {
2092
2453
  detach();
2093
2454
  const controller = createAbortController();
2094
2455
  controllerRef.current = controller;
@@ -2104,7 +2465,7 @@ function useAgentRun() {
2104
2465
  setIsAttached(false);
2105
2466
  }
2106
2467
  }, [client, createAbortController, detach]);
2107
- const cancel = React7.useCallback(async (runId) => {
2468
+ const cancel = React8.useCallback(async (runId) => {
2108
2469
  const result = await client.runs.cancel({ runId, idempotencyKey: createId() });
2109
2470
  detach();
2110
2471
  return result;
@@ -2113,9 +2474,9 @@ function useAgentRun() {
2113
2474
  }
2114
2475
  function useAgentAttachments() {
2115
2476
  const { client, createId } = useAgents24Runtime();
2116
- const [uploads, setUploads] = React7.useState({});
2117
- const [isUploading, setIsUploading] = React7.useState(false);
2118
- const uploadBatch = React7.useCallback(async (input) => {
2477
+ const [uploads, setUploads] = React8.useState({});
2478
+ const [isUploading, setIsUploading] = React8.useState(false);
2479
+ const uploadBatch = React8.useCallback(async (input) => {
2119
2480
  setIsUploading(true);
2120
2481
  try {
2121
2482
  const results = await client.attachments.uploadBatch({ uploads: input.uploads, threadId: input.threadId, requestedAgentId: input.requestedAgentId, idempotencyKey: createId(), signal: input.signal });
@@ -2128,7 +2489,7 @@ function useAgentAttachments() {
2128
2489
  setIsUploading(false);
2129
2490
  }
2130
2491
  }, [client, createId]);
2131
- const createContentAccess = React7.useCallback(
2492
+ const createContentAccess = React8.useCallback(
2132
2493
  (input) => client.attachments.createContentAccess(input),
2133
2494
  [client]
2134
2495
  );
@@ -2136,9 +2497,9 @@ function useAgentAttachments() {
2136
2497
  }
2137
2498
  function useAgentHitl() {
2138
2499
  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) => {
2500
+ const [isResolving, setIsResolving] = React8.useState(false);
2501
+ const [lastResult, setLastResult] = React8.useState(null);
2502
+ const resume = React8.useCallback(async (input) => {
2142
2503
  setIsResolving(true);
2143
2504
  try {
2144
2505
  const result = await client.hitl.resume({ ...input, idempotencyKey: createId() });
@@ -2152,8 +2513,8 @@ function useAgentHitl() {
2152
2513
  }
2153
2514
  function useAgentMcp() {
2154
2515
  const { client, createId } = useAgents24Runtime();
2155
- const [isAuthorizing, setIsAuthorizing] = React7.useState(false);
2156
- const startAuthorization = React7.useCallback(async (input) => {
2516
+ const [isAuthorizing, setIsAuthorizing] = React8.useState(false);
2517
+ const startAuthorization = React8.useCallback(async (input) => {
2157
2518
  setIsAuthorizing(true);
2158
2519
  try {
2159
2520
  return await client.mcp.startAuthorization({ ...input, idempotencyKey: createId() });
@@ -2161,7 +2522,7 @@ function useAgentMcp() {
2161
2522
  setIsAuthorizing(false);
2162
2523
  }
2163
2524
  }, [client, createId]);
2164
- const redeemCallback = React7.useCallback(async (input) => client.mcp.redeemCallback({ ...input, idempotencyKey: createId() }), [client, createId]);
2525
+ const redeemCallback = React8.useCallback(async (input) => client.mcp.redeemCallback({ ...input, idempotencyKey: createId() }), [client, createId]);
2165
2526
  return { isAuthorizing, redeemCallback, startAuthorization };
2166
2527
  }
2167
2528
  export {
@@ -2174,11 +2535,13 @@ export {
2174
2535
  assistantProjectionFromEvent,
2175
2536
  attachmentResultToChatAttachment,
2176
2537
  captureAgentChatThreadSelection,
2538
+ captureChatOperation,
2177
2539
  chatAttachmentIds,
2178
2540
  clearAgentChatBackgroundError,
2179
2541
  clearAgentChatOperationError,
2180
2542
  computerResourceTargets,
2181
2543
  contextCompressionPartFromEvent,
2544
+ createChatObservation,
2182
2545
  createPortableId,
2183
2546
  createRuntimeEventFrameBuffer,
2184
2547
  discardOptimisticSubmission,
@@ -2191,6 +2554,7 @@ export {
2191
2554
  optimisticSubmissionTurn,
2192
2555
  partsFromResponseBlocks,
2193
2556
  preferNewestThreadSummary,
2557
+ prependThreadTimeline,
2194
2558
  preserveMountedMessageIds,
2195
2559
  promoteThreadMessage,
2196
2560
  reconcileRefreshedThreads,
@@ -2203,6 +2567,7 @@ export {
2203
2567
  upsertStableAssistantMessage,
2204
2568
  useAgentAttachments,
2205
2569
  useAgentChat,
2570
+ useAgentChatFeedback,
2206
2571
  useAgentClient,
2207
2572
  useAgentHitl,
2208
2573
  useAgentMcp,
@@ -2210,6 +2575,7 @@ export {
2210
2575
  useAgentThread,
2211
2576
  useAgentThreads,
2212
2577
  useAgents24Runtime,
2578
+ useChatObservation,
2213
2579
  useThreadComputer,
2214
2580
  useThreadMessageQueue,
2215
2581
  withAgentChatBackgroundError,