agent-relay-server 0.33.0 → 0.34.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.
@@ -0,0 +1,397 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { randomUUID } from "node:crypto";
3
+ import { isRecord, stringValue, isMechanicalMessageKind } from "agent-relay-sdk";
4
+ import { ORCHESTRATOR_PROTOCOL_VERSION, VERSION } from "../config.ts";
5
+ import { parseJson } from "../utils";
6
+ import { isLiveIsolatedWorkspace } from "../workspace-phase";
7
+ import {
8
+ CONTRACT_REQUIREMENTS,
9
+ contractCompatibility,
10
+ parseRuntimeCapabilities,
11
+ parseRuntimeContracts,
12
+ parseRuntimePackage,
13
+ type RuntimeContracts,
14
+ } from "../contracts";
15
+ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
16
+ import { matchAgents } from "../agent-ref";
17
+ import { parseStringArray } from "./agents.ts";
18
+ import { artifactId } from "./artifacts.ts";
19
+ import type {
20
+ AgentCard,
21
+ ActivityEvent,
22
+ ActivityEventInput,
23
+ AgentKind,
24
+ AgentSessionGuard,
25
+ Artifact,
26
+ ArtifactBlob,
27
+ ArtifactKind,
28
+ ArtifactLink,
29
+ ArtifactSensitivity,
30
+ ArtifactVisibility,
31
+ AttachmentRef,
32
+ ChannelBinding,
33
+ ChannelBindingMode,
34
+ ChannelRouteTarget,
35
+ ChatHistoryImport,
36
+ ChatHistoryImportEntry,
37
+ ChannelSummary,
38
+ ChannelTargetHealth,
39
+ CreatePairInput,
40
+ HealthCheck,
41
+ HealthReport,
42
+ ManagedAgent,
43
+ ManagedSessionExitDiagnostics,
44
+ Message,
45
+ MessageDeliveryAttempt,
46
+ MessageDeliveryStatus,
47
+ Orchestrator,
48
+ OrchestratorHealth,
49
+ OrchestratorRuntimeInput,
50
+ OrchestratorStatus,
51
+ OrchestratorUpgradeState,
52
+ PairActionInput,
53
+ PairMessageInput,
54
+ PairSession,
55
+ PairStatus,
56
+ RegisterAgentInput,
57
+ ReplyObligation,
58
+ RegisterOrchestratorInput,
59
+ SendMessageInput,
60
+ PollQuery,
61
+ SpawnApprovalMode,
62
+ SpawnProvider,
63
+ Task,
64
+ TaskEvent,
65
+ TaskSeverity,
66
+ TaskStatus,
67
+ IntegrationEventInput,
68
+ IntegrationSummary,
69
+ IntegrationTaskStats,
70
+ InboxDraft,
71
+ InboxState,
72
+ InboxThreadState,
73
+ ContextSnapshot,
74
+ ContextState,
75
+ ProviderCapabilities,
76
+ TaskStatusInput,
77
+ WorkspaceMetadata,
78
+ WorkspaceRecord,
79
+ WorkspaceStatus,
80
+ } from "../types";
81
+
82
+ export const REACTION_EMOJI_ALIASES: Record<string, string> = {
83
+ "+1": "👍",
84
+ ":+1:": "👍",
85
+ "thumbsup": "👍",
86
+ ":thumbsup:": "👍",
87
+ "thumbs-up": "👍",
88
+ ":thumbs-up:": "👍",
89
+ "thumbs_up": "👍",
90
+ ":thumbs_up:": "👍",
91
+ "thumb_up": "👍",
92
+ ":thumb_up:": "👍",
93
+ "like": "👍",
94
+ ":like:": "👍",
95
+ "heart": "❤️",
96
+ ":heart:": "❤️",
97
+ "redheart": "❤️",
98
+ "red_heart": "❤️",
99
+ ":red_heart:": "❤️",
100
+ "check": "✅",
101
+ ":check:": "✅",
102
+ "checkmark": "✅",
103
+ ":checkmark:": "✅",
104
+ "white_check_mark": "✅",
105
+ ":white_check_mark:": "✅",
106
+ "eyes": "👀",
107
+ ":eyes:": "👀",
108
+ };
109
+
110
+ export function normalizeReactionEmoji(value: string): string {
111
+ const trimmed = value.trim();
112
+ return REACTION_EMOJI_ALIASES[trimmed.toLowerCase()] ?? trimmed;
113
+ }
114
+
115
+ export function rowToAgent(row: any): AgentCard {
116
+ return {
117
+ id: row.id,
118
+ name: row.name,
119
+ kind: row.kind ?? "provider",
120
+ label: row.label ?? undefined,
121
+ tags: parseStringArray(row.tags),
122
+ machine: row.machine ?? undefined,
123
+ rig: row.rig ?? undefined,
124
+ capabilities: parseStringArray(row.capabilities),
125
+ ready: row.ready === 1,
126
+ status: row.status,
127
+ instanceId: row.instance_id ?? undefined,
128
+ epoch: typeof row.epoch === "number" ? row.epoch : 0,
129
+ providerCapabilities: parseJson<ProviderCapabilities | undefined>(row.provider_capabilities, undefined),
130
+ context: parseJson<ContextState | undefined>(row.context_state, undefined),
131
+ meta: parseJson(row.meta, {}),
132
+ spawnedBy: row.spawned_by ?? undefined,
133
+ lastSeen: row.last_seen,
134
+ createdAt: row.created_at,
135
+ };
136
+ }
137
+
138
+ export function rowToContextSnapshot(row: any): ContextSnapshot {
139
+ const context = parseJson<ContextState>(row.context_state, {
140
+ utilization: row.utilization,
141
+ lifecycleState: row.lifecycle_state,
142
+ warmTopics: [],
143
+ activeMemories: [],
144
+ tasksSinceCompact: 0,
145
+ lastUpdatedAt: row.captured_at,
146
+ source: row.source,
147
+ confidence: row.confidence,
148
+ });
149
+ return {
150
+ id: row.id,
151
+ agentId: row.agent_id,
152
+ context,
153
+ utilization: row.utilization,
154
+ lifecycleState: row.lifecycle_state,
155
+ tokensUsed: row.tokens_used ?? undefined,
156
+ tokensMax: row.tokens_max ?? undefined,
157
+ source: row.source,
158
+ confidence: row.confidence,
159
+ capturedAt: row.captured_at,
160
+ };
161
+ }
162
+
163
+ export function rowToMessage(row: any): Message {
164
+ return {
165
+ id: row.id,
166
+ from: row.from_agent,
167
+ to: row.to_target,
168
+ kind: row.kind ?? "chat",
169
+ channel: row.channel ?? undefined,
170
+ subject: row.subject ?? undefined,
171
+ body: row.body,
172
+ threadId: row.thread_id ?? undefined,
173
+ replyTo: row.reply_to ?? undefined,
174
+ // Default (true) stays absent to match the `claimable` idiom and keep notification-free
175
+ // messages byte-identical on the wire; only an explicit notification surfaces false (#283).
176
+ replyExpected: row.reply_expected === 0 ? false : undefined,
177
+ claimable: row.claimable === 1 ? true : undefined,
178
+ claimedBy: row.claimed_by ?? undefined,
179
+ claimedAt: row.claimed_at ?? undefined,
180
+ claimExpiresAt: row.claim_expires_at ?? undefined,
181
+ idempotencyKey: row.idempotency_key ?? undefined,
182
+ deliveryStatus: row.delivery_status ?? "pending",
183
+ deliveryAttempts: row.delivery_attempts ?? 0,
184
+ deliveryLastError: row.delivery_last_error ?? undefined,
185
+ deliveryNextRetryAt: row.delivery_next_retry_at ?? undefined,
186
+ deliveryPoisonReason: row.delivery_poison_reason ?? undefined,
187
+ deliveryUpdatedAt: row.delivery_updated_at ?? undefined,
188
+ queuedAt: row.queued_at ?? undefined,
189
+ maxAgeSeconds: row.max_age_seconds ?? undefined,
190
+ resolvedToAgent: row.resolved_to_agent ?? undefined,
191
+ payload: parseJson(row.payload ?? "{}", {}),
192
+ meta: parseJson(row.meta, {}),
193
+ reactions: parseJson(row.reactions_json ?? "[]", []),
194
+ readBy: parseJson(row.read_by_agents ?? "[]", []),
195
+ createdAt: row.created_at,
196
+ occurredAt: row.occurred_at ?? undefined,
197
+ };
198
+ }
199
+
200
+ export function rowToArtifactBlob(row: any): ArtifactBlob {
201
+ return {
202
+ digest: row.digest,
203
+ storageUri: row.storage_uri,
204
+ mediaType: row.media_type,
205
+ size: row.size,
206
+ createdAt: row.created_at,
207
+ };
208
+ }
209
+
210
+ export function rowToArtifactLink(row: any): ArtifactLink {
211
+ return {
212
+ id: row.id,
213
+ artifactId: row.artifact_id,
214
+ entityType: row.entity_type,
215
+ entityId: row.entity_id,
216
+ role: row.role ?? undefined,
217
+ title: row.title ?? undefined,
218
+ createdBy: row.created_by,
219
+ createdAt: row.created_at,
220
+ };
221
+ }
222
+
223
+ export function rowToArtifact(row: any, links?: ArtifactLink[]): Artifact {
224
+ return {
225
+ id: row.id,
226
+ blobDigest: row.blob_digest,
227
+ mediaType: row.media_type,
228
+ kind: row.kind ?? "other",
229
+ filename: row.filename ?? undefined,
230
+ size: row.size,
231
+ digest: row.blob_digest,
232
+ visibility: row.visibility ?? "project",
233
+ sensitivity: row.sensitivity ?? "normal",
234
+ createdBy: row.created_by,
235
+ createdAt: row.created_at,
236
+ expiresAt: row.expires_at ?? undefined,
237
+ metadata: parseJson(row.metadata, {}),
238
+ ...(links ? { links } : {}),
239
+ url: `/api/artifacts/${encodeURIComponent(row.id)}/content`,
240
+ };
241
+ }
242
+
243
+ export function rowToMessageDeliveryAttempt(row: any): MessageDeliveryAttempt {
244
+ return {
245
+ id: row.id,
246
+ messageId: row.message_id,
247
+ agentId: row.agent_id ?? undefined,
248
+ action: row.action,
249
+ status: row.status,
250
+ error: row.error ?? undefined,
251
+ nextRetryAt: row.next_retry_at ?? undefined,
252
+ poisonReason: row.poison_reason ?? undefined,
253
+ createdAt: row.created_at,
254
+ };
255
+ }
256
+
257
+ export function rowToTask(row: any): Task {
258
+ return {
259
+ id: row.id,
260
+ source: row.source,
261
+ title: row.title,
262
+ body: row.body,
263
+ severity: row.severity as TaskSeverity,
264
+ status: row.status as TaskStatus,
265
+ target: row.target,
266
+ channel: row.channel ?? undefined,
267
+ dedupeKey: row.dedupe_key ?? undefined,
268
+ externalUrl: row.external_url ?? undefined,
269
+ occurrenceCount: row.occurrence_count,
270
+ claimedBy: row.claimed_by ?? undefined,
271
+ claimedAt: row.claimed_at ?? undefined,
272
+ claimExpiresAt: row.claim_expires_at ?? undefined,
273
+ messageId: row.message_id ?? undefined,
274
+ result: row.result ?? undefined,
275
+ metadata: parseJson(row.metadata, {}),
276
+ createdAt: row.created_at,
277
+ updatedAt: row.updated_at,
278
+ lastSeenAt: row.last_seen_at,
279
+ };
280
+ }
281
+
282
+ export function rowToTaskEvent(row: any): TaskEvent {
283
+ return {
284
+ id: row.id,
285
+ taskId: row.task_id,
286
+ source: row.source,
287
+ type: row.type,
288
+ severity: row.severity as TaskSeverity,
289
+ title: row.title,
290
+ body: row.body,
291
+ metadata: parseJson(row.metadata, {}),
292
+ createdAt: row.created_at,
293
+ };
294
+ }
295
+
296
+ export function rowToPair(row: any): PairSession {
297
+ return {
298
+ id: row.id,
299
+ requesterId: row.requester_id,
300
+ targetId: row.target_id,
301
+ status: row.status as PairStatus,
302
+ objective: row.objective ?? undefined,
303
+ createdAt: row.created_at,
304
+ updatedAt: row.updated_at,
305
+ expiresAt: row.expires_at,
306
+ acceptedAt: row.accepted_at ?? undefined,
307
+ endedAt: row.ended_at ?? undefined,
308
+ endedBy: row.ended_by ?? undefined,
309
+ lastMessageAt: row.last_message_at ?? undefined,
310
+ meta: parseJson(row.meta, {}),
311
+ };
312
+ }
313
+
314
+ export function rowToInboxThreadState(row: any): InboxThreadState {
315
+ return {
316
+ operatorId: row.operator_id,
317
+ peerId: row.peer_id,
318
+ readCursorMessageId: row.read_cursor_message_id ?? undefined,
319
+ archivedAtMessageId: row.archived_at_message_id ?? undefined,
320
+ updatedAt: row.updated_at,
321
+ };
322
+ }
323
+
324
+ export function rowToInboxDraft(row: any): InboxDraft {
325
+ return {
326
+ operatorId: row.operator_id,
327
+ peerId: row.peer_id,
328
+ body: row.body,
329
+ subject: row.subject ?? undefined,
330
+ channel: row.channel ?? undefined,
331
+ updatedAt: row.updated_at,
332
+ };
333
+ }
334
+
335
+ export function rowToChatHistoryImport(row: any, entries: ChatHistoryImportEntry[]): ChatHistoryImport {
336
+ return {
337
+ id: row.id,
338
+ targetAgentId: row.target_agent_id ?? undefined,
339
+ targetSpawnRequestId: row.target_spawn_request_id ?? undefined,
340
+ sourcePeerId: row.source_peer_id,
341
+ sourceAgentId: row.source_agent_id ?? undefined,
342
+ sourceThreadId: row.source_thread_id ?? undefined,
343
+ sourceAgentLabel: row.source_agent_label ?? undefined,
344
+ importedBy: row.imported_by,
345
+ importedAt: row.imported_at,
346
+ entries,
347
+ };
348
+ }
349
+
350
+ export function rowToChatHistoryImportEntry(row: any): ChatHistoryImportEntry {
351
+ const message = parseJson<Message>(row.message_snapshot, {} as Message);
352
+ return {
353
+ position: row.position,
354
+ originalMessageId: row.original_message_id,
355
+ originalFrom: row.original_from,
356
+ originalTo: row.original_to,
357
+ originalCreatedAt: row.original_created_at,
358
+ message,
359
+ };
360
+ }
361
+
362
+ export function rowToActivityEvent(row: any): ActivityEvent {
363
+ return {
364
+ id: row.id,
365
+ operatorId: row.operator_id ?? undefined,
366
+ clientId: row.client_id ?? undefined,
367
+ kind: row.kind,
368
+ title: row.title,
369
+ body: row.body ?? undefined,
370
+ meta: row.meta_text ?? undefined,
371
+ icon: row.icon ?? undefined,
372
+ view: row.view ?? undefined,
373
+ peer: row.peer_id ?? undefined,
374
+ messageId: row.message_id ?? undefined,
375
+ pairId: row.pair_id ?? undefined,
376
+ taskId: row.task_id ?? undefined,
377
+ agentId: row.agent_id ?? undefined,
378
+ metadata: parseJson(row.metadata, {}),
379
+ createdAt: row.created_at,
380
+ };
381
+ }
382
+
383
+
384
+ export const MSG_SELECT = `SELECT m.*, (
385
+ SELECT json_group_array(agent_id) FROM message_reads WHERE message_id = m.id
386
+ ) AS read_by_agents, (
387
+ SELECT json_group_array(json_object(
388
+ 'messageId', message_id,
389
+ 'actorId', actor_id,
390
+ 'emoji', emoji,
391
+ 'createdAt', created_at,
392
+ 'updatedAt', updated_at
393
+ )) FROM message_reactions WHERE message_id = m.id
394
+ ) AS reactions_json FROM messages m`;
395
+
396
+ // --- Agents ---
397
+
@@ -0,0 +1,160 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { randomUUID } from "node:crypto";
3
+ import { isRecord, stringValue, isMechanicalMessageKind } from "agent-relay-sdk";
4
+ import { ORCHESTRATOR_PROTOCOL_VERSION, VERSION } from "../config.ts";
5
+ import { parseJson } from "../utils";
6
+ import { isLiveIsolatedWorkspace } from "../workspace-phase";
7
+ import {
8
+ CONTRACT_REQUIREMENTS,
9
+ contractCompatibility,
10
+ parseRuntimeCapabilities,
11
+ parseRuntimeContracts,
12
+ parseRuntimePackage,
13
+ type RuntimeContracts,
14
+ } from "../contracts";
15
+ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
16
+ import { matchAgents } from "../agent-ref";
17
+ import { getDb } from "./connection.ts";
18
+ import type {
19
+ AgentCard,
20
+ ActivityEvent,
21
+ ActivityEventInput,
22
+ AgentKind,
23
+ AgentSessionGuard,
24
+ Artifact,
25
+ ArtifactBlob,
26
+ ArtifactKind,
27
+ ArtifactLink,
28
+ ArtifactSensitivity,
29
+ ArtifactVisibility,
30
+ AttachmentRef,
31
+ ChannelBinding,
32
+ ChannelBindingMode,
33
+ ChannelRouteTarget,
34
+ ChatHistoryImport,
35
+ ChatHistoryImportEntry,
36
+ ChannelSummary,
37
+ ChannelTargetHealth,
38
+ CreatePairInput,
39
+ HealthCheck,
40
+ HealthReport,
41
+ ManagedAgent,
42
+ ManagedSessionExitDiagnostics,
43
+ Message,
44
+ MessageDeliveryAttempt,
45
+ MessageDeliveryStatus,
46
+ Orchestrator,
47
+ OrchestratorHealth,
48
+ OrchestratorRuntimeInput,
49
+ OrchestratorStatus,
50
+ OrchestratorUpgradeState,
51
+ PairActionInput,
52
+ PairMessageInput,
53
+ PairSession,
54
+ PairStatus,
55
+ RegisterAgentInput,
56
+ ReplyObligation,
57
+ RegisterOrchestratorInput,
58
+ SendMessageInput,
59
+ PollQuery,
60
+ SpawnApprovalMode,
61
+ SpawnProvider,
62
+ Task,
63
+ TaskEvent,
64
+ TaskSeverity,
65
+ TaskStatus,
66
+ IntegrationEventInput,
67
+ IntegrationSummary,
68
+ IntegrationTaskStats,
69
+ InboxDraft,
70
+ InboxState,
71
+ InboxThreadState,
72
+ ContextSnapshot,
73
+ ContextState,
74
+ ProviderCapabilities,
75
+ TaskStatusInput,
76
+ WorkspaceMetadata,
77
+ WorkspaceRecord,
78
+ WorkspaceStatus,
79
+ } from "../types";
80
+
81
+ // --- Per-repo merge serialization lease (issue #157) -----------------------
82
+
83
+ export interface MergeLeaseRecord {
84
+ repoRoot: string;
85
+ workspaceId: string;
86
+ commandId?: string;
87
+ holder?: string;
88
+ acquiredAt: number;
89
+ expiresAt: number;
90
+ }
91
+
92
+ export function rowToMergeLease(row: any): MergeLeaseRecord {
93
+ return {
94
+ repoRoot: row.repo_root,
95
+ workspaceId: row.workspace_id,
96
+ commandId: row.command_id ?? undefined,
97
+ holder: row.holder ?? undefined,
98
+ acquiredAt: row.acquired_at,
99
+ expiresAt: row.expires_at,
100
+ };
101
+ }
102
+
103
+ export function getMergeLease(repoRoot: string): MergeLeaseRecord | null {
104
+ const row = getDb().query("SELECT * FROM workspace_merge_leases WHERE repo_root = ?").get(repoRoot) as any;
105
+ return row ? rowToMergeLease(row) : null;
106
+ }
107
+
108
+ export function listMergeLeases(): MergeLeaseRecord[] {
109
+ return (getDb().query("SELECT * FROM workspace_merge_leases ORDER BY acquired_at DESC").all() as any[]).map(rowToMergeLease);
110
+ }
111
+
112
+ export function releaseExpiredMergeLeases(now: number = Date.now()): string[] {
113
+ const expired = getDb().query("SELECT repo_root FROM workspace_merge_leases WHERE expires_at <= ?").all(now) as Array<{ repo_root: string }>;
114
+ if (!expired.length) return [];
115
+ getDb().query("DELETE FROM workspace_merge_leases WHERE expires_at <= ?").run(now);
116
+ return expired.map((r) => r.repo_root);
117
+ }
118
+
119
+ // Atomically acquire the per-repo merge lease. Succeeds if no live lease is held
120
+ // for the repo (or the existing one has expired). Serialized via getDb().transaction
121
+ // so two concurrent merge requests for the same repo can't both win.
122
+ export function acquireMergeLease(
123
+ repoRoot: string,
124
+ workspaceId: string,
125
+ holder?: string,
126
+ ): { ok: true; lease: MergeLeaseRecord } | { ok: false; lease: MergeLeaseRecord } {
127
+ return getDb().transaction(() => {
128
+ const now = Date.now();
129
+ const existing = getMergeLease(repoRoot);
130
+ if (existing && existing.expiresAt > now) return { ok: false as const, lease: existing };
131
+ const expiresAt = now + WORKSPACE_MERGE_LEASE_MS;
132
+ getDb().query(`
133
+ INSERT INTO workspace_merge_leases (repo_root, workspace_id, command_id, holder, acquired_at, expires_at)
134
+ VALUES (?, ?, NULL, ?, ?, ?)
135
+ ON CONFLICT(repo_root) DO UPDATE SET
136
+ workspace_id = excluded.workspace_id, command_id = NULL, holder = excluded.holder,
137
+ acquired_at = excluded.acquired_at, expires_at = excluded.expires_at
138
+ `).run(repoRoot, workspaceId, holder ?? null, now, expiresAt);
139
+ return { ok: true as const, lease: getMergeLease(repoRoot)! };
140
+ })();
141
+ }
142
+
143
+ // Attach the dispatched command id to a held lease so it can be released by
144
+ // command id when the merge settles.
145
+ export function setMergeLeaseCommand(repoRoot: string, commandId: string): void {
146
+ getDb().query("UPDATE workspace_merge_leases SET command_id = ? WHERE repo_root = ?").run(commandId, repoRoot);
147
+ }
148
+
149
+ // Release a merge lease. Guard by commandId/workspaceId when known so a stale
150
+ // release can't drop a newer lease for the same repo.
151
+ export function releaseMergeLease(opts: { repoRoot?: string; commandId?: string; workspaceId?: string }): boolean {
152
+ const where: string[] = [];
153
+ const params: string[] = [];
154
+ if (opts.repoRoot) { where.push("repo_root = ?"); params.push(opts.repoRoot); }
155
+ if (opts.commandId) { where.push("command_id = ?"); params.push(opts.commandId); }
156
+ if (opts.workspaceId) { where.push("workspace_id = ?"); params.push(opts.workspaceId); }
157
+ if (!where.length) return false;
158
+ return getDb().query(`DELETE FROM workspace_merge_leases WHERE ${where.join(" AND ")}`).run(...params).changes > 0;
159
+ }
160
+