mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
feat: implement scheduler ratcheting by excluding completed tasks
- Update `torch-lock check` to scan `task-logs/<cadence>` for completed tasks. - Add `--log-dir` and `--ignore-logs` flags to `check` command. - Implement `getIsoWeekStr` utility for robust weekly cadence checking. - Ensure completed agents are added to the `excluded` list, enabling the scheduler to skip them. - Add comprehensive tests for log checking and date utilities. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
63998fa8a7
commit
433b81d2d3
5 files changed
+252
-7
No files matched your search
+71
-7
@@ -23,13 +23,51 @@ import { getRoster as _getRoster } from './roster.mjs';
|
|||||||
import { queryLocks as _queryLocks, publishLock as _publishLock, parseLockEvent } from './lock-ops.mjs';
|
import { queryLocks as _queryLocks, publishLock as _publishLock, parseLockEvent } from './lock-ops.mjs';
|
||||||
import { cmdDashboard } from './dashboard.mjs';
|
import { cmdDashboard } from './dashboard.mjs';
|
||||||
import { ExitError } from './errors.mjs';
|
import { ExitError } from './errors.mjs';
|
||||||
import { todayDateStr, nowUnix } from './utils.mjs';
|
import { todayDateStr, nowUnix, getIsoWeekStr } from './utils.mjs';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
useWebSocketImplementation(WebSocket);
|
useWebSocketImplementation(WebSocket);
|
||||||
|
|
||||||
// Re-export for backward compatibility/library usage
|
// Re-export for backward compatibility/library usage
|
||||||
export { parseLockEvent, cmdDashboard, _queryLocks as queryLocks, _publishLock as publishLock };
|
export { parseLockEvent, cmdDashboard, _queryLocks as queryLocks, _publishLock as publishLock };
|
||||||
|
|
||||||
|
async function getCompletedAgents(cadence, logDir, deps) {
|
||||||
|
const { readdir = fs.readdir, getDateStr = todayDateStr, getIsoWeek = getIsoWeekStr } = deps;
|
||||||
|
const completed = new Set();
|
||||||
|
const targetDir = path.join(logDir, cadence);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = await readdir(targetDir);
|
||||||
|
const today = getDateStr();
|
||||||
|
const currentWeek = getIsoWeek();
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
// Format: YYYY-MM-DDTHH-mm-ssZ__<agent>__<status>.md
|
||||||
|
const match = file.match(/^(\d{4}-\d{2}-\d{2})T.*__([a-zA-Z0-9-_]+)__(.*)\.md$/);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const [_, datePart, agent, status] = match;
|
||||||
|
|
||||||
|
if (status === 'completed') {
|
||||||
|
if (cadence === 'daily' && datePart === today) {
|
||||||
|
completed.add(agent);
|
||||||
|
} else if (cadence === 'weekly') {
|
||||||
|
const fileWeek = getIsoWeek(datePart); // datePart is YYYY-MM-DD
|
||||||
|
if (fileWeek === currentWeek) {
|
||||||
|
completed.add(agent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code !== 'ENOENT') {
|
||||||
|
console.error(`Warning: Failed to read log dir ${targetDir}: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return completed;
|
||||||
|
}
|
||||||
|
|
||||||
export async function cmdCheck(cadence, deps = {}) {
|
export async function cmdCheck(cadence, deps = {}) {
|
||||||
const {
|
const {
|
||||||
@@ -40,7 +78,9 @@ export async function cmdCheck(cadence, deps = {}) {
|
|||||||
getRoster = _getRoster,
|
getRoster = _getRoster,
|
||||||
getDateStr = todayDateStr,
|
getDateStr = todayDateStr,
|
||||||
log = console.log,
|
log = console.log,
|
||||||
error = console.error
|
error = console.error,
|
||||||
|
logDir = 'task-logs',
|
||||||
|
ignoreLogs = false,
|
||||||
} = deps;
|
} = deps;
|
||||||
|
|
||||||
const relays = getRelays();
|
const relays = getRelays();
|
||||||
@@ -55,12 +95,20 @@ export async function cmdCheck(cadence, deps = {}) {
|
|||||||
error(`Paused agents: ${pausedAgents.join(', ')}`);
|
error(`Paused agents: ${pausedAgents.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let completedAgents = new Set();
|
||||||
|
if (!ignoreLogs) {
|
||||||
|
completedAgents = await getCompletedAgents(cadence, logDir, deps);
|
||||||
|
if (completedAgents.size > 0) {
|
||||||
|
error(`Completed agents (logs): ${[...completedAgents].join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const locks = await queryLocks(relays, cadence, dateStr, namespace);
|
const locks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||||
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
|
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
|
||||||
const roster = getRoster(cadence);
|
const roster = getRoster(cadence);
|
||||||
const rosterSet = new Set(roster);
|
const rosterSet = new Set(roster);
|
||||||
|
|
||||||
const excludedAgentsSet = new Set([...lockedAgents, ...pausedAgents]);
|
const excludedAgentsSet = new Set([...lockedAgents, ...pausedAgents, ...completedAgents]);
|
||||||
const excludedAgents = [...excludedAgentsSet];
|
const excludedAgents = [...excludedAgentsSet];
|
||||||
const unknownLockedAgents = lockedAgents.filter((agent) => !rosterSet.has(agent));
|
const unknownLockedAgents = lockedAgents.filter((agent) => !rosterSet.has(agent));
|
||||||
const available = roster.filter((a) => !excludedAgentsSet.has(a));
|
const available = roster.filter((a) => !excludedAgentsSet.has(a));
|
||||||
@@ -71,6 +119,7 @@ export async function cmdCheck(cadence, deps = {}) {
|
|||||||
date: dateStr,
|
date: dateStr,
|
||||||
locked: lockedAgents.sort(),
|
locked: lockedAgents.sort(),
|
||||||
paused: pausedAgents.sort(),
|
paused: pausedAgents.sort(),
|
||||||
|
completed: [...completedAgents].sort(),
|
||||||
excluded: excludedAgents.sort(),
|
excluded: excludedAgents.sort(),
|
||||||
available: available.sort(),
|
available: available.sort(),
|
||||||
lockCount: locks.length,
|
lockCount: locks.length,
|
||||||
@@ -280,7 +329,16 @@ export async function cmdList(cadence, deps = {}) {
|
|||||||
|
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
const args = { command: null, agent: null, cadence: null, dryRun: false, force: false, port: DEFAULT_DASHBOARD_PORT };
|
const args = {
|
||||||
|
command: null,
|
||||||
|
agent: null,
|
||||||
|
cadence: null,
|
||||||
|
dryRun: false,
|
||||||
|
force: false,
|
||||||
|
port: DEFAULT_DASHBOARD_PORT,
|
||||||
|
logDir: 'task-logs',
|
||||||
|
ignoreLogs: false,
|
||||||
|
};
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
if (argv.length > 0 && !argv[0].startsWith('-')) {
|
if (argv.length > 0 && !argv[0].startsWith('-')) {
|
||||||
@@ -304,6 +362,10 @@ function parseArgs(argv) {
|
|||||||
args.force = true;
|
args.force = true;
|
||||||
} else if (arg === '--port') {
|
} else if (arg === '--port') {
|
||||||
args.port = parseInt(argv[++i], 10) || DEFAULT_DASHBOARD_PORT;
|
args.port = parseInt(argv[++i], 10) || DEFAULT_DASHBOARD_PORT;
|
||||||
|
} else if (arg === '--log-dir') {
|
||||||
|
args.logDir = argv[++i];
|
||||||
|
} else if (arg === '--ignore-logs') {
|
||||||
|
args.ignoreLogs = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,8 +384,10 @@ Commands:
|
|||||||
update [--force] Update torch/ configuration (backups, merges)
|
update [--force] Update torch/ configuration (backups, merges)
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--dry-run Build and sign the event but do not publish
|
--dry-run Build and sign the event but do not publish
|
||||||
--force Overwrite existing files (for init) or all files (for update)
|
--force Overwrite existing files (for init) or all files (for update)
|
||||||
|
--log-dir Path to task logs directory (default: task-logs)
|
||||||
|
--ignore-logs Skip checking local logs for completed tasks
|
||||||
|
|
||||||
Environment:
|
Environment:
|
||||||
NOSTR_LOCK_NAMESPACE Namespace prefix for lock tags (default: torch)
|
NOSTR_LOCK_NAMESPACE Namespace prefix for lock tags (default: torch)
|
||||||
@@ -357,7 +421,7 @@ export async function main(argv) {
|
|||||||
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for check`);
|
console.error(`ERROR: --cadence <${[...VALID_CADENCES].join('|')}> is required for check`);
|
||||||
throw new ExitError(1, 'Missing cadence');
|
throw new ExitError(1, 'Missing cadence');
|
||||||
}
|
}
|
||||||
await cmdCheck(args.cadence);
|
await cmdCheck(args.cadence, { logDir: args.logDir, ignoreLogs: args.ignoreLogs });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,3 +5,25 @@ export function todayDateStr() {
|
|||||||
export function nowUnix() {
|
export function nowUnix() {
|
||||||
return Math.floor(Date.now() / 1000);
|
return Math.floor(Date.now() / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getIsoWeekStr(dateInput) {
|
||||||
|
// Parse input to ensure we work with a Date object
|
||||||
|
// If dateInput is YYYY-MM-DD string, new Date() treats it as UTC.
|
||||||
|
const d = dateInput ? new Date(dateInput) : new Date();
|
||||||
|
|
||||||
|
// If valid date, use UTC components to avoid local timezone issues
|
||||||
|
if (isNaN(d.getTime())) return '';
|
||||||
|
|
||||||
|
// Copy date so we don't mutate the original if passed by reference (though new Date() handles that)
|
||||||
|
const date = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
||||||
|
|
||||||
|
// ISO week date system: Thursday determines the year
|
||||||
|
// day: 0 (Sun) -> 6 (Sat). We want 1 (Mon) -> 7 (Sun)
|
||||||
|
const day = date.getUTCDay() || 7;
|
||||||
|
date.setUTCDate(date.getUTCDate() + 4 - day);
|
||||||
|
|
||||||
|
const yearStart = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
||||||
|
const weekNo = Math.ceil((((date - yearStart) / 86400000) + 1) / 7);
|
||||||
|
|
||||||
|
return `${date.getUTCFullYear()}-W${String(weekNo).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ describe('src/lib.mjs', () => {
|
|||||||
getDateStr: () => mockDateStr,
|
getDateStr: () => mockDateStr,
|
||||||
log: mockLog,
|
log: mockLog,
|
||||||
error: mockError,
|
error: mockError,
|
||||||
|
readdir: async () => [],
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { cmdCheck } from '../src/lib.mjs';
|
||||||
|
|
||||||
|
describe('Scheduler Ratchet Logic (Log Checking)', () => {
|
||||||
|
const mockRelays = ['wss://relay.test'];
|
||||||
|
const mockNamespace = 'test-ns';
|
||||||
|
const mockTtl = 3600;
|
||||||
|
const mockRoster = ['agentA', 'agentB', 'agentC'];
|
||||||
|
const mockDateStr = '2023-10-27'; // A Friday
|
||||||
|
const mockIsoWeekStr = '2023-W43';
|
||||||
|
|
||||||
|
// Base dependencies for cmdCheck
|
||||||
|
const baseDeps = {
|
||||||
|
getRelays: () => mockRelays,
|
||||||
|
getNamespace: () => mockNamespace,
|
||||||
|
getTtl: () => mockTtl,
|
||||||
|
getRoster: () => mockRoster,
|
||||||
|
loadTorchConfig: () => ({ scheduler: { paused: { daily: [], weekly: [] } } }),
|
||||||
|
getDateStr: () => mockDateStr,
|
||||||
|
getIsoWeek: () => mockIsoWeekStr,
|
||||||
|
queryLocks: async () => [], // No active locks by default
|
||||||
|
log: () => {},
|
||||||
|
error: () => {},
|
||||||
|
// Default readdir mock: returns empty array
|
||||||
|
readdir: async () => [],
|
||||||
|
logDir: 'mock-logs',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('excludes agents completed today (daily)', async () => {
|
||||||
|
const deps = {
|
||||||
|
...baseDeps,
|
||||||
|
readdir: async () => [
|
||||||
|
'2023-10-27T10-00-00Z__agentA__completed.md', // Matching date
|
||||||
|
'2023-10-26T10-00-00Z__agentB__completed.md', // Old date
|
||||||
|
'2023-10-27T12-00-00Z__agentC__failed.md', // Failed today
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await cmdCheck('daily', deps);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result.completed, ['agentA']);
|
||||||
|
assert.ok(result.excluded.includes('agentA'));
|
||||||
|
assert.ok(!result.excluded.includes('agentB')); // Completed yesterday, not excluded today
|
||||||
|
assert.ok(!result.excluded.includes('agentC')); // Failed, not excluded
|
||||||
|
assert.deepStrictEqual(result.available, ['agentB', 'agentC']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('excludes agents completed this week (weekly)', async () => {
|
||||||
|
const deps = {
|
||||||
|
...baseDeps,
|
||||||
|
getIsoWeek: (date) => {
|
||||||
|
if (!date) return mockIsoWeekStr;
|
||||||
|
// Mock simple week logic: 2023-10-23 to 2023-10-29 is W43
|
||||||
|
if (date >= '2023-10-23' && date <= '2023-10-29') return '2023-W43';
|
||||||
|
return '2023-W42';
|
||||||
|
},
|
||||||
|
readdir: async () => [
|
||||||
|
'2023-10-23T10-00-00Z__agentA__completed.md', // Monday (in week)
|
||||||
|
'2023-10-20T10-00-00Z__agentB__completed.md', // Last week
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await cmdCheck('weekly', deps);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result.completed, ['agentA']);
|
||||||
|
assert.ok(result.excluded.includes('agentA'));
|
||||||
|
assert.ok(!result.excluded.includes('agentB'));
|
||||||
|
assert.deepStrictEqual(result.available, ['agentB', 'agentC']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('combines locked and completed agents in exclusion list', async () => {
|
||||||
|
const deps = {
|
||||||
|
...baseDeps,
|
||||||
|
queryLocks: async () => [{ agent: 'agentB', eventId: 'ev1' }], // agentB is locked
|
||||||
|
readdir: async () => [
|
||||||
|
'2023-10-27T10-00-00Z__agentA__completed.md', // agentA is completed
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await cmdCheck('daily', deps);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result.completed, ['agentA']);
|
||||||
|
assert.deepStrictEqual(result.locked, ['agentB']);
|
||||||
|
|
||||||
|
// agentA (completed) and agentB (locked) should be excluded
|
||||||
|
assert.ok(result.excluded.includes('agentA'));
|
||||||
|
assert.ok(result.excluded.includes('agentB'));
|
||||||
|
assert.deepStrictEqual(result.available, ['agentC']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores logs when --ignore-logs is set', async () => {
|
||||||
|
const deps = {
|
||||||
|
...baseDeps,
|
||||||
|
readdir: async () => [
|
||||||
|
'2023-10-27T10-00-00Z__agentA__completed.md',
|
||||||
|
],
|
||||||
|
ignoreLogs: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await cmdCheck('daily', deps);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result.completed, []);
|
||||||
|
assert.ok(!result.excluded.includes('agentA'));
|
||||||
|
assert.deepStrictEqual(result.available, ['agentA', 'agentB', 'agentC']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles missing log directory gracefully', async () => {
|
||||||
|
const deps = {
|
||||||
|
...baseDeps,
|
||||||
|
readdir: async () => {
|
||||||
|
const err = new Error('ENOENT');
|
||||||
|
err.code = 'ENOENT';
|
||||||
|
throw err;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await cmdCheck('daily', deps);
|
||||||
|
assert.deepStrictEqual(result.completed, []);
|
||||||
|
assert.deepStrictEqual(result.available, ['agentA', 'agentB', 'agentC']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { getIsoWeekStr } from '../src/utils.mjs';
|
||||||
|
|
||||||
|
describe('Date Utilities', () => {
|
||||||
|
describe('getIsoWeekStr', () => {
|
||||||
|
it('returns correct ISO week for simple cases', () => {
|
||||||
|
assert.strictEqual(getIsoWeekStr('2023-01-02'), '2023-W01'); // Mon
|
||||||
|
assert.strictEqual(getIsoWeekStr('2023-01-08'), '2023-W01'); // Sun
|
||||||
|
assert.strictEqual(getIsoWeekStr('2023-01-09'), '2023-W02'); // Mon
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles year boundaries correctly', () => {
|
||||||
|
// 2023-01-01 is Sunday. Belong to last week of 2022.
|
||||||
|
assert.strictEqual(getIsoWeekStr('2023-01-01'), '2022-W52');
|
||||||
|
// 2024-01-01 is Monday. Week 1 of 2024.
|
||||||
|
assert.strictEqual(getIsoWeekStr('2024-01-01'), '2024-W01');
|
||||||
|
// 2023-12-31 is Sunday. Week 52 of 2023.
|
||||||
|
assert.strictEqual(getIsoWeekStr('2023-12-31'), '2023-W52');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles leap years', () => {
|
||||||
|
assert.strictEqual(getIsoWeekStr('2024-02-29'), '2024-W09');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty input (defaults to today)', () => {
|
||||||
|
// Just check it returns a string in correct format
|
||||||
|
const result = getIsoWeekStr();
|
||||||
|
assert.match(result, /^\d{4}-W\d{2}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles invalid input gracefully', () => {
|
||||||
|
assert.strictEqual(getIsoWeekStr('invalid-date'), '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user