baxian 2.0.18 → 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 (33) 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 +1 -0
  22. package/dist/agent/manager.d.ts.map +1 -1
  23. package/dist/agent/manager.js +85 -57
  24. package/dist/agent/manager.js.map +1 -1
  25. package/dist/agent/tmux-probe-poller.d.ts +0 -1
  26. package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
  27. package/dist/agent/tmux-probe-poller.js +11 -34
  28. package/dist/agent/tmux-probe-poller.js.map +1 -1
  29. package/dist/agent/tmux.d.ts +3 -14
  30. package/dist/agent/tmux.d.ts.map +1 -1
  31. package/dist/agent/tmux.js +35 -235
  32. package/dist/agent/tmux.js.map +1 -1
  33. package/package.json +1 -1
@@ -17,8 +17,9 @@ import { platformBindingMismatch, taskNeedsPlatformBindingAudit, } from '../plat
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';
@@ -1295,7 +1296,7 @@ export class AgentManager {
1295
1296
  fresh.paneId !== state.paneId) {
1296
1297
  continue;
1297
1298
  }
1298
- const onMenu = detectRuntimeMenu(stripped);
1299
+ const onMenu = isMenuRule(cfg.runtime, classifyScreen(cfg.runtime, stripped).matchedRuleId);
1299
1300
  if (onMenu && !pendingMenu) {
1300
1301
  pendingMenu = true;
1301
1302
  await this.emitIntervention(fresh.projectId, agentId, fresh.taskId, {
@@ -2981,36 +2982,49 @@ export class AgentManager {
2981
2982
  }
2982
2983
  async paneHasLiveTurn(tmux, pane, runtime) {
2983
2984
  const paneId = pane.paneId;
2984
- let first;
2985
- let firstTitle;
2986
- try {
2987
- first = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
2988
- firstTitle = await tmux.readPaneTitle(pane);
2989
- }
2990
- catch (err) {
2991
- console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness capture failed for pane ${paneId}:`, err);
2992
- return true;
2993
- }
2994
- for (let i = 1; i < RUNTIME_LIVENESS_SAMPLES; i++) {
2995
- await new Promise(r => setTimeout(r, this.runtimeLivenessProbeMs));
2996
- let frame;
2997
- let title;
2985
+ const sample = async (phase) => {
2998
2986
  try {
2999
- frame = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
3000
- title = await tmux.readPaneTitle(pane);
2987
+ return {
2988
+ frame: await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 }),
2989
+ title: await tmux.readPaneTitle(pane),
2990
+ };
3001
2991
  }
3002
2992
  catch (err) {
3003
- console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness re-capture failed for pane ${paneId}:`, err);
3004
- return true;
2993
+ console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness ${phase} failed for pane ${paneId}:`, err);
2994
+ return null;
3005
2995
  }
3006
- 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)
3007
3008
  return true;
3008
- if (hasRuntimeReadyView(frame, runtime))
3009
- return false;
3010
- if (frame !== first)
3009
+ if (titleTurnedWorking(next.title))
3011
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;
3012
3020
  }
3013
- 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;
3014
3028
  }
3015
3029
  async paneRunsRuntime(tmux, pane, runtime) {
3016
3030
  const paneId = pane.paneId;
@@ -6987,12 +7001,12 @@ export class AgentManager {
6987
7001
  const paneId = pane.paneId;
6988
7002
  const preTitle = await tmux.readPaneTitle(pane);
6989
7003
  const preFrame = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
6990
- const staticBusy = hasOscTitleWorking(preTitle)
6991
- ? (runtimeBusyCheck(preFrame, runtime) || !hasRuntimeReadyView(preFrame, runtime))
6992
- : runtimeBusyCheck(preFrame, runtime);
6993
- if (staticBusy && await this.paneHasLiveTurn(tmux, pane, runtime)) {
6994
- 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`);
6995
7007
  }
7008
+ // working pane 上清稿的 C-c 会打断进行中的 turn
7009
+ const paneWorking = preDetection.state === 'working';
6996
7010
  await revalidate?.();
6997
7011
  if (guardBeforePaste) {
6998
7012
  if (!(await guardBeforePaste())) {
@@ -7004,7 +7018,8 @@ export class AgentManager {
7004
7018
  pasted = await this.withTaskLock(async () => {
7005
7019
  if (!(await guardBeforePaste()))
7006
7020
  return false;
7007
- await tmux.clearComposerDraft(pane);
7021
+ if (!paneWorking)
7022
+ await tmux.clearComposerDraft(pane);
7008
7023
  await tmux.pasteStagedBuffer(paneId, staged.buf);
7009
7024
  return true;
7010
7025
  });
@@ -7019,6 +7034,10 @@ export class AgentManager {
7019
7034
  console.warn(`[AgentManager] staged buffer ${staged.buf} not confirmed dropped after failed paste (missing or unprobeable); failing closed to a composer scrub:`, dropErr);
7020
7035
  }
7021
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
+ }
7022
7041
  try {
7023
7042
  await tmux.clearComposerDraft(pane);
7024
7043
  }
@@ -7039,7 +7058,8 @@ export class AgentManager {
7039
7058
  }
7040
7059
  }
7041
7060
  else {
7042
- await tmux.clearComposerDraft(pane);
7061
+ if (!paneWorking)
7062
+ await tmux.clearComposerDraft(pane);
7043
7063
  await revalidate?.();
7044
7064
  await tmux.injectPrompt(pane, prompt, agentId);
7045
7065
  }
@@ -7056,6 +7076,9 @@ export class AgentManager {
7056
7076
  return true;
7057
7077
  });
7058
7078
  if (!submitted) {
7079
+ if (paneWorking) {
7080
+ throw new Error(`fence-rejected prompt stays in the composer of working pane ${paneId}`);
7081
+ }
7059
7082
  try {
7060
7083
  await tmux.clearComposerDraft(pane);
7061
7084
  }
@@ -7071,11 +7094,18 @@ export class AgentManager {
7071
7094
  }
7072
7095
  }
7073
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
+ }
7074
7102
  if (await this.clearComposerForReuse(tmux, pane, agentId))
7075
7103
  throw preAckErr;
7076
- const message = preAckErr instanceof Error ? preAckErr.message : String(preAckErr);
7077
7104
  throw new DispatchTerminalError('ack_unknown', `pre-ack failure left an unconfirmed composer on live pane ${paneId}: ${message}`);
7078
7105
  }
