Merge branch 'main' into refactor/lock-ops-reduction-3810095324988153600

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-20 16:40:51 -05:00
commit 2d4753a6cb
41 files changed
+7794 -241

No files matched your search

@@ -0,0 +1,23 @@
---
agent: onboarding-audit-agent
cadence: daily
run-start: 2026-02-20T18:30:00Z
prompt: src/prompts/daily/onboarding-audit-agent.md
---
# Context - Onboarding Audit
## Goal
Verify that a fresh developer can onboard successfully using the documentation.
## Scope
- `README.md`
- `CONTRIBUTING.md`
- Onboarding scripts (`npm ci`, `npm test`, `npm run build`)
## Constraints
- Must use a clean environment simulation.
- Must not modify `package.json` unless necessary.
## References
- Agent: `onboarding-audit-agent`
- Prompt: `src/prompts/daily/onboarding-audit-agent.md`
@@ -0,0 +1,21 @@
# Context - perf-agent - 2026-02-20
Agent: perf-agent
Cadence: daily
Run-Start: 2026-02-20T19:31:14Z
Prompt: src/prompts/daily/perf-agent.md
## Goal
Establish a performance baseline and identify P0/P1 issues using search patterns.
## Scope
- Scan codebase for performance-critical patterns (intervals, workers, etc.).
- Generate hits report.
- Audit /content documentation if needed (deferred to analysis).
## Constraints
- Minimal changes for baseline establishment.
- Must run search script.
## Assumptions
- This is the first run (or treated as such for baseline).
@@ -0,0 +1,22 @@
---
Agent: protocol-research-agent
Cadence: daily
Run-Start: 2026-02-20T21-17-56Z
Prompt: src/prompts/daily/protocol-research-agent.md
---
# Context
## Goal
Identify external protocol/spec dependencies used by this codebase, map them to implementation points, assess compliance risk, and propose the smallest safe improvements.
## Scope
- `PROTOCOL_INVENTORY.md`
- Codebase scan for protocol references
## Assumptions
- `grep` is sufficient for initial discovery of protocol references.
## Constraints
- Prefer incremental changes.
- Treat auth, cryptography, and trust boundaries as high risk.
@@ -0,0 +1,14 @@
---
agent: onboarding-audit-agent
cadence: daily
run-start: 2026-02-20T18:30:00Z
prompt: src/prompts/daily/onboarding-audit-agent.md
---
# Decisions - Onboarding Audit
- **Decision**: No changes to `README.md` or `CONTRIBUTING.md`.
- **Rationale**: Current instructions are accurate and functional. All commands passed as documented.
## References
- Agent: `onboarding-audit-agent`
- Prompt: `src/prompts/daily/onboarding-audit-agent.md`
@@ -0,0 +1,10 @@
# Decisions - perf-agent - 2026-02-20
Agent: perf-agent
Cadence: daily
Run-Start: 2026-02-20T19:31:14Z
Prompt: src/prompts/daily/perf-agent.md
## Key Decision
- **Baseline Scan**: Decided to run a full scan to establish a baseline `hits.json` file.
- **Rationale**: Need to know the current state before prioritizing fixes.
@@ -0,0 +1,13 @@
---
Agent: protocol-research-agent
Cadence: daily
Run-Start: 2026-02-20T21-17-56Z
Prompt: src/prompts/daily/protocol-research-agent.md
---
# Decisions
## Decision 1: Update nostr-tools version
- **Context**: `PROTOCOL_INVENTORY.md` listed `2.19.4` but `package.json` had `2.23.1`.
- **Decision**: Update `PROTOCOL_INVENTORY.md` to match `package.json`.
- **Rationale**: Inventory must reflect the actual codebase state.
+168 -199
View File
@@ -524,6 +524,167 @@ export async function cmdComplete(agent, cadence, optionsOrDryRun = false, deps
return { status: 'completed', eventId: event.id };
}
const COMMAND_HANDLERS = {
check: async (args) => {
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for check`);
throw new ExitError(1, 'Missing cadence');
}
await cmdCheck(args.cadence, {
logDir: args.logDir,
ignoreLogs: args.ignoreLogs,
json: args.json,
jsonFile: args.jsonFile,
quiet: args.quiet,
});
},
lock: async (args) => {
if (!args.agent) {
console.error('ERROR: --agent <name> is required for lock');
throw new ExitError(1, 'Missing agent');
}
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for lock`);
throw new ExitError(1, 'Missing cadence');
}
await cmdLock(args.agent, args.cadence, {
dryRun: args.dryRun,
platform: args.platform,
model: args.model
});
},
complete: async (args) => {
if (!args.agent) {
console.error('ERROR: --agent <name> is required for complete');
throw new ExitError(1, 'Missing agent');
}
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for complete`);
throw new ExitError(1, 'Missing cadence');
}
await cmdComplete(args.agent, args.cadence, {
dryRun: args.dryRun,
platform: args.platform,
model: args.model
});
},
list: async (args) => {
if (args.cadence && !VALID_CADENCES.has(args.cadence)) {
console.error(`ERROR: --cadence must be one of: ${[...VALID_CADENCES].join(', ')}`);
throw new ExitError(1, 'Invalid cadence');
}
await cmdList(args.cadence || null);
},
health: async (args) => {
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for health`);
throw new ExitError(1, 'Missing cadence');
}
const result = await runRelayHealthCheck({
cadence: args.cadence,
...(Number.isFinite(args.timeoutMs) ? { timeoutMs: args.timeoutMs } : {}),
...(Number.isFinite(args.allRelaysDownMinutes) ? { allRelaysDownMinutes: args.allRelaysDownMinutes } : {}),
...(Number.isFinite(args.minSuccessRate) ? { minSuccessRate: args.minSuccessRate } : {}),
...(Number.isFinite(args.windowMinutes) ? { windowMinutes: args.windowMinutes } : {}),
});
if (!result.ok) {
result.failureCategory = 'all relays unhealthy';
}
console.log(JSON.stringify(result, null, 2));
if (!result.ok) throw new ExitError(2, 'Relay health check failed');
},
dashboard: async (args) => {
await cmdDashboard(args.port, args.host);
},
init: async (args) => {
await cmdInit(args.force);
},
update: async (args) => {
await cmdUpdate(args.force);
},
remove: async (args) => {
await cmdRemove(args.force);
},
'list-memories': async (args) => {
const result = await listMemories({
agent_id: args.agent,
type: args.type,
tags: args.tags,
pinned: args.pinned,
limit: args.limit,
offset: args.offset,
});
if (!args.full && Array.isArray(result)) {
for (const memory of result) {
if (typeof memory.content === 'string' && memory.content.length > 200) {
memory.content = memory.content.slice(0, 200) + '... (truncated, use --full to see all)';
}
}
}
console.log(JSON.stringify(result, null, 2));
},
'inspect-memory': async (args) => {
if (!args.id) {
console.error('ERROR: --id <memoryId> is required for inspect-memory');
throw new ExitError(1, 'Missing memory id');
}
const result = await inspectMemory(args.id);
console.log(JSON.stringify(result, null, 2));
},
'pin-memory': async (args) => {
if (!args.id) {
console.error('ERROR: --id <memoryId> is required for pin-memory');
throw new ExitError(1, 'Missing memory id');
}
const result = await pinMemory(args.id);
console.log(JSON.stringify(result, null, 2));
},
'unpin-memory': async (args) => {
if (!args.id) {
console.error('ERROR: --id <memoryId> is required for unpin-memory');
throw new ExitError(1, 'Missing memory id');
}
const result = await unpinMemory(args.id);
console.log(JSON.stringify(result, null, 2));
},
'trigger-prune-dry-run': async (args) => {
const result = await triggerPruneDryRun({ retentionMs: args.retentionMs ?? undefined });
console.log(JSON.stringify(result, null, 2));
},
'memory-stats': async (args) => {
const result = await memoryStats({ windowMs: args.windowMs ?? undefined });
console.log(JSON.stringify(result, null, 2));
},
proposal: async (args) => {
if (!args.subcommand) {
console.error('ERROR: Missing subcommand for proposal (create, list, apply, reject, show)');
throw new ExitError(1, 'Missing subcommand');
}
await cmdProposal(args.subcommand, {
agent: args.agent,
target: args.target,
contentFile: args.content,
reason: args.reason,
id: args.id,
status: args.status
});
},
rollback: async (args) => {
await cmdRollback(args.target, args.strategy, { list: args.list });
},
backup: async (args) => {
const { cmdBackup, listBackups } = await import('./cmd-backup.mjs');
if (args.list) {
const backups = await listBackups();
console.log(JSON.stringify(backups, null, 2));
} else {
await cmdBackup({ output: args.output });
}
},
};
function usage() {
console.error(USAGE_TEXT);
}
@@ -543,205 +704,13 @@ export async function main(argv) {
throw new ExitError(1, 'No command specified');
}
switch (args.command) {
case 'check': {
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for check`);
throw new ExitError(1, 'Missing cadence');
}
await cmdCheck(args.cadence, {
logDir: args.logDir,
ignoreLogs: args.ignoreLogs,
json: args.json,
jsonFile: args.jsonFile,
quiet: args.quiet,
});
break;
}
case 'lock': {
if (!args.agent) {
console.error('ERROR: --agent <name> is required for lock');
throw new ExitError(1, 'Missing agent');
}
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for lock`);
throw new ExitError(1, 'Missing cadence');
}
await cmdLock(args.agent, args.cadence, {
dryRun: args.dryRun,
platform: args.platform,
model: args.model
});
break;
}
case 'complete': {
if (!args.agent) {
console.error('ERROR: --agent <name> is required for complete');
throw new ExitError(1, 'Missing agent');
}
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for complete`);
throw new ExitError(1, 'Missing cadence');
}
await cmdComplete(args.agent, args.cadence, {
dryRun: args.dryRun,
platform: args.platform,
model: args.model
});
break;
}
case 'list': {
if (args.cadence && !VALID_CADENCES.has(args.cadence)) {
console.error(`ERROR: --cadence must be one of: ${[...VALID_CADENCES].join(', ')}`);
throw new ExitError(1, 'Invalid cadence');
}
await cmdList(args.cadence || null);
break;
}
case 'health': {
if (!args.cadence || !VALID_CADENCES.has(args.cadence)) {
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for health`);
throw new ExitError(1, 'Missing cadence');
}
const result = await runRelayHealthCheck({
cadence: args.cadence,
...(Number.isFinite(args.timeoutMs) ? { timeoutMs: args.timeoutMs } : {}),
...(Number.isFinite(args.allRelaysDownMinutes) ? { allRelaysDownMinutes: args.allRelaysDownMinutes } : {}),
...(Number.isFinite(args.minSuccessRate) ? { minSuccessRate: args.minSuccessRate } : {}),
...(Number.isFinite(args.windowMinutes) ? { windowMinutes: args.windowMinutes } : {}),
});
if (!result.ok) {
result.failureCategory = 'all relays unhealthy';
}
console.log(JSON.stringify(result, null, 2));
if (!result.ok) throw new ExitError(2, 'Relay health check failed');
break;
}
case 'dashboard': {
await cmdDashboard(args.port, args.host);
break;
}
case 'init': {
await cmdInit(args.force);
break;
}
case 'update': {
await cmdUpdate(args.force);
break;
}
case 'remove': {
await cmdRemove(args.force);
break;
}
case 'list-memories': {
const result = await listMemories({
agent_id: args.agent,
type: args.type,
tags: args.tags,
pinned: args.pinned,
limit: args.limit,
offset: args.offset,
});
if (!args.full && Array.isArray(result)) {
for (const memory of result) {
if (typeof memory.content === 'string' && memory.content.length > 200) {
memory.content = memory.content.slice(0, 200) + '... (truncated, use --full to see all)';
}
}
}
console.log(JSON.stringify(result, null, 2));
break;
}
case 'inspect-memory': {
if (!args.id) {
console.error('ERROR: --id <memoryId> is required for inspect-memory');
throw new ExitError(1, 'Missing memory id');
}
const result = await inspectMemory(args.id);
console.log(JSON.stringify(result, null, 2));
break;
}
case 'pin-memory': {
if (!args.id) {
console.error('ERROR: --id <memoryId> is required for pin-memory');
throw new ExitError(1, 'Missing memory id');
}
const result = await pinMemory(args.id);
console.log(JSON.stringify(result, null, 2));
break;
}
case 'unpin-memory': {
if (!args.id) {
console.error('ERROR: --id <memoryId> is required for unpin-memory');
throw new ExitError(1, 'Missing memory id');
}
const result = await unpinMemory(args.id);
console.log(JSON.stringify(result, null, 2));
break;
}
case 'trigger-prune-dry-run': {
const result = await triggerPruneDryRun({ retentionMs: args.retentionMs ?? undefined });
console.log(JSON.stringify(result, null, 2));
break;
}
case 'memory-stats': {
const result = await memoryStats({ windowMs: args.windowMs ?? undefined });
console.log(JSON.stringify(result, null, 2));
break;
}
case 'proposal': {
if (!args.subcommand) {
console.error('ERROR: Missing subcommand for proposal (create, list, apply, reject, show)');
throw new ExitError(1, 'Missing subcommand');
}
await cmdProposal(args.subcommand, {
agent: args.agent,
target: args.target,
contentFile: args.content,
reason: args.reason,
id: args.id,
status: args.status
});
break;
}
case 'rollback': {
await cmdRollback(args.target, args.strategy, { list: args.list });
break;
}
case 'backup': {
const { cmdBackup, listBackups } = await import('./cmd-backup.mjs');
if (args.list) {
const backups = await listBackups();
console.log(JSON.stringify(backups, null, 2));
} else {
await cmdBackup({ output: args.output });
}
break;
}
default:
console.error(`ERROR: Unknown command: ${args.command}`);
usage();
throw new ExitError(1, 'Unknown command');
const handler = COMMAND_HANDLERS[args.command];
if (handler && Object.hasOwn(COMMAND_HANDLERS, args.command)) {
await handler(args);
} else {
console.error(`ERROR: Unknown command: ${args.command}`);
usage();
throw new ExitError(1, 'Unknown command');
}
} catch (err) {
if (err instanceof ExitError) {
+1 -1
View File
@@ -8,7 +8,7 @@ import {
KIND_APP_DATA,
MS_PER_SECOND,
} from './constants.mjs';
import { nowUnix } from './utils.mjs';
import { nowUnix, mergeRelayList } from './utils.mjs';
import { defaultHealthManager, buildRelayHealthConfig, RelayHealthManager } from './relay-health-manager.mjs';
import { publishLock, LockPublisher } from './lock-publisher.mjs';
import {
+1
View File
@@ -6,6 +6,7 @@ import {
getRelayFallbacks,
getMinActiveRelayPool,
} from './torch-config.mjs';
import { mergeRelayList } from './utils.mjs';
import { defaultHealthManager, buildRelayHealthConfig } from './relay-health-manager.mjs';
import {
withTimeout,
+1 -9
View File
@@ -49,15 +49,7 @@ export async function getCompletedAgents(cadence, logDir, deps) {
return completed;
}
export function withTimeout(promise, timeoutMs, timeoutMessage) {
let timeoutHandle;
const timeoutPromise = new Promise((_, reject) => {
timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
});
return Promise.race([promise, timeoutPromise]).finally(() => {
if (timeoutHandle) clearTimeout(timeoutHandle);
});
}
export { withTimeout };
export function relayListLabel(relays) {
return relays.join(', ');
+1 -8
View File
@@ -4,17 +4,10 @@ import WebSocket from 'ws';
import { finalizeEvent, generateSecretKey, getPublicKey } from 'nostr-tools/pure';
import { getNamespace, getRelays } from './torch-config.mjs';
import { MS_PER_SECOND } from './constants.mjs';
import { withTimeout } from './utils.mjs';
const PROBE_KIND = 27235;
function withTimeout(promise, timeoutMs, message) {
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(message)), timeoutMs);
});
return Promise.race([promise, timeoutPromise]).finally(() => clearTimeout(timeoutId));
}
function nowIso() {
return new Date().toISOString();
}
+9 -3
View File
@@ -132,8 +132,14 @@ export async function createProposal({ agent, target, newContent, reason }) {
return { id, diff };
}
export async function validateProposal(id) {
const { meta, newContent } = await getProposal(id);
export async function validateProposal(proposalOrId) {
let proposal;
if (typeof proposalOrId === 'string') {
proposal = await getProposal(proposalOrId);
} else {
proposal = proposalOrId;
}
const { meta, newContent } = proposal;
// 1. Allowlist check (redundant but safe)
const { isAllowed } = resolveAndValidateTarget(meta.target);
@@ -165,7 +171,7 @@ export async function applyProposal(id) {
throw new Error(`Proposal is ${meta.status}, cannot apply.`);
}
const validation = await validateProposal(id);
const validation = await validateProposal(proposal);
if (!validation.valid) {
// Mark as rejected if validation fails?
// Or just throw? The governance agent should decide.
+13 -2
View File
@@ -41,13 +41,24 @@ function loadMemoryStore() {
let currentSavePromise = null;
let pendingSavePromise = null;
let pendingSaveResolve = null;
let lastSaveTime = 0;
const MIN_SAVE_INTERVAL_MS = 1000;
async function performSave(store) {
try {
const now = Date.now();
const timeSinceLast = now - lastSaveTime;
if (timeSinceLast < MIN_SAVE_INTERVAL_MS) {
await new Promise((resolve) => setTimeout(resolve, MIN_SAVE_INTERVAL_MS - timeSinceLast));
}
const dir = path.dirname(MEMORY_FILE_PATH);
await fs.promises.mkdir(dir, { recursive: true });
const entries = [...store.entries()];
await fs.promises.writeFile(MEMORY_FILE_PATH, JSON.stringify(entries, null, 2), 'utf8');
const tmpPath = `${MEMORY_FILE_PATH}.${Date.now()}.${Math.random()}.tmp`;
await fs.promises.writeFile(tmpPath, JSON.stringify(entries, null, 2), 'utf8');
await fs.promises.rename(tmpPath, MEMORY_FILE_PATH);
lastSaveTime = Date.now();
} catch (err) {
console.error('Failed to save memory store:', err);
} finally {
@@ -105,7 +116,7 @@ const memoryRepository = {
const updated = { ...existing, last_seen: lastSeen };
memoryStore.set(id, updated);
storeVersion++;
await saveMemoryStore(memoryStore);
saveMemoryStore(memoryStore).catch((err) => console.error('Failed to save memory usage update:', err));
return updated;
},
async listPruneCandidates({ cutoff }) {
@@ -0,0 +1,24 @@
---
agent: onboarding-audit-agent
cadence: daily
run-start: 2026-02-20T18:30:00Z
prompt: src/prompts/daily/onboarding-audit-agent.md
---
# Test Log - Onboarding Audit
## Execution
- **Command**: `npm ci`
- **Result**: Success
- **Command**: `npm run build`
- **Result**: Success
- **Command**: `npm test`
- **Result**: Passed (181 tests)
- **Command**: `npm run lint`
- **Result**: Passed
## References
- Agent: `onboarding-audit-agent`
- Prompt: `src/prompts/daily/onboarding-audit-agent.md`
@@ -0,0 +1,13 @@
# Test Log - perf-agent - 2026-02-20
Agent: perf-agent
Cadence: daily
Run-Start: 2026-02-20T19:31:14Z
Prompt: src/prompts/daily/perf-agent.md
## Environment
- Agent: perf-agent
- Cadence: daily
- Command: node scripts/perf-search.mjs
- Result: Success (22 hits found).
@@ -0,0 +1,16 @@
---
Agent: protocol-research-agent
Cadence: daily
Run-Start: 2026-02-20T21-17-56Z
Prompt: src/prompts/daily/protocol-research-agent.md
---
# Test Log
## Command: grep for protocols
- **Command**: `grep -rE "protocol|spec|standard|rfc|cip|bip|nip" src/`
- **Result**: Found existing usages in `src/lock-ops.mjs` and `src/relay-health.mjs`. No new protocols found.
## Command: verify nostr-tools version
- **Command**: Checked `package.json` vs `PROTOCOL_INVENTORY.md`
- **Result**: Version mismatch identified and fixed.
+1 -1
View File
@@ -20,7 +20,7 @@ prompt: src/prompts/daily/audit-agent.md
- [ ] Create audit-report-2026-02-19.md
- [ ] Post summary to GitHub (issue/PR comment)
- [ ] Run memory store (MEMORY_STORED)
- [ ] Run npm run lint (repo validation)
- [x] Run npm run lint (repo validation)
## Completed tasks
- Read TORCH.md ✓
+3 -3
View File
@@ -7,11 +7,11 @@ Run Start: 2026-02-19T22-29-56Z
Run ID: session-1771540169
## Pending
- [ ] Verify claims against code (N/A - no claims found).
- [ ] Update documentation (N/A - no claims found).
- [ ] Validate changes (N/A - no changes made).
## Completed
- [x] Verify claims against code (N/A - no claims found).
- [x] Update documentation (N/A - no claims found).
- [x] Validate changes (N/A - no changes made).
- [x] Inventory `docs/` for relevant claims.
- [x] Check for user-facing upload/contribution guides.
+6 -6
View File
@@ -1,8 +1,8 @@
# TODO: Decompose src/lock-ops.mjs
- [ ] Extract `RelayHealthManager` and helpers to `src/relay-health-manager.mjs`
- [ ] Extract `LockPublisher` and helpers to `src/lock-publisher.mjs`
- [ ] Refactor `src/lock-ops.mjs` to import new modules
- [ ] Verify linting passes
- [ ] Verify tests pass (`npm test`)
- [ ] Verify file size reduction
- [x] Extract `RelayHealthManager` and helpers to `src/relay-health-manager.mjs`
- [x] Extract `LockPublisher` and helpers to `src/lock-publisher.mjs`
- [x] Refactor `src/lock-ops.mjs` to import new modules
- [x] Verify linting passes
- [x] Verify tests pass (`npm test`)
- [x] Verify file size reduction
+18
View File
@@ -0,0 +1,18 @@
---
agent: onboarding-audit-agent
cadence: daily
run-start: 2026-02-20T18:30:00Z
prompt: src/prompts/daily/onboarding-audit-agent.md
---
# Todo - Onboarding Audit
## Completed Tasks
- [x] Audit onboarding steps
- [x] Verify docs
## Pending Tasks
- None
## References
- Agent: `onboarding-audit-agent`
- Prompt: `src/prompts/daily/onboarding-audit-agent.md`
+16
View File
@@ -0,0 +1,16 @@
# TODO - perf-agent - 2026-02-20
Agent: perf-agent
Cadence: daily
Run-Start: 2026-02-20T19:31:14Z
Prompt: src/prompts/daily/perf-agent.md
## Done
- [x] Create run artifacts.
- [x] Run memory retrieval.
- [x] Run search patterns.
- [x] Generate daily report.
- [x] Store memory.
## Pending
- [ ] Analyze hits in depth (next run).
+13
View File
@@ -0,0 +1,13 @@
---
Agent: protocol-research-agent
Cadence: daily
Run-Start: 2026-02-20T21-17-56Z
Prompt: src/prompts/daily/protocol-research-agent.md
---
# Todo
## Pending
- [ ] Scan repo for protocol/spec references
- [ ] Update PROTOCOL_INVENTORY.md
- [ ] Create protocol report
+14
View File
@@ -42,8 +42,22 @@ export function detectPlatform() {
return null;
}
export function mergeRelayList(primaryRelays, fallbackRelays) {
return [...new Set([...primaryRelays, ...fallbackRelays])];
}
export function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
export function withTimeout(promise, timeoutMs, timeoutMessage) {
let timeoutHandle;
const timeoutPromise = new Promise((_, reject) => {
timeoutHandle = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
});
return Promise.race([promise, timeoutPromise]).finally(() => {
if (timeoutHandle) clearTimeout(timeoutHandle);
});
}