baxian 1.2.52 → 1.2.53
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/manager.d.ts +33 -6
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +320 -376
- 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/prompt.d.ts +8 -0
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +22 -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 +19 -23
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.d.ts +0 -5
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +6 -6
- 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/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +27 -0
- package/dist/api/tasks.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 +28 -32
- 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 +1 -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-DAPd4Nv_.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;
|
|
@@ -101,7 +95,7 @@ function cancelPhaseDowngrades(prev, next) {
|
|
|
101
95
|
return cancelPhaseRank(next) < cancelPhaseRank(prev);
|
|
102
96
|
}
|
|
103
97
|
const REGREET_REQUIRED_HOLD_PHASES = new Set(['greeting_failed']);
|
|
104
|
-
|
|
98
|
+
function shouldReleaseHeldBinding(state, boundTask) {
|
|
105
99
|
if (state.awaitingPhase != null && REGREET_REQUIRED_HOLD_PHASES.has(state.awaitingPhase))
|
|
106
100
|
return false;
|
|
107
101
|
const taskIsTerminal = !!boundTask && TERMINAL_STATUSES.includes(boundTask.status);
|
|
@@ -147,6 +141,7 @@ export class AgentManager {
|
|
|
147
141
|
manualReviewInFlight = new Set();
|
|
148
142
|
markCompleteInFlight = new Set();
|
|
149
143
|
specVerdictInFlight = new Set();
|
|
144
|
+
codeVerdictInFlight = new Set();
|
|
150
145
|
// 引用计数:cancelTask 与 startCreatedTaskSession 的清理段可合法并存(启动期 Stop),先完成者不得清掉后者的 guard。
|
|
151
146
|
cancelCleanupInFlight = new Map();
|
|
152
147
|
deletionInFlight = new Set();
|
|
@@ -240,6 +235,17 @@ export class AgentManager {
|
|
|
240
235
|
console.warn(`[AgentManager] safeEmit ${event.type} failed (audit log loss; state machine unaffected):`, err);
|
|
241
236
|
}
|
|
242
237
|
}
|
|
238
|
+
async emitIntervention(projectId, agentId, taskId, data) {
|
|
239
|
+
await this.safeEmit({
|
|
240
|
+
id: '',
|
|
241
|
+
type: 'human.intervention',
|
|
242
|
+
timestamp: new Date().toISOString(),
|
|
243
|
+
projectId,
|
|
244
|
+
...(agentId ? { agentId } : {}),
|
|
245
|
+
...(taskId ? { taskId } : {}),
|
|
246
|
+
data,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
243
249
|
async recordError(input) {
|
|
244
250
|
if (!this.errorRecordStore)
|
|
245
251
|
return;
|
|
@@ -591,14 +597,7 @@ export class AgentManager {
|
|
|
591
597
|
});
|
|
592
598
|
if (!wrote)
|
|
593
599
|
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
|
-
});
|
|
600
|
+
await this.emitIntervention(existing.projectId, agentId, undefined, { phase: 'greeting_failed', reason });
|
|
602
601
|
}
|
|
603
602
|
async regreetHeldAgent(agentId) {
|
|
604
603
|
const agent = this.getAgentConfig(agentId);
|
|
@@ -696,17 +695,9 @@ export class AgentManager {
|
|
|
696
695
|
});
|
|
697
696
|
if (!wrote)
|
|
698
697
|
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
|
-
},
|
|
698
|
+
await this.emitIntervention(projectIdForEmit, agentId, taskIdForEmit, {
|
|
699
|
+
phase: 'agent_dialog_pending',
|
|
700
|
+
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
701
|
});
|
|
711
702
|
}
|
|
712
703
|
async markBootstrapFailed(agentId, creationToken, errorMessage) {
|
|
@@ -917,16 +908,9 @@ export class AgentManager {
|
|
|
917
908
|
});
|
|
918
909
|
}
|
|
919
910
|
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
|
-
},
|
|
911
|
+
await this.emitIntervention(projectIdForEmit, agentId, undefined, {
|
|
912
|
+
phase: 'agent_dialog_resolved_runtime',
|
|
913
|
+
note: 'Runtime dialog resolved; agent REPL ready. Click Resume to continue.',
|
|
930
914
|
});
|
|
931
915
|
}
|
|
932
916
|
return;
|
|
@@ -990,18 +974,9 @@ export class AgentManager {
|
|
|
990
974
|
const onMenu = detectRuntimeMenu(stripped);
|
|
991
975
|
if (onMenu && !pendingMenu) {
|
|
992
976
|
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
|
-
},
|
|
977
|
+
await this.emitIntervention(fresh.projectId, agentId, fresh.taskId, {
|
|
978
|
+
phase: 'agent_runtime_menu_pending',
|
|
979
|
+
note: 'Agent paused on an interactive menu mid-task. Attach via web terminal and respond; baxian will auto-clear once the menu closes.',
|
|
1005
980
|
});
|
|
1006
981
|
}
|
|
1007
982
|
else if (!onMenu && pendingMenu) {
|
|
@@ -1349,17 +1324,9 @@ export class AgentManager {
|
|
|
1349
1324
|
});
|
|
1350
1325
|
if (!wrote)
|
|
1351
1326
|
return;
|
|
1352
|
-
await this.
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
timestamp: now,
|
|
1356
|
-
projectId,
|
|
1357
|
-
agentId,
|
|
1358
|
-
...(taskId ? { taskId } : {}),
|
|
1359
|
-
data: {
|
|
1360
|
-
phase,
|
|
1361
|
-
reason,
|
|
1362
|
-
},
|
|
1327
|
+
await this.emitIntervention(projectId, agentId, taskId, {
|
|
1328
|
+
phase,
|
|
1329
|
+
reason,
|
|
1363
1330
|
});
|
|
1364
1331
|
}
|
|
1365
1332
|
async markAwaitingIfAckUnknown(agentId, err, expectedTaskId) {
|
|
@@ -1480,18 +1447,10 @@ export class AgentManager {
|
|
|
1480
1447
|
if (shouldReleaseBinding) {
|
|
1481
1448
|
await this.lockManager.release(agentId);
|
|
1482
1449
|
}
|
|
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
|
-
},
|
|
1450
|
+
await this.emitIntervention(state.projectId, agentId, state.taskId, {
|
|
1451
|
+
phase: 'resumed',
|
|
1452
|
+
previousPhase: state.awaitingPhase,
|
|
1453
|
+
releasedBinding: shouldReleaseBinding,
|
|
1495
1454
|
});
|
|
1496
1455
|
return { resumed: true, releasedBinding: shouldReleaseBinding };
|
|
1497
1456
|
});
|
|
@@ -1686,19 +1645,11 @@ export class AgentManager {
|
|
|
1686
1645
|
catch (releaseErr) {
|
|
1687
1646
|
console.warn(`[AgentManager] failTaskForDispatchError: releaseAgentForTask(${agentId}) failed:`, releaseErr);
|
|
1688
1647
|
}
|
|
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
|
-
},
|
|
1648
|
+
await this.emitIntervention(transitioned?.task.projectId ?? '', agentId, taskId, {
|
|
1649
|
+
phase: `dispatch-failed:${err.reason}`,
|
|
1650
|
+
reason: err.reason,
|
|
1651
|
+
message: err.message,
|
|
1652
|
+
replDrained: err.replDrained,
|
|
1702
1653
|
});
|
|
1703
1654
|
}
|
|
1704
1655
|
async failTasksForAgent(agentId, reason, opts = {}) {
|
|
@@ -2081,6 +2032,10 @@ export class AgentManager {
|
|
|
2081
2032
|
const scanSnapshotOnRecover = isServerProtocol
|
|
2082
2033
|
|| (task.phase === undefined && task.status === 'in_progress')
|
|
2083
2034
|
|| (task.phase !== 'spec' && (task.status === 'review' || task.status === 'fixing'));
|
|
2035
|
+
const allowRecoveredReadFile = task.status === 'review'
|
|
2036
|
+
&& (task.phase === 'spec'
|
|
2037
|
+
|| (task.reviewMode === 'server' && task.batchTotal !== undefined)
|
|
2038
|
+
|| (task.reviewMode === 'server' && task.reviewWorktreeMode === 'base'));
|
|
2084
2039
|
try {
|
|
2085
2040
|
await this.startPhaseSignalWatch({
|
|
2086
2041
|
taskId: task.id,
|
|
@@ -2090,23 +2045,15 @@ export class AgentManager {
|
|
|
2090
2045
|
token: task.signalToken,
|
|
2091
2046
|
skipSnapshot: !scanSnapshotOnRecover,
|
|
2092
2047
|
recovered: true,
|
|
2093
|
-
...(
|
|
2048
|
+
...(allowRecoveredReadFile
|
|
2094
2049
|
? { onReadFile: (req) => { void this.handleReadFileRequest(task.id, agentId, req); } }
|
|
2095
2050
|
: {}),
|
|
2096
2051
|
});
|
|
2097
2052
|
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
|
-
},
|
|
2053
|
+
await this.emitIntervention(task.projectId, agentId, task.id, {
|
|
2054
|
+
phase: 'spec-signal-setup-during-recovery',
|
|
2055
|
+
kind: interventionKindLabel,
|
|
2056
|
+
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
2057
|
});
|
|
2111
2058
|
}
|
|
2112
2059
|
}
|
|
@@ -2318,18 +2265,10 @@ export class AgentManager {
|
|
|
2318
2265
|
rolledBack = { projectId: task.projectId };
|
|
2319
2266
|
});
|
|
2320
2267
|
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
|
-
},
|
|
2268
|
+
await this.emitIntervention(rolledBack.projectId, agentId, taskId, {
|
|
2269
|
+
phase: reason.phase,
|
|
2270
|
+
message: reason.message,
|
|
2271
|
+
note: 'Dispatch failed; the task is back in pending — re-dispatch it once the cause is resolved.',
|
|
2333
2272
|
});
|
|
2334
2273
|
}
|
|
2335
2274
|
const existing = await this.agentStore.get(agentId);
|
|
@@ -2944,14 +2883,23 @@ export class AgentManager {
|
|
|
2944
2883
|
const worktree = new WorktreeManager(runner);
|
|
2945
2884
|
const tmux = new TmuxManager(runner);
|
|
2946
2885
|
const isServerQaPhase = phase === 'server-review' || phase === 'server-recheck' || phase === 'server-spec-review';
|
|
2886
|
+
const isServerCodeReviewPhase = phase === 'server-review' || phase === 'server-recheck';
|
|
2947
2887
|
const customBranch = task.branch && !task.branch.startsWith(BRANCH_PREFIX) ? task.branch : undefined;
|
|
2948
|
-
//
|
|
2949
|
-
//
|
|
2950
|
-
const
|
|
2888
|
+
// Server code reviews materialize the captured head; other QA modes stay off origin/HEAD.
|
|
2889
|
+
// The fail-fast base resolution only runs where the default base is used.
|
|
2890
|
+
const reviewWorktree = isServerCodeReviewPhase && opts.serverContent !== undefined
|
|
2891
|
+
? await worktree.createDetachedForReviewHead(workdir, taskId, {
|
|
2892
|
+
patch: opts.serverContent,
|
|
2893
|
+
baseSha: opts.serverBaseSha,
|
|
2894
|
+
headSha: opts.serverHeadSha,
|
|
2895
|
+
headTree: opts.serverHeadTree,
|
|
2896
|
+
})
|
|
2897
|
+
: undefined;
|
|
2898
|
+
const worktreePath = reviewWorktree?.path ?? (isServerQaPhase
|
|
2951
2899
|
? await worktree.createDetachedAtBase(workdir, taskId)
|
|
2952
2900
|
: phase === 'review' || phase === 'recheck'
|
|
2953
2901
|
? await worktree.createDetached(workdir, taskId, task.branch)
|
|
2954
|
-
: await worktree.create(workdir, taskId, agent.workdir ? undefined : await this.resolveAutoBaseRef(runner, workdir), customBranch);
|
|
2902
|
+
: await worktree.create(workdir, taskId, agent.workdir ? undefined : await this.resolveAutoBaseRef(runner, workdir), customBranch));
|
|
2955
2903
|
await this.agentStore.update(agentId, (stateNow) => {
|
|
2956
2904
|
if (!stateNow || stateNow.taskId !== taskId)
|
|
2957
2905
|
return AGENT_STORE_NOOP;
|
|
@@ -2972,7 +2920,7 @@ export class AgentManager {
|
|
|
2972
2920
|
try {
|
|
2973
2921
|
const imagePaths = await this.imagePathsForDispatch(runner, task, phase);
|
|
2974
2922
|
let payloadOpts = {};
|
|
2975
|
-
if (opts.serverContent !== undefined || opts.serverPriorFindings || opts.serverPriorResponse) {
|
|
2923
|
+
if (opts.serverContent !== undefined || opts.serverDiffstat !== undefined || opts.serverInterdiff !== undefined || opts.serverPriorFindings || opts.serverPriorResponse) {
|
|
2976
2924
|
payloadOpts = await resolveServerPayloads(this.getReviewTransport(), agent, worktreePath, {
|
|
2977
2925
|
phase,
|
|
2978
2926
|
...(task.phase ? { taskPhase: task.phase } : {}),
|
|
@@ -2980,6 +2928,9 @@ export class AgentManager {
|
|
|
2980
2928
|
reviewRound: task.reviewRound,
|
|
2981
2929
|
...(opts.serverBatch ? { batch: opts.serverBatch } : {}),
|
|
2982
2930
|
...(opts.serverContent !== undefined ? { serverContent: opts.serverContent } : {}),
|
|
2931
|
+
...(reviewWorktree?.mode === 'head' ? { forceContentFile: true } : {}),
|
|
2932
|
+
...(opts.serverDiffstat !== undefined ? { serverDiffstat: opts.serverDiffstat } : {}),
|
|
2933
|
+
...(opts.serverInterdiff !== undefined ? { serverInterdiff: opts.serverInterdiff } : {}),
|
|
2983
2934
|
...(opts.serverPriorFindings ? { serverPriorFindings: opts.serverPriorFindings } : {}),
|
|
2984
2935
|
...(opts.serverPriorResponse ? { serverPriorResponse: opts.serverPriorResponse } : {}),
|
|
2985
2936
|
});
|
|
@@ -2994,7 +2945,11 @@ export class AgentManager {
|
|
|
2994
2945
|
...(promptSignalToken ? { signalToken: promptSignalToken } : {}),
|
|
2995
2946
|
...(promptSpecRound !== undefined ? { currentSpecRound: promptSpecRound } : {}),
|
|
2996
2947
|
...(imagePaths.length ? { imagePaths } : {}),
|
|
2997
|
-
...(
|
|
2948
|
+
...(reviewWorktree ? { serverReviewWorktree: reviewWorktree.mode } : {}),
|
|
2949
|
+
...(reviewWorktree?.fallbackReason ? { serverReviewFallbackReason: reviewWorktree.fallbackReason } : {}),
|
|
2950
|
+
...(opts.serverBaseSha !== undefined ? { serverBaseSha: opts.serverBaseSha } : {}),
|
|
2951
|
+
...(opts.serverHeadSha !== undefined ? { serverHeadSha: opts.serverHeadSha } : {}),
|
|
2952
|
+
...(opts.serverHeadTree !== undefined ? { serverHeadTree: opts.serverHeadTree } : {}),
|
|
2998
2953
|
...(opts.serverBatch ? { serverBatch: opts.serverBatch } : {}),
|
|
2999
2954
|
...payloadOpts,
|
|
3000
2955
|
});
|
|
@@ -3060,7 +3015,7 @@ export class AgentManager {
|
|
|
3060
3015
|
catch { }
|
|
3061
3016
|
return false;
|
|
3062
3017
|
}
|
|
3063
|
-
if (opts.armBeforeInject && !(await opts.armBeforeInject())) {
|
|
3018
|
+
if (opts.armBeforeInject && !(await opts.armBeforeInject({ serverReviewWorktree: reviewWorktree?.mode }))) {
|
|
3064
3019
|
try {
|
|
3065
3020
|
await worktree.removeWithBranch(workdir, worktreePath, customBranch);
|
|
3066
3021
|
}
|
|
@@ -3244,21 +3199,13 @@ export class AgentManager {
|
|
|
3244
3199
|
}
|
|
3245
3200
|
console.warn(`[AgentManager] dispatch ack timeout for pane ${paneId} agent ${agentId}: ${message}`);
|
|
3246
3201
|
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
|
-
},
|
|
3202
|
+
await this.emitIntervention(state?.projectId ?? '', agentId, state?.taskId, {
|
|
3203
|
+
phase: 'dispatch-ack-timeout',
|
|
3204
|
+
paneId,
|
|
3205
|
+
message,
|
|
3206
|
+
note: 'REPL did not acknowledge the pasted prompt within timeout. ' +
|
|
3207
|
+
'baxian intentionally did NOT send C-c — the input may still be queued. ' +
|
|
3208
|
+
'Attach via web terminal to verify and resolve.',
|
|
3262
3209
|
});
|
|
3263
3210
|
const composerDelivered = !/pane already busy at baseline/.test(message);
|
|
3264
3211
|
return { acked: false, composerDelivered };
|
|
@@ -3345,7 +3292,7 @@ export class AgentManager {
|
|
|
3345
3292
|
&& !ensure.freshRuntime;
|
|
3346
3293
|
const imagePaths = await this.imagePathsForDispatch(runner, task, phase);
|
|
3347
3294
|
let payloadOpts = {};
|
|
3348
|
-
if (opts.serverContent !== undefined || opts.serverPriorFindings || opts.serverPriorResponse) {
|
|
3295
|
+
if (opts.serverContent !== undefined || opts.serverDiffstat !== undefined || opts.serverInterdiff !== undefined || opts.serverPriorFindings || opts.serverPriorResponse) {
|
|
3349
3296
|
payloadOpts = await resolveServerPayloads(this.getReviewTransport(), agent, worktreePath, {
|
|
3350
3297
|
phase,
|
|
3351
3298
|
...(task.phase ? { taskPhase: task.phase } : {}),
|
|
@@ -3353,6 +3300,8 @@ export class AgentManager {
|
|
|
3353
3300
|
reviewRound: task.reviewRound,
|
|
3354
3301
|
...(opts.serverBatch ? { batch: opts.serverBatch } : {}),
|
|
3355
3302
|
...(opts.serverContent !== undefined ? { serverContent: opts.serverContent } : {}),
|
|
3303
|
+
...(opts.serverDiffstat !== undefined ? { serverDiffstat: opts.serverDiffstat } : {}),
|
|
3304
|
+
...(opts.serverInterdiff !== undefined ? { serverInterdiff: opts.serverInterdiff } : {}),
|
|
3356
3305
|
...(opts.serverPriorFindings ? { serverPriorFindings: opts.serverPriorFindings } : {}),
|
|
3357
3306
|
...(opts.serverPriorResponse ? { serverPriorResponse: opts.serverPriorResponse } : {}),
|
|
3358
3307
|
});
|
|
@@ -3369,7 +3318,6 @@ export class AgentManager {
|
|
|
3369
3318
|
: {}),
|
|
3370
3319
|
...(promptSpecRound !== undefined ? { currentSpecRound: promptSpecRound } : {}),
|
|
3371
3320
|
...(imagePaths.length ? { imagePaths } : {}),
|
|
3372
|
-
...(opts.serverDiffstat !== undefined ? { serverDiffstat: opts.serverDiffstat } : {}),
|
|
3373
3321
|
...(opts.serverBatch ? { serverBatch: opts.serverBatch } : {}),
|
|
3374
3322
|
...(opts.serverAfterDone ? { serverAfterDone: opts.serverAfterDone } : {}),
|
|
3375
3323
|
...payloadOpts,
|
|
@@ -3415,7 +3363,7 @@ export class AgentManager {
|
|
|
3415
3363
|
return false;
|
|
3416
3364
|
}
|
|
3417
3365
|
}
|
|
3418
|
-
if (opts.armBeforeInject && !(await opts.armBeforeInject())) {
|
|
3366
|
+
if (opts.armBeforeInject && !(await opts.armBeforeInject({}))) {
|
|
3419
3367
|
return false;
|
|
3420
3368
|
}
|
|
3421
3369
|
const now = new Date().toISOString();
|
|
@@ -3524,9 +3472,7 @@ export class AgentManager {
|
|
|
3524
3472
|
const boundTask = state.taskId ? await this.taskStore.get(state.taskId) : null;
|
|
3525
3473
|
if (state.taskId
|
|
3526
3474
|
&& boundTask?.id === state.taskId
|
|
3527
|
-
&& boundTask.status === 'merged'
|
|
3528
|
-
&& boundTask.prNumber != null
|
|
3529
|
-
&& boundTask.branch
|
|
3475
|
+
&& (boundTask.status === 'merged' || boundTask.status === 'done')
|
|
3530
3476
|
&& !state.creationToken
|
|
3531
3477
|
&& state.status !== 'awaiting_human') {
|
|
3532
3478
|
try {
|
|
@@ -3544,11 +3490,15 @@ export class AgentManager {
|
|
|
3544
3490
|
|| recovered.status === 'awaiting_human') {
|
|
3545
3491
|
continue;
|
|
3546
3492
|
}
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3493
|
+
if (boundTask.status === 'merged' && boundTask.prNumber != null && boundTask.branch) {
|
|
3494
|
+
await this.dispatchPostMergeCleanup(state.id, {
|
|
3495
|
+
taskId: boundTask.id,
|
|
3496
|
+
branch: boundTask.branch,
|
|
3497
|
+
});
|
|
3498
|
+
continue;
|
|
3499
|
+
}
|
|
3500
|
+
if (this.startCompactionThenRelease(recovered.id, recovered, boundTask.id))
|
|
3501
|
+
continue;
|
|
3552
3502
|
}
|
|
3553
3503
|
catch (cleanupErr) {
|
|
3554
3504
|
console.warn(`[recover] dispatchPostMergeCleanup(${state.id}, ${boundTask.id}) failed:`, cleanupErr);
|
|
@@ -3659,15 +3609,7 @@ export class AgentManager {
|
|
|
3659
3609
|
observation: { phase: 'recovery-failed' },
|
|
3660
3610
|
recommendation: 'Inspect or recreate the tmux session, then retry the affected task.',
|
|
3661
3611
|
});
|
|
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
|
-
});
|
|
3612
|
+
await this.emitIntervention(state.projectId, state.id, state.taskId, { phase: 'recovery-failed', error: message });
|
|
3671
3613
|
}
|
|
3672
3614
|
}
|
|
3673
3615
|
for (const c of deferredCleanups) {
|
|
@@ -3847,19 +3789,12 @@ export class AgentManager {
|
|
|
3847
3789
|
}
|
|
3848
3790
|
if (publishedCleanup) {
|
|
3849
3791
|
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
|
-
},
|
|
3792
|
+
await this.emitIntervention(result.projectId, undefined, taskId, {
|
|
3793
|
+
phase: 'cancel-published-artifact-cleanup-skipped',
|
|
3794
|
+
afterDone: publishedCleanup.afterDone,
|
|
3795
|
+
branch: publishedCleanup.branch,
|
|
3796
|
+
...(publishedCleanup.prNumber !== undefined ? { prNumber: publishedCleanup.prNumber } : {}),
|
|
3797
|
+
reason: 'dev pane stop unconfirmed; the publish prompt may still be running and would recreate the remote artifacts',
|
|
3863
3798
|
});
|
|
3864
3799
|
return result;
|
|
3865
3800
|
}
|
|
@@ -3896,19 +3831,12 @@ export class AgentManager {
|
|
|
3896
3831
|
}
|
|
3897
3832
|
catch (err) {
|
|
3898
3833
|
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
|
-
},
|
|
3834
|
+
await this.emitIntervention(result.projectId, undefined, taskId, {
|
|
3835
|
+
phase: 'cancel-published-artifact-cleanup-failed',
|
|
3836
|
+
afterDone: publishedCleanup.afterDone,
|
|
3837
|
+
branch: publishedCleanup.branch,
|
|
3838
|
+
...(publishedCleanup.prNumber !== undefined ? { prNumber: publishedCleanup.prNumber } : {}),
|
|
3839
|
+
error: err instanceof Error ? err.message : String(err),
|
|
3912
3840
|
});
|
|
3913
3841
|
}
|
|
3914
3842
|
}
|
|
@@ -4420,18 +4348,10 @@ export class AgentManager {
|
|
|
4420
4348
|
const cur = await this.agentStore.get(agentId);
|
|
4421
4349
|
if (!cur || cur.taskId !== expectedTaskId)
|
|
4422
4350
|
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
|
-
},
|
|
4351
|
+
await this.emitIntervention(cur.projectId, agentId, expectedTaskId, {
|
|
4352
|
+
phase: 'manual-review-dev-parked-qa-failed',
|
|
4353
|
+
note: 'Manual QA dispatch parked the dev agent into waiting; QA then failed to start. ' +
|
|
4354
|
+
'Dev binding is kept but no new prompt is running — re-dispatch the manual review or cancel the task.',
|
|
4435
4355
|
});
|
|
4436
4356
|
}
|
|
4437
4357
|
async retryTask(taskId) {
|
|
@@ -5008,30 +4928,14 @@ export class AgentManager {
|
|
|
5008
4928
|
if (task.agentId) {
|
|
5009
4929
|
const devOk = await this.markAgentWaiting(task.agentId, taskId).catch(() => false);
|
|
5010
4930
|
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
|
-
});
|
|
4931
|
+
await this.emitIntervention(task.projectId, task.agentId, taskId, { phase: 'spec-ready-dev-park-failed', devAgentId: task.agentId });
|
|
5020
4932
|
}
|
|
5021
4933
|
}
|
|
5022
4934
|
if (task.qaAgentId) {
|
|
5023
4935
|
const released = await this.releaseAgentForTask(task.qaAgentId, taskId, 'idle')
|
|
5024
4936
|
.catch(() => false);
|
|
5025
4937
|
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
|
-
});
|
|
4938
|
+
await this.emitIntervention(task.projectId, task.qaAgentId, taskId, { phase: 'spec-ready-qa-release-failed', qaAgentId: task.qaAgentId });
|
|
5035
4939
|
}
|
|
5036
4940
|
}
|
|
5037
4941
|
return transition.task;
|
|
@@ -5103,6 +5007,111 @@ export class AgentManager {
|
|
|
5103
5007
|
throw new ApiError(500, `Failed to dispatch spec fix for task ${taskId}`);
|
|
5104
5008
|
return dispatched;
|
|
5105
5009
|
}
|
|
5010
|
+
async submitCodeVerdict(taskId, comments) {
|
|
5011
|
+
const trimmed = comments?.trim();
|
|
5012
|
+
if (!trimmed) {
|
|
5013
|
+
throw new ApiError(400, 'comments is required for request-changes');
|
|
5014
|
+
}
|
|
5015
|
+
const task = await this.withTaskLock(async () => {
|
|
5016
|
+
const fresh = await this.taskStore.get(taskId);
|
|
5017
|
+
if (!fresh)
|
|
5018
|
+
throw new ApiError(404, `Task ${taskId} not found`);
|
|
5019
|
+
if (fresh.status !== 'ready') {
|
|
5020
|
+
throw new ApiError(409, `Task ${taskId} is ${fresh.status}; code verdict requires ready`);
|
|
5021
|
+
}
|
|
5022
|
+
if (fresh.phase === 'spec') {
|
|
5023
|
+
throw new ApiError(409, `Task ${taskId} is in the spec phase; reject the spec via the spec verdict instead`);
|
|
5024
|
+
}
|
|
5025
|
+
if (fresh.reviewMode !== 'server') {
|
|
5026
|
+
throw new ApiError(409, `Task ${taskId} is not in server review mode; request changes on the PR instead`);
|
|
5027
|
+
}
|
|
5028
|
+
if (!fresh.agentId) {
|
|
5029
|
+
throw new ApiError(400, `Task ${taskId} has no dev agent; cannot request changes`);
|
|
5030
|
+
}
|
|
5031
|
+
if (this.markCompleteInFlight.has(taskId)) {
|
|
5032
|
+
throw new ApiError(409, `Task ${taskId} is being completed (merge in progress); try again shortly`);
|
|
5033
|
+
}
|
|
5034
|
+
if (this.codeVerdictInFlight.has(taskId)) {
|
|
5035
|
+
throw new ApiError(409, `Task ${taskId} code verdict is already being processed`);
|
|
5036
|
+
}
|
|
5037
|
+
this.codeVerdictInFlight.add(taskId);
|
|
5038
|
+
return fresh;
|
|
5039
|
+
});
|
|
5040
|
+
try {
|
|
5041
|
+
return await this.executeCodeVerdict(task, trimmed);
|
|
5042
|
+
}
|
|
5043
|
+
finally {
|
|
5044
|
+
this.codeVerdictInFlight.delete(taskId);
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
async executeCodeVerdict(task, trimmed) {
|
|
5048
|
+
const taskId = task.id;
|
|
5049
|
+
const store = this.getReviewStore();
|
|
5050
|
+
if (!store)
|
|
5051
|
+
throw new ApiError(500, 'Review store unavailable');
|
|
5052
|
+
const round = Math.max(task.reviewRound, 1);
|
|
5053
|
+
const at = new Date().toISOString();
|
|
5054
|
+
// 打回消耗一轮修订;到达上限时拒绝而非派发注定进 max_rounds 的修订,让用户当场决策
|
|
5055
|
+
const cap = this.getConfig().review.rounds + (task.maxRoundsContinues ?? 0);
|
|
5056
|
+
if (round >= cap) {
|
|
5057
|
+
throw new ApiError(409, `Task ${taskId} has reached the code review round cap (${cap}); `
|
|
5058
|
+
+ 'confirm completion, or cancel the task');
|
|
5059
|
+
}
|
|
5060
|
+
const roundData = await store.getRound(taskId, 'code', round);
|
|
5061
|
+
// 无 QA 项目经 autoApproveCode 到达 ready 时从未存轮;补底轮次保证 userDecision 留痕与 fix 闭环
|
|
5062
|
+
const base = roundData ?? await this.synthesizeEmptyCodeRound(task, round, at);
|
|
5063
|
+
// runCodeFixSubmission 按 Math.max(reviewRound,1) 读轮、recheck 按 reviewRound+1 存新轮;
|
|
5064
|
+
// reviewRound 停在 0 会让 recheck 覆写本轮,故落库为 1 对齐两侧
|
|
5065
|
+
if (task.reviewRound === 0) {
|
|
5066
|
+
await this.withTaskLock(async () => {
|
|
5067
|
+
const fresh = await this.taskStore.get(taskId);
|
|
5068
|
+
if (!fresh)
|
|
5069
|
+
return;
|
|
5070
|
+
fresh.reviewRound = 1;
|
|
5071
|
+
fresh.updatedAt = new Date().toISOString();
|
|
5072
|
+
await this.taskStore.set(fresh);
|
|
5073
|
+
});
|
|
5074
|
+
}
|
|
5075
|
+
const priorFindings = base.findings?.findings ?? [];
|
|
5076
|
+
const nextUserId = `u-${priorFindings.filter(f => f.id.startsWith('u-')).length + 1}`;
|
|
5077
|
+
const mergedFindings = {
|
|
5078
|
+
round,
|
|
5079
|
+
verdict: 'request-changes',
|
|
5080
|
+
findings: [...priorFindings, { id: nextUserId, severity: 'major', message: trimmed }],
|
|
5081
|
+
};
|
|
5082
|
+
await store.putRound(taskId, 'code', {
|
|
5083
|
+
...base,
|
|
5084
|
+
findings: mergedFindings,
|
|
5085
|
+
userDecision: { verdict: 'request-changes', comments: trimmed, at },
|
|
5086
|
+
});
|
|
5087
|
+
const dispatched = await this.dispatchServerFixToDev(taskId, JSON.stringify(mergedFindings));
|
|
5088
|
+
if (!dispatched)
|
|
5089
|
+
throw new ApiError(500, `Failed to dispatch code fix for task ${taskId}`);
|
|
5090
|
+
return dispatched;
|
|
5091
|
+
}
|
|
5092
|
+
async synthesizeEmptyCodeRound(task, round, at) {
|
|
5093
|
+
const empty = { round, phase: 'code', content: '', startedAt: at };
|
|
5094
|
+
const dev = task.agentId ? this.getAgentConfig(task.agentId) : undefined;
|
|
5095
|
+
if (!dev || !task.agentId)
|
|
5096
|
+
return empty;
|
|
5097
|
+
// 读取失败不阻断打回:留痕与 fix 闭环优先于 diff 展示
|
|
5098
|
+
try {
|
|
5099
|
+
await this.refreshWorktreeCacheFor(task.agentId);
|
|
5100
|
+
const content = await this.getReviewTransport().readContent(task, dev, 'code');
|
|
5101
|
+
return {
|
|
5102
|
+
round,
|
|
5103
|
+
phase: 'code',
|
|
5104
|
+
content: content.content,
|
|
5105
|
+
...(content.diffstat ? { diffstat: content.diffstat } : {}),
|
|
5106
|
+
...(content.baseSha ? { baseSha: content.baseSha } : {}),
|
|
5107
|
+
startedAt: at,
|
|
5108
|
+
};
|
|
5109
|
+
}
|
|
5110
|
+
catch (err) {
|
|
5111
|
+
console.warn(`[AgentManager] submitCodeVerdict: code content read failed for ${task.id}; using empty round:`, err);
|
|
5112
|
+
return empty;
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5106
5115
|
async transitionToCodePhase(taskId) {
|
|
5107
5116
|
const task = await this.taskStore.get(taskId);
|
|
5108
5117
|
if (!task)
|
|
@@ -5125,29 +5134,13 @@ export class AgentManager {
|
|
|
5125
5134
|
const released = await this.releaseAgentForTask(task.qaAgentId, taskId, 'idle')
|
|
5126
5135
|
.catch(() => false);
|
|
5127
5136
|
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
|
-
});
|
|
5137
|
+
await this.emitIntervention(task.projectId, task.qaAgentId, taskId, { phase: 'code-phase-qa-release-failed', qaAgentId: task.qaAgentId });
|
|
5137
5138
|
}
|
|
5138
5139
|
}
|
|
5139
5140
|
const acquired = await this.acquireAgentForTask(devAgentId, taskId, 'code');
|
|
5140
5141
|
if (!acquired) {
|
|
5141
5142
|
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
|
-
});
|
|
5143
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, { phase: 'code-dev-acquire-failed', devAgentId });
|
|
5151
5144
|
return null;
|
|
5152
5145
|
}
|
|
5153
5146
|
let resumed = false;
|
|
@@ -5166,15 +5159,7 @@ export class AgentManager {
|
|
|
5166
5159
|
}
|
|
5167
5160
|
if (!resumed) {
|
|
5168
5161
|
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
|
-
});
|
|
5162
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, { phase: 'code-resume-failed', devAgentId });
|
|
5178
5163
|
return null;
|
|
5179
5164
|
}
|
|
5180
5165
|
return await this.taskStore.get(taskId);
|
|
@@ -5203,15 +5188,7 @@ export class AgentManager {
|
|
|
5203
5188
|
? (opts.phase === 'spec' ? 'spec-fixed' : 'code-fixed')
|
|
5204
5189
|
: (opts.phase === 'spec' ? 'spec-done' : 'code-done');
|
|
5205
5190
|
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
|
-
});
|
|
5191
|
+
await this.emitIntervention(task.projectId, task.agentId, taskId, { phase: 'server-review-no-qa-partner', devAgentId: task.agentId });
|
|
5215
5192
|
return null;
|
|
5216
5193
|
}
|
|
5217
5194
|
const roundField = opts.phase === 'spec' ? (task.specReviewRound ?? 0) : task.reviewRound;
|
|
@@ -5261,15 +5238,7 @@ export class AgentManager {
|
|
|
5261
5238
|
const acquired = await this.acquireAgentForTask(qaId, taskId, dispatchPhase);
|
|
5262
5239
|
if (!acquired) {
|
|
5263
5240
|
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
|
-
});
|
|
5241
|
+
await this.emitIntervention(projectId, qaId, taskId, { phase: 'server-review-qa-acquire-failed', qaAgentId: qaId });
|
|
5273
5242
|
return null;
|
|
5274
5243
|
}
|
|
5275
5244
|
if (devAgentId) {
|
|
@@ -5277,19 +5246,19 @@ export class AgentManager {
|
|
|
5277
5246
|
if (!devOk) {
|
|
5278
5247
|
await this.releaseAgentForTask(qaId, taskId, 'idle').catch(() => undefined);
|
|
5279
5248
|
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
|
-
});
|
|
5249
|
+
await this.emitIntervention(projectId, devAgentId, taskId, { phase: 'server-review-dev-park-failed', devAgentId });
|
|
5289
5250
|
return null;
|
|
5290
5251
|
}
|
|
5291
5252
|
}
|
|
5292
5253
|
}
|
|
5254
|
+
else {
|
|
5255
|
+
const qaState = await this.agentStore.get(qaId);
|
|
5256
|
+
if (qaState?.taskId !== taskId) {
|
|
5257
|
+
await this.setupPhaseSignal(taskId, qaId, expectedKind, { skipSnapshot: true });
|
|
5258
|
+
await this.emitIntervention(projectId, qaId, taskId, { phase: 'server-review-continuation-qa-not-bound', qaAgentId: qaId });
|
|
5259
|
+
return null;
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5293
5262
|
const roundPatch = opts.phase === 'spec'
|
|
5294
5263
|
? { specReviewRound: newRound, phase: 'spec' }
|
|
5295
5264
|
: { reviewRound: newRound };
|
|
@@ -5301,33 +5270,38 @@ export class AgentManager {
|
|
|
5301
5270
|
...(opts.batch
|
|
5302
5271
|
? { batchIndex: opts.batch.index, batchTotal: opts.batch.total }
|
|
5303
5272
|
: { batchIndex: undefined, batchTotal: undefined }),
|
|
5273
|
+
// Fresh dispatch: clear any stale mode until startSession materializes the worktree,
|
|
5274
|
+
// so a crash in this window recovers conservatively (no read-file) rather than
|
|
5275
|
+
// re-arming it for what might be a head-mode review.
|
|
5276
|
+
...(opts.continuation ? {} : { reviewWorktreeMode: undefined }),
|
|
5304
5277
|
...roundPatch,
|
|
5305
5278
|
});
|
|
5306
5279
|
if (!transition) {
|
|
5307
5280
|
if (!opts.continuation) {
|
|
5308
5281
|
await this.releaseAgentForTask(qaId, taskId, 'idle').catch(() => undefined);
|
|
5309
5282
|
}
|
|
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
|
-
});
|
|
5283
|
+
await this.emitIntervention(projectId, qaId, taskId, { phase: 'server-review-transition-failed', qaAgentId: qaId });
|
|
5319
5284
|
return null;
|
|
5320
5285
|
}
|
|
5286
|
+
let dispatchedWorktreeMode;
|
|
5321
5287
|
const sessionOpts = {
|
|
5322
5288
|
bypassTaskStatusGate: true,
|
|
5323
5289
|
signalToken: newToken,
|
|
5324
5290
|
serverContent: opts.content,
|
|
5291
|
+
...(opts.interdiff !== undefined ? { serverInterdiff: opts.interdiff } : {}),
|
|
5325
5292
|
...(opts.diffstat !== undefined ? { serverDiffstat: opts.diffstat } : {}),
|
|
5293
|
+
...(opts.baseSha !== undefined ? { serverBaseSha: opts.baseSha } : {}),
|
|
5294
|
+
...(opts.reviewHeadAnchorSha !== undefined ? { serverHeadSha: opts.reviewHeadAnchorSha } : {}),
|
|
5295
|
+
...(opts.headTree !== undefined ? { serverHeadTree: opts.headTree } : {}),
|
|
5326
5296
|
...(opts.batch ? { serverBatch: opts.batch } : {}),
|
|
5327
5297
|
...(opts.priorFindingsJson ? { serverPriorFindings: opts.priorFindingsJson } : {}),
|
|
5328
5298
|
...(opts.priorResponseJson ? { serverPriorResponse: opts.priorResponseJson } : {}),
|
|
5329
5299
|
...(opts.phase === 'spec' ? { currentSpecRound: newRound } : {}),
|
|
5330
|
-
armBeforeInject: () =>
|
|
5300
|
+
armBeforeInject: (ctx) => {
|
|
5301
|
+
dispatchedWorktreeMode = ctx.serverReviewWorktree;
|
|
5302
|
+
const allowReadFile = opts.phase === 'spec' || opts.continuation || ctx.serverReviewWorktree === 'base';
|
|
5303
|
+
return this.setupPhaseSignalWatcher(taskId, qaId, expectedKind, newToken, false, allowReadFile ? (req) => { void this.handleReadFileRequest(taskId, qaId, req); } : undefined);
|
|
5304
|
+
},
|
|
5331
5305
|
};
|
|
5332
5306
|
const rearmConsumedSignal = async () => {
|
|
5333
5307
|
if (opts.continuation) {
|
|
@@ -5366,17 +5340,14 @@ export class AgentManager {
|
|
|
5366
5340
|
await this.releaseAgentForTask(qaId, taskId, 'idle').catch(() => undefined);
|
|
5367
5341
|
}
|
|
5368
5342
|
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
|
-
});
|
|
5343
|
+
await this.emitIntervention(projectId, qaId, taskId, { phase: 'server-review-start-failed', qaAgentId: qaId });
|
|
5378
5344
|
return null;
|
|
5379
5345
|
}
|
|
5346
|
+
// Persist the materialized worktree mode so a restart can re-arm read-file for a base
|
|
5347
|
+
// fallback review (whose prompt depends on it) but not for a head-mode one.
|
|
5348
|
+
if (opts.phase !== 'spec' && !opts.continuation) {
|
|
5349
|
+
await this.updateTask(taskId, { reviewWorktreeMode: dispatchedWorktreeMode }).catch(() => { });
|
|
5350
|
+
}
|
|
5380
5351
|
return await this.taskStore.get(taskId);
|
|
5381
5352
|
}
|
|
5382
5353
|
async dispatchServerFixToDev(taskId, findingsJson) {
|
|
@@ -5416,15 +5387,7 @@ export class AgentManager {
|
|
|
5416
5387
|
const acquired = await this.acquireAgentForTask(devAgentId, taskId, 'server-feedback');
|
|
5417
5388
|
if (!acquired) {
|
|
5418
5389
|
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
|
-
});
|
|
5390
|
+
await this.emitIntervention(projectId, devAgentId, taskId, { phase: 'server-fix-dev-acquire-failed', devAgentId });
|
|
5428
5391
|
return null;
|
|
5429
5392
|
}
|
|
5430
5393
|
if (qaAgentId) {
|
|
@@ -5432,29 +5395,13 @@ export class AgentManager {
|
|
|
5432
5395
|
.catch(() => false);
|
|
5433
5396
|
if (!released) {
|
|
5434
5397
|
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
|
-
});
|
|
5398
|
+
await this.emitIntervention(projectId, qaAgentId, taskId, { phase: 'server-fix-qa-release-failed', qaAgentId });
|
|
5444
5399
|
return null;
|
|
5445
5400
|
}
|
|
5446
5401
|
}
|
|
5447
|
-
const transition = await this.transitionTaskStatus(taskId, 'fixing', { fromStatus: ['review', 'max_rounds', 'spec-ready'] }, { signalToken: newToken, fixDispatchedAt: new Date().toISOString() });
|
|
5402
|
+
const transition = await this.transitionTaskStatus(taskId, 'fixing', { fromStatus: ['review', 'max_rounds', 'spec-ready', 'ready'] }, { signalToken: newToken, fixDispatchedAt: new Date().toISOString() });
|
|
5448
5403
|
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
|
-
});
|
|
5404
|
+
await this.emitIntervention(projectId, devAgentId, taskId, { phase: 'server-fix-transition-failed', devAgentId });
|
|
5458
5405
|
return null;
|
|
5459
5406
|
}
|
|
5460
5407
|
let resumed = false;
|
|
@@ -5485,15 +5432,7 @@ export class AgentManager {
|
|
|
5485
5432
|
await rollbackToEntry();
|
|
5486
5433
|
await this.releaseAgentForTask(devAgentId, taskId, 'idle').catch(() => undefined);
|
|
5487
5434
|
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
|
-
});
|
|
5435
|
+
await this.emitIntervention(projectId, devAgentId, taskId, { phase: 'server-fix-resume-failed', devAgentId });
|
|
5497
5436
|
return null;
|
|
5498
5437
|
}
|
|
5499
5438
|
await this.armPostDispatchSignalOrHold(taskId, devAgentId, expectedKind, newToken);
|
|
@@ -5510,6 +5449,33 @@ export class AgentManager {
|
|
|
5510
5449
|
return `git:${parsed.host.toLowerCase()}/${parsed.path}`;
|
|
5511
5450
|
return `raw:${repo.trim()}`;
|
|
5512
5451
|
}
|
|
5452
|
+
async computeCodeInterdiff(taskId, round) {
|
|
5453
|
+
const store = this.getReviewStore();
|
|
5454
|
+
if (!store || !Number.isInteger(round) || round < 2)
|
|
5455
|
+
return { ok: false, reason: 'no-anchor' };
|
|
5456
|
+
const [cur, prev] = await Promise.all([
|
|
5457
|
+
store.getRound(taskId, 'code', round),
|
|
5458
|
+
store.getRound(taskId, 'code', round - 1),
|
|
5459
|
+
]);
|
|
5460
|
+
const curSha = cur?.headSha;
|
|
5461
|
+
const prevSha = prev?.headSha;
|
|
5462
|
+
if (!curSha || !prevSha)
|
|
5463
|
+
return { ok: false, reason: 'no-anchor' };
|
|
5464
|
+
const task = await this.taskStore.get(taskId);
|
|
5465
|
+
if (!task?.agentId)
|
|
5466
|
+
return { ok: false, reason: 'released' };
|
|
5467
|
+
const agent = this.getAgentConfig(task.agentId);
|
|
5468
|
+
const agentState = await this.agentStore.get(task.agentId);
|
|
5469
|
+
// A rebound agent's worktree belongs to its new task — its git state describes
|
|
5470
|
+
// another branch, so the interdiff would be meaningless. Same skip rule as
|
|
5471
|
+
// findLineageViolation: worktree must still be bound to THIS task.
|
|
5472
|
+
if (!agent || agentState?.taskId !== taskId || !agentState.worktreePath) {
|
|
5473
|
+
return { ok: false, reason: 'released' };
|
|
5474
|
+
}
|
|
5475
|
+
await this.refreshWorktreeCacheFor(task.agentId);
|
|
5476
|
+
const diff = await this.getReviewTransport().readInterdiff(agent, prevSha, curSha);
|
|
5477
|
+
return { ok: true, diff };
|
|
5478
|
+
}
|
|
5513
5479
|
async findLineageViolation(taskId, baseSha) {
|
|
5514
5480
|
const task = await this.taskStore.get(taskId);
|
|
5515
5481
|
if (!task?.agentId)
|
|
@@ -5567,36 +5533,20 @@ export class AgentManager {
|
|
|
5567
5533
|
violation = await this.findLineageViolation(taskId);
|
|
5568
5534
|
}
|
|
5569
5535
|
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
|
-
},
|
|
5536
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, {
|
|
5537
|
+
phase: 'server-after-done-lineage-check-failed',
|
|
5538
|
+
error: err instanceof Error ? err.message : String(err),
|
|
5539
|
+
note: 'Publish was not dispatched; mark-complete retries it once the repo state is fixed.',
|
|
5582
5540
|
});
|
|
5583
5541
|
return null;
|
|
5584
5542
|
}
|
|
5585
5543
|
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
|
-
},
|
|
5544
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, {
|
|
5545
|
+
phase: 'server-after-done-lineage-violation',
|
|
5546
|
+
offendingTaskId: violation.taskId,
|
|
5547
|
+
offendingBranch: violation.branch,
|
|
5548
|
+
offendingSha: violation.sha,
|
|
5549
|
+
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
5550
|
});
|
|
5601
5551
|
return null;
|
|
5602
5552
|
}
|
|
@@ -5611,15 +5561,7 @@ export class AgentManager {
|
|
|
5611
5561
|
const acquired = await this.acquireAgentForTask(devAgentId, taskId, 'server-after-done');
|
|
5612
5562
|
if (!acquired) {
|
|
5613
5563
|
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
|
-
});
|
|
5564
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, { phase: 'server-after-done-dev-acquire-failed', devAgentId });
|
|
5623
5565
|
return null;
|
|
5624
5566
|
}
|
|
5625
5567
|
await this.updateTask(taskId, { publishDispatchedAt: new Date().toISOString() });
|
|
@@ -5642,18 +5584,10 @@ export class AgentManager {
|
|
|
5642
5584
|
}
|
|
5643
5585
|
if (!resumed) {
|
|
5644
5586
|
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
|
-
},
|
|
5587
|
+
await this.emitIntervention(task.projectId, devAgentId, taskId, {
|
|
5588
|
+
phase: 'server-after-done-resume-failed',
|
|
5589
|
+
devAgentId,
|
|
5590
|
+
note: 'Publish prompt was not delivered; mark-complete retries the publish dispatch.',
|
|
5657
5591
|
});
|
|
5658
5592
|
return null;
|
|
5659
5593
|
}
|
|
@@ -5792,6 +5726,9 @@ export class AgentManager {
|
|
|
5792
5726
|
if (this.markCompleteInFlight.has(taskId)) {
|
|
5793
5727
|
throw new ApiError(409, `Task ${taskId} is already being completed`);
|
|
5794
5728
|
}
|
|
5729
|
+
if (this.codeVerdictInFlight.has(taskId)) {
|
|
5730
|
+
throw new ApiError(409, `Task ${taskId} code verdict is already being processed`);
|
|
5731
|
+
}
|
|
5795
5732
|
this.markCompleteInFlight.add(taskId);
|
|
5796
5733
|
return fresh;
|
|
5797
5734
|
});
|
|
@@ -5802,18 +5739,11 @@ export class AgentManager {
|
|
|
5802
5739
|
}
|
|
5803
5740
|
catch (err) {
|
|
5804
5741
|
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
|
-
},
|
|
5742
|
+
await this.emitIntervention(task.projectId, undefined, task.id, {
|
|
5743
|
+
phase: 'confirm-merge-failed',
|
|
5744
|
+
gate: task.status,
|
|
5745
|
+
error: message,
|
|
5746
|
+
note: 'Task stays at the gate: Confirm again to retry, or Cancel to retire the published artifact.',
|
|
5817
5747
|
});
|
|
5818
5748
|
throw new ApiError(409, `Merge failed for task ${task.id}: ${message}`);
|
|
5819
5749
|
}
|
|
@@ -5836,12 +5766,26 @@ export class AgentManager {
|
|
|
5836
5766
|
const state = await this.agentStore.get(id);
|
|
5837
5767
|
if (!state || state.taskId !== taskId)
|
|
5838
5768
|
continue;
|
|
5769
|
+
if (this.startCompactionThenRelease(id, state, taskId))
|
|
5770
|
+
continue;
|
|
5839
5771
|
await this.releaseAgentForTask(id, taskId, 'idle', { allowAwaitingHuman: true })
|
|
5840
5772
|
.catch(err => {
|
|
5841
5773
|
console.warn(`[AgentManager] confirm release ${id} failed:`, err);
|
|
5842
5774
|
});
|
|
5843
5775
|
}
|
|
5844
5776
|
}
|
|
5777
|
+
startCompactionThenRelease(agentId, state, taskId) {
|
|
5778
|
+
// 人工介入/重建中的 pane 不发 /clear;confirm 语义要求解绑,落回直接释放
|
|
5779
|
+
if (!state.paneId || state.status === 'awaiting_human' || state.creationToken)
|
|
5780
|
+
return false;
|
|
5781
|
+
const cfg = this.getAgentConfig(agentId);
|
|
5782
|
+
if (!cfg)
|
|
5783
|
+
return false;
|
|
5784
|
+
const tmux = new TmuxManager(this.createRunnerFor(cfg));
|
|
5785
|
+
void this.runPostMergeCompaction(tmux, state.paneId, agentId, taskId, agentRuntimeKindFor(cfg))
|
|
5786
|
+
.catch(err => console.warn(`[AgentManager] releaseTaskAgents compaction(${agentId}) failed:`, err));
|
|
5787
|
+
return true;
|
|
5788
|
+
}
|
|
5845
5789
|
repoMergeQueue = new Map();
|
|
5846
5790
|
async ffMergeBranch(task) {
|
|
5847
5791
|
const dev = this.getAgentConfig(task.agentId);
|