agent-ecosystem/src/main/services/team/fileLock.ts
iliya c3eea4d6eb feat: add cross-team communication orchestrator
Autonomous message routing between Agent Teams via MCP with cascade
protection. Inbox-first canonical delivery with cross-process file
lock (O_CREAT|O_EXCL) and best-effort relay for online teams.

- CascadeGuard: rate limit (10/min), chain depth (max 5), pair cooldown (3s)
- FileLock: cross-process safe via kernel-level atomic lock files
- CrossTeamService: validate → cascade → file-lock → inbox write → relay → outbox
- Unified lead resolver with members.meta.json normalization (trim+dedup)
- 3 MCP tools: cross_team_send, cross_team_list_targets, cross_team_get_outbox
- Controller module with sync file lock, same protocol as main
- IPC adapter with 3 Electron handlers
- 64 new tests across 8 test files
2026-03-09 18:45:15 +02:00

69 lines
1.7 KiB
TypeScript

import * as fs from 'fs';
import * as path from 'path';
const STALE_TIMEOUT_MS = 30_000;
const ACQUIRE_TIMEOUT_MS = 5_000;
const RETRY_INTERVAL_MS = 20;
function readLockAge(lockPath: string): number | null {
try {
const content = fs.readFileSync(lockPath, 'utf8');
const ts = parseInt(content.split('\n')[1] ?? '', 10);
if (Number.isFinite(ts)) return Date.now() - ts;
} catch {
/* lock may have been released concurrently */
}
return null;
}
function tryAcquire(lockPath: string): boolean {
try {
const dir = path.dirname(lockPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const fd = fs.openSync(lockPath, 'wx');
fs.writeSync(fd, `${process.pid}\n${Date.now()}\n`);
fs.closeSync(fd);
return true;
} catch (err) {
if ((err as NodeJS.ErrnoException).code === 'EEXIST') {
const age = readLockAge(lockPath);
if (age !== null && age > STALE_TIMEOUT_MS) {
try {
fs.unlinkSync(lockPath);
} catch {
/* another process may have cleaned it */
}
}
return false;
}
throw err;
}
}
function releaseLock(lockPath: string): void {
try {
fs.unlinkSync(lockPath);
} catch {
/* already released or cleaned up */
}
}
export async function withFileLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
const lockPath = `${filePath}.lock`;
const deadline = Date.now() + ACQUIRE_TIMEOUT_MS;
while (!tryAcquire(lockPath)) {
if (Date.now() >= deadline) {
throw new Error(`File lock timeout: ${filePath}`);
}
await new Promise((resolve) => setTimeout(resolve, RETRY_INTERVAL_MS));
}
try {
return await fn();
} finally {
releaseLock(lockPath);
}
}