@nocobase/app-plugin-agents 2.0.0-beta.0 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +9 -1
- package/dist/client/chat/coalesced-invalidation.d.ts +3 -8
- package/dist/client/chat/coalesced-invalidation.d.ts.map +1 -1
- package/dist/client/chat/coalesced-invalidation.js +16 -21
- package/dist/client/chat/coalesced-invalidation.js.map +1 -1
- package/dist/client/chat/controller.d.ts +30 -3
- package/dist/client/chat/controller.d.ts.map +1 -1
- package/dist/client/chat/controller.js +472 -76
- package/dist/client/chat/controller.js.map +1 -1
- package/dist/client/chat/provider.d.ts.map +1 -1
- package/dist/client/chat/provider.js +7 -4
- package/dist/client/chat/provider.js.map +1 -1
- package/dist/client/chat/use-chat.d.ts +4 -2
- package/dist/client/chat/use-chat.d.ts.map +1 -1
- package/dist/client/chat/use-chat.js +82 -51
- package/dist/client/chat/use-chat.js.map +1 -1
- package/dist/client/hooks/use-realtime-topic.d.ts +1 -1
- package/dist/client/hooks/use-realtime-topic.d.ts.map +1 -1
- package/dist/client/hooks/use-realtime-topic.js +8 -4
- package/dist/client/hooks/use-realtime-topic.js.map +1 -1
- package/dist/client/kit.d.ts.map +1 -1
- package/dist/client/kit.js +30 -5
- package/dist/client/kit.js.map +1 -1
- package/dist/client/locales/de-DE.d.ts.map +1 -1
- package/dist/client/locales/de-DE.js +2 -0
- package/dist/client/locales/de-DE.js.map +1 -1
- package/dist/client/locales/es-ES.d.ts.map +1 -1
- package/dist/client/locales/es-ES.js +2 -0
- package/dist/client/locales/es-ES.js.map +1 -1
- package/dist/client/locales/fr-FR.d.ts.map +1 -1
- package/dist/client/locales/fr-FR.js +2 -0
- package/dist/client/locales/fr-FR.js.map +1 -1
- package/dist/client/locales/id-ID.d.ts.map +1 -1
- package/dist/client/locales/id-ID.js +2 -0
- package/dist/client/locales/id-ID.js.map +1 -1
- package/dist/client/locales/ja-JP.d.ts.map +1 -1
- package/dist/client/locales/ja-JP.js +2 -0
- package/dist/client/locales/ja-JP.js.map +1 -1
- package/dist/client/locales/pt-BR.d.ts.map +1 -1
- package/dist/client/locales/pt-BR.js +2 -0
- package/dist/client/locales/pt-BR.js.map +1 -1
- package/dist/client/locales/runWait.en-US.d.ts +2 -0
- package/dist/client/locales/runWait.en-US.d.ts.map +1 -1
- package/dist/client/locales/runWait.en-US.js +2 -0
- package/dist/client/locales/runWait.en-US.js.map +1 -1
- package/dist/client/locales/runWait.zh-CN.d.ts.map +1 -1
- package/dist/client/locales/runWait.zh-CN.js +2 -0
- package/dist/client/locales/runWait.zh-CN.js.map +1 -1
- package/dist/client/locales/vi-VN.d.ts.map +1 -1
- package/dist/client/locales/vi-VN.js +2 -0
- package/dist/client/locales/vi-VN.js.map +1 -1
- package/dist/client/locales/zh-TW.d.ts.map +1 -1
- package/dist/client/locales/zh-TW.js +2 -0
- package/dist/client/locales/zh-TW.js.map +1 -1
- package/dist/client/pages/agents/agent-dialog.js +4 -1
- package/dist/client/pages/agents/agent-dialog.js.map +1 -1
- package/dist/client/runs/format-wait.d.ts.map +1 -1
- package/dist/client/runs/format-wait.js +2 -0
- package/dist/client/runs/format-wait.js.map +1 -1
- package/dist/package.json +1 -1
- package/dist/server/core/conversations/conversation.service.d.ts.map +1 -1
- package/dist/server/core/conversations/conversation.service.js +43 -3
- package/dist/server/core/conversations/conversation.service.js.map +1 -1
- package/dist/server/core/conversations/links.d.ts +3 -0
- package/dist/server/core/conversations/links.d.ts.map +1 -1
- package/dist/server/core/conversations/links.js +33 -0
- package/dist/server/core/conversations/links.js.map +1 -1
- package/dist/server/core/conversations/ports.d.ts +1 -1
- package/dist/server/core/conversations/ports.d.ts.map +1 -1
- package/dist/server/core/runs/claim.d.ts.map +1 -1
- package/dist/server/core/runs/claim.js +24 -7
- package/dist/server/core/runs/claim.js.map +1 -1
- package/dist/server/core/runs/run.service.d.ts +1 -1
- package/dist/server/core/runs/run.service.d.ts.map +1 -1
- package/dist/server/core/runs/run.service.js +7 -1
- package/dist/server/core/runs/run.service.js.map +1 -1
- package/dist/server/core/runs/workload.d.ts.map +1 -1
- package/dist/server/core/runs/workload.js +19 -2
- package/dist/server/core/runs/workload.js.map +1 -1
- package/dist/server/kernel/events.d.ts +2 -0
- package/dist/server/kernel/events.d.ts.map +1 -1
- package/dist/server/kernel/events.js.map +1 -1
- package/dist/server/online/agent/runner/backend.d.ts.map +1 -1
- package/dist/server/online/agent/runner/backend.js +17 -8
- package/dist/server/online/agent/runner/backend.js.map +1 -1
- package/dist/server/providers/agents.d.ts.map +1 -1
- package/dist/server/providers/agents.js +3 -0
- package/dist/server/providers/agents.js.map +1 -1
- package/dist/server/routes/schemas.d.ts +1 -1
- package/dist/server/routes/schemas.d.ts.map +1 -1
- package/dist/server/routes/schemas.js +6 -2
- package/dist/server/routes/schemas.js.map +1 -1
- package/dist/shared/conversations.d.ts +6 -0
- package/dist/shared/conversations.d.ts.map +1 -1
- package/dist/shared/conversations.js.map +1 -1
- package/dist/shared/runs.d.ts +7 -3
- package/dist/shared/runs.d.ts.map +1 -1
- package/dist/shared/runs.js +2 -0
- package/dist/shared/runs.js.map +1 -1
- package/dist/shared/ui-message.d.ts +2 -2
- package/dist/shared/ui-message.d.ts.map +1 -1
- package/dist/shared/ui-message.js +1 -1
- package/dist/shared/ui-message.js.map +1 -1
- package/package.json +11 -11
- package/skills/nocobase-app-plugin-agents/references/verification-and-troubleshooting.md +2 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { ApiClientError } from '@nocobase/app-client';
|
|
3
|
-
import { CancelledError } from '@tanstack/react-query';
|
|
3
|
+
import { CancelledError, hashKey } from '@tanstack/react-query';
|
|
4
4
|
import { isToolUIPart } from 'ai';
|
|
5
5
|
import { Chat, useChat as useSDKChat } from '@ai-sdk/react';
|
|
6
6
|
import { createContext, useContext, useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore, } from 'react';
|
|
7
|
-
import { useApiClient } from '@nocobase/app-client';
|
|
7
|
+
import { useApiClient, useRealtimeReadiness } from '@nocobase/app-client';
|
|
8
|
+
import { CONVERSATIONS_TOPIC, } from '../../shared/conversations.js';
|
|
8
9
|
import { ChatApi } from './api.js';
|
|
10
|
+
import { CHAT_INVALIDATION_MS, coalescedInvalidation, } from './coalesced-invalidation.js';
|
|
9
11
|
import { chatIsRunning } from './feedback.js';
|
|
10
12
|
import { ChatSubmissions } from './submissions.js';
|
|
11
13
|
import { agentsQueryClient } from '../query.js';
|
|
14
|
+
import { agentsKeys } from '../api/keys.js';
|
|
12
15
|
import { chatKeys } from './keys.js';
|
|
13
16
|
import { EMPTY_MESSAGES, messagesReducer, mergeUIMessages, } from './message-model.js';
|
|
14
17
|
function deferred() {
|
|
@@ -22,14 +25,82 @@ function deferred() {
|
|
|
22
25
|
/** Application coordination around one SDK Chat per conversation, shared by every surface. */
|
|
23
26
|
export class ChatController {
|
|
24
27
|
api;
|
|
28
|
+
invalidations;
|
|
29
|
+
invalidateQuery(key, exact = false) {
|
|
30
|
+
(this.invalidations ??= coalescedInvalidation(agentsQueryClient())).add(key, exact);
|
|
31
|
+
}
|
|
32
|
+
invalidateLists() {
|
|
33
|
+
for (const query of agentsQueryClient()
|
|
34
|
+
.getQueryCache()
|
|
35
|
+
.findAll({ queryKey: chatKeys.lists })) {
|
|
36
|
+
if (query.queryKey[3]?.archived !==
|
|
37
|
+
true)
|
|
38
|
+
this.invalidateQuery(query.queryKey, true);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Reuse a detail snapshot when replacing its row cannot change membership, order or a page cursor. */
|
|
42
|
+
syncLists(detail) {
|
|
43
|
+
const client = agentsQueryClient();
|
|
44
|
+
for (const query of client
|
|
45
|
+
.getQueryCache()
|
|
46
|
+
.findAll({ queryKey: chatKeys.lists })) {
|
|
47
|
+
const filter = query.queryKey[3];
|
|
48
|
+
if (filter?.archived === true)
|
|
49
|
+
continue;
|
|
50
|
+
const data = query.state.data;
|
|
51
|
+
const pageIndex = data?.pages?.findIndex((page) => page.items.some((item) => item.id === detail.id)) ?? -1;
|
|
52
|
+
const page = data?.pages?.[pageIndex];
|
|
53
|
+
const index = page?.items.findIndex((item) => item.id === detail.id) ?? -1;
|
|
54
|
+
const row = page?.items[index];
|
|
55
|
+
if (row &&
|
|
56
|
+
((row.lastSeq ?? 0) > (detail.lastSeq ?? 0) ||
|
|
57
|
+
row.updatedAt > detail.updatedAt))
|
|
58
|
+
continue;
|
|
59
|
+
const sameOrder = row &&
|
|
60
|
+
(row.lastMessageAt === detail.lastMessageAt ||
|
|
61
|
+
(pageIndex === 0 &&
|
|
62
|
+
index === 0 &&
|
|
63
|
+
detail.lastMessageAt >= row.lastMessageAt &&
|
|
64
|
+
(page.items.length > 1 || !page.nextCursor)));
|
|
65
|
+
if (!row ||
|
|
66
|
+
!sameOrder ||
|
|
67
|
+
query.state.fetchStatus !== 'idle' ||
|
|
68
|
+
row.archivedAt !== detail.archivedAt ||
|
|
69
|
+
row.showInChatList !== detail.showInChatList ||
|
|
70
|
+
filter?.q ||
|
|
71
|
+
filter?.agentId ||
|
|
72
|
+
filter?.mode ||
|
|
73
|
+
filter?.source ||
|
|
74
|
+
typeof filter === 'string') {
|
|
75
|
+
this.invalidateQuery(query.queryKey, true);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
client.setQueryData(query.queryKey, {
|
|
79
|
+
...data,
|
|
80
|
+
pages: data.pages.map((item, position) => position === pageIndex
|
|
81
|
+
? {
|
|
82
|
+
...item,
|
|
83
|
+
items: item.items.map((summary) => summary.id === detail.id ? { ...detail } : summary),
|
|
84
|
+
}
|
|
85
|
+
: item),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
25
89
|
historyEpochs = new Map();
|
|
90
|
+
messageEpochs = new Map();
|
|
26
91
|
replacements = new Map();
|
|
27
92
|
resendRequests = new Map();
|
|
93
|
+
streamFollowers = new Map();
|
|
94
|
+
detailReads = new Map();
|
|
95
|
+
refreshes = new Map();
|
|
28
96
|
reconciles = new Map();
|
|
29
97
|
submissions;
|
|
30
98
|
chats = new Map();
|
|
31
99
|
histories = new Map();
|
|
32
100
|
loads = new Map();
|
|
101
|
+
messageReads = new Map();
|
|
102
|
+
catchups = new Map();
|
|
103
|
+
reconnects = new Map();
|
|
33
104
|
refreshAgain = new Set();
|
|
34
105
|
recoveries = new Map();
|
|
35
106
|
admissions = new Map();
|
|
@@ -46,6 +117,7 @@ export class ChatController {
|
|
|
46
117
|
generation = 0;
|
|
47
118
|
detailVersions = new Map();
|
|
48
119
|
receiptVersions = new Map();
|
|
120
|
+
pendingReceipts = new Map();
|
|
49
121
|
commandVersions = new Map();
|
|
50
122
|
constructor(api) {
|
|
51
123
|
this.api = api;
|
|
@@ -80,7 +152,7 @@ export class ChatController {
|
|
|
80
152
|
recovery.error = undefined;
|
|
81
153
|
// Reads started before reconnection must not replace its replay or revive old errors.
|
|
82
154
|
this.nextDetailVersion(id);
|
|
83
|
-
this.
|
|
155
|
+
this.invalidateReplayReads(id);
|
|
84
156
|
this.publish();
|
|
85
157
|
};
|
|
86
158
|
}),
|
|
@@ -194,22 +266,33 @@ export class ChatController {
|
|
|
194
266
|
this.publish();
|
|
195
267
|
}
|
|
196
268
|
historyLifetime(id) {
|
|
197
|
-
|
|
269
|
+
return this.readLifetime(id, this.historyEpochs);
|
|
270
|
+
}
|
|
271
|
+
messageLifetime(id) {
|
|
272
|
+
return this.readLifetime(id, this.messageEpochs);
|
|
273
|
+
}
|
|
274
|
+
readLifetime(id, epochs) {
|
|
275
|
+
const active = this.lifetime(), epoch = epochs.get(id) ?? 0, command = this.commandVersions.get(id);
|
|
198
276
|
return () => active() &&
|
|
199
|
-
(
|
|
277
|
+
(epochs.get(id) ?? 0) === epoch &&
|
|
200
278
|
this.commandVersions.get(id) === command;
|
|
201
279
|
}
|
|
202
|
-
|
|
280
|
+
/** A resumed stream replaces replay snapshots, not already requested durable message revisions. */
|
|
281
|
+
invalidateReplayReads(id) {
|
|
203
282
|
this.historyEpochs.set(id, (this.historyEpochs.get(id) ?? 0) + 1);
|
|
204
283
|
this.loads.delete(id);
|
|
205
284
|
this.refreshAgain.delete(id);
|
|
285
|
+
this.historyMeta(id).loadingOlder = false;
|
|
286
|
+
}
|
|
287
|
+
invalidateHistoryReads(id, discardDeferred = false) {
|
|
288
|
+
this.invalidateReplayReads(id);
|
|
289
|
+
this.messageEpochs.set(id, (this.messageEpochs.get(id) ?? 0) + 1);
|
|
206
290
|
this.revisionQueue.delete(id);
|
|
207
291
|
if (discardDeferred)
|
|
208
292
|
this.deferredReads.delete(id);
|
|
209
293
|
for (const key of this.messageVersions.keys())
|
|
210
294
|
if (key.startsWith(`${id}:`))
|
|
211
295
|
this.messageVersions.delete(key);
|
|
212
|
-
this.historyMeta(id).loadingOlder = false;
|
|
213
296
|
}
|
|
214
297
|
historyReplaced(event) {
|
|
215
298
|
if (this.disposed || !this.replaceHistory(event))
|
|
@@ -403,42 +486,36 @@ export class ChatController {
|
|
|
403
486
|
return pending;
|
|
404
487
|
if (this.resendRequests.has(id))
|
|
405
488
|
return;
|
|
406
|
-
this.
|
|
489
|
+
this.invalidateReplayReads(id);
|
|
407
490
|
const active = this.historyLifetime(id);
|
|
408
491
|
const initial = this.chat(id).messages.filter((m) => m.metadata?.seq !== undefined);
|
|
409
492
|
const task = (async () => {
|
|
410
493
|
const found = [];
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
494
|
+
// Reconnect reads the current window once, covering insertions, revisions and removed replies together.
|
|
495
|
+
const first = await this.api.messages(id, {});
|
|
496
|
+
if (!active())
|
|
497
|
+
return;
|
|
498
|
+
found.push(...first.items);
|
|
499
|
+
let window = first;
|
|
500
|
+
const oldest = Math.min(...initial.map((m) => m.metadata.seq));
|
|
501
|
+
const seen = new Set();
|
|
502
|
+
while (window.nextCursor &&
|
|
503
|
+
window.items.length &&
|
|
504
|
+
window.items[0].metadata.seq > oldest) {
|
|
505
|
+
if (seen.has(window.nextCursor))
|
|
506
|
+
throw new Error('History pagination did not advance.');
|
|
507
|
+
seen.add(window.nextCursor);
|
|
508
|
+
window = await this.api.messages(id, { pageToken: window.nextCursor });
|
|
418
509
|
if (!active())
|
|
419
510
|
return;
|
|
420
|
-
found.push(...
|
|
511
|
+
found.push(...window.items);
|
|
421
512
|
}
|
|
422
513
|
const present = new Set(found.map((m) => m.id));
|
|
423
514
|
const missing = new Set(initial.filter((m) => !present.has(m.id)).map((m) => m.id));
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
do {
|
|
429
|
-
window = await this.api.messages(id, window?.nextCursor ? { pageToken: window.nextCursor } : {});
|
|
430
|
-
if (!active())
|
|
431
|
-
return;
|
|
432
|
-
found.push(...window.items);
|
|
433
|
-
if (!window.nextCursor ||
|
|
434
|
-
!window.items.length ||
|
|
435
|
-
window.items[0].metadata.seq <= oldest)
|
|
436
|
-
break;
|
|
437
|
-
if (seen.has(window.nextCursor))
|
|
438
|
-
throw new Error('History pagination did not advance.');
|
|
439
|
-
seen.add(window.nextCursor);
|
|
440
|
-
} while (window.nextCursor);
|
|
441
|
-
}
|
|
515
|
+
this.dispatch(id, { type: 'page', page: first });
|
|
516
|
+
this.submissions.observe(id, found);
|
|
517
|
+
this.historyMeta(id).loadedThrough = first.lastSeq;
|
|
518
|
+
this.historyMeta(id).error = undefined;
|
|
442
519
|
if (!active())
|
|
443
520
|
return;
|
|
444
521
|
const changed = found.find((m) => m.role === 'user' &&
|
|
@@ -469,6 +546,13 @@ export class ChatController {
|
|
|
469
546
|
? chat.messages.at(-1)?.id
|
|
470
547
|
: undefined;
|
|
471
548
|
chat.messages = mergeUIMessages(chat.messages.filter((m) => !missing.has(m.id) || m.id === activeId), found, activeId);
|
|
549
|
+
const deferred = this.deferredReads.get(id);
|
|
550
|
+
for (const message of found)
|
|
551
|
+
if ((message.metadata?.revision ?? 0) >=
|
|
552
|
+
(this.messageVersions.get(`${id}:${message.id}`) ?? 0))
|
|
553
|
+
deferred?.delete(message.id);
|
|
554
|
+
if (deferred?.size === 0)
|
|
555
|
+
this.deferredReads.delete(id);
|
|
472
556
|
if (window)
|
|
473
557
|
this.historyMeta(id).page = {
|
|
474
558
|
...this.historyMeta(id).page,
|
|
@@ -562,13 +646,41 @@ export class ChatController {
|
|
|
562
646
|
if (this.disposed)
|
|
563
647
|
return [];
|
|
564
648
|
const found = [];
|
|
565
|
-
const active = this.
|
|
649
|
+
const active = this.messageLifetime(id);
|
|
650
|
+
const replayActive = this.historyLifetime(id);
|
|
566
651
|
for (let offset = 0; offset < ids.length; offset += 100) {
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
652
|
+
const selection = ids.slice(offset, offset + 100);
|
|
653
|
+
const key = JSON.stringify([id, [...selection].sort()]);
|
|
654
|
+
let pendingRead = this.messageReads.get(key);
|
|
655
|
+
const shared = Boolean(pendingRead);
|
|
656
|
+
if (!pendingRead) {
|
|
657
|
+
pendingRead = this.api.messages(id, { ids: selection.join(',') });
|
|
658
|
+
this.messageReads.set(key, pendingRead);
|
|
659
|
+
const clear = () => {
|
|
660
|
+
if (this.messageReads.get(key) === pendingRead)
|
|
661
|
+
this.messageReads.delete(key);
|
|
662
|
+
};
|
|
663
|
+
void pendingRead.then(clear, clear);
|
|
664
|
+
}
|
|
665
|
+
const response = await pendingRead;
|
|
570
666
|
if (!active())
|
|
571
667
|
return [];
|
|
668
|
+
// Durable user/system revisions survive reconnection; old assistant snapshots must not overwrite replay.
|
|
669
|
+
const page = replayActive()
|
|
670
|
+
? response
|
|
671
|
+
: {
|
|
672
|
+
...response,
|
|
673
|
+
items: response.items.filter((message) => message.role !== 'assistant'),
|
|
674
|
+
};
|
|
675
|
+
if (shared &&
|
|
676
|
+
active() &&
|
|
677
|
+
page.items.some((message) => (message.metadata?.revision ?? 0) <
|
|
678
|
+
(this.messageVersions.get(`${id}:${message.id}`) ?? 0))) {
|
|
679
|
+
// A revision announced during the older read needs a causal trailing read, never a cancellation.
|
|
680
|
+
const saved = await this.reread(id, selection);
|
|
681
|
+
found.push(...saved);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
572
684
|
found.push(...page.items);
|
|
573
685
|
const pending = this.deferredReads.get(id);
|
|
574
686
|
for (const message of page.items) {
|
|
@@ -583,6 +695,42 @@ export class ChatController {
|
|
|
583
695
|
}
|
|
584
696
|
return found;
|
|
585
697
|
}
|
|
698
|
+
/** A cursor announcement shares the current read; only an actual gap needs a trailing page. */
|
|
699
|
+
messagesChanged(id, lastSeq) {
|
|
700
|
+
if (this.disposed || !this.watches.has(id))
|
|
701
|
+
return Promise.resolve();
|
|
702
|
+
const pending = this.catchups.get(id);
|
|
703
|
+
if (pending) {
|
|
704
|
+
pending.lastSeq = Math.max(pending.lastSeq, lastSeq);
|
|
705
|
+
return pending.task;
|
|
706
|
+
}
|
|
707
|
+
const active = this.historyLifetime(id);
|
|
708
|
+
const owner = { lastSeq, task: Promise.resolve() };
|
|
709
|
+
const run = async () => {
|
|
710
|
+
const reading = this.loads.get(id);
|
|
711
|
+
if (reading)
|
|
712
|
+
await reading;
|
|
713
|
+
while (active() &&
|
|
714
|
+
(!this.history(id).state.loaded ||
|
|
715
|
+
this.history(id).loadedThrough < owner.lastSeq)) {
|
|
716
|
+
const before = this.history(id).loadedThrough;
|
|
717
|
+
await this.loadHistory(id);
|
|
718
|
+
if (this.history(id).error)
|
|
719
|
+
throw this.history(id).error;
|
|
720
|
+
if (this.history(id).loadedThrough <= before)
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
owner.task = run().finally(() => {
|
|
725
|
+
if (this.catchups.get(id) === owner)
|
|
726
|
+
this.catchups.delete(id);
|
|
727
|
+
});
|
|
728
|
+
this.catchups.set(id, owner);
|
|
729
|
+
return owner.task;
|
|
730
|
+
}
|
|
731
|
+
isWatching(id) {
|
|
732
|
+
return this.watches.has(id);
|
|
733
|
+
}
|
|
586
734
|
watch(id) {
|
|
587
735
|
let watch = this.watches.get(id);
|
|
588
736
|
if (!watch) {
|
|
@@ -592,11 +740,15 @@ export class ChatController {
|
|
|
592
740
|
return;
|
|
593
741
|
const detail = agentsQueryClient().getQueryData(chatKeys.conversation(id));
|
|
594
742
|
const current = this.watches.get(id);
|
|
595
|
-
const
|
|
743
|
+
const status = this.chats.get(id)?.status;
|
|
744
|
+
const busy = chatIsRunning(detail ?? null) ||
|
|
745
|
+
status === 'submitted' ||
|
|
746
|
+
status === 'streaming';
|
|
747
|
+
const interval = busy ? 5000 : 30000;
|
|
596
748
|
if (Date.now() - current.last < interval)
|
|
597
749
|
return;
|
|
598
750
|
current.last = Date.now();
|
|
599
|
-
void this.refresh(id).catch(() => { });
|
|
751
|
+
void this.refresh(id, true, !busy, false).catch(() => { });
|
|
600
752
|
}, 5000);
|
|
601
753
|
watch = { refs: 0, timer, last: Date.now() };
|
|
602
754
|
this.watches.set(id, watch);
|
|
@@ -611,12 +763,24 @@ export class ChatController {
|
|
|
611
763
|
};
|
|
612
764
|
}
|
|
613
765
|
reconnect() {
|
|
614
|
-
|
|
615
|
-
|
|
766
|
+
this.invalidateLists();
|
|
767
|
+
for (const id of this.watches.keys()) {
|
|
768
|
+
if (this.reconnects.has(id))
|
|
769
|
+
continue;
|
|
770
|
+
const task = this.refresh(id)
|
|
616
771
|
.then(() => this.resume(id))
|
|
617
|
-
.catch(() => { })
|
|
772
|
+
.catch(() => { })
|
|
773
|
+
.finally(() => {
|
|
774
|
+
if (this.reconnects.get(id) === task)
|
|
775
|
+
this.reconnects.delete(id);
|
|
776
|
+
});
|
|
777
|
+
this.reconnects.set(id, task);
|
|
778
|
+
}
|
|
618
779
|
}
|
|
619
780
|
messageUpdated(id, messageId, revision) {
|
|
781
|
+
const saved = this.chat(id).messages.find((message) => message.id === messageId);
|
|
782
|
+
if ((saved?.metadata?.revision ?? -1) >= revision)
|
|
783
|
+
return;
|
|
620
784
|
const key = `${id}:${messageId}`;
|
|
621
785
|
if (((this.messageVersions.get(key) ?? -1) >= revision &&
|
|
622
786
|
!this.deferredReads.get(id)?.has(messageId)) ||
|
|
@@ -630,13 +794,21 @@ export class ChatController {
|
|
|
630
794
|
}
|
|
631
795
|
const queue = { pending: new Set([messageId]), task: Promise.resolve() };
|
|
632
796
|
this.revisionQueue.set(id, queue);
|
|
633
|
-
const active = this.
|
|
797
|
+
const active = this.messageLifetime(id);
|
|
634
798
|
queue.task = (async () => {
|
|
635
799
|
const attempted = new Set();
|
|
636
800
|
try {
|
|
801
|
+
// Let a concurrent cursor read or SDK finish supply the same durable revision first.
|
|
802
|
+
await new Promise((resolve) => setTimeout(resolve, CHAT_INVALIDATION_MS));
|
|
803
|
+
const reading = this.loads.get(id);
|
|
804
|
+
if (reading)
|
|
805
|
+
await reading;
|
|
637
806
|
while (active() && queue.pending.size) {
|
|
638
|
-
const ids = [...queue.pending]
|
|
807
|
+
const ids = [...queue.pending].filter((messageId) => (this.chat(id).messages.find((message) => message.id === messageId)?.metadata?.revision ?? -1) <
|
|
808
|
+
(this.messageVersions.get(`${id}:${messageId}`) ?? 0));
|
|
639
809
|
queue.pending.clear();
|
|
810
|
+
if (!ids.length)
|
|
811
|
+
continue;
|
|
640
812
|
ids.forEach((id) => attempted.add(id));
|
|
641
813
|
await this.reread(id, ids);
|
|
642
814
|
}
|
|
@@ -774,6 +946,19 @@ export class ChatController {
|
|
|
774
946
|
?.id ??
|
|
775
947
|
'');
|
|
776
948
|
}
|
|
949
|
+
followStream(id) {
|
|
950
|
+
const count = (this.streamFollowers.get(id) ?? 0) + 1;
|
|
951
|
+
this.streamFollowers.set(id, count);
|
|
952
|
+
if (count === 1)
|
|
953
|
+
this.receive(id);
|
|
954
|
+
return () => {
|
|
955
|
+
const remaining = (this.streamFollowers.get(id) ?? 1) - 1;
|
|
956
|
+
if (remaining === 0)
|
|
957
|
+
this.streamFollowers.delete(id);
|
|
958
|
+
else
|
|
959
|
+
this.streamFollowers.set(id, remaining);
|
|
960
|
+
};
|
|
961
|
+
}
|
|
777
962
|
receive(id, known) {
|
|
778
963
|
if (this.disposed)
|
|
779
964
|
return;
|
|
@@ -793,7 +978,10 @@ export class ChatController {
|
|
|
793
978
|
return;
|
|
794
979
|
if (!chatIsRunning(detail) || detail.execution?.waitingApproval) {
|
|
795
980
|
const chat = this.chats.get(id);
|
|
796
|
-
if (chat?.status !== 'submitted' &&
|
|
981
|
+
if (chat?.status !== 'submitted' &&
|
|
982
|
+
chat?.status !== 'streaming' &&
|
|
983
|
+
(!this.history(id).state.loaded ||
|
|
984
|
+
this.history(id).loadedThrough < (detail.lastSeq ?? 0)))
|
|
797
985
|
await this.loadHistory(id);
|
|
798
986
|
return;
|
|
799
987
|
}
|
|
@@ -870,7 +1058,13 @@ export class ChatController {
|
|
|
870
1058
|
recovery.phase = 'interrupted';
|
|
871
1059
|
recovery.error = chat.error;
|
|
872
1060
|
}
|
|
873
|
-
|
|
1061
|
+
if (agentsQueryClient()
|
|
1062
|
+
.getQueryCache()
|
|
1063
|
+
.find({ queryKey: chatKeys.conversation(id), exact: true })
|
|
1064
|
+
?.isActive())
|
|
1065
|
+
this.scheduleRefresh(id);
|
|
1066
|
+
else
|
|
1067
|
+
await this.refresh(id);
|
|
874
1068
|
}
|
|
875
1069
|
}
|
|
876
1070
|
catch (error) {
|
|
@@ -947,9 +1141,63 @@ export class ChatController {
|
|
|
947
1141
|
if (active())
|
|
948
1142
|
await this.refresh(id);
|
|
949
1143
|
}
|
|
950
|
-
|
|
1144
|
+
/** One detail request shared by the query observer and message reconciliation. */
|
|
1145
|
+
readDetail(id, fresh = false) {
|
|
1146
|
+
const pending = this.detailReads.get(id);
|
|
1147
|
+
if (pending)
|
|
1148
|
+
return fresh
|
|
1149
|
+
? pending.catch(() => undefined).then(() => this.readDetail(id))
|
|
1150
|
+
: pending;
|
|
1151
|
+
const task = this.api.conversation(id);
|
|
1152
|
+
this.detailReads.set(id, task);
|
|
1153
|
+
const clear = () => {
|
|
1154
|
+
if (this.detailReads.get(id) === task)
|
|
1155
|
+
this.detailReads.delete(id);
|
|
1156
|
+
};
|
|
1157
|
+
void task.then(clear, clear);
|
|
1158
|
+
return task;
|
|
1159
|
+
}
|
|
1160
|
+
refresh(id, fresh = true, idleProbe = false, reconcileHistory = true) {
|
|
1161
|
+
if (this.disposed)
|
|
1162
|
+
return Promise.resolve();
|
|
1163
|
+
const pending = this.refreshes.get(id);
|
|
1164
|
+
if (pending) {
|
|
1165
|
+
if (fresh) {
|
|
1166
|
+
pending.trailing = true;
|
|
1167
|
+
this.nextDetailVersion(id);
|
|
1168
|
+
}
|
|
1169
|
+
return pending.task;
|
|
1170
|
+
}
|
|
1171
|
+
const lifetime = this.lifetime(), commandVersion = this.commandVersions.get(id), active = () => lifetime() && this.commandVersions.get(id) === commandVersion;
|
|
1172
|
+
const owner = { trailing: false, task: Promise.resolve() };
|
|
1173
|
+
const run = async () => {
|
|
1174
|
+
do {
|
|
1175
|
+
owner.trailing = false;
|
|
1176
|
+
await this.refreshOnce(id, fresh, idleProbe, undefined, reconcileHistory);
|
|
1177
|
+
fresh = true;
|
|
1178
|
+
idleProbe = false;
|
|
1179
|
+
} while (owner.trailing && active());
|
|
1180
|
+
};
|
|
1181
|
+
owner.task = run().finally(() => {
|
|
1182
|
+
if (this.refreshes.get(id) === owner)
|
|
1183
|
+
this.refreshes.delete(id);
|
|
1184
|
+
});
|
|
1185
|
+
this.refreshes.set(id, owner);
|
|
1186
|
+
return owner.task;
|
|
1187
|
+
}
|
|
1188
|
+
async refreshOnce(id, fresh, idleProbe = false, snapshot, reconcileHistory = true) {
|
|
951
1189
|
if (this.disposed)
|
|
952
1190
|
return;
|
|
1191
|
+
const waiting = this.lifetime();
|
|
1192
|
+
if ((!fresh || !reconcileHistory) && !snapshot) {
|
|
1193
|
+
const query = agentsQueryClient()
|
|
1194
|
+
.getQueryCache()
|
|
1195
|
+
.find({ queryKey: chatKeys.conversation(id), exact: true });
|
|
1196
|
+
if (query?.state.fetchStatus === 'fetching' && query.promise)
|
|
1197
|
+
snapshot = (await query.promise);
|
|
1198
|
+
if (!waiting())
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
953
1201
|
const lifetime = this.lifetime(), commandVersion = this.commandVersions.get(id), active = () => lifetime() && this.commandVersions.get(id) === commandVersion, ordered = this.detailRequest(id);
|
|
954
1202
|
const submittedOwner = this.chat(id);
|
|
955
1203
|
const submittedInput = submittedOwner.status === 'submitted' && !this.admissions.has(id)
|
|
@@ -957,16 +1205,62 @@ export class ChatController {
|
|
|
957
1205
|
.reverse()
|
|
958
1206
|
.find((message) => message.role === 'user' && message.metadata?.seq !== undefined)
|
|
959
1207
|
: undefined;
|
|
960
|
-
const
|
|
1208
|
+
const query = agentsQueryClient()
|
|
1209
|
+
.getQueryCache()
|
|
1210
|
+
.find({ queryKey: chatKeys.conversation(id), exact: true });
|
|
1211
|
+
const cached = !fresh && query?.isActive() && query.state.fetchStatus === 'idle'
|
|
1212
|
+
? query.state.data
|
|
1213
|
+
: undefined;
|
|
1214
|
+
const previous = query?.state.data;
|
|
1215
|
+
const detail = snapshot ?? cached ?? (await this.readDetail(id, fresh));
|
|
961
1216
|
if (!active())
|
|
962
1217
|
return;
|
|
963
|
-
if (
|
|
1218
|
+
if (idleProbe &&
|
|
1219
|
+
previous &&
|
|
1220
|
+
!chatIsRunning(detail) &&
|
|
1221
|
+
detail.lastSeq !== undefined &&
|
|
1222
|
+
this.history(id).state.loaded &&
|
|
1223
|
+
!this.history(id).error &&
|
|
1224
|
+
this.history(id).loadedThrough >= detail.lastSeq &&
|
|
1225
|
+
hashKey([previous]) === hashKey([detail])) {
|
|
1226
|
+
// The durable cursor is current. Still reread revisions of the latest assistant response: a same-sequence
|
|
1227
|
+
// update may have lost its announcement, even though the conversation snapshot did not change.
|
|
1228
|
+
const latest = [...this.chat(id).messages]
|
|
1229
|
+
.reverse()
|
|
1230
|
+
.find((message) => message.role === 'assistant');
|
|
1231
|
+
const ids = [
|
|
1232
|
+
...new Set([
|
|
1233
|
+
...(this.deferredReads.get(id) ?? []),
|
|
1234
|
+
...(latest ? [latest.id] : []),
|
|
1235
|
+
]),
|
|
1236
|
+
];
|
|
1237
|
+
if (ids.length)
|
|
1238
|
+
await this.reread(id, ids);
|
|
1239
|
+
if (active() && ordered()) {
|
|
1240
|
+
this.remember(detail, false);
|
|
1241
|
+
this.publish();
|
|
1242
|
+
}
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (!active())
|
|
1246
|
+
return;
|
|
1247
|
+
const reconciled = fresh && !snapshot && reconcileHistory && ordered();
|
|
1248
|
+
if (reconciled)
|
|
964
1249
|
await this.reconcile(id, detail);
|
|
965
1250
|
if (!active())
|
|
966
1251
|
return;
|
|
967
1252
|
// A newer detail query must not discard this refresh's message catch-up.
|
|
968
1253
|
// loadHistory coalesces overlapping reads independently of detail ordering.
|
|
969
|
-
|
|
1254
|
+
const beforeThrough = this.history(id).loadedThrough;
|
|
1255
|
+
if ((!snapshot && !reconciled && fresh && detail.lastSeq === undefined) ||
|
|
1256
|
+
(reconciled && !ordered()) ||
|
|
1257
|
+
!this.history(id).state.loaded ||
|
|
1258
|
+
this.history(id).loadedThrough < (detail.lastSeq ?? 0)) {
|
|
1259
|
+
if (!reconcileHistory && detail.lastSeq !== undefined)
|
|
1260
|
+
await this.messagesChanged(id, detail.lastSeq);
|
|
1261
|
+
else
|
|
1262
|
+
await this.loadHistory(id, false, fresh);
|
|
1263
|
+
}
|
|
970
1264
|
if (!active())
|
|
971
1265
|
return;
|
|
972
1266
|
const history = this.history(id);
|
|
@@ -981,14 +1275,23 @@ export class ChatController {
|
|
|
981
1275
|
const revisable = [
|
|
982
1276
|
...new Set([
|
|
983
1277
|
...(this.deferredReads.get(id) ?? []),
|
|
984
|
-
...(detail.execution?.messageId
|
|
1278
|
+
...(detail.execution?.messageId &&
|
|
1279
|
+
((!snapshot &&
|
|
1280
|
+
!reconciled &&
|
|
1281
|
+
this.history(id).loadedThrough === beforeThrough) ||
|
|
1282
|
+
(this.chat(id).messages.find((m) => m.id === detail.execution?.messageId)?.metadata?.revision ?? -1) < (detail.execution.revision ?? 0))
|
|
985
1283
|
? [detail.execution.messageId]
|
|
986
|
-
:
|
|
1284
|
+
: !snapshot &&
|
|
1285
|
+
!reconciled &&
|
|
1286
|
+
this.history(id).loadedThrough === beforeThrough &&
|
|
1287
|
+
latest
|
|
987
1288
|
? [latest.id]
|
|
988
1289
|
: []),
|
|
989
1290
|
]),
|
|
990
1291
|
];
|
|
991
|
-
const saved = revisable.length
|
|
1292
|
+
const saved = revisable.length
|
|
1293
|
+
? await this.reread(id, revisable)
|
|
1294
|
+
: [...this.chat(id).messages];
|
|
992
1295
|
if (!active() || !ordered())
|
|
993
1296
|
return;
|
|
994
1297
|
const owner = this.chat(id), receiving = owner.messages.at(-1);
|
|
@@ -1023,7 +1326,8 @@ export class ChatController {
|
|
|
1023
1326
|
}
|
|
1024
1327
|
if (!active() || !ordered())
|
|
1025
1328
|
return;
|
|
1026
|
-
|
|
1329
|
+
if (!snapshot)
|
|
1330
|
+
this.remember(detail, false);
|
|
1027
1331
|
const chat = this.chat(id);
|
|
1028
1332
|
if (detail.execution?.status === 'idle') {
|
|
1029
1333
|
this.recoveries.delete(id);
|
|
@@ -1033,6 +1337,13 @@ export class ChatController {
|
|
|
1033
1337
|
this.publish();
|
|
1034
1338
|
}
|
|
1035
1339
|
scheduleRefresh(id) {
|
|
1340
|
+
if (agentsQueryClient()
|
|
1341
|
+
.getQueryCache()
|
|
1342
|
+
.find({ queryKey: chatKeys.conversation(id), exact: true })
|
|
1343
|
+
?.isActive()) {
|
|
1344
|
+
this.invalidateQuery(chatKeys.conversation(id), true);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1036
1347
|
if (this.completionReads.has(id))
|
|
1037
1348
|
return;
|
|
1038
1349
|
this.completionReads.add(id);
|
|
@@ -1042,14 +1353,16 @@ export class ChatController {
|
|
|
1042
1353
|
void this.refresh(id).catch(() => { });
|
|
1043
1354
|
});
|
|
1044
1355
|
}
|
|
1045
|
-
observeDetail(detail, refresh = true) {
|
|
1356
|
+
observeDetail(detail, refresh = true, synchronizeLists = true) {
|
|
1357
|
+
if (synchronizeLists)
|
|
1358
|
+
this.syncLists(detail);
|
|
1046
1359
|
if (detail.execution?.status === 'running')
|
|
1047
1360
|
this.receive(detail.id, detail);
|
|
1048
1361
|
if (!chatIsRunning(detail)) {
|
|
1049
1362
|
if (refresh &&
|
|
1050
1363
|
(this.chats.get(detail.id)?.status === 'streaming' ||
|
|
1051
1364
|
this.chats.get(detail.id)?.status === 'submitted'))
|
|
1052
|
-
this.
|
|
1365
|
+
void this.refreshOnce(detail.id, false, false, detail).catch(() => { });
|
|
1053
1366
|
this.stopRequests.delete(detail.id);
|
|
1054
1367
|
this.recoveries.delete(detail.id);
|
|
1055
1368
|
this.publish();
|
|
@@ -1088,29 +1401,103 @@ export class ChatController {
|
|
|
1088
1401
|
}
|
|
1089
1402
|
remember(detail, refresh = true) {
|
|
1090
1403
|
this.nextDetailVersion(detail.id);
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1404
|
+
// A reconnect already owns a fresh list read; its later history response must not schedule another one.
|
|
1405
|
+
this.observeDetail(detail, refresh, !this.reconnects.has(detail.id));
|
|
1406
|
+
const client = agentsQueryClient();
|
|
1407
|
+
client.setQueryData(chatKeys.conversation(detail.id), detail);
|
|
1408
|
+
}
|
|
1409
|
+
/** The HTTP receipt supplies this tab's snapshot; retain a newer read position for a trailing read. */
|
|
1410
|
+
observeReceipt(id, seenSeq) {
|
|
1411
|
+
const update = (item) => {
|
|
1412
|
+
if (item.id !== id || (item.seenSeq ?? -1) >= seenSeq)
|
|
1413
|
+
return item;
|
|
1414
|
+
return {
|
|
1415
|
+
...item,
|
|
1416
|
+
seenSeq,
|
|
1417
|
+
read: seenSeq >= (item.lastSeq ?? 0) || item.read,
|
|
1418
|
+
needsYou: item.needsYou &&
|
|
1419
|
+
item.needsYou.reason !== 'confirm' &&
|
|
1420
|
+
item.needsYou.seq <= seenSeq
|
|
1421
|
+
? null
|
|
1422
|
+
: item.needsYou,
|
|
1423
|
+
};
|
|
1424
|
+
};
|
|
1425
|
+
agentsQueryClient().setQueriesData({ queryKey: chatKeys.lists }, (old) => !old
|
|
1426
|
+
? old
|
|
1427
|
+
: Array.isArray(old)
|
|
1428
|
+
? old.map(update)
|
|
1429
|
+
: {
|
|
1430
|
+
...old,
|
|
1431
|
+
pages: old.pages.map((page) => ({
|
|
1432
|
+
...page,
|
|
1433
|
+
items: page.items.map(update),
|
|
1434
|
+
})),
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
deferReadReceipt(id, seenSeq, refresh) {
|
|
1438
|
+
const pending = this.pendingReceipts.get(id);
|
|
1439
|
+
if (!pending)
|
|
1440
|
+
return false;
|
|
1441
|
+
pending.seenSeq = Math.max(pending.seenSeq, seenSeq);
|
|
1442
|
+
pending.refresh = refresh;
|
|
1443
|
+
return true;
|
|
1094
1444
|
}
|
|
1095
1445
|
async markRead(id) {
|
|
1446
|
+
const client = agentsQueryClient();
|
|
1447
|
+
const key = chatKeys.conversation(id);
|
|
1448
|
+
const before = client.getQueryData(key);
|
|
1096
1449
|
const active = this.lifetime(), commandVersion = this.commandVersions.get(id), receiptVersion = (this.receiptVersions.get(id) ?? 0) + 1;
|
|
1097
1450
|
// Detail reads must not suppress the read transition needed by a viewed conversation.
|
|
1098
1451
|
this.receiptVersions.set(id, receiptVersion);
|
|
1452
|
+
const pending = { seenSeq: -1 };
|
|
1453
|
+
this.pendingReceipts.set(id, pending);
|
|
1099
1454
|
this.nextDetailVersion(id);
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1455
|
+
try {
|
|
1456
|
+
const detail = await this.api.markRead(id);
|
|
1457
|
+
if (active() &&
|
|
1458
|
+
this.commandVersions.get(id) === commandVersion &&
|
|
1459
|
+
this.receiptVersions.get(id) === receiptVersion) {
|
|
1460
|
+
this.observeReceipt(id, detail.seenSeq ?? 0);
|
|
1461
|
+
// Read receipts do not change execution or invalidate pending history.
|
|
1462
|
+
this.nextDetailVersion(id);
|
|
1463
|
+
client.setQueryData(key, (current) => {
|
|
1464
|
+
if (!current ||
|
|
1465
|
+
current === before ||
|
|
1466
|
+
(current.lastSeq ?? 0) < (detail.lastSeq ?? 0) ||
|
|
1467
|
+
(current.lastSeq === detail.lastSeq &&
|
|
1468
|
+
current.updatedAt < detail.updatedAt))
|
|
1469
|
+
return detail;
|
|
1470
|
+
// A later snapshot may already contain the next reply or a metadata change.
|
|
1471
|
+
const seenSeq = Math.max(current.seenSeq ?? 0, detail.seenSeq ?? 0);
|
|
1472
|
+
return {
|
|
1473
|
+
...current,
|
|
1474
|
+
seenSeq,
|
|
1475
|
+
read: seenSeq >= (current.lastSeq ?? 0) || current.read,
|
|
1476
|
+
needsYou: current.needsYou &&
|
|
1477
|
+
current.needsYou.reason !== 'confirm' &&
|
|
1478
|
+
current.needsYou.seq <= seenSeq
|
|
1479
|
+
? null
|
|
1480
|
+
: current.needsYou,
|
|
1481
|
+
};
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
return detail;
|
|
1485
|
+
}
|
|
1486
|
+
finally {
|
|
1487
|
+
if (this.pendingReceipts.get(id) === pending) {
|
|
1488
|
+
this.pendingReceipts.delete(id);
|
|
1489
|
+
const current = agentsQueryClient().getQueryData(chatKeys.conversation(id));
|
|
1490
|
+
if (active() && pending.seenSeq > (current?.seenSeq ?? -1))
|
|
1491
|
+
pending.refresh?.();
|
|
1492
|
+
}
|
|
1108
1493
|
}
|
|
1109
|
-
return detail;
|
|
1110
1494
|
}
|
|
1111
1495
|
async command(id, request, detail) {
|
|
1112
1496
|
const active = this.lifetime(), version = (this.commandVersions.get(id) ?? 0) + 1;
|
|
1113
1497
|
this.commandVersions.set(id, version);
|
|
1498
|
+
// A command replaces the snapshot generation; its reads must not wait on obsolete HTTP.
|
|
1499
|
+
this.detailReads.delete(id);
|
|
1500
|
+
this.refreshes.delete(id);
|
|
1114
1501
|
this.nextDetailVersion(id);
|
|
1115
1502
|
this.invalidateHistoryReads(id);
|
|
1116
1503
|
const recovery = this.recoveries.get(id);
|
|
@@ -1143,6 +1530,8 @@ export class ChatController {
|
|
|
1143
1530
|
this.disposed = false;
|
|
1144
1531
|
}
|
|
1145
1532
|
invalidate() {
|
|
1533
|
+
this.invalidations?.stop();
|
|
1534
|
+
this.invalidations = undefined;
|
|
1146
1535
|
if (this.disposed)
|
|
1147
1536
|
return;
|
|
1148
1537
|
this.disposed = true;
|
|
@@ -1155,8 +1544,12 @@ export class ChatController {
|
|
|
1155
1544
|
request.gate.reject(new CancelledError({ silent: true }));
|
|
1156
1545
|
this.resendRequests.clear();
|
|
1157
1546
|
this.historyEpochs.clear();
|
|
1547
|
+
this.messageEpochs.clear();
|
|
1158
1548
|
this.replacements.clear();
|
|
1159
1549
|
this.reconciles.clear();
|
|
1550
|
+
this.streamFollowers.clear();
|
|
1551
|
+
this.detailReads.clear();
|
|
1552
|
+
this.refreshes.clear();
|
|
1160
1553
|
for (const watch of this.watches.values())
|
|
1161
1554
|
clearInterval(watch.timer);
|
|
1162
1555
|
this.watches.clear();
|
|
@@ -1166,14 +1559,19 @@ export class ChatController {
|
|
|
1166
1559
|
this.completionReads.clear();
|
|
1167
1560
|
this.admissions.clear();
|
|
1168
1561
|
this.loads.clear();
|
|
1562
|
+
this.messageReads.clear();
|
|
1563
|
+
this.catchups.clear();
|
|
1564
|
+
this.reconnects.clear();
|
|
1169
1565
|
this.refreshAgain.clear();
|
|
1170
1566
|
this.commandVersions.clear();
|
|
1171
1567
|
this.receiptVersions.clear();
|
|
1568
|
+
this.pendingReceipts.clear();
|
|
1172
1569
|
this.stopRequests.clear();
|
|
1173
1570
|
this.approvalRequests.clear();
|
|
1174
1571
|
this.recoveries.clear();
|
|
1175
1572
|
this.submissions.invalidate();
|
|
1176
|
-
|
|
1573
|
+
// All plugin snapshots are viewer-scoped, including fresh agent options.
|
|
1574
|
+
agentsQueryClient().removeQueries({ queryKey: agentsKeys.all });
|
|
1177
1575
|
}
|
|
1178
1576
|
dispose() {
|
|
1179
1577
|
this.invalidate();
|
|
@@ -1192,10 +1590,7 @@ export function ChatClientProvider({ children, }) {
|
|
|
1192
1590
|
controller.activate();
|
|
1193
1591
|
const lifetimes = lifetimesRef.current;
|
|
1194
1592
|
lifetimes.set(controller, (lifetimes.get(controller) ?? 0) + 1);
|
|
1195
|
-
const reconnect = () => controller.reconnect();
|
|
1196
|
-
window.addEventListener('online', reconnect);
|
|
1197
1593
|
return () => {
|
|
1198
|
-
window.removeEventListener('online', reconnect);
|
|
1199
1594
|
controller.invalidate();
|
|
1200
1595
|
lifetimes.set(controller, (lifetimes.get(controller) ?? 1) - 1);
|
|
1201
1596
|
queueMicrotask(() => {
|
|
@@ -1216,12 +1611,13 @@ export function useChatController() {
|
|
|
1216
1611
|
}
|
|
1217
1612
|
export function useChatStream(id) {
|
|
1218
1613
|
const controller = useChatController();
|
|
1614
|
+
const ready = useRealtimeReadiness([CONVERSATIONS_TOPIC]);
|
|
1219
1615
|
useSDKChat({ chat: controller.chat(id ?? 'draft') });
|
|
1220
1616
|
useSyncExternalStore(controller.subscribe, controller.snapshot, controller.snapshot);
|
|
1221
1617
|
useEffect(() => {
|
|
1222
|
-
if (id)
|
|
1223
|
-
controller.
|
|
1224
|
-
}, [controller, id]);
|
|
1618
|
+
if (ready && id)
|
|
1619
|
+
return controller.followStream(id);
|
|
1620
|
+
}, [controller, id, ready]);
|
|
1225
1621
|
return id ? controller.state(id) : undefined;
|
|
1226
1622
|
}
|
|
1227
1623
|
export function useChatStreamState(id) {
|