baxian 2.0.17 → 2.0.19

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.
Files changed (63) hide show
  1. package/dist/agent/detect/classify.d.ts +7 -0
  2. package/dist/agent/detect/classify.d.ts.map +1 -0
  3. package/dist/agent/detect/classify.js +42 -0
  4. package/dist/agent/detect/classify.js.map +1 -0
  5. package/dist/agent/detect/gate.d.ts +2 -2
  6. package/dist/agent/detect/gate.d.ts.map +1 -1
  7. package/dist/agent/detect/gate.js +17 -63
  8. package/dist/agent/detect/gate.js.map +1 -1
  9. package/dist/agent/detect/manifest.d.ts +3 -2
  10. package/dist/agent/detect/manifest.d.ts.map +1 -1
  11. package/dist/agent/detect/manifest.js +36 -63
  12. package/dist/agent/detect/manifest.js.map +1 -1
  13. package/dist/agent/detect/manifests/claude-code.json +387 -153
  14. package/dist/agent/detect/manifests/codex.json +137 -63
  15. package/dist/agent/detect/manifests/opencode.json +69 -17
  16. package/dist/agent/detect/manifests/qodercli.json +84 -25
  17. package/dist/agent/detect/region.d.ts +2 -2
  18. package/dist/agent/detect/region.d.ts.map +1 -1
  19. package/dist/agent/detect/region.js +122 -101
  20. package/dist/agent/detect/region.js.map +1 -1
  21. package/dist/agent/manager.d.ts +3 -1
  22. package/dist/agent/manager.d.ts.map +1 -1
  23. package/dist/agent/manager.js +100 -91
  24. package/dist/agent/manager.js.map +1 -1
  25. package/dist/agent/prompt.d.ts +1 -0
  26. package/dist/agent/prompt.d.ts.map +1 -1
  27. package/dist/agent/prompt.js +38 -13
  28. package/dist/agent/prompt.js.map +1 -1
  29. package/dist/agent/tmux-probe-poller.d.ts +0 -1
  30. package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
  31. package/dist/agent/tmux-probe-poller.js +11 -34
  32. package/dist/agent/tmux-probe-poller.js.map +1 -1
  33. package/dist/agent/tmux.d.ts +3 -14
  34. package/dist/agent/tmux.d.ts.map +1 -1
  35. package/dist/agent/tmux.js +35 -235
  36. package/dist/agent/tmux.js.map +1 -1
  37. package/dist/event/handlers.d.ts.map +1 -1
  38. package/dist/event/handlers.js +7 -4
  39. package/dist/event/handlers.js.map +1 -1
  40. package/dist/platform/driver-host.d.ts.map +1 -1
  41. package/dist/platform/driver-host.js +8 -6
  42. package/dist/platform/driver-host.js.map +1 -1
  43. package/dist/platform/feedback.d.ts +3 -7
  44. package/dist/platform/feedback.d.ts.map +1 -1
  45. package/dist/platform/feedback.js +11 -18
  46. package/dist/platform/feedback.js.map +1 -1
  47. package/dist/platform/github-driver.d.ts.map +1 -1
  48. package/dist/platform/github-driver.js +1 -3
  49. package/dist/platform/github-driver.js.map +1 -1
  50. package/dist/platform/markers.d.ts +1 -10
  51. package/dist/platform/markers.d.ts.map +1 -1
  52. package/dist/platform/markers.js +11 -30
  53. package/dist/platform/markers.js.map +1 -1
  54. package/dist/platform/verdict-engine.d.ts +3 -16
  55. package/dist/platform/verdict-engine.d.ts.map +1 -1
  56. package/dist/platform/verdict-engine.js +6 -18
  57. package/dist/platform/verdict-engine.js.map +1 -1
  58. package/dist/shared/types.d.ts +0 -1
  59. package/dist/shared/types.d.ts.map +1 -1
  60. package/dist/shared/types.js.map +1 -1
  61. package/dist/state/task-store.js +1 -1
  62. package/dist/state/task-store.js.map +1 -1
  63. package/package.json +1 -1
@@ -12,13 +12,14 @@ import { versionTimeOf } from '../platform/row-schema.js';
12
12
  import { checkOpenPrBinding, checkPrBinding } from '../platform/pr-binding.js';
