baxian 2.0.2 → 2.0.3

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 (212) hide show
  1. package/dist/agent/branch.d.ts.map +1 -1
  2. package/dist/agent/branch.js +0 -7
  3. package/dist/agent/branch.js.map +1 -1
  4. package/dist/agent/detect/region.d.ts.map +1 -1
  5. package/dist/agent/detect/region.js +0 -3
  6. package/dist/agent/detect/region.js.map +1 -1
  7. package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
  8. package/dist/agent/dispatch-reconciler.js +0 -51
  9. package/dist/agent/dispatch-reconciler.js.map +1 -1
  10. package/dist/agent/lineage.d.ts.map +1 -1
  11. package/dist/agent/lineage.js +0 -9
  12. package/dist/agent/lineage.js.map +1 -1
  13. package/dist/agent/manager.d.ts +1 -4
  14. package/dist/agent/manager.d.ts.map +1 -1
  15. package/dist/agent/manager.js +10 -298
  16. package/dist/agent/manager.js.map +1 -1
  17. package/dist/agent/net-exec.d.ts.map +1 -1
  18. package/dist/agent/net-exec.js +0 -5
  19. package/dist/agent/net-exec.js.map +1 -1
  20. package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
  21. package/dist/agent/pane-streamer-manager.js +0 -36
  22. package/dist/agent/pane-streamer-manager.js.map +1 -1
  23. package/dist/agent/pane-streamer.d.ts +6 -0
  24. package/dist/agent/pane-streamer.d.ts.map +1 -1
  25. package/dist/agent/pane-streamer.js +126 -83
  26. package/dist/agent/pane-streamer.js.map +1 -1
  27. package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
  28. package/dist/agent/phase-signal-watcher.js +2 -77
  29. package/dist/agent/phase-signal-watcher.js.map +1 -1
  30. package/dist/agent/phase-signal.d.ts +0 -1
  31. package/dist/agent/phase-signal.d.ts.map +1 -1
  32. package/dist/agent/phase-signal.js +0 -17
  33. package/dist/agent/phase-signal.js.map +1 -1
  34. package/dist/agent/preflight.d.ts.map +1 -1
  35. package/dist/agent/preflight.js +0 -18
  36. package/dist/agent/preflight.js.map +1 -1
  37. package/dist/agent/prompt.d.ts.map +1 -1
  38. package/dist/agent/prompt.js +0 -1
  39. package/dist/agent/prompt.js.map +1 -1
  40. package/dist/agent/repo-store.d.ts.map +1 -1
  41. package/dist/agent/repo-store.js +0 -38
  42. package/dist/agent/repo-store.js.map +1 -1
  43. package/dist/agent/runner.d.ts.map +1 -1
  44. package/dist/agent/runner.js +0 -2
  45. package/dist/agent/runner.js.map +1 -1
  46. package/dist/agent/tmux-install.js +0 -1
  47. package/dist/agent/tmux-install.js.map +1 -1
  48. package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
  49. package/dist/agent/tmux-probe-poller.js +0 -7
  50. package/dist/agent/tmux-probe-poller.js.map +1 -1
  51. package/dist/agent/tmux.d.ts.map +1 -1
  52. package/dist/agent/tmux.js +0 -86
  53. package/dist/agent/tmux.js.map +1 -1
  54. package/dist/agent/vt-visible-text.d.ts +46 -1
  55. package/dist/agent/vt-visible-text.d.ts.map +1 -1
  56. package/dist/agent/vt-visible-text.js +639 -63
  57. package/dist/agent/vt-visible-text.js.map +1 -1
  58. package/dist/api/agents.d.ts.map +1 -1
  59. package/dist/api/agents.js +1 -42
  60. package/dist/api/agents.js.map +1 -1
  61. package/dist/api/config.d.ts.map +1 -1
  62. package/dist/api/config.js +3 -52
  63. package/dist/api/config.js.map +1 -1
  64. package/dist/api/hosts.d.ts.map +1 -1
  65. package/dist/api/hosts.js +2 -57
  66. package/dist/api/hosts.js.map +1 -1
  67. package/dist/api/platform-guard.d.ts.map +1 -1
  68. package/dist/api/platform-guard.js +0 -1
  69. package/dist/api/platform-guard.js.map +1 -1
  70. package/dist/api/probe.d.ts.map +1 -1
  71. package/dist/api/probe.js +0 -1
  72. package/dist/api/probe.js.map +1 -1
  73. package/dist/api/projects.d.ts.map +1 -1
  74. package/dist/api/projects.js +1 -14
  75. package/dist/api/projects.js.map +1 -1
  76. package/dist/api/tasks.d.ts.map +1 -1
  77. package/dist/api/tasks.js +0 -1
  78. package/dist/api/tasks.js.map +1 -1
  79. package/dist/app.d.ts +0 -2
  80. package/dist/app.d.ts.map +1 -1
  81. package/dist/app.js +0 -6
  82. package/dist/app.js.map +1 -1
  83. package/dist/cli.d.ts.map +1 -1
  84. package/dist/cli.js +4 -29
  85. package/dist/cli.js.map +1 -1
  86. package/dist/config/hot-reload.d.ts.map +1 -1
  87. package/dist/config/hot-reload.js +0 -4
  88. package/dist/config/hot-reload.js.map +1 -1
  89. package/dist/config/loader.d.ts.map +1 -1
  90. package/dist/config/loader.js +1 -24
  91. package/dist/config/loader.js.map +1 -1
  92. package/dist/config/normalizer.d.ts.map +1 -1
  93. package/dist/config/normalizer.js +0 -19
  94. package/dist/config/normalizer.js.map +1 -1
  95. package/dist/config/validator.d.ts.map +1 -1
  96. package/dist/config/validator.js +2 -200
  97. package/dist/config/validator.js.map +1 -1
  98. package/dist/event/handlers.d.ts.map +1 -1
  99. package/dist/event/handlers.js +0 -11
  100. package/dist/event/handlers.js.map +1 -1
  101. package/dist/event/log.js +0 -2
  102. package/dist/event/log.js.map +1 -1
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +0 -45
  105. package/dist/index.js.map +1 -1
  106. package/dist/platform/body-digest.d.ts.map +1 -1
  107. package/dist/platform/body-digest.js +0 -4
  108. package/dist/platform/body-digest.js.map +1 -1
  109. package/dist/platform/command-renderer.d.ts.map +1 -1
  110. package/dist/platform/command-renderer.js +0 -3
  111. package/dist/platform/command-renderer.js.map +1 -1
  112. package/dist/platform/comment-cursor.d.ts.map +1 -1
  113. package/dist/platform/comment-cursor.js +0 -21
  114. package/dist/platform/comment-cursor.js.map +1 -1
  115. package/dist/platform/driver-spec.d.ts.map +1 -1
  116. package/dist/platform/driver-spec.js +1 -42
  117. package/dist/platform/driver-spec.js.map +1 -1
  118. package/dist/platform/field-mapper.d.ts.map +1 -1
  119. package/dist/platform/field-mapper.js +3 -11
  120. package/dist/platform/field-mapper.js.map +1 -1
  121. package/dist/platform/git-driver.d.ts.map +1 -1
  122. package/dist/platform/git-driver.js +0 -20
  123. package/dist/platform/git-driver.js.map +1 -1
  124. package/dist/platform/manifest.d.ts.map +1 -1
  125. package/dist/platform/manifest.js +0 -3
  126. package/dist/platform/manifest.js.map +1 -1
  127. package/dist/platform/markers.d.ts.map +1 -1
  128. package/dist/platform/markers.js +0 -19
  129. package/dist/platform/markers.js.map +1 -1
  130. package/dist/platform/platform-poller.d.ts.map +1 -1
  131. package/dist/platform/platform-poller.js +0 -43
  132. package/dist/platform/platform-poller.js.map +1 -1
  133. package/dist/platform/plugin-registry.d.ts.map +1 -1
  134. package/dist/platform/plugin-registry.js +1 -12
  135. package/dist/platform/plugin-registry.js.map +1 -1
  136. package/dist/platform/pr-conversation-cache.d.ts.map +1 -1
  137. package/dist/platform/pr-conversation-cache.js +0 -17
  138. package/dist/platform/pr-conversation-cache.js.map +1 -1
  139. package/dist/platform/preflight-exec.d.ts.map +1 -1
  140. package/dist/platform/preflight-exec.js +0 -9
  141. package/dist/platform/preflight-exec.js.map +1 -1
  142. package/dist/platform/response-parser.d.ts.map +1 -1
  143. package/dist/platform/response-parser.js +0 -4
  144. package/dist/platform/response-parser.js.map +1 -1
  145. package/dist/platform/review-timeline.d.ts.map +1 -1
  146. package/dist/platform/review-timeline.js +0 -22
  147. package/dist/platform/review-timeline.js.map +1 -1
  148. package/dist/platform/row-schema.d.ts.map +1 -1
  149. package/dist/platform/row-schema.js +0 -11
  150. package/dist/platform/row-schema.js.map +1 -1
  151. package/dist/platform/startup.d.ts.map +1 -1
  152. package/dist/platform/startup.js +1 -12
  153. package/dist/platform/startup.js.map +1 -1
  154. package/dist/platform/types.d.ts.map +1 -1
  155. package/dist/platform/types.js +0 -5
  156. package/dist/platform/types.js.map +1 -1
  157. package/dist/platform/verdict-engine.d.ts.map +1 -1
  158. package/dist/platform/verdict-engine.js +0 -15
  159. package/dist/platform/verdict-engine.js.map +1 -1
  160. package/dist/shared/concurrency.d.ts.map +1 -1
  161. package/dist/shared/concurrency.js +0 -2
  162. package/dist/shared/concurrency.js.map +1 -1
  163. package/dist/shared/constants.d.ts +0 -2
  164. package/dist/shared/constants.d.ts.map +1 -1
  165. package/dist/shared/constants.js +0 -3
  166. package/dist/shared/constants.js.map +1 -1
  167. package/dist/shared/git-url.d.ts.map +1 -1
  168. package/dist/shared/git-url.js +0 -9
  169. package/dist/shared/git-url.js.map +1 -1
  170. package/dist/shared/types.d.ts +0 -11
  171. package/dist/shared/types.d.ts.map +1 -1
  172. package/dist/shared/types.js.map +1 -1
  173. package/dist/skill/registry.d.ts.map +1 -1
  174. package/dist/skill/registry.js +0 -5
  175. package/dist/skill/registry.js.map +1 -1
  176. package/dist/skills/baxian-signals/SKILL.md +0 -6
  177. package/dist/state/agent-store.d.ts.map +1 -1
  178. package/dist/state/agent-store.js +0 -6
  179. package/dist/state/agent-store.js.map +1 -1
  180. package/dist/state/error-record-store.d.ts.map +1 -1
  181. package/dist/state/error-record-store.js +0 -5
  182. package/dist/state/error-record-store.js.map +1 -1
  183. package/dist/state/init.d.ts.map +1 -1
  184. package/dist/state/init.js +0 -1
  185. package/dist/state/init.js.map +1 -1
  186. package/dist/state/lock.d.ts.map +1 -1
  187. package/dist/state/lock.js +0 -1
  188. package/dist/state/lock.js.map +1 -1
  189. package/dist/state/task-store.d.ts.map +1 -1
  190. package/dist/state/task-store.js +0 -3
  191. package/dist/state/task-store.js.map +1 -1
  192. package/dist/terminal/attach.d.ts.map +1 -1
  193. package/dist/terminal/attach.js +0 -4
  194. package/dist/terminal/attach.js.map +1 -1
  195. package/dist/terminal/stream-ws.js +0 -5
  196. package/dist/terminal/stream-ws.js.map +1 -1
  197. package/dist/web/assets/{index-BS2adAfI.css → index-yqxawAPJ.css} +1 -1
  198. package/dist/web/index.html +2 -2
  199. package/package.json +1 -1
  200. package/dist/agent/root-agent-runtime.d.ts +0 -80
  201. package/dist/agent/root-agent-runtime.d.ts.map +0 -1
  202. package/dist/agent/root-agent-runtime.js +0 -637
  203. package/dist/agent/root-agent-runtime.js.map +0 -1
  204. package/dist/agent/root-recovery-coordinator.d.ts +0 -124
  205. package/dist/agent/root-recovery-coordinator.d.ts.map +0 -1
  206. package/dist/agent/root-recovery-coordinator.js +0 -1557
  207. package/dist/agent/root-recovery-coordinator.js.map +0 -1
  208. package/dist/state/root-recovery-store.d.ts +0 -107
  209. package/dist/state/root-recovery-store.d.ts.map +0 -1
  210. package/dist/state/root-recovery-store.js +0 -400
  211. package/dist/state/root-recovery-store.js.map +0 -1
  212. /package/dist/web/assets/{index-BKiM0MSM.js → index-CiAnjs1_.js} +0 -0
@@ -27,7 +27,6 @@ import { resolveProjectTool } from '../config/validator.js';
27
27
  import { SHA_HEX_SOURCE } from '../platform/types.js';
