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.
- package/dist/agent/branch.d.ts.map +1 -1
- package/dist/agent/branch.js +0 -7
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/detect/region.d.ts.map +1 -1
- package/dist/agent/detect/region.js +0 -3
- package/dist/agent/detect/region.js.map +1 -1
- package/dist/agent/dispatch-reconciler.d.ts.map +1 -1
- package/dist/agent/dispatch-reconciler.js +0 -51
- package/dist/agent/dispatch-reconciler.js.map +1 -1
- package/dist/agent/lineage.d.ts.map +1 -1
- package/dist/agent/lineage.js +0 -9
- package/dist/agent/lineage.js.map +1 -1
- package/dist/agent/manager.d.ts +1 -4
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +10 -298
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/net-exec.d.ts.map +1 -1
- package/dist/agent/net-exec.js +0 -5
- package/dist/agent/net-exec.js.map +1 -1
- package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
- package/dist/agent/pane-streamer-manager.js +0 -36
- package/dist/agent/pane-streamer-manager.js.map +1 -1
- package/dist/agent/pane-streamer.d.ts +6 -0
- package/dist/agent/pane-streamer.d.ts.map +1 -1
- package/dist/agent/pane-streamer.js +126 -83
- package/dist/agent/pane-streamer.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +2 -77
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +0 -1
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +0 -17
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +0 -18
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +0 -1
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +0 -38
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +0 -2
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tmux-install.js +0 -1
- package/dist/agent/tmux-install.js.map +1 -1
- package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
- package/dist/agent/tmux-probe-poller.js +0 -7
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +0 -86
- package/dist/agent/tmux.js.map +1 -1
- package/dist/agent/vt-visible-text.d.ts +46 -1
- package/dist/agent/vt-visible-text.d.ts.map +1 -1
- package/dist/agent/vt-visible-text.js +639 -63
- package/dist/agent/vt-visible-text.js.map +1 -1
- package/dist/api/agents.d.ts.map +1 -1
- package/dist/api/agents.js +1 -42
- package/dist/api/agents.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +3 -52
- package/dist/api/config.js.map +1 -1
- package/dist/api/hosts.d.ts.map +1 -1
- package/dist/api/hosts.js +2 -57
- package/dist/api/hosts.js.map +1 -1
- package/dist/api/platform-guard.d.ts.map +1 -1
- package/dist/api/platform-guard.js +0 -1
- package/dist/api/platform-guard.js.map +1 -1
- package/dist/api/probe.d.ts.map +1 -1
- package/dist/api/probe.js +0 -1
- package/dist/api/probe.js.map +1 -1
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +1 -14
- package/dist/api/projects.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +0 -1
- package/dist/api/tasks.js.map +1 -1
- package/dist/app.d.ts +0 -2
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +0 -6
- package/dist/app.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -29
- package/dist/cli.js.map +1 -1
- package/dist/config/hot-reload.d.ts.map +1 -1
- package/dist/config/hot-reload.js +0 -4
- package/dist/config/hot-reload.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +1 -24
- package/dist/config/loader.js.map +1 -1
- package/dist/config/normalizer.d.ts.map +1 -1
- package/dist/config/normalizer.js +0 -19
- package/dist/config/normalizer.js.map +1 -1
- package/dist/config/validator.d.ts.map +1 -1
- package/dist/config/validator.js +2 -200
- package/dist/config/validator.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +0 -11
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/log.js +0 -2
- package/dist/event/log.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -45
- package/dist/index.js.map +1 -1
- package/dist/platform/body-digest.d.ts.map +1 -1
- package/dist/platform/body-digest.js +0 -4
- package/dist/platform/body-digest.js.map +1 -1
- package/dist/platform/command-renderer.d.ts.map +1 -1
- package/dist/platform/command-renderer.js +0 -3
- package/dist/platform/command-renderer.js.map +1 -1
- package/dist/platform/comment-cursor.d.ts.map +1 -1
- package/dist/platform/comment-cursor.js +0 -21
- package/dist/platform/comment-cursor.js.map +1 -1
- package/dist/platform/driver-spec.d.ts.map +1 -1
- package/dist/platform/driver-spec.js +1 -42
- package/dist/platform/driver-spec.js.map +1 -1
- package/dist/platform/field-mapper.d.ts.map +1 -1
- package/dist/platform/field-mapper.js +3 -11
- package/dist/platform/field-mapper.js.map +1 -1
- package/dist/platform/git-driver.d.ts.map +1 -1
- package/dist/platform/git-driver.js +0 -20
- package/dist/platform/git-driver.js.map +1 -1
- package/dist/platform/manifest.d.ts.map +1 -1
- package/dist/platform/manifest.js +0 -3
- package/dist/platform/manifest.js.map +1 -1
- package/dist/platform/markers.d.ts.map +1 -1
- package/dist/platform/markers.js +0 -19
- package/dist/platform/markers.js.map +1 -1
- package/dist/platform/platform-poller.d.ts.map +1 -1
- package/dist/platform/platform-poller.js +0 -43
- package/dist/platform/platform-poller.js.map +1 -1
- package/dist/platform/plugin-registry.d.ts.map +1 -1
- package/dist/platform/plugin-registry.js +1 -12
- package/dist/platform/plugin-registry.js.map +1 -1
- package/dist/platform/pr-conversation-cache.d.ts.map +1 -1
- package/dist/platform/pr-conversation-cache.js +0 -17
- package/dist/platform/pr-conversation-cache.js.map +1 -1
- package/dist/platform/preflight-exec.d.ts.map +1 -1
- package/dist/platform/preflight-exec.js +0 -9
- package/dist/platform/preflight-exec.js.map +1 -1
- package/dist/platform/response-parser.d.ts.map +1 -1
- package/dist/platform/response-parser.js +0 -4
- package/dist/platform/response-parser.js.map +1 -1
- package/dist/platform/review-timeline.d.ts.map +1 -1
- package/dist/platform/review-timeline.js +0 -22
- package/dist/platform/review-timeline.js.map +1 -1
- package/dist/platform/row-schema.d.ts.map +1 -1
- package/dist/platform/row-schema.js +0 -11
- package/dist/platform/row-schema.js.map +1 -1
- package/dist/platform/startup.d.ts.map +1 -1
- package/dist/platform/startup.js +1 -12
- package/dist/platform/startup.js.map +1 -1
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/platform/types.js +0 -5
- package/dist/platform/types.js.map +1 -1
- package/dist/platform/verdict-engine.d.ts.map +1 -1
- package/dist/platform/verdict-engine.js +0 -15
- package/dist/platform/verdict-engine.js.map +1 -1
- package/dist/shared/concurrency.d.ts.map +1 -1
- package/dist/shared/concurrency.js +0 -2
- package/dist/shared/concurrency.js.map +1 -1
- package/dist/shared/constants.d.ts +0 -2
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +0 -3
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/git-url.d.ts.map +1 -1
- package/dist/shared/git-url.js +0 -9
- package/dist/shared/git-url.js.map +1 -1
- package/dist/shared/types.d.ts +0 -11
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/skill/registry.d.ts.map +1 -1
- package/dist/skill/registry.js +0 -5
- package/dist/skill/registry.js.map +1 -1
- package/dist/skills/baxian-signals/SKILL.md +0 -6
- package/dist/state/agent-store.d.ts.map +1 -1
- package/dist/state/agent-store.js +0 -6
- package/dist/state/agent-store.js.map +1 -1
- package/dist/state/error-record-store.d.ts.map +1 -1
- package/dist/state/error-record-store.js +0 -5
- package/dist/state/error-record-store.js.map +1 -1
- package/dist/state/init.d.ts.map +1 -1
- package/dist/state/init.js +0 -1
- package/dist/state/init.js.map +1 -1
- package/dist/state/lock.d.ts.map +1 -1
- package/dist/state/lock.js +0 -1
- package/dist/state/lock.js.map +1 -1
- package/dist/state/task-store.d.ts.map +1 -1
- package/dist/state/task-store.js +0 -3
- package/dist/state/task-store.js.map +1 -1
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js +0 -4
- package/dist/terminal/attach.js.map +1 -1
- package/dist/terminal/stream-ws.js +0 -5
- package/dist/terminal/stream-ws.js.map +1 -1
- package/dist/web/assets/{index-BS2adAfI.css → index-yqxawAPJ.css} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/agent/root-agent-runtime.d.ts +0 -80
- package/dist/agent/root-agent-runtime.d.ts.map +0 -1
- package/dist/agent/root-agent-runtime.js +0 -637
- package/dist/agent/root-agent-runtime.js.map +0 -1
- package/dist/agent/root-recovery-coordinator.d.ts +0 -124
- package/dist/agent/root-recovery-coordinator.d.ts.map +0 -1
- package/dist/agent/root-recovery-coordinator.js +0 -1557
- package/dist/agent/root-recovery-coordinator.js.map +0 -1
- package/dist/state/root-recovery-store.d.ts +0 -107
- package/dist/state/root-recovery-store.d.ts.map +0 -1
- package/dist/state/root-recovery-store.js +0 -400
- package/dist/state/root-recovery-store.js.map +0 -1
- /package/dist/web/assets/{index-BKiM0MSM.js → index-CiAnjs1_.js} +0 -0
package/dist/agent/manager.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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;
|