mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Improve scheduler lock observability and reliability reporting
This commit is contained in:
1 parent
76b089327a
commit
ecb26dfa99
9 files changed
+384
-11
No files matched your search
@@ -24,3 +24,13 @@ Track only **active, reproducible, unresolved** issues.
|
|||||||
- **Workaround:** Run `npm run validate:scheduler` separately and inspect output, or ignore errors for now.
|
- **Workaround:** Run `npm run validate:scheduler` separately and inspect output, or ignore errors for now.
|
||||||
- **Impact:** CI/local testing blocked.
|
- **Impact:** CI/local testing blocked.
|
||||||
- **Last verified:** 2026-02-14
|
- **Last verified:** 2026-02-14
|
||||||
|
|
||||||
|
### Recurring scheduler lock backend failures in recent task logs
|
||||||
|
- **Status:** Monitoring
|
||||||
|
- **Area:** Runtime
|
||||||
|
- **Symptom:** Multiple scheduler runs fail with `Lock backend error` before prompt handoff/validation.
|
||||||
|
- **Trigger/Conditions:** Running scheduler cycles when relay connectivity is unstable or lock backend operations time out; observed in both `daily` and `weekly` task logs.
|
||||||
|
- **Workaround:** Retry with explicit platform and inspect reliability report output (`npm run report:lock-reliability`), then validate relay health (`node scripts/agent/check-relay-health.mjs --cadence <daily|weekly>`) before rerunning scheduler.
|
||||||
|
- **Impact:** Scheduled agent execution is skipped for affected cycles until lock acquisition succeeds.
|
||||||
|
- **Last verified:** 2026-02-14
|
||||||
|
|
||||||
@@ -127,11 +127,27 @@ If you are developing `torch-lock` itself:
|
|||||||
- `npm run dashboard:serve`
|
- `npm run dashboard:serve`
|
||||||
- `npm test` (run unit tests)
|
- `npm test` (run unit tests)
|
||||||
- `npm run lint` (run linter)
|
- `npm run lint` (run linter)
|
||||||
|
- `npm run report:lock-reliability` (aggregate recent scheduler logs into markdown+JSON reliability reports)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Scheduler lock reliability reporting
|
||||||
|
|
||||||
|
Use the reporting hook below to inspect lock backend reliability trends by platform/cadence/error category/relay:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run report:lock-reliability
|
||||||
|
```
|
||||||
|
|
||||||
|
This writes:
|
||||||
|
- `artifacts/lock-reliability/lock-reliability-summary.md`
|
||||||
|
- `artifacts/lock-reliability/lock-reliability-summary.json`
|
||||||
|
|
||||||
|
Recommended cadence: run this at least weekly (or wire into your CI/cron scheduler) and compare `codex` vs other `platform` values.
|
||||||
|
|
||||||
|
|
||||||
## Memory rollout plan
|
## Memory rollout plan
|
||||||
|
|
||||||
1. Deploy memory schema changes with scheduler jobs disabled (`TORCH_MEMORY_ENABLED=false` or subsystem flags set to `false`).
|
1. Deploy memory schema changes with scheduler jobs disabled (`TORCH_MEMORY_ENABLED=false` or subsystem flags set to `false`).
|
||||||
|
|||||||
+2
-1
@@ -38,7 +38,8 @@
|
|||||||
"lock:complete": "node bin/torch-lock.mjs complete",
|
"lock:complete": "node bin/torch-lock.mjs complete",
|
||||||
"validate:scheduler": "node scripts/validate-scheduler-roster.mjs && node scripts/validate-prompt-contract.mjs && node scripts/validate-scheduler-flow-parity.mjs",
|
"validate:scheduler": "node scripts/validate-scheduler-roster.mjs && node scripts/validate-prompt-contract.mjs && node scripts/validate-scheduler-flow-parity.mjs",
|
||||||
"scheduler:daily": "node scripts/agent/run-scheduler-cycle.mjs daily",
|
"scheduler:daily": "node scripts/agent/run-scheduler-cycle.mjs daily",
|
||||||
"scheduler:weekly": "node scripts/agent/run-scheduler-cycle.mjs weekly"
|
"scheduler:weekly": "node scripts/agent/run-scheduler-cycle.mjs weekly",
|
||||||
|
"report:lock-reliability": "node scripts/agent/summarize-lock-reliability.mjs --since-days 30"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nostr-tools": "2.19.4",
|
"nostr-tools": "2.19.4",
|
||||||
|
|||||||
@@ -421,10 +421,15 @@ function toYamlScalar(value) {
|
|||||||
return `'${str.replace(/'/g, "''")}'`;
|
return `'${str.replace(/'/g, "''")}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeLog({ cadence, agent, status, reason, detail, metadata = {} }) {
|
async function writeLog({ cadence, agent, status, reason, detail, platform, metadata = {} }) {
|
||||||
const logDir = path.resolve(process.cwd(), 'task-logs', cadence);
|
const logDir = path.resolve(process.cwd(), 'task-logs', cadence);
|
||||||
await fs.mkdir(logDir, { recursive: true });
|
await fs.mkdir(logDir, { recursive: true });
|
||||||
const file = `${ts()}__${agent}__${status}.md`;
|
const file = `${ts()}__${agent}__${status}.md`;
|
||||||
|
const mergedMetadata = {
|
||||||
|
platform: platform || process.env.AGENT_PLATFORM || 'unknown',
|
||||||
|
...metadata,
|
||||||
|
};
|
||||||
|
|
||||||
const body = [
|
const body = [
|
||||||
'---',
|
'---',
|
||||||
`cadence: ${cadence}`,
|
`cadence: ${cadence}`,
|
||||||
@@ -432,7 +437,7 @@ async function writeLog({ cadence, agent, status, reason, detail, metadata = {}
|
|||||||
`status: ${status}`,
|
`status: ${status}`,
|
||||||
`created_at: ${new Date().toISOString()}`,
|
`created_at: ${new Date().toISOString()}`,
|
||||||
`timestamp: ${new Date().toISOString()}`,
|
`timestamp: ${new Date().toISOString()}`,
|
||||||
...Object.entries(metadata)
|
...Object.entries(mergedMetadata)
|
||||||
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||||
.map(([key, value]) => `${key}: ${toYamlScalar(value)}`),
|
.map(([key, value]) => `${key}: ${toYamlScalar(value)}`),
|
||||||
'---',
|
'---',
|
||||||
@@ -441,7 +446,7 @@ async function writeLog({ cadence, agent, status, reason, detail, metadata = {}
|
|||||||
'',
|
'',
|
||||||
`- reason: ${reason}`,
|
`- reason: ${reason}`,
|
||||||
detail ? `- detail: ${detail}` : null,
|
detail ? `- detail: ${detail}` : null,
|
||||||
...Object.entries(metadata)
|
...Object.entries(mergedMetadata)
|
||||||
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
||||||
.map(([key, value]) => `- ${key}: ${value}`),
|
.map(([key, value]) => `- ${key}: ${value}`),
|
||||||
'',
|
'',
|
||||||
@@ -483,6 +488,7 @@ async function main() {
|
|||||||
cadence,
|
cadence,
|
||||||
agent: 'scheduler',
|
agent: 'scheduler',
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
platform,
|
||||||
reason: 'Lock backend unavailable preflight',
|
reason: 'Lock backend unavailable preflight',
|
||||||
detail: `Preflight failed (${preflight.failureCategory}). Retry scheduler after verifying relay connectivity, relay URLs, and DNS/network status.`,
|
detail: `Preflight failed (${preflight.failureCategory}). Retry scheduler after verifying relay connectivity, relay URLs, and DNS/network status.`,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -531,6 +537,7 @@ async function main() {
|
|||||||
cadence,
|
cadence,
|
||||||
agent: 'scheduler',
|
agent: 'scheduler',
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
platform,
|
||||||
reason: ALL_EXCLUDED_REASON,
|
reason: ALL_EXCLUDED_REASON,
|
||||||
});
|
});
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -559,6 +566,7 @@ async function main() {
|
|||||||
cadence,
|
cadence,
|
||||||
agent: selectedAgent,
|
agent: selectedAgent,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
platform,
|
||||||
reason: 'Lock backend error',
|
reason: 'Lock backend error',
|
||||||
detail: `Lock backend error (${backendCategory}) after ${lockAttempt.attempts} attempt(s). Retry ${lockCommand} after verifying relay connectivity, relay URLs, and DNS/network status.`,
|
detail: `Lock backend error (${backendCategory}) after ${lockAttempt.attempts} attempt(s). Retry ${lockCommand} after verifying relay connectivity, relay URLs, and DNS/network status.`,
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -578,6 +586,7 @@ async function main() {
|
|||||||
cadence,
|
cadence,
|
||||||
agent: selectedAgent,
|
agent: selectedAgent,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
platform,
|
||||||
reason: 'Failed to acquire lock',
|
reason: 'Failed to acquire lock',
|
||||||
});
|
});
|
||||||
process.exit(lockResult.code);
|
process.exit(lockResult.code);
|
||||||
@@ -594,7 +603,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
outputChunks.push(retrieveResult.stdout, retrieveResult.stderr);
|
outputChunks.push(retrieveResult.stdout, retrieveResult.stderr);
|
||||||
if (retrieveResult.code !== 0) {
|
if (retrieveResult.code !== 0) {
|
||||||
await writeLog({ cadence, agent: selectedAgent, status: 'failed', reason: 'Memory retrieval command failed', detail: schedulerConfig.memoryPolicy.retrieveCommand });
|
await writeLog({ cadence, agent: selectedAgent, status: 'failed', platform, reason: 'Memory retrieval command failed', detail: schedulerConfig.memoryPolicy.retrieveCommand });
|
||||||
process.exit(retrieveResult.code);
|
process.exit(retrieveResult.code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -605,14 +614,14 @@ async function main() {
|
|||||||
});
|
});
|
||||||
outputChunks.push(handoff.stdout, handoff.stderr);
|
outputChunks.push(handoff.stdout, handoff.stderr);
|
||||||
if (handoff.code !== 0) {
|
if (handoff.code !== 0) {
|
||||||
await writeLog({ cadence, agent: selectedAgent, status: 'failed', reason: 'Prompt/handoff execution failed', detail: 'Handoff callback failed.' });
|
await writeLog({ cadence, agent: selectedAgent, status: 'failed', platform, reason: 'Prompt/handoff execution failed', detail: 'Handoff callback failed.' });
|
||||||
process.exit(handoff.code);
|
process.exit(handoff.code);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const detail = schedulerConfig.missingHandoffCommandForMode
|
const detail = schedulerConfig.missingHandoffCommandForMode
|
||||||
? 'Missing scheduler handoff command for non-interactive run. Set scheduler.handoffCommandByCadence.daily|weekly in torch-config.json.'
|
? 'Missing scheduler handoff command for non-interactive run. Set scheduler.handoffCommandByCadence.daily|weekly in torch-config.json.'
|
||||||
: 'No handoff callback configured. Set scheduler.handoffCommandByCadence.daily|weekly in torch-config.json.';
|
: 'No handoff callback configured. Set scheduler.handoffCommandByCadence.daily|weekly in torch-config.json.';
|
||||||
await writeLog({ cadence, agent: selectedAgent, status: 'failed', reason: 'Prompt/handoff execution failed', detail });
|
await writeLog({ cadence, agent: selectedAgent, status: 'failed', platform, reason: 'Prompt/handoff execution failed', detail });
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,7 +631,7 @@ async function main() {
|
|||||||
});
|
});
|
||||||
outputChunks.push(storeResult.stdout, storeResult.stderr);
|
outputChunks.push(storeResult.stdout, storeResult.stderr);
|
||||||
if (storeResult.code !== 0) {
|
if (storeResult.code !== 0) {
|
||||||
await writeLog({ cadence, agent: selectedAgent, status: 'failed', reason: 'Memory storage command failed', detail: schedulerConfig.memoryPolicy.storeCommand });
|
await writeLog({ cadence, agent: selectedAgent, status: 'failed', platform, reason: 'Memory storage command failed', detail: schedulerConfig.memoryPolicy.storeCommand });
|
||||||
process.exit(storeResult.code);
|
process.exit(storeResult.code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -649,6 +658,7 @@ async function main() {
|
|||||||
cadence,
|
cadence,
|
||||||
agent: selectedAgent,
|
agent: selectedAgent,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
platform,
|
||||||
reason: 'Required memory steps not verified',
|
reason: 'Required memory steps not verified',
|
||||||
detail: `Missing evidence for: ${missingSteps.join(', ')}`,
|
detail: `Missing evidence for: ${missingSteps.join(', ')}`,
|
||||||
});
|
});
|
||||||
@@ -679,6 +689,7 @@ async function main() {
|
|||||||
cadence,
|
cadence,
|
||||||
agent: selectedAgent,
|
agent: selectedAgent,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
platform,
|
||||||
reason: 'Missing required run artifacts',
|
reason: 'Missing required run artifacts',
|
||||||
detail,
|
detail,
|
||||||
});
|
});
|
||||||
@@ -690,7 +701,7 @@ async function main() {
|
|||||||
if (!parts.length) continue;
|
if (!parts.length) continue;
|
||||||
const result = await runCommand(parts[0], parts.slice(1));
|
const result = await runCommand(parts[0], parts.slice(1));
|
||||||
if (result.code !== 0) {
|
if (result.code !== 0) {
|
||||||
await writeLog({ cadence, agent: selectedAgent, status: 'failed', reason: 'Validation failed', detail: validation });
|
await writeLog({ cadence, agent: selectedAgent, status: 'failed', platform, reason: 'Validation failed', detail: validation });
|
||||||
process.exit(result.code);
|
process.exit(result.code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -706,12 +717,13 @@ async function main() {
|
|||||||
cadence,
|
cadence,
|
||||||
agent: selectedAgent,
|
agent: selectedAgent,
|
||||||
status: 'failed',
|
status: 'failed',
|
||||||
|
platform,
|
||||||
reason: `Completion publish failed. Retry npm run lock:complete -- --agent ${selectedAgent} --cadence ${cadence} after verifying relay connectivity`,
|
reason: `Completion publish failed. Retry npm run lock:complete -- --agent ${selectedAgent} --cadence ${cadence} after verifying relay connectivity`,
|
||||||
});
|
});
|
||||||
process.exit(completeResult.code);
|
process.exit(completeResult.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
await writeLog({ cadence, agent: selectedAgent, status: 'completed', reason: 'Scheduler cycle completed successfully' });
|
await writeLog({ cadence, agent: selectedAgent, status: 'completed', platform, reason: 'Scheduler cycle completed successfully' });
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const VALID_CADENCES = ['daily', 'weekly'];
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const options = {
|
||||||
|
sinceDays: 14,
|
||||||
|
logRoot: path.resolve(process.cwd(), 'task-logs'),
|
||||||
|
outDir: path.resolve(process.cwd(), 'artifacts', 'lock-reliability'),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
|
const token = argv[i];
|
||||||
|
if (token === '--since-days') {
|
||||||
|
options.sinceDays = Number.parseInt(argv[i + 1] || '14', 10);
|
||||||
|
i += 1;
|
||||||
|
} else if (token === '--log-root') {
|
||||||
|
options.logRoot = path.resolve(process.cwd(), argv[i + 1] || 'task-logs');
|
||||||
|
i += 1;
|
||||||
|
} else if (token === '--out-dir') {
|
||||||
|
options.outDir = path.resolve(process.cwd(), argv[i + 1] || path.join('artifacts', 'lock-reliability'));
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isFinite(options.sinceDays) || options.sinceDays < 0) {
|
||||||
|
throw new Error(`Invalid --since-days value: ${options.sinceDays}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFrontmatter(markdown) {
|
||||||
|
const text = String(markdown || '');
|
||||||
|
if (!text.startsWith('---\n')) return {};
|
||||||
|
const end = text.indexOf('\n---', 4);
|
||||||
|
if (end === -1) return {};
|
||||||
|
const frontmatter = text.slice(4, end).split(/\r?\n/);
|
||||||
|
const result = {};
|
||||||
|
for (const line of frontmatter) {
|
||||||
|
const match = line.match(/^([a-zA-Z0-9_]+):\s*(.+)\s*$/);
|
||||||
|
if (!match) continue;
|
||||||
|
const [, key, rawValue] = match;
|
||||||
|
result[key] = rawValue.replace(/^'/, '').replace(/'$/, '').replace(/''/g, "'");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function inc(map, key) {
|
||||||
|
map.set(key, (map.get(key) || 0) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectRelayEndpoints({ frontmatter, body }) {
|
||||||
|
const relays = new Set();
|
||||||
|
const relayList = frontmatter.relay_list || '';
|
||||||
|
relayList.split(',').map((v) => v.trim()).filter(Boolean).forEach((relay) => relays.add(relay));
|
||||||
|
|
||||||
|
const allText = `${body}\n${Object.values(frontmatter).join('\n')}`;
|
||||||
|
const urlMatches = allText.match(/wss?:\/\/[^\s'`),]+/g) || [];
|
||||||
|
for (const url of urlMatches) {
|
||||||
|
relays.add(url.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!relays.size) relays.add('unknown');
|
||||||
|
return [...relays];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBackendCategory({ frontmatter, body }) {
|
||||||
|
if (frontmatter.backend_category) return frontmatter.backend_category;
|
||||||
|
if (frontmatter.preflight_failure_category) return `preflight:${frontmatter.preflight_failure_category}`;
|
||||||
|
|
||||||
|
const reasonMatch = body.match(/- reason:\s*([^\n]+)/i) || body.match(/reason:\s*([^\n]+)/i);
|
||||||
|
const reason = reasonMatch?.[1]?.trim() || '';
|
||||||
|
|
||||||
|
if (/lock backend/i.test(reason)) return 'lock backend error (uncategorized)';
|
||||||
|
if (/completion publish failed/i.test(reason)) return 'completion publish failed';
|
||||||
|
if (/preflight/i.test(reason)) return 'preflight failed (uncategorized)';
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toSortedObject(counterMap) {
|
||||||
|
return Object.fromEntries([...counterMap.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])));
|
||||||
|
}
|
||||||
|
|
||||||
|
function asMarkdown(report) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push('# Lock Reliability Summary');
|
||||||
|
lines.push('');
|
||||||
|
lines.push(`- Generated at: ${report.generatedAt}`);
|
||||||
|
lines.push(`- Window: last ${report.sinceDays} day(s)`);
|
||||||
|
lines.push(`- Logs scanned: ${report.logsScanned}`);
|
||||||
|
lines.push(`- Runs considered: ${report.runsConsidered}`);
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
lines.push('## Totals');
|
||||||
|
lines.push('');
|
||||||
|
lines.push(`- completed: ${report.statusCounts.completed || 0}`);
|
||||||
|
lines.push(`- failed: ${report.statusCounts.failed || 0}`);
|
||||||
|
lines.push('');
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
['By platform', report.byPlatform],
|
||||||
|
['By cadence', report.byCadence],
|
||||||
|
['By backend error category', report.byBackendCategory],
|
||||||
|
['By relay endpoint', report.byRelayEndpoint],
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [title, obj] of sections) {
|
||||||
|
lines.push(`## ${title}`);
|
||||||
|
lines.push('');
|
||||||
|
const entries = Object.entries(obj);
|
||||||
|
if (!entries.length) {
|
||||||
|
lines.push('- (none)');
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of entries) {
|
||||||
|
lines.push(`- ${key}: ${value}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (report.recurringBackendErrors.length) {
|
||||||
|
lines.push('## Recurring backend errors (count >= 2)');
|
||||||
|
lines.push('');
|
||||||
|
for (const entry of report.recurringBackendErrors) {
|
||||||
|
lines.push(`- ${entry.category}: ${entry.count}`);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readLogFile(filePath, cadence, cutoffMs) {
|
||||||
|
const stat = await fs.stat(filePath);
|
||||||
|
if (!stat.isFile() || stat.mtimeMs < cutoffMs) return null;
|
||||||
|
|
||||||
|
const basename = path.basename(filePath);
|
||||||
|
const match = basename.match(/^.+__([^_]+?)__(completed|failed)\.md$/);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
const body = await fs.readFile(filePath, 'utf8');
|
||||||
|
const frontmatter = parseFrontmatter(body);
|
||||||
|
const status = frontmatter.status || match[2];
|
||||||
|
const platform = frontmatter.platform || 'unknown';
|
||||||
|
const backendCategory = status === 'failed'
|
||||||
|
? getBackendCategory({ frontmatter, body })
|
||||||
|
: 'n/a';
|
||||||
|
const relayEndpoints = collectRelayEndpoints({ frontmatter, body });
|
||||||
|
|
||||||
|
return {
|
||||||
|
filePath,
|
||||||
|
cadence: frontmatter.cadence || cadence,
|
||||||
|
status,
|
||||||
|
platform,
|
||||||
|
backendCategory,
|
||||||
|
relayEndpoints,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const options = parseArgs(process.argv.slice(2));
|
||||||
|
const now = Date.now();
|
||||||
|
const cutoffMs = now - (options.sinceDays * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const byPlatform = new Map();
|
||||||
|
const byCadence = new Map();
|
||||||
|
const byBackendCategory = new Map();
|
||||||
|
const byRelayEndpoint = new Map();
|
||||||
|
const statusCounts = new Map();
|
||||||
|
const records = [];
|
||||||
|
let logsScanned = 0;
|
||||||
|
|
||||||
|
for (const cadence of VALID_CADENCES) {
|
||||||
|
const cadenceDir = path.join(options.logRoot, cadence);
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
entries = await fs.readdir(cadenceDir);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(cadenceDir, entry);
|
||||||
|
logsScanned += 1;
|
||||||
|
const record = await readLogFile(fullPath, cadence, cutoffMs);
|
||||||
|
if (!record) continue;
|
||||||
|
|
||||||
|
records.push(record);
|
||||||
|
inc(statusCounts, record.status);
|
||||||
|
inc(byPlatform, record.platform);
|
||||||
|
inc(byCadence, record.cadence);
|
||||||
|
if (record.status === 'failed' && record.backendCategory) {
|
||||||
|
inc(byBackendCategory, record.backendCategory);
|
||||||
|
}
|
||||||
|
for (const relay of record.relayEndpoints) {
|
||||||
|
inc(byRelayEndpoint, relay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const recurringBackendErrors = [...byBackendCategory.entries()]
|
||||||
|
.filter(([, count]) => count >= 2)
|
||||||
|
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||||
|
.map(([category, count]) => ({ category, count }));
|
||||||
|
|
||||||
|
const report = {
|
||||||
|
generatedAt: new Date().toISOString(),
|
||||||
|
sinceDays: options.sinceDays,
|
||||||
|
logRoot: options.logRoot,
|
||||||
|
logsScanned,
|
||||||
|
runsConsidered: records.length,
|
||||||
|
statusCounts: toSortedObject(statusCounts),
|
||||||
|
byPlatform: toSortedObject(byPlatform),
|
||||||
|
byCadence: toSortedObject(byCadence),
|
||||||
|
byBackendCategory: toSortedObject(byBackendCategory),
|
||||||
|
byRelayEndpoint: toSortedObject(byRelayEndpoint),
|
||||||
|
recurringBackendErrors,
|
||||||
|
};
|
||||||
|
|
||||||
|
await fs.mkdir(options.outDir, { recursive: true });
|
||||||
|
const markdown = asMarkdown(report);
|
||||||
|
const markdownPath = path.join(options.outDir, 'lock-reliability-summary.md');
|
||||||
|
const jsonPath = path.join(options.outDir, 'lock-reliability-summary.json');
|
||||||
|
|
||||||
|
await fs.writeFile(markdownPath, markdown, 'utf8');
|
||||||
|
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
markdown: path.relative(process.cwd(), markdownPath),
|
||||||
|
json: path.relative(process.cwd(), jsonPath),
|
||||||
|
recurringBackendErrors,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error?.stack || error?.message || String(error));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -64,6 +64,19 @@ For weekly repository-fit maintenance, TORCH also includes `src/prompts/weekly/r
|
|||||||
|
|
||||||
Operational note: scheduler handoff commands are treated as required execution steps. A non-zero exit code (or missing command) is a hard failure: the scheduler writes a `_failed.md` task log, exits immediately, and does not publish `lock:complete` for that run.
|
Operational note: scheduler handoff commands are treated as required execution steps. A non-zero exit code (or missing command) is a hard failure: the scheduler writes a `_failed.md` task log, exits immediately, and does not publish `lock:complete` for that run.
|
||||||
|
|
||||||
|
## Scheduler lock reliability reporting
|
||||||
|
|
||||||
|
Run the lock reliability summary to aggregate recent scheduler outcomes by platform, cadence, backend error category, and relay endpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run report:lock-reliability
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- `artifacts/lock-reliability/lock-reliability-summary.md`
|
||||||
|
- `artifacts/lock-reliability/lock-reliability-summary.json`
|
||||||
|
|
||||||
|
|
||||||
## Roster precedence
|
## Roster precedence
|
||||||
|
|
||||||
The lock CLI resolves roster names in this order:
|
The lock CLI resolves roster names in this order:
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ Every agent prompt invoked by the schedulers (daily/weekly) MUST enforce this co
|
|||||||
|
|
||||||
- `_completed.md` MUST be created only after completion publish succeeds.
|
- `_completed.md` MUST be created only after completion publish succeeds.
|
||||||
- `_failed.md` is required when step 11, step 12, or step 13 fails, and should include the failure reason and next retry action.
|
- `_failed.md` is required when step 11, step 12, or step 13 fails, and should include the failure reason and next retry action.
|
||||||
|
- Include `platform` in frontmatter using `AGENT_PLATFORM` (or the scheduler `--platform` value) for both `_completed.md` and `_failed.md`.
|
||||||
|
|
||||||
15. Commit/push behavior is delegated outside this scheduler script.
|
15. Commit/push behavior is delegated outside this scheduler script.
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,10 @@ test('accepts required memory policy when markers or artifacts are produced', {
|
|||||||
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||||
|
|
||||||
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
|
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
|
||||||
assert.ok(logs.some((name) => name.endsWith('__completed.md')));
|
const completedLog = logs.find((name) => name.endsWith('__completed.md'));
|
||||||
|
assert.ok(completedLog);
|
||||||
|
const completedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', completedLog), 'utf8');
|
||||||
|
assert.match(completedBody, /platform: 'codex'/);
|
||||||
|
|
||||||
await fs.access(path.join(fixture.root, '.scheduler-memory', 'retrieve-daily.ok'));
|
await fs.access(path.join(fixture.root, '.scheduler-memory', 'retrieve-daily.ok'));
|
||||||
await fs.access(path.join(fixture.root, '.scheduler-memory', 'store-daily.ok'));
|
await fs.access(path.join(fixture.root, '.scheduler-memory', 'store-daily.ok'));
|
||||||
@@ -188,6 +191,7 @@ test('records backend failure metadata when lock command exits with code 2', { c
|
|||||||
assert.match(failedBody, /lock_stderr_excerpt: 'publish failed to all relays token=\[REDACTED\] SECRET_KEY=\[REDACTED\]'/);
|
assert.match(failedBody, /lock_stderr_excerpt: 'publish failed to all relays token=\[REDACTED\] SECRET_KEY=\[REDACTED\]'/);
|
||||||
assert.match(failedBody, /lock_stdout_excerpt: 'websocket: connection refused'/);
|
assert.match(failedBody, /lock_stdout_excerpt: 'websocket: connection refused'/);
|
||||||
assert.match(failedBody, /Retry AGENT_PLATFORM=codex npm run lock:lock -- --agent agent-a --cadence daily/);
|
assert.match(failedBody, /Retry AGENT_PLATFORM=codex npm run lock:lock -- --agent agent-a --cadence daily/);
|
||||||
|
assert.match(failedBody, /platform: 'codex'/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
|
||||||
|
const SCRIPT = path.resolve('scripts/agent/summarize-lock-reliability.mjs');
|
||||||
|
|
||||||
|
function runNode(args, cwd) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const child = spawn(process.execPath, [SCRIPT, ...args], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
||||||
|
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
||||||
|
child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('summarizes recent lock reliability by platform/cadence/backend/relay', async () => {
|
||||||
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'lock-reliability-'));
|
||||||
|
await fs.mkdir(path.join(root, 'task-logs', 'daily'), { recursive: true });
|
||||||
|
await fs.mkdir(path.join(root, 'task-logs', 'weekly'), { recursive: true });
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, 'task-logs', 'daily', '2026-02-14T00-00-00Z__agent-a__failed.md'), `---
|
||||||
|
cadence: daily
|
||||||
|
agent: agent-a
|
||||||
|
status: failed
|
||||||
|
platform: codex
|
||||||
|
backend_category: publish failed to all relays
|
||||||
|
relay_list: 'wss://relay-1.test, wss://relay-2.test'
|
||||||
|
---
|
||||||
|
# Scheduler failed
|
||||||
|
- reason: Lock backend error
|
||||||
|
`, 'utf8');
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, 'task-logs', 'weekly', '2026-02-14T00-00-01Z__agent-b__failed.md'), `---
|
||||||
|
cadence: weekly
|
||||||
|
agent: agent-b
|
||||||
|
status: failed
|
||||||
|
platform: codex
|
||||||
|
preflight_failure_category: relay query timeout
|
||||||
|
relay_list: 'wss://relay-1.test'
|
||||||
|
---
|
||||||
|
# Scheduler failed
|
||||||
|
- reason: Lock backend unavailable preflight
|
||||||
|
`, 'utf8');
|
||||||
|
|
||||||
|
await fs.writeFile(path.join(root, 'task-logs', 'daily', '2026-02-14T00-00-02Z__agent-c__completed.md'), `---
|
||||||
|
cadence: daily
|
||||||
|
agent: agent-c
|
||||||
|
status: completed
|
||||||
|
platform: claude
|
||||||
|
---
|
||||||
|
# Scheduler completed
|
||||||
|
`, 'utf8');
|
||||||
|
|
||||||
|
const result = await runNode(['--since-days', '365', '--out-dir', 'artifacts/reliability'], root);
|
||||||
|
assert.equal(result.code, 0, `stdout=${result.stdout}\nstderr=${result.stderr}`);
|
||||||
|
|
||||||
|
const jsonReport = JSON.parse(await fs.readFile(path.join(root, 'artifacts', 'reliability', 'lock-reliability-summary.json'), 'utf8'));
|
||||||
|
assert.equal(jsonReport.byPlatform.codex, 2);
|
||||||
|
assert.equal(jsonReport.byPlatform.claude, 1);
|
||||||
|
assert.equal(jsonReport.byCadence.daily, 2);
|
||||||
|
assert.equal(jsonReport.byCadence.weekly, 1);
|
||||||
|
assert.equal(jsonReport.byBackendCategory['publish failed to all relays'], 1);
|
||||||
|
assert.equal(jsonReport.byBackendCategory['preflight:relay query timeout'], 1);
|
||||||
|
assert.equal(jsonReport.byRelayEndpoint['wss://relay-1.test'], 2);
|
||||||
|
|
||||||
|
const markdown = await fs.readFile(path.join(root, 'artifacts', 'reliability', 'lock-reliability-summary.md'), 'utf8');
|
||||||
|
assert.match(markdown, /## By platform/);
|
||||||
|
assert.match(markdown, /- codex: 2/);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user