@copilotz/chat-adapter 0.9.19 → 0.9.22

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
@@ -583,6 +583,20 @@ async function fetchThreads(userId, getRequestHeaders) {
583
583
  }
584
584
  return data;
585
585
  }
586
+ async function fetchThreadActivity(threadId, getRequestHeaders) {
587
+ const res = await fetch(apiUrl(`/v1/threads/${encodeURIComponent(threadId)}/activity`), {
588
+ headers: await withAuthHeaders(
589
+ { Accept: "application/json" },
590
+ getRequestHeaders
591
+ )
592
+ });
593
+ if (!res.ok) {
594
+ const errorText = await res.text().catch(() => res.statusText);
595
+ throw new Error(errorText || `Failed to load thread activity (${res.status})`);
596
+ }
597
+ const { data } = await res.json();
598
+ return data;
599
+ }
586
600
  async function fetchAgents(getRequestHeaders) {
587
601
  const response = await fetch(apiUrl("/v1/agents"), {
588
602
  method: "GET",
@@ -1665,6 +1679,8 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
1665
1679
  });
1666
1680
  const persistedToolUpdatesRef = useRef2([]);
1667
1681
  const liveToolUpdatesRef = useRef2([]);
1682
+ const stopRequestedRef = useRef2(false);
1683
+ const recoveryPollGenerationRef = useRef2(0);
1668
1684
  threadsRef.current = threads;
1669
1685
  threadMetadataMapRef.current = threadMetadataMap;
1670
1686
  threadExternalIdMapRef.current = threadExternalIdMap;
@@ -2070,6 +2086,8 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2070
2086
  [userId, fetchAndSetThreadsState, loadThreadMessages, getRequestHeaders]
2071
2087
  );
2072
2088
  const handleStop = useCallback2(() => {
2089
+ stopRequestedRef.current = true;
2090
+ recoveryPollGenerationRef.current += 1;
2073
2091
  abortControllerRef.current?.abort();
2074
2092
  abortControllerRef.current = null;
2075
2093
  setIsStreaming(false);
@@ -2100,6 +2118,52 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2100
2118
  )
2101
2119
  );
2102
2120
  }, []);
