agent-relay-server 0.19.3 → 0.20.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 +44 -8
- package/package.json +2 -2
- package/public/index.html +94 -90
- package/runner/src/adapter.ts +2 -0
- package/src/config-store.ts +2 -0
- package/src/db.ts +107 -0
- package/src/routes.ts +13 -0
- package/src/token-db.ts +3 -3
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.19.3",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
|
@@ -10774,21 +10774,57 @@
|
|
|
10774
10774
|
"System"
|
|
10775
10775
|
],
|
|
10776
10776
|
"description": "Returns agent count, online count, and message statistics.",
|
|
10777
|
+
"parameters": [
|
|
10778
|
+
{
|
|
10779
|
+
"name": "period",
|
|
10780
|
+
"in": "query",
|
|
10781
|
+
"schema": {
|
|
10782
|
+
"type": "string"
|
|
10783
|
+
}
|
|
10784
|
+
}
|
|
10785
|
+
],
|
|
10777
10786
|
"responses": {
|
|
10778
10787
|
"200": {
|
|
10779
10788
|
"description": "Success",
|
|
10780
10789
|
"content": {
|
|
10781
10790
|
"application/json": {}
|
|
10782
10791
|
}
|
|
10792
|
+
}
|
|
10793
|
+
},
|
|
10794
|
+
"security": [
|
|
10795
|
+
{
|
|
10796
|
+
"bearerAuth": []
|
|
10783
10797
|
},
|
|
10784
|
-
|
|
10785
|
-
"
|
|
10798
|
+
{
|
|
10799
|
+
"tokenHeader": []
|
|
10800
|
+
},
|
|
10801
|
+
{
|
|
10802
|
+
"tokenQuery": []
|
|
10803
|
+
}
|
|
10804
|
+
]
|
|
10805
|
+
}
|
|
10806
|
+
},
|
|
10807
|
+
"/api/stats/analytics": {
|
|
10808
|
+
"get": {
|
|
10809
|
+
"operationId": "getAnalyticsRoute",
|
|
10810
|
+
"summary": "Get Analytics",
|
|
10811
|
+
"tags": [
|
|
10812
|
+
"System"
|
|
10813
|
+
],
|
|
10814
|
+
"parameters": [
|
|
10815
|
+
{
|
|
10816
|
+
"name": "period",
|
|
10817
|
+
"in": "query",
|
|
10818
|
+
"schema": {
|
|
10819
|
+
"type": "string"
|
|
10820
|
+
}
|
|
10821
|
+
}
|
|
10822
|
+
],
|
|
10823
|
+
"responses": {
|
|
10824
|
+
"200": {
|
|
10825
|
+
"description": "Success",
|
|
10786
10826
|
"content": {
|
|
10787
|
-
"application/json": {
|
|
10788
|
-
"schema": {
|
|
10789
|
-
"$ref": "#/components/schemas/Error"
|
|
10790
|
-
}
|
|
10791
|
-
}
|
|
10827
|
+
"application/json": {}
|
|
10792
10828
|
}
|
|
10793
10829
|
}
|
|
10794
10830
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Lightweight HTTP message relay for inter-agent communication across machines",
|
|
5
5
|
"module": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"CONTRIBUTING.md"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"agent-relay-sdk": "0.2.
|
|
36
|
+
"agent-relay-sdk": "0.2.11"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"prepack": "bun run build:dashboard:bundle >&2",
|
package/public/index.html
CHANGED
|
@@ -11944,6 +11944,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
11944
11944
|
agents: [],
|
|
11945
11945
|
agentsById: {},
|
|
11946
11946
|
messages: [],
|
|
11947
|
+
threadHistory: {},
|
|
11947
11948
|
pairs: [],
|
|
11948
11949
|
tasks: [],
|
|
11949
11950
|
orchestrators: [],
|
|
@@ -12028,6 +12029,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12028
12029
|
chatHasNewItems: false,
|
|
12029
12030
|
pendingForkImport: null,
|
|
12030
12031
|
analyticsPeriod: "24h",
|
|
12032
|
+
analyticsData: null,
|
|
12031
12033
|
inboxReadCursors: {},
|
|
12032
12034
|
inboxArchivedThreads: {},
|
|
12033
12035
|
inboxDrafts: {},
|
|
@@ -12152,7 +12154,8 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12152
12154
|
context: true,
|
|
12153
12155
|
skills: true,
|
|
12154
12156
|
plugins: true,
|
|
12155
|
-
statusLine: true
|
|
12157
|
+
statusLine: true,
|
|
12158
|
+
mcp: true
|
|
12156
12159
|
},
|
|
12157
12160
|
skills: [],
|
|
12158
12161
|
plugins: [],
|
|
@@ -12248,6 +12251,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12248
12251
|
s.fetchAgents()
|
|
12249
12252
|
]);
|
|
12250
12253
|
if (view === "files") await s.fetchOrchestrators();
|
|
12254
|
+
if (view === "analytics") await s.fetchAnalytics();
|
|
12251
12255
|
},
|
|
12252
12256
|
startClock() {
|
|
12253
12257
|
useClock.getState().start();
|
|
@@ -12362,6 +12366,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12362
12366
|
if (view === "maintenance") work.push(s.fetchMaintenanceJobs());
|
|
12363
12367
|
if (view === "profiles") work.push(s.fetchAgentProfiles());
|
|
12364
12368
|
if (view === "workspaces") work.push(s.fetchWorkspaces());
|
|
12369
|
+
if (view === "analytics") work.push(s.fetchAnalytics());
|
|
12365
12370
|
await Promise.all(work);
|
|
12366
12371
|
} finally {
|
|
12367
12372
|
set({ _refreshInFlight: false });
|
|
@@ -12372,6 +12377,15 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12372
12377
|
set({ stats: await api("GET", "/stats") });
|
|
12373
12378
|
} catch {}
|
|
12374
12379
|
},
|
|
12380
|
+
async fetchAnalytics() {
|
|
12381
|
+
try {
|
|
12382
|
+
set({ analyticsData: await api("GET", "/stats/analytics?period=" + get().analyticsPeriod) });
|
|
12383
|
+
} catch {}
|
|
12384
|
+
},
|
|
12385
|
+
setAnalyticsPeriod(period) {
|
|
12386
|
+
set({ analyticsPeriod: period });
|
|
12387
|
+
get().fetchAnalytics();
|
|
12388
|
+
},
|
|
12375
12389
|
async fetchHealth() {
|
|
12376
12390
|
try {
|
|
12377
12391
|
set({ health: await api("GET", "/health") });
|
|
@@ -12630,6 +12644,16 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
12630
12644
|
set({ messages: mergeFetchedMessages(get().messages, messages) });
|
|
12631
12645
|
} catch {}
|
|
12632
12646
|
},
|
|
12647
|
+
async fetchThreadHistory(peer) {
|
|
12648
|
+
if (!peer) return;
|
|
12649
|
+
try {
|
|
12650
|
+
const history = await api("GET", "/messages?for=" + encodeURIComponent(peer) + "&limit=500");
|
|
12651
|
+
set({ threadHistory: {
|
|
12652
|
+
...get().threadHistory,
|
|
12653
|
+
[peer]: history
|
|
12654
|
+
} });
|
|
12655
|
+
} catch {}
|
|
12656
|
+
},
|
|
12633
12657
|
async fetchChatHistoryImports() {
|
|
12634
12658
|
try {
|
|
12635
12659
|
set({ chatHistoryImports: await api("GET", "/chat/history-imports?limit=500") });
|
|
@@ -13240,6 +13264,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
13240
13264
|
openInboxThread(peer, messages = []) {
|
|
13241
13265
|
set({ selectedInboxThread: peer });
|
|
13242
13266
|
if (messages.length) get().markInboxThreadRead(peer, messages);
|
|
13267
|
+
get().fetchThreadHistory(peer);
|
|
13243
13268
|
},
|
|
13244
13269
|
async sendChatMessage(body, thread, attachments = []) {
|
|
13245
13270
|
if (!body.trim() && attachments.length === 0 || get().chatSending) return;
|
|
@@ -13995,7 +14020,8 @@ var useRelayStore = create$1()(persist((set, get) => ({
|
|
|
13995
14020
|
context: true,
|
|
13996
14021
|
skills: true,
|
|
13997
14022
|
plugins: true,
|
|
13998
|
-
statusLine: true
|
|
14023
|
+
statusLine: true,
|
|
14024
|
+
mcp: true
|
|
13999
14025
|
},
|
|
14000
14026
|
skills: [],
|
|
14001
14027
|
plugins: [],
|
|
@@ -125295,6 +125321,7 @@ function useChatAgents() {
|
|
|
125295
125321
|
]);
|
|
125296
125322
|
}
|
|
125297
125323
|
var NO_STATUS_EVENTS = [];
|
|
125324
|
+
var NO_THREAD_HISTORY = [];
|
|
125298
125325
|
function useAgentStatusEvents(agentId) {
|
|
125299
125326
|
return useRelayStore((s) => s.chatStatusEvents[agentId] ?? NO_STATUS_EVENTS);
|
|
125300
125327
|
}
|
|
@@ -126768,6 +126795,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126768
126795
|
const doAgentAction = useRelayStore((s) => s.doAgentAction);
|
|
126769
126796
|
const forkFromAgent = useRelayStore((s) => s.forkFromAgent);
|
|
126770
126797
|
const chatHistoryImports = useRelayStore((s) => s.chatHistoryImports);
|
|
126798
|
+
const peerHistory = useRelayStore((s) => s.threadHistory[s.selectedInboxThread] ?? NO_THREAD_HISTORY);
|
|
126771
126799
|
const openConfirm = useRelayStore((s) => s.openConfirm);
|
|
126772
126800
|
const showError = useRelayStore((s) => s.showError);
|
|
126773
126801
|
const orchestrators = useRelayStore((s) => s.orchestrators);
|
|
@@ -126805,6 +126833,18 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126805
126833
|
const filePreviewRequest = (0, import_react.useRef)(0);
|
|
126806
126834
|
const statusEvents = useAgentStatusEvents(selectedInboxThread);
|
|
126807
126835
|
const thread = (0, import_react.useMemo)(() => threads.find((t) => t.peer === selectedInboxThread) || null, [threads, selectedInboxThread]);
|
|
126836
|
+
const threadMessages = (0, import_react.useMemo)(() => {
|
|
126837
|
+
const live = thread?.messages ?? [];
|
|
126838
|
+
if (peerHistory.length === 0) return live;
|
|
126839
|
+
const byId = /* @__PURE__ */ new Map();
|
|
126840
|
+
for (const m of peerHistory) if (inboxPeer(m) === selectedInboxThread && !isReactionEventMessage(m)) byId.set(m.id, m);
|
|
126841
|
+
for (const m of live) byId.set(m.id, m);
|
|
126842
|
+
return [...byId.values()].sort((a, b) => a.id - b.id);
|
|
126843
|
+
}, [
|
|
126844
|
+
thread?.messages,
|
|
126845
|
+
peerHistory,
|
|
126846
|
+
selectedInboxThread
|
|
126847
|
+
]);
|
|
126808
126848
|
const agent = agentsById[selectedInboxThread] || null;
|
|
126809
126849
|
const agentSpawnRequestId = typeof agent?.meta?.spawnRequestId === "string" ? agent.meta.spawnRequestId : "";
|
|
126810
126850
|
const importedHistory = (0, import_react.useMemo)(() => {
|
|
@@ -126824,7 +126864,7 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126824
126864
|
const draft = inboxDrafts[selectedInboxThread] || "";
|
|
126825
126865
|
const importedEntryCount = importedHistory.reduce((sum, history) => sum + history.entries.length, 0);
|
|
126826
126866
|
const pinnedScroll = usePinnedScroll(selectedInboxThread, [
|
|
126827
|
-
|
|
126867
|
+
threadMessages.length,
|
|
126828
126868
|
statusEvents.length,
|
|
126829
126869
|
importedEntryCount,
|
|
126830
126870
|
pendingApproval?.id,
|
|
@@ -126880,8 +126920,8 @@ function ChatPanel({ threads, onBack, showBackButton }) {
|
|
|
126880
126920
|
if (filePreviewCloseTimer.current !== null) window.clearTimeout(filePreviewCloseTimer.current);
|
|
126881
126921
|
};
|
|
126882
126922
|
}, []);
|
|
126883
|
-
const timeline = (0, import_react.useMemo)(() => buildTimeline(
|
|
126884
|
-
|
|
126923
|
+
const timeline = (0, import_react.useMemo)(() => buildTimeline(threadMessages, statusEvents, agent?.createdAt, importedHistory), [
|
|
126924
|
+
threadMessages,
|
|
126885
126925
|
statusEvents,
|
|
126886
126926
|
agent?.createdAt,
|
|
126887
126927
|
importedHistory
|
|
@@ -152495,13 +152535,14 @@ var PERIODS = [
|
|
|
152495
152535
|
function periodMs(id) {
|
|
152496
152536
|
return PERIODS.find((p) => p.id === id)?.ms ?? 864e5;
|
|
152497
152537
|
}
|
|
152498
|
-
|
|
152499
|
-
|
|
152500
|
-
|
|
152501
|
-
|
|
152502
|
-
|
|
152503
|
-
|
|
152504
|
-
|
|
152538
|
+
var CATEGORY_ORDER = [
|
|
152539
|
+
"Messages",
|
|
152540
|
+
"Replies",
|
|
152541
|
+
"Work items",
|
|
152542
|
+
"System",
|
|
152543
|
+
"Pair",
|
|
152544
|
+
"Channel"
|
|
152545
|
+
];
|
|
152505
152546
|
function StatCard({ label, value, icon: Icon, className = "" }) {
|
|
152506
152547
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Card, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, {
|
|
152507
152548
|
className: "pt-4",
|
|
@@ -152522,11 +152563,11 @@ function StatCard({ label, value, icon: Icon, className = "" }) {
|
|
|
152522
152563
|
}
|
|
152523
152564
|
function PeriodSelector() {
|
|
152524
152565
|
const period = useRelayStore((s) => s.analyticsPeriod);
|
|
152525
|
-
const
|
|
152566
|
+
const setPeriod = useRelayStore((s) => s.setAnalyticsPeriod);
|
|
152526
152567
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152527
152568
|
className: "flex items-center gap-1 bg-muted rounded-lg p-1",
|
|
152528
152569
|
children: PERIODS.map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
|
|
152529
|
-
onClick: () =>
|
|
152570
|
+
onClick: () => setPeriod(p.id),
|
|
152530
152571
|
className: `px-3 py-1 rounded-md text-xs font-medium transition-colors ${period === p.id ? "bg-background text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`,
|
|
152531
152572
|
children: p.label
|
|
152532
152573
|
}, p.id))
|
|
@@ -152566,67 +152607,38 @@ function useChart(id, getOptions, deps) {
|
|
|
152566
152607
|
}
|
|
152567
152608
|
function AnalyticsView() {
|
|
152568
152609
|
const agents = useRelayStore((s) => s.agents);
|
|
152569
|
-
const
|
|
152570
|
-
useRelayStore((s) => s.stats);
|
|
152610
|
+
const data = useRelayStore((s) => s.analyticsData);
|
|
152571
152611
|
const period = useRelayStore((s) => s.analyticsPeriod);
|
|
152572
152612
|
const theme = useRelayStore((s) => s.theme);
|
|
152573
152613
|
const onlineCount = useOnlineCount();
|
|
152574
152614
|
const busyCount = useBusyCount();
|
|
152575
152615
|
const isDark = THEMES.find((t) => t.id === theme)?.dark ?? true;
|
|
152576
|
-
const cutoff = Date.now() - periodMs(period);
|
|
152577
|
-
const filteredMessages = (0, import_react.useMemo)(() => {
|
|
152578
|
-
return messages.filter((m) => {
|
|
152579
|
-
const ts = toTimestamp(m.createdAt);
|
|
152580
|
-
return ts && ts >= cutoff;
|
|
152581
|
-
});
|
|
152582
|
-
}, [messages, cutoff]);
|
|
152583
152616
|
const volumeData = (0, import_react.useMemo)(() => {
|
|
152584
|
-
|
|
152585
|
-
|
|
152586
|
-
|
|
152587
|
-
|
|
152588
|
-
const
|
|
152589
|
-
"Messages",
|
|
152590
|
-
"Replies",
|
|
152591
|
-
"Work items",
|
|
152592
|
-
"System",
|
|
152593
|
-
"Pair",
|
|
152594
|
-
"Channel"
|
|
152595
|
-
];
|
|
152596
|
-
const series = {};
|
|
152597
|
-
for (const cat of categories) series[cat] = new Array(bucketCount).fill(0);
|
|
152598
|
-
const labels = [];
|
|
152599
|
-
for (let i = 0; i < bucketCount; i++) {
|
|
152600
|
-
const t = /* @__PURE__ */ new Date(now - (bucketCount - 1 - i) * bucketMs);
|
|
152601
|
-
if (ms <= 864e5) labels.push(t.toLocaleTimeString(void 0, {
|
|
152602
|
-
hour: "2-digit",
|
|
152603
|
-
minute: "2-digit"
|
|
152604
|
-
}));
|
|
152605
|
-
else labels.push(t.toLocaleDateString(void 0, {
|
|
152606
|
-
month: "short",
|
|
152607
|
-
day: "numeric"
|
|
152608
|
-
}));
|
|
152609
|
-
}
|
|
152610
|
-
for (const msg of filteredMessages) {
|
|
152611
|
-
const ts = toTimestamp(msg.createdAt);
|
|
152612
|
-
if (!ts) continue;
|
|
152613
|
-
const bucketIdx = Math.floor((ts - (now - ms)) / bucketMs);
|
|
152614
|
-
if (bucketIdx < 0 || bucketIdx >= bucketCount) continue;
|
|
152615
|
-
const bucket = series[categorizeMessage(msg)];
|
|
152616
|
-
if (bucket) bucket[bucketIdx] = (bucket[bucketIdx] ?? 0) + 1;
|
|
152617
|
-
}
|
|
152617
|
+
if (!data) return {
|
|
152618
|
+
labels: [],
|
|
152619
|
+
series: []
|
|
152620
|
+
};
|
|
152621
|
+
const asTime = periodMs(period) <= 864e5;
|
|
152618
152622
|
return {
|
|
152619
|
-
labels
|
|
152620
|
-
|
|
152621
|
-
|
|
152622
|
-
|
|
152623
|
-
|
|
152624
|
-
|
|
152625
|
-
|
|
152626
|
-
|
|
152623
|
+
labels: data.buckets.map((b) => {
|
|
152624
|
+
const t = new Date(b.start);
|
|
152625
|
+
return asTime ? t.toLocaleTimeString(void 0, {
|
|
152626
|
+
hour: "2-digit",
|
|
152627
|
+
minute: "2-digit"
|
|
152628
|
+
}) : t.toLocaleDateString(void 0, {
|
|
152629
|
+
month: "short",
|
|
152630
|
+
day: "numeric"
|
|
152631
|
+
});
|
|
152632
|
+
}),
|
|
152633
|
+
series: CATEGORY_ORDER.flatMap((cat) => {
|
|
152634
|
+
const series = data.buckets.map((b) => b.counts[cat] ?? 0);
|
|
152635
|
+
return series.some((v) => v > 0) ? [{
|
|
152636
|
+
name: cat,
|
|
152637
|
+
data: series
|
|
152638
|
+
}] : [];
|
|
152627
152639
|
})
|
|
152628
152640
|
};
|
|
152629
|
-
}, [
|
|
152641
|
+
}, [data, period]);
|
|
152630
152642
|
const utilizationData = (0, import_react.useMemo)(() => {
|
|
152631
152643
|
let idle = 0, busy = 0;
|
|
152632
152644
|
for (const a of agents) {
|
|
@@ -152641,14 +152653,7 @@ function AnalyticsView() {
|
|
|
152641
152653
|
};
|
|
152642
152654
|
}, [agents]);
|
|
152643
152655
|
const heatmapData = (0, import_react.useMemo)(() => {
|
|
152644
|
-
const grid = Array.from({ length: 7 }, () => new Array(24).fill(0));
|
|
152645
|
-
for (const msg of filteredMessages) {
|
|
152646
|
-
const ts = toTimestamp(msg.createdAt);
|
|
152647
|
-
if (!ts) continue;
|
|
152648
|
-
const d = new Date(ts);
|
|
152649
|
-
const row = grid[d.getDay()];
|
|
152650
|
-
if (row) row[d.getHours()] = (row[d.getHours()] ?? 0) + 1;
|
|
152651
|
-
}
|
|
152656
|
+
const grid = data?.heatmap ?? Array.from({ length: 7 }, () => new Array(24).fill(0));
|
|
152652
152657
|
return [
|
|
152653
152658
|
"Sun",
|
|
152654
152659
|
"Mon",
|
|
@@ -152659,24 +152664,20 @@ function AnalyticsView() {
|
|
|
152659
152664
|
"Sat"
|
|
152660
152665
|
].map((name, dayIdx) => ({
|
|
152661
152666
|
name,
|
|
152662
|
-
data: (grid[dayIdx] ??
|
|
152667
|
+
data: (grid[dayIdx] ?? new Array(24).fill(0)).map((v, hour) => ({
|
|
152663
152668
|
x: String(hour).padStart(2, "0"),
|
|
152664
152669
|
y: v
|
|
152665
152670
|
}))
|
|
152666
152671
|
}));
|
|
152667
|
-
}, [
|
|
152672
|
+
}, [data]);
|
|
152668
152673
|
const activityBreakdown = (0, import_react.useMemo)(() => {
|
|
152669
|
-
const counts =
|
|
152670
|
-
|
|
152671
|
-
const cat = categorizeMessage(msg);
|
|
152672
|
-
counts[cat] = (counts[cat] ?? 0) + 1;
|
|
152673
|
-
}
|
|
152674
|
-
const entries = Object.entries(counts).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]);
|
|
152674
|
+
const counts = data?.categories;
|
|
152675
|
+
const entries = counts ? Object.entries(counts).filter(([, v]) => v > 0).sort((a, b) => b[1] - a[1]) : [];
|
|
152675
152676
|
return {
|
|
152676
152677
|
labels: entries.map(([k]) => k),
|
|
152677
152678
|
values: entries.map(([, v]) => v)
|
|
152678
152679
|
};
|
|
152679
|
-
}, [
|
|
152680
|
+
}, [data]);
|
|
152680
152681
|
const chartColors = isDark ? {
|
|
152681
152682
|
text: "rgba(255,255,255,0.4)",
|
|
152682
152683
|
grid: "rgba(255,255,255,0.06)",
|
|
@@ -152815,8 +152816,9 @@ function AnalyticsView() {
|
|
|
152815
152816
|
}] }
|
|
152816
152817
|
} }
|
|
152817
152818
|
}), [heatmapData, isDark]);
|
|
152818
|
-
const periodMessageCount =
|
|
152819
|
-
const reactionCount =
|
|
152819
|
+
const periodMessageCount = data?.totalMessages ?? 0;
|
|
152820
|
+
const reactionCount = data?.totalReactions ?? 0;
|
|
152821
|
+
const hasHeatmapData = (data?.totalMessages ?? 0) > 0;
|
|
152820
152822
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
152821
152823
|
className: "space-y-6",
|
|
152822
152824
|
children: [
|
|
@@ -152882,7 +152884,7 @@ function AnalyticsView() {
|
|
|
152882
152884
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardTitle, {
|
|
152883
152885
|
className: "text-sm",
|
|
152884
152886
|
children: "Busiest hours"
|
|
152885
|
-
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children:
|
|
152887
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardContent, { children: !hasHeatmapData ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
152886
152888
|
className: "flex items-center justify-center h-[240px] text-muted-foreground text-sm",
|
|
152887
152889
|
children: "No data for heatmap"
|
|
152888
152890
|
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { id: "analytics-heatmap-chart" }) })] })
|
|
@@ -157595,7 +157597,8 @@ function AgentProfileModal() {
|
|
|
157595
157597
|
context: isHost,
|
|
157596
157598
|
skills: isHost,
|
|
157597
157599
|
plugins: isHost,
|
|
157598
|
-
statusLine: isHost
|
|
157600
|
+
statusLine: isHost,
|
|
157601
|
+
mcp: isHost
|
|
157599
157602
|
},
|
|
157600
157603
|
mcp: {
|
|
157601
157604
|
...profile.mcp,
|
|
@@ -157695,14 +157698,15 @@ function AgentProfileModal() {
|
|
|
157695
157698
|
"context",
|
|
157696
157699
|
"skills",
|
|
157697
157700
|
"plugins",
|
|
157698
|
-
"statusLine"
|
|
157701
|
+
"statusLine",
|
|
157702
|
+
"mcp"
|
|
157699
157703
|
].map((key) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
157700
157704
|
className: "flex items-center justify-between gap-2",
|
|
157701
157705
|
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, {
|
|
157702
157706
|
className: "capitalize",
|
|
157703
|
-
children: key === "statusLine" ? "Status Line" : key
|
|
157707
|
+
children: key === "statusLine" ? "Status Line" : key === "mcp" ? "MCP Tools" : key
|
|
157704
157708
|
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Switch, {
|
|
157705
|
-
checked: profile.relay[key],
|
|
157709
|
+
checked: profile.relay[key] ?? true,
|
|
157706
157710
|
onCheckedChange: (v) => updateRelay({ [key]: v }),
|
|
157707
157711
|
disabled: readOnly
|
|
157708
157712
|
})]
|
package/runner/src/adapter.ts
CHANGED
package/src/config-store.ts
CHANGED
|
@@ -204,6 +204,7 @@ function agentProfileDefaults(input: Pick<AgentProfile, "name" | "base"> & Parti
|
|
|
204
204
|
skills: input.relay?.skills ?? !isolated,
|
|
205
205
|
plugins: input.relay?.plugins ?? !isolated,
|
|
206
206
|
statusLine: input.relay?.statusLine ?? !isolated,
|
|
207
|
+
mcp: input.relay?.mcp ?? !isolated,
|
|
207
208
|
},
|
|
208
209
|
skills: input.skills ?? [],
|
|
209
210
|
plugins: input.plugins ?? [],
|
|
@@ -274,6 +275,7 @@ function validateAgentProfile(key: string, value: unknown): AgentProfile {
|
|
|
274
275
|
skills: relay.skills === undefined ? defaults.relay.skills : cleanBoolean(relay.skills, "relay.skills"),
|
|
275
276
|
plugins: relay.plugins === undefined ? defaults.relay.plugins : cleanBoolean(relay.plugins, "relay.plugins"),
|
|
276
277
|
statusLine: relay.statusLine === undefined ? defaults.relay.statusLine : cleanBoolean(relay.statusLine, "relay.statusLine"),
|
|
278
|
+
mcp: relay.mcp === undefined ? defaults.relay.mcp : cleanBoolean(relay.mcp, "relay.mcp"),
|
|
277
279
|
},
|
|
278
280
|
skills: cleanAgentProfileAssetArray(value.skills, "skills"),
|
|
279
281
|
plugins: cleanAgentProfileAssetArray(value.plugins, "plugins"),
|
package/src/db.ts
CHANGED
|
@@ -4678,6 +4678,113 @@ export function getStats(): {
|
|
|
4678
4678
|
return { version: VERSION, agents, online, messages, messagesLast24h, tasks, openTasks };
|
|
4679
4679
|
}
|
|
4680
4680
|
|
|
4681
|
+
// Analytics aggregation periods. Bucket counts mirror the dashboard chart
|
|
4682
|
+
// granularity so the client renders pre-aggregated buckets directly. This is the
|
|
4683
|
+
// single source of truth — the dashboard no longer windows raw messages (which
|
|
4684
|
+
// capped it at the last ~100 rows ≈ 3h on a busy relay; the data was always
|
|
4685
|
+
// retained server-side per RETENTION_DAYS). See issue #212 for the Layer-2 rollup
|
|
4686
|
+
// that will let stats outlive the message-retention window.
|
|
4687
|
+
export const ANALYTICS_PERIODS = {
|
|
4688
|
+
"1h": { ms: 3_600_000, buckets: 12 },
|
|
4689
|
+
"6h": { ms: 21_600_000, buckets: 12 },
|
|
4690
|
+
"24h": { ms: 86_400_000, buckets: 24 },
|
|
4691
|
+
"7d": { ms: 604_800_000, buckets: 14 },
|
|
4692
|
+
"14d": { ms: 1_209_600_000, buckets: 14 },
|
|
4693
|
+
"30d": { ms: 2_592_000_000, buckets: 15 },
|
|
4694
|
+
} as const;
|
|
4695
|
+
|
|
4696
|
+
export type AnalyticsPeriod = keyof typeof ANALYTICS_PERIODS;
|
|
4697
|
+
|
|
4698
|
+
// Message → category, the ONE place this mapping lives (server-side SQL). Order is
|
|
4699
|
+
// significant: a claimable/system/pair/channel message is classified as such even
|
|
4700
|
+
// when it is also a reply; only an otherwise-plain reply counts as "Replies".
|
|
4701
|
+
export const ANALYTICS_CATEGORIES = ["Messages", "Replies", "Work items", "System", "Pair", "Channel"] as const;
|
|
4702
|
+
export type AnalyticsCategory = (typeof ANALYTICS_CATEGORIES)[number];
|
|
4703
|
+
const ANALYTICS_CATEGORY_SQL = `
|
|
4704
|
+
CASE
|
|
4705
|
+
WHEN claimable = 1 THEN 'Work items'
|
|
4706
|
+
WHEN kind = 'system' OR from_agent = 'system' THEN 'System'
|
|
4707
|
+
WHEN kind = 'pair' THEN 'Pair'
|
|
4708
|
+
WHEN kind = 'channel.event' THEN 'Channel'
|
|
4709
|
+
WHEN reply_to IS NOT NULL THEN 'Replies'
|
|
4710
|
+
ELSE 'Messages'
|
|
4711
|
+
END`;
|
|
4712
|
+
|
|
4713
|
+
export interface AnalyticsData {
|
|
4714
|
+
period: AnalyticsPeriod;
|
|
4715
|
+
start: number;
|
|
4716
|
+
now: number;
|
|
4717
|
+
bucketMs: number;
|
|
4718
|
+
bucketCount: number;
|
|
4719
|
+
// Per-bucket category counts, oldest → newest, length === bucketCount.
|
|
4720
|
+
buckets: Array<{ start: number; counts: Record<AnalyticsCategory, number> }>;
|
|
4721
|
+
// Category totals over the whole period (powers the breakdown donut).
|
|
4722
|
+
categories: Record<AnalyticsCategory, number>;
|
|
4723
|
+
// Busiest-hours grid: heatmap[dayOfWeek 0=Sun][hour 0-23], in server-local time.
|
|
4724
|
+
heatmap: number[][];
|
|
4725
|
+
totalMessages: number;
|
|
4726
|
+
totalReactions: number;
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
function emptyCategoryCounts(): Record<AnalyticsCategory, number> {
|
|
4730
|
+
return { Messages: 0, Replies: 0, "Work items": 0, System: 0, Pair: 0, Channel: 0 };
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
export function getAnalytics(period: AnalyticsPeriod, now: number = Date.now()): AnalyticsData {
|
|
4734
|
+
const { ms, buckets: bucketCount } = ANALYTICS_PERIODS[period] ?? ANALYTICS_PERIODS["24h"];
|
|
4735
|
+
const start = now - ms;
|
|
4736
|
+
const bucketMs = ms / bucketCount;
|
|
4737
|
+
|
|
4738
|
+
const buckets = Array.from({ length: bucketCount }, (_, i) => ({
|
|
4739
|
+
start: start + i * bucketMs,
|
|
4740
|
+
counts: emptyCategoryCounts(),
|
|
4741
|
+
}));
|
|
4742
|
+
const categories = emptyCategoryCounts();
|
|
4743
|
+
|
|
4744
|
+
// One pass: bucket index × category counts. CAST floors toward zero; created_at
|
|
4745
|
+
// in (start, now] keeps the index in [0, bucketCount).
|
|
4746
|
+
const volumeRows = db
|
|
4747
|
+
.query(
|
|
4748
|
+
`SELECT CAST((created_at - ?) / ? AS INTEGER) AS bucket, ${ANALYTICS_CATEGORY_SQL} AS category, COUNT(*) AS c
|
|
4749
|
+
FROM messages WHERE created_at > ? AND created_at <= ? GROUP BY bucket, category`,
|
|
4750
|
+
)
|
|
4751
|
+
.all(start, bucketMs, start, now) as Array<{ bucket: number; category: AnalyticsCategory; c: number }>;
|
|
4752
|
+
for (const row of volumeRows) {
|
|
4753
|
+
const idx = Math.min(Math.max(row.bucket, 0), bucketCount - 1);
|
|
4754
|
+
const bucket = buckets[idx];
|
|
4755
|
+
if (bucket && row.category in bucket.counts) {
|
|
4756
|
+
bucket.counts[row.category] += row.c;
|
|
4757
|
+
categories[row.category] += row.c;
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
const totalMessages = Object.values(categories).reduce((sum, v) => sum + v, 0);
|
|
4761
|
+
|
|
4762
|
+
// Busiest-hours heatmap (day-of-week × hour), server-local time.
|
|
4763
|
+
const heatmap: number[][] = Array.from({ length: 7 }, () => new Array(24).fill(0));
|
|
4764
|
+
const heatRows = db
|
|
4765
|
+
.query(
|
|
4766
|
+
`SELECT CAST(strftime('%w', created_at / 1000, 'unixepoch', 'localtime') AS INTEGER) AS dow,
|
|
4767
|
+
CAST(strftime('%H', created_at / 1000, 'unixepoch', 'localtime') AS INTEGER) AS hour, COUNT(*) AS c
|
|
4768
|
+
FROM messages WHERE created_at > ? AND created_at <= ? GROUP BY dow, hour`,
|
|
4769
|
+
)
|
|
4770
|
+
.all(start, now) as Array<{ dow: number; hour: number; c: number }>;
|
|
4771
|
+
for (const row of heatRows) {
|
|
4772
|
+
const day = heatmap[row.dow];
|
|
4773
|
+
if (day && row.hour >= 0 && row.hour < 24) day[row.hour] = row.c;
|
|
4774
|
+
}
|
|
4775
|
+
|
|
4776
|
+
const totalReactions = (
|
|
4777
|
+
db
|
|
4778
|
+
.query(
|
|
4779
|
+
`SELECT COUNT(*) AS c FROM message_reactions r
|
|
4780
|
+
JOIN messages m ON m.id = r.message_id WHERE m.created_at > ? AND m.created_at <= ?`,
|
|
4781
|
+
)
|
|
4782
|
+
.get(start, now) as { c: number }
|
|
4783
|
+
).c;
|
|
4784
|
+
|
|
4785
|
+
return { period, start, now, bucketMs, bucketCount, buckets, categories, heatmap, totalMessages, totalReactions };
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4681
4788
|
export function getHealth(now: number = Date.now()): HealthReport {
|
|
4682
4789
|
const checks: HealthCheck[] = [];
|
|
4683
4790
|
|
package/src/routes.ts
CHANGED
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
markRead,
|
|
28
28
|
deleteMessage,
|
|
29
29
|
getStats,
|
|
30
|
+
getAnalytics,
|
|
31
|
+
ANALYTICS_PERIODS,
|
|
32
|
+
type AnalyticsPeriod,
|
|
30
33
|
getHealth,
|
|
31
34
|
getLatestMessageId,
|
|
32
35
|
ingestIntegrationEvent,
|
|
@@ -6514,6 +6517,15 @@ const getEvents: Handler = (req) => {
|
|
|
6514
6517
|
// --- Stats ---
|
|
6515
6518
|
|
|
6516
6519
|
const getStatsRoute: Handler = () => json(getStats());
|
|
6520
|
+
|
|
6521
|
+
const getAnalyticsRoute: Handler = (req) => {
|
|
6522
|
+
const period = new URL(req.url).searchParams.get("period") ?? "24h";
|
|
6523
|
+
if (!(period in ANALYTICS_PERIODS)) {
|
|
6524
|
+
return error(`period must be one of ${Object.keys(ANALYTICS_PERIODS).join(", ")}`);
|
|
6525
|
+
}
|
|
6526
|
+
return json(getAnalytics(period as AnalyticsPeriod));
|
|
6527
|
+
};
|
|
6528
|
+
|
|
6517
6529
|
const getHealthRoute: Handler = () => json(getHealth());
|
|
6518
6530
|
|
|
6519
6531
|
const getMaintenanceJobs: Handler = () => json(listMaintenanceJobs());
|
|
@@ -6819,6 +6831,7 @@ const routes: Route[] = [
|
|
|
6819
6831
|
|
|
6820
6832
|
route("GET", "/api/events", getEvents),
|
|
6821
6833
|
route("GET", "/api/stats", getStatsRoute),
|
|
6834
|
+
route("GET", "/api/stats/analytics", getAnalyticsRoute),
|
|
6822
6835
|
route("GET", "/api/health", getHealthRoute),
|
|
6823
6836
|
route("GET", "/api/maintenance/jobs", getMaintenanceJobs),
|
|
6824
6837
|
route("POST", "/api/maintenance/jobs/:id/run", postMaintenanceJobRun),
|
package/src/token-db.ts
CHANGED
|
@@ -57,7 +57,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
57
57
|
name: "Provider Agent",
|
|
58
58
|
description: "Coding-agent runtime access for messages, commands, tasks, and scoped memory reads.",
|
|
59
59
|
role: "provider",
|
|
60
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read"],
|
|
60
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use"],
|
|
61
61
|
ttlSeconds: 24 * 60 * 60,
|
|
62
62
|
builtIn: true,
|
|
63
63
|
createdBy: "system",
|
|
@@ -67,7 +67,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
67
67
|
name: "Provider Child Agent",
|
|
68
68
|
description: "Delegated child-agent runtime access, constrained to its parent and spawn request.",
|
|
69
69
|
role: "provider",
|
|
70
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read"],
|
|
70
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use"],
|
|
71
71
|
constraints: { canDelegate: false },
|
|
72
72
|
ttlSeconds: 2 * 60 * 60,
|
|
73
73
|
builtIn: true,
|
|
@@ -78,7 +78,7 @@ const BUILT_IN_PROFILES: Array<Omit<TokenProfile, "createdAt" | "updatedAt">> =
|
|
|
78
78
|
name: "Provider Interactive Agent",
|
|
79
79
|
description: "User-launched provider runtime access constrained to its own agent and cwd for long interactive sessions.",
|
|
80
80
|
role: "provider",
|
|
81
|
-
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read"],
|
|
81
|
+
scope: ["agent:read", "agent:write", "message:read", "message:send", "command:read", "command:write", "task:read", "task:write", "memory:read", "artifact:read", "artifact:write", "mcp:use"],
|
|
82
82
|
constraints: { terminalAttach: false, logsRead: false, canDelegate: false },
|
|
83
83
|
ttlSeconds: 30 * 24 * 60 * 60,
|
|
84
84
|
builtIn: true,
|