fix(member-work-sync): avoid self-blocking proof recovery

This commit is contained in:
777genius 2026-05-14 01:54:16 +03:00
parent 39c52c3847
commit d048113c1d
4 changed files with 99 additions and 1 deletions

View file

@ -485,6 +485,7 @@ export class MemberWorkSyncNudgeDispatcher {
memberName: item.memberName,
nowIso,
workSyncIntent: item.payload.workSyncIntent,
workSyncIntentKey: item.payload.workSyncIntentKey,
taskRefs: item.payload.taskRefs,
});
if (busy?.busy) {

View file

@ -205,6 +205,7 @@ export interface MemberWorkSyncBusySignalPort {
memberName: string;
nowIso: string;
workSyncIntent?: MemberWorkSyncOutboxItem['payload']['workSyncIntent'];
workSyncIntentKey?: MemberWorkSyncOutboxItem['payload']['workSyncIntentKey'];
taskRefs?: MemberWorkSyncOutboxItem['payload']['taskRefs'];
}): Promise<{ busy: boolean; reason?: string; retryAfterIso?: string }>;
}

View file

@ -12753,6 +12753,7 @@ export class TeamProvisioningService {
memberName: string;
nowIso: string;
workSyncIntent?: 'agenda_sync' | 'review_pickup';
workSyncIntentKey?: string;
taskRefs?: TaskRef[];
}): Promise<{
busy: boolean;
@ -12785,7 +12786,9 @@ export class TeamProvisioningService {
(message) => message.messageKind !== 'member_work_sync_nudge'
);
const blockingForegroundMessages = foregroundMessages.filter(
(message) => !this.isCurrentReviewPickupRequestForegroundMessage(message, input)
(message) =>
!this.isCurrentReviewPickupRequestForegroundMessage(message, input) &&
!this.isCurrentProofMissingRecoveryForegroundMessage(message, input)
);
const unreadForeground = blockingForegroundMessages.find(
(message) =>
@ -22214,6 +22217,24 @@ export class TeamProvisioningService {
);
}
private isCurrentProofMissingRecoveryForegroundMessage(
message: InboxMessage,
input: { workSyncIntent?: 'agenda_sync' | 'review_pickup'; workSyncIntentKey?: string }
): boolean {
if (input.workSyncIntent !== 'agenda_sync') {
return false;
}
const prefix = 'proof-missing:';
const intentKey = input.workSyncIntentKey?.trim();
if (!intentKey?.startsWith(prefix)) {
return false;
}
const originalMessageId = intentKey.slice(prefix.length).trim();
return this.hasStableMessageId(message) && message.messageId.trim() === originalMessageId;
}
private openCodeReviewPickupRequestTextMentionsTask(input: {
summary: string;
text: string;

View file

@ -3127,6 +3127,81 @@ Messages:
});
});
it('does not let proof-missing recovery get blocked by its original unread message', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';
const laneId = 'secondary:opencode:jack';
const teamsBasePath = getTeamsBasePath();
hoisted.files.set(
`${teamsBasePath}/${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' },
],
})
);
hoisted.files.set(
`${teamsBasePath}/${teamName}/inboxes/jack.json`,
JSON.stringify([
{
from: 'user',
to: 'jack',
text: 'Please check the current issue.',
timestamp: '2026-02-23T17:31:00.000Z',
read: false,
messageId: 'foreground-message-1',
messageKind: 'direct',
},
])
);
(service as any).resolveOpenCodeMemberDeliveryIdentity = vi.fn(async () => ({
ok: true,
canonicalMemberName: 'jack',
laneId,
}));
vi.spyOn(OpenCodeRuntimeStore, 'readOpenCodeRuntimeLaneIndex').mockResolvedValue({
version: 1,
updatedAt: '2026-02-23T17:30:00.000Z',
lanes: {
[laneId]: {
laneId,
state: 'active',
updatedAt: '2026-02-23T17:30:00.000Z',
},
},
});
vi.spyOn(service as any, 'createOpenCodePromptDeliveryLedger').mockReturnValue({
getActiveForMember: vi.fn(async () => null),
});
const sameMessageRecoveryBusy = await service.getOpenCodeMemberDeliveryBusyStatus({
teamName,
memberName: 'jack',
nowIso: '2026-02-23T17:31:10.000Z',
workSyncIntent: 'agenda_sync',
workSyncIntentKey: 'proof-missing:foreground-message-1',
});
expect(sameMessageRecoveryBusy).toEqual({ busy: false });
const unrelatedRecoveryBusy = await service.getOpenCodeMemberDeliveryBusyStatus({
teamName,
memberName: 'jack',
nowIso: '2026-02-23T17:31:10.000Z',
workSyncIntent: 'agenda_sync',
workSyncIntentKey: 'proof-missing:another-message',
});
expect(unrelatedRecoveryBusy).toMatchObject({
busy: true,
reason: 'opencode_foreground_inbox_unread',
activeMessageId: 'foreground-message-1',
});
});
it('does not treat the current unread OpenCode review request as busy for review-pickup checks', async () => {
const service = new TeamProvisioningService();
const teamName = 'my-team';