perf: batch per-member task-interval resume into one locked pass

During launch the live-status loop resumes every alive member every audit cycle.
resumeActiveIntervalsForMember runs a synchronous file-lock + full read of every
task file, so for an N-member team with M task files it did N locked passes x M
readFileSync per cycle (e.g. 6 members x 20 task files), blocking the main event
loop. Profiling a 6-member mixed launch showed mutateTeamTasks/withFileLockSync as
a top main-thread cost (~14%).

Add resumeActiveIntervalsForMembers that applies the identical per-member resume
logic against a member set in a single locked pass, and use it in the live-status
loop. Same mutations, but one lock + task read per cycle instead of one per member.
Adds a test covering multi-member resume in one pass.
This commit is contained in:
777genius 2026-05-30 10:02:01 +03:00
parent aa9a1bba8c
commit f79ea145d7
3 changed files with 119 additions and 8 deletions

View file

@ -13895,14 +13895,17 @@ export class TeamProvisioningService {
this.getOpenCodeSecondaryBootstrapPendingMemberNames(snapshot),
});
const runtimeObservedAt = nowIso();
for (const [memberName, entry] of Object.entries(nextStatuses)) {
if (entry.runtimeAlive === true) {
this.taskActivityIntervalService.resumeActiveIntervalsForMember(
teamName,
memberName,
runtimeObservedAt
);
}
const aliveMemberNames = Object.entries(nextStatuses)
.filter(([, entry]) => entry.runtimeAlive === true)
.map(([memberName]) => memberName);
if (aliveMemberNames.length > 0) {
// Resume all alive members in a single locked task-file pass per cycle
// instead of one synchronous lock + full task read per member.
this.taskActivityIntervalService.resumeActiveIntervalsForMembers(
teamName,
aliveMemberNames,
runtimeObservedAt
);
}
const expectedMembers = snapshot ? this.getPersistedLaunchMemberNames(snapshot) : undefined;
const summary = expectedMembers

View file

@ -443,6 +443,62 @@ export class TeamTaskActivityIntervalService {
});
}
/**
* Batched equivalent of resumeActiveIntervalsForMember for several members in a
* single task-file pass. During launch the live-status loop resumes every alive
* member every audit cycle; doing that per member meant one synchronous
* file-lock + read of every task file PER member PER cycle. This applies the
* identical per-member resume logic against a member set in one locked pass, so
* the mutations are exactly the same but the lock + reads happen once per cycle
* instead of once per member.
*/
resumeActiveIntervalsForMembers(
teamName: string,
memberNames: readonly string[],
at = new Date().toISOString()
): ActivityIntervalResult {
const memberKeys = new Set(
memberNames.map((name) => normalizeMemberName(name)).filter((key): key is string => !!key)
);
if (memberKeys.size === 0) return { changedTasks: 0 };
return this.mutateTeamTasks(teamName, (task) => {
let changed = false;
if (
task.status === 'in_progress' &&
memberKeys.has(normalizeMemberName(task.owner)) &&
!hasOpenWorkInterval(task)
) {
const activeStartedAt = getActiveWorkStartedAt(task);
task.workIntervals = [
...(Array.isArray(task.workIntervals) ? task.workIntervals : []),
{ startedAt: resumeStartIso(activeStartedAt, at) },
];
changed = true;
}
const activeReview = getActiveReviewStart(task);
if (
task.status === 'completed' &&
activeReview &&
memberKeys.has(normalizeMemberName(activeReview.reviewer)) &&
!hasOpenReviewInterval(task, activeReview.reviewer)
) {
task.reviewIntervals = [
...(Array.isArray(task.reviewIntervals) ? task.reviewIntervals : []),
{
reviewer: activeReview.reviewer,
startedAt: resumeStartIso(activeReview.startedAt, at),
},
];
changed = true;
}
return changed;
});
}
repairStaleIntervalsAfterCrash(
teamName: string,
launchSnapshot?: PersistedTeamLaunchSnapshot | null

View file

@ -465,6 +465,58 @@ describe('TeamTaskActivityIntervalService', () => {
]);
});
it('resumes active intervals for multiple members in a single pass', async () => {
await writeTask('alpha', {
id: 'bob-task',
subject: 'Bob work',
owner: 'bob',
status: 'in_progress',
workIntervals: [
{ startedAt: '2026-05-08T10:00:00.000Z', completedAt: '2026-05-08T10:05:00.000Z' },
],
historyEvents: [],
});
await writeTask('alpha', {
id: 'alice-task',
subject: 'Alice work',
owner: 'alice',
status: 'in_progress',
workIntervals: [
{ startedAt: '2026-05-08T11:00:00.000Z', completedAt: '2026-05-08T11:05:00.000Z' },
],
historyEvents: [],
});
await writeTask('alpha', {
id: 'carol-task',
subject: 'Carol work',
owner: 'carol',
status: 'in_progress',
workIntervals: [
{ startedAt: '2026-05-08T12:00:00.000Z', completedAt: '2026-05-08T12:05:00.000Z' },
],
historyEvents: [],
});
const result = new TeamTaskActivityIntervalService().resumeActiveIntervalsForMembers(
'alpha',
['bob', 'alice'],
'2026-05-08T10:20:00.000Z'
);
const bobTask = await readTask('alpha', 'bob-task');
const aliceTask = await readTask('alpha', 'alice-task');
const carolTask = await readTask('alpha', 'carol-task');
// Both listed members resumed in one pass; a member outside the set is untouched.
expect(result.changedTasks).toBe(2);
expect((bobTask.workIntervals as unknown[]).at(-1)).toEqual({
startedAt: '2026-05-08T10:20:00.000Z',
});
expect((aliceTask.workIntervals as unknown[]).at(-1)).toEqual({
startedAt: '2026-05-08T10:20:00.000Z',
});
expect(carolTask.workIntervals).toHaveLength(1);
});
it('reopens and closes lead work intervals across activity changes', async () => {
await writeTask('alpha', {
id: 'lead-task',