2121
+ const finalizeStreamingPlaceholders = useCallback2(() => {
2122
+ setIsStreaming(false);
2123
+ setMessages((prev) => {
2124
+ const hasStreaming = prev.some((msg) => msg.isStreaming);
2125
+ if (!hasStreaming) return prev;
2126
+ return prev.map((msg) => msg.isStreaming ? closeAssistantMessage(msg) : msg);
2127
+ });
2128
+ }, []);
2129
+ const startThreadActivityRecovery = useCallback2(
2130
+ (threadId) => {
2131
+ const generation = ++recoveryPollGenerationRef.current;
2132
+ setIsStreaming(true);
2133
+ void (async () => {
2134
+ let delayMs = 2e3;
2135
+ let attempts = 0;
2136
+ while (recoveryPollGenerationRef.current === generation) {
2137
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2138
+ if (recoveryPollGenerationRef.current !== generation) return;
2139
+ if (currentThreadIdRef.current !== threadId) return;
2140
+ try {
2141
+ const activity = await fetchThreadActivity(threadId, getRequestHeaders);
2142
+ if (activity.status === "running") {
2143
+ attempts += 1;
2144
+ if (attempts % 3 === 0) {
2145
+ await loadThreadMessages(threadId);
2146
+ }
2147
+ if (attempts >= 15) {
2148
+ delayMs = 5e3;
2149
+ }
2150
+ continue;
2151
+ }
2152
+ await loadThreadMessages(threadId);
2153
+ finalizeStreamingPlaceholders();
2154
+ return;
2155
+ } catch (error) {
2156
+ if (isAbortError(error)) return;
2157
+ attempts += 1;
2158
+ if (attempts >= 15) {
2159
+ delayMs = 5e3;
2160
+ }
2161
+ }
2162
+ }
2163
+ })();
2164
+ },
2165
+ [finalizeStreamingPlaceholders, getRequestHeaders, loadThreadMessages]
2166
+ );
2103
2167
  const sendCopilotzMessage = useCallback2(
2104
2168
  async (params) => {
2105
2169
  let currentAssistantId = params.assistantMessageId ?? generateId();
@@ -2242,8 +2306,22 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2242
2306
  const abortController = new AbortController();
2243
2307
  abortControllerRef.current?.abort();
2244
2308
  abortControllerRef.current = abortController;
2309
+ stopRequestedRef.current = false;
2310
+ recoveryPollGenerationRef.current += 1;
2245
2311
  setIsStreaming(true);
2246
2312
  liveToolUpdatesRef.current = [];
2313
+ let streamError = null;
2314
+ const resolveActivityThreadId = async () => {
2315
+ if (params.threadId) return params.threadId;
2316
+ const curId = currentThreadIdRef.current;
2317
+ if (curId && threadExternalIdMapRef.current[curId] !== curId) {
2318
+ return curId;
2319
+ }
2320
+ if (params.threadExternalId) {
2321
+ return await fetchAndSetThreadsState(params.userId, params.threadExternalId);
2322
+ }
2323
+ return null;
2324
+ };
2247
2325
  try {
2248
2326
  const normalizedUserMetadata = params.userMetadata ? JSON.parse(JSON.stringify(params.userMetadata)) : void 0;
2249
2327
  const contextSeed = userContextSeedRef.current;
@@ -2405,18 +2483,40 @@ function useCopilotz({ userId, userName, userAvatar, assistantName, agentOptions
2405
2483
  },
2406
2484
  signal: abortController.signal
2407
2485
  });
2408
- } finally {
2409
- setIsStreaming(false);
2410
- setMessages((prev) => {
2411
- const hasStreaming = prev.some((msg) => msg.isStreaming);
2412
- if (!hasStreaming) return prev;
2413
- return prev.map((msg) => msg.isStreaming ? closeAssistantMessage(msg) : msg);
2414
- });
2415
- abortControllerRef.current = null;
2486
+ } catch (error) {
2487
+ streamError = error;
2488
+ }
2489
+ const wasStopped = stopRequestedRef.current || abortController.signal.aborted || isAbortError(streamError);
2490
+ let recoveryStarted = false;
2491
+ if (!wasStopped) {
2492
+ try {
2493
+ const activityThreadId = await resolveActivityThreadId();
2494
+ if (activityThreadId) {
2495
+ const activity = await fetchThreadActivity(activityThreadId, getRequestHeaders);
2496
+ if (activity.status === "running") {
2497
+ recoveryStarted = true;
2498
+ startThreadActivityRecovery(activityThreadId);
2499
+ } else if (streamError || activity.status === "failed") {
2500
+ await loadThreadMessages(activityThreadId);
2501
+ }
2502
+ }
2503
+ } catch (activityError) {
2504
+ if (!streamError) {
2505
+ console.warn("Unable to verify Copilotz thread activity after stream close", activityError);
2506
+ }
2507
+ }
2508
+ }
2509
+ abortControllerRef.current = null;
2510
+ if (recoveryStarted) {
2511
+ return currentAssistantId;
2512
+ }
2513
+ finalizeStreamingPlaceholders();
2514
+ if (streamError) {
2515
+ throw streamError;
2416
2516
  }
2417
2517
  return currentAssistantId;
2418
2518
  },
2419
- [applyEventInterceptor, handleStreamMessageEvent, handleStreamAssetEvent, getRequestHeaders]
2519
+ [applyEventInterceptor, handleStreamMessageEvent, handleStreamAssetEvent, fetchAndSetThreadsState, finalizeStreamingPlaceholders, getRequestHeaders, loadThreadMessages, startThreadActivityRecovery]
2420
2520
  );
2421
2521
  const handleSendMessage = useCallback2(
2422
2522
  async (content, attachments = []) => {