baxian 2.0.18 → 2.0.20

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.
Files changed (44) hide show
  1. package/dist/agent/branch.d.ts.map +1 -1
  2. package/dist/agent/branch.js +1 -1
  3. package/dist/agent/branch.js.map +1 -1
  4. package/dist/agent/detect/classify.d.ts +7 -0
  5. package/dist/agent/detect/classify.d.ts.map +1 -0
  6. package/dist/agent/detect/classify.js +42 -0
  7. package/dist/agent/detect/classify.js.map +1 -0
  8. package/dist/agent/detect/gate.d.ts +2 -2
  9. package/dist/agent/detect/gate.d.ts.map +1 -1
  10. package/dist/agent/detect/gate.js +17 -63
  11. package/dist/agent/detect/gate.js.map +1 -1
  12. package/dist/agent/detect/manifest.d.ts +3 -2
  13. package/dist/agent/detect/manifest.d.ts.map +1 -1
  14. package/dist/agent/detect/manifest.js +36 -63
  15. package/dist/agent/detect/manifest.js.map +1 -1
  16. package/dist/agent/detect/manifests/claude-code.json +387 -153
  17. package/dist/agent/detect/manifests/codex.json +137 -63
  18. package/dist/agent/detect/manifests/opencode.json +69 -17
  19. package/dist/agent/detect/manifests/qodercli.json +84 -25
  20. package/dist/agent/detect/region.d.ts +2 -2
  21. package/dist/agent/detect/region.d.ts.map +1 -1
  22. package/dist/agent/detect/region.js +122 -101
  23. package/dist/agent/detect/region.js.map +1 -1
  24. package/dist/agent/manager.d.ts +2 -0
  25. package/dist/agent/manager.d.ts.map +1 -1
  26. package/dist/agent/manager.js +147 -60
  27. package/dist/agent/manager.js.map +1 -1
  28. package/dist/agent/prompt.js +4 -5
  29. package/dist/agent/prompt.js.map +1 -1
  30. package/dist/agent/repo-store.d.ts +2 -0
  31. package/dist/agent/repo-store.d.ts.map +1 -1
  32. package/dist/agent/repo-store.js +41 -6
  33. package/dist/agent/repo-store.js.map +1 -1
  34. package/dist/agent/tmux-probe-poller.d.ts +0 -1
  35. package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
  36. package/dist/agent/tmux-probe-poller.js +11 -34
  37. package/dist/agent/tmux-probe-poller.js.map +1 -1
  38. package/dist/agent/tmux.d.ts +3 -14
  39. package/dist/agent/tmux.d.ts.map +1 -1
  40. package/dist/agent/tmux.js +37 -236
  41. package/dist/agent/tmux.js.map +1 -1
  42. package/dist/web/assets/{index-BFjlgPUQ.js → index-DzkRM7gR.js} +13 -13
  43. package/dist/web/index.html +1 -1
  44. package/package.json +1 -1
@@ -17,8 +17,9 @@ import { platformBindingMismatch, taskNeedsPlatformBindingAudit, } from '../plat
17
17
  import { AGENT_STORE_NOOP } from '../state/agent-store.js';
18
18
  import { createRunner, LocalRunner, shellQuote, resolveAgentHost, workdirHostGroupKey, } from './runner.js';
19
19
  import { isTransientNetworkFailure } from './net-exec.js';
20
+ import { classifyScreen, isMenuRule } from './detect/classify.js';
20
21
  import { imageFilename, agentHostPath, writeImageToHost } from './image-input.js';
21
- import { TmuxManager, ReplNotReadyError, detectStartupDialog, detectRuntimeMenu, runtimeBusyCheck, hasRuntimeReadyView, hasReplProcTitle, isShellProcTitle, hasOscTitleWorking, hasOscTitleIdle, screenAllowsTitleIdle, PaneGoneError, } from './tmux.js';
22
+ import { TmuxManager, ReplNotReadyError, detectStartupDialog, hasRuntimeReadyView, hasReplProcTitle, isShellProcTitle, PaneGoneError, } from './tmux.js';
22
23
  import { BranchManager, DirtyWorkdirError, ReviewHeadMismatchError, isAutoDeletableTaskBranch, } from './branch.js';
23
24
  import { RepoStore, createRepoStoreCache } from './repo-store.js';
