agent-relay-server 0.120.6 → 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-So86mU9-.js → MessageBubble-BaPGFJxq.js} +2 -2
- package/public/assets/{MessageBubble-So86mU9-.js.map → MessageBubble-BaPGFJxq.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-DBZYAsww.js → PlanGraphPanel-BXCVfmln.js} +2 -2
- package/public/assets/{PlanGraphPanel-DBZYAsww.js.map → PlanGraphPanel-BXCVfmln.js.map} +1 -1
- package/public/assets/{activity-CZNwmXg1.js → activity-D_shwAZt.js} +2 -2
- package/public/assets/{activity-CZNwmXg1.js.map → activity-D_shwAZt.js.map} +1 -1
- package/public/assets/{agent-profiles-BhadoGwi.js → agent-profiles-BrfVz5IV.js} +2 -2
- package/public/assets/{agent-profiles-BhadoGwi.js.map → agent-profiles-BrfVz5IV.js.map} +1 -1
- package/public/assets/{agents-DWn1xx9x.js → agents-B5w-Z9Ic.js} +2 -2
- package/public/assets/{agents-DWn1xx9x.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-BExrtKkb.js → automation-DOTL8BFs.js} +2 -2
- package/public/assets/{automation-BExrtKkb.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-C7OhKtr6.js → chat-PeU3iOaa.js} +2 -2
- package/public/assets/{chat-C7OhKtr6.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-BCQT2qSb.js → formatted-body-C1FgzlT1.js} +3 -3
- package/public/assets/{formatted-body-BCQT2qSb.js.map → formatted-body-C1FgzlT1.js.map} +1 -1
- package/public/assets/{formatted-body-impl-Bk7wnESl.js → formatted-body-impl-BTpU1xH4.js} +2 -2
- package/public/assets/{formatted-body-impl-Bk7wnESl.js.map → formatted-body-impl-BTpU1xH4.js.map} +1 -1
- package/public/assets/{index-CRraCeo_.js → index-DZEahmyc.js} +6 -6
- package/public/assets/{index-CRraCeo_.js.map → index-DZEahmyc.js.map} +1 -1
- package/public/assets/{insights-DEgES6jz.js → insights-BqmgSpQm.js} +2 -2
- package/public/assets/{insights-DEgES6jz.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-BgVB6_gO.js → maintenance-lM7FeXVC.js} +2 -2
- package/public/assets/{maintenance-BgVB6_gO.js.map → maintenance-lM7FeXVC.js.map} +1 -1
- package/public/assets/{managed-agents-8_NKSkhI.js → managed-agents-DaPNv3yY.js} +2 -2
- package/public/assets/{managed-agents-8_NKSkhI.js.map → managed-agents-DaPNv3yY.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-yxQkrd2M.js → markdown-preview-impl-Wia3Ho1s.js} +2 -2
- package/public/assets/{markdown-preview-impl-yxQkrd2M.js.map → markdown-preview-impl-Wia3Ho1s.js.map} +1 -1
- package/public/assets/{memory-CJD8eekg.js → memory-B1v_zD-M.js} +2 -2
- package/public/assets/{memory-CJD8eekg.js.map → memory-B1v_zD-M.js.map} +1 -1
- package/public/assets/{messages-DLHMZbHZ.js → messages-BNOx3yK6.js} +2 -2
- package/public/assets/{messages-DLHMZbHZ.js.map → messages-BNOx3yK6.js.map} +1 -1
- package/public/assets/{orchestrators-DEOep7FX.js → orchestrators-DWFy4asl.js} +2 -2
- package/public/assets/{orchestrators-DEOep7FX.js.map → orchestrators-DWFy4asl.js.map} +1 -1
- package/public/assets/{overview-BgpfO6mt.js → overview-C_12cFPM.js} +2 -2
- package/public/assets/{overview-BgpfO6mt.js.map → overview-C_12cFPM.js.map} +1 -1
- package/public/assets/{pairs-8NVKHh96.js → pairs-DIc7gX83.js} +2 -2
- package/public/assets/{pairs-8NVKHh96.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-fqyLwUa-.js → prompt-settings-CtumNgG8.js} +2 -2
- package/public/assets/{prompt-settings-fqyLwUa-.js.map → prompt-settings-CtumNgG8.js.map} +1 -1
- package/public/assets/{registry-BKbLL4mO.js → registry-DzUhgMq9.js} +2 -2
- package/public/assets/{registry-BKbLL4mO.js.map → registry-DzUhgMq9.js.map} +1 -1
- package/public/assets/{security-BlTFTbq0.js → security-BJ3_u0uY.js} +2 -2
- package/public/assets/{security-BlTFTbq0.js.map → security-BJ3_u0uY.js.map} +1 -1
- package/public/assets/{select-Zd_qh0uU.js → select-AWvlctyQ.js} +2 -2
- package/public/assets/{select-Zd_qh0uU.js.map → select-AWvlctyQ.js.map} +1 -1
- package/public/assets/{settings-nHuSVRkL.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-C3JhQ8vB.js → tasks-BEE9bBf7.js} +2 -2
- package/public/assets/{tasks-C3JhQ8vB.js.map → tasks-BEE9bBf7.js.map} +1 -1
- package/public/assets/{teams-C9E9Yt39.js → teams-C27H_Gwo.js} +2 -2
- package/public/assets/{teams-C9E9Yt39.js.map → teams-C27H_Gwo.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-FP0JlsvS.js → terminal-viewer-impl-WD2TboA7.js} +2 -2
- package/public/assets/{terminal-viewer-impl-FP0JlsvS.js.map → terminal-viewer-impl-WD2TboA7.js.map} +1 -1
- package/public/assets/types-uVOXgvMT.js.map +1 -1
- package/public/assets/{work-queue-Ema7Hy3s.js → work-queue-CtmPK2hK.js} +2 -2
- package/public/assets/{work-queue-Ema7Hy3s.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/config-store.ts +2 -0
- package/src/config.ts +4 -0
- package/src/db/connection.ts +3 -1
- 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 +22 -0
- package/src/event-loop-lag.ts +90 -0
- package/src/maintenance/jobs.ts +20 -6
- package/src/retention-config-store.ts +32 -0
- package/src/retention-config.ts +58 -0
- package/src/routes/messages.ts +13 -1
- package/src/routing-policy-store.ts +11 -1
- package/src/server.ts +2 -0
- package/src/services/send-message.ts +63 -21
- package/src/services/spawn-registration-watchdog.ts +74 -9
- package/src/settings/registry.ts +11 -0
- package/src/sse.ts +54 -9
- package/public/assets/settings-nHuSVRkL.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/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/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 (
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { getDb, getDbPath, runDbMaintenanceWorker } from "./connection.ts";
|
|
2
|
+
|
|
3
|
+
interface RetentionPruneInput {
|
|
4
|
+
messageCutoff: number;
|
|
5
|
+
activityCutoff: number;
|
|
6
|
+
activityMaxRows: number;
|
|
7
|
+
spawnCommandCutoff: number;
|
|
8
|
+
batchSize: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface RetentionPruneResult {
|
|
12
|
+
prunedMessages: number;
|
|
13
|
+
prunedActivityEvents: number;
|
|
14
|
+
prunedSpawnCommands: number;
|
|
15
|
+
offThread: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function boundedBatchSize(batchSize: number): number {
|
|
19
|
+
return Math.min(Math.max(Math.trunc(batchSize), 1), 50_000);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function pruneMessagesBefore(cutoff: number, batchSize: number): number {
|
|
23
|
+
return getDb().transaction(() => {
|
|
24
|
+
getDb().query(`
|
|
25
|
+
UPDATE messages
|
|
26
|
+
SET reply_to = NULL
|
|
27
|
+
WHERE reply_to IN (
|
|
28
|
+
SELECT id FROM messages
|
|
29
|
+
WHERE created_at < ?
|
|
30
|
+
ORDER BY created_at ASC, id ASC
|
|
31
|
+
LIMIT ?
|
|
32
|
+
)
|
|
33
|
+
`).run(cutoff, batchSize);
|
|
34
|
+
return getDb().query(`
|
|
35
|
+
DELETE FROM messages
|
|
36
|
+
WHERE id IN (
|
|
37
|
+
SELECT id FROM messages
|
|
38
|
+
WHERE created_at < ?
|
|
39
|
+
ORDER BY created_at ASC, id ASC
|
|
40
|
+
LIMIT ?
|
|
41
|
+
)
|
|
42
|
+
`).run(cutoff, batchSize).changes;
|
|
43
|
+
})();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pruneActivityEvents(input: RetentionPruneInput): number {
|
|
47
|
+
return getDb().query(`
|
|
48
|
+
DELETE FROM activity_events
|
|
49
|
+
WHERE id IN (
|
|
50
|
+
SELECT ae.id
|
|
51
|
+
FROM activity_events ae
|
|
52
|
+
WHERE (
|
|
53
|
+
ae.created_at < ?
|
|
54
|
+
OR ae.id NOT IN (
|
|
55
|
+
SELECT id
|
|
56
|
+
FROM activity_events
|
|
57
|
+
ORDER BY created_at DESC, id DESC
|
|
58
|
+
LIMIT ?
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
AND NOT (
|
|
62
|
+
ae.kind = 'state'
|
|
63
|
+
AND ae.title = 'Agent registered'
|
|
64
|
+
AND EXISTS (
|
|
65
|
+
SELECT 1
|
|
66
|
+
FROM commands c
|
|
67
|
+
WHERE c.type = 'agent.spawn'
|
|
68
|
+
AND c.status = 'succeeded'
|
|
69
|
+
AND coalesce(json_extract(c.params, '$.registrationConfirmed'), 0) != 1
|
|
70
|
+
AND (
|
|
71
|
+
c.correlation_id = json_extract(ae.metadata, '$.spawnRequestId')
|
|
72
|
+
OR json_extract(c.params, '$.spawnRequestId') = json_extract(ae.metadata, '$.spawnRequestId')
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
ORDER BY ae.created_at ASC, ae.id ASC
|
|
77
|
+
LIMIT ?
|
|
78
|
+
)
|
|
79
|
+
`).run(input.activityCutoff, input.activityMaxRows, input.batchSize).changes;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function pruneTerminalSpawnCommands(cutoff: number, batchSize: number): number {
|
|
83
|
+
return getDb().query(`
|
|
84
|
+
DELETE FROM commands
|
|
85
|
+
WHERE id IN (
|
|
86
|
+
SELECT c.id
|
|
87
|
+
FROM commands c
|
|
88
|
+
WHERE c.type = 'agent.spawn'
|
|
89
|
+
AND c.status IN ('succeeded', 'failed', 'canceled', 'timed_out')
|
|
90
|
+
AND c.updated_at < ?
|
|
91
|
+
AND NOT (
|
|
92
|
+
c.status = 'succeeded'
|
|
93
|
+
AND coalesce(json_extract(c.params, '$.registrationConfirmed'), 0) != 1
|
|
94
|
+
)
|
|
95
|
+
AND NOT EXISTS (
|
|
96
|
+
SELECT 1
|
|
97
|
+
FROM managed_agent_state mas
|
|
98
|
+
WHERE mas.spawn_request_id IS NOT NULL
|
|
99
|
+
AND (
|
|
100
|
+
mas.spawn_request_id = c.correlation_id
|
|
101
|
+
OR mas.spawn_request_id = json_extract(c.params, '$.spawnRequestId')
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
ORDER BY c.updated_at ASC, c.created_at ASC
|
|
105
|
+
LIMIT ?
|
|
106
|
+
)
|
|
107
|
+
`).run(cutoff, batchSize).changes;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function pruneRetentionBatch(input: RetentionPruneInput): Omit<RetentionPruneResult, "offThread"> {
|
|
111
|
+
const batchSize = boundedBatchSize(input.batchSize);
|
|
112
|
+
return getDb().transaction(() => ({
|
|
113
|
+
prunedMessages: pruneMessagesBefore(input.messageCutoff, batchSize),
|
|
114
|
+
prunedActivityEvents: pruneActivityEvents({ ...input, batchSize }),
|
|
115
|
+
prunedSpawnCommands: pruneTerminalSpawnCommands(input.spawnCommandCutoff, batchSize),
|
|
116
|
+
}))();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function pruneRetentionBatchAsync(input: RetentionPruneInput, options: { signal?: AbortSignal } = {}): Promise<RetentionPruneResult> {
|
|
120
|
+
const normalized = { ...input, batchSize: boundedBatchSize(input.batchSize) };
|
|
121
|
+
if (!getDbPath() || getDbPath() === ":memory:") {
|
|
122
|
+
return { ...pruneRetentionBatch(normalized), offThread: false };
|
|
123
|
+
}
|
|
124
|
+
const result = await runDbMaintenanceWorker<Omit<RetentionPruneResult, "offThread">>(
|
|
125
|
+
"retention-prune",
|
|
126
|
+
[JSON.stringify(normalized)],
|
|
127
|
+
options.signal,
|
|
128
|
+
);
|
|
129
|
+
return { ...result, offThread: true };
|
|
130
|
+
}
|
package/src/db/schema.ts
CHANGED
|
@@ -114,6 +114,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
114
114
|
last_seen INTEGER NOT NULL,
|
|
115
115
|
created_at INTEGER NOT NULL
|
|
116
116
|
);
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_agents_spawn_request ON agents(json_extract(meta, '$.spawnRequestId'));
|
|
117
118
|
CREATE TABLE IF NOT EXISTS notification_subscriptions (
|
|
118
119
|
id TEXT PRIMARY KEY,
|
|
119
120
|
agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
|
|
@@ -865,6 +866,9 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
865
866
|
CREATE INDEX IF NOT EXISTS idx_activity_operator ON activity_events(operator_id, created_at);
|
|
866
867
|
CREATE INDEX IF NOT EXISTS idx_activity_created ON activity_events(created_at);
|
|
867
868
|
CREATE INDEX IF NOT EXISTS idx_activity_agent ON activity_events(agent_id, created_at); CREATE INDEX IF NOT EXISTS idx_activity_policy_created ON activity_events(json_extract(metadata, '$.policyName'), created_at DESC, id DESC);
|
|
869
|
+
CREATE INDEX IF NOT EXISTS idx_activity_registered_spawn_request
|
|
870
|
+
ON activity_events(json_extract(metadata, '$.spawnRequestId'))
|
|
871
|
+
WHERE kind = 'state' AND title = 'Agent registered';
|
|
868
872
|
-- issue_ref_id/issue_action indexes are created in applyMigrations() AFTER the ALTER — never inline here (#483: inline index over a not-yet-migrated column crashes initDb on existing DBs).
|
|
869
873
|
|
|
870
874
|
CREATE TABLE IF NOT EXISTS channels (
|
|
@@ -942,6 +946,7 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
942
946
|
CREATE INDEX IF NOT EXISTS idx_commands_status ON commands(status);
|
|
943
947
|
CREATE INDEX IF NOT EXISTS idx_commands_created ON commands(created_at);
|
|
944
948
|
CREATE INDEX IF NOT EXISTS idx_commands_correlation ON commands(correlation_id);
|
|
949
|
+
CREATE INDEX IF NOT EXISTS idx_commands_spawn_watchdog ON commands(type, status, updated_at);
|
|
945
950
|
|
|
946
951
|
CREATE TABLE IF NOT EXISTS recipe_instances (
|
|
947
952
|
id TEXT PRIMARY KEY,
|
package/src/db/stats.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { summarizeExpiredUndeliveredMessages } from "./delivery.ts";
|
|
|
20
20
|
import { messageTaskClosed } from "./claim-sql.ts";
|
|
21
21
|
import { claimTaskWhere } from "./task-core.ts";
|
|
22
22
|
import { getDb } from "./connection.ts";
|
|
23
|
+
import { getEventLoopLagSnapshot } from "../event-loop-lag";
|
|
23
24
|
import type {
|
|
24
25
|
AgentCard,
|
|
25
26
|
ActivityEvent,
|
|
@@ -408,6 +409,27 @@ export function getHealth(now: number = Date.now()): HealthReport {
|
|
|
408
409
|
checks.push({ name: "database", status: "error", detail: e instanceof Error ? e.message : "database check failed" });
|
|
409
410
|
}
|
|
410
411
|
|
|
412
|
+
const eventLoopLag = getEventLoopLagSnapshot();
|
|
413
|
+
const eventLoopLagHealthMaxMs = Math.max(eventLoopLag.maxMs, eventLoopLag.maxSinceLastReadMs);
|
|
414
|
+
const eventLoopLagWarn = eventLoopLag.warnThresholdMs > 0 && eventLoopLag.sampleCount > 0 && eventLoopLagHealthMaxMs >= eventLoopLag.warnThresholdMs;
|
|
415
|
+
checks.push({
|
|
416
|
+
name: "event-loop-lag",
|
|
417
|
+
status: eventLoopLagWarn ? "warn" : "ok",
|
|
418
|
+
detail: eventLoopLagWarn
|
|
419
|
+
? `event loop lag max ${Math.round(eventLoopLagHealthMaxMs)}ms over ${eventLoopLag.sampleCount}/${eventLoopLag.windowSamples} sample(s)`
|
|
420
|
+
: undefined,
|
|
421
|
+
metrics: {
|
|
422
|
+
currentMs: eventLoopLag.currentMs,
|
|
423
|
+
maxMs: eventLoopLag.maxMs,
|
|
424
|
+
maxSinceLastReadMs: eventLoopLag.maxSinceLastReadMs,
|
|
425
|
+
sampleCount: eventLoopLag.sampleCount,
|
|
426
|
+
windowSamples: eventLoopLag.windowSamples,
|
|
427
|
+
sampleIntervalMs: eventLoopLag.sampleIntervalMs,
|
|
428
|
+
warnThresholdMs: eventLoopLag.warnThresholdMs,
|
|
429
|
+
...(eventLoopLag.lastSampleAt === undefined ? {} : { lastSampleAt: eventLoopLag.lastSampleAt }),
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
|
|
411
433
|
const staleLiveAgents = (getDb()
|
|
412
434
|
.query("SELECT COUNT(*) as c FROM agents WHERE status != 'offline' AND last_seen <= ? AND id NOT IN ('user', 'system')")
|
|
413
435
|
.get(now - STALE_TTL_MS) as any).c as number;
|