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.
- package/dist/agent/detect/classify.d.ts +7 -0
- package/dist/agent/detect/classify.d.ts.map +1 -0
- package/dist/agent/detect/classify.js +42 -0
- package/dist/agent/detect/classify.js.map +1 -0
- package/dist/agent/detect/gate.d.ts +2 -2
- package/dist/agent/detect/gate.d.ts.map +1 -1
- package/dist/agent/detect/gate.js +17 -63
- package/dist/agent/detect/gate.js.map +1 -1
- package/dist/agent/detect/manifest.d.ts +3 -2
- package/dist/agent/detect/manifest.d.ts.map +1 -1
- package/dist/agent/detect/manifest.js +36 -63
- package/dist/agent/detect/manifest.js.map +1 -1
- package/dist/agent/detect/manifests/claude-code.json +387 -153
- package/dist/agent/detect/manifests/codex.json +137 -63
- package/dist/agent/detect/manifests/opencode.json +69 -17
- package/dist/agent/detect/manifests/qodercli.json +84 -25
- package/dist/agent/detect/region.d.ts +2 -2
- package/dist/agent/detect/region.d.ts.map +1 -1
- package/dist/agent/detect/region.js +122 -101
- package/dist/agent/detect/region.js.map +1 -1
- package/dist/agent/manager.d.ts +1 -0
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +85 -57
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/tmux-probe-poller.d.ts +0 -1
- package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
- package/dist/agent/tmux-probe-poller.js +11 -34
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.d.ts +3 -14
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +35 -235
- package/dist/agent/tmux.js.map +1 -1
- package/package.json +1 -1
package/dist/agent/manager.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
3000
|
-
|
|
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
|
|
3004
|
-
return
|
|
2993
|
+
console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness ${phase} failed for pane ${paneId}:`, err);
|
|
2994
|
+
return null;
|
|
3005
2995
|
}
|
|
3006
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
6991
|
-
|
|
6992
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8865
|
-
if (!
|
|
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
|
|
8894
|
-
|
|
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) {
|