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
69 lines
1.7 KiB
TypeScript
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);
|
|
}
|
|
}
|