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.
- 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 +3 -1
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +100 -91
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/prompt.d.ts +1 -0
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +38 -13
- package/dist/agent/prompt.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/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +7 -4
- package/dist/event/handlers.js.map +1 -1
- package/dist/platform/driver-host.d.ts.map +1 -1
- package/dist/platform/driver-host.js +8 -6
- package/dist/platform/driver-host.js.map +1 -1
- package/dist/platform/feedback.d.ts +3 -7
- package/dist/platform/feedback.d.ts.map +1 -1
- package/dist/platform/feedback.js +11 -18
- package/dist/platform/feedback.js.map +1 -1
- package/dist/platform/github-driver.d.ts.map +1 -1
- package/dist/platform/github-driver.js +1 -3
- package/dist/platform/github-driver.js.map +1 -1
- package/dist/platform/markers.d.ts +1 -10
- package/dist/platform/markers.d.ts.map +1 -1
- package/dist/platform/markers.js +11 -30
- package/dist/platform/markers.js.map +1 -1
- package/dist/platform/verdict-engine.d.ts +3 -16
- package/dist/platform/verdict-engine.d.ts.map +1 -1
- package/dist/platform/verdict-engine.js +6 -18
- package/dist/platform/verdict-engine.js.map +1 -1
- package/dist/shared/types.d.ts +0 -1
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/state/task-store.js +1 -1
- package/dist/state/task-store.js.map +1 -1
- package/package.json +1 -1
package/dist/agent/manager.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
3001
|
-
|
|
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
|
|
3005
|
-
return
|
|
2993
|
+
console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness ${phase} failed for pane ${paneId}:`, err);
|
|
2994
|
+
return null;
|
|
3006
2995
|
}
|
|
3007
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5474
|
-
|
|
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 (
|
|
5490
|
-
throw new PlatformMergeRecheckError('
|
|
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
|
|
7011
|
-
|
|
7012
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
8884
|
-
if (!
|
|
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
|
|
8913
|
-
|
|
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) {
|