24
25
  import { PhaseSignalWatcher, } from './phase-signal-watcher.js';
@@ -180,12 +181,17 @@ export function canDispatchWithBinding(binding) {
180
181
  const RECOVERABLE_QA_DISPATCH_HOLD_PHASES = new Set([
181
182
  'checkout-preparation-failed',
182
183
  'dirty-workdir',
184
+ 'branch-cleanup-pending',
183
185
  ]);
184
186
  export function isRecoverableQaDispatchHold(binding) {
185
187
  return binding?.status === 'awaiting_human'
186
188
  && binding.awaitingPhase != null
187
189
  && RECOVERABLE_QA_DISPATCH_HOLD_PHASES.has(binding.awaitingPhase);
188
190
  }
191
+ const GIT_REVIEW_DISPATCH_HOLD_PHASES = new Set([
192
+ ...RECOVERABLE_QA_DISPATCH_HOLD_PHASES,
193
+ 'dispatch-failed:ack_unknown',
194
+ ]);
189
195
  const CANCEL_CLEANUP_HOLD_PHASES = new Set(['cancel-clearing', 'cancel-interrupt-failed']);
190
196
  function isCancelCleanupHold(binding) {
191
197
  return binding?.awaitingPhase != null && CANCEL_CLEANUP_HOLD_PHASES.has(binding.awaitingPhase);
@@ -1295,7 +1301,7 @@ export class AgentManager {
1295
1301
  fresh.paneId !== state.paneId) {
1296
1302
  continue;
1297
1303
  }
1298
- const onMenu = detectRuntimeMenu(stripped);
1304
+ const onMenu = isMenuRule(cfg.runtime, classifyScreen(cfg.runtime, stripped).matchedRuleId);
1299
1305
  if (onMenu && !pendingMenu) {
1300
1306
  pendingMenu = true;
1301
1307
  await this.emitIntervention(fresh.projectId, agentId, fresh.taskId, {
@@ -1517,6 +1523,8 @@ export class AgentManager {
1517
1523
  : null;
1518
1524
  const sameTaskLocked = existingToken !== null;
1519
1525
  const reentryPhases = new Set(['fix', 'post-approve', 'code']);
1526
+ // fix / post-approve 由 continueSession 投递,没有配对的 clearBootstrapMarker
1527
+ const unfinalizedPhases = new Set(['fix', 'post-approve']);
1520
1528
  const sameTaskReentry = state?.taskId === taskId &&
1521
1529
  !state.creationToken &&
1522
1530
  state.status !== 'awaiting_human' &&
@@ -1553,7 +1561,7 @@ export class AgentManager {
1553
1561
  projectId: cfg.projectId,
1554
1562
  taskId,
1555
1563
  lockToken: token,
1556
- ...(phase === 'code' && !sameTaskReentry ? { bootstrappingTaskId: taskId } : {}),
1564
+ ...(!unfinalizedPhases.has(phase) ? { bootstrappingTaskId: taskId } : {}),
1557
1565
  updatedAt: now,
1558
1566
  };
1559
1567
  });
@@ -2981,36 +2989,49 @@ export class AgentManager {
2981
2989
  }
2982
2990
  async paneHasLiveTurn(tmux, pane, runtime) {
2983
2991
  const paneId = pane.paneId;
2984
- let first;
2985
- let firstTitle;
2986
- try {
2987
- first = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
2988
- firstTitle = await tmux.readPaneTitle(pane);
2989
- }
2990
- catch (err) {
2991
- console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness capture failed for pane ${paneId}:`, err);
2992
- return true;
2993
- }
2994
- for (let i = 1; i < RUNTIME_LIVENESS_SAMPLES; i++) {
2995
- await new Promise(r => setTimeout(r, this.runtimeLivenessProbeMs));
2996
- let frame;
2997
- let title;
2992
+ const sample = async (phase) => {
2998
2993
  try {
2999
- frame = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
3000
- title = await tmux.readPaneTitle(pane);
2994
+ return {
2995
+ frame: await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 }),
2996
+ title: await tmux.readPaneTitle(pane),
2997
+ };
3001
2998
  }
3002
2999
  catch (err) {
3003
- console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness re-capture failed for pane ${paneId}:`, err);
3004
- return true;
3000
+ console.warn(`[AgentManager] interruptPaneAndWaitReady: liveness ${phase} failed for pane ${paneId}:`, err);
3001
+ return null;
3005
3002
  }
3006
- if (title !== firstTitle && hasOscTitleWorking(title))
3003
+ };
3004
+ const first = await sample('capture');
3005
+ if (!first)
3006
+ return true;
3007
+ // 空屏幕 = 只让标题规则说话:标题相对首帧变了且判 working,说明 turn 还活着
3008
+ const titleTurnedWorking = (title) => title !== first.title && classifyScreen(runtime, '', title).state === 'working';
3009
+ let prev = first.frame;
3010
+ let settledSinceChange = true;
3011
+ for (let i = 1; i < RUNTIME_LIVENESS_SAMPLES; i++) {
3012
+ await new Promise(r => setTimeout(r, this.runtimeLivenessProbeMs));
3013
+ const next = await sample('re-capture');
3014
+ if (!next)
3007
3015
  return true;
3008
- if (hasRuntimeReadyView(frame, runtime))
3009
- return false;
3010
- if (frame !== first)
3016
+ if (titleTurnedWorking(next.title))
3011
3017
  return true;
3018
+ if (next.frame === prev) {
3019
+ settledSinceChange = true;
3020
+ continue;
3021
+ }
3022
+ if (!hasRuntimeReadyView(next.frame, runtime))
3023
+ return true;
3024
+ // working→idle 的最新帧已 ready:不能仅凭帧差判 live,需再观察一拍确认它稳定
3025
+ prev = next.frame;
3026
+ settledSinceChange = false;
3012
3027
  }
3013
- return false;
3028
+ if (settledSinceChange)
3029
+ return false;
3030
+ await new Promise(r => setTimeout(r, this.runtimeLivenessProbeMs));
3031
+ const confirm = await sample('confirm capture');
3032
+ if (!confirm)
3033
+ return true;
3034
+ return titleTurnedWorking(confirm.title) || confirm.frame !== prev;
3014
3035
  }
3015
3036
  async paneRunsRuntime(tmux, pane, runtime) {
3016
3037
  const paneId = pane.paneId;
@@ -4612,13 +4633,37 @@ export class AgentManager {
4612
4633
  }
4613
4634
  next.reviewDispatchedAt = now;
4614
4635
  delete next.reviewDispatch;
4615
- if (next.attention?.reason === 'git-review-dispatch-recovery-uncertain') {
4636
+ if (next.attention?.reason === 'git-review-dispatch-recovery-uncertain'
4637
+ || await this.dispatchHoldAttentionIsStale(next)) {
4616
4638
  delete next.attention;
4617
4639
  }
4618
4640
  await this.taskStore.set(next);
4619
4641
  return true;
4620
4642
  });
4621
4643
  }
4644
+ // a hold and its task attention are written as a pair, so no live hold means this one was consumed
4645
+ async dispatchHoldAttentionIsStale(task) {
4646
+ const reason = task.attention?.reason;
4647
+ if (reason === undefined || !GIT_REVIEW_DISPATCH_HOLD_PHASES.has(reason))
4648
+ return false;
4649
+ for (const participantId of [task.agentId, task.qaAgentId]) {
4650
+ if (!participantId)
4651
+ continue;
4652
+ let participant;
4653
+ try {
4654
+ participant = await this.agentStore.get(participantId);
4655
+ }
4656
+ catch (err) {
4657
+ console.error(`[AgentManager] hold liveness probe for ${participantId} failed:`, err);
4658
+ return false;
4659
+ }
4660
+ if (participant?.taskId === task.id
4661
+ && participant.status === 'awaiting_human'
4662
+ && participant.awaitingPhase === reason)
4663
+ return false;
4664
+ }
4665
+ return true;
4666
+ }
4622
4667
  async recoverPendingGitReviewDispatchHold(taskId, expectedGeneration) {
4623
4668
  return this.withTaskLock(async () => {
4624
4669
  const task = await this.taskStore.get(taskId);
@@ -6987,12 +7032,12 @@ export class AgentManager {
6987
7032
  const paneId = pane.paneId;
6988
7033
  const preTitle = await tmux.readPaneTitle(pane);
6989
7034
  const preFrame = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
6990
- const staticBusy = hasOscTitleWorking(preTitle)
6991
- ? (runtimeBusyCheck(preFrame, runtime) || !hasRuntimeReadyView(preFrame, runtime))
6992
- : runtimeBusyCheck(preFrame, runtime);
6993
- if (staticBusy && await this.paneHasLiveTurn(tmux, pane, runtime)) {
6994
- throw new ReplNotReadyError(paneId, runtime, preFrame, `pre-inject busy check: pane ${paneId} is still running a turn; dispatch aborted`);
7035
+ const preDetection = classifyScreen(runtime, preFrame, preTitle);
7036
+ if (preDetection.state === 'pending') {
7037
+ throw new ReplNotReadyError(paneId, runtime, preFrame, `pre-inject check: pane ${paneId} shows a blocker; dispatch aborted`);
6995
7038
  }
7039
+ // working pane 上清稿的 C-c 会打断进行中的 turn
7040
+ const paneWorking = preDetection.state === 'working';
6996
7041
  await revalidate?.();
6997
7042
  if (guardBeforePaste) {
6998
7043
  if (!(await guardBeforePaste())) {
@@ -7004,7 +7049,8 @@ export class AgentManager {
7004
7049
  pasted = await this.withTaskLock(async () => {
7005
7050
  if (!(await guardBeforePaste()))
7006
7051
  return false;
7007
- await tmux.clearComposerDraft(pane);
7052
+ if (!paneWorking)
7053
+ await tmux.clearComposerDraft(pane);
7008
7054
  await tmux.pasteStagedBuffer(paneId, staged.buf);
7009
7055
  return true;
7010
7056
  });
@@ -7019,6 +7065,10 @@ export class AgentManager {
7019
7065
  console.warn(`[AgentManager] staged buffer ${staged.buf} not confirmed dropped after failed paste (missing or unprobeable); failing closed to a composer scrub:`, dropErr);
7020
7066
  }
7021
7067
  if (bufferConsumed) {
7068
+ const message = pasteErr instanceof Error ? pasteErr.message : String(pasteErr);
7069
+ if (paneWorking) {
7070
+ throw new DispatchTerminalError('ack_unknown', `paste outcome unknown on working pane ${paneId}; composer left untouched to keep the live turn intact: ${message}`);
7071
+ }
7022
7072
  try {
7023
7073
  await tmux.clearComposerDraft(pane);
7024
7074
  }
@@ -7039,7 +7089,8 @@ export class AgentManager {
7039
7089
  }
7040
7090
  }
7041
7091
  else {
7042
- await tmux.clearComposerDraft(pane);
7092
+ if (!paneWorking)
7093
+ await tmux.clearComposerDraft(pane);
7043
7094
  await revalidate?.();
7044
7095
  await tmux.injectPrompt(pane, prompt, agentId);
7045
7096
  }
@@ -7056,6 +7107,9 @@ export class AgentManager {
7056
7107
  return true;
7057
7108
  });
7058
7109
  if (!submitted) {
7110
+ if (paneWorking) {
7111
+ throw new Error(`fence-rejected prompt stays in the composer of working pane ${paneId}`);
7112
+ }
7059
7113
  try {
7060
7114
  await tmux.clearComposerDraft(pane);
7061
7115
  }
@@ -7071,11 +7125,18 @@ export class AgentManager {
7071
7125
  }
7072
7126
  }
7073
7127
  catch (preAckErr) {
7128
+ const message = preAckErr instanceof Error ? preAckErr.message : String(preAckErr);
7129
+ // working pane 上 scrub 的 C-c 会打断进行中的 turn:交人工核验,不动 composer
7130
+ if (paneWorking) {
7131
+ throw new DispatchTerminalError('ack_unknown', `pre-ack failure left an unconfirmed composer on working pane ${paneId}: ${message}`);
7132
+ }
7074
7133
  if (await this.clearComposerForReuse(tmux, pane, agentId))
7075
7134
  throw preAckErr;
7076
- const message = preAckErr instanceof Error ? preAckErr.message : String(preAckErr);
7077
7135
  throw new DispatchTerminalError('ack_unknown', `pre-ack failure left an unconfirmed composer on live pane ${paneId}: ${message}`);
7078
7136
  }
7137
+ // 判据必须取自粘贴之前:粘贴后的基线含提示词正文,会自匹配 working 规则
7138
+ if (paneWorking)
7139
+ return { acked: true, composerDelivered: true };
7079
7140
  let staleDuringResend = false;
7080
7141
  try {
7081
7142
  await tmux.waitSubmitAck(pane, baseline, runtime, {
@@ -7122,8 +7183,7 @@ export class AgentManager {
7122
7183
  'baxian intentionally did NOT send C-c — the input may still be queued. ' +
7123
7184
  'Attach via web terminal to verify and resolve.',
7124
7185
  });
7125
- const composerDelivered = !/pane already busy at baseline/.test(message);
7126
- return { acked: false, composerDelivered };
7186
+ return { acked: false, composerDelivered: true };
7127
7187
  }
7128
7188
  }
7129
7189
  async clearComposerForReuse(tmux, pane, agentId) {
@@ -7487,10 +7547,12 @@ export class AgentManager {
7487
7547
  return false;
7488
7548
  }
7489
7549
  }
7490
- async clearBootstrapMarker(agentId, taskId) {
7550
+ async clearBootstrapMarker(agentId, taskId, expectedLockToken) {
7491
7551
  await this.agentStore.update(agentId, (latest) => {
7492
7552
  if (!latest || latest.taskId !== taskId || latest.bootstrappingTaskId === undefined)
7493
7553
  return AGENT_STORE_NOOP;
7554
+ if (expectedLockToken !== undefined && latest.lockToken !== expectedLockToken)
7555
+ return AGENT_STORE_NOOP;
7494
7556
  const { bootstrappingTaskId: _delivered, ...rest } = latest;
7495
7557
  return { ...rest, updatedAt: new Date().toISOString() };
7496
7558
  });
@@ -8043,6 +8105,13 @@ export class AgentManager {
8043
8105
  await this.resetGitReviewDispatchClaim(taskId, lease);
8044
8106
  throw new ApiError(409, `QA agent ${qaId} could not be released for git review dispatch`);
8045
8107
  }
8108
+ if (previousQa.status === 'awaiting_human' && previousQa.awaitingPhase !== undefined) {
8109
+ await this.emitIntervention(task.projectId, qaId, taskId, {
8110
+ phase: 'git-review-dispatch-hold-cleared',
8111
+ previousPhase: previousQa.awaitingPhase,
8112
+ generation: lease.generation,
8113
+ });
8114
+ }
8046
8115
  opts.onSideEffect?.();
8047
8116
  }
8048
8117
  let acquiredLockToken;
@@ -8861,17 +8930,8 @@ export class AgentManager {
8861
8930
  });
8862
8931
  await new Promise(r => setTimeout(r, this.compactIdlePollMs));
8863
8932
  while (true) {
8864
- const current = await tmux.displayMessage(pane, '#{pane_current_command}');
8865
- if (!hasReplProcTitle(current, runtime)) {
8866
- throw new Error(`waitForReplPromptReady: pane ${paneId} pane_current_command=${current.trim()} (not runtime, REPL may have exited)`);
8867
- }
8868
- const cap = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
8869
- const ready = hasRuntimeReadyView(cap, runtime)
8870
- || (screenAllowsTitleIdle(cap, runtime) && hasOscTitleIdle(await tmux.readPaneTitle(pane), runtime));
8871
- if (detectRuntimeMenu(cap) || (!ready && detectStartupDialog(cap, runtime))) {
8872
- throw new Error(`waitForReplPromptReady: pane ${paneId} shows menu/dialog, not a ready REPL prompt`);
8873
- }
8874
- if (!runtimeBusyCheck(cap, runtime)) {
8933
+ const { cap, ready, working } = await this.probeReplPrompt(tmux, pane, runtime);
8934
+ if (!working) {
8875
8935
  if (!ready) {
8876
8936
  throw new Error(`waitForReplPromptReady: pane ${paneId} observed idle but no ready anchor (stale frame?)`);
8877
8937
  }
@@ -8883,6 +8943,23 @@ export class AgentManager {
8883
8943
  await new Promise(r => setTimeout(r, this.compactIdlePollMs));
8884
8944
  }
8885
8945
  }
8946
+ async probeReplPrompt(tmux, pane, runtime) {
8947
+ const paneId = pane.paneId;
8948
+ const current = await tmux.displayMessage(pane, '#{pane_current_command}');
8949
+ if (!hasReplProcTitle(current, runtime)) {
8950
+ throw new Error(`waitForReplPromptReady: pane ${paneId} pane_current_command=${current.trim()} (not runtime, REPL may have exited)`);
8951
+ }
8952
+ const cap = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
8953
+ let detection = classifyScreen(runtime, cap);
8954
+ // 标题规则优先级最高:屏幕判 idle 不作数,必须带标题复核
8955
+ if (detection.state === 'idle')
8956
+ detection = classifyScreen(runtime, cap, await tmux.readPaneTitle(pane));
8957
+ const ready = hasRuntimeReadyView(cap, runtime, detection);
8958
+ if (detection.state === 'pending' || detection.skipStateUpdate || detectStartupDialog(cap, runtime)) {
8959
+ throw new Error(`waitForReplPromptReady: pane ${paneId} shows menu/dialog, not a ready REPL prompt`);
8960
+ }
8961
+ return { cap, ready, working: detection.state === 'working' };
8962
+ }
8886
8963
  async waitForReplPromptStableIdle(tmux, pane, runtime, timeoutMs) {
8887
8964
  const paneId = pane.paneId;
8888
8965
  const samples = Math.max(1, this.readyStableSamples);
@@ -8890,17 +8967,8 @@ export class AgentManager {
8890
8967
  const deadline = Date.now() + timeoutMs + (samples - 1) * spacing;
8891
8968
  let streak = 0;
8892
8969
  while (true) {
8893
- const current = await tmux.displayMessage(pane, '#{pane_current_command}');
8894
- if (!hasReplProcTitle(current, runtime)) {
8895
- throw new Error(`waitForReplPromptReady: pane ${paneId} pane_current_command=${current.trim()} (not runtime, REPL may have exited)`);
8896
- }
8897
- const cap = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
8898
- const ready = hasRuntimeReadyView(cap, runtime)
8899
- || (screenAllowsTitleIdle(cap, runtime) && hasOscTitleIdle(await tmux.readPaneTitle(pane), runtime));
8900
- if (detectRuntimeMenu(cap) || (!ready && detectStartupDialog(cap, runtime))) {
8901
- throw new Error(`waitForReplPromptReady: pane ${paneId} shows menu/dialog, not a ready REPL prompt`);
8902
- }
8903
- streak = ready && !runtimeBusyCheck(cap, runtime) ? streak + 1 : 0;
8970
+ const { cap, ready } = await this.probeReplPrompt(tmux, pane, runtime);
8971
+ streak = ready ? streak + 1 : 0;
8904
8972
  if (streak >= samples)
8905
8973
  return;
8906
8974
  if (Date.now() >= deadline) {
@@ -9659,6 +9727,15 @@ export class AgentManager {
9659
9727
  console.warn(`[AgentManager] transitionToCodePhase(${taskId}): stale-generation acquire rollback failed:`, err);
9660
9728
  }
9661
9729
  }
9730
+ else {
9731
+ try {
9732
+ await this.clearBootstrapMarker(devAgentId, taskId, acquiredLockToken);
9733
+ }
9734
+ catch (clearErr) {
9735
+ console.warn(`[AgentManager] transitionToCodePhase(${taskId}): clearing the reentry bootstrap marker failed:`, clearErr);
9736
+ throw clearErr;
9737
+ }
9738
+ }
9662
9739
  return null;
9663
9740
  }
9664
9741
  this.stopPhaseSignalWatcher(taskId);
@@ -9692,6 +9769,16 @@ export class AgentManager {
9692
9769
  });
9693
9770
  }
9694
9771
  }
9772
+ else {
9773
+ try {
9774
+ await this.clearBootstrapMarker(devAgentId, taskId, acquiredLockToken);
9775
+ }
9776
+ catch (clearErr) {
9777
+ // 原始派发错误在这里落日志,抛出去的是更需要处理的回滚失败
9778
+ console.error(`[AgentManager] transitionToCodePhase dispatch(dev=${devAgentId}) failed:`, err);
9779
+ throw clearErr;
9780
+ }
9781
+ }
9695
9782
  }
9696
9783
  else if (!(err instanceof EnsureSessionError && err.partial.handled)) {
9697
9784
  await this.markAwaitingHuman(devAgentId, 'code-dispatch-failed', 'Code-phase prompt was not delivered after spec approval; the task looks in_progress but the dev never received it. Resume/restart the agent or cancel the task.', { expectedTaskId: taskId }).catch(() => undefined);