lazy-gravity 0.6.0 → 0.6.2

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.
@@ -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
- // Capture baseline text
512
- try {
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
- // Capture baseline process logs as already-seen keys
521
- try {
522
- const logResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.PROCESS_LOGS));
523
- const logEntries = logResult?.result?.value;
524
- if (Array.isArray(logEntries)) {
525
- this.seenProcessLogKeys = new Set(logEntries
526
- .map((s) => (s || '').replace(/\r/g, '').trim())
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
- catch {
532
- // baseline capture only
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.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED));
542
- const baselineClassified = (0, assistantDomExtractor_1.classifyAssistantSegments)(structuredBaseline?.result?.value);
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.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.CLICK_STOP_BUTTON));
593
- const value = result?.result?.value;
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
- return value ?? { ok: false, error: 'CDP evaluation returned empty' };
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
- buildEvaluateParams(expression) {
697
- const params = {
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
- const contextId = this.cdpService.getPrimaryContextId?.();
703
- if (contextId !== null && contextId !== undefined) {
704
- params.contextId = contextId;
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.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.STOP_BUTTON));
742
- const stopValue = stopResult?.result?.value;
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.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.QUOTA_ERROR));
746
- const quotaDetected = quotaResult?.result?.value === true;
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.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED));
754
- const payload = structuredResult?.result?.value;
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.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_TEXT));
780
- const rawText = textResult?.result?.value;
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.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.PROCESS_LOGS));
791
- const logEntries = logResult?.result?.value;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazy-gravity",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Control Antigravity from anywhere — a local, secure bot (Discord + Telegram) that lets you remotely operate Antigravity on your home PC from your smartphone.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {