agent-working-memory 0.6.0 → 0.6.1

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 (122) hide show
  1. package/README.md +15 -9
  2. package/dist/adapters/claude-code.d.ts +4 -0
  3. package/dist/adapters/claude-code.d.ts.map +1 -0
  4. package/dist/adapters/claude-code.js +218 -0
  5. package/dist/adapters/claude-code.js.map +1 -0
  6. package/dist/adapters/codex.d.ts +4 -0
  7. package/dist/adapters/codex.d.ts.map +1 -0
  8. package/dist/adapters/codex.js +226 -0
  9. package/dist/adapters/codex.js.map +1 -0
  10. package/dist/adapters/common.d.ts +34 -0
  11. package/dist/adapters/common.d.ts.map +1 -0
  12. package/dist/adapters/common.js +145 -0
  13. package/dist/adapters/common.js.map +1 -0
  14. package/dist/adapters/cursor.d.ts +4 -0
  15. package/dist/adapters/cursor.d.ts.map +1 -0
  16. package/dist/adapters/cursor.js +138 -0
  17. package/dist/adapters/cursor.js.map +1 -0
  18. package/dist/adapters/http.d.ts +4 -0
  19. package/dist/adapters/http.d.ts.map +1 -0
  20. package/dist/adapters/http.js +88 -0
  21. package/dist/adapters/http.js.map +1 -0
  22. package/dist/adapters/index.d.ts +7 -0
  23. package/dist/adapters/index.d.ts.map +1 -0
  24. package/dist/adapters/index.js +21 -0
  25. package/dist/adapters/index.js.map +1 -0
  26. package/dist/adapters/types.d.ts +65 -0
  27. package/dist/adapters/types.d.ts.map +1 -0
  28. package/dist/adapters/types.js +4 -0
  29. package/dist/adapters/types.js.map +1 -0
  30. package/dist/cli.js +104 -230
  31. package/dist/cli.js.map +1 -1
  32. package/dist/coordination/events.d.ts +59 -0
  33. package/dist/coordination/events.d.ts.map +1 -0
  34. package/dist/coordination/events.js +28 -0
  35. package/dist/coordination/events.js.map +1 -0
  36. package/dist/coordination/index.d.ts +10 -1
  37. package/dist/coordination/index.d.ts.map +1 -1
  38. package/dist/coordination/index.js +87 -3
  39. package/dist/coordination/index.js.map +1 -1
  40. package/dist/coordination/peer-decisions.d.ts +40 -0
  41. package/dist/coordination/peer-decisions.d.ts.map +1 -0
  42. package/dist/coordination/peer-decisions.js +82 -0
  43. package/dist/coordination/peer-decisions.js.map +1 -0
  44. package/dist/coordination/plugin-loader.d.ts +18 -0
  45. package/dist/coordination/plugin-loader.d.ts.map +1 -0
  46. package/dist/coordination/plugin-loader.js +55 -0
  47. package/dist/coordination/plugin-loader.js.map +1 -0
  48. package/dist/coordination/plugin.d.ts +40 -0
  49. package/dist/coordination/plugin.d.ts.map +1 -0
  50. package/dist/coordination/plugin.js +22 -0
  51. package/dist/coordination/plugin.js.map +1 -0
  52. package/dist/coordination/routes.d.ts +2 -1
  53. package/dist/coordination/routes.d.ts.map +1 -1
  54. package/dist/coordination/routes.js +899 -76
  55. package/dist/coordination/routes.js.map +1 -1
  56. package/dist/coordination/schema.d.ts.map +1 -1
  57. package/dist/coordination/schema.js +72 -14
  58. package/dist/coordination/schema.js.map +1 -1
  59. package/dist/coordination/schemas.d.ts +84 -3
  60. package/dist/coordination/schemas.d.ts.map +1 -1
  61. package/dist/coordination/schemas.js +71 -1
  62. package/dist/coordination/schemas.js.map +1 -1
  63. package/dist/coordination/stale.d.ts.map +1 -1
  64. package/dist/coordination/stale.js +2 -1
  65. package/dist/coordination/stale.js.map +1 -1
  66. package/dist/coordination/types.d.ts +252 -0
  67. package/dist/coordination/types.d.ts.map +1 -0
  68. package/dist/coordination/types.js +8 -0
  69. package/dist/coordination/types.js.map +1 -0
  70. package/dist/coordination/write-mutex.d.ts +26 -0
  71. package/dist/coordination/write-mutex.d.ts.map +1 -0
  72. package/dist/coordination/write-mutex.js +63 -0
  73. package/dist/coordination/write-mutex.js.map +1 -0
  74. package/dist/core/embeddings.d.ts +2 -0
  75. package/dist/core/embeddings.d.ts.map +1 -1
  76. package/dist/core/embeddings.js +4 -0
  77. package/dist/core/embeddings.js.map +1 -1
  78. package/dist/engine/activation.d.ts.map +1 -1
  79. package/dist/engine/activation.js +16 -3
  80. package/dist/engine/activation.js.map +1 -1
  81. package/dist/engine/consolidation.d.ts.map +1 -1
  82. package/dist/engine/consolidation.js +15 -6
  83. package/dist/engine/consolidation.js.map +1 -1
  84. package/dist/engine/retraction.d.ts +3 -1
  85. package/dist/engine/retraction.d.ts.map +1 -1
  86. package/dist/engine/retraction.js +19 -6
  87. package/dist/engine/retraction.js.map +1 -1
  88. package/dist/index.js +6 -18
  89. package/dist/index.js.map +1 -1
  90. package/dist/mcp.js +52 -3
  91. package/dist/mcp.js.map +1 -1
  92. package/dist/storage/sqlite.d.ts +6 -1
  93. package/dist/storage/sqlite.d.ts.map +1 -1
  94. package/dist/storage/sqlite.js +39 -3
  95. package/dist/storage/sqlite.js.map +1 -1
  96. package/package.json +1 -1
  97. package/src/adapters/claude-code.ts +234 -0
  98. package/src/adapters/codex.ts +262 -0
  99. package/src/adapters/common.ts +172 -0
  100. package/src/adapters/cursor.ts +150 -0
  101. package/src/adapters/http.ts +100 -0
  102. package/src/adapters/index.ts +31 -0
  103. package/src/adapters/types.ts +75 -0
  104. package/src/cli.ts +107 -238
  105. package/src/coordination/events.ts +90 -0
  106. package/src/coordination/index.ts +102 -3
  107. package/src/coordination/peer-decisions.ts +105 -0
  108. package/src/coordination/plugin-loader.ts +60 -0
  109. package/src/coordination/plugin.ts +44 -0
  110. package/src/coordination/routes.ts +1176 -105
  111. package/src/coordination/schema.ts +67 -14
  112. package/src/coordination/schemas.ts +85 -1
  113. package/src/coordination/stale.ts +3 -2
  114. package/src/coordination/types.ts +311 -0
  115. package/src/coordination/write-mutex.ts +69 -0
  116. package/src/core/embeddings.ts +5 -0
  117. package/src/engine/activation.ts +13 -3
  118. package/src/engine/consolidation.ts +15 -6
  119. package/src/engine/retraction.ts +22 -6
  120. package/src/index.ts +6 -15
  121. package/src/mcp.ts +73 -9
  122. package/src/storage/sqlite.ts +39 -3
