chrome-ai-bridge 2.4.0 → 2.5.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.
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { connectViaExtensionRaw } from './extension-raw.js';
4
4
  import { CdpClient } from './cdp-client.js';
5
- import { logConnectionState, logInfo, logError, logWarn } from './mcp-logger.js';
5
+ import { logConnectionState, logInfo, logError, logWarn } from './debug-logger.js';
6
6
  import { DOM_UTILS_CODE } from './utils/index.js';
7
7
  import { getDriver } from './drivers/index.js';
8
8
  import { NetworkInterceptor } from './network-interceptor.js';
@@ -49,18 +49,29 @@ function setClientForAgent(kind, client, relay) {
49
49
  conn.geminiRelay = relay;
50
50
  }
51
51
  }
52
- const CONNECT_REUSE_TIMEOUT_MS = Number(process.env.MCP_CONNECT_REUSE_TIMEOUT_MS || '12000');
53
- const CONNECT_NEWTAB_TIMEOUT_MS = Number(process.env.MCP_CONNECT_NEWTAB_TIMEOUT_MS || '20000');
54
- const MCP_TOOL_BUDGET_MS = Number(process.env.CAI_MCP_TOOL_BUDGET_MS || '50000');
55
- const RESPONSE_WAIT_MAX_MS = Number(process.env.CAI_RESPONSE_WAIT_MAX_MS || '40000');
56
- const BUDGET_RESERVE_MS = Number(process.env.CAI_MCP_BUDGET_RESERVE_MS || '3000');
57
- function getRemainingBudgetMs(startMs) {
58
- return MCP_TOOL_BUDGET_MS - (nowMs() - startMs) - BUDGET_RESERVE_MS;
52
+ // Env var deprecation helpers
53
+ function envWithFallback(newName, oldName, defaultVal) {
54
+ if (process.env[newName])
55
+ return process.env[newName];
56
+ if (process.env[oldName]) {
57
+ console.error(`[deprecation] ${oldName} is deprecated, use ${newName} instead`);
58
+ return process.env[oldName];
59
+ }
60
+ return defaultVal;
59
61
  }
60
- function getResponseWaitBudgetMs(startMs, ceilingMs, stage) {
61
- const remaining = getRemainingBudgetMs(startMs);
62
+ const CONNECT_REUSE_TIMEOUT_MS = Number(envWithFallback('CAI_CONNECT_REUSE_TIMEOUT_MS', 'MCP_CONNECT_REUSE_TIMEOUT_MS', '12000'));
63
+ const CONNECT_NEWTAB_TIMEOUT_MS = Number(envWithFallback('CAI_CONNECT_NEWTAB_TIMEOUT_MS', 'MCP_CONNECT_NEWTAB_TIMEOUT_MS', '20000'));
64
+ const TOOL_BUDGET_MS = Number(envWithFallback('CAI_TOOL_BUDGET_MS', 'CAI_MCP_TOOL_BUDGET_MS', '300000'));
65
+ const RESPONSE_WAIT_MAX_MS = Number(process.env.CAI_RESPONSE_WAIT_MAX_MS || '300000');
66
+ const BUDGET_RESERVE_MS = Number(envWithFallback('CAI_BUDGET_RESERVE_MS', 'CAI_MCP_BUDGET_RESERVE_MS', '3000'));
67
+ function getRemainingBudgetMs(startMs, overrideBudgetMs) {
68
+ return (overrideBudgetMs ?? TOOL_BUDGET_MS) - (nowMs() - startMs) - BUDGET_RESERVE_MS;
69
+ }
70
+ function getResponseWaitBudgetMs(startMs, ceilingMs, stage, overrideBudgetMs) {
71
+ const effectiveBudget = overrideBudgetMs ?? TOOL_BUDGET_MS;
72
+ const remaining = getRemainingBudgetMs(startMs, overrideBudgetMs);
62
73
  if (remaining <= 1000) {
63
- throw new Error(`MCP_TOOL_BUDGET_EXCEEDED: stage=${stage} budgetMs=${MCP_TOOL_BUDGET_MS} reserveMs=${BUDGET_RESERVE_MS}`);
74
+ throw new Error(`TOOL_BUDGET_EXCEEDED: stage=${stage} budgetMs=${effectiveBudget} reserveMs=${BUDGET_RESERVE_MS}`);
64
75
  }
65
76
  return Math.max(1000, Math.min(ceilingMs, remaining));
66
77
  }
@@ -72,6 +83,14 @@ function nowMs() {
72
83
  * 軽量なevaluateコマンドで接続が生きているかチェック
73
84
  */
74
85
  async function isConnectionHealthy(client, kind) {
86
+ // Fast-path: if relay is already disconnected, skip the expensive evaluate call
87
+ if (kind) {
88
+ const relay = getRelayFromAgent(kind);
89
+ if (relay && !relay.isReady()) {
90
+ logConnectionState(kind, 'unhealthy', { elapsed: 0, error: 'relay not ready (fast-path)' });
91
+ return false;
92
+ }
93
+ }
75
94
  const startTime = Date.now();
76
95
  try {
77
96
  // 4秒タイムアウトで簡単なコマンドを実行(2秒では不十分な場合があった)
@@ -228,7 +247,7 @@ export async function resetConnection(kind) {
228
247
  }
229
248
  /**
230
249
  * 全接続をクリーンアップ(プロセス終了時用)
231
- * MCPサーバー終了時にゾンビプロセスを防ぐために使用
250
+ * サーバー終了時にゾンビプロセスを防ぐために使用
232
251
  */
233
252
  export async function cleanupAllConnections() {
234
253
  // Snapshot entries before clearing to avoid mutation during iteration
@@ -505,7 +524,17 @@ async function navigate(client, url) {
505
524
  await client.send('Page.navigate', { url });
506
525
  await client.waitForFunction(`document.readyState === 'complete'`, 30000);
507
526
  }
508
- async function askChatGPTFastInternal(question, debug) {
527
+ /** Strip conversation-specific paths (/c/<id>, /app/<id>) to prevent chat pollution on reuse */
528
+ function getBaseUrl(kind, url) {
529
+ if (kind === 'chatgpt') {
530
+ return url.replace(/\/c\/[a-zA-Z0-9_-]+.*$/, '/');
531
+ }
532
+ if (kind === 'gemini') {
533
+ return url.replace(/\/app\/[a-zA-Z0-9_-]+.*$/, '/');
534
+ }
535
+ return url;
536
+ }
537
+ async function askChatGPTFastInternal(question, debug, budgetMs) {
509
538
  const t0 = nowMs();
510
539
  const timings = {};
511
540
  logInfo('chatgpt', 'askChatGPTFast started', { questionLength: question.length });
@@ -523,16 +552,14 @@ async function askChatGPTFastInternal(question, debug) {
523
552
  // SPA描画安定化のため追加待機
524
553
  await new Promise(r => setTimeout(r, 500));
525
554
  console.error('[ChatGPT] Waited 500ms for SPA rendering');
526
- // 既存チャット(/c/を含むURL)の場合、メッセージが描画されるまで待機
555
+ // 既存チャット(/c/を含むURL)の場合、新規チャットへ遷移
556
+ // 同じ会話に質問を投入すると前回のコンテキストが応答に影響するため
527
557
  const currentUrl = await client.evaluate('location.href');
528
558
  if (currentUrl.includes('/c/')) {
529
- try {
530
- await client.waitForFunction(`document.querySelectorAll('[data-message-author-role="assistant"]').length > 0`, 5000);
531
- console.error('[ChatGPT] Existing chat messages loaded');
532
- }
533
- catch {
534
- console.error('[ChatGPT] No existing messages found, continuing as new chat');
535
- }
559
+ console.error('[ChatGPT] Existing conversation detected, navigating to new chat...');
560
+ await navigate(client, 'https://chatgpt.com/');
561
+ await new Promise(r => setTimeout(r, 500));
562
+ console.error('[ChatGPT] New chat page loaded');
536
563
  }
537
564
  // 入力欄が表示されるまで待機してから取得
538
565
  const tWaitInput = nowMs();
@@ -1008,21 +1035,33 @@ async function askChatGPTFastInternal(question, debug) {
1008
1035
  const tWaitResp = nowMs();
1009
1036
  console.error('[ChatGPT] Waiting for response (using stop button detection)...');
1010
1037
  // 60秒 caller deadline を超えないよう、残り予算内で待機する。
1011
- const maxWaitMs = getResponseWaitBudgetMs(t0, RESPONSE_WAIT_MAX_MS, 'chatgpt-response');
1038
+ const maxWaitMs = getResponseWaitBudgetMs(t0, RESPONSE_WAIT_MAX_MS, 'chatgpt-response', budgetMs);
1012
1039
  const pollIntervalMs = 1000;
1040
+ const IDLE_TIMEOUT_MS = 30000; // ストップボタン消失後、30秒間無活動でタイムアウト
1013
1041
  const startWait = Date.now();
1042
+ let lastActivityAt = Date.now(); // 最後にストップボタンorテキスト成長を検出した時刻
1014
1043
  let lastLoggedState = '';
1015
1044
  let sawStopButton = false; // 生成中状態を検出したかどうか
1016
1045
  let streamingText = ''; // ストリーミング中に取得したテキスト(完了後に折りたたまれる対策)
1017
- while (Date.now() - startWait < maxWaitMs) {
1046
+ let textStableCount = 0; // テキスト長が安定した回数(2-poll confirmation)
1047
+ let lastTextLength = -1; // 前回のテキスト長
1048
+ let stopButtonGoneCount = 0; // ストップボタンが連続不在のポール数(Thinkingフェーズ切替誤判定防止)
1049
+ let textGrowingCount = 0; // テキストが成長中のポール数(成長中はフォールバック抑止)
1050
+ // ストップボタンが見えている間は無制限に待つ(maxWaitMs は絶対安全上限)
1051
+ while (Date.now() - startWait < maxWaitMs && Date.now() - lastActivityAt < IDLE_TIMEOUT_MS) {
1018
1052
  const state = await client.evaluate(`
1019
1053
  (() => {
1020
1054
  ${DOM_UTILS_CODE}
1021
1055
 
1022
1056
  // 停止ボタン検出(フォールバックセレクター付き)
1023
1057
  const stopBtn = document.querySelector('button[data-testid="stop-button"]') ||
1058
+ document.querySelector('button[aria-label="ストリーミングの停止"]') ||
1059
+ document.querySelector('button[aria-label="Stop streaming"]') ||
1024
1060
  document.querySelector('button[aria-label*="停止"]') ||
1025
- document.querySelector('button[aria-label*="Stop"]');
1061
+ document.querySelector('button[aria-label*="Stop"]') ||
1062
+ [...document.querySelectorAll('button')].find(b =>
1063
+ b.querySelector('rect') && (b.textContent || '').trim() === ''
1064
+ );
1026
1065
  const buttons = __collectDeep(['button', '[role="button"]']).nodes;
1027
1066
  // 送信ボタン検出(フォールバックセレクター付き)
1028
1067
  // 注意: 応答完了後は音声ボタンに置き換わり、送信ボタンがDOMから消える
@@ -1060,7 +1099,7 @@ async function askChatGPTFastInternal(question, debug) {
1060
1099
  // 「今すぐ回答」「Skip thinking」ボタンがある場合はThinking進行中
1061
1100
  const hasSkipThinkingButton = bodyText.includes('今すぐ回答') ||
1062
1101
  bodyText.includes('Skip thinking');
1063
- const isStillGenerating = (hasGeneratingText && !hasThinkingComplete) || hasSkipThinkingButton;
1102
+ const isStillGenerating = Boolean(stopBtn) || (hasGeneratingText && !hasThinkingComplete) || hasSkipThinkingButton;
1064
1103
 
1065
1104
  // 最後のアシスタントメッセージに実際のテキストがあるかチェック
1066
1105
  // 旧UIセレクター + 新UI(article)の両方を試す
@@ -1273,14 +1312,16 @@ async function askChatGPTFastInternal(question, debug) {
1273
1312
  })()
1274
1313
  `);
1275
1314
  // stopボタンを検出したらフラグを立てる(生成が始まった証拠)
1315
+ // ストップボタンが見えている間はアクティブとみなし、タイムアウトを延長し続ける
1276
1316
  if (state.hasStopButton) {
1277
1317
  sawStopButton = true;
1318
+ lastActivityAt = Date.now();
1278
1319
  }
1279
1320
  // 状態が変化した場合のみログ出力
1280
1321
  const currentState = JSON.stringify(state);
1281
1322
  if (currentState !== lastLoggedState) {
1282
1323
  const elapsed = Math.round((Date.now() - startWait) / 1000);
1283
- console.error(`[ChatGPT] State @${elapsed}s: stop=${state.hasStopButton}, send=${state.sendButtonFound}(disabled=${state.sendButtonDisabled}), assistant=${state.assistantMsgCount}, inputHasText=${state.inputBoxHasText}, sawStop=${sawStopButton}, generating=${state.isStillGenerating}, skipThink=${state.hasSkipThinkingButton}, hasText=${state.hasResponseText}`);
1324
+ console.error(`[ChatGPT] State @${elapsed}s: stop=${state.hasStopButton}, send=${state.sendButtonFound}(disabled=${state.sendButtonDisabled}), assistant=${state.assistantMsgCount}, inputHasText=${state.inputBoxHasText}, sawStop=${sawStopButton}, generating=${state.isStillGenerating}, skipThink=${state.hasSkipThinkingButton}, hasText=${state.hasResponseText}, textGrow=${textGrowingCount}`);
1284
1325
  lastLoggedState = currentState;
1285
1326
  }
1286
1327
  // 応答完了条件(Thinkingモード対応版):
@@ -1288,46 +1329,81 @@ async function askChatGPTFastInternal(question, debug) {
1288
1329
  // 2. AND 入力欄が空
1289
1330
  // 3. AND 新しいアシスタントメッセージが増えた
1290
1331
  // 注: hasResponseText は CDP でテキスト取得できない場合があるため必須条件から外す
1291
- if (sawStopButton && !state.hasStopButton && !state.inputBoxHasText &&
1332
+ // テキスト安定性チェック(全完了条件で共通使用)
1333
+ const currentTextLen = state.debug_lastAssistantInnerTextLen;
1334
+ if (currentTextLen === lastTextLength && currentTextLen > 0) {
1335
+ textStableCount++;
1336
+ textGrowingCount = 0;
1337
+ }
1338
+ else if (currentTextLen > lastTextLength) {
1339
+ textStableCount = 0;
1340
+ textGrowingCount++;
1341
+ lastTextLength = currentTextLen;
1342
+ lastActivityAt = Date.now();
1343
+ }
1344
+ else {
1345
+ textStableCount = 0;
1346
+ textGrowingCount = 0;
1347
+ lastTextLength = currentTextLen;
1348
+ }
1349
+ // ストップボタン不在の連続カウント(Thinkingフェーズ切替時の一瞬消失を除外)
1350
+ if (sawStopButton && !state.hasStopButton) {
1351
+ stopButtonGoneCount++;
1352
+ }
1353
+ else {
1354
+ stopButtonGoneCount = 0;
1355
+ }
1356
+ if (sawStopButton && stopButtonGoneCount >= 3 && !state.inputBoxHasText &&
1292
1357
  state.assistantMsgCount > initialAssistantCount) {
1293
- console.error(`[ChatGPT] Response complete - stop button disappeared, input empty, assistant count increased (${initialAssistantCount} -> ${state.assistantMsgCount})`);
1294
- // ChatGPT 5.2 Thinking: 完了直後にストリーミング中のテキストをキャプチャ
1295
- // (完了後は折りたたまれてしまうため、この時点で取得)
1296
- streamingText = await client.evaluate(`
1297
- (() => {
1298
- const msgs = document.querySelectorAll('[data-message-author-role="assistant"]');
1299
- if (msgs.length === 0) return '';
1300
- const last = msgs[msgs.length - 1];
1301
- // .markdown, .result-thinking, または直接テキストを試す
1302
- const md = last.querySelector('.markdown');
1303
- if (md) {
1304
- const t = (md.innerText || md.textContent || '').trim();
1305
- if (t.length > 0) return t;
1306
- }
1307
- const rt = last.querySelector('.result-thinking');
1308
- if (rt) {
1309
- const t = (rt.innerText || rt.textContent || '').trim();
1310
- if (t.length > 0) return t;
1358
+ // 2-poll confirmation: テキスト長が2回連続安定してから完了とする
1359
+ if (textStableCount >= 2) {
1360
+ console.error(`[ChatGPT] Response complete - stop gone for ${stopButtonGoneCount} polls, text stable for ${textStableCount} polls (len=${currentTextLen})`);
1361
+ streamingText = await client.evaluate(`
1362
+ (() => {
1363
+ const msgs = document.querySelectorAll('[data-message-author-role="assistant"]');
1364
+ if (msgs.length === 0) return '';
1365
+ const last = msgs[msgs.length - 1];
1366
+ const md = last.querySelector('.markdown');
1367
+ if (md) {
1368
+ const t = (md.innerText || md.textContent || '').trim();
1369
+ if (t.length > 0) return t;
1370
+ }
1371
+ const rt = last.querySelector('.result-thinking');
1372
+ if (rt) {
1373
+ const t = (rt.innerText || rt.textContent || '').trim();
1374
+ if (t.length > 0) return t;
1375
+ }
1376
+ return (last.innerText || last.textContent || '').trim();
1377
+ })()
1378
+ `);
1379
+ break;
1311
1380
  }
1312
- return (last.innerText || last.textContent || '').trim();
1313
- })()
1314
- `);
1315
- break;
1381
+ // まだ安定していない 次のポールまで待機
1382
+ console.error(`[ChatGPT] Stop button gone (${stopButtonGoneCount} polls) but text not stable yet (len=${currentTextLen}, stableCount=${textStableCount})`);
1383
+ await new Promise(r => setTimeout(r, pollIntervalMs));
1384
+ continue;
1316
1385
  }
1317
1386
  // フォールバック: 5秒以上待って、stopボタンなし、入力欄空、新しいアシスタントメッセージが増えた
1318
1387
  // (stopボタンを見逃した場合の救済)
1319
1388
  const elapsed = Date.now() - startWait;
1320
1389
  if (elapsed > 5000 && !state.hasStopButton && !state.inputBoxHasText &&
1321
- state.assistantMsgCount > initialAssistantCount && !state.isStillGenerating) {
1322
- console.error(`[ChatGPT] Response complete - fallback after 5s (no stop button, input empty, assistant count increased ${initialAssistantCount} -> ${state.assistantMsgCount})`);
1323
- break;
1390
+ state.assistantMsgCount > initialAssistantCount && !state.isStillGenerating &&
1391
+ textGrowingCount === 0) {
1392
+ if (textStableCount >= 2) {
1393
+ console.error(`[ChatGPT] Response complete - fallback after 5s, text stable (len=${currentTextLen}, stableCount=${textStableCount})`);
1394
+ break;
1395
+ }
1396
+ console.error(`[ChatGPT] Fallback conditions met but text not stable yet (len=${currentTextLen}, stableCount=${textStableCount})`);
1324
1397
  }
1325
1398
  // Thinkingモード専用フォールバック: stopボタンなしでも、生成完了していれば完了
1326
1399
  // 重要: 「今すぐ回答」ボタンがある間は、まだThinking中なので待機を継続
1327
1400
  if (elapsed > 10000 && !state.isStillGenerating && !state.hasSkipThinkingButton &&
1328
1401
  state.assistantMsgCount > initialAssistantCount && !state.inputBoxHasText) {
1329
- console.error(`[ChatGPT] Response complete - Thinking mode fallback after 10s (generating complete, no skip button)`);
1330
- break;
1402
+ if (textStableCount >= 2) {
1403
+ console.error(`[ChatGPT] Response complete - Thinking mode fallback after 10s, text stable (len=${currentTextLen}, stableCount=${textStableCount})`);
1404
+ break;
1405
+ }
1406
+ console.error(`[ChatGPT] Thinking fallback conditions met but text not stable yet (len=${currentTextLen}, stableCount=${textStableCount})`);
1331
1407
  }
1332
1408
  await new Promise(r => setTimeout(r, pollIntervalMs));
1333
1409
  }
@@ -1361,9 +1437,14 @@ async function askChatGPTFastInternal(question, debug) {
1361
1437
  };
1362
1438
  })()
1363
1439
  `);
1364
- console.error(`[ChatGPT] Timeout - final state: ${JSON.stringify(finalState)}`);
1440
+ const elapsedMs = Date.now() - startWait;
1441
+ const idleMs = Date.now() - lastActivityAt;
1442
+ const reason = idleMs >= IDLE_TIMEOUT_MS
1443
+ ? `idle for ${Math.round(idleMs / 1000)}s (no stop button or text growth)`
1444
+ : `absolute ceiling ${maxWaitMs}ms reached`;
1445
+ console.error(`[ChatGPT] Timeout - ${reason}, elapsed=${Math.round(elapsedMs / 1000)}s, final state: ${JSON.stringify(finalState)}`);
1365
1446
  await resetConnection('chatgpt');
1366
- throw new Error(`Timed out waiting for ChatGPT response (${maxWaitMs}ms). Final state: ${JSON.stringify(finalState)}`);
1447
+ throw new Error(`Timed out waiting for ChatGPT response (${Math.round(elapsedMs / 1000)}s, ${reason}). Final state: ${JSON.stringify(finalState)}`);
1367
1448
  }
1368
1449
  // ChatGPT 5.2 Thinking モデル対応:
1369
1450
  // 回答が「思考」として折りたたまれている場合は展開してからテキストを取得
@@ -1401,7 +1482,7 @@ async function askChatGPTFastInternal(question, debug) {
1401
1482
  // 回答完了後、DOM安定化のための追加待機
1402
1483
  // ChatGPT Thinkingモードでは、停止ボタン消失後も最終回答がレンダリングされるまで遅延がある
1403
1484
  // 回答テキストが存在するまでポーリングで待機
1404
- const maxWaitForText = getResponseWaitBudgetMs(t0, 15000, 'chatgpt-finalize');
1485
+ const maxWaitForText = getResponseWaitBudgetMs(t0, 15000, 'chatgpt-finalize', budgetMs);
1405
1486
  const pollInterval = 200;
1406
1487
  const waitStart = Date.now();
1407
1488
  let hasResponseText = false;
@@ -1941,7 +2022,7 @@ async function askChatGPTFastInternal(question, debug) {
1941
2022
  console.error(`[ChatGPT] Response extracted: ${finalAnswer.slice(0, 100)}...`);
1942
2023
  const finalUrl = await client.evaluate('location.href');
1943
2024
  if (finalUrl && finalUrl.includes('chatgpt.com')) {
1944
- await saveAgentSession('chatgpt', finalUrl);
2025
+ await saveAgentSession('chatgpt', getBaseUrl('chatgpt', finalUrl));
1945
2026
  }
1946
2027
  timings.waitResponseMs = nowMs() - tWaitResp;
1947
2028
  timings.totalMs = nowMs() - t0;
@@ -2053,7 +2134,7 @@ async function askChatGPTFastInternal(question, debug) {
2053
2134
  * Driver経由でChatGPTに質問(実験的)
2054
2135
  * 環境変数 CAI_USE_DRIVERS=1 で有効化
2055
2136
  */
2056
- async function askChatGPTViaDriver(question, debug) {
2137
+ async function askChatGPTViaDriver(question, debug, budgetMs) {
2057
2138
  const t0 = nowMs();
2058
2139
  const timings = {};
2059
2140
  // 接続
@@ -2082,7 +2163,7 @@ async function askChatGPTViaDriver(question, debug) {
2082
2163
  timings.sendMs = nowMs() - tSend;
2083
2164
  // 応答待機
2084
2165
  const tWaitResp = nowMs();
2085
- const driverWaitBudgetMs = getResponseWaitBudgetMs(t0, RESPONSE_WAIT_MAX_MS, 'chatgpt-driver-response');
2166
+ const driverWaitBudgetMs = getResponseWaitBudgetMs(t0, RESPONSE_WAIT_MAX_MS, 'chatgpt-driver-response', budgetMs);
2086
2167
  await driver.waitForResponse({ maxWaitMs: driverWaitBudgetMs });
2087
2168
  timings.waitResponseMs = nowMs() - tWaitResp;
2088
2169
  // 応答抽出
@@ -2096,7 +2177,7 @@ async function askChatGPTViaDriver(question, debug) {
2096
2177
  // セッション保存
2097
2178
  const finalUrl = await driver.getCurrentUrl();
2098
2179
  if (finalUrl.includes('chatgpt.com')) {
2099
- await saveAgentSession('chatgpt', finalUrl);
2180
+ await saveAgentSession('chatgpt', getBaseUrl('chatgpt', finalUrl));
2100
2181
  }
2101
2182
  timings.totalMs = nowMs() - t0;
2102
2183
  // 履歴保存
@@ -2120,7 +2201,7 @@ async function askChatGPTViaDriver(question, debug) {
2120
2201
  /**
2121
2202
  * Driver経由でGeminiに質問(実験的)
2122
2203
  */
2123
- async function askGeminiViaDriver(question, debug) {
2204
+ async function askGeminiViaDriver(question, debug, budgetMs) {
2124
2205
  const t0 = nowMs();
2125
2206
  const timings = {};
2126
2207
  // 接続
@@ -2150,7 +2231,7 @@ async function askGeminiViaDriver(question, debug) {
2150
2231
  timings.sendMs = nowMs() - tSend;
2151
2232
  // 応答待機
2152
2233
  const tWaitResp = nowMs();
2153
- const driverWaitBudgetMs = getResponseWaitBudgetMs(t0, RESPONSE_WAIT_MAX_MS, 'gemini-driver-response');
2234
+ const driverWaitBudgetMs = getResponseWaitBudgetMs(t0, RESPONSE_WAIT_MAX_MS, 'gemini-driver-response', budgetMs);
2154
2235
  await driver.waitForResponse({ maxWaitMs: driverWaitBudgetMs });
2155
2236
  timings.waitResponseMs = nowMs() - tWaitResp;
2156
2237
  // 応答抽出
@@ -2164,7 +2245,7 @@ async function askGeminiViaDriver(question, debug) {
2164
2245
  // セッション保存
2165
2246
  const finalUrl = await driver.getCurrentUrl();
2166
2247
  if (finalUrl.includes('gemini.google.com')) {
2167
- await saveAgentSession('gemini', finalUrl);
2248
+ await saveAgentSession('gemini', getBaseUrl('gemini', finalUrl));
2168
2249
  }
2169
2250
  timings.totalMs = nowMs() - t0;
2170
2251
  // 履歴保存
@@ -2190,24 +2271,24 @@ const USE_DRIVERS = process.env.CAI_USE_DRIVERS === '1';
2190
2271
  /**
2191
2272
  * ChatGPTに質問して回答を取得(後方互換用)
2192
2273
  */
2193
- export async function askChatGPTFast(question, debug) {
2274
+ export async function askChatGPTFast(question, debug, budgetMs) {
2194
2275
  if (USE_DRIVERS) {
2195
- const result = await askChatGPTViaDriver(question, debug);
2276
+ const result = await askChatGPTViaDriver(question, debug, budgetMs);
2196
2277
  return result.answer;
2197
2278
  }
2198
- const result = await askChatGPTFastInternal(question, debug);
2279
+ const result = await askChatGPTFastInternal(question, debug, budgetMs);
2199
2280
  return result.answer;
2200
2281
  }
2201
2282
  /**
2202
2283
  * ChatGPTに質問して回答とタイミング情報を取得
2203
2284
  */
2204
- export async function askChatGPTFastWithTimings(question, debug) {
2285
+ export async function askChatGPTFastWithTimings(question, debug, budgetMs) {
2205
2286
  if (USE_DRIVERS) {
2206
- return askChatGPTViaDriver(question, debug);
2287
+ return askChatGPTViaDriver(question, debug, budgetMs);
2207
2288
  }
2208
- return askChatGPTFastInternal(question, debug);
2289
+ return askChatGPTFastInternal(question, debug, budgetMs);
2209
2290
  }
2210
- async function askGeminiFastInternal(question, debug) {
2291
+ async function askGeminiFastInternal(question, debug, budgetMs) {
2211
2292
  const t0 = nowMs();
2212
2293
  const timings = {};
2213
2294
  const client = await getClient('gemini');
@@ -2234,31 +2315,15 @@ async function askGeminiFastInternal(question, debug) {
2234
2315
  // SPA描画安定化のため追加待機
2235
2316
  await new Promise(r => setTimeout(r, 500));
2236
2317
  console.error('[Gemini] Waited 500ms for SPA rendering');
2237
- // 既存チャット(URLにチャットIDが含まれる)の場合、メッセージが描画されるまで待機
2318
+ // 既存チャット(URLにチャットIDが含まれる)の場合、新規チャットへ遷移
2319
+ // 同じ会話に質問を投入すると前回のコンテキストが応答に影響するため
2238
2320
  const geminiCurrentUrl = await client.evaluate('location.href');
2239
- // 既存チャットのURLパターン: /app/xxxxx (チャットID)
2240
2321
  const isExistingGeminiChat = /\/app\/[a-zA-Z0-9]+/.test(geminiCurrentUrl);
2241
2322
  if (isExistingGeminiChat) {
2242
- try {
2243
- await client.waitForFunction(`document.querySelectorAll('model-response, .model-response').length > 0`, 5000);
2244
- console.error('[Gemini] Existing chat messages loaded');
2245
- // 既存チャットの状態をチェック(停止ボタンがスタックしていないか)
2246
- const stuckCheckResult = await checkGeminiStuckState(client);
2247
- if (stuckCheckResult.isStuck) {
2248
- console.error(`[Gemini] Existing chat appears stuck (stop button detected for ${stuckCheckResult.waitedMs}ms). Clearing session and retrying.`);
2249
- // 協調クリーンアップ(RelayServer + Client + Session を一括リセット)
2250
- await resetConnection('gemini');
2251
- // エラーを投げて、呼び出し元でリトライを促す
2252
- throw new Error('GEMINI_STUCK_EXISTING_CHAT: Previous chat appears stuck (stop button visible). Session cleared, please retry.');
2253
- }
2254
- }
2255
- catch (error) {
2256
- // GEMINI_STUCK_* エラーは再スロー(リトライ用)
2257
- if (error instanceof Error && error.message.includes('GEMINI_STUCK_')) {
2258
- throw error;
2259
- }
2260
- console.error('[Gemini] No existing messages found, continuing as new chat');
2261
- }
2323
+ console.error('[Gemini] Existing conversation detected, navigating to new chat...');
2324
+ await navigate(client, 'https://gemini.google.com/');
2325
+ await new Promise(r => setTimeout(r, 500));
2326
+ console.error('[Gemini] New chat page loaded');
2262
2327
  }
2263
2328
  const tWaitInput = nowMs();
2264
2329
  await client.waitForFunction(`!!document.querySelector('[role="textbox"], div[contenteditable="true"], textarea') || !!document.querySelector('a[href*="accounts.google.com"]')`, 15000);
@@ -2680,7 +2745,7 @@ async function askGeminiFastInternal(question, debug) {
2680
2745
  const tWaitResp = nowMs();
2681
2746
  console.error('[Gemini] Waiting for response completion (polling with diagnostics)...');
2682
2747
  // ChatGPT側と同様のポーリングループで応答完了を検出
2683
- const maxWaitMs = getResponseWaitBudgetMs(t0, RESPONSE_WAIT_MAX_MS, 'gemini-response');
2748
+ const maxWaitMs = getResponseWaitBudgetMs(t0, RESPONSE_WAIT_MAX_MS, 'gemini-response', budgetMs);
2684
2749
  const pollIntervalMs = 1000;
2685
2750
  const startWait = Date.now();
2686
2751
  let lastLoggedState = '';
@@ -2953,7 +3018,7 @@ async function askGeminiFastInternal(question, debug) {
2953
3018
  console.error(`[Gemini] Response extracted: ${normalized.slice(0, 100)}...`);
2954
3019
  const finalUrl = await client.evaluate('location.href');
2955
3020
  if (finalUrl && finalUrl.includes('gemini.google.com')) {
2956
- await saveAgentSession('gemini', finalUrl);
3021
+ await saveAgentSession('gemini', getBaseUrl('gemini', finalUrl));
2957
3022
  }
2958
3023
  timings.waitResponseMs = nowMs() - tWaitResp;
2959
3024
  timings.totalMs = nowMs() - t0;
@@ -3054,22 +3119,22 @@ async function askGeminiFastInternal(question, debug) {
3054
3119
  /**
3055
3120
  * Geminiに質問して回答を取得(後方互換用)
3056
3121
  */
3057
- export async function askGeminiFast(question, debug) {
3122
+ export async function askGeminiFast(question, debug, budgetMs) {
3058
3123
  if (USE_DRIVERS) {
3059
- const result = await askGeminiViaDriver(question, debug);
3124
+ const result = await askGeminiViaDriver(question, debug, budgetMs);
3060
3125
  return result.answer;
3061
3126
  }
3062
- const result = await askGeminiFastInternal(question, debug);
3127
+ const result = await askGeminiFastInternal(question, debug, budgetMs);
3063
3128
  return result.answer;
3064
3129
  }
3065
3130
  /**
3066
3131
  * Geminiに質問して回答とタイミング情報を取得
3067
3132
  */
3068
- export async function askGeminiFastWithTimings(question, debug) {
3133
+ export async function askGeminiFastWithTimings(question, debug, budgetMs) {
3069
3134
  if (USE_DRIVERS) {
3070
- return askGeminiViaDriver(question, debug);
3135
+ return askGeminiViaDriver(question, debug, budgetMs);
3071
3136
  }
3072
- return askGeminiFastInternal(question, debug);
3137
+ return askGeminiFastInternal(question, debug, budgetMs);
3073
3138
  }
3074
3139
  export async function takeCdpSnapshot(kind, options) {
3075
3140
  const result = {
@@ -5,9 +5,9 @@
5
5
  */
6
6
  import fs from 'node:fs';
7
7
  import debug from 'debug';
8
- const mcpDebugNamespace = 'mcp:log';
8
+ const cabDebugNamespace = 'cab:log';
9
9
  const namespacesToEnable = [
10
- mcpDebugNamespace,
10
+ cabDebugNamespace,
11
11
  ...(process.env['DEBUG'] ? [process.env['DEBUG']] : []),
12
12
  ];
13
13
  export function saveLogsToFile(fileName) {
@@ -24,4 +24,4 @@ export function saveLogsToFile(fileName) {
24
24
  });
25
25
  return logFile;
26
26
  }
27
- export const logger = debug(mcpDebugNamespace);
27
+ export const logger = debug(cabDebugNamespace);