aboutsummaryrefslogtreecommitdiff
path: root/modules/core/module-leases.js
blob: b79870c7ce9fe0fa2277af3872a079af0f7b12ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// 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 <agent-id> [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`);
            }
        });
    }
}