diff --git a/src/main/services/team/TeamTaskActivityIntervalService.ts b/src/main/services/team/TeamTaskActivityIntervalService.ts index 3d4765c0..68057def 100644 --- a/src/main/services/team/TeamTaskActivityIntervalService.ts +++ b/src/main/services/team/TeamTaskActivityIntervalService.ts @@ -333,6 +333,10 @@ function writeTaskFile(filePath: string, task: MutableTeamTask): void { export class TeamTaskActivityIntervalService { private readonly resumeMembersCache = new Map(); + private getBoardStateLockPath(teamName: string): string { + return `${path.join(getTeamsBasePath(), teamName, 'board-state')}.lock`; + } + private mutateTeamTasksWithLock( teamName: string, run: () => ActivityIntervalResult @@ -520,6 +524,18 @@ export class TeamTaskActivityIntervalService { if (memberKeys.size === 0) return { changedTasks: 0 }; const memberKey = this.makeMemberSetKey(memberKeys); + const cachedBeforeLock = this.resumeMembersCache.get(teamName); + if (cachedBeforeLock?.memberKey === memberKey) { + const beforeLockSignature = this.readTaskDirectorySignature(teamName); + if ( + beforeLockSignature && + cachedBeforeLock.signatureKey === beforeLockSignature.key && + !fs.existsSync(this.getBoardStateLockPath(teamName)) + ) { + return { changedTasks: 0 }; + } + } + const result = this.mutateTeamTasksWithLock(teamName, () => { const beforeSignature = this.readTaskDirectorySignature(teamName); const cached = this.resumeMembersCache.get(teamName); diff --git a/test/main/services/team/TeamTaskActivityIntervalService.test.ts b/test/main/services/team/TeamTaskActivityIntervalService.test.ts index a14c6eeb..64ea5f34 100644 --- a/test/main/services/team/TeamTaskActivityIntervalService.test.ts +++ b/test/main/services/team/TeamTaskActivityIntervalService.test.ts @@ -547,6 +547,44 @@ describe('TeamTaskActivityIntervalService', () => { expect(jsonParseSpy).not.toHaveBeenCalled(); }); + it('skips the task lock after an unchanged batched resume no-op pass', async () => { + await writeTask('alpha', { + id: 'bob-task', + subject: 'Bob work', + owner: 'bob', + status: 'in_progress', + workIntervals: [{ startedAt: '2026-05-08T10:00:00.000Z' }], + historyEvents: [], + }); + + const service = new TeamTaskActivityIntervalService(); + expect( + service.resumeActiveIntervalsForMembers( + 'alpha', + ['bob'], + '2026-05-08T10:20:00.000Z' + ).changedTasks + ).toBe(0); + + const mutateWithLockSpy = vi.spyOn( + TeamTaskActivityIntervalService.prototype as unknown as { + mutateTeamTasksWithLock: ( + teamName: string, + run: () => { changedTasks: number; failed?: boolean } + ) => { changedTasks: number; failed?: boolean }; + }, + 'mutateTeamTasksWithLock' + ); + const secondResult = service.resumeActiveIntervalsForMembers( + 'alpha', + ['bob'], + '2026-05-08T10:25:00.000Z' + ); + + expect(secondResult.changedTasks).toBe(0); + expect(mutateWithLockSpy).not.toHaveBeenCalled(); + }); + it('refreshes batched resume cache when a task file changes', async () => { await writeTask('alpha', { id: 'bob-task',