7106
+ // 判据必须取自粘贴之前:粘贴后的基线含提示词正文,会自匹配 working 规则
7107
+ if (paneWorking)
7108
+ return { acked: true, composerDelivered: true };
7079
7109
  let staleDuringResend = false;
7080
7110
  try {
7081
7111
  await tmux.waitSubmitAck(pane, baseline, runtime, {
@@ -7122,8 +7152,7 @@ export class AgentManager {
7122
7152
  'baxian intentionally did NOT send C-c — the input may still be queued. ' +
7123
7153
  'Attach via web terminal to verify and resolve.',
7124
7154
  });
7125
- const composerDelivered = !/pane already busy at baseline/.test(message);
7126
- return { acked: false, composerDelivered };
7155
+ return { acked: false, composerDelivered: true };
7127
7156
  }
7128
7157
  }
7129
7158
  async clearComposerForReuse(tmux, pane, agentId) {
@@ -8861,17 +8890,8 @@ export class AgentManager {
8861
8890
  });
8862
8891
  await new Promise(r => setTimeout(r, this.compactIdlePollMs));
8863
8892
  while (true) {
8864
- const current = await tmux.displayMessage(pane, '#{pane_current_command}');
8865
- if (!hasReplProcTitle(current, runtime)) {
8866
- throw new Error(`waitForReplPromptReady: pane ${paneId} pane_current_command=${current.trim()} (not runtime, REPL may have exited)`);
8867
- }
8868
- const cap = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
8869
- const ready = hasRuntimeReadyView(cap, runtime)
8870
- || (screenAllowsTitleIdle(cap, runtime) && hasOscTitleIdle(await tmux.readPaneTitle(pane), runtime));
8871
- if (detectRuntimeMenu(cap) || (!ready && detectStartupDialog(cap, runtime))) {
8872
- throw new Error(`waitForReplPromptReady: pane ${paneId} shows menu/dialog, not a ready REPL prompt`);
8873
- }
8874
- if (!runtimeBusyCheck(cap, runtime)) {
8893
+ const { cap, ready, working } = await this.probeReplPrompt(tmux, pane, runtime);
8894
+ if (!working) {
8875
8895
  if (!ready) {
8876
8896
  throw new Error(`waitForReplPromptReady: pane ${paneId} observed idle but no ready anchor (stale frame?)`);
8877
8897
  }
@@ -8883,6 +8903,23 @@ export class AgentManager {
8883
8903
  await new Promise(r => setTimeout(r, this.compactIdlePollMs));
8884
8904
  }
8885
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
+ }
8886
8923
  async waitForReplPromptStableIdle(tmux, pane, runtime, timeoutMs) {
8887
8924
  const paneId = pane.paneId;
8888
8925
  const samples = Math.max(1, this.readyStableSamples);
@@ -8890,17 +8927,8 @@ export class AgentManager {
8890
8927
  const deadline = Date.now() + timeoutMs + (samples - 1) * spacing;
8891
8928
  let streak = 0;
8892
8929
  while (true) {
8893
- const current = await tmux.displayMessage(pane, '#{pane_current_command}');
8894
- if (!hasReplProcTitle(current, runtime)) {
8895
- throw new Error(`waitForReplPromptReady: pane ${paneId} pane_current_command=${current.trim()} (not runtime, REPL may have exited)`);
8896
- }
8897
- const cap = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
8898
- const ready = hasRuntimeReadyView(cap, runtime)
8899
- || (screenAllowsTitleIdle(cap, runtime) && hasOscTitleIdle(await tmux.readPaneTitle(pane), runtime));
8900
- if (detectRuntimeMenu(cap) || (!ready && detectStartupDialog(cap, runtime))) {
8901
- throw new Error(`waitForReplPromptReady: pane ${paneId} shows menu/dialog, not a ready REPL prompt`);
8902
- }
8903
- streak = ready && !runtimeBusyCheck(cap, runtime) ? streak + 1 : 0;
8930
+ const { cap, ready } = await this.probeReplPrompt(tmux, pane, runtime);
8931
+ streak = ready ? streak + 1 : 0;
8904
8932
  if (streak >= samples)
8905
8933
  return;
8906
8934
  if (Date.now() >= deadline) {