fix(member-work-sync): avoid self-blocking proof recovery
This commit is contained in:
parent
39c52c3847
commit
d048113c1d
4 changed files with 99 additions and 1 deletions
|
|
@ -485,6 +485,7 @@ export class MemberWorkSyncNudgeDispatcher {
|
||||||
memberName: item.memberName,
|
memberName: item.memberName,
|
||||||
nowIso,
|
nowIso,
|
||||||
workSyncIntent: item.payload.workSyncIntent,
|
workSyncIntent: item.payload.workSyncIntent,
|
||||||
|
workSyncIntentKey: item.payload.workSyncIntentKey,
|
||||||
taskRefs: item.payload.taskRefs,
|
taskRefs: item.payload.taskRefs,
|
||||||
});
|
});
|
||||||
if (busy?.busy) {
|
if (busy?.busy) {
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,7 @@ export interface MemberWorkSyncBusySignalPort {
|
||||||
memberName: string;
|
memberName: string;
|
||||||
nowIso: string;
|
nowIso: string;
|
||||||
workSyncIntent?: MemberWorkSyncOutboxItem['payload']['workSyncIntent'];
|
workSyncIntent?: MemberWorkSyncOutboxItem['payload']['workSyncIntent'];
|
||||||
|
workSyncIntentKey?: MemberWorkSyncOutboxItem['payload']['workSyncIntentKey'];
|
||||||
taskRefs?: MemberWorkSyncOutboxItem['payload']['taskRefs'];
|
taskRefs?: MemberWorkSyncOutboxItem['payload']['taskRefs'];
|
||||||
}): Promise<{ busy: boolean; reason?: string; retryAfterIso?: string }>;
|
}): Promise<{ busy: boolean; reason?: string; retryAfterIso?: string }>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12753,6 +12753,7 @@ export class TeamProvisioningService {
|
||||||
memberName: string;
|
memberName: string;
|
||||||
nowIso: string;
|
nowIso: string;
|
||||||
workSyncIntent?: 'agenda_sync' | 'review_pickup';
|
workSyncIntent?: 'agenda_sync' | 'review_pickup';
|
||||||
|
workSyncIntentKey?: string;
|
||||||
taskRefs?: TaskRef[];
|
taskRefs?: TaskRef[];
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
|
|
@ -12785,7 +12786,9 @@ export class TeamProvisioningService {
|
||||||
(message) => message.messageKind !== 'member_work_sync_nudge'
|
(message) => message.messageKind !== 'member_work_sync_nudge'
|
||||||
);
|
);
|
||||||
const blockingForegroundMessages = foregroundMessages.filter(
|
const blockingForegroundMessages = foregroundMessages.filter(
|
||||||
(message) => !this.isCurrentReviewPickupRequestForegroundMessage(message, input)
|
(message) =>
|
||||||
|
!this.isCurrentReviewPickupRequestForegroundMessage(message, input) &&
|
||||||
|
!this.isCurrentProofMissingRecoveryForegroundMessage(message, input)
|
||||||
);
|
);
|
||||||
const unreadForeground = blockingForegroundMessages.find(
|
const unreadForeground = blockingForegroundMessages.find(
|
||||||
(message) =>
|
(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: {
|
private openCodeReviewPickupRequestTextMentionsTask(input: {
|
||||||
summary: string;
|
summary: string;
|
||||||
text: string;
|
text: string;
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
it('does not treat the current unread OpenCode review request as busy for review-pickup checks', async () => {
|
||||||
const service = new TeamProvisioningService();
|
const service = new TeamProvisioningService();
|
||||||
const teamName = 'my-team';
|
const teamName = 'my-team';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue