universal-agent-memory 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/cli.js +207 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/agent.d.ts +20 -0
- package/dist/cli/agent.d.ts.map +1 -0
- package/dist/cli/agent.js +388 -0
- package/dist/cli/agent.js.map +1 -0
- package/dist/cli/coord.d.ts +7 -0
- package/dist/cli/coord.d.ts.map +1 -0
- package/dist/cli/coord.js +145 -0
- package/dist/cli/coord.js.map +1 -0
- package/dist/cli/deploy.d.ts +19 -0
- package/dist/cli/deploy.d.ts.map +1 -0
- package/dist/cli/deploy.js +267 -0
- package/dist/cli/deploy.js.map +1 -0
- package/dist/cli/task.d.ts +33 -0
- package/dist/cli/task.d.ts.map +1 -0
- package/dist/cli/task.js +570 -0
- package/dist/cli/task.js.map +1 -0
- package/dist/coordination/database.d.ts +13 -0
- package/dist/coordination/database.d.ts.map +1 -0
- package/dist/coordination/database.js +131 -0
- package/dist/coordination/database.js.map +1 -0
- package/dist/coordination/deploy-batcher.d.ts +38 -0
- package/dist/coordination/deploy-batcher.d.ts.map +1 -0
- package/dist/coordination/deploy-batcher.js +401 -0
- package/dist/coordination/deploy-batcher.js.map +1 -0
- package/dist/coordination/index.d.ts +4 -0
- package/dist/coordination/index.d.ts.map +1 -0
- package/dist/coordination/index.js +4 -0
- package/dist/coordination/index.js.map +1 -0
- package/dist/coordination/service.d.ts +79 -0
- package/dist/coordination/service.d.ts.map +1 -0
- package/dist/coordination/service.js +591 -0
- package/dist/coordination/service.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/tasks/coordination.d.ts +74 -0
- package/dist/tasks/coordination.d.ts.map +1 -0
- package/dist/tasks/coordination.js +237 -0
- package/dist/tasks/coordination.js.map +1 -0
- package/dist/tasks/database.d.ts +14 -0
- package/dist/tasks/database.d.ts.map +1 -0
- package/dist/tasks/database.js +128 -0
- package/dist/tasks/database.js.map +1 -0
- package/dist/tasks/index.d.ts +5 -0
- package/dist/tasks/index.d.ts.map +1 -0
- package/dist/tasks/index.js +5 -0
- package/dist/tasks/index.js.map +1 -0
- package/dist/tasks/service.d.ts +39 -0
- package/dist/tasks/service.d.ts.map +1 -0
- package/dist/tasks/service.js +582 -0
- package/dist/tasks/service.js.map +1 -0
- package/dist/tasks/types.d.ts +224 -0
- package/dist/tasks/types.d.ts.map +1 -0
- package/dist/tasks/types.js +64 -0
- package/dist/tasks/types.js.map +1 -0
- package/dist/types/coordination.d.ts +240 -0
- package/dist/types/coordination.d.ts.map +1 -0
- package/dist/types/coordination.js +43 -0
- package/dist/types/coordination.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { AgentRegistryEntry, AgentMessage, WorkClaim, WorkAnnouncement, WorkOverlap, CollaborationSuggestion, DeployAction, CoordinationStatus, AgentStatus, MessageChannel, ClaimType, WorkIntentType, DeployActionType, DeployStatus, MessagePayload } from '../types/coordination.js';
|
|
2
|
+
export interface CoordinationServiceConfig {
|
|
3
|
+
dbPath?: string;
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
heartbeatIntervalMs?: number;
|
|
6
|
+
claimExpiryMs?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class CoordinationService {
|
|
9
|
+
private db;
|
|
10
|
+
private sessionId;
|
|
11
|
+
private heartbeatIntervalMs;
|
|
12
|
+
private claimExpiryMs;
|
|
13
|
+
constructor(config?: CoordinationServiceConfig);
|
|
14
|
+
register(name: string, capabilities?: string[], worktreeBranch?: string): string;
|
|
15
|
+
heartbeat(agentId: string): void;
|
|
16
|
+
updateStatus(agentId: string, status: AgentStatus, currentTask?: string): void;
|
|
17
|
+
deregister(agentId: string): void;
|
|
18
|
+
getAgent(agentId: string): AgentRegistryEntry | null;
|
|
19
|
+
getActiveAgents(): AgentRegistryEntry[];
|
|
20
|
+
cleanupStaleAgents(): number;
|
|
21
|
+
claimResource(agentId: string, resource: string, claimType?: ClaimType): boolean;
|
|
22
|
+
releaseResource(agentId: string, resource: string): void;
|
|
23
|
+
releaseAllClaims(agentId: string): void;
|
|
24
|
+
isResourceClaimed(resource: string): string | null;
|
|
25
|
+
getResourceClaims(resource: string): WorkClaim[];
|
|
26
|
+
getAgentClaims(agentId: string): WorkClaim[];
|
|
27
|
+
/**
|
|
28
|
+
* Announce intent to work on a resource. Does NOT lock - just informs other agents.
|
|
29
|
+
* Returns overlap info if other agents are also working on related resources.
|
|
30
|
+
*/
|
|
31
|
+
announceWork(agentId: string, resource: string, intentType: WorkIntentType, options?: {
|
|
32
|
+
description?: string;
|
|
33
|
+
filesAffected?: string[];
|
|
34
|
+
estimatedMinutes?: number;
|
|
35
|
+
}): {
|
|
36
|
+
announcement: WorkAnnouncement;
|
|
37
|
+
overlaps: WorkOverlap[];
|
|
38
|
+
suggestions: CollaborationSuggestion[];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Mark work as complete on a resource.
|
|
42
|
+
*/
|
|
43
|
+
completeWork(agentId: string, resource: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Get all active work announcements (not completed).
|
|
46
|
+
*/
|
|
47
|
+
getActiveWork(): WorkAnnouncement[];
|
|
48
|
+
/**
|
|
49
|
+
* Get work announcements for a specific resource.
|
|
50
|
+
*/
|
|
51
|
+
getWorkOnResource(resource: string): WorkAnnouncement[];
|
|
52
|
+
/**
|
|
53
|
+
* Detect overlapping work that might cause merge conflicts.
|
|
54
|
+
*/
|
|
55
|
+
detectOverlaps(resource: string, excludeAgentId?: string): WorkOverlap[];
|
|
56
|
+
private assessConflictRisk;
|
|
57
|
+
private generateOverlapSuggestion;
|
|
58
|
+
/**
|
|
59
|
+
* Generate collaboration suggestions based on overlaps.
|
|
60
|
+
*/
|
|
61
|
+
generateCollaborationSuggestions(agentId: string, _resource: string, overlaps: WorkOverlap[]): CollaborationSuggestion[];
|
|
62
|
+
private suggestMergeOrder;
|
|
63
|
+
broadcast(fromAgent: string, channel: MessageChannel, payload: MessagePayload, priority?: number): void;
|
|
64
|
+
send(fromAgent: string, toAgent: string, payload: MessagePayload, priority?: number): void;
|
|
65
|
+
private sendMessage;
|
|
66
|
+
receive(agentId: string, channel?: MessageChannel, markAsRead?: boolean): AgentMessage[];
|
|
67
|
+
getPendingMessages(agentId: string): number;
|
|
68
|
+
queueDeploy(agentId: string, actionType: DeployActionType, target: string, payload?: Record<string, unknown>, options?: {
|
|
69
|
+
priority?: number;
|
|
70
|
+
executeAfter?: Date;
|
|
71
|
+
dependencies?: string[];
|
|
72
|
+
}): number;
|
|
73
|
+
getPendingDeploys(): DeployAction[];
|
|
74
|
+
getReadyDeploys(): DeployAction[];
|
|
75
|
+
updateDeployStatus(deployId: number, status: DeployStatus, batchId?: string): void;
|
|
76
|
+
getStatus(): CoordinationStatus;
|
|
77
|
+
cleanup(): void;
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/coordination/service.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,SAAS,EACT,gBAAgB,EAChB,WAAW,EACX,uBAAuB,EACvB,YAAY,EACZ,kBAAkB,EAClB,WAAW,EAEX,cAAc,EACd,SAAS,EACT,cAAc,EAEd,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACf,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,aAAa,CAAS;gBAElB,MAAM,GAAE,yBAA8B;IAUlD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,MAAM;IAahF,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAShC,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAS9E,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAajC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI;IAiBpD,eAAe,IAAI,kBAAkB,EAAE;IAkBvC,kBAAkB,IAAI,MAAM;IA2B5B,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,GAAE,SAAuB,GAAG,OAAO;IAgC7F,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQxD,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQvC,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAWlD,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE;IAWhD,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE;IAc5C;;;OAGG;IACH,YAAY,CACV,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,cAAc,EAC1B,OAAO,GAAE;QACP,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;QACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;KACtB,GACL;QAAE,YAAY,EAAE,gBAAgB,CAAC;QAAC,QAAQ,EAAE,WAAW,EAAE,CAAC;QAAC,WAAW,EAAE,uBAAuB,EAAE,CAAA;KAAE;IAsDtG;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAerD;;OAEG;IACH,aAAa,IAAI,gBAAgB,EAAE;IAoBnC;;OAEG;IACH,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,EAAE;IAoBvD;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,cAAc,CAAC,EAAE,MAAM,GAAG,WAAW,EAAE;IA4ExE,OAAO,CAAC,kBAAkB;IAqB1B,OAAO,CAAC,yBAAyB;IAuBjC;;OAEG;IACH,gCAAgC,CAC9B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,WAAW,EAAE,GACtB,uBAAuB,EAAE;IA0C5B,OAAO,CAAC,iBAAiB;IAezB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,SAAI,GAAG,IAAI;IAIlG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,SAAI,GAAG,IAAI;IAIrF,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,UAAU,UAAO,GAAG,YAAY,EAAE;IAuCrF,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAc3C,WAAW,CACT,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,IAAI,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;KAAO,GAChF,MAAM;IAwBT,iBAAiB,IAAI,YAAY,EAAE;IAkBnC,eAAe,IAAI,YAAY,EAAE;IAmBjC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAWlF,SAAS,IAAI,kBAAkB;IA+B/B,OAAO,IAAI,IAAI;CAqBhB"}
|
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { CoordinationDatabase, getDefaultCoordinationDbPath } from './database.js';
|
|
3
|
+
export class CoordinationService {
|
|
4
|
+
db;
|
|
5
|
+
sessionId;
|
|
6
|
+
heartbeatIntervalMs;
|
|
7
|
+
claimExpiryMs;
|
|
8
|
+
constructor(config = {}) {
|
|
9
|
+
const dbPath = config.dbPath || getDefaultCoordinationDbPath();
|
|
10
|
+
this.db = CoordinationDatabase.getInstance(dbPath).getDatabase();
|
|
11
|
+
this.sessionId = config.sessionId || randomUUID();
|
|
12
|
+
this.heartbeatIntervalMs = config.heartbeatIntervalMs || 30000;
|
|
13
|
+
this.claimExpiryMs = config.claimExpiryMs || 300000; // 5 minutes
|
|
14
|
+
}
|
|
15
|
+
// ==================== Agent Lifecycle ====================
|
|
16
|
+
register(name, capabilities, worktreeBranch) {
|
|
17
|
+
const id = randomUUID();
|
|
18
|
+
const now = new Date().toISOString();
|
|
19
|
+
const stmt = this.db.prepare(`
|
|
20
|
+
INSERT INTO agent_registry (id, name, session_id, status, worktree_branch, started_at, last_heartbeat, capabilities)
|
|
21
|
+
VALUES (?, ?, ?, 'active', ?, ?, ?, ?)
|
|
22
|
+
`);
|
|
23
|
+
stmt.run(id, name, this.sessionId, worktreeBranch || null, now, now, capabilities ? JSON.stringify(capabilities) : null);
|
|
24
|
+
return id;
|
|
25
|
+
}
|
|
26
|
+
heartbeat(agentId) {
|
|
27
|
+
const stmt = this.db.prepare(`
|
|
28
|
+
UPDATE agent_registry
|
|
29
|
+
SET last_heartbeat = ?
|
|
30
|
+
WHERE id = ?
|
|
31
|
+
`);
|
|
32
|
+
stmt.run(new Date().toISOString(), agentId);
|
|
33
|
+
}
|
|
34
|
+
updateStatus(agentId, status, currentTask) {
|
|
35
|
+
const stmt = this.db.prepare(`
|
|
36
|
+
UPDATE agent_registry
|
|
37
|
+
SET status = ?, current_task = ?, last_heartbeat = ?
|
|
38
|
+
WHERE id = ?
|
|
39
|
+
`);
|
|
40
|
+
stmt.run(status, currentTask || null, new Date().toISOString(), agentId);
|
|
41
|
+
}
|
|
42
|
+
deregister(agentId) {
|
|
43
|
+
// Release all claims
|
|
44
|
+
this.releaseAllClaims(agentId);
|
|
45
|
+
// Update status
|
|
46
|
+
const stmt = this.db.prepare(`
|
|
47
|
+
UPDATE agent_registry
|
|
48
|
+
SET status = 'completed'
|
|
49
|
+
WHERE id = ?
|
|
50
|
+
`);
|
|
51
|
+
stmt.run(agentId);
|
|
52
|
+
}
|
|
53
|
+
getAgent(agentId) {
|
|
54
|
+
const stmt = this.db.prepare(`
|
|
55
|
+
SELECT id, name, session_id as sessionId, status, current_task as currentTask,
|
|
56
|
+
worktree_branch as worktreeBranch, started_at as startedAt,
|
|
57
|
+
last_heartbeat as lastHeartbeat, capabilities
|
|
58
|
+
FROM agent_registry
|
|
59
|
+
WHERE id = ?
|
|
60
|
+
`);
|
|
61
|
+
const row = stmt.get(agentId);
|
|
62
|
+
if (!row)
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
...row,
|
|
66
|
+
capabilities: row.capabilities ? JSON.parse(row.capabilities) : undefined,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
getActiveAgents() {
|
|
70
|
+
const stmt = this.db.prepare(`
|
|
71
|
+
SELECT id, name, session_id as sessionId, status, current_task as currentTask,
|
|
72
|
+
worktree_branch as worktreeBranch, started_at as startedAt,
|
|
73
|
+
last_heartbeat as lastHeartbeat, capabilities
|
|
74
|
+
FROM agent_registry
|
|
75
|
+
WHERE status IN ('active', 'idle')
|
|
76
|
+
ORDER BY started_at DESC
|
|
77
|
+
`);
|
|
78
|
+
const rows = stmt.all();
|
|
79
|
+
return rows.map((row) => ({
|
|
80
|
+
...row,
|
|
81
|
+
capabilities: row.capabilities ? JSON.parse(row.capabilities) : undefined,
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
84
|
+
// Cleanup stale agents (no heartbeat for too long)
|
|
85
|
+
cleanupStaleAgents() {
|
|
86
|
+
const cutoff = new Date(Date.now() - this.heartbeatIntervalMs * 3).toISOString();
|
|
87
|
+
// Get stale agents
|
|
88
|
+
const staleStmt = this.db.prepare(`
|
|
89
|
+
SELECT id FROM agent_registry
|
|
90
|
+
WHERE status IN ('active', 'idle') AND last_heartbeat < ?
|
|
91
|
+
`);
|
|
92
|
+
const staleAgents = staleStmt.all(cutoff);
|
|
93
|
+
// Release their claims
|
|
94
|
+
for (const agent of staleAgents) {
|
|
95
|
+
this.releaseAllClaims(agent.id);
|
|
96
|
+
}
|
|
97
|
+
// Mark as failed
|
|
98
|
+
const updateStmt = this.db.prepare(`
|
|
99
|
+
UPDATE agent_registry
|
|
100
|
+
SET status = 'failed'
|
|
101
|
+
WHERE status IN ('active', 'idle') AND last_heartbeat < ?
|
|
102
|
+
`);
|
|
103
|
+
const result = updateStmt.run(cutoff);
|
|
104
|
+
return result.changes;
|
|
105
|
+
}
|
|
106
|
+
// ==================== Work Claims ====================
|
|
107
|
+
claimResource(agentId, resource, claimType = 'exclusive') {
|
|
108
|
+
const now = new Date().toISOString();
|
|
109
|
+
const expiresAt = new Date(Date.now() + this.claimExpiryMs).toISOString();
|
|
110
|
+
// Check for existing exclusive claim
|
|
111
|
+
const checkStmt = this.db.prepare(`
|
|
112
|
+
SELECT agent_id, claim_type FROM work_claims
|
|
113
|
+
WHERE resource = ? AND (expires_at IS NULL OR expires_at > ?)
|
|
114
|
+
`);
|
|
115
|
+
const existing = checkStmt.get(resource, now);
|
|
116
|
+
if (existing) {
|
|
117
|
+
if (existing.claim_type === 'exclusive') {
|
|
118
|
+
return false; // Resource already exclusively claimed
|
|
119
|
+
}
|
|
120
|
+
if (claimType === 'exclusive') {
|
|
121
|
+
return false; // Can't get exclusive claim when shared claims exist
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const stmt = this.db.prepare(`
|
|
126
|
+
INSERT INTO work_claims (resource, agent_id, claim_type, claimed_at, expires_at)
|
|
127
|
+
VALUES (?, ?, ?, ?, ?)
|
|
128
|
+
`);
|
|
129
|
+
stmt.run(resource, agentId, claimType, now, expiresAt);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false; // Constraint violation (duplicate exclusive claim)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
releaseResource(agentId, resource) {
|
|
137
|
+
const stmt = this.db.prepare(`
|
|
138
|
+
DELETE FROM work_claims
|
|
139
|
+
WHERE agent_id = ? AND resource = ?
|
|
140
|
+
`);
|
|
141
|
+
stmt.run(agentId, resource);
|
|
142
|
+
}
|
|
143
|
+
releaseAllClaims(agentId) {
|
|
144
|
+
const stmt = this.db.prepare(`
|
|
145
|
+
DELETE FROM work_claims
|
|
146
|
+
WHERE agent_id = ?
|
|
147
|
+
`);
|
|
148
|
+
stmt.run(agentId);
|
|
149
|
+
}
|
|
150
|
+
isResourceClaimed(resource) {
|
|
151
|
+
const now = new Date().toISOString();
|
|
152
|
+
const stmt = this.db.prepare(`
|
|
153
|
+
SELECT agent_id FROM work_claims
|
|
154
|
+
WHERE resource = ? AND claim_type = 'exclusive'
|
|
155
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
156
|
+
`);
|
|
157
|
+
const row = stmt.get(resource, now);
|
|
158
|
+
return row?.agent_id || null;
|
|
159
|
+
}
|
|
160
|
+
getResourceClaims(resource) {
|
|
161
|
+
const now = new Date().toISOString();
|
|
162
|
+
const stmt = this.db.prepare(`
|
|
163
|
+
SELECT id, resource, agent_id as agentId, claim_type as claimType,
|
|
164
|
+
claimed_at as claimedAt, expires_at as expiresAt
|
|
165
|
+
FROM work_claims
|
|
166
|
+
WHERE resource = ? AND (expires_at IS NULL OR expires_at > ?)
|
|
167
|
+
`);
|
|
168
|
+
return stmt.all(resource, now);
|
|
169
|
+
}
|
|
170
|
+
getAgentClaims(agentId) {
|
|
171
|
+
const stmt = this.db.prepare(`
|
|
172
|
+
SELECT id, resource, agent_id as agentId, claim_type as claimType,
|
|
173
|
+
claimed_at as claimedAt, expires_at as expiresAt
|
|
174
|
+
FROM work_claims
|
|
175
|
+
WHERE agent_id = ?
|
|
176
|
+
`);
|
|
177
|
+
return stmt.all(agentId);
|
|
178
|
+
}
|
|
179
|
+
// ==================== Work Announcements (Collaborative) ====================
|
|
180
|
+
// NOTE: Agents work in isolated git worktrees, so they don't NEED to claim resources.
|
|
181
|
+
// Announcements are informational - they help optimize velocity and minimize merge conflicts.
|
|
182
|
+
/**
|
|
183
|
+
* Announce intent to work on a resource. Does NOT lock - just informs other agents.
|
|
184
|
+
* Returns overlap info if other agents are also working on related resources.
|
|
185
|
+
*/
|
|
186
|
+
announceWork(agentId, resource, intentType, options = {}) {
|
|
187
|
+
const agent = this.getAgent(agentId);
|
|
188
|
+
const now = new Date().toISOString();
|
|
189
|
+
const estimatedCompletion = options.estimatedMinutes
|
|
190
|
+
? new Date(Date.now() + options.estimatedMinutes * 60000).toISOString()
|
|
191
|
+
: null;
|
|
192
|
+
const stmt = this.db.prepare(`
|
|
193
|
+
INSERT INTO work_announcements
|
|
194
|
+
(agent_id, agent_name, worktree_branch, intent_type, resource, description, files_affected, estimated_completion, announced_at)
|
|
195
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
196
|
+
`);
|
|
197
|
+
const result = stmt.run(agentId, agent?.name || null, agent?.worktreeBranch || null, intentType, resource, options.description || null, options.filesAffected ? JSON.stringify(options.filesAffected) : null, estimatedCompletion, now);
|
|
198
|
+
const announcement = {
|
|
199
|
+
id: result.lastInsertRowid,
|
|
200
|
+
agentId,
|
|
201
|
+
agentName: agent?.name,
|
|
202
|
+
worktreeBranch: agent?.worktreeBranch,
|
|
203
|
+
intentType,
|
|
204
|
+
resource,
|
|
205
|
+
description: options.description,
|
|
206
|
+
filesAffected: options.filesAffected,
|
|
207
|
+
estimatedCompletion: estimatedCompletion || undefined,
|
|
208
|
+
announcedAt: now,
|
|
209
|
+
};
|
|
210
|
+
// Detect overlaps and generate suggestions
|
|
211
|
+
const overlaps = this.detectOverlaps(resource, agentId);
|
|
212
|
+
const suggestions = this.generateCollaborationSuggestions(agentId, resource, overlaps);
|
|
213
|
+
// Broadcast to other agents about potential overlap
|
|
214
|
+
if (overlaps.length > 0) {
|
|
215
|
+
this.broadcast(agentId, 'coordination', {
|
|
216
|
+
action: 'work_overlap_detected',
|
|
217
|
+
resource,
|
|
218
|
+
data: { overlaps, suggestions },
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return { announcement, overlaps, suggestions };
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Mark work as complete on a resource.
|
|
225
|
+
*/
|
|
226
|
+
completeWork(agentId, resource) {
|
|
227
|
+
const stmt = this.db.prepare(`
|
|
228
|
+
UPDATE work_announcements
|
|
229
|
+
SET completed_at = ?
|
|
230
|
+
WHERE agent_id = ? AND resource = ? AND completed_at IS NULL
|
|
231
|
+
`);
|
|
232
|
+
stmt.run(new Date().toISOString(), agentId, resource);
|
|
233
|
+
// Broadcast completion so others know merge order
|
|
234
|
+
this.broadcast(agentId, 'coordination', {
|
|
235
|
+
action: 'work_completed',
|
|
236
|
+
resource,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Get all active work announcements (not completed).
|
|
241
|
+
*/
|
|
242
|
+
getActiveWork() {
|
|
243
|
+
const stmt = this.db.prepare(`
|
|
244
|
+
SELECT
|
|
245
|
+
wa.id, wa.agent_id as agentId, wa.agent_name as agentName,
|
|
246
|
+
wa.worktree_branch as worktreeBranch, wa.intent_type as intentType,
|
|
247
|
+
wa.resource, wa.description, wa.files_affected as filesAffected,
|
|
248
|
+
wa.estimated_completion as estimatedCompletion, wa.announced_at as announcedAt
|
|
249
|
+
FROM work_announcements wa
|
|
250
|
+
JOIN agent_registry ar ON wa.agent_id = ar.id
|
|
251
|
+
WHERE wa.completed_at IS NULL AND ar.status IN ('active', 'idle')
|
|
252
|
+
ORDER BY wa.announced_at DESC
|
|
253
|
+
`);
|
|
254
|
+
const rows = stmt.all();
|
|
255
|
+
return rows.map((row) => ({
|
|
256
|
+
...row,
|
|
257
|
+
filesAffected: row.filesAffected ? JSON.parse(row.filesAffected) : undefined,
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get work announcements for a specific resource.
|
|
262
|
+
*/
|
|
263
|
+
getWorkOnResource(resource) {
|
|
264
|
+
const stmt = this.db.prepare(`
|
|
265
|
+
SELECT
|
|
266
|
+
wa.id, wa.agent_id as agentId, wa.agent_name as agentName,
|
|
267
|
+
wa.worktree_branch as worktreeBranch, wa.intent_type as intentType,
|
|
268
|
+
wa.resource, wa.description, wa.files_affected as filesAffected,
|
|
269
|
+
wa.estimated_completion as estimatedCompletion, wa.announced_at as announcedAt
|
|
270
|
+
FROM work_announcements wa
|
|
271
|
+
JOIN agent_registry ar ON wa.agent_id = ar.id
|
|
272
|
+
WHERE wa.resource LIKE ? AND wa.completed_at IS NULL AND ar.status IN ('active', 'idle')
|
|
273
|
+
ORDER BY wa.announced_at DESC
|
|
274
|
+
`);
|
|
275
|
+
const rows = stmt.all(`%${resource}%`);
|
|
276
|
+
return rows.map((row) => ({
|
|
277
|
+
...row,
|
|
278
|
+
filesAffected: row.filesAffected ? JSON.parse(row.filesAffected) : undefined,
|
|
279
|
+
}));
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Detect overlapping work that might cause merge conflicts.
|
|
283
|
+
*/
|
|
284
|
+
detectOverlaps(resource, excludeAgentId) {
|
|
285
|
+
const activeWork = this.getActiveWork();
|
|
286
|
+
const overlaps = [];
|
|
287
|
+
// Group by resource pattern (file, directory, or module)
|
|
288
|
+
const resourceParts = resource.split('/');
|
|
289
|
+
const directory = resourceParts.slice(0, -1).join('/');
|
|
290
|
+
// Find work on same file
|
|
291
|
+
const sameFile = activeWork.filter((w) => w.agentId !== excludeAgentId && w.resource === resource);
|
|
292
|
+
// Find work on same directory
|
|
293
|
+
const sameDirectory = activeWork.filter((w) => w.agentId !== excludeAgentId &&
|
|
294
|
+
w.resource !== resource &&
|
|
295
|
+
w.resource.startsWith(directory + '/'));
|
|
296
|
+
// Find work with overlapping files
|
|
297
|
+
const overlappingFiles = activeWork.filter((w) => {
|
|
298
|
+
if (w.agentId === excludeAgentId)
|
|
299
|
+
return false;
|
|
300
|
+
if (!w.filesAffected)
|
|
301
|
+
return false;
|
|
302
|
+
return w.filesAffected.some((f) => f === resource || resource.includes(f) || f.includes(resource));
|
|
303
|
+
});
|
|
304
|
+
if (sameFile.length > 0) {
|
|
305
|
+
overlaps.push({
|
|
306
|
+
resource,
|
|
307
|
+
agents: sameFile.map((w) => ({
|
|
308
|
+
id: w.agentId,
|
|
309
|
+
name: w.agentName || 'unknown',
|
|
310
|
+
intentType: w.intentType,
|
|
311
|
+
worktreeBranch: w.worktreeBranch,
|
|
312
|
+
description: w.description,
|
|
313
|
+
})),
|
|
314
|
+
conflictRisk: this.assessConflictRisk(sameFile),
|
|
315
|
+
suggestion: this.generateOverlapSuggestion(sameFile, 'same_file'),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (sameDirectory.length > 0) {
|
|
319
|
+
overlaps.push({
|
|
320
|
+
resource: directory,
|
|
321
|
+
agents: sameDirectory.map((w) => ({
|
|
322
|
+
id: w.agentId,
|
|
323
|
+
name: w.agentName || 'unknown',
|
|
324
|
+
intentType: w.intentType,
|
|
325
|
+
worktreeBranch: w.worktreeBranch,
|
|
326
|
+
description: w.description,
|
|
327
|
+
})),
|
|
328
|
+
conflictRisk: this.assessConflictRisk(sameDirectory, 'directory'),
|
|
329
|
+
suggestion: this.generateOverlapSuggestion(sameDirectory, 'same_directory'),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (overlappingFiles.length > 0) {
|
|
333
|
+
overlaps.push({
|
|
334
|
+
resource: 'files_overlap',
|
|
335
|
+
agents: overlappingFiles.map((w) => ({
|
|
336
|
+
id: w.agentId,
|
|
337
|
+
name: w.agentName || 'unknown',
|
|
338
|
+
intentType: w.intentType,
|
|
339
|
+
worktreeBranch: w.worktreeBranch,
|
|
340
|
+
description: w.description,
|
|
341
|
+
})),
|
|
342
|
+
conflictRisk: 'medium',
|
|
343
|
+
suggestion: this.generateOverlapSuggestion(overlappingFiles, 'files_overlap'),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return overlaps;
|
|
347
|
+
}
|
|
348
|
+
assessConflictRisk(work, type = 'file') {
|
|
349
|
+
if (work.length === 0)
|
|
350
|
+
return 'none';
|
|
351
|
+
// Multiple agents editing same file = high risk
|
|
352
|
+
const editors = work.filter((w) => w.intentType === 'editing' || w.intentType === 'refactoring');
|
|
353
|
+
if (editors.length >= 2)
|
|
354
|
+
return 'critical';
|
|
355
|
+
if (editors.length === 1 && work.length > 1)
|
|
356
|
+
return 'high';
|
|
357
|
+
// Refactoring has higher conflict potential
|
|
358
|
+
if (work.some((w) => w.intentType === 'refactoring'))
|
|
359
|
+
return 'high';
|
|
360
|
+
// Directory-level work is lower risk
|
|
361
|
+
if (type === 'directory')
|
|
362
|
+
return 'medium';
|
|
363
|
+
// Review/test/document are low risk
|
|
364
|
+
const lowRiskTypes = ['reviewing', 'testing', 'documenting'];
|
|
365
|
+
if (work.every((w) => lowRiskTypes.includes(w.intentType)))
|
|
366
|
+
return 'low';
|
|
367
|
+
return 'medium';
|
|
368
|
+
}
|
|
369
|
+
generateOverlapSuggestion(work, type) {
|
|
370
|
+
const agentNames = work.map((w) => w.agentName || w.agentId.slice(0, 8)).join(', ');
|
|
371
|
+
switch (type) {
|
|
372
|
+
case 'same_file':
|
|
373
|
+
return `Multiple agents (${agentNames}) working on same file. Consider: ` +
|
|
374
|
+
`1) Coordinate merge order - who finishes first should merge first, ` +
|
|
375
|
+
`2) Split into non-overlapping sections, ` +
|
|
376
|
+
`3) One agent waits for other to complete.`;
|
|
377
|
+
case 'same_directory':
|
|
378
|
+
return `Agents (${agentNames}) working in same directory. Usually safe with worktrees, ` +
|
|
379
|
+
`but watch for: import changes, shared types/interfaces, barrel files (index.ts).`;
|
|
380
|
+
case 'files_overlap':
|
|
381
|
+
return `Agents (${agentNames}) have overlapping file changes. Review affected files ` +
|
|
382
|
+
`and coordinate merge order to minimize conflicts.`;
|
|
383
|
+
default:
|
|
384
|
+
return `Overlap detected with ${agentNames}. Coordinate to optimize velocity.`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Generate collaboration suggestions based on overlaps.
|
|
389
|
+
*/
|
|
390
|
+
generateCollaborationSuggestions(agentId, _resource, overlaps) {
|
|
391
|
+
const suggestions = [];
|
|
392
|
+
for (const overlap of overlaps) {
|
|
393
|
+
const allAgents = [agentId, ...overlap.agents.map((a) => a.id)];
|
|
394
|
+
// Critical/High risk: suggest sequential work
|
|
395
|
+
if (overlap.conflictRisk === 'critical' || overlap.conflictRisk === 'high') {
|
|
396
|
+
suggestions.push({
|
|
397
|
+
type: 'sequence',
|
|
398
|
+
agents: allAgents,
|
|
399
|
+
reason: `High merge conflict risk on ${overlap.resource}. Sequential work recommended.`,
|
|
400
|
+
suggestedOrder: this.suggestMergeOrder(overlap),
|
|
401
|
+
estimatedMergeComplexity: overlap.conflictRisk,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
// Medium risk: suggest merge order
|
|
405
|
+
if (overlap.conflictRisk === 'medium') {
|
|
406
|
+
suggestions.push({
|
|
407
|
+
type: 'merge_order',
|
|
408
|
+
agents: allAgents,
|
|
409
|
+
reason: `Medium conflict risk. Agree on merge order to avoid rebase pain.`,
|
|
410
|
+
suggestedOrder: this.suggestMergeOrder(overlap),
|
|
411
|
+
estimatedMergeComplexity: 'medium',
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
// Low risk: parallel is fine
|
|
415
|
+
if (overlap.conflictRisk === 'low') {
|
|
416
|
+
suggestions.push({
|
|
417
|
+
type: 'parallel',
|
|
418
|
+
agents: allAgents,
|
|
419
|
+
reason: `Low conflict risk. Parallel work is safe. Watch for shared imports.`,
|
|
420
|
+
estimatedMergeComplexity: 'low',
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return suggestions;
|
|
425
|
+
}
|
|
426
|
+
suggestMergeOrder(overlap) {
|
|
427
|
+
// Prefer: review/test first, then docs, then edits, then refactoring
|
|
428
|
+
const priorityOrder = ['reviewing', 'testing', 'documenting', 'editing', 'refactoring'];
|
|
429
|
+
return overlap.agents
|
|
430
|
+
.sort((a, b) => {
|
|
431
|
+
const aPriority = priorityOrder.indexOf(a.intentType);
|
|
432
|
+
const bPriority = priorityOrder.indexOf(b.intentType);
|
|
433
|
+
return aPriority - bPriority;
|
|
434
|
+
})
|
|
435
|
+
.map((a) => a.name || a.id);
|
|
436
|
+
}
|
|
437
|
+
// ==================== Messaging ====================
|
|
438
|
+
broadcast(fromAgent, channel, payload, priority = 5) {
|
|
439
|
+
this.sendMessage(fromAgent, undefined, channel, 'notification', payload, priority);
|
|
440
|
+
}
|
|
441
|
+
send(fromAgent, toAgent, payload, priority = 5) {
|
|
442
|
+
this.sendMessage(fromAgent, toAgent, 'direct', 'request', payload, priority);
|
|
443
|
+
}
|
|
444
|
+
sendMessage(fromAgent, toAgent, channel, type, payload, priority) {
|
|
445
|
+
const stmt = this.db.prepare(`
|
|
446
|
+
INSERT INTO agent_messages (channel, from_agent, to_agent, type, payload, priority, created_at)
|
|
447
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
448
|
+
`);
|
|
449
|
+
stmt.run(channel, fromAgent, toAgent, type, JSON.stringify(payload), priority, new Date().toISOString());
|
|
450
|
+
}
|
|
451
|
+
receive(agentId, channel, markAsRead = true) {
|
|
452
|
+
let sql = `
|
|
453
|
+
SELECT id, channel, from_agent as fromAgent, to_agent as toAgent, type,
|
|
454
|
+
payload, priority, created_at as createdAt, read_at as readAt, expires_at as expiresAt
|
|
455
|
+
FROM agent_messages
|
|
456
|
+
WHERE (to_agent = ? OR (to_agent IS NULL AND channel != 'direct'))
|
|
457
|
+
AND read_at IS NULL
|
|
458
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
459
|
+
`;
|
|
460
|
+
const params = [agentId, new Date().toISOString()];
|
|
461
|
+
if (channel) {
|
|
462
|
+
sql += ' AND channel = ?';
|
|
463
|
+
params.push(channel);
|
|
464
|
+
}
|
|
465
|
+
sql += ' ORDER BY priority DESC, created_at ASC';
|
|
466
|
+
const stmt = this.db.prepare(sql);
|
|
467
|
+
const rows = stmt.all(...params);
|
|
468
|
+
const messages = rows.map((row) => ({
|
|
469
|
+
...row,
|
|
470
|
+
payload: JSON.parse(row.payload),
|
|
471
|
+
}));
|
|
472
|
+
if (markAsRead && messages.length > 0) {
|
|
473
|
+
const ids = messages.map((m) => m.id);
|
|
474
|
+
const updateStmt = this.db.prepare(`
|
|
475
|
+
UPDATE agent_messages
|
|
476
|
+
SET read_at = ?
|
|
477
|
+
WHERE id IN (${ids.map(() => '?').join(',')})
|
|
478
|
+
`);
|
|
479
|
+
updateStmt.run(new Date().toISOString(), ...ids);
|
|
480
|
+
}
|
|
481
|
+
return messages;
|
|
482
|
+
}
|
|
483
|
+
getPendingMessages(agentId) {
|
|
484
|
+
const stmt = this.db.prepare(`
|
|
485
|
+
SELECT COUNT(*) as count
|
|
486
|
+
FROM agent_messages
|
|
487
|
+
WHERE (to_agent = ? OR (to_agent IS NULL AND channel != 'direct'))
|
|
488
|
+
AND read_at IS NULL
|
|
489
|
+
AND (expires_at IS NULL OR expires_at > ?)
|
|
490
|
+
`);
|
|
491
|
+
const row = stmt.get(agentId, new Date().toISOString());
|
|
492
|
+
return row.count;
|
|
493
|
+
}
|
|
494
|
+
// ==================== Deploy Queue ====================
|
|
495
|
+
queueDeploy(agentId, actionType, target, payload, options = {}) {
|
|
496
|
+
const now = new Date().toISOString();
|
|
497
|
+
const executeAfter = options.executeAfter?.toISOString() ||
|
|
498
|
+
new Date(Date.now() + 30000).toISOString(); // Default 30s delay for batching
|
|
499
|
+
const stmt = this.db.prepare(`
|
|
500
|
+
INSERT INTO deploy_queue (agent_id, action_type, target, payload, status, queued_at, execute_after, priority, dependencies)
|
|
501
|
+
VALUES (?, ?, ?, ?, 'pending', ?, ?, ?, ?)
|
|
502
|
+
`);
|
|
503
|
+
const result = stmt.run(agentId, actionType, target, payload ? JSON.stringify(payload) : null, now, executeAfter, options.priority || 5, options.dependencies ? JSON.stringify(options.dependencies) : null);
|
|
504
|
+
return result.lastInsertRowid;
|
|
505
|
+
}
|
|
506
|
+
getPendingDeploys() {
|
|
507
|
+
const stmt = this.db.prepare(`
|
|
508
|
+
SELECT id, agent_id as agentId, action_type as actionType, target, payload,
|
|
509
|
+
status, batch_id as batchId, queued_at as queuedAt,
|
|
510
|
+
execute_after as executeAfter, priority, dependencies
|
|
511
|
+
FROM deploy_queue
|
|
512
|
+
WHERE status = 'pending'
|
|
513
|
+
ORDER BY priority DESC, queued_at ASC
|
|
514
|
+
`);
|
|
515
|
+
const rows = stmt.all();
|
|
516
|
+
return rows.map((row) => ({
|
|
517
|
+
...row,
|
|
518
|
+
payload: row.payload ? JSON.parse(row.payload) : undefined,
|
|
519
|
+
dependencies: row.dependencies ? JSON.parse(row.dependencies) : undefined,
|
|
520
|
+
}));
|
|
521
|
+
}
|
|
522
|
+
getReadyDeploys() {
|
|
523
|
+
const now = new Date().toISOString();
|
|
524
|
+
const stmt = this.db.prepare(`
|
|
525
|
+
SELECT id, agent_id as agentId, action_type as actionType, target, payload,
|
|
526
|
+
status, batch_id as batchId, queued_at as queuedAt,
|
|
527
|
+
execute_after as executeAfter, priority, dependencies
|
|
528
|
+
FROM deploy_queue
|
|
529
|
+
WHERE status = 'pending' AND execute_after <= ?
|
|
530
|
+
ORDER BY priority DESC, queued_at ASC
|
|
531
|
+
`);
|
|
532
|
+
const rows = stmt.all(now);
|
|
533
|
+
return rows.map((row) => ({
|
|
534
|
+
...row,
|
|
535
|
+
payload: row.payload ? JSON.parse(row.payload) : undefined,
|
|
536
|
+
dependencies: row.dependencies ? JSON.parse(row.dependencies) : undefined,
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
updateDeployStatus(deployId, status, batchId) {
|
|
540
|
+
const stmt = this.db.prepare(`
|
|
541
|
+
UPDATE deploy_queue
|
|
542
|
+
SET status = ?, batch_id = ?
|
|
543
|
+
WHERE id = ?
|
|
544
|
+
`);
|
|
545
|
+
stmt.run(status, batchId || null, deployId);
|
|
546
|
+
}
|
|
547
|
+
// ==================== Status ====================
|
|
548
|
+
getStatus() {
|
|
549
|
+
const activeAgents = this.getActiveAgents();
|
|
550
|
+
const claimsStmt = this.db.prepare(`
|
|
551
|
+
SELECT id, resource, agent_id as agentId, claim_type as claimType,
|
|
552
|
+
claimed_at as claimedAt, expires_at as expiresAt
|
|
553
|
+
FROM work_claims
|
|
554
|
+
WHERE expires_at IS NULL OR expires_at > ?
|
|
555
|
+
`);
|
|
556
|
+
const activeClaims = claimsStmt.all(new Date().toISOString());
|
|
557
|
+
const pendingDeploys = this.getPendingDeploys();
|
|
558
|
+
// Count pending messages (broadcast + unclaimed)
|
|
559
|
+
const msgStmt = this.db.prepare(`
|
|
560
|
+
SELECT COUNT(*) as count
|
|
561
|
+
FROM agent_messages
|
|
562
|
+
WHERE read_at IS NULL AND (expires_at IS NULL OR expires_at > ?)
|
|
563
|
+
`);
|
|
564
|
+
const msgRow = msgStmt.get(new Date().toISOString());
|
|
565
|
+
return {
|
|
566
|
+
activeAgents,
|
|
567
|
+
activeClaims,
|
|
568
|
+
pendingDeploys,
|
|
569
|
+
pendingMessages: msgRow.count,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
// ==================== Cleanup ====================
|
|
573
|
+
cleanup() {
|
|
574
|
+
const cutoff = new Date(Date.now() - 86400000).toISOString(); // 24 hours ago
|
|
575
|
+
// Clean old messages
|
|
576
|
+
this.db.prepare(`DELETE FROM agent_messages WHERE created_at < ?`).run(cutoff);
|
|
577
|
+
// Clean expired claims
|
|
578
|
+
this.db.prepare(`DELETE FROM work_claims WHERE expires_at < ?`).run(new Date().toISOString());
|
|
579
|
+
// Clean old completed agents
|
|
580
|
+
this.db.prepare(`
|
|
581
|
+
DELETE FROM agent_registry
|
|
582
|
+
WHERE status IN ('completed', 'failed') AND started_at < ?
|
|
583
|
+
`).run(cutoff);
|
|
584
|
+
// Clean old completed deploys
|
|
585
|
+
this.db.prepare(`
|
|
586
|
+
DELETE FROM deploy_queue
|
|
587
|
+
WHERE status IN ('completed', 'failed') AND queued_at < ?
|
|
588
|
+
`).run(cutoff);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
//# sourceMappingURL=service.js.map
|