agent-relay-server 0.32.4 → 0.33.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 (125) hide show
  1. package/package.json +2 -2
  2. package/public/assets/{activity-DT1JGHnp.js → activity-B0_uE6Yh.js} +2 -2
  3. package/public/assets/{activity-DT1JGHnp.js.map → activity-B0_uE6Yh.js.map} +1 -1
  4. package/public/assets/{agent-profiles-CrMemMkZ.js → agent-profiles-Rwxrcf9F.js} +2 -2
  5. package/public/assets/{agent-profiles-CrMemMkZ.js.map → agent-profiles-Rwxrcf9F.js.map} +1 -1
  6. package/public/assets/{agents-Bl-rrgOy.js → agents-Dp1EXJc8.js} +2 -2
  7. package/public/assets/{agents-Bl-rrgOy.js.map → agents-Dp1EXJc8.js.map} +1 -1
  8. package/public/assets/{analytics-a663ak56.js → analytics-D5OT5ajj.js} +2 -2
  9. package/public/assets/{analytics-a663ak56.js.map → analytics-D5OT5ajj.js.map} +1 -1
  10. package/public/assets/automation-Dm6rXNxK.js +2 -0
  11. package/public/assets/{automation-CiaLThdO.js.map → automation-Dm6rXNxK.js.map} +1 -1
  12. package/public/assets/{branch-state-badge-D4ur3m3_.js → branch-state-badge-FX5Yww2s.js} +2 -2
  13. package/public/assets/{branch-state-badge-D4ur3m3_.js.map → branch-state-badge-FX5Yww2s.js.map} +1 -1
  14. package/public/assets/{channels-o9KLTHoK.js → channels--rdAiX17.js} +2 -2
  15. package/public/assets/{channels-o9KLTHoK.js.map → channels--rdAiX17.js.map} +1 -1
  16. package/public/assets/chat-JZAEDGfX.js +2 -0
  17. package/public/assets/chat-JZAEDGfX.js.map +1 -0
  18. package/public/assets/{connectors-CdC806mA.js → connectors-Bx4gzvNf.js} +2 -2
  19. package/public/assets/{connectors-CdC806mA.js.map → connectors-Bx4gzvNf.js.map} +1 -1
  20. package/public/assets/display-Bebqs1qu.js +3 -0
  21. package/public/assets/display-Bebqs1qu.js.map +1 -0
  22. package/public/assets/{formatted-body-impl-Ca74OAEH.js → formatted-body-impl-CVq4qHix.js} +2 -2
  23. package/public/assets/{formatted-body-impl-Ca74OAEH.js.map → formatted-body-impl-CVq4qHix.js.map} +1 -1
  24. package/public/assets/{index-C_33ymaw.js → index-BHRtR4q7.js} +8 -8
  25. package/public/assets/{index-C_33ymaw.js.map → index-BHRtR4q7.js.map} +1 -1
  26. package/public/assets/{insights-ClI68s39.js → insights-yJFgCa3o.js} +2 -2
  27. package/public/assets/{insights-ClI68s39.js.map → insights-yJFgCa3o.js.map} +1 -1
  28. package/public/assets/{integrations-1nxMizDY.js → integrations-k1HIONjo.js} +2 -2
  29. package/public/assets/{integrations-1nxMizDY.js.map → integrations-k1HIONjo.js.map} +1 -1
  30. package/public/assets/maintenance-CsoOFBXx.js +2 -0
  31. package/public/assets/{maintenance-DiFNzNPN.js.map → maintenance-CsoOFBXx.js.map} +1 -1
  32. package/public/assets/{managed-agents-Do3dKvfj.js → managed-agents-Q3HuVjGg.js} +2 -2
  33. package/public/assets/{managed-agents-Do3dKvfj.js.map → managed-agents-Q3HuVjGg.js.map} +1 -1
  34. package/public/assets/{markdown-preview-impl-CLA0J255.js → markdown-preview-impl-CnsMjrnu.js} +2 -2
  35. package/public/assets/{markdown-preview-impl-CLA0J255.js.map → markdown-preview-impl-CnsMjrnu.js.map} +1 -1
  36. package/public/assets/{memory-IjwqFzBd.js → memory-D3-K5eJS.js} +2 -2
  37. package/public/assets/{memory-IjwqFzBd.js.map → memory-D3-K5eJS.js.map} +1 -1
  38. package/public/assets/{messages-DjvWqHyn.js → messages-B4lCP5rS.js} +2 -2
  39. package/public/assets/{messages-DjvWqHyn.js.map → messages-B4lCP5rS.js.map} +1 -1
  40. package/public/assets/{orchestrators-D2IqDxDT.js → orchestrators-CRoZtLeQ.js} +2 -2
  41. package/public/assets/{orchestrators-D2IqDxDT.js.map → orchestrators-CRoZtLeQ.js.map} +1 -1
  42. package/public/assets/{overview-DKC3TbAh.js → overview-CxCU2fOF.js} +2 -2
  43. package/public/assets/{overview-DKC3TbAh.js.map → overview-CxCU2fOF.js.map} +1 -1
  44. package/public/assets/pairs-unqjPlmq.js +2 -0
  45. package/public/assets/{pairs-WpKCPE1n.js.map → pairs-unqjPlmq.js.map} +1 -1
  46. package/public/assets/{security-BF7ZtPQe.js → security-B7HhSYNy.js} +2 -2
  47. package/public/assets/{security-BF7ZtPQe.js.map → security-B7HhSYNy.js.map} +1 -1
  48. package/public/assets/{settings-CQnjrTa-.js → settings-B9NDhsAb.js} +2 -2
  49. package/public/assets/{settings-CQnjrTa-.js.map → settings-B9NDhsAb.js.map} +1 -1
  50. package/public/assets/store-DiSzYHj9.js +9 -0
  51. package/public/assets/{store-C9VcSo05.js.map → store-DiSzYHj9.js.map} +1 -1
  52. package/public/assets/{tasks-CbN_GSSb.js → tasks-CIQolvNm.js} +2 -2
  53. package/public/assets/{tasks-CbN_GSSb.js.map → tasks-CIQolvNm.js.map} +1 -1
  54. package/public/assets/{terminal-viewer-impl-BJRohThT.js → terminal-viewer-impl-DCifVqFR.js} +2 -2
  55. package/public/assets/{terminal-viewer-impl-BJRohThT.js.map → terminal-viewer-impl-DCifVqFR.js.map} +1 -1
  56. package/public/assets/{work-queue-C5xLBLmm.js → work-queue-Dr3c1V6O.js} +2 -2
  57. package/public/assets/{work-queue-C5xLBLmm.js.map → work-queue-Dr3c1V6O.js.map} +1 -1
  58. package/public/assets/{workspaces-D91H3wDX.js → workspaces-B1Jxop7h.js} +3 -3
  59. package/public/assets/{workspaces-D91H3wDX.js.map → workspaces-B1Jxop7h.js.map} +1 -1
  60. package/public/index.html +3 -3
  61. package/runner/src/adapter.ts +1 -1
  62. package/src/agent-lifecycle-events.ts +137 -0
  63. package/src/artifact-storage.ts +3 -5
  64. package/src/channel-target.ts +24 -0
  65. package/src/cli/_shared.ts +80 -0
  66. package/src/cli/agent-detect.ts +188 -0
  67. package/src/cli/agent-meta.ts +95 -0
  68. package/src/cli/context-probe.ts +88 -0
  69. package/src/cli/daemon.ts +111 -0
  70. package/src/cli/dev.ts +173 -0
  71. package/src/cli/index.ts +361 -0
  72. package/src/cli/introspect.ts +73 -0
  73. package/src/cli/memory.ts +37 -0
  74. package/src/cli/message.ts +201 -0
  75. package/src/cli/orchestrator.ts +227 -0
  76. package/src/cli/pair.ts +125 -0
  77. package/src/cli/provider.ts +209 -0
  78. package/src/cli/recipe.ts +110 -0
  79. package/src/cli/reply.ts +141 -0
  80. package/src/cli/setup.ts +57 -0
  81. package/src/cli/steward.ts +59 -0
  82. package/src/cli/token.ts +81 -0
  83. package/src/cli/upgrade.ts +193 -0
  84. package/src/cli/workspace.ts +215 -0
  85. package/src/cli.ts +4 -2718
  86. package/src/config-store.ts +10 -6
  87. package/src/db/activity.ts +194 -0
  88. package/src/db/agent-search.ts +174 -0
  89. package/src/db/agents.ts +551 -0
  90. package/src/db/artifacts.ts +342 -0
  91. package/src/db/channels.ts +576 -0
  92. package/src/db/connection.ts +71 -0
  93. package/src/db/delivery.ts +395 -0
  94. package/src/db/inbox.ts +249 -0
  95. package/src/db/index.ts +23 -0
  96. package/src/db/integrations.ts +339 -0
  97. package/src/db/mappers.ts +397 -0
  98. package/src/db/merge-lease.ts +160 -0
  99. package/src/db/message-reads.ts +304 -0
  100. package/src/db/messages.ts +434 -0
  101. package/src/db/migrations.ts +431 -0
  102. package/src/db/orchestrators.ts +358 -0
  103. package/src/db/pairs.ts +324 -0
  104. package/src/db/schema.ts +758 -0
  105. package/src/db/stats.ts +337 -0
  106. package/src/db/tasks.ts +407 -0
  107. package/src/db/workspaces.ts +440 -0
  108. package/src/db.ts +4 -5721
  109. package/src/maintenance.ts +4 -0
  110. package/src/mcp-errors.ts +7 -0
  111. package/src/mcp.ts +32 -34
  112. package/src/routes/agents-spawn.ts +9 -1
  113. package/src/routes/agents.ts +5 -0
  114. package/src/routes/commands.ts +15 -0
  115. package/src/routes/integrations.ts +6 -8
  116. package/src/spawn-targets.ts +159 -0
  117. package/src/utils.ts +16 -1
  118. package/public/assets/automation-CiaLThdO.js +0 -2
  119. package/public/assets/chat-5hvHZcAe.js +0 -2
  120. package/public/assets/chat-5hvHZcAe.js.map +0 -1
  121. package/public/assets/display-JI19Vc7L.js +0 -3
  122. package/public/assets/display-JI19Vc7L.js.map +0 -1
  123. package/public/assets/maintenance-DiFNzNPN.js +0 -2
  124. package/public/assets/pairs-WpKCPE1n.js +0 -2
  125. package/public/assets/store-C9VcSo05.js +0 -9
