@arcote.tech/arc-chat 0.7.13 → 0.7.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-chat",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.15",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Chat module with AI integration for Arc framework",
|
|
7
7
|
"main": "./src/index.ts",
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"type-check": "tsc --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"peerDependencies": {
|
|
13
|
-
"@arcote.tech/arc": "^0.7.
|
|
14
|
-
"@arcote.tech/arc-ai": "^0.7.
|
|
15
|
-
"@arcote.tech/arc-ai-voice": "^0.7.
|
|
16
|
-
"@arcote.tech/arc-auth": "^0.7.
|
|
17
|
-
"@arcote.tech/arc-ds": "^0.7.
|
|
18
|
-
"@arcote.tech/platform": "^0.7.
|
|
13
|
+
"@arcote.tech/arc": "^0.7.15",
|
|
14
|
+
"@arcote.tech/arc-ai": "^0.7.15",
|
|
15
|
+
"@arcote.tech/arc-ai-voice": "^0.7.15",
|
|
16
|
+
"@arcote.tech/arc-auth": "^0.7.15",
|
|
17
|
+
"@arcote.tech/arc-ds": "^0.7.15",
|
|
18
|
+
"@arcote.tech/platform": "^0.7.15",
|
|
19
19
|
"lucide-react": ">=0.400.0",
|
|
20
20
|
"react": ">=18.0.0",
|
|
21
21
|
"typescript": "^5.0.0"
|
|
@@ -625,6 +625,18 @@ export function createAiGenerationListener(config: AiGenerationListenerConfig) {
|
|
|
625
625
|
const provider = resolveProvider(model, scopeId);
|
|
626
626
|
if (!provider) return;
|
|
627
627
|
|
|
628
|
+
// Otwórz in-memory stream PRZED pierwszym awaitem (getByScope niżej +
|
|
629
|
+
// buildInstructions w pętli). Async listenery w arc startują
|
|
630
|
+
// synchronicznie w trakcie emit i suspendują na 1. await, więc wpis
|
|
631
|
+
// powstaje ZANIM mutacja zwróci → klient subskrybujący zaraz po mutacji
|
|
632
|
+
// nie wyprzedzi startStream (to był główny powód 410 ~1/5, szczególnie
|
|
633
|
+
// dla stage'ów z wolnym buildInstructions). Idempotent; runGenerationLoop
|
|
634
|
+
// woła ponownie dla iteracji 2+ (multi-turn).
|
|
635
|
+
const preAssistantId = (
|
|
636
|
+
event.payload as { assistantMessageId?: string }
|
|
637
|
+
).assistantMessageId;
|
|
638
|
+
if (preAssistantId) startStream(preAssistantId);
|
|
639
|
+
|
|
628
640
|
const dbMessages = await ctx
|
|
629
641
|
.query(messageElement)
|
|
630
642
|
.getByScope({ scopeId });
|
|
@@ -733,6 +745,14 @@ export function createAiResumeListener(config: AiGenerationListenerConfig) {
|
|
|
733
745
|
.handle(async (ctx, event) => {
|
|
734
746
|
const { sessionId, scopeId, toolCallId } = event.payload;
|
|
735
747
|
|
|
748
|
+
// Otwórz in-memory stream przed 1. awaitem — patrz listener generation.
|
|
749
|
+
// (provider sprawdzany niżej po await; rzadki misconfig sprząta
|
|
750
|
+
// MAX_STREAM_MS w stream-registry.)
|
|
751
|
+
const preAssistantId = (
|
|
752
|
+
event.payload as { assistantMessageId?: string }
|
|
753
|
+
).assistantMessageId;
|
|
754
|
+
if (preAssistantId) startStream(preAssistantId);
|
|
755
|
+
|
|
736
756
|
const dbMessages = await ctx
|
|
737
757
|
.query(messageElement)
|
|
738
758
|
.getByScope({ scopeId });
|
|
@@ -830,6 +850,9 @@ export function createAiRetryListener(config: AiGenerationListenerConfig) {
|
|
|
830
850
|
model: modelName,
|
|
831
851
|
} = event.payload as any;
|
|
832
852
|
|
|
853
|
+
// Otwórz in-memory stream przed 1. awaitem — patrz listener generation.
|
|
854
|
+
if (assistantMsgId) startStream(assistantMsgId);
|
|
855
|
+
|
|
833
856
|
const dbMessages = await ctx
|
|
834
857
|
.query(messageElement)
|
|
835
858
|
.getByScope({ scopeId });
|
|
@@ -554,21 +554,30 @@ export function createChatComponent(
|
|
|
554
554
|
|
|
555
555
|
(async () => {
|
|
556
556
|
try {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
557
|
+
// 410 = brak in-memory streamu dla messageId. Serwer tworzy stream
|
|
558
|
+
// synchronicznie ze startem turnu (listener przed 1. awaitem), więc
|
|
559
|
+
// race "GET przed startStream" jest zamknięty — ale zostaje krótki
|
|
560
|
+
// residualny race i okno restartu serwera. Ponów kilka razy z
|
|
561
|
+
// backoffem zanim uznasz turn za przerwany: startStream / grace
|
|
562
|
+
// window zwykle dogania w tym czasie.
|
|
563
|
+
let res: Response | null = null;
|
|
564
|
+
const MAX_410_RETRIES = 4;
|
|
565
|
+
const RETRY_DELAY_MS = 300;
|
|
566
|
+
for (let attempt = 0; ; attempt++) {
|
|
567
|
+
res = await fetch(`/route/chat/${chatName}/stream/${messageId}`, {
|
|
560
568
|
credentials: "include",
|
|
561
569
|
signal: ctrl.signal,
|
|
562
570
|
headers: { Accept: "text/event-stream" },
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
571
|
+
});
|
|
572
|
+
if (res.status !== 410) break;
|
|
573
|
+
if (attempt >= MAX_410_RETRIES) {
|
|
574
|
+
// Naprawdę nieosiągalny (restart mid-stream / poza grace window).
|
|
575
|
+
setInterruptedIds((prev) => new Set(prev).add(messageId));
|
|
576
|
+
setIsStreaming(false);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
await new Promise<void>((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
580
|
+
if (cancelled) return;
|
|
572
581
|
}
|
|
573
582
|
if (!res.ok) throw new Error(`Stream failed: ${res.status}`);
|
|
574
583
|
|
|
@@ -33,6 +33,13 @@ interface MessageStream {
|
|
|
33
33
|
>;
|
|
34
34
|
subscribers: Set<ReadableStreamDefaultController<Uint8Array>>;
|
|
35
35
|
keepAliveInterval?: ReturnType<typeof setInterval>;
|
|
36
|
+
/**
|
|
37
|
+
* Hard safety cap. `startStream` schedules it; `finalize` clears it. If a
|
|
38
|
+
* generation never calls `finalize()` (listener threw before the loop, no
|
|
39
|
+
* provider, hung stream) this evicts the entry so the registry can't grow
|
|
40
|
+
* unbounded — the only TTL on a *live* (non-finalized) stream.
|
|
41
|
+
*/
|
|
42
|
+
maxLifetimeTimer?: ReturnType<typeof setTimeout>;
|
|
36
43
|
finalized: boolean;
|
|
37
44
|
finalEvent?: ChatStreamEvent;
|
|
38
45
|
}
|
|
@@ -40,6 +47,9 @@ interface MessageStream {
|
|
|
40
47
|
const streams = new Map<string, MessageStream>();
|
|
41
48
|
const FINALIZE_GRACE_MS = 5_000;
|
|
42
49
|
const KEEPALIVE_INTERVAL_MS = 5_000;
|
|
50
|
+
// Generous upper bound for a single turn's stream (one LLM response, even with
|
|
51
|
+
// tools). Far longer than any realistic generation; only trips on a leak.
|
|
52
|
+
const MAX_STREAM_MS = 10 * 60_000;
|
|
43
53
|
const encoder = new TextEncoder();
|
|
44
54
|
|
|
45
55
|
function encode(event: ChatStreamEvent): Uint8Array {
|
|
@@ -75,13 +85,40 @@ function ensureKeepAlive(s: MessageStream): void {
|
|
|
75
85
|
*/
|
|
76
86
|
export function startStream(messageId: string): void {
|
|
77
87
|
if (streams.has(messageId)) return;
|
|
78
|
-
|
|
88
|
+
const s: MessageStream = {
|
|
79
89
|
messageId,
|
|
80
90
|
currentBlocks: [],
|
|
81
91
|
toolCallsById: new Map(),
|
|
82
92
|
subscribers: new Set(),
|
|
83
93
|
finalized: false,
|
|
84
|
-
}
|
|
94
|
+
};
|
|
95
|
+
// Leak guard — see MAX_STREAM_MS. Cleared in finalize() on the normal path.
|
|
96
|
+
s.maxLifetimeTimer = setTimeout(() => evict(messageId), MAX_STREAM_MS);
|
|
97
|
+
streams.set(messageId, s);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Force-drop a stream that never finalized (safety net). Closes any subscribers
|
|
102
|
+
* and tears down timers. No-op if already gone.
|
|
103
|
+
*/
|
|
104
|
+
function evict(messageId: string): void {
|
|
105
|
+
const s = streams.get(messageId);
|
|
106
|
+
if (!s) return;
|
|
107
|
+
if (s.keepAliveInterval) {
|
|
108
|
+
clearInterval(s.keepAliveInterval);
|
|
109
|
+
s.keepAliveInterval = undefined;
|
|
110
|
+
}
|
|
111
|
+
if (s.maxLifetimeTimer) {
|
|
112
|
+
clearTimeout(s.maxLifetimeTimer);
|
|
113
|
+
s.maxLifetimeTimer = undefined;
|
|
114
|
+
}
|
|
115
|
+
for (const ctrl of s.subscribers) {
|
|
116
|
+
try {
|
|
117
|
+
ctrl.close();
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
s.subscribers.clear();
|
|
121
|
+
streams.delete(messageId);
|
|
85
122
|
}
|
|
86
123
|
|
|
87
124
|
/**
|
|
@@ -266,6 +303,12 @@ export function finalize(
|
|
|
266
303
|
clearInterval(s.keepAliveInterval);
|
|
267
304
|
s.keepAliveInterval = undefined;
|
|
268
305
|
}
|
|
306
|
+
// Normal teardown owns the lifecycle now — cancel the safety cap so it can't
|
|
307
|
+
// fire during/after the grace window.
|
|
308
|
+
if (s.maxLifetimeTimer) {
|
|
309
|
+
clearTimeout(s.maxLifetimeTimer);
|
|
310
|
+
s.maxLifetimeTimer = undefined;
|
|
311
|
+
}
|
|
269
312
|
|
|
270
313
|
setTimeout(() => {
|
|
271
314
|
streams.delete(messageId);
|