fix: prevent duplicate messages on unconfirmed delivery and deduplicate review prompt

sendWithConfirmation now treats unconfirmed delivery as a soft success
since the message was already sent, preventing the dispatch hash from
staying stale and causing duplicate sends on the next poll cycle.

review-check command now sources its prompt from the lifecycle reaction
config instead of hardcoding it, keeping both paths aligned.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jayesh Sharma 2026-03-06 23:07:12 +05:30
parent a6c9dbd599
commit 77685a5d81
3 changed files with 19 additions and 7 deletions

View File

@ -12,7 +12,7 @@ interface ReviewInfo {
reviewDecision: string | null;
}
const REVIEW_FIX_PROMPT =
const DEFAULT_REVIEW_FIX_PROMPT =
"There are review comments on your PR. Check with `gh pr view --comments` and `gh api` for inline comments. Address each one, push fixes, and reply.";
async function checkPRReviews(
@ -74,6 +74,11 @@ export function registerReviewCheck(program: Command): void {
process.exit(1);
}
// Source prompt from lifecycle reaction config so review-check and
// the lifecycle worker stay aligned.
const reviewFixPrompt =
config.reactions["changes-requested"]?.message ?? DEFAULT_REVIEW_FIX_PROMPT;
const sm = await getSessionManager(config);
const sessions = await sm.list(projectId);
@ -129,7 +134,7 @@ export function registerReviewCheck(program: Command): void {
if (!opts.dryRun) {
try {
await sm.send(result.sessionId, REVIEW_FIX_PROMPT);
await sm.send(result.sessionId, reviewFixPrompt);
console.log(chalk.green(` -> Fix prompt sent`));
} catch (err) {
console.error(chalk.red(` -> Failed to send: ${err}`));

View File

@ -1279,7 +1279,7 @@ describe("send", () => {
);
});
it("throws when delivery cannot be confirmed", async () => {
it("resolves when delivery cannot be confirmed (message already sent)", async () => {
writeMetadata(sessionsDir, "app-1", {
worktree: "/tmp",
branch: "main",
@ -1291,9 +1291,11 @@ describe("send", () => {
vi.mocked(mockAgent.detectActivity).mockReturnValue("idle");
const sm = createSessionManager({ config, registry: mockRegistry });
await expect(sm.send("app-1", "Fix the CI failures")).rejects.toThrow(
"could not be confirmed",
);
// Should resolve without throwing — the message was already sent via
// sendMessage, so unconfirmed delivery is treated as a soft success
// to avoid duplicate dispatches on the next poll cycle.
await expect(sm.send("app-1", "Fix the CI failures")).resolves.toBeUndefined();
expect(mockRuntime.sendMessage).toHaveBeenCalled();
});
it("throws for nonexistent session", async () => {

View File

@ -1154,7 +1154,12 @@ export function createSessionManager(deps: SessionManagerDeps): SessionManager {
}
}
throw new Error(`Message to session ${sessionId} could not be confirmed`);
// Message was already sent via runtimePlugin.sendMessage above — if we
// cannot *confirm* delivery (e.g. agent is slow to show output), treat it
// as a soft success rather than throwing. Throwing here caused the caller
// to report failure, which prevented the dispatch-hash from updating and
// led to duplicate messages on the next poll cycle.
return;
};
let prepared = await prepareSession();