baxian 2.0.18 → 2.0.20
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/branch.d.ts.map +1 -1
- package/dist/agent/branch.js +1 -1
- package/dist/agent/branch.js.map +1 -1
- 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 +2 -0
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +147 -60
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/prompt.js +4 -5
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +2 -0
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +41 -6
- package/dist/agent/repo-store.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 +37 -236
- package/dist/agent/tmux.js.map +1 -1
- package/dist/web/assets/{index-BFjlgPUQ.js → index-DzkRM7gR.js} +13 -13
- package/dist/web/index.html +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';
|
|
@@ -180,12 +181,17 @@ export function canDispatchWithBinding(binding) {
|
|
|
180
181
|
const RECOVERABLE_QA_DISPATCH_HOLD_PHASES = new Set([
|
|
181
182
|
'checkout-preparation-failed',
|
|
182
183
|
'dirty-workdir',
|
|
184
|
+
'branch-cleanup-pending',
|
|
183
185
|
]);
|
|
184
186
|
export function isRecoverableQaDispatchHold(binding) {
|
|
185
187
|
return binding?.status === 'awaiting_human'
|
|
186
188
|
&& binding.awaitingPhase != null
|
|
187
189
|
&& RECOVERABLE_QA_DISPATCH_HOLD_PHASES.has(binding.awaitingPhase);
|
|
188
190
|
}
|
|
191
|
+
const GIT_REVIEW_DISPATCH_HOLD_PHASES = new Set([
|
|
192
|
+
...RECOVERABLE_QA_DISPATCH_HOLD_PHASES,
|
|
193
|
+
'dispatch-failed:ack_unknown',
|
|
194
|
+
]);
|
|
189
195
|
const CANCEL_CLEANUP_HOLD_PHASES = new Set(['cancel-clearing', 'cancel-interrupt-failed']);
|
|
190
196
|
function isCancelCleanupHold(binding) {
|
|
191
197
|
return binding?.awaitingPhase != null && CANCEL_CLEANUP_HOLD_PHASES.has(binding.awaitingPhase);
|
|
@@ -1295,7 +1301,7 @@ export class AgentManager {
|
|
|
1295
1301
|
fresh.paneId !== state.paneId) {
|
|
1296
1302
|
continue;
|
|
1297
1303
|
}
|
|
1298
|
-
const onMenu =
|
|
1304
|
+
const onMenu = isMenuRule(cfg.runtime, classifyScreen(cfg.runtime, stripped).matchedRuleId);
|
|
1299
1305
|
if (onMenu && !pendingMenu) {
|
|
1300
1306
|
pendingMenu = true;
|
|
1301
1307
|
await this.emitIntervention(fresh.projectId, agentId, fresh.taskId, {
|
|
@@ -1517,6 +1523,8 @@ export class AgentManager {
|
|
|
1517
1523
|
: null;
|
|
1518
1524
|
const sameTaskLocked = existingToken !== null;
|
|
1519
1525
|
const reentryPhases = new Set(['fix', 'post-approve', 'code']);
|
|
1526
|
+
// fix / post-approve 由 continueSession 投递,没有配对的 clearBootstrapMarker
|
|
1527
|
+
const unfinalizedPhases = new Set(['fix', 'post-approve']);
|
|
1520
1528
|
const sameTaskReentry = state?.taskId === taskId &&
|
|
1521
1529
|
!state.creationToken &&
|
|
1522
1530
|
state.status !== 'awaiting_human' &&
|
|
@@ -1553,7 +1561,7 @@ export class AgentManager {
|
|
|
1553
1561
|
projectId: cfg.projectId,
|
|
1554
1562
|
taskId,
|
|
1555
1563
|
lockToken: token,
|
|
1556
|
-
...(phase
|
|
1564
|
+
...(!unfinalizedPhases.has(phase) ? { bootstrappingTaskId: taskId } : {}),
|
|
1557
1565
|
updatedAt: now,
|
|
1558
1566
|
};
|
|
1559
1567
|
});
|
|
@@ -2981,36 +2989,49 @@ export class AgentManager {
|
|
|
2981
2989
|
}
|
|
2982
2990
|
async paneHasLiveTurn(tmux, pane, runtime) {
|
|
2983
2991
|
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;
|
|
2992
|
+
const sample = async (phase) => {
|
|
2998
2993
|
try {
|
|
2999
|
-
|
|
3000
|
-
|
|
2994
|
+
return {
|
|
2995
|
+
frame: await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 }),
|
|
2996
|
+
title: await tmux.readPaneTitle(pane),
|
|
2997
|
+
};
|
|
3001
2998
|
}
|
|
3002
2999
|
catch (err) {
|
|
3003
|
-
console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness
|
|
3004
|
-
return
|
|
3000
|
+
console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness ${phase} failed for pane ${paneId}:`, err);
|
|
3001
|
+
return null;
|
|
3005
3002
|
}
|
|
3006
|
-
|
|
3003
|
+
};
|
|
3004
|
+
const first = await sample('capture');
|
|
3005
|
+
if (!first)
|
|
3006
|
+
return true;
|
|
3007
|
+
// 空屏幕 = 只让标题规则说话:标题相对首帧变了且判 working,说明 turn 还活着
|
|
3008
|
+
const titleTurnedWorking = (title) => title !== first.title && classifyScreen(runtime, '', title).state === 'working';
|
|
3009
|
+
let prev = first.frame;
|
|
3010
|
+
let settledSinceChange = true;
|
|
3011
|
+
for (let i = 1; i < RUNTIME_LIVENESS_SAMPLES; i++) {
|
|
3012
|
+
await new Promise(r => setTimeout(r, this.runtimeLivenessProbeMs));
|
|
3013
|
+
const next = await sample('re-capture');
|
|
3014
|
+
if (!next)
|
|
3007
3015
|
return true;
|
|
3008
|
-
if (
|
|
3009
|
-
return false;
|
|
3010
|
-
if (frame !== first)
|
|
3016
|
+
if (titleTurnedWorking(next.title))
|
|
3011
3017
|
return true;
|
|
3018
|
+
if (next.frame === prev) {
|
|
3019
|
+
settledSinceChange = true;
|
|
3020
|
+
continue;
|
|
3021
|
+
}
|
|
3022
|
+
if (!hasRuntimeReadyView(next.frame, runtime))
|
|
3023
|
+
return true;
|
|
3024
|
+
// working→idle 的最新帧已 ready:不能仅凭帧差判 live,需再观察一拍确认它稳定
|
|
3025
|
+
prev = next.frame;
|
|
3026
|
+
settledSinceChange = false;
|
|
3012
3027
|
}
|
|
3013
|
-
|
|
3028
|
+
if (settledSinceChange)
|
|
3029
|
+
return false;
|
|
3030
|
+
await new Promise(r => setTimeout(r, this.runtimeLivenessProbeMs));
|
|
3031
|
+
const confirm = await sample('confirm capture');
|
|
3032
|
+
if (!confirm)
|
|
3033
|
+
return true;
|
|
3034
|
+
return titleTurnedWorking(confirm.title) || confirm.frame !== prev;
|
|
3014
3035
|
}
|
|
3015
3036
|
async paneRunsRuntime(tmux, pane, runtime) {
|
|
3016
3037
|
const paneId = pane.paneId;
|
|
@@ -4612,13 +4633,37 @@ export class AgentManager {
|
|
|
4612
4633
|
}
|
|
4613
4634
|
next.reviewDispatchedAt = now;
|
|
4614
4635
|
delete next.reviewDispatch;
|
|
4615
|
-
if (next.attention?.reason === 'git-review-dispatch-recovery-uncertain'
|
|
4636
|
+
if (next.attention?.reason === 'git-review-dispatch-recovery-uncertain'
|
|
4637
|
+
|| await this.dispatchHoldAttentionIsStale(next)) {
|
|
4616
4638
|
delete next.attention;
|
|
4617
4639
|
}
|
|
4618
4640
|
await this.taskStore.set(next);
|
|
4619
4641
|
return true;
|
|
4620
4642
|
});
|
|
4621
4643
|
}
|
|
4644
|
+
// a hold and its task attention are written as a pair, so no live hold means this one was consumed
|
|
4645
|
+
async dispatchHoldAttentionIsStale(task) {
|
|
4646
|
+
const reason = task.attention?.reason;
|
|
4647
|
+
if (reason === undefined || !GIT_REVIEW_DISPATCH_HOLD_PHASES.has(reason))
|
|
4648
|
+
return false;
|
|
4649
|
+
for (const participantId of [task.agentId, task.qaAgentId]) {
|
|
4650
|
+
if (!participantId)
|
|
4651
|
+
continue;
|
|
4652
|
+
let participant;
|
|
4653
|
+
try {
|
|
4654
|
+
participant = await this.agentStore.get(participantId);
|
|
4655
|
+
}
|
|
4656
|
+
catch (err) {
|
|
4657
|
+
console.error(`[AgentManager] hold liveness probe for ${participantId} failed:`, err);
|
|
4658
|
+
return false;
|
|
4659
|
+
}
|
|
4660
|
+
if (participant?.taskId === task.id
|
|
4661
|
+
&& participant.status === 'awaiting_human'
|
|
4662
|
+
&& participant.awaitingPhase === reason)
|
|
4663
|
+
return false;
|
|
4664
|
+
}
|
|
4665
|
+
return true;
|
|
4666
|
+
}
|
|
4622
4667
|
async recoverPendingGitReviewDispatchHold(taskId, expectedGeneration) {
|
|
4623
4668
|
return this.withTaskLock(async () => {
|
|
4624
4669
|
const task = await this.taskStore.get(taskId);
|
|
@@ -6987,12 +7032,12 @@ export class AgentManager {
|
|
|
6987
7032
|
const paneId = pane.paneId;
|
|
6988
7033
|
const preTitle = await tmux.readPaneTitle(pane);
|
|
6989
7034
|
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`);
|
|
7035
|
+
const preDetection = classifyScreen(runtime, preFrame, preTitle);
|
|
7036
|
+
if (preDetection.state === 'pending') {
|
|
7037
|
+
throw new ReplNotReadyError(paneId, runtime, preFrame, `pre-inject check: pane ${paneId} shows a blocker; dispatch aborted`);
|
|
6995
7038
|
}
|
|
7039
|
+
// working pane 上清稿的 C-c 会打断进行中的 turn
|
|
7040
|
+
const paneWorking = preDetection.state === 'working';
|
|
6996
7041
|
await revalidate?.();
|
|
6997
7042
|
if (guardBeforePaste) {
|
|
6998
7043
|
if (!(await guardBeforePaste())) {
|
|
@@ -7004,7 +7049,8 @@ export class AgentManager {
|
|
|
7004
7049
|
pasted = await this.withTaskLock(async () => {
|
|
7005
7050
|
if (!(await guardBeforePaste()))
|
|
7006
7051
|
return false;
|
|
7007
|
-
|
|
7052
|
+
if (!paneWorking)
|
|
7053
|
+
await tmux.clearComposerDraft(pane);
|
|
7008
7054
|
await tmux.pasteStagedBuffer(paneId, staged.buf);
|
|
7009
7055
|
return true;
|
|
7010
7056
|
});
|
|
@@ -7019,6 +7065,10 @@ export class AgentManager {
|
|
|
7019
7065
|
console.warn(`[AgentManager] staged buffer ${staged.buf} not confirmed dropped after failed paste (missing or unprobeable); failing closed to a composer scrub:`, dropErr);
|
|
7020
7066
|
}
|
|
7021
7067
|
if (bufferConsumed) {
|
|
7068
|
+
const message = pasteErr instanceof Error ? pasteErr.message : String(pasteErr);
|
|
7069
|
+
if (paneWorking) {
|
|
7070
|
+
throw new DispatchTerminalError('ack_unknown', `paste outcome unknown on working pane ${paneId}; composer left untouched to keep the live turn intact: ${message}`);
|
|
7071
|
+
}
|
|
7022
7072
|
try {
|
|
7023
7073
|
await tmux.clearComposerDraft(pane);
|
|
7024
7074
|
}
|
|
@@ -7039,7 +7089,8 @@ export class AgentManager {
|
|
|
7039
7089
|
}
|
|
7040
7090
|
}
|
|
7041
7091
|
else {
|
|
7042
|
-
|
|
7092
|
+
if (!paneWorking)
|
|
7093
|
+
await tmux.clearComposerDraft(pane);
|
|
7043
7094
|
await revalidate?.();
|
|
7044
7095
|
await tmux.injectPrompt(pane, prompt, agentId);
|
|
7045
7096
|
}
|
|
@@ -7056,6 +7107,9 @@ export class AgentManager {
|
|
|
7056
7107
|
return true;
|
|
7057
7108
|
});
|
|
7058
7109
|
if (!submitted) {
|
|
7110
|
+
if (paneWorking) {
|
|
7111
|
+
throw new Error(`fence-rejected prompt stays in the composer of working pane ${paneId}`);
|
|
7112
|
+
}
|
|
7059
7113
|
try {
|
|
7060
7114
|
await tmux.clearComposerDraft(pane);
|
|
7061
7115
|
}
|
|
@@ -7071,11 +7125,18 @@ export class AgentManager {
|
|
|
7071
7125
|
}
|
|
7072
7126
|
}
|
|
7073
7127
|
catch (preAckErr) {
|
|
7128
|
+
const message = preAckErr instanceof Error ? preAckErr.message : String(preAckErr);
|
|
7129
|
+
// working pane 上 scrub 的 C-c 会打断进行中的 turn:交人工核验,不动 composer
|
|
7130
|
+
if (paneWorking) {
|
|
7131
|
+
throw new DispatchTerminalError('ack_unknown', `pre-ack failure left an unconfirmed composer on working pane ${paneId}: ${message}`);
|
|
7132
|
+
}
|
|
7074
7133
|
if (await this.clearComposerForReuse(tmux, pane, agentId))
|
|
7075
7134
|
throw preAckErr;
|
|
7076
|
-
const message = preAckErr instanceof Error ? preAckErr.message : String(preAckErr);
|
|
7077
7135
|
throw new DispatchTerminalError('ack_unknown', `pre-ack failure left an unconfirmed composer on live pane ${paneId}: ${message}`);
|
|
7078
7136
|
}
|
|
7137
|
+
// 判据必须取自粘贴之前:粘贴后的基线含提示词正文,会自匹配 working 规则
|
|
7138
|
+
if (paneWorking)
|
|
7139
|
+
return { acked: true, composerDelivered: true };
|
|
7079
7140
|
let staleDuringResend = false;
|
|
7080
7141
|
try {
|
|
7081
7142
|
await tmux.waitSubmitAck(pane, baseline, runtime, {
|
|
@@ -7122,8 +7183,7 @@ export class AgentManager {
|
|
|
7122
7183
|
'baxian intentionally did NOT send C-c — the input may still be queued. ' +
|
|
7123
7184
|
'Attach via web terminal to verify and resolve.',
|
|
7124
7185
|
});
|
|
7125
|
-
|
|
7126
|
-
return { acked: false, composerDelivered };
|
|
7186
|
+
return { acked: false, composerDelivered: true };
|
|
7127
7187
|
}
|
|
7128
7188
|
}
|
|
7129
7189
|
async clearComposerForReuse(tmux, pane, agentId) {
|
|
@@ -7487,10 +7547,12 @@ export class AgentManager {
|
|
|
7487
7547
|
return false;
|
|
7488
7548
|
}
|
|
7489
7549
|
}
|
|
7490
|
-
async clearBootstrapMarker(agentId, taskId) {
|
|
7550
|
+
async clearBootstrapMarker(agentId, taskId, expectedLockToken) {
|
|
7491
7551
|
await this.agentStore.update(agentId, (latest) => {
|
|
7492
7552
|
if (!latest || latest.taskId !== taskId || latest.bootstrappingTaskId === undefined)
|
|
7493
7553
|
return AGENT_STORE_NOOP;
|
|
7554
|
+
if (expectedLockToken !== undefined && latest.lockToken !== expectedLockToken)
|
|
7555
|
+
return AGENT_STORE_NOOP;
|
|
7494
7556
|
const { bootstrappingTaskId: _delivered, ...rest } = latest;
|
|
7495
7557
|
return { ...rest, updatedAt: new Date().toISOString() };
|
|
7496
7558
|
});
|
|
@@ -8043,6 +8105,13 @@ export class AgentManager {
|
|
|
8043
8105
|
await this.resetGitReviewDispatchClaim(taskId, lease);
|
|
8044
8106
|
throw new ApiError(409, `QA agent ${qaId} could not be released for git review dispatch`);
|
|
8045
8107
|
}
|
|
8108
|
+
if (previousQa.status === 'awaiting_human' && previousQa.awaitingPhase !== undefined) {
|
|
8109
|
+
await this.emitIntervention(task.projectId, qaId, taskId, {
|
|
8110
|
+
phase: 'git-review-dispatch-hold-cleared',
|
|
8111
|
+
previousPhase: previousQa.awaitingPhase,
|
|
8112
|
+
generation: lease.generation,
|
|
8113
|
+
});
|
|
8114
|
+
}
|
|
8046
8115
|
opts.onSideEffect?.();
|
|
8047
8116
|
}
|
|
8048
8117
|
let acquiredLockToken;
|
|
@@ -8861,17 +8930,8 @@ export class AgentManager {
|
|
|
8861
8930
|
});
|
|
8862
8931
|
await new Promise(r => setTimeout(r, this.compactIdlePollMs));
|
|
8863
8932
|
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)) {
|
|
8933
|
+
const { cap, ready, working } = await this.probeReplPrompt(tmux, pane, runtime);
|
|
8934
|
+
if (!working) {
|
|
8875
8935
|
if (!ready) {
|
|
8876
8936
|
throw new Error(`waitForReplPromptReady: pane ${paneId} observed idle but no ready anchor (stale frame?)`);
|
|
8877
8937
|
}
|
|
@@ -8883,6 +8943,23 @@ export class AgentManager {
|
|
|
8883
8943
|
await new Promise(r => setTimeout(r, this.compactIdlePollMs));
|
|
8884
8944
|
}
|
|
8885
8945
|
}
|
|
8946
|
+
async probeReplPrompt(tmux, pane, runtime) {
|
|
8947
|
+
const paneId = pane.paneId;
|
|
8948
|
+
const current = await tmux.displayMessage(pane, '#{pane_current_command}');
|
|
8949
|
+
if (!hasReplProcTitle(current, runtime)) {
|
|
8950
|
+
throw new Error(`waitForReplPromptReady: pane ${paneId} pane_current_command=${current.trim()} (not runtime, REPL may have exited)`);
|
|
8951
|
+
}
|
|
8952
|
+
const cap = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
|
|
8953
|
+
let detection = classifyScreen(runtime, cap);
|
|
8954
|
+
// 标题规则优先级最高:屏幕判 idle 不作数,必须带标题复核
|
|
8955
|
+
if (detection.state === 'idle')
|
|
8956
|
+
detection = classifyScreen(runtime, cap, await tmux.readPaneTitle(pane));
|
|
8957
|
+
const ready = hasRuntimeReadyView(cap, runtime, detection);
|
|
8958
|
+
if (detection.state === 'pending' || detection.skipStateUpdate || detectStartupDialog(cap, runtime)) {
|
|
8959
|
+
throw new Error(`waitForReplPromptReady: pane ${paneId} shows menu/dialog, not a ready REPL prompt`);
|
|
8960
|
+
}
|
|
8961
|
+
return { cap, ready, working: detection.state === 'working' };
|
|
8962
|
+
}
|
|
8886
8963
|
async waitForReplPromptStableIdle(tmux, pane, runtime, timeoutMs) {
|
|
8887
8964
|
const paneId = pane.paneId;
|
|
8888
8965
|
const samples = Math.max(1, this.readyStableSamples);
|
|
@@ -8890,17 +8967,8 @@ export class AgentManager {
|
|
|
8890
8967
|
const deadline = Date.now() + timeoutMs + (samples - 1) * spacing;
|
|
8891
8968
|
let streak = 0;
|
|
8892
8969
|
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;
|
|
8970
|
+
const { cap, ready } = await this.probeReplPrompt(tmux, pane, runtime);
|
|
8971
|
+
streak = ready ? streak + 1 : 0;
|
|
8904
8972
|
if (streak >= samples)
|
|
8905
8973
|
return;
|
|
8906
8974
|
if (Date.now() >= deadline) {
|
|
@@ -9659,6 +9727,15 @@ export class AgentManager {
|
|
|
9659
9727
|
console.warn(`[AgentManager] transitionToCodePhase(${taskId}): stale-generation acquire rollback failed:`, err);
|
|
9660
9728
|
}
|
|
9661
9729
|
}
|
|
9730
|
+
else {
|
|
9731
|
+
try {
|
|
9732
|
+
await this.clearBootstrapMarker(devAgentId, taskId, acquiredLockToken);
|
|
9733
|
+
}
|
|
9734
|
+
catch (clearErr) {
|
|
9735
|
+
console.warn(`[AgentManager] transitionToCodePhase(${taskId}): clearing the reentry bootstrap marker failed:`, clearErr);
|
|
9736
|
+
throw clearErr;
|
|
9737
|
+
}
|
|
9738
|
+
}
|
|
9662
9739
|
return null;
|
|
9663
9740
|
}
|
|
9664
9741
|
this.stopPhaseSignalWatcher(taskId);
|
|
@@ -9692,6 +9769,16 @@ export class AgentManager {
|
|
|
9692
9769
|
});
|
|
9693
9770
|
}
|
|
9694
9771
|
}
|
|
9772
|
+
else {
|
|
9773
|
+
try {
|
|
9774
|
+
await this.clearBootstrapMarker(devAgentId, taskId, acquiredLockToken);
|
|
9775
|
+
}
|
|
9776
|
+
catch (clearErr) {
|
|
9777
|
+
// 原始派发错误在这里落日志,抛出去的是更需要处理的回滚失败
|
|
9778
|
+
console.error(`[AgentManager] transitionToCodePhase dispatch(dev=${devAgentId}) failed:`, err);
|
|
9779
|
+
throw clearErr;
|
|
9780
|
+
}
|
|
9781
|
+
}
|
|
9695
9782
|
}
|
|
9696
9783
|
else if (!(err instanceof EnsureSessionError && err.partial.handled)) {
|
|
9697
9784
|
await this.markAwaitingHuman(devAgentId, 'code-dispatch-failed', 'Code-phase prompt was not delivered after spec approval; the task looks in_progress but the dev never received it. Resume/restart the agent or cancel the task.', { expectedTaskId: taskId }).catch(() => undefined);
|