Merge branch 'main' into refactor/merge-relay-list-7360990203888330524

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-20 16:35:59 -05:00
commit 756806a99e
12 files changed
+3577 -53

No files matched your search

+3
View File
@@ -56,6 +56,9 @@ AGENT_PLATFORM=codex npx --no-install torch-lock complete --agent docs-agent --c
# Convenience script for the above
npm run lock:complete -- --agent docs-agent --cadence daily
# Generic usage for lock completion
npm run lock:complete -- --agent <agent> --cadence <cadence>
# Initialize TORCH in a new repository
npx --no-install torch-lock init
+2 -2
View File
@@ -382,10 +382,10 @@ Validation behavior:
The scheduler is the primary entry point for automated agent execution. It handles lock acquisition, environment setup, agent handoff, and artifact verification.
```bash
# Run the daily rotation
# Run full daily scheduler cycle
npm run scheduler:daily
# Run the weekly rotation
# Run full weekly scheduler cycle
npm run scheduler:weekly
```
-10
View File
@@ -55,16 +55,6 @@ function filterActiveLocks(locks) {
return locks.filter((lock) => !lock.expiresAt || lock.expiresAt > now);
}
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);
});
}
function relayListLabel(relays) {
return relays.join(', ');
}
-10
View File
@@ -9,16 +9,6 @@ import {
import { mergeRelayList } from './utils.mjs';
import { defaultHealthManager, buildRelayHealthConfig } from './relay-health-manager.mjs';
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);
});
}
function relayListLabel(relays) {
return relays.join(', ');
}
+2 -10
View File
@@ -1,6 +1,6 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { todayDateStr, getIsoWeekStr } from './utils.mjs';
import { todayDateStr, getIsoWeekStr, withTimeout } from './utils.mjs';
/**
* Scans the local log directory to identify agents that have already completed their task
@@ -48,15 +48,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 }) {
+10
View File
@@ -51,3 +51,13 @@ export function ensureDir(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);
});
}
+51
View File
@@ -0,0 +1,51 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { withTimeout } from '../src/utils.mjs';
describe('Async Utilities', () => {
describe('withTimeout', () => {
it('resolves when promise completes before timeout', async () => {
const result = await withTimeout(
new Promise((resolve) => setTimeout(() => resolve('success'), 10)),
200,
'Timed out'
);
assert.strictEqual(result, 'success');
});
it('rejects when promise times out', async () => {
try {
await withTimeout(
new Promise((resolve) => setTimeout(() => resolve('success'), 200)),
10,
'Operation timed out'
);
assert.fail('Should have rejected');
} catch (err) {
assert.strictEqual(err.message, 'Operation timed out');
}
});
it('clears timeout on success', async () => {
const result = await withTimeout(
Promise.resolve('fast'),
1000,
'timeout'
);
assert.strictEqual(result, 'fast');
});
it('propagates original promise rejection', async () => {
try {
await withTimeout(
Promise.reject(new Error('Original error')),
100,
'Timeout'
);
assert.fail('Should have rejected');
} catch (err) {
assert.strictEqual(err.message, 'Original error');
}
});
});
});
+3486
View File
File diff suppressed because it is too large. Load diff
-8
View File
@@ -1,8 +0,0 @@
{
"dashboard": {
"auth": "admin:password123",
"relays": [
"wss://secure.relay.com"
]
}
}