agentgui 1.0.651 → 1.0.653
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/database.js +7 -7
- package/package.json +1 -1
- package/server.js +18 -152
- package/static/index.html +2 -3
- package/static/js/streaming-renderer.js +0 -9
- package/lib/compressor.js +0 -125
- package/static/js/request-manager.js +0 -138
package/database.js
CHANGED
|
@@ -1108,13 +1108,13 @@ export const queries = {
|
|
|
1108
1108
|
}
|
|
1109
1109
|
|
|
1110
1110
|
const deleteAllStmt = db.transaction(() => {
|
|
1111
|
-
prep('DELETE FROM stream_updates');
|
|
1112
|
-
prep('DELETE FROM chunks');
|
|
1113
|
-
prep('DELETE FROM events');
|
|
1114
|
-
prep('DELETE FROM voice_cache');
|
|
1115
|
-
prep('DELETE FROM sessions');
|
|
1116
|
-
prep('DELETE FROM messages');
|
|
1117
|
-
prep('DELETE FROM conversations');
|
|
1111
|
+
prep('DELETE FROM stream_updates').run();
|
|
1112
|
+
prep('DELETE FROM chunks').run();
|
|
1113
|
+
prep('DELETE FROM events').run();
|
|
1114
|
+
prep('DELETE FROM voice_cache').run();
|
|
1115
|
+
prep('DELETE FROM sessions').run();
|
|
1116
|
+
prep('DELETE FROM messages').run();
|
|
1117
|
+
prep('DELETE FROM conversations').run();
|
|
1118
1118
|
});
|
|
1119
1119
|
|
|
1120
1120
|
deleteAllStmt();
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -302,7 +302,6 @@ const rateLimitState = new Map();
|
|
|
302
302
|
const activeProcessesByRunId = new Map();
|
|
303
303
|
const activeProcessesByConvId = new Map(); // Store process handles by conversationId for steering
|
|
304
304
|
const steeringTimeouts = new Map(); // Track timeout handles for process cleanup
|
|
305
|
-
const acpQueries = queries;
|
|
306
305
|
const checkpointManager = new CheckpointManager(queries);
|
|
307
306
|
const STUCK_AGENT_THRESHOLD_MS = 600000;
|
|
308
307
|
const NO_PID_GRACE_PERIOD_MS = 60000;
|
|
@@ -1592,115 +1591,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
1592
1591
|
return;
|
|
1593
1592
|
}
|
|
1594
1593
|
|
|
1595
|
-
const oldRunByIdMatch = pathOnly.match(/^\/api\/runs\/([^/]+)$/);
|
|
1596
|
-
if (oldRunByIdMatch) {
|
|
1597
|
-
const runId = oldRunByIdMatch[1];
|
|
1598
|
-
const session = queries.getSession(runId);
|
|
1599
|
-
|
|
1600
|
-
if (!session) {
|
|
1601
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1602
|
-
return;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
if (req.method === 'GET') {
|
|
1606
|
-
sendJSON(req, res, 200, {
|
|
1607
|
-
id: session.id,
|
|
1608
|
-
status: session.status,
|
|
1609
|
-
started_at: session.started_at,
|
|
1610
|
-
completed_at: session.completed_at,
|
|
1611
|
-
agentId: session.agentId,
|
|
1612
|
-
input: null,
|
|
1613
|
-
output: null
|
|
1614
|
-
});
|
|
1615
|
-
return;
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
if (req.method === 'DELETE') {
|
|
1619
|
-
queries.deleteSession(runId);
|
|
1620
|
-
sendJSON(req, res, 204, {});
|
|
1621
|
-
return;
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
if (req.method === 'POST') {
|
|
1625
|
-
if (session.status !== 'interrupted') {
|
|
1626
|
-
sendJSON(req, res, 409, { error: 'Can only resume interrupted runs' });
|
|
1627
|
-
return;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
let body = '';
|
|
1631
|
-
for await (const chunk of req) { body += chunk; }
|
|
1632
|
-
let parsed = {};
|
|
1633
|
-
try { parsed = body ? JSON.parse(body) : {}; } catch {}
|
|
1634
|
-
|
|
1635
|
-
const { input } = parsed;
|
|
1636
|
-
if (!input) {
|
|
1637
|
-
sendJSON(req, res, 400, { error: 'Missing input in request body' });
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
const conv = queries.getConversation(session.conversationId);
|
|
1642
|
-
const resolvedAgentId = session.agentId || conv?.agentId || 'claude-code';
|
|
1643
|
-
const resolvedModel = conv?.model || null;
|
|
1644
|
-
const cwd = conv?.workingDirectory || STARTUP_CWD;
|
|
1645
|
-
|
|
1646
|
-
queries.updateSession(runId, { status: 'pending' });
|
|
1647
|
-
|
|
1648
|
-
const message = queries.createMessage(session.conversationId, 'user', typeof input === 'string' ? input : JSON.stringify(input));
|
|
1649
|
-
|
|
1650
|
-
processMessageWithStreaming(session.conversationId, message.id, runId, typeof input === 'string' ? input : JSON.stringify(input), resolvedAgentId, resolvedModel);
|
|
1651
|
-
|
|
1652
|
-
sendJSON(req, res, 200, {
|
|
1653
|
-
id: session.id,
|
|
1654
|
-
status: 'pending',
|
|
1655
|
-
started_at: session.started_at,
|
|
1656
|
-
agentId: resolvedAgentId
|
|
1657
|
-
});
|
|
1658
|
-
return;
|
|
1659
|
-
}
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
const oldRunCancelMatch = pathOnly.match(/^\/api\/runs\/([^/]+)\/cancel$/);
|
|
1663
|
-
if (oldRunCancelMatch && req.method === 'POST') {
|
|
1664
|
-
const runId = oldRunCancelMatch[1];
|
|
1665
|
-
const session = queries.getSession(runId);
|
|
1666
|
-
|
|
1667
|
-
if (!session) {
|
|
1668
|
-
sendJSON(req, res, 404, { error: 'Run not found' });
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
const conversationId = session.conversationId;
|
|
1673
|
-
const entry = activeExecutions.get(conversationId);
|
|
1674
|
-
|
|
1675
|
-
if (entry && entry.sessionId === runId) {
|
|
1676
|
-
const { pid } = entry;
|
|
1677
|
-
if (pid) {
|
|
1678
|
-
try {
|
|
1679
|
-
process.kill(-pid, 'SIGKILL');
|
|
1680
|
-
} catch {
|
|
1681
|
-
try {
|
|
1682
|
-
process.kill(pid, 'SIGKILL');
|
|
1683
|
-
} catch (e) {}
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
queries.updateSession(runId, { status: 'interrupted', completed_at: Date.now() });
|
|
1689
|
-
queries.setIsStreaming(conversationId, false);
|
|
1690
|
-
activeExecutions.delete(conversationId);
|
|
1691
|
-
|
|
1692
|
-
broadcastSync({
|
|
1693
|
-
type: 'streaming_complete',
|
|
1694
|
-
sessionId: runId,
|
|
1695
|
-
conversationId,
|
|
1696
|
-
interrupted: true,
|
|
1697
|
-
timestamp: Date.now()
|
|
1698
|
-
});
|
|
1699
|
-
|
|
1700
|
-
sendJSON(req, res, 204, {});
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
1594
|
const scriptsMatch = pathOnly.match(/^\/api\/conversations\/([^/]+)\/scripts$/);
|
|
1705
1595
|
if (scriptsMatch && req.method === 'GET') {
|
|
1706
1596
|
const conv = queries.getConversation(scriptsMatch[1]);
|
|
@@ -3597,6 +3487,21 @@ function createChunkBatcher() {
|
|
|
3597
3487
|
return { add, drain };
|
|
3598
3488
|
}
|
|
3599
3489
|
|
|
3490
|
+
function parseRateLimitResetTime(text) {
|
|
3491
|
+
const match = text.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
|
|
3492
|
+
if (!match) return 300;
|
|
3493
|
+
let hours = parseInt(match[1], 10);
|
|
3494
|
+
const minutes = match[2] ? parseInt(match[2], 10) : 0;
|
|
3495
|
+
const period = match[3]?.toLowerCase();
|
|
3496
|
+
if (period === 'pm' && hours !== 12) hours += 12;
|
|
3497
|
+
if (period === 'am' && hours === 12) hours = 0;
|
|
3498
|
+
const now = new Date();
|
|
3499
|
+
const resetTime = new Date(now);
|
|
3500
|
+
resetTime.setUTCHours(hours, minutes, 0, 0);
|
|
3501
|
+
if (resetTime <= now) resetTime.setUTCDate(resetTime.getUTCDate() + 1);
|
|
3502
|
+
return Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3600
3505
|
async function processMessageWithStreaming(conversationId, messageId, sessionId, content, agentId, model, subAgent) {
|
|
3601
3506
|
const startTime = Date.now();
|
|
3602
3507
|
touchACP(agentId);
|
|
@@ -3700,28 +3605,8 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3700
3605
|
if (rateLimitTextMatch) {
|
|
3701
3606
|
debugLog(`[rate-limit] Detected rate limit message in stream for conv ${conversationId}`);
|
|
3702
3607
|
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
const resetTimeMatch = block.text.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
|
|
3706
|
-
if (resetTimeMatch) {
|
|
3707
|
-
let hours = parseInt(resetTimeMatch[1], 10);
|
|
3708
|
-
const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
|
|
3709
|
-
const period = resetTimeMatch[3]?.toLowerCase();
|
|
3710
|
-
|
|
3711
|
-
if (period === 'pm' && hours !== 12) hours += 12;
|
|
3712
|
-
if (period === 'am' && hours === 12) hours = 0;
|
|
3713
|
-
|
|
3714
|
-
const now = new Date();
|
|
3715
|
-
const resetTime = new Date(now);
|
|
3716
|
-
resetTime.setUTCHours(hours, minutes, 0, 0);
|
|
3717
|
-
|
|
3718
|
-
if (resetTime <= now) {
|
|
3719
|
-
resetTime.setUTCDate(resetTime.getUTCDate() + 1);
|
|
3720
|
-
}
|
|
3721
|
-
|
|
3722
|
-
retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
|
|
3723
|
-
debugLog(`[rate-limit] Parsed reset time: ${resetTime.toISOString()}, retry in ${retryAfterSec}s`);
|
|
3724
|
-
}
|
|
3608
|
+
const retryAfterSec = parseRateLimitResetTime(block.text);
|
|
3609
|
+
debugLog(`[rate-limit] Parsed reset time, retry in ${retryAfterSec}s`);
|
|
3725
3610
|
|
|
3726
3611
|
// Kill the running process
|
|
3727
3612
|
const entry = activeExecutions.get(conversationId);
|
|
@@ -3833,26 +3718,7 @@ async function processMessageWithStreaming(conversationId, messageId, sessionId,
|
|
|
3833
3718
|
if (rateLimitResultMatch) {
|
|
3834
3719
|
debugLog(`[rate-limit] Detected rate limit in result for conv ${conversationId}`);
|
|
3835
3720
|
|
|
3836
|
-
|
|
3837
|
-
const resetTimeMatch = resultText.match(/resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*\(?(UTC|[A-Z]{2,4})\)?/i);
|
|
3838
|
-
if (resetTimeMatch) {
|
|
3839
|
-
let hours = parseInt(resetTimeMatch[1], 10);
|
|
3840
|
-
const minutes = resetTimeMatch[2] ? parseInt(resetTimeMatch[2], 10) : 0;
|
|
3841
|
-
const period = resetTimeMatch[3]?.toLowerCase();
|
|
3842
|
-
|
|
3843
|
-
if (period === 'pm' && hours !== 12) hours += 12;
|
|
3844
|
-
if (period === 'am' && hours === 12) hours = 0;
|
|
3845
|
-
|
|
3846
|
-
const now = new Date();
|
|
3847
|
-
const resetTime = new Date(now);
|
|
3848
|
-
resetTime.setUTCHours(hours, minutes, 0, 0);
|
|
3849
|
-
|
|
3850
|
-
if (resetTime <= now) {
|
|
3851
|
-
resetTime.setUTCDate(resetTime.getUTCDate() + 1);
|
|
3852
|
-
}
|
|
3853
|
-
|
|
3854
|
-
retryAfterSec = Math.max(60, Math.ceil((resetTime.getTime() - now.getTime()) / 1000));
|
|
3855
|
-
}
|
|
3721
|
+
const retryAfterSec = parseRateLimitResetTime(resultText);
|
|
3856
3722
|
|
|
3857
3723
|
const entry = activeExecutions.get(conversationId);
|
|
3858
3724
|
if (entry && entry.pid) {
|
package/static/index.html
CHANGED
|
@@ -3245,6 +3245,7 @@
|
|
|
3245
3245
|
var _escHtmlRe = /[&<>"']/g;
|
|
3246
3246
|
window._escHtml = function(t) { return t.replace(_escHtmlRe, function(c) { return _escHtmlMap[c]; }); };
|
|
3247
3247
|
</script>
|
|
3248
|
+
<script defer src="/gm/js/conversations.js"></script>
|
|
3248
3249
|
<script defer src="/gm/js/event-processor.js"></script>
|
|
3249
3250
|
<script defer src="/gm/js/streaming-renderer.js"></script>
|
|
3250
3251
|
<script defer src="/gm/js/image-loader.js"></script>
|
|
@@ -3252,12 +3253,10 @@
|
|
|
3252
3253
|
<script defer src="/gm/js/event-consolidator.js"></script>
|
|
3253
3254
|
<script defer src="/gm/js/websocket-manager.js"></script>
|
|
3254
3255
|
<script defer src="/gm/js/ws-client.js"></script>
|
|
3255
|
-
|
|
3256
|
-
<script defer src="/gm/js/syntax-highlighter.js"></script>
|
|
3256
|
+
<script defer src="/gm/js/syntax-highlighter.js"></script>
|
|
3257
3257
|
<script defer src="/gm/js/dialogs.js"></script>
|
|
3258
3258
|
<script defer src="/gm/js/ui-components.js"></script>
|
|
3259
3259
|
<script defer src="/gm/js/state-barrier.js"></script>
|
|
3260
|
-
<script defer src="/gm/js/conversations.js"></script>
|
|
3261
3260
|
<script defer src="/gm/js/terminal.js"></script>
|
|
3262
3261
|
<script defer src="/gm/js/script-runner.js"></script>
|
|
3263
3262
|
<script defer src="/gm/js/tools-manager.js"></script>
|
|
@@ -4,15 +4,6 @@
|
|
|
4
4
|
* for Claude Code streaming execution display
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
function pathSplit(p) {
|
|
8
|
-
return p.split(/[\/\\]/).filter(Boolean);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function pathBasename(p) {
|
|
12
|
-
const parts = pathSplit(p);
|
|
13
|
-
return parts.length ? parts.pop() : '';
|
|
14
|
-
}
|
|
15
|
-
|
|
16
7
|
class StreamingRenderer {
|
|
17
8
|
constructor(config = {}) {
|
|
18
9
|
// Configuration
|
package/lib/compressor.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Compressor: tokenize text fields → msgpackr → optional gzip
|
|
3
|
-
*
|
|
4
|
-
* Transport: event → msgpackr.pack() → gzip if >512B → binary WS frame
|
|
5
|
-
* Storage: text → token array (Uint32) → msgpackr.pack() → BLOB
|
|
6
|
-
*/
|
|
7
|
-
import zlib from 'zlib';
|
|
8
|
-
import { pack, unpack } from 'msgpackr';
|
|
9
|
-
import { encode as encodeTokens, decode as decodeTokens } from 'gpt-tokenizer';
|
|
10
|
-
|
|
11
|
-
// Magic prefix stored at start of compressed BLOBs so we can detect them
|
|
12
|
-
const MAGIC = Buffer.from([0xC0, 0xDE]); // "CODE"
|
|
13
|
-
const GZIP_MAGIC = Buffer.from([0x1f, 0x8b]);
|
|
14
|
-
|
|
15
|
-
// ── Token helpers ─────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
export function tokenize(text) {
|
|
18
|
-
if (typeof text !== 'string' || text.length === 0) return null;
|
|
19
|
-
try {
|
|
20
|
-
return new Uint32Array(encodeTokens(text));
|
|
21
|
-
} catch {
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function detokenize(tokens) {
|
|
27
|
-
try {
|
|
28
|
-
const arr = tokens instanceof Uint32Array ? Array.from(tokens) : tokens;
|
|
29
|
-
return decodeTokens(arr);
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ── Storage compression (text → tokens → msgpack BLOB) ────────────────────────
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Compress a string for database storage.
|
|
39
|
-
* Returns a Buffer starting with MAGIC prefix.
|
|
40
|
-
*/
|
|
41
|
-
export function compressForStorage(text) {
|
|
42
|
-
if (typeof text !== 'string') return null;
|
|
43
|
-
const tokens = tokenize(text);
|
|
44
|
-
if (!tokens) return null;
|
|
45
|
-
const packed = pack({ t: Array.from(tokens) });
|
|
46
|
-
return Buffer.concat([MAGIC, packed]);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Decompress a storage BLOB back to a string.
|
|
51
|
-
* Returns null if not a compressed BLOB (caller should use raw value).
|
|
52
|
-
*/
|
|
53
|
-
export function decompressFromStorage(buf) {
|
|
54
|
-
if (!Buffer.isBuffer(buf) && !(buf instanceof Uint8Array)) return null;
|
|
55
|
-
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
56
|
-
if (b.length < MAGIC.length || b[0] !== MAGIC[0] || b[1] !== MAGIC[1]) return null;
|
|
57
|
-
try {
|
|
58
|
-
const { t } = unpack(b.slice(MAGIC.length));
|
|
59
|
-
return detokenize(t);
|
|
60
|
-
} catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ── Transport compression (event → msgpack → gzip → binary frame) ─────────────
|
|
66
|
-
|
|
67
|
-
const GZ_THRESHOLD = 512; // bytes — compress if msgpack payload exceeds this
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Pack one event (or array of events) into a binary buffer for WebSocket transport.
|
|
71
|
-
* Format: [ 0x01 ] + gzip(msgpack(data)) when compressed
|
|
72
|
-
* [ 0x00 ] + msgpack(data) when not compressed
|
|
73
|
-
*/
|
|
74
|
-
export function packForTransport(data) {
|
|
75
|
-
const packed = pack(data);
|
|
76
|
-
if (packed.length > GZ_THRESHOLD) {
|
|
77
|
-
try {
|
|
78
|
-
const compressed = zlib.gzipSync(packed, { level: 6 });
|
|
79
|
-
if (compressed.length < packed.length * 0.9) {
|
|
80
|
-
const out = Buffer.allocUnsafe(1 + compressed.length);
|
|
81
|
-
out[0] = 0x01; // flag: gzipped
|
|
82
|
-
compressed.copy(out, 1);
|
|
83
|
-
return out;
|
|
84
|
-
}
|
|
85
|
-
} catch (_) {}
|
|
86
|
-
}
|
|
87
|
-
const out = Buffer.allocUnsafe(1 + packed.length);
|
|
88
|
-
out[0] = 0x00; // flag: plain msgpack
|
|
89
|
-
packed.copy(out, 1);
|
|
90
|
-
return out;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Unpack a binary buffer received from WebSocket transport.
|
|
95
|
-
*/
|
|
96
|
-
export function unpackFromTransport(buf) {
|
|
97
|
-
const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
98
|
-
if (b.length < 2) return null;
|
|
99
|
-
const flag = b[0];
|
|
100
|
-
const payload = b.slice(1);
|
|
101
|
-
if (flag === 0x01) {
|
|
102
|
-
return unpack(zlib.gunzipSync(payload));
|
|
103
|
-
}
|
|
104
|
-
return unpack(payload);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ── Tokenize specific fields in an event object (for storage) ─────────────────
|
|
108
|
-
|
|
109
|
-
const TEXT_FIELDS = ['content', 'text', 'data', 'output', 'input', 'message', 'prompt', 'response'];
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Walk an event object and compress any large text fields in-place for storage.
|
|
113
|
-
* Returns the (possibly mutated) object — safe to pass to JSON.stringify or pack().
|
|
114
|
-
*/
|
|
115
|
-
export function compressEventFields(obj) {
|
|
116
|
-
if (!obj || typeof obj !== 'object') return obj;
|
|
117
|
-
for (const key of TEXT_FIELDS) {
|
|
118
|
-
const val = obj[key];
|
|
119
|
-
if (typeof val === 'string' && val.length > 64) {
|
|
120
|
-
const buf = compressForStorage(val);
|
|
121
|
-
if (buf) obj[key] = buf;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return obj;
|
|
125
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Request Manager - Phase 2: Request Lifetime Management
|
|
3
|
-
* Tracks in-flight requests with unique IDs, enables cancellation on navigation
|
|
4
|
-
* Prevents race conditions where older requests complete after newer ones
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
class RequestManager {
|
|
8
|
-
constructor() {
|
|
9
|
-
this._requestId = 0;
|
|
10
|
-
this._inflightRequests = new Map(); // requestId -> { conversationId, abortController, timestamp, priority }
|
|
11
|
-
this._activeLoadId = null; // Track which request is currently being rendered
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Start a new load request for a conversation
|
|
16
|
-
* Returns a request token that must be verified before rendering
|
|
17
|
-
*/
|
|
18
|
-
startLoadRequest(conversationId, priority = 'normal') {
|
|
19
|
-
const requestId = ++this._requestId;
|
|
20
|
-
const abortController = new AbortController();
|
|
21
|
-
|
|
22
|
-
this._inflightRequests.set(requestId, {
|
|
23
|
-
conversationId,
|
|
24
|
-
abortController,
|
|
25
|
-
timestamp: Date.now(),
|
|
26
|
-
priority,
|
|
27
|
-
status: 'pending'
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
requestId,
|
|
32
|
-
abortSignal: abortController.signal,
|
|
33
|
-
cancel: () => this._cancelRequest(requestId),
|
|
34
|
-
verify: () => this._verifyRequest(requestId, conversationId)
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Mark request as completed (allows rendering)
|
|
40
|
-
*/
|
|
41
|
-
completeRequest(requestId) {
|
|
42
|
-
const req = this._inflightRequests.get(requestId);
|
|
43
|
-
if (req) {
|
|
44
|
-
req.status = 'completed';
|
|
45
|
-
this._activeLoadId = requestId;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Verify request is still valid before rendering
|
|
51
|
-
* Returns true only if this is the most recent request for this conversation
|
|
52
|
-
*/
|
|
53
|
-
_verifyRequest(requestId, conversationId) {
|
|
54
|
-
const req = this._inflightRequests.get(requestId);
|
|
55
|
-
|
|
56
|
-
// Request not found or cancelled
|
|
57
|
-
if (!req) return false;
|
|
58
|
-
|
|
59
|
-
// Request is for different conversation
|
|
60
|
-
if (req.conversationId !== conversationId) return false;
|
|
61
|
-
|
|
62
|
-
// Find all requests for this conversation
|
|
63
|
-
const allForConv = Array.from(this._inflightRequests.entries())
|
|
64
|
-
.filter(([_, r]) => r.conversationId === conversationId && r.status === 'completed')
|
|
65
|
-
.sort((a, b) => b[0] - a[0]); // Sort by requestId descending (newest first)
|
|
66
|
-
|
|
67
|
-
// This request is the newest completed one for this conversation
|
|
68
|
-
return allForConv.length > 0 && allForConv[0][0] === requestId;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Cancel a request (aborts any pending network operations)
|
|
73
|
-
*/
|
|
74
|
-
_cancelRequest(requestId) {
|
|
75
|
-
const req = this._inflightRequests.get(requestId);
|
|
76
|
-
if (req) {
|
|
77
|
-
req.status = 'cancelled';
|
|
78
|
-
req.abortController.abort();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Cancel all pending requests for a conversation
|
|
84
|
-
*/
|
|
85
|
-
cancelConversationRequests(conversationId) {
|
|
86
|
-
for (const [id, req] of this._inflightRequests.entries()) {
|
|
87
|
-
if (req.conversationId === conversationId && req.status !== 'completed') {
|
|
88
|
-
this._cancelRequest(id);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Cancel all in-flight requests
|
|
95
|
-
*/
|
|
96
|
-
cancelAllRequests() {
|
|
97
|
-
for (const [id, req] of this._inflightRequests.entries()) {
|
|
98
|
-
if (req.status !== 'completed') {
|
|
99
|
-
this._cancelRequest(id);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Clean up old requests to prevent memory leak
|
|
106
|
-
*/
|
|
107
|
-
cleanup() {
|
|
108
|
-
const now = Date.now();
|
|
109
|
-
const maxAge = 60000; // Keep requests for 60 seconds
|
|
110
|
-
|
|
111
|
-
for (const [id, req] of this._inflightRequests.entries()) {
|
|
112
|
-
if (now - req.timestamp > maxAge) {
|
|
113
|
-
this._inflightRequests.delete(id);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Get debug info about in-flight requests
|
|
120
|
-
*/
|
|
121
|
-
getDebugInfo() {
|
|
122
|
-
return {
|
|
123
|
-
activeLoadId: this._activeLoadId,
|
|
124
|
-
inflightRequests: Array.from(this._inflightRequests.entries()).map(([id, req]) => ({
|
|
125
|
-
requestId: id,
|
|
126
|
-
conversationId: req.conversationId,
|
|
127
|
-
timestamp: req.timestamp,
|
|
128
|
-
status: req.status,
|
|
129
|
-
priority: req.priority,
|
|
130
|
-
age: Date.now() - req.timestamp
|
|
131
|
-
}))
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (typeof window !== 'undefined') {
|
|
137
|
-
window.RequestManager = new RequestManager();
|
|
138
|
-
}
|