From e0165150245b512921414b810c2b020a8b036191 Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 14 May 2026 02:14:09 +0300 Subject: [PATCH] fix(opencode): queue specific relay behind active relay --- .../services/team/TeamProvisioningService.ts | 27 ++- .../team/TeamProvisioningServiceRelay.test.ts | 170 ++++++++++-------- 2 files changed, 115 insertions(+), 82 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index d3ad9445..b3312349 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -21748,10 +21748,9 @@ export class TeamProvisioningService { const relayKey = this.getOpenCodeMemberRelayKey(teamName, memberName); const existing = this.openCodeMemberInboxRelayInFlight.get(relayKey); if (existing) { - const existingResult = await existing; const onlyMessageId = options.onlyMessageId?.trim(); if (!onlyMessageId) { - return existingResult; + return existing; } const inboxMessages = await this.inboxReader .getMessagesFor(teamName, memberName) @@ -21764,7 +21763,6 @@ export class TeamProvisioningService { delivered: 1, failed: 0, lastDelivery: { delivered: true }, - diagnostics: existingResult.diagnostics, }; } if (!targetMessage) { @@ -21782,6 +21780,29 @@ export class TeamProvisioningService { diagnostics: [diagnostic], }; } + + const diagnostic = `opencode_inbox_relay_queued_behind_active_relay: ${relayKey}/${onlyMessageId}`; + this.scheduleOpenCodeMemberInboxDeliveryWake({ + teamName, + memberName, + messageId: onlyMessageId, + delayMs: 500, + }); + return { + relayed: 0, + attempted: 1, + delivered: 0, + failed: 0, + lastDelivery: { + delivered: true, + accepted: false, + responsePending: true, + queuedBehindMessageId: onlyMessageId, + reason: 'opencode_inbox_relay_queued_behind_active_relay', + diagnostics: [diagnostic], + }, + diagnostics: [diagnostic], + }; } const work = (async (): Promise => { diff --git a/test/main/services/team/TeamProvisioningServiceRelay.test.ts b/test/main/services/team/TeamProvisioningServiceRelay.test.ts index 94607291..7cfcee09 100644 --- a/test/main/services/team/TeamProvisioningServiceRelay.test.ts +++ b/test/main/services/team/TeamProvisioningServiceRelay.test.ts @@ -2706,90 +2706,102 @@ Messages: } }); - it('does not let an older in-flight OpenCode relay mask a specific UI-send message', async () => { + it('queues a specific OpenCode relay behind an active member relay without duplicate prompts', async () => { + vi.useFakeTimers(); const service = new TeamProvisioningService(); const teamName = 'my-team'; - hoisted.files.set( - `/mock/teams/${teamName}/config.json`, - JSON.stringify({ - name: teamName, - projectPath: '/tmp/my-team', - members: [ - { name: 'team-lead', agentType: 'team-lead' }, - { name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' }, - ], - }) - ); - seedMemberInbox(teamName, 'jack', [ - { - from: 'bob', - to: 'jack', - text: 'Older watcher message.', - timestamp: '2026-02-23T17:00:00.000Z', - read: false, - messageId: 'opencode-inflight-old', - }, - ]); + try { + hoisted.files.set( + `/mock/teams/${teamName}/config.json`, + JSON.stringify({ + name: teamName, + projectPath: '/tmp/my-team', + members: [ + { name: 'team-lead', agentType: 'team-lead' }, + { name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' }, + ], + }) + ); + seedMemberInbox(teamName, 'jack', [ + { + from: 'bob', + to: 'jack', + text: 'Older watcher message.', + timestamp: '2026-02-23T17:00:00.000Z', + read: false, + messageId: 'opencode-inflight-old', + }, + ]); - const oldDeliveryStarted = createDeferred(); - const releaseOldDelivery = createDeferred(); - const deliverSpy = vi - .spyOn(service, 'deliverOpenCodeMemberMessage') - .mockImplementation(async (_teamName, input) => { - if (input.messageId === 'opencode-inflight-old') { - oldDeliveryStarted.resolve(undefined); - await releaseOldDelivery.promise; - } - return { delivered: true, diagnostics: [] }; + const oldDeliveryStarted = createDeferred(); + const releaseOldDelivery = createDeferred(); + const deliverSpy = vi + .spyOn(service, 'deliverOpenCodeMemberMessage') + .mockImplementation(async (_teamName, input) => { + if (input.messageId === 'opencode-inflight-old') { + oldDeliveryStarted.resolve(undefined); + await releaseOldDelivery.promise; + } + return { delivered: true, diagnostics: [] }; + }); + + const watcherRelay = service.relayOpenCodeMemberInboxMessages(teamName, 'jack'); + await oldDeliveryStarted.promise; + seedMemberInbox(teamName, 'jack', [ + { + from: 'bob', + to: 'jack', + text: 'Older watcher message.', + timestamp: '2026-02-23T17:00:00.000Z', + read: false, + messageId: 'opencode-inflight-old', + }, + { + from: 'user', + to: 'jack', + text: 'New UI message.', + timestamp: '2026-02-23T17:00:01.000Z', + read: false, + messageId: 'opencode-inflight-new', + }, + ]); + + await expect( + service.relayOpenCodeMemberInboxMessages(teamName, 'jack', { + onlyMessageId: 'opencode-inflight-new', + source: 'ui-send', + deliveryMetadata: { replyRecipient: 'user' }, + }) + ).resolves.toMatchObject({ + attempted: 1, + delivered: 0, + failed: 0, + lastDelivery: { + delivered: true, + accepted: false, + responsePending: true, + queuedBehindMessageId: 'opencode-inflight-new', + reason: 'opencode_inbox_relay_queued_behind_active_relay', + }, }); + releaseOldDelivery.resolve(undefined); - const watcherRelay = service.relayOpenCodeMemberInboxMessages(teamName, 'jack'); - await oldDeliveryStarted.promise; - seedMemberInbox(teamName, 'jack', [ - { - from: 'bob', - to: 'jack', - text: 'Older watcher message.', - timestamp: '2026-02-23T17:00:00.000Z', - read: false, - messageId: 'opencode-inflight-old', - }, - { - from: 'user', - to: 'jack', - text: 'New UI message.', - timestamp: '2026-02-23T17:00:01.000Z', - read: false, - messageId: 'opencode-inflight-new', - }, - ]); - - const uiRelay = service.relayOpenCodeMemberInboxMessages(teamName, 'jack', { - onlyMessageId: 'opencode-inflight-new', - source: 'ui-send', - deliveryMetadata: { replyRecipient: 'user' }, - }); - releaseOldDelivery.resolve(undefined); - - await expect(watcherRelay).resolves.toMatchObject({ - attempted: 1, - delivered: 1, - }); - await expect(uiRelay).resolves.toMatchObject({ - attempted: 1, - delivered: 1, - failed: 0, - }); - expect(deliverSpy).toHaveBeenCalledWith( - teamName, - expect.objectContaining({ messageId: 'opencode-inflight-old' }) - ); - expect(deliverSpy).toHaveBeenCalledWith( - teamName, - expect.objectContaining({ messageId: 'opencode-inflight-new' }) - ); - const rows = JSON.parse(hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]'); - expect(rows.map((row: { read?: boolean }) => row.read)).toEqual([true, true]); + await expect(watcherRelay).resolves.toMatchObject({ + attempted: 1, + delivered: 1, + }); + expect(deliverSpy).toHaveBeenCalledTimes(1); + expect(deliverSpy).toHaveBeenCalledWith( + teamName, + expect.objectContaining({ messageId: 'opencode-inflight-old' }) + ); + const rows = JSON.parse( + hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]' + ); + expect(rows.map((row: { read?: boolean }) => row.read)).toEqual([true, false]); + } finally { + vi.useRealTimers(); + } }); it('treats an already-read specific OpenCode inbox row as delivered for UI-send relay', async () => {