baxian 1.2.59 → 1.2.61
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 +19 -13
- package/dist/agent/branch.js.map +1 -1
- package/dist/agent/dispatch-reconciler.d.ts +45 -0
- package/dist/agent/dispatch-reconciler.d.ts.map +1 -0
- package/dist/agent/dispatch-reconciler.js +513 -0
- package/dist/agent/dispatch-reconciler.js.map +1 -0
- package/dist/agent/manager.d.ts +123 -12
- package/dist/agent/manager.d.ts.map +1 -1
- package/dist/agent/manager.js +1607 -420
- package/dist/agent/manager.js.map +1 -1
- package/dist/agent/pane-streamer-manager.d.ts +70 -0
- package/dist/agent/pane-streamer-manager.d.ts.map +1 -1
- package/dist/agent/pane-streamer-manager.js +487 -5
- package/dist/agent/pane-streamer-manager.js.map +1 -1
- package/dist/agent/pane-streamer.d.ts +48 -1
- package/dist/agent/pane-streamer.d.ts.map +1 -1
- package/dist/agent/pane-streamer.js +338 -15
- package/dist/agent/pane-streamer.js.map +1 -1
- package/dist/agent/phase-signal-watcher.d.ts +43 -2
- package/dist/agent/phase-signal-watcher.d.ts.map +1 -1
- package/dist/agent/phase-signal-watcher.js +301 -43
- package/dist/agent/phase-signal-watcher.js.map +1 -1
- package/dist/agent/phase-signal.d.ts +9 -0
- package/dist/agent/phase-signal.d.ts.map +1 -1
- package/dist/agent/phase-signal.js +63 -5
- package/dist/agent/phase-signal.js.map +1 -1
- package/dist/agent/preflight.d.ts +20 -1
- package/dist/agent/preflight.d.ts.map +1 -1
- package/dist/agent/preflight.js +235 -27
- package/dist/agent/preflight.js.map +1 -1
- package/dist/agent/prompt.d.ts +8 -0
- package/dist/agent/prompt.d.ts.map +1 -1
- package/dist/agent/prompt.js +45 -2
- package/dist/agent/prompt.js.map +1 -1
- package/dist/agent/repo-store.d.ts +5 -1
- package/dist/agent/repo-store.d.ts.map +1 -1
- package/dist/agent/repo-store.js +199 -70
- package/dist/agent/repo-store.js.map +1 -1
- package/dist/agent/review-transport.d.ts.map +1 -1
- package/dist/agent/review-transport.js +4 -2
- package/dist/agent/review-transport.js.map +1 -1
- package/dist/agent/tmux-probe-poller.d.ts.map +1 -1
- package/dist/agent/tmux-probe-poller.js +29 -20
- package/dist/agent/tmux-probe-poller.js.map +1 -1
- package/dist/agent/tmux.d.ts +79 -32
- package/dist/agent/tmux.d.ts.map +1 -1
- package/dist/agent/tmux.js +435 -225
- package/dist/agent/tmux.js.map +1 -1
- package/dist/api/config.d.ts.map +1 -1
- package/dist/api/config.js +18 -0
- package/dist/api/config.js.map +1 -1
- package/dist/api/projects.d.ts.map +1 -1
- package/dist/api/projects.js +241 -98
- package/dist/api/projects.js.map +1 -1
- package/dist/api/tasks.d.ts.map +1 -1
- package/dist/api/tasks.js +29 -2
- package/dist/api/tasks.js.map +1 -1
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +1 -0
- package/dist/app.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +18 -3
- package/dist/cli.js.map +1 -1
- package/dist/config/hot-reload.d.ts.map +1 -1
- package/dist/config/hot-reload.js +1 -0
- package/dist/config/hot-reload.js.map +1 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +48 -3
- package/dist/config/loader.js.map +1 -1
- package/dist/config/validator.js +11 -1
- package/dist/config/validator.js.map +1 -1
- package/dist/errors.d.ts +2 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -1
- package/dist/errors.js.map +1 -1
- package/dist/event/handlers.d.ts.map +1 -1
- package/dist/event/handlers.js +138 -41
- package/dist/event/handlers.js.map +1 -1
- package/dist/event/log.d.ts.map +1 -1
- package/dist/event/log.js +17 -2
- package/dist/event/log.js.map +1 -1
- package/dist/github/pr-conversation-cache.d.ts +48 -0
- package/dist/github/pr-conversation-cache.d.ts.map +1 -0
- package/dist/github/pr-conversation-cache.js +199 -0
- package/dist/github/pr-conversation-cache.js.map +1 -0
- package/dist/github/pr-conversation.d.ts +2 -2
- package/dist/github/pr-conversation.d.ts.map +1 -1
- package/dist/github/pr-conversation.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/index.js.map +1 -1
- package/dist/platform/driver-spec.d.ts.map +1 -1
- package/dist/platform/driver-spec.js +33 -0
- package/dist/platform/driver-spec.js.map +1 -1
- package/dist/platform/git-driver.d.ts +7 -1
- package/dist/platform/git-driver.d.ts.map +1 -1
- package/dist/platform/git-driver.js +33 -2
- package/dist/platform/git-driver.js.map +1 -1
- package/dist/platform/platform-poller.d.ts +8 -0
- package/dist/platform/platform-poller.d.ts.map +1 -1
- package/dist/platform/platform-poller.js +80 -1
- 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 +14 -2
- package/dist/platform/plugin-registry.js.map +1 -1
- package/dist/platform/plugins/github/driver.json +3 -0
- package/dist/platform/plugins/github/skills/baxian-cli-gh/SKILL.md +16 -7
- package/dist/platform/preflight-exec.d.ts +21 -0
- package/dist/platform/preflight-exec.d.ts.map +1 -0
- package/dist/platform/preflight-exec.js +62 -0
- package/dist/platform/preflight-exec.js.map +1 -0
- package/dist/platform/review-timeline.d.ts +16 -0
- package/dist/platform/review-timeline.d.ts.map +1 -0
- package/dist/platform/review-timeline.js +195 -0
- package/dist/platform/review-timeline.js.map +1 -0
- package/dist/platform/startup.d.ts +5 -1
- package/dist/platform/startup.d.ts.map +1 -1
- package/dist/platform/startup.js +25 -0
- package/dist/platform/startup.js.map +1 -1
- package/dist/platform/types.d.ts +2 -0
- package/dist/platform/types.d.ts.map +1 -1
- package/dist/platform/types.js +1 -1
- package/dist/platform/types.js.map +1 -1
- package/dist/shared/constants.d.ts +6 -0
- package/dist/shared/constants.d.ts.map +1 -1
- package/dist/shared/constants.js +6 -0
- package/dist/shared/constants.js.map +1 -1
- package/dist/shared/git-url.d.ts +1 -0
- package/dist/shared/git-url.d.ts.map +1 -1
- package/dist/shared/git-url.js +3 -0
- package/dist/shared/git-url.js.map +1 -1
- package/dist/shared/types.d.ts +23 -6
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/skill/registry.d.ts +9 -2
- package/dist/skill/registry.d.ts.map +1 -1
- package/dist/skill/registry.js +88 -54
- package/dist/skill/registry.js.map +1 -1
- package/dist/skills/baxian-pr-feedback/SKILL.md +2 -0
- package/dist/skills/baxian-pr-recheck/SKILL.md +2 -0
- package/dist/skills/baxian-pr-review/SKILL.md +2 -0
- package/dist/skills/baxian-research/SKILL.md +1 -1
- package/dist/skills/baxian-signals/SKILL.md +19 -4
- package/dist/skills/baxian-task-check/SKILL.md +4 -2
- package/dist/state/agent-store.d.ts +5 -1
- package/dist/state/agent-store.d.ts.map +1 -1
- package/dist/state/agent-store.js +56 -17
- package/dist/state/agent-store.js.map +1 -1
- package/dist/state/lock.d.ts +9 -0
- package/dist/state/lock.d.ts.map +1 -1
- package/dist/state/lock.js +24 -1
- package/dist/state/lock.js.map +1 -1
- package/dist/state/task-store.js +4 -4
- package/dist/state/task-store.js.map +1 -1
- package/dist/terminal/attach.d.ts +10 -1
- package/dist/terminal/attach.d.ts.map +1 -1
- package/dist/terminal/attach.js +41 -20
- package/dist/terminal/attach.js.map +1 -1
- package/dist/terminal/stream-ws.d.ts.map +1 -1
- package/dist/terminal/stream-ws.js +27 -0
- package/dist/terminal/stream-ws.js.map +1 -1
- package/dist/web/assets/{index-DfcbGriX.css → index-BPv6vv3U.css} +1 -1
- package/dist/web/assets/index-CUJtd0bC.js +16 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DskVWe7e.js +0 -13
package/dist/event/handlers.js
CHANGED
|
@@ -3,6 +3,8 @@ import { ackRevisionKey } from '../platform/markers.js';
|
|
|
3
3
|
import { SHA_HEX_SOURCE } from '../platform/types.js';
|
|
4
4
|
import { BRANCH_PREFIX, TASK_TERMINAL_STATUS_SET, isRecord, isSpecStagePhase, isValidBranchName, } from '../shared/index.js';
|
|
5
5
|
import { DispatchTerminalError, EnsureSessionError, isRecoverableQaDispatchHold } from '../agent/manager.js';
|
|
6
|
+
import { ReplNotReadyError } from '../agent/tmux.js';
|
|
7
|
+
import { DirtyWorkdirError } from '../agent/branch.js';
|
|
6
8
|
// 平台 SHA 文法单点(spec §5.3:7-64 位 hex);legacy github 值恒 40 位,放宽为 additive
|
|
7
9
|
const HEAD_SHA_RE = new RegExp(`^${SHA_HEX_SOURCE}$`);
|
|
8
10
|
function validHeadSha(value) {
|
|
@@ -509,7 +511,7 @@ async function recoverOneGitPendingTask(bus, manager, task) {
|
|
|
509
511
|
await dispatchDevPostApproveCheck(bus, manager, task, approvedHead, { redispatchCount: nextCount });
|
|
510
512
|
}
|
|
511
513
|
}
|
|
512
|
-
async function releaseQaAfterSkippedRollback(manager, taskId, qaId, logPrefix) {
|
|
514
|
+
async function releaseQaAfterSkippedRollback(manager, taskId, qaId, logPrefix, ownAcquire = {}) {
|
|
513
515
|
const now = await manager.getTask(taskId);
|
|
514
516
|
if (now && !TASK_TERMINAL_STATUS_SET.has(now.status)) {
|
|
515
517
|
// pass 被并发接管:QA 已被接管方 re-acquire,释放会误清其绑定;
|
|
@@ -518,8 +520,9 @@ async function releaseQaAfterSkippedRollback(manager, taskId, qaId, logPrefix) {
|
|
|
518
520
|
await manager.rearmPhaseSignalForCurrentPass(taskId);
|
|
519
521
|
return;
|
|
520
522
|
}
|
|
521
|
-
// 终态/已删:没有接管方持有这只 QA,而 cancel 清理可能没见到刚写入的 qaAgentId
|
|
522
|
-
|
|
523
|
+
// 终态/已删:没有接管方持有这只 QA,而 cancel 清理可能没见到刚写入的 qaAgentId。
|
|
524
|
+
// 仍钉住本次 acquire 世代——本 handler 的每一条失败释放都只清自己那一代
|
|
525
|
+
await manager.releaseAgentForTask(qaId, taskId, 'idle', ownAcquire).catch(err => {
|
|
523
526
|
console.error(`${logPrefix} releaseAgentForTask(QA=${qaId}) after terminal-skip rollback failed:`, err);
|
|
524
527
|
return false;
|
|
525
528
|
});
|
|
@@ -563,6 +566,12 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
563
566
|
...(anchorAtDispatch ? { reviewHeadAnchorSha: anchorAtDispatch } : {}),
|
|
564
567
|
reviewDispatchedAt: new Date().toISOString(),
|
|
565
568
|
signalToken: createSignalToken(),
|
|
569
|
+
// 这些起点的轮次在 prompt 确认送达后才计(consumeReviewRoundIntent);持久化「未计」意图,
|
|
570
|
+
// 供 busy pending/可恢复 hold/进程重启后的任何补派方恰好补计一次
|
|
571
|
+
reviewRoundPending: (taskBeforeTransition.status === 'in_progress'
|
|
572
|
+
|| taskBeforeTransition.status === 'approved'
|
|
573
|
+
|| taskBeforeTransition.status === 'merge-ready') ? true
|
|
574
|
+
: (taskBeforeTransition.status === 'review' ? taskBeforeTransition.reviewRoundPending : undefined),
|
|
566
575
|
// pair 与新轮可见性原子(spec §7):transition 后到 rotate 的窗口内旧 pair 不得授权本轮
|
|
567
576
|
...(taskBeforeTransition.reviewMode === 'git'
|
|
568
577
|
? { ...manager.mintReviewTokenPair(), reviewDispatchPending: true }
|
|
@@ -571,7 +580,6 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
571
580
|
if (!result)
|
|
572
581
|
return;
|
|
573
582
|
const { task: transitioned, previousStatus } = result;
|
|
574
|
-
const expectedRound = transitioned.reviewRound;
|
|
575
583
|
await manager.clearPostApproveCompletion(transitioned.id);
|
|
576
584
|
let devAlreadyWaiting = false;
|
|
577
585
|
if (previousStatus === 'approved' || previousStatus === 'merge-ready') {
|
|
@@ -589,14 +597,34 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
589
597
|
return;
|
|
590
598
|
}
|
|
591
599
|
}
|
|
600
|
+
// review 起点且首评从未送达(round=0 + 未计轮 intent 在场)仍是首评:不得换成 recheck prompt/skill
|
|
601
|
+
const deferredFirstReview = previousStatus === 'review'
|
|
602
|
+
&& taskBeforeTransition.reviewRound === 0
|
|
603
|
+
&& taskBeforeTransition.reviewRoundPending === true;
|
|
604
|
+
const qaPhase = previousStatus === 'in_progress' || deferredFirstReview ? 'review' : 'recheck';
|
|
592
605
|
if (previousStatus === 'review' && transitioned.qaAgentId) {
|
|
593
606
|
const released = await manager
|
|
594
|
-
.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle')
|
|
607
|
+
.releaseAgentForTask(transitioned.qaAgentId, transitioned.id, 'idle', { deferWhenBusy: true })
|
|
595
608
|
.catch(err => {
|
|
596
609
|
console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${transitioned.qaAgentId}) for review→review push failed:`, err);
|
|
597
610
|
return false;
|
|
598
611
|
});
|
|
599
612
|
if (!released) {
|
|
613
|
+
// 忙碌拒绝的形状:QA 仍绑定本任务且未落 hold(release 对 REPL 忙碌不再写 hold)。
|
|
614
|
+
// 这是「欠一次投递」而非故障:登记 pending,由对账在 pane 空闲后按标准入口补派
|
|
615
|
+
const qaAfter = await manager.getAgentState(transitioned.qaAgentId).catch(() => null);
|
|
616
|
+
const deferrable = qaAfter?.taskId === transitioned.id && qaAfter.status !== 'awaiting_human';
|
|
617
|
+
if (deferrable && transitioned.signalToken !== undefined) {
|
|
618
|
+
manager.registerPendingDispatchRetry(transitioned.id, {
|
|
619
|
+
kind: 'qa-recheck',
|
|
620
|
+
agentId: transitioned.qaAgentId,
|
|
621
|
+
signalToken: transitioned.signalToken,
|
|
622
|
+
qaPhase,
|
|
623
|
+
});
|
|
624
|
+
console.warn(`[EventHandler] pr.updated QA ${qaPhase} deferred for task=${transitioned.id}: QA still busy on the ` +
|
|
625
|
+
`previous turn; reconciler will redispatch when idle`);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
600
628
|
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
601
629
|
phase: 'qa-release-failed-cannot-recheck',
|
|
602
630
|
qaAgentId: transitioned.qaAgentId,
|
|
@@ -604,10 +632,6 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
604
632
|
return;
|
|
605
633
|
}
|
|
606
634
|
}
|
|
607
|
-
const qaPhase = previousStatus === 'fixing' || previousStatus === 'review'
|
|
608
|
-
|| previousStatus === 'approved' || previousStatus === 'merge-ready'
|
|
609
|
-
? 'recheck'
|
|
610
|
-
: 'review';
|
|
611
635
|
const qaId = transitioned.qaAgentId;
|
|
612
636
|
if (!qaId) {
|
|
613
637
|
if (!devAlreadyWaiting && !(await manager.markAgentWaiting(agentId, transitioned.id))) {
|
|
@@ -627,7 +651,12 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
627
651
|
});
|
|
628
652
|
return;
|
|
629
653
|
}
|
|
630
|
-
|
|
654
|
+
// 锁代由 acquire 原子交出(事后重读只会读到 successor 的代):派发放弃时只释放自己这一代,
|
|
655
|
+
// successor 若已重新 acquire 同一 QA(同 task 同 agent,绕过 release 的 taskId 校验)不得被误清
|
|
656
|
+
let ownLockToken;
|
|
657
|
+
const acquired = await manager.acquireAgentForTask(qa.id, transitioned.id, qaPhase, {
|
|
658
|
+
onAcquired: (lockToken) => { ownLockToken = lockToken; },
|
|
659
|
+
});
|
|
631
660
|
if (!acquired) {
|
|
632
661
|
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
633
662
|
phase: 'qa-acquire-failed',
|
|
@@ -636,6 +665,7 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
636
665
|
});
|
|
637
666
|
return;
|
|
638
667
|
}
|
|
668
|
+
const ownAcquire = ownLockToken !== undefined ? { expectedLockToken: ownLockToken } : {};
|
|
639
669
|
const { token: dispatchToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, qa.id, ['pr-approved', 'pr-changes-requested']);
|
|
640
670
|
if (!armed) {
|
|
641
671
|
console.warn(`[EventHandler] pr.updated verdict watcher failed to arm for task=${transitioned.id} (${qaPhase}); rolling back recheck dispatch`);
|
|
@@ -646,16 +676,21 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
646
676
|
signalToken: taskBeforeTransition.signalToken,
|
|
647
677
|
reviewHeadAnchorSha: taskBeforeTransition.reviewHeadAnchorSha,
|
|
648
678
|
reviewDispatchedAt: taskBeforeTransition.reviewDispatchedAt,
|
|
679
|
+
// git 回滚保留 durable pending 供 sweep 重试:计轮 intent 必须同持(transition 写入值),
|
|
680
|
+
// 否则最终送达的首评按 stale intent 少计一轮
|
|
681
|
+
reviewRoundPending: taskBeforeTransition.reviewMode === 'git'
|
|
682
|
+
? transitioned.reviewRoundPending
|
|
683
|
+
: taskBeforeTransition.reviewRoundPending,
|
|
649
684
|
...(taskBeforeTransition.reviewMode === 'git'
|
|
650
685
|
? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
|
|
651
686
|
: {}),
|
|
652
687
|
}, { expect: { status: 'review', signalToken: dispatchToken }, rearmSkipSnapshot });
|
|
653
688
|
if (!rolledBack) {
|
|
654
|
-
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated arm-failure');
|
|
689
|
+
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated arm-failure', ownAcquire);
|
|
655
690
|
return;
|
|
656
691
|
}
|
|
657
692
|
}
|
|
658
|
-
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle').catch(err => {
|
|
693
|
+
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire).catch(err => {
|
|
659
694
|
console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${qa.id}) after arm-failure rollback failed:`, err);
|
|
660
695
|
return false;
|
|
661
696
|
});
|
|
@@ -669,7 +704,9 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
669
704
|
let started = false;
|
|
670
705
|
let dispatchErr = null;
|
|
671
706
|
try {
|
|
672
|
-
started = await manager.startSession(transitioned.id, qa.id, qaPhase
|
|
707
|
+
started = await manager.startSession(transitioned.id, qa.id, qaPhase, {
|
|
708
|
+
dispatchPassToken: dispatchToken,
|
|
709
|
+
});
|
|
673
710
|
}
|
|
674
711
|
catch (err) {
|
|
675
712
|
dispatchErr = err;
|
|
@@ -680,6 +717,11 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
680
717
|
if (dispatchErr instanceof DispatchTerminalError) {
|
|
681
718
|
await manager.failTaskForDispatchError(transitioned.id, qaPhase, qa.id, dispatchErr);
|
|
682
719
|
}
|
|
720
|
+
else if (dispatchErr instanceof EnsureSessionError && dispatchErr.partial.busyPending) {
|
|
721
|
+
// 遇忙不是故障:pass 保持 armed、QA 保持绑定,pending 登记已在 startSession 内完成,交对账补派
|
|
722
|
+
console.warn(`[EventHandler] pr.updated QA ${qaPhase} deferred for task=${transitioned.id}: QA pane busy; ` +
|
|
723
|
+
`reconciler will redispatch when idle`);
|
|
724
|
+
}
|
|
683
725
|
else if (dispatchErr instanceof EnsureSessionError && dispatchErr.partial.handled) {
|
|
684
726
|
// handled 不止代表可恢复派发 hold(checkout-cleanup-failed / dialog 等也走这里),指引须按实际相位分流
|
|
685
727
|
const qaState = await manager.getAgentState(qa.id).catch((stateErr) => {
|
|
@@ -707,23 +749,27 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
707
749
|
signalToken: taskBeforeTransition.signalToken,
|
|
708
750
|
reviewHeadAnchorSha: taskBeforeTransition.reviewHeadAnchorSha,
|
|
709
751
|
reviewDispatchedAt: taskBeforeTransition.reviewDispatchedAt,
|
|
752
|
+
// git 保留 durable pending 即保留计轮 intent(同 arm-failure 站点)
|
|
753
|
+
reviewRoundPending: taskBeforeTransition.reviewMode === 'git'
|
|
754
|
+
? transitioned.reviewRoundPending
|
|
755
|
+
: taskBeforeTransition.reviewRoundPending,
|
|
710
756
|
...(taskBeforeTransition.reviewMode === 'git'
|
|
711
757
|
? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
|
|
712
758
|
: {}),
|
|
713
759
|
}, { expect: { status: 'review', signalToken: dispatchToken }, rearmSkipSnapshot });
|
|
714
760
|
if (rolledBack) {
|
|
715
|
-
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle')
|
|
761
|
+
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire)
|
|
716
762
|
.catch(err => {
|
|
717
763
|
console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${qa.id}) after start-not-true failed:`, err);
|
|
718
764
|
return false;
|
|
719
765
|
});
|
|
720
766
|
}
|
|
721
767
|
else {
|
|
722
|
-
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated recheck');
|
|
768
|
+
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.updated recheck', ownAcquire);
|
|
723
769
|
}
|
|
724
770
|
}
|
|
725
771
|
else {
|
|
726
|
-
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle')
|
|
772
|
+
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire)
|
|
727
773
|
.catch(err => {
|
|
728
774
|
console.error(`[EventHandler] pr.updated releaseAgentForTask(QA=${qa.id}) after start-not-true failed:`, err);
|
|
729
775
|
return false;
|
|
@@ -746,9 +792,9 @@ async function handlePrCodePush(bus, manager, event) {
|
|
|
746
792
|
if (taskBeforeTransition.reviewMode === 'git') {
|
|
747
793
|
await manager.clearReviewDispatchPending(transitioned.id, dispatchToken);
|
|
748
794
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
795
|
+
// 送达确认后按本次 pass CAS 消费未计轮 intent:flag 本身已编码「哪些起点开新轮」
|
|
796
|
+
// (review→review 的既计轮 recheck 无 flag 即天然 no-op),successor 换代则留给它自己消费
|
|
797
|
+
await manager.consumeReviewRoundIntent(transitioned.id, dispatchToken);
|
|
752
798
|
const ok = devAlreadyWaiting || (await manager.markAgentWaiting(agentId, transitioned.id));
|
|
753
799
|
if (!ok) {
|
|
754
800
|
await manager.markAwaitingHuman(qa.id, 'dev-wait-gate-failed-after-qa-started', `QA review for task ${transitioned.id} started but dev wait-gate failed; QA prompt may still be running, needs operator decision.`, { expectedTaskId: transitioned.id }).catch(err => {
|
|
@@ -777,10 +823,15 @@ async function redispatchReviewForStaleVerdict(manager, task) {
|
|
|
777
823
|
try {
|
|
778
824
|
// fromStatus/expectSignalToken 在 dispatch 锁内复核:本地快照到这里的窗口里任务可能已离开 review
|
|
779
825
|
// 或被并发 dispatcher 换代 pass,不能靠上面的预检
|
|
826
|
+
const entry = manager.getPendingDispatchRetry(task.id);
|
|
827
|
+
const qaPhase = entry?.kind === 'qa-recheck' && entry.signalToken === task.signalToken && entry.qaPhase !== undefined
|
|
828
|
+
? entry.qaPhase
|
|
829
|
+
: (task.reviewRound === 0 ? 'review' : undefined);
|
|
780
830
|
await manager.dispatchReviewToQa(task.id, {
|
|
781
831
|
fromStatus: ['review'],
|
|
782
|
-
bumpRound:
|
|
832
|
+
bumpRound: task.reviewRoundPending === true,
|
|
783
833
|
expectSignalToken: task.signalToken,
|
|
834
|
+
...(qaPhase !== undefined ? { qaPhase } : {}),
|
|
784
835
|
});
|
|
785
836
|
}
|
|
786
837
|
catch (err) {
|
|
@@ -833,9 +884,17 @@ async function handleReviewApproval(bus, manager, task, reviewedHeadSha, current
|
|
|
833
884
|
fromStatus: ['review'],
|
|
834
885
|
// 校验后提交前的并发换代(push/重派轮换 token+pair)使旧 APPROVE 失效(spec §7)
|
|
835
886
|
...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
|
|
836
|
-
},
|
|
837
|
-
|
|
838
|
-
|
|
887
|
+
}, {
|
|
888
|
+
...prPatch,
|
|
889
|
+
...provenanceFields,
|
|
890
|
+
...gitHeadPatch(task, anchor.headSha),
|
|
891
|
+
// 崩溃在「prompt 已送达、bump 未落盘」窗口时 pending 轮次由 verdict 消费补计
|
|
892
|
+
reviewRound: Math.max(1, task.reviewRound + (task.reviewRoundPending === true ? 1 : 0)),
|
|
893
|
+
reviewRoundPending: undefined,
|
|
894
|
+
// verdict 即送达证据:durable pending 随 transition 原子消费,封死「送达后 verdict 抢先、
|
|
895
|
+
// startSession 的 clear 因 status/token 已变而拒绝」留下 stale 凭据的竞态
|
|
896
|
+
reviewDispatchPending: undefined,
|
|
897
|
+
});
|
|
839
898
|
if (!result)
|
|
840
899
|
return;
|
|
841
900
|
const { task: transitioned } = result;
|
|
@@ -874,7 +933,7 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
874
933
|
});
|
|
875
934
|
return;
|
|
876
935
|
}
|
|
877
|
-
const reviewedRound = task.reviewRound ===
|
|
936
|
+
const reviewedRound = Math.max(1, task.reviewRound + (task.reviewRoundPending === true ? 1 : 0));
|
|
878
937
|
const nextRound = reviewedRound + 1;
|
|
879
938
|
if (task.status === 'approved' && nextRound <= manager.getConfig().review.rounds) {
|
|
880
939
|
const devState = await manager.getAgentState(task.agentId);
|
|
@@ -919,7 +978,15 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
919
978
|
fromStatus: ['review', 'approved', 'merge-ready'],
|
|
920
979
|
// fail 方向同样按入口代际 CAS(spec §7):await 窗口内 push/重派换代后旧 RC 不得推进新轮
|
|
921
980
|
...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
|
|
922
|
-
}, {
|
|
981
|
+
}, {
|
|
982
|
+
...prPatch,
|
|
983
|
+
...gitHeadPatch(task, anchor.headSha),
|
|
984
|
+
reviewRound: nextRound,
|
|
985
|
+
reviewRoundPending: undefined,
|
|
986
|
+
// verdict 即送达证据(同 APPROVE):不消费会让 sweep 在 fixing 态按 stale pending 把任务拽回 review
|
|
987
|
+
reviewDispatchPending: undefined,
|
|
988
|
+
fixDispatchedAt: new Date().toISOString(),
|
|
989
|
+
});
|
|
923
990
|
if (!result)
|
|
924
991
|
return;
|
|
925
992
|
const { task: transitioned, previousStatus } = result;
|
|
@@ -947,7 +1014,7 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
947
1014
|
});
|
|
948
1015
|
return;
|
|
949
1016
|
}
|
|
950
|
-
const { armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed');
|
|
1017
|
+
const { token: fixToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, transitioned.agentId, 'pr-fixed');
|
|
951
1018
|
if (!armed) {
|
|
952
1019
|
console.warn(`[EventHandler] REQUEST_CHANGES pr-fixed watcher failed to arm for task=${transitioned.id}; holding dev (not dispatching fix)`);
|
|
953
1020
|
await manager.markAwaitingHuman(transitioned.agentId, 'signal-arm-failed:pr-fixed', 'pr-fixed watcher failed to arm; the fix was not dispatched (its completion signal would have no consumer). Cancel the task or delete the agent to retry.', { expectedTaskId: transitioned.id });
|
|
@@ -956,7 +1023,13 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
956
1023
|
let resumed = false;
|
|
957
1024
|
let dispatchErr = null;
|
|
958
1025
|
try {
|
|
959
|
-
resumed = await manager.continueSession(transitioned.id, transitioned.agentId, 'fix'
|
|
1026
|
+
resumed = await manager.continueSession(transitioned.id, transitioned.agentId, 'fix', {
|
|
1027
|
+
signalToken: fixToken,
|
|
1028
|
+
guardBeforeInject: async () => {
|
|
1029
|
+
const freshTask = await manager.getTask(transitioned.id);
|
|
1030
|
+
return freshTask?.status === 'fixing' && freshTask.signalToken === fixToken;
|
|
1031
|
+
},
|
|
1032
|
+
});
|
|
960
1033
|
}
|
|
961
1034
|
catch (err) {
|
|
962
1035
|
dispatchErr = err;
|
|
@@ -969,15 +1042,28 @@ async function handleReviewRequestChanges(bus, manager, task, reviewedHeadSha, c
|
|
|
969
1042
|
await manager.failTaskForDispatchError(transitioned.id, 'fix', transitioned.agentId, dispatchErr);
|
|
970
1043
|
}
|
|
971
1044
|
else {
|
|
972
|
-
await manager.markAgentWaiting(transitioned.agentId, transitioned.id)
|
|
1045
|
+
const parked = await manager.markAgentWaiting(transitioned.agentId, transitioned.id)
|
|
973
1046
|
.catch(err => {
|
|
974
1047
|
console.error(`[EventHandler] REQUEST_CHANGES markAgentWaiting(dev=${transitioned.agentId}) rollback failed:`, err);
|
|
975
1048
|
return false;
|
|
976
1049
|
});
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1050
|
+
if (parked && (dispatchErr instanceof ReplNotReadyError || dispatchErr instanceof DirtyWorkdirError)) {
|
|
1051
|
+
// dev 回合在途(忙碌/未提交改动)是常态而非故障:登记待补派,回合结束后由对账 re-continue。
|
|
1052
|
+
// 停驻失败说明绑定/锁已不受控,pending 无法证明还有可承接的 session,必须走人工告警
|
|
1053
|
+
manager.registerPendingDispatchRetry(transitioned.id, {
|
|
1054
|
+
kind: 'dev-fix',
|
|
1055
|
+
agentId: transitioned.agentId,
|
|
1056
|
+
signalToken: fixToken,
|
|
1057
|
+
});
|
|
1058
|
+
console.warn(`[EventHandler] REQUEST_CHANGES fix deferred for task=${transitioned.id}: dev pane busy or workdir dirty ` +
|
|
1059
|
+
`(${dispatchErr.name}); reconciler will re-continue when idle`);
|
|
1060
|
+
}
|
|
1061
|
+
else {
|
|
1062
|
+
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1063
|
+
phase: 'fix-resume-failed',
|
|
1064
|
+
reviewRound: transitioned.reviewRound,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
981
1067
|
}
|
|
982
1068
|
}
|
|
983
1069
|
}
|
|
@@ -985,7 +1071,7 @@ async function handleMaxRounds(bus, manager, task, prPatch, reviewedRound) {
|
|
|
985
1071
|
const result = await manager.transitionTaskStatus(task.id, 'max_rounds', {
|
|
986
1072
|
fromStatus: ['review', 'approved', 'merge-ready'],
|
|
987
1073
|
...(task.reviewMode === 'git' ? { expectSignalToken: task.signalToken } : {}),
|
|
988
|
-
}, { ...prPatch, reviewRound: reviewedRound });
|
|
1074
|
+
}, { ...prPatch, reviewRound: reviewedRound, reviewRoundPending: undefined, reviewDispatchPending: undefined });
|
|
989
1075
|
if (!result)
|
|
990
1076
|
return;
|
|
991
1077
|
const { task: transitioned } = result;
|
|
@@ -1230,6 +1316,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1230
1316
|
...(reconciledBranch ? { branch: reconciledBranch } : {}),
|
|
1231
1317
|
...gitAdoptionFields,
|
|
1232
1318
|
reviewDispatchedAt: new Date().toISOString(),
|
|
1319
|
+
reviewRoundPending: taskBeforeTransition?.status === 'in_progress' ? true : undefined,
|
|
1233
1320
|
signalToken: createSignalToken(),
|
|
1234
1321
|
...(isGit ? { ...manager.mintReviewTokenPair(), reviewDispatchPending: true } : {}),
|
|
1235
1322
|
});
|
|
@@ -1253,7 +1340,6 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1253
1340
|
console.log(`[EventHandler] pr.created reconciled branch for task ${event.taskId}: ` +
|
|
1254
1341
|
`${taskBeforeTransition?.branch} → ${reconciledBranch}`);
|
|
1255
1342
|
}
|
|
1256
|
-
const expectedRound = transitioned.reviewRound;
|
|
1257
1343
|
const qaId = transitioned.qaAgentId;
|
|
1258
1344
|
if (!qaId) {
|
|
1259
1345
|
const ok = await manager.markAgentWaiting(event.agentId, transitioned.id);
|
|
@@ -1273,7 +1359,10 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1273
1359
|
});
|
|
1274
1360
|
return;
|
|
1275
1361
|
}
|
|
1276
|
-
|
|
1362
|
+
let ownLockToken;
|
|
1363
|
+
const acquired = await manager.acquireAgentForTask(qa.id, transitioned.id, 'review', {
|
|
1364
|
+
onAcquired: (lockToken) => { ownLockToken = lockToken; },
|
|
1365
|
+
});
|
|
1277
1366
|
if (!acquired) {
|
|
1278
1367
|
await emitIntervention(bus, transitioned.projectId, transitioned.agentId, transitioned.id, {
|
|
1279
1368
|
phase: 'qa-acquire-failed',
|
|
@@ -1281,6 +1370,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1281
1370
|
});
|
|
1282
1371
|
return;
|
|
1283
1372
|
}
|
|
1373
|
+
const ownAcquire = ownLockToken !== undefined ? { expectedLockToken: ownLockToken } : {};
|
|
1284
1374
|
const { token: dispatchToken, armed } = await manager.rotateAndSetupPhaseSignal(transitioned.id, qa.id, ['pr-approved', 'pr-changes-requested']);
|
|
1285
1375
|
if (!armed) {
|
|
1286
1376
|
console.warn(`[EventHandler] pr.created verdict watcher failed to arm for task=${transitioned.id}; rolling back review dispatch`);
|
|
@@ -1289,15 +1379,19 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1289
1379
|
signalToken: taskBeforeTransition?.signalToken,
|
|
1290
1380
|
reviewHeadAnchorSha: taskBeforeTransition?.reviewHeadAnchorSha,
|
|
1291
1381
|
reviewDispatchedAt: taskBeforeTransition?.reviewDispatchedAt,
|
|
1382
|
+
// git 保留 durable pending 即保留计轮 intent(同 pr.updated 回滚站点)
|
|
1383
|
+
reviewRoundPending: taskBeforeTransition?.reviewMode === 'git'
|
|
1384
|
+
? transitioned.reviewRoundPending
|
|
1385
|
+
: taskBeforeTransition?.reviewRoundPending,
|
|
1292
1386
|
...(taskBeforeTransition?.reviewMode === 'git'
|
|
1293
1387
|
? { restorePair: true, passToken: taskBeforeTransition.passToken, failToken: taskBeforeTransition.failToken }
|
|
1294
1388
|
: {}),
|
|
1295
1389
|
}, { expect: { status: 'review', signalToken: dispatchToken } });
|
|
1296
1390
|
if (!rolledBack) {
|
|
1297
|
-
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.created arm-failure');
|
|
1391
|
+
await releaseQaAfterSkippedRollback(manager, transitioned.id, qa.id, '[EventHandler] pr.created arm-failure', ownAcquire);
|
|
1298
1392
|
return;
|
|
1299
1393
|
}
|
|
1300
|
-
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle').catch(err => {
|
|
1394
|
+
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire).catch(err => {
|
|
1301
1395
|
console.error(`[EventHandler] pr.created releaseAgentForTask(QA=${qa.id}) after arm-failure rollback failed:`, err);
|
|
1302
1396
|
return false;
|
|
1303
1397
|
});
|
|
@@ -1310,7 +1404,9 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1310
1404
|
let started = false;
|
|
1311
1405
|
let dispatchErr = null;
|
|
1312
1406
|
try {
|
|
1313
|
-
started = await manager.startSession(transitioned.id, qa.id, 'review'
|
|
1407
|
+
started = await manager.startSession(transitioned.id, qa.id, 'review', {
|
|
1408
|
+
dispatchPassToken: dispatchToken,
|
|
1409
|
+
});
|
|
1314
1410
|
}
|
|
1315
1411
|
catch (err) {
|
|
1316
1412
|
dispatchErr = err;
|
|
@@ -1325,7 +1421,7 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1325
1421
|
else if (dispatchErr instanceof EnsureSessionError && dispatchErr.partial.handled) {
|
|
1326
1422
|
}
|
|
1327
1423
|
else {
|
|
1328
|
-
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle')
|
|
1424
|
+
await manager.releaseAgentForTask(qa.id, transitioned.id, 'idle', ownAcquire)
|
|
1329
1425
|
.catch(err => {
|
|
1330
1426
|
console.error(`[EventHandler] pr.created releaseAgentForTask(QA=${qa.id}) after start-not-true failed:`, err);
|
|
1331
1427
|
return false;
|
|
@@ -1337,9 +1433,10 @@ export function registerEventHandlers(bus, manager) {
|
|
|
1337
1433
|
}
|
|
1338
1434
|
return;
|
|
1339
1435
|
}
|
|
1340
|
-
if (
|
|
1341
|
-
await manager.
|
|
1436
|
+
if (taskBeforeTransition?.reviewMode === 'git') {
|
|
1437
|
+
await manager.clearReviewDispatchPending(transitioned.id, dispatchToken);
|
|
1342
1438
|
}
|
|
1439
|
+
await manager.consumeReviewRoundIntent(transitioned.id, dispatchToken);
|
|
1343
1440
|
const ok = await manager.markAgentWaiting(event.agentId, transitioned.id);
|
|
1344
1441
|
if (!ok) {
|
|
1345
1442
|
await manager.markAwaitingHuman(qa.id, 'dev-wait-gate-failed-after-qa-started', `QA review for task ${transitioned.id} started but dev wait-gate failed; QA prompt may still be running, needs operator decision.`, { expectedTaskId: transitioned.id }).catch(err => {
|