@@ -11,22 +11,21 @@ import type Database from 'better-sqlite3';
11
11
  const COORDINATION_TABLES = `
12
12
  -- Coordination: agents in the hive
13
13
  CREATE TABLE IF NOT EXISTS coord_agents (
14
- id TEXT PRIMARY KEY,
15
- name TEXT NOT NULL,
16
- role TEXT NOT NULL DEFAULT 'worker',
17
- status TEXT NOT NULL DEFAULT 'idle',
18
- pid INTEGER,
19
- started_at TEXT NOT NULL DEFAULT (datetime('now')),
20
- last_seen TEXT NOT NULL DEFAULT (datetime('now')),
21
- current_task TEXT,
22
- metadata TEXT,
23
- capabilities TEXT,
24
- workspace TEXT
14
+ id TEXT PRIMARY KEY,
15
+ name TEXT NOT NULL,
16
+ role TEXT NOT NULL DEFAULT 'worker',
17
+ status TEXT NOT NULL DEFAULT 'idle',
18
+ pid INTEGER,
19
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
20
+ last_seen TEXT NOT NULL DEFAULT (datetime('now')),
21
+ current_task TEXT,
22
+ metadata TEXT,
23
+ capabilities TEXT,
24
+ workspace TEXT,
25
+ session_token TEXT
25
26
  );
26
27
 
27
- -- Prevent duplicate agent registrations for the same name+workspace
28
- CREATE UNIQUE INDEX IF NOT EXISTS idx_coord_agents_name_workspace
29
- ON coord_agents (name, COALESCE(workspace, ''));
28
+ -- unique index on (name, workspace) is created after dedup in initCoordinationTables
30
29
 
31
30
  -- Coordination: assignments
32
31
  CREATE TABLE IF NOT EXISTS coord_assignments (
@@ -43,6 +42,7 @@ CREATE TABLE IF NOT EXISTS coord_assignments (
43
42
  result TEXT,
44
43
  commit_sha TEXT,
45
44
  workspace TEXT,
45
+ context TEXT,
46
46
  FOREIGN KEY (agent_id) REFERENCES coord_agents(id),
47
47
  FOREIGN KEY (blocked_by) REFERENCES coord_assignments(id)
48
48
  );
@@ -96,6 +96,32 @@ CREATE TABLE IF NOT EXISTS coord_decisions (
96
96
  CREATE INDEX IF NOT EXISTS idx_coord_decisions_assignment
97
97
  ON coord_decisions (assignment_id, created_at);
98
98
 
99
+ -- Coordination: channel sessions (push-based coordination)
100
+ CREATE TABLE IF NOT EXISTS coord_channel_sessions (
101
+ agent_id TEXT PRIMARY KEY REFERENCES coord_agents(id),
102
+ channel_id TEXT NOT NULL,
103
+ connected_at TEXT NOT NULL DEFAULT (datetime('now')),
104
+ last_push_at TEXT,
105
+ push_count INTEGER NOT NULL DEFAULT 0,
106
+ status TEXT NOT NULL DEFAULT 'connected'
107
+ );
108
+
109
+ -- Coordination: named mailbox (persistent message queue per worker name)
110
+ -- Messages survive AWM restarts and worker disconnects.
111
+ -- Delivered on next /next poll, then marked delivered.
112
+ CREATE TABLE IF NOT EXISTS coord_mailbox (
113
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
114
+ worker_name TEXT NOT NULL,
115
+ workspace TEXT,
116
+ message TEXT NOT NULL,
117
+ source TEXT DEFAULT 'coordinator',
118
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
119
+ delivered_at TEXT,
120
+ read_at TEXT
121
+ );
122
+ CREATE INDEX IF NOT EXISTS idx_coord_mailbox_worker
123
+ ON coord_mailbox (worker_name, workspace, delivered_at);
124
+
99
125
  -- Coordination: event audit trail
100
126
  CREATE TABLE IF NOT EXISTS coord_events (
101
127
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -111,10 +137,37 @@ CREATE TABLE IF NOT EXISTS coord_events (
111
137
  * Safe to call multiple times (CREATE IF NOT EXISTS).
112
138
  */
113
139
  export function initCoordinationTables(db: Database.Database): void {
140
+ // Create tables (unique index is added separately after dedup)
114
141
  db.exec(COORDINATION_TABLES);
115
142
 
143
+ // Deduplicate coord_agents before creating the unique index.
144
+ // Keep only the most recently seen row for each (name, workspace) pair.
145
+ try {
146
+ db.exec(`
147
+ DELETE FROM coord_agents
148
+ WHERE rowid NOT IN (
149
+ SELECT MAX(rowid) FROM coord_agents
150
+ GROUP BY name, COALESCE(workspace, '')
151
+ )
152
+ `);
153
+ } catch { /* table may not exist yet — that's fine */ }
154
+
155
+ // Now create the unique index safely
156
+ try {
157
+ db.exec(`
158
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_coord_agents_name_workspace
159
+ ON coord_agents (name, COALESCE(workspace, ''));
160
+ `);
161
+ } catch {
162
+ // Index may already exist from a previous run — that's fine
163
+ }
164
+
116
165
  // Migrations: add columns to existing coord_assignments tables
117
166
  try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN commit_sha TEXT`); } catch { /* exists */ }
118
167
  try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN priority INTEGER NOT NULL DEFAULT 0`); } catch { /* exists */ }
119
168
  try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN blocked_by TEXT`); } catch { /* exists */ }
169
+ try { db.exec(`ALTER TABLE coord_assignments ADD COLUMN context TEXT`); } catch { /* exists */ }
170
+
171
+ // Migration: session token for hijack prevention (added 2026-03-26)
172
+ try { db.exec(`ALTER TABLE coord_agents ADD COLUMN session_token TEXT`); } catch { /* exists */ }
120
173
  }
@@ -9,7 +9,7 @@ import { z } from 'zod';
9
9
 
10
10
  // ─── Enums ──────────────────────────────────────────────────────
11
11
 
12
- export const agentRoleEnum = z.enum(['worker', 'orchestrator', 'dev-lead']);
12
+ export const agentRoleEnum = z.enum(['worker', 'orchestrator', 'coordinator', 'dev-lead']);
13
13
  export const agentStatusEnum = z.enum(['idle', 'working', 'dead']);
14
14
  export const assignmentStatusEnum = z.enum(['in_progress', 'completed', 'failed', 'blocked']);
15
15
  export const commandEnum = z.enum(['BUILD_FREEZE', 'PAUSE', 'RESUME', 'SHUTDOWN']);
