lazy-gravity 0.0.4 → 0.2.0
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/README.md +22 -7
- package/dist/bin/cli.js +18 -18
- package/dist/bin/commands/doctor.js +25 -19
- package/dist/bin/commands/start.js +25 -2
- package/dist/bot/index.js +445 -126
- package/dist/commands/joinCommandHandler.js +302 -0
- package/dist/commands/joinDetachCommandHandler.js +285 -0
- package/dist/commands/registerSlashCommands.js +40 -0
- package/dist/commands/workspaceCommandHandler.js +17 -28
- package/dist/database/chatSessionRepository.js +10 -0
- package/dist/database/userPreferenceRepository.js +72 -0
- package/dist/events/interactionCreateHandler.js +338 -30
- package/dist/events/messageCreateHandler.js +161 -47
- package/dist/services/antigravityLauncher.js +4 -3
- package/dist/services/approvalDetector.js +7 -0
- package/dist/services/assistantDomExtractor.js +339 -0
- package/dist/services/cdpBridgeManager.js +323 -39
- package/dist/services/cdpConnectionPool.js +117 -33
- package/dist/services/cdpService.js +149 -53
- package/dist/services/chatSessionService.js +229 -8
- package/dist/services/errorPopupDetector.js +271 -0
- package/dist/services/planningDetector.js +318 -0
- package/dist/services/responseMonitor.js +308 -70
- package/dist/services/retryStore.js +46 -0
- package/dist/services/updateCheckService.js +147 -0
- package/dist/services/userMessageDetector.js +221 -0
- package/dist/ui/buttonUtils.js +33 -0
- package/dist/ui/modeUi.js +11 -1
- package/dist/ui/modelsUi.js +24 -13
- package/dist/ui/outputUi.js +30 -0
- package/dist/ui/projectListUi.js +83 -0
- package/dist/ui/sessionPickerUi.js +48 -0
- package/dist/utils/antigravityPaths.js +94 -0
- package/dist/utils/configLoader.js +18 -0
- package/dist/utils/discordButtonUtils.js +33 -0
- package/dist/utils/discordFormatter.js +149 -16
- package/dist/utils/htmlToDiscordMarkdown.js +184 -0
- package/dist/utils/logBuffer.js +47 -0
- package/dist/utils/logFileTransport.js +147 -0
- package/dist/utils/logger.js +86 -21
- package/dist/utils/pathUtils.js +57 -0
- package/dist/utils/plainTextFormatter.js +70 -0
- package/dist/utils/processLogBuffer.js +4 -0
- package/package.json +4 -4
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ResponseMonitor = exports.RESPONSE_SELECTORS = void 0;
|
|
4
4
|
const logger_1 = require("../utils/logger");
|
|
5
|
+
const assistantDomExtractor_1 = require("./assistantDomExtractor");
|
|
5
6
|
/** Lean DOM selectors for response extraction */
|
|
6
7
|
exports.RESPONSE_SELECTORS = {
|
|
7
8
|
/** Scored selector approach for extracting response text.
|
|
@@ -27,7 +28,7 @@ exports.RESPONSE_SELECTORS = {
|
|
|
27
28
|
const looksLikeActivityLog = (text) => {
|
|
28
29
|
const normalized = (text || '').trim().toLowerCase();
|
|
29
30
|
if (!normalized) return false;
|
|
30
|
-
const activityPattern = /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|analyzed|read|wrote|ran)/i;
|
|
31
|
+
const activityPattern = /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|fetching|connecting|creating|updating|deleting|installing|building|compiling|deploying|checking|scanning|parsing|resolving|downloading|uploading|analyzed|read|wrote|ran|created|updated|deleted|fetched|built|compiled|installed|resolved|downloaded|connected)\\b/i;
|
|
31
32
|
if (activityPattern.test(normalized) && normalized.length <= 220) return true;
|
|
32
33
|
if (/^initiating\\s/i.test(normalized) && normalized.length <= 500) return true;
|
|
33
34
|
if (/^thought for\\s/i.test(normalized) && normalized.length <= 500) return true;
|
|
@@ -43,6 +44,8 @@ exports.RESPONSE_SELECTORS = {
|
|
|
43
44
|
const isInsideExcludedContainer = (node) => {
|
|
44
45
|
if (node.closest('details')) return true;
|
|
45
46
|
if (node.closest('[class*="feedback"], footer')) return true;
|
|
47
|
+
if (node.closest('.notify-user-container')) return true;
|
|
48
|
+
if (node.closest('[role="dialog"]')) return true;
|
|
46
49
|
return false;
|
|
47
50
|
};
|
|
48
51
|
|
|
@@ -57,6 +60,15 @@ exports.RESPONSE_SELECTORS = {
|
|
|
57
60
|
return false;
|
|
58
61
|
};
|
|
59
62
|
|
|
63
|
+
const looksLikeQuotaPopup = (text) => {
|
|
64
|
+
var lower = (text || '').trim().toLowerCase();
|
|
65
|
+
// Inline error: "Error You have exhausted your quota on this model."
|
|
66
|
+
if (lower.includes('exhausted your quota') || lower.includes('exhausted quota')) return true;
|
|
67
|
+
// Popup: quota keyword + dismiss/upgrade button text
|
|
68
|
+
if (!lower.includes('model quota reached') && !lower.includes('quota exceeded') && !lower.includes('rate limit')) return false;
|
|
69
|
+
return lower.includes('dismiss') || lower.includes('upgrade');
|
|
70
|
+
};
|
|
71
|
+
|
|
60
72
|
const combinedSelector = selectors.map((s) => s.sel).join(', ');
|
|
61
73
|
const seen = new Set();
|
|
62
74
|
|
|
@@ -72,6 +84,7 @@ exports.RESPONSE_SELECTORS = {
|
|
|
72
84
|
if (looksLikeActivityLog(text)) continue;
|
|
73
85
|
if (looksLikeFeedbackFooter(text)) continue;
|
|
74
86
|
if (looksLikeToolOutput(text)) continue;
|
|
87
|
+
if (looksLikeQuotaPopup(text)) continue;
|
|
75
88
|
// Prefer recency first: return the newest acceptable node.
|
|
76
89
|
return text;
|
|
77
90
|
}
|
|
@@ -185,7 +198,7 @@ exports.RESPONSE_SELECTORS = {
|
|
|
185
198
|
const looksLikeActivityLog = (text) => {
|
|
186
199
|
const normalized = (text || '').trim().toLowerCase();
|
|
187
200
|
if (!normalized) return false;
|
|
188
|
-
const activityPattern = /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|analyzed|read|wrote|ran)/i;
|
|
201
|
+
const activityPattern = /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|fetching|connecting|creating|updating|deleting|installing|building|compiling|deploying|checking|scanning|parsing|resolving|downloading|uploading|analyzed|read|wrote|ran|created|updated|deleted|fetched|built|compiled|installed|resolved|downloaded|connected)\\b/i;
|
|
189
202
|
if (activityPattern.test(normalized) && normalized.length <= 220) return true;
|
|
190
203
|
if (/^initiating\\s/i.test(normalized) && normalized.length <= 500) return true;
|
|
191
204
|
if (/^thought for\\s/i.test(normalized) && normalized.length <= 500) return true;
|
|
@@ -199,6 +212,8 @@ exports.RESPONSE_SELECTORS = {
|
|
|
199
212
|
const isInsideExcludedContainer = (node) => {
|
|
200
213
|
if (node.closest('details')) return true;
|
|
201
214
|
if (node.closest('[class*="feedback"], footer')) return true;
|
|
215
|
+
if (node.closest('.notify-user-container')) return true;
|
|
216
|
+
if (node.closest('[role="dialog"]')) return true;
|
|
202
217
|
return false;
|
|
203
218
|
};
|
|
204
219
|
const looksLikeToolOutput = (text) => {
|
|
@@ -229,6 +244,12 @@ exports.RESPONSE_SELECTORS = {
|
|
|
229
244
|
else if (looksLikeActivityLog(text)) skip = 'activity-log';
|
|
230
245
|
else if (looksLikeFeedbackFooter(text)) skip = 'feedback-footer';
|
|
231
246
|
else if (looksLikeToolOutput(text)) skip = 'tool-output';
|
|
247
|
+
else {
|
|
248
|
+
var qlower = (text || '').trim().toLowerCase();
|
|
249
|
+
if (qlower.includes('exhausted your quota') || qlower.includes('exhausted quota')) skip = 'quota-popup';
|
|
250
|
+
else if ((qlower.includes('model quota reached') || qlower.includes('quota exceeded') || qlower.includes('rate limit'))
|
|
251
|
+
&& (qlower.includes('dismiss') || qlower.includes('upgrade'))) skip = 'quota-popup';
|
|
252
|
+
}
|
|
232
253
|
const classes = (node.className || '').toString().slice(0, 80);
|
|
233
254
|
results.push({
|
|
234
255
|
sel,
|
|
@@ -263,7 +284,7 @@ exports.RESPONSE_SELECTORS = {
|
|
|
263
284
|
const looksLikeActivityLog = (text) => {
|
|
264
285
|
const normalized = (text || '').trim().toLowerCase();
|
|
265
286
|
if (!normalized) return false;
|
|
266
|
-
const activityPattern = /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|analyzed|read|wrote|ran)/i;
|
|
287
|
+
const activityPattern = /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|fetching|connecting|creating|updating|deleting|installing|building|compiling|deploying|checking|scanning|parsing|resolving|downloading|uploading|analyzed|read|wrote|ran|created|updated|deleted|fetched|built|compiled|installed|resolved|downloaded|connected)\\b/i;
|
|
267
288
|
if (activityPattern.test(normalized) && normalized.length <= 220) return true;
|
|
268
289
|
if (/^initiating\\s/i.test(normalized) && normalized.length <= 500) return true;
|
|
269
290
|
if (/^thought for\\s/i.test(normalized) && normalized.length <= 500) return true;
|
|
@@ -284,6 +305,8 @@ exports.RESPONSE_SELECTORS = {
|
|
|
284
305
|
const isInsideExcludedContainer = (node) => {
|
|
285
306
|
if (node.closest('details')) return true;
|
|
286
307
|
if (node.closest('[class*="feedback"], footer')) return true;
|
|
308
|
+
if (node.closest('.notify-user-container')) return true;
|
|
309
|
+
if (node.closest('[role="dialog"]')) return true;
|
|
287
310
|
return false;
|
|
288
311
|
};
|
|
289
312
|
|
|
@@ -309,11 +332,32 @@ exports.RESPONSE_SELECTORS = {
|
|
|
309
332
|
|
|
310
333
|
return results;
|
|
311
334
|
})()`,
|
|
312
|
-
/** Quota error detection */
|
|
335
|
+
/** Quota error detection — text-based h3 span match first, class-based fallback second */
|
|
313
336
|
QUOTA_ERROR: `(() => {
|
|
314
337
|
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
315
338
|
const scope = panel || document;
|
|
339
|
+
const QUOTA_KEYWORDS = ['model quota reached', 'rate limit', 'quota exceeded', 'exhausted your quota', 'exhausted quota'];
|
|
340
|
+
const isInsideResponse = (node) =>
|
|
341
|
+
node.closest('.rendered-markdown, .prose, pre, code, [data-message-author-role="assistant"], [data-message-role="assistant"], [class*="message-content"]');
|
|
316
342
|
|
|
343
|
+
// Primary: text-based detection via h3 span (Tailwind-only popup)
|
|
344
|
+
const headings = scope.querySelectorAll('h3 span, h3');
|
|
345
|
+
for (const el of headings) {
|
|
346
|
+
if (isInsideResponse(el)) continue;
|
|
347
|
+
const text = (el.textContent || '').trim().toLowerCase();
|
|
348
|
+
if (QUOTA_KEYWORDS.some(kw => text.includes(kw))) return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Inline error: "Error You have exhausted your quota on this model."
|
|
352
|
+
// Appears in process log area as a span inside flex containers
|
|
353
|
+
const inlineSpans = scope.querySelectorAll('span');
|
|
354
|
+
for (const el of inlineSpans) {
|
|
355
|
+
if (isInsideResponse(el)) continue;
|
|
356
|
+
const text = (el.textContent || '').trim().toLowerCase();
|
|
357
|
+
if (text.includes('exhausted your quota') || text.includes('exhausted quota')) return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Fallback: semantic class-based detection
|
|
317
361
|
const errorSelectors = [
|
|
318
362
|
'[role="alert"]',
|
|
319
363
|
'[class*="error"]',
|
|
@@ -327,16 +371,67 @@ exports.RESPONSE_SELECTORS = {
|
|
|
327
371
|
];
|
|
328
372
|
const errorElements = scope.querySelectorAll(errorSelectors.join(', '));
|
|
329
373
|
for (const el of errorElements) {
|
|
330
|
-
if (el
|
|
331
|
-
continue;
|
|
332
|
-
}
|
|
374
|
+
if (isInsideResponse(el)) continue;
|
|
333
375
|
const text = (el.textContent || '').trim().toLowerCase();
|
|
334
|
-
if (
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
376
|
+
if (QUOTA_KEYWORDS.some(kw => text.includes(kw))) return true;
|
|
337
377
|
}
|
|
338
378
|
return false;
|
|
339
379
|
})()`,
|
|
380
|
+
/** Structured DOM extraction — walks DOM to produce typed segment array */
|
|
381
|
+
RESPONSE_STRUCTURED: (0, assistantDomExtractor_1.extractAssistantSegmentsPayloadScript)(),
|
|
382
|
+
/** One-shot DOM diagnostic — dumps DOM structure around activity areas */
|
|
383
|
+
DOM_DIAGNOSTIC: `(() => {
|
|
384
|
+
var panel = document.querySelector('.antigravity-agent-side-panel');
|
|
385
|
+
var scope = panel || document;
|
|
386
|
+
var diag = { detailsCount: 0, detailsDump: [], activityNodes: [], allTextNodes: [] };
|
|
387
|
+
|
|
388
|
+
// 1. Dump all <details> elements
|
|
389
|
+
var details = scope.querySelectorAll('details');
|
|
390
|
+
diag.detailsCount = details.length;
|
|
391
|
+
for (var i = 0; i < Math.min(details.length, 5); i++) {
|
|
392
|
+
diag.detailsDump.push({
|
|
393
|
+
outerHTML: details[i].outerHTML.slice(0, 500),
|
|
394
|
+
summaryText: (details[i].querySelector('summary') || {}).textContent || '(no summary)',
|
|
395
|
+
childCount: details[i].children.length
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 2. Find all text nodes that look like activity
|
|
400
|
+
var selectors = '.rendered-markdown, .leading-relaxed.select-text, .flex.flex-col.gap-y-3, [data-message-author-role="assistant"], [data-message-role="assistant"], [class*="assistant-message"], [class*="message-content"], [class*="markdown-body"], .prose';
|
|
401
|
+
var nodes = scope.querySelectorAll(selectors);
|
|
402
|
+
for (var j = 0; j < nodes.length; j++) {
|
|
403
|
+
var text = (nodes[j].innerText || nodes[j].textContent || '').trim();
|
|
404
|
+
if (!text || text.length < 2) continue;
|
|
405
|
+
diag.allTextNodes.push({
|
|
406
|
+
tag: nodes[j].tagName,
|
|
407
|
+
className: (nodes[j].className || '').toString().slice(0, 100),
|
|
408
|
+
text: text.slice(0, 200),
|
|
409
|
+
insideDetails: !!nodes[j].closest('details'),
|
|
410
|
+
length: text.length
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 3. Broader scan: any element with activity-like text
|
|
415
|
+
var allEls = scope.querySelectorAll('*');
|
|
416
|
+
for (var k = 0; k < allEls.length; k++) {
|
|
417
|
+
var el = allEls[k];
|
|
418
|
+
if (el.children.length > 2) continue; // only leaf-ish nodes
|
|
419
|
+
var t = (el.textContent || '').trim();
|
|
420
|
+
if (!t || t.length < 5 || t.length > 300) continue;
|
|
421
|
+
var lower = t.toLowerCase();
|
|
422
|
+
if (/^(?:analy[sz]|read|writ|run|search|think|process|execut|debug|test)/i.test(lower) || /\\//.test(t)) {
|
|
423
|
+
diag.activityNodes.push({
|
|
424
|
+
tag: el.tagName,
|
|
425
|
+
className: (el.className || '').toString().slice(0, 100),
|
|
426
|
+
text: t.slice(0, 200),
|
|
427
|
+
parentTag: el.parentElement ? el.parentElement.tagName : null,
|
|
428
|
+
parentClass: el.parentElement ? (el.parentElement.className || '').toString().slice(0, 100) : null,
|
|
429
|
+
insideDetails: !!el.closest('details')
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return diag;
|
|
434
|
+
})()`,
|
|
340
435
|
};
|
|
341
436
|
/**
|
|
342
437
|
* Lean AI response monitor.
|
|
@@ -351,13 +446,13 @@ class ResponseMonitor {
|
|
|
351
446
|
pollIntervalMs;
|
|
352
447
|
maxDurationMs;
|
|
353
448
|
stopGoneConfirmCount;
|
|
449
|
+
extractionMode;
|
|
354
450
|
onProgress;
|
|
355
451
|
onComplete;
|
|
356
452
|
onTimeout;
|
|
357
453
|
onPhaseChange;
|
|
358
454
|
onProcessLog;
|
|
359
455
|
pollTimer = null;
|
|
360
|
-
timeoutTimer = null;
|
|
361
456
|
isRunning = false;
|
|
362
457
|
lastText = null;
|
|
363
458
|
baselineText = null;
|
|
@@ -366,11 +461,21 @@ class ResponseMonitor {
|
|
|
366
461
|
stopGoneCount = 0;
|
|
367
462
|
quotaDetected = false;
|
|
368
463
|
seenProcessLogKeys = new Set();
|
|
464
|
+
structuredDiagLogged = false;
|
|
465
|
+
// CDP disconnect handling (#48)
|
|
466
|
+
isPaused = false;
|
|
467
|
+
onCdpDisconnected = null;
|
|
468
|
+
onCdpReconnected = null;
|
|
469
|
+
onCdpReconnectFailed = null;
|
|
470
|
+
// Activity-based timeout (#49)
|
|
471
|
+
lastActivityTime = 0;
|
|
369
472
|
constructor(options) {
|
|
370
473
|
this.cdpService = options.cdpService;
|
|
371
474
|
this.pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
372
475
|
this.maxDurationMs = options.maxDurationMs ?? 300000;
|
|
373
476
|
this.stopGoneConfirmCount = options.stopGoneConfirmCount ?? 3;
|
|
477
|
+
this.extractionMode = options.extractionMode
|
|
478
|
+
?? (process.env.EXTRACTION_MODE === 'legacy' ? 'legacy' : 'structured');
|
|
374
479
|
this.onProgress = options.onProgress;
|
|
375
480
|
this.onComplete = options.onComplete;
|
|
376
481
|
this.onTimeout = options.onTimeout;
|
|
@@ -379,18 +484,31 @@ class ResponseMonitor {
|
|
|
379
484
|
}
|
|
380
485
|
/** Start monitoring */
|
|
381
486
|
async start() {
|
|
487
|
+
return this.initMonitoring(false);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Start monitoring in passive mode.
|
|
491
|
+
* Same as start() but with generationStarted=true, so text changes
|
|
492
|
+
* are detected immediately without waiting for the stop button to appear.
|
|
493
|
+
* Used when joining an existing session that may already be generating.
|
|
494
|
+
*/
|
|
495
|
+
async startPassive() {
|
|
496
|
+
return this.initMonitoring(true);
|
|
497
|
+
}
|
|
498
|
+
/** Internal initialization shared between start() and startPassive() */
|
|
499
|
+
async initMonitoring(passive) {
|
|
382
500
|
if (this.isRunning)
|
|
383
501
|
return;
|
|
384
502
|
this.isRunning = true;
|
|
503
|
+
this.isPaused = false;
|
|
385
504
|
this.lastText = null;
|
|
386
505
|
this.baselineText = null;
|
|
387
|
-
this.generationStarted =
|
|
388
|
-
this.currentPhase = 'waiting';
|
|
506
|
+
this.generationStarted = passive;
|
|
507
|
+
this.currentPhase = passive ? 'generating' : 'waiting';
|
|
389
508
|
this.stopGoneCount = 0;
|
|
390
509
|
this.quotaDetected = false;
|
|
391
510
|
this.seenProcessLogKeys = new Set();
|
|
392
|
-
|
|
393
|
-
this.onPhaseChange?.('waiting', null);
|
|
511
|
+
this.onPhaseChange?.(this.currentPhase, null);
|
|
394
512
|
// Capture baseline text
|
|
395
513
|
try {
|
|
396
514
|
const baseResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_TEXT));
|
|
@@ -414,35 +532,44 @@ class ResponseMonitor {
|
|
|
414
532
|
catch {
|
|
415
533
|
// baseline capture only
|
|
416
534
|
}
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
535
|
+
// In structured mode, also capture activity lines from the structured
|
|
536
|
+
// extraction to align the baseline with polling logic. The PROCESS_LOGS
|
|
537
|
+
// script skips <details> content, but structured extraction (Pass 2)
|
|
538
|
+
// explicitly walks <details> elements — without this, tool-call/thinking
|
|
539
|
+
// entries from previous turns leak into the process log as "new" entries.
|
|
540
|
+
if (this.extractionMode === 'structured') {
|
|
541
|
+
try {
|
|
542
|
+
const structuredBaseline = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED));
|
|
543
|
+
const baselineClassified = (0, assistantDomExtractor_1.classifyAssistantSegments)(structuredBaseline?.result?.value);
|
|
544
|
+
if (baselineClassified.diagnostics.source === 'dom-structured') {
|
|
545
|
+
for (const line of baselineClassified.activityLines) {
|
|
546
|
+
const key = (line || '').replace(/\r/g, '').trim().slice(0, 200);
|
|
547
|
+
if (key)
|
|
548
|
+
this.seenProcessLogKeys.add(key);
|
|
549
|
+
}
|
|
428
550
|
}
|
|
429
|
-
}
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// structured baseline is best-effort
|
|
554
|
+
}
|
|
430
555
|
}
|
|
431
|
-
|
|
432
|
-
|
|
556
|
+
// Activity-based timeout: track last activity time instead of fixed timer (#49)
|
|
557
|
+
this.lastActivityTime = Date.now();
|
|
558
|
+
// Register CDP connection event listeners (#48)
|
|
559
|
+
this.registerCdpConnectionListeners();
|
|
560
|
+
const mode = passive ? 'Passive monitoring' : 'Monitoring';
|
|
561
|
+
logger_1.logger.debug(`── ${mode} started | poll=${this.pollIntervalMs}ms inactivityTimeout=${this.maxDurationMs / 1000}s baseline=${this.baselineText?.length ?? 0}ch`);
|
|
433
562
|
this.schedulePoll();
|
|
434
563
|
}
|
|
435
564
|
/** Stop monitoring */
|
|
436
565
|
async stop() {
|
|
437
566
|
this.isRunning = false;
|
|
567
|
+
this.isPaused = false;
|
|
568
|
+
this.unregisterCdpConnectionListeners();
|
|
438
569
|
if (this.pollTimer) {
|
|
439
570
|
clearTimeout(this.pollTimer);
|
|
440
571
|
this.pollTimer = null;
|
|
441
572
|
}
|
|
442
|
-
if (this.timeoutTimer) {
|
|
443
|
-
clearTimeout(this.timeoutTimer);
|
|
444
|
-
this.timeoutTimer = null;
|
|
445
|
-
}
|
|
446
573
|
}
|
|
447
574
|
/** Get current phase */
|
|
448
575
|
getPhase() {
|
|
@@ -494,14 +621,71 @@ class ResponseMonitor {
|
|
|
494
621
|
case 'quotaReached':
|
|
495
622
|
logger_1.logger.warn('Quota Reached');
|
|
496
623
|
break;
|
|
624
|
+
case 'disconnected':
|
|
625
|
+
logger_1.logger.warn(`CDP Disconnected — paused (${len} chars captured)`);
|
|
626
|
+
break;
|
|
497
627
|
default:
|
|
498
628
|
logger_1.logger.phase(`${phase}`);
|
|
499
629
|
}
|
|
500
630
|
this.onPhaseChange?.(phase, text);
|
|
501
631
|
}
|
|
502
632
|
}
|
|
633
|
+
registerCdpConnectionListeners() {
|
|
634
|
+
this.onCdpDisconnected = () => {
|
|
635
|
+
if (!this.isRunning)
|
|
636
|
+
return;
|
|
637
|
+
logger_1.logger.warn('[ResponseMonitor] CDP disconnected — pausing poll');
|
|
638
|
+
this.isPaused = true;
|
|
639
|
+
if (this.pollTimer) {
|
|
640
|
+
clearTimeout(this.pollTimer);
|
|
641
|
+
this.pollTimer = null;
|
|
642
|
+
}
|
|
643
|
+
this.setPhase('disconnected', this.lastText);
|
|
644
|
+
};
|
|
645
|
+
this.onCdpReconnected = () => {
|
|
646
|
+
if (!this.isRunning)
|
|
647
|
+
return;
|
|
648
|
+
logger_1.logger.warn('[ResponseMonitor] CDP reconnected — resuming poll');
|
|
649
|
+
this.isPaused = false;
|
|
650
|
+
this.lastActivityTime = Date.now();
|
|
651
|
+
const resumePhase = this.generationStarted ? 'generating' : 'waiting';
|
|
652
|
+
this.setPhase(resumePhase, this.lastText);
|
|
653
|
+
this.schedulePoll();
|
|
654
|
+
};
|
|
655
|
+
this.onCdpReconnectFailed = async (err) => {
|
|
656
|
+
if (!this.isRunning)
|
|
657
|
+
return;
|
|
658
|
+
logger_1.logger.error('[ResponseMonitor] CDP reconnection failed — stopping monitor:', err.message);
|
|
659
|
+
const lastText = this.lastText ?? '';
|
|
660
|
+
this.setPhase('disconnected', lastText);
|
|
661
|
+
await this.stop();
|
|
662
|
+
try {
|
|
663
|
+
await Promise.resolve(this.onTimeout?.(lastText));
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
logger_1.logger.error('[ResponseMonitor] timeout callback failed:', error);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
this.cdpService.on('disconnected', this.onCdpDisconnected);
|
|
670
|
+
this.cdpService.on('reconnected', this.onCdpReconnected);
|
|
671
|
+
this.cdpService.on('reconnectFailed', this.onCdpReconnectFailed);
|
|
672
|
+
}
|
|
673
|
+
unregisterCdpConnectionListeners() {
|
|
674
|
+
if (this.onCdpDisconnected) {
|
|
675
|
+
this.cdpService.removeListener('disconnected', this.onCdpDisconnected);
|
|
676
|
+
this.onCdpDisconnected = null;
|
|
677
|
+
}
|
|
678
|
+
if (this.onCdpReconnected) {
|
|
679
|
+
this.cdpService.removeListener('reconnected', this.onCdpReconnected);
|
|
680
|
+
this.onCdpReconnected = null;
|
|
681
|
+
}
|
|
682
|
+
if (this.onCdpReconnectFailed) {
|
|
683
|
+
this.cdpService.removeListener('reconnectFailed', this.onCdpReconnectFailed);
|
|
684
|
+
this.onCdpReconnectFailed = null;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
503
687
|
schedulePoll() {
|
|
504
|
-
if (!this.isRunning)
|
|
688
|
+
if (!this.isRunning || this.isPaused)
|
|
505
689
|
return;
|
|
506
690
|
this.pollTimer = setTimeout(async () => {
|
|
507
691
|
await this.poll();
|
|
@@ -523,11 +707,34 @@ class ResponseMonitor {
|
|
|
523
707
|
return params;
|
|
524
708
|
}
|
|
525
709
|
/**
|
|
526
|
-
*
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
710
|
+
* Emit new process log entries, deduplicating against previously seen keys.
|
|
711
|
+
*/
|
|
712
|
+
emitNewProcessLogs(entries) {
|
|
713
|
+
const newEntries = [];
|
|
714
|
+
for (const line of entries) {
|
|
715
|
+
const normalized = (line || '').replace(/\r/g, '').trim();
|
|
716
|
+
if (!normalized)
|
|
717
|
+
continue;
|
|
718
|
+
const key = normalized.slice(0, 200);
|
|
719
|
+
if (this.seenProcessLogKeys.has(key))
|
|
720
|
+
continue;
|
|
721
|
+
this.seenProcessLogKeys.add(key);
|
|
722
|
+
newEntries.push(normalized.slice(0, 300));
|
|
723
|
+
}
|
|
724
|
+
if (newEntries.length > 0) {
|
|
725
|
+
this.lastActivityTime = Date.now();
|
|
726
|
+
try {
|
|
727
|
+
this.onProcessLog?.(newEntries.join('\n\n'));
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
// callback error
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Single poll cycle.
|
|
736
|
+
* - Legacy mode: 4 CDP calls (stop, quota, text, process logs).
|
|
737
|
+
* - Structured mode: 3-4 CDP calls (stop, quota, structured; legacy text on fallback).
|
|
531
738
|
*/
|
|
532
739
|
async poll() {
|
|
533
740
|
try {
|
|
@@ -538,45 +745,62 @@ class ResponseMonitor {
|
|
|
538
745
|
// 2. Quota error check
|
|
539
746
|
const quotaResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.QUOTA_ERROR));
|
|
540
747
|
const quotaDetected = quotaResult?.result?.value === true;
|
|
541
|
-
// 3. Text extraction
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const normalized = (raw || '').replace(/\r/g, '').trim();
|
|
557
|
-
if (!normalized)
|
|
558
|
-
continue;
|
|
559
|
-
const key = normalized.slice(0, 200);
|
|
560
|
-
if (this.seenProcessLogKeys.has(key))
|
|
561
|
-
continue;
|
|
562
|
-
this.seenProcessLogKeys.add(key);
|
|
563
|
-
newEntries.push(normalized.slice(0, 300));
|
|
564
|
-
}
|
|
565
|
-
if (newEntries.length > 0) {
|
|
566
|
-
try {
|
|
567
|
-
this.onProcessLog?.(newEntries.join('\n\n'));
|
|
748
|
+
// 3. Text extraction (structured or legacy)
|
|
749
|
+
let currentText = null;
|
|
750
|
+
let structuredHandledLogs = false;
|
|
751
|
+
if (this.extractionMode === 'structured') {
|
|
752
|
+
// Structured: use DOM segment extraction with HTML-to-Markdown
|
|
753
|
+
try {
|
|
754
|
+
const structuredResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED));
|
|
755
|
+
const payload = structuredResult?.result?.value;
|
|
756
|
+
const classified = (0, assistantDomExtractor_1.classifyAssistantSegments)(payload);
|
|
757
|
+
if (classified.diagnostics.source === 'dom-structured') {
|
|
758
|
+
currentText = classified.finalOutputText.trim() || null;
|
|
759
|
+
structuredHandledLogs = true;
|
|
760
|
+
if (!this.structuredDiagLogged) {
|
|
761
|
+
this.structuredDiagLogged = true;
|
|
762
|
+
logger_1.logger.debug('[ResponseMonitor] Structured extraction OK — segments:', classified.diagnostics.segmentCounts);
|
|
568
763
|
}
|
|
569
|
-
|
|
570
|
-
|
|
764
|
+
// Emit structured activity lines as process logs
|
|
765
|
+
if (classified.activityLines.length > 0) {
|
|
766
|
+
this.emitNewProcessLogs(classified.activityLines);
|
|
571
767
|
}
|
|
572
768
|
}
|
|
769
|
+
else if (!this.structuredDiagLogged) {
|
|
770
|
+
this.structuredDiagLogged = true;
|
|
771
|
+
logger_1.logger.warn('[ResponseMonitor:poll] Structured extraction failed — reason:', classified.diagnostics.fallbackReason ?? 'unknown', '| payload type:', typeof payload, '| payload:', payload === null ? 'null' : payload === undefined ? 'undefined' : 'object');
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
logger_1.logger.warn('[ResponseMonitor:poll] RESPONSE_STRUCTURED failed, falling back to legacy:', error);
|
|
573
776
|
}
|
|
574
777
|
}
|
|
575
|
-
|
|
576
|
-
|
|
778
|
+
// Legacy path (or fallback from structured)
|
|
779
|
+
if (currentText === null) {
|
|
780
|
+
const textResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_TEXT));
|
|
781
|
+
const rawText = textResult?.result?.value;
|
|
782
|
+
const exceptionDetail = textResult?.result?.exceptionDetails ?? textResult?.exceptionDetails;
|
|
783
|
+
if (exceptionDetail) {
|
|
784
|
+
logger_1.logger.warn('[ResponseMonitor:poll] RESPONSE_TEXT threw:', exceptionDetail.text ?? JSON.stringify(exceptionDetail).slice(0, 200));
|
|
785
|
+
}
|
|
786
|
+
currentText = typeof rawText === 'string' ? rawText.trim() || null : null;
|
|
787
|
+
}
|
|
788
|
+
// 4. Process log extraction — always when structured didn't handle it
|
|
789
|
+
if (!structuredHandledLogs) {
|
|
790
|
+
try {
|
|
791
|
+
const logResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.PROCESS_LOGS));
|
|
792
|
+
const logEntries = logResult?.result?.value;
|
|
793
|
+
if (Array.isArray(logEntries)) {
|
|
794
|
+
this.emitNewProcessLogs(logEntries);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
catch {
|
|
798
|
+
// process log extraction is best-effort
|
|
799
|
+
}
|
|
577
800
|
}
|
|
578
801
|
// Handle stop button appearing
|
|
579
802
|
if (isGenerating) {
|
|
803
|
+
this.lastActivityTime = Date.now();
|
|
580
804
|
if (!this.generationStarted) {
|
|
581
805
|
this.generationStarted = true;
|
|
582
806
|
this.setPhase('thinking', null);
|
|
@@ -611,6 +835,7 @@ class ResponseMonitor {
|
|
|
611
835
|
// Text change handling
|
|
612
836
|
const textChanged = effectiveText !== null && effectiveText !== this.lastText;
|
|
613
837
|
if (textChanged) {
|
|
838
|
+
this.lastActivityTime = Date.now();
|
|
614
839
|
this.lastText = effectiveText;
|
|
615
840
|
if (this.currentPhase === 'waiting' || this.currentPhase === 'thinking') {
|
|
616
841
|
this.setPhase('generating', effectiveText);
|
|
@@ -636,6 +861,19 @@ class ResponseMonitor {
|
|
|
636
861
|
return;
|
|
637
862
|
}
|
|
638
863
|
}
|
|
864
|
+
// Activity-based inactivity timeout (#49)
|
|
865
|
+
if (this.maxDurationMs > 0 && Date.now() - this.lastActivityTime >= this.maxDurationMs) {
|
|
866
|
+
const lastText = this.lastText ?? '';
|
|
867
|
+
this.setPhase('timeout', lastText);
|
|
868
|
+
await this.stop();
|
|
869
|
+
try {
|
|
870
|
+
await Promise.resolve(this.onTimeout?.(lastText));
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
logger_1.logger.error('[ResponseMonitor] timeout callback failed:', error);
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
639
877
|
}
|
|
640
878
|
catch (error) {
|
|
641
879
|
logger_1.logger.error('[ResponseMonitor] poll error:', error);
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// Retry store — keeps retry info for the Retry button on errors
|
|
4
|
+
// Extracted to avoid circular dependency between bot/index.ts and
|
|
5
|
+
// interactionCreateHandler.ts.
|
|
6
|
+
// =============================================================================
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.RETRY_BTN_PREFIX = void 0;
|
|
9
|
+
exports.storeRetry = storeRetry;
|
|
10
|
+
exports.getRetryInfo = getRetryInfo;
|
|
11
|
+
exports.deleteRetryInfo = deleteRetryInfo;
|
|
12
|
+
exports.RETRY_BTN_PREFIX = 'retry_prompt_';
|
|
13
|
+
const MAX_RETRY_STORE_SIZE = 100;
|
|
14
|
+
/** TTL for retry entries — matches Discord interaction token lifetime (15 min) */
|
|
15
|
+
const RETRY_TTL_MS = 15 * 60 * 1000;
|
|
16
|
+
const retryStore = new Map();
|
|
17
|
+
/** Prune entries older than RETRY_TTL_MS */
|
|
18
|
+
function pruneExpired() {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const [k, v] of retryStore) {
|
|
21
|
+
if (now - v.createdAt > RETRY_TTL_MS)
|
|
22
|
+
retryStore.delete(k);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function storeRetry(key, info) {
|
|
26
|
+
pruneExpired();
|
|
27
|
+
if (retryStore.size >= MAX_RETRY_STORE_SIZE) {
|
|
28
|
+
const firstKey = retryStore.keys().next().value;
|
|
29
|
+
if (firstKey !== undefined)
|
|
30
|
+
retryStore.delete(firstKey);
|
|
31
|
+
}
|
|
32
|
+
retryStore.set(key, { ...info, createdAt: Date.now() });
|
|
33
|
+
}
|
|
34
|
+
function getRetryInfo(key) {
|
|
35
|
+
const entry = retryStore.get(key);
|
|
36
|
+
if (!entry)
|
|
37
|
+
return undefined;
|
|
38
|
+
if (Date.now() - entry.createdAt > RETRY_TTL_MS) {
|
|
39
|
+
retryStore.delete(key);
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
function deleteRetryInfo(key) {
|
|
45
|
+
retryStore.delete(key);
|
|
46
|
+
}
|