baxian 1.2.52 → 1.2.54
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/manifests/opencode.json +44 -0
- package/dist/agent/detect/manifests/qodercli.json +49 -0
- package/dist/agent/manager.d.ts +34 -6
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +365 -381
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/net-exec.d.ts +0 -1
- package/dist/agent/net-exec.d.ts.map +1 -1
- package/dist/agent/net-exec.js +1 -1
- package/dist/agent/net-exec.js.map +1 -1
- package/dist/agent/pane-streamer-manager.js +1 -1
- package/dist/agent/pane-streamer-manager.js.map +1 -1
- package/dist/agent/pane-streamer.d.ts +2 -3
- package/dist/agent/pane-streamer.d.ts.map +1 -1
- package/dist/agent/pane-streamer.js +1 -1
- package/dist/agent/pane-streamer.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +0 -1
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +0 -5
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +2 -0
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/prompt.d.ts +8 -0
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +24 -3
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/review-transport.d.ts +11 -2
- package/dist/agent/review-transport.d.ts.map +1 -1
- package/dist/agent/review-transport.js +34 -10
- package/dist/agent/review-transport.js.map +1 -1
- package/dist/agent/runner.d.ts +0 -3
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +1 -18
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
- package/dist/agent/tmux-probe-poller.js +21 -23
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.d.ts +1 -6
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +65 -11
- package/dist/agent/tmux.js.map +1 -1
- package/dist/agent/worktree.d.ts +16 -0
- package/dist/agent/worktree.d.ts.map +1 -1
- package/dist/agent/worktree.js +119 -1
- package/dist/agent/worktree.js.map +1 -1
- package/dist/api/probe.d.ts +0 -10
- package/dist/api/probe.d.ts.map +1 -1
- package/dist/api/probe.js +9 -1
- package/dist/api/probe.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +27 -0
- package/dist/api/tasks.js.map +1 -1
- package/dist/config/validator.js +7 -1
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts +0 -2
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +18 -24
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/log.d.ts.map +1 -1
- package/dist/event/log.js +3 -5
- package/dist/event/log.js.map +1 -1
- package/dist/event/server-handlers.d.ts.map +1 -1
- package/dist/event/server-handlers.js +121 -38
- package/dist/event/server-handlers.js.map +1 -1
- package/dist/github/mapper.d.ts +0 -40
- package/dist/github/mapper.d.ts.map +1 -1
- package/dist/github/mapper.js +0 -140
- package/dist/github/mapper.js.map +1 -1
- package/dist/shared/concurrency.d.ts +3 -0
- package/dist/shared/concurrency.d.ts.map +1 -0
- package/dist/shared/concurrency.js +16 -0
- package/dist/shared/concurrency.js.map +1 -0
- package/dist/shared/constants.d.ts +0 -10
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +3 -10
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/types.d.ts +29 -33
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/skill/registry.d.ts +2 -5
- package/dist/skill/registry.d.ts.map +1 -1
- package/dist/skill/registry.js +2 -2
- package/dist/skill/registry.js.map +1 -1
- package/dist/skills/baxian-server-review/SKILL.md +6 -4
- package/dist/skills/baxian-signals/SKILL.md +3 -1
- package/dist/state/error-record-store.d.ts +2 -0
- package/dist/state/error-record-store.d.ts.map +1 -1
- package/dist/state/error-record-store.js +17 -1
- package/dist/state/error-record-store.js.map +1 -1
- package/dist/state/pet-store.js +3 -1
- package/dist/state/pet-store.js.map +1 -1
- package/dist/state/snapshot.d.ts.map +1 -1
- package/dist/state/snapshot.js +5 -3
- package/dist/state/snapshot.js.map +1 -1
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +23 -16
- package/dist/state/task-store.js.map +1 -1
- package/dist/web/assets/index-Cx-wa7zd.js +13 -0
- package/dist/web/assets/{index-B1JSXKdx.css → index-DfcbGriX.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/agent/detect/index.d.ts +0 -5
- package/dist/agent/detect/index.d.ts.map +0 -1
- package/dist/agent/detect/index.js +0 -5
- package/dist/agent/detect/index.js.map +0 -1
- package/dist/agent/diff-split.d.ts +0 -10
- package/dist/agent/diff-split.d.ts.map +0 -1
- package/dist/agent/diff-split.js +0 -79
- package/dist/agent/diff-split.js.map +0 -1
- package/dist/agent/index.d.ts +0 -8
- package/dist/agent/index.d.ts.map +0 -1
- package/dist/agent/index.js +0 -8
- package/dist/agent/index.js.map +0 -1
- package/dist/config/index.d.ts +0 -5
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js +0 -5
- package/dist/config/index.js.map +0 -1
- package/dist/event/index.d.ts +0 -4
- package/dist/event/index.d.ts.map +0 -1
- package/dist/event/index.js +0 -4
- package/dist/event/index.js.map +0 -1
- package/dist/github/index.d.ts +0 -4
- package/dist/github/index.d.ts.map +0 -1
- package/dist/github/index.js +0 -3
- package/dist/github/index.js.map +0 -1
- package/dist/skill/index.d.ts +0 -2
- package/dist/skill/index.d.ts.map +0 -1
- package/dist/skill/index.js +0 -2
- package/dist/skill/index.js.map +0 -1
- package/dist/state/index.d.ts +0 -6
- package/dist/state/index.d.ts.map +0 -1
- package/dist/state/index.js +0 -6
- package/dist/state/index.js.map +0 -1
- package/dist/terminal/index.d.ts +0 -3
- package/dist/terminal/index.d.ts.map +0 -1
- package/dist/terminal/index.js +0 -3
- package/dist/terminal/index.js.map +0 -1
- package/dist/web/assets/index-CJhHx7jR.js +0 -11
package/dist/agent/manager.js
CHANGED
|
@@ -35,12 +35,6 @@ export class CleanupFailedError extends Error {
|
|
|
35
35
|
this.name = 'CleanupFailedError';
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
export class DispatchTransientError extends Error {
|
|
39
|
-
constructor(message) {
|
|
40
|
-
super(message);
|
|
41
|
-
this.name = 'DispatchTransientError';
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
38
|
export class DispatchTerminalError extends Error {
|
|
45
39
|
reason;
|
|
46
40
|
replDrained;
|
|
@@ -60,11 +54,22 @@ export function buildLaunchCommand(agent) {
|
|
|
60
54
|
case 'codex':
|
|
61
55
|
segments.push('codex --dangerously-bypass-approvals-and-sandbox');
|
|
62
56
|
break;
|
|
57
|
+
case 'opencode':
|
|
58
|
+
// opencode materializes skills into .agents/skills (shared with codex; see skillSubdirFor).
|
|
59
|
+
// Its .agents/.claude compatibility scans can't both be disabled by env, so also turn off
|
|
60
|
+
// the .claude/skills scan — that closes the last collision source: a claude-code agent's
|
|
61
|
+
// .claude/skills/baxian-* in a shared base repo.
|
|
62
|
+
segments.push('env OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1 opencode --auto');
|
|
63
|
+
break;
|
|
64
|
+
case 'qodercli':
|
|
65
|
+
segments.push('qodercli --dangerously-skip-permissions');
|
|
66
|
+
break;
|
|
63
67
|
}
|
|
64
68
|
if (agent.model) {
|
|
65
69
|
segments.push(`--model ${shellQuote(agent.model)}`);
|
|
66
70
|
}
|
|
67
|
-
|
|
71
|
+
// opencode has no --add-dir; extra roots go through its permission model instead (validator rejects the pairing).
|
|
72
|
+
if (agent.addDirs && agent.addDirs.length > 0 && agent.runtime !== 'opencode') {
|
|
68
73
|
for (const dir of agent.addDirs) {
|
|
69
74
|
segments.push(`--add-dir ${shellQuote(dir)}`);
|
|
70
75
|
}
|
|
@@ -74,6 +79,30 @@ export function buildLaunchCommand(agent) {
|
|
|
74
79
|
function agentRuntimeKindFor(agent) {
|
|
75
80
|
return agent.runtime;
|
|
76
81
|
}
|
|
82
|
+
// restart-repl drives the REPL back to a shell before relaunch. Each runtime exits
|
|
83
|
+
// via its own slash command (a bare "exit" would be sent to the model as chat text).
|
|
84
|
+
const REPL_EXIT_COMMAND = {
|
|
85
|
+
'claude-code': '/exit',
|
|
86
|
+
codex: '/quit',
|
|
87
|
+
opencode: '/exit',
|
|
88
|
+
qodercli: '/quit',
|
|
89
|
+
};
|
|
90
|
+
export function skillSubdirFor(runtime) {
|
|
91
|
+
switch (runtime) {
|
|
92
|
+
case 'codex':
|
|
93
|
+
case 'opencode':
|
|
94
|
+
// opencode has no way to disable its .agents/skills compatibility scan (only .claude via
|
|
95
|
+
// env), and when it shares a base repo with a codex agent it loads the codex copy of a
|
|
96
|
+
// same-named baxian-* skill (verified: the .agents copy wins over .opencode). So opencode
|
|
97
|
+
// materializes into .agents/skills too — both runtimes share one runtime-agnostic copy
|
|
98
|
+
// (serialized by the same skill-dir lock) instead of colliding on duplicate names.
|
|
99
|
+
return '.agents/skills';
|
|
100
|
+
case 'qodercli':
|
|
101
|
+
return '.qoder/skills';
|
|
102
|
+
default:
|
|
103
|
+
return '.claude/skills';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
77
106
|
const DEFAULT_DISPATCH_ACK_TIMEOUT_MS = 30_000;
|
|
78
107
|
const DEFAULT_DISPATCH_SETTLE_TIMEOUT_MS = 3_000;
|
|
79
108
|
const GH_NET = { timeout: GH_EXEC_TIMEOUT_MS, retries: 1 };
|
|
@@ -101,7 +130,7 @@ function cancelPhaseDowngrades(prev, next) {
|
|
|
101
130
|
return cancelPhaseRank(next) < cancelPhaseRank(prev);
|
|
102
131
|
}
|
|
103
132
|
const REGREET_REQUIRED_HOLD_PHASES = new Set(['greeting_failed']);
|
|
104
|
-
|
|
133
|
+
function shouldReleaseHeldBinding(state, boundTask) {
|
|
105
134
|
if (state.awaitingPhase != null && REGREET_REQUIRED_HOLD_PHASES.has(state.awaitingPhase))
|
|
106
135
|
return false;
|
|
107
136
|
const taskIsTerminal = !!boundTask && TERMINAL_STATUSES.includes(boundTask.status);
|
|
@@ -147,6 +176,7 @@ export class AgentManager {
|
|
|
147
176
|
manualReviewInFlight = new Set();
|
|
148
177
|
markCompleteInFlight = new Set();
|
|
149
178
|
specVerdictInFlight = new Set();
|
|
179
|
+
codeVerdictInFlight = new Set();
|
|
150
180
|
// 引用计数:cancelTask 与 startCreatedTaskSession 的清理段可合法并存(启动期 Stop),先完成者不得清掉后者的 guard。
|
|
151
181
|
cancelCleanupInFlight = new Map();
|
|
152
182
|
deletionInFlight = new Set();
|
|
@@ -240,6 +270,17 @@ export class AgentManager {
|
|
|
240
270
|
console.warn(`[AgentManager] safeEmit ${event.type} failed (audit log loss; state machine unaffected):`, err);
|
|
241
271
|
}
|
|
242
272
|
}
|
|
273
|
+
async emitIntervention(projectId, agentId, taskId, data) {
|
|
274
|
+
await this.safeEmit({
|
|
275
|
+
id: '',
|
|
276
|
+
type: 'human.intervention',
|
|
277
|
+
timestamp: new Date().toISOString(),
|
|
278
|
+
projectId,
|
|
279
|
+
...(agentId ? { agentId } : {}),
|
|
280
|
+
...(taskId ? { taskId } : {}),
|
|
281
|
+
data,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
243
284
|
async recordError(input) {
|
|
244
285
|
if (!this.errorRecordStore)
|
|
245
286
|
return;
|
|
@@ -333,9 +374,14 @@ export class AgentManager {
|
|
|
333
374
|
async provisionRepoSkills(runner, agent, workdir) {
|
|
334
375
|
if (this.skillRegistry.names().length === 0)
|
|
335
376
|
return;
|
|
336
|
-
const subdir = agent.runtime
|
|
377
|
+
const subdir = skillSubdirFor(agent.runtime);
|
|
337
378
|
const destRoot = `${workdir}/${subdir}`;
|
|
338
379
|
await this.excludeInjectedSkills(runner, workdir, subdir);
|
|
380
|
+
// opencode's skills are model-invoked, not slash commands; the dispatched /<skill> sigil
|
|
381
|
+
// reaches the model as message text and it loads the skill from .opencode/skills. We do NOT
|
|
382
|
+
// wrap skills as .opencode/commands: opencode expands $ARGUMENTS in a command template and
|
|
383
|
+
// would execute a `!`cmd`` / inline a @path embedded in an untrusted task title/description
|
|
384
|
+
// before the skill runs. Message-body payloads are not expanded, so this path is injection-safe.
|
|
339
385
|
await this.runUnderSkillDirLock(this.skillDirLockKey(agent, workdir), async () => {
|
|
340
386
|
await this.ensureSkillDirSafe(runner, workdir, subdir);
|
|
341
387
|
await this.skillRegistry.materialize((path, content) => this.atomicWriteFile(runner, path, content), destRoot);
|
|
@@ -344,7 +390,7 @@ export class AgentManager {
|
|
|
344
390
|
skillDirChain = new Map();
|
|
345
391
|
skillDirLockKey(agent, workdir) {
|
|
346
392
|
const host = hostGroupKey(agent.mode, resolveAgentHost(this.config.host, agent.host));
|
|
347
|
-
const subdir = agent.runtime
|
|
393
|
+
const subdir = skillSubdirFor(agent.runtime);
|
|
348
394
|
const dir = workdir.replace(/\/+$/, '');
|
|
349
395
|
return `${host}:${dir}:${subdir}`;
|
|
350
396
|
}
|
|
@@ -591,14 +637,7 @@ export class AgentManager {
|
|
|
591
637
|
});
|
|
592
638
|
if (!wrote)
|
|
593
639
|
return;
|
|
594
|
-
await this.
|
|
595
|
-
id: '',
|
|
596
|
-
type: 'human.intervention',
|
|
597
|
-
timestamp: now,
|
|
598
|
-
projectId: existing.projectId,
|
|
599
|
-
agentId,
|
|
600
|
-
data: { phase: 'greeting_failed', reason },
|
|
601
|
-
});
|
|
640
|
+
await this.emitIntervention(existing.projectId, agentId, undefined, { phase: 'greeting_failed', reason });
|
|
602
641
|
}
|
|
603
642
|
async regreetHeldAgent(agentId) {
|
|
604
643
|
const agent = this.getAgentConfig(agentId);
|
|
@@ -696,17 +735,9 @@ export class AgentManager {
|
|
|
696
735
|
});
|
|
697
736
|
if (!wrote)
|
|
698
737
|
return;
|
|
699
|
-
await this.
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
timestamp: now,
|
|
703
|
-
projectId: projectIdForEmit,
|
|
704
|
-
agentId,
|
|
705
|
-
...(taskIdForEmit ? { taskId: taskIdForEmit } : {}),
|
|
706
|
-
data: {
|
|
707
|
-
phase: 'agent_dialog_pending',
|
|
708
|
-
reason: 'Agent REPL launched but blocked on a startup dialog (e.g. CLI update notice). Operator should attach via web terminal and dismiss it; baxian will auto-detect ready and resume.',
|
|
709
|
-
},
|
|
738
|
+
await this.emitIntervention(projectIdForEmit, agentId, taskIdForEmit, {
|
|
739
|
+
phase: 'agent_dialog_pending',
|
|
740
|
+
reason: 'Agent REPL launched but blocked on a startup dialog (e.g. CLI update notice). Operator should attach via web terminal and dismiss it; baxian will auto-detect ready and resume.',
|
|
710
741
|
});
|
|
711
742
|
}
|
|
712
743
|
async markBootstrapFailed(agentId, creationToken, errorMessage) {
|
|
@@ -917,16 +948,9 @@ export class AgentManager {
|
|
|
917
948
|
});
|
|
918
949
|
}
|
|
919
950
|
else {
|
|
920
|
-
await this.
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
timestamp: now,
|
|
924
|
-
projectId: projectIdForEmit,
|
|
925
|
-
agentId,
|
|
926
|
-
data: {
|
|
927
|
-
phase: 'agent_dialog_resolved_runtime',
|
|
928
|
-
note: 'Runtime dialog resolved; agent REPL ready. Click Resume to continue.',
|
|
929
|
-
},
|
|
951
|
+
await this.emitIntervention(projectIdForEmit, agentId, undefined, {
|
|
952
|
+
phase: 'agent_dialog_resolved_runtime',
|
|
953
|
+
note: 'Runtime dialog resolved; agent REPL ready. Click Resume to continue.',
|
|
930
954
|
});
|
|
931
955
|
}
|
|
932
956
|
return;
|
|
@@ -990,18 +1014,9 @@ export class AgentManager {
|
|
|
990
1014
|
const onMenu = detectRuntimeMenu(stripped);
|
|
991
1015
|
if (onMenu && !pendingMenu) {
|
|
992
1016
|
pendingMenu = true;
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
type: 'human.intervention',
|
|
997
|
-
timestamp: now,
|
|
998
|
-
projectId: fresh.projectId,
|
|
999
|
-
agentId,
|
|
1000
|
-
taskId: fresh.taskId,
|
|
1001
|
-
data: {
|
|
1002
|
-
phase: 'agent_runtime_menu_pending',
|
|
1003
|
-
note: 'Agent paused on an interactive menu mid-task. Attach via web terminal and respond; baxian will auto-clear once the menu closes.',
|
|
1004
|
-
},
|
|
1017
|
+
await this.emitIntervention(fresh.projectId, agentId, fresh.taskId, {
|
|
1018
|
+
phase: 'agent_runtime_menu_pending',
|
|
1019
|
+
note: 'Agent paused on an interactive menu mid-task. Attach via web terminal and respond; baxian will auto-clear once the menu closes.',
|
|
1005
1020
|
});
|
|
1006
1021
|
}
|
|
1007
1022
|
else if (!onMenu && pendingMenu) {
|
|
@@ -1349,17 +1364,9 @@ export class AgentManager {
|
|
|
1349
1364
|
});
|
|
1350
1365
|
if (!wrote)
|
|
1351
1366
|
return;
|
|
1352
|
-
await this.
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
timestamp: now,
|
|
1356
|
-
projectId,
|
|
1357
|
-
agentId,
|
|
1358
|
-
...(taskId ? { taskId } : {}),
|
|
1359
|
-
data: {
|
|
1360
|
-
phase,
|
|
1361
|
-
reason,
|
|
1362
|
-
},
|
|
1367
|
+
await this.emitIntervention(projectId, agentId, taskId, {
|
|
1368
|
+
phase,
|
|
1369
|
+
reason,
|
|
1363
1370
|
});
|
|
1364
1371
|
}
|
|
1365
1372
|
async markAwaitingIfAckUnknown(agentId, err, expectedTaskId) {
|
|
@@ -1480,18 +1487,10 @@ export class AgentManager {
|
|
|
1480
1487
|
if (shouldReleaseBinding) {
|
|
1481
1488
|
await this.lockManager.release(agentId);
|
|
1482
1489
|
}
|
|
1483
|
-
await this.
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
projectId: state.projectId,
|
|
1488
|
-
agentId,
|
|
1489
|
-
...(state.taskId ? { taskId: state.taskId } : {}),
|
|
1490
|
-
data: {
|
|
1491
|
-
phase: 'resumed',
|
|
1492
|
-
previousPhase: state.awaitingPhase,
|
|
1493
|
-
releasedBinding: shouldReleaseBinding,
|
|
1494
|
-
},
|
|
1490
|
+
await this.emitIntervention(state.projectId, agentId, state.taskId, {
|
|
1491
|
+
phase: 'resumed',
|
|
1492
|
+
previousPhase: state.awaitingPhase,
|
|
1493
|
+
releasedBinding: shouldReleaseBinding,
|
|
1495
1494
|
});
|
|
1496
1495
|
return { resumed: true, releasedBinding: shouldReleaseBinding };
|
|
1497
1496
|
});
|
|
@@ -1686,19 +1685,11 @@ export class AgentManager {
|
|
|
1686
1685
|
catch (releaseErr) {
|
|
1687
1686
|
console.warn(`[AgentManager] failTaskForDispatchError: releaseAgentForTask(${agentId}) failed:`, releaseErr);
|
|
1688
1687
|
}
|
|
1689
|
-
await this.
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
agentId,
|
|
1695
|
-
taskId,
|
|
1696
|
-
data: {
|
|
1697
|
-
phase: `dispatch-failed:${err.reason}`,
|
|
1698
|
-
reason: err.reason,
|
|
1699
|
-
message: err.message,
|
|
1700
|
-
replDrained: err.replDrained,
|
|
1701
|
-
},
|
|
1688
|
+
await this.emitIntervention(transitioned?.task.projectId ?? '', agentId, taskId, {
|
|
1689
|
+
phase: `dispatch-failed:${err.reason}`,
|
|
1690
|
+
reason: err.reason,
|
|
1691
|
+
message: err.message,
|
|
1692
|
+
replDrained: err.replDrained,
|
|
1702
1693
|
});
|
|
1703
1694
|
}
|
|
1704
1695
|
async failTasksForAgent(agentId, reason, opts = {}) {
|
|
@@ -1787,10 +1778,10 @@ export class AgentManager {
|
|
|
1787
1778
|
const paneId = await tmux.getSinglePaneId(agentId);
|
|
1788
1779
|
await tmux.sendKeysToPane(paneId, 'C-c');
|
|
1789
1780
|
const cmd = await this.pollPaneCommandStable(tmux, paneId, { timeoutMs: 2_000 });
|
|
1790
|
-
const RUNTIME = /^(?:claude|codex|node
|
|
1781
|
+
const RUNTIME = /^(?:claude|codex|node|opencode|qodercli(?:-[\d.]+)?|\d+\.\d+\.\d+)$/;
|
|
1791
1782
|
const SHELL = /^(?:zsh|bash|sh|fish)$/;
|
|
1792
1783
|
if (RUNTIME.test(cmd)) {
|
|
1793
|
-
await tmux.sendKeysToPane(paneId,
|
|
1784
|
+
await tmux.sendKeysToPane(paneId, REPL_EXIT_COMMAND[cfg.runtime], 'Enter');
|
|
1794
1785
|
await this.pollPaneCommandStable(tmux, paneId, { timeoutMs: 2_000, expectShell: true });
|
|
1795
1786
|
}
|
|
1796
1787
|
else if (!SHELL.test(cmd)) {
|
|
@@ -2081,6 +2072,10 @@ export class AgentManager {
|
|
|
2081
2072
|
const scanSnapshotOnRecover = isServerProtocol
|
|
2082
2073
|
|| (task.phase === undefined && task.status === 'in_progress')
|
|
2083
2074
|
|| (task.phase !== 'spec' && (task.status === 'review' || task.status === 'fixing'));
|
|
2075
|
+
const allowRecoveredReadFile = task.status === 'review'
|
|
2076
|
+
&& (task.phase === 'spec'
|
|
2077
|
+
|| (task.reviewMode === 'server' && task.batchTotal !== undefined)
|
|
2078
|
+
|| (task.reviewMode === 'server' && task.reviewWorktreeMode === 'base'));
|
|
2084
2079
|
try {
|
|
2085
2080
|
await this.startPhaseSignalWatch({
|
|
2086
2081
|
taskId: task.id,
|
|
@@ -2090,23 +2085,15 @@ export class AgentManager {
|
|
|
2090
2085
|
token: task.signalToken,
|
|
2091
2086
|
skipSnapshot: !scanSnapshotOnRecover,
|
|
2092
2087
|
recovered: true,
|
|
2093
|
-
...(
|
|
2088
|
+
...(allowRecoveredReadFile
|
|
2094
2089
|
? { onReadFile: (req) => { void this.handleReadFileRequest(task.id, agentId, req); } }
|
|
2095
2090
|
: {}),
|
|
2096
2091
|
});
|
|
2097
2092
|
if (interventionKindLabel) {
|
|
2098
|
-
await this.
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
projectId: task.projectId,
|
|
2103
|
-
agentId,
|
|
2104
|
-
taskId: task.id,
|
|
2105
|
-
data: {
|
|
2106
|
-
phase: 'spec-signal-setup-during-recovery',
|
|
2107
|
-
kind: interventionKindLabel,
|
|
2108
|
-
note: 'Task is waiting for a spec signal after server recovery; if no signal arrives, the prompt may not have been fully delivered before the previous crash. Inspect the agent pane and consider manual retry or transition.',
|
|
2109
|
-
},
|
|
2093
|
+
await this.emitIntervention(task.projectId, agentId, task.id, {
|
|
2094
|
+
phase: 'spec-signal-setup-during-recovery',
|
|
2095
|
+
kind: interventionKindLabel,
|
|
2096
|
+
note: 'Task is waiting for a spec signal after server recovery; if no signal arrives, the prompt may not have been fully delivered before the previous crash. Inspect the agent pane and consider manual retry or transition.',
|
|
2110
2097
|
});
|
|
2111
2098
|
}
|
|
2112
2099
|
}
|
|
@@ -2318,18 +2305,10 @@ export class AgentManager {
|
|
|
2318
2305
|
rolledBack = { projectId: task.projectId };
|
|
2319
2306
|
});
|
|
2320
2307
|
if (rolledBack && reason) {
|
|
2321
|
-
await this.
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
projectId: rolledBack.projectId,
|
|
2326
|
-
agentId,
|
|
2327
|
-
taskId,
|
|
2328
|
-
data: {
|
|
2329
|
-
phase: reason.phase,
|
|
2330
|
-
message: reason.message,
|
|
2331
|
-
note: 'Dispatch failed; the task is back in pending — re-dispatch it once the cause is resolved.',
|
|
2332
|
-
},
|
|
2308
|
+
await this.emitIntervention(rolledBack.projectId, agentId, taskId, {
|
|
2309
|
+
phase: reason.phase,
|
|
2310
|
+
message: reason.message,
|
|
2311
|
+
note: 'Dispatch failed; the task is back in pending — re-dispatch it once the cause is resolved.',
|
|
2333
2312
|
});
|
|
2334
2313
|
}
|
|
2335
2314
|
const existing = await this.agentStore.get(agentId);
|
|
@@ -2944,14 +2923,23 @@ export class AgentManager {
|
|
|
2944
2923
|
const worktree = new WorktreeManager(runner);
|
|
2945
2924
|
const tmux = new TmuxManager(runner);
|
|
2946
2925
|
const isServerQaPhase = phase === 'server-review' || phase === 'server-recheck' || phase === 'server-spec-review';
|
|
2926
|
+
const isServerCodeReviewPhase = phase === 'server-review' || phase === 'server-recheck';
|
|
2947
2927
|
const customBranch = task.branch && !task.branch.startsWith(BRANCH_PREFIX) ? task.branch : undefined;
|
|
2948
|
-
//
|
|
2949
|
-
//
|
|
2950
|
-
const
|
|
2928
|
+
// Server code reviews materialize the captured head; other QA modes stay off origin/HEAD.
|
|
2929
|
+
// The fail-fast base resolution only runs where the default base is used.
|
|
2930
|
+
const reviewWorktree = isServerCodeReviewPhase && opts.serverContent !== undefined
|
|
2931
|
+
? await worktree.createDetachedForReviewHead(workdir, taskId, {
|
|
2932
|
+
patch: opts.serverContent,
|
|
2933
|
+
baseSha: opts.serverBaseSha,
|
|
2934
|
+
headSha: opts.serverHeadSha,
|
|
2935
|
+
headTree: opts.serverHeadTree,
|
|
2936
|
+
})
|
|
2937
|
+
: undefined;
|
|
2938
|
+
const worktreePath = reviewWorktree?.path ?? (isServerQaPhase
|
|
2951
2939
|
? await worktree.createDetachedAtBase(workdir, taskId)
|
|
2952
2940
|
: phase === 'review' || phase === 'recheck'
|
|
2953
2941
|
? await worktree.createDetached(workdir, taskId, task.branch)
|
|
2954
|
-
: await worktree.create(workdir, taskId, agent.workdir ? undefined : await this.resolveAutoBaseRef(runner, workdir), customBranch);
|
|
2942
|
+
: await worktree.create(workdir, taskId, agent.workdir ? undefined : await this.resolveAutoBaseRef(runner, workdir), customBranch));
|
|
2955
2943
|
await this.agentStore.update(agentId, (stateNow) => {
|
|
2956
2944
|
if (!stateNow || stateNow.taskId !== taskId)
|
|
2957
2945
|
return AGENT_STORE_NOOP;
|
|
@@ -2972,7 +2960,7 @@ export class AgentManager {
|
|
|
2972
2960
|
try {
|
|
2973
2961
|
const imagePaths = await this.imagePathsForDispatch(runner, task, phase);
|
|
2974
2962
|
let payloadOpts = {};
|
|
2975
|
-
if (opts.serverContent !== undefined || opts.serverPriorFindings || opts.serverPriorResponse) {
|
|
2963
|
+
if (opts.serverContent !== undefined || opts.serverDiffstat !== undefined || opts.serverInterdiff !== undefined || opts.serverPriorFindings || opts.serverPriorResponse) {
|
|
2976
2964
|
payloadOpts = await resolveServerPayloads(this.getReviewTransport(), agent, worktreePath, {
|
|
2977
2965
|
phase,
|
|
2978
2966
|
...(task.phase ? { taskPhase: task.phase } : {}),
|
|
@@ -2980,6 +2968,9 @@ export class AgentManager {
|
|
|
2980
2968
|
reviewRound: task.reviewRound,
|
|
2981
2969
|
...(opts.serverBatch ? { batch: opts.serverBatch } : {}),
|
|
2982
2970
|
...(opts.serverContent !== undefined ? { serverContent: opts.serverContent } : {}),
|
|
2971
|
+
...(reviewWorktree?.mode === 'head' ? { forceContentFile: true } : {}),
|
|
2972
|
+
...(opts.serverDiffstat !== undefined ? { serverDiffstat: opts.serverDiffstat } : {}),
|
|
2973
|
+
...(opts.serverInterdiff !== undefined ? { serverInterdiff: opts.serverInterdiff } : {}),
|
|
2983
2974
|
...(opts.serverPriorFindings ? { serverPriorFindings: opts.serverPriorFindings } : {}),
|
|
2984
2975
|
...(opts.serverPriorResponse ? { serverPriorResponse: opts.serverPriorResponse } : {}),
|
|
2985
2976
|
});
|
|
@@ -2994,7 +2985,11 @@ export class AgentManager {
|
|
|
2994
2985
|
...(promptSignalToken ? { signalToken: promptSignalToken } : {}),
|
|
2995
2986
|
...(promptSpecRound !== undefined ? { currentSpecRound: promptSpecRound } : {}),
|
|
2996
2987
|
...(imagePaths.length ? { imagePaths } : {}),
|
|
2997
|
-
...(
|
|
2988
|
+
...(reviewWorktree ? { serverReviewWorktree: reviewWorktree.mode } : {}),
|
|
2989
|
+
...(reviewWorktree?.fallbackReason ? { serverReviewFallbackReason: reviewWorktree.fallbackReason } : {}),
|
|
2990
|
+
...(opts.serverBaseSha !== undefined ? { serverBaseSha: opts.serverBaseSha } : {}),
|
|
2991
|
+
...(opts.serverHeadSha !== undefined ? { serverHeadSha: opts.serverHeadSha } : {}),
|
|
2992
|
+
...(opts.serverHeadTree !== undefined ? { serverHeadTree: opts.serverHeadTree } : {}),
|
|
2998
2993
|
...(opts.serverBatch ? { serverBatch: opts.serverBatch } : {}),
|
|
2999
2994
|
...payloadOpts,
|
|
3000
2995
|
});
|
|
@@ -3060,7 +3055,7 @@ export class AgentManager {
|
|
|
3060
3055
|
catch { }
|
|
3061
3056
|
return false;
|
|
3062
3057
|
}
|
|
3063
|
-
if (opts.armBeforeInject && !(await opts.armBeforeInject())) {
|
|
3058
|
+
if (opts.armBeforeInject && !(await opts.armBeforeInject({ serverReviewWorktree: reviewWorktree?.mode }))) {
|
|
3064
3059
|
try {
|
|
3065
3060
|
await worktree.removeWithBranch(workdir, worktreePath, customBranch);
|
|
3066
3061
|
}
|
|
@@ -3244,21 +3239,13 @@ export class AgentManager {
|
|
|
3244
3239
|
}
|
|
3245
3240
|
console.warn(`[AgentManager] dispatch ack timeout for pane ${paneId} agent ${agentId}: ${message}`);
|
|
3246
3241
|
const state = await this.agentStore.get(agentId).catch(() => null);
|
|
3247
|
-
await this.
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
data: {
|
|
3255
|
-
phase: 'dispatch-ack-timeout',
|
|
3256
|
-
paneId,
|
|
3257
|
-
message,
|
|
3258
|
-
note: 'REPL did not acknowledge the pasted prompt within timeout. ' +
|
|
3259
|
-
'baxian intentionally did NOT send C-c — the input may still be queued. ' +
|
|
3260
|
-
'Attach via web terminal to verify and resolve.',
|
|
3261
|
-
},
|
|
3242
|
+
await this.emitIntervention(state?.projectId ?? '', agentId, state?.taskId, {
|
|
3243
|
+
phase: 'dispatch-ack-timeout',
|
|
3244
|
+
paneId,
|
|
3245
|
+
message,
|
|
3246
|
+
note: 'REPL did not acknowledge the pasted prompt within timeout. ' +
|
|
3247
|
+
'baxian intentionally did NOT send C-c — the input may still be queued. ' +
|
|
3248
|
+
'Attach via web terminal to verify and resolve.',
|
|
3262
3249
|
});
|
|
3263
3250
|
const composerDelivered = !/pane already busy at baseline/.test(message);
|
|
3264
3251
|
return { acked: false, composerDelivered };
|
|
@@ -3345,7 +3332,7 @@ export class AgentManager {
|
|
|
3345
3332
|
&& !ensure.freshRuntime;
|
|
3346
3333
|
const imagePaths = await this.imagePathsForDispatch(runner, task, phase);
|
|
3347
3334
|
let payloadOpts = {};
|
|
3348
|
-
if (opts.serverContent !== undefined || opts.serverPriorFindings || opts.serverPriorResponse) {
|
|
3335
|
+
if (opts.serverContent !== undefined || opts.serverDiffstat !== undefined || opts.serverInterdiff !== undefined || opts.serverPriorFindings || opts.serverPriorResponse) {
|
|
3349
3336
|
payloadOpts = await resolveServerPayloads(this.getReviewTransport(), agent, worktreePath, {
|
|
3350
3337
|
phase,
|
|
3351
3338
|
...(task.phase ? { taskPhase: task.phase } : {}),
|
|
@@ -3353,6 +3340,8 @@ export class AgentManager {
|
|
|
3353
3340
|
reviewRound: task.reviewRound,
|
|
3354
3341
|
...(opts.serverBatch ? { batch: opts.serverBatch } : {}),
|
|
3355
3342
|
...(opts.serverContent !== undefined ? { serverContent: opts.serverContent } : {}),
|
|
3343
|
+
...(opts.serverDiffstat !== undefined ? { serverDiffstat: opts.serverDiffstat } : {}),
|
|
3344
|
+
...(opts.serverInterdiff !== undefined ? { serverInterdiff: opts.serverInterdiff } : {}),
|
|
3356
3345
|
...(opts.serverPriorFindings ? { serverPriorFindings: opts.serverPriorFindings } : {}),
|
|
3357
3346
|
...(opts.serverPriorResponse ? { serverPriorResponse: opts.serverPriorResponse } : {}),
|
|
3358
3347
|
});
|
|
@@ -3369,7 +3358,6 @@ export class AgentManager {
|
|
|
3369
3358
|
: {}),
|
|
3370
3359
|
...(promptSpecRound !== undefined ? { currentSpecRound: promptSpecRound } : {}),
|
|
3371
3360
|
...(imagePaths.length ? { imagePaths } : {}),
|
|
3372
|
-
...(opts.serverDiffstat !== undefined ? { serverDiffstat: opts.serverDiffstat } : {}),
|
|
3373
3361
|
...(opts.serverBatch ? { serverBatch: opts.serverBatch } : {}),
|
|
3374
3362
|
...(opts.serverAfterDone ? { serverAfterDone: opts.serverAfterDone } : {}),
|
|
3375
3363
|
...payloadOpts,
|
|
@@ -3415,7 +3403,7 @@ export class AgentManager {
|
|
|
3415
3403
|
return false;
|
|
3416
3404
|
}
|
|
3417
3405
|
}
|
|
3418
|
-
if (opts.armBeforeInject && !(await opts.armBeforeInject())) {
|
|
3406
|
+
if (opts.armBeforeInject && !(await opts.armBeforeInject({}))) {
|
|
3419
3407
|
return false;
|
|
3420
3408
|
}
|
|
3421
3409
|
const now = new Date().toISOString();
|
|
@@ -3524,9 +3512,7 @@ export class AgentManager {
|
|
|
3524
3512
|
const boundTask = state.taskId ? await this.taskStore.get(state.taskId) : null;
|
|
3525
3513
|
if (state.taskId
|
|
3526
3514
|
&& boundTask?.id === state.taskId
|
|
3527
|
-
&& boundTask.status === 'merged'
|
|
3528
|
-
&& boundTask.prNumber != null
|
|
3529
|
-
&& boundTask.branch
|
|
3515
|
+
&& (boundTask.status === 'merged' || boundTask.status === 'done')
|
|
3530
3516
|
&& !state.creationToken
|
|
3531
3517
|
&& state.status !== 'awaiting_human') {
|
|
3532
3518
|
try {
|
|
@@ -3544,11 +3530,15 @@ export class AgentManager {
|
|
|
3544
3530
|
|| recovered.status === 'awaiting_human') {
|
|
3545
3531
|
continue;
|
|
3546
3532
|
}
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3533
|
+
if (boundTask.status === 'merged' && boundTask.prNumber != null && boundTask.branch) {
|
|
3534
|
+
await this.dispatchPostMergeCleanup(state.id, {
|
|
3535
|
+
taskId: boundTask.id,
|
|
3536
|
+
branch: boundTask.branch,
|
|
3537
|
+
});
|
|
3538
|
+
continue;
|
|
3539
|
+
}
|
|
3540
|
+
if (this.startCompactionThenRelease(recovered.id, recovered, boundTask.id))
|
|
3541
|
+
continue;
|
|
3552
3542
|
}
|
|
3553
3543
|
catch (cleanupErr) {
|
|
3554
3544
|
console.warn(`[recover] dispatchPostMergeCleanup(${state.id}, ${boundTask.id}) failed:`, cleanupErr);
|
|
@@ -3659,15 +3649,7 @@ export class AgentManager {
|
|
|
3659
3649
|
observation: { phase: 'recovery-failed' },
|
|
3660
3650
|
recommendation: 'Inspect or recreate the tmux session, then retry the affected task.',
|
|
3661
3651
|
});
|
|
3662
|
-
await this.
|
|
3663
|
-
id: '',
|
|
3664
|
-
type: 'human.intervention',
|
|
3665
|
-
timestamp: new Date().toISOString(),
|
|
3666
|
-
projectId: state.projectId,
|
|
3667
|
-
agentId: state.id,
|
|
3668
|
-
taskId: state.taskId ?? '',
|
|
3669
|
-
data: { phase: 'recovery-failed', error: message },
|
|
3670
|
-
});
|
|
3652
|
+
await this.emitIntervention(state.projectId, state.id, state.taskId, { phase: 'recovery-failed', error: message });
|
|
3671
3653
|
}
|
|
3672
3654
|
}
|
|
3673
3655
|
for (const c of deferredCleanups) {
|
|
@@ -3847,19 +3829,12 @@ export class AgentManager {
|
|
|
3847
3829
|
}
|
|
3848
3830
|
if (publishedCleanup) {
|
|
3849
3831
|
if (publishedCleanup.mayBeInFlight && !devStopConfirmed) {
|
|
3850
|
-
await this.
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
data: {
|
|
3857
|
-
phase: 'cancel-published-artifact-cleanup-skipped',
|
|
3858
|
-
afterDone: publishedCleanup.afterDone,
|
|
3859
|
-
branch: publishedCleanup.branch,
|
|
3860
|
-
...(publishedCleanup.prNumber !== undefined ? { prNumber: publishedCleanup.prNumber } : {}),
|
|
3861
|
-
reason: 'dev pane stop unconfirmed; the publish prompt may still be running and would recreate the remote artifacts',
|
|
3862
|
-
},
|
|
3832
|
+
await this.emitIntervention(result.projectId, undefined, taskId, {
|
|
3833
|
+
phase: 'cancel-published-artifact-cleanup-skipped',
|
|
3834
|
+
afterDone: publishedCleanup.afterDone,
|
|
3835
|
+
branch: publishedCleanup.branch,
|
|
3836
|
+
...(publishedCleanup.prNumber !== undefined ? { prNumber: publishedCleanup.prNumber } : {}),
|
|
3837
|
+
reason: 'dev pane stop unconfirmed; the publish prompt may still be running and would recreate the remote artifacts',
|
|
3863
3838
|
});
|
|
3864
3839
|
return result;
|
|
3865
3840
|
}
|
|
@@ -3896,19 +3871,12 @@ export class AgentManager {
|
|
|
3896
3871
|
}
|
|
3897
3872
|
catch (err) {
|
|
3898
3873
|
console.warn(`[AgentManager] cancelTask remote retirement failed for ${taskId}:`, err);
|
|
3899
|
-
await this.
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
data: {
|
|
3906
|
-
phase: 'cancel-published-artifact-cleanup-failed',
|
|
3907
|
-
afterDone: publishedCleanup.afterDone,
|
|
3908
|
-
branch: publishedCleanup.branch,
|
|
3909
|
-
...(publishedCleanup.prNumber !== undefined ? { prNumber: publishedCleanup.prNumber } : {}),
|
|
3910
|
-
error: err instanceof Error ? err.message : String(err),
|
|
3911
|
-
},
|
|
3874
|
+
await this.emitIntervention(result.projectId, undefined, taskId, {
|
|
3875
|
+
phase: 'cancel-published-artifact-cleanup-failed',
|
|
3876
|
+
afterDone: publishedCleanup.afterDone,
|
|
3877
|
+
branch: publishedCleanup.branch,
|
|
3878
|
+
...(publishedCleanup.prNumber !== undefined ? { prNumber: publishedCleanup.prNumber } : {}),
|
|
3879
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3912
3880
|
});
|
|
3913
3881
|
}
|
|
3914
3882
|
}
|
|
@@ -4420,18 +4388,10 @@ export class AgentManager {
|
|
|
4420
4388
|
const cur = await this.agentStore.get(agentId);
|
|
4421
4389
|
if (!cur || cur.taskId !== expectedTaskId)
|
|
4422
4390
|
return;
|
|
4423
|
-
await this.
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
projectId: cur.projectId,
|
|
4428
|
-
agentId,
|
|
4429
|
-
taskId: expectedTaskId,
|
|
4430
|
-
data: {
|
|
4431
|
-
phase: 'manual-review-dev-parked-qa-failed',
|
|
4432
|
-
note: 'Manual QA dispatch parked the dev agent into waiting; QA then failed to start. ' +
|
|
4433
|
-
'Dev binding is kept but no new prompt is running — re-dispatch the manual review or cancel the task.',
|
|
4434
|
-
},
|
|
4391
|
+
await this.emitIntervention(cur.projectId, agentId, expectedTaskId, {
|
|
4392
|
+
phase: 'manual-review-dev-parked-qa-failed',
|
|
4393
|
+
note: 'Manual QA dispatch parked the dev agent into waiting; QA then failed to start. ' +
|
|
4394
|
+
'Dev binding is kept but no new prompt is running — re-dispatch the manual review or cancel the task.',
|
|
4435
4395
|
});
|
|
4436
4396
|
}
|
|
4437
4397
|
async retryTask(taskId) {
|
|
@@ -5008,30 +4968,14 @@ export class AgentManager {
|
|
|
5008
4968
|
if (task.agentId) {
|
|
5009
4969
|
const devOk = await this.markAgentWaiting(task.agentId, taskId).catch(() => false);
|
|
5010
4970
|
if (!devOk) {
|
|
5011
|
-
await this.
|
|
5012
|
-
id: '',
|
|
5013
|
-
type: 'human.intervention',
|
|
5014
|
-
timestamp: new Date().toISOString(),
|
|
5015
|
-
projectId: task.projectId,
|
|
5016
|
-
agentId: task.agentId,
|
|
5017
|
-
taskId,
|
|
5018
|
-
data: { phase: 'spec-ready-dev-park-failed', devAgentId: task.agentId },
|
|
5019
|
-
});
|
|
4971
|
+
await this.emitIntervention(task.projectId, task.agentId, taskId, { phase: 'spec-ready-dev-park-failed', devAgentId: task.agentId });
|
|
5020
4972
|
}
|
|
5021
4973
|
}
|
|
5022
4974
|
if (task.qaAgentId) {
|
|
5023
4975
|
const released = await this.releaseAgentForTask(task.qaAgentId, taskId, 'idle')
|
|
5024
4976
|
.catch(() => false);
|
|
5025
4977
|
if (!released) {
|
|
5026
|
-
await this.
|
|
5027
|
-
id: '',
|
|
5028
|
-
type: 'human.intervention',
|
|
5029
|
-
timestamp: new Date().toISOString(),
|
|
5030
|
-
projectId: task.projectId,
|
|
5031
|
-
agentId: task.qaAgentId,
|
|
5032
|
-
taskId,
|
|
5033
|
-
data: { phase: 'spec-ready-qa-release-failed', qaAgentId: task.qaAgentId },
|
|
5034
|
-
});
|
|
4978
|
+
await this.emitIntervention(task.projectId, task.qaAgentId, taskId, { phase: 'spec-ready-qa-release-failed', qaAgentId: task.qaAgentId });
|
|
5035
4979
|
}
|
|
5036
4980
|
}
|
|
5037
4981
|
return transition.task;
|
|
@@ -5103,6 +5047,111 @@ export class AgentManager {
|
|
|
5103
5047
|
throw new ApiError(500, `Failed to dispatch spec fix for task ${taskId}`);
|
|
5104
5048
|
return dispatched;
|
|
5105
5049
|
}
|
|
5050
|
+
async submitCodeVerdict(taskId, comments) {
|
|
5051
|
+
const trimmed = comments?.trim();
|
|
5052
|
+
if (!trimmed) {
|
|
5053
|
+
throw new ApiError(400, 'comments is required for request-changes');
|
|
5054
|
+
}
|
|
5055
|
+
const task = await this.withTaskLock(async () => {
|
|
5056
|
+
const fresh = await this.taskStore.get(taskId);
|
|
5057
|
+
if (!fresh)
|
|
5058
|
+
throw new ApiError(404, `Task ${taskId} not found`);
|
|
5059
|
+
if (fresh.status !== 'ready') {
|
|
5060
|
+
throw new ApiError(409, `Task ${taskId} is ${fresh.status}; code verdict requires ready`);
|
|
5061
|
+
}
|
|
5062
|
+
if (fresh.phase === 'spec') {
|
|
5063
|
+
throw new ApiError(409, `Task ${taskId} is in the spec phase; reject the spec via the spec verdict instead`);
|
|
5064
|
+
}
|
|
5065
|
+
if (fresh.reviewMode !== 'server') {
|
|
5066
|
+
throw new ApiError(409, `Task ${taskId} is not in server review mode; request changes on the PR instead`);
|
|
5067
|
+
}
|
|
5068
|
+
if (!fresh.agentId) {
|
|
5069
|
+
throw new ApiError(400, `Task ${taskId} has no dev agent; cannot request changes`);
|
|
5070
|
+
}
|
|
5071
|
+
if (this.markCompleteInFlight.has(taskId)) {
|
|
5072
|
+
throw new ApiError(409, `Task ${taskId} is being completed (merge in progress); try again shortly`);
|
|
5073
|
+
}
|
|
5074
|
+
if (this.codeVerdictInFlight.has(taskId)) {
|
|
5075
|
+
throw new ApiError(409, `Task ${taskId} code verdict is already being processed`);
|
|
5076
|
+
}
|
|
5077
|
+
this.codeVerdictInFlight.add(taskId);
|
|
5078
|
+
return fresh;
|
|
5079
|
+
});
|
|
5080
|
+
try {
|
|
5081
|
+
return await this.executeCodeVerdict(task, trimmed);
|
|
5082
|
+
}
|
|
5083
|
+
finally {
|
|
5084
|
+
this.codeVerdictInFlight.delete(taskId);
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
async executeCodeVerdict(task, trimmed) {
|
|
5088
|
+
const taskId = task.id;
|
|
5089
|
+
const store = this.getReviewStore();
|
|
5090
|
+
if (!store)
|
|
5091
|
+
throw new ApiError(500, 'Review store unavailable');
|
|
5092
|
+
const round = Math.max(task.reviewRound, 1);
|
|
5093
|
+
const at = new Date().toISOString();
|
|
5094
|
+
// 打回消耗一轮修订;到达上限时拒绝而非派发注定进 max_rounds 的修订,让用户当场决策
|
|
5095
|
+
const cap = this.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
|
|
5096
|
+
if (round >= cap) {
|
|
5097
|
+
throw new ApiError(409, `Task ${taskId} has reached the code review round cap (${cap}); `
|
|
5098
|
+
+ 'confirm completion, or cancel the task');
|
|
5099
|
+
}
|
|
5100
|
+
const roundData = await store.getRound(taskId, 'code', round);
|
|
5101
|
+
// 无 QA 项目经 autoApproveCode 到达 ready 时从未存轮;补底轮次保证 userDecision 留痕与 fix 闭环
|
|
5102
|
+
const base = roundData ?? await this.synthesizeEmptyCodeRound(task, round, at);
|
|
5103
|
+
// runCodeFixSubmission 按 Math.max(reviewRound,1) 读轮、recheck 按 reviewRound+1 存新轮;
|
|
5104
|
+
// reviewRound 停在 0 会让 recheck 覆写本轮,故落库为 1 对齐两侧
|
|
5105
|
+
if (task.reviewRound === 0) {
|
|
5106
|
+
await this.withTaskLock(async () => {
|
|
5107
|
+
const fresh = await this.taskStore.get(taskId);
|
|
5108
|
+
if (!fresh)
|
|
5109
|
+
return;
|
|
5110
|
+
fresh.reviewRound = 1;
|
|
5111
|
+
fresh.updatedAt = new Date().toISOString();
|
|
5112
|
+
await this.taskStore.set(fresh);
|
|
5113
|
+
});
|
|
5114
|
+
}
|
|
5115
|
+
const priorFindings = base.findings?.findings ?? [];
|
|
5116
|
+
const nextUserId = `u-${priorFindings.filter(f => f.id.startsWith('u-')).length + 1}`;
|
|
5117
|
+
const mergedFindings = {
|
|
5118
|
+
round,
|
|
5119
|
+
verdict: 'request-changes',
|
|
5120
|
+
findings: [...priorFindings, { id: nextUserId, severity: 'major', message: trimmed }],
|
|
5121
|
+
};
|
|
5122
|
+
await store.putRound(taskId, 'code', {
|
|
5123
|
+
...base,
|
|
5124
|
+
findings: mergedFindings,
|
|
5125
|
+
userDecision: { verdict: 'request-changes', comments: trimmed, at },
|
|
5126
|
+
});
|
|
5127
|
+
const dispatched = await this.dispatchServerFixToDev(taskId, JSON.stringify(mergedFindings));
|
|
5128
|
+
if (!dispatched)
|
|
5129
|
+
throw new ApiError(500, `Failed to dispatch code fix for task ${taskId}`);
|
|
5130
|
+
return dispatched;
|
|
5131
|
+
}
|
|
5132
|
+
async synthesizeEmptyCodeRound(task, round, at) {
|
|
5133
|
+
const empty = { round, phase: 'code', content: '', startedAt: at };
|
|
5134
|
+
const dev = task.agentId ? this.getAgentConfig(task.agentId) : undefined;
|
|
5135
|
+
if (!dev || !task.agentId)
|
|
5136
|
+
return empty;
|
|
5137
|
+
// 读取失败不阻断打回:留痕与 fix 闭环优先于 diff 展示
|
|
5138
|
+
try {
|
|
5139
|
+
await this.refreshWorktreeCacheFor(task.agentId);
|
|
5140
|
+
const content = await this.getReviewTransport().readContent(task, dev, 'code');
|
|
5141
|
+
return {
|
|
5142
|
+
round,
|
|
5143
|
+
phase: 'code',
|
|
5144
|
+
content: content.content,
|
|
5145
|
+
...(content.diffstat ? { diffstat: content.diffstat } : {}),
|
|
5146
|
+
...(content.baseSha ? { baseSha: content.baseSha } : {}),
|
|
5147
|
+
startedAt: at,
|
|
5148
|
+
};
|
|
5149
|
+
}
|
|
5150
|
+
catch (err) {
|
|
5151
|
+
console.warn(`[AgentManager] submitCodeVerdict: code content read failed for ${task.id}; using empty round:`, err);
|
|
5152
|
+
return empty;
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5106
5155
|
async transitionToCodePhase(taskId) {
|
|
5107
5156
|
const task = await this.taskStore.get(taskId);
|
|
5108
5157
|
if (!task)
|
|
@@ -5125,29 +5174,13 @@ export class AgentManager {
|
|
|
5125
5174
|
const released = await this.releaseAgentForTask(task.qaAgentId, taskId, 'idle')
|
|
5126
5175
|
.catch(() => false);
|
|
5127
5176
|
if (!released) {
|
|
5128
|
-
await this.
|
|
5129
|
-
id: '',
|
|
5130
|
-
type: 'human.intervention',
|
|
5131
|
-
timestamp: new Date().toISOString(),
|
|
5132
|
-
projectId: task.projectId,
|
|
5133
|
-
agentId: task.qaAgentId,
|
|
5134
|
-
taskId,
|
|
5135
|
-
data: { phase: 'code-phase-qa-release-failed', qaAgentId: task.qaAgentId },
|
|
5136
|
-
});
|
|
5177
|
+
await this.emitIntervention(task.projectId, task.qaAgentId, taskId, { phase: 'code-phase-qa-release-failed', qaAgentId: task.qaAgentId });
|
|
5137
5178
|
}
|
|
5138
5179
|
}
|
|
5139
5180
|
const acquired = await this.acquireAgentForTask(devAgentId, taskId, 'code');
|
|
5140
5181
|
if (!acquired) {
|
|
5141
5182
|
await this.markAwaitingHuman(devAgentId, 'code-dispatch-failed', 'Dev could not be acquired for the code phase after spec approval; the task looks in_progress but the code prompt was never dispatched. Resume the agent to redispatch or cancel the task.', { expectedTaskId: taskId }).catch(() => undefined);
|
|
5142
|
-
await this.
|
|
5143
|
-
id: '',
|
|
5144
|
-
type: 'human.intervention',
|
|
5145
|
-
timestamp: new Date().toISOString(),
|
|
5146
|
-
projectId: task.projectId,
|
|
5147
|
-
agentId: devAgentId,
|
|
5148
|
-
taskId,
|
|
5149
|
-
data: { phase: 'code-dev-acquire-failed', devAgentId },
|
|
5150
|
-
});
|
|
5183
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, { phase: 'code-dev-acquire-failed', devAgentId });
|
|
5151
5184
|
return null;
|
|
5152
5185
|
}
|
|
5153
5186
|
let resumed = false;
|
|
@@ -5166,15 +5199,7 @@ export class AgentManager {
|
|
|
5166
5199
|
}
|
|
5167
5200
|
if (!resumed) {
|
|
5168
5201
|
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);
|
|
5169
|
-
await this.
|
|
5170
|
-
id: '',
|
|
5171
|
-
type: 'human.intervention',
|
|
5172
|
-
timestamp: new Date().toISOString(),
|
|
5173
|
-
projectId: task.projectId,
|
|
5174
|
-
agentId: devAgentId,
|
|
5175
|
-
taskId,
|
|
5176
|
-
data: { phase: 'code-resume-failed', devAgentId },
|
|
5177
|
-
});
|
|
5202
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, { phase: 'code-resume-failed', devAgentId });
|
|
5178
5203
|
return null;
|
|
5179
5204
|
}
|
|
5180
5205
|
return await this.taskStore.get(taskId);
|
|
@@ -5203,15 +5228,7 @@ export class AgentManager {
|
|
|
5203
5228
|
? (opts.phase === 'spec' ? 'spec-fixed' : 'code-fixed')
|
|
5204
5229
|
: (opts.phase === 'spec' ? 'spec-done' : 'code-done');
|
|
5205
5230
|
await this.setupPhaseSignal(taskId, task.agentId, entryKind, { skipSnapshot: true });
|
|
5206
|
-
await this.
|
|
5207
|
-
id: '',
|
|
5208
|
-
type: 'human.intervention',
|
|
5209
|
-
timestamp: new Date().toISOString(),
|
|
5210
|
-
projectId: task.projectId,
|
|
5211
|
-
agentId: task.agentId,
|
|
5212
|
-
taskId,
|
|
5213
|
-
data: { phase: 'server-review-no-qa-partner', devAgentId: task.agentId },
|
|
5214
|
-
});
|
|
5231
|
+
await this.emitIntervention(task.projectId, task.agentId, taskId, { phase: 'server-review-no-qa-partner', devAgentId: task.agentId });
|
|
5215
5232
|
return null;
|
|
5216
5233
|
}
|
|
5217
5234
|
const roundField = opts.phase === 'spec' ? (task.specReviewRound ?? 0) : task.reviewRound;
|
|
@@ -5261,15 +5278,7 @@ export class AgentManager {
|
|
|
5261
5278
|
const acquired = await this.acquireAgentForTask(qaId, taskId, dispatchPhase);
|
|
5262
5279
|
if (!acquired) {
|
|
5263
5280
|
await rearmEntrySignal();
|
|
5264
|
-
await this.
|
|
5265
|
-
id: '',
|
|
5266
|
-
type: 'human.intervention',
|
|
5267
|
-
timestamp: new Date().toISOString(),
|
|
5268
|
-
projectId,
|
|
5269
|
-
agentId: qaId,
|
|
5270
|
-
taskId,
|
|
5271
|
-
data: { phase: 'server-review-qa-acquire-failed', qaAgentId: qaId },
|
|
5272
|
-
});
|
|
5281
|
+
await this.emitIntervention(projectId, qaId, taskId, { phase: 'server-review-qa-acquire-failed', qaAgentId: qaId });
|
|
5273
5282
|
return null;
|
|
5274
5283
|
}
|
|
5275
5284
|
if (devAgentId) {
|
|
@@ -5277,19 +5286,19 @@ export class AgentManager {
|
|
|
5277
5286
|
if (!devOk) {
|
|
5278
5287
|
await this.releaseAgentForTask(qaId, taskId, 'idle').catch(() => undefined);
|
|
5279
5288
|
await rearmEntrySignal();
|
|
5280
|
-
await this.
|
|
5281
|
-
id: '',
|
|
5282
|
-
type: 'human.intervention',
|
|
5283
|
-
timestamp: new Date().toISOString(),
|
|
5284
|
-
projectId,
|
|
5285
|
-
agentId: devAgentId,
|
|
5286
|
-
taskId,
|
|
5287
|
-
data: { phase: 'server-review-dev-park-failed', devAgentId },
|
|
5288
|
-
});
|
|
5289
|
+
await this.emitIntervention(projectId, devAgentId, taskId, { phase: 'server-review-dev-park-failed', devAgentId });
|
|
5289
5290
|
return null;
|
|
5290
5291
|
}
|
|
5291
5292
|
}
|
|
5292
5293
|
}
|
|
5294
|
+
else {
|
|
5295
|
+
const qaState = await this.agentStore.get(qaId);
|
|
5296
|
+
if (qaState?.taskId !== taskId) {
|
|
5297
|
+
await this.setupPhaseSignal(taskId, qaId, expectedKind, { skipSnapshot: true });
|
|
5298
|
+
await this.emitIntervention(projectId, qaId, taskId, { phase: 'server-review-continuation-qa-not-bound', qaAgentId: qaId });
|
|
5299
|
+
return null;
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5293
5302
|
const roundPatch = opts.phase === 'spec'
|
|
5294
5303
|
? { specReviewRound: newRound, phase: 'spec' }
|
|
5295
5304
|
: { reviewRound: newRound };
|
|
@@ -5301,33 +5310,38 @@ export class AgentManager {
|
|
|
5301
5310
|
...(opts.batch
|
|
5302
5311
|
? { batchIndex: opts.batch.index, batchTotal: opts.batch.total }
|
|
5303
5312
|
: { batchIndex: undefined, batchTotal: undefined }),
|
|
5313
|
+
// Fresh dispatch: clear any stale mode until startSession materializes the worktree,
|
|
5314
|
+
// so a crash in this window recovers conservatively (no read-file) rather than
|
|
5315
|
+
// re-arming it for what might be a head-mode review.
|
|
5316
|
+
...(opts.continuation ? {} : { reviewWorktreeMode: undefined }),
|
|
5304
5317
|
...roundPatch,
|
|
5305
5318
|
});
|
|
5306
5319
|
if (!transition) {
|
|
5307
5320
|
if (!opts.continuation) {
|
|
5308
5321
|
await this.releaseAgentForTask(qaId, taskId, 'idle').catch(() => undefined);
|
|
5309
5322
|
}
|
|
5310
|
-
await this.
|
|
5311
|
-
id: '',
|
|
5312
|
-
type: 'human.intervention',
|
|
5313
|
-
timestamp: new Date().toISOString(),
|
|
5314
|
-
projectId,
|
|
5315
|
-
agentId: qaId,
|
|
5316
|
-
taskId,
|
|
5317
|
-
data: { phase: 'server-review-transition-failed', qaAgentId: qaId },
|
|
5318
|
-
});
|
|
5323
|
+
await this.emitIntervention(projectId, qaId, taskId, { phase: 'server-review-transition-failed', qaAgentId: qaId });
|
|
5319
5324
|
return null;
|
|
5320
5325
|
}
|
|
5326
|
+
let dispatchedWorktreeMode;
|
|
5321
5327
|
const sessionOpts = {
|
|
5322
5328
|
bypassTaskStatusGate: true,
|
|
5323
5329
|
signalToken: newToken,
|
|
5324
5330
|
serverContent: opts.content,
|
|
5331
|
+
...(opts.interdiff !== undefined ? { serverInterdiff: opts.interdiff } : {}),
|
|
5325
5332
|
...(opts.diffstat !== undefined ? { serverDiffstat: opts.diffstat } : {}),
|
|
5333
|
+
...(opts.baseSha !== undefined ? { serverBaseSha: opts.baseSha } : {}),
|
|
5334
|
+
...(opts.reviewHeadAnchorSha !== undefined ? { serverHeadSha: opts.reviewHeadAnchorSha } : {}),
|
|
5335
|
+
...(opts.headTree !== undefined ? { serverHeadTree: opts.headTree } : {}),
|
|
5326
5336
|
...(opts.batch ? { serverBatch: opts.batch } : {}),
|
|
5327
5337
|
...(opts.priorFindingsJson ? { serverPriorFindings: opts.priorFindingsJson } : {}),
|
|
5328
5338
|
...(opts.priorResponseJson ? { serverPriorResponse: opts.priorResponseJson } : {}),
|
|
5329
5339
|
...(opts.phase === 'spec' ? { currentSpecRound: newRound } : {}),
|
|
5330
|
-
armBeforeInject: () =>
|
|
5340
|
+
armBeforeInject: (ctx) => {
|
|
5341
|
+
dispatchedWorktreeMode = ctx.serverReviewWorktree;
|
|
5342
|
+
const allowReadFile = opts.phase === 'spec' || opts.continuation || ctx.serverReviewWorktree === 'base';
|
|
5343
|
+
return this.setupPhaseSignalWatcher(taskId, qaId, expectedKind, newToken, false, allowReadFile ? (req) => { void this.handleReadFileRequest(taskId, qaId, req); } : undefined);
|
|
5344
|
+
},
|
|
5331
5345
|
};
|
|
5332
5346
|
const rearmConsumedSignal = async () => {
|
|
5333
5347
|
if (opts.continuation) {
|
|
@@ -5366,17 +5380,14 @@ export class AgentManager {
|
|
|
5366
5380
|
await this.releaseAgentForTask(qaId, taskId, 'idle').catch(() => undefined);
|
|
5367
5381
|
}
|
|
5368
5382
|
await rearmConsumedSignal();
|
|
5369
|
-
await this.
|
|
5370
|
-
id: '',
|
|
5371
|
-
type: 'human.intervention',
|
|
5372
|
-
timestamp: new Date().toISOString(),
|
|
5373
|
-
projectId,
|
|
5374
|
-
agentId: qaId,
|
|
5375
|
-
taskId,
|
|
5376
|
-
data: { phase: 'server-review-start-failed', qaAgentId: qaId },
|
|
5377
|
-
});
|
|
5383
|
+
await this.emitIntervention(projectId, qaId, taskId, { phase: 'server-review-start-failed', qaAgentId: qaId });
|
|
5378
5384
|
return null;
|
|
5379
5385
|
}
|
|
5386
|
+
// Persist the materialized worktree mode so a restart can re-arm read-file for a base
|
|
5387
|
+
// fallback review (whose prompt depends on it) but not for a head-mode one.
|
|
5388
|
+
if (opts.phase !== 'spec' && !opts.continuation) {
|
|
5389
|
+
await this.updateTask(taskId, { reviewWorktreeMode: dispatchedWorktreeMode }).catch(() => { });
|
|
5390
|
+
}
|
|
5380
5391
|
return await this.taskStore.get(taskId);
|
|
5381
5392
|
}
|
|
5382
5393
|
async dispatchServerFixToDev(taskId, findingsJson) {
|
|
@@ -5416,15 +5427,7 @@ export class AgentManager {
|
|
|
5416
5427
|
const acquired = await this.acquireAgentForTask(devAgentId, taskId, 'server-feedback');
|
|
5417
5428
|
if (!acquired) {
|
|
5418
5429
|
await rearmReviewedSignal();
|
|
5419
|
-
await this.
|
|
5420
|
-
id: '',
|
|
5421
|
-
type: 'human.intervention',
|
|
5422
|
-
timestamp: new Date().toISOString(),
|
|
5423
|
-
projectId,
|
|
5424
|
-
agentId: devAgentId,
|
|
5425
|
-
taskId,
|
|
5426
|
-
data: { phase: 'server-fix-dev-acquire-failed', devAgentId },
|
|
5427
|
-
});
|
|
5430
|
+
await this.emitIntervention(projectId, devAgentId, taskId, { phase: 'server-fix-dev-acquire-failed', devAgentId });
|
|
5428
5431
|
return null;
|
|
5429
5432
|
}
|
|
5430
5433
|
if (qaAgentId) {
|
|
@@ -5432,29 +5435,13 @@ export class AgentManager {
|
|
|
5432
5435
|
.catch(() => false);
|
|
5433
5436
|
if (!released) {
|
|
5434
5437
|
await rearmReviewedSignal();
|
|
5435
|
-
await this.
|
|
5436
|
-
id: '',
|
|
5437
|
-
type: 'human.intervention',
|
|
5438
|
-
timestamp: new Date().toISOString(),
|
|
5439
|
-
projectId,
|
|
5440
|
-
agentId: qaAgentId,
|
|
5441
|
-
taskId,
|
|
5442
|
-
data: { phase: 'server-fix-qa-release-failed', qaAgentId },
|
|
5443
|
-
});
|
|
5438
|
+
await this.emitIntervention(projectId, qaAgentId, taskId, { phase: 'server-fix-qa-release-failed', qaAgentId });
|
|
5444
5439
|
return null;
|
|
5445
5440
|
}
|
|
5446
5441
|
}
|
|
5447
|
-
const transition = await this.transitionTaskStatus(taskId, 'fixing', { fromStatus: ['review', 'max_rounds', 'spec-ready'] }, { signalToken: newToken, fixDispatchedAt: new Date().toISOString() });
|
|
5442
|
+
const transition = await this.transitionTaskStatus(taskId, 'fixing', { fromStatus: ['review', 'max_rounds', 'spec-ready', 'ready'] }, { signalToken: newToken, fixDispatchedAt: new Date().toISOString() });
|
|
5448
5443
|
if (!transition) {
|
|
5449
|
-
await this.
|
|
5450
|
-
id: '',
|
|
5451
|
-
type: 'human.intervention',
|
|
5452
|
-
timestamp: new Date().toISOString(),
|
|
5453
|
-
projectId,
|
|
5454
|
-
agentId: devAgentId,
|
|
5455
|
-
taskId,
|
|
5456
|
-
data: { phase: 'server-fix-transition-failed', devAgentId },
|
|
5457
|
-
});
|
|
5444
|
+
await this.emitIntervention(projectId, devAgentId, taskId, { phase: 'server-fix-transition-failed', devAgentId });
|
|
5458
5445
|
return null;
|
|
5459
5446
|
}
|
|
5460
5447
|
let resumed = false;
|
|
@@ -5485,15 +5472,7 @@ export class AgentManager {
|
|
|
5485
5472
|
await rollbackToEntry();
|
|
5486
5473
|
await this.releaseAgentForTask(devAgentId, taskId, 'idle').catch(() => undefined);
|
|
5487
5474
|
await rearmReviewedSignal();
|
|
5488
|
-
await this.
|
|
5489
|
-
id: '',
|
|
5490
|
-
type: 'human.intervention',
|
|
5491
|
-
timestamp: new Date().toISOString(),
|
|
5492
|
-
projectId,
|
|
5493
|
-
agentId: devAgentId,
|
|
5494
|
-
taskId,
|
|
5495
|
-
data: { phase: 'server-fix-resume-failed', devAgentId },
|
|
5496
|
-
});
|
|
5475
|
+
await this.emitIntervention(projectId, devAgentId, taskId, { phase: 'server-fix-resume-failed', devAgentId });
|
|
5497
5476
|
return null;
|
|
5498
5477
|
}
|
|
5499
5478
|
await this.armPostDispatchSignalOrHold(taskId, devAgentId, expectedKind, newToken);
|
|
@@ -5510,6 +5489,33 @@ export class AgentManager {
|
|
|
5510
5489
|
return `git:${parsed.host.toLowerCase()}/${parsed.path}`;
|
|
5511
5490
|
return `raw:${repo.trim()}`;
|
|
5512
5491
|
}
|
|
5492
|
+
async computeCodeInterdiff(taskId, round) {
|
|
5493
|
+
const store = this.getReviewStore();
|
|
5494
|
+
if (!store || !Number.isInteger(round) || round < 2)
|
|
5495
|
+
return { ok: false, reason: 'no-anchor' };
|
|
5496
|
+
const [cur, prev] = await Promise.all([
|
|
5497
|
+
store.getRound(taskId, 'code', round),
|
|
5498
|
+
store.getRound(taskId, 'code', round - 1),
|
|
5499
|
+
]);
|
|
5500
|
+
const curSha = cur?.headSha;
|
|
5501
|
+
const prevSha = prev?.headSha;
|
|
5502
|
+
if (!curSha || !prevSha)
|
|
5503
|
+
return { ok: false, reason: 'no-anchor' };
|
|
5504
|
+
const task = await this.taskStore.get(taskId);
|
|
5505
|
+
if (!task?.agentId)
|
|
5506
|
+
return { ok: false, reason: 'released' };
|
|
5507
|
+
const agent = this.getAgentConfig(task.agentId);
|
|
5508
|
+
const agentState = await this.agentStore.get(task.agentId);
|
|
5509
|
+
// A rebound agent's worktree belongs to its new task — its git state describes
|
|
5510
|
+
// another branch, so the interdiff would be meaningless. Same skip rule as
|
|
5511
|
+
// findLineageViolation: worktree must still be bound to THIS task.
|
|
5512
|
+
if (!agent || agentState?.taskId !== taskId || !agentState.worktreePath) {
|
|
5513
|
+
return { ok: false, reason: 'released' };
|
|
5514
|
+
}
|
|
5515
|
+
await this.refreshWorktreeCacheFor(task.agentId);
|
|
5516
|
+
const diff = await this.getReviewTransport().readInterdiff(agent, prevSha, curSha);
|
|
5517
|
+
return { ok: true, diff };
|
|
5518
|
+
}
|
|
5513
5519
|
async findLineageViolation(taskId, baseSha) {
|
|
5514
5520
|
const task = await this.taskStore.get(taskId);
|
|
5515
5521
|
if (!task?.agentId)
|
|
@@ -5567,36 +5573,20 @@ export class AgentManager {
|
|
|
5567
5573
|
violation = await this.findLineageViolation(taskId);
|
|
5568
5574
|
}
|
|
5569
5575
|
catch (err) {
|
|
5570
|
-
await this.
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
projectId: task.projectId,
|
|
5575
|
-
agentId: devAgentId,
|
|
5576
|
-
taskId,
|
|
5577
|
-
data: {
|
|
5578
|
-
phase: 'server-after-done-lineage-check-failed',
|
|
5579
|
-
error: err instanceof Error ? err.message : String(err),
|
|
5580
|
-
note: 'Publish was not dispatched; mark-complete retries it once the repo state is fixed.',
|
|
5581
|
-
},
|
|
5576
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, {
|
|
5577
|
+
phase: 'server-after-done-lineage-check-failed',
|
|
5578
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5579
|
+
note: 'Publish was not dispatched; mark-complete retries it once the repo state is fixed.',
|
|
5582
5580
|
});
|
|
5583
5581
|
return null;
|
|
5584
5582
|
}
|
|
5585
5583
|
if (violation) {
|
|
5586
|
-
await this.
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
taskId,
|
|
5593
|
-
data: {
|
|
5594
|
-
phase: 'server-after-done-lineage-violation',
|
|
5595
|
-
offendingTaskId: violation.taskId,
|
|
5596
|
-
offendingBranch: violation.branch,
|
|
5597
|
-
offendingSha: violation.sha,
|
|
5598
|
-
note: 'The task branch embeds another active task\'s commits; publishing would leak them into this PR. Rebase the branch onto origin/HEAD, then mark-complete to retry the publish.',
|
|
5599
|
-
},
|
|
5584
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, {
|
|
5585
|
+
phase: 'server-after-done-lineage-violation',
|
|
5586
|
+
offendingTaskId: violation.taskId,
|
|
5587
|
+
offendingBranch: violation.branch,
|
|
5588
|
+
offendingSha: violation.sha,
|
|
5589
|
+
note: 'The task branch embeds another active task\'s commits; publishing would leak them into this PR. Rebase the branch onto origin/HEAD, then mark-complete to retry the publish.',
|
|
5600
5590
|
});
|
|
5601
5591
|
return null;
|
|
5602
5592
|
}
|
|
@@ -5611,15 +5601,7 @@ export class AgentManager {
|
|
|
5611
5601
|
const acquired = await this.acquireAgentForTask(devAgentId, taskId, 'server-after-done');
|
|
5612
5602
|
if (!acquired) {
|
|
5613
5603
|
await rollbackToken();
|
|
5614
|
-
await this.
|
|
5615
|
-
id: '',
|
|
5616
|
-
type: 'human.intervention',
|
|
5617
|
-
timestamp: new Date().toISOString(),
|
|
5618
|
-
projectId: task.projectId,
|
|
5619
|
-
agentId: devAgentId,
|
|
5620
|
-
taskId,
|
|
5621
|
-
data: { phase: 'server-after-done-dev-acquire-failed', devAgentId },
|
|
5622
|
-
});
|
|
5604
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, { phase: 'server-after-done-dev-acquire-failed', devAgentId });
|
|
5623
5605
|
return null;
|
|
5624
5606
|
}
|
|
5625
5607
|
await this.updateTask(taskId, { publishDispatchedAt: new Date().toISOString() });
|
|
@@ -5642,18 +5624,10 @@ export class AgentManager {
|
|
|
5642
5624
|
}
|
|
5643
5625
|
if (!resumed) {
|
|
5644
5626
|
await rollbackToken();
|
|
5645
|
-
await this.
|
|
5646
|
-
|
|
5647
|
-
|
|
5648
|
-
|
|
5649
|
-
projectId: task.projectId,
|
|
5650
|
-
agentId: devAgentId,
|
|
5651
|
-
taskId,
|
|
5652
|
-
data: {
|
|
5653
|
-
phase: 'server-after-done-resume-failed',
|
|
5654
|
-
devAgentId,
|
|
5655
|
-
note: 'Publish prompt was not delivered; mark-complete retries the publish dispatch.',
|
|
5656
|
-
},
|
|
5627
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, {
|
|
5628
|
+
phase: 'server-after-done-resume-failed',
|
|
5629
|
+
devAgentId,
|
|
5630
|
+
note: 'Publish prompt was not delivered; mark-complete retries the publish dispatch.',
|
|
5657
5631
|
});
|
|
5658
5632
|
return null;
|
|
5659
5633
|
}
|
|
@@ -5792,6 +5766,9 @@ export class AgentManager {
|
|
|
5792
5766
|
if (this.markCompleteInFlight.has(taskId)) {
|
|
5793
5767
|
throw new ApiError(409, `Task ${taskId} is already being completed`);
|
|
5794
5768
|
}
|
|
5769
|
+
if (this.codeVerdictInFlight.has(taskId)) {
|
|
5770
|
+
throw new ApiError(409, `Task ${taskId} code verdict is already being processed`);
|
|
5771
|
+
}
|
|
5795
5772
|
this.markCompleteInFlight.add(taskId);
|
|
5796
5773
|
return fresh;
|
|
5797
5774
|
});
|
|
@@ -5802,18 +5779,11 @@ export class AgentManager {
|
|
|
5802
5779
|
}
|
|
5803
5780
|
catch (err) {
|
|
5804
5781
|
const message = err instanceof Error ? err.message : String(err);
|
|
5805
|
-
await this.
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
taskId: task.id,
|
|
5811
|
-
data: {
|
|
5812
|
-
phase: 'confirm-merge-failed',
|
|
5813
|
-
gate: task.status,
|
|
5814
|
-
error: message,
|
|
5815
|
-
note: 'Task stays at the gate: Confirm again to retry, or Cancel to retire the published artifact.',
|
|
5816
|
-
},
|
|
5782
|
+
await this.emitIntervention(task.projectId, undefined, task.id, {
|
|
5783
|
+
phase: 'confirm-merge-failed',
|
|
5784
|
+
gate: task.status,
|
|
5785
|
+
error: message,
|
|
5786
|
+
note: 'Task stays at the gate: Confirm again to retry, or Cancel to retire the published artifact.',
|
|
5817
5787
|
});
|
|
5818
5788
|
throw new ApiError(409, `Merge failed for task ${task.id}: ${message}`);
|
|
5819
5789
|
}
|
|
@@ -5836,12 +5806,26 @@ export class AgentManager {
|
|
|
5836
5806
|
const state = await this.agentStore.get(id);
|
|
5837
5807
|
if (!state || state.taskId !== taskId)
|
|
5838
5808
|
continue;
|
|
5809
|
+
if (this.startCompactionThenRelease(id, state, taskId))
|
|
5810
|
+
continue;
|
|
5839
5811
|
await this.releaseAgentForTask(id, taskId, 'idle', { allowAwaitingHuman: true })
|
|
5840
5812
|
.catch(err => {
|
|
5841
5813
|
console.warn(`[AgentManager] confirm release ${id} failed:`, err);
|
|
5842
5814
|
});
|
|
5843
5815
|
}
|
|
5844
5816
|
}
|
|
5817
|
+
startCompactionThenRelease(agentId, state, taskId) {
|
|
5818
|
+
// 人工介入/重建中的 pane 不发 /clear;confirm 语义要求解绑,落回直接释放
|
|
5819
|
+
if (!state.paneId || state.status === 'awaiting_human' || state.creationToken)
|
|
5820
|
+
return false;
|
|
5821
|
+
const cfg = this.getAgentConfig(agentId);
|
|
5822
|
+
if (!cfg)
|
|
5823
|
+
return false;
|
|
5824
|
+
const tmux = new TmuxManager(this.createRunnerFor(cfg));
|
|
5825
|
+
void this.runPostMergeCompaction(tmux, state.paneId, agentId, taskId, agentRuntimeKindFor(cfg))
|
|
5826
|
+
.catch(err => console.warn(`[AgentManager] releaseTaskAgents compaction(${agentId}) failed:`, err));
|
|
5827
|
+
return true;
|
|
5828
|
+
}
|
|
5845
5829
|
repoMergeQueue = new Map();
|
|
5846
5830
|
async ffMergeBranch(task) {
|
|
5847
5831
|
const dev = this.getAgentConfig(task.agentId);
|