mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-29 08:51:53 +00:00
Merge branch 'main' into perf/async-memory-save-18240015948353785856
This commit is contained in:
46 files changed
+1755
-633
No files matched your search
@@ -1,61 +0,0 @@
|
||||
import {
|
||||
loadTorchConfig,
|
||||
getRelays,
|
||||
getNamespace,
|
||||
} from './torch-config.mjs';
|
||||
import { getRoster } from './roster.mjs';
|
||||
import { queryLocks } from './lock-ops.mjs';
|
||||
import { todayDateStr } from './utils.mjs';
|
||||
|
||||
export async function cmdCheck(cadence, deps = {}) {
|
||||
const {
|
||||
loadTorchConfigFn = loadTorchConfig,
|
||||
getRelaysFn = getRelays,
|
||||
getNamespaceFn = getNamespace,
|
||||
getRosterFn = getRoster,
|
||||
queryLocksFn = queryLocks,
|
||||
todayDateStrFn = todayDateStr,
|
||||
} = deps;
|
||||
|
||||
const relays = await getRelaysFn();
|
||||
const namespace = await getNamespaceFn();
|
||||
const dateStr = todayDateStrFn();
|
||||
const config = await loadTorchConfigFn();
|
||||
const pausedAgents = (cadence === 'daily' ? config.scheduler.paused.daily : config.scheduler.paused.weekly) || [];
|
||||
|
||||
console.error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`);
|
||||
console.error(`Relays: ${relays.join(', ')}`);
|
||||
if (pausedAgents.length > 0) {
|
||||
console.error(`Paused agents: ${pausedAgents.join(', ')}`);
|
||||
}
|
||||
|
||||
const locks = await queryLocksFn(relays, cadence, dateStr, namespace);
|
||||
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
|
||||
const roster = await getRosterFn(cadence);
|
||||
|
||||
const excludedAgents = [...new Set([...lockedAgents, ...pausedAgents])];
|
||||
const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent));
|
||||
const available = roster.filter((a) => !excludedAgents.includes(a));
|
||||
|
||||
const result = {
|
||||
namespace,
|
||||
cadence,
|
||||
date: dateStr,
|
||||
locked: lockedAgents.sort(),
|
||||
paused: pausedAgents.sort(),
|
||||
excluded: excludedAgents.sort(),
|
||||
available: available.sort(),
|
||||
lockCount: locks.length,
|
||||
unknownLockedAgents: unknownLockedAgents.sort(),
|
||||
locks: locks.map((l) => ({
|
||||
agent: l.agent,
|
||||
eventId: l.eventId,
|
||||
createdAt: l.createdAtIso,
|
||||
expiresAt: l.expiresAtIso,
|
||||
platform: l.platform,
|
||||
})),
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
||||
import {
|
||||
getRelays,
|
||||
getNamespace,
|
||||
getTtl,
|
||||
} from './torch-config.mjs';
|
||||
import {
|
||||
RACE_CHECK_DELAY_MS,
|
||||
KIND_APP_DATA,
|
||||
} from './constants.mjs';
|
||||
import { getRoster } from './roster.mjs';
|
||||
import { queryLocks, publishLock } from './lock-ops.mjs';
|
||||
import { todayDateStr, nowUnix } from './utils.mjs';
|
||||
import { ExitError } from './errors.mjs';
|
||||
|
||||
export async function cmdLock(agent, cadence, dryRun = false) {
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const ttl = await getTtl();
|
||||
const now = nowUnix();
|
||||
const expiresAt = now + ttl;
|
||||
|
||||
console.error(`Locking: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`);
|
||||
console.error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`);
|
||||
console.error(`Relays: ${relays.join(', ')}`);
|
||||
|
||||
const roster = await getRoster(cadence);
|
||||
if (!roster.includes(agent)) {
|
||||
console.error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
|
||||
console.error(`Allowed ${cadence} agents: ${roster.join(', ')}`);
|
||||
throw new ExitError(1, 'Agent not in roster');
|
||||
}
|
||||
|
||||
console.error('Step 1: Checking for existing locks...');
|
||||
const existingLocks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const conflicting = existingLocks.filter((l) => l.agent === agent);
|
||||
|
||||
if (conflicting.length > 0) {
|
||||
const earliest = conflicting.sort((a, b) => a.createdAt - b.createdAt)[0];
|
||||
console.error(
|
||||
`LOCK DENIED: ${agent} already locked by event ${earliest.eventId} ` +
|
||||
`(created ${earliest.createdAtIso}, platform: ${earliest.platform})`,
|
||||
);
|
||||
console.log('LOCK_STATUS=denied');
|
||||
console.log('LOCK_REASON=already_locked');
|
||||
console.log(`LOCK_EXISTING_EVENT=${earliest.eventId}`);
|
||||
throw new ExitError(3, 'Lock denied');
|
||||
}
|
||||
|
||||
console.error('Step 2: Generating ephemeral keypair...');
|
||||
const sk = generateSecretKey();
|
||||
const pk = getPublicKey(sk);
|
||||
console.error(` Ephemeral pubkey: ${pk.slice(0, 16)}...`);
|
||||
|
||||
console.error('Step 3: Building lock event...');
|
||||
const event = finalizeEvent(
|
||||
{
|
||||
kind: KIND_APP_DATA,
|
||||
created_at: now,
|
||||
tags: [
|
||||
['d', `${namespace}-lock/${cadence}/${agent}/${dateStr}`],
|
||||
['t', `${namespace}-agent-lock`],
|
||||
['t', `${namespace}-lock-${cadence}`],
|
||||
['t', `${namespace}-lock-${cadence}-${dateStr}`],
|
||||
['expiration', String(expiresAt)],
|
||||
],
|
||||
content: JSON.stringify({
|
||||
agent,
|
||||
cadence,
|
||||
status: 'started',
|
||||
namespace,
|
||||
date: dateStr,
|
||||
platform: process.env.AGENT_PLATFORM || 'unknown',
|
||||
lockedAt: new Date(now * 1000).toISOString(),
|
||||
expiresAt: new Date(expiresAt * 1000).toISOString(),
|
||||
}),
|
||||
},
|
||||
sk,
|
||||
);
|
||||
|
||||
console.error(` Event ID: ${event.id}`);
|
||||
|
||||
if (dryRun) {
|
||||
console.error('Step 4: [DRY RUN] Skipping publish — event built but not sent');
|
||||
console.error('RACE CHECK: won (dry run — no real contention possible)');
|
||||
} else {
|
||||
console.error('Step 4: Publishing to relays...');
|
||||
await publishLock(relays, event);
|
||||
|
||||
console.error('Step 5: Race check...');
|
||||
await new Promise((resolve) => setTimeout(resolve, RACE_CHECK_DELAY_MS));
|
||||
|
||||
const postLocks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const racingLocks = postLocks
|
||||
.filter((l) => l.agent === agent)
|
||||
.sort((a, b) => a.createdAt - b.createdAt);
|
||||
|
||||
if (racingLocks.length > 1 && racingLocks[0].eventId !== event.id) {
|
||||
const winner = racingLocks[0];
|
||||
console.error(
|
||||
`RACE CHECK: lost (earlier lock by event ${winner.eventId}, created ${winner.createdAtIso})`,
|
||||
);
|
||||
console.log('LOCK_STATUS=race_lost');
|
||||
console.log('LOCK_REASON=earlier_claim_exists');
|
||||
console.log(`LOCK_WINNER_EVENT=${winner.eventId}`);
|
||||
throw new ExitError(3, 'Race check lost');
|
||||
}
|
||||
|
||||
console.error('RACE CHECK: won');
|
||||
}
|
||||
|
||||
console.log('LOCK_STATUS=ok');
|
||||
console.log(`LOCK_EVENT_ID=${event.id}`);
|
||||
console.log(`LOCK_PUBKEY=${pk}`);
|
||||
console.log(`LOCK_AGENT=${agent}`);
|
||||
console.log(`LOCK_CADENCE=${cadence}`);
|
||||
console.log(`LOCK_DATE=${dateStr}`);
|
||||
console.log(`LOCK_EXPIRES=${expiresAt}`);
|
||||
console.log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`);
|
||||
return { status: 'ok', eventId: event.id };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# Context for Run 2026-02-17T16-36-25Z
|
||||
- Agent: todo-triage-agent
|
||||
- Cadence: daily
|
||||
- Run-start: 2026-02-17T16-36-25Z
|
||||
|
||||
## Goal
|
||||
Scan the repository for TODO/FIXME/XXX markers and triage them.
|
||||
|
||||
## Scope
|
||||
src/, docs/, bin/, scripts/ directories.
|
||||
|
||||
## Constraints
|
||||
Only fix trivial items. Report others.
|
||||
|
||||
## Active Prompt
|
||||
src/prompts/daily/todo-triage-agent.md
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
agent: design-system-audit-agent
|
||||
cadence: daily
|
||||
session: session-2026-02-18T02:35:22Z
|
||||
run-start: 2026-02-18T02:35:22Z
|
||||
prompt: src/prompts/daily/design-system-audit-agent.md
|
||||
---
|
||||
|
||||
# Context
|
||||
- Goal: Audit design system compliance.
|
||||
- Scope: Daily audit of src/ and dashboard/.
|
||||
- Constraints: Non-blocking, reporting only.
|
||||
+36
-17
@@ -7,6 +7,13 @@ import { fileURLToPath } from 'node:url';
|
||||
import { DEFAULT_DASHBOARD_PORT } from './constants.mjs';
|
||||
import { getDashboardAuth, parseTorchConfig } from './torch-config.mjs';
|
||||
|
||||
const SECURITY_HEADERS = {
|
||||
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss:; object-src 'none'; base-uri 'self';",
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'X-Frame-Options': 'DENY',
|
||||
'Referrer-Policy': 'strict-origin-when-cross-origin'
|
||||
};
|
||||
|
||||
function timingSafeCompare(a, b) {
|
||||
const bufA = Buffer.from(a);
|
||||
const bufB = Buffer.from(b);
|
||||
@@ -30,7 +37,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
if (auth) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader) {
|
||||
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
|
||||
res.writeHead(401, { ...SECURITY_HEADERS, 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
|
||||
res.end('Authentication required');
|
||||
return;
|
||||
}
|
||||
@@ -48,7 +55,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
|
||||
res.writeHead(401, { ...SECURITY_HEADERS, 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
|
||||
res.end('Invalid credentials');
|
||||
return;
|
||||
}
|
||||
@@ -60,7 +67,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
|
||||
// Redirect / to /dashboard/
|
||||
if (pathname === '/' || pathname === '/dashboard') {
|
||||
res.writeHead(302, { 'Location': '/dashboard/' });
|
||||
res.writeHead(302, { ...SECURITY_HEADERS, 'Location': '/dashboard/' });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
@@ -105,31 +112,43 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
const rawConfig = JSON.parse(configContent);
|
||||
const safeConfig = parseTorchConfig(rawConfig);
|
||||
|
||||
// Security: Sanitize sensitive fields
|
||||
if (safeConfig.dashboard) {
|
||||
delete safeConfig.dashboard.auth;
|
||||
}
|
||||
delete safeConfig.configPath;
|
||||
// Security: Whitelist only fields required by the dashboard frontend
|
||||
const publicConfig = {
|
||||
dashboard: safeConfig.dashboard ? { ...safeConfig.dashboard } : {},
|
||||
nostrLock: safeConfig.nostrLock
|
||||
? {
|
||||
namespace: safeConfig.nostrLock.namespace,
|
||||
relays: safeConfig.nostrLock.relays,
|
||||
}
|
||||
: {},
|
||||
};
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
// Remove sensitive dashboard auth
|
||||
if (publicConfig.dashboard.auth) {
|
||||
delete publicConfig.dashboard.auth;
|
||||
}
|
||||
|
||||
res.writeHead(200, { ...SECURITY_HEADERS, 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(safeConfig, null, 2));
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error('Error parsing torch-config.json:', err);
|
||||
res.writeHead(500);
|
||||
res.writeHead(500, SECURITY_HEADERS);
|
||||
res.end('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
res.writeHead(404);
|
||||
res.writeHead(404, SECURITY_HEADERS);
|
||||
res.end('Not Found');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Security check: prevent directory traversal
|
||||
const safePath = path.normalize(pathname).replace(new RegExp('^(\\.\\.[\\/\\\\])+'), '');
|
||||
let filePath = path.join(packageRoot, safePath);
|
||||
// Resolve path relative to packageRoot.
|
||||
// We strip the leading slash from pathname (which comes from URL) to treat it as relative.
|
||||
const relativePath = pathname.replace(/^\//, '');
|
||||
let filePath = path.resolve(packageRoot, relativePath);
|
||||
|
||||
// Security check: restrict access to allowed paths
|
||||
const allowedPaths = [
|
||||
@@ -146,7 +165,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
});
|
||||
|
||||
if (!isAllowed) {
|
||||
res.writeHead(403);
|
||||
res.writeHead(403, SECURITY_HEADERS);
|
||||
res.end('Forbidden');
|
||||
return;
|
||||
}
|
||||
@@ -160,7 +179,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
|
||||
// Check if file exists and is a file
|
||||
if (!fileStat || !fileStat.isFile()) {
|
||||
res.writeHead(404);
|
||||
res.writeHead(404, SECURITY_HEADERS);
|
||||
res.end('Not Found');
|
||||
return;
|
||||
}
|
||||
@@ -181,12 +200,12 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
case '.md': contentType = 'text/markdown'; break;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.writeHead(200, { ...SECURITY_HEADERS, 'Content-Type': contentType });
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
} catch (err) {
|
||||
console.error('Dashboard Server Error:', err);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500);
|
||||
res.writeHead(500, SECURITY_HEADERS);
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Decisions for Run 2026-02-17T16-36-25Z
|
||||
- Agent: todo-triage-agent
|
||||
- Cadence: daily
|
||||
- Run-start: 2026-02-17T16-36-25Z
|
||||
|
||||
## Decision
|
||||
No changes required.
|
||||
|
||||
## Rationale
|
||||
No actionable TODOs were found in the codebase. The scan returned only documentation references and test data (mktemp usage).
|
||||
|
||||
Agent: todo-triage-agent
|
||||
Prompt: src/prompts/daily/todo-triage-agent.md
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
agent: design-system-audit-agent
|
||||
cadence: daily
|
||||
session: session-2026-02-18T02:35:22Z
|
||||
run-start: 2026-02-18T02:35:22Z
|
||||
prompt: src/prompts/daily/design-system-audit-agent.md
|
||||
---
|
||||
|
||||
# Decisions
|
||||
- Decision: Use automated script for audit.
|
||||
- Rationale: No existing tool covers all requirements.
|
||||
+102
-39
@@ -73,10 +73,27 @@ function relayListLabel(relays) {
|
||||
return relays.join(', ');
|
||||
}
|
||||
|
||||
const PUBLISH_ERROR_CODES = {
|
||||
TIMEOUT: 'publish_timeout',
|
||||
DNS: 'dns_resolution',
|
||||
TCP: 'tcp_connect_timeout',
|
||||
TLS: 'tls_handshake',
|
||||
WEBSOCKET: 'websocket_open_failure',
|
||||
NETWORK: 'network_timeout',
|
||||
CONNECTION_RESET: 'connection_reset',
|
||||
RELAY_UNAVAILABLE: 'relay_unavailable',
|
||||
PERMANENT: 'permanent_validation_error',
|
||||
};
|
||||
|
||||
const PUBLISH_FAILURE_CATEGORIES = {
|
||||
QUORUM_FAILURE: 'relay_publish_quorum_failure',
|
||||
NON_RETRYABLE: 'relay_publish_non_retryable',
|
||||
};
|
||||
|
||||
function classifyPublishError(message) {
|
||||
const normalized = String(message || '').toLowerCase();
|
||||
if (normalized.includes('publish timed out after') || normalized.includes('publish timeout')) {
|
||||
return 'publish_timeout';
|
||||
return PUBLISH_ERROR_CODES.TIMEOUT;
|
||||
}
|
||||
if (
|
||||
normalized.includes('enotfound')
|
||||
@@ -84,14 +101,14 @@ function classifyPublishError(message) {
|
||||
|| normalized.includes('getaddrinfo')
|
||||
|| (normalized.includes('dns') && normalized.includes('websocket'))
|
||||
) {
|
||||
return 'dns_resolution';
|
||||
return PUBLISH_ERROR_CODES.DNS;
|
||||
}
|
||||
if (
|
||||
normalized.includes('connect etimedout')
|
||||
|| normalized.includes('tcp connect timed out')
|
||||
|| normalized.includes('connect timeout')
|
||||
) {
|
||||
return 'tcp_connect_timeout';
|
||||
return PUBLISH_ERROR_CODES.TCP;
|
||||
}
|
||||
if (
|
||||
normalized.includes('tls')
|
||||
@@ -99,20 +116,20 @@ function classifyPublishError(message) {
|
||||
|| normalized.includes('certificate')
|
||||
|| normalized.includes('handshake')
|
||||
) {
|
||||
return 'tls_handshake';
|
||||
return PUBLISH_ERROR_CODES.TLS;
|
||||
}
|
||||
if (
|
||||
normalized.includes('websocket')
|
||||
|| normalized.includes('bad response')
|
||||
|| normalized.includes('unexpected server response')
|
||||
) {
|
||||
return 'websocket_open_failure';
|
||||
return PUBLISH_ERROR_CODES.WEBSOCKET;
|
||||
}
|
||||
if (normalized.includes('timed out') || normalized.includes('timeout') || normalized.includes('etimedout')) {
|
||||
return 'network_timeout';
|
||||
return PUBLISH_ERROR_CODES.NETWORK;
|
||||
}
|
||||
if (normalized.includes('econnreset') || normalized.includes('connection reset') || normalized.includes('socket hang up')) {
|
||||
return 'connection_reset';
|
||||
return PUBLISH_ERROR_CODES.CONNECTION_RESET;
|
||||
}
|
||||
if (
|
||||
normalized.includes('unavailable')
|
||||
@@ -122,21 +139,21 @@ function classifyPublishError(message) {
|
||||
|| normalized.includes('enotfound')
|
||||
|| normalized.includes('503')
|
||||
) {
|
||||
return 'relay_unavailable';
|
||||
return PUBLISH_ERROR_CODES.RELAY_UNAVAILABLE;
|
||||
}
|
||||
return 'permanent_validation_error';
|
||||
return PUBLISH_ERROR_CODES.PERMANENT;
|
||||
}
|
||||
|
||||
function isTransientPublishCategory(category) {
|
||||
return [
|
||||
'publish_timeout',
|
||||
'dns_resolution',
|
||||
'tcp_connect_timeout',
|
||||
'tls_handshake',
|
||||
'websocket_open_failure',
|
||||
'network_timeout',
|
||||
'connection_reset',
|
||||
'relay_unavailable',
|
||||
PUBLISH_ERROR_CODES.TIMEOUT,
|
||||
PUBLISH_ERROR_CODES.DNS,
|
||||
PUBLISH_ERROR_CODES.TCP,
|
||||
PUBLISH_ERROR_CODES.TLS,
|
||||
PUBLISH_ERROR_CODES.WEBSOCKET,
|
||||
PUBLISH_ERROR_CODES.NETWORK,
|
||||
PUBLISH_ERROR_CODES.CONNECTION_RESET,
|
||||
PUBLISH_ERROR_CODES.RELAY_UNAVAILABLE,
|
||||
].includes(category);
|
||||
}
|
||||
|
||||
@@ -188,6 +205,8 @@ export class RelayHealthManager {
|
||||
constructor() {
|
||||
this.metricsByRelay = new Map();
|
||||
this.lastSnapshotAt = 0;
|
||||
this._metricsVersion = 0;
|
||||
this._sortedCache = null;
|
||||
}
|
||||
|
||||
ensureMetrics(relay, config) {
|
||||
@@ -203,31 +222,72 @@ export class RelayHealthManager {
|
||||
lastResultAt: null,
|
||||
};
|
||||
this.metricsByRelay.set(relay, metrics);
|
||||
this._metricsVersion += 1;
|
||||
}
|
||||
return metrics;
|
||||
}
|
||||
|
||||
rankRelays(relays, config, nowMs = Date.now()) {
|
||||
const summaries = relays.map((relay) => {
|
||||
const metrics = this.ensureMetrics(relay, config);
|
||||
const summary = summarizeRelayMetrics(metrics, nowMs);
|
||||
return {
|
||||
relay,
|
||||
summary,
|
||||
score: computeRelayScore(summary),
|
||||
};
|
||||
});
|
||||
for (const relay of relays) {
|
||||
this.ensureMetrics(relay, config);
|
||||
}
|
||||
|
||||
return summaries.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
if (a.summary.quarantined !== b.summary.quarantined) return a.summary.quarantined ? 1 : -1;
|
||||
if (a.summary.averageLatencyMs !== b.summary.averageLatencyMs) {
|
||||
if (a.summary.averageLatencyMs === null) return 1;
|
||||
if (b.summary.averageLatencyMs === null) return -1;
|
||||
return a.summary.averageLatencyMs - b.summary.averageLatencyMs;
|
||||
if (
|
||||
!this._sortedCache
|
||||
|| this._sortedCache.version !== this._metricsVersion
|
||||
|| nowMs >= this._sortedCache.validUntil
|
||||
) {
|
||||
let minQuarantineUntil = Infinity;
|
||||
const entries = [];
|
||||
|
||||
for (const metrics of this.metricsByRelay.values()) {
|
||||
const summary = summarizeRelayMetrics(metrics, nowMs);
|
||||
const score = computeRelayScore(summary);
|
||||
if (metrics.quarantineUntil > nowMs && metrics.quarantineUntil < minQuarantineUntil) {
|
||||
minQuarantineUntil = metrics.quarantineUntil;
|
||||
}
|
||||
entries.push({
|
||||
relay: metrics.relay,
|
||||
summary,
|
||||
score,
|
||||
metrics,
|
||||
});
|
||||
}
|
||||
return a.relay.localeCompare(b.relay);
|
||||
});
|
||||
|
||||
entries.sort((a, b) => {
|
||||
if (b.score !== a.score) return b.score - a.score;
|
||||
if (a.summary.quarantined !== b.summary.quarantined) return a.summary.quarantined ? 1 : -1;
|
||||
if (a.summary.averageLatencyMs !== b.summary.averageLatencyMs) {
|
||||
if (a.summary.averageLatencyMs === null) return 1;
|
||||
if (b.summary.averageLatencyMs === null) return -1;
|
||||
return a.summary.averageLatencyMs - b.summary.averageLatencyMs;
|
||||
}
|
||||
return a.relay.localeCompare(b.relay);
|
||||
});
|
||||
|
||||
this._sortedCache = {
|
||||
version: this._metricsVersion,
|
||||
validUntil: minQuarantineUntil,
|
||||
entries,
|
||||
};
|
||||
}
|
||||
|
||||
const requestedSet = new Set(relays);
|
||||
return this._sortedCache.entries
|
||||
.filter((entry) => requestedSet.has(entry.relay))
|
||||
.map((entry) => {
|
||||
const quarantineRemainingMs = entry.metrics.quarantineUntil > nowMs
|
||||
? entry.metrics.quarantineUntil - nowMs
|
||||
: 0;
|
||||
return {
|
||||
relay: entry.relay,
|
||||
score: entry.score,
|
||||
summary: {
|
||||
...entry.summary,
|
||||
quarantineRemainingMs,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
prioritizeRelays(relays, config, nowMs = Date.now()) {
|
||||
@@ -275,6 +335,7 @@ export class RelayHealthManager {
|
||||
metrics.quarantineUntil = nowMs + metrics.cooldownMs;
|
||||
metrics.cooldownMs = Math.min(config.maxQuarantineCooldownMs, Math.floor(metrics.cooldownMs * 1.5));
|
||||
}
|
||||
this._metricsVersion += 1;
|
||||
}
|
||||
|
||||
collectSnapshot(relays, config, nowMs = Date.now()) {
|
||||
@@ -300,6 +361,8 @@ export class RelayHealthManager {
|
||||
reset() {
|
||||
this.metricsByRelay.clear();
|
||||
this.lastSnapshotAt = 0;
|
||||
this._metricsVersion = 0;
|
||||
this._sortedCache = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,7 +558,7 @@ export class LockPublisher {
|
||||
let lastAttemptState = null;
|
||||
const retryTimeline = [];
|
||||
const overallStartedAt = Date.now();
|
||||
let terminalFailureCategory = 'relay_publish_non_retryable';
|
||||
let terminalFailureCategory = PUBLISH_FAILURE_CATEGORIES.NON_RETRYABLE;
|
||||
|
||||
for (let attemptNumber = 1; attemptNumber <= this.maxAttempts; attemptNumber += 1) {
|
||||
const startedAtMs = Date.now();
|
||||
@@ -583,11 +646,11 @@ export class LockPublisher {
|
||||
|
||||
determineFailureCategory(hasTransientFailure, hasPermanentFailure, attemptNumber) {
|
||||
if (hasTransientFailure && !hasPermanentFailure && attemptNumber >= this.maxAttempts) {
|
||||
return 'relay_publish_quorum_failure';
|
||||
return PUBLISH_FAILURE_CATEGORIES.QUORUM_FAILURE;
|
||||
} else if (hasPermanentFailure) {
|
||||
return 'relay_publish_non_retryable';
|
||||
return PUBLISH_FAILURE_CATEGORIES.NON_RETRYABLE;
|
||||
} else {
|
||||
return 'relay_publish_quorum_failure';
|
||||
return PUBLISH_FAILURE_CATEGORIES.QUORUM_FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-10
@@ -5,6 +5,7 @@ import crypto from 'node:crypto';
|
||||
import readline from 'node:readline/promises';
|
||||
import { stdin as input, stdout as output } from 'node:process';
|
||||
import { DEFAULT_RELAYS } from './constants.mjs';
|
||||
import { ensureDir } from './utils.mjs';
|
||||
|
||||
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
@@ -36,12 +37,6 @@ function getPaths(root, installDirName) {
|
||||
};
|
||||
}
|
||||
|
||||
function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function copyDir(src, dest) {
|
||||
if (fs.existsSync(src)) {
|
||||
fs.cpSync(src, dest, { recursive: true });
|
||||
@@ -152,11 +147,26 @@ async function interactiveInit(cwd) {
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveConfiguration(cwd, mockAnswers) {
|
||||
if (mockAnswers) {
|
||||
return mockAnswers;
|
||||
function validateInstallDir(dir) {
|
||||
if (dir === '.') return;
|
||||
|
||||
// Strict validation to prevent command injection
|
||||
// Only allow alphanumeric, hyphens, underscores, slashes, and periods.
|
||||
if (!/^[a-zA-Z0-9_\-\/\.]+$/.test(dir)) {
|
||||
throw new Error(`Invalid directory name: "${dir}". Only alphanumeric characters, hyphens, underscores, slashes, and periods are allowed.`);
|
||||
}
|
||||
return await interactiveInit(cwd);
|
||||
}
|
||||
|
||||
async function resolveConfiguration(cwd, mockAnswers) {
|
||||
let config;
|
||||
if (mockAnswers) {
|
||||
config = mockAnswers;
|
||||
} else {
|
||||
config = await interactiveInit(cwd);
|
||||
}
|
||||
|
||||
validateInstallDir(config.installDir);
|
||||
return config;
|
||||
}
|
||||
|
||||
function ensureInstallDirectory(paths, force, installDir) {
|
||||
|
||||
+17
-7
@@ -53,20 +53,20 @@ const FALLBACK_ROSTER = {
|
||||
|
||||
let cachedCanonicalRoster = null;
|
||||
|
||||
function loadCanonicalRoster() {
|
||||
function loadCanonicalRoster(fsModule = fs) {
|
||||
if (cachedCanonicalRoster) return cachedCanonicalRoster;
|
||||
|
||||
let rosterPath = ROSTER_FILE;
|
||||
|
||||
// Prefer user-managed roster if present
|
||||
if (fs.existsSync(USER_ROSTER_FILE)) {
|
||||
if (fsModule.existsSync(USER_ROSTER_FILE)) {
|
||||
rosterPath = USER_ROSTER_FILE;
|
||||
} else if (fs.existsSync(CWD_ROSTER_FILE)) {
|
||||
} else if (fsModule.existsSync(CWD_ROSTER_FILE)) {
|
||||
rosterPath = CWD_ROSTER_FILE;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
|
||||
const parsed = JSON.parse(fsModule.readFileSync(rosterPath, 'utf8'));
|
||||
const daily = Array.isArray(parsed.daily) ? parsed.daily.map((item) => String(item).trim()).filter(Boolean) : [];
|
||||
const weekly = Array.isArray(parsed.weekly)
|
||||
? parsed.weekly.map((item) => String(item).trim()).filter(Boolean)
|
||||
@@ -86,6 +86,11 @@ function loadCanonicalRoster() {
|
||||
return cachedCanonicalRoster;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function _resetRosterCache() {
|
||||
cachedCanonicalRoster = null;
|
||||
}
|
||||
|
||||
function parseEnvRoster(value) {
|
||||
if (!value) return null;
|
||||
return value
|
||||
@@ -94,11 +99,16 @@ function parseEnvRoster(value) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export async function getRoster(cadence) {
|
||||
const config = await loadTorchConfig();
|
||||
export async function getRoster(cadence, deps = {}) {
|
||||
const {
|
||||
loadTorchConfig: loadTorchConfigFn = loadTorchConfig,
|
||||
fs: fsModule = fs,
|
||||
} = deps;
|
||||
|
||||
const config = await loadTorchConfigFn();
|
||||
const dailyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_DAILY_ROSTER);
|
||||
const weeklyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_WEEKLY_ROSTER);
|
||||
const canonical = loadCanonicalRoster();
|
||||
const canonical = loadCanonicalRoster(fsModule);
|
||||
const dailyFromConfig = config.nostrLock.dailyRoster;
|
||||
const weeklyFromConfig = config.nostrLock.weeklyRoster;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getMemoryPruneMode, isMemoryIngestEnabled } from './feature-flags.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import util from 'node:util';
|
||||
import { ensureDir } from '../../utils.mjs';
|
||||
|
||||
const MEMORY_FILE_PATH = path.join(process.cwd(), '.scheduler-memory', 'memory-store.json');
|
||||
const debug = util.debuglog('torch-memory');
|
||||
|
||||
@@ -67,22 +67,29 @@ function evaluateCandidate(memory, policy) {
|
||||
* @param {{ getEmbedding?: (memory: import('./schema.js').MemoryRecord) => number[] | null | undefined, similarityThreshold: number, duplicateWindowMs: number }} options
|
||||
*/
|
||||
function groupNearDuplicates(memories, options) {
|
||||
// Sort by created_at to enable early exit in the inner loop
|
||||
const sortedMemories = [...memories].sort((a, b) => a.created_at - b.created_at);
|
||||
|
||||
const groups = [];
|
||||
const consumed = new Set();
|
||||
|
||||
for (let i = 0; i < memories.length; i += 1) {
|
||||
const base = memories[i];
|
||||
for (let i = 0; i < sortedMemories.length; i += 1) {
|
||||
const base = sortedMemories[i];
|
||||
if (consumed.has(base.id) || base.merged_into) continue;
|
||||
|
||||
const baseEmbedding = options.getEmbedding?.(base);
|
||||
const group = [base];
|
||||
|
||||
for (let j = i + 1; j < memories.length; j += 1) {
|
||||
const probe = memories[j];
|
||||
for (let j = i + 1; j < sortedMemories.length; j += 1) {
|
||||
const probe = sortedMemories[j];
|
||||
|
||||
// Optimization: Since sorted by created_at, if the time difference exceeds the window,
|
||||
// all subsequent items will also be outside the window.
|
||||
if (probe.created_at - base.created_at > options.duplicateWindowMs) break;
|
||||
|
||||
if (consumed.has(probe.id) || probe.merged_into || probe.id === base.id) continue;
|
||||
|
||||
const withinWindow = Math.abs(base.created_at - probe.created_at) <= options.duplicateWindowMs;
|
||||
if (!withinWindow || !hasTagOverlap(base.tags, probe.tags)) continue;
|
||||
if (!hasTagOverlap(base.tags, probe.tags)) continue;
|
||||
|
||||
const similarity = cosineSimilarity(baseEmbedding, options.getEmbedding?.(probe));
|
||||
if (similarity >= options.similarityThreshold) {
|
||||
@@ -189,12 +196,13 @@ export async function createLifecyclePlan(memories, options) {
|
||||
});
|
||||
|
||||
const templates = loadMemoryPromptTemplates({ templateDir: options.templateDir });
|
||||
const condensedGroups = [];
|
||||
const condensedGroups = await Promise.all(
|
||||
groups.map((group) => condenseGroup(group, options.generateSummary, templates.condense))
|
||||
);
|
||||
|
||||
const mergedIntoMap = new Map();
|
||||
|
||||
for (const group of groups) {
|
||||
const condensed = await condenseGroup(group, options.generateSummary, templates.condense);
|
||||
condensedGroups.push(condensed);
|
||||
for (const condensed of condensedGroups) {
|
||||
for (const mergedId of condensed.mergedIds) {
|
||||
mergedIntoMap.set(mergedId, condensed.merged.id);
|
||||
}
|
||||
@@ -265,9 +273,7 @@ export async function createLifecyclePlan(memories, options) {
|
||||
* @param {Awaited<ReturnType<typeof createLifecyclePlan>>} plan
|
||||
*/
|
||||
export async function applyLifecycleActions(repository, plan) {
|
||||
const results = [];
|
||||
|
||||
for (const decision of plan.actions) {
|
||||
const results = await Promise.all(plan.actions.map(async (decision) => {
|
||||
if (decision.merged_into && typeof repository.markMerged === 'function') {
|
||||
await repository.markMerged(decision.id, decision.merged_into);
|
||||
}
|
||||
@@ -280,8 +286,8 @@ export async function applyLifecycleActions(repository, plan) {
|
||||
await repository.keepMemory?.(decision.id, decision.reason);
|
||||
}
|
||||
|
||||
results.push(decision);
|
||||
}
|
||||
return decision;
|
||||
}));
|
||||
|
||||
return {
|
||||
applied: results,
|
||||
|
||||
@@ -31,17 +31,20 @@ function computeRecencyDecay(timestamp, now, halfLifeMs) {
|
||||
return Math.exp(-(age / halfLifeMs));
|
||||
}
|
||||
|
||||
function textMatchSimilarity(query, memory) {
|
||||
const normalizedQuery = String(query ?? '').trim().toLowerCase();
|
||||
if (!normalizedQuery) return 0;
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
||||
const haystack = `${memory.summary} ${memory.content}`.toLowerCase();
|
||||
if (haystack.includes(normalizedQuery)) return 1;
|
||||
function textMatchSimilarity(queryTerms, queryRegexes, memory) {
|
||||
if (!queryTerms || queryTerms.length === 0) return 0;
|
||||
|
||||
const queryTerms = normalizedQuery.split(/\s+/).filter(Boolean);
|
||||
if (queryTerms.length === 0) return 0;
|
||||
let matchedTerms = 0;
|
||||
for (const regex of queryRegexes) {
|
||||
if (regex.test(memory.summary) || regex.test(memory.content)) {
|
||||
matchedTerms++;
|
||||
}
|
||||
}
|
||||
|
||||
const matchedTerms = queryTerms.filter((term) => haystack.includes(term)).length;
|
||||
return matchedTerms / queryTerms.length;
|
||||
}
|
||||
|
||||
@@ -102,8 +105,11 @@ async function buildSimilarityIndex(memories, params) {
|
||||
}
|
||||
}
|
||||
|
||||
const queryTerms = normalizedQuery.toLowerCase().split(/\s+/).filter(Boolean);
|
||||
const queryRegexes = queryTerms.map((t) => new RegExp(escapeRegExp(t), 'i'));
|
||||
|
||||
for (const memory of memories) {
|
||||
const lexicalScore = textMatchSimilarity(normalizedQuery, memory);
|
||||
const lexicalScore = textMatchSimilarity(queryTerms, queryRegexes, memory);
|
||||
if (lexicalScore > 0) {
|
||||
similarityById.set(memory.id, lexicalScore);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Test Logs for Run 2026-02-17T16-36-25Z
|
||||
- Agent: todo-triage-agent
|
||||
- Cadence: daily
|
||||
- Run-start: 2026-02-17T16-36-25Z
|
||||
|
||||
- Command: Scan for TODOs
|
||||
Result: 0 actionable items found.
|
||||
|
||||
Agent: todo-triage-agent
|
||||
Prompt: src/prompts/daily/todo-triage-agent.md
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
agent: design-system-audit-agent
|
||||
cadence: daily
|
||||
session: session-2026-02-18T02:35:22Z
|
||||
run-start: 2026-02-18T02:35:22Z
|
||||
prompt: src/prompts/daily/design-system-audit-agent.md
|
||||
---
|
||||
|
||||
# Test Log
|
||||
- Command: npm run lint
|
||||
- Result: Passed (with warnings)
|
||||
@@ -0,0 +1,13 @@
|
||||
# Todo List for Run 2026-02-17T16-36-25Z
|
||||
- Agent: todo-triage-agent
|
||||
- Cadence: daily
|
||||
- Run-start: 2026-02-17T16-36-25Z
|
||||
|
||||
## Completed
|
||||
- [x] Scan codebase for TODOs (todo-triage-agent)
|
||||
- [x] Triage findings (0 found)
|
||||
|
||||
## Pending
|
||||
None.
|
||||
|
||||
Ref: src/prompts/daily/todo-triage-agent.md
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
agent: design-system-audit-agent
|
||||
cadence: daily
|
||||
session: session-2026-02-18T02:35:22Z
|
||||
run-start: 2026-02-18T02:35:22Z
|
||||
prompt: src/prompts/daily/design-system-audit-agent.md
|
||||
---
|
||||
|
||||
# Todo
|
||||
|
||||
## Pending Tasks
|
||||
- [ ] Review unsanctioned colors.
|
||||
@@ -1,3 +1,5 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
export function todayDateStr() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
@@ -34,3 +36,9 @@ export function detectPlatform() {
|
||||
if (process.env.GOOSE_API_KEY || process.env.GOOSE_SESSION_ID) return 'goose';
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user