baxian 2.0.31 → 2.0.33
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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/agent/branch.d.ts +2 -1
- package/dist/agent/branch.d.ts.map +1 -1
- package/dist/agent/branch.js +17 -4
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/manager.d.ts +19 -0
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +298 -41
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/tmux.d.ts +4 -1
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +27 -0
- package/dist/agent/tmux.js.map +1 -1
- package/dist/agent/untracked-files.d.ts +23 -0
- package/dist/agent/untracked-files.d.ts.map +1 -0
- package/dist/agent/untracked-files.js +218 -0
- package/dist/agent/untracked-files.js.map +1 -0
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +0 -11
- package/dist/api/projects.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +12 -0
- package/dist/api/tasks.js.map +1 -1
- package/dist/app.d.ts +0 -2
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +0 -2
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/shared/constants.d.ts +5 -6
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +6 -6
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/types.d.ts +6 -9
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/state/snapshot.d.ts +1 -5
- package/dist/state/snapshot.d.ts.map +1 -1
- package/dist/state/snapshot.js +5 -30
- package/dist/state/snapshot.js.map +1 -1
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +11 -2
- package/dist/state/task-store.js.map +1 -1
- package/dist/web/assets/index-CqHBL6U6.css +1 -0
- package/dist/web/assets/index-DTb6HI9L.js +19 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/agent/pet-input.d.ts +0 -18
- package/dist/agent/pet-input.d.ts.map +0 -1
- package/dist/agent/pet-input.js +0 -88
- package/dist/agent/pet-input.js.map +0 -1
- package/dist/api/pets.d.ts +0 -3
- package/dist/api/pets.d.ts.map +0 -1
- package/dist/api/pets.js +0 -77
- package/dist/api/pets.js.map +0 -1
- package/dist/state/pet-store.d.ts +0 -35
- package/dist/state/pet-store.d.ts.map +0 -1
- package/dist/state/pet-store.js +0 -189
- package/dist/state/pet-store.js.map +0 -1
- package/dist/web/assets/index-B77QkCk7.css +0 -1
- package/dist/web/assets/index-CbTk_gj8.js +0 -19
package/dist/agent/manager.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { randomBytes } from 'node:crypto';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
3
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
4
4
|
import { join, normalize } from 'node:path';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
5
|
+
import { hostname, tmpdir } from 'node:os';
|
|
6
6
|
import { createSignalToken, PASSIVE_VERDICT_WATCH } from './phase-signal.js';
|
|
7
7
|
import { tmuxInstallHint } from './preflight.js';
|
|
8
|
-
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, TERMINAL_INTERVENTION_PHASES, effectiveTaskReviewRound, isSpecStagePhase, isTaskOpen, taskAttentionGeneration, taskGenerationGuard, taskReviewRound, taskMatchesGeneration, } from '../shared/index.js';
|
|
8
|
+
import { BRANCH_PREFIX, isValidBranchName, retainedUntrackedFilesWithinLimit, 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, TERMINAL_INTERVENTION_PHASES, effectiveTaskReviewRound, isSpecStagePhase, isTaskOpen, taskAttentionGeneration, taskGenerationGuard, taskReviewRound, taskMatchesGeneration, } from '../shared/index.js';
|
|
9
9
|
import { buildProjectDriver, buildProjectPromptContext, makeDriverExec, repoIdentityKey, } from '../platform/driver-host.js';
|
|
10
10
|
import { DriverOpError, safeDriverErrorText, validateCommentBody, } from '../platform/types.js';
|
|
11
11
|
import { versionTimeOf } from '../platform/row-schema.js';
|
|
@@ -22,6 +22,7 @@ import { imageFilename, agentHostPath, writeImageToHost } from './image-input.js
|
|
|
22
22
|
import { TmuxManager, TmuxOutcomeUnknownError, ReplNotReadyError, isBlockedOnStartupDialog, hasRuntimeReadyView, hasReplProcTitle, isShellProcTitle, PaneGoneError, } from './tmux.js';
|
|
23
23
|
import { BranchManager, DirtyWorkdirError, ReviewHeadMismatchError, isAutoDeletableTaskBranch, } from './branch.js';
|
|
24
24
|
import { RepoStore, createRepoStoreCache } from './repo-store.js';
|
|
25
|
+
import { inspectUntrackedFiles } from './untracked-files.js';
|
|
25
26
|
import { PhaseSignalWatcher, } from './phase-signal-watcher.js';
|
|
26
27
|
import { buildPromptInline, buildGreetingPrompt, PromptSizeError, MAX_PROMPT_BYTES_ROUTE_LIMIT, } from './prompt.js';
|
|
27
28
|
import { ApiError } from '../errors.js';
|
|
@@ -182,9 +183,9 @@ const RUNTIME_LIVENESS_SAMPLES = 3;
|
|
|
182
183
|
export function canDispatchWithBinding(binding) {
|
|
183
184
|
return !binding?.taskId && !binding?.creationToken && binding?.status !== 'awaiting_human';
|
|
184
185
|
}
|
|
186
|
+
const CHECKOUT_PREPARATION_HOLD_PHASES = new Set(['checkout-preparation-failed', 'dirty-workdir']);
|
|
185
187
|
const RECOVERABLE_QA_DISPATCH_HOLD_PHASES = new Set([
|
|
186
|
-
|
|
187
|
-
'dirty-workdir',
|
|
188
|
+
...CHECKOUT_PREPARATION_HOLD_PHASES,
|
|
188
189
|
'branch-cleanup-pending',
|
|
189
190
|
]);
|
|
190
191
|
export function isRecoverableQaDispatchHold(binding) {
|
|
@@ -211,6 +212,30 @@ function cancelPhaseDowngrades(prev, next) {
|
|
|
211
212
|
return cancelPhaseRank(next) < cancelPhaseRank(prev);
|
|
212
213
|
}
|
|
213
214
|
const REGREET_REQUIRED_HOLD_PHASES = new Set(['greeting_failed']);
|
|
215
|
+
const TASK_REPLAY_FAILED_RUNBOOK = 'Resolve the cause, then retry the current step from the task page. '
|
|
216
|
+
+ 'If the task branch was never created, cancel the task and run it again. '
|
|
217
|
+
+ 'Resume does not resend the task prompt.';
|
|
218
|
+
const DELIVERED_BOOTSTRAP_RUNBOOK = 'The initial prompt was already delivered; replaying would dispatch the task twice. '
|
|
219
|
+
+ 'Verify the task outcome in the terminal, or cancel the task.';
|
|
220
|
+
const PROMPT_MAYBE_RUNNING_PHASES = new Set([
|
|
221
|
+
'dispatch-failed:ack_unknown',
|
|
222
|
+
'dev-wait-gate-failed-after-qa-started',
|
|
223
|
+
]);
|
|
224
|
+
function taskReplayFailure(error) {
|
|
225
|
+
const dirty = error instanceof DirtyWorkdirError;
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
227
|
+
if (error instanceof DispatchTerminalError && error.reason === 'ack_unknown') {
|
|
228
|
+
return {
|
|
229
|
+
phase: 'dispatch-failed:ack_unknown',
|
|
230
|
+
reason: `Task prompt delivery requires verification: ${message}. The prompt may already be running. Verify the task outcome in the terminal, or cancel the task.`,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const preparation = dirty ? 'Save the Workdir changes by committing or stashing them first. ' : '';
|
|
234
|
+
return {
|
|
235
|
+
phase: dirty ? 'dirty-workdir' : 'restart-redispatch-failed',
|
|
236
|
+
reason: `Task prompt replay failed: ${message}. ${preparation}${TASK_REPLAY_FAILED_RUNBOOK}`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
214
239
|
const RECOVERY_FAILED_AGENT_RUNBOOK = 'Fix the agent Workdir (or wait for the transient failure to clear), then Resume the agent.';
|
|
215
240
|
const RECOVERY_FAILED_TASK_RUNBOOK = 'Fix the agent Workdir (or wait for the transient failure to clear), '
|
|
216
241
|
+ 'then Resume the agent and use the recommended action on the task page.';
|
|
@@ -1856,14 +1881,14 @@ export class AgentManager {
|
|
|
1856
1881
|
}
|
|
1857
1882
|
}
|
|
1858
1883
|
if (cfg.role === 'dev' && boundTask) {
|
|
1859
|
-
const branches =
|
|
1884
|
+
const branches = this.taskBranchManager(runner, agentId, expectedTaskId);
|
|
1860
1885
|
const cleanup = await branches.cleanupTaskBranch(releaseWorkdir, this.cleanupIdentityFor(boundTask), () => this.assertTaskGeneration(agentId, expectedTaskId, lockToken, releaseWorkdir).then(() => undefined));
|
|
1861
1886
|
await this.persistBranchCleanupOutcome(boundTask, agentId, cleanup);
|
|
1862
1887
|
if (cleanup.status !== 'deleted')
|
|
1863
1888
|
await branches.parkOnDefaultDetached(releaseWorkdir);
|
|
1864
1889
|
}
|
|
1865
1890
|
else {
|
|
1866
|
-
await
|
|
1891
|
+
await this.taskBranchManager(runner, agentId, expectedTaskId).parkOnDefaultDetached(releaseWorkdir);
|
|
1867
1892
|
}
|
|
1868
1893
|
await this.assertTaskGeneration(agentId, expectedTaskId, lockToken, releaseWorkdir);
|
|
1869
1894
|
}
|
|
@@ -2066,7 +2091,7 @@ export class AgentManager {
|
|
|
2066
2091
|
if (!(await this.lockManager.isOwner(cfg.id, owner, token)))
|
|
2067
2092
|
continue;
|
|
2068
2093
|
for (const task of targets) {
|
|
2069
|
-
const cleanup = await
|
|
2094
|
+
const cleanup = await this.taskBranchManager(runner, cfg.id, task.id).cleanupTaskBranch(workdir, this.cleanupIdentityFor(task), () => this.assertTaskLockOwner(cfg.id, owner, token));
|
|
2070
2095
|
cleanupResults.push({ task, cleanup });
|
|
2071
2096
|
}
|
|
2072
2097
|
}
|
|
@@ -2711,16 +2736,25 @@ export class AgentManager {
|
|
|
2711
2736
|
return { resumed: false, releasedBinding: false, reason };
|
|
2712
2737
|
}
|
|
2713
2738
|
const boundTask = state.taskId ? await this.taskStore.get(state.taskId) : null;
|
|
2714
|
-
|
|
2715
|
-
'
|
|
2716
|
-
'
|
|
2717
|
-
|
|
2739
|
+
if (boundTask && ((state.awaitingPhase === 'restart-redispatch-failed'
|
|
2740
|
+
&& (boundTask.status === 'in_progress' || boundTask.status === 'fixing' || boundTask.status === 'approved'))
|
|
2741
|
+
|| (state.awaitingPhase === 'bootstrap-marker-clear-failed' && boundTask.status === 'in_progress'))) {
|
|
2742
|
+
const runbook = state.awaitingPhase === 'bootstrap-marker-clear-failed'
|
|
2743
|
+
? DELIVERED_BOOTSTRAP_RUNBOOK : TASK_REPLAY_FAILED_RUNBOOK;
|
|
2744
|
+
const reason = `Resume is blocked for active task ${boundTask.id} (${state.awaitingPhase}). ${runbook}`;
|
|
2745
|
+
console.warn(`[AgentManager] resumeAgent: agent ${agentId} — ${reason}`);
|
|
2746
|
+
return { resumed: false, releasedBinding: false, reason };
|
|
2747
|
+
}
|
|
2718
2748
|
if (state.awaitingPhase != null
|
|
2719
2749
|
&& PROMPT_MAYBE_RUNNING_PHASES.has(state.awaitingPhase)
|
|
2720
|
-
&& boundTask && ACTIVE_TASK_STATUSES.has(boundTask.status)
|
|
2750
|
+
&& boundTask && ACTIVE_TASK_STATUSES.has(boundTask.status)
|
|
2751
|
+
&& !(state.awaitingPhase === 'dispatch-failed:ack_unknown'
|
|
2752
|
+
&& boundTask.devAgentId === agentId && boundTask.qaAgentId !== agentId
|
|
2753
|
+
&& ['spec-ready', 'review', 'merge-ready', 'max_rounds'].includes(boundTask.status))) {
|
|
2721
2754
|
const uncertainRecovery = state.awaitingPhase === 'dispatch-failed:ack_unknown'
|
|
2755
|
+
&& boundTask.status === 'review' && boundTask.qaAgentId === agentId
|
|
2722
2756
|
? ' Confirm the uncertain review dispatch as not delivered, cancel the task,'
|
|
2723
|
-
: `
|
|
2757
|
+
: ` Verify the task outcome in the terminal, cancel task ${state.taskId},`;
|
|
2724
2758
|
const reason = `Prompt may still be running (${state.awaitingPhase}); Resume is blocked until the task outcome arrives.${uncertainRecovery} or DELETE the agent to recover.`;
|
|
2725
2759
|
console.warn(`[AgentManager] resumeAgent: agent ${agentId} — ${reason}`);
|
|
2726
2760
|
return { resumed: false, releasedBinding: false, reason };
|
|
@@ -3430,7 +3464,7 @@ export class AgentManager {
|
|
|
3430
3464
|
await tmux.clearComposerDraft(pane, runtime);
|
|
3431
3465
|
// 退出文本与 Enter 分两次守卫写:前台在两者之间换了进程,回车就不发,/exit 留在草稿里等下一次清稿,而不会打进 shell 或别的进程
|
|
3432
3466
|
try {
|
|
3433
|
-
if (!await this.reachedRuntime(() => tmux.submitToRuntime(pane, runtime, REPL_EXIT_COMMAND[cfg.runtime])))
|
|
3467
|
+
if (!await this.reachedRuntime(() => tmux.submitToRuntime(pane, runtime, REPL_EXIT_COMMAND[cfg.runtime], { expectRuntimeExit: true })))
|
|
3434
3468
|
return;
|
|
3435
3469
|
}
|
|
3436
3470
|
catch (err) {
|
|
@@ -3643,7 +3677,8 @@ export class AgentManager {
|
|
|
3643
3677
|
retireOldOnce();
|
|
3644
3678
|
if (!(await args.stillCurrent(newToken)))
|
|
3645
3679
|
return false;
|
|
3646
|
-
const
|
|
3680
|
+
const failure = taskReplayFailure(err);
|
|
3681
|
+
const held = await args.holdFailure(newToken, failure.phase, failure.reason);
|
|
3647
3682
|
if (held)
|
|
3648
3683
|
return true;
|
|
3649
3684
|
if (!(await args.stillCurrent(newToken)))
|
|
@@ -3703,7 +3738,13 @@ export class AgentManager {
|
|
|
3703
3738
|
};
|
|
3704
3739
|
const resumed = await args.continueWith(newToken, fence);
|
|
3705
3740
|
if (resumed) {
|
|
3706
|
-
|
|
3741
|
+
try {
|
|
3742
|
+
await args.afterDelivered?.(newToken);
|
|
3743
|
+
}
|
|
3744
|
+
catch (err) {
|
|
3745
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3746
|
+
throw new DispatchTerminalError('ack_unknown', `The prompt for task ${taskId} was already delivered, but saving its delivery status failed: ${message}`);
|
|
3747
|
+
}
|
|
3707
3748
|
return true;
|
|
3708
3749
|
}
|
|
3709
3750
|
releaseClaimOnce();
|
|
@@ -3758,14 +3799,17 @@ export class AgentManager {
|
|
|
3758
3799
|
});
|
|
3759
3800
|
const holdReplayFailure = (phase, reason) => this.holdReplayFailureIfCurrent(agentId, task, phase, reason, entryHold);
|
|
3760
3801
|
try {
|
|
3802
|
+
const mapped = this.mapTaskStateToExpectedWatcher(task);
|
|
3803
|
+
if (entryHold.phase && PROMPT_MAYBE_RUNNING_PHASES.has(entryHold.phase)
|
|
3804
|
+
&& ((task.status === 'approved' && agent.role === 'dev') || mapped?.agentId === agentId))
|
|
3805
|
+
return true;
|
|
3761
3806
|
if (task.status === 'approved' && agent.role === 'dev') {
|
|
3762
3807
|
return await this.replayApprovedHolderAfterReplRestart(agentId, task, entryHold, { clearEntryHold, holdReplayFailure });
|
|
3763
3808
|
}
|
|
3764
|
-
const mapped = this.mapTaskStateToExpectedWatcher(task);
|
|
3765
3809
|
if (!mapped || mapped.agentId !== agentId)
|
|
3766
3810
|
return false;
|
|
3767
3811
|
if (!task.signalToken) {
|
|
3768
|
-
return await holdReplayFailure('restart-redispatch-failed', '
|
|
3812
|
+
return await holdReplayFailure('restart-redispatch-failed', 'The task has no signal token to replay its prompt with; cancel the task and run it again.');
|
|
3769
3813
|
}
|
|
3770
3814
|
const signalToken = task.signalToken;
|
|
3771
3815
|
const replayTask = (phase, opts = {}, afterDelivered) => this.runRotatedReplay({
|
|
@@ -3788,7 +3832,7 @@ export class AgentManager {
|
|
|
3788
3832
|
return true;
|
|
3789
3833
|
},
|
|
3790
3834
|
stillCurrent: taskStillCurrent,
|
|
3791
|
-
holdFailure: (newToken, reason) => this.holdReplayFailureIfCurrent(agentId, { ...task, signalToken: newToken },
|
|
3835
|
+
holdFailure: (newToken, phase, reason) => this.holdReplayFailureIfCurrent(agentId, { ...task, signalToken: newToken }, phase, reason, entryHold),
|
|
3792
3836
|
continueWith: (newToken, fence) => this.continueSession(taskId, agentId, phase, {
|
|
3793
3837
|
...opts,
|
|
3794
3838
|
signalToken: newToken,
|
|
@@ -3799,7 +3843,7 @@ export class AgentManager {
|
|
|
3799
3843
|
});
|
|
3800
3844
|
const replayInitialBootstrap = async (phase, opts) => {
|
|
3801
3845
|
if (entryHold.phase === 'bootstrap-marker-clear-failed') {
|
|
3802
|
-
return holdReplayFailure('
|
|
3846
|
+
return holdReplayFailure('bootstrap-marker-clear-failed', DELIVERED_BOOTSTRAP_RUNBOOK);
|
|
3803
3847
|
}
|
|
3804
3848
|
const bindingBefore = await this.agentStore.get(agentId);
|
|
3805
3849
|
if (!(await clearEntryHold()))
|
|
@@ -3845,8 +3889,8 @@ export class AgentManager {
|
|
|
3845
3889
|
return false;
|
|
3846
3890
|
}
|
|
3847
3891
|
catch (err) {
|
|
3848
|
-
const
|
|
3849
|
-
if (await holdReplayFailure(
|
|
3892
|
+
const failure = taskReplayFailure(err);
|
|
3893
|
+
if (await holdReplayFailure(failure.phase, failure.reason)) {
|
|
3850
3894
|
return true;
|
|
3851
3895
|
}
|
|
3852
3896
|
throw err;
|
|
@@ -3915,7 +3959,7 @@ export class AgentManager {
|
|
|
3915
3959
|
return true;
|
|
3916
3960
|
},
|
|
3917
3961
|
stillCurrent: completionStillCurrent,
|
|
3918
|
-
holdFailure: (newToken, reason) => this.holdReplayFailureIfCurrent(agentId, task,
|
|
3962
|
+
holdFailure: (newToken, phase, reason) => this.holdReplayFailureIfCurrent(agentId, task, phase, reason, entryHold, async (fresh) => episodeMatches(fresh) && completionTokenMatches(fresh, newToken)),
|
|
3919
3963
|
continueWith: (newToken, fence) => this.continueSession(taskId, agentId, 'post-approve', {
|
|
3920
3964
|
signalToken: newToken,
|
|
3921
3965
|
postApproveEpisode: {
|
|
@@ -4212,6 +4256,10 @@ export class AgentManager {
|
|
|
4212
4256
|
else {
|
|
4213
4257
|
recommendedActions = ['advance', 'cancel'];
|
|
4214
4258
|
}
|
|
4259
|
+
if (reason === 'bootstrap-marker-clear-failed'
|
|
4260
|
+
|| (reason === 'dispatch-failed:ack_unknown' && task.status !== 'review')) {
|
|
4261
|
+
recommendedActions = recommendedActions.filter(action => action !== 'advance');
|
|
4262
|
+
}
|
|
4215
4263
|
if (task.attention
|
|
4216
4264
|
&& Date.parse(task.attention.occurredAt) > Date.parse(occurredAt))
|
|
4217
4265
|
return;
|
|
@@ -6013,6 +6061,204 @@ export class AgentManager {
|
|
|
6013
6061
|
return guarded.ran ? guarded.value : null;
|
|
6014
6062
|
});
|
|
6015
6063
|
}
|
|
6064
|
+
untrackedFilesHost(agent) {
|
|
6065
|
+
return hostGroupKey(agent.mode, resolveAgentHost(this.config.host, agent.host));
|
|
6066
|
+
}
|
|
6067
|
+
taskBranchManager(runner, agentId, taskId) {
|
|
6068
|
+
return new BranchManager(runner, async (workdir) => {
|
|
6069
|
+
const agent = this.getAgentConfig(agentId);
|
|
6070
|
+
const task = await this.taskStore.get(taskId);
|
|
6071
|
+
return task?.retainedUntrackedFiles?.find(entry => entry.agentId === agentId
|
|
6072
|
+
&& entry.workdir === workdir && agent && entry.host === this.untrackedFilesHost(agent))?.pathsBase64 ?? [];
|
|
6073
|
+
});
|
|
6074
|
+
}
|
|
6075
|
+
supportsUntrackedFiles(binding, agent) {
|
|
6076
|
+
return binding?.status === 'awaiting_human' && !!agent
|
|
6077
|
+
&& (RECOVERABLE_QA_DISPATCH_HOLD_PHASES.has(binding.awaitingPhase ?? '')
|
|
6078
|
+
|| (agent.role === 'dev' && binding.awaitingPhase === 'restart-redispatch-failed'));
|
|
6079
|
+
}
|
|
6080
|
+
async untrackedFilesContext(taskId, agentId) {
|
|
6081
|
+
const task = await this.taskStore.get(taskId);
|
|
6082
|
+
const binding = await this.agentStore.get(agentId);
|
|
6083
|
+
const agent = this.getAgentConfig(agentId);
|
|
6084
|
+
if (!task || !agent || binding?.taskId !== taskId || !binding.workdir || agent.projectId !== task.projectId) {
|
|
6085
|
+
throw new ApiError(409, 'Task or agent binding changed; refresh the task');
|
|
6086
|
+
}
|
|
6087
|
+
if (this.isDeletionInFlight(agentId) || this.cancelCleanupInFlight.has(taskId)) {
|
|
6088
|
+
throw new ApiError(409, 'Task cancellation or agent removal is in progress; wait for it to finish');
|
|
6089
|
+
}
|
|
6090
|
+
if (this.compactInFlight.has(agentId) || this.maintenanceInFlight.has(agentId)) {
|
|
6091
|
+
throw new ApiError(409, 'Agent maintenance is in progress; refresh the file list when it finishes');
|
|
6092
|
+
}
|
|
6093
|
+
if (this.resolveWorkdir(agent, binding) !== binding.workdir)
|
|
6094
|
+
throw new ApiError(409, 'Agent working directory changed; check its configuration');
|
|
6095
|
+
if (task.reviewDispatch?.phase === 'uncertain' || task.reviewDispatch?.phase === 'claimed') {
|
|
6096
|
+
throw new ApiError(409, 'Review delivery is pending or uncertain; verify its outcome before handling files');
|
|
6097
|
+
}
|
|
6098
|
+
if (!this.supportsUntrackedFiles(binding, agent))
|
|
6099
|
+
throw new ApiError(409, 'This agent hold cannot be resumed by handling files');
|
|
6100
|
+
const claim = await this.lockManager.claimOf(agentId);
|
|
6101
|
+
if (claim?.taskId !== taskId || (binding.lockToken && binding.lockToken !== claim.token)) {
|
|
6102
|
+
throw new ApiError(409, 'Agent ownership changed; refresh the task');
|
|
6103
|
+
}
|
|
6104
|
+
const host = this.untrackedFilesHost(agent);
|
|
6105
|
+
const retained = task.retainedUntrackedFiles?.find(entry => entry.agentId === agentId
|
|
6106
|
+
&& entry.workdir === binding.workdir && entry.host === host)?.pathsBase64 ?? [];
|
|
6107
|
+
let recovery;
|
|
6108
|
+
if (agent.role === 'qa' && task.status === 'review') {
|
|
6109
|
+
if (task.qaAgentId !== agentId || !task.phase || task.deliveryConfirmation?.phase !== task.phase || !task.prNumber) {
|
|
6110
|
+
throw new ApiError(409, 'Review delivery must be verified in the task before handling files');
|
|
6111
|
+
}
|
|
6112
|
+
recovery = 'qa';
|
|
6113
|
+
}
|
|
6114
|
+
else if (binding.awaitingPhase === 'branch-cleanup-pending' || agent.role === 'qa'
|
|
6115
|
+
|| task.status === 'review' || TERMINAL_STATUSES.includes(task.status)) {
|
|
6116
|
+
recovery = 'release';
|
|
6117
|
+
}
|
|
6118
|
+
else if (['in_progress', 'fixing', 'approved'].includes(task.status)) {
|
|
6119
|
+
if (task.status === 'approved' && task.postApproveRevoked)
|
|
6120
|
+
throw new ApiError(409, 'Post-approval work was revoked; confirm recovery in the task before handling files');
|
|
6121
|
+
recovery = 'dev';
|
|
6122
|
+
}
|
|
6123
|
+
else {
|
|
6124
|
+
throw new ApiError(409, `Task cannot continue from ${task.status}; use the task actions before handling files`);
|
|
6125
|
+
}
|
|
6126
|
+
const devFallback = task.branchLocalCleaned?.remoteTipSha
|
|
6127
|
+
?? (binding.bootstrappingTaskId === taskId && CHECKOUT_PREPARATION_HOLD_PHASES.has(binding.awaitingPhase ?? '') ? 'origin/HEAD' : undefined);
|
|
6128
|
+
const targetRefs = recovery === 'release' ? ['origin/HEAD'] : recovery === 'qa'
|
|
6129
|
+
? ['origin/HEAD', task.reviewHeadAnchorSha ?? task.latestHeadSha ?? `refs/remotes/origin/${task.branch}`]
|
|
6130
|
+
: [[`refs/heads/${task.branch}`, ...(devFallback ? [devFallback] : [])]];
|
|
6131
|
+
return { task, binding: { ...binding, lockToken: claim.token }, agent, host, retained, recovery, targetRefs,
|
|
6132
|
+
workdir: binding.workdir, runner: this.createRunnerFor(agent) };
|
|
6133
|
+
}
|
|
6134
|
+
untrackedFilesToken(context, fingerprint) {
|
|
6135
|
+
return createHash('sha256').update(JSON.stringify({
|
|
6136
|
+
taskId: context.task.id, generation: taskAttentionGeneration(context.task),
|
|
6137
|
+
head: context.task.reviewHeadAnchorSha, latestHead: context.task.latestHeadSha,
|
|
6138
|
+
branch: context.task.branch, agentId: context.agent.id, host: context.host,
|
|
6139
|
+
workdir: context.workdir, lockToken: context.binding.lockToken,
|
|
6140
|
+
hold: context.binding.awaitingPhase, nonce: context.binding.awaitingNonce,
|
|
6141
|
+
pane: context.binding.paneId, creation: context.binding.creationToken,
|
|
6142
|
+
bootstrap: context.binding.bootstrappingTaskId, reviewDispatch: context.task.reviewDispatch,
|
|
6143
|
+
branchLocalCleaned: context.task.branchLocalCleaned, recovery: context.recovery,
|
|
6144
|
+
runtime: context.agent.runtime, fingerprint, retained: context.retained,
|
|
6145
|
+
})).digest('hex');
|
|
6146
|
+
}
|
|
6147
|
+
retainedUntrackedSelection(context, pathsBase64) {
|
|
6148
|
+
return [
|
|
6149
|
+
...(context.task.retainedUntrackedFiles ?? []).filter(entry => entry.agentId !== context.agent.id
|
|
6150
|
+
|| entry.workdir !== context.workdir || entry.host !== context.host),
|
|
6151
|
+
{ agentId: context.agent.id, host: context.host, workdir: context.workdir, pathsBase64: [...new Set([...context.retained, ...pathsBase64])] },
|
|
6152
|
+
];
|
|
6153
|
+
}
|
|
6154
|
+
async getUntrackedFiles(taskId, agentId) {
|
|
6155
|
+
if (!this.supportsUntrackedFiles(await this.agentStore.get(agentId), this.getAgentConfig(agentId)))
|
|
6156
|
+
return null;
|
|
6157
|
+
const context = await this.untrackedFilesContext(taskId, agentId);
|
|
6158
|
+
const snapshot = await inspectUntrackedFiles(context.runner, context.workdir, undefined, context.targetRefs);
|
|
6159
|
+
return this.withTaskLock(async () => {
|
|
6160
|
+
const fresh = await this.untrackedFilesContext(taskId, agentId);
|
|
6161
|
+
if (this.untrackedFilesToken(context, snapshot.fingerprint) !== this.untrackedFilesToken(fresh, snapshot.fingerprint)) {
|
|
6162
|
+
throw new ApiError(409, 'Task changed during file inspection; refresh the file list');
|
|
6163
|
+
}
|
|
6164
|
+
const files = snapshot.files.filter(file => !fresh.retained.includes(file.pathBase64));
|
|
6165
|
+
const pending = new Set(files.map(file => file.pathBase64));
|
|
6166
|
+
return {
|
|
6167
|
+
agentId, host: fresh.agent.mode === 'remote' ? fresh.agent.host : hostname(),
|
|
6168
|
+
workdir: fresh.workdir, token: this.untrackedFilesToken(fresh, snapshot.fingerprint), files,
|
|
6169
|
+
trackedChanges: snapshot.trackedChanges, conflicts: snapshot.conflicts,
|
|
6170
|
+
manualCleanupRequired: snapshot.conflicts.some(file => !pending.has(file.pathBase64)),
|
|
6171
|
+
keepLimitExceeded: !retainedUntrackedFilesWithinLimit(this.retainedUntrackedSelection(fresh, [...pending])),
|
|
6172
|
+
};
|
|
6173
|
+
});
|
|
6174
|
+
}
|
|
6175
|
+
async resolveUntrackedFiles(taskId, agentId, action, token) {
|
|
6176
|
+
let filesHandled = false;
|
|
6177
|
+
try {
|
|
6178
|
+
const current = await this.untrackedFilesContext(taskId, agentId);
|
|
6179
|
+
const tmux = new TmuxManager(current.runner);
|
|
6180
|
+
const runtime = await this.inspectReleaseRuntime(tmux, agentId, agentRuntimeKindFor(current.agent));
|
|
6181
|
+
if (runtime.kind === 'hold')
|
|
6182
|
+
throw new ApiError(409, runtime.reason);
|
|
6183
|
+
if (runtime.kind === 'pane') {
|
|
6184
|
+
await this.waitForReplPromptReady(tmux, runtime.pane, current.agent.runtime, this.cleanComposerWaitMs, { stableIdle: true });
|
|
6185
|
+
}
|
|
6186
|
+
const snapshot = await inspectUntrackedFiles(current.runner, current.workdir, undefined, current.targetRefs);
|
|
6187
|
+
const prepared = await this.withTaskLock(async () => {
|
|
6188
|
+
let context = await this.untrackedFilesContext(taskId, agentId);
|
|
6189
|
+
if (this.untrackedFilesToken(context, snapshot.fingerprint) !== token) {
|
|
6190
|
+
throw new ApiError(409, 'Files or task changed; refresh the list and choose again');
|
|
6191
|
+
}
|
|
6192
|
+
if (this.agentOperationQueues.has(agentId))
|
|
6193
|
+
throw new ApiError(409, 'Agent operation is in progress; refresh the list when it finishes');
|
|
6194
|
+
const pathsBase64 = snapshot.files.filter(file => !context.retained.includes(file.pathBase64)).map(file => file.pathBase64);
|
|
6195
|
+
if (snapshot.conflicts.some(file => action === 'keep' || !pathsBase64.includes(file.pathBase64))) {
|
|
6196
|
+
throw new ApiError(409, 'Checkout would overwrite local files; move, rename or remove the conflicting files in the terminal');
|
|
6197
|
+
}
|
|
6198
|
+
if (action === 'continue' && pathsBase64.length > 0)
|
|
6199
|
+
throw new ApiError(409, 'Untracked files need a decision; refresh the list and choose Keep or Discard');
|
|
6200
|
+
if (action !== 'continue' && pathsBase64.length === 0)
|
|
6201
|
+
throw new ApiError(409, 'No untracked files remain to handle; refresh the list and continue the task');
|
|
6202
|
+
if (snapshot.trackedChanges)
|
|
6203
|
+
throw new ApiError(409, 'Tracked changes must be saved before continuing');
|
|
6204
|
+
if (action === 'keep') {
|
|
6205
|
+
const retainedUntrackedFiles = this.retainedUntrackedSelection(context, pathsBase64);
|
|
6206
|
+
if (!retainedUntrackedFilesWithinLimit(retainedUntrackedFiles))
|
|
6207
|
+
throw new ApiError(409, 'Retained file limit exceeded (1000 paths or 64 KiB per task); reduce the list in the terminal or discard the listed files');
|
|
6208
|
+
await this.taskStore.set({ ...context.task, retainedUntrackedFiles, updatedAt: new Date().toISOString() });
|
|
6209
|
+
filesHandled = true;
|
|
6210
|
+
context = await this.untrackedFilesContext(taskId, agentId);
|
|
6211
|
+
}
|
|
6212
|
+
// Register the agent lease before releasing the task lock; never reacquire the task lock inside it.
|
|
6213
|
+
const processing = this.outsideTaskMutationScope(() => this.withAgentOperationLease(agentId, async () => {
|
|
6214
|
+
if (action !== 'discard')
|
|
6215
|
+
return;
|
|
6216
|
+
await inspectUntrackedFiles(context.runner, context.workdir, { fingerprint: snapshot.fingerprint, pathsBase64 }, context.targetRefs);
|
|
6217
|
+
filesHandled = true;
|
|
6218
|
+
}));
|
|
6219
|
+
return { context, pathsBase64, processing };
|
|
6220
|
+
});
|
|
6221
|
+
await prepared.processing;
|
|
6222
|
+
return await this.withTaskLock(async () => {
|
|
6223
|
+
const context = await this.withAgentOperationLease(agentId, async () => {
|
|
6224
|
+
const refreshed = await this.untrackedFilesContext(taskId, agentId);
|
|
6225
|
+
if (this.untrackedFilesToken(refreshed, snapshot.fingerprint) !== this.untrackedFilesToken(prepared.context, snapshot.fingerprint)) {
|
|
6226
|
+
throw new ApiError(409, 'Task changed during file handling; refresh the task before continuing');
|
|
6227
|
+
}
|
|
6228
|
+
await this.auditHumanTaskOperation(refreshed.task, 'advance', `untracked-files:${action}`, undefined, { agentId, pathsBase64: prepared.pathsBase64 });
|
|
6229
|
+
if (refreshed.recovery === 'dev' && refreshed.binding.bootstrappingTaskId === taskId
|
|
6230
|
+
&& refreshed.task.branch && CHECKOUT_PREPARATION_HOLD_PHASES.has(refreshed.binding.awaitingPhase ?? '')) {
|
|
6231
|
+
await this.taskBranchManager(refreshed.runner, agentId, taskId).switchToTaskBranch(refreshed.workdir, taskId, refreshed.task.branch, refreshed.task.branchCreatedByBaxian === true, refreshed.task.branchLocalCleaned ? { restorableRemoteTip: refreshed.task.branchLocalCleaned.remoteTipSha } : {});
|
|
6232
|
+
if (refreshed.task.branchLocalCleaned)
|
|
6233
|
+
await this.clearBranchLocalCleaned(taskId);
|
|
6234
|
+
}
|
|
6235
|
+
return refreshed;
|
|
6236
|
+
});
|
|
6237
|
+
if (context.recovery === 'qa') {
|
|
6238
|
+
return this.dispatchReviewToQa(taskId, { fromStatus: ['review'], expectedTask: taskGenerationGuard(context.task) });
|
|
6239
|
+
}
|
|
6240
|
+
if (context.recovery === 'release') {
|
|
6241
|
+
const released = await this.releaseAgentForTask(agentId, taskId, 'idle', {
|
|
6242
|
+
allowAwaitingHuman: true, expectedTask: taskGenerationGuard(context.task),
|
|
6243
|
+
expectedLockToken: context.binding.lockToken,
|
|
6244
|
+
expectedHold: { phase: context.binding.awaitingPhase, since: context.binding.awaitingSince, nonce: context.binding.awaitingNonce },
|
|
6245
|
+
});
|
|
6246
|
+
if (!released)
|
|
6247
|
+
throw new ApiError(409, 'Workdir release is still blocked; inspect the task');
|
|
6248
|
+
await this.emitIntervention(context.task.projectId, agentId, taskId, { phase: 'resumed', previousPhase: context.binding.awaitingPhase });
|
|
6249
|
+
}
|
|
6250
|
+
else {
|
|
6251
|
+
await this.advanceTask(taskId, { executor: 'dev', agentId });
|
|
6252
|
+
}
|
|
6253
|
+
return (await this.taskStore.get(taskId));
|
|
6254
|
+
});
|
|
6255
|
+
}
|
|
6256
|
+
catch (error) {
|
|
6257
|
+
if (!filesHandled)
|
|
6258
|
+
throw error;
|
|
6259
|
+
throw new ApiError(error instanceof ApiError ? error.status : 500, `${action === 'keep' ? 'Files were kept' : 'Listed files were discarded'}, but continuing the task failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
6260
|
+
}
|
|
6261
|
+
}
|
|
6016
6262
|
createRunnerFor(agent) {
|
|
6017
6263
|
if (this.runnerFactory) {
|
|
6018
6264
|
return this.runnerFactory(agent);
|
|
@@ -7078,7 +7324,7 @@ export class AgentManager {
|
|
|
7078
7324
|
const { paneId, pane, workdir } = ensure;
|
|
7079
7325
|
const runner = this.createRunnerFor(agent);
|
|
7080
7326
|
const tmux = new TmuxManager(runner);
|
|
7081
|
-
const branchManager =
|
|
7327
|
+
const branchManager = this.taskBranchManager(runner, agentId, taskId);
|
|
7082
7328
|
const lockState = await this.agentStore.get(agentId);
|
|
7083
7329
|
if (!lockState || lockState.taskId !== taskId || lockState.lockToken !== lockToken)
|
|
7084
7330
|
return false;
|
|
@@ -7294,8 +7540,7 @@ export class AgentManager {
|
|
|
7294
7540
|
}
|
|
7295
7541
|
catch (clearErr) {
|
|
7296
7542
|
console.warn(`[AgentManager] startSession: clearing bootstrap marker for task=${taskId} failed:`, clearErr);
|
|
7297
|
-
await this.markAwaitingHuman(agentId, 'bootstrap-marker-clear-failed',
|
|
7298
|
-
'not re-dispatch an already-running task. Verify the agent via web terminal, then Resume.', { expectedTaskId: taskId }).catch((holdErr) => {
|
|
7543
|
+
await this.markAwaitingHuman(agentId, 'bootstrap-marker-clear-failed', `Clearing the in-flight bootstrap marker failed. ${DELIVERED_BOOTSTRAP_RUNBOOK}`, { expectedTaskId: taskId }).catch((holdErr) => {
|
|
7299
7544
|
console.warn(`[AgentManager] startSession: hold after marker-clear failure for task=${taskId} failed:`, holdErr);
|
|
7300
7545
|
});
|
|
7301
7546
|
}
|
|
@@ -7687,7 +7932,7 @@ export class AgentManager {
|
|
|
7687
7932
|
return false;
|
|
7688
7933
|
}
|
|
7689
7934
|
if (agent.role === 'dev' && task.branch) {
|
|
7690
|
-
const branches =
|
|
7935
|
+
const branches = this.taskBranchManager(runner, agentId, taskId);
|
|
7691
7936
|
if (!opts.allowDirtyWorkdir)
|
|
7692
7937
|
await branches.assertClean(verifiedWorkdir);
|
|
7693
7938
|
const actualRef = await branches.currentRef(verifiedWorkdir);
|
|
@@ -7862,7 +8107,8 @@ export class AgentManager {
|
|
|
7862
8107
|
async rollbackUndeliveredBootstrap(state) {
|
|
7863
8108
|
if (!state.taskId
|
|
7864
8109
|
|| state.bootstrappingTaskId !== state.taskId
|
|
7865
|
-
|| state.awaitingPhase === 'bootstrap-marker-clear-failed'
|
|
8110
|
+
|| state.awaitingPhase === 'bootstrap-marker-clear-failed'
|
|
8111
|
+
|| (state.awaitingPhase !== undefined && PROMPT_MAYBE_RUNNING_PHASES.has(state.awaitingPhase))) {
|
|
7866
8112
|
return false;
|
|
7867
8113
|
}
|
|
7868
8114
|
const boundTask = await this.taskStore.get(state.taskId);
|
|
@@ -7973,8 +8219,7 @@ export class AgentManager {
|
|
|
7973
8219
|
}
|
|
7974
8220
|
catch (err) {
|
|
7975
8221
|
console.warn(`[AgentManager] replay: clearing bootstrap marker for task=${taskId} failed:`, err);
|
|
7976
|
-
await this.markAwaitingHuman(agentId, 'bootstrap-marker-clear-failed',
|
|
7977
|
-
'not roll back an already-running task. Verify the agent via web terminal, then Resume.', { expectedTaskId: taskId }).catch((holdErr) => {
|
|
8222
|
+
await this.markAwaitingHuman(agentId, 'bootstrap-marker-clear-failed', `Clearing the in-flight bootstrap marker failed. ${DELIVERED_BOOTSTRAP_RUNBOOK}`, { expectedTaskId: taskId }).catch((holdErr) => {
|
|
7978
8223
|
console.warn(`[AgentManager] replay: hold after marker-clear failure for task=${taskId} failed:`, holdErr);
|
|
7979
8224
|
});
|
|
7980
8225
|
return;
|
|
@@ -8141,9 +8386,11 @@ export class AgentManager {
|
|
|
8141
8386
|
return {
|
|
8142
8387
|
...withBinding,
|
|
8143
8388
|
status: 'awaiting_human',
|
|
8389
|
+
...(latest.bootstrappingTaskId !== undefined ? { bootstrappingTaskId: latest.bootstrappingTaskId } : {}),
|
|
8144
8390
|
...(latest.awaitingPhase !== undefined ? { awaitingPhase: latest.awaitingPhase } : {}),
|
|
8145
8391
|
...(latest.awaitingReason !== undefined ? { awaitingReason: latest.awaitingReason } : {}),
|
|
8146
8392
|
...(latest.awaitingSince !== undefined ? { awaitingSince: latest.awaitingSince } : {}),
|
|
8393
|
+
...(latest.awaitingNonce !== undefined ? { awaitingNonce: latest.awaitingNonce } : {}),
|
|
8147
8394
|
};
|
|
8148
8395
|
});
|
|
8149
8396
|
if (state.taskId && !shouldReleaseBinding && !cancelHold) {
|
|
@@ -8786,6 +9033,24 @@ export class AgentManager {
|
|
|
8786
9033
|
return dispatched;
|
|
8787
9034
|
}
|
|
8788
9035
|
}
|
|
9036
|
+
async replayDevTaskForAdvance(task) {
|
|
9037
|
+
const taskId = task.id;
|
|
9038
|
+
if (!task.devAgentId)
|
|
9039
|
+
throw new ApiError(409, `Task ${taskId} has no Dev participant`);
|
|
9040
|
+
const entryBinding = await this.agentStore.get(task.devAgentId);
|
|
9041
|
+
if (entryBinding?.taskId === taskId && entryBinding.status === 'awaiting_human'
|
|
9042
|
+
&& entryBinding.awaitingPhase && PROMPT_MAYBE_RUNNING_PHASES.has(entryBinding.awaitingPhase)) {
|
|
9043
|
+
throw new ApiError(409, `Task ${taskId}: Prompt may still be running (${entryBinding.awaitingPhase}). Verify the outcome in the terminal, or cancel the task.`);
|
|
9044
|
+
}
|
|
9045
|
+
const replayed = await this.redispatchTaskPromptAfterReplRestart(task.devAgentId, taskId);
|
|
9046
|
+
if (!replayed)
|
|
9047
|
+
throw new ApiError(409, `Task ${taskId} changed or its Dev prompt could not be replayed`);
|
|
9048
|
+
const binding = await this.agentStore.get(task.devAgentId);
|
|
9049
|
+
if (binding?.taskId === taskId && binding.status === 'awaiting_human') {
|
|
9050
|
+
throw new ApiError(409, `Task ${taskId} is blocked (${binding.awaitingPhase ?? 'awaiting_human'}): ${binding.awaitingReason ?? 'Inspect the task and its agent before retrying.'}`);
|
|
9051
|
+
}
|
|
9052
|
+
return (await this.taskStore.get(taskId)) ?? task;
|
|
9053
|
+
}
|
|
8789
9054
|
async advanceTask(taskId, opts = {}) {
|
|
8790
9055
|
let task = await this.taskStore.get(taskId);
|
|
8791
9056
|
if (!task)
|
|
@@ -8817,12 +9082,7 @@ export class AgentManager {
|
|
|
8817
9082
|
throw new ApiError(409, 'Review cannot advance to Dev without a request-changes verdict');
|
|
8818
9083
|
}
|
|
8819
9084
|
else if (task.status === 'in_progress' || task.status === 'fixing') {
|
|
8820
|
-
|
|
8821
|
-
throw new ApiError(409, `Task ${taskId} has no Dev participant`);
|
|
8822
|
-
const replayed = await this.redispatchTaskPromptAfterReplRestart(task.devAgentId, taskId);
|
|
8823
|
-
if (!replayed)
|
|
8824
|
-
throw new ApiError(409, `Task ${taskId} changed or its Dev prompt could not be replayed`);
|
|
8825
|
-
updated = (await this.taskStore.get(taskId)) ?? task;
|
|
9085
|
+
updated = await this.replayDevTaskForAdvance(task);
|
|
8826
9086
|
}
|
|
8827
9087
|
else if (task.status === 'approved') {
|
|
8828
9088
|
if (!task.devAgentId)
|
|
@@ -8833,10 +9093,7 @@ export class AgentManager {
|
|
|
8833
9093
|
}
|
|
8834
9094
|
task = await this.restoreRevokedPostApprove(task);
|
|
8835
9095
|
}
|
|
8836
|
-
|
|
8837
|
-
if (!replayed)
|
|
8838
|
-
throw new ApiError(409, `Task ${taskId} changed or its post-approve prompt could not be replayed`);
|
|
8839
|
-
updated = (await this.taskStore.get(taskId)) ?? task;
|
|
9096
|
+
updated = await this.replayDevTaskForAdvance(task);
|
|
8840
9097
|
}
|
|
8841
9098
|
else {
|
|
8842
9099
|
throw new ApiError(409, `Task ${taskId} cannot be advanced from ${task.status}`);
|