agent-relay-server 0.33.0 → 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.
- package/package.json +1 -1
- package/src/channel-target.ts +24 -0
- package/src/db/activity.ts +194 -0
- package/src/db/agent-search.ts +174 -0
- package/src/db/agents.ts +551 -0
- package/src/db/artifacts.ts +342 -0
- package/src/db/channels.ts +576 -0
- package/src/db/connection.ts +71 -0
- package/src/db/delivery.ts +395 -0
- package/src/db/inbox.ts +249 -0
- package/src/db/index.ts +23 -0
- package/src/db/integrations.ts +339 -0
- package/src/db/mappers.ts +397 -0
- package/src/db/merge-lease.ts +160 -0
- package/src/db/message-reads.ts +304 -0
- package/src/db/messages.ts +434 -0
- package/src/db/migrations.ts +431 -0
- package/src/db/orchestrators.ts +358 -0
- package/src/db/pairs.ts +324 -0
- package/src/db/schema.ts +758 -0
- package/src/db/stats.ts +337 -0
- package/src/db/tasks.ts +407 -0
- package/src/db/workspaces.ts +440 -0
- package/src/db.ts +4 -5721
- package/src/routes/integrations.ts +6 -8
package/package.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ChannelRouteTarget } from "./types";
|
|
2
|
+
|
|
3
|
+
// Canonical parser for a channel route-target address — the `pool:` / `policy:` /
|
|
4
|
+
// `label:` / `tag:` / `cap:` / `orchestrator:` / `broadcast` wire form. Single
|
|
5
|
+
// source of truth: both the routes POST path (routeTargetFromAddress) and the db
|
|
6
|
+
// legacy-target path (routeTargetFromLegacyTarget) call this, so they can't drift.
|
|
7
|
+
//
|
|
8
|
+
// They DID drift — the db copy silently dropped `policy:`, so a `policy:`
|
|
9
|
+
// configured agent target parsed to { type: "agent", id: "policy:foo" } and never
|
|
10
|
+
// routed (surfaced in #299, fixed with the db split #298). Keep this the only
|
|
11
|
+
// place that maps an address string to a ChannelRouteTarget.
|
|
12
|
+
//
|
|
13
|
+
// Pure and non-throwing: it always returns a typed target. Callers apply their
|
|
14
|
+
// own policy on top (e.g. the routes path rejects `orchestrator:` as unsupported).
|
|
15
|
+
export function parseChannelRouteTarget(target: string): ChannelRouteTarget {
|
|
16
|
+
if (target.startsWith("pool:")) return { type: "pool", id: target.slice("pool:".length) };
|
|
17
|
+
if (target.startsWith("policy:")) return { type: "policy", id: target.slice("policy:".length) };
|
|
18
|
+
if (target.startsWith("label:")) return { type: "label", id: target.slice("label:".length) };
|
|
19
|
+
if (target.startsWith("tag:")) return { type: "tag", id: target.slice("tag:".length) };
|
|
20
|
+
if (target.startsWith("cap:")) return { type: "capability", id: target.slice("cap:".length) };
|
|
21
|
+
if (target === "broadcast") return { type: "broadcast" };
|
|
22
|
+
if (target.startsWith("orchestrator:")) return { type: "orchestrator", id: target.slice("orchestrator:".length) };
|
|
23
|
+
return { type: "agent", id: target };
|
|
24
|
+
}
|
|
@@ -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.
|