13
13
  import { collectPendingFeedback, scanCommentSourcesOnce } from '../platform/feedback.js';
14
14
  import { prReviewCacheRevision } from '../platform/pr-conversation-cache.js';
15
- import { deadTokens, recheckPassProvenance } from '../platform/verdict-engine.js';
15
+ import { deadTokens, liveVerdictVeto } from '../platform/verdict-engine.js';
16
16
  import { platformBindingMismatch, taskNeedsPlatformBindingAudit, } from '../platform/startup.js';
17
17
  import { AGENT_STORE_NOOP } from '../state/agent-store.js';
18
18
  import { createRunner, LocalRunner, shellQuote, resolveAgentHost, workdirHostGroupKey, } from './runner.js';
19
19
  import { isTransientNetworkFailure } from './net-exec.js';
20
+ import { classifyScreen, isMenuRule } from './detect/classify.js';
20
21
  import { imageFilename, agentHostPath, writeImageToHost } from './image-input.js';
21
- import { TmuxManager, ReplNotReadyError, detectStartupDialog, detectRuntimeMenu, runtimeBusyCheck, hasRuntimeReadyView, hasReplProcTitle, isShellProcTitle, hasOscTitleWorking, hasOscTitleIdle, screenAllowsTitleIdle, PaneGoneError, } from './tmux.js';
22
+ import { TmuxManager, ReplNotReadyError, detectStartupDialog, hasRuntimeReadyView, hasReplProcTitle, isShellProcTitle, PaneGoneError, } from './tmux.js';
22
23
  import { BranchManager, DirtyWorkdirError, ReviewHeadMismatchError, isAutoDeletableTaskBranch, } from './branch.js';
23
24
  import { RepoStore, createRepoStoreCache } from './repo-store.js';
24
25
  import { PhaseSignalWatcher, } from './phase-signal-watcher.js';
