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 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,

View file

@ -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<void> {
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<void> {
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<void> {
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);
});
});
}