fix(member-work-sync): lock pending report writes

This commit is contained in:
777genius 2026-04-29 14:16:10 +03:00
parent cd70fc09cc
commit bf1f3b6b02
2 changed files with 63 additions and 52 deletions

View file

@ -2,6 +2,7 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const runtimeHelpers = require('./runtimeHelpers.js'); const runtimeHelpers = require('./runtimeHelpers.js');
const { withFileLockSync } = require('./fileLock.js');
const DEFAULT_WAIT_TIMEOUT_MS = 10000; const DEFAULT_WAIT_TIMEOUT_MS = 10000;
const MIN_WAIT_TIMEOUT_MS = 1000; const MIN_WAIT_TIMEOUT_MS = 1000;
@ -173,25 +174,28 @@ function writePendingReportFile(filePath, data) {
function appendPendingReportIntent(context, body, reason) { function appendPendingReportIntent(context, body, reason) {
const filePath = path.join(context.paths.teamDir, '.member-work-sync', 'pending-reports.json'); const filePath = path.join(context.paths.teamDir, '.member-work-sync', 'pending-reports.json');
const data = readPendingReportFile(filePath); const { id } = withFileLockSync(filePath, () => {
const request = { const data = readPendingReportFile(filePath);
...body, const request = {
source: 'mcp', ...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(),
}; };
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 { return {
accepted: false, accepted: false,
pendingValidation: true, pendingValidation: true,

View file

@ -2,6 +2,7 @@ import { atomicWriteAsync } from '@main/utils/atomicWrite';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { mkdir, readFile, rename } from 'fs/promises'; import { mkdir, readFile, rename } from 'fs/promises';
import { withFileLock } from '@main/services/team/fileLock';
import type { import type {
MemberWorkSyncReportIntent, MemberWorkSyncReportIntent,
MemberWorkSyncReportRequest, MemberWorkSyncReportRequest,
@ -106,34 +107,38 @@ export class JsonMemberWorkSyncStore
async write(status: MemberWorkSyncStatus): Promise<void> { async write(status: MemberWorkSyncStatus): Promise<void> {
await this.enqueue(status.teamName, async () => { await this.enqueue(status.teamName, async () => {
const existing = await this.readFile(status.teamName); await withFileLock(this.paths.getStatusPath(status.teamName), async () => {
existing.members[normalizeMemberKey(status.memberName)] = status; const existing = await this.readFile(status.teamName);
await mkdir(this.paths.getTeamDir(status.teamName), { recursive: true }); existing.members[normalizeMemberKey(status.memberName)] = status;
await atomicWriteAsync( await mkdir(this.paths.getTeamDir(status.teamName), { recursive: true });
this.paths.getStatusPath(status.teamName), await atomicWriteAsync(
JSON.stringify(existing, null, 2) this.paths.getStatusPath(status.teamName),
); JSON.stringify(existing, null, 2)
);
});
}); });
} }
async appendPendingReport(request: MemberWorkSyncReportRequest, reason: string): Promise<void> { async appendPendingReport(request: MemberWorkSyncReportRequest, reason: string): Promise<void> {
const id = buildPendingReportIntentId(request); const id = buildPendingReportIntentId(request);
await this.enqueue(request.teamName, async () => { await this.enqueue(request.teamName, async () => {
const existing = await this.readPendingFile(request.teamName); await withFileLock(this.paths.getPendingReportsPath(request.teamName), async () => {
const current = existing.intents[id]; const existing = await this.readPendingFile(request.teamName);
if (current && current.status !== 'pending') { const current = existing.intents[id];
return; if (current && current.status !== 'pending') {
} return;
existing.intents[id] = { }
id, existing.intents[id] = {
teamName: request.teamName, id,
memberName: request.memberName, teamName: request.teamName,
request, memberName: request.memberName,
reason: current?.reason ?? reason, request,
status: 'pending', reason: current?.reason ?? reason,
recordedAt: current?.recordedAt ?? new Date().toISOString(), status: 'pending',
}; recordedAt: current?.recordedAt ?? new Date().toISOString(),
await this.writePendingFile(request.teamName, existing); };
await this.writePendingFile(request.teamName, existing);
});
}); });
} }
@ -154,18 +159,20 @@ export class JsonMemberWorkSyncStore
} }
): Promise<void> { ): Promise<void> {
await this.enqueue(teamName, async () => { await this.enqueue(teamName, async () => {
const existing = await this.readPendingFile(teamName); await withFileLock(this.paths.getPendingReportsPath(teamName), async () => {
const current = existing.intents[id]; const existing = await this.readPendingFile(teamName);
if (!current || current.status !== 'pending') { const current = existing.intents[id];
return; if (!current || current.status !== 'pending') {
} return;
existing.intents[id] = { }
...current, existing.intents[id] = {
status: result.status, ...current,
resultCode: result.resultCode, status: result.status,
processedAt: result.processedAt, resultCode: result.resultCode,
}; processedAt: result.processedAt,
await this.writePendingFile(teamName, existing); };
await this.writePendingFile(teamName, existing);
});
}); });
} }