@@ -469,17 +469,21 @@ function validateInsightsConfig(value: unknown): InsightsConfig {
469
469
  const NOTIFICATIONS_CONFIG_DEFAULTS: NotificationsConfig = {
470
470
  enabled: true,
471
471
  branchLanded: true,
472
+ agentReady: true,
473
+ agentExited: true,
474
+ agentSpawnFailed: true,
472
475
  };
473
476
 
474
477
  function validateNotificationsConfig(value: unknown): NotificationsConfig {
475
478
  if (!isRecord(value)) throw new ValidationError("notifications config value must be an object");
479
+ const bool = (key: keyof NotificationsConfig): boolean =>
480
+ value[key] === undefined ? NOTIFICATIONS_CONFIG_DEFAULTS[key] : cleanBoolean(value[key], key);
476
481
  return {
477
- enabled: value.enabled === undefined
478
- ? NOTIFICATIONS_CONFIG_DEFAULTS.enabled
479
- : cleanBoolean(value.enabled, "enabled"),
480
- branchLanded: value.branchLanded === undefined
481
- ? NOTIFICATIONS_CONFIG_DEFAULTS.branchLanded
482
- : cleanBoolean(value.branchLanded, "branchLanded"),
482
+ enabled: bool("enabled"),
483
+ branchLanded: bool("branchLanded"),
484
+ agentReady: bool("agentReady"),
485
+ agentExited: bool("agentExited"),
486
+ agentSpawnFailed: bool("agentSpawnFailed"),
483
487
  };
484
488
  }
485
489
 
@@ -0,0 +1,194 @@
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 { rowToActivityEvent } from "./mappers.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 function listActivityEvents(input: {
83
+ operatorId?: string;
84
+ agentId?: string;
85
+ limit?: number;
86
+ since?: number;
87
+ } = {}): ActivityEvent[] {
88
+ const conditions: string[] = [];
89
+ const params: any[] = [];
90
+ if (input.operatorId) {
91
+ conditions.push("operator_id = ?");
92
+ params.push(input.operatorId);
93
+ }
94
+ if (input.agentId) {
95
+ conditions.push("agent_id = ?");
96
+ params.push(input.agentId);
97
+ }
98
+ if (input.since !== undefined) {
99
+ conditions.push("created_at >= ?");
100
+ params.push(input.since);
101
+ }
102
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
103
+ params.push(input.limit ?? 200);
104
+ return (getDb().query(
105
+ `SELECT * FROM activity_events ${where} ORDER BY created_at DESC, id DESC LIMIT ?`,
106
+ ).all(...params) as any[]).map(rowToActivityEvent);
107
+ }
108
+
109
+ export interface AgentTimelineEntry {
110
+ ts: number;
111
+ source: "activity" | "command" | "delivery";
112
+ kind: string;
113
+ title: string;
114
+ detail?: string;
115
+ metadata?: Record<string, unknown>;
116
+ }
117
+
118
+ // One unified, newest-first stream of everything that happened to an agent —
119
+ // lifecycle transitions, liveness mismatches, commands + outcomes, and delivery
120
+ // attempts — joined by agentId and (for managed agents) policyName. Backs the
121
+ // agent-drawer timeline without new write paths.
122
+ export function getAgentTimeline(agentId: string, opts: { limit?: number; since?: number } = {}): AgentTimelineEntry[] {
123
+ const limit = opts.limit ?? 200;
124
+ const since = opts.since;
125
+ const entries: AgentTimelineEntry[] = [];
126
+
127
+ for (const ev of listActivityEvents({ agentId, limit, since })) {
128
+ entries.push({ ts: ev.createdAt, source: "activity", kind: ev.kind, title: ev.title, detail: ev.body, metadata: ev.metadata });
129
+ }
130
+
131
+ // Managed agents: include policy-scoped lifecycle transitions recorded before
132
+ // the agent registered (agent_id null), correlated via metadata.policyName.
133
+ const managed = getDb().query("SELECT policy_name FROM managed_agent_state WHERE agent_id = ? LIMIT 1").get(agentId) as { policy_name?: string } | undefined;
134
+ if (managed?.policy_name) {
135
+ const rows = (since !== undefined
136
+ ? getDb().query("SELECT * FROM activity_events WHERE agent_id IS NULL AND json_extract(metadata, '$.policyName') = ? AND created_at >= ? ORDER BY created_at DESC LIMIT ?").all(managed.policy_name, since, limit)
137
+ : getDb().query("SELECT * FROM activity_events WHERE agent_id IS NULL AND json_extract(metadata, '$.policyName') = ? ORDER BY created_at DESC LIMIT ?").all(managed.policy_name, limit)) as any[];
138
+ for (const ev of rows.map(rowToActivityEvent)) {
139
+ entries.push({ ts: ev.createdAt, source: "activity", kind: ev.kind, title: ev.title, detail: ev.body, metadata: ev.metadata });
140
+ }
141
+ }
142
+
143
+ // Commands targeting this agent (spawn/shutdown/restart/compact) with outcome.
144
+ const cmds = (since !== undefined
145
+ ? getDb().query("SELECT id, type, status, result, correlation_id, created_at, updated_at FROM commands WHERE target = ? AND updated_at >= ? ORDER BY updated_at DESC LIMIT ?").all(agentId, since, limit)
146
+ : getDb().query("SELECT id, type, status, result, correlation_id, created_at, updated_at FROM commands WHERE target = ? ORDER BY updated_at DESC LIMIT ?").all(agentId, limit)) as any[];
147
+ for (const c of cmds) {
148
+ entries.push({ ts: c.updated_at ?? c.created_at, source: "command", kind: c.type, title: `${c.type} · ${c.status}`, detail: c.result ?? undefined, metadata: { id: c.id, status: c.status, correlationId: c.correlation_id ?? undefined } });
149
+ }
150
+
151
+ // Delivery attempts involving this agent (failures, retries, poison).
152
+ const attempts = (since !== undefined
153
+ ? getDb().query("SELECT * FROM message_delivery_attempts WHERE agent_id = ? AND created_at >= ? ORDER BY created_at DESC LIMIT ?").all(agentId, since, limit)
154
+ : getDb().query("SELECT * FROM message_delivery_attempts WHERE agent_id = ? ORDER BY created_at DESC LIMIT ?").all(agentId, limit)) as any[];
155
+ for (const row of attempts) {
156
+ entries.push({ ts: row.created_at, source: "delivery", kind: `delivery.${row.action}`, title: `delivery ${row.status}`, detail: row.error ?? undefined, metadata: { messageId: row.message_id, status: row.status, poisonReason: row.poison_reason ?? undefined } });
157
+ }
158
+
159
+ entries.sort((a, b) => b.ts - a.ts);
160
+ return entries.slice(0, limit);
161
+ }
162
+
163
+ export function createActivityEvent(input: ActivityEventInput): ActivityEvent {
164
+ if (input.clientId) {
165
+ const existing = getDb().query("SELECT * FROM activity_events WHERE client_id = ?").get(input.clientId) as any | undefined;
166
+ if (existing) return rowToActivityEvent(existing);
167
+ }
168
+ const now = Date.now();
169
+ const result = getDb().query(`
170
+ INSERT INTO activity_events (
171
+ operator_id, client_id, kind, title, body, meta_text, icon, view, peer_id,
172
+ message_id, pair_id, task_id, agent_id, metadata, created_at
173
+ )
174
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
175
+ `).run(
176
+ input.operatorId ?? null,
177
+ input.clientId ?? null,
178
+ input.kind,
179
+ input.title,
180
+ input.body ?? null,
181
+ input.meta ?? null,
182
+ input.icon ?? null,
183
+ input.view ?? null,
184
+ input.peer ?? null,
185
+ input.messageId ?? null,
186
+ input.pairId ?? null,
187
+ input.taskId ?? null,
188
+ input.agentId ?? null,
189
+ JSON.stringify(input.metadata ?? {}),
190
+ now,
191
+ );
192
+ return rowToActivityEvent(getDb().query("SELECT * FROM activity_events WHERE id = ?").get(Number(result.lastInsertRowid)));
193
+ }
194
+
@@ -0,0 +1,174 @@
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 { rowToAgent } from "./mappers.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 interface AgentSearchFilter {
83
+ /** Substring match on label OR name (case-insensitive). */
84
+ label?: string;
85
+ /** One or more capabilities (OR within the field), via json_each(capabilities). */
86
+ capability?: string | string[];
87
+ /** One or more tags (OR within the field), via json_each(tags). */
88
+ tag?: string | string[];
89
+ /** Real model provider — providerCapabilities.model.provider (e.g. "claude", "codex"). */
90
+ provider?: string | string[];
91
+ machine?: string | string[];
92
+ /** provider/orchestrator/channel/etc. */
93
+ kind?: string | string[];
94
+ /** Specific status(es), the "running" shorthand (live + ready + fresh), or "all". */
95
+ status?: string | string[];
96
+ /** Exact parent agent id — backs the `spawnedBy:` / `spawnedBy:me` fleet filter (#221). */
97
+ spawnedBy?: string;
98
+ }
99
+
100
+ export interface AgentSearchSort {
101
+ sortBy?: "lastActive" | "created" | "label";
102
+ order?: "asc" | "desc";
103
+ limit?: number;
104
+ }
105
+
106
+ export const AGENT_SEARCH_MAX_LIMIT = 200;
107
+
108
+ export function searchValues(value: string | string[] | undefined): string[] {
109
+ if (value == null) return [];
110
+ return (Array.isArray(value) ? value : [value]).filter((v): v is string => typeof v === "string" && v.length > 0);
111
+ }
112
+
113
+ // Unified fleet search: all filters optional and AND-combined; array values OR within a
114
+ // field. One parameterized query (no JS post-filter, no injection). Backs relay_find_agents
115
+ // (#219) and the running-only relay_agent_status default (#218). "running" mirrors the
116
+ // canonical liveness predicate (see getDb().ts delivery/steward checks + isAgentOnline).
117
+ export function searchAgents(filter: AgentSearchFilter = {}, sort: AgentSearchSort = {}): AgentCard[] {
118
+ const clauses: string[] = ["1=1"];
119
+ const params: any[] = [];
120
+ const inList = (column: string, values: string[]) => {
121
+ clauses.push(`${column} IN (${values.map(() => "?").join(",")})`);
122
+ params.push(...values);
123
+ };
124
+
125
+ if (filter.label) {
126
+ clauses.push("(label LIKE ? OR name LIKE ?)");
127
+ params.push(`%${filter.label}%`, `%${filter.label}%`);
128
+ }
129
+ const caps = searchValues(filter.capability);
130
+ if (caps.length) {
131
+ clauses.push(`EXISTS (SELECT 1 FROM json_each(capabilities) WHERE value IN (${caps.map(() => "?").join(",")}))`);
132
+ params.push(...caps);
133
+ }
134
+ const tags = searchValues(filter.tag);
135
+ if (tags.length) {
136
+ clauses.push(`EXISTS (SELECT 1 FROM json_each(tags) WHERE value IN (${tags.map(() => "?").join(",")}))`);
137
+ params.push(...tags);
138
+ }
139
+ const providers = searchValues(filter.provider);
140
+ if (providers.length) {
141
+ clauses.push(`json_extract(provider_capabilities, '$.model.provider') IN (${providers.map(() => "?").join(",")})`);
142
+ params.push(...providers);
143
+ }
144
+ const machines = searchValues(filter.machine);
145
+ if (machines.length) inList("machine", machines);
146
+ const kinds = searchValues(filter.kind);
147
+ if (kinds.length) inList("kind", kinds);
148
+ if (filter.spawnedBy) {
149
+ clauses.push("spawned_by = ?");
150
+ params.push(filter.spawnedBy);
151
+ }
152
+
153
+ const statuses = searchValues(filter.status);
154
+ if (!statuses.includes("all")) {
155
+ if (statuses.includes("running")) {
156
+ clauses.push("status NOT IN ('offline', 'stale') AND ready = 1 AND last_seen > ?");
157
+ params.push(Date.now() - STALE_TTL_MS);
158
+ }
159
+ const explicit = statuses.filter((s) => s !== "running" && s !== "all");
160
+ if (explicit.length) inList("status", explicit);
161
+ }
162
+
163
+ const sortColumn = sort.sortBy === "created" ? "created_at" : sort.sortBy === "label" ? "label COLLATE NOCASE" : "last_seen";
164
+ const order = sort.order === "asc" ? "ASC" : "DESC";
165
+ const limit = Math.max(1, Math.min(sort.limit ?? 50, AGENT_SEARCH_MAX_LIMIT));
166
+ params.push(limit);
167
+
168
+ const sql = `SELECT * FROM agents WHERE ${clauses.join(" AND ")} ORDER BY ${sortColumn} ${order} LIMIT ?`;
169
+ return (getDb().query(sql).all(...params) as any[]).map(rowToAgent);
170
+ }
171
+
172
+ // Count of a parent's currently-LIVE children — the spawn quota numerator (#221). "Live"
173
+ // mirrors the running predicate (not offline/stale, ready, fresh); a child that exits frees
174
+ // a slot. Uncapped COUNT (unlike searchAgents' LIMIT) so the quota is exact at any fleet size.