baxian 2.0.28 → 2.0.30
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/bootstrap.js +2 -2
- package/dist/agent/bootstrap.js.map +1 -1
- package/dist/agent/manager.d.ts +27 -1
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +352 -130
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/runner.d.ts +3 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +6 -0
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tmux-probe-poller.d.ts +12 -2
- package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
- package/dist/agent/tmux-probe-poller.js +199 -57
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.d.ts +21 -5
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +289 -51
- package/dist/agent/tmux.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +38 -4
- package/dist/api/config.js.map +1 -1
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +112 -92
- package/dist/api/projects.js.map +1 -1
- package/dist/web/assets/{index-CItUtzU7.js → index-vQFMnvpO.js} +3 -3
- package/dist/web/index.html +1 -1
- package/package.json +2 -1
package/dist/agent/manager.js
CHANGED
|
@@ -15,11 +15,11 @@ import { prReviewCacheRevision } from '../platform/pr-conversation-cache.js';
|
|
|
15
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
|
-
import { createRunner, LocalRunner, shellQuote, resolveAgentHost, workdirHostGroupKey, } from './runner.js';
|
|
18
|
+
import { createRunner, ExecNotStartedError, LocalRunner, shellQuote, hostGroupKey, resolveAgentHost, workdirHostGroupKey, } from './runner.js';
|
|
19
19
|
import { isTransientNetworkFailure } from './net-exec.js';
|
|
20
20
|
import { classifyScreen, isMenuRule } from './detect/classify.js';
|
|
21
21
|
import { imageFilename, agentHostPath, writeImageToHost } from './image-input.js';
|
|
22
|
-
import { TmuxManager, ReplNotReadyError, isBlockedOnStartupDialog, hasRuntimeReadyView, hasReplProcTitle, isShellProcTitle, PaneGoneError, } from './tmux.js';
|
|
22
|
+
import { TmuxManager, TmuxOutcomeUnknownError, ReplNotReadyError, isBlockedOnStartupDialog, hasRuntimeReadyView, hasReplProcTitle, isShellProcTitle, PaneGoneError, } from './tmux.js';
|
|
23
23
|
import { BranchManager, DirtyWorkdirError, ReviewHeadMismatchError, isAutoDeletableTaskBranch, } from './branch.js';
|
|
24
24
|
import { RepoStore, createRepoStoreCache } from './repo-store.js';
|
|
25
25
|
import { PhaseSignalWatcher, } from './phase-signal-watcher.js';
|
|
@@ -149,6 +149,8 @@ const DEFAULT_DISPATCH_ACK_TIMEOUT_MS = 30_000;
|
|
|
149
149
|
const DEFAULT_NEED_INPUT_RETRY_INTERVAL_MS = 5_000;
|
|
150
150
|
const DEFAULT_DISPATCH_SETTLE_TIMEOUT_MS = 3_000;
|
|
151
151
|
const DELETE_CLEANUP_TIMEOUT_MS = 15_000;
|
|
152
|
+
const MANUAL_CONTEXT_COMMAND_TIMEOUT_MS = 5_000;
|
|
153
|
+
const MANUAL_CONTEXT_TIMEOUT_MS = 15_000;
|
|
152
154
|
const PLATFORM_HEAD_SHA_RE = new RegExp(`^${SHA_HEX_SOURCE}$`);
|
|
153
155
|
function branchCleanupIdentity(task) {
|
|
154
156
|
return {
|
|
@@ -262,6 +264,8 @@ export class AgentManager {
|
|
|
262
264
|
compactIdleWaitMs = 5 * 60_000;
|
|
263
265
|
compactIdlePollMs = 2_000;
|
|
264
266
|
manualCompactWaitMs = 5_000;
|
|
267
|
+
restartInterruptWaitMs = 10_000;
|
|
268
|
+
replExitWaitMs = 30_000;
|
|
265
269
|
runtimeLivenessProbeMs = 700;
|
|
266
270
|
cleanComposerWaitMs = 5_000;
|
|
267
271
|
platformVerificationRetryDelayMs = 2_000;
|
|
@@ -278,6 +282,7 @@ export class AgentManager {
|
|
|
278
282
|
divergedAgents = new Set();
|
|
279
283
|
deletionGeneration = new Map();
|
|
280
284
|
compactInFlight = new Set();
|
|
285
|
+
maintenanceInFlight = new Set();
|
|
281
286
|
platformBindingInterventionKeys = new Set();
|
|
282
287
|
constructor(deps) {
|
|
283
288
|
const config = prepareConfig(deps.config);
|
|
@@ -305,7 +310,17 @@ export class AgentManager {
|
|
|
305
310
|
this.needInputRetryIntervalMs = deps.needInputRetryIntervalMs ?? DEFAULT_NEED_INPUT_RETRY_INTERVAL_MS;
|
|
306
311
|
this.dispatchAckTimeoutMs = deps.dispatchAckTimeoutMs ?? DEFAULT_DISPATCH_ACK_TIMEOUT_MS;
|
|
307
312
|
this.dispatchSettleTimeoutMs = deps.dispatchSettleTimeoutMs ?? DEFAULT_DISPATCH_SETTLE_TIMEOUT_MS;
|
|
308
|
-
this.cancelInterruptGuardWaitMs = this.dispatchAckTimeoutMs + 5_000;
|
|
313
|
+
this.cancelInterruptGuardWaitMs = deps.cancelInterruptGuardWaitMs ?? this.dispatchAckTimeoutMs + 5_000;
|
|
314
|
+
this.restartInterruptWaitMs = deps.restartInterruptWaitMs ?? this.restartInterruptWaitMs;
|
|
315
|
+
this.replExitWaitMs = deps.replExitWaitMs ?? this.replExitWaitMs;
|
|
316
|
+
this.dispatchAckResendIntervalMs = deps.dispatchAckResendIntervalMs ?? this.dispatchAckResendIntervalMs;
|
|
317
|
+
this.compactIdleWaitMs = deps.compactIdleWaitMs ?? this.compactIdleWaitMs;
|
|
318
|
+
this.manualCompactWaitMs = deps.manualCompactWaitMs ?? this.manualCompactWaitMs;
|
|
319
|
+
this.runtimeMenuPollIntervalMs = deps.runtimeMenuPollIntervalMs ?? this.runtimeMenuPollIntervalMs;
|
|
320
|
+
this.runtimeLivenessProbeMs = deps.runtimeLivenessProbeMs ?? this.runtimeLivenessProbeMs;
|
|
321
|
+
this.cleanComposerWaitMs = deps.cleanComposerWaitMs ?? this.cleanComposerWaitMs;
|
|
322
|
+
this.compactIdlePollMs = deps.compactIdlePollMs ?? this.compactIdlePollMs;
|
|
323
|
+
this.readyStableSpacingMs = deps.readyStableSpacingMs ?? this.readyStableSpacingMs;
|
|
309
324
|
this.agentIndex = buildAgentIndex(config);
|
|
310
325
|
this.platformRunner = deps.platformRunner ?? new LocalRunner();
|
|
311
326
|
this.platformDriverExec = makeDriverExec(this.platformRunner);
|
|
@@ -897,7 +912,7 @@ export class AgentManager {
|
|
|
897
912
|
console.warn(`[bootstrap] greeting attempt ${attempt}/${this.greetingMaxAttempts} for ${agentId}: ${outcome}`);
|
|
898
913
|
if (outcome === 'no-agent')
|
|
899
914
|
break;
|
|
900
|
-
if (attempt < this.greetingMaxAttempts && !(await this.clearComposerForReuse(tmux, pane, agentId))) {
|
|
915
|
+
if (attempt < this.greetingMaxAttempts && !(await this.clearComposerForReuse(tmux, pane, agentId, agent.runtime))) {
|
|
901
916
|
break;
|
|
902
917
|
}
|
|
903
918
|
}
|
|
@@ -3144,7 +3159,7 @@ export class AgentManager {
|
|
|
3144
3159
|
}
|
|
3145
3160
|
async clearComposerAndConfirmReady(tmux, pane, runtime) {
|
|
3146
3161
|
try {
|
|
3147
|
-
await tmux.clearComposerDraft(pane);
|
|
3162
|
+
await tmux.clearComposerDraft(pane, runtime);
|
|
3148
3163
|
}
|
|
3149
3164
|
catch (err) {
|
|
3150
3165
|
console.warn(`[AgentManager] interruptPaneAndWaitReady: composer clear failed for pane ${pane.paneId}:`, err);
|
|
@@ -3214,10 +3229,10 @@ export class AgentManager {
|
|
|
3214
3229
|
}
|
|
3215
3230
|
return true;
|
|
3216
3231
|
}
|
|
3217
|
-
async paneReachedReplReady(tmux, pane, runtime, timeoutMs) {
|
|
3232
|
+
async paneReachedReplReady(tmux, pane, runtime, timeoutMs, opts = {}) {
|
|
3218
3233
|
const paneId = pane.paneId;
|
|
3219
3234
|
try {
|
|
3220
|
-
await tmux.waitReplReady(pane, runtime, { timeoutMs, scrollback: 0, titleIdleFastPath: true });
|
|
3235
|
+
await tmux.waitReplReady(pane, runtime, { timeoutMs, scrollback: 0, titleIdleFastPath: true, ...opts });
|
|
3221
3236
|
return true;
|
|
3222
3237
|
}
|
|
3223
3238
|
catch (err) {
|
|
@@ -3323,23 +3338,28 @@ export class AgentManager {
|
|
|
3323
3338
|
});
|
|
3324
3339
|
}
|
|
3325
3340
|
async failTasksForAgent(agentId, reason) {
|
|
3326
|
-
const failed = await this.withTaskLock(
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3341
|
+
const failed = await this.withTaskLock(() => this.failBoundTasksLocked(agentId));
|
|
3342
|
+
return this.publishFailedTasks(agentId, reason, failed);
|
|
3343
|
+
}
|
|
3344
|
+
async failBoundTasksLocked(agentId) {
|
|
3345
|
+
const tasks = await this.taskStore.list({});
|
|
3346
|
+
const out = [];
|
|
3347
|
+
for (const t of tasks) {
|
|
3348
|
+
const bound = t.agentId === agentId || t.qaAgentId === agentId;
|
|
3349
|
+
if (t.status === 'merge-ready')
|
|
3350
|
+
continue;
|
|
3351
|
+
if (ACTIVE_TASK_STATUSES.has(t.status) && bound) {
|
|
3352
|
+
const failedTask = this.stripGitStatusScopedState(t, 'failed');
|
|
3353
|
+
failedTask.status = 'failed';
|
|
3354
|
+
failedTask.updatedAt = new Date().toISOString();
|
|
3355
|
+
await this.taskStore.set(failedTask);
|
|
3356
|
+
out.push(failedTask);
|
|
3340
3357
|
}
|
|
3341
|
-
|
|
3342
|
-
|
|
3358
|
+
}
|
|
3359
|
+
return out;
|
|
3360
|
+
}
|
|
3361
|
+
// 事件与伙伴释放/排空留在 task lock 外:排空可能等待需要 task lock 的操作
|
|
3362
|
+
async publishFailedTasks(agentId, reason, failed) {
|
|
3343
3363
|
for (const t of failed) {
|
|
3344
3364
|
await this.safeEmit({
|
|
3345
3365
|
id: '',
|
|
@@ -3378,22 +3398,103 @@ export class AgentManager {
|
|
|
3378
3398
|
}
|
|
3379
3399
|
async pollPaneCommandStable(tmux, pane, opts) {
|
|
3380
3400
|
const deadline = Date.now() + opts.timeoutMs;
|
|
3381
|
-
const SHELL = /^(?:zsh|bash|sh|fish)$/;
|
|
3382
3401
|
let last = '';
|
|
3383
3402
|
while (Date.now() < deadline) {
|
|
3384
3403
|
await new Promise(r => setTimeout(r, 100));
|
|
3385
3404
|
const raw = await tmux.displayMessage(pane, '#{pane_current_command}');
|
|
3386
3405
|
last = raw.trim();
|
|
3387
|
-
if (opts.expectShell ?
|
|
3406
|
+
if (opts.expectShell ? isShellProcTitle(last) : last !== '')
|
|
3388
3407
|
return last;
|
|
3389
3408
|
}
|
|
3390
3409
|
return last;
|
|
3391
3410
|
}
|
|
3411
|
+
// 不盲发 C-c:空 composer 上的 C-c 会让 codex 立即退出并花数秒生成 recap,之后的 /quit 与重启命令全都打进正在退出的 TUI
|
|
3412
|
+
async exitReplForRestart(tmux, pane, cfg) {
|
|
3413
|
+
const runtime = agentRuntimeKindFor(cfg);
|
|
3414
|
+
// 已空闲就不发 Escape:codex 上它会武装 backtrack 提示,vim Insert 模式下会切到 Normal;只有 turn 进行中才需要打断
|
|
3415
|
+
let foreground = await this.waitIdleOrShell(tmux, pane, runtime);
|
|
3416
|
+
if (foreground === 'busy') {
|
|
3417
|
+
if (await this.reachedRuntime(() => tmux.sendKeysToRuntime(pane, runtime, 'Escape'))) {
|
|
3418
|
+
await new Promise(r => setTimeout(r, 200));
|
|
3419
|
+
}
|
|
3420
|
+
foreground = await this.waitIdleOrShell(tmux, pane, runtime);
|
|
3421
|
+
if (foreground === 'busy') {
|
|
3422
|
+
throw new Error(`restart-repl: ${runtime} in pane ${pane.paneId} did not return to an idle prompt after Escape; ` +
|
|
3423
|
+
'interrupt or finish the running turn via the web terminal, then retry');
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
if (foreground === 'shell')
|
|
3427
|
+
return;
|
|
3428
|
+
await tmux.clearComposerDraft(pane, runtime);
|
|
3429
|
+
// 退出文本与 Enter 是同一条守卫命令:到达前 runtime 已退到 shell 就整组不发(/exit 不会被 shell 当命令执行),前台是 vim 这类进程也不会只留半行在 composer
|
|
3430
|
+
try {
|
|
3431
|
+
if (!await this.reachedRuntime(() => tmux.submitToRuntime(pane, runtime, REPL_EXIT_COMMAND[cfg.runtime])))
|
|
3432
|
+
return;
|
|
3433
|
+
}
|
|
3434
|
+
catch (err) {
|
|
3435
|
+
if (!(err instanceof TmuxOutcomeUnknownError))
|
|
3436
|
+
throw err;
|
|
3437
|
+
console.warn(`[AgentManager] restart-repl: exit outcome unknown for ${cfg.id}; checking for shell before relaunch`, err);
|
|
3438
|
+
}
|
|
3439
|
+
const after = await this.pollPaneCommandStable(tmux, pane, { timeoutMs: this.replExitWaitMs, expectShell: true });
|
|
3440
|
+
if (!isShellProcTitle(after)) {
|
|
3441
|
+
throw new Error(`restart-repl: ${runtime} in pane ${pane.paneId} did not exit within ${this.replExitWaitMs}ms ` +
|
|
3442
|
+
`(pane_current_command=${after || 'unknown'}); not relaunching over a live runtime`);
|
|
3443
|
+
}
|
|
3444
|
+
}
|
|
3445
|
+
// 服务端因前台已是 shell 而拒绝的 runtime 写不是失败:runtime 在按键到达前自己退了,交回按 shell 处理的分支;其他前台进程的拒绝照常抛出
|
|
3446
|
+
async reachedRuntime(write) {
|
|
3447
|
+
try {
|
|
3448
|
+
await write();
|
|
3449
|
+
return true;
|
|
3450
|
+
}
|
|
3451
|
+
catch (err) {
|
|
3452
|
+
if (err instanceof ReplNotReadyError && err.shellForeground)
|
|
3453
|
+
return false;
|
|
3454
|
+
throw err;
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
// 等待期间 runtime 可能自行退到 shell(例如被误杀后 recap 刚结束):此时 Escape/退出命令都会打进 shell,应直接交回 relaunch
|
|
3458
|
+
async waitIdleOrShell(tmux, pane, runtime) {
|
|
3459
|
+
if (await this.paneReachedReplReady(tmux, pane, runtime, this.restartInterruptWaitMs, { failFastOnShell: true, runtimeSeen: true })) {
|
|
3460
|
+
return 'idle';
|
|
3461
|
+
}
|
|
3462
|
+
const current = (await tmux.displayMessage(pane, '#{pane_current_command}')).trim();
|
|
3463
|
+
return isShellProcTitle(current) ? 'shell' : 'busy';
|
|
3464
|
+
}
|
|
3465
|
+
// restart-repl / retry 从退出/重建到就绪后的清理与任务提示词 replay 是一段维护操作:两段并发会各自对同一个新 runtime replay 一次
|
|
3466
|
+
tryBeginMaintenance(agentId) {
|
|
3467
|
+
if (this.maintenanceInFlight.has(agentId))
|
|
3468
|
+
return false;
|
|
3469
|
+
this.maintenanceInFlight.add(agentId);
|
|
3470
|
+
return true;
|
|
3471
|
+
}
|
|
3472
|
+
endMaintenance(agentId) {
|
|
3473
|
+
this.maintenanceInFlight.delete(agentId);
|
|
3474
|
+
}
|
|
3475
|
+
isMaintenanceInFlight(agentId) {
|
|
3476
|
+
return this.maintenanceInFlight.has(agentId);
|
|
3477
|
+
}
|
|
3392
3478
|
async restartReplOnly(agentId, opts = {}) {
|
|
3393
3479
|
const genAtEntry = opts.expectedGeneration ?? this.deletionGenerationOf(agentId);
|
|
3394
3480
|
if (!this.deletionGateOpen(agentId, genAtEntry)) {
|
|
3395
3481
|
throw new Error(`restart-repl: agent ${agentId} is being deleted or was recreated; aborting`);
|
|
3396
3482
|
}
|
|
3483
|
+
if (!this.getAgentConfig(agentId))
|
|
3484
|
+
throw new Error(`Unknown agent: ${agentId}`);
|
|
3485
|
+
// 退出/重启是一整段状态机,只有最后的 relaunch 在服务端原子:整段与 compact/注入/上传共用 pane 互斥,与 retry/删除共用会话生命周期链
|
|
3486
|
+
if (!this.tryAcquireCompactGuard(agentId)) {
|
|
3487
|
+
throw new ApiError(409, `Agent ${agentId} pane is busy (compact, upload, dispatch or another restart in progress); retry shortly`);
|
|
3488
|
+
}
|
|
3489
|
+
try {
|
|
3490
|
+
await this.runUnderSessionLifecycle(agentId, () => this.restartReplOnlyLocked(agentId, genAtEntry));
|
|
3491
|
+
}
|
|
3492
|
+
finally {
|
|
3493
|
+
this.compactInFlight.delete(agentId);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
async restartReplOnlyLocked(agentId, genAtEntry) {
|
|
3497
|
+
// 配置在拿到生命周期链之后才解析:排队期间的热更新可能改了 runtime/model 等启动参数,入口捕获的旧值会拉起旧 runtime
|
|
3397
3498
|
const cfg = this.getAgentConfig(agentId);
|
|
3398
3499
|
if (!cfg)
|
|
3399
3500
|
throw new Error(`Unknown agent: ${agentId}`);
|
|
@@ -3410,23 +3511,22 @@ export class AgentManager {
|
|
|
3410
3511
|
throw new Error(`restart-repl: agent ${agentId} is being deleted or was recreated; aborting`);
|
|
3411
3512
|
}
|
|
3412
3513
|
const pane = await tmux.getSinglePaneByRef(snapshot.ref, agentId);
|
|
3413
|
-
await tmux.sendKeysToPane(pane, 'C-c');
|
|
3414
|
-
const cmd = await this.pollPaneCommandStable(tmux, pane, { timeoutMs: 2_000 });
|
|
3415
3514
|
const RUNTIME = /^(?:claude|codex|node|opencode|qodercli(?:-[\d.]+)?|\d+\.\d+\.\d+)$/;
|
|
3416
|
-
const
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3515
|
+
const readForeground = async () => {
|
|
3516
|
+
const cmd = (await tmux.displayMessage(pane, '#{pane_current_command}')).trim();
|
|
3517
|
+
if (!RUNTIME.test(cmd) && !isShellProcTitle(cmd)) {
|
|
3518
|
+
throw new Error(`restart-repl precondition failed: unexpected pane state "${cmd}"`);
|
|
3519
|
+
}
|
|
3520
|
+
return cmd;
|
|
3521
|
+
};
|
|
3522
|
+
await readForeground();
|
|
3424
3523
|
const project = this.getProjectConfig(cfg.projectId);
|
|
3425
3524
|
if (!project)
|
|
3426
3525
|
throw new Error(`restart-repl: project ${cfg.projectId} does not exist`);
|
|
3427
3526
|
if (!this.deletionGateOpen(agentId, genAtEntry)) {
|
|
3428
3527
|
throw new Error(`restart-repl: agent ${agentId} is being deleted or was recreated; aborting`);
|
|
3429
3528
|
}
|
|
3529
|
+
// 可能耗时或失败的准备(fetch、Workdir 核对)都放在退出 REPL 之前:失败时 REPL 仍活着,pane 停在 shell 的窗口只剩 relaunch 一瞬
|
|
3430
3530
|
const { workdir } = await this.ensureWorkdir(cfg, project, runner);
|
|
3431
3531
|
const paneWorkdir = await tmux.getPaneCurrentPath(pane);
|
|
3432
3532
|
if (!await sameDirOnHost(runner, paneWorkdir, workdir)) {
|
|
@@ -3437,10 +3537,25 @@ export class AgentManager {
|
|
|
3437
3537
|
throw new Error(`restart-repl: agent ${agentId} is being deleted or was recreated; aborting`);
|
|
3438
3538
|
}
|
|
3439
3539
|
await this.setSessionOptions(tmux, agentId, snapshot.ref, [[WORKDIR_SESSION_OPTION, workdir]]);
|
|
3540
|
+
// 热更新不在这条链上:退出与 relaunch 之间启动参数或连接目标又变了就停下,不能按一套配置退出、按另一套(或在另一台机器上)拉起
|
|
3541
|
+
const launchCommand = launchCommandIn(workdir, cfg);
|
|
3542
|
+
const restartTarget = (agent) => `${hostGroupKey(agent.mode, resolveAgentHost(this.config.host, agent.host))} ${launchCommandIn(workdir, agent)}`;
|
|
3543
|
+
const target = restartTarget(cfg);
|
|
3544
|
+
const assertTargetUnchanged = (stage) => {
|
|
3545
|
+
const now = this.getAgentConfig(agentId);
|
|
3546
|
+
if (!now || restartTarget(now) !== target) {
|
|
3547
|
+
throw new Error(`restart-repl: agent ${agentId} config changed while ${stage}; run Restart REPL again against the current config`);
|
|
3548
|
+
}
|
|
3549
|
+
};
|
|
3550
|
+
assertTargetUnchanged('preparing the restart');
|
|
3551
|
+
if (RUNTIME.test(await readForeground())) {
|
|
3552
|
+
await this.exitReplForRestart(tmux, pane, cfg);
|
|
3553
|
+
}
|
|
3554
|
+
assertTargetUnchanged('exiting the runtime');
|
|
3440
3555
|
const runtime = agentRuntimeKindFor(cfg);
|
|
3441
3556
|
const relaunch = async () => {
|
|
3442
|
-
|
|
3443
|
-
await tmux.
|
|
3557
|
+
// C-c 只丢弃 shell readline 里的残留输入(操作员误键、晚到的弄脏键),启动命令不能接在残留文本后面执行;前台不是 shell 则整组按键都不发
|
|
3558
|
+
await tmux.submitCommandOnShell(pane, launchCommand);
|
|
3444
3559
|
await tmux.handleTrustDialog(pane, runtime, {
|
|
3445
3560
|
timeoutMs: this.bootstrapTimeoutsMs.trustDialog,
|
|
3446
3561
|
});
|
|
@@ -6339,7 +6454,7 @@ export class AgentManager {
|
|
|
6339
6454
|
const tmux = new TmuxManager(runner);
|
|
6340
6455
|
const pane = await this.resolveClaimedPane(tmux, agentId, paneId);
|
|
6341
6456
|
await assertUploadStillValid();
|
|
6342
|
-
await tmux.injectPrompt(pane, `${path} `, agentId);
|
|
6457
|
+
await tmux.injectPrompt(pane, `${path} `, agentId, cfg.runtime);
|
|
6343
6458
|
return { path };
|
|
6344
6459
|
}
|
|
6345
6460
|
finally {
|
|
@@ -6354,6 +6469,21 @@ export class AgentManager {
|
|
|
6354
6469
|
throw new ApiError(409, `Agent ${agentId} compact or upload already in progress`);
|
|
6355
6470
|
}
|
|
6356
6471
|
let guardHandedOff = false;
|
|
6472
|
+
const startedAt = performance.now();
|
|
6473
|
+
let deadline = startedAt + MANUAL_CONTEXT_TIMEOUT_MS;
|
|
6474
|
+
const timings = {};
|
|
6475
|
+
let stage = 'session';
|
|
6476
|
+
let outcome = 'failed';
|
|
6477
|
+
const timed = async (name, fn) => {
|
|
6478
|
+
stage = name;
|
|
6479
|
+
const start = performance.now();
|
|
6480
|
+
try {
|
|
6481
|
+
return await fn();
|
|
6482
|
+
}
|
|
6483
|
+
finally {
|
|
6484
|
+
timings[name] = Math.round(performance.now() - start);
|
|
6485
|
+
}
|
|
6486
|
+
};
|
|
6357
6487
|
try {
|
|
6358
6488
|
const state = await this.agentStore.get(agentId);
|
|
6359
6489
|
const paneId = state?.paneId;
|
|
@@ -6370,28 +6500,61 @@ export class AgentManager {
|
|
|
6370
6500
|
throw new ApiError(409, `Agent ${agentId} session changed while waiting; ${command} aborted`);
|
|
6371
6501
|
}
|
|
6372
6502
|
};
|
|
6373
|
-
const
|
|
6374
|
-
const
|
|
6375
|
-
|
|
6503
|
+
const runner = this.createRunnerFor(cfg);
|
|
6504
|
+
const tmux = new TmuxManager({
|
|
6505
|
+
exec: (cmd, options) => {
|
|
6506
|
+
const remaining = Math.ceil(deadline - performance.now());
|
|
6507
|
+
if (remaining <= 0)
|
|
6508
|
+
throw new ExecNotStartedError(`Context command deadline exceeded during ${guardHandedOff ? 'post idle wait' : stage}`);
|
|
6509
|
+
const timeout = Math.min(options?.timeout ?? MANUAL_CONTEXT_COMMAND_TIMEOUT_MS, remaining, MANUAL_CONTEXT_COMMAND_TIMEOUT_MS);
|
|
6510
|
+
return runner.exec(cmd, { ...options, timeout });
|
|
6511
|
+
},
|
|
6512
|
+
execWithStdin: runner.execWithStdin.bind(runner),
|
|
6513
|
+
writeFile: runner.writeFile.bind(runner),
|
|
6514
|
+
});
|
|
6515
|
+
const pane = await timed('session', () => this.resolveClaimedPane(tmux, agentId, paneId));
|
|
6516
|
+
const waitReady = async (label) => {
|
|
6376
6517
|
try {
|
|
6377
|
-
await this.
|
|
6518
|
+
await this.waitForReplPromptStableIdle(tmux, pane, cfg.runtime, this.manualCompactWaitMs, { manual: true });
|
|
6378
6519
|
}
|
|
6379
6520
|
catch (err) {
|
|
6521
|
+
if (err instanceof ExecNotStartedError)
|
|
6522
|
+
throw err;
|
|
6523
|
+
console.warn(`[AgentManager] sendSlashCommand(${agentId}, ${command}) ${label}: runtime not at an idle prompt:`, err);
|
|
6380
6524
|
const detail = err instanceof Error ? err.message : String(err);
|
|
6381
6525
|
throw new ApiError(409, `Agent ${agentId} runtime is not at an idle REPL prompt: ${detail}`);
|
|
6382
6526
|
}
|
|
6383
6527
|
};
|
|
6384
|
-
await waitReady();
|
|
6528
|
+
await timed('readyBeforeClear', () => waitReady('before composer clear'));
|
|
6385
6529
|
await assertSessionUnchanged();
|
|
6386
|
-
|
|
6387
|
-
|
|
6530
|
+
try {
|
|
6531
|
+
await timed('composerClear', () => tmux.clearComposerDraft(pane, cfg.runtime));
|
|
6532
|
+
}
|
|
6533
|
+
catch (err) {
|
|
6534
|
+
console.warn(`[AgentManager] sendSlashCommand(${agentId}, ${command}) composer clear failed:`, err);
|
|
6535
|
+
if (err instanceof ReplNotReadyError || err instanceof PaneGoneError) {
|
|
6536
|
+
throw new ApiError(409, `Agent ${agentId} composer could not be cleared before ${command}: ${err.message}`);
|
|
6537
|
+
}
|
|
6538
|
+
throw err;
|
|
6539
|
+
}
|
|
6540
|
+
await timed('readyAfterClear', () => waitReady('after composer clear'));
|
|
6388
6541
|
await assertSessionUnchanged();
|
|
6389
6542
|
if (command === '/clear') {
|
|
6390
|
-
await this.setSessionOptions(tmux, agentId, pane.session, [[TASK_CONTEXT_SESSION_OPTION, '']]);
|
|
6543
|
+
await timed('contextMarker', () => this.setSessionOptions(tmux, agentId, pane.session, [[TASK_CONTEXT_SESSION_OPTION, '']]));
|
|
6544
|
+
}
|
|
6545
|
+
let submitError;
|
|
6546
|
+
try {
|
|
6547
|
+
await timed('submit', () => tmux.submitToRuntime(pane, cfg.runtime, command));
|
|
6548
|
+
outcome = 'submitted';
|
|
6549
|
+
}
|
|
6550
|
+
catch (err) {
|
|
6551
|
+
if (!(err instanceof TmuxOutcomeUnknownError))
|
|
6552
|
+
throw err;
|
|
6553
|
+
submitError = err;
|
|
6554
|
+
outcome = 'unknown';
|
|
6391
6555
|
}
|
|
6392
|
-
await tmux.sendKeysLiteral(pane, command);
|
|
6393
|
-
await tmux.sendEnter(pane);
|
|
6394
6556
|
guardHandedOff = true;
|
|
6557
|
+
deadline = performance.now() + this.compactIdleWaitMs;
|
|
6395
6558
|
void this.outsideTaskMutationScope(() => this.waitForReplPromptReady(tmux, pane, cfg.runtime, this.compactIdleWaitMs))
|
|
6396
6559
|
.catch(err => {
|
|
6397
6560
|
console.warn(`[AgentManager] sendSlashCommand(${agentId}, ${command}) post idle wait failed:`, err);
|
|
@@ -6399,10 +6562,22 @@ export class AgentManager {
|
|
|
6399
6562
|
.finally(() => {
|
|
6400
6563
|
this.compactInFlight.delete(agentId);
|
|
6401
6564
|
});
|
|
6565
|
+
if (submitError)
|
|
6566
|
+
throw new ApiError(504, `${command} execution outcome unknown; inspect the agent terminal before retrying: ${submitError.message}`);
|
|
6567
|
+
}
|
|
6568
|
+
catch (err) {
|
|
6569
|
+
if (!guardHandedOff && (err instanceof ExecNotStartedError
|
|
6570
|
+
|| (err instanceof Error && isTransientNetworkFailure(err.message)))) {
|
|
6571
|
+
throw new ApiError(409, `Agent ${agentId} could not prepare ${command} during ${stage}; ${command} was not submitted: ${err.message}`);
|
|
6572
|
+
}
|
|
6573
|
+
throw err;
|
|
6402
6574
|
}
|
|
6403
6575
|
finally {
|
|
6404
6576
|
if (!guardHandedOff)
|
|
6405
6577
|
this.compactInFlight.delete(agentId);
|
|
6578
|
+
console.info(`[AgentManager] sendSlashCommand(${agentId}, ${command}) timing:`, {
|
|
6579
|
+
outcome, stage, totalMs: Math.round(performance.now() - startedAt), ...timings,
|
|
6580
|
+
});
|
|
6406
6581
|
}
|
|
6407
6582
|
}
|
|
6408
6583
|
async compactAgent(agentId) {
|
|
@@ -7224,8 +7399,8 @@ export class AgentManager {
|
|
|
7224
7399
|
if (!(await guardBeforePaste()))
|
|
7225
7400
|
return false;
|
|
7226
7401
|
if (!paneWorking)
|
|
7227
|
-
await tmux.clearComposerDraft(pane);
|
|
7228
|
-
await tmux.pasteStagedBuffer(
|
|
7402
|
+
await tmux.clearComposerDraft(pane, runtime);
|
|
7403
|
+
await tmux.pasteStagedBuffer(pane, staged.buf, runtime);
|
|
7229
7404
|
return true;
|
|
7230
7405
|
});
|
|
7231
7406
|
}
|
|
@@ -7244,7 +7419,7 @@ export class AgentManager {
|
|
|
7244
7419
|
throw new DispatchTerminalError('ack_unknown', `paste outcome unknown on working pane ${paneId}; composer left untouched to keep the live turn intact: ${message}`);
|
|
7245
7420
|
}
|
|
7246
7421
|
try {
|
|
7247
|
-
await tmux.clearComposerDraft(pane);
|
|
7422
|
+
await tmux.clearComposerDraft(pane, runtime);
|
|
7248
7423
|
}
|
|
7249
7424
|
catch (clearErr) {
|
|
7250
7425
|
console.warn(`[AgentManager] composer scrub after unknown paste outcome failed for pane ${paneId}:`, clearErr);
|
|
@@ -7264,9 +7439,9 @@ export class AgentManager {
|
|
|
7264
7439
|
}
|
|
7265
7440
|
else {
|
|
7266
7441
|
if (!paneWorking)
|
|
7267
|
-
await tmux.clearComposerDraft(pane);
|
|
7442
|
+
await tmux.clearComposerDraft(pane, runtime);
|
|
7268
7443
|
await revalidate?.();
|
|
7269
|
-
await tmux.injectPrompt(pane, prompt, agentId);
|
|
7444
|
+
await tmux.injectPrompt(pane, prompt, agentId, runtime);
|
|
7270
7445
|
}
|
|
7271
7446
|
let baseline;
|
|
7272
7447
|
let baselineTitle = '';
|
|
@@ -7277,7 +7452,7 @@ export class AgentManager {
|
|
|
7277
7452
|
const submitted = await this.withTaskLock(async () => {
|
|
7278
7453
|
if (!(await guardBeforePaste()))
|
|
7279
7454
|
return false;
|
|
7280
|
-
await tmux.sendEnter(pane);
|
|
7455
|
+
await tmux.sendEnter(pane, runtime);
|
|
7281
7456
|
return true;
|
|
7282
7457
|
});
|
|
7283
7458
|
if (!submitted) {
|
|
@@ -7285,7 +7460,7 @@ export class AgentManager {
|
|
|
7285
7460
|
throw new Error(`fence-rejected prompt stays in the composer of working pane ${paneId}`);
|
|
7286
7461
|
}
|
|
7287
7462
|
try {
|
|
7288
|
-
await tmux.clearComposerDraft(pane);
|
|
7463
|
+
await tmux.clearComposerDraft(pane, runtime);
|
|
7289
7464
|
}
|
|
7290
7465
|
catch (err) {
|
|
7291
7466
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -7295,7 +7470,7 @@ export class AgentManager {
|
|
|
7295
7470
|
}
|
|
7296
7471
|
}
|
|
7297
7472
|
else {
|
|
7298
|
-
await tmux.sendEnter(pane);
|
|
7473
|
+
await tmux.sendEnter(pane, runtime);
|
|
7299
7474
|
}
|
|
7300
7475
|
}
|
|
7301
7476
|
catch (preAckErr) {
|
|
@@ -7304,7 +7479,7 @@ export class AgentManager {
|
|
|
7304
7479
|
if (paneWorking) {
|
|
7305
7480
|
throw new DispatchTerminalError('ack_unknown', `pre-ack failure left an unconfirmed composer on working pane ${paneId}: ${message}`);
|
|
7306
7481
|
}
|
|
7307
|
-
if (await this.clearComposerForReuse(tmux, pane, agentId))
|
|
7482
|
+
if (await this.clearComposerForReuse(tmux, pane, agentId, runtime))
|
|
7308
7483
|
throw preAckErr;
|
|
7309
7484
|
throw new DispatchTerminalError('ack_unknown', `pre-ack failure left an unconfirmed composer on live pane ${paneId}: ${message}`);
|
|
7310
7485
|
}
|
|
@@ -7321,13 +7496,13 @@ export class AgentManager {
|
|
|
7321
7496
|
const resent = await this.withTaskLock(async () => {
|
|
7322
7497
|
if (!(await guardBeforePaste()))
|
|
7323
7498
|
return false;
|
|
7324
|
-
await tmux.sendEnter(pane);
|
|
7499
|
+
await tmux.sendEnter(pane, runtime);
|
|
7325
7500
|
return true;
|
|
7326
7501
|
});
|
|
7327
7502
|
if (!resent)
|
|
7328
7503
|
staleDuringResend = true;
|
|
7329
7504
|
}
|
|
7330
|
-
: () => tmux.sendEnter(pane),
|
|
7505
|
+
: () => tmux.sendEnter(pane, runtime),
|
|
7331
7506
|
resendIntervalMs: this.dispatchAckResendIntervalMs,
|
|
7332
7507
|
});
|
|
7333
7508
|
return { acked: true, composerDelivered: true };
|
|
@@ -7339,7 +7514,7 @@ export class AgentManager {
|
|
|
7339
7514
|
}
|
|
7340
7515
|
if (staleDuringResend) {
|
|
7341
7516
|
try {
|
|
7342
|
-
await tmux.clearComposerDraft(pane);
|
|
7517
|
+
await tmux.clearComposerDraft(pane, runtime);
|
|
7343
7518
|
}
|
|
7344
7519
|
catch (scrubErr) {
|
|
7345
7520
|
const scrubMessage = scrubErr instanceof Error ? scrubErr.message : String(scrubErr);
|
|
@@ -7360,10 +7535,10 @@ export class AgentManager {
|
|
|
7360
7535
|
return { acked: false, composerDelivered: true };
|
|
7361
7536
|
}
|
|
7362
7537
|
}
|
|
7363
|
-
async clearComposerForReuse(tmux, pane, agentId) {
|
|
7538
|
+
async clearComposerForReuse(tmux, pane, agentId, runtime) {
|
|
7364
7539
|
const paneId = pane.paneId;
|
|
7365
7540
|
try {
|
|
7366
|
-
await tmux.clearComposerDraft(pane);
|
|
7541
|
+
await tmux.clearComposerDraft(pane, runtime);
|
|
7367
7542
|
return true;
|
|
7368
7543
|
}
|
|
7369
7544
|
catch (err) {
|
|
@@ -8014,58 +8189,65 @@ export class AgentManager {
|
|
|
8014
8189
|
console.warn('[AgentManager] recover: pending git review retry failed:', err);
|
|
8015
8190
|
});
|
|
8016
8191
|
}
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8192
|
+
// absent 结论在等锁期间可能已过期:与 retry/删除的会话重建同处一条生命周期链,链内重探仍 absent、binding 写入时 stillCurrent 仍真,binding 与任务才在同一临界区内一起落下
|
|
8193
|
+
async reconcileFailedAgent(agentId, opts = {}) {
|
|
8194
|
+
const stillCurrent = opts.stillCurrent ?? (() => true);
|
|
8195
|
+
const reconciled = await this.runUnderSessionLifecycle(agentId, async () => {
|
|
8196
|
+
if (!(await this.sessionStillAbsent(agentId)))
|
|
8197
|
+
return null;
|
|
8198
|
+
return this.withTaskLock(async () => {
|
|
8199
|
+
let projectId = '';
|
|
8200
|
+
let timestamp = '';
|
|
8201
|
+
let hadBinding = false;
|
|
8202
|
+
let changed = false;
|
|
8203
|
+
let taskId;
|
|
8204
|
+
await this.agentStore.update(agentId, (existing) => {
|
|
8205
|
+
if (!existing || !stillCurrent())
|
|
8206
|
+
return AGENT_STORE_NOOP;
|
|
8207
|
+
if (existing.creationToken)
|
|
8208
|
+
return AGENT_STORE_NOOP;
|
|
8209
|
+
if (existing.awaitingPhase != null && REGREET_REQUIRED_HOLD_PHASES.has(existing.awaitingPhase)) {
|
|
8210
|
+
return AGENT_STORE_NOOP;
|
|
8211
|
+
}
|
|
8212
|
+
timestamp = new Date().toISOString();
|
|
8213
|
+
projectId = existing.projectId;
|
|
8214
|
+
hadBinding = !!existing.taskId;
|
|
8215
|
+
taskId = existing.taskId;
|
|
8216
|
+
if (!existing.taskId
|
|
8217
|
+
&& !existing.startedAt
|
|
8218
|
+
&& !existing.paneId
|
|
8219
|
+
&& !existing.creationToken) {
|
|
8220
|
+
return AGENT_STORE_NOOP;
|
|
8221
|
+
}
|
|
8222
|
+
changed = true;
|
|
8223
|
+
if (existing.taskId) {
|
|
8224
|
+
return {
|
|
8225
|
+
...existing,
|
|
8226
|
+
paneId: undefined,
|
|
8227
|
+
status: 'awaiting_human',
|
|
8228
|
+
awaitingPhase: 'runtime-missing',
|
|
8229
|
+
awaitingReason: 'The tmux session is missing; restart the REPL before releasing this task binding.',
|
|
8230
|
+
awaitingSince: timestamp,
|
|
8231
|
+
updatedAt: timestamp,
|
|
8232
|
+
};
|
|
8233
|
+
}
|
|
8044
8234
|
return {
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
awaitingPhase: 'runtime-missing',
|
|
8049
|
-
awaitingReason: 'The tmux session is missing; restart the REPL before releasing this task binding.',
|
|
8050
|
-
awaitingSince: timestamp,
|
|
8235
|
+
id: existing.id,
|
|
8236
|
+
projectId: existing.projectId,
|
|
8237
|
+
...(existing.workdir !== undefined ? { workdir: existing.workdir } : {}),
|
|
8051
8238
|
updatedAt: timestamp,
|
|
8052
8239
|
};
|
|
8053
|
-
}
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
updatedAt: timestamp,
|
|
8059
|
-
};
|
|
8240
|
+
});
|
|
8241
|
+
if (!projectId || !changed)
|
|
8242
|
+
return null;
|
|
8243
|
+
const failed = hadBinding ? await this.failBoundTasksLocked(agentId) : [];
|
|
8244
|
+
return { projectId, timestamp, hadBinding, taskId, failed };
|
|
8060
8245
|
});
|
|
8061
|
-
if (!projectId || !changed)
|
|
8062
|
-
return null;
|
|
8063
|
-
return { projectId, timestamp, hadBinding, taskId };
|
|
8064
8246
|
});
|
|
8065
8247
|
if (!reconciled)
|
|
8066
8248
|
return false;
|
|
8067
8249
|
if (reconciled.hadBinding) {
|
|
8068
|
-
await this.
|
|
8250
|
+
await this.publishFailedTasks(agentId, 'tmux-probe=absent', reconciled.failed);
|
|
8069
8251
|
}
|
|
8070
8252
|
await this.recordError({
|
|
8071
8253
|
agentId,
|
|
@@ -8088,6 +8270,21 @@ export class AgentManager {
|
|
|
8088
8270
|
});
|
|
8089
8271
|
return true;
|
|
8090
8272
|
}
|
|
8273
|
+
// 链内重探与探测器同一口径:没有同名会话、或同名会话的 claim 不是本 agent 才算 absent;探不到(含超时)不能按 absent 落 hold,那会把刚重建好的 REPL 上的任务标成失败
|
|
8274
|
+
async sessionStillAbsent(agentId) {
|
|
8275
|
+
const cfg = this.getAgentConfig(agentId);
|
|
8276
|
+
if (!cfg)
|
|
8277
|
+
return true;
|
|
8278
|
+
try {
|
|
8279
|
+
const snapshot = await new TmuxManager(this.createRunnerFor(cfg))
|
|
8280
|
+
.getSessionSnapshot(agentId, { timeout: this.config.server.tmuxProbeTimeoutMs });
|
|
8281
|
+
return snapshot === null || snapshot.claim !== agentId;
|
|
8282
|
+
}
|
|
8283
|
+
catch (err) {
|
|
8284
|
+
console.warn(`[AgentManager] reconcileFailedAgent(${agentId}): session re-probe inconclusive; leaving the binding untouched:`, err);
|
|
8285
|
+
return false;
|
|
8286
|
+
}
|
|
8287
|
+
}
|
|
8091
8288
|
async cancelTask(taskId) {
|
|
8092
8289
|
let devToRelease;
|
|
8093
8290
|
let qaToRelease;
|
|
@@ -8536,12 +8733,9 @@ export class AgentManager {
|
|
|
8536
8733
|
}
|
|
8537
8734
|
opts.onSideEffect?.();
|
|
8538
8735
|
lease = begun.task.reviewDispatch;
|
|
8539
|
-
|
|
8540
|
-
|
|
8541
|
-
|
|
8542
|
-
signalToken: begun.task.signalToken,
|
|
8543
|
-
};
|
|
8544
|
-
}
|
|
8736
|
+
// 调用方那一代已在 beginGitReviewPass 内校验过,此后要挡的是 pass 开出去之后的改动
|
|
8737
|
+
if (opts.expectedTask)
|
|
8738
|
+
dispatchExpectedTask = taskGenerationGuard(begun.task);
|
|
8545
8739
|
}
|
|
8546
8740
|
const dispatched = await this.dispatchGitReviewLease(taskId, {
|
|
8547
8741
|
expectedGeneration: lease.generation,
|
|
@@ -9149,39 +9343,67 @@ export class AgentManager {
|
|
|
9149
9343
|
await new Promise(r => setTimeout(r, this.compactIdlePollMs));
|
|
9150
9344
|
}
|
|
9151
9345
|
}
|
|
9152
|
-
async probeReplPrompt(tmux, pane, runtime) {
|
|
9346
|
+
async probeReplPrompt(tmux, pane, runtime, snapshotOpts) {
|
|
9153
9347
|
const paneId = pane.paneId;
|
|
9154
|
-
const
|
|
9348
|
+
const snapshot = snapshotOpts
|
|
9349
|
+
? await tmux.readReplSnapshot(pane, runtime, { timeout: snapshotOpts.timeoutMs })
|
|
9350
|
+
: undefined;
|
|
9351
|
+
const current = snapshot?.current ?? await tmux.displayMessage(pane, '#{pane_current_command}');
|
|
9155
9352
|
if (!hasReplProcTitle(current, runtime)) {
|
|
9156
|
-
|
|
9353
|
+
const detail = `waitForReplPromptReady: pane ${paneId} pane_current_command=${current.trim()} (not runtime, REPL may have exited)`;
|
|
9354
|
+
if (snapshot)
|
|
9355
|
+
throw new ReplNotReadyError(paneId, runtime, snapshot.cap, detail);
|
|
9356
|
+
throw new Error(detail);
|
|
9157
9357
|
}
|
|
9158
|
-
const cap = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0, runtime });
|
|
9358
|
+
const cap = snapshot?.cap ?? await tmux.capturePaneById(pane, { ansi: false, scrollback: 0, runtime });
|
|
9159
9359
|
let detection = classifyScreen(runtime, cap);
|
|
9160
9360
|
// 标题规则优先级最高:屏幕判 idle 不作数,必须带标题复核
|
|
9161
9361
|
if (detection.state === 'idle')
|
|
9162
|
-
detection = classifyScreen(runtime, cap, await tmux.readPaneTitle(pane));
|
|
9362
|
+
detection = classifyScreen(runtime, cap, snapshot?.title ?? await tmux.readPaneTitle(pane));
|
|
9163
9363
|
const ready = hasRuntimeReadyView(cap, runtime, detection);
|
|
9164
9364
|
if (detection.state === 'pending' || detection.skipStateUpdate) {
|
|
9165
|
-
|
|
9365
|
+
const detail = `waitForReplPromptReady: pane ${paneId} shows menu/dialog, not a ready REPL prompt`;
|
|
9366
|
+
if (snapshot)
|
|
9367
|
+
throw new ReplNotReadyError(paneId, runtime, cap, detail);
|
|
9368
|
+
throw new Error(detail);
|
|
9166
9369
|
}
|
|
9167
9370
|
return { cap, ready, working: detection.state === 'working' };
|
|
9168
9371
|
}
|
|
9169
|
-
async waitForReplPromptStableIdle(tmux, pane, runtime, timeoutMs) {
|
|
9372
|
+
async waitForReplPromptStableIdle(tmux, pane, runtime, timeoutMs, opts = {}) {
|
|
9170
9373
|
const paneId = pane.paneId;
|
|
9171
9374
|
const samples = Math.max(1, this.readyStableSamples);
|
|
9172
|
-
const spacing = this.readyStableSpacingMs;
|
|
9173
|
-
const
|
|
9375
|
+
const spacing = opts.manual ? Math.min(200, this.readyStableSpacingMs) : this.readyStableSpacingMs;
|
|
9376
|
+
const stabilityWindow = opts.manual ? 0 : (samples - 1) * spacing;
|
|
9377
|
+
const deadline = performance.now() + timeoutMs + stabilityWindow;
|
|
9174
9378
|
let streak = 0;
|
|
9379
|
+
let cap = '';
|
|
9380
|
+
let lastError;
|
|
9175
9381
|
while (true) {
|
|
9176
|
-
const
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
9382
|
+
const remaining = Math.ceil(deadline - performance.now());
|
|
9383
|
+
if (remaining <= 0)
|
|
9384
|
+
break;
|
|
9385
|
+
try {
|
|
9386
|
+
const probe = await this.probeReplPrompt(tmux, pane, runtime, opts.manual ? { timeoutMs: remaining } : undefined);
|
|
9387
|
+
cap = probe.cap;
|
|
9388
|
+
lastError = undefined;
|
|
9389
|
+
streak = probe.ready ? streak + 1 : 0;
|
|
9390
|
+
if (streak >= samples)
|
|
9391
|
+
return;
|
|
9182
9392
|
}
|
|
9183
|
-
|
|
9393
|
+
catch (err) {
|
|
9394
|
+
if (!opts.manual || !(err instanceof Error)
|
|
9395
|
+
|| err instanceof ExecNotStartedError || err instanceof PaneGoneError
|
|
9396
|
+
|| !(err instanceof ReplNotReadyError || isTransientNetworkFailure(err.message)))
|
|
9397
|
+
throw err;
|
|
9398
|
+
lastError = err;
|
|
9399
|
+
if (err instanceof ReplNotReadyError)
|
|
9400
|
+
cap = err.lastScreen;
|
|
9401
|
+
streak = 0;
|
|
9402
|
+
}
|
|
9403
|
+
await new Promise(r => setTimeout(r, Math.max(0, Math.min(spacing, deadline - performance.now()))));
|
|
9184
9404
|
}
|
|
9405
|
+
throw new ReplNotReadyError(paneId, runtime, cap, `stable idle not confirmed within ${timeoutMs}ms (+${stabilityWindow}ms stability window)`
|
|
9406
|
+
+ (lastError ? `; last observation: ${lastError.message}` : ''));
|
|
9185
9407
|
}
|
|
9186
9408
|
stopPhaseSignalWatcher(taskId) {
|
|
9187
9409
|
this.phaseSignalWatcher?.stop(taskId);
|
|
@@ -10060,8 +10282,8 @@ export class AgentManager {
|
|
|
10060
10282
|
|| (taskId !== undefined && afterResolve.lockToken !== lockToken)) {
|
|
10061
10283
|
throw new Error(`injectTextToAgent: agent ${agentId} binding changed before paste`);
|
|
10062
10284
|
}
|
|
10063
|
-
await tmux.injectPrompt(pane, text, agentId);
|
|
10064
|
-
await tmux.sendEnter(pane);
|
|
10285
|
+
await tmux.injectPrompt(pane, text, agentId, cfg.runtime);
|
|
10286
|
+
await tmux.sendEnter(pane, cfg.runtime);
|
|
10065
10287
|
}
|
|
10066
10288
|
finally {
|
|
10067
10289
|
this.compactInFlight.delete(agentId);
|