@nocobase/app-plugin-agents 1.0.0-beta.29 → 2.0.0-beta.1
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 +80 -0
- package/README.md +8 -19
- package/dist/client/api/client.d.ts +2 -2
- package/dist/client/api/client.d.ts.map +1 -1
- package/dist/client/api/client.js +2 -2
- package/dist/client/api/client.js.map +1 -1
- package/dist/client/api/runners.d.ts +2 -2
- package/dist/client/api/runners.d.ts.map +1 -1
- package/dist/client/api/runners.js +5 -2
- package/dist/client/api/runners.js.map +1 -1
- package/dist/client/chat/api.d.ts +1 -1
- package/dist/client/chat/api.d.ts.map +1 -1
- package/dist/client/chat/api.js +6 -2
- package/dist/client/chat/api.js.map +1 -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 +31 -7
- package/dist/client/chat/controller.d.ts.map +1 -1
- package/dist/client/chat/controller.js +616 -120
- 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/submissions.d.ts +2 -0
- package/dist/client/chat/submissions.d.ts.map +1 -1
- package/dist/client/chat/submissions.js +1 -0
- package/dist/client/chat/submissions.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 +84 -55
- package/dist/client/chat/use-chat.js.map +1 -1
- package/dist/client/components/ag-tag-tones.d.ts +7 -0
- package/dist/client/components/ag-tag-tones.d.ts.map +1 -0
- package/dist/client/components/ag-tag-tones.js +14 -0
- package/dist/client/components/ag-tag-tones.js.map +1 -0
- package/dist/client/components/ag-tag.d.ts +2 -1
- package/dist/client/components/ag-tag.d.ts.map +1 -1
- package/dist/client/components/ag-tag.js +1 -13
- package/dist/client/components/ag-tag.js.map +1 -1
- package/dist/client/components/quota-status.d.ts +2 -1
- package/dist/client/components/quota-status.d.ts.map +1 -1
- package/dist/client/components/quota-status.js +2 -2
- package/dist/client/components/quota-status.js.map +1 -1
- package/dist/client/components/ui/pagination.d.ts +6 -0
- package/dist/client/components/ui/pagination.d.ts.map +1 -0
- package/dist/client/components/ui/pagination.js +13 -0
- package/dist/client/components/ui/pagination.js.map +1 -0
- package/dist/client/hooks/use-notify.d.ts.map +1 -1
- package/dist/client/hooks/use-notify.js +4 -0
- package/dist/client/hooks/use-notify.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 +26 -10
- package/dist/client/locales/de-DE.js.map +1 -1
- package/dist/client/locales/en-US.d.ts +11 -0
- package/dist/client/locales/en-US.d.ts.map +1 -1
- package/dist/client/locales/en-US.js +21 -10
- package/dist/client/locales/en-US.js.map +1 -1
- package/dist/client/locales/es-ES.d.ts.map +1 -1
- package/dist/client/locales/es-ES.js +26 -10
- 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 +26 -10
- 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 +26 -10
- 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 +26 -10
- 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 +26 -10
- package/dist/client/locales/pt-BR.js.map +1 -1
- package/dist/client/locales/runtimes.en-US.d.ts +5 -0
- package/dist/client/locales/runtimes.en-US.d.ts.map +1 -1
- package/dist/client/locales/runtimes.en-US.js +5 -0
- package/dist/client/locales/runtimes.en-US.js.map +1 -1
- package/dist/client/locales/runtimes.zh-CN.d.ts.map +1 -1
- package/dist/client/locales/runtimes.zh-CN.js +5 -0
- package/dist/client/locales/runtimes.zh-CN.js.map +1 -1
- package/dist/client/locales/vi-VN.d.ts.map +1 -1
- package/dist/client/locales/vi-VN.js +26 -10
- package/dist/client/locales/vi-VN.js.map +1 -1
- package/dist/client/locales/zh-CN.d.ts.map +1 -1
- package/dist/client/locales/zh-CN.js +21 -10
- package/dist/client/locales/zh-CN.js.map +1 -1
- package/dist/client/locales/zh-TW.d.ts.map +1 -1
- package/dist/client/locales/zh-TW.js +26 -10
- package/dist/client/locales/zh-TW.js.map +1 -1
- package/dist/client/model-suggestion.d.ts +2 -0
- package/dist/client/model-suggestion.d.ts.map +1 -1
- package/dist/client/model-suggestion.js +27 -2
- package/dist/client/model-suggestion.js.map +1 -1
- package/dist/client/pages/agents/agent-dialog.d.ts.map +1 -1
- package/dist/client/pages/agents/agent-dialog.js +10 -8
- package/dist/client/pages/agents/agent-dialog.js.map +1 -1
- package/dist/client/pages/agents/available-models-drawer.d.ts.map +1 -1
- package/dist/client/pages/agents/available-models-drawer.js +2 -1
- package/dist/client/pages/agents/available-models-drawer.js.map +1 -1
- package/dist/client/pages/agents/good-at-templates.d.ts +8 -0
- package/dist/client/pages/agents/good-at-templates.d.ts.map +1 -0
- package/dist/client/pages/agents/good-at-templates.js +29 -0
- package/dist/client/pages/agents/good-at-templates.js.map +1 -0
- package/dist/client/pages/runtimes/runner-cells.d.ts.map +1 -1
- package/dist/client/pages/runtimes/runner-cells.js +3 -1
- package/dist/client/pages/runtimes/runner-cells.js.map +1 -1
- package/dist/client/pages/runtimes/runner-pagination.d.ts +15 -0
- package/dist/client/pages/runtimes/runner-pagination.d.ts.map +1 -0
- package/dist/client/pages/runtimes/runner-pagination.js +9 -0
- package/dist/client/pages/runtimes/runner-pagination.js.map +1 -0
- package/dist/client/pages/runtimes/runner-runs.d.ts +3 -2
- package/dist/client/pages/runtimes/runner-runs.d.ts.map +1 -1
- package/dist/client/pages/runtimes/runner-runs.js +38 -15
- package/dist/client/pages/runtimes/runner-runs.js.map +1 -1
- package/dist/client/pages/runtimes/runner-sheet.js +1 -1
- package/dist/client/pages/runtimes/runner-sheet.js.map +1 -1
- package/dist/client/pages/runtimes/runner-workspaces.d.ts.map +1 -1
- package/dist/client/pages/runtimes/runner-workspaces.js +7 -3
- package/dist/client/pages/runtimes/runner-workspaces.js.map +1 -1
- package/dist/client/runs/refresh-run.d.ts +4 -0
- package/dist/client/runs/refresh-run.d.ts.map +1 -0
- package/dist/client/runs/refresh-run.js +8 -0
- package/dist/client/runs/refresh-run.js.map +1 -0
- package/dist/client/runs/run-hooks.d.ts.map +1 -1
- package/dist/client/runs/run-hooks.js +2 -1
- package/dist/client/runs/run-hooks.js.map +1 -1
- package/dist/database/migrations/.manifest.json +8 -0
- package/dist/database/migrations/202611010003_ag_runner_run_history_index.d.ts +4 -0
- package/dist/database/migrations/202611010003_ag_runner_run_history_index.d.ts.map +1 -0
- package/dist/database/migrations/202611010003_ag_runner_run_history_index.js +17 -0
- package/dist/database/migrations/202611010003_ag_runner_run_history_index.js.map +1 -0
- package/dist/database/migrations/202611070001_ag_conversation_project.d.ts +4 -0
- package/dist/database/migrations/202611070001_ag_conversation_project.d.ts.map +1 -0
- package/dist/database/migrations/202611070001_ag_conversation_project.js +17 -0
- package/dist/database/migrations/202611070001_ag_conversation_project.js.map +1 -0
- package/dist/package.json +4 -6
- package/dist/server/composition.d.ts +0 -17
- package/dist/server/composition.d.ts.map +1 -1
- package/dist/server/composition.js +7 -20
- package/dist/server/composition.js.map +1 -1
- package/dist/server/core/conversations/conversation.service.d.ts.map +1 -1
- package/dist/server/core/conversations/conversation.service.js +174 -22
- package/dist/server/core/conversations/conversation.service.js.map +1 -1
- package/dist/server/core/conversations/conversation.store.d.ts +1 -0
- package/dist/server/core/conversations/conversation.store.d.ts.map +1 -1
- package/dist/server/core/conversations/conversation.store.js.map +1 -1
- package/dist/server/core/conversations/index.d.ts +1 -1
- package/dist/server/core/conversations/index.d.ts.map +1 -1
- package/dist/server/core/conversations/index.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 +14 -0
- package/dist/server/core/conversations/ports.d.ts.map +1 -1
- package/dist/server/core/runs/claim.d.ts +7 -1
- package/dist/server/core/runs/claim.d.ts.map +1 -1
- package/dist/server/core/runs/claim.js +69 -17
- package/dist/server/core/runs/claim.js.map +1 -1
- package/dist/server/core/runs/ports.d.ts +2 -0
- package/dist/server/core/runs/ports.d.ts.map +1 -1
- package/dist/server/core/runs/ports.js.map +1 -1
- package/dist/server/core/runs/run.service.d.ts +6 -1
- package/dist/server/core/runs/run.service.d.ts.map +1 -1
- package/dist/server/core/runs/run.service.js +29 -1
- package/dist/server/core/runs/run.service.js.map +1 -1
- package/dist/server/core/runs/run.store.d.ts +1 -1
- package/dist/server/core/runs/run.store.d.ts.map +1 -1
- package/dist/server/core/runs/run.store.js +1 -1
- package/dist/server/core/runs/run.store.js.map +1 -1
- package/dist/server/core/runs/runner-view.d.ts +8 -4
- package/dist/server/core/runs/runner-view.d.ts.map +1 -1
- package/dist/server/core/runs/runner-view.js +89 -32
- package/dist/server/core/runs/runner-view.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/locales/de-DE.d.ts.map +1 -1
- package/dist/server/locales/de-DE.js +5 -0
- package/dist/server/locales/de-DE.js.map +1 -1
- package/dist/server/locales/en-US.d.ts +7 -1
- package/dist/server/locales/en-US.d.ts.map +1 -1
- package/dist/server/locales/en-US.js +5 -0
- package/dist/server/locales/en-US.js.map +1 -1
- package/dist/server/locales/es-ES.d.ts.map +1 -1
- package/dist/server/locales/es-ES.js +5 -0
- package/dist/server/locales/es-ES.js.map +1 -1
- package/dist/server/locales/fr-FR.d.ts.map +1 -1
- package/dist/server/locales/fr-FR.js +5 -0
- package/dist/server/locales/fr-FR.js.map +1 -1
- package/dist/server/locales/id-ID.d.ts.map +1 -1
- package/dist/server/locales/id-ID.js +5 -0
- package/dist/server/locales/id-ID.js.map +1 -1
- package/dist/server/locales/ja-JP.d.ts.map +1 -1
- package/dist/server/locales/ja-JP.js +5 -0
- package/dist/server/locales/ja-JP.js.map +1 -1
- package/dist/server/locales/pt-BR.d.ts.map +1 -1
- package/dist/server/locales/pt-BR.js +5 -0
- package/dist/server/locales/pt-BR.js.map +1 -1
- package/dist/server/locales/vi-VN.d.ts.map +1 -1
- package/dist/server/locales/vi-VN.js +5 -0
- package/dist/server/locales/vi-VN.js.map +1 -1
- package/dist/server/locales/zh-CN.d.ts +2 -2
- package/dist/server/locales/zh-CN.d.ts.map +1 -1
- package/dist/server/locales/zh-CN.js +5 -0
- package/dist/server/locales/zh-CN.js.map +1 -1
- package/dist/server/locales/zh-TW.d.ts.map +1 -1
- package/dist/server/locales/zh-TW.js +5 -0
- package/dist/server/locales/zh-TW.js.map +1 -1
- package/dist/server/online/agent/native/backend.d.ts.map +1 -1
- package/dist/server/online/agent/native/backend.js +2 -0
- package/dist/server/online/agent/native/backend.js.map +1 -1
- package/dist/server/online/agent/native/session.d.ts.map +1 -1
- package/dist/server/online/agent/native/session.js +3 -1
- package/dist/server/online/agent/native/session.js.map +1 -1
- package/dist/server/online/agent/runner/backend.d.ts.map +1 -1
- package/dist/server/online/agent/runner/backend.js +21 -9
- package/dist/server/online/agent/runner/backend.js.map +1 -1
- package/dist/server/online/agent/runner/projection.d.ts +1 -1
- package/dist/server/online/agent/runner/projection.d.ts.map +1 -1
- package/dist/server/online/agent/runner/projection.js +33 -3
- package/dist/server/online/agent/runner/projection.js.map +1 -1
- package/dist/server/online/agent/service.d.ts.map +1 -1
- package/dist/server/online/agent/service.js +3 -0
- package/dist/server/online/agent/service.js.map +1 -1
- package/dist/server/online/agent/types.d.ts +3 -0
- package/dist/server/online/agent/types.d.ts.map +1 -1
- package/dist/server/providers/agents.d.ts +5 -9
- package/dist/server/providers/agents.d.ts.map +1 -1
- package/dist/server/providers/agents.js +12 -13
- package/dist/server/providers/agents.js.map +1 -1
- package/dist/server/routes/chat.d.ts.map +1 -1
- package/dist/server/routes/chat.js +37 -0
- package/dist/server/routes/chat.js.map +1 -1
- package/dist/server/routes/runners/admin.d.ts.map +1 -1
- package/dist/server/routes/runners/admin.js +16 -7
- package/dist/server/routes/runners/admin.js.map +1 -1
- package/dist/server/routes/runners/runner.d.ts.map +1 -1
- package/dist/server/routes/runners/runner.js +6 -4
- package/dist/server/routes/runners/runner.js.map +1 -1
- package/dist/server/routes/schemas.d.ts +8 -2
- package/dist/server/routes/schemas.d.ts.map +1 -1
- package/dist/server/routes/schemas.js +46 -3
- package/dist/server/routes/schemas.js.map +1 -1
- package/dist/server/tokens.d.ts +1 -2
- package/dist/server/tokens.d.ts.map +1 -1
- package/dist/server/tokens.js.map +1 -1
- package/dist/shared/conversations.d.ts +20 -0
- package/dist/shared/conversations.d.ts.map +1 -1
- package/dist/shared/conversations.js.map +1 -1
- package/dist/shared/runners.d.ts +18 -2
- package/dist/shared/runners.d.ts.map +1 -1
- package/dist/shared/runners.js +20 -0
- package/dist/shared/runners.js.map +1 -1
- package/dist/shared/ui-message.d.ts +4 -2
- package/dist/shared/ui-message.d.ts.map +1 -1
- package/dist/shared/ui-message.js +2 -1
- package/dist/shared/ui-message.js.map +1 -1
- package/docs/ui-message-clients.md +5 -1
- package/docs/ui-messages.md +3 -1
- package/docs/vectors-migration.md +91 -0
- package/package.json +14 -16
- package/skills/nocobase-app-plugin-agents/SKILL.md +1 -1
- package/skills/nocobase-app-plugin-agents/references/extension-points.md +0 -1
- package/skills/nocobase-app-plugin-agents/references/model-services.md +3 -3
- package/skills/nocobase-app-plugin-agents/references/online-agent.md +6 -0
- package/skills/nocobase-app-plugin-agents/references/setup-and-config.md +0 -4
- package/skills/nocobase-app-plugin-agents/references/verification-and-troubleshooting.md +0 -2
- package/dist/server/vectors/index.d.ts +0 -7
- package/dist/server/vectors/index.d.ts.map +0 -1
- package/dist/server/vectors/index.js +0 -6
- package/dist/server/vectors/index.js.map +0 -1
- package/dist/server/vectors/naming.d.ts +0 -6
- package/dist/server/vectors/naming.d.ts.map +0 -1
- package/dist/server/vectors/naming.js +0 -15
- package/dist/server/vectors/naming.js.map +0 -1
- package/dist/server/vectors/pgvector.d.ts +0 -26
- package/dist/server/vectors/pgvector.d.ts.map +0 -1
- package/dist/server/vectors/pgvector.js +0 -346
- package/dist/server/vectors/pgvector.js.map +0 -1
- package/dist/server/vectors/sqlite-vec.d.ts +0 -14
- package/dist/server/vectors/sqlite-vec.d.ts.map +0 -1
- package/dist/server/vectors/sqlite-vec.js +0 -286
- package/dist/server/vectors/sqlite-vec.js.map +0 -1
- package/dist/server/vectors/types.d.ts +0 -256
- package/dist/server/vectors/types.d.ts.map +0 -1
- package/dist/server/vectors/types.js +0 -2
- package/dist/server/vectors/types.js.map +0 -1
- package/dist/server/vectors/vectors.d.ts +0 -34
- package/dist/server/vectors/vectors.d.ts.map +0 -1
- package/dist/server/vectors/vectors.js +0 -866
- package/dist/server/vectors/vectors.js.map +0 -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,18 +25,83 @@ 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();
|
|
26
90
|
replacements = new Map();
|
|
27
91
|
resendRequests = new Map();
|
|
92
|
+
streamFollowers = new Map();
|
|
93
|
+
detailReads = new Map();
|
|
94
|
+
refreshes = new Map();
|
|
28
95
|
reconciles = new Map();
|
|
29
96
|
submissions;
|
|
30
97
|
chats = new Map();
|
|
31
98
|
histories = new Map();
|
|
32
99
|
loads = new Map();
|
|
100
|
+
messageReads = new Map();
|
|
101
|
+
catchups = new Map();
|
|
102
|
+
reconnects = new Map();
|
|
33
103
|
refreshAgain = new Set();
|
|
34
|
-
|
|
35
|
-
resuming = new Map();
|
|
36
|
-
recoveryErrors = new Map();
|
|
104
|
+
recoveries = new Map();
|
|
37
105
|
admissions = new Map();
|
|
38
106
|
watches = new Map();
|
|
39
107
|
messageVersions = new Map();
|
|
@@ -47,6 +115,8 @@ export class ChatController {
|
|
|
47
115
|
disposed = false;
|
|
48
116
|
generation = 0;
|
|
49
117
|
detailVersions = new Map();
|
|
118
|
+
receiptVersions = new Map();
|
|
119
|
+
pendingReceipts = new Map();
|
|
50
120
|
commandVersions = new Map();
|
|
51
121
|
constructor(api) {
|
|
52
122
|
this.api = api;
|
|
@@ -69,7 +139,22 @@ export class ChatController {
|
|
|
69
139
|
const admission = this.admissions.get(id);
|
|
70
140
|
if (!this.disposed && admission?.clientId === clientId)
|
|
71
141
|
admission.gate.resolve();
|
|
72
|
-
}, (options) => this.resendAccepted(id, options))
|
|
142
|
+
}, (options) => this.resendAccepted(id, options), () => {
|
|
143
|
+
const active = this.lifetime(), recovery = this.recoveries.get(id);
|
|
144
|
+
return (available) => {
|
|
145
|
+
if (!active() ||
|
|
146
|
+
this.chats.get(id) !== chat ||
|
|
147
|
+
!recovery ||
|
|
148
|
+
this.recoveries.get(id) !== recovery)
|
|
149
|
+
return;
|
|
150
|
+
recovery.phase = available ? 'live' : 'history';
|
|
151
|
+
recovery.error = undefined;
|
|
152
|
+
// Reads started before reconnection must not replace its replay or revive old errors.
|
|
153
|
+
this.nextDetailVersion(id);
|
|
154
|
+
this.invalidateHistoryReads(id);
|
|
155
|
+
this.publish();
|
|
156
|
+
};
|
|
157
|
+
}),
|
|
73
158
|
onFinish: () => {
|
|
74
159
|
if (!this.disposed && this.chats.get(id) === chat)
|
|
75
160
|
this.scheduleRefresh(id);
|
|
@@ -180,15 +265,18 @@ export class ChatController {
|
|
|
180
265
|
this.publish();
|
|
181
266
|
}
|
|
182
267
|
historyLifetime(id) {
|
|
183
|
-
const active = this.lifetime(), epoch = this.historyEpochs.get(id) ?? 0;
|
|
184
|
-
return () => active() &&
|
|
268
|
+
const active = this.lifetime(), epoch = this.historyEpochs.get(id) ?? 0, command = this.commandVersions.get(id);
|
|
269
|
+
return () => active() &&
|
|
270
|
+
(this.historyEpochs.get(id) ?? 0) === epoch &&
|
|
271
|
+
this.commandVersions.get(id) === command;
|
|
185
272
|
}
|
|
186
|
-
invalidateHistoryReads(id) {
|
|
273
|
+
invalidateHistoryReads(id, discardDeferred = false) {
|
|
187
274
|
this.historyEpochs.set(id, (this.historyEpochs.get(id) ?? 0) + 1);
|
|
188
275
|
this.loads.delete(id);
|
|
189
276
|
this.refreshAgain.delete(id);
|
|
190
277
|
this.revisionQueue.delete(id);
|
|
191
|
-
|
|
278
|
+
if (discardDeferred)
|
|
279
|
+
this.deferredReads.delete(id);
|
|
192
280
|
for (const key of this.messageVersions.keys())
|
|
193
281
|
if (key.startsWith(`${id}:`))
|
|
194
282
|
this.messageVersions.delete(key);
|
|
@@ -221,8 +309,8 @@ export class ChatController {
|
|
|
221
309
|
if (previous?.executionId === event.executionId ||
|
|
222
310
|
(previous && previous.lastSeq > event.lastSeq))
|
|
223
311
|
return false;
|
|
224
|
-
this.invalidateHistoryReads(id);
|
|
225
|
-
this.
|
|
312
|
+
this.invalidateHistoryReads(id, true);
|
|
313
|
+
this.recoveries.delete(id);
|
|
226
314
|
this.replacements.set(id, event);
|
|
227
315
|
this.dispatch(id, { type: 'replaceHistory', ...event });
|
|
228
316
|
this.historyMeta(id).loadedThrough = Math.max(this.historyMeta(id).loadedThrough, event.lastSeq);
|
|
@@ -391,37 +479,31 @@ export class ChatController {
|
|
|
391
479
|
const initial = this.chat(id).messages.filter((m) => m.metadata?.seq !== undefined);
|
|
392
480
|
const task = (async () => {
|
|
393
481
|
const found = [];
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
482
|
+
// Reconnect reads the current window once, covering insertions, revisions and removed replies together.
|
|
483
|
+
const first = await this.api.messages(id, {});
|
|
484
|
+
if (!active())
|
|
485
|
+
return;
|
|
486
|
+
found.push(...first.items);
|
|
487
|
+
let window = first;
|
|
488
|
+
const oldest = Math.min(...initial.map((m) => m.metadata.seq));
|
|
489
|
+
const seen = new Set();
|
|
490
|
+
while (window.nextCursor &&
|
|
491
|
+
window.items.length &&
|
|
492
|
+
window.items[0].metadata.seq > oldest) {
|
|
493
|
+
if (seen.has(window.nextCursor))
|
|
494
|
+
throw new Error('History pagination did not advance.');
|
|
495
|
+
seen.add(window.nextCursor);
|
|
496
|
+
window = await this.api.messages(id, { pageToken: window.nextCursor });
|
|
401
497
|
if (!active())
|
|
402
498
|
return;
|
|
403
|
-
found.push(...
|
|
499
|
+
found.push(...window.items);
|
|
404
500
|
}
|
|
405
501
|
const present = new Set(found.map((m) => m.id));
|
|
406
502
|
const missing = new Set(initial.filter((m) => !present.has(m.id)).map((m) => m.id));
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
do {
|
|
412
|
-
window = await this.api.messages(id, window?.nextCursor ? { pageToken: window.nextCursor } : {});
|
|
413
|
-
if (!active())
|
|
414
|
-
return;
|
|
415
|
-
found.push(...window.items);
|
|
416
|
-
if (!window.nextCursor ||
|
|
417
|
-
!window.items.length ||
|
|
418
|
-
window.items[0].metadata.seq <= oldest)
|
|
419
|
-
break;
|
|
420
|
-
if (seen.has(window.nextCursor))
|
|
421
|
-
throw new Error('History pagination did not advance.');
|
|
422
|
-
seen.add(window.nextCursor);
|
|
423
|
-
} while (window.nextCursor);
|
|
424
|
-
}
|
|
503
|
+
this.dispatch(id, { type: 'page', page: first });
|
|
504
|
+
this.submissions.observe(id, found);
|
|
505
|
+
this.historyMeta(id).loadedThrough = first.lastSeq;
|
|
506
|
+
this.historyMeta(id).error = undefined;
|
|
425
507
|
if (!active())
|
|
426
508
|
return;
|
|
427
509
|
const changed = found.find((m) => m.role === 'user' &&
|
|
@@ -452,6 +534,13 @@ export class ChatController {
|
|
|
452
534
|
? chat.messages.at(-1)?.id
|
|
453
535
|
: undefined;
|
|
454
536
|
chat.messages = mergeUIMessages(chat.messages.filter((m) => !missing.has(m.id) || m.id === activeId), found, activeId);
|
|
537
|
+
const deferred = this.deferredReads.get(id);
|
|
538
|
+
for (const message of found)
|
|
539
|
+
if ((message.metadata?.revision ?? 0) >=
|
|
540
|
+
(this.messageVersions.get(`${id}:${message.id}`) ?? 0))
|
|
541
|
+
deferred?.delete(message.id);
|
|
542
|
+
if (deferred?.size === 0)
|
|
543
|
+
this.deferredReads.delete(id);
|
|
455
544
|
if (window)
|
|
456
545
|
this.historyMeta(id).page = {
|
|
457
546
|
...this.historyMeta(id).page,
|
|
@@ -547,9 +636,29 @@ export class ChatController {
|
|
|
547
636
|
const found = [];
|
|
548
637
|
const active = this.historyLifetime(id);
|
|
549
638
|
for (let offset = 0; offset < ids.length; offset += 100) {
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
639
|
+
const selection = ids.slice(offset, offset + 100);
|
|
640
|
+
const key = JSON.stringify([id, [...selection].sort()]);
|
|
641
|
+
let pendingRead = this.messageReads.get(key);
|
|
642
|
+
const shared = Boolean(pendingRead);
|
|
643
|
+
if (!pendingRead) {
|
|
644
|
+
pendingRead = this.api.messages(id, { ids: selection.join(',') });
|
|
645
|
+
this.messageReads.set(key, pendingRead);
|
|
646
|
+
const clear = () => {
|
|
647
|
+
if (this.messageReads.get(key) === pendingRead)
|
|
648
|
+
this.messageReads.delete(key);
|
|
649
|
+
};
|
|
650
|
+
void pendingRead.then(clear, clear);
|
|
651
|
+
}
|
|
652
|
+
const page = await pendingRead;
|
|
653
|
+
if (shared &&
|
|
654
|
+
active() &&
|
|
655
|
+
page.items.some((message) => (message.metadata?.revision ?? 0) <
|
|
656
|
+
(this.messageVersions.get(`${id}:${message.id}`) ?? 0))) {
|
|
657
|
+
// A revision announced during the older read needs a causal trailing read, never a cancellation.
|
|
658
|
+
const saved = await this.reread(id, selection);
|
|
659
|
+
found.push(...saved);
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
553
662
|
if (!active())
|
|
554
663
|
return [];
|
|
555
664
|
found.push(...page.items);
|
|
@@ -566,6 +675,42 @@ export class ChatController {
|
|
|
566
675
|
}
|
|
567
676
|
return found;
|
|
568
677
|
}
|
|
678
|
+
/** A cursor announcement shares the current read; only an actual gap needs a trailing page. */
|
|
679
|
+
messagesChanged(id, lastSeq) {
|
|
680
|
+
if (this.disposed || !this.watches.has(id))
|
|
681
|
+
return Promise.resolve();
|
|
682
|
+
const pending = this.catchups.get(id);
|
|
683
|
+
if (pending) {
|
|
684
|
+
pending.lastSeq = Math.max(pending.lastSeq, lastSeq);
|
|
685
|
+
return pending.task;
|
|
686
|
+
}
|
|
687
|
+
const active = this.historyLifetime(id);
|
|
688
|
+
const owner = { lastSeq, task: Promise.resolve() };
|
|
689
|
+
const run = async () => {
|
|
690
|
+
const reading = this.loads.get(id);
|
|
691
|
+
if (reading)
|
|
692
|
+
await reading;
|
|
693
|
+
while (active() &&
|
|
694
|
+
(!this.history(id).state.loaded ||
|
|
695
|
+
this.history(id).loadedThrough < owner.lastSeq)) {
|
|
696
|
+
const before = this.history(id).loadedThrough;
|
|
697
|
+
await this.loadHistory(id);
|
|
698
|
+
if (this.history(id).error)
|
|
699
|
+
throw this.history(id).error;
|
|
700
|
+
if (this.history(id).loadedThrough <= before)
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
owner.task = run().finally(() => {
|
|
705
|
+
if (this.catchups.get(id) === owner)
|
|
706
|
+
this.catchups.delete(id);
|
|
707
|
+
});
|
|
708
|
+
this.catchups.set(id, owner);
|
|
709
|
+
return owner.task;
|
|
710
|
+
}
|
|
711
|
+
isWatching(id) {
|
|
712
|
+
return this.watches.has(id);
|
|
713
|
+
}
|
|
569
714
|
watch(id) {
|
|
570
715
|
let watch = this.watches.get(id);
|
|
571
716
|
if (!watch) {
|
|
@@ -575,11 +720,15 @@ export class ChatController {
|
|
|
575
720
|
return;
|
|
576
721
|
const detail = agentsQueryClient().getQueryData(chatKeys.conversation(id));
|
|
577
722
|
const current = this.watches.get(id);
|
|
578
|
-
const
|
|
723
|
+
const status = this.chats.get(id)?.status;
|
|
724
|
+
const busy = chatIsRunning(detail ?? null) ||
|
|
725
|
+
status === 'submitted' ||
|
|
726
|
+
status === 'streaming';
|
|
727
|
+
const interval = busy ? 5000 : 30000;
|
|
579
728
|
if (Date.now() - current.last < interval)
|
|
580
729
|
return;
|
|
581
730
|
current.last = Date.now();
|
|
582
|
-
void this.refresh(id).catch(() => { });
|
|
731
|
+
void this.refresh(id, true, !busy, false).catch(() => { });
|
|
583
732
|
}, 5000);
|
|
584
733
|
watch = { refs: 0, timer, last: Date.now() };
|
|
585
734
|
this.watches.set(id, watch);
|
|
@@ -594,12 +743,24 @@ export class ChatController {
|
|
|
594
743
|
};
|
|
595
744
|
}
|
|
596
745
|
reconnect() {
|
|
597
|
-
|
|
598
|
-
|
|
746
|
+
this.invalidateLists();
|
|
747
|
+
for (const id of this.watches.keys()) {
|
|
748
|
+
if (this.reconnects.has(id))
|
|
749
|
+
continue;
|
|
750
|
+
const task = this.refresh(id)
|
|
599
751
|
.then(() => this.resume(id))
|
|
600
|
-
.catch(() => { })
|
|
752
|
+
.catch(() => { })
|
|
753
|
+
.finally(() => {
|
|
754
|
+
if (this.reconnects.get(id) === task)
|
|
755
|
+
this.reconnects.delete(id);
|
|
756
|
+
});
|
|
757
|
+
this.reconnects.set(id, task);
|
|
758
|
+
}
|
|
601
759
|
}
|
|
602
760
|
messageUpdated(id, messageId, revision) {
|
|
761
|
+
const saved = this.chat(id).messages.find((message) => message.id === messageId);
|
|
762
|
+
if ((saved?.metadata?.revision ?? -1) >= revision)
|
|
763
|
+
return;
|
|
603
764
|
const key = `${id}:${messageId}`;
|
|
604
765
|
if (((this.messageVersions.get(key) ?? -1) >= revision &&
|
|
605
766
|
!this.deferredReads.get(id)?.has(messageId)) ||
|
|
@@ -617,9 +778,17 @@ export class ChatController {
|
|
|
617
778
|
queue.task = (async () => {
|
|
618
779
|
const attempted = new Set();
|
|
619
780
|
try {
|
|
781
|
+
// Let a concurrent cursor read or SDK finish supply the same durable revision first.
|
|
782
|
+
await new Promise((resolve) => setTimeout(resolve, CHAT_INVALIDATION_MS));
|
|
783
|
+
const reading = this.loads.get(id);
|
|
784
|
+
if (reading)
|
|
785
|
+
await reading;
|
|
620
786
|
while (active() && queue.pending.size) {
|
|
621
|
-
const ids = [...queue.pending]
|
|
787
|
+
const ids = [...queue.pending].filter((messageId) => (this.chat(id).messages.find((message) => message.id === messageId)?.metadata?.revision ?? -1) <
|
|
788
|
+
(this.messageVersions.get(`${id}:${messageId}`) ?? 0));
|
|
622
789
|
queue.pending.clear();
|
|
790
|
+
if (!ids.length)
|
|
791
|
+
continue;
|
|
623
792
|
ids.forEach((id) => attempted.add(id));
|
|
624
793
|
await this.reread(id, ids);
|
|
625
794
|
}
|
|
@@ -664,14 +833,16 @@ export class ChatController {
|
|
|
664
833
|
async sendInput(id, input) {
|
|
665
834
|
if (this.disposed)
|
|
666
835
|
throw new CancelledError({ silent: true });
|
|
667
|
-
|
|
836
|
+
let chat = this.chat(id);
|
|
668
837
|
if (chat.status === 'submitted' || chat.status === 'streaming')
|
|
669
838
|
throw new Error('This conversation is already receiving a response.');
|
|
670
839
|
if (this.resendRequests.has(id))
|
|
671
840
|
throw new Error('This conversation is busy.');
|
|
672
841
|
const admitted = deferred();
|
|
673
842
|
this.admissions.set(id, { clientId: input.clientId, gate: admitted });
|
|
674
|
-
this.
|
|
843
|
+
this.recoveries.delete(id);
|
|
844
|
+
// A new turn owns its callbacks, including when the previous transport finishes late.
|
|
845
|
+
chat = this.renewChat(id, chat.messages);
|
|
675
846
|
const active = this.lifetime();
|
|
676
847
|
const sending = chat.sendMessage({
|
|
677
848
|
id: input.clientId,
|
|
@@ -697,6 +868,7 @@ export class ChatController {
|
|
|
697
868
|
}, {
|
|
698
869
|
body: {
|
|
699
870
|
...(input.agentId ? { agentId: input.agentId } : {}),
|
|
871
|
+
...(input.effort !== undefined ? { effort: input.effort } : {}),
|
|
700
872
|
...(input.context ? { context: input.context } : {}),
|
|
701
873
|
...(input.attachmentIds?.length
|
|
702
874
|
? { attachmentIds: input.attachmentIds }
|
|
@@ -727,42 +899,98 @@ export class ChatController {
|
|
|
727
899
|
}
|
|
728
900
|
state(id) {
|
|
729
901
|
const chat = this.chats.get(id);
|
|
902
|
+
const recovery = this.recoveries.get(id);
|
|
903
|
+
const recovering = recovery?.phase === 'recovering';
|
|
904
|
+
const error = recovering ? undefined : (chat?.error ?? recovery?.error);
|
|
730
905
|
return chat
|
|
731
906
|
? {
|
|
732
907
|
status: chat.status,
|
|
733
|
-
error
|
|
908
|
+
error,
|
|
909
|
+
recovery: recovering
|
|
910
|
+
? 'recovering'
|
|
911
|
+
: error
|
|
912
|
+
? 'interrupted'
|
|
913
|
+
: recovery?.phase === 'history'
|
|
914
|
+
? 'history'
|
|
915
|
+
: undefined,
|
|
734
916
|
}
|
|
735
917
|
: undefined;
|
|
736
918
|
}
|
|
919
|
+
recoveryKey(id, detail) {
|
|
920
|
+
return (detail.run?.id ??
|
|
921
|
+
detail.execution?.messageId ??
|
|
922
|
+
(detail.execution?.status === 'running'
|
|
923
|
+
? this.recoveries.get(id)?.key
|
|
924
|
+
: undefined) ??
|
|
925
|
+
[...this.chat(id).messages].reverse().find((m) => m.role === 'user')
|
|
926
|
+
?.id ??
|
|
927
|
+
'');
|
|
928
|
+
}
|
|
929
|
+
followStream(id) {
|
|
930
|
+
const count = (this.streamFollowers.get(id) ?? 0) + 1;
|
|
931
|
+
this.streamFollowers.set(id, count);
|
|
932
|
+
if (count === 1)
|
|
933
|
+
this.receive(id);
|
|
934
|
+
return () => {
|
|
935
|
+
const remaining = (this.streamFollowers.get(id) ?? 1) - 1;
|
|
936
|
+
if (remaining === 0)
|
|
937
|
+
this.streamFollowers.delete(id);
|
|
938
|
+
else
|
|
939
|
+
this.streamFollowers.set(id, remaining);
|
|
940
|
+
};
|
|
941
|
+
}
|
|
737
942
|
receive(id, known) {
|
|
738
|
-
if (this.disposed
|
|
943
|
+
if (this.disposed)
|
|
739
944
|
return;
|
|
740
945
|
const detail = known ??
|
|
741
946
|
agentsQueryClient().getQueryData(chatKeys.conversation(id));
|
|
742
|
-
if (!detail
|
|
947
|
+
if (!detail ||
|
|
948
|
+
this.recoveries.get(id)?.key === this.recoveryKey(id, detail))
|
|
743
949
|
return;
|
|
744
|
-
this.
|
|
745
|
-
if (detail?.execution?.status === 'idle' ||
|
|
746
|
-
detail?.execution?.waitingApproval)
|
|
747
|
-
return;
|
|
748
|
-
void this.resume(id);
|
|
950
|
+
void this.resume(id, detail);
|
|
749
951
|
}
|
|
750
|
-
async resume(id) {
|
|
952
|
+
async resume(id, known) {
|
|
751
953
|
if (this.disposed)
|
|
752
954
|
return;
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
955
|
+
const detail = known ??
|
|
956
|
+
agentsQueryClient().getQueryData(chatKeys.conversation(id));
|
|
957
|
+
if (!detail)
|
|
958
|
+
return;
|
|
959
|
+
if (!chatIsRunning(detail) || detail.execution?.waitingApproval) {
|
|
960
|
+
const chat = this.chats.get(id);
|
|
961
|
+
if (chat?.status !== 'submitted' &&
|
|
962
|
+
chat?.status !== 'streaming' &&
|
|
963
|
+
(!this.history(id).state.loaded ||
|
|
964
|
+
this.history(id).loadedThrough < (detail.lastSeq ?? 0)))
|
|
965
|
+
await this.loadHistory(id);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
const key = this.recoveryKey(id, detail);
|
|
969
|
+
const pending = this.recoveries.get(id);
|
|
756
970
|
let chat = this.chat(id);
|
|
757
|
-
if (chat.status === 'submitted' || chat.status === 'streaming')
|
|
971
|
+
if (chat.status === 'submitted' || chat.status === 'streaming') {
|
|
972
|
+
if (!pending || pending.key !== key)
|
|
973
|
+
this.recoveries.set(id, { key, phase: 'live' });
|
|
758
974
|
return;
|
|
975
|
+
}
|
|
976
|
+
if (pending?.key === key) {
|
|
977
|
+
if (pending.task)
|
|
978
|
+
return pending.task;
|
|
979
|
+
if (pending.phase === 'history')
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
759
982
|
const active = this.lifetime();
|
|
760
983
|
const replacement = this.replacements.get(id);
|
|
761
|
-
const
|
|
984
|
+
const command = this.commandVersions.get(id);
|
|
985
|
+
const recovery = { key, phase: 'recovering' };
|
|
986
|
+
this.recoveries.set(id, recovery);
|
|
987
|
+
this.publish();
|
|
988
|
+
const current = () => active() &&
|
|
989
|
+
this.commandVersions.get(id) === command &&
|
|
990
|
+
this.recoveries.get(id) === recovery &&
|
|
762
991
|
this.replacements.get(id) === replacement &&
|
|
763
|
-
this.chats.get(id) === chat
|
|
764
|
-
|
|
765
|
-
chat.status !== 'streaming';
|
|
992
|
+
this.chats.get(id) === chat;
|
|
993
|
+
const canResume = () => current() && chat.status !== 'submitted' && chat.status !== 'streaming';
|
|
766
994
|
const task = (async () => {
|
|
767
995
|
let interrupted;
|
|
768
996
|
try {
|
|
@@ -770,6 +998,8 @@ export class ChatController {
|
|
|
770
998
|
// A new input can begin while the recovery snapshot is still being read.
|
|
771
999
|
if (!canResume())
|
|
772
1000
|
return;
|
|
1001
|
+
if (this.history(id).error)
|
|
1002
|
+
throw this.history(id).error;
|
|
773
1003
|
if (chat.error) {
|
|
774
1004
|
const previous = chat;
|
|
775
1005
|
interrupted = [...previous.messages]
|
|
@@ -790,36 +1020,63 @@ export class ChatController {
|
|
|
790
1020
|
if (!active())
|
|
791
1021
|
return;
|
|
792
1022
|
}
|
|
793
|
-
this.recoveryErrors.delete(id);
|
|
794
1023
|
chat.clearError();
|
|
795
1024
|
const response = [...chat.messages]
|
|
796
1025
|
.reverse()
|
|
797
1026
|
.find((message) => message.role === 'assistant');
|
|
798
|
-
if (response
|
|
1027
|
+
if (response &&
|
|
1028
|
+
(response === interrupted ||
|
|
1029
|
+
!key ||
|
|
1030
|
+
response.metadata?.executionId === key ||
|
|
1031
|
+
response.id === detail.execution?.messageId)) {
|
|
1032
|
+
interrupted ??= response;
|
|
799
1033
|
chat.messages = mergeUIMessages(chat.messages, [], response.id);
|
|
1034
|
+
}
|
|
800
1035
|
await chat.resumeStream();
|
|
801
|
-
if (
|
|
802
|
-
|
|
1036
|
+
if (current()) {
|
|
1037
|
+
if (chat.error) {
|
|
1038
|
+
recovery.phase = 'interrupted';
|
|
1039
|
+
recovery.error = chat.error;
|
|
1040
|
+
}
|
|
1041
|
+
if (agentsQueryClient()
|
|
1042
|
+
.getQueryCache()
|
|
1043
|
+
.find({ queryKey: chatKeys.conversation(id), exact: true })
|
|
1044
|
+
?.isActive())
|
|
1045
|
+
this.scheduleRefresh(id);
|
|
1046
|
+
else
|
|
1047
|
+
await this.refresh(id);
|
|
1048
|
+
}
|
|
803
1049
|
}
|
|
804
1050
|
catch (error) {
|
|
805
|
-
if (
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
1051
|
+
if (current()) {
|
|
1052
|
+
if (chat.error) {
|
|
1053
|
+
recovery.phase = 'interrupted';
|
|
1054
|
+
recovery.error = chat.error;
|
|
1055
|
+
}
|
|
1056
|
+
else {
|
|
1057
|
+
// A failed history read is not evidence that the transport failed.
|
|
1058
|
+
this.historyMeta(id).error = error;
|
|
1059
|
+
if (recovery.phase === 'recovering')
|
|
1060
|
+
recovery.phase = 'live';
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
809
1063
|
}
|
|
810
1064
|
finally {
|
|
811
|
-
if (
|
|
1065
|
+
if (current()) {
|
|
812
1066
|
const display = interrupted;
|
|
813
1067
|
if (display &&
|
|
814
1068
|
canResume() &&
|
|
815
1069
|
!chat.messages.some((message) => message.id === display.id))
|
|
816
1070
|
chat.messages = mergeUIMessages(chat.messages, [display]);
|
|
817
|
-
|
|
1071
|
+
recovery.task = undefined;
|
|
1072
|
+
if (recovery.phase === 'recovering' &&
|
|
1073
|
+
(chat.status === 'submitted' || chat.status === 'streaming'))
|
|
1074
|
+
recovery.phase = 'live';
|
|
818
1075
|
this.publish();
|
|
819
1076
|
}
|
|
820
1077
|
}
|
|
821
1078
|
})();
|
|
822
|
-
|
|
1079
|
+
recovery.task = task;
|
|
823
1080
|
return task;
|
|
824
1081
|
}
|
|
825
1082
|
async approve(id, input) {
|
|
@@ -864,21 +1121,127 @@ export class ChatController {
|
|
|
864
1121
|
if (active())
|
|
865
1122
|
await this.refresh(id);
|
|
866
1123
|
}
|
|
867
|
-
|
|
1124
|
+
/** One detail request shared by the query observer and message reconciliation. */
|
|
1125
|
+
readDetail(id, fresh = false) {
|
|
1126
|
+
const pending = this.detailReads.get(id);
|
|
1127
|
+
if (pending)
|
|
1128
|
+
return fresh
|
|
1129
|
+
? pending.catch(() => undefined).then(() => this.readDetail(id))
|
|
1130
|
+
: pending;
|
|
1131
|
+
const task = this.api.conversation(id);
|
|
1132
|
+
this.detailReads.set(id, task);
|
|
1133
|
+
const clear = () => {
|
|
1134
|
+
if (this.detailReads.get(id) === task)
|
|
1135
|
+
this.detailReads.delete(id);
|
|
1136
|
+
};
|
|
1137
|
+
void task.then(clear, clear);
|
|
1138
|
+
return task;
|
|
1139
|
+
}
|
|
1140
|
+
refresh(id, fresh = true, idleProbe = false, reconcileHistory = true) {
|
|
868
1141
|
if (this.disposed)
|
|
1142
|
+
return Promise.resolve();
|
|
1143
|
+
const pending = this.refreshes.get(id);
|
|
1144
|
+
if (pending) {
|
|
1145
|
+
if (fresh) {
|
|
1146
|
+
pending.trailing = true;
|
|
1147
|
+
this.nextDetailVersion(id);
|
|
1148
|
+
}
|
|
1149
|
+
return pending.task;
|
|
1150
|
+
}
|
|
1151
|
+
const lifetime = this.lifetime(), commandVersion = this.commandVersions.get(id), active = () => lifetime() && this.commandVersions.get(id) === commandVersion;
|
|
1152
|
+
const owner = { trailing: false, task: Promise.resolve() };
|
|
1153
|
+
const run = async () => {
|
|
1154
|
+
do {
|
|
1155
|
+
owner.trailing = false;
|
|
1156
|
+
await this.refreshOnce(id, fresh, idleProbe, undefined, reconcileHistory);
|
|
1157
|
+
fresh = true;
|
|
1158
|
+
idleProbe = false;
|
|
1159
|
+
} while (owner.trailing && active());
|
|
1160
|
+
};
|
|
1161
|
+
owner.task = run().finally(() => {
|
|
1162
|
+
if (this.refreshes.get(id) === owner)
|
|
1163
|
+
this.refreshes.delete(id);
|
|
1164
|
+
});
|
|
1165
|
+
this.refreshes.set(id, owner);
|
|
1166
|
+
return owner.task;
|
|
1167
|
+
}
|
|
1168
|
+
async refreshOnce(id, fresh, idleProbe = false, snapshot, reconcileHistory = true) {
|
|
1169
|
+
if (this.disposed)
|
|
1170
|
+
return;
|
|
1171
|
+
const waiting = this.lifetime();
|
|
1172
|
+
if ((!fresh || !reconcileHistory) && !snapshot) {
|
|
1173
|
+
const query = agentsQueryClient()
|
|
1174
|
+
.getQueryCache()
|
|
1175
|
+
.find({ queryKey: chatKeys.conversation(id), exact: true });
|
|
1176
|
+
if (query?.state.fetchStatus === 'fetching' && query.promise)
|
|
1177
|
+
snapshot = (await query.promise);
|
|
1178
|
+
if (!waiting())
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
const lifetime = this.lifetime(), commandVersion = this.commandVersions.get(id), active = () => lifetime() && this.commandVersions.get(id) === commandVersion, ordered = this.detailRequest(id);
|
|
1182
|
+
const submittedOwner = this.chat(id);
|
|
1183
|
+
const submittedInput = submittedOwner.status === 'submitted' && !this.admissions.has(id)
|
|
1184
|
+
? [...submittedOwner.messages]
|
|
1185
|
+
.reverse()
|
|
1186
|
+
.find((message) => message.role === 'user' && message.metadata?.seq !== undefined)
|
|
1187
|
+
: undefined;
|
|
1188
|
+
const query = agentsQueryClient()
|
|
1189
|
+
.getQueryCache()
|
|
1190
|
+
.find({ queryKey: chatKeys.conversation(id), exact: true });
|
|
1191
|
+
const cached = !fresh && query?.isActive() && query.state.fetchStatus === 'idle'
|
|
1192
|
+
? query.state.data
|
|
1193
|
+
: undefined;
|
|
1194
|
+
const previous = query?.state.data;
|
|
1195
|
+
const detail = snapshot ?? cached ?? (await this.readDetail(id, fresh));
|
|
1196
|
+
if (!active())
|
|
1197
|
+
return;
|
|
1198
|
+
if (idleProbe &&
|
|
1199
|
+
previous &&
|
|
1200
|
+
!chatIsRunning(detail) &&
|
|
1201
|
+
detail.lastSeq !== undefined &&
|
|
1202
|
+
this.history(id).state.loaded &&
|
|
1203
|
+
!this.history(id).error &&
|
|
1204
|
+
this.history(id).loadedThrough >= detail.lastSeq &&
|
|
1205
|
+
hashKey([previous]) === hashKey([detail])) {
|
|
1206
|
+
// The durable cursor is current. Still reread revisions of the latest assistant response: a same-sequence
|
|
1207
|
+
// update may have lost its announcement, even though the conversation snapshot did not change.
|
|
1208
|
+
const latest = [...this.chat(id).messages]
|
|
1209
|
+
.reverse()
|
|
1210
|
+
.find((message) => message.role === 'assistant');
|
|
1211
|
+
const ids = [
|
|
1212
|
+
...new Set([
|
|
1213
|
+
...(this.deferredReads.get(id) ?? []),
|
|
1214
|
+
...(latest ? [latest.id] : []),
|
|
1215
|
+
]),
|
|
1216
|
+
];
|
|
1217
|
+
if (ids.length)
|
|
1218
|
+
await this.reread(id, ids);
|
|
1219
|
+
if (active() && ordered()) {
|
|
1220
|
+
this.remember(detail, false);
|
|
1221
|
+
this.publish();
|
|
1222
|
+
}
|
|
869
1223
|
return;
|
|
870
|
-
|
|
871
|
-
const detail = await this.api.conversation(id);
|
|
1224
|
+
}
|
|
872
1225
|
if (!active())
|
|
873
1226
|
return;
|
|
874
|
-
|
|
1227
|
+
const reconciled = fresh && !snapshot && reconcileHistory && ordered();
|
|
1228
|
+
if (reconciled)
|
|
875
1229
|
await this.reconcile(id, detail);
|
|
876
1230
|
if (!active())
|
|
877
1231
|
return;
|
|
878
1232
|
// A newer detail query must not discard this refresh's message catch-up.
|
|
879
1233
|
// loadHistory coalesces overlapping reads independently of detail ordering.
|
|
880
|
-
|
|
881
|
-
if (!
|
|
1234
|
+
const beforeThrough = this.history(id).loadedThrough;
|
|
1235
|
+
if ((!snapshot && !reconciled && fresh && detail.lastSeq === undefined) ||
|
|
1236
|
+
(reconciled && !ordered()) ||
|
|
1237
|
+
!this.history(id).state.loaded ||
|
|
1238
|
+
this.history(id).loadedThrough < (detail.lastSeq ?? 0)) {
|
|
1239
|
+
if (!reconcileHistory && detail.lastSeq !== undefined)
|
|
1240
|
+
await this.messagesChanged(id, detail.lastSeq);
|
|
1241
|
+
else
|
|
1242
|
+
await this.loadHistory(id, false, fresh);
|
|
1243
|
+
}
|
|
1244
|
+
if (!active())
|
|
882
1245
|
return;
|
|
883
1246
|
const history = this.history(id);
|
|
884
1247
|
if (history.error)
|
|
@@ -892,25 +1255,49 @@ export class ChatController {
|
|
|
892
1255
|
const revisable = [
|
|
893
1256
|
...new Set([
|
|
894
1257
|
...(this.deferredReads.get(id) ?? []),
|
|
895
|
-
...(detail.execution?.messageId
|
|
1258
|
+
...(detail.execution?.messageId &&
|
|
1259
|
+
((!snapshot &&
|
|
1260
|
+
!reconciled &&
|
|
1261
|
+
this.history(id).loadedThrough === beforeThrough) ||
|
|
1262
|
+
(this.chat(id).messages.find((m) => m.id === detail.execution?.messageId)?.metadata?.revision ?? -1) < (detail.execution.revision ?? 0))
|
|
896
1263
|
? [detail.execution.messageId]
|
|
897
|
-
:
|
|
1264
|
+
: !snapshot &&
|
|
1265
|
+
!reconciled &&
|
|
1266
|
+
this.history(id).loadedThrough === beforeThrough &&
|
|
1267
|
+
latest
|
|
898
1268
|
? [latest.id]
|
|
899
1269
|
: []),
|
|
900
1270
|
]),
|
|
901
1271
|
];
|
|
902
|
-
const saved = revisable.length
|
|
1272
|
+
const saved = revisable.length
|
|
1273
|
+
? await this.reread(id, revisable)
|
|
1274
|
+
: [...this.chat(id).messages];
|
|
903
1275
|
if (!active() || !ordered())
|
|
904
1276
|
return;
|
|
905
1277
|
const owner = this.chat(id), receiving = owner.messages.at(-1);
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
(
|
|
909
|
-
|
|
910
|
-
|
|
1278
|
+
const lastUser = [...owner.messages]
|
|
1279
|
+
.reverse()
|
|
1280
|
+
.find((message) => message.role === 'user');
|
|
1281
|
+
const executionId = submittedInput?.metadata?.executionId ?? this.recoveries.get(id)?.key;
|
|
1282
|
+
const submittedTerminal = owner === submittedOwner &&
|
|
1283
|
+
owner.status === 'submitted' &&
|
|
1284
|
+
submittedInput &&
|
|
1285
|
+
lastUser?.id === submittedInput.id &&
|
|
1286
|
+
detail.execution?.status === 'idle' &&
|
|
1287
|
+
[...saved, ...owner.messages].some((message) => message.role !== 'user' &&
|
|
911
1288
|
message.metadata?.seq !== undefined &&
|
|
912
|
-
message.metadata
|
|
913
|
-
message.metadata.
|
|
1289
|
+
message.metadata.seq > submittedInput.metadata.seq &&
|
|
1290
|
+
((executionId && message.metadata.executionId === executionId) ||
|
|
1291
|
+
message.id === detail.execution?.messageId));
|
|
1292
|
+
if (submittedTerminal ||
|
|
1293
|
+
(owner.status === 'streaming' &&
|
|
1294
|
+
receiving?.role === 'assistant' &&
|
|
1295
|
+
(detail.execution?.status === 'idle' ||
|
|
1296
|
+
detail.execution?.waitingApproval) &&
|
|
1297
|
+
saved.some((message) => message.id === receiving.id &&
|
|
1298
|
+
message.metadata?.seq !== undefined &&
|
|
1299
|
+
message.metadata?.executionId === receiving.metadata?.executionId &&
|
|
1300
|
+
message.metadata.revision >= (detail.execution?.revision ?? 0)))) {
|
|
914
1301
|
// Durable completion can release a lost subscriber; this never calls business Stop.
|
|
915
1302
|
await owner.stop();
|
|
916
1303
|
if (!active() || !ordered())
|
|
@@ -919,23 +1306,24 @@ export class ChatController {
|
|
|
919
1306
|
}
|
|
920
1307
|
if (!active() || !ordered())
|
|
921
1308
|
return;
|
|
922
|
-
|
|
1309
|
+
if (!snapshot)
|
|
1310
|
+
this.remember(detail, false);
|
|
923
1311
|
const chat = this.chat(id);
|
|
924
1312
|
if (detail.execution?.status === 'idle') {
|
|
925
|
-
this.
|
|
926
|
-
if (chat.error
|
|
927
|
-
saved.some((message) => message.id === chat.messages.at(-1)?.id &&
|
|
928
|
-
message.metadata?.seq !== undefined))
|
|
1313
|
+
this.recoveries.delete(id);
|
|
1314
|
+
if (chat.error)
|
|
929
1315
|
this.renewChat(id, chat.messages);
|
|
930
1316
|
}
|
|
931
|
-
else if (detail.execution?.status === 'running' &&
|
|
932
|
-
!detail.execution.waitingApproval &&
|
|
933
|
-
chat.status !== 'submitted' &&
|
|
934
|
-
chat.status !== 'streaming')
|
|
935
|
-
this.recoveryErrors.set(id, new Error('Execution remains active; reconcile saved history or resume its stream.'));
|
|
936
1317
|
this.publish();
|
|
937
1318
|
}
|
|
938
1319
|
scheduleRefresh(id) {
|
|
1320
|
+
if (agentsQueryClient()
|
|
1321
|
+
.getQueryCache()
|
|
1322
|
+
.find({ queryKey: chatKeys.conversation(id), exact: true })
|
|
1323
|
+
?.isActive()) {
|
|
1324
|
+
this.invalidateQuery(chatKeys.conversation(id), true);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
939
1327
|
if (this.completionReads.has(id))
|
|
940
1328
|
return;
|
|
941
1329
|
this.completionReads.add(id);
|
|
@@ -945,14 +1333,18 @@ export class ChatController {
|
|
|
945
1333
|
void this.refresh(id).catch(() => { });
|
|
946
1334
|
});
|
|
947
1335
|
}
|
|
948
|
-
observeDetail(detail) {
|
|
1336
|
+
observeDetail(detail, refresh = true, synchronizeLists = true) {
|
|
1337
|
+
if (synchronizeLists)
|
|
1338
|
+
this.syncLists(detail);
|
|
949
1339
|
if (detail.execution?.status === 'running')
|
|
950
1340
|
this.receive(detail.id, detail);
|
|
951
1341
|
if (!chatIsRunning(detail)) {
|
|
952
|
-
if (
|
|
953
|
-
this.
|
|
1342
|
+
if (refresh &&
|
|
1343
|
+
(this.chats.get(detail.id)?.status === 'streaming' ||
|
|
1344
|
+
this.chats.get(detail.id)?.status === 'submitted'))
|
|
1345
|
+
void this.refreshOnce(detail.id, false, false, detail).catch(() => { });
|
|
954
1346
|
this.stopRequests.delete(detail.id);
|
|
955
|
-
this.
|
|
1347
|
+
this.recoveries.delete(detail.id);
|
|
956
1348
|
this.publish();
|
|
957
1349
|
}
|
|
958
1350
|
}
|
|
@@ -987,16 +1379,112 @@ export class ChatController {
|
|
|
987
1379
|
const active = this.lifetime(), version = this.nextDetailVersion(id);
|
|
988
1380
|
return () => active() && this.detailVersions.get(id) === version;
|
|
989
1381
|
}
|
|
990
|
-
remember(detail) {
|
|
1382
|
+
remember(detail, refresh = true) {
|
|
991
1383
|
this.nextDetailVersion(detail.id);
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1384
|
+
// A reconnect already owns a fresh list read; its later history response must not schedule another one.
|
|
1385
|
+
this.observeDetail(detail, refresh, !this.reconnects.has(detail.id));
|
|
1386
|
+
const client = agentsQueryClient();
|
|
1387
|
+
client.setQueryData(chatKeys.conversation(detail.id), detail);
|
|
1388
|
+
}
|
|
1389
|
+
/** The HTTP receipt supplies this tab's snapshot; retain a newer read position for a trailing read. */
|
|
1390
|
+
observeReceipt(id, seenSeq) {
|
|
1391
|
+
const update = (item) => {
|
|
1392
|
+
if (item.id !== id || (item.seenSeq ?? -1) >= seenSeq)
|
|
1393
|
+
return item;
|
|
1394
|
+
return {
|
|
1395
|
+
...item,
|
|
1396
|
+
seenSeq,
|
|
1397
|
+
read: seenSeq >= (item.lastSeq ?? 0) || item.read,
|
|
1398
|
+
needsYou: item.needsYou &&
|
|
1399
|
+
item.needsYou.reason !== 'confirm' &&
|
|
1400
|
+
item.needsYou.seq <= seenSeq
|
|
1401
|
+
? null
|
|
1402
|
+
: item.needsYou,
|
|
1403
|
+
};
|
|
1404
|
+
};
|
|
1405
|
+
agentsQueryClient().setQueriesData({ queryKey: chatKeys.lists }, (old) => !old
|
|
1406
|
+
? old
|
|
1407
|
+
: Array.isArray(old)
|
|
1408
|
+
? old.map(update)
|
|
1409
|
+
: {
|
|
1410
|
+
...old,
|
|
1411
|
+
pages: old.pages.map((page) => ({
|
|
1412
|
+
...page,
|
|
1413
|
+
items: page.items.map(update),
|
|
1414
|
+
})),
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
deferReadReceipt(id, seenSeq, refresh) {
|
|
1418
|
+
const pending = this.pendingReceipts.get(id);
|
|
1419
|
+
if (!pending)
|
|
1420
|
+
return false;
|
|
1421
|
+
pending.seenSeq = Math.max(pending.seenSeq, seenSeq);
|
|
1422
|
+
pending.refresh = refresh;
|
|
1423
|
+
return true;
|
|
1424
|
+
}
|
|
1425
|
+
async markRead(id) {
|
|
1426
|
+
const client = agentsQueryClient();
|
|
1427
|
+
const key = chatKeys.conversation(id);
|
|
1428
|
+
const before = client.getQueryData(key);
|
|
1429
|
+
const active = this.lifetime(), commandVersion = this.commandVersions.get(id), receiptVersion = (this.receiptVersions.get(id) ?? 0) + 1;
|
|
1430
|
+
// Detail reads must not suppress the read transition needed by a viewed conversation.
|
|
1431
|
+
this.receiptVersions.set(id, receiptVersion);
|
|
1432
|
+
const pending = { seenSeq: -1 };
|
|
1433
|
+
this.pendingReceipts.set(id, pending);
|
|
1434
|
+
this.nextDetailVersion(id);
|
|
1435
|
+
try {
|
|
1436
|
+
const detail = await this.api.markRead(id);
|
|
1437
|
+
if (active() &&
|
|
1438
|
+
this.commandVersions.get(id) === commandVersion &&
|
|
1439
|
+
this.receiptVersions.get(id) === receiptVersion) {
|
|
1440
|
+
this.observeReceipt(id, detail.seenSeq ?? 0);
|
|
1441
|
+
// Read receipts do not change execution or invalidate pending history.
|
|
1442
|
+
this.nextDetailVersion(id);
|
|
1443
|
+
client.setQueryData(key, (current) => {
|
|
1444
|
+
if (!current ||
|
|
1445
|
+
current === before ||
|
|
1446
|
+
(current.lastSeq ?? 0) < (detail.lastSeq ?? 0) ||
|
|
1447
|
+
(current.lastSeq === detail.lastSeq &&
|
|
1448
|
+
current.updatedAt < detail.updatedAt))
|
|
1449
|
+
return detail;
|
|
1450
|
+
// A later snapshot may already contain the next reply or a metadata change.
|
|
1451
|
+
const seenSeq = Math.max(current.seenSeq ?? 0, detail.seenSeq ?? 0);
|
|
1452
|
+
return {
|
|
1453
|
+
...current,
|
|
1454
|
+
seenSeq,
|
|
1455
|
+
read: seenSeq >= (current.lastSeq ?? 0) || current.read,
|
|
1456
|
+
needsYou: current.needsYou &&
|
|
1457
|
+
current.needsYou.reason !== 'confirm' &&
|
|
1458
|
+
current.needsYou.seq <= seenSeq
|
|
1459
|
+
? null
|
|
1460
|
+
: current.needsYou,
|
|
1461
|
+
};
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
return detail;
|
|
1465
|
+
}
|
|
1466
|
+
finally {
|
|
1467
|
+
if (this.pendingReceipts.get(id) === pending) {
|
|
1468
|
+
this.pendingReceipts.delete(id);
|
|
1469
|
+
const current = agentsQueryClient().getQueryData(chatKeys.conversation(id));
|
|
1470
|
+
if (active() && pending.seenSeq > (current?.seenSeq ?? -1))
|
|
1471
|
+
pending.refresh?.();
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
995
1474
|
}
|
|
996
1475
|
async command(id, request, detail) {
|
|
997
1476
|
const active = this.lifetime(), version = (this.commandVersions.get(id) ?? 0) + 1;
|
|
998
1477
|
this.commandVersions.set(id, version);
|
|
1478
|
+
// A command replaces the snapshot generation; its reads must not wait on obsolete HTTP.
|
|
1479
|
+
this.detailReads.delete(id);
|
|
1480
|
+
this.refreshes.delete(id);
|
|
999
1481
|
this.nextDetailVersion(id);
|
|
1482
|
+
this.invalidateHistoryReads(id);
|
|
1483
|
+
const recovery = this.recoveries.get(id);
|
|
1484
|
+
if (recovery?.phase === 'history')
|
|
1485
|
+
this.recoveries.set(id, { key: recovery.key, phase: 'history' });
|
|
1486
|
+
else
|
|
1487
|
+
this.recoveries.delete(id);
|
|
1000
1488
|
await agentsQueryClient().cancelQueries({
|
|
1001
1489
|
queryKey: chatKeys.conversation(id),
|
|
1002
1490
|
exact: true,
|
|
@@ -1022,6 +1510,8 @@ export class ChatController {
|
|
|
1022
1510
|
this.disposed = false;
|
|
1023
1511
|
}
|
|
1024
1512
|
invalidate() {
|
|
1513
|
+
this.invalidations?.stop();
|
|
1514
|
+
this.invalidations = undefined;
|
|
1025
1515
|
if (this.disposed)
|
|
1026
1516
|
return;
|
|
1027
1517
|
this.disposed = true;
|
|
@@ -1036,6 +1526,9 @@ export class ChatController {
|
|
|
1036
1526
|
this.historyEpochs.clear();
|
|
1037
1527
|
this.replacements.clear();
|
|
1038
1528
|
this.reconciles.clear();
|
|
1529
|
+
this.streamFollowers.clear();
|
|
1530
|
+
this.detailReads.clear();
|
|
1531
|
+
this.refreshes.clear();
|
|
1039
1532
|
for (const watch of this.watches.values())
|
|
1040
1533
|
clearInterval(watch.timer);
|
|
1041
1534
|
this.watches.clear();
|
|
@@ -1045,14 +1538,19 @@ export class ChatController {
|
|
|
1045
1538
|
this.completionReads.clear();
|
|
1046
1539
|
this.admissions.clear();
|
|
1047
1540
|
this.loads.clear();
|
|
1541
|
+
this.messageReads.clear();
|
|
1542
|
+
this.catchups.clear();
|
|
1543
|
+
this.reconnects.clear();
|
|
1048
1544
|
this.refreshAgain.clear();
|
|
1049
1545
|
this.commandVersions.clear();
|
|
1546
|
+
this.receiptVersions.clear();
|
|
1547
|
+
this.pendingReceipts.clear();
|
|
1050
1548
|
this.stopRequests.clear();
|
|
1051
1549
|
this.approvalRequests.clear();
|
|
1052
|
-
this.
|
|
1053
|
-
this.resumed.clear();
|
|
1550
|
+
this.recoveries.clear();
|
|
1054
1551
|
this.submissions.invalidate();
|
|
1055
|
-
|
|
1552
|
+
// All plugin snapshots are viewer-scoped, including fresh agent options.
|
|
1553
|
+
agentsQueryClient().removeQueries({ queryKey: agentsKeys.all });
|
|
1056
1554
|
}
|
|
1057
1555
|
dispose() {
|
|
1058
1556
|
this.invalidate();
|
|
@@ -1071,10 +1569,7 @@ export function ChatClientProvider({ children, }) {
|
|
|
1071
1569
|
controller.activate();
|
|
1072
1570
|
const lifetimes = lifetimesRef.current;
|
|
1073
1571
|
lifetimes.set(controller, (lifetimes.get(controller) ?? 0) + 1);
|
|
1074
|
-
const reconnect = () => controller.reconnect();
|
|
1075
|
-
window.addEventListener('online', reconnect);
|
|
1076
1572
|
return () => {
|
|
1077
|
-
window.removeEventListener('online', reconnect);
|
|
1078
1573
|
controller.invalidate();
|
|
1079
1574
|
lifetimes.set(controller, (lifetimes.get(controller) ?? 1) - 1);
|
|
1080
1575
|
queueMicrotask(() => {
|
|
@@ -1095,12 +1590,13 @@ export function useChatController() {
|
|
|
1095
1590
|
}
|
|
1096
1591
|
export function useChatStream(id) {
|
|
1097
1592
|
const controller = useChatController();
|
|
1593
|
+
const ready = useRealtimeReadiness([CONVERSATIONS_TOPIC]);
|
|
1098
1594
|
useSDKChat({ chat: controller.chat(id ?? 'draft') });
|
|
1099
1595
|
useSyncExternalStore(controller.subscribe, controller.snapshot, controller.snapshot);
|
|
1100
1596
|
useEffect(() => {
|
|
1101
|
-
if (id)
|
|
1102
|
-
controller.
|
|
1103
|
-
}, [controller, id]);
|
|
1597
|
+
if (ready && id)
|
|
1598
|
+
return controller.followStream(id);
|
|
1599
|
+
}, [controller, id, ready]);
|
|
1104
1600
|
return id ? controller.state(id) : undefined;
|
|
1105
1601
|
}
|
|
1106
1602
|
export function useChatStreamState(id) {
|