diff --git a/src/main/claude/claude-structured-prompt-ownership.ts b/src/main/claude/claude-structured-prompt-ownership.ts index 7694fe3d8eb..569a519c69b 100644 --- a/src/main/claude/claude-structured-prompt-ownership.ts +++ b/src/main/claude/claude-structured-prompt-ownership.ts @@ -77,8 +77,14 @@ export async function cancelClaudeStructuredTurn(input: { // The translator owns turn identity. A session with no journal has published no // turn row for a client to name, so it holds no identity this request can contradict. const ownsRequestedTurn = (): boolean => { - const currentTurnId = session.translator?.currentTurnId ?? null - return currentTurnId === null || currentTurnId === request.turnId + const translator = session.translator + if (!translator) { + return session.dispatchSequence === 0 + } + const currentTurnId = translator.currentTurnId + return currentTurnId === null + ? session.dispatchSequence === 0 + : currentTurnId === request.turnId } const isCurrent = (): boolean => sessions.get(request.sessionId) === session && diff --git a/src/main/claude/claude-turn-ownership.test.ts b/src/main/claude/claude-turn-ownership.test.ts index 05bd15bc630..7616e011989 100644 --- a/src/main/claude/claude-turn-ownership.test.ts +++ b/src/main/claude/claude-turn-ownership.test.ts @@ -109,6 +109,26 @@ describe('Claude turn ownership', () => { expect(connection.calls.some((call) => call.subtype === 'interrupt')).toBe(true) }) + it('refuses a stale id after the owned turn settles', async () => { + const claude = fakeClaude({ replayUuid: 'echo-turn' }) + const { adapter, bodies, connection } = await acquiredWithJournal(claude) + + await adapter.dispatch({ + sessionId: 'session-1', + clientMessageId: 'client-1', + body: USER_MESSAGE, + fence: 7 + }) + expect(runningTurnId(bodies)).toBe('echo-turn') + completeTurn(connection, 'result-1') + expect(runningTurnId(bodies)).toBeNull() + + await expect( + adapter.cancelTurn({ sessionId: 'session-1', turnId: 'echo-turn', fence: 7 }) + ).resolves.toEqual({ cancelled: false }) + expect(connection.calls.some((call) => call.subtype === 'interrupt')).toBe(false) + }) + it('still stops an echo-opened turn', async () => { const claude = fakeClaude({ replayUuid: 'echo-turn' }) const { adapter, bodies, connection } = await acquiredWithJournal(claude)