@@ -45,7 +46,6 @@ function samePassProvenance(left, right) {
45
46
  && right !== undefined
46
47
  && left.sourceKey === right.sourceKey
47
48
  && left.id === right.id
48
- && left.bodyDigest === right.bodyDigest
49
49
  && left.token === right.token
50
50
  && left.failToken === right.failToken
51
51
  && left.anchorSha === right.anchorSha;
@@ -1296,7 +1296,7 @@ export class AgentManager {
1296
1296
  fresh.paneId !== state.paneId) {
1297
1297
  continue;
1298
1298
  }
1299
- const onMenu = detectRuntimeMenu(stripped);
1299
+ const onMenu = isMenuRule(cfg.runtime, classifyScreen(cfg.runtime, stripped).matchedRuleId);
1300
1300
  if (onMenu && !pendingMenu) {
1301
1301
  pendingMenu = true;
1302
1302
  await this.emitIntervention(fresh.projectId, agentId, fresh.taskId, {
@@ -2982,36 +2982,49 @@ export class AgentManager {
2982
2982
  }
2983
2983
  async paneHasLiveTurn(tmux, pane, runtime) {
2984
2984
  const paneId = pane.paneId;
2985
- let first;
2986
- let firstTitle;
2987
- try {
2988
- first = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
2989
- firstTitle = await tmux.readPaneTitle(pane);
2990
- }
2991
- catch (err) {
2992
- console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness capture failed for pane ${paneId}:`, err);
2993
- return true;
2994
- }
2995
- for (let i = 1; i < RUNTIME_LIVENESS_SAMPLES; i++) {
2996
- await new Promise(r => setTimeout(r, this.runtimeLivenessProbeMs));
2997
- let frame;
2998
- let title;
2985
+ const sample = async (phase) => {
2999
2986
  try {
3000
- frame = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
3001
- title = await tmux.readPaneTitle(pane);
2987
+ return {
2988
+ frame: await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 }),
2989
+ title: await tmux.readPaneTitle(pane),
2990
+ };
3002
2991
  }
3003
2992
  catch (err) {
3004
- console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness re-capture failed for pane ${paneId}:`, err);
3005
- return true;
2993
+ console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness ${phase} failed for pane ${paneId}:`, err);
2994
+ return null;
3006
2995
  }
3007
- if (title !== firstTitle && hasOscTitleWorking(title))
2996
+ };
2997
+ const first = await sample('capture');
2998
+ if (!first)
2999
+ return true;
3000
+ // 空屏幕 = 只让标题规则说话:标题相对首帧变了且判 working,说明 turn 还活着
3001
+ const titleTurnedWorking = (title) => title !== first.title && classifyScreen(runtime, '', title).state === 'working';
3002
+ let prev = first.frame;
3003
+ let settledSinceChange = true;
3004
+ for (let i = 1; i < RUNTIME_LIVENESS_SAMPLES; i++) {
3005
+ await new Promise(r => setTimeout(r, this.runtimeLivenessProbeMs));
3006
+ const next = await sample('re-capture');
3007
+ if (!next)
3008
3008
  return true;
3009
- if (hasRuntimeReadyView(frame, runtime))
3010
- return false;
3011
- if (frame !== first)
3009
+ if (titleTurnedWorking(next.title))
3012
3010
  return true;
3011
+ if (next.frame === prev) {
3012
+ settledSinceChange = true;
3013
+ continue;
3014
+ }
3015
+ if (!hasRuntimeReadyView(next.frame, runtime))
3016
+ return true;
3017
+ // working→idle 的最新帧已 ready:不能仅凭帧差判 live,需再观察一拍确认它稳定
3018
+ prev = next.frame;
3019
+ settledSinceChange = false;
3013
3020
  }
3014
- return false;
3021
+ if (settledSinceChange)
3022
+ return false;
3023
+ await new Promise(r => setTimeout(r, this.runtimeLivenessProbeMs));
3024
+ const confirm = await sample('confirm capture');
3025
+ if (!confirm)
3026
+ return true;
3027
+ return titleTurnedWorking(confirm.title) || confirm.frame !== prev;
3015
3028
  }
3016
3029
  async paneRunsRuntime(tmux, pane, runtime) {
3017
3030
  const paneId = pane.paneId;
@@ -5419,25 +5432,16 @@ export class AgentManager {
5419
5432
  const { task, driver } = await this.platformTaskAndDriver(taskId);
5420
5433
  if (task.prNumber === undefined)
5421
5434
  return { kind: 'provenance-invalid', reason: 'no bound PR' };
5422
- const provenance = task.passProvenance;
5423
- if (!provenance)
5435
+ if (!task.passProvenance)
5424
5436
  return { kind: 'provenance-invalid', reason: 'no accepted pass provenance' };
5425
5437
  const scans = await this.platformScanComments(driver, task.prNumber);
5426
- const recheck = recheckPassProvenance({
5427
- token: provenance.token,
5428
- failToken: provenance.failToken,
5429
- anchorSha: provenance.anchorSha,
5430
- carrier: { sourceKey: provenance.sourceKey, id: provenance.id, bodyDigest: provenance.bodyDigest },
5431
- }, scans);
5432
- if (!recheck.ok) {
5433
- return recheck.reason === 'source-scan-incomplete'
5434
- ? { kind: 'retryable', reason: recheck.reason }
5435
- : { kind: 'provenance-invalid', reason: `provenance ${recheck.reason}` };
5436
- }
5437
5438
  const pending = collectPendingFeedback(scans);
5438
5439
  if (!pending.allSourcesOk)
5439
5440
  return { kind: 'retryable', reason: 'source scan incomplete' };
5440
- return { kind: 'valid', pendingCount: pending.pending.size };
5441
+ const veto = liveVerdictVeto(task.passProvenance, scans);
5442
+ if (veto)
5443
+ return { kind: 'provenance-invalid', reason: `provenance ${veto}` };
5444
+ return { kind: 'valid', pending: pending.pending };
5441
5445
  }
5442
5446
  async platformPendingFeedback(taskId) {
5443
5447
  const { task, driver } = await this.platformTaskAndDriver(taskId);
@@ -5470,24 +5474,14 @@ export class AgentManager {
5470
5474
  if (provenance.anchorSha.toLowerCase() !== opts.expectedHeadSha.toLowerCase()) {
5471
5475
  throw new PlatformMergeRecheckError('provenance-invalid', `pass provenance anchors ${provenance.anchorSha}, not the merge head ${opts.expectedHeadSha}`);
5472
5476
  }
5473
- const scans = await this.platformScanComments(driver, prNumber);
5474
- const recheck = recheckPassProvenance({
5475
- token: provenance.token,
5476
- failToken: provenance.failToken,
5477
- anchorSha: provenance.anchorSha,
5478
- carrier: { sourceKey: provenance.sourceKey, id: provenance.id, bodyDigest: provenance.bodyDigest },
5479
- }, scans);
5480
- if (!recheck.ok) {
5481
- if (recheck.reason === 'source-scan-incomplete') {
5482
- throw new Error('merge blocked: provenance source-scan-incomplete');
5483
- }
5484
- throw new PlatformMergeRecheckError('provenance-invalid', `provenance ${recheck.reason}`);
5485
- }
5486
- const pending = collectPendingFeedback(scans);
5487
- if (!pending.allSourcesOk)
5477
+ const verified = await this.platformVerifyAcceptedPass(taskId);
5478
+ if (verified.kind === 'retryable')
5488
5479
  throw new Error('merge blocked: pending-feedback scan incomplete');
5489
- if (pending.pending.size > 0) {
5490
- throw new PlatformMergeRecheckError('pending-feedback', `${pending.pending.size} pending feedback revision(s) without ack`);
5480
+ if (verified.kind === 'provenance-invalid') {
5481
+ throw new PlatformMergeRecheckError('provenance-invalid', verified.reason);
5482
+ }
5483
+ if (verified.pending.size > 0) {
5484
+ throw new PlatformMergeRecheckError('pending-feedback', `${verified.pending.size} pending feedback comment(s) without ack`);
5491
5485
  }
5492
5486
  }
5493
5487
  const fresh = await this.taskStore.get(taskId);
@@ -7007,12 +7001,12 @@ export class AgentManager {
7007
7001
  const paneId = pane.paneId;
7008
7002
  const preTitle = await tmux.readPaneTitle(pane);
7009
7003
  const preFrame = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
7010
- const staticBusy = hasOscTitleWorking(preTitle)
7011
- ? (runtimeBusyCheck(preFrame, runtime) || !hasRuntimeReadyView(preFrame, runtime))
7012
- : runtimeBusyCheck(preFrame, runtime);
7013
- if (staticBusy && await this.paneHasLiveTurn(tmux, pane, runtime)) {
7014
- throw new ReplNotReadyError(paneId, runtime, preFrame, `pre-inject busy check: pane ${paneId} is still running a turn; dispatch aborted`);
7004
+ const preDetection = classifyScreen(runtime, preFrame, preTitle);
7005
+ if (preDetection.state === 'pending') {
7006
+ throw new ReplNotReadyError(paneId, runtime, preFrame, `pre-inject check: pane ${paneId} shows a blocker; dispatch aborted`);
7015
7007
  }
7008
+ // working pane 上清稿的 C-c 会打断进行中的 turn
7009
+ const paneWorking = preDetection.state === 'working';
7016
7010
  await revalidate?.();
7017
7011
  if (guardBeforePaste) {
7018
7012
  if (!(await guardBeforePaste())) {
@@ -7024,7 +7018,8 @@ export class AgentManager {
7024
7018
  pasted = await this.withTaskLock(async () => {
7025
7019
  if (!(await guardBeforePaste()))
7026
7020
  return false;
7027
- await tmux.clearComposerDraft(pane);
7021
+ if (!paneWorking)
7022
+ await tmux.clearComposerDraft(pane);
7028
7023
  await tmux.pasteStagedBuffer(paneId, staged.buf);
7029
7024
  return true;
7030
7025
  });
@@ -7039,6 +7034,10 @@ export class AgentManager {
7039
7034
  console.warn(`[AgentManager] staged buffer ${staged.buf} not confirmed dropped after failed paste (missing or unprobeable); failing closed to a composer scrub:`, dropErr);
7040
7035
  }
7041
7036
  if (bufferConsumed) {
7037
+ const message = pasteErr instanceof Error ? pasteErr.message : String(pasteErr);
7038
+ if (paneWorking) {
7039
+ throw new DispatchTerminalError('ack_unknown', `paste outcome unknown on working pane ${paneId}; composer left untouched to keep the live turn intact: ${message}`);
7040
+ }
7042
7041
  try {
7043
7042
  await tmux.clearComposerDraft(pane);
7044
7043
  }
@@ -7059,7 +7058,8 @@ export class AgentManager {
7059
7058
  }
7060
7059
  }
7061
7060
  else {
7062
- await tmux.clearComposerDraft(pane);
7061
+ if (!paneWorking)
7062
+ await tmux.clearComposerDraft(pane);
7063
7063
  await revalidate?.();
7064
7064
  await tmux.injectPrompt(pane, prompt, agentId);
7065
7065
  }
@@ -7076,6 +7076,9 @@ export class AgentManager {
7076
7076
  return true;
7077
7077
  });
7078
7078
  if (!submitted) {
7079
+ if (paneWorking) {
7080
+ throw new Error(`fence-rejected prompt stays in the composer of working pane ${paneId}`);
7081
+ }
7079
7082
  try {
7080
7083
  await tmux.clearComposerDraft(pane);
7081
7084
  }
@@ -7091,11 +7094,18 @@ export class AgentManager {
7091
7094
  }
7092
7095
  }
7093
7096
  catch (preAckErr) {
7097
+ const message = preAckErr instanceof Error ? preAckErr.message : String(preAckErr);
7098
+ // working pane 上 scrub 的 C-c 会打断进行中的 turn:交人工核验,不动 composer
7099
+ if (paneWorking) {
7100
+ throw new DispatchTerminalError('ack_unknown', `pre-ack failure left an unconfirmed composer on working pane ${paneId}: ${message}`);
7101
+ }
7094
7102
  if (await this.clearComposerForReuse(tmux, pane, agentId))
7095
7103
  throw preAckErr;
7096
- const message = preAckErr instanceof Error ? preAckErr.message : String(preAckErr);
7097
7104
  throw new DispatchTerminalError('ack_unknown', `pre-ack failure left an unconfirmed composer on live pane ${paneId}: ${message}`);
7098
7105
  }
7106
+ // 判据必须取自粘贴之前:粘贴后的基线含提示词正文,会自匹配 working 规则
7107
+ if (paneWorking)
7108
+ return { acked: true, composerDelivered: true };
7099
7109
  let staleDuringResend = false;
7100
7110
  try {
7101
7111
  await tmux.waitSubmitAck(pane, baseline, runtime, {
@@ -7142,8 +7152,7 @@ export class AgentManager {
7142
7152
  'baxian intentionally did NOT send C-c — the input may still be queued. ' +
7143
7153
  'Attach via web terminal to verify and resolve.',
7144
7154
  });
7145
- const composerDelivered = !/pane already busy at baseline/.test(message);
7146
- return { acked: false, composerDelivered };
7155
+ return { acked: false, composerDelivered: true };
7147
7156
  }
7148
7157
  }
7149
7158
  async clearComposerForReuse(tmux, pane, agentId) {
@@ -7291,6 +7300,7 @@ export class AgentManager {
7291
7300
  ...(platform ? { platform } : {}),
7292
7301
  ...(signalToken ? { signalToken } : {}),
7293
7302
  ...(imagePaths.length ? { imagePaths } : {}),
7303
+ ...(opts.pendingFeedback?.length ? { pendingFeedback: opts.pendingFeedback } : {}),
7294
7304
  });
7295
7305
  }
7296
7306
  catch (err) {
@@ -8379,7 +8389,7 @@ export class AgentManager {
8379
8389
  if (accepted.kind !== 'valid') {
8380
8390
  throw new ApiError(409, `Cannot restore task ${task.id}: ${accepted.reason}`);
8381
8391
  }
8382
- if (accepted.pendingCount > 0) {
8392
+ if (accepted.pending.size > 0) {
8383
8393
  throw new ApiError(409, `Cannot restore task ${task.id}: the accepted pass has pending feedback`);
8384
8394
  }
8385
8395
  const reviewedPass = task.passProvenance;
@@ -8880,17 +8890,8 @@ export class AgentManager {
8880
8890
  });
8881
8891
  await new Promise(r => setTimeout(r, this.compactIdlePollMs));
8882
8892
  while (true) {
8883
- const current = await tmux.displayMessage(pane, '#{pane_current_command}');
8884
- if (!hasReplProcTitle(current, runtime)) {
8885
- throw new Error(`waitForReplPromptReady: pane ${paneId} pane_current_command=${current.trim()} (not runtime, REPL may have exited)`);
8886
- }
8887
- const cap = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
8888
- const ready = hasRuntimeReadyView(cap, runtime)
8889
- || (screenAllowsTitleIdle(cap, runtime) && hasOscTitleIdle(await tmux.readPaneTitle(pane), runtime));
8890
- if (detectRuntimeMenu(cap) || (!ready && detectStartupDialog(cap, runtime))) {
8891
- throw new Error(`waitForReplPromptReady: pane ${paneId} shows menu/dialog, not a ready REPL prompt`);
8892
- }
8893
- if (!runtimeBusyCheck(cap, runtime)) {
8893
+ const { cap, ready, working } = await this.probeReplPrompt(tmux, pane, runtime);
8894
+ if (!working) {
8894
8895
  if (!ready) {
8895
8896
  throw new Error(`waitForReplPromptReady: pane ${paneId} observed idle but no ready anchor (stale frame?)`);
8896
8897
  }
@@ -8902,6 +8903,23 @@ export class AgentManager {
8902
8903
  await new Promise(r => setTimeout(r, this.compactIdlePollMs));
8903
8904
  }
8904
8905
  }
8906
+ async probeReplPrompt(tmux, pane, runtime) {
8907
+ const paneId = pane.paneId;
8908
+ const current = await tmux.displayMessage(pane, '#{pane_current_command}');
8909
+ if (!hasReplProcTitle(current, runtime)) {
8910
+ throw new Error(`waitForReplPromptReady: pane ${paneId} pane_current_command=${current.trim()} (not runtime, REPL may have exited)`);
8911
+ }
8912
+ const cap = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
8913
+ let detection = classifyScreen(runtime, cap);
8914
+ // 标题规则优先级最高:屏幕判 idle 不作数,必须带标题复核
8915
+ if (detection.state === 'idle')
8916
+ detection = classifyScreen(runtime, cap, await tmux.readPaneTitle(pane));
8917
+ const ready = hasRuntimeReadyView(cap, runtime, detection);
8918
+ if (detection.state === 'pending' || detection.skipStateUpdate || detectStartupDialog(cap, runtime)) {
8919
+ throw new Error(`waitForReplPromptReady: pane ${paneId} shows menu/dialog, not a ready REPL prompt`);
8920
+ }
8921
+ return { cap, ready, working: detection.state === 'working' };
8922
+ }
8905
8923
  async waitForReplPromptStableIdle(tmux, pane, runtime, timeoutMs) {
8906
8924
  const paneId = pane.paneId;
8907
8925
  const samples = Math.max(1, this.readyStableSamples);
@@ -8909,17 +8927,8 @@ export class AgentManager {
8909
8927
  const deadline = Date.now() + timeoutMs + (samples - 1) * spacing;
8910
8928
  let streak = 0;
8911
8929
  while (true) {
8912
- const current = await tmux.displayMessage(pane, '#{pane_current_command}');
8913
- if (!hasReplProcTitle(current, runtime)) {
8914
- throw new Error(`waitForReplPromptReady: pane ${paneId} pane_current_command=${current.trim()} (not runtime, REPL may have exited)`);
8915
- }
8916
- const cap = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
8917
- const ready = hasRuntimeReadyView(cap, runtime)
8918
- || (screenAllowsTitleIdle(cap, runtime) && hasOscTitleIdle(await tmux.readPaneTitle(pane), runtime));
8919
- if (detectRuntimeMenu(cap) || (!ready && detectStartupDialog(cap, runtime))) {
8920
- throw new Error(`waitForReplPromptReady: pane ${paneId} shows menu/dialog, not a ready REPL prompt`);
8921
- }
8922
- streak = ready && !runtimeBusyCheck(cap, runtime) ? streak + 1 : 0;
8930
+ const { cap, ready } = await this.probeReplPrompt(tmux, pane, runtime);
8931
+ streak = ready ? streak + 1 : 0;
8923
8932
  if (streak >= samples)
8924
8933
  return;
8925
8934
  if (Date.now() >= deadline) {