@@ -30,6 +30,7 @@ export const checkinSchema = z.object({
30
30
  metadata: z.record(z.string(), z.unknown()).optional(),
31
31
  capabilities: z.array(z.string().max(50)).max(20).optional(),
32
32
  workspace: z.string().max(50).optional(),
33
+ channelUrl: z.string().url().max(200).optional(),
33
34
  });
34
35
 
35
36
  export const checkoutSchema = z.object({
@@ -40,11 +41,13 @@ export const checkoutSchema = z.object({
40
41
 
41
42
  export const assignCreateSchema = z.object({
42
43
  agentId: z.string().uuid().optional(),
44
+ worker_name: z.string().min(1).max(50).optional(),
43
45
  task: z.string().min(1).max(1000),
44
46
  description: z.string().max(5000).optional(),
45
47
  workspace: z.string().max(50).optional(),
46
48
  priority: z.number().int().min(0).max(10).default(0),
47
49
  blocked_by: z.string().uuid().optional(),
50
+ context: z.string().max(10000).optional(),
48
51
  });
49
52
 
50
53
  export const assignmentQuerySchema = z.object({
@@ -58,12 +61,27 @@ export const nextSchema = z.object({
58
61
  workspace: z.string().max(50).optional(),
59
62
  role: agentRoleEnum.default('worker'),
60
63
  capabilities: z.array(z.string().max(50)).max(20).optional(),
64
+ channelUrl: z.string().url().max(200).optional(),
61
65
  });
62
66
 
63
67
  export const assignmentClaimSchema = z.object({
64
68
  agentId: z.string().uuid(),
65
69
  });
66
70
 
71
+ export const assignmentsListSchema = z.object({
72
+ status: z.enum(['pending', 'assigned', 'in_progress', 'completed', 'failed', 'blocked']).optional(),
73
+ workspace: z.string().max(50).optional(),
74
+ agent_id: z.string().uuid().optional(),
75
+ limit: z.coerce.number().int().min(1).max(100).default(20),
76
+ offset: z.coerce.number().int().min(0).default(0),
77
+ });
78
+
79
+ export const reassignSchema = z.object({
80
+ assignmentId: z.string().uuid(),
81
+ targetAgentId: z.string().uuid().optional(),
82
+ target_worker_name: z.string().min(1).max(50).optional(),
83
+ });
84
+
67
85
  export const assignmentUpdateSchema = z.object({
68
86
  status: assignmentStatusEnum,
69
87
  result: z.string().max(10000).optional(),
@@ -118,6 +136,11 @@ export const findingsQuerySchema = z.object({
118
136
  limit: z.coerce.number().int().min(1).max(200).default(50),
119
137
  });
120
138
 
139
+ export const findingUpdateSchema = z.object({
140
+ status: findingStatusEnum.optional(),
141
+ suggestion: z.string().max(5000).optional(),
142
+ });
143
+
121
144
  // ─── Param Schemas ─────────────────────────────────────────────
122
145
 
123
146
  export const assignmentIdParamSchema = z.object({ id: z.string().uuid() });
@@ -134,12 +157,23 @@ export const pulseSchema = z.object({
134
157
  export const decisionsQuerySchema = z.object({
135
158
  since_id: z.coerce.number().int().min(0).default(0),
136
159
  assignment_id: z.string().max(100).optional(),
160
+ workspace: z.string().max(50).optional(),
137
161
  limit: z.coerce.number().int().min(1).max(200).default(20),
138
162
  });
139
163
 
164
+ export const decisionCreateSchema = z.object({
165
+ agentId: z.string().uuid(),
166
+ assignment_id: z.string().max(100).optional(),
167
+ tags: z.string().max(500).optional(),
168
+ summary: z.string().min(1).max(5000),
169
+ });
170
+
140
171
  // ─── Status / Events ────────────────────────────────────────────
141
172
 
142
173
  export const eventsQuerySchema = z.object({
174
+ since_id: z.coerce.number().int().min(0).default(0),
175
+ agent_id: z.string().uuid().optional(),
176
+ event_type: z.string().max(50).optional(),
143
177
  limit: z.coerce.number().int().min(1).max(200).default(50),
144
178
  });
145
179
 
@@ -153,3 +187,53 @@ export const workersQuerySchema = z.object({
153
187
  status: agentStatusEnum.optional(),
154
188
  workspace: z.string().max(50).optional(),
155
189
  });
190
+
191
+ // ─── Agent Params ─────────────────────────────────────────
192
+
193
+ export const agentIdParamSchema = z.object({ id: z.string().uuid() });
194
+
195
+ // ─── Timeline ─────────────────────────────────────────────
196
+
197
+ export const timelineQuerySchema = z.object({
198
+ limit: z.coerce.number().int().min(1).max(200).default(50),
199
+ since: z.string().max(30).optional(),
200
+ });
201
+
202
+ // ─── Channel Sessions ──────────────────────────────────────────
203
+
204
+ export const channelRegisterSchema = z.object({
205
+ agentId: z.string().uuid(),
206
+ channelId: z.string().min(1).max(200),
207
+ });
208
+
209
+ export const channelDeregisterSchema = z.object({
210
+ agentId: z.string().uuid(),
211
+ });
212
+
213
+ export const channelPushSchema = z.object({
214
+ agentId: z.string().uuid(),
215
+ message: z.string().min(1).max(10000),
216
+ });
217
+
218
+ // ─── Stats ─────────────────────────────────────────────────────
219
+
220
+ export const statsResponseSchema = z.object({
221
+ workers: z.object({
222
+ total: z.number().int(),
223
+ alive: z.number().int(),
224
+ idle: z.number().int(),
225
+ working: z.number().int(),
226
+ }),
227
+ tasks: z.object({
228
+ total_assigned: z.number().int(),
229
+ completed: z.number().int(),
230
+ failed: z.number().int(),
231
+ pending: z.number().int(),
232
+ avg_completion_seconds: z.number().nullable(),
233
+ }),
234
+ decisions: z.object({
235
+ total: z.number().int(),
236
+ last_hour: z.number().int(),
237
+ }),
238
+ uptime_seconds: z.number(),
239
+ });
@@ -80,6 +80,9 @@ export function purgeDeadAgents(db: Database.Database, maxAgeHours = 24): number
80
80
 
81
81
  /** Clean slate on startup: mark all live agents dead, release locks, clear commands. */
82
82
  export function cleanSlate(db: Database.Database): void {
83
+ // Always clear commands, even if no alive agents remain
84
+ db.prepare(`UPDATE coord_commands SET cleared_at = datetime('now') WHERE cleared_at IS NULL`).run();
85
+
83
86
  const alive = db.prepare(
84
87
  `SELECT id, name FROM coord_agents WHERE status != 'dead'`
85
88
  ).all() as Array<{ id: string; name: string }>;
@@ -91,7 +94,5 @@ export function cleanSlate(db: Database.Database): void {
91
94
  db.prepare(`DELETE FROM coord_locks WHERE agent_id = ?`).run(agent.id);
92
95
  }
93
96
 
94
- db.prepare(`UPDATE coord_commands SET cleared_at = datetime('now') WHERE cleared_at IS NULL`).run();
95
-
96
97
  console.log(` Coordination clean slate: marked ${alive.length} agent(s) from previous session as dead`);
97
98
  }
@@ -0,0 +1,311 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * TypeScript interfaces for coordination API responses.
5
+ * These types support typed API clients consuming the coordination HTTP endpoints.
6
+ */
7
+
8
+ // ─── Shared Primitives ─────────────────────────────────────────
9
+
10
+ export type AgentRole = 'worker' | 'orchestrator' | 'coordinator' | 'dev-lead';
11
+ export type AgentStatus = 'idle' | 'working' | 'dead';
12
+ export type AssignmentStatus = 'pending' | 'assigned' | 'in_progress' | 'completed' | 'failed' | 'blocked';
13
+ export type CommandType = 'BUILD_FREEZE' | 'PAUSE' | 'RESUME' | 'SHUTDOWN';
14
+ export type FindingSeverity = 'critical' | 'error' | 'warn' | 'info';
15
+ export type FindingStatus = 'open' | 'resolved';
16
+
17
+ // ─── Agent ─────────────────────────────────────────────────────
18
+
19
+ export interface CheckinResponse {
20
+ agentId: string;
21
+ action: 'registered' | 'heartbeat' | 'reconnected';
22
+ status: string;
23
+ workspace: string | null;
24
+ }
25
+
26
+ export interface AgentDetail {
27
+ id: string;
28
+ name: string;
29
+ role: AgentRole;
30
+ status: AgentStatus;
31
+ current_task: string | null;
32
+ pid: number | null;
33
+ capabilities: string | null;
34
+ workspace: string | null;
35
+ metadata: string | null;
36
+ last_seen: string;
37
+ started_at: string | null;
38
+ seconds_since_seen: number;
39
+ }
40
+
41
+ export interface AgentResponse {
42
+ agent: AgentDetail;
43
+ assignment: AssignmentSummary | null;
44
+ locks: LockEntry[];
45
+ }
46
+
47
+ // ─── Assignments ───────────────────────────────────────────────
48
+
49
+ export interface Assignment {
50
+ id: string;
51
+ agent_id: string | null;
52
+ task: string;
53
+ description: string | null;
54
+ status: AssignmentStatus;
55
+ priority: number;
56
+ blocked_by: string | null;
57
+ created_at: string;
58
+ started_at: string | null;
59
+ completed_at: string | null;
60
+ result: string | null;
61
+ commit_sha: string | null;
62
+ workspace: string | null;
63
+ context: string | null;
64
+ }
65
+
66
+ export interface AssignmentSummary {
67
+ id: string;
68
+ task: string;
69
+ status: AssignmentStatus;
70
+ priority: number;
71
+ created_at: string;
72
+ }
73
+
74
+ export interface AssignmentWithAgent extends Assignment {
75
+ agent_name: string | null;
76
+ is_blocked: 0 | 1;
77
+ }
78
+
79
+ export interface AssignCreateResponse {
80
+ assignmentId: string;
81
+ status: 'assigned' | 'pending';
82
+ }
83
+
84
+ export interface AssignmentsListResponse {
85
+ assignments: AssignmentWithAgent[];
86
+ total: number;
87
+ }
88
+
89
+ // ─── Next ──────────────────────────────────────────────────────
90
+
91
+ export interface CommandEntry {
92
+ id: number;
93
+ command: CommandType;
94
+ reason: string | null;
95
+ issued_by: string | null;
96
+ issued_at: string;
97
+ workspace: string | null;
98
+ }
99
+
100
+ export interface NextResponse {
101
+ agentId: string;
102
+ status: string;
103
+ assignment: Assignment | null;
104
+ commands: CommandEntry[];
105
+ }
106
+
107
+ // ─── Locks ─────────────────────────────────────────────────────
108
+
109
+ export interface LockEntry {
110
+ file_path: string;
111
+ locked_at: string;
112
+ reason: string | null;
113
+ }
114
+
115
+ export interface LockWithAgent extends LockEntry {
116
+ agent_id: string;
117
+ agent_name: string;
118
+ }
119
+
120
+ export interface LocksResponse {
121
+ locks: LockWithAgent[];
122
+ }
123
+
124
+ // ─── Commands ──────────────────────────────────────────────────
125
+
126
+ export interface CommandResponse {
127
+ active: boolean;
128
+ command?: CommandType;
129
+ reason?: string | null;
130
+ issued_at?: string;
131
+ commands: CommandEntry[];
132
+ }
133
+
134
+ // ─── Workers ───────────────────────────────────────────────────
135
+
136
+ export interface WorkerEntry {
137
+ id: string;
138
+ name: string;
139
+ role: AgentRole;
140
+ status: AgentStatus;
141
+ currentTask: string | null;
142
+ capabilities: string[];
143
+ workspace: string | null;
144
+ lastSeen: string;
145
+ secondsSinceSeen: number;
146
+ alive: boolean;
147
+ }
148
+
149
+ export interface WorkersResponse {
150
+ count: number;
151
+ idle: number;
152
+ working: number;
153
+ workers: WorkerEntry[];
154
+ }
155
+
156
+ // ─── Events ────────────────────────────────────────────────────
157
+
158
+ export interface EventEntry {
159
+ id: number;
160
+ agent_id: string | null;
161
+ agent_name: string | null;
162
+ event_type: string;
163
+ detail: string | null;
164
+ created_at: string;
165
+ }
166
+
167
+ export interface EventsResponse {
168
+ events: EventEntry[];
169
+ last_id: number;
170
+ }
171
+
172
+ // ─── Decisions ─────────────────────────────────────────────────
173
+
174
+ export interface DecisionEntry {
175
+ id: number;
176
+ author_id: string;
177
+ author_name: string;
178
+ assignment_id: string | null;
179
+ tags: string | null;
180
+ summary: string;
181
+ created_at: string;
182
+ }
183
+
184
+ export interface DecisionsResponse {
185
+ decisions: DecisionEntry[];
186
+ }
187
+
188
+ // ─── Findings ──────────────────────────────────────────────────
189
+
190
+ export interface FindingEntry {
191
+ id: number;
192
+ category: string;
193
+ severity: FindingSeverity;
194
+ file_path: string | null;
195
+ line_number: number | null;
196
+ description: string;
197
+ suggestion: string | null;
198
+ status: FindingStatus;
199
+ created_at: string;
200
+ agent_name: string;
201
+ }
202
+
203
+ export interface FindingSeverityCount {
204
+ severity: FindingSeverity;
205
+ count: number;
206
+ }
207
+
208
+ export interface FindingsResponse {
209
+ findings: FindingEntry[];
210
+ stats: FindingSeverityCount[];
211
+ }
212
+
213
+ // ─── Channel Sessions ─────────────────────────────────────────
214
+
215
+ export interface ChannelSession {
216
+ agent_id: string;
217
+ agent_name: string;
218
+ channel_id: string;
219
+ connected_at: string;
220
+ last_push_at: string | null;
221
+ push_count: number;
222
+ status: string;
223
+ }
224
+
225
+ export interface ChannelSessionsResponse {
226
+ sessions: ChannelSession[];
227
+ }
228
+
229
+ // ─── Status ────────────────────────────────────────────────────
230
+
231
+ export interface StatusResponse {
232
+ agents: Array<{
233
+ id: string;
234
+ name: string;
235
+ role: AgentRole;
236
+ status: AgentStatus;
237
+ current_task: string | null;
238
+ last_seen: string;
239
+ seconds_since_seen: number;
240
+ }>;
241
+ assignments: Array<{
242
+ id: string;
243
+ task: string;
244
+ description: string | null;
245
+ status: AssignmentStatus;
246
+ agent_id: string | null;
247
+ agent_name: string | null;
248
+ created_at: string;
249
+ started_at: string | null;
250
+ completed_at: string | null;
251
+ }>;
252
+ locks: LockWithAgent[];
253
+ stats: {
254
+ alive_agents: number;
255
+ busy_agents: number;
256
+ pending_tasks: number;
257
+ active_tasks: number;
258
+ active_locks: number;
259
+ open_findings: number;
260
+ urgent_findings: number;
261
+ };
262
+ recentFindings: Array<{
263
+ id: number;
264
+ category: string;
265
+ severity: FindingSeverity;
266
+ file_path: string | null;
267
+ description: string;
268
+ agent_name: string;
269
+ created_at: string;
270
+ }>;
271
+ }
272
+
273
+ // ─── Stats ─────────────────────────────────────────────────────
274
+
275
+ export interface StatsResponse {
276
+ workers: {
277
+ total: number;
278
+ alive: number;
279
+ idle: number;
280
+ working: number;
281
+ };
282
+ tasks: {
283
+ total_assigned: number;
284
+ completed: number;
285
+ failed: number;
286
+ pending: number;
287
+ avg_completion_seconds: number | null;
288
+ };
289
+ decisions: {
290
+ total: number;
291
+ last_hour: number;
292
+ };
293
+ uptime_seconds: number;
294
+ }
295
+
296
+ // ─── Stale ─────────────────────────────────────────────────────
297
+
298
+ export interface StaleAgent {
299
+ id: string;
300
+ name: string;
301
+ role: string;
302
+ status: string;
303
+ last_seen: string;
304
+ seconds_since_seen: number;
305
+ }
306
+
307
+ export interface StaleResponse {
308
+ stale: StaleAgent[];
309
+ threshold_seconds: number;
310
+ cleaned?: number;
311
+ }
@@ -0,0 +1,69 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Promise-based write mutex for SQLite coordination routes.
5
+ *
6
+ * SQLite only allows one writer at a time. Under burst from 5+ concurrent
7
+ * workers, multiple async handlers can race for the write lock, causing
8
+ * SQLITE_BUSY errors. This mutex serializes write operations (POST, PATCH,
9
+ * PUT, DELETE) through a single-concurrency queue while keeping reads (GET)
10
+ * unguarded for full parallelism.
11
+ *
12
+ * Usage: Register as a Fastify preHandler hook in the coordination module.
13
+ */
14
+
15
+ /**
16
+ * Creates a simple single-concurrency mutex.
17
+ * acquire() returns a release function — call it when done.
18
+ */
19
+ export function createWriteMutex(): {
20
+ acquire(): Promise<() => void>;
21
+ pending: () => number;
22
+ } {
23
+ let queue: Array<(release: () => void) => void> = [];
24
+ let locked = false;
25
+
26
+ function release(): void {
27
+ const next = queue.shift();
28
+ if (next) {
29
+ // Hand lock directly to next waiter (no unlock/relock gap)
30
+ next(release);
31
+ } else {
32
+ locked = false;
33
+ }
34
+ }
35
+
36
+ function acquire(): Promise<() => void> {
37
+ if (!locked) {
38
+ locked = true;
39
+ return Promise.resolve(release);
40
+ }
41
+ return new Promise<() => void>((resolve) => {
42
+ queue.push(resolve);
43
+ });
44
+ }
45
+
46
+ return {
47
+ acquire,
48
+ pending: () => queue.length,
49
+ };
50
+ }
51
+
52
+ /** HTTP methods that perform writes and need serialization. */
53
+ const WRITE_METHODS = new Set(['POST', 'PATCH', 'PUT', 'DELETE']);
54
+
55
+ /** Routes that are safe to skip the mutex (read-only despite POST method). */
56
+ const READ_ONLY_ROUTES = new Set(['/next']);
57
+
58
+ /**
59
+ * Check if a request needs write serialization.
60
+ * GET/HEAD/OPTIONS are always reads. POST /next is a read (checkin is idempotent upsert
61
+ * but low-contention). All other write methods go through the mutex.
62
+ */
63
+ export function needsWriteLock(method: string, url: string): boolean {
64
+ if (!WRITE_METHODS.has(method)) return false;
65
+ // Strip query string for route matching
66
+ const path = url.split('?')[0];
67
+ if (READ_ONLY_ROUTES.has(path)) return false;
68
+ return true;
69
+ }
@@ -53,6 +53,11 @@ export async function embed(text: string): Promise<number[]> {
53
53
  return Array.from(result.data as Float32Array).slice(0, DIMENSIONS);
54
54
  }
55
55
 
56
+ /** Get the current embedding model ID (for version tracking in stored embeddings) */
57
+ export function getModelId(): string {
58
+ return MODEL_ID;
59
+ }
60
+
56
61
  /**
57
62
  * Generate embeddings for multiple texts in a batch.
58
63
  * More efficient than calling embed() in a loop.