fix(opencode): queue specific relay behind active relay
This commit is contained in:
parent
bc571f5fc7
commit
e016515024
2 changed files with 115 additions and 82 deletions
|
|
@ -21748,10 +21748,9 @@ export class TeamProvisioningService {
|
||||||
const relayKey = this.getOpenCodeMemberRelayKey(teamName, memberName);
|
const relayKey = this.getOpenCodeMemberRelayKey(teamName, memberName);
|
||||||
const existing = this.openCodeMemberInboxRelayInFlight.get(relayKey);
|
const existing = this.openCodeMemberInboxRelayInFlight.get(relayKey);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
const existingResult = await existing;
|
|
||||||
const onlyMessageId = options.onlyMessageId?.trim();
|
const onlyMessageId = options.onlyMessageId?.trim();
|
||||||
if (!onlyMessageId) {
|
if (!onlyMessageId) {
|
||||||
return existingResult;
|
return existing;
|
||||||
}
|
}
|
||||||
const inboxMessages = await this.inboxReader
|
const inboxMessages = await this.inboxReader
|
||||||
.getMessagesFor(teamName, memberName)
|
.getMessagesFor(teamName, memberName)
|
||||||
|
|
@ -21764,7 +21763,6 @@ export class TeamProvisioningService {
|
||||||
delivered: 1,
|
delivered: 1,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
lastDelivery: { delivered: true },
|
lastDelivery: { delivered: true },
|
||||||
diagnostics: existingResult.diagnostics,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!targetMessage) {
|
if (!targetMessage) {
|
||||||
|
|
@ -21782,6 +21780,29 @@ export class TeamProvisioningService {
|
||||||
diagnostics: [diagnostic],
|
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<OpenCodeMemberInboxRelayResult> => {
|
const work = (async (): Promise<OpenCodeMemberInboxRelayResult> => {
|
||||||
|
|
|
||||||
|
|
@ -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 service = new TeamProvisioningService();
|
||||||
const teamName = 'my-team';
|
const teamName = 'my-team';
|
||||||
hoisted.files.set(
|
try {
|
||||||
`/mock/teams/${teamName}/config.json`,
|
hoisted.files.set(
|
||||||
JSON.stringify({
|
`/mock/teams/${teamName}/config.json`,
|
||||||
name: teamName,
|
JSON.stringify({
|
||||||
projectPath: '/tmp/my-team',
|
name: teamName,
|
||||||
members: [
|
projectPath: '/tmp/my-team',
|
||||||
{ name: 'team-lead', agentType: 'team-lead' },
|
members: [
|
||||||
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
|
{ name: 'team-lead', agentType: 'team-lead' },
|
||||||
],
|
{ name: 'jack', role: 'developer', providerId: 'opencode', model: 'openrouter/test' },
|
||||||
})
|
],
|
||||||
);
|
})
|
||||||
seedMemberInbox(teamName, 'jack', [
|
);
|
||||||
{
|
seedMemberInbox(teamName, 'jack', [
|
||||||
from: 'bob',
|
{
|
||||||
to: 'jack',
|
from: 'bob',
|
||||||
text: 'Older watcher message.',
|
to: 'jack',
|
||||||
timestamp: '2026-02-23T17:00:00.000Z',
|
text: 'Older watcher message.',
|
||||||
read: false,
|
timestamp: '2026-02-23T17:00:00.000Z',
|
||||||
messageId: 'opencode-inflight-old',
|
read: false,
|
||||||
},
|
messageId: 'opencode-inflight-old',
|
||||||
]);
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const oldDeliveryStarted = createDeferred<void>();
|
const oldDeliveryStarted = createDeferred<void>();
|
||||||
const releaseOldDelivery = createDeferred<void>();
|
const releaseOldDelivery = createDeferred<void>();
|
||||||
const deliverSpy = vi
|
const deliverSpy = vi
|
||||||
.spyOn(service, 'deliverOpenCodeMemberMessage')
|
.spyOn(service, 'deliverOpenCodeMemberMessage')
|
||||||
.mockImplementation(async (_teamName, input) => {
|
.mockImplementation(async (_teamName, input) => {
|
||||||
if (input.messageId === 'opencode-inflight-old') {
|
if (input.messageId === 'opencode-inflight-old') {
|
||||||
oldDeliveryStarted.resolve(undefined);
|
oldDeliveryStarted.resolve(undefined);
|
||||||
await releaseOldDelivery.promise;
|
await releaseOldDelivery.promise;
|
||||||
}
|
}
|
||||||
return { delivered: true, diagnostics: [] };
|
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 expect(watcherRelay).resolves.toMatchObject({
|
||||||
await oldDeliveryStarted.promise;
|
attempted: 1,
|
||||||
seedMemberInbox(teamName, 'jack', [
|
delivered: 1,
|
||||||
{
|
});
|
||||||
from: 'bob',
|
expect(deliverSpy).toHaveBeenCalledTimes(1);
|
||||||
to: 'jack',
|
expect(deliverSpy).toHaveBeenCalledWith(
|
||||||
text: 'Older watcher message.',
|
teamName,
|
||||||
timestamp: '2026-02-23T17:00:00.000Z',
|
expect.objectContaining({ messageId: 'opencode-inflight-old' })
|
||||||
read: false,
|
);
|
||||||
messageId: 'opencode-inflight-old',
|
const rows = JSON.parse(
|
||||||
},
|
hoisted.files.get(`/mock/teams/${teamName}/inboxes/jack.json`) ?? '[]'
|
||||||
{
|
);
|
||||||
from: 'user',
|
expect(rows.map((row: { read?: boolean }) => row.read)).toEqual([true, false]);
|
||||||
to: 'jack',
|
} finally {
|
||||||
text: 'New UI message.',
|
vi.useRealTimers();
|
||||||
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]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats an already-read specific OpenCode inbox row as delivered for UI-send relay', async () => {
|
it('treats an already-read specific OpenCode inbox row as delivered for UI-send relay', async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue