diff --git a/agent-teams-controller/src/internal/workSync.js b/agent-teams-controller/src/internal/workSync.js index 63036fc6..5ac48411 100644 --- a/agent-teams-controller/src/internal/workSync.js +++ b/agent-teams-controller/src/internal/workSync.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const runtimeHelpers = require('./runtimeHelpers.js'); +const { withFileLockSync } = require('./fileLock.js'); const DEFAULT_WAIT_TIMEOUT_MS = 10000; const MIN_WAIT_TIMEOUT_MS = 1000; @@ -173,25 +174,28 @@ function writePendingReportFile(filePath, data) { function appendPendingReportIntent(context, body, reason) { const filePath = path.join(context.paths.teamDir, '.member-work-sync', 'pending-reports.json'); - const data = readPendingReportFile(filePath); - const request = { - ...body, - source: 'mcp', - }; - const id = buildPendingIntentId(request); - const current = data.intents[id]; - if (!current || current.status === 'pending') { - data.intents[id] = { - id, - teamName: body.teamName, - memberName: body.memberName, - request, - reason, - status: 'pending', - recordedAt: current && current.recordedAt ? current.recordedAt : new Date().toISOString(), + const { id } = withFileLockSync(filePath, () => { + const data = readPendingReportFile(filePath); + const request = { + ...body, + source: 'mcp', }; - writePendingReportFile(filePath, data); - } + const intentId = buildPendingIntentId(request); + const current = data.intents[intentId]; + if (!current || current.status === 'pending') { + data.intents[intentId] = { + id: intentId, + teamName: body.teamName, + memberName: body.memberName, + request, + reason, + status: 'pending', + recordedAt: current && current.recordedAt ? current.recordedAt : new Date().toISOString(), + }; + writePendingReportFile(filePath, data); + } + return { id: intentId }; + }); return { accepted: false, pendingValidation: true, diff --git a/src/features/member-work-sync/main/infrastructure/JsonMemberWorkSyncStore.ts b/src/features/member-work-sync/main/infrastructure/JsonMemberWorkSyncStore.ts index f5329e4d..ec2df82e 100644 --- a/src/features/member-work-sync/main/infrastructure/JsonMemberWorkSyncStore.ts +++ b/src/features/member-work-sync/main/infrastructure/JsonMemberWorkSyncStore.ts @@ -2,6 +2,7 @@ import { atomicWriteAsync } from '@main/utils/atomicWrite'; import { createHash } from 'crypto'; import { mkdir, readFile, rename } from 'fs/promises'; +import { withFileLock } from '@main/services/team/fileLock'; import type { MemberWorkSyncReportIntent, MemberWorkSyncReportRequest, @@ -106,34 +107,38 @@ export class JsonMemberWorkSyncStore async write(status: MemberWorkSyncStatus): Promise { await this.enqueue(status.teamName, async () => { - const existing = await this.readFile(status.teamName); - existing.members[normalizeMemberKey(status.memberName)] = status; - await mkdir(this.paths.getTeamDir(status.teamName), { recursive: true }); - await atomicWriteAsync( - this.paths.getStatusPath(status.teamName), - JSON.stringify(existing, null, 2) - ); + await withFileLock(this.paths.getStatusPath(status.teamName), async () => { + const existing = await this.readFile(status.teamName); + existing.members[normalizeMemberKey(status.memberName)] = status; + await mkdir(this.paths.getTeamDir(status.teamName), { recursive: true }); + await atomicWriteAsync( + this.paths.getStatusPath(status.teamName), + JSON.stringify(existing, null, 2) + ); + }); }); } async appendPendingReport(request: MemberWorkSyncReportRequest, reason: string): Promise { const id = buildPendingReportIntentId(request); await this.enqueue(request.teamName, async () => { - const existing = await this.readPendingFile(request.teamName); - const current = existing.intents[id]; - if (current && current.status !== 'pending') { - return; - } - existing.intents[id] = { - id, - teamName: request.teamName, - memberName: request.memberName, - request, - reason: current?.reason ?? reason, - status: 'pending', - recordedAt: current?.recordedAt ?? new Date().toISOString(), - }; - await this.writePendingFile(request.teamName, existing); + await withFileLock(this.paths.getPendingReportsPath(request.teamName), async () => { + const existing = await this.readPendingFile(request.teamName); + const current = existing.intents[id]; + if (current && current.status !== 'pending') { + return; + } + existing.intents[id] = { + id, + teamName: request.teamName, + memberName: request.memberName, + request, + reason: current?.reason ?? reason, + status: 'pending', + recordedAt: current?.recordedAt ?? new Date().toISOString(), + }; + await this.writePendingFile(request.teamName, existing); + }); }); } @@ -154,18 +159,20 @@ export class JsonMemberWorkSyncStore } ): Promise { await this.enqueue(teamName, async () => { - const existing = await this.readPendingFile(teamName); - const current = existing.intents[id]; - if (!current || current.status !== 'pending') { - return; - } - existing.intents[id] = { - ...current, - status: result.status, - resultCode: result.resultCode, - processedAt: result.processedAt, - }; - await this.writePendingFile(teamName, existing); + await withFileLock(this.paths.getPendingReportsPath(teamName), async () => { + const existing = await this.readPendingFile(teamName); + const current = existing.intents[id]; + if (!current || current.status !== 'pending') { + return; + } + existing.intents[id] = { + ...current, + status: result.status, + resultCode: result.resultCode, + processedAt: result.processedAt, + }; + await this.writePendingFile(teamName, existing); + }); }); }