// module-leases.js — Lease system for agent task delegation // Commands: lease-create, lease-list, lease-revoke, lease-renew class LeaseModule { constructor() { this.commands = {}; this.terminal = null; this._leases = {}; this._watchdogTimer = null; } registerCommands() { this.commands['lease-create'] = (args) => this.createLease(args); this.commands['lease-list'] = () => this.listLeases(); this.commands['lease-revoke'] = (args) => this.revokeLease(args[0]); this.commands['lease-renew'] = (args) => this.renewLease(args[0], parseInt(args[1]) || 30); } init(terminal) { this.terminal = terminal; if (!this.terminal.fs.readFile('/leases/')) { this.terminal.fs.mkdir('leases'); } // Restore persisted leases const files = this.terminal.fs.ls('/leases/'); if (files) { files.forEach(f => { if (f.name.endsWith('.json')) { const data = this.terminal.fs.readFile(`/leases/${f.name}`); if (data) { try { const lease = JSON.parse(data); this._leases[lease.id] = lease; } catch (e) {} } } }); } // Start watchdog every 5 seconds this._watchdogTimer = setInterval(() => this._checkExpired(), 5000); this.terminal.print('📜 Lease system loaded', 'info'); } createLease(args) { const workerId = args[1]; const duration = parseInt(args[2]) || 30; const task = args.slice(3).join(' ') || 'generic task'; if (!workerId) return this.terminal.print('Usage: lease-create [duration_sec] [task]', 'error'); const lease = { id: `lease-${Date.now()}`, workerId, orchestratorId: 'orchestrator-0', task, createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + duration * 1000).toISOString(), status: 'active', duration }; this._leases[lease.id] = lease; this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease)); this.terminal.print(`📜 Lease ${lease.id} created for ${workerId} (${duration}s, task: ${task.substring(0, 40)})`, 'success'); return lease; } listLeases() { const all = Object.values(this._leases); const active = all.filter(l => l.status === 'active'); const expired = all.filter(l => l.status === 'expired'); const revoked = all.filter(l => l.status === 'revoked'); this.terminal.print(`📜 Leases: ${active.length} active, ${expired.length} expired, ${revoked.length} revoked`, 'header'); active.forEach(l => { const remaining = Math.max(0, Math.round((new Date(l.expiresAt) - Date.now()) / 1000)); this.terminal.print(` ${l.id} → ${l.workerId} (${remaining}s remain) — ${l.task.substring(0, 40)}`, 'info'); }); } revokeLease(id) { if (!this._leases[id]) return this.terminal.print(`❌ Lease ${id} not found`, 'error'); this._leases[id].status = 'revoked'; this._leases[id].revokedAt = new Date().toISOString(); this.terminal.fs.writeFile(`leases/${id}.json`, JSON.stringify(this._leases[id])); this.terminal.print(`✅ Lease ${id} revoked`, 'warning'); } renewLease(id, additionalSeconds) { if (!this._leases[id] || this._leases[id].status !== 'active') { return this.terminal.print(`❌ Lease ${id} not found or not active`, 'error'); } const lease = this._leases[id]; const newExpiry = new Date(Date.now() + additionalSeconds * 1000); lease.expiresAt = newExpiry.toISOString(); lease.duration += additionalSeconds; lease.renewedAt = new Date().toISOString(); this.terminal.fs.writeFile(`leases/${id}.json`, JSON.stringify(lease)); this.terminal.print(`✅ Lease ${id} renewed (+${additionalSeconds}s)`, 'success'); } _checkExpired() { const now = Date.now(); Object.values(this._leases).forEach(lease => { if (lease.status === 'active' && new Date(lease.expiresAt) < now) { lease.status = 'expired'; lease.expiredAt = new Date().toISOString(); this.terminal.fs.writeFile(`leases/${lease.id}.json`, JSON.stringify(lease)); this.terminal.log('INFO', 'Lease', `Lease ${lease.id} for ${lease.workerId} expired`); } }); } }