lazy-gravity 0.6.1 → 0.7.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/dist/bot/index.js +416 -45
- package/dist/bot/telegramCommands.js +175 -48
- package/dist/bot/telegramJoinCommand.js +170 -0
- package/dist/bot/telegramMessageHandler.js +27 -7
- package/dist/bot/telegramProjectCommand.js +71 -18
- package/dist/bot/telegramStartupTarget.js +54 -0
- package/dist/commands/chatCommandHandler.js +8 -12
- package/dist/commands/joinCommandHandler.js +16 -10
- package/dist/commands/registerSlashCommands.js +13 -1
- package/dist/commands/workspaceCommandHandler.js +22 -7
- package/dist/database/accountPreferenceRepository.js +29 -0
- package/dist/database/channelPreferenceRepository.js +29 -0
- package/dist/database/chatSessionRepository.js +66 -3
- package/dist/database/telegramBindingRepository.js +13 -0
- package/dist/events/interactionCreateHandler.js +194 -13
- package/dist/events/messageCreateHandler.js +103 -7
- package/dist/handlers/accountSelectAction.js +45 -0
- package/dist/handlers/modelButtonAction.js +13 -0
- package/dist/services/cdpBridgeManager.js +23 -18
- package/dist/services/cdpConnectionPool.js +133 -206
- package/dist/services/chatSessionService.js +199 -16
- package/dist/services/responseMonitor.js +235 -48
- package/dist/services/userMessageDetector.js +4 -4
- package/dist/ui/accountUi.js +60 -0
- package/dist/utils/accountUtils.js +36 -0
- package/dist/utils/cdpPorts.js +97 -2
- package/dist/utils/configLoader.js +14 -0
- package/package.json +1 -1
|
@@ -30,6 +30,56 @@ const GET_CHAT_TITLE_SCRIPT = `(() => {
|
|
|
30
30
|
const hasActiveChat = title.length > 0 && title !== 'Agent';
|
|
31
31
|
return { title: title || '(Untitled)', hasActiveChat };
|
|
32
32
|
})()`;
|
|
33
|
+
const GET_SESSION_VIEW_STATE_SCRIPT = `(() => {
|
|
34
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
35
|
+
if (!panel) {
|
|
36
|
+
return {
|
|
37
|
+
panelFound: false,
|
|
38
|
+
title: '',
|
|
39
|
+
hasActiveChat: false,
|
|
40
|
+
hasLoadingIndicator: false,
|
|
41
|
+
hasRenderableContent: false,
|
|
42
|
+
renderablePreview: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const header = panel.querySelector('div[class*="border-b"]');
|
|
46
|
+
const titleEl = header?.querySelector('div[class*="text-ellipsis"]');
|
|
47
|
+
const title = titleEl ? (titleEl.textContent || '').trim() : '';
|
|
48
|
+
const hasActiveChat = title.length > 0 && title !== 'Agent';
|
|
49
|
+
|
|
50
|
+
const bodyCandidates = Array.from(panel.querySelectorAll(
|
|
51
|
+
'[data-message-author-role], [data-message-role], .rendered-markdown, .prose'
|
|
52
|
+
));
|
|
53
|
+
const renderablePreview = bodyCandidates.filter((el) => {
|
|
54
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
55
|
+
const text = (el.textContent || '').trim();
|
|
56
|
+
if (el.offsetParent === null || text.length === 0) return false;
|
|
57
|
+
if (/^\\/\\*\\s*Copied from /i.test(text)) return false;
|
|
58
|
+
return true;
|
|
59
|
+
}).slice(0, 5).map((el) => ({
|
|
60
|
+
tag: el.tagName,
|
|
61
|
+
className: el.className || '',
|
|
62
|
+
text: ((el.textContent || '').trim()).slice(0, 160),
|
|
63
|
+
}));
|
|
64
|
+
const hasRenderableContent = renderablePreview.length > 0;
|
|
65
|
+
|
|
66
|
+
const hasLoadingIndicator = Boolean(
|
|
67
|
+
panel.querySelector(
|
|
68
|
+
'[role="progressbar"], ' +
|
|
69
|
+
'svg[class*="animate-spin"], div[class*="animate-spin"], ' +
|
|
70
|
+
'svg[class*="spinner"], div[class*="spinner"], div[class*="loading"]'
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
panelFound: true,
|
|
76
|
+
title: title || '(Untitled)',
|
|
77
|
+
hasActiveChat,
|
|
78
|
+
hasLoadingIndicator,
|
|
79
|
+
hasRenderableContent,
|
|
80
|
+
renderablePreview,
|
|
81
|
+
};
|
|
82
|
+
})()`;
|
|
33
83
|
/**
|
|
34
84
|
* Script to find the Past Conversations button and return its coordinates.
|
|
35
85
|
* We use coordinates so that the actual click is done via CDP Input.dispatchMouseEvent,
|
|
@@ -432,6 +482,10 @@ class ChatSessionService {
|
|
|
432
482
|
static ACTIVATE_SESSION_MAX_WAIT_MS = 30000;
|
|
433
483
|
static ACTIVATE_SESSION_RETRY_INTERVAL_MS = 800;
|
|
434
484
|
static LIST_SESSIONS_TARGET = 20;
|
|
485
|
+
static HYDRATE_RETRY_DELAY_MS = 700;
|
|
486
|
+
static REOPEN_RETRY_ATTEMPTS = 4;
|
|
487
|
+
static REOPEN_NEW_CHAT_DELAY_MS = 400;
|
|
488
|
+
static REOPEN_HISTORY_DELAY_MS = 1000;
|
|
435
489
|
/**
|
|
436
490
|
* List recent sessions by opening the Past Conversations panel.
|
|
437
491
|
*
|
|
@@ -556,6 +610,25 @@ class ChatSessionService {
|
|
|
556
610
|
type: 'mouseReleased', x, y, button: 'left', clickCount: 1,
|
|
557
611
|
});
|
|
558
612
|
}
|
|
613
|
+
async dispatchNewConversationShortcut(cdpService) {
|
|
614
|
+
const modifiers = process.platform === 'darwin' ? 8 : 10;
|
|
615
|
+
await cdpService.call('Input.dispatchKeyEvent', {
|
|
616
|
+
type: 'keyDown',
|
|
617
|
+
key: 'L',
|
|
618
|
+
code: 'KeyL',
|
|
619
|
+
modifiers,
|
|
620
|
+
windowsVirtualKeyCode: 76,
|
|
621
|
+
nativeVirtualKeyCode: 76,
|
|
622
|
+
});
|
|
623
|
+
await cdpService.call('Input.dispatchKeyEvent', {
|
|
624
|
+
type: 'keyUp',
|
|
625
|
+
key: 'L',
|
|
626
|
+
code: 'KeyL',
|
|
627
|
+
modifiers,
|
|
628
|
+
windowsVirtualKeyCode: 76,
|
|
629
|
+
nativeVirtualKeyCode: 76,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
559
632
|
/**
|
|
560
633
|
* Start a new chat session in the Antigravity UI.
|
|
561
634
|
*
|
|
@@ -597,27 +670,23 @@ class ChatSessionService {
|
|
|
597
670
|
if (!btnState.enabled) {
|
|
598
671
|
return { ok: true };
|
|
599
672
|
}
|
|
600
|
-
//
|
|
601
|
-
await
|
|
602
|
-
|
|
603
|
-
});
|
|
604
|
-
await cdpService.call('Input.dispatchMouseEvent', {
|
|
605
|
-
type: 'mousePressed', x: btnState.x, y: btnState.y,
|
|
606
|
-
button: 'left', clickCount: 1,
|
|
607
|
-
});
|
|
608
|
-
await cdpService.call('Input.dispatchMouseEvent', {
|
|
609
|
-
type: 'mouseReleased', x: btnState.x, y: btnState.y,
|
|
610
|
-
button: 'left', clickCount: 1,
|
|
611
|
-
});
|
|
612
|
-
// Wait for UI to update after click
|
|
673
|
+
// Prefer the keyboard shortcut because some Antigravity builds bind hover tips to the button target.
|
|
674
|
+
await this.dispatchNewConversationShortcut(cdpService);
|
|
675
|
+
// Wait for UI to update after shortcut
|
|
613
676
|
await new Promise(r => setTimeout(r, 1500));
|
|
614
677
|
// Check if button changed to not-allowed (evidence that a new chat was opened)
|
|
615
678
|
const afterState = await this.getNewChatButtonState(cdpService, contexts);
|
|
616
679
|
if (afterState.found && !afterState.enabled) {
|
|
617
680
|
return { ok: true };
|
|
618
681
|
}
|
|
619
|
-
//
|
|
620
|
-
|
|
682
|
+
// Fallback for older builds where the shortcut is not wired.
|
|
683
|
+
await this.cdpMouseClick(cdpService, btnState.x, btnState.y);
|
|
684
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
685
|
+
const afterFallback = await this.getNewChatButtonState(cdpService, contexts);
|
|
686
|
+
if (afterFallback.found && !afterFallback.enabled) {
|
|
687
|
+
return { ok: true };
|
|
688
|
+
}
|
|
689
|
+
return { ok: false, error: 'New conversation shortcut and button click did not change state' };
|
|
621
690
|
}
|
|
622
691
|
catch (error) {
|
|
623
692
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -655,6 +724,104 @@ class ChatSessionService {
|
|
|
655
724
|
return { title: '(Failed to retrieve)', hasActiveChat: false };
|
|
656
725
|
}
|
|
657
726
|
}
|
|
727
|
+
async getCurrentSessionViewState(cdpService) {
|
|
728
|
+
try {
|
|
729
|
+
const contexts = cdpService.getContexts();
|
|
730
|
+
for (const ctx of contexts) {
|
|
731
|
+
try {
|
|
732
|
+
const result = await cdpService.call('Runtime.evaluate', {
|
|
733
|
+
expression: GET_SESSION_VIEW_STATE_SCRIPT,
|
|
734
|
+
returnByValue: true,
|
|
735
|
+
contextId: ctx.id,
|
|
736
|
+
});
|
|
737
|
+
const value = result?.result?.value;
|
|
738
|
+
const hasPanel = value?.panelFound === true;
|
|
739
|
+
const looksUseful = Boolean(hasPanel ||
|
|
740
|
+
value?.hasLoadingIndicator ||
|
|
741
|
+
value?.hasRenderableContent ||
|
|
742
|
+
(typeof value?.title === 'string' && value.title.trim().length > 0));
|
|
743
|
+
if (value && looksUseful) {
|
|
744
|
+
return {
|
|
745
|
+
title: value.title,
|
|
746
|
+
hasActiveChat: value.hasActiveChat ?? false,
|
|
747
|
+
panelFound: value.panelFound ?? false,
|
|
748
|
+
hasLoadingIndicator: value.hasLoadingIndicator ?? false,
|
|
749
|
+
hasRenderableContent: value.hasRenderableContent ?? false,
|
|
750
|
+
renderablePreview: Array.isArray(value.renderablePreview) ? value.renderablePreview : [],
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
catch (_) { /* try next context */ }
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
catch (_) { /* fall through */ }
|
|
758
|
+
return {
|
|
759
|
+
title: '(Failed to retrieve)',
|
|
760
|
+
hasActiveChat: false,
|
|
761
|
+
panelFound: false,
|
|
762
|
+
hasLoadingIndicator: false,
|
|
763
|
+
hasRenderableContent: false,
|
|
764
|
+
renderablePreview: [],
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
async refreshSessionViewIfStuck(cdpService, title) {
|
|
768
|
+
const state = await this.getCurrentSessionViewState(cdpService);
|
|
769
|
+
if (state.title.trim() !== title.trim()) {
|
|
770
|
+
return { ok: false, error: `Current title mismatch before refresh (expected="${title}", actual="${state.title}")` };
|
|
771
|
+
}
|
|
772
|
+
if (!state.hasLoadingIndicator && state.hasRenderableContent) {
|
|
773
|
+
return { ok: true };
|
|
774
|
+
}
|
|
775
|
+
const bounce = await this.recoverSessionViewWithNewConversationBounce(cdpService, title);
|
|
776
|
+
if (bounce.ok) {
|
|
777
|
+
return bounce;
|
|
778
|
+
}
|
|
779
|
+
return {
|
|
780
|
+
ok: false,
|
|
781
|
+
error: `Session "${title}" still appears stuck after new-conversation recovery ` +
|
|
782
|
+
`(${bounce.error || 'unknown'})`,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
async recoverSessionViewWithNewConversationBounce(cdpService, title, options) {
|
|
786
|
+
const state = await this.getCurrentSessionViewState(cdpService);
|
|
787
|
+
if (state.title.trim() === title.trim() && !state.hasLoadingIndicator && state.hasRenderableContent) {
|
|
788
|
+
return { ok: true };
|
|
789
|
+
}
|
|
790
|
+
const maxAttempts = options?.maxAttempts ?? ChatSessionService.REOPEN_RETRY_ATTEMPTS;
|
|
791
|
+
const newChatDelayMs = options?.newChatDelayMs ?? ChatSessionService.REOPEN_NEW_CHAT_DELAY_MS;
|
|
792
|
+
const reopenDelayMs = options?.reopenDelayMs ?? ChatSessionService.REOPEN_HISTORY_DELAY_MS;
|
|
793
|
+
let lastError = `Session "${title}" still appears stuck before recovery`;
|
|
794
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
795
|
+
const newChat = await this.startNewChat(cdpService);
|
|
796
|
+
if (!newChat.ok) {
|
|
797
|
+
lastError =
|
|
798
|
+
`Attempt ${attempt}/${maxAttempts}: failed to open a fresh new conversation before reopening "${title}": ` +
|
|
799
|
+
`${newChat.error || 'unknown'}`;
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
await new Promise((resolve) => setTimeout(resolve, newChatDelayMs));
|
|
803
|
+
const reopened = await this.activateSessionByTitle(cdpService, title, {
|
|
804
|
+
maxWaitMs: 8000,
|
|
805
|
+
retryIntervalMs: 300,
|
|
806
|
+
allowVisibilityWarmupMs: 1000,
|
|
807
|
+
});
|
|
808
|
+
if (!reopened.ok) {
|
|
809
|
+
lastError =
|
|
810
|
+
`Attempt ${attempt}/${maxAttempts}: failed to reopen "${title}" after new conversation: ` +
|
|
811
|
+
`${reopened.error || 'unknown'}`;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
await new Promise((resolve) => setTimeout(resolve, reopenDelayMs));
|
|
815
|
+
const after = await this.getCurrentSessionViewState(cdpService);
|
|
816
|
+
if (after.title.trim() === title.trim() && (!after.hasLoadingIndicator || after.hasRenderableContent)) {
|
|
817
|
+
return { ok: true };
|
|
818
|
+
}
|
|
819
|
+
lastError =
|
|
820
|
+
`Attempt ${attempt}/${maxAttempts}: session "${title}" still appears stuck after reopening ` +
|
|
821
|
+
`(loading=${after.hasLoadingIndicator}, content=${after.hasRenderableContent}, actual="${after.title}")`;
|
|
822
|
+
}
|
|
823
|
+
return { ok: false, error: lastError };
|
|
824
|
+
}
|
|
658
825
|
/**
|
|
659
826
|
* Activate an existing chat by title.
|
|
660
827
|
* Returns ok:false if the target chat cannot be located or verified.
|
|
@@ -669,12 +836,14 @@ class ChatSessionService {
|
|
|
669
836
|
}
|
|
670
837
|
const maxWaitMs = options?.maxWaitMs ?? ChatSessionService.ACTIVATE_SESSION_MAX_WAIT_MS;
|
|
671
838
|
const retryIntervalMs = options?.retryIntervalMs ?? ChatSessionService.ACTIVATE_SESSION_RETRY_INTERVAL_MS;
|
|
839
|
+
const allowVisibilityWarmupMs = options?.allowVisibilityWarmupMs ?? 0;
|
|
672
840
|
let usedPastConversations = false;
|
|
673
841
|
let directResult = { ok: false, error: 'not attempted' };
|
|
674
842
|
let pastResult = null;
|
|
675
843
|
let clicked = false;
|
|
676
|
-
|
|
844
|
+
let startedAt = Date.now();
|
|
677
845
|
let attempts = 0;
|
|
846
|
+
let warmupConsumed = false;
|
|
678
847
|
while (Date.now() - startedAt <= maxWaitMs) {
|
|
679
848
|
attempts += 1;
|
|
680
849
|
directResult = await this.tryActivateByDirectSidePanel(cdpService, title);
|
|
@@ -704,8 +873,21 @@ class ChatSessionService {
|
|
|
704
873
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
705
874
|
const after = await this.getCurrentSessionInfo(cdpService);
|
|
706
875
|
if (after.title.trim() === title.trim()) {
|
|
876
|
+
if (usedPastConversations) {
|
|
877
|
+
await this.closePanelWithEscape(cdpService);
|
|
878
|
+
}
|
|
707
879
|
return { ok: true };
|
|
708
880
|
}
|
|
881
|
+
if (!warmupConsumed && allowVisibilityWarmupMs > 0 && after.title.trim() === 'Agent') {
|
|
882
|
+
warmupConsumed = true;
|
|
883
|
+
startedAt = Date.now();
|
|
884
|
+
await new Promise((resolve) => setTimeout(resolve, allowVisibilityWarmupMs));
|
|
885
|
+
return this.activateSessionByTitle(cdpService, title, {
|
|
886
|
+
maxWaitMs,
|
|
887
|
+
retryIntervalMs,
|
|
888
|
+
allowVisibilityWarmupMs: 0,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
709
891
|
// If direct side-panel activation hit the wrong row, try the explicit Past Conversations flow.
|
|
710
892
|
if (!usedPastConversations) {
|
|
711
893
|
const viaPast = await this.tryActivateByPastConversations(cdpService, title);
|
|
@@ -713,6 +895,7 @@ class ChatSessionService {
|
|
|
713
895
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
714
896
|
const afterPast = await this.getCurrentSessionInfo(cdpService);
|
|
715
897
|
if (afterPast.title.trim() === title.trim()) {
|
|
898
|
+
await this.closePanelWithEscape(cdpService);
|
|
716
899
|
return { ok: true };
|
|
717
900
|
}
|
|
718
901
|
return {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ResponseMonitor = exports.RESPONSE_SELECTORS = void 0;
|
|
4
|
+
exports.captureResponseMonitorBaseline = captureResponseMonitorBaseline;
|
|
4
5
|
const logger_1 = require("../utils/logger");
|
|
5
6
|
const assistantDomExtractor_1 = require("./assistantDomExtractor");
|
|
6
7
|
/** Lean DOM selectors for response extraction */
|
|
@@ -433,6 +434,125 @@ exports.RESPONSE_SELECTORS = {
|
|
|
433
434
|
return diag;
|
|
434
435
|
})()`,
|
|
435
436
|
};
|
|
437
|
+
/**
|
|
438
|
+
* Prefer the active runtime context first, then fall back to any discovered contexts.
|
|
439
|
+
*/
|
|
440
|
+
function getOrderedContextTargets(cdpService) {
|
|
441
|
+
const primaryId = cdpService.getPrimaryContextId?.() ?? null;
|
|
442
|
+
const rawContexts = cdpService.getContexts?.() ?? [];
|
|
443
|
+
const contexts = rawContexts
|
|
444
|
+
.filter((ctx) => ctx && typeof ctx.id === 'number')
|
|
445
|
+
.map((ctx) => ({
|
|
446
|
+
id: ctx.id,
|
|
447
|
+
name: typeof ctx.name === 'string' ? ctx.name : undefined,
|
|
448
|
+
url: typeof ctx.url === 'string' ? ctx.url : undefined,
|
|
449
|
+
}));
|
|
450
|
+
if (contexts.length === 0) {
|
|
451
|
+
return primaryId !== null ? [{ id: primaryId }] : [];
|
|
452
|
+
}
|
|
453
|
+
if (primaryId === null) {
|
|
454
|
+
return contexts;
|
|
455
|
+
}
|
|
456
|
+
const primary = contexts.find((ctx) => ctx.id === primaryId);
|
|
457
|
+
const ordered = primary ? [primary] : [{ id: primaryId }];
|
|
458
|
+
for (const ctx of contexts) {
|
|
459
|
+
if (ctx.id !== primaryId)
|
|
460
|
+
ordered.push(ctx);
|
|
461
|
+
}
|
|
462
|
+
return ordered;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Evaluate an expression across known runtime contexts until one returns an acceptable value.
|
|
466
|
+
*/
|
|
467
|
+
async function evaluateAcrossContexts(cdpService, expression, accept, options) {
|
|
468
|
+
const awaitPromise = options?.awaitPromise ?? true;
|
|
469
|
+
const targets = getOrderedContextTargets(cdpService);
|
|
470
|
+
let firstValue = null;
|
|
471
|
+
let lastError = null;
|
|
472
|
+
if (targets.length === 0) {
|
|
473
|
+
const result = await cdpService.call('Runtime.evaluate', {
|
|
474
|
+
expression,
|
|
475
|
+
returnByValue: true,
|
|
476
|
+
awaitPromise,
|
|
477
|
+
});
|
|
478
|
+
return {
|
|
479
|
+
value: (result?.result?.value ?? null),
|
|
480
|
+
contextId: null,
|
|
481
|
+
contextName: null,
|
|
482
|
+
contextUrl: null,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
for (const target of targets) {
|
|
486
|
+
try {
|
|
487
|
+
const result = await cdpService.call('Runtime.evaluate', {
|
|
488
|
+
expression,
|
|
489
|
+
returnByValue: true,
|
|
490
|
+
awaitPromise,
|
|
491
|
+
contextId: target.id,
|
|
492
|
+
});
|
|
493
|
+
const value = (result?.result?.value ?? null);
|
|
494
|
+
const probed = {
|
|
495
|
+
value,
|
|
496
|
+
contextId: target.id,
|
|
497
|
+
contextName: target.name ?? null,
|
|
498
|
+
contextUrl: target.url ?? null,
|
|
499
|
+
};
|
|
500
|
+
if (!firstValue)
|
|
501
|
+
firstValue = probed;
|
|
502
|
+
if (accept(value)) {
|
|
503
|
+
return probed;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
lastError = error;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (firstValue) {
|
|
511
|
+
return firstValue;
|
|
512
|
+
}
|
|
513
|
+
if (lastError) {
|
|
514
|
+
throw lastError;
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
value: null,
|
|
518
|
+
contextId: null,
|
|
519
|
+
contextName: null,
|
|
520
|
+
contextUrl: null,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Capture the current assistant/output DOM state before sending a new prompt.
|
|
525
|
+
* This avoids races where a fast reply is mistaken for baseline text.
|
|
526
|
+
*/
|
|
527
|
+
async function captureResponseMonitorBaseline(cdpService) {
|
|
528
|
+
let text = null;
|
|
529
|
+
try {
|
|
530
|
+
const textResult = await evaluateAcrossContexts(cdpService, exports.RESPONSE_SELECTORS.RESPONSE_TEXT, (value) => typeof value === 'string' && value.trim().length > 0);
|
|
531
|
+
text = typeof textResult.value === 'string' ? textResult.value.trim() || null : null;
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
text = null;
|
|
535
|
+
}
|
|
536
|
+
const processLogKeys = new Set();
|
|
537
|
+
try {
|
|
538
|
+
const logResult = await evaluateAcrossContexts(cdpService, exports.RESPONSE_SELECTORS.PROCESS_LOGS, (value) => Array.isArray(value) && value.length > 0);
|
|
539
|
+
const logEntries = logResult.value;
|
|
540
|
+
if (Array.isArray(logEntries)) {
|
|
541
|
+
for (const entry of logEntries) {
|
|
542
|
+
const key = String(entry || '').replace(/\r/g, '').trim().slice(0, 200);
|
|
543
|
+
if (key)
|
|
544
|
+
processLogKeys.add(key);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch {
|
|
549
|
+
// best-effort baseline capture
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
text,
|
|
553
|
+
processLogKeys: Array.from(processLogKeys),
|
|
554
|
+
};
|
|
555
|
+
}
|
|
436
556
|
/**
|
|
437
557
|
* Lean AI response monitor.
|
|
438
558
|
*
|
|
@@ -452,6 +572,8 @@ class ResponseMonitor {
|
|
|
452
572
|
onTimeout;
|
|
453
573
|
onPhaseChange;
|
|
454
574
|
onProcessLog;
|
|
575
|
+
initialBaselineText;
|
|
576
|
+
initialSeenProcessLogKeys;
|
|
455
577
|
pollTimer = null;
|
|
456
578
|
isRunning = false;
|
|
457
579
|
lastText = null;
|
|
@@ -462,6 +584,7 @@ class ResponseMonitor {
|
|
|
462
584
|
quotaDetected = false;
|
|
463
585
|
seenProcessLogKeys = new Set();
|
|
464
586
|
structuredDiagLogged = false;
|
|
587
|
+
lastContentContextId = null;
|
|
465
588
|
// CDP disconnect handling (#48)
|
|
466
589
|
isPaused = false;
|
|
467
590
|
onCdpDisconnected = null;
|
|
@@ -480,6 +603,8 @@ class ResponseMonitor {
|
|
|
480
603
|
this.onTimeout = options.onTimeout;
|
|
481
604
|
this.onPhaseChange = options.onPhaseChange;
|
|
482
605
|
this.onProcessLog = options.onProcessLog;
|
|
606
|
+
this.initialBaselineText = options.initialBaselineText;
|
|
607
|
+
this.initialSeenProcessLogKeys = options.initialSeenProcessLogKeys;
|
|
483
608
|
}
|
|
484
609
|
/** Start monitoring */
|
|
485
610
|
async start() {
|
|
@@ -508,28 +633,38 @@ class ResponseMonitor {
|
|
|
508
633
|
this.quotaDetected = false;
|
|
509
634
|
this.seenProcessLogKeys = new Set();
|
|
510
635
|
this.onPhaseChange?.(this.currentPhase, null);
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const baseResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_TEXT));
|
|
514
|
-
const rawValue = baseResult?.result?.value;
|
|
515
|
-
this.baselineText = typeof rawValue === 'string' ? rawValue.trim() || null : null;
|
|
516
|
-
}
|
|
517
|
-
catch {
|
|
518
|
-
this.baselineText = null;
|
|
636
|
+
if (this.initialBaselineText !== undefined) {
|
|
637
|
+
this.baselineText = this.initialBaselineText;
|
|
519
638
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
.filter((s) => s.length > 0)
|
|
528
|
-
.map((s) => s.slice(0, 200)));
|
|
639
|
+
else {
|
|
640
|
+
try {
|
|
641
|
+
const baseResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.RESPONSE_TEXT, (value) => typeof value === 'string' && value.trim().length > 0);
|
|
642
|
+
this.baselineText = typeof baseResult.value === 'string' ? baseResult.value.trim() || null : null;
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
this.baselineText = null;
|
|
529
646
|
}
|
|
530
647
|
}
|
|
531
|
-
|
|
532
|
-
|
|
648
|
+
if (this.initialSeenProcessLogKeys !== undefined) {
|
|
649
|
+
this.seenProcessLogKeys = new Set(this.initialSeenProcessLogKeys
|
|
650
|
+
.map((s) => (s || '').replace(/\r/g, '').trim())
|
|
651
|
+
.filter((s) => s.length > 0)
|
|
652
|
+
.map((s) => s.slice(0, 200)));
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
try {
|
|
656
|
+
const logResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.PROCESS_LOGS, (value) => Array.isArray(value) && value.length > 0);
|
|
657
|
+
const logEntries = logResult.value;
|
|
658
|
+
if (Array.isArray(logEntries)) {
|
|
659
|
+
this.seenProcessLogKeys = new Set(logEntries
|
|
660
|
+
.map((s) => (s || '').replace(/\r/g, '').trim())
|
|
661
|
+
.filter((s) => s.length > 0)
|
|
662
|
+
.map((s) => s.slice(0, 200)));
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch {
|
|
666
|
+
// baseline capture only
|
|
667
|
+
}
|
|
533
668
|
}
|
|
534
669
|
// In structured mode, also capture activity lines from the structured
|
|
535
670
|
// extraction to align the baseline with polling logic. The PROCESS_LOGS
|
|
@@ -538,8 +673,8 @@ class ResponseMonitor {
|
|
|
538
673
|
// entries from previous turns leak into the process log as "new" entries.
|
|
539
674
|
if (this.extractionMode === 'structured') {
|
|
540
675
|
try {
|
|
541
|
-
const structuredBaseline = await this.
|
|
542
|
-
const baselineClassified = (0, assistantDomExtractor_1.classifyAssistantSegments)(structuredBaseline
|
|
676
|
+
const structuredBaseline = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED, (value) => (0, assistantDomExtractor_1.classifyAssistantSegments)(value).diagnostics.source === 'dom-structured');
|
|
677
|
+
const baselineClassified = (0, assistantDomExtractor_1.classifyAssistantSegments)(structuredBaseline.value);
|
|
543
678
|
if (baselineClassified.diagnostics.source === 'dom-structured') {
|
|
544
679
|
for (const line of baselineClassified.activityLines) {
|
|
545
680
|
const key = (line || '').replace(/\r/g, '').trim().slice(0, 200);
|
|
@@ -589,12 +724,15 @@ class ResponseMonitor {
|
|
|
589
724
|
/** Click the stop button to interrupt LLM generation */
|
|
590
725
|
async clickStopButton() {
|
|
591
726
|
try {
|
|
592
|
-
const result = await this.
|
|
593
|
-
const value = result
|
|
727
|
+
const result = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.CLICK_STOP_BUTTON, (value) => !!(value && typeof value === 'object' && value.ok));
|
|
728
|
+
const value = result.value;
|
|
594
729
|
if (this.isRunning) {
|
|
595
730
|
await this.stop();
|
|
596
731
|
}
|
|
597
|
-
|
|
732
|
+
if (value && typeof value.ok === 'boolean') {
|
|
733
|
+
return value;
|
|
734
|
+
}
|
|
735
|
+
return { ok: false, error: 'CDP evaluation returned empty' };
|
|
598
736
|
}
|
|
599
737
|
catch (error) {
|
|
600
738
|
return { ok: false, error: error.message || 'Failed to click stop button' };
|
|
@@ -693,17 +831,70 @@ class ResponseMonitor {
|
|
|
693
831
|
}
|
|
694
832
|
}, this.pollIntervalMs);
|
|
695
833
|
}
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
expression,
|
|
699
|
-
returnByValue: true,
|
|
834
|
+
async evaluateAcrossContexts(expression, accept) {
|
|
835
|
+
const result = await evaluateAcrossContexts(this.cdpService, expression, accept, {
|
|
700
836
|
awaitPromise: true,
|
|
701
|
-
};
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
837
|
+
});
|
|
838
|
+
if (result.contextId !== null
|
|
839
|
+
&& accept(result.value)
|
|
840
|
+
&& this.lastContentContextId !== result.contextId) {
|
|
841
|
+
this.lastContentContextId = result.contextId;
|
|
842
|
+
logger_1.logger.debug(`[ResponseMonitor] Using context ${result.contextId} (${result.contextName ?? 'unknown'} | ${result.contextUrl ?? 'no-url'})`);
|
|
843
|
+
}
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
async logStructuredExtractionDiagnostics(payload) {
|
|
847
|
+
try {
|
|
848
|
+
const dumpResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.DUMP_ALL_TEXTS, (value) => Array.isArray(value) && value.length > 0);
|
|
849
|
+
const dumpValue = Array.isArray(dumpResult.value) ? dumpResult.value : [];
|
|
850
|
+
const accepted = dumpValue.filter((entry) => !entry?.skip).slice(0, 5);
|
|
851
|
+
const skipped = dumpValue.filter((entry) => entry?.skip).slice(0, 5);
|
|
852
|
+
logger_1.logger.warn(`[ResponseMonitor:diag] Structured payload invalid — ${dumpValue.length} candidate(s), ` +
|
|
853
|
+
`${accepted.length} accepted, ${skipped.length} skipped ` +
|
|
854
|
+
`(context=${dumpResult.contextId ?? 'none'})`);
|
|
855
|
+
logger_1.logger.debug('[ResponseMonitor:diag] Candidate details:', JSON.stringify({
|
|
856
|
+
payloadType: payload === null ? 'null' : typeof payload,
|
|
857
|
+
contextId: dumpResult.contextId,
|
|
858
|
+
contextUrl: dumpResult.contextUrl,
|
|
859
|
+
totalCandidates: dumpValue.length,
|
|
860
|
+
accepted: accepted.map((entry) => ({
|
|
861
|
+
sel: entry.sel,
|
|
862
|
+
len: entry.len,
|
|
863
|
+
preview: entry.preview,
|
|
864
|
+
})),
|
|
865
|
+
skipped: skipped.map((entry) => ({
|
|
866
|
+
sel: entry.sel,
|
|
867
|
+
skip: entry.skip,
|
|
868
|
+
len: entry.len,
|
|
869
|
+
preview: entry.preview,
|
|
870
|
+
})),
|
|
871
|
+
}));
|
|
872
|
+
}
|
|
873
|
+
catch (error) {
|
|
874
|
+
logger_1.logger.warn('[ResponseMonitor:diag] DUMP_ALL_TEXTS failed:', error);
|
|
875
|
+
}
|
|
876
|
+
try {
|
|
877
|
+
const domResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.DOM_DIAGNOSTIC, (value) => !!value && typeof value === 'object' && (Array.isArray(value.allTextNodes)
|
|
878
|
+
|| Array.isArray(value.activityNodes)
|
|
879
|
+
|| Array.isArray(value.detailsDump)));
|
|
880
|
+
const domValue = domResult.value;
|
|
881
|
+
logger_1.logger.warn(`[ResponseMonitor:diag] DOM_DIAGNOSTIC — ` +
|
|
882
|
+
`details=${domValue?.detailsCount ?? 0}, ` +
|
|
883
|
+
`activity=${Array.isArray(domValue?.activityNodes) ? domValue.activityNodes.length : 0}, ` +
|
|
884
|
+
`textNodes=${Array.isArray(domValue?.allTextNodes) ? domValue.allTextNodes.length : 0} ` +
|
|
885
|
+
`(context=${domResult.contextId ?? 'none'})`);
|
|
886
|
+
logger_1.logger.debug('[ResponseMonitor:diag] DOM_DIAGNOSTIC details:', JSON.stringify({
|
|
887
|
+
contextId: domResult.contextId,
|
|
888
|
+
contextUrl: domResult.contextUrl,
|
|
889
|
+
detailsCount: domValue?.detailsCount ?? null,
|
|
890
|
+
detailsDump: Array.isArray(domValue?.detailsDump) ? domValue.detailsDump.slice(0, 3) : [],
|
|
891
|
+
activityNodes: Array.isArray(domValue?.activityNodes) ? domValue.activityNodes.slice(0, 5) : [],
|
|
892
|
+
allTextNodes: Array.isArray(domValue?.allTextNodes) ? domValue.allTextNodes.slice(0, 5) : [],
|
|
893
|
+
}));
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
logger_1.logger.warn('[ResponseMonitor:diag] DOM_DIAGNOSTIC failed:', error);
|
|
705
897
|
}
|
|
706
|
-
return params;
|
|
707
898
|
}
|
|
708
899
|
/**
|
|
709
900
|
* Emit new process log entries, deduplicating against previously seen keys.
|
|
@@ -738,20 +929,20 @@ class ResponseMonitor {
|
|
|
738
929
|
async poll() {
|
|
739
930
|
try {
|
|
740
931
|
// 1. Stop button check
|
|
741
|
-
const stopResult = await this.
|
|
742
|
-
const stopValue = stopResult
|
|
932
|
+
const stopResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.STOP_BUTTON, (value) => !!(value && typeof value === 'object' && value.isGenerating));
|
|
933
|
+
const stopValue = stopResult.value;
|
|
743
934
|
const isGenerating = !!(stopValue && typeof stopValue === 'object' && stopValue.isGenerating);
|
|
744
935
|
// 2. Quota error check
|
|
745
|
-
const quotaResult = await this.
|
|
746
|
-
const quotaDetected = quotaResult
|
|
936
|
+
const quotaResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.QUOTA_ERROR, (value) => value === true);
|
|
937
|
+
const quotaDetected = quotaResult.value === true;
|
|
747
938
|
// 3. Text extraction (structured or legacy)
|
|
748
939
|
let currentText = null;
|
|
749
940
|
let structuredHandledLogs = false;
|
|
750
941
|
if (this.extractionMode === 'structured') {
|
|
751
942
|
// Structured: use DOM segment extraction with HTML-to-Markdown
|
|
752
943
|
try {
|
|
753
|
-
const structuredResult = await this.
|
|
754
|
-
const payload = structuredResult
|
|
944
|
+
const structuredResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED, (value) => (0, assistantDomExtractor_1.classifyAssistantSegments)(value).diagnostics.source === 'dom-structured');
|
|
945
|
+
const payload = structuredResult.value;
|
|
755
946
|
const classified = (0, assistantDomExtractor_1.classifyAssistantSegments)(payload);
|
|
756
947
|
if (classified.diagnostics.source === 'dom-structured') {
|
|
757
948
|
currentText = classified.finalOutputText.trim() || null;
|
|
@@ -768,6 +959,7 @@ class ResponseMonitor {
|
|
|
768
959
|
else if (!this.structuredDiagLogged) {
|
|
769
960
|
this.structuredDiagLogged = true;
|
|
770
961
|
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');
|
|
962
|
+
await this.logStructuredExtractionDiagnostics(payload);
|
|
771
963
|
}
|
|
772
964
|
}
|
|
773
965
|
catch (error) {
|
|
@@ -776,19 +968,14 @@ class ResponseMonitor {
|
|
|
776
968
|
}
|
|
777
969
|
// Legacy path (or fallback from structured)
|
|
778
970
|
if (currentText === null) {
|
|
779
|
-
const textResult = await this.
|
|
780
|
-
|
|
781
|
-
const exceptionDetail = textResult?.result?.exceptionDetails ?? textResult?.exceptionDetails;
|
|
782
|
-
if (exceptionDetail) {
|
|
783
|
-
logger_1.logger.warn('[ResponseMonitor:poll] RESPONSE_TEXT threw:', exceptionDetail.text ?? JSON.stringify(exceptionDetail).slice(0, 200));
|
|
784
|
-
}
|
|
785
|
-
currentText = typeof rawText === 'string' ? rawText.trim() || null : null;
|
|
971
|
+
const textResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.RESPONSE_TEXT, (value) => typeof value === 'string' && value.trim().length > 0);
|
|
972
|
+
currentText = typeof textResult.value === 'string' ? textResult.value.trim() || null : null;
|
|
786
973
|
}
|
|
787
974
|
// 4. Process log extraction — always when structured didn't handle it
|
|
788
975
|
if (!structuredHandledLogs) {
|
|
789
976
|
try {
|
|
790
|
-
const logResult = await this.
|
|
791
|
-
const logEntries = logResult
|
|
977
|
+
const logResult = await this.evaluateAcrossContexts(exports.RESPONSE_SELECTORS.PROCESS_LOGS, (value) => Array.isArray(value) && value.length > 0);
|
|
978
|
+
const logEntries = logResult.value;
|
|
792
979
|
if (Array.isArray(logEntries)) {
|
|
793
980
|
this.emitNewProcessLogs(logEntries);
|
|
794
981
|
}
|