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.
- package/package.json +2 -2
- package/public/assets/{activity-DT1JGHnp.js → activity-B0_uE6Yh.js} +2 -2
- package/public/assets/{activity-DT1JGHnp.js.map → activity-B0_uE6Yh.js.map} +1 -1
- package/public/assets/{agent-profiles-CrMemMkZ.js → agent-profiles-Rwxrcf9F.js} +2 -2
- package/public/assets/{agent-profiles-CrMemMkZ.js.map → agent-profiles-Rwxrcf9F.js.map} +1 -1
- package/public/assets/{agents-Bl-rrgOy.js → agents-Dp1EXJc8.js} +2 -2
- package/public/assets/{agents-Bl-rrgOy.js.map → agents-Dp1EXJc8.js.map} +1 -1
- package/public/assets/{analytics-a663ak56.js → analytics-D5OT5ajj.js} +2 -2
- package/public/assets/{analytics-a663ak56.js.map → analytics-D5OT5ajj.js.map} +1 -1
- package/public/assets/automation-Dm6rXNxK.js +2 -0
- package/public/assets/{automation-CiaLThdO.js.map → automation-Dm6rXNxK.js.map} +1 -1
- package/public/assets/{branch-state-badge-D4ur3m3_.js → branch-state-badge-FX5Yww2s.js} +2 -2
- package/public/assets/{branch-state-badge-D4ur3m3_.js.map → branch-state-badge-FX5Yww2s.js.map} +1 -1
- package/public/assets/{channels-o9KLTHoK.js → channels--rdAiX17.js} +2 -2
- package/public/assets/{channels-o9KLTHoK.js.map → channels--rdAiX17.js.map} +1 -1
- package/public/assets/chat-JZAEDGfX.js +2 -0
- package/public/assets/chat-JZAEDGfX.js.map +1 -0
- package/public/assets/{connectors-CdC806mA.js → connectors-Bx4gzvNf.js} +2 -2
- package/public/assets/{connectors-CdC806mA.js.map → connectors-Bx4gzvNf.js.map} +1 -1
- package/public/assets/display-Bebqs1qu.js +3 -0
- package/public/assets/display-Bebqs1qu.js.map +1 -0
- package/public/assets/{formatted-body-impl-Ca74OAEH.js → formatted-body-impl-CVq4qHix.js} +2 -2
- package/public/assets/{formatted-body-impl-Ca74OAEH.js.map → formatted-body-impl-CVq4qHix.js.map} +1 -1
- package/public/assets/{index-C_33ymaw.js → index-BHRtR4q7.js} +8 -8
- package/public/assets/{index-C_33ymaw.js.map → index-BHRtR4q7.js.map} +1 -1
- package/public/assets/{insights-ClI68s39.js → insights-yJFgCa3o.js} +2 -2
- package/public/assets/{insights-ClI68s39.js.map → insights-yJFgCa3o.js.map} +1 -1
- package/public/assets/{integrations-1nxMizDY.js → integrations-k1HIONjo.js} +2 -2
- package/public/assets/{integrations-1nxMizDY.js.map → integrations-k1HIONjo.js.map} +1 -1
- package/public/assets/maintenance-CsoOFBXx.js +2 -0
- package/public/assets/{maintenance-DiFNzNPN.js.map → maintenance-CsoOFBXx.js.map} +1 -1
- package/public/assets/{managed-agents-Do3dKvfj.js → managed-agents-Q3HuVjGg.js} +2 -2
- package/public/assets/{managed-agents-Do3dKvfj.js.map → managed-agents-Q3HuVjGg.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CLA0J255.js → markdown-preview-impl-CnsMjrnu.js} +2 -2
- package/public/assets/{markdown-preview-impl-CLA0J255.js.map → markdown-preview-impl-CnsMjrnu.js.map} +1 -1
- package/public/assets/{memory-IjwqFzBd.js → memory-D3-K5eJS.js} +2 -2
- package/public/assets/{memory-IjwqFzBd.js.map → memory-D3-K5eJS.js.map} +1 -1
- package/public/assets/{messages-DjvWqHyn.js → messages-B4lCP5rS.js} +2 -2
- package/public/assets/{messages-DjvWqHyn.js.map → messages-B4lCP5rS.js.map} +1 -1
- package/public/assets/{orchestrators-D2IqDxDT.js → orchestrators-CRoZtLeQ.js} +2 -2
- package/public/assets/{orchestrators-D2IqDxDT.js.map → orchestrators-CRoZtLeQ.js.map} +1 -1
- package/public/assets/{overview-DKC3TbAh.js → overview-CxCU2fOF.js} +2 -2
- package/public/assets/{overview-DKC3TbAh.js.map → overview-CxCU2fOF.js.map} +1 -1
- package/public/assets/pairs-unqjPlmq.js +2 -0
- package/public/assets/{pairs-WpKCPE1n.js.map → pairs-unqjPlmq.js.map} +1 -1
- package/public/assets/{security-BF7ZtPQe.js → security-B7HhSYNy.js} +2 -2
- package/public/assets/{security-BF7ZtPQe.js.map → security-B7HhSYNy.js.map} +1 -1
- package/public/assets/{settings-CQnjrTa-.js → settings-B9NDhsAb.js} +2 -2
- package/public/assets/{settings-CQnjrTa-.js.map → settings-B9NDhsAb.js.map} +1 -1
- package/public/assets/store-DiSzYHj9.js +9 -0
- package/public/assets/{store-C9VcSo05.js.map → store-DiSzYHj9.js.map} +1 -1
- package/public/assets/{tasks-CbN_GSSb.js → tasks-CIQolvNm.js} +2 -2
- package/public/assets/{tasks-CbN_GSSb.js.map → tasks-CIQolvNm.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BJRohThT.js → terminal-viewer-impl-DCifVqFR.js} +2 -2
- package/public/assets/{terminal-viewer-impl-BJRohThT.js.map → terminal-viewer-impl-DCifVqFR.js.map} +1 -1
- package/public/assets/{work-queue-C5xLBLmm.js → work-queue-Dr3c1V6O.js} +2 -2
- package/public/assets/{work-queue-C5xLBLmm.js.map → work-queue-Dr3c1V6O.js.map} +1 -1
- package/public/assets/{workspaces-D91H3wDX.js → workspaces-B1Jxop7h.js} +3 -3
- package/public/assets/{workspaces-D91H3wDX.js.map → workspaces-B1Jxop7h.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/src/adapter.ts +1 -1
- package/src/agent-lifecycle-events.ts +137 -0
- package/src/artifact-storage.ts +3 -5
- package/src/channel-target.ts +24 -0
- package/src/cli/_shared.ts +80 -0
- package/src/cli/agent-detect.ts +188 -0
- package/src/cli/agent-meta.ts +95 -0
- package/src/cli/context-probe.ts +88 -0
- package/src/cli/daemon.ts +111 -0
- package/src/cli/dev.ts +173 -0
- package/src/cli/index.ts +361 -0
- package/src/cli/introspect.ts +73 -0
- package/src/cli/memory.ts +37 -0
- package/src/cli/message.ts +201 -0
- package/src/cli/orchestrator.ts +227 -0
- package/src/cli/pair.ts +125 -0
- package/src/cli/provider.ts +209 -0
- package/src/cli/recipe.ts +110 -0
- package/src/cli/reply.ts +141 -0
- package/src/cli/setup.ts +57 -0
- package/src/cli/steward.ts +59 -0
- package/src/cli/token.ts +81 -0
- package/src/cli/upgrade.ts +193 -0
- package/src/cli/workspace.ts +215 -0
- package/src/cli.ts +4 -2718
- package/src/config-store.ts +10 -6
- 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/maintenance.ts +4 -0
- package/src/mcp-errors.ts +7 -0
- package/src/mcp.ts +32 -34
- package/src/routes/agents-spawn.ts +9 -1
- package/src/routes/agents.ts +5 -0
- package/src/routes/commands.ts +15 -0
- package/src/routes/integrations.ts +6 -8
- package/src/spawn-targets.ts +159 -0
- package/src/utils.ts +16 -1
- package/public/assets/automation-CiaLThdO.js +0 -2
- package/public/assets/chat-5hvHZcAe.js +0 -2
- package/public/assets/chat-5hvHZcAe.js.map +0 -1
- package/public/assets/display-JI19Vc7L.js +0 -3
- package/public/assets/display-JI19Vc7L.js.map +0 -1
- package/public/assets/maintenance-DiFNzNPN.js +0 -2
- package/public/assets/pairs-WpKCPE1n.js +0 -2
- package/public/assets/store-C9VcSo05.js +0 -9
package/src/db/agents.ts
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
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 { evaluatePoolBindings, expectedChannelAgentId, upsertChannelForAgent } from "./channels.ts";
|
|
18
|
+
import { ValidationError, getDb } from "./connection.ts";
|
|
19
|
+
import { rowToAgent, rowToContextSnapshot } from "./mappers.ts";
|
|
20
|
+
import { closeOpenPairsForAgent } from "./pairs.ts";
|
|
21
|
+
import { TASK_SELECT, insertTaskEvent } from "./tasks.ts";
|
|
22
|
+
import { electWorkspaceStewards, electWorkspaceStewardsForAgent } from "./workspaces.ts";
|
|
23
|
+
import type {
|
|
24
|
+
AgentCard,
|
|
25
|
+
ActivityEvent,
|
|
26
|
+
ActivityEventInput,
|
|
27
|
+
AgentKind,
|
|
28
|
+
AgentSessionGuard,
|
|
29
|
+
Artifact,
|
|
30
|
+
ArtifactBlob,
|
|
31
|
+
ArtifactKind,
|
|
32
|
+
ArtifactLink,
|
|
33
|
+
ArtifactSensitivity,
|
|
34
|
+
ArtifactVisibility,
|
|
35
|
+
AttachmentRef,
|
|
36
|
+
ChannelBinding,
|
|
37
|
+
ChannelBindingMode,
|
|
38
|
+
ChannelRouteTarget,
|
|
39
|
+
ChatHistoryImport,
|
|
40
|
+
ChatHistoryImportEntry,
|
|
41
|
+
ChannelSummary,
|
|
42
|
+
ChannelTargetHealth,
|
|
43
|
+
CreatePairInput,
|
|
44
|
+
HealthCheck,
|
|
45
|
+
HealthReport,
|
|
46
|
+
ManagedAgent,
|
|
47
|
+
ManagedSessionExitDiagnostics,
|
|
48
|
+
Message,
|
|
49
|
+
MessageDeliveryAttempt,
|
|
50
|
+
MessageDeliveryStatus,
|
|
51
|
+
Orchestrator,
|
|
52
|
+
OrchestratorHealth,
|
|
53
|
+
OrchestratorRuntimeInput,
|
|
54
|
+
OrchestratorStatus,
|
|
55
|
+
OrchestratorUpgradeState,
|
|
56
|
+
PairActionInput,
|
|
57
|
+
PairMessageInput,
|
|
58
|
+
PairSession,
|
|
59
|
+
PairStatus,
|
|
60
|
+
RegisterAgentInput,
|
|
61
|
+
ReplyObligation,
|
|
62
|
+
RegisterOrchestratorInput,
|
|
63
|
+
SendMessageInput,
|
|
64
|
+
PollQuery,
|
|
65
|
+
SpawnApprovalMode,
|
|
66
|
+
SpawnProvider,
|
|
67
|
+
Task,
|
|
68
|
+
TaskEvent,
|
|
69
|
+
TaskSeverity,
|
|
70
|
+
TaskStatus,
|
|
71
|
+
IntegrationEventInput,
|
|
72
|
+
IntegrationSummary,
|
|
73
|
+
IntegrationTaskStats,
|
|
74
|
+
InboxDraft,
|
|
75
|
+
InboxState,
|
|
76
|
+
InboxThreadState,
|
|
77
|
+
ContextSnapshot,
|
|
78
|
+
ContextState,
|
|
79
|
+
ProviderCapabilities,
|
|
80
|
+
TaskStatusInput,
|
|
81
|
+
WorkspaceMetadata,
|
|
82
|
+
WorkspaceRecord,
|
|
83
|
+
WorkspaceStatus,
|
|
84
|
+
} from "../types";
|
|
85
|
+
|
|
86
|
+
const CONTEXT_SNAPSHOT_DEBOUNCE_MS = 60_000;
|
|
87
|
+
|
|
88
|
+
export function parseStringArray(raw: string): string[] {
|
|
89
|
+
const parsed = parseJson<unknown>(raw, []);
|
|
90
|
+
if (!Array.isArray(parsed)) return [];
|
|
91
|
+
return parsed.filter((value): value is string => typeof value === "string");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function normalizeTags(tags: string[] | undefined): string[] {
|
|
95
|
+
return [...new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function inferAgentKind(input: Pick<RegisterAgentInput, "id" | "kind" | "tags" | "capabilities" | "meta">): AgentKind {
|
|
99
|
+
if (input.kind) return input.kind;
|
|
100
|
+
if (input.id === "user") return "user";
|
|
101
|
+
if (input.id === "system") return "system";
|
|
102
|
+
const metaKind = stringValue(input.meta?.kind);
|
|
103
|
+
if (metaKind === "channel" || metaKind === "communication-channel") return "channel";
|
|
104
|
+
if (metaKind === "orchestrator") return "orchestrator";
|
|
105
|
+
if ((input.tags ?? []).includes("channel") || (input.capabilities ?? []).includes("channel")) return "channel";
|
|
106
|
+
return "provider";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function inferProviderTag(input: RegisterAgentInput): "claude" | "codex" | undefined {
|
|
110
|
+
if (inferAgentKind(input) !== "provider") return undefined;
|
|
111
|
+
const meta = input.meta ?? {};
|
|
112
|
+
const values = [
|
|
113
|
+
input.id,
|
|
114
|
+
input.name,
|
|
115
|
+
input.rig,
|
|
116
|
+
...(input.tags ?? []),
|
|
117
|
+
stringValue(meta.provider),
|
|
118
|
+
stringValue(meta.client),
|
|
119
|
+
stringValue(meta.runtime),
|
|
120
|
+
stringValue(meta.agentType),
|
|
121
|
+
].filter((value): value is string => typeof value === "string");
|
|
122
|
+
|
|
123
|
+
if (values.some((value) => value.toLowerCase().includes("codex"))) return "codex";
|
|
124
|
+
if (values.some((value) => value.toLowerCase().includes("claude"))) return "claude";
|
|
125
|
+
|
|
126
|
+
// Older Claude hooks did not always send an explicit provider tag, but did
|
|
127
|
+
// send Claude-style approval metadata. Codex also sends approvalMode, so this
|
|
128
|
+
// fallback only runs after Codex signals have been ruled out.
|
|
129
|
+
if (Object.prototype.hasOwnProperty.call(meta, "approvalMode")) return "claude";
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function tagsWithProvider(input: RegisterAgentInput): string[] {
|
|
134
|
+
const tags = normalizeTags(input.tags);
|
|
135
|
+
const provider = inferProviderTag(input);
|
|
136
|
+
if (!provider || tags.includes(provider)) return tags;
|
|
137
|
+
return [provider, ...tags];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
export function upsertAgent(input: RegisterAgentInput): AgentCard {
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
const kind = inferAgentKind(input);
|
|
144
|
+
if (kind === "channel") {
|
|
145
|
+
const expectedId = expectedChannelAgentId(input);
|
|
146
|
+
if (input.id !== expectedId) {
|
|
147
|
+
throw new ValidationError(`channel agent id must be canonical: ${expectedId}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const tags = tagsWithProvider(input);
|
|
151
|
+
// Preserve the existing label across re-registrations unless the caller
|
|
152
|
+
// explicitly sends one (including null to clear).
|
|
153
|
+
const labelProvided = Object.prototype.hasOwnProperty.call(input, "label");
|
|
154
|
+
const readyProvided = Object.prototype.hasOwnProperty.call(input, "ready");
|
|
155
|
+
const instanceProvided = Boolean(input.instanceId);
|
|
156
|
+
const stmt = getDb().query(`
|
|
157
|
+
INSERT INTO agents (id, name, kind, label, tags, machine, rig, capabilities, ready, status, instance_id, epoch, provider_capabilities, context_state, meta, spawned_by, last_seen, created_at)
|
|
158
|
+
VALUES ($id, $name, $kind, $label, $tags, $machine, $rig, $capabilities, $ready, $status, $instanceId, $initialEpoch, $providerCapabilities, $contextState, $meta, $spawnedBy, $now, $now)
|
|
159
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
160
|
+
name = $name,
|
|
161
|
+
kind = $kind,
|
|
162
|
+
label = CASE WHEN $labelProvided = 1 THEN $label ELSE agents.label END,
|
|
163
|
+
tags = $tags,
|
|
164
|
+
machine = coalesce($machine, agents.machine),
|
|
165
|
+
rig = coalesce($rig, agents.rig),
|
|
166
|
+
capabilities = $capabilities,
|
|
167
|
+
ready = CASE WHEN $readyProvided = 1 THEN $ready ELSE agents.ready END,
|
|
168
|
+
status = $status,
|
|
169
|
+
instance_id = CASE WHEN $instanceProvided = 1 THEN $instanceId ELSE agents.instance_id END,
|
|
170
|
+
epoch = CASE
|
|
171
|
+
WHEN $instanceProvided = 1 AND (agents.instance_id IS NULL OR agents.instance_id <> $instanceId) THEN agents.epoch + 1
|
|
172
|
+
ELSE agents.epoch
|
|
173
|
+
END,
|
|
174
|
+
provider_capabilities = coalesce($providerCapabilities, agents.provider_capabilities),
|
|
175
|
+
context_state = coalesce($contextState, agents.context_state),
|
|
176
|
+
meta = $meta,
|
|
177
|
+
spawned_by = coalesce($spawnedBy, agents.spawned_by),
|
|
178
|
+
last_seen = $now
|
|
179
|
+
`);
|
|
180
|
+
|
|
181
|
+
stmt.run({
|
|
182
|
+
$id: input.id,
|
|
183
|
+
$name: input.name,
|
|
184
|
+
$kind: kind,
|
|
185
|
+
$label: input.label ?? null,
|
|
186
|
+
$labelProvided: labelProvided ? 1 : 0,
|
|
187
|
+
$tags: JSON.stringify(tags),
|
|
188
|
+
$machine: input.machine ?? null,
|
|
189
|
+
$rig: input.rig ?? null,
|
|
190
|
+
$capabilities: JSON.stringify(input.capabilities ?? []),
|
|
191
|
+
$ready: input.ready ? 1 : 0,
|
|
192
|
+
$readyProvided: readyProvided ? 1 : 0,
|
|
193
|
+
$status: input.status ?? "idle",
|
|
194
|
+
$instanceId: input.instanceId ?? null,
|
|
195
|
+
$instanceProvided: instanceProvided ? 1 : 0,
|
|
196
|
+
$initialEpoch: instanceProvided ? 1 : 0,
|
|
197
|
+
$providerCapabilities: input.providerCapabilities ? JSON.stringify(input.providerCapabilities) : null,
|
|
198
|
+
$contextState: input.context ? JSON.stringify(input.context) : null,
|
|
199
|
+
$meta: JSON.stringify(input.meta ?? {}),
|
|
200
|
+
$spawnedBy: input.spawnedBy ?? null,
|
|
201
|
+
$now: now,
|
|
202
|
+
});
|
|
203
|
+
if (input.context) recordContextSnapshot(input.id, input.context, now);
|
|
204
|
+
|
|
205
|
+
const agent = getAgent(input.id)!;
|
|
206
|
+
if (agent.kind === "channel") upsertChannelForAgent(agent);
|
|
207
|
+
evaluatePoolBindings();
|
|
208
|
+
// A (re)joining agent may revive a dormant repo steward — re-elect for the
|
|
209
|
+
// repos it owns live workspaces in (issue #157, steward survives offline gap).
|
|
210
|
+
if (agent.status !== "offline") electWorkspaceStewardsForAgent(agent.id);
|
|
211
|
+
return agent;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function validateAgentSession(id: string, guard?: AgentSessionGuard): { ok: boolean; error?: string } {
|
|
215
|
+
if (!guard || (!guard.instanceId && guard.epoch === undefined)) return { ok: true };
|
|
216
|
+
const agent = getAgent(id);
|
|
217
|
+
if (!agent) return { ok: false, error: "agent not found" };
|
|
218
|
+
if (guard.instanceId && agent.instanceId !== guard.instanceId) {
|
|
219
|
+
return { ok: false, error: "stale agent instance" };
|
|
220
|
+
}
|
|
221
|
+
if (guard.epoch !== undefined && agent.epoch !== guard.epoch) {
|
|
222
|
+
return { ok: false, error: "stale agent instance" };
|
|
223
|
+
}
|
|
224
|
+
return { ok: true };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function setLabel(id: string, label: string | null): boolean {
|
|
228
|
+
const normalized = label && label.trim() ? label.trim() : null;
|
|
229
|
+
return (
|
|
230
|
+
getDb().query("UPDATE agents SET label = ? WHERE id = ?").run(normalized, id).changes > 0
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function setTags(id: string, tags: string[]): AgentCard | null {
|
|
235
|
+
const normalized = [...new Set(tags.map((tag) => tag.trim()).filter(Boolean))];
|
|
236
|
+
const result = getDb().query("UPDATE agents SET tags = ?, last_seen = ? WHERE id = ?").run(JSON.stringify(normalized), Date.now(), id);
|
|
237
|
+
return result.changes > 0 ? getAgent(id) : null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function getAgent(id: string): AgentCard | null {
|
|
241
|
+
const row = getDb().query("SELECT * FROM agents WHERE id = ?").get(id) as any;
|
|
242
|
+
return row ? rowToAgent(row) : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function listAgents(filter?: {
|
|
246
|
+
tag?: string;
|
|
247
|
+
machine?: string;
|
|
248
|
+
status?: string;
|
|
249
|
+
}): AgentCard[] {
|
|
250
|
+
let sql = "SELECT * FROM agents WHERE 1=1";
|
|
251
|
+
const params: any[] = [];
|
|
252
|
+
|
|
253
|
+
if (filter?.tag) {
|
|
254
|
+
sql += " AND EXISTS (SELECT 1 FROM json_each(tags) WHERE value = ?)";
|
|
255
|
+
params.push(filter.tag);
|
|
256
|
+
}
|
|
257
|
+
if (filter?.machine) {
|
|
258
|
+
sql += " AND machine = ?";
|
|
259
|
+
params.push(filter.machine);
|
|
260
|
+
}
|
|
261
|
+
if (filter?.status) {
|
|
262
|
+
sql += " AND status = ?";
|
|
263
|
+
params.push(filter.status);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
sql += " ORDER BY last_seen DESC";
|
|
267
|
+
return (getDb().query(sql).all(...params) as any[]).map(rowToAgent);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
export function countLiveSpawnedAgents(parentId: string, now: number = Date.now()): number {
|
|
272
|
+
const row = getDb().query(
|
|
273
|
+
"SELECT count(*) AS n FROM agents WHERE spawned_by = ? AND status NOT IN ('offline', 'stale') AND ready = 1 AND last_seen > ?",
|
|
274
|
+
).get(parentId, now - STALE_TTL_MS) as { n: number };
|
|
275
|
+
return row.n;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function setStatus(id: string, status: AgentCard["status"], guard?: AgentSessionGuard): boolean {
|
|
279
|
+
if (!validateAgentSession(id, guard).ok) return false;
|
|
280
|
+
const now = Date.now();
|
|
281
|
+
const ready = status === "offline" ? 0 : undefined;
|
|
282
|
+
const sql = ready === 0
|
|
283
|
+
? "UPDATE agents SET status = ?, ready = 0, last_seen = ? WHERE id = ?"
|
|
284
|
+
: "UPDATE agents SET status = ?, last_seen = ? WHERE id = ?";
|
|
285
|
+
const changed = getDb().query(sql).run(status, now, id).changes > 0;
|
|
286
|
+
if (changed && status === "offline") closeOpenPairsForAgent(id, now);
|
|
287
|
+
if (changed && status === "offline") electWorkspaceStewards();
|
|
288
|
+
return changed;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function markReady(id: string, ready: boolean, guard?: AgentSessionGuard): boolean {
|
|
292
|
+
if (!validateAgentSession(id, guard).ok) return false;
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
return (
|
|
295
|
+
getDb()
|
|
296
|
+
.query("UPDATE agents SET ready = ?, last_seen = ? WHERE id = ?")
|
|
297
|
+
.run(ready ? 1 : 0, now, id).changes > 0
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function mergeAgentMeta(id: string, meta: Record<string, unknown>, guard?: AgentSessionGuard): boolean {
|
|
302
|
+
if (!validateAgentSession(id, guard).ok) return false;
|
|
303
|
+
const agent = getAgent(id);
|
|
304
|
+
if (!agent) return false;
|
|
305
|
+
const merged = { ...(agent.meta ?? {}), ...meta };
|
|
306
|
+
const result = getDb()
|
|
307
|
+
.query("UPDATE agents SET meta = ?, last_seen = ? WHERE id = ?")
|
|
308
|
+
.run(JSON.stringify(merged), Date.now(), id);
|
|
309
|
+
return result.changes > 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function recordAgentExitDiagnostics(id: string, diagnostics: ManagedSessionExitDiagnostics): AgentCard | null {
|
|
313
|
+
const agent = getAgent(id);
|
|
314
|
+
if (!agent) return null;
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
const merged = {
|
|
317
|
+
...(agent.meta ?? {}),
|
|
318
|
+
lastError: diagnostics.lastError,
|
|
319
|
+
lastExit: diagnostics,
|
|
320
|
+
lastExitAt: diagnostics.detectedAt || now,
|
|
321
|
+
};
|
|
322
|
+
const result = getDb()
|
|
323
|
+
.query("UPDATE agents SET status = 'offline', ready = 0, meta = ?, last_seen = ? WHERE id = ?")
|
|
324
|
+
.run(JSON.stringify(merged), now, id);
|
|
325
|
+
if (result.changes <= 0) return null;
|
|
326
|
+
closeOpenPairsForAgent(id, now);
|
|
327
|
+
return getAgent(id);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function heartbeat(
|
|
331
|
+
id: string,
|
|
332
|
+
guard?: AgentSessionGuard,
|
|
333
|
+
runtime?: { providerCapabilities?: ProviderCapabilities; context?: ContextState },
|
|
334
|
+
): boolean {
|
|
335
|
+
if (!validateAgentSession(id, guard).ok) return false;
|
|
336
|
+
const now = Date.now();
|
|
337
|
+
if (runtime?.providerCapabilities || runtime?.context) {
|
|
338
|
+
const result = getDb()
|
|
339
|
+
.query(`
|
|
340
|
+
UPDATE agents SET
|
|
341
|
+
last_seen = ?,
|
|
342
|
+
status = CASE WHEN status = 'offline' THEN 'idle' ELSE status END,
|
|
343
|
+
provider_capabilities = coalesce(?, provider_capabilities),
|
|
344
|
+
context_state = coalesce(?, context_state)
|
|
345
|
+
WHERE id = ?
|
|
346
|
+
`)
|
|
347
|
+
.run(
|
|
348
|
+
now,
|
|
349
|
+
runtime.providerCapabilities ? JSON.stringify(runtime.providerCapabilities) : null,
|
|
350
|
+
runtime.context ? JSON.stringify(runtime.context) : null,
|
|
351
|
+
id,
|
|
352
|
+
);
|
|
353
|
+
if (result.changes > 0 && runtime.context) recordContextSnapshot(id, runtime.context, now);
|
|
354
|
+
return result.changes > 0;
|
|
355
|
+
}
|
|
356
|
+
const result = getDb()
|
|
357
|
+
.query("UPDATE agents SET last_seen = ?, status = CASE WHEN status = 'offline' THEN 'idle' ELSE status END WHERE id = ?")
|
|
358
|
+
.run(now, id);
|
|
359
|
+
return result.changes > 0;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function listContextSnapshots(agentId: string, options: { since?: number; limit?: number } = {}): ContextSnapshot[] {
|
|
363
|
+
const limit = Math.max(1, Math.min(options.limit ?? 288, 1000));
|
|
364
|
+
const params: any[] = [agentId];
|
|
365
|
+
let sql = "SELECT * FROM context_snapshots WHERE agent_id = ?";
|
|
366
|
+
if (options.since !== undefined) {
|
|
367
|
+
sql += " AND captured_at >= ?";
|
|
368
|
+
params.push(options.since);
|
|
369
|
+
}
|
|
370
|
+
sql += " ORDER BY captured_at ASC LIMIT ?";
|
|
371
|
+
params.push(limit);
|
|
372
|
+
return (getDb().query(sql).all(...params) as any[]).map(rowToContextSnapshot);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function pruneContextSnapshots(agentId?: string, olderThan = Date.now() - DAY_MS): number {
|
|
376
|
+
const result = agentId
|
|
377
|
+
? getDb().query("DELETE FROM context_snapshots WHERE agent_id = ? AND captured_at < ?").run(agentId, olderThan)
|
|
378
|
+
: getDb().query("DELETE FROM context_snapshots WHERE captured_at < ?").run(olderThan);
|
|
379
|
+
return result.changes;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function recordContextSnapshot(agentId: string, context: ContextState, now: number): void {
|
|
383
|
+
const latest = getDb()
|
|
384
|
+
.query("SELECT captured_at FROM context_snapshots WHERE agent_id = ? ORDER BY captured_at DESC LIMIT 1")
|
|
385
|
+
.get(agentId) as { captured_at: number } | undefined;
|
|
386
|
+
if (latest && latest.captured_at > now - CONTEXT_SNAPSHOT_DEBOUNCE_MS) return;
|
|
387
|
+
|
|
388
|
+
getDb().query(`
|
|
389
|
+
INSERT INTO context_snapshots (
|
|
390
|
+
agent_id,
|
|
391
|
+
utilization,
|
|
392
|
+
lifecycle_state,
|
|
393
|
+
tokens_used,
|
|
394
|
+
tokens_max,
|
|
395
|
+
source,
|
|
396
|
+
confidence,
|
|
397
|
+
context_state,
|
|
398
|
+
captured_at
|
|
399
|
+
)
|
|
400
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
401
|
+
`).run(
|
|
402
|
+
agentId,
|
|
403
|
+
context.utilization,
|
|
404
|
+
context.lifecycleState,
|
|
405
|
+
context.tokensUsed ?? null,
|
|
406
|
+
context.tokensMax ?? null,
|
|
407
|
+
context.source,
|
|
408
|
+
context.confidence,
|
|
409
|
+
JSON.stringify(context),
|
|
410
|
+
now,
|
|
411
|
+
);
|
|
412
|
+
pruneContextSnapshots(agentId, now - DAY_MS);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
export function reapStaleAgents(ttlMs: number = STALE_TTL_MS): string[] {
|
|
417
|
+
const now = Date.now();
|
|
418
|
+
const cutoff = now - ttlMs;
|
|
419
|
+
getDb().query("UPDATE agents SET last_seen = ? WHERE id IN ('user', 'system')").run(now);
|
|
420
|
+
const rows = getDb()
|
|
421
|
+
.query(
|
|
422
|
+
"UPDATE agents SET status = 'offline', ready = 0 WHERE status NOT IN ('offline', 'stale') AND last_seen < ? AND id NOT IN ('user', 'system') RETURNING id"
|
|
423
|
+
)
|
|
424
|
+
.all(cutoff) as any[];
|
|
425
|
+
for (const row of rows) {
|
|
426
|
+
revokeRuntimeTokensForAgent(row.id, now);
|
|
427
|
+
closeOpenPairsForAgent(row.id, now);
|
|
428
|
+
}
|
|
429
|
+
return rows.map((r: any) => r.id);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// On-demand automation tasks (targetMode=on_demand_agent) are bound to a single
|
|
433
|
+
// ephemeral agent spawned just for that task. When that agent's claim is released —
|
|
434
|
+
// clean shutdown, prune, lease expiry, or orphan-grace — the task must NOT return to
|
|
435
|
+
// the claimable pool: its target is a unique `label:automation-…` that no other agent
|
|
436
|
+
// will ever match, so a re-opened task lingers forever as an orphaned claim. Resolve
|
|
437
|
+
// those as done instead; automation reconcile then settles the run as succeeded.
|
|
438
|
+
// Run this BEFORE the generic re-open UPDATE at each release site, with the same WHERE
|
|
439
|
+
// condition: it flips matching single-target tasks to 'done', which the subsequent
|
|
440
|
+
// re-open (gated on status IN claimed/in_progress/blocked/orphaned) then skips.
|
|
441
|
+
export function settleSingleTargetOnDemandTasks(condition: string, params: any[], now: number, reason: string): void {
|
|
442
|
+
const selectSql = `${TASK_SELECT} WHERE (${condition}) AND json_extract(metadata, '$.targetMode') = 'on_demand_agent' AND status IN ('claimed', 'in_progress', 'blocked', 'orphaned')`;
|
|
443
|
+
const rows = getDb().query(selectSql).all(...params) as any[];
|
|
444
|
+
if (rows.length === 0) return;
|
|
445
|
+
getDb().query(
|
|
446
|
+
`UPDATE tasks SET status = 'done', claim_expires_at = NULL, updated_at = ?, last_seen_at = ? WHERE (${condition}) AND json_extract(metadata, '$.targetMode') = 'on_demand_agent' AND status IN ('claimed', 'in_progress', 'blocked', 'orphaned')`
|
|
447
|
+
).run(now, now, ...params);
|
|
448
|
+
for (const row of rows) {
|
|
449
|
+
insertTaskEvent(row.id, {
|
|
450
|
+
source: "agent-relay",
|
|
451
|
+
type: "task.auto-completed",
|
|
452
|
+
severity: row.severity,
|
|
453
|
+
title: "On-demand task auto-resolved",
|
|
454
|
+
body: `On-demand agent ${row.claimed_by ?? "(unknown)"} exited (${reason}); task resolved so it does not orphan`,
|
|
455
|
+
metadata: { agentId: row.claimed_by, reason, completedBy: "relay" },
|
|
456
|
+
}, now);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
|
|
461
|
+
const cutoff = Date.now() - maxOfflineMs;
|
|
462
|
+
return getDb().transaction(() => {
|
|
463
|
+
const rows = getDb()
|
|
464
|
+
.query(
|
|
465
|
+
"SELECT id FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system')"
|
|
466
|
+
)
|
|
467
|
+
.all(cutoff) as any[];
|
|
468
|
+
if (rows.length === 0) return [];
|
|
469
|
+
const now = Date.now();
|
|
470
|
+
|
|
471
|
+
// Release claims held by pruned agents so work becomes claimable again.
|
|
472
|
+
getDb()
|
|
473
|
+
.query(
|
|
474
|
+
"UPDATE messages SET claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL WHERE claimed_by IN (SELECT id FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system'))"
|
|
475
|
+
)
|
|
476
|
+
.run(cutoff);
|
|
477
|
+
|
|
478
|
+
const offlineClaimCondition = "claimed_by IN (SELECT id FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system'))";
|
|
479
|
+
settleSingleTargetOnDemandTasks(offlineClaimCondition, [cutoff], now, "agent-pruned");
|
|
480
|
+
|
|
481
|
+
getDb()
|
|
482
|
+
.query(
|
|
483
|
+
`UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE ${offlineClaimCondition} AND status IN ('claimed', 'in_progress', 'blocked')`
|
|
484
|
+
)
|
|
485
|
+
.run(now, cutoff);
|
|
486
|
+
|
|
487
|
+
for (const row of rows) {
|
|
488
|
+
revokeRuntimeTokensForAgent(row.id, now);
|
|
489
|
+
closeOpenPairsForAgent(row.id, now);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
getDb()
|
|
493
|
+
.query(
|
|
494
|
+
"DELETE FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system')"
|
|
495
|
+
)
|
|
496
|
+
.run(cutoff);
|
|
497
|
+
|
|
498
|
+
return rows.map((r: any) => r.id);
|
|
499
|
+
})();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export function deleteAgent(id: string): { ok: boolean; error?: string } {
|
|
503
|
+
if (id === "user" || id === "system") {
|
|
504
|
+
return { ok: false, error: "built-in agent cannot be deleted" };
|
|
505
|
+
}
|
|
506
|
+
const deleted = getDb().transaction(() => {
|
|
507
|
+
// Release any claims held by this agent so the tasks become claimable again.
|
|
508
|
+
// from_agent is left intact as historical record.
|
|
509
|
+
const now = Date.now();
|
|
510
|
+
getDb().query("UPDATE messages SET claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL WHERE claimed_by = ?").run(id);
|
|
511
|
+
settleSingleTargetOnDemandTasks("claimed_by = ?", [id], now, "agent-removed");
|
|
512
|
+
getDb().query("UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE claimed_by = ? AND status IN ('claimed', 'in_progress', 'blocked')").run(now, id);
|
|
513
|
+
revokeRuntimeTokensForAgent(id, now);
|
|
514
|
+
closeOpenPairsForAgent(id, now);
|
|
515
|
+
return getDb().query("DELETE FROM agents WHERE id = ?").run(id).changes > 0;
|
|
516
|
+
})();
|
|
517
|
+
return deleted ? { ok: true } : { ok: false, error: "agent not found" };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function revokeRuntimeTokensForAgent(agentId: string, now = Date.now()): string[] {
|
|
521
|
+
const row = getDb().query("SELECT meta FROM agents WHERE id = ?").get(agentId) as { meta?: string } | undefined;
|
|
522
|
+
const jtis = runtimeTokenJtisFromMeta(parseJson(row?.meta ?? "{}", {}));
|
|
523
|
+
if (jtis.length === 0) return [];
|
|
524
|
+
const revokedAt = Math.floor(now / 1000);
|
|
525
|
+
const placeholders = jtis.map(() => "?").join(",");
|
|
526
|
+
const rows = getDb().query(`
|
|
527
|
+
UPDATE tokens
|
|
528
|
+
SET revoked_at = ?
|
|
529
|
+
WHERE revoked_at IS NULL
|
|
530
|
+
AND role = 'provider'
|
|
531
|
+
AND profile_id IN ('provider-agent', 'provider-interactive', 'provider-child')
|
|
532
|
+
AND jti IN (${placeholders})
|
|
533
|
+
RETURNING jti
|
|
534
|
+
`).all(revokedAt, ...jtis) as Array<{ jti: string }>;
|
|
535
|
+
return rows.map((item) => item.jti);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function runtimeTokenJtisFromMeta(meta: Record<string, unknown>): string[] {
|
|
539
|
+
const jtis = new Set<string>();
|
|
540
|
+
const auth = isRecord(meta.auth) ? meta.auth : undefined;
|
|
541
|
+
const direct = typeof auth?.jti === "string" ? auth.jti : typeof meta.runtimeTokenJti === "string" ? meta.runtimeTokenJti : undefined;
|
|
542
|
+
if (direct) jtis.add(direct);
|
|
543
|
+
const extra = Array.isArray(auth?.jtis) ? auth.jtis : Array.isArray(meta.runtimeTokenJtis) ? meta.runtimeTokenJtis : [];
|
|
544
|
+
for (const item of extra) {
|
|
545
|
+
if (typeof item === "string") jtis.add(item);
|
|
546
|
+
}
|
|
547
|
+
return [...jtis];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// --- Tasks ---
|
|
551
|
+
|