diff --git a/src/main/claude/claude-structured-dispatch.ts b/src/main/claude/claude-structured-dispatch.ts index 80585df1323..c7cd733f504 100644 --- a/src/main/claude/claude-structured-dispatch.ts +++ b/src/main/claude/claude-structured-dispatch.ts @@ -78,11 +78,36 @@ function isClaudeUserMessageReplay(message: Record): boolean { return message.type === 'user' && message.parent_tool_use_id === null && message.isReplay === true } +let warnedAboutMissingReplayMarker = false + +/** A CLI that echoes without the marker cannot be told apart at runtime from a + * turn that simply had no boundary inside the ack budget: both just report + * "delivery unconfirmed". Say it once so the cause is diagnosable. */ +function warnIfEchoLacksReplayMarker(message: Record): void { + if ( + warnedAboutMissingReplayMarker || + message.type !== 'user' || + message.parent_tool_use_id !== null || + message.isReplay !== undefined || + message.tool_use_result !== undefined + ) { + return + } + warnedAboutMissingReplayMarker = true + console.warn( + '[claude-structured] user frame arrived without isReplay while a send was waiting; ' + + 'sends will report delivery unconfirmed until the CLI stamps the marker' + ) +} + export function resolveClaudeReplayWaiter( session: ClaudeSession, message: Record ): void { const isUserReplay = isClaudeUserMessageReplay(message) + if (!isUserReplay && session.dispatchWaiters.length > 0) { + warnIfEchoLacksReplayMarker(message) + } const isCompletedCommand = message.type === 'result' if ( (!isUserReplay && !isCompletedCommand) || diff --git a/src/main/claude/claude-structured-launch-resolution.test.ts b/src/main/claude/claude-structured-launch-resolution.test.ts index e04605b4830..cb09b3a1895 100644 --- a/src/main/claude/claude-structured-launch-resolution.test.ts +++ b/src/main/claude/claude-structured-launch-resolution.test.ts @@ -60,6 +60,10 @@ describe('claude structured launch resolution', () => { expect(first.args).toContain('--setting-sources') expect(first.args).toContain(CLAUDE_DEFAULT_SETTING_SOURCES.join(',')) expect(CLAUDE_STRUCTURED_BASE_ARGS).toContain('--verbose') + // Load-bearing: `resolveClaudeReplayWaiter` settles a send only on the + // `isReplay` marker this flag asks for. Drop it and every send blocks its + // whole ack budget and lands "delivery unconfirmed". + expect(CLAUDE_STRUCTURED_BASE_ARGS).toContain('--replay-user-messages') }) it('resumes the session and leaf at the durable chain head', async () => {