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.
Files changed (67) hide show
  1. package/dist/bin/cli.js +207 -0
  2. package/dist/bin/cli.js.map +1 -1
  3. package/dist/cli/agent.d.ts +20 -0
  4. package/dist/cli/agent.d.ts.map +1 -0
  5. package/dist/cli/agent.js +388 -0
  6. package/dist/cli/agent.js.map +1 -0
  7. package/dist/cli/coord.d.ts +7 -0
  8. package/dist/cli/coord.d.ts.map +1 -0
  9. package/dist/cli/coord.js +145 -0
  10. package/dist/cli/coord.js.map +1 -0
  11. package/dist/cli/deploy.d.ts +19 -0
  12. package/dist/cli/deploy.d.ts.map +1 -0
  13. package/dist/cli/deploy.js +267 -0
  14. package/dist/cli/deploy.js.map +1 -0
  15. package/dist/cli/task.d.ts +33 -0
  16. package/dist/cli/task.d.ts.map +1 -0
  17. package/dist/cli/task.js +570 -0
  18. package/dist/cli/task.js.map +1 -0
  19. package/dist/coordination/database.d.ts +13 -0
  20. package/dist/coordination/database.d.ts.map +1 -0
  21. package/dist/coordination/database.js +131 -0
  22. package/dist/coordination/database.js.map +1 -0
  23. package/dist/coordination/deploy-batcher.d.ts +38 -0
  24. package/dist/coordination/deploy-batcher.d.ts.map +1 -0
  25. package/dist/coordination/deploy-batcher.js +401 -0
  26. package/dist/coordination/deploy-batcher.js.map +1 -0
  27. package/dist/coordination/index.d.ts +4 -0
  28. package/dist/coordination/index.d.ts.map +1 -0
  29. package/dist/coordination/index.js +4 -0
  30. package/dist/coordination/index.js.map +1 -0
  31. package/dist/coordination/service.d.ts +79 -0
  32. package/dist/coordination/service.d.ts.map +1 -0
  33. package/dist/coordination/service.js +591 -0
  34. package/dist/coordination/service.js.map +1 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/tasks/coordination.d.ts +74 -0
  40. package/dist/tasks/coordination.d.ts.map +1 -0
  41. package/dist/tasks/coordination.js +237 -0
  42. package/dist/tasks/coordination.js.map +1 -0
  43. package/dist/tasks/database.d.ts +14 -0
  44. package/dist/tasks/database.d.ts.map +1 -0
  45. package/dist/tasks/database.js +128 -0
  46. package/dist/tasks/database.js.map +1 -0
  47. package/dist/tasks/index.d.ts +5 -0
  48. package/dist/tasks/index.d.ts.map +1 -0
  49. package/dist/tasks/index.js +5 -0
  50. package/dist/tasks/index.js.map +1 -0
  51. package/dist/tasks/service.d.ts +39 -0
  52. package/dist/tasks/service.d.ts.map +1 -0
  53. package/dist/tasks/service.js +582 -0
  54. package/dist/tasks/service.js.map +1 -0
  55. package/dist/tasks/types.d.ts +224 -0
  56. package/dist/tasks/types.d.ts.map +1 -0
  57. package/dist/tasks/types.js +64 -0
  58. package/dist/tasks/types.js.map +1 -0
  59. package/dist/types/coordination.d.ts +240 -0
  60. package/dist/types/coordination.d.ts.map +1 -0
  61. package/dist/types/coordination.js +43 -0
  62. package/dist/types/coordination.js.map +1 -0
  63. package/dist/types/index.d.ts +1 -0
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/dist/types/index.js +1 -0
  66. package/dist/types/index.js.map +1 -1
  67. 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":"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"}
@@ -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