baxian 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/branch.d.ts +0 -1
- package/dist/agent/branch.d.ts.map +1 -1
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
- package/dist/agent/dispatch-reconciler.js +10 -5
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/manager.d.ts +42 -13
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +568 -165
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/net-exec.d.ts +3 -0
- package/dist/agent/net-exec.d.ts.map +1 -1
- package/dist/agent/net-exec.js +6 -0
- package/dist/agent/net-exec.js.map +1 -1
- package/dist/agent/pane-streamer-manager.d.ts +6 -6
- package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
- package/dist/agent/pane-streamer-manager.js.map +1 -1
- package/dist/agent/pane-streamer.d.ts +2 -2
- package/dist/agent/pane-streamer.d.ts.map +1 -1
- package/dist/agent/pane-streamer.js +46 -36
- package/dist/agent/pane-streamer.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +15 -1
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +113 -6
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +7 -0
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +14 -5
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/prompt.d.ts +0 -5
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +5 -18
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +4 -1
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +33 -7
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/root-agent-runtime.d.ts +80 -0
- package/dist/agent/root-agent-runtime.d.ts.map +1 -0
- package/dist/agent/root-agent-runtime.js +635 -0
- package/dist/agent/root-agent-runtime.js.map +1 -0
- package/dist/agent/root-recovery-coordinator.d.ts +125 -0
- package/dist/agent/root-recovery-coordinator.d.ts.map +1 -0
- package/dist/agent/root-recovery-coordinator.js +1579 -0
- package/dist/agent/root-recovery-coordinator.js.map +1 -0
- package/dist/agent/runner.d.ts +1 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +103 -7
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tmux-probe-poller.d.ts +1 -0
- package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
- package/dist/agent/tmux-probe-poller.js +16 -10
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.d.ts +3 -0
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +11 -5
- package/dist/agent/tmux.js.map +1 -1
- package/dist/api/agents.d.ts.map +1 -1
- package/dist/api/agents.js +42 -1
- package/dist/api/agents.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +77 -7
- package/dist/api/config.js.map +1 -1
- package/dist/api/hosts.d.ts.map +1 -1
- package/dist/api/hosts.js +68 -13
- package/dist/api/hosts.js.map +1 -1
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +23 -7
- package/dist/api/projects.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +6 -0
- package/dist/app.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +49 -7
- package/dist/cli.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +22 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/normalizer.d.ts.map +1 -1
- package/dist/config/normalizer.js +18 -0
- package/dist/config/normalizer.js.map +1 -1
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/config/validator.js +194 -23
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +0 -3
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/server-handlers.d.ts.map +1 -1
- package/dist/event/server-handlers.js +201 -128
- package/dist/event/server-handlers.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -1
- package/dist/platform/platform-poller.d.ts.map +1 -1
- package/dist/platform/platform-poller.js +1 -5
- package/dist/platform/platform-poller.js.map +1 -1
- package/dist/shared/constants.d.ts +7 -1
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +5 -0
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/types.d.ts +25 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +18 -0
- package/dist/shared/types.js.map +1 -1
- package/dist/skills/baxian-pr-feedback/SKILL.md +2 -4
- package/dist/skills/baxian-pr-recheck/SKILL.md +2 -0
- package/dist/skills/baxian-pr-review/SKILL.md +2 -0
- package/dist/skills/baxian-research/SKILL.md +1 -1
- package/dist/skills/baxian-server-feedback/SKILL.md +1 -1
- package/dist/skills/baxian-server-review/SKILL.md +2 -2
- package/dist/skills/baxian-signals/SKILL.md +6 -0
- package/dist/state/init.d.ts.map +1 -1
- package/dist/state/init.js +1 -0
- package/dist/state/init.js.map +1 -1
- package/dist/state/lock.d.ts +6 -0
- package/dist/state/lock.d.ts.map +1 -1
- package/dist/state/lock.js +8 -0
- package/dist/state/lock.js.map +1 -1
- package/dist/state/root-recovery-store.d.ts +107 -0
- package/dist/state/root-recovery-store.d.ts.map +1 -0
- package/dist/state/root-recovery-store.js +400 -0
- package/dist/state/root-recovery-store.js.map +1 -0
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +7 -3
- package/dist/state/task-store.js.map +1 -1
- package/dist/terminal/attach.d.ts +3 -3
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js.map +1 -1
- package/dist/terminal/stream-ws.d.ts.map +1 -1
- package/dist/terminal/stream-ws.js.map +1 -1
- package/package.json +1 -1
package/dist/agent/manager.js
CHANGED
|
@@ -4,7 +4,7 @@ import { join, normalize } from 'node:path';
|
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { createSignalToken, PASSIVE_VERDICT_WATCH } from './phase-signal.js';
|
|
6
6
|
import { tmuxInstallHint } from './preflight.js';
|
|
7
|
-
import { BRANCH_PREFIX, isValidBranchName, PHASE_EXPECTED_STATUS, PHASE_REQUIRES_AGENT_BOUND_TO_TASK, TASK_TERMINAL_STATUSES as TERMINAL_STATUSES, TASK_ACTIVE_STATUS_SET as ACTIVE_TASK_STATUSES, TASK_OWNER_ROLES, isSpecStagePhase, isGitHubRepo, parseGitRemote, repoIdentityKey, repoSlug, } from '../shared/index.js';
|
|
7
|
+
import { BRANCH_PREFIX, isValidBranchName, PHASE_EXPECTED_STATUS, PHASE_REQUIRES_AGENT_BOUND_TO_TASK, TASK_TERMINAL_STATUSES as TERMINAL_STATUSES, TASK_ACTIVE_STATUS_SET as ACTIVE_TASK_STATUSES, TASK_OWNER_ROLES, isSpecStagePhase, isGitHubRepo, parseGitRemote, repoIdentityKey, repoSlug, taskGenerationGuard, taskMatchesGeneration, } from '../shared/index.js';
|
|
8
8
|
import { buildProjectDriver, makeDriverExec } from '../platform/driver-host.js';
|
|
9
9
|
import { DriverOpError, buildDriverRunContext, GitDriver, safeDriverErrorText } from '../platform/git-driver.js';
|
|
10
10
|
import { checkOpenPrBinding, checkPrBinding } from '../platform/pr-binding.js';
|
|
@@ -28,6 +28,14 @@ import { ApiError } from '../errors.js';
|
|
|
28
28
|
import { prepareConfig } from '../config/loader.js';
|
|
29
29
|
import { projectAfterDone, projectReviewMode, resolveProjectTool } from '../config/validator.js';
|
|
30
30
|
import { SHA_HEX_SOURCE } from '../platform/types.js';
|
|
31
|
+
// 这两个入口冲突证明当前调用已被并发 pass 接管,应判 stale 而不是派发失败。
|
|
32
|
+
const BENIGN_DISPATCH_CONFLICT_CODES = new Set(['dispatch-superseded', 'dispatch-in-flight']);
|
|
33
|
+
export function isBenignDispatchConflict(err) {
|
|
34
|
+
return err instanceof ApiError
|
|
35
|
+
&& err.status === 409
|
|
36
|
+
&& err.code !== undefined
|
|
37
|
+
&& BENIGN_DISPATCH_CONFLICT_CODES.has(err.code);
|
|
38
|
+
}
|
|
31
39
|
export class EnsureSessionError extends Error {
|
|
32
40
|
partial;
|
|
33
41
|
constructor(partial, message) {
|
|
@@ -185,6 +193,13 @@ export function isRecoverableQaDispatchHold(binding) {
|
|
|
185
193
|
&& binding.awaitingPhase != null
|
|
186
194
|
&& RECOVERABLE_QA_DISPATCH_HOLD_PHASES.has(binding.awaitingPhase);
|
|
187
195
|
}
|
|
196
|
+
function shouldBumpServerReviewRound(requested, task, qaBinding) {
|
|
197
|
+
if (requested !== undefined)
|
|
198
|
+
return requested;
|
|
199
|
+
return !(task.status === 'review'
|
|
200
|
+
&& qaBinding?.taskId === task.id
|
|
201
|
+
&& isRecoverableQaDispatchHold(qaBinding));
|
|
202
|
+
}
|
|
188
203
|
const TURN_COMPLETED_AWAITING_PHASES = new Set();
|
|
189
204
|
const CANCEL_CLEANUP_HOLD_PHASES = new Set(['cancel-clearing', 'cancel-interrupt-failed']);
|
|
190
205
|
function isCancelCleanupHold(binding) {
|
|
@@ -366,6 +381,42 @@ export class AgentManager {
|
|
|
366
381
|
getReviewStore() {
|
|
367
382
|
return this.reviewStore;
|
|
368
383
|
}
|
|
384
|
+
async putReviewRoundIfCurrent(taskId, data, expectedTask) {
|
|
385
|
+
return this.withTaskLock(async () => {
|
|
386
|
+
const task = await this.taskStore.get(taskId);
|
|
387
|
+
if (!task || !taskMatchesGeneration(task, expectedTask))
|
|
388
|
+
return false;
|
|
389
|
+
if (!this.reviewStore)
|
|
390
|
+
throw new Error('ReviewStore is not configured');
|
|
391
|
+
await this.reviewStore.putRound(taskId, data.phase, data);
|
|
392
|
+
return true;
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
async deleteReviewExchangeIfCurrent(taskId, agent, artifact, expectedTask, expectedRecovery) {
|
|
396
|
+
return this.withTaskLock(async () => {
|
|
397
|
+
const task = await this.taskStore.get(taskId);
|
|
398
|
+
if (!task || !taskMatchesGeneration(task, expectedTask))
|
|
399
|
+
return false;
|
|
400
|
+
const recovery = task.serverSignalRecovery;
|
|
401
|
+
if (expectedRecovery && (!recovery
|
|
402
|
+
|| recovery.mode !== expectedRecovery.mode
|
|
403
|
+
|| recovery.signalKind !== expectedRecovery.signalKind
|
|
404
|
+
|| recovery.phase !== expectedRecovery.phase
|
|
405
|
+
|| recovery.round !== expectedRecovery.round
|
|
406
|
+
|| recovery.sourceToken !== expectedRecovery.sourceToken
|
|
407
|
+
|| recovery.failureSignature !== expectedRecovery.failureSignature
|
|
408
|
+
|| recovery.createdAt !== expectedRecovery.createdAt))
|
|
409
|
+
return false;
|
|
410
|
+
const transport = this.getReviewTransport();
|
|
411
|
+
if (artifact === 'findings') {
|
|
412
|
+
await transport.deleteFindings(agent);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
await transport.deleteResponse(agent);
|
|
416
|
+
}
|
|
417
|
+
return true;
|
|
418
|
+
});
|
|
419
|
+
}
|
|
369
420
|
setServerReviewDriver(driver) {
|
|
370
421
|
this.serverReviewDriver = driver;
|
|
371
422
|
}
|
|
@@ -382,11 +433,14 @@ export class AgentManager {
|
|
|
382
433
|
const project = this.getProjectConfig(projectId);
|
|
383
434
|
return project ? projectAfterDone(this.config, project) : (this.config.review.afterDone ?? null);
|
|
384
435
|
}
|
|
385
|
-
async commitServerAfterDone(taskId) {
|
|
436
|
+
async commitServerAfterDone(taskId, expectedTask) {
|
|
386
437
|
return this.withTaskLock(async () => {
|
|
387
438
|
const task = await this.taskStore.get(taskId);
|
|
388
439
|
if (!task)
|
|
389
440
|
throw new ApiError(404, `Task ${taskId} not found`);
|
|
441
|
+
if (expectedTask && !taskMatchesGeneration(task, expectedTask)) {
|
|
442
|
+
throw new ApiError(409, `Task ${taskId} review generation changed before afterDone was committed`, 'dispatch-superseded');
|
|
443
|
+
}
|
|
390
444
|
if (task.afterDone === 'pr' && task.platformBinding === undefined) {
|
|
391
445
|
throw new Error(`platform binding missing for task ${task.id}; refusing to reconstruct it from live config`);
|
|
392
446
|
}
|
|
@@ -1814,9 +1868,13 @@ export class AgentManager {
|
|
|
1814
1868
|
}
|
|
1815
1869
|
if (opts.expectedTask
|
|
1816
1870
|
&& boundTask
|
|
1817
|
-
&& (boundTask.status !== opts.expectedTask.status
|
|
1818
|
-
|| boundTask.phase !== opts.expectedTask.phase
|
|
1819
|
-
|| boundTask.signalToken !== opts.expectedTask.signalToken)
|
|
1871
|
+
&& (('status' in opts.expectedTask && boundTask.status !== opts.expectedTask.status)
|
|
1872
|
+
|| ('phase' in opts.expectedTask && boundTask.phase !== opts.expectedTask.phase)
|
|
1873
|
+
|| ('signalToken' in opts.expectedTask && boundTask.signalToken !== opts.expectedTask.signalToken)
|
|
1874
|
+
|| ('agentId' in opts.expectedTask && boundTask.agentId !== opts.expectedTask.agentId)
|
|
1875
|
+
|| ('reviewRound' in opts.expectedTask && boundTask.reviewRound !== opts.expectedTask.reviewRound)
|
|
1876
|
+
|| ('specReviewRound' in opts.expectedTask
|
|
1877
|
+
&& boundTask.specReviewRound !== opts.expectedTask.specReviewRound))) {
|
|
1820
1878
|
console.warn(`[AgentManager] releaseAgentForTask: task ${expectedTaskId} moved past the captured pass; skipping ${mode} transition`);
|
|
1821
1879
|
return false;
|
|
1822
1880
|
}
|
|
@@ -2930,6 +2988,9 @@ export class AgentManager {
|
|
|
2930
2988
|
status: boundTask.status,
|
|
2931
2989
|
phase: boundTask.phase,
|
|
2932
2990
|
signalToken: boundTask.signalToken,
|
|
2991
|
+
agentId: boundTask.agentId,
|
|
2992
|
+
reviewRound: boundTask.reviewRound,
|
|
2993
|
+
specReviewRound: boundTask.specReviewRound,
|
|
2933
2994
|
},
|
|
2934
2995
|
heldLockToken: state.lockToken,
|
|
2935
2996
|
projectId: state.projectId,
|
|
@@ -3106,6 +3167,7 @@ export class AgentManager {
|
|
|
3106
3167
|
fromStatus: ['review'],
|
|
3107
3168
|
expectPhase: reviewPass.phase,
|
|
3108
3169
|
expectSignalToken: reviewPass.signalToken,
|
|
3170
|
+
expectedTask: reviewPass,
|
|
3109
3171
|
...(resumeQaPhase !== undefined ? { qaPhase: resumeQaPhase } : {}),
|
|
3110
3172
|
onQaAcquired: (lockToken) => { recoveryLockToken = lockToken; },
|
|
3111
3173
|
});
|
|
@@ -3163,9 +3225,7 @@ export class AgentManager {
|
|
|
3163
3225
|
let recoveryReadFailed = false;
|
|
3164
3226
|
try {
|
|
3165
3227
|
const taskNow = await this.taskStore.get(reviewTaskId);
|
|
3166
|
-
passSuperseded = taskNow
|
|
3167
|
-
|| taskNow.phase !== reviewPass.phase
|
|
3168
|
-
|| taskNow.signalToken !== reviewPass.signalToken;
|
|
3228
|
+
passSuperseded = !taskNow || !taskMatchesGeneration(taskNow, reviewPass);
|
|
3169
3229
|
}
|
|
3170
3230
|
catch (readErr) {
|
|
3171
3231
|
recoveryReadFailed = true;
|
|
@@ -3386,9 +3446,16 @@ export class AgentManager {
|
|
|
3386
3446
|
...(opts.expectedReviewDispatch !== undefined
|
|
3387
3447
|
? { expectReviewDispatch: opts.expectedReviewDispatch }
|
|
3388
3448
|
: {}),
|
|
3449
|
+
...(opts.expectedTask !== undefined ? { expectTask: opts.expectedTask } : {}),
|
|
3450
|
+
...(opts.expectedLockToken !== undefined
|
|
3451
|
+
? { expectAgentLock: { agentId, token: opts.expectedLockToken } }
|
|
3452
|
+
: {}),
|
|
3389
3453
|
});
|
|
3390
3454
|
if (!transitioned
|
|
3391
|
-
&& (opts.expectedPostApproveEpisode !== undefined
|
|
3455
|
+
&& (opts.expectedPostApproveEpisode !== undefined
|
|
3456
|
+
|| opts.expectedReviewDispatch !== undefined
|
|
3457
|
+
|| opts.expectedTask !== undefined
|
|
3458
|
+
|| opts.expectedLockToken !== undefined)) {
|
|
3392
3459
|
console.warn(`[AgentManager] failTaskForDispatchError: dispatch generation for ${taskId} was superseded; skipping stale failure`);
|
|
3393
3460
|
return;
|
|
3394
3461
|
}
|
|
@@ -3732,11 +3799,14 @@ export class AgentManager {
|
|
|
3732
3799
|
retireOldOnce();
|
|
3733
3800
|
}
|
|
3734
3801
|
}
|
|
3735
|
-
async redispatchTaskPromptAfterReplRestart(agentId, taskId) {
|
|
3802
|
+
async redispatchTaskPromptAfterReplRestart(agentId, taskId, expected) {
|
|
3736
3803
|
const agent = this.getAgentConfig(agentId);
|
|
3737
3804
|
const loadedTask = await this.taskStore.get(taskId);
|
|
3738
3805
|
if (!agent || !loadedTask || !TASK_OWNER_ROLES.has(agent.role))
|
|
3739
3806
|
return false;
|
|
3807
|
+
if (expected && !taskMatchesGeneration(loadedTask, expected))
|
|
3808
|
+
return false;
|
|
3809
|
+
const rootGenerationMatches = (fresh, signalToken = expected?.signalToken) => !expected || taskMatchesGeneration(fresh, { ...expected, signalToken });
|
|
3740
3810
|
let task = loadedTask;
|
|
3741
3811
|
if (task.agentId !== agentId)
|
|
3742
3812
|
return false;
|
|
@@ -3754,7 +3824,8 @@ export class AgentManager {
|
|
|
3754
3824
|
return fresh?.agentId === agentId
|
|
3755
3825
|
&& fresh.status === task.status
|
|
3756
3826
|
&& fresh.phase === task.phase
|
|
3757
|
-
&& fresh.signalToken === signalToken
|
|
3827
|
+
&& fresh.signalToken === signalToken
|
|
3828
|
+
&& rootGenerationMatches(fresh, signalToken);
|
|
3758
3829
|
};
|
|
3759
3830
|
const clearEntryHold = async () => this.withTaskLock(async () => {
|
|
3760
3831
|
if (!(await taskStillCurrent()))
|
|
@@ -3765,10 +3836,10 @@ export class AgentManager {
|
|
|
3765
3836
|
}
|
|
3766
3837
|
return this.clearAwaitingHuman(agentId, { expectedHold: entryHold });
|
|
3767
3838
|
});
|
|
3768
|
-
const holdReplayFailure = (phase, reason) => this.holdReplayFailureIfCurrent(agentId, task, phase, reason, entryHold);
|
|
3839
|
+
const holdReplayFailure = (phase, reason) => this.holdReplayFailureIfCurrent(agentId, task, phase, reason, entryHold, async (fresh) => rootGenerationMatches(fresh, task.signalToken));
|
|
3769
3840
|
try {
|
|
3770
3841
|
if (task.status === 'approved' && agent.role === 'dev') {
|
|
3771
|
-
return await this.replayApprovedHolderAfterReplRestart(agentId, task, entryHold, { clearEntryHold, holdReplayFailure });
|
|
3842
|
+
return await this.replayApprovedHolderAfterReplRestart(agentId, task, entryHold, { clearEntryHold, holdReplayFailure, rootGenerationMatches });
|
|
3772
3843
|
}
|
|
3773
3844
|
if (task.serverSignalRecovery) {
|
|
3774
3845
|
const recovery = task.serverSignalRecovery;
|
|
@@ -3809,7 +3880,8 @@ export class AgentManager {
|
|
|
3809
3880
|
if (fresh?.agentId !== agentId
|
|
3810
3881
|
|| fresh.status !== task.status
|
|
3811
3882
|
|| fresh.phase !== task.phase
|
|
3812
|
-
|| fresh.signalToken !== signalToken
|
|
3883
|
+
|| fresh.signalToken !== signalToken
|
|
3884
|
+
|| !rootGenerationMatches(fresh, signalToken))
|
|
3813
3885
|
return false;
|
|
3814
3886
|
const rotatePendingPr = mapped.expectedKinds.includes('pr-created')
|
|
3815
3887
|
&& fresh.pendingPrSignalToken === signalToken;
|
|
@@ -3822,7 +3894,7 @@ export class AgentManager {
|
|
|
3822
3894
|
return true;
|
|
3823
3895
|
},
|
|
3824
3896
|
stillCurrent: taskStillCurrent,
|
|
3825
|
-
holdFailure: (newToken, reason) => this.holdReplayFailureIfCurrent(agentId, { ...task, signalToken: newToken }, 'restart-redispatch-failed', reason, entryHold),
|
|
3897
|
+
holdFailure: (newToken, reason) => this.holdReplayFailureIfCurrent(agentId, { ...task, signalToken: newToken }, 'restart-redispatch-failed', reason, entryHold, async (fresh) => rootGenerationMatches(fresh, newToken)),
|
|
3826
3898
|
continueWith: (newToken, fence) => this.continueSession(taskId, agentId, phase, {
|
|
3827
3899
|
...opts,
|
|
3828
3900
|
signalToken: newToken,
|
|
@@ -3921,10 +3993,49 @@ export class AgentManager {
|
|
|
3921
3993
|
throw err;
|
|
3922
3994
|
}
|
|
3923
3995
|
}
|
|
3996
|
+
async redispatchCurrentTaskPhase(taskId, expected) {
|
|
3997
|
+
const current = await this.taskStore.get(taskId);
|
|
3998
|
+
if (!current || !taskMatchesGeneration(current, expected))
|
|
3999
|
+
return 'stale';
|
|
4000
|
+
if (current.status === 'review') {
|
|
4001
|
+
let changedByRedispatch = false;
|
|
4002
|
+
try {
|
|
4003
|
+
await this.dispatchReviewToQa(taskId, {
|
|
4004
|
+
fromStatus: ['review'],
|
|
4005
|
+
bumpRound: false,
|
|
4006
|
+
expectSignalToken: current.signalToken,
|
|
4007
|
+
expectPhase: current.phase,
|
|
4008
|
+
expectedTask: expected,
|
|
4009
|
+
onSideEffect: () => { changedByRedispatch = true; },
|
|
4010
|
+
});
|
|
4011
|
+
return 'dispatched';
|
|
4012
|
+
}
|
|
4013
|
+
catch (err) {
|
|
4014
|
+
if (!changedByRedispatch && isBenignDispatchConflict(err))
|
|
4015
|
+
return 'stale';
|
|
4016
|
+
if (!changedByRedispatch
|
|
4017
|
+
&& err instanceof ApiError
|
|
4018
|
+
&& err.code === 'dispatch-unsupported') {
|
|
4019
|
+
return 'unsupported';
|
|
4020
|
+
}
|
|
4021
|
+
if (changedByRedispatch)
|
|
4022
|
+
throw err;
|
|
4023
|
+
const fresh = await this.taskStore.get(taskId);
|
|
4024
|
+
if (!fresh || !taskMatchesGeneration(fresh, expected))
|
|
4025
|
+
return 'stale';
|
|
4026
|
+
throw err;
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
const redispatched = await this.redispatchTaskPromptAfterReplRestart(current.agentId, taskId, expected);
|
|
4030
|
+
if (redispatched)
|
|
4031
|
+
return 'dispatched';
|
|
4032
|
+
const fresh = await this.taskStore.get(taskId);
|
|
4033
|
+
return !fresh || !taskMatchesGeneration(fresh, expected) ? 'stale' : 'unsupported';
|
|
4034
|
+
}
|
|
3924
4035
|
// approved hosts two live holder passes — post-approve and server after-done; a parked pre-publish holder stays on the waiting path.
|
|
3925
4036
|
async replayApprovedHolderAfterReplRestart(agentId, task, entryHold, holdGen) {
|
|
3926
4037
|
const taskId = task.id;
|
|
3927
|
-
const { clearEntryHold, holdReplayFailure } = holdGen;
|
|
4038
|
+
const { clearEntryHold, holdReplayFailure, rootGenerationMatches } = holdGen;
|
|
3928
4039
|
if (task.reviewMode === 'server') {
|
|
3929
4040
|
if (!task.publishDispatchedAt)
|
|
3930
4041
|
return false;
|
|
@@ -3954,7 +4065,8 @@ export class AgentManager {
|
|
|
3954
4065
|
return fresh?.agentId === agentId
|
|
3955
4066
|
&& fresh.status === task.status
|
|
3956
4067
|
&& fresh.phase === task.phase
|
|
3957
|
-
&& fresh.signalToken === signalToken
|
|
4068
|
+
&& fresh.signalToken === signalToken
|
|
4069
|
+
&& rootGenerationMatches(fresh, signalToken);
|
|
3958
4070
|
};
|
|
3959
4071
|
return this.runRotatedReplay({
|
|
3960
4072
|
agentId,
|
|
@@ -3966,13 +4078,14 @@ export class AgentManager {
|
|
|
3966
4078
|
if (fresh?.agentId !== agentId
|
|
3967
4079
|
|| fresh.status !== task.status
|
|
3968
4080
|
|| fresh.phase !== task.phase
|
|
3969
|
-
|| fresh.signalToken !== token
|
|
4081
|
+
|| fresh.signalToken !== token
|
|
4082
|
+
|| !rootGenerationMatches(fresh, token))
|
|
3970
4083
|
return false;
|
|
3971
4084
|
await this.taskStore.set({ ...fresh, signalToken: newToken, updatedAt: new Date().toISOString() });
|
|
3972
4085
|
return true;
|
|
3973
4086
|
},
|
|
3974
4087
|
stillCurrent,
|
|
3975
|
-
holdFailure: (newToken, reason) => this.holdReplayFailureIfCurrent(agentId, { ...task, signalToken: newToken }, 'restart-redispatch-failed', reason, entryHold),
|
|
4088
|
+
holdFailure: (newToken, reason) => this.holdReplayFailureIfCurrent(agentId, { ...task, signalToken: newToken }, 'restart-redispatch-failed', reason, entryHold, async (fresh) => rootGenerationMatches(fresh, newToken)),
|
|
3976
4089
|
continueWith: (newToken, fence) => this.continueSession(taskId, agentId, 'server-after-done', {
|
|
3977
4090
|
signalToken: newToken,
|
|
3978
4091
|
preserveDispatchOutputs: true,
|
|
@@ -4014,6 +4127,7 @@ export class AgentManager {
|
|
|
4014
4127
|
&& fresh.status === task.status
|
|
4015
4128
|
&& fresh.phase === task.phase
|
|
4016
4129
|
&& fresh.signalToken === task.signalToken
|
|
4130
|
+
&& rootGenerationMatches(fresh, task.signalToken)
|
|
4017
4131
|
&& fresh.reviewMode === task.reviewMode
|
|
4018
4132
|
&& fresh.postApproveRevoked === undefined
|
|
4019
4133
|
&& fresh.postApproveHeadSha === task.postApproveHeadSha
|
|
@@ -4675,6 +4789,8 @@ export class AgentManager {
|
|
|
4675
4789
|
const current = await this.taskStore.get(taskId);
|
|
4676
4790
|
if (!current || current.reviewMode !== 'git' || TERMINAL_STATUSES.includes(current.status))
|
|
4677
4791
|
return null;
|
|
4792
|
+
if (opts.expectedTask && !taskMatchesGeneration(current, opts.expectedTask))
|
|
4793
|
+
return null;
|
|
4678
4794
|
if (!opts.fromStatus.includes(current.status))
|
|
4679
4795
|
return null;
|
|
4680
4796
|
if (current.reviewDispatch?.phase === 'uncertain')
|
|
@@ -4726,7 +4842,7 @@ export class AgentManager {
|
|
|
4726
4842
|
return { task: next, previousStatus };
|
|
4727
4843
|
});
|
|
4728
4844
|
}
|
|
4729
|
-
async claimGitReviewDispatch(taskId, expectedGeneration) {
|
|
4845
|
+
async claimGitReviewDispatch(taskId, expectedGeneration, expectedTask) {
|
|
4730
4846
|
return this.withTaskLock(async () => {
|
|
4731
4847
|
if (this.markCompleteInFlight.has(taskId))
|
|
4732
4848
|
return null;
|
|
@@ -4734,6 +4850,8 @@ export class AgentManager {
|
|
|
4734
4850
|
const lease = task?.reviewDispatch;
|
|
4735
4851
|
if (!task || !lease || lease.phase !== 'pending')
|
|
4736
4852
|
return null;
|
|
4853
|
+
if (expectedTask && !taskMatchesGeneration(task, expectedTask))
|
|
4854
|
+
return null;
|
|
4737
4855
|
if (expectedGeneration !== undefined && lease.generation !== expectedGeneration)
|
|
4738
4856
|
return null;
|
|
4739
4857
|
if (!this.gitReviewLeaseMatches(task, lease))
|
|
@@ -5320,7 +5438,9 @@ export class AgentManager {
|
|
|
5320
5438
|
return;
|
|
5321
5439
|
}
|
|
5322
5440
|
try {
|
|
5323
|
-
await this.
|
|
5441
|
+
const deleted = await this.deleteReviewExchangeIfCurrent(task.id, dev, 'response', taskGenerationGuard(fresh), current);
|
|
5442
|
+
if (!deleted)
|
|
5443
|
+
return;
|
|
5324
5444
|
}
|
|
5325
5445
|
catch (err) {
|
|
5326
5446
|
await hold(`server-${recovery.phase}-feedback-recovery-cleanup-failed`, `The rejected response was archived but could not be removed during recovery: `
|
|
@@ -5344,10 +5464,16 @@ export class AgentManager {
|
|
|
5344
5464
|
const scanSnapshotOnRecover = isServerProtocol
|
|
5345
5465
|
|| (task.phase === undefined && task.status === 'in_progress')
|
|
5346
5466
|
|| (!specStage && (task.status === 'review' || task.status === 'fixing'));
|
|
5467
|
+
// 侧道读 dev workdir,对 head 评审会注入未验证内容——mode 不明时关侧道、发干预交人工。
|
|
5347
5468
|
const allowRecoveredReadFile = task.status === 'review'
|
|
5348
5469
|
&& (specStage
|
|
5349
5470
|
|| (task.reviewMode === 'server' && task.batchTotal !== undefined)
|
|
5350
5471
|
|| (task.reviewMode === 'server' && task.reviewCheckoutMode === 'base'));
|
|
5472
|
+
const ambiguousCheckout = task.status === 'review'
|
|
5473
|
+
&& task.reviewMode === 'server'
|
|
5474
|
+
&& !specStage
|
|
5475
|
+
&& task.reviewCheckoutMode === undefined
|
|
5476
|
+
&& task.batchTotal === undefined;
|
|
5351
5477
|
try {
|
|
5352
5478
|
await this.startPhaseSignalWatch({
|
|
5353
5479
|
taskId: task.id,
|
|
@@ -5362,12 +5488,29 @@ export class AgentManager {
|
|
|
5362
5488
|
? { onReadFile: (req) => { void this.handleReadFileRequest(task.id, agentId, req); } }
|
|
5363
5489
|
: {}),
|
|
5364
5490
|
});
|
|
5365
|
-
if (interventionKindLabel) {
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5491
|
+
if (interventionKindLabel || ambiguousCheckout) {
|
|
5492
|
+
// snapshot 命中完成信号时不发邀请手工 retry 的干预:结算窗口内的人工重派正是要杜绝的入口;
|
|
5493
|
+
// 结算若推进了任务态(token 轮换/状态离开),该干预已无的放矢。
|
|
5494
|
+
if (this.phaseSignalWatcher?.isSettling(task.id)) {
|
|
5495
|
+
await this.phaseSignalWatcher.awaitSettled(task.id);
|
|
5496
|
+
const fresh = await this.taskStore.get(task.id);
|
|
5497
|
+
if (!fresh || fresh.status !== task.status || fresh.signalToken !== signalToken) {
|
|
5498
|
+
return;
|
|
5499
|
+
}
|
|
5500
|
+
}
|
|
5501
|
+
if (ambiguousCheckout) {
|
|
5502
|
+
await this.emitIntervention(task.projectId, agentId, task.id, {
|
|
5503
|
+
phase: 'server-review-recovery-ambiguous-checkout',
|
|
5504
|
+
note: 'The recovered server code review has no persisted checkout mode (crash before persist, or a pre-rename base task rewritten since). The read-file side-channel stays closed because it reads the dev workdir, not the reviewed head. Inspect the QA pane; redispatch the review to converge on a verified head checkout.',
|
|
5505
|
+
});
|
|
5506
|
+
}
|
|
5507
|
+
if (interventionKindLabel) {
|
|
5508
|
+
await this.emitIntervention(task.projectId, agentId, task.id, {
|
|
5509
|
+
phase: 'spec-signal-setup-during-recovery',
|
|
5510
|
+
kind: interventionKindLabel,
|
|
5511
|
+
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.',
|
|
5512
|
+
});
|
|
5513
|
+
}
|
|
5371
5514
|
}
|
|
5372
5515
|
}
|
|
5373
5516
|
catch (err) {
|
|
@@ -5796,6 +5939,29 @@ export class AgentManager {
|
|
|
5796
5939
|
return;
|
|
5797
5940
|
}
|
|
5798
5941
|
if (branch.headSha === undefined) {
|
|
5942
|
+
try {
|
|
5943
|
+
await this.eventBus.emit({
|
|
5944
|
+
id: `outbox:${task.id}:${intent.prNumber}:remote-branch-deleted:${generation}`,
|
|
5945
|
+
type: 'task.updated',
|
|
5946
|
+
timestamp: new Date().toISOString(),
|
|
5947
|
+
projectId: task.projectId,
|
|
5948
|
+
taskId: task.id,
|
|
5949
|
+
data: {
|
|
5950
|
+
status: task.status,
|
|
5951
|
+
operation: 'git-branch-delete',
|
|
5952
|
+
outcome: 'succeeded',
|
|
5953
|
+
generation,
|
|
5954
|
+
prNumber: intent.prNumber,
|
|
5955
|
+
branch: intent.branch,
|
|
5956
|
+
expectedHeadSha: intent.expectedHeadSha,
|
|
5957
|
+
remoteProjectId: intent.remoteProjectId,
|
|
5958
|
+
},
|
|
5959
|
+
});
|
|
5960
|
+
}
|
|
5961
|
+
catch (err) {
|
|
5962
|
+
await this.recordRemoteCleanupFailure(task, generation, 'persist', safeDriverErrorText(err));
|
|
5963
|
+
throw err;
|
|
5964
|
+
}
|
|
5799
5965
|
await this.mutateRemoteCleanup(taskId, generation, () => undefined);
|
|
5800
5966
|
return;
|
|
5801
5967
|
}
|
|
@@ -5996,6 +6162,8 @@ export class AgentManager {
|
|
|
5996
6162
|
return false;
|
|
5997
6163
|
if ('phase' in alsoExpect && task.phase !== alsoExpect.phase)
|
|
5998
6164
|
return false;
|
|
6165
|
+
if ('agentId' in alsoExpect && task.agentId !== alsoExpect.agentId)
|
|
6166
|
+
return false;
|
|
5999
6167
|
if ('reviewRound' in alsoExpect && task.reviewRound !== alsoExpect.reviewRound)
|
|
6000
6168
|
return false;
|
|
6001
6169
|
if ('specReviewRound' in alsoExpect && task.specReviewRound !== alsoExpect.specReviewRound)
|
|
@@ -6194,38 +6362,52 @@ export class AgentManager {
|
|
|
6194
6362
|
}
|
|
6195
6363
|
async transitionTaskStatus(taskId, toStatus, guard, patch) {
|
|
6196
6364
|
return this.withTaskLock(async () => {
|
|
6197
|
-
const
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
return null;
|
|
6207
|
-
if (Object.hasOwn(guard, 'expectPhase') && task.phase !== guard.expectPhase)
|
|
6208
|
-
return null;
|
|
6209
|
-
if (guard.expectPostApproveEpisode !== undefined
|
|
6210
|
-
&& !this.postApproveEpisodeMatches(task, guard.expectPostApproveEpisode))
|
|
6211
|
-
return null;
|
|
6212
|
-
if (guard.expectReviewDispatch !== undefined
|
|
6213
|
-
&& !this.gitReviewDispatchMatches(task, guard.expectReviewDispatch))
|
|
6214
|
-
return null;
|
|
6215
|
-
if (patch?.branch && patch.branch !== task.branch) {
|
|
6216
|
-
const existing = await this.findTaskByBranch(patch.branch, task.projectId);
|
|
6217
|
-
if (existing && existing.id !== taskId)
|
|
6365
|
+
const transition = async () => {
|
|
6366
|
+
if (guard.expectAgentLock) {
|
|
6367
|
+
const binding = await this.agentStore.get(guard.expectAgentLock.agentId);
|
|
6368
|
+
if (binding?.taskId !== taskId
|
|
6369
|
+
|| binding.lockToken !== guard.expectAgentLock.token)
|
|
6370
|
+
return null;
|
|
6371
|
+
}
|
|
6372
|
+
const task = await this.taskStore.get(taskId);
|
|
6373
|
+
if (!task)
|
|
6218
6374
|
return null;
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6375
|
+
const previousStatus = task.status;
|
|
6376
|
+
if (TERMINAL_STATUSES.includes(previousStatus))
|
|
6377
|
+
return null;
|
|
6378
|
+
if (!guard.fromStatus.includes(previousStatus))
|
|
6379
|
+
return null;
|
|
6380
|
+
if (guard.expectTask && !taskMatchesGeneration(task, guard.expectTask))
|
|
6381
|
+
return null;
|
|
6382
|
+
if (Object.hasOwn(guard, 'expectSignalToken') && task.signalToken !== guard.expectSignalToken)
|
|
6383
|
+
return null;
|
|
6384
|
+
if (Object.hasOwn(guard, 'expectPhase') && task.phase !== guard.expectPhase)
|
|
6385
|
+
return null;
|
|
6386
|
+
if (guard.expectPostApproveEpisode !== undefined
|
|
6387
|
+
&& !this.postApproveEpisodeMatches(task, guard.expectPostApproveEpisode))
|
|
6388
|
+
return null;
|
|
6389
|
+
if (guard.expectReviewDispatch !== undefined
|
|
6390
|
+
&& !this.gitReviewDispatchMatches(task, guard.expectReviewDispatch))
|
|
6391
|
+
return null;
|
|
6392
|
+
if (patch?.branch && patch.branch !== task.branch) {
|
|
6393
|
+
const existing = await this.findTaskByBranch(patch.branch, task.projectId);
|
|
6394
|
+
if (existing && existing.id !== taskId)
|
|
6395
|
+
return null;
|
|
6396
|
+
}
|
|
6397
|
+
const next = this.stripGitStatusScopedState(task, toStatus);
|
|
6398
|
+
if (previousStatus !== toStatus)
|
|
6399
|
+
delete next.serverSignalRecovery;
|
|
6400
|
+
Object.assign(next, patch ?? {}, {
|
|
6401
|
+
status: toStatus,
|
|
6402
|
+
updatedAt: new Date().toISOString(),
|
|
6403
|
+
});
|
|
6404
|
+
await this.taskStore.set(next);
|
|
6405
|
+
return { task: next, previousStatus };
|
|
6406
|
+
};
|
|
6407
|
+
if (!guard.expectAgentLock)
|
|
6408
|
+
return transition();
|
|
6409
|
+
const guarded = await this.lockManager.runIfOwner(guard.expectAgentLock.agentId, taskId, guard.expectAgentLock.token, transition);
|
|
6410
|
+
return guarded.ran ? guarded.value : null;
|
|
6229
6411
|
});
|
|
6230
6412
|
}
|
|
6231
6413
|
createRunnerFor(agent) {
|
|
@@ -7379,11 +7561,6 @@ export class AgentManager {
|
|
|
7379
7561
|
...(promptSignalToken ? { signalToken: promptSignalToken } : {}),
|
|
7380
7562
|
...(promptSpecRound !== undefined ? { currentSpecRound: promptSpecRound } : {}),
|
|
7381
7563
|
...(imagePaths.length ? { imagePaths } : {}),
|
|
7382
|
-
...(reviewCheckout ? { serverReviewCheckout: reviewCheckout.mode } : {}),
|
|
7383
|
-
...(reviewCheckout?.fallbackReason ? { serverReviewFallbackReason: reviewCheckout.fallbackReason } : {}),
|
|
7384
|
-
...(opts.serverBaseSha !== undefined ? { serverBaseSha: opts.serverBaseSha } : {}),
|
|
7385
|
-
...(opts.serverHeadSha !== undefined ? { serverHeadSha: opts.serverHeadSha } : {}),
|
|
7386
|
-
...(opts.serverHeadTree !== undefined ? { serverHeadTree: opts.serverHeadTree } : {}),
|
|
7387
7564
|
...(opts.serverBatch ? { serverBatch: opts.serverBatch } : {}),
|
|
7388
7565
|
...(opts.serverFindingsDigest ? { serverFindingsDigest: opts.serverFindingsDigest } : {}),
|
|
7389
7566
|
...(opts.serverFeedbackCorrection ? { serverFeedbackCorrection: opts.serverFeedbackCorrection } : {}),
|
|
@@ -7933,9 +8110,6 @@ export class AgentManager {
|
|
|
7933
8110
|
await this.assertTaskGeneration(agentId, taskId, lockToken, verifiedWorkdir);
|
|
7934
8111
|
},
|
|
7935
8112
|
});
|
|
7936
|
-
const useIncrementalNudge = typeof opts.postApproveRedispatchCount === 'number'
|
|
7937
|
-
&& opts.postApproveRedispatchCount > 0
|
|
7938
|
-
&& !ensure.freshRuntime;
|
|
7939
8113
|
const imagePaths = await this.imagePathsForDispatch(runner, task, phase);
|
|
7940
8114
|
let payloadOpts = {};
|
|
7941
8115
|
if (opts.serverContent !== undefined || opts.serverDiffstat !== undefined || opts.serverInterdiff !== undefined || opts.serverPriorFindings || opts.serverPriorResponse) {
|
|
@@ -7963,11 +8137,9 @@ export class AgentManager {
|
|
|
7963
8137
|
hasQaPartner: task.qaAgentId !== undefined,
|
|
7964
8138
|
...(platformCli ? { platformCli } : {}),
|
|
7965
8139
|
...(signalToken ? { signalToken } : {}),
|
|
7966
|
-
...(useIncrementalNudge
|
|
7967
|
-
? { postApproveRedispatchCount: opts.postApproveRedispatchCount }
|
|
7968
|
-
: {}),
|
|
7969
8140
|
...(promptSpecRound !== undefined ? { currentSpecRound: promptSpecRound } : {}),
|
|
7970
8141
|
...(imagePaths.length ? { imagePaths } : {}),
|
|
8142
|
+
...(opts.serverReviewCheckout ? { serverReviewCheckout: opts.serverReviewCheckout } : {}),
|
|
7971
8143
|
...(opts.serverBatch ? { serverBatch: opts.serverBatch } : {}),
|
|
7972
8144
|
...(opts.serverAfterDone ? { serverAfterDone: opts.serverAfterDone } : {}),
|
|
7973
8145
|
...(opts.serverFindingsDigest ? { serverFindingsDigest: opts.serverFindingsDigest } : {}),
|
|
@@ -8732,10 +8904,11 @@ export class AgentManager {
|
|
|
8732
8904
|
&& this.gitReviewLeaseMatches(task, lease);
|
|
8733
8905
|
}
|
|
8734
8906
|
async dispatchGitReviewLease(taskId, opts = {}) {
|
|
8735
|
-
const claimed = await this.claimGitReviewDispatch(taskId, opts.expectedGeneration);
|
|
8907
|
+
const claimed = await this.claimGitReviewDispatch(taskId, opts.expectedGeneration, opts.expectedTask);
|
|
8736
8908
|
if (!claimed) {
|
|
8737
8909
|
throw new ApiError(409, `Git review dispatch for ${taskId} was already claimed or superseded`, 'dispatch-superseded');
|
|
8738
8910
|
}
|
|
8911
|
+
opts.onSideEffect?.();
|
|
8739
8912
|
const { task, lease } = claimed;
|
|
8740
8913
|
const qaId = task.qaAgentId;
|
|
8741
8914
|
if (!qaId || !this.gitReviewQaConfigUsable(task)) {
|
|
@@ -8747,7 +8920,7 @@ export class AgentManager {
|
|
|
8747
8920
|
if (previousQa?.taskId === taskId) {
|
|
8748
8921
|
if (previousQa.status === 'awaiting_human' && !isRecoverableQaDispatchHold(previousQa)) {
|
|
8749
8922
|
await this.resetGitReviewDispatchClaim(taskId, lease);
|
|
8750
|
-
throw new ApiError(409, `QA agent ${qaId} is awaiting human (${previousQa.awaitingPhase ?? 'unknown phase'})
|
|
8923
|
+
throw new ApiError(409, `QA agent ${qaId} is awaiting human (${previousQa.awaitingPhase ?? 'unknown phase'})`, 'dispatch-unsupported');
|
|
8751
8924
|
}
|
|
8752
8925
|
const released = await this.releaseAgentForTask(qaId, taskId, 'idle', {
|
|
8753
8926
|
expectedTask: { status: 'review', phase: task.phase, signalToken: lease.signalToken },
|
|
@@ -8768,11 +8941,13 @@ export class AgentManager {
|
|
|
8768
8941
|
await this.resetGitReviewDispatchClaim(taskId, lease);
|
|
8769
8942
|
throw new ApiError(409, `QA agent ${qaId} could not be released for git review dispatch`);
|
|
8770
8943
|
}
|
|
8944
|
+
opts.onSideEffect?.();
|
|
8771
8945
|
}
|
|
8772
8946
|
let acquiredLockToken;
|
|
8773
8947
|
const acquired = await this.acquireAgentForTask(qaId, taskId, qaPhase, {
|
|
8774
8948
|
onAcquired: token => {
|
|
8775
8949
|
acquiredLockToken = token;
|
|
8950
|
+
opts.onSideEffect?.();
|
|
8776
8951
|
opts.onQaAcquired?.(token);
|
|
8777
8952
|
},
|
|
8778
8953
|
});
|
|
@@ -8867,6 +9042,9 @@ export class AgentManager {
|
|
|
8867
9042
|
async dispatchReviewToQa(taskId, opts = {}) {
|
|
8868
9043
|
let routeTask = await this.taskStore.get(taskId);
|
|
8869
9044
|
if (routeTask?.reviewMode === 'git' && !isSpecStagePhase(routeTask.phase)) {
|
|
9045
|
+
if (opts.expectedTask && !taskMatchesGeneration(routeTask, opts.expectedTask)) {
|
|
9046
|
+
throw new ApiError(409, `Task ${taskId} review generation changed during redispatch`, 'dispatch-superseded');
|
|
9047
|
+
}
|
|
8870
9048
|
if (opts.fromStatus && !opts.fromStatus.includes(routeTask.status)) {
|
|
8871
9049
|
throw new ApiError(409, `Task ${taskId} status is ${routeTask.status}; review dispatch requires ${opts.fromStatus.join('/')}`, 'dispatch-superseded');
|
|
8872
9050
|
}
|
|
@@ -8876,6 +9054,9 @@ export class AgentManager {
|
|
|
8876
9054
|
if (Object.hasOwn(opts, 'expectPhase') && routeTask.phase !== opts.expectPhase) {
|
|
8877
9055
|
throw new ApiError(409, `Task ${taskId} review phase changed during redispatch`, 'dispatch-superseded');
|
|
8878
9056
|
}
|
|
9057
|
+
if (!routeTask.qaAgentId || this.getAgentConfig(routeTask.qaAgentId)?.role !== 'qa') {
|
|
9058
|
+
throw new ApiError(409, `Task ${taskId} has no available QA participant`, 'dispatch-unsupported');
|
|
9059
|
+
}
|
|
8879
9060
|
let lease = routeTask.reviewDispatch;
|
|
8880
9061
|
if (lease?.phase === 'uncertain') {
|
|
8881
9062
|
if (opts.confirmUncertainNotDelivered !== true) {
|
|
@@ -8897,17 +9078,32 @@ export class AgentManager {
|
|
|
8897
9078
|
}
|
|
8898
9079
|
routeTask = confirmed;
|
|
8899
9080
|
lease = confirmed.reviewDispatch;
|
|
9081
|
+
opts.onSideEffect?.();
|
|
9082
|
+
if (opts.expectedTask && !taskMatchesGeneration(routeTask, opts.expectedTask)) {
|
|
9083
|
+
throw new ApiError(409, `Task ${taskId} review generation changed during uncertain-delivery confirmation`, 'dispatch-superseded');
|
|
9084
|
+
}
|
|
8900
9085
|
}
|
|
9086
|
+
const routeQaId = routeTask.qaAgentId;
|
|
9087
|
+
if (!routeQaId || this.getAgentConfig(routeQaId)?.role !== 'qa') {
|
|
9088
|
+
throw new ApiError(409, `Task ${taskId} has no available QA participant`, 'dispatch-unsupported');
|
|
9089
|
+
}
|
|
9090
|
+
const qaBinding = await this.agentStore.get(routeQaId);
|
|
9091
|
+
if (qaBinding?.taskId === taskId
|
|
9092
|
+
&& qaBinding.status === 'awaiting_human'
|
|
9093
|
+
&& !isRecoverableQaDispatchHold(qaBinding)) {
|
|
9094
|
+
throw new ApiError(409, `QA agent ${routeQaId} is awaiting human (${qaBinding.awaitingPhase ?? 'unknown phase'})`, 'dispatch-unsupported');
|
|
9095
|
+
}
|
|
9096
|
+
let dispatchExpectedTask = opts.expectedTask;
|
|
8901
9097
|
if (lease === undefined) {
|
|
8902
9098
|
if (routeTask.prNumber === undefined) {
|
|
8903
|
-
throw new ApiError(400, `Task ${taskId} has no PR yet; cannot dispatch review
|
|
9099
|
+
throw new ApiError(400, `Task ${taskId} has no PR yet; cannot dispatch review`, 'dispatch-unsupported');
|
|
8904
9100
|
}
|
|
8905
9101
|
const verified = await this.platformVerifyPrBinding(taskId, routeTask.prNumber);
|
|
8906
9102
|
if (!verified.ok) {
|
|
8907
|
-
throw new ApiError(409, `Cannot dispatch git review for ${taskId}: binding ${verified.reason}
|
|
9103
|
+
throw new ApiError(409, `Cannot dispatch git review for ${taskId}: binding ${verified.reason}`, 'dispatch-unsupported');
|
|
8908
9104
|
}
|
|
8909
9105
|
if (!PLATFORM_HEAD_SHA_RE.test(verified.headSha)) {
|
|
8910
|
-
throw new ApiError(502, `Cannot dispatch git review for ${taskId}: invalid platform head
|
|
9106
|
+
throw new ApiError(502, `Cannot dispatch git review for ${taskId}: invalid platform head`, 'dispatch-unsupported');
|
|
8911
9107
|
}
|
|
8912
9108
|
const begun = await this.beginGitReviewPass(taskId, {
|
|
8913
9109
|
fromStatus: [routeTask.status],
|
|
@@ -8916,17 +9112,27 @@ export class AgentManager {
|
|
|
8916
9112
|
...(opts.qaPhase !== undefined ? { qaPhase: opts.qaPhase } : {}),
|
|
8917
9113
|
expectSignalToken: routeTask.signalToken,
|
|
8918
9114
|
expectPhase: routeTask.phase,
|
|
9115
|
+
...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
|
|
8919
9116
|
patch: { latestHeadSha: verified.headSha },
|
|
8920
9117
|
});
|
|
8921
9118
|
if (!begun?.task.reviewDispatch) {
|
|
8922
9119
|
throw new ApiError(409, `Task ${taskId} changed before git review pass creation`, 'dispatch-superseded');
|
|
8923
9120
|
}
|
|
9121
|
+
opts.onSideEffect?.();
|
|
8924
9122
|
lease = begun.task.reviewDispatch;
|
|
9123
|
+
if (opts.expectedTask) {
|
|
9124
|
+
dispatchExpectedTask = {
|
|
9125
|
+
...opts.expectedTask,
|
|
9126
|
+
signalToken: begun.task.signalToken,
|
|
9127
|
+
};
|
|
9128
|
+
}
|
|
8925
9129
|
}
|
|
8926
9130
|
const dispatched = await this.dispatchGitReviewLease(taskId, {
|
|
8927
9131
|
expectedGeneration: lease.generation,
|
|
9132
|
+
...(dispatchExpectedTask !== undefined ? { expectedTask: dispatchExpectedTask } : {}),
|
|
8928
9133
|
...(opts.pendingBudget !== undefined ? { pendingBudget: opts.pendingBudget } : {}),
|
|
8929
9134
|
...(opts.onQaAcquired !== undefined ? { onQaAcquired: opts.onQaAcquired } : {}),
|
|
9135
|
+
...(opts.onSideEffect !== undefined ? { onSideEffect: opts.onSideEffect } : {}),
|
|
8930
9136
|
});
|
|
8931
9137
|
opts.onPassArmed?.(lease.signalToken);
|
|
8932
9138
|
return dispatched;
|
|
@@ -8938,12 +9144,19 @@ export class AgentManager {
|
|
|
8938
9144
|
if (this.markCompleteInFlight.has(taskId)) {
|
|
8939
9145
|
throw new ApiError(409, `Task ${taskId} is being completed (merge in progress); try again shortly`, 'dispatch-in-flight');
|
|
8940
9146
|
}
|
|
9147
|
+
// 早闸在 claim 前零副作用拒绝:driver 的 putRound/删 response 副作用都在终闸之前。
|
|
9148
|
+
if (this.phaseSignalWatcher?.isSettling(taskId)) {
|
|
9149
|
+
throw new ApiError(409, `Task ${taskId} has a review completion settling; retry after it lands`, 'review-settling');
|
|
9150
|
+
}
|
|
8941
9151
|
const task = await this.taskStore.get(taskId);
|
|
8942
9152
|
if (!task)
|
|
8943
9153
|
throw new ApiError(404, `Task ${taskId} not found`);
|
|
8944
9154
|
if (task.reviewMode !== 'server' && !isSpecStagePhase(task.phase)) {
|
|
8945
9155
|
throw new ApiError(409, `Task ${taskId} review mode changed during dispatch; retry`, 'dispatch-superseded');
|
|
8946
9156
|
}
|
|
9157
|
+
if (opts.expectedTask && !taskMatchesGeneration(task, opts.expectedTask)) {
|
|
9158
|
+
throw new ApiError(409, `Task ${taskId} review generation changed during redispatch`, 'dispatch-superseded');
|
|
9159
|
+
}
|
|
8947
9160
|
if (opts.fromStatus && !opts.fromStatus.includes(task.status)) {
|
|
8948
9161
|
throw new ApiError(409, `Task ${taskId} status is ${task.status}; review dispatch requires ${opts.fromStatus.join('/')}`, 'dispatch-superseded');
|
|
8949
9162
|
}
|
|
@@ -8954,27 +9167,29 @@ export class AgentManager {
|
|
|
8954
9167
|
throw new ApiError(409, `Task ${taskId} review pass changed during redispatch (phase changed); aborting`, 'dispatch-superseded');
|
|
8955
9168
|
}
|
|
8956
9169
|
if (!MANUAL_SERVER_REVIEW_STATUSES.includes(task.status)) {
|
|
8957
|
-
throw new ApiError(409, `Task ${taskId} status is ${task.status}; manual server-side review requires ${MANUAL_SERVER_REVIEW_STATUSES.join('/')}
|
|
9170
|
+
throw new ApiError(409, `Task ${taskId} status is ${task.status}; manual server-side review requires ${MANUAL_SERVER_REVIEW_STATUSES.join('/')}`, 'dispatch-unsupported');
|
|
8958
9171
|
}
|
|
8959
9172
|
if (!this.serverReviewDriver) {
|
|
8960
|
-
throw new ApiError(409, `Server review pipeline is not configured; cannot dispatch review for ${taskId}
|
|
9173
|
+
throw new ApiError(409, `Server review pipeline is not configured; cannot dispatch review for ${taskId}`, 'dispatch-unsupported');
|
|
8961
9174
|
}
|
|
8962
9175
|
if (task.status === 'in_progress' && task.phase === undefined) {
|
|
8963
|
-
throw new ApiError(409, `Task ${taskId} has no phase yet (the dev has not delivered spec-done/code-done); wait for the dev signal or use Cancel/Retry
|
|
9176
|
+
throw new ApiError(409, `Task ${taskId} has no phase yet (the dev has not delivered spec-done/code-done); wait for the dev signal or use Cancel/Retry`, 'dispatch-unsupported');
|
|
8964
9177
|
}
|
|
8965
9178
|
const qaAvailable = task.qaAgentId !== undefined && this.getAgentConfig(task.qaAgentId)?.role === 'qa';
|
|
8966
9179
|
if (!qaAvailable) {
|
|
8967
|
-
throw new ApiError(400, `Task ${taskId} has no QA partner configured; manual review requires a QA agent
|
|
9180
|
+
throw new ApiError(400, `Task ${taskId} has no QA partner configured; manual review requires a QA agent`, 'dispatch-unsupported');
|
|
8968
9181
|
}
|
|
8969
9182
|
const isSpec = isSpecStagePhase(task.phase);
|
|
9183
|
+
const qaBinding = await this.agentStore.get(task.qaAgentId);
|
|
9184
|
+
const bumpRound = shouldBumpServerReviewRound(opts.bumpRound, task, qaBinding);
|
|
8970
9185
|
const cap = this.config.review.rounds + (task.maxRoundsContinues ?? 0);
|
|
8971
9186
|
const round = isSpec ? (task.specReviewRound ?? 0) : task.reviewRound;
|
|
8972
|
-
const nextRound =
|
|
9187
|
+
const nextRound = bumpRound ? round + 1 : round;
|
|
8973
9188
|
if (nextRound < 1) {
|
|
8974
|
-
throw new ApiError(409, `Task ${taskId} has no current review round to redispatch
|
|
9189
|
+
throw new ApiError(409, `Task ${taskId} has no current review round to redispatch`, 'dispatch-unsupported');
|
|
8975
9190
|
}
|
|
8976
9191
|
if (nextRound > cap) {
|
|
8977
|
-
throw new ApiError(409, `Task ${taskId} reached the review round cap (${cap}); continue or cancel it instead
|
|
9192
|
+
throw new ApiError(409, `Task ${taskId} reached the review round cap (${cap}); continue or cancel it instead`, 'dispatch-unsupported');
|
|
8978
9193
|
}
|
|
8979
9194
|
this.manualReviewInFlight.add(taskId);
|
|
8980
9195
|
return {
|
|
@@ -8983,35 +9198,45 @@ export class AgentManager {
|
|
|
8983
9198
|
status: task.status,
|
|
8984
9199
|
phase: task.phase,
|
|
8985
9200
|
signalToken: task.signalToken,
|
|
9201
|
+
agentId: task.agentId,
|
|
9202
|
+
reviewRound: task.reviewRound,
|
|
9203
|
+
specReviewRound: task.specReviewRound,
|
|
8986
9204
|
},
|
|
8987
|
-
bumpRound
|
|
9205
|
+
bumpRound,
|
|
9206
|
+
onSideEffect: opts.onSideEffect,
|
|
8988
9207
|
};
|
|
8989
9208
|
});
|
|
8990
9209
|
return this.runManualServerReview(taskId, claim);
|
|
8991
9210
|
}
|
|
8992
9211
|
async runManualServerReview(taskId, claim) {
|
|
8993
9212
|
try {
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
:
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9213
|
+
const run = async (ownerPermit) => {
|
|
9214
|
+
// 不在此处预释放旧 QA:可失败的准备工作(读 diff/spec、存轮次)失败时旧 pass 必须原样保留;
|
|
9215
|
+
// 同任务的 QA 重新绑定由 dispatchServerReviewToQa 在派发前完成。
|
|
9216
|
+
// 先入的结算持互斥跑完后,下面的任务代 CAS 拒绝本次手工;反向由 owner permit 过终闸。
|
|
9217
|
+
const fresh = await this.taskStore.get(taskId);
|
|
9218
|
+
if (!fresh
|
|
9219
|
+
|| !taskMatchesGeneration(fresh, claim.reviewPass)
|
|
9220
|
+
|| !MANUAL_SERVER_REVIEW_STATUSES.includes(fresh.status)) {
|
|
9221
|
+
throw new ApiError(409, `Task ${taskId} changed during manual review dispatch; aborting`);
|
|
9222
|
+
}
|
|
9223
|
+
const driverOpts = {
|
|
9224
|
+
bumpRound: claim.bumpRound,
|
|
9225
|
+
expectedTask: claim.reviewPass,
|
|
9226
|
+
...(claim.onSideEffect !== undefined ? { onSideEffect: claim.onSideEffect } : {}),
|
|
9227
|
+
...(ownerPermit !== undefined ? { settlePermit: ownerPermit } : {}),
|
|
9228
|
+
};
|
|
9229
|
+
const dispatched = claim.isSpec
|
|
9230
|
+
? await this.serverReviewDriver.dispatchSpecReview(fresh, driverOpts)
|
|
9231
|
+
: await this.serverReviewDriver.dispatchCodeReview(fresh, driverOpts);
|
|
9232
|
+
if (!dispatched) {
|
|
9233
|
+
throw new ApiError(500, `Manual review dispatch for ${taskId} did not start; check the event feed for the cause`);
|
|
9234
|
+
}
|
|
9235
|
+
return (await this.taskStore.get(taskId));
|
|
9236
|
+
};
|
|
9237
|
+
return this.phaseSignalWatcher
|
|
9238
|
+
? await this.phaseSignalWatcher.runExclusive(taskId, run)
|
|
9239
|
+
: await run();
|
|
9015
9240
|
}
|
|
9016
9241
|
finally {
|
|
9017
9242
|
this.manualReviewInFlight.delete(taskId);
|
|
@@ -9857,27 +10082,49 @@ export class AgentManager {
|
|
|
9857
10082
|
const task = await this.taskStore.get(taskId);
|
|
9858
10083
|
if (!task)
|
|
9859
10084
|
return null;
|
|
9860
|
-
const transition = await this.transitionTaskStatus(taskId, 'spec-ready', {
|
|
10085
|
+
const transition = await this.transitionTaskStatus(taskId, 'spec-ready', {
|
|
10086
|
+
fromStatus: ['review', 'in_progress', 'fixing'],
|
|
10087
|
+
...(opts.expectedTask !== undefined ? { expectTask: opts.expectedTask } : {}),
|
|
10088
|
+
}, {
|
|
9861
10089
|
phase: 'spec',
|
|
9862
10090
|
...(opts.specReviewRound !== undefined ? { specReviewRound: opts.specReviewRound } : {}),
|
|
9863
10091
|
});
|
|
9864
10092
|
if (!transition)
|
|
9865
10093
|
return null;
|
|
10094
|
+
const parked = transition.task;
|
|
10095
|
+
const parkedGeneration = {
|
|
10096
|
+
status: parked.status,
|
|
10097
|
+
phase: parked.phase,
|
|
10098
|
+
signalToken: parked.signalToken,
|
|
10099
|
+
agentId: parked.agentId,
|
|
10100
|
+
reviewRound: parked.reviewRound,
|
|
10101
|
+
specReviewRound: parked.specReviewRound,
|
|
10102
|
+
};
|
|
9866
10103
|
this.stopPhaseSignalWatcher(taskId);
|
|
9867
|
-
if (
|
|
9868
|
-
const devOk = await this.markAgentWaiting(
|
|
10104
|
+
if (parked.agentId) {
|
|
10105
|
+
const devOk = await this.markAgentWaiting(parked.agentId, taskId, {
|
|
10106
|
+
expectedTask: parkedGeneration,
|
|
10107
|
+
}).catch(() => false);
|
|
9869
10108
|
if (!devOk) {
|
|
9870
|
-
await this.emitIntervention(
|
|
10109
|
+
await this.emitIntervention(parked.projectId, parked.agentId, taskId, {
|
|
10110
|
+
phase: 'spec-ready-dev-park-failed',
|
|
10111
|
+
devAgentId: parked.agentId,
|
|
10112
|
+
});
|
|
9871
10113
|
}
|
|
9872
10114
|
}
|
|
9873
|
-
if (
|
|
9874
|
-
const released = await this.releaseAgentForTask(
|
|
10115
|
+
if (parked.qaAgentId) {
|
|
10116
|
+
const released = await this.releaseAgentForTask(parked.qaAgentId, taskId, 'idle', {
|
|
10117
|
+
expectedTask: parkedGeneration,
|
|
10118
|
+
})
|
|
9875
10119
|
.catch(() => false);
|
|
9876
10120
|
if (!released) {
|
|
9877
|
-
await this.emitIntervention(
|
|
10121
|
+
await this.emitIntervention(parked.projectId, parked.qaAgentId, taskId, {
|
|
10122
|
+
phase: 'spec-ready-qa-release-failed',
|
|
10123
|
+
qaAgentId: parked.qaAgentId,
|
|
10124
|
+
});
|
|
9878
10125
|
}
|
|
9879
10126
|
}
|
|
9880
|
-
return
|
|
10127
|
+
return parked;
|
|
9881
10128
|
}
|
|
9882
10129
|
async submitSpecVerdict(taskId, verdict, comments) {
|
|
9883
10130
|
const trimmed = comments?.trim();
|
|
@@ -10217,8 +10464,13 @@ export class AgentManager {
|
|
|
10217
10464
|
})
|
|
10218
10465
|
: this.continueSession(task.id, devAgentId, 'code', dispatchOpts);
|
|
10219
10466
|
}
|
|
10220
|
-
async transitionToCodePhase(taskId) {
|
|
10221
|
-
const task = await this.
|
|
10467
|
+
async transitionToCodePhase(taskId, expectedTask) {
|
|
10468
|
+
const task = await this.withTaskLock(async () => {
|
|
10469
|
+
const current = await this.taskStore.get(taskId);
|
|
10470
|
+
if (expectedTask && current && !taskMatchesGeneration(current, expectedTask))
|
|
10471
|
+
return null;
|
|
10472
|
+
return current;
|
|
10473
|
+
});
|
|
10222
10474
|
if (!task)
|
|
10223
10475
|
return null;
|
|
10224
10476
|
const devAgentId = task.devAgentId;
|
|
@@ -10242,7 +10494,10 @@ export class AgentManager {
|
|
|
10242
10494
|
const researchAgentId = task.researchAgentId;
|
|
10243
10495
|
const researchHandoff = this.isResearchHandoff(task, devAgentId);
|
|
10244
10496
|
const releaseParticipant = async (agentId, failurePhase) => {
|
|
10245
|
-
const released = await this.releaseAgentIfBound(agentId, taskId, {
|
|
10497
|
+
const released = await this.releaseAgentIfBound(agentId, taskId, {
|
|
10498
|
+
allowAwaitingHuman: true,
|
|
10499
|
+
...(expectedTask !== undefined ? { expectedTask } : {}),
|
|
10500
|
+
})
|
|
10246
10501
|
.catch(() => false);
|
|
10247
10502
|
if (!released) {
|
|
10248
10503
|
await this.emitIntervention(task.projectId, agentId, taskId, { phase: failurePhase, agentId });
|
|
@@ -10257,7 +10512,10 @@ export class AgentManager {
|
|
|
10257
10512
|
return null;
|
|
10258
10513
|
}
|
|
10259
10514
|
const devBoundBefore = (await this.agentStore.get(devAgentId))?.taskId === taskId;
|
|
10260
|
-
|
|
10515
|
+
let acquiredLockToken;
|
|
10516
|
+
const acquired = await this.acquireAgentForTask(devAgentId, taskId, 'code', {
|
|
10517
|
+
onAcquired: token => { acquiredLockToken = token; },
|
|
10518
|
+
});
|
|
10261
10519
|
if (!acquired) {
|
|
10262
10520
|
await this.emitIntervention(task.projectId, devAgentId, taskId, {
|
|
10263
10521
|
phase: 'code-dev-acquire-failed',
|
|
@@ -10265,13 +10523,25 @@ export class AgentManager {
|
|
|
10265
10523
|
});
|
|
10266
10524
|
return null;
|
|
10267
10525
|
}
|
|
10268
|
-
const acquiredLockToken = (await this.agentStore.get(devAgentId))?.lockToken;
|
|
10269
10526
|
const newToken = createSignalToken();
|
|
10270
10527
|
// spec 阶段的扩轮决策不延续到 code 评审:cap 共享字段,进入编码时归零
|
|
10271
|
-
const transition = await this.transitionTaskStatus(taskId, 'in_progress', {
|
|
10528
|
+
const transition = await this.transitionTaskStatus(taskId, 'in_progress', {
|
|
10529
|
+
fromStatus: ['review', 'fixing', 'in_progress', 'spec-ready', 'max_rounds'],
|
|
10530
|
+
...(expectedTask !== undefined ? { expectTask: expectedTask } : {}),
|
|
10531
|
+
}, { agentId: devAgentId, phase: 'code', maxRoundsContinues: 0, ...this.dispatchTokenFields({ ...task, phase: 'code' }, newToken) });
|
|
10272
10532
|
if (!transition) {
|
|
10273
|
-
|
|
10274
|
-
|
|
10533
|
+
if (!devBoundBefore) {
|
|
10534
|
+
try {
|
|
10535
|
+
await this.releaseAgentForTask(devAgentId, taskId, 'idle', {
|
|
10536
|
+
allowAwaitingHuman: true,
|
|
10537
|
+
...(expectedTask !== undefined ? { expectedTask } : {}),
|
|
10538
|
+
...(acquiredLockToken !== undefined ? { expectedLockToken: acquiredLockToken } : {}),
|
|
10539
|
+
});
|
|
10540
|
+
}
|
|
10541
|
+
catch (err) {
|
|
10542
|
+
console.warn(`[AgentManager] transitionToCodePhase(${taskId}): stale-generation acquire rollback failed:`, err);
|
|
10543
|
+
}
|
|
10544
|
+
}
|
|
10275
10545
|
return null;
|
|
10276
10546
|
}
|
|
10277
10547
|
this.stopPhaseSignalWatcher(taskId);
|
|
@@ -10337,12 +10607,22 @@ export class AgentManager {
|
|
|
10337
10607
|
: (opts.recheck ? 'server-recheck' : 'server-review');
|
|
10338
10608
|
const expectedKind = opts.phase === 'spec' ? 'spec-reviewed' : 'code-reviewed';
|
|
10339
10609
|
const claim = await this.withTaskLock(async () => {
|
|
10610
|
+
// 终闸与 newToken 铸发同临界区:settling 中仅结算 handler 链(settle permit)
|
|
10611
|
+
// 或当前互斥 FIFO owner(手工先入者)可开新 pass。
|
|
10612
|
+
if (this.phaseSignalWatcher?.isSettling(taskId)
|
|
10613
|
+
&& !this.phaseSignalWatcher.settlePermitMatches(taskId, opts.settlePermit)
|
|
10614
|
+
&& !this.phaseSignalWatcher.exclusiveOwnerMatches(taskId, opts.settlePermit)) {
|
|
10615
|
+
throw new ApiError(409, `Task ${taskId} has a review completion settling; retry after it lands`, 'review-settling');
|
|
10616
|
+
}
|
|
10340
10617
|
const task = await this.taskStore.get(taskId);
|
|
10341
10618
|
if (!task)
|
|
10342
10619
|
throw new Error(`dispatchServerReviewToQa: task ${taskId} not found`);
|
|
10343
10620
|
if (task.reviewMode !== 'server' && opts.phase !== 'spec') {
|
|
10344
10621
|
throw new Error(`dispatchServerReviewToQa: task ${taskId} is not in server review mode`);
|
|
10345
10622
|
}
|
|
10623
|
+
if (opts.expectedTask && !taskMatchesGeneration(task, opts.expectedTask)) {
|
|
10624
|
+
throw new ApiError(409, `Task ${taskId} review generation changed before server dispatch`, 'dispatch-superseded');
|
|
10625
|
+
}
|
|
10346
10626
|
const qaId = task.qaAgentId;
|
|
10347
10627
|
if (!qaId) {
|
|
10348
10628
|
if (!opts.callerOwnsConsumedSignalFailure) {
|
|
@@ -10371,11 +10651,13 @@ export class AgentManager {
|
|
|
10371
10651
|
return null;
|
|
10372
10652
|
}
|
|
10373
10653
|
const roundField = opts.phase === 'spec' ? (task.specReviewRound ?? 0) : task.reviewRound;
|
|
10654
|
+
const qaBinding = await this.agentStore.get(qaId);
|
|
10655
|
+
const bumpRound = shouldBumpServerReviewRound(opts.bumpRound, task, qaBinding);
|
|
10374
10656
|
const newRound = opts.continuation
|
|
10375
10657
|
? Math.max(roundField, 1)
|
|
10376
|
-
:
|
|
10377
|
-
? roundField
|
|
10378
|
-
: roundField
|
|
10658
|
+
: bumpRound
|
|
10659
|
+
? roundField + 1
|
|
10660
|
+
: roundField;
|
|
10379
10661
|
if (newRound < 1) {
|
|
10380
10662
|
throw new Error(`dispatchServerReviewToQa: task ${taskId} has no current ${opts.phase} review round to redispatch`);
|
|
10381
10663
|
}
|
|
@@ -10391,6 +10673,7 @@ export class AgentManager {
|
|
|
10391
10673
|
originalBatchIndex: task.batchIndex,
|
|
10392
10674
|
originalBatchTotal: task.batchTotal,
|
|
10393
10675
|
originalPhase: task.phase,
|
|
10676
|
+
originalReviewCheckoutMode: task.reviewCheckoutMode,
|
|
10394
10677
|
};
|
|
10395
10678
|
});
|
|
10396
10679
|
if (!claim)
|
|
@@ -10402,11 +10685,14 @@ export class AgentManager {
|
|
|
10402
10685
|
fromStatus: [installedPass.status],
|
|
10403
10686
|
expectPhase: installedPass.phase,
|
|
10404
10687
|
expectSignalToken: installedPass.signalToken,
|
|
10688
|
+
expectTask: installedPass,
|
|
10405
10689
|
}, {
|
|
10406
10690
|
signalToken: claim.originalToken,
|
|
10407
10691
|
batchIndex: claim.originalBatchIndex,
|
|
10408
10692
|
batchTotal: claim.originalBatchTotal,
|
|
10409
10693
|
phase: claim.originalPhase,
|
|
10694
|
+
// fresh 派发在 install 时清过 mode:不还原会把仍在跑的 base pass 降级成 unknown
|
|
10695
|
+
reviewCheckoutMode: claim.originalReviewCheckoutMode,
|
|
10410
10696
|
...(opts.phase === 'spec'
|
|
10411
10697
|
? { specReviewRound: claim.originalRound }
|
|
10412
10698
|
: { reviewRound: claim.originalRound }),
|
|
@@ -10448,17 +10734,35 @@ export class AgentManager {
|
|
|
10448
10734
|
if (!opts.continuation) {
|
|
10449
10735
|
const prevQa = await this.agentStore.get(qaId);
|
|
10450
10736
|
if (prevQa?.taskId === taskId) {
|
|
10451
|
-
|
|
10737
|
+
if (prevQa.status === 'awaiting_human' && !isRecoverableQaDispatchHold(prevQa)) {
|
|
10738
|
+
throw new ApiError(409, `QA agent ${qaId} is awaiting human (${prevQa.awaitingPhase ?? 'unknown phase'}); resolve that hold before redispatching server review`, 'dispatch-unsupported');
|
|
10739
|
+
}
|
|
10740
|
+
// 手工重派:上一 pass 的 QA 仍绑定本任务,先释放再重新 acquire
|
|
10452
10741
|
const released = await this.releaseAgentForTask(qaId, taskId, 'idle', {
|
|
10742
|
+
...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
|
|
10453
10743
|
...(prevQa.lockToken !== undefined ? { expectedLockToken: prevQa.lockToken } : {}),
|
|
10744
|
+
...(prevQa.status === 'awaiting_human'
|
|
10745
|
+
? {
|
|
10746
|
+
allowAwaitingHuman: true,
|
|
10747
|
+
expectedHold: {
|
|
10748
|
+
phase: prevQa.awaitingPhase,
|
|
10749
|
+
since: prevQa.awaitingSince,
|
|
10750
|
+
nonce: prevQa.awaitingNonce,
|
|
10751
|
+
},
|
|
10752
|
+
}
|
|
10753
|
+
: {}),
|
|
10454
10754
|
});
|
|
10455
10755
|
if (!released) {
|
|
10456
10756
|
await rearmEntrySignal();
|
|
10457
10757
|
return null;
|
|
10458
10758
|
}
|
|
10759
|
+
opts.onSideEffect?.();
|
|
10459
10760
|
}
|
|
10460
10761
|
const acquired = await this.acquireAgentForTask(qaId, taskId, dispatchPhase, {
|
|
10461
|
-
onAcquired: (lockToken) => {
|
|
10762
|
+
onAcquired: (lockToken) => {
|
|
10763
|
+
acquiredLockToken = lockToken;
|
|
10764
|
+
opts.onSideEffect?.();
|
|
10765
|
+
},
|
|
10462
10766
|
});
|
|
10463
10767
|
if (!acquired) {
|
|
10464
10768
|
await rearmEntrySignal();
|
|
@@ -10471,7 +10775,9 @@ export class AgentManager {
|
|
|
10471
10775
|
return null;
|
|
10472
10776
|
}
|
|
10473
10777
|
if (devAgentId) {
|
|
10474
|
-
const devOk = await this.markAgentWaiting(devAgentId, taskId
|
|
10778
|
+
const devOk = await this.markAgentWaiting(devAgentId, taskId, {
|
|
10779
|
+
...(opts.expectedTask !== undefined ? { expectedTask: opts.expectedTask } : {}),
|
|
10780
|
+
});
|
|
10475
10781
|
if (!devOk) {
|
|
10476
10782
|
await releaseOwnAcquire('dev park failed');
|
|
10477
10783
|
await rearmEntrySignal();
|
|
@@ -10505,6 +10811,7 @@ export class AgentManager {
|
|
|
10505
10811
|
fromStatus: [claim.originalStatus],
|
|
10506
10812
|
expectPhase: claim.originalPhase,
|
|
10507
10813
|
expectSignalToken: claim.originalToken,
|
|
10814
|
+
...(opts.expectedTask !== undefined ? { expectTask: opts.expectedTask } : {}),
|
|
10508
10815
|
}, {
|
|
10509
10816
|
signalToken: newToken,
|
|
10510
10817
|
reviewDispatchedAt: new Date().toISOString(),
|
|
@@ -10530,15 +10837,18 @@ export class AgentManager {
|
|
|
10530
10837
|
}
|
|
10531
10838
|
return null;
|
|
10532
10839
|
}
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10840
|
+
opts.onSideEffect?.();
|
|
10841
|
+
const installedPass = taskGenerationGuard(transition.task);
|
|
10842
|
+
const passStillCurrent = async () => {
|
|
10843
|
+
const current = await this.taskStore.get(taskId);
|
|
10844
|
+
return current !== null && taskMatchesGeneration(current, installedPass);
|
|
10537
10845
|
};
|
|
10538
10846
|
let dispatchedCheckoutMode;
|
|
10539
10847
|
const sessionOpts = {
|
|
10540
10848
|
bypassTaskStatusGate: true,
|
|
10541
10849
|
signalToken: newToken,
|
|
10850
|
+
dispatchPassToken: newToken,
|
|
10851
|
+
guardBeforeInject: passStillCurrent,
|
|
10542
10852
|
serverContent: opts.content,
|
|
10543
10853
|
...(opts.interdiff !== undefined ? { serverInterdiff: opts.interdiff } : {}),
|
|
10544
10854
|
...(opts.diffstat !== undefined ? { serverDiffstat: opts.diffstat } : {}),
|
|
@@ -10549,7 +10859,10 @@ export class AgentManager {
|
|
|
10549
10859
|
...(opts.priorFindingsJson ? { serverPriorFindings: opts.priorFindingsJson } : {}),
|
|
10550
10860
|
...(opts.priorResponseJson ? { serverPriorResponse: opts.priorResponseJson } : {}),
|
|
10551
10861
|
...(opts.phase === 'spec' ? { currentSpecRound: newRound } : {}),
|
|
10552
|
-
armBeforeInject: (ctx) => {
|
|
10862
|
+
armBeforeInject: (ctx) => this.withTaskLock(async () => {
|
|
10863
|
+
const current = await this.taskStore.get(taskId);
|
|
10864
|
+
if (!current || !taskMatchesGeneration(current, installedPass))
|
|
10865
|
+
return false;
|
|
10553
10866
|
dispatchedCheckoutMode = ctx.serverReviewCheckout;
|
|
10554
10867
|
const allowReadFile = opts.phase === 'spec' || opts.continuation || ctx.serverReviewCheckout === 'base';
|
|
10555
10868
|
return this.setupPhaseSignalWatcher(taskId, qaId, expectedKind, newToken, {
|
|
@@ -10558,7 +10871,7 @@ export class AgentManager {
|
|
|
10558
10871
|
? { onReadFile: (req) => { void this.handleReadFileRequest(taskId, qaId, req); } }
|
|
10559
10872
|
: {}),
|
|
10560
10873
|
});
|
|
10561
|
-
},
|
|
10874
|
+
}),
|
|
10562
10875
|
};
|
|
10563
10876
|
const rearmConsumedSignal = async () => {
|
|
10564
10877
|
if (opts.callerOwnsConsumedSignalFailure)
|
|
@@ -10572,20 +10885,24 @@ export class AgentManager {
|
|
|
10572
10885
|
};
|
|
10573
10886
|
let started = false;
|
|
10574
10887
|
try {
|
|
10888
|
+
// 批次轮早于 head 物化且其持久化 mode 不可信(改名丢失/尽力而为),续传恒重发 base。
|
|
10575
10889
|
started = opts.continuation
|
|
10576
|
-
? await this.continueSession(taskId, qaId, dispatchPhase, sessionOpts)
|
|
10890
|
+
? await this.continueSession(taskId, qaId, dispatchPhase, { ...sessionOpts, serverReviewCheckout: 'base' })
|
|
10577
10891
|
: await this.startSession(taskId, qaId, dispatchPhase, sessionOpts);
|
|
10578
10892
|
}
|
|
10579
10893
|
catch (err) {
|
|
10580
|
-
this.
|
|
10894
|
+
this.phaseSignalWatcher?.stopAgentIfToken(taskId, qaId, newToken);
|
|
10581
10895
|
if (err instanceof DispatchTerminalError) {
|
|
10582
|
-
await this.failTaskForDispatchError(taskId, dispatchPhase, qaId, err,
|
|
10896
|
+
await this.failTaskForDispatchError(taskId, dispatchPhase, qaId, err, {
|
|
10897
|
+
...ownAcquire(),
|
|
10898
|
+
expectedTask: installedPass,
|
|
10899
|
+
});
|
|
10583
10900
|
}
|
|
10584
10901
|
else if (err instanceof EnsureSessionError && err.partial.handled) {
|
|
10585
10902
|
}
|
|
10586
10903
|
else {
|
|
10587
10904
|
const rolledBack = await rollback(installedPass);
|
|
10588
|
-
if (!opts.continuation) {
|
|
10905
|
+
if (rolledBack && !opts.continuation) {
|
|
10589
10906
|
await releaseOwnAcquire('session dispatch threw');
|
|
10590
10907
|
}
|
|
10591
10908
|
if (rolledBack)
|
|
@@ -10594,14 +10911,14 @@ export class AgentManager {
|
|
|
10594
10911
|
throw err;
|
|
10595
10912
|
}
|
|
10596
10913
|
if (!started) {
|
|
10597
|
-
this.
|
|
10914
|
+
this.phaseSignalWatcher?.stopAgentIfToken(taskId, qaId, newToken);
|
|
10598
10915
|
const rolledBack = await rollback(installedPass);
|
|
10599
|
-
if (!opts.continuation) {
|
|
10916
|
+
if (rolledBack && !opts.continuation) {
|
|
10600
10917
|
await releaseOwnAcquire('session did not start');
|
|
10601
10918
|
}
|
|
10602
10919
|
if (rolledBack)
|
|
10603
10920
|
await rearmConsumedSignal();
|
|
10604
|
-
if (!opts.callerOwnsConsumedSignalFailure) {
|
|
10921
|
+
if (rolledBack && !opts.callerOwnsConsumedSignalFailure) {
|
|
10605
10922
|
await this.emitIntervention(projectId, qaId, taskId, {
|
|
10606
10923
|
phase: 'server-review-start-failed',
|
|
10607
10924
|
qaAgentId: qaId,
|
|
@@ -10612,7 +10929,11 @@ export class AgentManager {
|
|
|
10612
10929
|
// Persist the checkout mode so base-mode recovery can re-arm read-file.
|
|
10613
10930
|
if (opts.phase !== 'spec' && !opts.continuation) {
|
|
10614
10931
|
try {
|
|
10615
|
-
await this.
|
|
10932
|
+
const persisted = await this.updateTaskIfStatus(taskId, installedPass.status, { reviewCheckoutMode: dispatchedCheckoutMode }, installedPass);
|
|
10933
|
+
if (!persisted) {
|
|
10934
|
+
console.warn(`[AgentManager] review checkout mode for ${taskId} was superseded before persistence; skipping stale write`);
|
|
10935
|
+
return await this.taskStore.get(taskId);
|
|
10936
|
+
}
|
|
10616
10937
|
}
|
|
10617
10938
|
catch (err) {
|
|
10618
10939
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -10634,6 +10955,9 @@ export class AgentManager {
|
|
|
10634
10955
|
if (task.reviewMode !== 'server' && !isSpecStagePhase(task.phase)) {
|
|
10635
10956
|
throw new Error(`dispatchServerFixToDev: task ${taskId} is not in server review mode`);
|
|
10636
10957
|
}
|
|
10958
|
+
if (opts.expectedTask && !taskMatchesGeneration(task, opts.expectedTask)) {
|
|
10959
|
+
throw new ApiError(409, `Task ${taskId} review generation changed before server fix dispatch`, 'dispatch-superseded');
|
|
10960
|
+
}
|
|
10637
10961
|
if (!task.agentId)
|
|
10638
10962
|
throw new Error(`dispatchServerFixToDev: task ${taskId} has no dev agent`);
|
|
10639
10963
|
return {
|
|
@@ -10646,34 +10970,51 @@ export class AgentManager {
|
|
|
10646
10970
|
currentSpecRound: task.specReviewRound,
|
|
10647
10971
|
originalStatus: task.status,
|
|
10648
10972
|
originalToken: task.signalToken,
|
|
10973
|
+
entryGeneration: taskGenerationGuard(task),
|
|
10649
10974
|
};
|
|
10650
10975
|
});
|
|
10651
10976
|
if (!claim)
|
|
10652
10977
|
return null;
|
|
10653
|
-
const { devAgentId, qaAgentId, projectId, newToken, taskPhase, currentSpecRound } = claim;
|
|
10654
|
-
const rollbackToEntry = async () => {
|
|
10655
|
-
await this.transitionTaskStatus(taskId, claim.originalStatus, { fromStatus: ['fixing'] }, { signalToken: claim.originalToken }).catch(() => undefined);
|
|
10656
|
-
};
|
|
10978
|
+
const { devAgentId, qaAgentId, projectId, newToken, taskPhase, currentSpecRound, entryGeneration, } = claim;
|
|
10657
10979
|
const expectedKind = taskPhase === 'spec' ? 'spec-fixed' : 'code-fixed';
|
|
10658
|
-
const rearmReviewedSignal = async () => {
|
|
10980
|
+
const rearmReviewedSignal = async (expectedTask = entryGeneration) => {
|
|
10659
10981
|
if (opts.callerOwnsConsumedSignalFailure)
|
|
10660
10982
|
return;
|
|
10661
10983
|
if (!qaAgentId)
|
|
10662
10984
|
return;
|
|
10663
10985
|
const reviewedKind = taskPhase === 'spec' ? 'spec-reviewed' : 'code-reviewed';
|
|
10664
|
-
await this.
|
|
10986
|
+
await this.withTaskLock(async () => {
|
|
10987
|
+
const current = await this.taskStore.get(taskId);
|
|
10988
|
+
if (!current?.signalToken || !taskMatchesGeneration(current, expectedTask))
|
|
10989
|
+
return;
|
|
10990
|
+
await this.setupPhaseSignal(taskId, qaAgentId, reviewedKind, {
|
|
10991
|
+
skipSnapshot: true,
|
|
10992
|
+
});
|
|
10993
|
+
});
|
|
10665
10994
|
};
|
|
10666
10995
|
const devBoundBefore = (await this.agentStore.get(devAgentId))?.taskId === taskId;
|
|
10667
|
-
|
|
10996
|
+
let acquiredDevLockToken;
|
|
10997
|
+
const releaseFreshAcquire = async (devLockToken) => {
|
|
10668
10998
|
if (devBoundBefore)
|
|
10669
10999
|
return;
|
|
10670
|
-
|
|
10671
|
-
.
|
|
10672
|
-
|
|
10673
|
-
|
|
11000
|
+
try {
|
|
11001
|
+
const released = await this.releaseAgentForTask(devAgentId, taskId, 'idle', {
|
|
11002
|
+
expectedTask: entryGeneration,
|
|
11003
|
+
expectedLockToken: devLockToken,
|
|
11004
|
+
});
|
|
11005
|
+
if (!released) {
|
|
11006
|
+
console.error(`[AgentManager] dispatchServerFixToDev: releasing freshly acquired dev `
|
|
11007
|
+
+ `${devAgentId} for ${taskId} was refused`);
|
|
11008
|
+
}
|
|
11009
|
+
}
|
|
11010
|
+
catch (err) {
|
|
11011
|
+
console.error(`[AgentManager] dispatchServerFixToDev: releasing freshly acquired dev `
|
|
11012
|
+
+ `${devAgentId} for ${taskId} failed:`, err);
|
|
10674
11013
|
}
|
|
10675
11014
|
};
|
|
10676
|
-
const acquired = await this.acquireAgentForTask(devAgentId, taskId, 'server-feedback'
|
|
11015
|
+
const acquired = await this.acquireAgentForTask(devAgentId, taskId, 'server-feedback', {
|
|
11016
|
+
onAcquired: (lockToken) => { acquiredDevLockToken = lockToken; },
|
|
11017
|
+
});
|
|
10677
11018
|
if (!acquired) {
|
|
10678
11019
|
await rearmReviewedSignal();
|
|
10679
11020
|
if (!opts.callerOwnsConsumedSignalFailure) {
|
|
@@ -10684,11 +11025,18 @@ export class AgentManager {
|
|
|
10684
11025
|
}
|
|
10685
11026
|
return null;
|
|
10686
11027
|
}
|
|
11028
|
+
const devLockToken = acquiredDevLockToken;
|
|
11029
|
+
if (!devLockToken) {
|
|
11030
|
+
throw new Error(`dispatchServerFixToDev: acquireAgentForTask did not return the Dev lock token for ${devAgentId}`);
|
|
11031
|
+
}
|
|
10687
11032
|
if (qaAgentId) {
|
|
10688
|
-
const released = await this.releaseAgentIfBound(qaAgentId, taskId)
|
|
10689
|
-
.catch(() =>
|
|
11033
|
+
const released = await this.releaseAgentIfBound(qaAgentId, taskId, { expectedTask: entryGeneration })
|
|
11034
|
+
.catch((err) => {
|
|
11035
|
+
console.error(`[AgentManager] dispatchServerFixToDev: releasing QA ${qaAgentId} for ${taskId} failed:`, err);
|
|
11036
|
+
return false;
|
|
11037
|
+
});
|
|
10690
11038
|
if (!released) {
|
|
10691
|
-
await releaseFreshAcquire();
|
|
11039
|
+
await releaseFreshAcquire(devLockToken);
|
|
10692
11040
|
await rearmReviewedSignal();
|
|
10693
11041
|
if (!opts.callerOwnsConsumedSignalFailure) {
|
|
10694
11042
|
await this.emitIntervention(projectId, qaAgentId, taskId, {
|
|
@@ -10699,9 +11047,14 @@ export class AgentManager {
|
|
|
10699
11047
|
return null;
|
|
10700
11048
|
}
|
|
10701
11049
|
}
|
|
10702
|
-
const transition = await this.transitionTaskStatus(taskId, 'fixing', {
|
|
11050
|
+
const transition = await this.transitionTaskStatus(taskId, 'fixing', {
|
|
11051
|
+
fromStatus: ['review', 'max_rounds', 'spec-ready', 'ready'],
|
|
11052
|
+
expectSignalToken: claim.originalToken,
|
|
11053
|
+
expectTask: entryGeneration,
|
|
11054
|
+
expectAgentLock: { agentId: devAgentId, token: devLockToken },
|
|
11055
|
+
}, { signalToken: newToken, fixDispatchedAt: new Date().toISOString() });
|
|
10703
11056
|
if (!transition) {
|
|
10704
|
-
await releaseFreshAcquire();
|
|
11057
|
+
await releaseFreshAcquire(devLockToken);
|
|
10705
11058
|
if (!opts.callerOwnsConsumedSignalFailure) {
|
|
10706
11059
|
await this.emitIntervention(projectId, devAgentId, taskId, {
|
|
10707
11060
|
phase: 'server-fix-transition-failed',
|
|
@@ -10710,6 +11063,47 @@ export class AgentManager {
|
|
|
10710
11063
|
}
|
|
10711
11064
|
return null;
|
|
10712
11065
|
}
|
|
11066
|
+
const installedFix = taskGenerationGuard(transition.task);
|
|
11067
|
+
const fixingPassStillCurrent = async () => {
|
|
11068
|
+
const current = await this.taskStore.get(taskId);
|
|
11069
|
+
return current !== null && taskMatchesGeneration(current, installedFix);
|
|
11070
|
+
};
|
|
11071
|
+
const rollbackToEntry = async () => {
|
|
11072
|
+
try {
|
|
11073
|
+
const rolledBack = await this.transitionTaskStatus(taskId, claim.originalStatus, {
|
|
11074
|
+
fromStatus: ['fixing'],
|
|
11075
|
+
expectTask: installedFix,
|
|
11076
|
+
expectAgentLock: { agentId: devAgentId, token: devLockToken },
|
|
11077
|
+
}, { signalToken: claim.originalToken });
|
|
11078
|
+
if (!rolledBack) {
|
|
11079
|
+
console.warn(`[AgentManager] dispatchServerFixToDev rollback for ${taskId} was superseded `
|
|
11080
|
+
+ `by a newer task or Dev lock generation`);
|
|
11081
|
+
}
|
|
11082
|
+
return rolledBack?.task ?? null;
|
|
11083
|
+
}
|
|
11084
|
+
catch (err) {
|
|
11085
|
+
console.error(`[AgentManager] dispatchServerFixToDev rollback failed for ${taskId}:`, err);
|
|
11086
|
+
return null;
|
|
11087
|
+
}
|
|
11088
|
+
};
|
|
11089
|
+
const cleanupUndeliveredFix = async () => {
|
|
11090
|
+
const rolledBack = await rollbackToEntry();
|
|
11091
|
+
if (!rolledBack)
|
|
11092
|
+
return false;
|
|
11093
|
+
const rolledBackGeneration = taskGenerationGuard(rolledBack);
|
|
11094
|
+
const released = await this.releaseAgentForTask(devAgentId, taskId, 'idle', {
|
|
11095
|
+
expectedTask: rolledBackGeneration,
|
|
11096
|
+
expectedLockToken: devLockToken,
|
|
11097
|
+
}).catch((err) => {
|
|
11098
|
+
console.error(`[AgentManager] dispatchServerFixToDev: releasing dev ${devAgentId} after rollback failed:`, err);
|
|
11099
|
+
return false;
|
|
11100
|
+
});
|
|
11101
|
+
if (!released) {
|
|
11102
|
+
console.error(`[AgentManager] dispatchServerFixToDev: releasing dev ${devAgentId} after rollback was refused`);
|
|
11103
|
+
}
|
|
11104
|
+
await rearmReviewedSignal(rolledBackGeneration);
|
|
11105
|
+
return true;
|
|
11106
|
+
};
|
|
10713
11107
|
let resumed = false;
|
|
10714
11108
|
try {
|
|
10715
11109
|
const feedbackPhase = taskPhase === 'spec' ? 'spec' : 'code';
|
|
@@ -10727,29 +11121,39 @@ export class AgentManager {
|
|
|
10727
11121
|
signalToken: newToken,
|
|
10728
11122
|
serverPriorFindings: findingsJson,
|
|
10729
11123
|
serverFindingsDigest: findingsDigest,
|
|
11124
|
+
armBeforeInject: () => this.withTaskLock(async () => {
|
|
11125
|
+
const current = await this.taskStore.get(taskId);
|
|
11126
|
+
if (!current || !taskMatchesGeneration(current, installedFix))
|
|
11127
|
+
return false;
|
|
11128
|
+
return this.setupPhaseSignalWatcher(taskId, devAgentId, expectedKind, newToken, {
|
|
11129
|
+
needInputMode: 'fresh',
|
|
11130
|
+
});
|
|
11131
|
+
}),
|
|
11132
|
+
guardBeforeInject: fixingPassStillCurrent,
|
|
10730
11133
|
...(taskPhase === 'spec' && currentSpecRound !== undefined
|
|
10731
11134
|
? { currentSpecRound }
|
|
10732
11135
|
: {}),
|
|
10733
11136
|
});
|
|
10734
11137
|
}
|
|
10735
11138
|
catch (err) {
|
|
11139
|
+
this.phaseSignalWatcher?.stopAgentIfToken(taskId, devAgentId, newToken);
|
|
10736
11140
|
if (err instanceof DispatchTerminalError) {
|
|
10737
|
-
await this.failTaskForDispatchError(taskId, 'server-feedback', devAgentId, err
|
|
11141
|
+
await this.failTaskForDispatchError(taskId, 'server-feedback', devAgentId, err, {
|
|
11142
|
+
expectedTask: installedFix,
|
|
11143
|
+
expectedLockToken: devLockToken,
|
|
11144
|
+
});
|
|
10738
11145
|
}
|
|
10739
11146
|
else if (err instanceof EnsureSessionError && err.partial.handled) {
|
|
10740
11147
|
}
|
|
10741
11148
|
else {
|
|
10742
|
-
await
|
|
10743
|
-
await this.releaseAgentForTask(devAgentId, taskId, 'idle').catch(() => undefined);
|
|
10744
|
-
await rearmReviewedSignal();
|
|
11149
|
+
await cleanupUndeliveredFix();
|
|
10745
11150
|
}
|
|
10746
11151
|
throw err;
|
|
10747
11152
|
}
|
|
10748
11153
|
if (!resumed) {
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
|
|
10752
|
-
if (!opts.callerOwnsConsumedSignalFailure) {
|
|
11154
|
+
this.phaseSignalWatcher?.stopAgentIfToken(taskId, devAgentId, newToken);
|
|
11155
|
+
const cleaned = await cleanupUndeliveredFix();
|
|
11156
|
+
if (cleaned && !opts.callerOwnsConsumedSignalFailure) {
|
|
10753
11157
|
await this.emitIntervention(projectId, devAgentId, taskId, {
|
|
10754
11158
|
phase: 'server-fix-resume-failed',
|
|
10755
11159
|
devAgentId,
|
|
@@ -10757,7 +11161,6 @@ export class AgentManager {
|
|
|
10757
11161
|
}
|
|
10758
11162
|
return null;
|
|
10759
11163
|
}
|
|
10760
|
-
await this.armPostDispatchSignalOrHold(taskId, devAgentId, expectedKind, newToken);
|
|
10761
11164
|
return await this.taskStore.get(taskId);
|
|
10762
11165
|
}
|
|
10763
11166
|
projectRepoKey(projectId) {
|