baxian 2.0.32 → 2.1.0
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 +4 -1
- package/dist/agent/branch.d.ts.map +1 -1
- package/dist/agent/branch.js +67 -9
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/detect/manifests/codex.json +1 -1
- package/dist/agent/manager.d.ts +18 -0
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +209 -10
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +4 -2
- 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/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +12 -0
- package/dist/api/tasks.js.map +1 -1
- package/dist/shared/constants.d.ts +5 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +6 -0
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/types.d.ts +6 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.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/web/assets/index-CiQfh8FL.js +0 -19
- package/dist/web/assets/index-ZHKFSFZy.css +0 -1
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) {
|
|
@@ -1880,14 +1881,14 @@ export class AgentManager {
|
|
|
1880
1881
|
}
|
|
1881
1882
|
}
|
|
1882
1883
|
if (cfg.role === 'dev' && boundTask) {
|
|
1883
|
-
const branches =
|
|
1884
|
+
const branches = this.taskBranchManager(runner, agentId, expectedTaskId);
|
|
1884
1885
|
const cleanup = await branches.cleanupTaskBranch(releaseWorkdir, this.cleanupIdentityFor(boundTask), () => this.assertTaskGeneration(agentId, expectedTaskId, lockToken, releaseWorkdir).then(() => undefined));
|
|
1885
1886
|
await this.persistBranchCleanupOutcome(boundTask, agentId, cleanup);
|
|
1886
1887
|
if (cleanup.status !== 'deleted')
|
|
1887
1888
|
await branches.parkOnDefaultDetached(releaseWorkdir);
|
|
1888
1889
|
}
|
|
1889
1890
|
else {
|
|
1890
|
-
await
|
|
1891
|
+
await this.taskBranchManager(runner, agentId, expectedTaskId).parkOnDefaultDetached(releaseWorkdir);
|
|
1891
1892
|
}
|
|
1892
1893
|
await this.assertTaskGeneration(agentId, expectedTaskId, lockToken, releaseWorkdir);
|
|
1893
1894
|
}
|
|
@@ -2090,7 +2091,7 @@ export class AgentManager {
|
|
|
2090
2091
|
if (!(await this.lockManager.isOwner(cfg.id, owner, token)))
|
|
2091
2092
|
continue;
|
|
2092
2093
|
for (const task of targets) {
|
|
2093
|
-
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));
|
|
2094
2095
|
cleanupResults.push({ task, cleanup });
|
|
2095
2096
|
}
|
|
2096
2097
|
}
|
|
@@ -6060,6 +6061,204 @@ export class AgentManager {
|
|
|
6060
6061
|
return guarded.ran ? guarded.value : null;
|
|
6061
6062
|
});
|
|
6062
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
|
+
}
|
|
6063
6262
|
createRunnerFor(agent) {
|
|
6064
6263
|
if (this.runnerFactory) {
|
|
6065
6264
|
return this.runnerFactory(agent);
|
|
@@ -7125,7 +7324,7 @@ export class AgentManager {
|
|
|
7125
7324
|
const { paneId, pane, workdir } = ensure;
|
|
7126
7325
|
const runner = this.createRunnerFor(agent);
|
|
7127
7326
|
const tmux = new TmuxManager(runner);
|
|
7128
|
-
const branchManager =
|
|
7327
|
+
const branchManager = this.taskBranchManager(runner, agentId, taskId);
|
|
7129
7328
|
const lockState = await this.agentStore.get(agentId);
|
|
7130
7329
|
if (!lockState || lockState.taskId !== taskId || lockState.lockToken !== lockToken)
|
|
7131
7330
|
return false;
|
|
@@ -7733,7 +7932,7 @@ export class AgentManager {
|
|
|
7733
7932
|
return false;
|
|
7734
7933
|
}
|
|
7735
7934
|
if (agent.role === 'dev' && task.branch) {
|
|
7736
|
-
const branches =
|
|
7935
|
+
const branches = this.taskBranchManager(runner, agentId, taskId);
|
|
7737
7936
|
if (!opts.allowDirtyWorkdir)
|
|
7738
7937
|
await branches.assertClean(verifiedWorkdir);
|
|
7739
7938
|
const actualRef = await branches.currentRef(verifiedWorkdir);
|