agent-relay-server 0.120.5 → 0.121.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/openapi.json +1 -1
- package/package.json +3 -3
- package/public/assets/{MessageBubble-BVff6PSi.js → MessageBubble-BaPGFJxq.js} +2 -2
- package/public/assets/{MessageBubble-BVff6PSi.js.map → MessageBubble-BaPGFJxq.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BjrVU5Tg.js → PlanGraphPanel-BXCVfmln.js} +2 -2
- package/public/assets/{PlanGraphPanel-BjrVU5Tg.js.map → PlanGraphPanel-BXCVfmln.js.map} +1 -1
- package/public/assets/{activity-Bk9LEUr4.js → activity-D_shwAZt.js} +2 -2
- package/public/assets/{activity-Bk9LEUr4.js.map → activity-D_shwAZt.js.map} +1 -1
- package/public/assets/{agent-profiles-DSVDoQ_N.js → agent-profiles-BrfVz5IV.js} +2 -2
- package/public/assets/{agent-profiles-DSVDoQ_N.js.map → agent-profiles-BrfVz5IV.js.map} +1 -1
- package/public/assets/agents-B5w-Z9Ic.js +2 -0
- package/public/assets/{agents-8rQuJqii.js.map → agents-B5w-Z9Ic.js.map} +1 -1
- package/public/assets/{analytics-DifGnk7q.js → analytics-Bm3D12UN.js} +2 -2
- package/public/assets/{analytics-DifGnk7q.js.map → analytics-Bm3D12UN.js.map} +1 -1
- package/public/assets/{automation-CMxQAf3l.js → automation-DOTL8BFs.js} +2 -2
- package/public/assets/{automation-CMxQAf3l.js.map → automation-DOTL8BFs.js.map} +1 -1
- package/public/assets/{branch-state-badge-CyC5Qrhc.js → branch-state-badge-BlqeJZ5-.js} +2 -2
- package/public/assets/{branch-state-badge-CyC5Qrhc.js.map → branch-state-badge-BlqeJZ5-.js.map} +1 -1
- package/public/assets/{channels-BLkdt2XR.js → channels-BoiKfK6Q.js} +2 -2
- package/public/assets/{channels-BLkdt2XR.js.map → channels-BoiKfK6Q.js.map} +1 -1
- package/public/assets/chat-PeU3iOaa.js +2 -0
- package/public/assets/{chat-jlXywb9G.js.map → chat-PeU3iOaa.js.map} +1 -1
- package/public/assets/{connectors-C0n12Je8.js → connectors-BvV8Hee2.js} +2 -2
- package/public/assets/{connectors-C0n12Je8.js.map → connectors-BvV8Hee2.js.map} +1 -1
- package/public/assets/{formatted-body-CzehtPtx.js → formatted-body-C1FgzlT1.js} +3 -3
- package/public/assets/{formatted-body-CzehtPtx.js.map → formatted-body-C1FgzlT1.js.map} +1 -1
- package/public/assets/{formatted-body-impl-D3Bnt0Fj.js → formatted-body-impl-BTpU1xH4.js} +2 -2
- package/public/assets/{formatted-body-impl-D3Bnt0Fj.js.map → formatted-body-impl-BTpU1xH4.js.map} +1 -1
- package/public/assets/index-DZEahmyc.js +25 -0
- package/public/assets/index-DZEahmyc.js.map +1 -0
- package/public/assets/{insights-3H6KttBi.js → insights-BqmgSpQm.js} +2 -2
- package/public/assets/{insights-3H6KttBi.js.map → insights-BqmgSpQm.js.map} +1 -1
- package/public/assets/{integrations-v14ehskT.js → integrations-CuT92zQi.js} +2 -2
- package/public/assets/{integrations-v14ehskT.js.map → integrations-CuT92zQi.js.map} +1 -1
- package/public/assets/{maintenance-BANYcBcn.js → maintenance-lM7FeXVC.js} +2 -2
- package/public/assets/{maintenance-BANYcBcn.js.map → maintenance-lM7FeXVC.js.map} +1 -1
- package/public/assets/{managed-agents-DCFMWKqt.js → managed-agents-DaPNv3yY.js} +3 -3
- package/public/assets/{managed-agents-DCFMWKqt.js.map → managed-agents-DaPNv3yY.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-CfaR-GRX.js → markdown-preview-impl-Wia3Ho1s.js} +2 -2
- package/public/assets/{markdown-preview-impl-CfaR-GRX.js.map → markdown-preview-impl-Wia3Ho1s.js.map} +1 -1
- package/public/assets/{memory-BmkUEyd5.js → memory-B1v_zD-M.js} +2 -2
- package/public/assets/{memory-BmkUEyd5.js.map → memory-B1v_zD-M.js.map} +1 -1
- package/public/assets/{messages-BQ_fVnSA.js → messages-BNOx3yK6.js} +2 -2
- package/public/assets/{messages-BQ_fVnSA.js.map → messages-BNOx3yK6.js.map} +1 -1
- package/public/assets/{orchestrators-DC7y9vQD.js → orchestrators-DWFy4asl.js} +2 -2
- package/public/assets/{orchestrators-DC7y9vQD.js.map → orchestrators-DWFy4asl.js.map} +1 -1
- package/public/assets/{overview-6lzywFHq.js → overview-C_12cFPM.js} +2 -2
- package/public/assets/{overview-6lzywFHq.js.map → overview-C_12cFPM.js.map} +1 -1
- package/public/assets/{pairs-CRrRIaHI.js → pairs-DIc7gX83.js} +2 -2
- package/public/assets/{pairs-CRrRIaHI.js.map → pairs-DIc7gX83.js.map} +1 -1
- package/public/assets/{projects-CZJW35C7.js → projects-DPjvJUzT.js} +2 -2
- package/public/assets/{projects-CZJW35C7.js.map → projects-DPjvJUzT.js.map} +1 -1
- package/public/assets/{prompt-settings-DvlnMq8H.js → prompt-settings-CtumNgG8.js} +2 -2
- package/public/assets/{prompt-settings-DvlnMq8H.js.map → prompt-settings-CtumNgG8.js.map} +1 -1
- package/public/assets/{registry-3TWg4zbE.js → registry-DzUhgMq9.js} +2 -2
- package/public/assets/{registry-3TWg4zbE.js.map → registry-DzUhgMq9.js.map} +1 -1
- package/public/assets/{security-A2VYKHeI.js → security-BJ3_u0uY.js} +2 -2
- package/public/assets/{security-A2VYKHeI.js.map → security-BJ3_u0uY.js.map} +1 -1
- package/public/assets/{select-_gK5Ojjc.js → select-AWvlctyQ.js} +2 -2
- package/public/assets/{select-_gK5Ojjc.js.map → select-AWvlctyQ.js.map} +1 -1
- package/public/assets/{settings-BZ26ewcu.js → settings-5ofqi-bN.js} +4 -4
- package/public/assets/settings-5ofqi-bN.js.map +1 -0
- package/public/assets/store-D5et8685.js +9 -0
- package/public/assets/store-D5et8685.js.map +1 -0
- package/public/assets/{tasks-DlokSl_P.js → tasks-BEE9bBf7.js} +2 -2
- package/public/assets/{tasks-DlokSl_P.js.map → tasks-BEE9bBf7.js.map} +1 -1
- package/public/assets/{teams-t7M1HMgS.js → teams-C27H_Gwo.js} +2 -2
- package/public/assets/{teams-t7M1HMgS.js.map → teams-C27H_Gwo.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-DxgrO-jH.js → terminal-viewer-impl-WD2TboA7.js} +22 -22
- package/public/assets/{terminal-viewer-impl-DxgrO-jH.js.map → terminal-viewer-impl-WD2TboA7.js.map} +1 -1
- package/public/assets/types-uVOXgvMT.js.map +1 -1
- package/public/assets/{work-queue-V8LR-3Gi.js → work-queue-CtmPK2hK.js} +2 -2
- package/public/assets/{work-queue-V8LR-3Gi.js.map → work-queue-CtmPK2hK.js.map} +1 -1
- package/public/assets/{workspaces-DI317fMk.js → workspaces-B26EKv_H.js} +2 -2
- package/public/assets/{workspaces-DI317fMk.js.map → workspaces-B26EKv_H.js.map} +1 -1
- package/public/index.html +3 -3
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/agent-card-projection.ts +39 -9
- package/src/bus-outbox.ts +30 -0
- package/src/bus.ts +57 -1
- package/src/commands-db.ts +14 -0
- package/src/config-store.ts +2 -0
- package/src/config.ts +4 -0
- package/src/db/connection.ts +3 -1
- package/src/db/delivery.ts +85 -3
- package/src/db/index.ts +1 -0
- package/src/db/maintenance-worker.ts +91 -0
- package/src/db/messages.ts +19 -4
- package/src/db/migrations.ts +7 -0
- package/src/db/retention.ts +130 -0
- package/src/db/schema.ts +5 -0
- package/src/db/stats.ts +32 -8
- package/src/db/task-entity.ts +7 -8
- package/src/event-loop-lag.ts +90 -0
- package/src/lifecycle-manager.ts +11 -12
- package/src/maintenance/jobs.ts +23 -7
- package/src/maintenance/workspaces.ts +28 -47
- package/src/managed-policy-escalation.ts +47 -0
- package/src/mcp-task-tools.ts +4 -5
- package/src/retention-config-store.ts +32 -0
- package/src/retention-config.ts +58 -0
- package/src/routes/commands.ts +8 -245
- package/src/routes/messages.ts +13 -1
- package/src/routes/tasks.ts +3 -5
- package/src/routing-policy-store.ts +11 -1
- package/src/server.ts +2 -0
- package/src/services/issue-lifecycle.ts +23 -8
- package/src/services/send-message.ts +63 -21
- package/src/services/spawn-registration-watchdog.ts +74 -9
- package/src/services/task-entity.ts +54 -2
- package/src/settings/registry.ts +11 -0
- package/src/sse.ts +54 -9
- package/src/workspace-auto-merge.ts +94 -34
- package/src/workspace-command-settle.ts +227 -0
- package/src/workspace-escalation.ts +45 -0
- package/src/workspace-merge-progress.ts +16 -0
- package/src/workspace-orphans.ts +91 -3
- package/public/assets/agents-8rQuJqii.js +0 -2
- package/public/assets/chat-jlXywb9G.js +0 -2
- package/public/assets/index-4g3PbGVN.js +0 -25
- package/public/assets/index-4g3PbGVN.js.map +0 -1
- package/public/assets/settings-BZ26ewcu.js.map +0 -1
- package/public/assets/store-nXGxgz2Q.js +0 -9
- package/public/assets/store-nXGxgz2Q.js.map +0 -1
|
@@ -1,19 +1,30 @@
|
|
|
1
|
-
// The ONE projection
|
|
1
|
+
// The ONE projection source for agent cards before they leave the relay:
|
|
2
2
|
// branch-state badge fields (#236) + the resolved `issueRepo` for chat #NNN linkify (#533).
|
|
3
3
|
//
|
|
4
4
|
// Every `agent.status` emitter MUST route through this — the direct dashboard emitter
|
|
5
|
-
// (sse.ts) AND the bus lifecycle emitter (bus.ts). The dashboard
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// which made #NNN links and branch badges flicker on/off every few seconds (#560). Keeping
|
|
9
|
-
// one projection here is the structural guarantee that projection == reality on every
|
|
10
|
-
// surface; a new emitter can't reintroduce the drift without going around this function.
|
|
5
|
+
// (sse.ts) AND the bus lifecycle emitter (bus.ts). The dashboard merges the slim status
|
|
6
|
+
// projection into its cached card, so a raw (un-enriched) status tick would still blank
|
|
7
|
+
// derived fields on fresh/new cards until the next REST poll (#560).
|
|
11
8
|
|
|
9
|
+
import { isRecord } from "agent-relay-sdk";
|
|
12
10
|
import { attachBranchState } from "./agent-branch-state";
|
|
13
11
|
import { deriveAgentBusyHealth } from "./agent-busy-health";
|
|
14
12
|
import { attachIssueRepo } from "./agent-issue-repo";
|
|
15
13
|
import type { AgentCard } from "./types";
|
|
16
14
|
|
|
15
|
+
const OMITTED_STATUS_META_KEYS = new Set([
|
|
16
|
+
"agentProfile",
|
|
17
|
+
"auth",
|
|
18
|
+
"providerCapabilities",
|
|
19
|
+
"lifecycleCapabilities",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const OMITTED_STATUS_WORKSPACE_KEYS = new Set([
|
|
23
|
+
"deps",
|
|
24
|
+
"probe",
|
|
25
|
+
"symlinks",
|
|
26
|
+
]);
|
|
27
|
+
|
|
17
28
|
/** Enrich an AgentCard with the derived fields the dashboard expects on every surface. */
|
|
18
29
|
export function projectAgentCard(agent: AgentCard): AgentCard {
|
|
19
30
|
const projected = attachIssueRepo(attachBranchState(agent));
|
|
@@ -21,11 +32,30 @@ export function projectAgentCard(agent: AgentCard): AgentCard {
|
|
|
21
32
|
return busyHealth ? { ...projected, meta: { ...(projected.meta ?? {}), busyHealth } } : projected;
|
|
22
33
|
}
|
|
23
34
|
|
|
35
|
+
function projectStatusMeta(meta: AgentCard["meta"]): AgentCard["meta"] | undefined {
|
|
36
|
+
if (!meta) return undefined;
|
|
37
|
+
const projected: Record<string, unknown> = {};
|
|
38
|
+
for (const [key, value] of Object.entries(meta)) {
|
|
39
|
+
if (OMITTED_STATUS_META_KEYS.has(key)) continue;
|
|
40
|
+
if (key === "workspace" && isRecord(value)) {
|
|
41
|
+
projected.workspace = Object.fromEntries(Object.entries(value).filter(([workspaceKey]) => !OMITTED_STATUS_WORKSPACE_KEYS.has(workspaceKey)));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
projected[key] = value;
|
|
45
|
+
}
|
|
46
|
+
return projected;
|
|
47
|
+
}
|
|
48
|
+
|
|
24
49
|
/** The `agent.status` SSE payload: the enriched card when the agent is known, else the
|
|
25
|
-
* minimal offline stub the dashboard treats as "agent gone". Shared by both emitters.
|
|
50
|
+
* minimal offline stub the dashboard treats as "agent gone". Shared by both emitters.
|
|
51
|
+
* Runtime status stays live, while bulky static/secret launch projection fields stay
|
|
52
|
+
* behind GET /api/agents/:id and the normal dashboard polling path. */
|
|
26
53
|
export function projectAgentStatusData(
|
|
27
54
|
agentId: string,
|
|
28
55
|
agent: AgentCard | null | undefined,
|
|
29
56
|
): AgentCard | { id: string; status: string } {
|
|
30
|
-
|
|
57
|
+
if (!agent) return { id: agentId, status: "offline" };
|
|
58
|
+
const projected = projectAgentCard(agent);
|
|
59
|
+
const meta = projectStatusMeta(projected.meta);
|
|
60
|
+
return meta ? { ...projected, meta } : { ...projected, meta: undefined };
|
|
31
61
|
}
|
package/src/bus-outbox.ts
CHANGED
|
@@ -20,6 +20,23 @@ interface OutboxRow {
|
|
|
20
20
|
|
|
21
21
|
export function appendEvent(type: string, source: string, data: unknown, subject?: string): number {
|
|
22
22
|
const timestamp = Date.now();
|
|
23
|
+
if (type === "message.batch") {
|
|
24
|
+
const messages = batchMessages(data);
|
|
25
|
+
if (messages.length > 0) {
|
|
26
|
+
return getDb().transaction(() => {
|
|
27
|
+
let firstSeq = 0;
|
|
28
|
+
const insert = getDb().query(`
|
|
29
|
+
INSERT INTO bus_outbox (event_type, source, subject, data, timestamp)
|
|
30
|
+
VALUES (?, ?, ?, ?, ?)
|
|
31
|
+
`);
|
|
32
|
+
for (const message of messages) {
|
|
33
|
+
const result = insert.run("message.new", message.from, String(message.id), JSON.stringify(message), timestamp);
|
|
34
|
+
if (firstSeq === 0) firstSeq = Number(result.lastInsertRowid);
|
|
35
|
+
}
|
|
36
|
+
return firstSeq;
|
|
37
|
+
})();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
23
40
|
const result = getDb().query(`
|
|
24
41
|
INSERT INTO bus_outbox (event_type, source, subject, data, timestamp)
|
|
25
42
|
VALUES (?, ?, ?, ?, ?)
|
|
@@ -73,3 +90,16 @@ function parseData(raw: string): Record<string, unknown> {
|
|
|
73
90
|
return {};
|
|
74
91
|
}
|
|
75
92
|
}
|
|
93
|
+
|
|
94
|
+
function batchMessages(data: unknown): Array<{ id: number; from: string } & Record<string, unknown>> {
|
|
95
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return [];
|
|
96
|
+
const messages = (data as { messages?: unknown }).messages;
|
|
97
|
+
if (!Array.isArray(messages)) return [];
|
|
98
|
+
return messages.filter((message): message is { id: number; from: string } & Record<string, unknown> =>
|
|
99
|
+
Boolean(message) &&
|
|
100
|
+
typeof message === "object" &&
|
|
101
|
+
!Array.isArray(message) &&
|
|
102
|
+
typeof (message as { id?: unknown }).id === "number" &&
|
|
103
|
+
typeof (message as { from?: unknown }).from === "string"
|
|
104
|
+
);
|
|
105
|
+
}
|
package/src/bus.ts
CHANGED
|
@@ -471,7 +471,7 @@ function handleResume(ws: BusWebSocket, frameId: string, since: number): void {
|
|
|
471
471
|
sendError(ws, frameId, "REPLAY_GAP", "requested cursor is older than retained bus outbox events");
|
|
472
472
|
return;
|
|
473
473
|
}
|
|
474
|
-
const events = replayEvents(since).
|
|
474
|
+
const events = replayEvents(since).flatMap(outboxToPayloads);
|
|
475
475
|
send(ws, {
|
|
476
476
|
type: "resumed",
|
|
477
477
|
payload: {
|
|
@@ -503,6 +503,10 @@ function withRegistered(
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
function broadcastRelayEvent(event: RelayEvent): void {
|
|
506
|
+
if (event.type === "message.batch") {
|
|
507
|
+
broadcastMessageBatch(event);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
506
510
|
const frame = JSON.stringify({
|
|
507
511
|
type: "event",
|
|
508
512
|
payload: {
|
|
@@ -521,6 +525,41 @@ function broadcastRelayEvent(event: RelayEvent): void {
|
|
|
521
525
|
}
|
|
522
526
|
}
|
|
523
527
|
|
|
528
|
+
function broadcastMessageBatch(event: RelayEvent): void {
|
|
529
|
+
const messages = batchMessages(event.data);
|
|
530
|
+
const messageEvents = messages.map((message, index) => {
|
|
531
|
+
const messageEvent: RelayEvent = {
|
|
532
|
+
...event,
|
|
533
|
+
seq: event.seq + index,
|
|
534
|
+
type: "message.new",
|
|
535
|
+
source: message.from,
|
|
536
|
+
subject: String(message.id),
|
|
537
|
+
data: message as unknown as Record<string, unknown>,
|
|
538
|
+
};
|
|
539
|
+
return {
|
|
540
|
+
event: messageEvent,
|
|
541
|
+
frame: JSON.stringify({
|
|
542
|
+
type: "event",
|
|
543
|
+
payload: {
|
|
544
|
+
seq: messageEvent.seq,
|
|
545
|
+
eventType: "message.new",
|
|
546
|
+
source: message.from,
|
|
547
|
+
subject: String(message.id),
|
|
548
|
+
data: message,
|
|
549
|
+
timestamp: event.timestamp,
|
|
550
|
+
},
|
|
551
|
+
}),
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
const agentCache = new Map<string, AgentCard | null>();
|
|
555
|
+
for (const conn of busConnections.values()) {
|
|
556
|
+
for (const { event: messageEvent, frame } of messageEvents) {
|
|
557
|
+
if (!connectionWantsEvent(conn, messageEvent, agentCache)) continue;
|
|
558
|
+
conn.ws.send(frame);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
524
563
|
function fanoutAgent(agentId: string, cache: Map<string, AgentCard | null>): AgentCard | null {
|
|
525
564
|
if (!cache.has(agentId)) cache.set(agentId, getAgent(agentId));
|
|
526
565
|
return cache.get(agentId) ?? null;
|
|
@@ -565,6 +604,23 @@ function outboxToPayload(event: BusEvent) {
|
|
|
565
604
|
};
|
|
566
605
|
}
|
|
567
606
|
|
|
607
|
+
function outboxToPayloads(event: BusEvent) {
|
|
608
|
+
if (event.eventType !== "message.batch") return [outboxToPayload(event)];
|
|
609
|
+
return batchMessages(event.data).map((message, index) => ({
|
|
610
|
+
seq: event.seq + index,
|
|
611
|
+
eventType: "message.new",
|
|
612
|
+
source: message.from,
|
|
613
|
+
subject: String(message.id),
|
|
614
|
+
data: message as unknown as Record<string, unknown>,
|
|
615
|
+
timestamp: event.timestamp,
|
|
616
|
+
}));
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function batchMessages(data: Record<string, unknown>): Message[] {
|
|
620
|
+
const messages = (data as { messages?: unknown }).messages;
|
|
621
|
+
return Array.isArray(messages) ? messages as Message[] : [];
|
|
622
|
+
}
|
|
623
|
+
|
|
568
624
|
function emitAgentStatusEvent(agentId: string): void {
|
|
569
625
|
const agent = getAgent(agentId);
|
|
570
626
|
// Same enriched projection as sse.ts's emitAgentStatus — without this the bus lifecycle
|
package/src/commands-db.ts
CHANGED
|
@@ -38,11 +38,13 @@ const TERMINAL_STATUSES: CommandStatus[] = ["succeeded", "failed", "canceled", "
|
|
|
38
38
|
// committed to doing the work, not from enqueue.
|
|
39
39
|
const SPAWN_TTL_MS = 180_000;
|
|
40
40
|
const LIFECYCLE_TTL_MS = 30_000;
|
|
41
|
+
const WORKSPACE_COMMAND_TTL_MS = 20 * 60_000;
|
|
41
42
|
const DISPATCH_STATUSES: CommandStatus[] = ["accepted", "running"];
|
|
42
43
|
|
|
43
44
|
function commandTtlMs(type: string): number | undefined {
|
|
44
45
|
if (type === "agent.spawn") return SPAWN_TTL_MS;
|
|
45
46
|
if (type === "agent.shutdown" || type === "agent.restart" || type === "agent.reconnect" || type === "agent.kill" || type === "agent.compact" || type === "agent.clearContext" || type === "agent.injectContext") return LIFECYCLE_TTL_MS;
|
|
47
|
+
if (type === "workspace.merge" || type === "workspace.cleanup" || type === "workspace.reconcile" || type === "workspace.idle-refresh") return WORKSPACE_COMMAND_TTL_MS;
|
|
46
48
|
return undefined;
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -148,6 +150,18 @@ export function findSpawnCommandByRequestId(spawnRequestId: string, statuses?: C
|
|
|
148
150
|
return rows[0] ? rowToCommand(rows[0]) : null;
|
|
149
151
|
}
|
|
150
152
|
|
|
153
|
+
export function findLatestCommandByCorrelation(type: string, correlationId: string): Command | null {
|
|
154
|
+
const row = getDb()
|
|
155
|
+
.query(`
|
|
156
|
+
SELECT * FROM commands
|
|
157
|
+
WHERE type = ? AND correlation_id = ?
|
|
158
|
+
ORDER BY created_at DESC
|
|
159
|
+
LIMIT 1
|
|
160
|
+
`)
|
|
161
|
+
.get(type, correlationId) as CommandRow | undefined;
|
|
162
|
+
return row ? rowToCommand(row) : null;
|
|
163
|
+
}
|
|
164
|
+
|
|
151
165
|
export function countInFlightSpawnCommands(parentId: string): number {
|
|
152
166
|
const row = getDb()
|
|
153
167
|
.query(
|
package/src/config-store.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
validateNotificationsConfig,
|
|
10
10
|
} from "./notification-settings";
|
|
11
11
|
import { DEFAULT_SELF_RESUME_CONFIG, validateSelfResumeConfig } from "./self-resume-config";
|
|
12
|
+
import { RETENTION_NAMESPACE, validateRetentionConfig } from "./retention-config";
|
|
12
13
|
import { TEAM_TEMPLATE_NAMESPACE, validateTeamTemplate } from "./team-template-config";
|
|
13
14
|
import { PROJECT_INSTRUCTION_POLICY_NAMES } from "./project-instructions";
|
|
14
15
|
import { renderTemplate } from "./prompt-resolver";
|
|
@@ -674,6 +675,7 @@ function normalizeValue(namespace: string, key: string, value: unknown): unknown
|
|
|
674
675
|
if (namespace === SELF_RESUME_NAMESPACE) return validateSelfResumeConfig(value);
|
|
675
676
|
if (namespace === WORKSPACE_NAMESPACE) return validateWorkspaceConfig(value);
|
|
676
677
|
if (namespace === LANDING_NAMESPACE) return validateLandingConfig(value);
|
|
678
|
+
if (namespace === RETENTION_NAMESPACE) return validateRetentionConfig(value);
|
|
677
679
|
if (namespace === TEAM_TEMPLATE_NAMESPACE) return validateTeamTemplate(key, value);
|
|
678
680
|
if (JSON.stringify(value) === undefined) throw new ValidationError("value must be valid JSON");
|
|
679
681
|
return value;
|
package/src/config.ts
CHANGED
|
@@ -64,6 +64,10 @@ export const STUCK_BUSY_PROVIDER_TURN_MS = envPositiveInt("AGENT_RELAY_STUCK_BUS
|
|
|
64
64
|
export const STUCK_BUSY_STARTUP_MS = envPositiveInt("AGENT_RELAY_STUCK_BUSY_STARTUP_MS", STUCK_BUSY_THRESHOLD_MS);
|
|
65
65
|
export const STUCK_BUSY_RECONNECT_MS = envPositiveInt("AGENT_RELAY_STUCK_BUSY_RECONNECT_MS", STUCK_BUSY_THRESHOLD_MS);
|
|
66
66
|
export const STUCK_BUSY_SHUTDOWN_MS = envPositiveInt("AGENT_RELAY_STUCK_BUSY_SHUTDOWN_MS", STUCK_BUSY_THRESHOLD_MS);
|
|
67
|
+
export const EVENT_LOOP_LAG_SAMPLE_MS = envPositiveInt("AGENT_RELAY_EVENT_LOOP_LAG_SAMPLE_MS", 1_000);
|
|
68
|
+
export const EVENT_LOOP_LAG_WINDOW_SAMPLES = envPositiveInt("AGENT_RELAY_EVENT_LOOP_LAG_WINDOW_SAMPLES", 60);
|
|
69
|
+
export const EVENT_LOOP_LAG_WARN_MS = envNonNegativeNumber("AGENT_RELAY_EVENT_LOOP_LAG_WARN_MS", 250);
|
|
70
|
+
export const EVENT_LOOP_LAG_LOG_MS = envNonNegativeNumber("AGENT_RELAY_EVENT_LOOP_LAG_LOG_MS", EVENT_LOOP_LAG_WARN_MS);
|
|
67
71
|
// #713 — provider-token silence ALONE is not a wedge signal for Claude (long turns, extended
|
|
68
72
|
// thinking, and native compaction all go token-silent while healthy). Before declaring stuck we
|
|
69
73
|
// ALSO require the transport heartbeat to be genuinely dead: no server frame / heartbeat-ack within
|
package/src/db/connection.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { DB_SLOW_QUERY_MS } from "../config";
|
|
4
5
|
|
|
@@ -79,7 +80,8 @@ export async function runDbMaintenanceWorker<T>(operation: string, args: string[
|
|
|
79
80
|
throw new Error("database worker requires a file-backed database");
|
|
80
81
|
}
|
|
81
82
|
if (signal?.aborted) throw new Error("database worker aborted");
|
|
82
|
-
const
|
|
83
|
+
const bunPath = existsSync(process.execPath) ? process.execPath : (Bun.which("bun") ?? process.execPath);
|
|
84
|
+
const proc = Bun.spawn([bunPath, join(import.meta.dir, "maintenance-worker.ts"), operation, dbPath, ...args], {
|
|
83
85
|
stdin: "ignore",
|
|
84
86
|
stdout: "pipe",
|
|
85
87
|
stderr: "pipe",
|
package/src/db/delivery.ts
CHANGED
|
@@ -474,13 +474,95 @@ export function resolveQueuedDirectMessages(agentId: string): Message[] {
|
|
|
474
474
|
* message that died without ever reaching its recipient — as a health signal.
|
|
475
475
|
*/
|
|
476
476
|
export function countExpiredUndeliveredMessages(now: number = Date.now(), windowMs = 86_400_000): number {
|
|
477
|
-
return (
|
|
478
|
-
|
|
477
|
+
return summarizeExpiredUndeliveredMessages(now, windowMs).unexplainedCount;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
interface ExpiredUndeliveredRow {
|
|
481
|
+
id: number;
|
|
482
|
+
toTarget: string;
|
|
483
|
+
kind: string;
|
|
484
|
+
subject?: string | null;
|
|
485
|
+
policyValue?: string | null;
|
|
486
|
+
policyStatus?: string | null;
|
|
487
|
+
policyAgentId?: string | null;
|
|
488
|
+
policyLastError?: string | null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export interface ExpiredUndeliveredMessagesSummary {
|
|
492
|
+
totalCount: number;
|
|
493
|
+
unexplainedCount: number;
|
|
494
|
+
explainedManagedPolicyCount: number;
|
|
495
|
+
subjects: NonNullable<HealthCheck["subjects"]>;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function managedPolicyExpiryExplanation(row: ExpiredUndeliveredRow): string | undefined {
|
|
499
|
+
if (!row.toTarget.startsWith("policy:")) return undefined;
|
|
500
|
+
if (typeof row.policyValue !== "string") return undefined;
|
|
501
|
+
const policy = parseJson<Record<string, unknown>>(row.policyValue, {});
|
|
502
|
+
if (policy.enabled === false) return "managed policy is disabled";
|
|
503
|
+
if (row.policyStatus === "backoff" || row.policyStatus === "failed") {
|
|
504
|
+
return `managed policy is ${row.policyStatus}${row.policyLastError ? `: ${row.policyLastError}` : ""}`;
|
|
505
|
+
}
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Classify queued-message expiries that happened within the trailing window.
|
|
511
|
+
* Channel/direct/unknown expiries remain a silent-loss health warning. Managed
|
|
512
|
+
* policy expiries with an explicit unavailable policy state are explained by the
|
|
513
|
+
* policy health surface instead, so they stay visible without degrading global
|
|
514
|
+
* health as generic message loss (#998).
|
|
515
|
+
*/
|
|
516
|
+
export function summarizeExpiredUndeliveredMessages(now: number = Date.now(), windowMs = 86_400_000): ExpiredUndeliveredMessagesSummary {
|
|
517
|
+
const rows = getDb().query(`
|
|
518
|
+
SELECT
|
|
519
|
+
m.id,
|
|
520
|
+
m.to_target AS toTarget,
|
|
521
|
+
m.kind,
|
|
522
|
+
m.subject,
|
|
523
|
+
c.value AS policyValue,
|
|
524
|
+
s.status AS policyStatus,
|
|
525
|
+
s.agent_id AS policyAgentId,
|
|
526
|
+
s.last_error AS policyLastError
|
|
527
|
+
FROM messages m
|
|
528
|
+
LEFT JOIN config c
|
|
529
|
+
ON c.namespace = 'spawn-policy'
|
|
530
|
+
AND c.key = substr(m.to_target, 8)
|
|
531
|
+
LEFT JOIN managed_agent_state s
|
|
532
|
+
ON s.policy_name = substr(m.to_target, 8)
|
|
479
533
|
WHERE delivery_status = 'dead'
|
|
480
534
|
AND delivery_poison_reason = 'queued message expired'
|
|
481
535
|
AND delivery_updated_at IS NOT NULL
|
|
482
536
|
AND delivery_updated_at >= ?
|
|
483
|
-
`).
|
|
537
|
+
`).all(now - windowMs) as ExpiredUndeliveredRow[];
|
|
538
|
+
|
|
539
|
+
let unexplainedCount = 0;
|
|
540
|
+
let explainedManagedPolicyCount = 0;
|
|
541
|
+
const explainedByTarget = new Map<string, { count: number; detail: string }>();
|
|
542
|
+
|
|
543
|
+
for (const row of rows) {
|
|
544
|
+
const explanation = managedPolicyExpiryExplanation(row);
|
|
545
|
+
if (!explanation) {
|
|
546
|
+
unexplainedCount += 1;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
explainedManagedPolicyCount += 1;
|
|
550
|
+
const existing = explainedByTarget.get(row.toTarget);
|
|
551
|
+
if (existing) existing.count += 1;
|
|
552
|
+
else explainedByTarget.set(row.toTarget, { count: 1, detail: explanation });
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
totalCount: rows.length,
|
|
557
|
+
unexplainedCount,
|
|
558
|
+
explainedManagedPolicyCount,
|
|
559
|
+
subjects: [...explainedByTarget.entries()].slice(0, 10).map(([target, item]) => ({
|
|
560
|
+
id: target,
|
|
561
|
+
label: target,
|
|
562
|
+
status: "explained",
|
|
563
|
+
detail: `${item.count} expired message(s); ${item.detail}`,
|
|
564
|
+
})),
|
|
565
|
+
};
|
|
484
566
|
}
|
|
485
567
|
|
|
486
568
|
export function expireQueuedMessages(now: number = Date.now()): Message[] {
|
package/src/db/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./continuations.ts";
|
|
|
33
33
|
export * from "./continuation-archives.ts";
|
|
34
34
|
export * from "./crash-reports.ts";
|
|
35
35
|
export * from "./terminal-events.ts";
|
|
36
|
+
export * from "./retention.ts";
|
|
36
37
|
export * from "./plans.ts";
|
|
37
38
|
export * from "./teams.ts";
|
|
38
39
|
export * from "./blackboard.ts";
|
|
@@ -29,6 +29,97 @@ try {
|
|
|
29
29
|
return db.query("DELETE FROM messages WHERE created_at < ?").run(cutoff).changes;
|
|
30
30
|
})();
|
|
31
31
|
console.log(JSON.stringify({ prunedMessages }));
|
|
32
|
+
} else if (operation === "retention-prune") {
|
|
33
|
+
if (!arg) throw new Error("retention-prune requires options JSON");
|
|
34
|
+
const opts = JSON.parse(arg) as {
|
|
35
|
+
messageCutoff: number;
|
|
36
|
+
activityCutoff: number;
|
|
37
|
+
activityMaxRows: number;
|
|
38
|
+
spawnCommandCutoff: number;
|
|
39
|
+
batchSize: number;
|
|
40
|
+
};
|
|
41
|
+
const batchSize = Math.min(Math.max(Math.trunc(opts.batchSize), 1), 50_000);
|
|
42
|
+
const result = db.transaction(() => {
|
|
43
|
+
db.query(`
|
|
44
|
+
UPDATE messages
|
|
45
|
+
SET reply_to = NULL
|
|
46
|
+
WHERE reply_to IN (
|
|
47
|
+
SELECT id FROM messages
|
|
48
|
+
WHERE created_at < ?
|
|
49
|
+
ORDER BY created_at ASC, id ASC
|
|
50
|
+
LIMIT ?
|
|
51
|
+
)
|
|
52
|
+
`).run(opts.messageCutoff, batchSize);
|
|
53
|
+
const prunedMessages = db.query(`
|
|
54
|
+
DELETE FROM messages
|
|
55
|
+
WHERE id IN (
|
|
56
|
+
SELECT id FROM messages
|
|
57
|
+
WHERE created_at < ?
|
|
58
|
+
ORDER BY created_at ASC, id ASC
|
|
59
|
+
LIMIT ?
|
|
60
|
+
)
|
|
61
|
+
`).run(opts.messageCutoff, batchSize).changes;
|
|
62
|
+
const prunedActivityEvents = db.query(`
|
|
63
|
+
DELETE FROM activity_events
|
|
64
|
+
WHERE id IN (
|
|
65
|
+
SELECT ae.id
|
|
66
|
+
FROM activity_events ae
|
|
67
|
+
WHERE (
|
|
68
|
+
ae.created_at < ?
|
|
69
|
+
OR ae.id NOT IN (
|
|
70
|
+
SELECT id
|
|
71
|
+
FROM activity_events
|
|
72
|
+
ORDER BY created_at DESC, id DESC
|
|
73
|
+
LIMIT ?
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
AND NOT (
|
|
77
|
+
ae.kind = 'state'
|
|
78
|
+
AND ae.title = 'Agent registered'
|
|
79
|
+
AND EXISTS (
|
|
80
|
+
SELECT 1
|
|
81
|
+
FROM commands c
|
|
82
|
+
WHERE c.type = 'agent.spawn'
|
|
83
|
+
AND c.status = 'succeeded'
|
|
84
|
+
AND coalesce(json_extract(c.params, '$.registrationConfirmed'), 0) != 1
|
|
85
|
+
AND (
|
|
86
|
+
c.correlation_id = json_extract(ae.metadata, '$.spawnRequestId')
|
|
87
|
+
OR json_extract(c.params, '$.spawnRequestId') = json_extract(ae.metadata, '$.spawnRequestId')
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
ORDER BY ae.created_at ASC, ae.id ASC
|
|
92
|
+
LIMIT ?
|
|
93
|
+
)
|
|
94
|
+
`).run(opts.activityCutoff, opts.activityMaxRows, batchSize).changes;
|
|
95
|
+
const prunedSpawnCommands = db.query(`
|
|
96
|
+
DELETE FROM commands
|
|
97
|
+
WHERE id IN (
|
|
98
|
+
SELECT c.id
|
|
99
|
+
FROM commands c
|
|
100
|
+
WHERE c.type = 'agent.spawn'
|
|
101
|
+
AND c.status IN ('succeeded', 'failed', 'canceled', 'timed_out')
|
|
102
|
+
AND c.updated_at < ?
|
|
103
|
+
AND NOT (
|
|
104
|
+
c.status = 'succeeded'
|
|
105
|
+
AND coalesce(json_extract(c.params, '$.registrationConfirmed'), 0) != 1
|
|
106
|
+
)
|
|
107
|
+
AND NOT EXISTS (
|
|
108
|
+
SELECT 1
|
|
109
|
+
FROM managed_agent_state mas
|
|
110
|
+
WHERE mas.spawn_request_id IS NOT NULL
|
|
111
|
+
AND (
|
|
112
|
+
mas.spawn_request_id = c.correlation_id
|
|
113
|
+
OR mas.spawn_request_id = json_extract(c.params, '$.spawnRequestId')
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
ORDER BY c.updated_at ASC, c.created_at ASC
|
|
117
|
+
LIMIT ?
|
|
118
|
+
)
|
|
119
|
+
`).run(opts.spawnCommandCutoff, batchSize).changes;
|
|
120
|
+
return { prunedMessages, prunedActivityEvents, prunedSpawnCommands };
|
|
121
|
+
})();
|
|
122
|
+
console.log(JSON.stringify(result));
|
|
32
123
|
} else {
|
|
33
124
|
throw new Error(`unknown maintenance operation: ${operation}`);
|
|
34
125
|
}
|
package/src/db/messages.ts
CHANGED
|
@@ -402,7 +402,21 @@ function sanitizeOccurredAt(occurredAt: number | undefined, receivedAt: number):
|
|
|
402
402
|
return Math.floor(occurredAt);
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
-
export
|
|
405
|
+
export interface SendMessageResult {
|
|
406
|
+
message: Message;
|
|
407
|
+
created: boolean;
|
|
408
|
+
updated?: boolean;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function sendMessageWithResult(input: SendMessageInput): SendMessageResult {
|
|
412
|
+
return sendMessageWithResultInternal(input, true);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function sendMessagesBatchWithResult(inputs: SendMessageInput[]): SendMessageResult[] {
|
|
416
|
+
return getDb().transaction(() => inputs.map((input) => sendMessageWithResultInternal(input, false)))();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function sendMessageWithResultInternal(input: SendMessageInput, wrapTransaction: boolean): SendMessageResult {
|
|
406
420
|
const now = Date.now();
|
|
407
421
|
const payload = input.payload ?? {};
|
|
408
422
|
const attachmentRefs = cleanAttachmentRefs(payload);
|
|
@@ -495,7 +509,7 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
|
|
|
495
509
|
maxAgeSeconds = DEFAULT_FANOUT_TTL_SECONDS;
|
|
496
510
|
}
|
|
497
511
|
|
|
498
|
-
const
|
|
512
|
+
const insertMessage = () => {
|
|
499
513
|
const result = insert.run({
|
|
500
514
|
$from: input.from,
|
|
501
515
|
$to: input.to,
|
|
@@ -528,7 +542,8 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
|
|
|
528
542
|
else enforceQueueLimit(input.to);
|
|
529
543
|
}
|
|
530
544
|
return newId;
|
|
531
|
-
}
|
|
545
|
+
};
|
|
546
|
+
const id = wrapTransaction ? getDb().transaction(insertMessage)() : insertMessage();
|
|
532
547
|
|
|
533
548
|
return { message: getMessage(id)!, created: true };
|
|
534
549
|
}
|
|
@@ -543,7 +558,7 @@ export function getThread(messageId: number): Message[] {
|
|
|
543
558
|
const threadId = msg.threadId ?? msg.id;
|
|
544
559
|
return (
|
|
545
560
|
getDb()
|
|
546
|
-
.query(`${MSG_SELECT} WHERE m.thread_id = ? ORDER BY m.created_at ASC`)
|
|
561
|
+
.query(`${MSG_SELECT} WHERE m.thread_id = ? ORDER BY m.created_at ASC, m.id ASC`)
|
|
547
562
|
.all(threadId) as any[]
|
|
548
563
|
).map(rowToMessage);
|
|
549
564
|
}
|
package/src/db/migrations.ts
CHANGED
|
@@ -706,6 +706,7 @@ export function applyMigrations(): void {
|
|
|
706
706
|
getDb().run("ALTER TABLE agents ADD COLUMN team_id TEXT");
|
|
707
707
|
}
|
|
708
708
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_agents_team ON agents(team_id)");
|
|
709
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_agents_spawn_request ON agents(json_extract(meta, '$.spawnRequestId'))");
|
|
709
710
|
getDb().run(`
|
|
710
711
|
CREATE TABLE IF NOT EXISTS notification_subscriptions (
|
|
711
712
|
id TEXT PRIMARY KEY,
|
|
@@ -913,6 +914,12 @@ export function applyMigrations(): void {
|
|
|
913
914
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_agent_active_memories_agent ON agent_active_memories(agent_id)");
|
|
914
915
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_agents_label ON agents(label)");
|
|
915
916
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_agents_kind ON agents(kind)");
|
|
917
|
+
getDb().run(`
|
|
918
|
+
CREATE INDEX IF NOT EXISTS idx_activity_registered_spawn_request
|
|
919
|
+
ON activity_events(json_extract(metadata, '$.spawnRequestId'))
|
|
920
|
+
WHERE kind = 'state' AND title = 'Agent registered'
|
|
921
|
+
`);
|
|
922
|
+
getDb().run("CREATE INDEX IF NOT EXISTS idx_commands_spawn_watchdog ON commands(type, status, updated_at)");
|
|
916
923
|
|
|
917
924
|
getDb().run(`
|
|
918
925
|
CREATE TABLE IF NOT EXISTS integration_registry (
|