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
|
@@ -0,0 +1,395 @@
|
|
|
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 { getAgent, listAgents } from "./agents.ts";
|
|
18
|
+
import { channelProviderForAgent } from "./channels.ts";
|
|
19
|
+
import { getDb } from "./connection.ts";
|
|
20
|
+
import { MSG_SELECT, rowToMessage, rowToMessageDeliveryAttempt } from "./mappers.ts";
|
|
21
|
+
import { getMessage } from "./messages.ts";
|
|
22
|
+
import type {
|
|
23
|
+
AgentCard,
|
|
24
|
+
ActivityEvent,
|
|
25
|
+
ActivityEventInput,
|
|
26
|
+
AgentKind,
|
|
27
|
+
AgentSessionGuard,
|
|
28
|
+
Artifact,
|
|
29
|
+
ArtifactBlob,
|
|
30
|
+
ArtifactKind,
|
|
31
|
+
ArtifactLink,
|
|
32
|
+
ArtifactSensitivity,
|
|
33
|
+
ArtifactVisibility,
|
|
34
|
+
AttachmentRef,
|
|
35
|
+
ChannelBinding,
|
|
36
|
+
ChannelBindingMode,
|
|
37
|
+
ChannelRouteTarget,
|
|
38
|
+
ChatHistoryImport,
|
|
39
|
+
ChatHistoryImportEntry,
|
|
40
|
+
ChannelSummary,
|
|
41
|
+
ChannelTargetHealth,
|
|
42
|
+
CreatePairInput,
|
|
43
|
+
HealthCheck,
|
|
44
|
+
HealthReport,
|
|
45
|
+
ManagedAgent,
|
|
46
|
+
ManagedSessionExitDiagnostics,
|
|
47
|
+
Message,
|
|
48
|
+
MessageDeliveryAttempt,
|
|
49
|
+
MessageDeliveryStatus,
|
|
50
|
+
Orchestrator,
|
|
51
|
+
OrchestratorHealth,
|
|
52
|
+
OrchestratorRuntimeInput,
|
|
53
|
+
OrchestratorStatus,
|
|
54
|
+
OrchestratorUpgradeState,
|
|
55
|
+
PairActionInput,
|
|
56
|
+
PairMessageInput,
|
|
57
|
+
PairSession,
|
|
58
|
+
PairStatus,
|
|
59
|
+
RegisterAgentInput,
|
|
60
|
+
ReplyObligation,
|
|
61
|
+
RegisterOrchestratorInput,
|
|
62
|
+
SendMessageInput,
|
|
63
|
+
PollQuery,
|
|
64
|
+
SpawnApprovalMode,
|
|
65
|
+
SpawnProvider,
|
|
66
|
+
Task,
|
|
67
|
+
TaskEvent,
|
|
68
|
+
TaskSeverity,
|
|
69
|
+
TaskStatus,
|
|
70
|
+
IntegrationEventInput,
|
|
71
|
+
IntegrationSummary,
|
|
72
|
+
IntegrationTaskStats,
|
|
73
|
+
InboxDraft,
|
|
74
|
+
InboxState,
|
|
75
|
+
InboxThreadState,
|
|
76
|
+
ContextSnapshot,
|
|
77
|
+
ContextState,
|
|
78
|
+
ProviderCapabilities,
|
|
79
|
+
TaskStatusInput,
|
|
80
|
+
WorkspaceMetadata,
|
|
81
|
+
WorkspaceRecord,
|
|
82
|
+
WorkspaceStatus,
|
|
83
|
+
} from "../types";
|
|
84
|
+
|
|
85
|
+
export function queueDepthLimit(target: string): number {
|
|
86
|
+
const row = getDb().query("SELECT value FROM config WHERE namespace = 'system' AND key = 'message-queue'").get() as { value?: string } | undefined;
|
|
87
|
+
const parsed = row?.value ? parseJson<Record<string, unknown>>(row.value, {}) : {};
|
|
88
|
+
const perTarget = parsed?.maxDepthPerTarget;
|
|
89
|
+
if (typeof perTarget === "number" && Number.isSafeInteger(perTarget) && perTarget > 0) return perTarget;
|
|
90
|
+
const targetLimits = parsed?.targetLimits;
|
|
91
|
+
if (targetLimits && typeof targetLimits === "object" && !Array.isArray(targetLimits)) {
|
|
92
|
+
const value = (targetLimits as Record<string, unknown>)[target];
|
|
93
|
+
if (typeof value === "number" && Number.isSafeInteger(value) && value > 0) return value;
|
|
94
|
+
}
|
|
95
|
+
return 100;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function enforceQueueLimit(target: string): void {
|
|
99
|
+
const limit = queueDepthLimit(target);
|
|
100
|
+
const rows = getDb().query(`
|
|
101
|
+
SELECT id FROM messages
|
|
102
|
+
WHERE to_target = ? AND delivery_status = 'queued'
|
|
103
|
+
ORDER BY queued_at DESC, id DESC
|
|
104
|
+
LIMIT -1 OFFSET ?
|
|
105
|
+
`).all(target, limit) as Array<{ id: number }>;
|
|
106
|
+
if (rows.length === 0) return;
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
getDb().transaction(() => {
|
|
109
|
+
for (const row of rows) {
|
|
110
|
+
insertMessageDeliveryAttempt(row.id, {
|
|
111
|
+
action: "mark-dead",
|
|
112
|
+
status: "dead",
|
|
113
|
+
error: "queue depth limit exceeded",
|
|
114
|
+
poisonReason: "queue depth limit exceeded",
|
|
115
|
+
}, now);
|
|
116
|
+
setMessageDeliveryState(row.id, {
|
|
117
|
+
status: "dead",
|
|
118
|
+
error: "queue depth limit exceeded",
|
|
119
|
+
poisonReason: "queue depth limit exceeded",
|
|
120
|
+
nextRetryAt: null,
|
|
121
|
+
}, now);
|
|
122
|
+
}
|
|
123
|
+
})();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function isDeliveryAgent(agent: AgentCard): boolean {
|
|
127
|
+
return agent.status !== "offline" &&
|
|
128
|
+
agent.id !== "user" &&
|
|
129
|
+
agent.id !== "system" &&
|
|
130
|
+
agent.kind !== "channel" &&
|
|
131
|
+
agent.meta?.kind !== "channel";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function isChannelAgentId(agentId: string): boolean {
|
|
135
|
+
const agent = getAgent(agentId);
|
|
136
|
+
return Boolean(agent && (
|
|
137
|
+
agent.kind === "channel" ||
|
|
138
|
+
agent.meta?.kind === "channel" ||
|
|
139
|
+
agent.tags.includes("channel") ||
|
|
140
|
+
agent.capabilities.includes("channel")
|
|
141
|
+
));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function legacyChannelTargets(agent: AgentCard | null | undefined): string[] {
|
|
145
|
+
if (!agent || !isChannelAgentId(agent.id)) return [];
|
|
146
|
+
const aliases = new Set<string>();
|
|
147
|
+
const provider = channelProviderForAgent(agent);
|
|
148
|
+
if (provider && provider !== "custom") aliases.add(provider);
|
|
149
|
+
const channelType = stringValue(agent.meta?.channelType);
|
|
150
|
+
if (channelType) aliases.add(channelType);
|
|
151
|
+
const transport = stringValue(agent.meta?.transport);
|
|
152
|
+
if (transport) aliases.add(transport);
|
|
153
|
+
const providerTag = agent.tags.find((tag) => tag.startsWith("channel:"))?.slice("channel:".length);
|
|
154
|
+
if (providerTag) aliases.add(providerTag);
|
|
155
|
+
aliases.delete(agent.id);
|
|
156
|
+
return [...aliases];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function matchingDeliveryAgents(target: string): AgentCard[] {
|
|
160
|
+
if (!target) return [];
|
|
161
|
+
const candidates = listAgents().filter(isDeliveryAgent);
|
|
162
|
+
if (target === "broadcast") return candidates;
|
|
163
|
+
const direct = getAgent(target);
|
|
164
|
+
if (direct) return isDeliveryAgent(direct) ? [direct] : [];
|
|
165
|
+
if (target.startsWith("tag:")) {
|
|
166
|
+
const tag = target.slice(4);
|
|
167
|
+
return candidates.filter((agent) => agent.tags.includes(tag));
|
|
168
|
+
}
|
|
169
|
+
if (target.startsWith("cap:")) {
|
|
170
|
+
const cap = target.slice(4);
|
|
171
|
+
return candidates.filter((agent) => agent.capabilities.includes(cap));
|
|
172
|
+
}
|
|
173
|
+
if (target.startsWith("label:")) {
|
|
174
|
+
const label = target.slice(6);
|
|
175
|
+
return candidates.filter((agent) => agent.label === label);
|
|
176
|
+
}
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
export function getMessageDeliveryAttempts(messageId: number, limit = 50): MessageDeliveryAttempt[] {
|
|
182
|
+
const safeLimit = Math.min(Math.max(limit, 1), 200);
|
|
183
|
+
return (getDb().query(`
|
|
184
|
+
SELECT * FROM message_delivery_attempts
|
|
185
|
+
WHERE message_id = ?
|
|
186
|
+
ORDER BY created_at DESC, id DESC
|
|
187
|
+
LIMIT ?
|
|
188
|
+
`).all(messageId, safeLimit) as any[]).map(rowToMessageDeliveryAttempt);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function insertMessageDeliveryAttempt(
|
|
192
|
+
messageId: number,
|
|
193
|
+
input: {
|
|
194
|
+
agentId?: string;
|
|
195
|
+
action?: MessageDeliveryAttempt["action"];
|
|
196
|
+
status: MessageDeliveryStatus;
|
|
197
|
+
error?: string;
|
|
198
|
+
nextRetryAt?: number;
|
|
199
|
+
poisonReason?: string;
|
|
200
|
+
},
|
|
201
|
+
now: number,
|
|
202
|
+
): void {
|
|
203
|
+
getDb().query(`
|
|
204
|
+
INSERT INTO message_delivery_attempts (message_id, agent_id, action, status, error, next_retry_at, poison_reason, created_at)
|
|
205
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
206
|
+
`).run(
|
|
207
|
+
messageId,
|
|
208
|
+
input.agentId ?? null,
|
|
209
|
+
input.action ?? "attempt",
|
|
210
|
+
input.status,
|
|
211
|
+
input.error ?? null,
|
|
212
|
+
input.nextRetryAt ?? null,
|
|
213
|
+
input.poisonReason ?? null,
|
|
214
|
+
now,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function setMessageDeliveryState(
|
|
219
|
+
messageId: number,
|
|
220
|
+
input: {
|
|
221
|
+
status: MessageDeliveryStatus;
|
|
222
|
+
error?: string | null;
|
|
223
|
+
nextRetryAt?: number | null;
|
|
224
|
+
poisonReason?: string | null;
|
|
225
|
+
incrementAttempts?: boolean;
|
|
226
|
+
},
|
|
227
|
+
now: number,
|
|
228
|
+
): boolean {
|
|
229
|
+
return getDb().query(`
|
|
230
|
+
UPDATE messages
|
|
231
|
+
SET delivery_status = ?,
|
|
232
|
+
delivery_attempts = delivery_attempts + ?,
|
|
233
|
+
delivery_last_error = ?,
|
|
234
|
+
delivery_next_retry_at = ?,
|
|
235
|
+
delivery_poison_reason = ?,
|
|
236
|
+
delivery_updated_at = ?
|
|
237
|
+
WHERE id = ?
|
|
238
|
+
`).run(
|
|
239
|
+
input.status,
|
|
240
|
+
input.incrementAttempts ? 1 : 0,
|
|
241
|
+
input.error ?? null,
|
|
242
|
+
input.nextRetryAt ?? null,
|
|
243
|
+
input.poisonReason ?? null,
|
|
244
|
+
now,
|
|
245
|
+
messageId,
|
|
246
|
+
).changes > 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function recordMessageDeliveryAttempt(messageId: number, input: {
|
|
250
|
+
agentId?: string;
|
|
251
|
+
status: MessageDeliveryStatus;
|
|
252
|
+
error?: string;
|
|
253
|
+
nextRetryAt?: number;
|
|
254
|
+
poisonReason?: string;
|
|
255
|
+
}): { ok: boolean; error?: string; message?: Message; attempts?: MessageDeliveryAttempt[] } {
|
|
256
|
+
if (!getMessage(messageId)) return { ok: false, error: "message not found" };
|
|
257
|
+
const now = Date.now();
|
|
258
|
+
getDb().transaction(() => {
|
|
259
|
+
insertMessageDeliveryAttempt(messageId, { ...input, action: "attempt" }, now);
|
|
260
|
+
setMessageDeliveryState(messageId, {
|
|
261
|
+
status: input.status,
|
|
262
|
+
error: input.status === "delivered" ? null : input.error ?? null,
|
|
263
|
+
nextRetryAt: input.status === "delivered" ? null : input.nextRetryAt ?? null,
|
|
264
|
+
poisonReason: input.status === "dead" ? input.poisonReason ?? input.error ?? null : null,
|
|
265
|
+
incrementAttempts: true,
|
|
266
|
+
}, now);
|
|
267
|
+
})();
|
|
268
|
+
return { ok: true, message: getMessage(messageId)!, attempts: getMessageDeliveryAttempts(messageId) };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function applyMessageDeliveryAction(messageId: number, input: {
|
|
272
|
+
action: "retry-now" | "mark-dead" | "clear";
|
|
273
|
+
reason?: string;
|
|
274
|
+
agentId?: string;
|
|
275
|
+
}): { ok: boolean; error?: string; message?: Message; attempts?: MessageDeliveryAttempt[] } {
|
|
276
|
+
if (!getMessage(messageId)) return { ok: false, error: "message not found" };
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
const status: MessageDeliveryStatus =
|
|
279
|
+
input.action === "retry-now" ? "pending" :
|
|
280
|
+
input.action === "mark-dead" ? "dead" :
|
|
281
|
+
"delivered";
|
|
282
|
+
const reason = input.reason?.trim() || undefined;
|
|
283
|
+
getDb().transaction(() => {
|
|
284
|
+
insertMessageDeliveryAttempt(messageId, {
|
|
285
|
+
agentId: input.agentId,
|
|
286
|
+
action: input.action,
|
|
287
|
+
status,
|
|
288
|
+
error: input.action === "mark-dead" ? reason : undefined,
|
|
289
|
+
poisonReason: input.action === "mark-dead" ? reason : undefined,
|
|
290
|
+
}, now);
|
|
291
|
+
setMessageDeliveryState(messageId, {
|
|
292
|
+
status,
|
|
293
|
+
error: input.action === "mark-dead" ? reason ?? "marked dead" : null,
|
|
294
|
+
nextRetryAt: null,
|
|
295
|
+
poisonReason: input.action === "mark-dead" ? reason ?? "marked dead" : null,
|
|
296
|
+
incrementAttempts: false,
|
|
297
|
+
}, now);
|
|
298
|
+
})();
|
|
299
|
+
return { ok: true, message: getMessage(messageId)!, attempts: getMessageDeliveryAttempts(messageId) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function getMessageDeliveryStatus(id: number): Pick<Message, "id" | "to" | "deliveryStatus" | "deliveryAttempts" | "deliveryLastError" | "deliveryNextRetryAt" | "deliveryPoisonReason" | "deliveryUpdatedAt" | "queuedAt" | "maxAgeSeconds" | "resolvedToAgent"> & { attempts: MessageDeliveryAttempt[] } | null {
|
|
303
|
+
const message = getMessage(id);
|
|
304
|
+
if (!message) return null;
|
|
305
|
+
return {
|
|
306
|
+
id: message.id,
|
|
307
|
+
to: message.to,
|
|
308
|
+
deliveryStatus: message.deliveryStatus,
|
|
309
|
+
deliveryAttempts: message.deliveryAttempts,
|
|
310
|
+
deliveryLastError: message.deliveryLastError,
|
|
311
|
+
deliveryNextRetryAt: message.deliveryNextRetryAt,
|
|
312
|
+
deliveryPoisonReason: message.deliveryPoisonReason,
|
|
313
|
+
deliveryUpdatedAt: message.deliveryUpdatedAt,
|
|
314
|
+
queuedAt: message.queuedAt,
|
|
315
|
+
maxAgeSeconds: message.maxAgeSeconds,
|
|
316
|
+
resolvedToAgent: message.resolvedToAgent,
|
|
317
|
+
attempts: getMessageDeliveryAttempts(id),
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function listQueuedMessages(target: string, limit = 100): Message[] {
|
|
322
|
+
const safeLimit = Math.min(Math.max(limit, 1), 500);
|
|
323
|
+
return (getDb().query(`
|
|
324
|
+
${MSG_SELECT}
|
|
325
|
+
WHERE m.to_target = ? AND m.delivery_status = 'queued'
|
|
326
|
+
ORDER BY m.queued_at ASC, m.id ASC
|
|
327
|
+
LIMIT ?
|
|
328
|
+
`).all(target, safeLimit) as any[]).map(rowToMessage);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function resolveQueuedPolicyMessages(policyName: string, agentId: string): Message[] {
|
|
332
|
+
const target = `policy:${policyName}`;
|
|
333
|
+
const rows = getDb().query(`
|
|
334
|
+
SELECT m.id
|
|
335
|
+
FROM messages m
|
|
336
|
+
WHERE m.to_target = ?
|
|
337
|
+
AND m.delivery_status IN ('queued', 'pending')
|
|
338
|
+
AND NOT EXISTS (SELECT 1 FROM message_reads mr WHERE mr.message_id = m.id)
|
|
339
|
+
AND (
|
|
340
|
+
m.delivery_status = 'queued'
|
|
341
|
+
OR m.resolved_to_agent IS NULL
|
|
342
|
+
OR m.resolved_to_agent != ?
|
|
343
|
+
)
|
|
344
|
+
ORDER BY COALESCE(m.queued_at, m.created_at) ASC, m.id ASC
|
|
345
|
+
`).all(target, agentId) as Array<{ id: number }>;
|
|
346
|
+
if (rows.length === 0) return [];
|
|
347
|
+
const ids = rows.map((row) => row.id);
|
|
348
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
349
|
+
getDb().query(`
|
|
350
|
+
UPDATE messages
|
|
351
|
+
SET delivery_status = 'pending',
|
|
352
|
+
resolved_to_agent = ?,
|
|
353
|
+
delivery_attempts = delivery_attempts + 1,
|
|
354
|
+
delivery_last_error = NULL,
|
|
355
|
+
delivery_next_retry_at = NULL,
|
|
356
|
+
delivery_poison_reason = NULL,
|
|
357
|
+
delivery_updated_at = ?
|
|
358
|
+
WHERE id IN (${placeholders})
|
|
359
|
+
`).run(agentId, Date.now(), ...ids);
|
|
360
|
+
const now = Date.now();
|
|
361
|
+
for (const id of ids) {
|
|
362
|
+
insertMessageDeliveryAttempt(id, { agentId, status: "pending" }, now);
|
|
363
|
+
}
|
|
364
|
+
return ids.map((id) => getMessage(id)).filter((message): message is Message => Boolean(message));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function expireQueuedMessages(now: number = Date.now()): Message[] {
|
|
368
|
+
const rows = getDb().query(`
|
|
369
|
+
SELECT id FROM messages
|
|
370
|
+
WHERE delivery_status = 'queued'
|
|
371
|
+
AND queued_at IS NOT NULL
|
|
372
|
+
AND coalesce(max_age_seconds, 86400) >= 0
|
|
373
|
+
AND queued_at + (coalesce(max_age_seconds, 86400) * 1000) <= ?
|
|
374
|
+
`).all(now) as Array<{ id: number }>;
|
|
375
|
+
if (rows.length === 0) return [];
|
|
376
|
+
const ids = rows.map((row) => row.id);
|
|
377
|
+
getDb().transaction(() => {
|
|
378
|
+
for (const id of ids) {
|
|
379
|
+
insertMessageDeliveryAttempt(id, {
|
|
380
|
+
action: "mark-dead",
|
|
381
|
+
status: "dead",
|
|
382
|
+
error: "queued message expired",
|
|
383
|
+
poisonReason: "queued message expired",
|
|
384
|
+
}, now);
|
|
385
|
+
setMessageDeliveryState(id, {
|
|
386
|
+
status: "dead",
|
|
387
|
+
error: "queued message expired",
|
|
388
|
+
poisonReason: "queued message expired",
|
|
389
|
+
nextRetryAt: null,
|
|
390
|
+
}, now);
|
|
391
|
+
}
|
|
392
|
+
})();
|
|
393
|
+
return ids.map((id) => getMessage(id)).filter((message): message is Message => Boolean(message));
|
|
394
|
+
}
|
|
395
|
+
|
package/src/db/inbox.ts
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
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 { ValidationError, getDb } from "./connection.ts";
|
|
18
|
+
import { rowToChatHistoryImport, rowToChatHistoryImportEntry, rowToInboxDraft, rowToInboxThreadState } from "./mappers.ts";
|
|
19
|
+
import { getMessage } from "./messages.ts";
|
|
20
|
+
import type {
|
|
21
|
+
AgentCard,
|
|
22
|
+
ActivityEvent,
|
|
23
|
+
ActivityEventInput,
|
|
24
|
+
AgentKind,
|
|
25
|
+
AgentSessionGuard,
|
|
26
|
+
Artifact,
|
|
27
|
+
ArtifactBlob,
|
|
28
|
+
ArtifactKind,
|
|
29
|
+
ArtifactLink,
|
|
30
|
+
ArtifactSensitivity,
|
|
31
|
+
ArtifactVisibility,
|
|
32
|
+
AttachmentRef,
|
|
33
|
+
ChannelBinding,
|
|
34
|
+
ChannelBindingMode,
|
|
35
|
+
ChannelRouteTarget,
|
|
36
|
+
ChatHistoryImport,
|
|
37
|
+
ChatHistoryImportEntry,
|
|
38
|
+
ChannelSummary,
|
|
39
|
+
ChannelTargetHealth,
|
|
40
|
+
CreatePairInput,
|
|
41
|
+
HealthCheck,
|
|
42
|
+
HealthReport,
|
|
43
|
+
ManagedAgent,
|
|
44
|
+
ManagedSessionExitDiagnostics,
|
|
45
|
+
Message,
|
|
46
|
+
MessageDeliveryAttempt,
|
|
47
|
+
MessageDeliveryStatus,
|
|
48
|
+
Orchestrator,
|
|
49
|
+
OrchestratorHealth,
|
|
50
|
+
OrchestratorRuntimeInput,
|
|
51
|
+
OrchestratorStatus,
|
|
52
|
+
OrchestratorUpgradeState,
|
|
53
|
+
PairActionInput,
|
|
54
|
+
PairMessageInput,
|
|
55
|
+
PairSession,
|
|
56
|
+
PairStatus,
|
|
57
|
+
RegisterAgentInput,
|
|
58
|
+
ReplyObligation,
|
|
59
|
+
RegisterOrchestratorInput,
|
|
60
|
+
SendMessageInput,
|
|
61
|
+
PollQuery,
|
|
62
|
+
SpawnApprovalMode,
|
|
63
|
+
SpawnProvider,
|
|
64
|
+
Task,
|
|
65
|
+
TaskEvent,
|
|
66
|
+
TaskSeverity,
|
|
67
|
+
TaskStatus,
|
|
68
|
+
IntegrationEventInput,
|
|
69
|
+
IntegrationSummary,
|
|
70
|
+
IntegrationTaskStats,
|
|
71
|
+
InboxDraft,
|
|
72
|
+
InboxState,
|
|
73
|
+
InboxThreadState,
|
|
74
|
+
ContextSnapshot,
|
|
75
|
+
ContextState,
|
|
76
|
+
ProviderCapabilities,
|
|
77
|
+
TaskStatusInput,
|
|
78
|
+
WorkspaceMetadata,
|
|
79
|
+
WorkspaceRecord,
|
|
80
|
+
WorkspaceStatus,
|
|
81
|
+
} from "../types";
|
|
82
|
+
|
|
83
|
+
export function getInboxState(operatorId: string): InboxState {
|
|
84
|
+
const threads = (getDb().query(
|
|
85
|
+
"SELECT * FROM inbox_thread_state WHERE operator_id = ? ORDER BY updated_at DESC",
|
|
86
|
+
).all(operatorId) as any[]).map(rowToInboxThreadState);
|
|
87
|
+
const drafts = (getDb().query(
|
|
88
|
+
"SELECT * FROM inbox_drafts WHERE operator_id = ? ORDER BY updated_at DESC",
|
|
89
|
+
).all(operatorId) as any[]).map(rowToInboxDraft);
|
|
90
|
+
return { operatorId, threads, drafts };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function setInboxThreadState(input: {
|
|
94
|
+
operatorId: string;
|
|
95
|
+
peerId: string;
|
|
96
|
+
readCursorMessageId?: number | null;
|
|
97
|
+
archivedAtMessageId?: number | null;
|
|
98
|
+
}): InboxThreadState {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const current = getDb().query(
|
|
101
|
+
"SELECT * FROM inbox_thread_state WHERE operator_id = ? AND peer_id = ?",
|
|
102
|
+
).get(input.operatorId, input.peerId) as any | undefined;
|
|
103
|
+
|
|
104
|
+
const readCursorMessageId = Object.prototype.hasOwnProperty.call(input, "readCursorMessageId")
|
|
105
|
+
? input.readCursorMessageId ?? null
|
|
106
|
+
: current?.read_cursor_message_id ?? null;
|
|
107
|
+
const archivedAtMessageId = Object.prototype.hasOwnProperty.call(input, "archivedAtMessageId")
|
|
108
|
+
? input.archivedAtMessageId ?? null
|
|
109
|
+
: current?.archived_at_message_id ?? null;
|
|
110
|
+
|
|
111
|
+
getDb().query(`
|
|
112
|
+
INSERT INTO inbox_thread_state (operator_id, peer_id, read_cursor_message_id, archived_at_message_id, updated_at)
|
|
113
|
+
VALUES (?, ?, ?, ?, ?)
|
|
114
|
+
ON CONFLICT(operator_id, peer_id) DO UPDATE SET
|
|
115
|
+
read_cursor_message_id = excluded.read_cursor_message_id,
|
|
116
|
+
archived_at_message_id = excluded.archived_at_message_id,
|
|
117
|
+
updated_at = excluded.updated_at
|
|
118
|
+
`).run(input.operatorId, input.peerId, readCursorMessageId, archivedAtMessageId, now);
|
|
119
|
+
|
|
120
|
+
return rowToInboxThreadState(getDb().query(
|
|
121
|
+
"SELECT * FROM inbox_thread_state WHERE operator_id = ? AND peer_id = ?",
|
|
122
|
+
).get(input.operatorId, input.peerId));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function setInboxDraft(input: {
|
|
126
|
+
operatorId: string;
|
|
127
|
+
peerId: string;
|
|
128
|
+
body: string;
|
|
129
|
+
subject?: string | null;
|
|
130
|
+
channel?: string | null;
|
|
131
|
+
}): InboxDraft {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
getDb().query(`
|
|
134
|
+
INSERT INTO inbox_drafts (operator_id, peer_id, body, subject, channel, updated_at)
|
|
135
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
136
|
+
ON CONFLICT(operator_id, peer_id) DO UPDATE SET
|
|
137
|
+
body = excluded.body,
|
|
138
|
+
subject = excluded.subject,
|
|
139
|
+
channel = excluded.channel,
|
|
140
|
+
updated_at = excluded.updated_at
|
|
141
|
+
`).run(input.operatorId, input.peerId, input.body, input.subject ?? null, input.channel ?? null, now);
|
|
142
|
+
|
|
143
|
+
return rowToInboxDraft(getDb().query(
|
|
144
|
+
"SELECT * FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?",
|
|
145
|
+
).get(input.operatorId, input.peerId));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function deleteInboxDraft(operatorId: string, peerId: string): boolean {
|
|
149
|
+
return getDb().query("DELETE FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?").run(operatorId, peerId).changes > 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createChatHistoryImport(input: {
|
|
153
|
+
targetAgentId?: string;
|
|
154
|
+
targetSpawnRequestId?: string;
|
|
155
|
+
sourcePeerId: string;
|
|
156
|
+
sourceAgentId?: string;
|
|
157
|
+
sourceThreadId?: string;
|
|
158
|
+
sourceAgentLabel?: string;
|
|
159
|
+
importedBy?: string;
|
|
160
|
+
messageIds: number[];
|
|
161
|
+
}): ChatHistoryImport {
|
|
162
|
+
if (!input.targetAgentId && !input.targetSpawnRequestId) {
|
|
163
|
+
throw new ValidationError("targetAgentId or targetSpawnRequestId required");
|
|
164
|
+
}
|
|
165
|
+
if (!input.sourcePeerId.trim()) throw new ValidationError("sourcePeerId required");
|
|
166
|
+
const uniqueMessageIds = [...new Set(input.messageIds.map((id) => Number(id)).filter((id) => Number.isInteger(id) && id > 0))];
|
|
167
|
+
if (uniqueMessageIds.length === 0) throw new ValidationError("messageIds required");
|
|
168
|
+
if (uniqueMessageIds.length > 500) throw new ValidationError("messageIds max 500");
|
|
169
|
+
|
|
170
|
+
const messages = uniqueMessageIds.map((id) => getMessage(id));
|
|
171
|
+
if (messages.some((message) => !message)) throw new ValidationError("message not found");
|
|
172
|
+
const orderedMessages = (messages as Message[]).sort((a, b) => a.id - b.id);
|
|
173
|
+
|
|
174
|
+
const id = randomUUID();
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
getDb().transaction(() => {
|
|
177
|
+
getDb().query(`
|
|
178
|
+
INSERT INTO chat_history_imports (
|
|
179
|
+
id, target_agent_id, target_spawn_request_id, source_peer_id, source_agent_id,
|
|
180
|
+
source_thread_id, source_agent_label, imported_by, imported_at
|
|
181
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
182
|
+
`).run(
|
|
183
|
+
id,
|
|
184
|
+
input.targetAgentId ?? null,
|
|
185
|
+
input.targetSpawnRequestId ?? null,
|
|
186
|
+
input.sourcePeerId,
|
|
187
|
+
input.sourceAgentId ?? null,
|
|
188
|
+
input.sourceThreadId ?? null,
|
|
189
|
+
input.sourceAgentLabel ?? null,
|
|
190
|
+
input.importedBy ?? "user",
|
|
191
|
+
now,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const insertEntry = getDb().query(`
|
|
195
|
+
INSERT INTO chat_history_import_entries (
|
|
196
|
+
import_id, position, original_message_id, original_from, original_to, original_created_at, message_snapshot
|
|
197
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
198
|
+
`);
|
|
199
|
+
orderedMessages.forEach((message, index) => {
|
|
200
|
+
insertEntry.run(
|
|
201
|
+
id,
|
|
202
|
+
index,
|
|
203
|
+
message.id,
|
|
204
|
+
message.from,
|
|
205
|
+
message.to,
|
|
206
|
+
Number(message.createdAt),
|
|
207
|
+
JSON.stringify(message),
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
})();
|
|
211
|
+
|
|
212
|
+
return getChatHistoryImport(id)!;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function getChatHistoryImport(id: string): ChatHistoryImport | null {
|
|
216
|
+
const row = getDb().query("SELECT * FROM chat_history_imports WHERE id = ?").get(id) as any | undefined;
|
|
217
|
+
if (!row) return null;
|
|
218
|
+
const entries = (getDb().query(
|
|
219
|
+
"SELECT * FROM chat_history_import_entries WHERE import_id = ? ORDER BY position ASC",
|
|
220
|
+
).all(id) as any[]).map(rowToChatHistoryImportEntry);
|
|
221
|
+
return rowToChatHistoryImport(row, entries);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function listChatHistoryImports(input: {
|
|
225
|
+
targetAgentId?: string;
|
|
226
|
+
targetSpawnRequestId?: string;
|
|
227
|
+
limit?: number;
|
|
228
|
+
} = {}): ChatHistoryImport[] {
|
|
229
|
+
const conditions: string[] = [];
|
|
230
|
+
const params: any[] = [];
|
|
231
|
+
if (input.targetAgentId) {
|
|
232
|
+
conditions.push("target_agent_id = ?");
|
|
233
|
+
params.push(input.targetAgentId);
|
|
234
|
+
}
|
|
235
|
+
if (input.targetSpawnRequestId) {
|
|
236
|
+
conditions.push("target_spawn_request_id = ?");
|
|
237
|
+
params.push(input.targetSpawnRequestId);
|
|
238
|
+
}
|
|
239
|
+
const limit = Math.max(1, Math.min(input.limit ?? 100, 500));
|
|
240
|
+
const where = conditions.length ? `WHERE ${conditions.join(" OR ")}` : "";
|
|
241
|
+
const rows = getDb().query(`SELECT * FROM chat_history_imports ${where} ORDER BY imported_at ASC LIMIT ?`).all(...params, limit) as any[];
|
|
242
|
+
return rows.map((row) => {
|
|
243
|
+
const entries = (getDb().query(
|
|
244
|
+
"SELECT * FROM chat_history_import_entries WHERE import_id = ? ORDER BY position ASC",
|
|
245
|
+
).all(row.id) as any[]).map(rowToChatHistoryImportEntry);
|
|
246
|
+
return rowToChatHistoryImport(row, entries);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Barrel for the db/ package. src/db.ts re-exports this, preserving the
|
|
2
|
+
// historical `import { ... } from "./db"` surface while the implementation
|
|
3
|
+
// lives in per-domain modules. See docs/architecture.md (DB schema) and #298.
|
|
4
|
+
export * from "./connection.ts";
|
|
5
|
+
export * from "./schema.ts";
|
|
6
|
+
export * from "./migrations.ts";
|
|
7
|
+
export * from "./mappers.ts";
|
|
8
|
+
export * from "./channels.ts";
|
|
9
|
+
export * from "./agents.ts";
|
|
10
|
+
export * from "./agent-search.ts";
|
|
11
|
+
export * from "./tasks.ts";
|
|
12
|
+
export * from "./integrations.ts";
|
|
13
|
+
export * from "./pairs.ts";
|
|
14
|
+
export * from "./artifacts.ts";
|
|
15
|
+
export * from "./messages.ts";
|
|
16
|
+
export * from "./delivery.ts";
|
|
17
|
+
export * from "./message-reads.ts";
|
|
18
|
+
export * from "./inbox.ts";
|
|
19
|
+
export * from "./activity.ts";
|
|
20
|
+
export * from "./stats.ts";
|
|
21
|
+
export * from "./orchestrators.ts";
|
|
22
|
+
export * from "./workspaces.ts";
|
|
23
|
+
export * from "./merge-lease.ts";
|