From 12efdefd1f95e441eaf8a34f5a7807b61e588167 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 25 Sep 2026 00:02:53 -0700 Subject: [PATCH] fix(native-chat): keep the owner-status read behind an in-flight attach The handoff removal dropped the per-session queue from `handoffStatus`, so a read landing mid-start reported the reservation (no owner) instead of the settled chat owner, and shipped desktop clients blocked worktree activation on it. The read is queued again, as it was before the removal. Co-Authored-By: Claude --- .../structured-agent-session-host.test.ts | 25 +++++++++++++++++++ .../structured-agent-session-host.ts | 13 ++++++---- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts index b980c4e422c..0deaf9d9140 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host.test.ts @@ -639,6 +639,31 @@ describe('restart', () => { }) }) + it('answers the owner status of a starting chat once its start settles', async () => { + await attach() + await reboot(async () => ({ outcome: 'pid-absent' })) + await host.restoreReadableSessions() + const started = Promise.withResolvers() + const release = Promise.withResolvers() + const settled = acquire.getMockImplementation() + if (!settled) { + throw new Error('missing acquire implementation') + } + acquire.mockImplementationOnce(async (input) => { + started.resolve() + await release.promise + return settled(input) + }) + + const hold = host.hold(SESSION, 'surface-1') + await started.promise + const status = host.handoffStatus(SESSION) + release.resolve() + await hold + + await expect(status).resolves.toMatchObject({ owner: 'native', stage: null }) + }) + it("keeps a session whose owner cannot be probed out of a live writer's hands", async () => { await attach() const held = store.getRecord(SESSION)?.lease.runtimeFence ?? 0 diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts index 8f29c6ec322..ba508b67bcf 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-host.ts @@ -271,11 +271,14 @@ export class StructuredAgentSessionHost { async handoffStatus(sessionId: string): Promise { this.requireSession(sessionId) - const record = this.deps.store.getRecord(sessionId) - if (!record) { - throw new Error('agent_session_identity_required') - } - return structuredAgentSessionOwnerStatus(record) + // Queued behind an in-flight attach, so a starting chat answers with its settled owner. + return this.serialize(sessionId, async () => { + const record = this.deps.store.getRecord(sessionId) + if (!record) { + throw new Error('agent_session_identity_required') + } + return structuredAgentSessionOwnerStatus(record) + }) } history: StructuredAgentSessionBackgroundTaskChannel['history'] = (request) =>