28
28
  import { bodyDigest } from '../platform/body-digest.js';
29
29
  import { validateCommentBody } from '../platform/command-renderer.js';
30
- // 这两个入口冲突证明当前调用已被并发 pass 接管,应判 stale 而不是派发失败。
31
30
  const BENIGN_DISPATCH_CONFLICT_CODES = new Set(['dispatch-superseded', 'dispatch-in-flight']);
32
31
  export function isBenignDispatchConflict(err) {
33
32
  return err instanceof ApiError
@@ -81,10 +80,6 @@ export function buildLaunchCommand(agent) {
81
80
  segments.push('--dangerously-bypass-approvals-and-sandbox');
82
81
  break;
83
82
  case 'opencode':
84
- // opencode materializes skills into .agents/skills (shared with codex; see skillSubdirFor).
85
- // Its .agents/.claude compatibility scans can't both be disabled by env, so also turn off
86
- // the .claude/skills scan — that closes the last collision source: a claude-code agent's
87
- // .claude/skills/baxian-* in a shared base repo.
88
83
  segments.push('env OPENCODE_DISABLE_CLAUDE_CODE_SKILLS=1 opencode');
89
84
  if (yolo)
90
85
  segments.push('--auto');
@@ -98,7 +93,6 @@ export function buildLaunchCommand(agent) {
98
93
  if (agent.model) {
99
94
  segments.push(`--model ${shellQuote(agent.model)}`);
100
95
  }
101
- // opencode has no --add-dir; extra roots go through its permission model instead (validator rejects the pairing).
102
96
  if (agent.addDirs && agent.addDirs.length > 0 && agent.runtime !== 'opencode') {
103
97
  for (const dir of agent.addDirs) {
104
98
  segments.push(`--add-dir ${shellQuote(dir)}`);
@@ -106,15 +100,12 @@ export function buildLaunchCommand(agent) {
106
100
  }
107
101
  return segments.join(' ');
108
102
  }
109
- // cd re-resolves by pathname: a reused pane shell may sit on a deleted-and-recreated cwd inode.
110
103
  export function launchCommandIn(dir, agent) {
111
104
  return `cd ${shellQuote(dir)} && ${buildLaunchCommand(agent)}`;
112
105
  }
113
106
  function agentRuntimeKindFor(agent) {
114
107
  return agent.runtime;
115
108
  }
116
- // restart-repl drives the REPL back to a shell before relaunch. Each runtime exits
117
- // via its own slash command (a bare "exit" would be sent to the model as chat text).
118
109
  const REPL_EXIT_COMMAND = {
119
110
  'claude-code': '/exit',
120
111
  codex: '/quit',
@@ -146,11 +137,6 @@ export function skillSubdirFor(runtime) {
146
137
  switch (runtime) {
147
138
  case 'codex':
148
139
  case 'opencode':
149
- // opencode has no way to disable its .agents/skills compatibility scan (only .claude via
150
- // env), and when it shares a base repo with a codex agent it loads the codex copy of a
151
- // same-named baxian-* skill (verified: the .agents copy wins over .opencode). So opencode
152
- // materializes into .agents/skills too — both runtimes share one runtime-agnostic copy
153
- // (serialized by the same skill-dir lock) instead of colliding on duplicate names.
154
140
  return '.agents/skills';
155
141
  case 'qodercli':
156
142
  return '.qoder/skills';
@@ -161,8 +147,6 @@ export function skillSubdirFor(runtime) {
161
147
  const DEFAULT_DISPATCH_ACK_TIMEOUT_MS = 30_000;
162
148
  const DEFAULT_NEED_INPUT_RETRY_INTERVAL_MS = 5_000;
163
149
  const DEFAULT_DISPATCH_SETTLE_TIMEOUT_MS = 3_000;
164
- // DELETE runtime cleanup runs while holding the deletion tombstone + rotated lock; an unbounded remote
165
- // probe/kill would block rollback/finally forever, so every tmux round-trip in it carries this deadline.
166
150
  const DELETE_CLEANUP_TIMEOUT_MS = 15_000;
167
151
  const PLATFORM_HEAD_SHA_RE = new RegExp(`^${SHA_HEX_SOURCE}$`);
168
152
  function createSignalTokenExcluding(...excluded) {
@@ -186,7 +170,6 @@ const RUNTIME_LIVENESS_SAMPLES = 3;
186
170
  export function canDispatchWithBinding(binding) {
187
171
  return !binding?.taskId && !binding?.creationToken && binding?.status !== 'awaiting_human';
188
172
  }
189
- // 派发期 hold:prompt 从未注入、pane 无在途回合,重派安全;ack_unknown 一类不在此列
190
173
  export const RECOVERABLE_QA_DISPATCH_HOLD_PHASES = new Set([
191
174
  'checkout-preparation-failed',
192
175
  'dirty-workdir',
@@ -220,8 +203,6 @@ function shouldReleaseHeldBinding(state, boundTask) {
220
203
  if (state.awaitingPhase != null && REGREET_REQUIRED_HOLD_PHASES.has(state.awaitingPhase))
221
204
  return false;
222
205
  const taskIsTerminal = !!boundTask && TERMINAL_STATUSES.includes(boundTask.status);
223
- // 任务已退回 verdict 门禁且当前持有者不是本 agent:这次绑定是转码失败的残留
224
- // (fresh-acquire 释放被脏树挡下),Resume 时补完释放,否则 dev 被永久占住
225
206
  const staleCodeHandoff = state.awaitingPhase != null
226
207
  && CHECKOUT_HOLD_PHASES.has(state.awaitingPhase)
227
208
  && !!boundTask
@@ -273,10 +254,8 @@ export class AgentManager {
273
254
  cancelInterruptGuardWaitMs = DEFAULT_DISPATCH_ACK_TIMEOUT_MS + 5_000;
274
255
  markCompleteInFlight = new Set();
275
256
  specVerdictInFlight = new Set();
276
- // 引用计数:cancelTask 与 startCreatedTaskSession 的清理段可合法并存(启动期 Stop),先完成者不得清掉后者的 guard。
277
257
  cancelCleanupInFlight = new Map();
278
258
  deletionInFlight = new Set();
279
- // Diverged ids live in a separate set the DELETE finally never clears: a post-commit replaceConfig throw must fail-stop for the whole process.
280
259
  divergedAgents = new Set();
281
260
  deletionGeneration = new Map();
282
261
  compactInFlight = new Set();
@@ -401,7 +380,6 @@ export class AgentManager {
401
380
  data,
402
381
  });
403
382
  }
404
- // 配置层 intervention(无 agent/task 上下文):如 repo-conflict——离线绕过锁的同 repo 冲突项目。
405
383
  async emitConfigIntervention(projectId, data) {
406
384
  const key = this.configInterventionKey(projectId, data);
407
385
  if (this.configInterventionKeys.has(key))
@@ -487,7 +465,6 @@ export class AgentManager {
487
465
  this.deletionInFlight.add(id);
488
466
  return null;
489
467
  }
490
- // Scan + claim share createTask/dispatchPendingTask's withTaskLock, so a delete can't pass an open-task scan that a concurrent commit then orphans.
491
468
  async scanOpenThenClaimDeletion(targets) {
492
469
  return this.withTaskLock(async () => {
493
470
  for (const id of targets) {
@@ -519,7 +496,6 @@ export class AgentManager {
519
496
  for (const id of agentIds)
520
497
  this.deletionInFlight.delete(id);
521
498
  }
522
- // strict 读取:守卫扫描不能把读失败折叠成「无活跃任务」放行提交
523
499
  async listActiveParticipantSeats() {
524
500
  const tasks = await this.taskStore.listStrict();
525
501
  return tasks
@@ -541,21 +517,18 @@ export class AgentManager {
541
517
  return { taskId: task.id, projectId: task.projectId, participants: [...byId.values()] };
542
518
  });
543
519
  }
544
- // A completed DELETE keeps blocking this id past tombstone release; the process dies with any suspended flow, so no persistence is needed.
545
520
  bumpDeletionGeneration(agentId) {
546
521
  this.deletionGeneration.set(agentId, this.deletionGenerationOf(agentId) + 1);
547
522
  }
548
523
  deletionGenerationOf(agentId) {
549
524
  return this.deletionGeneration.get(agentId) ?? 0;
550
525
  }
551
- // Boolean tombstone alone is ABA-prone (delete→recreate same id); commits must hold both the tombstone-clear and an unchanged generation from entry.
552
526
  deletionGateOpen(agentId, genAtEntry) {
553
527
  return !this.isDeletionInFlight(agentId) && this.deletionGenerationOf(agentId) === genAtEntry;
554
528
  }
555
529
  isDeletionInFlight(agentId) {
556
530
  return this.deletionInFlight.has(agentId) || this.divergedAgents.has(agentId);
557
531
  }
558
- // replaceConfig 抛错后的 fail-stop:进入 divergedAgents(不随 DELETE finally 释放),重建/分配持续被拒直到重启。
559
532
  retainTombstoneForDivergence(agentId) {
560
533
  this.divergedAgents.add(agentId);
561
534
  }
@@ -574,7 +547,6 @@ export class AgentManager {
574
547
  }
575
548
  const runner = this.createRunnerFor(agent);
576
549
  const tmux = new TmuxManager(runner);
577
- // Side-effects run inside the lifecycle section (serialized with DELETE cleanup) behind a re-checked generation gate; the entry gate is only a fast-fail.
578
550
  return this.runUnderSessionLifecycle(agentId, () => this.ensureSessionLocked(tmux, runner, agent, project, agentId, mode, genAtEntry));
579
551
  }
580
552
  async ensureSessionLocked(tmux, runner, agent, project, agentId, mode, genAtEntry) {
@@ -592,7 +564,6 @@ export class AgentManager {
592
564
  catch (err) {
593
565
  throw new EnsureSessionError({ createdSession: false, agentId }, `ensureWorkdir failed: ${err instanceof Error ? err.message : String(err)}`);
594
566
  }
595
- // Gate the Workdir write too: a DELETE→recreate during the slow ensureWorkdir must not persist a stale Workdir onto the new incarnation.
596
567
  assertGenerationOpen();
597
568
  await this.agentStore.update(agentId, (existing) => {
598
569
  if (!existing || !this.deletionGateOpen(agentId, genAtEntry))
@@ -615,7 +586,6 @@ export class AgentManager {
615
586
  throw new EnsureSessionError({ createdSession: false, agentId }, `tmux probe failed: ${err instanceof Error ? err.message : String(err)}; ` +
616
587
  `if tmux is missing, ${tmuxInstallHint(agent.projectId)}`);
617
588
  }
618
- // Re-gate after the probe await: a DELETE may have tombstoned mid-probe, and build/adopt would else create a session cleanup can only reap later.
619
589
  assertGenerationOpen();
620
590
  if (!snapshot) {
621
591
  return this.buildFreshSession(tmux, agent, agentId, workdir, workdir);
@@ -651,11 +621,6 @@ export class AgentManager {
651
621
  const subdir = skillSubdirFor(agent.runtime);
652
622
  const destRoot = `${workdir}/${subdir}`;
653
623
  await this.excludeInjectedSkills(runner, workdir, subdir);
654
- // opencode's skills are model-invoked, not slash commands; the dispatched /<skill> sigil
655
- // reaches the model as message text and it loads the skill from .opencode/skills. We do NOT
656
- // wrap skills as .opencode/commands: opencode expands $ARGUMENTS in a command template and
657
- // would execute a `!`cmd`` / inline a @path embedded in an untrusted task title/description
658
- // before the skill runs. Message-body payloads are not expanded, so this path is injection-safe.
659
624
  await this.runUnderSkillDirLock(this.skillDirLockKey(agent, workdir), async () => {
660
625
  await this.ensureSkillDirSafe(runner, workdir, subdir, [
661
626
  ...this.skillRegistry.names(),
@@ -664,7 +629,6 @@ export class AgentManager {
664
629
  await this.skillRegistry.materialize((path, content) => this.atomicWriteFile(runner, workdir, path, content), destRoot, scope);
665
630
  });
666
631
  }
667
- // A rollback only kills if no adoption bumped the generation since its create.
668
632
  sessionLifecycleChain = new Map();
669
633
  adoptGeneration = new Map();
670
634
  runUnderSessionLifecycle(agentId, fn) {
@@ -678,7 +642,6 @@ export class AgentManager {
678
642
  });
679
643
  return cur;
680
644
  }
681
- // Inside the lifecycle lock, a generation bump since create means handoff — stand down.
682
645
  async rollbackCreatedSession(agentId, partial, reason, opts = {}) {
683
646
  await this.runUnderSessionLifecycle(agentId, async () => {
684
647
  try {
@@ -715,7 +678,6 @@ export class AgentManager {
715
678
  return;
716
679
  }
717
680
  const runner = this.createRunnerFor(cfg);
718
- // Rollback may run before or after the claim write, so accept empty-or-ours.
719
681
  const outcome = await new TmuxManager(runner).killSessionRef(ref, { kind: 'emptyOr', claim: agentId });
720
682
  if (outcome === 'refused') {
721
683
  console.warn(`[AgentManager] ${agentId} rollback (${reason}): tmux generation/claim changed — leaving session ${ref.sessionId} untouched`);
@@ -766,7 +728,7 @@ export class AgentManager {
766
728
  const top = subdir.split('/')[0];
767
729
  const keep = keepNames.map((n) => `! -name ${shellQuote(n)}`).join(' ');
768
730
  const inner =
769
- // canonicalSelfGuard (cd+pwd -P) rejects a rebound MIDDLE ancestor, not just a symlinked leaf, so the find/rm can't escape.
731
+ // Keep the canonical Workdir check and find/rm in one command to fail closed on a rebound ancestor.
770
732
  `${canonicalSelfGuard(workdir)} && ` +
771
733
  `cd ${shellQuote(workdir)} && ` +
772
734
  `for d in ${top} ${subdir}; do if [ -L "$d" ]; then ` +
@@ -795,9 +757,7 @@ export class AgentManager {
795
757
  return tagged !== this.skillRegistry.contentHash(this.agentSkillScope(agentId));
796
758
  }
797
759
  async excludeInjectedSkills(runner, workdir, subdir) {
798
- const inner =
799
- // canonicalSelfGuard (cd+pwd -P) rejects a rebound MIDDLE ancestor too, so the info/exclude write can't escape.
800
- `${canonicalSelfGuard(workdir)} && ` +
760
+ const inner = `${canonicalSelfGuard(workdir)} && ` +
801
761
  `cd ${shellQuote(workdir)} && ` +
802
762
  `if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then echo BX_SKILLS_NON_GIT; exit 0; fi; ` +
803
763
  `tracked="$(git ls-files -- ${shellQuote(`${subdir}/baxian-*`)} 2>/dev/null | head -5)"; ` +
@@ -828,15 +788,12 @@ export class AgentManager {
828
788
  ['mouse', 'on'],
829
789
  ];
830
790
  }
831
- // A name (or even a bare $id) can rebind to a same-name successor mid-flight;
832
- // every option write goes through the identity-checked by-ref batch.
833
791
  async setSessionOptions(tmux, agentId, ref, entries, expectedClaim = agentId) {
834
792
  const outcome = await tmux.setSessionOptionsIfAlive(ref, entries, { expectedClaim });
835
793
  if (outcome === 'gone') {
836
794
  throw new Error(`tmux session ${ref.sessionId} (${agentId}) vanished before its options could be applied`);
837
795
  }
838
796
  }
839
- // Snapshot's claim check is a filter; the returned PaneRef re-proves generation+claim on every op.
840
797
  async resolveClaimedPane(tmux, agentId, expectedPaneId) {
841
798
  const snapshot = await tmux.getSessionSnapshot(agentId);
842
799
  if (!snapshot)
@@ -861,7 +818,6 @@ export class AgentManager {
861
818
  try {
862
819
  createdRef = await tmux.createSession(agentId, sessionDir);
863
820
  createdSession = true;
864
- // First batch writes the claim itself, so it must prove the session is still unclaimed.
865
821
  await this.setSessionOptions(tmux, agentId, createdRef, [
866
822
  ['@baxian-agent-id', agentId],
867
823
  ['@baxian-runtime', agent.runtime],
@@ -1038,7 +994,6 @@ export class AgentManager {
1038
994
  const agent = this.getAgentConfig(agentId);
1039
995
  if (!agent)
1040
996
  return false;
1041
- // Pin the incarnation before reading state so a DELETE→recreate reusing %0 can't route the greeting handshake to the new session.
1042
997
  const genAtEntry = this.deletionGenerationOf(agentId);
1043
998
  const state = await this.agentStore.get(agentId);
1044
999
  if (state?.awaitingPhase !== 'greeting_failed')
@@ -1170,7 +1125,6 @@ export class AgentManager {
1170
1125
  if (!(err instanceof EnsureSessionError) || !err.partial.dialogPending) {
1171
1126
  return false;
1172
1127
  }
1173
- // The error belongs to the caller's incarnation; gate every side-effect so a DELETE→recreate reusing %0 can't write the stale dialog/fail onto the new agent.
1174
1128
  const gateOpen = () => opts.expectedGeneration === undefined || this.deletionGateOpen(agentId, opts.expectedGeneration);
1175
1129
  let state = await this.agentStore.get(agentId);
1176
1130
  if (!state || !gateOpen())
@@ -1440,7 +1394,6 @@ export class AgentManager {
1440
1394
  signal.addEventListener('abort', onAbort, { once: true });
1441
1395
  });
1442
1396
  }
1443
- // Runs inside the caller's lifecycle section (ensureSessionLocked routed the snapshot here).
1444
1397
  async adoptOrRestartSessionLocked(tmux, runner, agent, agentId, workdir, sessionDir, snapshot) {
1445
1398
  const sessionRef = snapshot.ref;
1446
1399
  this.adoptGeneration.set(agentId, (this.adoptGeneration.get(agentId) ?? 0) + 1);
@@ -1582,8 +1535,6 @@ export class AgentManager {
1582
1535
  throw new EnsureSessionError({ createdSession: false, agentId }, `REPL relaunch failed: ${relErr instanceof Error ? relErr.message : String(relErr)}`);
1583
1536
  }
1584
1537
  }
1585
- // Frees a bootstrap blocked on its predecessor's half-created session (nonce
1586
- // env present but claim never written); anything else is left alone.
1587
1538
  async reclaimHalfCreatedSession(tmux, agentId, knownSnapshot) {
1588
1539
  let snapshot;
1589
1540
  try {
@@ -1607,7 +1558,6 @@ export class AgentManager {
1607
1558
  return false;
1608
1559
  let outcome;
1609
1560
  try {
1610
- // claim-still-empty rides the kill itself — a claim racing the probes makes the server refuse.
1611
1561
  outcome = await tmux.killSessionRef(snapshot.ref, { kind: 'unclaimed' });
1612
1562
  }
1613
1563
  catch (err) {
@@ -1642,14 +1592,10 @@ export class AgentManager {
1642
1592
  console.warn(`[AgentManager] ${agentId}: session ${sessionRef.sessionId} already gone (${reason})`);
1643
1593
  }
1644
1594
  }
1645
- // Create-on-null only under this predicate (evaluated inside the store's serialized commit): closes the delete→recreate ABA yet keeps lazy first-allocation for config-only agents.
1646
1595
  bindingInitPredicate(agentId, genAtEntry) {
1647
1596
  return this.deletionGateOpen(agentId, genAtEntry) && this.getAgentConfig(agentId) !== undefined;
1648
1597
  }
1649
- async acquireAgentForTask(agentId, taskId, phase,
1650
- // 锁代必须由 acquire 原子交出:事后重读 claim/binding 的窗口里并发派发方可以
1651
- // release + re-acquire 同一 agent/同一 task,读到的会是 successor 的代
1652
- opts = {}) {
1598
+ async acquireAgentForTask(agentId, taskId, phase, opts = {}) {
1653
1599
  const genAtEntry = this.deletionGenerationOf(agentId);
1654
1600
  if (this.isDeletionInFlight(agentId))
1655
1601
  return false;
@@ -1769,15 +1715,12 @@ export class AgentManager {
1769
1715
  return false;
1770
1716
  }
1771
1717
  }
1772
- // expectedHold 是 hold 世代快照的严格 CAS(全 undefined 表示“期望无 hold”):
1773
- // hold 消失(被并发 Resume 清掉)或换代同样意味着接管,不得继续释放
1774
1718
  const matchesExpectedHold = (binding) => {
1775
1719
  const held = binding.status === 'awaiting_human';
1776
1720
  return (held ? binding.awaitingPhase : undefined) === opts.expectedHold.phase
1777
1721
  && (held ? binding.awaitingSince : undefined) === opts.expectedHold.since
1778
1722
  && (held ? binding.awaitingNonce : undefined) === opts.expectedHold.nonce;
1779
1723
  };
1780
- // 世代门前置:hold 换代时任何 pane/workdir 副作用都不得发生(末尾 CAS 只兜最后窗口)
1781
1724
  if (opts.expectedHold && !matchesExpectedHold(state)) {
1782
1725
  console.warn(`[AgentManager] releaseAgentForTask: agent ${agentId} hold generation changed or cleared before release; refusing`);
1783
1726
  return false;
@@ -1801,7 +1744,6 @@ export class AgentManager {
1801
1744
  if (!latest)
1802
1745
  return AGENT_STORE_NOOP;
1803
1746
  if (opts.clearAwaitingHuman && latest.status === 'awaiting_human') {
1804
- // The hold-generation CAS lives inside the atomic update: a hold written after any pre-read survives.
1805
1747
  if (opts.expectedHold
1806
1748
  && (latest.awaitingPhase !== opts.expectedHold.phase
1807
1749
  || latest.awaitingSince !== opts.expectedHold.since
@@ -1819,8 +1761,6 @@ export class AgentManager {
1819
1761
  ...(latest.workdir !== undefined ? { workdir: latest.workdir } : {}),
1820
1762
  ...(latest.startedAt !== undefined ? { startedAt: latest.startedAt } : {}),
1821
1763
  ...(latest.creationToken !== undefined ? { creationToken: latest.creationToken } : {}),
1822
- // Release voids the open question: strip the watermark, bump the epoch so
1823
- // any in-flight/queued commit of the old generation fences server-side.
1824
1764
  needInput: { epoch: (latest.needInput?.epoch ?? 0) + 1 },
1825
1765
  };
1826
1766
  return cleared;
@@ -1852,7 +1792,6 @@ export class AgentManager {
1852
1792
  || !await this.lockManager.isOwner(agentId, expectedTaskId, lockToken)) {
1853
1793
  return false;
1854
1794
  }
1855
- // 释放中被写入的新 hold(如 ack_unknown)比 cleanup 失败信息更关键,不得覆盖
1856
1795
  if (!holdMatchesExpected(latest)) {
1857
1796
  console.warn(`[AgentManager] releaseAgentForTask: agent ${agentId} hold generation changed during release; not overwriting with branch-cleanup-pending`);
1858
1797
  return false;
@@ -1918,7 +1857,6 @@ export class AgentManager {
1918
1857
  sessionConfirmedAbsent = true;
1919
1858
  }
1920
1859
  await this.assertTaskGeneration(agentId, expectedTaskId, lockToken, releaseWorkdir);
1921
- // 仍重验持久化世代,防护当前进程租约之外的状态写入。
1922
1860
  if (opts.expectedHold) {
1923
1861
  const beforeBranchOps = await this.agentStore.get(agentId);
1924
1862
  if (!beforeBranchOps || !matchesExpectedHold(beforeBranchOps)) {
@@ -1990,9 +1928,6 @@ export class AgentManager {
1990
1928
  await this.assertTaskGeneration(agentId, expectedTaskId, lockToken, releaseWorkdir);
1991
1929
  }
1992
1930
  catch (err) {
1993
- // REPL 忙碌不是清理失败:pane 完好、绑定未动、非 dev 无分支凭据可留。落
1994
- // branch-cleanup-pending(不在可恢复白名单)会把常态忙碌变成永久 hold(#558 验收 1),
1995
- // 故拒绝释放而不落 hold,由调用方按「仍绑定且未 hold」判定重新排队本 pass
1996
1931
  if (opts.deferWhenBusy && err instanceof ReplNotReadyError && cfg.role !== 'dev') {
1997
1932
  console.warn(`[AgentManager] releaseAgentForTask: agent ${agentId} REPL is busy; refusing release without a hold ` +
1998
1933
  `(the pass can be re-queued): ${err.message}`);
@@ -2019,7 +1954,6 @@ export class AgentManager {
2019
1954
  || (state.workdir !== undefined && existing.workdir !== state.workdir)) {
2020
1955
  return AGENT_STORE_NOOP;
2021
1956
  }
2022
- // 世代 CAS 在原子写入内判定:预读后被重写或清除的 hold 都不得被这次释放清掉
2023
1957
  if (opts.expectedHold && !matchesExpectedHold(existing)) {
2024
1958
  holdGenerationChanged = true;
2025
1959
  return AGENT_STORE_NOOP;
@@ -2125,7 +2059,6 @@ export class AgentManager {
2125
2059
  if (candidateTasks.length === 0)
2126
2060
  continue;
2127
2061
  const candidateByBranch = new Map(candidateTasks.map(task => [task.branch, task]));
2128
- // Pin the incarnation before the first state/workdir/refs snapshot so a DELETE→same-id recreate (possibly a different workdir) in the read→gate window is rejected.
2129
2062
  const genAtEntry = this.deletionGenerationOf(cfg.id);
2130
2063
  const state = await this.agentStore.get(cfg.id);
2131
2064
  if (!state || !canDispatchWithBinding(state))
@@ -2224,20 +2157,11 @@ export class AgentManager {
2224
2157
  }
2225
2158
  }
2226
2159
  }
2227
- // ---- need-input watermark: epoch-fenced monotonic persistence + retry convergence ----
2228
2160
  needInputRetry = new Map();
2229
2161
  needInputRetryInFlight = new Set();
2230
2162
  gitReviewRecoveryAlerted = new Set();
2231
- // Watermark writes currently in the store chain. A restore bump snapshots these along
2232
- // with the queue: an answer whose write has not settled yet would otherwise be invisible
2233
- // to the migration and its error-enqueue would land after the arm already cleared the
2234
- // queue — deleting the only proof the user answered.
2235
2163
  needInputWritesInFlight = new Map();
2236
2164
  needInputWriteSeq = 0;
2237
- // Serializes the probe→bump→queue-clear section of concurrent arms for one
2238
- // (agentId, taskId): without it a stale replay probed before a successor's bump can
2239
- // itself bump afterwards, ghost-fencing the installed successor. All locked steps are
2240
- // bounded (memory probe + one store update); subscribe stays outside the lock.
2241
2165
  needInputArmLocks = new Map();
2242
2166
  needInputRetryTimer = null;
2243
2167
  needInputRetryKey(agentId, taskId, epoch) {
@@ -2252,12 +2176,6 @@ export class AgentManager {
2252
2176
  return null;
2253
2177
  return { agentId, taskId, epoch };
2254
2178
  }
2255
- // Standard commit entry: one agentStore.update, NEVER called from inside another
2256
- // updater (the store's per-id chain is non-reentrant).
2257
- // The ledger entry lives until the result is fully handed over — an error is IN the
2258
- // retry queue before deregistration, in the same synchronous continuation. Splitting
2259
- // those two across promise jobs would open a window where neither the ledger nor the
2260
- // queue holds the intent and a concurrent restore snapshot loses the answer.
2261
2179
  async commitNeedInputWatermark(intent) {
2262
2180
  const writeId = ++this.needInputWriteSeq;
2263
2181
  this.needInputWritesInFlight.set(writeId, intent);
@@ -2325,8 +2243,6 @@ export class AgentManager {
2325
2243
  });
2326
2244
  this.ensureNeedInputRetryTimer();
2327
2245
  }
2328
- // ok deletes only when the queued item is still covered by what was written
2329
- // (a concurrent edge may have raised it mid-flight); fenced is terminal either way.
2330
2246
  settleNeedInputRetry(key, wroteAskSeq, wroteAnsweredSeq, result) {
2331
2247
  const cur = this.needInputRetry.get(key);
2332
2248
  if (!cur)
@@ -2411,9 +2327,6 @@ export class AgentManager {
2411
2327
  }
2412
2328
  return owed;
2413
2329
  }
2414
- // Only the generation this arm directly succeeds (bumps are +1 and serialized per key).
2415
- // Anything older was already superseded — a fresh arm in between stripped its question
2416
- // on purpose, and transplanting it would relight a prompt nobody asked.
2417
2330
  collectPredecessorNeedInputDelta(agentId, taskId, epoch) {
2418
2331
  let delta = null;
2419
2332
  const merge = (wm) => {
@@ -2449,14 +2362,7 @@ export class AgentManager {
2449
2362
  });
2450
2363
  return run;
2451
2364
  }
2452
- // 'restore' must also merge queued retry seqs of the CURRENT persisted epoch: after
2453
- // session-gone the queue can hold the only proof an answer happened (the entry is
2454
- // gone, so needInputInherit has nothing to merge), and the arm clears the queue.
2455
2365
  async bumpNeedInputEpochForArm(agentId, taskId, mode) {
2456
- // Contributions are collected INSIDE the updater: it runs when the store chain
2457
- // reaches this bump, so an answer whose commit started while the bump was queued
2458
- // (registered in the ledger, or already error-queued) is still visible. A snapshot
2459
- // taken at call time would miss exactly that window.
2460
2366
  const collectContributions = () => {
2461
2367
  const queued = new Map();
2462
2368
  if (mode !== 'restore')
@@ -2507,16 +2413,10 @@ export class AgentManager {
2507
2413
  }
2508
2414
  catch (err) {
2509
2415
  console.warn(`[AgentManager] needInput epoch bump failed (agent=${agentId} task=${taskId}):`, err);
2510
- // Fall back to the persisted watermark for a restore: without it the entry would
2511
- // start at 0/0 and swallow the answer to a question that is still open in the
2512
- // store, leaving the badge lit forever. No bump means no generation advance —
2513
- // the caller must not treat this as a fresh generation.
2514
2416
  if (mode === 'restore') {
2515
2417
  try {
2516
2418
  const cur = await this.agentStore.get(agentId);
2517
2419
  if (cur?.taskId === taskId && cur.needInput) {
2518
- // Same-generation intents still owed to the store hold questions the
2519
- // persisted watermark has not caught up with; merge them in.
2520
2420
  const owed = this.collectSameEpochNeedInput(agentId, taskId, cur.needInput.epoch);
2521
2421
  return {
2522
2422
  wm: {
@@ -2536,16 +2436,12 @@ export class AgentManager {
2536
2436
  }
2537
2437
  return { wm, bumped: wm !== null };
2538
2438
  }
2539
- // Single entry for arming phase-signal watches — every path must flow through the
2540
- // epoch bump, or stale-generation commits could survive the re-arm.
2541
2439
  async startPhaseSignalWatch(args) {
2542
2440
  if (!this.phaseSignalWatcher)
2543
2441
  return true;
2544
2442
  const { needInputMode, ...rest } = args;
2545
2443
  const mode = needInputMode ?? 'restore';
2546
2444
  const watcher = this.phaseSignalWatcher;
2547
- // Claim ownership before bumping: a rejected arm must not bump (it would fence the
2548
- // owner), and the claim keeps this arm visible to later probes while it subscribes.
2549
2445
  const gateResult = await this.withNeedInputArmLock(args.agentId, args.taskId, async () => {
2550
2446
  const claim = watcher.claimArm?.({
2551
2447
  taskId: args.taskId,
@@ -2558,8 +2454,6 @@ export class AgentManager {
2558
2454
  if (claim === null)
2559
2455
  return 'rejected';
2560
2456
  const bump = await this.bumpNeedInputEpochForArm(args.agentId, args.taskId, mode);
2561
- // Only a real generation advance may drop the queue: same-generation intents
2562
- // (bump failed, watermark read back) are still owed to the store.
2563
2457
  if (bump.bumped)
2564
2458
  this.clearNeedInputRetryFor(args.agentId, args.taskId);
2565
2459
  return { ...bump, claim };
@@ -2567,8 +2461,6 @@ export class AgentManager {
2567
2461
  if (gateResult === 'rejected')
2568
2462
  return false;
2569
2463
  const { wm, bumped, claim } = gateResult;
2570
- // Same synchronous section as the bump result: old-generation answers must not slip
2571
- // between the bump and the handoff (start() migrates the predecessor synchronously).
2572
2464
  if (wm !== null && bumped && mode === 'restore') {
2573
2465
  const delta = this.collectPredecessorNeedInputDelta(args.agentId, args.taskId, wm.epoch);
2574
2466
  if (delta) {
@@ -2581,14 +2473,10 @@ export class AgentManager {
2581
2473
  });
2582
2474
  }
2583
2475
  }
2584
- // wm === null (no binding / store error): arm anyway with badge commits disabled —
2585
- // the badge is display-only, the signal watch drives task progress.
2586
2476
  try {
2587
2477
  return await watcher.start({
2588
2478
  ...rest,
2589
2479
  ...(claim != null ? { armClaimId: claim } : {}),
2590
- // Inherit is about restore semantics, not about having a generation: a degraded
2591
- // restore still has to keep the predecessor's pending question in memory.
2592
2480
  needInputInherit: mode === 'restore',
2593
2481
  ...(wm !== null ? { needInput: wm } : {}),
2594
2482
  });
@@ -2597,10 +2485,6 @@ export class AgentManager {
2597
2485
  watcher.releaseArm?.(claim);
2598
2486
  }
2599
2487
  }
2600
- // Web-fallback confirm: settles the newest known question (store ∪ retry queue) as
2601
- // answered in ONE store update — usable when no watch entry exists (pre-recovery,
2602
- // arm failure, post-stop). Queue contributions are snapshotted synchronously; the
2603
- // updater derives everything else from `existing`, so no nested store calls.
2604
2488
  async confirmNeedInputAnswered(agentId) {
2605
2489
  const queueAsk = new Map();
2606
2490
  for (const [key, wm] of this.needInputRetry) {
@@ -2654,15 +2538,7 @@ export class AgentManager {
2654
2538
  this.settleNeedInputRetry(this.needInputRetryKey(agentId, s.taskId, s.epoch), s.askSeq, s.askSeq, 'ok');
2655
2539
  }
2656
2540
  }
2657
- // The user typed into the agent's terminal — its open question counts as answered.
2658
- // The store fallback runs only when no watch entry exists (pre-recovery, arm failure,
2659
- // post-stop): with a live entry it would race the entry's in-flight writes and could
2660
- // confirm an ask that arrived after this input.
2661
2541
  async notifyHumanTerminalInput(agentId) {
2662
- // Rearm first: it settles entry state synchronously, so a question printed during
2663
- // this call cannot be confirmed by mistake. The binding read only decides whether a
2664
- // watcher of the CURRENT task took the input — a leftover entry of a finished task
2665
- // must not suppress the store fallback.
2666
2542
  const handled = await this.phaseSignalWatcher?.rearmNeedInput(agentId);
2667
2543
  let taskId;
2668
2544
  try {
@@ -2813,11 +2689,8 @@ export class AgentManager {
2813
2689
  }
2814
2690
  if ((state.awaitingPhase === 'dirty-workdir' || state.awaitingPhase === 'checkout-preparation-failed')
2815
2691
  && boundTask && ACTIVE_TASK_STATUSES.has(boundTask.status)
2816
- // verdict 门禁状态没有在途派发:清掉 hold 后由 spec verdict 重新走完整转码,Resume 放行
2817
2692
  && boundTask.status !== 'spec-ready' && boundTask.status !== 'max_rounds') {
2818
2693
  if (state.taskId && boundTask.qaAgentId === agentId) {
2819
- // 停驻的 review pass 已死(任务离开 review):重派会伪造新 pass,只清 hold 并释放绑定;
2820
- // 快照 hold/task 世代供释放路径 CAS,且不得借用 cancel-cleanup 的绕行凭据
2821
2694
  if (boundTask.status !== 'review') {
2822
2695
  return {
2823
2696
  resumed: true,
@@ -2992,8 +2865,6 @@ export class AgentManager {
2992
2865
  await this.markAwaitingHuman(agentId, 'code-dispatch-failed', reason, { expectedTaskId: result.redispatchCodeTaskId }).catch(() => undefined);
2993
2866
  return { resumed: false, releasedBinding: false, reason };
2994
2867
  }
2995
- // 重派与首派同样不经 startSession 的 post-ack finalize:残留的 bootstrap 标记
2996
- // 会让下一次 recover 把已送达的 code prompt 再判成未送达
2997
2868
  const lockToken = (await this.agentStore.get(agentId))?.lockToken;
2998
2869
  await this.finalizeReplayedBootstrap(agentId, result.redispatchCodeTaskId, 'code', lockToken);
2999
2870
  }
@@ -3055,7 +2926,6 @@ export class AgentManager {
3055
2926
  console.error(`[AgentManager] resumeAgent review redispatch failed for ${agentId}:`, err);
3056
2927
  const reason = `QA review redispatch on Resume failed: ${err instanceof Error ? err.message : String(err)}`;
3057
2928
  const reviewTaskId = result.redispatchReviewTaskId;
3058
- // 锁代未变(未被 successor re-acquire)才允许补挂;markAwaitingHuman 内的 CAS 原子复核同一条件
3059
2929
  const restoreHoldIfStillOurs = async (phase, holdReason) => {
3060
2930
  const after = await this.agentStore.get(agentId);
3061
2931
  if (after?.taskId === reviewTaskId
@@ -3067,7 +2937,6 @@ export class AgentManager {
3067
2937
  });
3068
2938
  }
3069
2939
  };
3070
- // handled 只代表 startSession 已尝试落 hold;落库可能已失败,必须核实并按需补挂
3071
2940
  if (err instanceof EnsureSessionError && err.partial.handled) {
3072
2941
  try {
3073
2942
  await restoreHoldIfStillOurs(result.previousPhase ?? 'checkout-preparation-failed', reason);
@@ -3083,7 +2952,6 @@ export class AgentManager {
3083
2952
  };
3084
2953
  }
3085
2954
  }
3086
- // ack_unknown:prompt 可能已在跑,必须恢复 ack hold 的可见性,而非按普通失败补挂 checkout hold
3087
2955
  if (err instanceof DispatchTerminalError && err.reason === 'ack_unknown') {
3088
2956
  try {
3089
2957
  await restoreHoldIfStillOurs('dispatch-failed:ack_unknown', `${err.message}. Prompt may still be running in the pane; verify before resuming.`);
@@ -3099,7 +2967,6 @@ export class AgentManager {
3099
2967
  };
3100
2968
  }
3101
2969
  }
3102
- // pass 被接管(token 轮换或任务离开 review)说明该 pass 已死,补挂会把新 pass 的 QA 打成 hold
3103
2970
  let passSuperseded = false;
3104
2971
  let recoveryReadFailed = false;
3105
2972
  try {
@@ -3114,7 +2981,6 @@ export class AgentManager {
3114
2981
  return { resumed: false, releasedBinding: false, reason };
3115
2982
  try {
3116
2983
  if (recoveryReadFailed) {
3117
- // 读失败时无法证明接管,优先恢复可见性:锁代/绑定 CAS 全部由 markAwaitingHuman 原子判定
3118
2984
  await this.markAwaitingHuman(agentId, result.previousPhase ?? 'checkout-preparation-failed', reason, {
3119
2985
  expectedTaskId: reviewTaskId,
3120
2986
  ...(recoveryLockToken !== undefined ? { expectedLockToken: recoveryLockToken } : {}),
@@ -3391,7 +3257,6 @@ export class AgentManager {
3391
3257
  const out = [];
3392
3258
  for (const t of tasks) {
3393
3259
  const bound = t.agentId === agentId || t.qaAgentId === agentId;
3394
- // spec-ready 不豁免:approve/打回都依赖 dev Workdir,dev 丢失后停驻只是假活
3395
3260
  if (t.status === 'merge-ready')
3396
3261
  continue;
3397
3262
  if (ACTIVE_TASK_STATUSES.has(t.status) && bound) {
@@ -3456,7 +3321,6 @@ export class AgentManager {
3456
3321
  return last;
3457
3322
  }
3458
3323
  async restartReplOnly(agentId, opts = {}) {
3459
- // Re-verify the entry generation so a request that slept across a DELETE→recreate can't restart the NEW incarnation's REPL (claim check alone can't tell).
3460
3324
  const genAtEntry = opts.expectedGeneration ?? this.deletionGenerationOf(agentId);
3461
3325
  if (!this.deletionGateOpen(agentId, genAtEntry)) {
3462
3326
  throw new Error(`restart-repl: agent ${agentId} is being deleted or was recreated; aborting`);
@@ -3491,7 +3355,6 @@ export class AgentManager {
3491
3355
  const project = this.getProjectConfig(cfg.projectId);
3492
3356
  if (!project)
3493
3357
  throw new Error(`restart-repl: project ${cfg.projectId} does not exist`);
3494
- // Re-verify before Workdir/skill/session-option side-effects: pane writes fail-closed on a stale PaneRef, but ensureWorkdir/skills would write old-cfg files under a recreated id.
3495
3358
  if (!this.deletionGateOpen(agentId, genAtEntry)) {
3496
3359
  throw new Error(`restart-repl: agent ${agentId} is being deleted or was recreated; aborting`);
3497
3360
  }
@@ -3520,7 +3383,6 @@ export class AgentManager {
3520
3383
  await this.tagSessionSkillsVersion(tmux, agentId, snapshot.ref);
3521
3384
  };
3522
3385
  await this.runUnderSkillDirLock(this.skillDirLockKey(cfg, workdir), relaunch);
3523
- // Final writeback is generation-gated too, so a DELETE→recreate reusing %0 can't persist the old incarnation's paneId onto the new agent.
3524
3386
  await this.agentStore.update(agentId, (state) => {
3525
3387
  if (!state || !this.deletionGateOpen(agentId, genAtEntry))
3526
3388
  return AGENT_STORE_NOOP;
@@ -3531,7 +3393,6 @@ export class AgentManager {
3531
3393
  };
3532
3394
  });
3533
3395
  }
3534
- // Holds only the pass captured in the task tuple: a successor pass (rotated token/phase/status) is never overwritten.
3535
3396
  async holdReplayFailureIfCurrent(agentId, taskAtEntry, holdPhase, reason, entryHold, extraCurrent) {
3536
3397
  return this.withTaskLock(async () => {
3537
3398
  const fresh = await this.taskStore.get(taskAtEntry.id);
@@ -3547,7 +3408,6 @@ export class AgentManager {
3547
3408
  expectedHold: entryHold,
3548
3409
  }))
3549
3410
  return true;
3550
- // Entry hold gone (Resume raced): write over the empty generation, never over a foreign hold.
3551
3411
  if (await this.markAwaitingHuman(agentId, holdPhase, reason, {
3552
3412
  expectedTaskId: taskAtEntry.id,
3553
3413
  expectedHold: { phase: undefined, since: undefined, nonce: undefined },
@@ -3678,14 +3538,11 @@ export class AgentManager {
3678
3538
  retireOldOnce();
3679
3539
  }
3680
3540
  }
3681
- async redispatchTaskPromptAfterReplRestart(agentId, taskId, expected) {
3541
+ async redispatchTaskPromptAfterReplRestart(agentId, taskId) {
3682
3542
  const agent = this.getAgentConfig(agentId);
3683
3543
  const loadedTask = await this.taskStore.get(taskId);
3684
3544
  if (!agent || !loadedTask || !TASK_OWNER_ROLES.has(agent.role))
3685
3545
  return false;
3686
- if (expected && !taskMatchesGeneration(loadedTask, expected))
3687
- return false;
3688
- const rootGenerationMatches = (fresh, signalToken = expected?.signalToken) => !expected || taskMatchesGeneration(fresh, { ...expected, signalToken });
3689
3546
  const task = loadedTask;
3690
3547
  if (task.agentId !== agentId)
3691
3548
  return false;
@@ -3702,8 +3559,7 @@ export class AgentManager {
3702
3559
  return fresh?.agentId === agentId
3703
3560
  && fresh.status === task.status
3704
3561
  && fresh.phase === task.phase
3705
- && fresh.signalToken === signalToken
3706
- && rootGenerationMatches(fresh, signalToken);
3562
+ && fresh.signalToken === signalToken;
3707
3563
  };
3708
3564
  const clearEntryHold = async () => this.withTaskLock(async () => {
3709
3565
  if (!(await taskStillCurrent()))
@@ -3714,10 +3570,10 @@ export class AgentManager {
3714
3570
  }
3715
3571
  return this.clearAwaitingHuman(agentId, { expectedHold: entryHold });
3716
3572
  });
3717
- const holdReplayFailure = (phase, reason) => this.holdReplayFailureIfCurrent(agentId, task, phase, reason, entryHold, async (fresh) => rootGenerationMatches(fresh, task.signalToken));
3573
+ const holdReplayFailure = (phase, reason) => this.holdReplayFailureIfCurrent(agentId, task, phase, reason, entryHold);
3718
3574
  try {
3719
3575
  if (task.status === 'approved' && agent.role === 'dev') {
3720
- return await this.replayApprovedHolderAfterReplRestart(agentId, task, entryHold, { clearEntryHold, holdReplayFailure, rootGenerationMatches });
3576
+ return await this.replayApprovedHolderAfterReplRestart(agentId, task, entryHold, { clearEntryHold, holdReplayFailure });
3721
3577
  }
3722
3578
  const mapped = this.mapTaskStateToExpectedWatcher(task);
3723
3579
  if (!mapped || mapped.agentId !== agentId)
@@ -3736,8 +3592,7 @@ export class AgentManager {
3736
3592
  if (fresh?.agentId !== agentId
3737
3593
  || fresh.status !== task.status
3738
3594
  || fresh.phase !== task.phase
3739
- || fresh.signalToken !== signalToken
3740
- || !rootGenerationMatches(fresh, signalToken))
3595
+ || fresh.signalToken !== signalToken)
3741
3596
  return false;
3742
3597
  const rotatePendingPr = mapped.expectedKinds.includes('pr-created')
3743
3598
  && fresh.pendingPrSignalToken === signalToken;
@@ -3750,7 +3605,7 @@ export class AgentManager {
3750
3605
  return true;
3751
3606
  },
3752
3607
  stillCurrent: taskStillCurrent,
3753
- holdFailure: (newToken, reason) => this.holdReplayFailureIfCurrent(agentId, { ...task, signalToken: newToken }, 'restart-redispatch-failed', reason, entryHold, async (fresh) => rootGenerationMatches(fresh, newToken)),
3608
+ holdFailure: (newToken, reason) => this.holdReplayFailureIfCurrent(agentId, { ...task, signalToken: newToken }, 'restart-redispatch-failed', reason, entryHold),
3754
3609
  continueWith: (newToken, fence) => this.continueSession(taskId, agentId, phase, {
3755
3610
  ...opts,
3756
3611
  signalToken: newToken,
@@ -3773,7 +3628,6 @@ export class AgentManager {
3773
3628
  });
3774
3629
  };
3775
3630
  if (task.status === 'in_progress' && task.phase === undefined && agent.role === 'dev') {
3776
- // An undelivered bootstrap never started work, so the fresh-dispatch clean gate still applies.
3777
3631
  const delivered = (await this.agentStore.get(agentId))?.bootstrappingTaskId !== taskId;
3778
3632
  return await replayInitialBootstrap('develop', {
3779
3633
  ...(delivered ? { allowDirtyWorkdir: true } : {}),
@@ -3796,7 +3650,6 @@ export class AgentManager {
3796
3650
  ...(agent.role === 'dev' ? { allowDirtyWorkdir: true } : {}),
3797
3651
  });
3798
3652
  }
3799
- // A direct (no-SDD) pass never persists a phase, so fixing + undefined is the plain code-feedback round.
3800
3653
  if (task.status === 'fixing'
3801
3654
  && (task.phase === 'code' || task.phase === undefined)
3802
3655
  && agent.role === 'dev') {
@@ -3809,7 +3662,6 @@ export class AgentManager {
3809
3662
  return false;
3810
3663
  }
3811
3664
  catch (err) {
3812
- // A post-entry throw may land after clearEntryHold; only a tuple-checked hold on the live generation is safe.
3813
3665
  const message = err instanceof Error ? err.message : String(err);
3814
3666
  if (await holdReplayFailure('restart-redispatch-failed', `REPL restarted but replaying the task prompt failed: ${message}. Resume to retry, or cancel the task.`)) {
3815
3667
  return true;
@@ -3817,49 +3669,9 @@ export class AgentManager {
3817
3669
  throw err;
3818
3670
  }
3819
3671
  }
3820
- async redispatchCurrentTaskPhase(taskId, expected) {
3821
- const current = await this.taskStore.get(taskId);
3822
- if (!current || !taskMatchesGeneration(current, expected))
3823
- return 'stale';
3824
- if (current.status === 'review') {
3825
- let changedByRedispatch = false;
3826
- try {
3827
- await this.dispatchReviewToQa(taskId, {
3828
- fromStatus: ['review'],
3829
- bumpRound: false,
3830
- expectSignalToken: current.signalToken,
3831
- expectPhase: current.phase,
3832
- expectedTask: expected,
3833
- onSideEffect: () => { changedByRedispatch = true; },
3834
- });
3835
- return 'dispatched';
3836
- }
3837
- catch (err) {
3838
- if (!changedByRedispatch && isBenignDispatchConflict(err))
3839
- return 'stale';
3840
- if (!changedByRedispatch
3841
- && err instanceof ApiError
3842
- && err.code === 'dispatch-unsupported') {
3843
- return 'unsupported';
3844
- }
3845
- if (changedByRedispatch)
3846
- throw err;
3847
- const fresh = await this.taskStore.get(taskId);
3848
- if (!fresh || !taskMatchesGeneration(fresh, expected))
3849
- return 'stale';
3850
- throw err;
3851
- }
3852
- }
3853
- const redispatched = await this.redispatchTaskPromptAfterReplRestart(current.agentId, taskId, expected);
3854
- if (redispatched)
3855
- return 'dispatched';
3856
- const fresh = await this.taskStore.get(taskId);
3857
- return !fresh || !taskMatchesGeneration(fresh, expected) ? 'stale' : 'unsupported';
3858
- }
3859
- // approved hosts the post-approve holder pass.
3860
3672
  async replayApprovedHolderAfterReplRestart(agentId, task, entryHold, holdGen) {
3861
3673
  const taskId = task.id;
3862
- const { clearEntryHold, holdReplayFailure, rootGenerationMatches } = holdGen;
3674
+ const { clearEntryHold, holdReplayFailure } = holdGen;
3863
3675
  if (task.postApproveRevoked) {
3864
3676
  return holdReplayFailure('restart-redispatch-failed', `The post-approve completion was deliberately revoked (${task.postApproveRevoked.reason}); ` +
3865
3677
  'the block stays effective, so the pass is not replayed. Re-request changes to restart the review round, or cancel the task.');
@@ -3871,7 +3683,6 @@ export class AgentManager {
3871
3683
  return holdReplayFailure('restart-redispatch-failed', 'REPL restarted during the post-approve pass but the stored completion cannot be tied to a persisted approved head; re-request the review verdict or cancel the task.');
3872
3684
  }
3873
3685
  if (stored.approvedHeadSha !== task.postApproveHeadSha) {
3874
- // Provably a dead episode's residue: retire it by token, then rebuild for the current head.
3875
3686
  await this.clearPostApproveCompletionIfMatches(taskId, { generation: stored.generation, token: stored.token, headSha: stored.approvedHeadSha });
3876
3687
  completion = await this.holdIncompleteGitPostApproveEpisode(holdReplayFailure);
3877
3688
  }
@@ -3891,7 +3702,6 @@ export class AgentManager {
3891
3702
  && fresh.status === task.status
3892
3703
  && fresh.phase === task.phase
3893
3704
  && fresh.signalToken === task.signalToken
3894
- && rootGenerationMatches(fresh, task.signalToken)
3895
3705
  && fresh.postApproveRevoked === undefined
3896
3706
  && fresh.postApproveHeadSha === task.postApproveHeadSha
3897
3707
  && fresh.postApproveGeneration === task.postApproveGeneration
@@ -3963,7 +3773,6 @@ export class AgentManager {
3963
3773
  continue;
3964
3774
  const runner = this.createRunnerFor(cfg);
3965
3775
  const tmux = new TmuxManager(runner);
3966
- // No releaseAgentForTask: the binding dies with the state file in phase 3, and deletability was already enforced by the phase-1 API gates — so cleanup only stops watchers and kills the session.
3967
3776
  this.stopRuntimeMenuWatch(id);
3968
3777
  if (this.paneStreamerManager) {
3969
3778
  try {
@@ -3990,7 +3799,6 @@ export class AgentManager {
3990
3799
  return;
3991
3800
  }
3992
3801
  if (snapshot.claim === null) {
3993
- // A nonce-owned unclaimed session is this project's own half-created leftover — reclaim it.
3994
3802
  let nonceOwned;
3995
3803
  try {
3996
3804
  nonceOwned = await tmux.hasCreationNonce(id, cleanupOpts);
@@ -4143,8 +3951,6 @@ export class AgentManager {
4143
3951
  const task = await this.taskStore.get(taskId);
4144
3952
  return task ? this.completionOf(task) : null;
4145
3953
  }
4146
- // completion 与效果字段同居 TaskState:consumedFeedback/pendingRedispatch 与 token CAS
4147
- // 必须同一次持久化(spec §6 双写协议)。
4148
3954
  completionOf(task) {
4149
3955
  if (task.postApproveToken === undefined || task.postApproveHeadSha === undefined
4150
3956
  || task.postApproveGeneration === undefined || task.postApprovePhase === undefined)
@@ -4268,7 +4074,6 @@ export class AgentManager {
4268
4074
  this.phaseSignalWatcher?.stop(taskId);
4269
4075
  });
4270
4076
  }
4271
- // CAS for an existing pass: never creates a record nor touches the marker, so it cannot resurrect a revoked completion.
4272
4077
  async updatePostApproveCompletionIfToken(taskId, expectedToken, patch, opts = {}) {
4273
4078
  return this.withTaskLock(async () => {
4274
4079
  const task = await this.taskStore.get(taskId);
@@ -4289,13 +4094,11 @@ export class AgentManager {
4289
4094
  return true;
4290
4095
  });
4291
4096
  }
4292
- // Deliberate clear: the persisted marker keeps a restart replay from rebuilding the completion and bypassing the block.
4293
4097
  async revokePostApproveCompletion(taskId, reason, opts = {}) {
4294
4098
  return this.withTaskLock(async () => {
4295
4099
  const task = await this.taskStore.get(taskId);
4296
4100
  if (!task || task.status !== 'approved')
4297
4101
  return false;
4298
- // Head binds the revoke to one approved episode; an unpersisted head cannot disprove it, so blocking stays fail closed.
4299
4102
  if (opts.expectedHeadSha !== undefined
4300
4103
  && task.postApproveHeadSha !== undefined
4301
4104
  && task.postApproveHeadSha !== opts.expectedHeadSha)
@@ -4333,7 +4136,6 @@ export class AgentManager {
4333
4136
  return true;
4334
4137
  });
4335
4138
  }
4336
- // Marker/token/head/pending are re-checked inside the committing critical section, so a concurrent revoke never loses.
4337
4139
  async completeApprovedPassToMergeReady(taskId, expected) {
4338
4140
  return this.withTaskLock(async () => {
4339
4141
  const task = await this.taskStore.get(taskId);
@@ -4498,8 +4300,6 @@ export class AgentManager {
4498
4300
  headSha: completion.approvedHeadSha,
4499
4301
  });
4500
4302
  }
4501
- // 启动期按「未引用」跳过 skill 池的插件,被热配置首次引用时同步重扫;仍不可物化即拒绝提交——
4502
- // 可解析但物化为空的 tool 会让派发要求加载一个不存在的 skill。
4503
4303
  async ensurePluginSkillPools(next) {
4504
4304
  if (!this.pluginRegistry)
4505
4305
  return;
@@ -4508,7 +4308,6 @@ export class AgentManager {
4508
4308
  if (tool === undefined)
4509
4309
  continue;
4510
4310
  const plugin = this.pluginRegistry.resolveTool(tool);
4511
- // 静默放行会存下一个「无 driver、无平台 skill、重启即 fatal」的配置(启动期同判据)
4512
4311
  if (plugin === undefined) {
4513
4312
  throw new Error(`project '${project.id}': no git-driver plugin provides tool '${tool}' — install it under ~/.baxian/plugins/ first`);
4514
4313
  }
@@ -4550,7 +4349,6 @@ export class AgentManager {
4550
4349
  && task.failToken === lease.failToken
4551
4350
  && lease.effectiveRound === effectiveTaskReviewRound(task);
4552
4351
  }
4553
- // startSession 按 agent 的 projectId 解析 repo/workdir,项目归属不入判会跨仓库检出
4554
4352
  gitReviewQaConfigUsable(task) {
4555
4353
  const qaAgentId = requireTaskQaAgentId(task, 'gitReviewQaConfigUsable');
4556
4354
  const config = this.getAgentConfig(qaAgentId);
@@ -4935,7 +4733,6 @@ export class AgentManager {
4935
4733
  continue;
4936
4734
  const qaAgentId = requireTaskQaAgentId(task, 'retryPendingGitReviewDispatches');
4937
4735
  if (!this.gitReviewQaConfigUsable(task)) {
4938
- // 快照可能已过期:告警前 live 复核;按 lease 代际告警一次,emit 成功才落闩
4939
4736
  const latch = `qa-config:${task.id}:${lease.generation}`;
4940
4737
  if (!this.gitReviewRecoveryAlerted.has(latch)) {
4941
4738
  try {
@@ -5109,7 +4906,6 @@ export class AgentManager {
5109
4906
  const tasks = await this.taskStore.list({ status: 'approved' });
5110
4907
  for (const task of tasks) {
5111
4908
  if (task.postApproveRevoked) {
5112
- // Finish an interrupted revoke: the marker is authoritative, the leftover record must not re-arm.
5113
4909
  try {
5114
4910
  if (task.postApproveToken !== undefined) {
5115
4911
  await this.taskStore.set({
@@ -5230,7 +5026,6 @@ export class AgentManager {
5230
5026
  }
5231
5027
  }
5232
5028
  }
5233
- // 创建期快照(spec §4):git 任务在建任务那一刻定身份。
5234
5029
  platformBindingFields(projectId) {
5235
5030
  return { platformBinding: this.platformBindingFor(projectId) };
5236
5031
  }
@@ -5247,7 +5042,6 @@ export class AgentManager {
5247
5042
  tool,
5248
5043
  };
5249
5044
  }
5250
- // agent 面一律 PATH 解析 tool;server 面 binary/env 不进入该轨。
5251
5045
  agentGitPreflightContext(projectId) {
5252
5046
  const project = this.getProjectConfig(projectId);
5253
5047
  if (!project || !this.pluginRegistry)
@@ -5268,8 +5062,6 @@ export class AgentManager {
5268
5062
  driverFor: (exec) => new GitDriver(plugin, renderCtx, exec),
5269
5063
  };
5270
5064
  }
5271
- // cli 字段族每次派发实时渲染,不做创建时快照。渲染前按持久化 binding 校验身份——
5272
- // 离线漂移的配置会把旧任务的 PR 号/分支/令牌带到新仓库,与平台操作同一守卫。
5273
5065
  platformCliContextOf(task) {
5274
5066
  this.assertPlatformBinding(task);
5275
5067
  const project = this.getProjectConfig(task.projectId);
@@ -5287,13 +5079,11 @@ export class AgentManager {
5287
5079
  ...(project.gitCli?.notes !== undefined ? { notes: project.gitCli.notes } : {}),
5288
5080
  };
5289
5081
  }
5290
- // base 快照一经持久化不可变;无快照保持缺省,由 agent 现场显式查询。
5291
5082
  async ensureGitBaseSnapshot(task, phase) {
5292
5083
  if (phase !== 'develop' && phase !== 'code')
5293
5084
  return task;
5294
5085
  if (task.baseBranch !== undefined)
5295
5086
  return task;
5296
- // 读默认分支与落快照都以 binding 为前提:失配任务先写入新仓库的 base 会永久污染不可变快照
5297
5087
  this.assertPlatformBinding(task);
5298
5088
  const base = this.platformDefaultBranchOf?.(task.projectId);
5299
5089
  if (base === undefined)
@@ -5339,7 +5129,6 @@ export class AgentManager {
5339
5129
  throw new Error(`no git driver resolvable for project ${task.projectId}`);
5340
5130
  return { task, driver };
5341
5131
  }
5342
- // 配置锁也覆盖终态任务尚未完成的远端清理;否则 cancel 落 intent 后即可改写/删除其 binding。
5343
5132
  async listActiveGitTasks(projectId) {
5344
5133
  const tasks = await this.taskStore.listStrict(projectId !== undefined ? { projectId } : undefined);
5345
5134
  return tasks.filter(t => (t.remoteCleanup !== undefined && t.remoteCleanup.stage !== 'manual')
@@ -5352,8 +5141,6 @@ export class AgentManager {
5352
5141
  const [row] = await driver.runOp('prView', { prNumber: task.prNumber });
5353
5142
  return row;
5354
5143
  }
5355
- // git 模式的 head 刷新一律经任务解析出的 driver(spec §4 两执行面):QA checkout 的
5356
- // moved-head fallback 不得旁路到硬编码 gh,否则自定义/非 github driver 会错误调用 gh。
5357
5144
  async platformFetchPrHeadSha(taskId) {
5358
5145
  const headSha = String((await this.platformFetchPrView(taskId)).headSha);
5359
5146
  if (!PLATFORM_HEAD_SHA_RE.test(headSha)) {
@@ -5997,8 +5784,6 @@ export class AgentManager {
5997
5784
  return this.repoCache;
5998
5785
  }
5999
5786
  async rollbackFailedDispatch(taskId, agentId, reason, expectedLockToken) {
6000
- // Pin the incarnation: task rollback / intervention below await, and a DELETE→same-id recreate in that
6001
- // window must not let the final state write-back resurrect the deleted agent.
6002
5787
  const genAtEntry = this.deletionGenerationOf(agentId);
6003
5788
  const existing = await this.agentStore.get(agentId);
6004
5789
  if (existing && existing.taskId !== undefined && existing.taskId !== taskId) {
@@ -6046,8 +5831,6 @@ export class AgentManager {
6046
5831
  else {
6047
5832
  const now = new Date().toISOString();
6048
5833
  await this.agentStore.update(agentId, (latest) => {
6049
- // Never resurrect a deleted/recreated incarnation: if the state is gone or the generation moved
6050
- // since entry, this rollback is stale — clear nothing rather than rebuild from the old snapshot.
6051
5834
  if (!latest || !this.deletionGateOpen(agentId, genAtEntry))
6052
5835
  return AGENT_STORE_NOOP;
6053
5836
  if (latest.taskId === taskId && latest.lockToken !== expectedLockToken)
@@ -6145,7 +5928,6 @@ export class AgentManager {
6145
5928
  };
6146
5929
  return this.persistQueuedTask(unassigned, 'unassigned');
6147
5930
  }
6148
- // Capture generation before the first config/group read so a DELETE→same-id recreate in the read→commit window fails the gate.
6149
5931
  const genAtEntry = this.deletionGenerationOf(input.preferredAgentId);
6150
5932
  const target = await this.pickAgent(projectId, input.preferredAgentId);
6151
5933
  const group = this.findAgentGroup(input.preferredAgentId);
@@ -6155,7 +5937,6 @@ export class AgentManager {
6155
5937
  const qa = group?.find(agent => agent.role === 'qa');
6156
5938
  if (!qa)
6157
5939
  throw new ApiError(409, `Agent ${input.preferredAgentId} has no qa agent in its group`);
6158
- // Pin EVERY participant's generation: a member (e.g. QA) deleted→reintroduced after the snapshot leaves isDeletionInFlight false and the preferred generation unchanged, yet the task would still carry its stale id.
6159
5940
  const participantGens = new Map([dev.id, qa.id]
6160
5941
  .map(id => [id, this.deletionGenerationOf(id)]));
6161
5942
  const participantsFresh = () => this.deletionGenerationOf(input.preferredAgentId) === genAtEntry
@@ -6168,8 +5949,6 @@ export class AgentManager {
6168
5949
  qaAgentId: qa.id,
6169
5950
  status: 'pending',
6170
5951
  };
6171
- // A DELETE→recreate of the preferred OR any participant after the group snapshot must not persist a
6172
- // pending task carrying its stale devAgentId/qaAgentId; reject and let the client re-select.
6173
5952
  const persistQueuedOrReject = (reason) => {
6174
5953
  if (!participantsFresh()) {
6175
5954
  throw new ApiError(409, `Agent ${input.preferredAgentId} or a group member is being deleted or recreated during task creation; please retry`);
@@ -6186,7 +5965,6 @@ export class AgentManager {
6186
5965
  if (!lockToken) {
6187
5966
  return persistQueuedOrReject('agent_locked');
6188
5967
  }
6189
- // Three-step transaction (queued task → binding → in_progress): any failure leaves at worst a queued task and/or a stale binding to it, both reclaimable — never an active task with no binding.
6190
5968
  const releaseLock = async () => {
6191
5969
  try {
6192
5970
  await this.lockManager.releaseIfOwner(target.id, taskId, lockToken);
@@ -6219,7 +5997,6 @@ export class AgentManager {
6219
5997
  ...(existing?.paneId !== undefined ? { paneId: existing.paneId } : {}),
6220
5998
  ...(existing?.workdir !== undefined ? { workdir: existing.workdir } : {}),
6221
5999
  ...(existing?.creationToken !== undefined ? { creationToken: existing.creationToken } : {}),
6222
- // A new task never inherits the previous one's question: void the watermark.
6223
6000
  needInput: { epoch: (existing?.needInput?.epoch ?? 0) + 1 },
6224
6001
  };
6225
6002
  });
@@ -6232,7 +6009,6 @@ export class AgentManager {
6232
6009
  await releaseLock();
6233
6010
  return (await this.taskStore.get(taskId)) ?? queued;
6234
6011
  }
6235
- // Binding-before-active: a DELETE slipping in after the binding commit sees a still-'pending' task and can rotate the lock; re-verify lock ownership + preferred/participant generations before the active write, else roll back.
6236
6012
  if (this.isDeletionInFlight(target.id)
6237
6013
  || !this.deletionGateOpen(target.id, genAtEntry)
6238
6014
  || !participantsFresh()
@@ -6378,8 +6154,6 @@ export class AgentManager {
6378
6154
  const cfg = this.getAgentConfig(agentId);
6379
6155
  if (!cfg)
6380
6156
  throw new ApiError(404, `Unknown agent: ${agentId}`);
6381
- // Pin the incarnation before reading paneId: paneId equality is ABA-blind (a recreate reuses %0), so a
6382
- // DELETE→same-id recreate during the upload would otherwise paste the stale image into the new session.
6383
6157
  const genAtEntry = this.deletionGenerationOf(agentId);
6384
6158
  const state = await this.agentStore.get(agentId);
6385
6159
  const paneId = state?.paneId;
@@ -6413,7 +6187,6 @@ export class AgentManager {
6413
6187
  await assertUploadStillValid();
6414
6188
  const tmux = new TmuxManager(runner);
6415
6189
  const pane = await this.resolveClaimedPane(tmux, agentId, paneId);
6416
- // resolveClaimedPane resolved by the CURRENT same-name session; re-verify once more before injecting so a DELETE→recreate in that gap can't take the paste.
6417
6190
  await assertUploadStillValid();
6418
6191
  await tmux.injectPrompt(pane, `${path} `, agentId);
6419
6192
  return { path };
@@ -6486,9 +6259,6 @@ export class AgentManager {
6486
6259
  }
6487
6260
  registerPendingDispatchRetry(taskId, entry, budgetOverride) {
6488
6261
  const prev = this.pendingDispatchRetryByTask.get(taskId);
6489
- // 预算随登记代走:同 kind 但 pass 换代(token/agent 变化)时必须重置 since 与告警标志。
6490
- // budgetOverride 供对账内部重排延续同一笔欠投递的预算——它只经 startSession 的 armedToken
6491
- // fence 注入,外部 successor 的登记结构上拿不到
6492
6262
  const sameGeneration = prev?.kind === entry.kind
6493
6263
  && prev.agentId === entry.agentId
6494
6264
  && prev.signalToken === entry.signalToken;
@@ -6528,8 +6298,6 @@ export class AgentManager {
6528
6298
  return null;
6529
6299
  if (phase !== 'review' && phase !== 'recheck')
6530
6300
  return null;
6531
- // 无本次派发的 pass 令牌就不登记(宁走既有失败路径,不造无 fence 重试);
6532
- // 令牌漂移说明 pass 已被接管,忙碌属于旧派发,不得登记到 successor 上
6533
6301
  const passToken = dispatch.passToken;
6534
6302
  if (!passToken)
6535
6303
  return null;
@@ -6652,7 +6420,6 @@ export class AgentManager {
6652
6420
  error: `Task ${taskId} has no preferredAgentId; agentId is required in request body`,
6653
6421
  };
6654
6422
  }
6655
- // Capture generation before the first config/state read so a DELETE→same-id recreate in the read→commit window fails the gate.
6656
6423
  const genAtEntry = this.deletionGenerationOf(agentId);
6657
6424
  const cfg = this.getAgentConfig(agentId);
6658
6425
  if (!cfg)
@@ -6698,7 +6465,6 @@ export class AgentManager {
6698
6465
  await this.lockManager.releaseIfOwner(agentId, taskId, lockToken);
6699
6466
  return { task: fresh, errorCode: 409, error: `Agent ${agentId} has no qa agent in its group` };
6700
6467
  }
6701
- // Pin every participant's generation (mirror createTask) so a member deleted→recreated before the active write isn't baked in.
6702
6468
  const participantGens = new Map([devId, qaId]
6703
6469
  .map(pid => [pid, this.deletionGenerationOf(pid)]));
6704
6470
  const participantsFresh = () => [...participantGens].every(([pid, gen]) => this.deletionGenerationOf(pid) === gen && !this.isDeletionInFlight(pid));
@@ -6712,8 +6478,6 @@ export class AgentManager {
6712
6478
  status: 'in_progress',
6713
6479
  updatedAt: now,
6714
6480
  };
6715
- // Binding-first: the fresh task snapshot already exists on disk, so a failure at any later
6716
- // step leaves at worst a stale binding to a still-unclaimed task — an existing reclaimable state.
6717
6481
  const releaseLock = async () => {
6718
6482
  try {
6719
6483
  await this.lockManager.releaseIfOwner(agentId, taskId, lockToken);
@@ -6739,7 +6503,6 @@ export class AgentManager {
6739
6503
  ...(existing?.paneId !== undefined ? { paneId: existing.paneId } : {}),
6740
6504
  ...(existing?.workdir !== undefined ? { workdir: existing.workdir } : {}),
6741
6505
  ...(existing?.creationToken !== undefined ? { creationToken: existing.creationToken } : {}),
6742
- // A new task never inherits the previous one's question: void the watermark.
6743
6506
  needInput: { epoch: (existing?.needInput?.epoch ?? 0) + 1 },
6744
6507
  };
6745
6508
  });
@@ -6752,7 +6515,6 @@ export class AgentManager {
6752
6515
  await releaseLock();
6753
6516
  return { task: fresh, errorCode: 409, error: `Agent ${agentId} is being deleted` };
6754
6517
  }
6755
- // Re-verify lock ownership + preferred/participant generations before the active write, else a DELETE after the binding commit orphans it.
6756
6518
  if (this.isDeletionInFlight(agentId)
6757
6519
  || !this.deletionGateOpen(agentId, genAtEntry)
6758
6520
  || !participantsFresh()
@@ -6883,7 +6645,6 @@ export class AgentManager {
6883
6645
  const agent = this.getAgentConfig(agentId);
6884
6646
  if (!agent)
6885
6647
  throw new Error(`Unknown agent: ${agentId}`);
6886
- // Pin the incarnation: a DELETE completing during dispatch bumps this, and every gated commit/side-effect below refuses the stale generation.
6887
6648
  const genAtEntry = this.deletionGenerationOf(agentId);
6888
6649
  const task = await this.taskStore.get(taskId);
6889
6650
  if (!task)
@@ -6930,8 +6691,6 @@ export class AgentManager {
6930
6691
  return false;
6931
6692
  }
6932
6693
  const assertOwner = async () => {
6933
- // Also gate on the entry generation: a DELETE→same-id recreate can pass the task/lock check on the
6934
- // reused binding, so every Workdir/runtime side-effect guarded by assertOwner must fail closed on it.
6935
6694
  if (!this.deletionGateOpen(agentId, genAtEntry)) {
6936
6695
  throw new Error(`Agent ${agentId} is being deleted or was recreated for task ${taskId}; operation aborted`);
6937
6696
  }
@@ -7049,7 +6808,6 @@ export class AgentManager {
7049
6808
  await this.agentStore.update(agentId, (stateNow) => {
7050
6809
  if (!stateNow || stateNow.taskId !== taskId)
7051
6810
  return AGENT_STORE_NOOP;
7052
- // Watermark stays: the arm-time epoch bump owns superseding the old question.
7053
6811
  return {
7054
6812
  ...stateNow,
7055
6813
  paneId,
@@ -7131,7 +6889,6 @@ export class AgentManager {
7131
6889
  cancelHoldWon = true;
7132
6890
  return AGENT_STORE_NOOP;
7133
6891
  }
7134
- // Binding must already exist and still hold our lock; never re-create a deleted agent's state nor write under a bumped deletion generation (ABA).
7135
6892
  if (!existing || existing.taskId !== taskId || existing.lockToken !== lockToken
7136
6893
  || !this.deletionGateOpen(agentId, genAtEntry)) {
7137
6894
  deletedDuringDispatch = true;
@@ -7162,10 +6919,6 @@ export class AgentManager {
7162
6919
  return false;
7163
6920
  }
7164
6921
  agentMarkedRunning = true;
7165
- // armed pass 的 fence 延伸到最终 paste:checkout//clear/artifacts 的 await 窗口里 pass 可被
7166
- // push/其他 dispatcher 轮换,旧 prompt 不得注入 successor;guard 在 composer/paste/Enter
7167
- // 的 task-mutation 队列槽内复核(与轮换串行),漂移即放弃投递。
7168
- // cancel 只写终态不轮换 token,故 fence 须同时复核任务仍处本相位的可投递状态
7169
6922
  const passDeliverableStatuses = opts.dialogFailFromStatuses ?? PHASE_EXPECTED_STATUS[phase] ?? [...ACTIVE_TASK_STATUSES];
7170
6923
  const passStillArmed = opts.dispatchPassToken !== undefined
7171
6924
  ? async () => {
@@ -7187,8 +6940,6 @@ export class AgentManager {
7187
6940
  console.warn(`[AgentManager] startSession[${phase}]: injection fenced off (pass superseded) for task ${taskId}`);
7188
6941
  return false;
7189
6942
  }
7190
- // prompt 已确认投递:只 CAS 清理本次派发那一代的登记;successor 在窗口内登记的新 pending 不受影响,
7191
- // 且清理先于可失败的审计事件——事件写失败不能把已投递的 prompt 重新变回 pending
7192
6943
  if (opts.dispatchPassToken !== undefined) {
7193
6944
  this.clearPendingDispatchRetryIfMatches(taskId, { agentId, signalToken: opts.dispatchPassToken });
7194
6945
  }
@@ -7215,8 +6966,6 @@ export class AgentManager {
7215
6966
  return true;
7216
6967
  }
7217
6968
  catch (err) {
7218
- // 注入阶段的忙碌(预注入活回合检查)仍在任何按键之前:登记 pending 并保持 checkout/绑定原样,
7219
- // 在途回合还在使用该工作树,park/release 会拽走它
7220
6969
  const injectBusyPend = await this.queueQaBusyPendingRetry(taskId, agentId, phase, ensure.createdSession, err, { passToken: opts.dispatchPassToken, pendingBudget: opts.dispatchPendingBudget });
7221
6970
  if (injectBusyPend)
7222
6971
  throw injectBusyPend;
@@ -7322,7 +7071,6 @@ export class AgentManager {
7322
7071
  }
7323
7072
  async injectAndAwaitAckSteps(tmux, pane, prompt, agentId, runtime, revalidate, guardBeforePaste) {
7324
7073
  const paneId = pane.paneId;
7325
- // 静态忙信号(文本忙样式/working title)可能是残稿或 stale title 冒充,一律由帧活性仲裁;ready 视图覆盖 stale title。
7326
7074
  const preTitle = await tmux.readPaneTitle(pane);
7327
7075
  const preFrame = await tmux.capturePaneById(pane, { ansi: false, scrollback: 0 });
7328
7076
  const staticBusy = hasOscTitleWorking(preTitle)
@@ -7333,12 +7081,10 @@ export class AgentManager {
7333
7081
  }
7334
7082
  await revalidate?.();
7335
7083
  if (guardBeforePaste) {
7336
- // A stale replay must not touch the pane at all: the guard runs before the first composer scrub.
7337
7084
  if (!(await guardBeforePaste())) {
7338
7085
  return { acked: false, composerDelivered: false, aborted: true };
7339
7086
  }
7340
7087
  const staged = await tmux.stagePromptBuffer(paneId, prompt, agentId);
7341
- // Fence + scrub + paste share one task-mutation-queue slot, so a rotation lands before or after, never between.
7342
7088
  let pasted = false;
7343
7089
  try {
7344
7090
  pasted = await this.withTaskLock(async () => {
@@ -7350,7 +7096,6 @@ export class AgentManager {
7350
7096
  });
7351
7097
  }
7352
7098
  catch (pasteErr) {
7353
- // Buffer still present proves the paste never landed; missing/unprobeable fails closed to a composer scrub.
7354
7099
  let bufferConsumed = true;
7355
7100
  try {
7356
7101
  await tmux.dropStagedBuffer(staged.buf);
@@ -7390,7 +7135,6 @@ export class AgentManager {
7390
7135
  baseline = await tmux.captureSettledSnapshot(pane, { timeoutMs: this.dispatchSettleTimeoutMs });
7391
7136
  baselineTitle = await tmux.readPaneTitle(pane);
7392
7137
  if (guardBeforePaste) {
7393
- // The Enter is the actual submission; it takes the same fenced queue slot.
7394
7138
  const submitted = await this.withTaskLock(async () => {
7395
7139
  if (!(await guardBeforePaste()))
7396
7140
  return false;
@@ -7402,7 +7146,6 @@ export class AgentManager {
7402
7146
  await tmux.clearComposerDraft(pane);
7403
7147
  }
7404
7148
  catch (err) {
7405
- // Fail closed: a submit-ready stale prompt could not be confirmed gone.
7406
7149
  const message = err instanceof Error ? err.message : String(err);
7407
7150
  throw new Error(`fence-rejected prompt could not be scrubbed from the composer of pane ${paneId}; verify the pane before reuse: ${message}`);
7408
7151
  }
@@ -7426,7 +7169,6 @@ export class AgentManager {
7426
7169
  baselineTitle,
7427
7170
  resend: guardBeforePaste
7428
7171
  ? async () => {
7429
- // A resend is a fresh submission of whatever sits in the composer; it takes the same fence.
7430
7172
  const resent = await this.withTaskLock(async () => {
7431
7173
  if (!(await guardBeforePaste()))
7432
7174
  return false;
@@ -7588,8 +7330,6 @@ export class AgentManager {
7588
7330
  await branches.assertClean(verifiedWorkdir);
7589
7331
  const actualRef = await branches.currentRef(verifiedWorkdir);
7590
7332
  if (actualRef !== `refs/heads/${task.branch}`) {
7591
- // dev 可能已被释放过(如 spec max_rounds 停驻默认分支);switchToTaskBranch 自带
7592
- // assertClean,脏树时抛 DirtyWorkdirError fail-closed,不会覆盖未提交的改动
7593
7333
  await this.assertTaskGeneration(agentId, taskId, lockToken, verifiedWorkdir);
7594
7334
  await branches.switchToTaskBranch(verifiedWorkdir, taskId, task.branch, task.branchCreatedByBaxian === true, {
7595
7335
  requireExistingWork: true,
@@ -7662,7 +7402,6 @@ export class AgentManager {
7662
7402
  return false;
7663
7403
  }
7664
7404
  await this.assertTaskLockOwner(agentId, taskId, lockToken);
7665
- // A revoke marks before clearing and a push transitions before clearing; every half-window must abort the paste.
7666
7405
  const postApprovePassStillLive = async () => {
7667
7406
  const [taskFresh, completionFresh] = await Promise.all([
7668
7407
  this.taskStore.get(taskId),
@@ -7720,9 +7459,6 @@ export class AgentManager {
7720
7459
  || latest.workdir !== verifiedWorkdir) {
7721
7460
  return AGENT_STORE_NOOP;
7722
7461
  }
7723
- // The continuation prompt supersedes the previous question, but the arm-time
7724
- // epoch bump owns that transition — stripping here would race an arm that
7725
- // already re-established the watermark (post-approve arms before injecting).
7726
7462
  return {
7727
7463
  ...latest,
7728
7464
  paneId,
@@ -7835,7 +7571,6 @@ export class AgentManager {
7835
7571
  return { ...rest, updatedAt: new Date().toISOString() };
7836
7572
  });
7837
7573
  }
7838
- // continueSession never runs startSession's post-ack finalization; a surviving marker would make recover() roll the running task back to pending.
7839
7574
  async finalizeReplayedBootstrap(agentId, taskId, phase, expectedLockToken) {
7840
7575
  let cleared = false;
7841
7576
  let deliveredWorkdir;
@@ -8190,8 +7925,6 @@ export class AgentManager {
8190
7925
  if (this.markCompleteInFlight.has(taskId)) {
8191
7926
  throw new ApiError(409, `Task ${taskId} is being completed (merge in progress); try again shortly`);
8192
7927
  }
8193
- // 终态不早退:状态不再改写,但残留的 pane/绑定(上次清理中途失败)仍走同一条强制清理路径。
8194
- // 唯一例外:清理已在进行中的重复取消——重跑只会重复中断同一批 pane,等它收尾即可。
8195
7928
  const alreadyTerminal = TERMINAL_STATUSES.includes(task.status);
8196
7929
  if (alreadyTerminal && this.cancelCleanupInFlight.has(taskId))
8197
7930
  return task;
@@ -8238,10 +7971,8 @@ export class AgentManager {
8238
7971
  cleanupClaimed = true;
8239
7972
  return task;
8240
7973
  });
8241
- // stop again post-write: a rollback may have re-armed between the pre-lock stop() and the cancelled write
8242
7974
  if (cleanupClaimed)
8243
7975
  this.phaseSignalWatcher?.stop(taskId);
8244
- // 两阶段:先中断全部 pane 再释放任一 binding,避免 dev 先空闲、被派新任务时旧任务的 qa prompt 仍在跑。
8245
7976
  try {
8246
7977
  const stopped = [];
8247
7978
  for (const id of [devToRelease, qaToRelease]) {
@@ -9005,8 +8736,6 @@ export class AgentManager {
9005
8736
  const agent = this.getAgentConfig(agentId);
9006
8737
  if (!agent)
9007
8738
  return;
9008
- // Pin the incarnation before the first state/paneId read: paneId equality is ABA-blind (a recreated
9009
- // agent reuses %0), so a DELETE→same-id recreate would otherwise let post-merge /clear hit a new session.
9010
8739
  const genAtEntry = this.deletionGenerationOf(agentId);
9011
8740
  const state = await this.agentStore.get(agentId);
9012
8741
  if (!state)
@@ -9062,7 +8791,6 @@ export class AgentManager {
9062
8791
  return;
9063
8792
  }
9064
8793
  }
9065
- // A DELETE→recreate (gen bump) before this pane work must skip it, or /clear lands on the new incarnation's reused pane.
9066
8794
  if (!this.deletionGateOpen(agentId, genAtEntry))
9067
8795
  return;
9068
8796
  const runner = this.createRunnerFor(agent);
@@ -9284,15 +9012,11 @@ export class AgentManager {
9284
9012
  return;
9285
9013
  }
9286
9014
  if (Date.now() >= deadline) {
9287
- // 与 stableIdle 分支同一类型:忙碌超时是「常态忙」而非故障,调用方(release/派发)
9288
- // 据此延后重排;菜单/对话框/REPL 退出等仍是普通 Error,不得被当成可延后
9289
9015
  throw new ReplNotReadyError(paneId, runtime, cap, `stayed busy past ${timeoutMs}ms`);
9290
9016
  }
9291
9017
  await new Promise(r => setTimeout(r, this.compactIdlePollMs));
9292
9018
  }
9293
9019
  }
9294
- // 注入前置门:codex 忙碌回合的工具间隙会闪现单帧 idle footer(#558 事故放大器),
9295
- // 连续 K 帧稳定就绪才放行,任何忙碌/不确定帧清零计数;超时按忙碌分类抛 ReplNotReadyError。
9296
9020
  async waitForReplPromptStableIdle(tmux, pane, runtime, timeoutMs) {
9297
9021
  const paneId = pane.paneId;
9298
9022
  const samples = Math.max(1, this.readyStableSamples);
@@ -9370,8 +9094,6 @@ export class AgentManager {
9370
9094
  async armPostDispatchSignalOrHold(taskId, agentId, expectedKinds, token, skipSnapshot = false) {
9371
9095
  const armed = await this.setupPhaseSignalWatcher(taskId, agentId, expectedKinds, token, {
9372
9096
  skipSnapshot,
9373
- // Every post-dispatch arm follows a freshly injected prompt: the previous
9374
- // question is superseded, its watermark must not carry over.
9375
9097
  needInputMode: 'fresh',
9376
9098
  });
9377
9099
  if (!armed)
@@ -9812,7 +9534,6 @@ export class AgentManager {
9812
9534
  return null;
9813
9535
  }
9814
9536
  const newToken = createSignalToken();
9815
- // spec 阶段的扩轮决策不延续到 code 评审:cap 共享字段,进入编码时归零
9816
9537
  const transition = await this.transitionTaskStatus(taskId, 'in_progress', {
9817
9538
  fromStatus: ['review', 'fixing', 'in_progress', 'spec-ready', 'max_rounds'],
9818
9539
  ...(expectedTask !== undefined ? { expectTask: expectedTask } : {}),
@@ -9868,8 +9589,6 @@ export class AgentManager {
9868
9589
  }
9869
9590
  else if (err instanceof EnsureSessionError && err.partial.handled
9870
9591
  && (task.status === 'spec-ready' || task.status === 'max_rounds')) {
9871
- // dev 已被 checkout/dialog hold 且 prompt 未送达;resumeAgent 不重派这些 hold,
9872
- // 任务停在 in_progress 会同时封死 Resume 与 verdict——退回 verdict 门禁状态
9873
9592
  const rolledBack = await this.transitionTaskStatus(taskId, task.status, { fromStatus: ['in_progress'] }, {
9874
9593
  ...(task.phase !== undefined ? { phase: task.phase } : {}),
9875
9594
  agentId: task.agentId,
@@ -9881,8 +9600,6 @@ export class AgentManager {
9881
9600
  + 'moved on; leaving its status as-is');
9882
9601
  }
9883
9602
  else if (!devBoundBefore) {
9884
- // 转码前 dev 未持有本任务(如 max_rounds 重占用):任务已退回
9885
- // verdict 门禁,释放本次新占用,否则 dev 会被回退后的任务永久占住
9886
9603
  const released = await this.releaseAgentForTask(devAgentId, taskId, 'idle', { allowAwaitingHuman: true })
9887
9604
  .catch(() => false);
9888
9605
  if (!released) {
@@ -9907,8 +9624,6 @@ export class AgentManager {
9907
9624
  });
9908
9625
  return null;
9909
9626
  }
9910
- // 重占用(released dev)的 acquire 会写 bootstrappingTaskId,而 continueSession 不做
9911
- // startSession 的 post-ack finalize;残留标记会让 recover 把已运行的任务按未送达回滚
9912
9627
  await this.finalizeReplayedBootstrap(devAgentId, taskId, 'code', acquiredLockToken);
9913
9628
  return await this.taskStore.get(taskId);
9914
9629
  }
@@ -9916,7 +9631,6 @@ export class AgentManager {
9916
9631
  const cfg = this.getAgentConfig(agentId);
9917
9632
  if (!cfg)
9918
9633
  throw new Error(`injectTextToAgent: unknown agent ${agentId}`);
9919
- // Pin the incarnation before reading state so a DELETE→recreate reusing %0 can't route the text to the new session.
9920
9634
  const genAtEntry = this.deletionGenerationOf(agentId);
9921
9635
  await this.acquireCompactGuard(agentId);
9922
9636
  try {
@@ -9953,7 +9667,6 @@ export class AgentManager {
9953
9667
  }
9954
9668
  const tmux = new TmuxManager(this.createRunnerFor(cfg));
9955
9669
  const pane = await this.resolveClaimedPane(tmux, agentId, paneId);
9956
- // resolveClaimedPane resolved by the CURRENT same-name session; re-verify incarnation + binding before paste (same as the image path).
9957
9670
  if (!this.deletionGateOpen(agentId, genAtEntry)) {
9958
9671
  throw new Error(`injectTextToAgent: agent ${agentId} was deleted or recreated before paste`);
9959
9672
  }
@@ -10119,7 +9832,6 @@ export class AgentManager {
10119
9832
  }
10120
9833
  }
10121
9834
  startCompactionThenRelease(agentId, state, taskId) {
10122
- // 人工介入/重建中的 pane 不发 /clear;confirm 语义要求解绑,落回直接释放
10123
9835
  const expectedPaneId = state.paneId;
10124
9836
  if (!expectedPaneId || state.status === 'awaiting_human' || state.creationToken)
10125
9837
  return false;