agent-relay-server 0.57.2 → 0.59.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 +151 -1
- package/package.json +2 -2
- package/public/assets/{MessageBubble-CHKzD4RY.js → MessageBubble-BrEKymoV.js} +2 -2
- package/public/assets/{MessageBubble-CHKzD4RY.js.map → MessageBubble-BrEKymoV.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-BFVTJa0q.js → PlanGraphPanel-CXix1C-H.js} +2 -2
- package/public/assets/{PlanGraphPanel-BFVTJa0q.js.map → PlanGraphPanel-CXix1C-H.js.map} +1 -1
- package/public/assets/{activity-M-dFLU_O.js → activity-CRafQ1Je.js} +2 -2
- package/public/assets/{activity-M-dFLU_O.js.map → activity-CRafQ1Je.js.map} +1 -1
- package/public/assets/{agent-profiles-CvHSoyCX.js → agent-profiles-DW4o5-ND.js} +2 -2
- package/public/assets/{agent-profiles-CvHSoyCX.js.map → agent-profiles-DW4o5-ND.js.map} +1 -1
- package/public/assets/agents-Cbegv0Hm.js +2 -0
- package/public/assets/{agents-DZBM5dp_.js.map → agents-Cbegv0Hm.js.map} +1 -1
- package/public/assets/analytics-DX6T7JX3.js +910 -0
- package/public/assets/{analytics-bvjb3Urs.js.map → analytics-DX6T7JX3.js.map} +1 -1
- package/public/assets/{automation-CDSn9Uq-.js → automation-CzSBv4gg.js} +2 -2
- package/public/assets/{automation-CDSn9Uq-.js.map → automation-CzSBv4gg.js.map} +1 -1
- package/public/assets/{badge-Dj6PREuI.js → badge-Cy6qO_Jz.js} +2 -2
- package/public/assets/{badge-Dj6PREuI.js.map → badge-Cy6qO_Jz.js.map} +1 -1
- package/public/assets/{branch-state-badge-CdYg2HYc.js → branch-state-badge-CRA50r81.js} +2 -2
- package/public/assets/{branch-state-badge-CdYg2HYc.js.map → branch-state-badge-CRA50r81.js.map} +1 -1
- package/public/assets/{button-Cqpufqam.js → button-73wpRw65.js} +2 -2
- package/public/assets/{button-Cqpufqam.js.map → button-73wpRw65.js.map} +1 -1
- package/public/assets/{card-OmDF73CE.js → card-BU6PvVHv.js} +2 -2
- package/public/assets/{card-OmDF73CE.js.map → card-BU6PvVHv.js.map} +1 -1
- package/public/assets/{channels-BkxrdOFZ.js → channels-B1MQCPCT.js} +2 -2
- package/public/assets/{channels-BkxrdOFZ.js.map → channels-B1MQCPCT.js.map} +1 -1
- package/public/assets/chat-BqIaFYGJ.js +2 -0
- package/public/assets/{chat-YzX1z76l.js.map → chat-BqIaFYGJ.js.map} +1 -1
- package/public/assets/{connectors-Bg3rHgl_.js → connectors-C9aFJIrh.js} +2 -2
- package/public/assets/{connectors-Bg3rHgl_.js.map → connectors-C9aFJIrh.js.map} +1 -1
- package/public/assets/{copy-button-DI7b8A7_.js → copy-button-HRopUwui.js} +2 -2
- package/public/assets/{copy-button-DI7b8A7_.js.map → copy-button-HRopUwui.js.map} +1 -1
- package/public/assets/display-CketawEF.js.map +1 -1
- package/public/assets/{dist-CNjnTZEI.js → dist-CC5f7oYw.js} +2 -2
- package/public/assets/{dist-CNjnTZEI.js.map → dist-CC5f7oYw.js.map} +1 -1
- package/public/assets/{dist-DkeTrbTA.js → dist-CLHMeXYi.js} +2 -2
- package/public/assets/{dist-DkeTrbTA.js.map → dist-CLHMeXYi.js.map} +1 -1
- package/public/assets/{dist-C5t_OxI0.js → dist-Ci1MM51P.js} +2 -2
- package/public/assets/{dist-C5t_OxI0.js.map → dist-Ci1MM51P.js.map} +1 -1
- package/public/assets/{dist-CV3sgFiv.js → dist-Cko2KPW8.js} +2 -2
- package/public/assets/{dist-CV3sgFiv.js.map → dist-Cko2KPW8.js.map} +1 -1
- package/public/assets/dist-D41tzJYg.js +2 -0
- package/public/assets/{dist-yYauqYKR.js.map → dist-D41tzJYg.js.map} +1 -1
- package/public/assets/{dist-CoUvwLTy.js → dist-DFLciw0h.js} +2 -2
- package/public/assets/{dist-CoUvwLTy.js.map → dist-DFLciw0h.js.map} +1 -1
- package/public/assets/dist-DUoZnlB2.js +2 -0
- package/public/assets/{dist-DmhyU0Na.js.map → dist-DUoZnlB2.js.map} +1 -1
- package/public/assets/dist-De2oguBm.js +2 -0
- package/public/assets/{dist-DxkMwkrr.js.map → dist-De2oguBm.js.map} +1 -1
- package/public/assets/{dist-1x-LE9Uk.js → dist-DmN94rc8.js} +2 -2
- package/public/assets/{dist-1x-LE9Uk.js.map → dist-DmN94rc8.js.map} +1 -1
- package/public/assets/{dist-DV2tZ2fq.js → dist-Kkpnjx_M.js} +2 -2
- package/public/assets/{dist-DV2tZ2fq.js.map → dist-Kkpnjx_M.js.map} +1 -1
- package/public/assets/{dist-Ca3ujqG_.js → dist-T7u7IZGJ.js} +2 -2
- package/public/assets/{dist-Ca3ujqG_.js.map → dist-T7u7IZGJ.js.map} +1 -1
- package/public/assets/{es2015-DcavLcQ0.js → es2015-Df08Opjx.js} +2 -2
- package/public/assets/{es2015-DcavLcQ0.js.map → es2015-Df08Opjx.js.map} +1 -1
- package/public/assets/{formatted-body-impl-D8i9qqL1.js → formatted-body-impl-DOXH7oDF.js} +2 -2
- package/public/assets/{formatted-body-impl-D8i9qqL1.js.map → formatted-body-impl-DOXH7oDF.js.map} +1 -1
- package/public/assets/index-DaNOQJci.css +2 -0
- package/public/assets/index-br_Y6Cvt.js +21 -0
- package/public/assets/{index-Ba2RDJaA.js.map → index-br_Y6Cvt.js.map} +1 -1
- package/public/assets/{input-C_dl_9fG.js → input-B2EDz-BE.js} +2 -2
- package/public/assets/{input-C_dl_9fG.js.map → input-B2EDz-BE.js.map} +1 -1
- package/public/assets/{insights-D1aANwZ2.js → insights-B1edq5lk.js} +2 -2
- package/public/assets/{insights-D1aANwZ2.js.map → insights-B1edq5lk.js.map} +1 -1
- package/public/assets/{integrations-BrAr9bap.js → integrations-Cm4fjp7_.js} +2 -2
- package/public/assets/{integrations-BrAr9bap.js.map → integrations-Cm4fjp7_.js.map} +1 -1
- package/public/assets/{lib-B3iMVsf-.js → lib-BZrjIp-o.js} +3 -3
- package/public/assets/{lib-B3iMVsf-.js.map → lib-BZrjIp-o.js.map} +1 -1
- package/public/assets/lucide-react-H_-8G79A.js +9 -0
- package/public/assets/{lucide-react-CMhBract.js.map → lucide-react-H_-8G79A.js.map} +1 -1
- package/public/assets/{maintenance-C-xspsMG.js → maintenance-D6zvw0fU.js} +2 -2
- package/public/assets/{maintenance-C-xspsMG.js.map → maintenance-D6zvw0fU.js.map} +1 -1
- package/public/assets/{managed-agents-Bf3ni0vo.js → managed-agents-CzfEznM1.js} +2 -2
- package/public/assets/{managed-agents-Bf3ni0vo.js.map → managed-agents-CzfEznM1.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DKbUBTf_.js → markdown-preview-impl-DZlCaTHQ.js} +2 -2
- package/public/assets/{markdown-preview-impl-DKbUBTf_.js.map → markdown-preview-impl-DZlCaTHQ.js.map} +1 -1
- package/public/assets/memory-CUUAzJDc.js +2 -0
- package/public/assets/{memory-DeLwJFwH.js.map → memory-CUUAzJDc.js.map} +1 -1
- package/public/assets/messages-6QZH46LG.js +2 -0
- package/public/assets/{messages-Cww6dZnM.js.map → messages-6QZH46LG.js.map} +1 -1
- package/public/assets/{orchestrators-DRjwROXw.js → orchestrators-CrcqkPuc.js} +2 -2
- package/public/assets/{orchestrators-DRjwROXw.js.map → orchestrators-CrcqkPuc.js.map} +1 -1
- package/public/assets/overview-418xXZO_.js +2 -0
- package/public/assets/{overview-DjcRomU_.js.map → overview-418xXZO_.js.map} +1 -1
- package/public/assets/pairs-CnedATgT.js +2 -0
- package/public/assets/{pairs-DT0u2fnW.js.map → pairs-CnedATgT.js.map} +1 -1
- package/public/assets/{react-dom-Dtl9-x_1.js → react-dom-WI6TjVe7.js} +2 -2
- package/public/assets/{react-dom-Dtl9-x_1.js.map → react-dom-WI6TjVe7.js.map} +1 -1
- package/public/assets/{security-Bwg3oFI8.js → security-DS13pbag.js} +2 -2
- package/public/assets/{security-Bwg3oFI8.js.map → security-DS13pbag.js.map} +1 -1
- package/public/assets/{select-Cariwv69.js → select-BOprGZDS.js} +2 -2
- package/public/assets/{select-Cariwv69.js.map → select-BOprGZDS.js.map} +1 -1
- package/public/assets/{settings-Bd6Ug3DO.js → settings-BgInn_f3.js} +4 -4
- package/public/assets/{settings-Bd6Ug3DO.js.map → settings-BgInn_f3.js.map} +1 -1
- package/public/assets/{store-BgJq_lN9.js → store-DHO_a8BD.js} +5 -5
- package/public/assets/{store-BgJq_lN9.js.map → store-DHO_a8BD.js.map} +1 -1
- package/public/assets/{tasks-DVz1zhOU.js → tasks-B-4oCPuv.js} +2 -2
- package/public/assets/{tasks-DVz1zhOU.js.map → tasks-B-4oCPuv.js.map} +1 -1
- package/public/assets/teams-sCQAs_QA.js +2 -0
- package/public/assets/{teams-TqFntFB_.js.map → teams-sCQAs_QA.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-OsJ5crlx.js → terminal-viewer-impl-BhqS2XpS.js} +3 -3
- package/public/assets/{terminal-viewer-impl-OsJ5crlx.js.map → terminal-viewer-impl-BhqS2XpS.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-BYHcL7sP.js → use-keyboard-viewport-fOL_3RHB.js} +2 -2
- package/public/assets/{use-keyboard-viewport-BYHcL7sP.js.map → use-keyboard-viewport-fOL_3RHB.js.map} +1 -1
- package/public/assets/{work-queue-B4b2jPLR.js → work-queue-BDhjlwUH.js} +2 -2
- package/public/assets/{work-queue-B4b2jPLR.js.map → work-queue-BDhjlwUH.js.map} +1 -1
- package/public/assets/workspaces-6I-C69nf.js +3 -0
- package/public/assets/{workspaces-m97LP4Eu.js.map → workspaces-6I-C69nf.js.map} +1 -1
- package/public/index.html +21 -21
- package/src/db/index.ts +2 -0
- package/src/db/project-entries.ts +270 -0
- package/src/db/projects.ts +304 -0
- package/src/db/schema.ts +50 -0
- package/src/db/stats.ts +209 -38
- package/src/db/teams.ts +13 -2
- package/src/maintenance.ts +9 -2
- package/src/mcp-project-tools.ts +352 -0
- package/src/mcp.ts +6 -0
- package/src/project-ingest.ts +186 -0
- package/src/project-instructions.ts +176 -0
- package/src/routes/agents-spawn.ts +2 -0
- package/src/routes/commands.ts +11 -5
- package/src/routes/index.ts +5 -0
- package/src/routes/projects.ts +55 -0
- package/src/runtime-tokens.ts +6 -0
- package/src/security.ts +4 -0
- package/src/services/spawn-agent.ts +33 -2
- package/src/services/team.ts +5 -1
- package/src/token-db.ts +3 -3
- package/src/workspace-merge.ts +11 -0
- package/src/workspace-pr-completion.ts +57 -1
- package/public/assets/agents-DZBM5dp_.js +0 -2
- package/public/assets/analytics-bvjb3Urs.js +0 -910
- package/public/assets/chat-YzX1z76l.js +0 -2
- package/public/assets/dist-DmhyU0Na.js +0 -2
- package/public/assets/dist-DxkMwkrr.js +0 -2
- package/public/assets/dist-yYauqYKR.js +0 -2
- package/public/assets/index-Ba2RDJaA.js +0 -21
- package/public/assets/index-CtFXWxBJ.css +0 -2
- package/public/assets/lucide-react-CMhBract.js +0 -9
- package/public/assets/memory-DeLwJFwH.js +0 -2
- package/public/assets/messages-Cww6dZnM.js +0 -2
- package/public/assets/overview-DjcRomU_.js +0 -2
- package/public/assets/pairs-DT0u2fnW.js +0 -2
- package/public/assets/teams-TqFntFB_.js +0 -2
- package/public/assets/workspaces-m97LP4Eu.js +0 -3
package/src/db/stats.ts
CHANGED
|
@@ -135,20 +135,35 @@ export const ANALYTICS_PERIODS = {
|
|
|
135
135
|
|
|
136
136
|
export type AnalyticsPeriod = keyof typeof ANALYTICS_PERIODS;
|
|
137
137
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
138
|
+
// ─── Analytics taxonomy ─────────────────────────────────────────────────────
|
|
139
|
+
// The relay carries three distinct streams of activity, and analytics keeps them
|
|
140
|
+
// SEPARATE rather than collapsing into one "messages" count (which is ~94%
|
|
141
|
+
// session-mirror noise on a busy relay — see the work layer below):
|
|
142
|
+
// 1. Conversation — real inter-agent / human messaging (chat, replies, channel, pair, work items)
|
|
143
|
+
// 2. Work — session-mirror cognition: tool calls, reasoning, narration, responses
|
|
144
|
+
// 3. Fleet — agent registrations/spawns + per-agent activity (NOT online/offline:
|
|
145
|
+
// agents start, work, and shut down constantly — point-in-time status is noise here)
|
|
146
|
+
|
|
147
|
+
// Conversation categories. Order is significant: a claimable/channel/pair message
|
|
148
|
+
// is classified as such even when it is also a reply; only an otherwise-plain reply
|
|
149
|
+
// counts as "Replies". Session/control/system rows are EXCLUDED (they're streams 2/mechanical).
|
|
150
|
+
export const CONVERSATION_CATEGORIES = ["Chat", "Replies", "Channel", "Pair", "Work items"] as const;
|
|
151
|
+
export type ConversationCategory = (typeof CONVERSATION_CATEGORIES)[number];
|
|
152
|
+
const CONVERSATION_CATEGORY_SQL = `
|
|
144
153
|
CASE
|
|
145
154
|
WHEN claimable = 1 THEN 'Work items'
|
|
146
|
-
WHEN kind = 'system' OR from_agent = 'system' THEN 'System'
|
|
147
|
-
WHEN kind = 'pair' THEN 'Pair'
|
|
148
155
|
WHEN kind = 'channel.event' THEN 'Channel'
|
|
156
|
+
WHEN kind = 'pair' THEN 'Pair'
|
|
149
157
|
WHEN reply_to IS NOT NULL THEN 'Replies'
|
|
150
|
-
ELSE '
|
|
158
|
+
ELSE 'Chat'
|
|
151
159
|
END`;
|
|
160
|
+
// Only real messaging — excludes the session-mirror lane and mechanical/system rows.
|
|
161
|
+
const CONVERSATION_FILTER = `kind NOT IN ('session','control','system') AND from_agent != 'system'`;
|
|
162
|
+
|
|
163
|
+
// Session-mirror event sub-types we surface as the "agent work" composition. (prompt
|
|
164
|
+
// is counted in the user-interaction stream; notice is mechanical chatter.)
|
|
165
|
+
export const WORK_TYPES = ["tool", "reasoning", "narration", "response"] as const;
|
|
166
|
+
export type WorkType = (typeof WORK_TYPES)[number];
|
|
152
167
|
|
|
153
168
|
export interface AnalyticsData {
|
|
154
169
|
period: AnalyticsPeriod;
|
|
@@ -156,52 +171,196 @@ export interface AnalyticsData {
|
|
|
156
171
|
now: number;
|
|
157
172
|
bucketMs: number;
|
|
158
173
|
bucketCount: number;
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
// 1. Conversation — real messaging volume, per-bucket (oldest→newest) + totals.
|
|
175
|
+
conversation: {
|
|
176
|
+
buckets: Array<{ start: number; counts: Record<ConversationCategory, number> }>;
|
|
177
|
+
totals: Record<ConversationCategory, number>;
|
|
178
|
+
total: number;
|
|
179
|
+
};
|
|
180
|
+
// 2. Work — session-mirror cognition: per-bucket type counts, totals, tool leaderboard, turn count.
|
|
181
|
+
work: {
|
|
182
|
+
buckets: Array<{ start: number; counts: Record<WorkType, number> }>;
|
|
183
|
+
totals: Record<WorkType, number>;
|
|
184
|
+
topTools: Array<{ name: string; count: number }>;
|
|
185
|
+
turns: number;
|
|
186
|
+
toolCalls: number;
|
|
187
|
+
};
|
|
188
|
+
// 3. User interaction — human prompts, reply rate, and agent reply latency (median is
|
|
189
|
+
// the headline; avg is tail-inflated by agents that were away mid-conversation).
|
|
190
|
+
user: {
|
|
191
|
+
prompts: number;
|
|
192
|
+
replied: number;
|
|
193
|
+
replyRate: number; // 0..1
|
|
194
|
+
latencyMs: { median: number; avg: number; p90: number } | null;
|
|
195
|
+
buckets: Array<{ start: number; prompts: number }>;
|
|
196
|
+
};
|
|
197
|
+
// 4. Fleet — activity-based, never online/offline. Registrations (spawns) over time,
|
|
198
|
+
// how many were agent-spawned, distinct active agents, and a per-agent leaderboard.
|
|
199
|
+
fleet: {
|
|
200
|
+
activeAgents: number;
|
|
201
|
+
registrations: number;
|
|
202
|
+
spawnedByAgent: number;
|
|
203
|
+
buckets: Array<{ start: number; registrations: number }>;
|
|
204
|
+
leaderboard: Array<{ id: string; label: string | null; turns: number; toolCalls: number; messages: number }>;
|
|
205
|
+
};
|
|
206
|
+
// Busiest-hours grid: heatmap[dayOfWeek 0=Sun][hour 0-23], server-local time, all activity.
|
|
164
207
|
heatmap: number[][];
|
|
165
|
-
totalMessages: number;
|
|
166
208
|
totalReactions: number;
|
|
167
209
|
}
|
|
168
210
|
|
|
169
|
-
|
|
170
|
-
return {
|
|
211
|
+
function emptyConversationCounts(): Record<ConversationCategory, number> {
|
|
212
|
+
return { Chat: 0, Replies: 0, Channel: 0, Pair: 0, "Work items": 0 };
|
|
213
|
+
}
|
|
214
|
+
function emptyWorkCounts(): Record<WorkType, number> {
|
|
215
|
+
return { tool: 0, reasoning: 0, narration: 0, response: 0 };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Percentile over an unsorted list of numbers (linear interpolation-free nearest-rank).
|
|
219
|
+
function percentile(sorted: number[], p: number): number {
|
|
220
|
+
if (sorted.length === 0) return 0;
|
|
221
|
+
const idx = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
|
|
222
|
+
return sorted[idx] ?? 0;
|
|
171
223
|
}
|
|
172
224
|
|
|
173
225
|
export function getAnalytics(period: AnalyticsPeriod, now: number = Date.now()): AnalyticsData {
|
|
174
226
|
const { ms, buckets: bucketCount } = ANALYTICS_PERIODS[period] ?? ANALYTICS_PERIODS["24h"];
|
|
175
227
|
const start = now - ms;
|
|
176
228
|
const bucketMs = ms / bucketCount;
|
|
229
|
+
const db = getDb();
|
|
230
|
+
// created_at in (start, now] keeps the floored bucket index in [0, bucketCount).
|
|
231
|
+
const clampIdx = (bucket: number) => Math.min(Math.max(bucket, 0), bucketCount - 1);
|
|
232
|
+
const bucketStarts = Array.from({ length: bucketCount }, (_, i) => start + i * bucketMs);
|
|
177
233
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const categories = emptyCategoryCounts();
|
|
183
|
-
|
|
184
|
-
// One pass: bucket index × category counts. CAST floors toward zero; created_at
|
|
185
|
-
// in (start, now] keeps the index in [0, bucketCount).
|
|
186
|
-
const volumeRows = getDb()
|
|
234
|
+
// ── 1. Conversation: real messaging volume ──
|
|
235
|
+
const conversationBuckets = bucketStarts.map((s) => ({ start: s, counts: emptyConversationCounts() }));
|
|
236
|
+
const conversationTotals = emptyConversationCounts();
|
|
237
|
+
const convRows = db
|
|
187
238
|
.query(
|
|
188
|
-
`SELECT CAST((created_at - ?) / ? AS INTEGER) AS bucket, ${
|
|
189
|
-
FROM messages WHERE created_at > ? AND created_at <= ? GROUP BY bucket, category`,
|
|
239
|
+
`SELECT CAST((created_at - ?) / ? AS INTEGER) AS bucket, ${CONVERSATION_CATEGORY_SQL} AS category, COUNT(*) AS c
|
|
240
|
+
FROM messages WHERE created_at > ? AND created_at <= ? AND ${CONVERSATION_FILTER} GROUP BY bucket, category`,
|
|
190
241
|
)
|
|
191
|
-
.all(start, bucketMs, start, now) as Array<{ bucket: number; category:
|
|
192
|
-
for (const row of
|
|
193
|
-
const
|
|
194
|
-
const bucket = buckets[idx];
|
|
242
|
+
.all(start, bucketMs, start, now) as Array<{ bucket: number; category: ConversationCategory; c: number }>;
|
|
243
|
+
for (const row of convRows) {
|
|
244
|
+
const bucket = conversationBuckets[clampIdx(row.bucket)];
|
|
195
245
|
if (bucket && row.category in bucket.counts) {
|
|
196
246
|
bucket.counts[row.category] += row.c;
|
|
197
|
-
|
|
247
|
+
conversationTotals[row.category] += row.c;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const conversationTotal = Object.values(conversationTotals).reduce((sum, v) => sum + v, 0);
|
|
251
|
+
|
|
252
|
+
// ── 2. Work: session-mirror cognition ──
|
|
253
|
+
const workBuckets = bucketStarts.map((s) => ({ start: s, counts: emptyWorkCounts() }));
|
|
254
|
+
const workTotals = emptyWorkCounts();
|
|
255
|
+
const workRows = db
|
|
256
|
+
.query(
|
|
257
|
+
`SELECT CAST((created_at - ?) / ? AS INTEGER) AS bucket,
|
|
258
|
+
json_extract(payload, '$.session.type') AS wtype, COUNT(*) AS c
|
|
259
|
+
FROM messages WHERE created_at > ? AND created_at <= ? AND kind = 'session'
|
|
260
|
+
AND json_extract(payload, '$.session.type') IN ('tool','reasoning','narration','response')
|
|
261
|
+
GROUP BY bucket, wtype`,
|
|
262
|
+
)
|
|
263
|
+
.all(start, bucketMs, start, now) as Array<{ bucket: number; wtype: WorkType; c: number }>;
|
|
264
|
+
for (const row of workRows) {
|
|
265
|
+
const bucket = workBuckets[clampIdx(row.bucket)];
|
|
266
|
+
if (bucket && row.wtype in bucket.counts) {
|
|
267
|
+
bucket.counts[row.wtype] += row.c;
|
|
268
|
+
workTotals[row.wtype] += row.c;
|
|
198
269
|
}
|
|
199
270
|
}
|
|
200
|
-
const
|
|
271
|
+
const topTools = db
|
|
272
|
+
.query(
|
|
273
|
+
`SELECT json_extract(payload, '$.session.label') AS name, COUNT(*) AS count
|
|
274
|
+
FROM messages WHERE created_at > ? AND created_at <= ? AND kind = 'session'
|
|
275
|
+
AND json_extract(payload, '$.session.type') = 'tool'
|
|
276
|
+
AND json_extract(payload, '$.session.label') IS NOT NULL
|
|
277
|
+
GROUP BY name ORDER BY count DESC LIMIT 12`,
|
|
278
|
+
)
|
|
279
|
+
.all(start, now) as Array<{ name: string; count: number }>;
|
|
280
|
+
const turns = (
|
|
281
|
+
db
|
|
282
|
+
.query(
|
|
283
|
+
`SELECT COUNT(DISTINCT json_extract(payload, '$.session.turnId')) AS c
|
|
284
|
+
FROM messages WHERE created_at > ? AND created_at <= ? AND kind = 'session'
|
|
285
|
+
AND json_extract(payload, '$.session.turnId') IS NOT NULL`,
|
|
286
|
+
)
|
|
287
|
+
.get(start, now) as { c: number }
|
|
288
|
+
).c;
|
|
289
|
+
|
|
290
|
+
// ── 3. User interaction: human prompts + agent reply latency ──
|
|
291
|
+
const userBuckets = bucketStarts.map((s) => ({ start: s, prompts: 0 }));
|
|
292
|
+
const promptRows = db
|
|
293
|
+
.query(
|
|
294
|
+
`SELECT CAST((created_at - ?) / ? AS INTEGER) AS bucket, COUNT(*) AS c
|
|
295
|
+
FROM messages WHERE created_at > ? AND created_at <= ? AND from_agent = 'user' GROUP BY bucket`,
|
|
296
|
+
)
|
|
297
|
+
.all(start, bucketMs, start, now) as Array<{ bucket: number; c: number }>;
|
|
298
|
+
let prompts = 0;
|
|
299
|
+
for (const row of promptRows) {
|
|
300
|
+
const bucket = userBuckets[clampIdx(row.bucket)];
|
|
301
|
+
if (bucket) bucket.prompts += row.c;
|
|
302
|
+
prompts += row.c;
|
|
303
|
+
}
|
|
304
|
+
// Latency = first threaded reply from a non-user agent, in (start, now] by the prompt's time.
|
|
305
|
+
const latencyRows = db
|
|
306
|
+
.query(
|
|
307
|
+
`SELECT (r.created_at - u.created_at) AS delta
|
|
308
|
+
FROM messages u JOIN messages r ON r.reply_to = u.id
|
|
309
|
+
WHERE u.from_agent = 'user' AND r.from_agent != 'user'
|
|
310
|
+
AND u.created_at > ? AND u.created_at <= ? AND r.created_at >= u.created_at`,
|
|
311
|
+
)
|
|
312
|
+
.all(start, now) as Array<{ delta: number }>;
|
|
313
|
+
const deltas = latencyRows.map((r) => r.delta).sort((a, b) => a - b);
|
|
314
|
+
const replied = deltas.length;
|
|
315
|
+
const latencyMs =
|
|
316
|
+
replied > 0
|
|
317
|
+
? {
|
|
318
|
+
median: percentile(deltas, 50),
|
|
319
|
+
avg: Math.round(deltas.reduce((sum, d) => sum + d, 0) / replied),
|
|
320
|
+
p90: percentile(deltas, 90),
|
|
321
|
+
}
|
|
322
|
+
: null;
|
|
323
|
+
|
|
324
|
+
// ── 4. Fleet: registrations (spawns) over time + per-agent leaderboard ──
|
|
325
|
+
const fleetBuckets = bucketStarts.map((s) => ({ start: s, registrations: 0 }));
|
|
326
|
+
const regRows = db
|
|
327
|
+
.query(
|
|
328
|
+
`SELECT CAST((created_at - ?) / ? AS INTEGER) AS bucket, COUNT(*) AS c,
|
|
329
|
+
SUM(CASE WHEN spawned_by IS NOT NULL THEN 1 ELSE 0 END) AS spawned
|
|
330
|
+
FROM agents WHERE created_at > ? AND created_at <= ? GROUP BY bucket`,
|
|
331
|
+
)
|
|
332
|
+
.all(start, bucketMs, start, now) as Array<{ bucket: number; c: number; spawned: number }>;
|
|
333
|
+
let registrations = 0;
|
|
334
|
+
let spawnedByAgent = 0;
|
|
335
|
+
for (const row of regRows) {
|
|
336
|
+
const bucket = fleetBuckets[clampIdx(row.bucket)];
|
|
337
|
+
if (bucket) bucket.registrations += row.c;
|
|
338
|
+
registrations += row.c;
|
|
339
|
+
spawnedByAgent += row.spawned ?? 0;
|
|
340
|
+
}
|
|
341
|
+
const activeAgents = (
|
|
342
|
+
db
|
|
343
|
+
.query(
|
|
344
|
+
`SELECT COUNT(DISTINCT from_agent) AS c FROM messages
|
|
345
|
+
WHERE created_at > ? AND created_at <= ? AND from_agent NOT IN ('system','user')`,
|
|
346
|
+
)
|
|
347
|
+
.get(start, now) as { c: number }
|
|
348
|
+
).c;
|
|
349
|
+
const leaderboard = db
|
|
350
|
+
.query(
|
|
351
|
+
`SELECT m.from_agent AS id, a.label AS label,
|
|
352
|
+
COUNT(DISTINCT CASE WHEN m.kind = 'session' THEN json_extract(m.payload, '$.session.turnId') END) AS turns,
|
|
353
|
+
SUM(CASE WHEN m.kind = 'session' AND json_extract(m.payload, '$.session.type') = 'tool' THEN 1 ELSE 0 END) AS toolCalls,
|
|
354
|
+
SUM(CASE WHEN m.kind != 'session' THEN 1 ELSE 0 END) AS messages
|
|
355
|
+
FROM messages m LEFT JOIN agents a ON a.id = m.from_agent
|
|
356
|
+
WHERE m.created_at > ? AND m.created_at <= ? AND m.from_agent NOT IN ('system','user')
|
|
357
|
+
GROUP BY m.from_agent ORDER BY (toolCalls + messages) DESC LIMIT 10`,
|
|
358
|
+
)
|
|
359
|
+
.all(start, now) as Array<{ id: string; label: string | null; turns: number; toolCalls: number; messages: number }>;
|
|
201
360
|
|
|
202
|
-
// Busiest-hours heatmap (day-of-week × hour
|
|
361
|
+
// ── Busiest-hours heatmap (all activity, day-of-week × hour, server-local time) ──
|
|
203
362
|
const heatmap: number[][] = Array.from({ length: 7 }, () => new Array(24).fill(0));
|
|
204
|
-
const heatRows =
|
|
363
|
+
const heatRows = db
|
|
205
364
|
.query(
|
|
206
365
|
`SELECT CAST(strftime('%w', created_at / 1000, 'unixepoch', 'localtime') AS INTEGER) AS dow,
|
|
207
366
|
CAST(strftime('%H', created_at / 1000, 'unixepoch', 'localtime') AS INTEGER) AS hour, COUNT(*) AS c
|
|
@@ -214,7 +373,7 @@ export function getAnalytics(period: AnalyticsPeriod, now: number = Date.now()):
|
|
|
214
373
|
}
|
|
215
374
|
|
|
216
375
|
const totalReactions = (
|
|
217
|
-
|
|
376
|
+
db
|
|
218
377
|
.query(
|
|
219
378
|
`SELECT COUNT(*) AS c FROM message_reactions r
|
|
220
379
|
JOIN messages m ON m.id = r.message_id WHERE m.created_at > ? AND m.created_at <= ?`,
|
|
@@ -222,7 +381,19 @@ export function getAnalytics(period: AnalyticsPeriod, now: number = Date.now()):
|
|
|
222
381
|
.get(start, now) as { c: number }
|
|
223
382
|
).c;
|
|
224
383
|
|
|
225
|
-
return {
|
|
384
|
+
return {
|
|
385
|
+
period,
|
|
386
|
+
start,
|
|
387
|
+
now,
|
|
388
|
+
bucketMs,
|
|
389
|
+
bucketCount,
|
|
390
|
+
conversation: { buckets: conversationBuckets, totals: conversationTotals, total: conversationTotal },
|
|
391
|
+
work: { buckets: workBuckets, totals: workTotals, topTools, turns, toolCalls: workTotals.tool },
|
|
392
|
+
user: { prompts, replied, replyRate: prompts > 0 ? replied / prompts : 0, latencyMs, buckets: userBuckets },
|
|
393
|
+
fleet: { activeAgents, registrations, spawnedByAgent, buckets: fleetBuckets, leaderboard },
|
|
394
|
+
heatmap,
|
|
395
|
+
totalReactions,
|
|
396
|
+
};
|
|
226
397
|
}
|
|
227
398
|
|
|
228
399
|
export function getHealth(now: number = Date.now()): HealthReport {
|
package/src/db/teams.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { getAgent, setAgentTeam } from "./agents.ts";
|
|
3
3
|
import { getDb, ValidationError } from "./connection.ts";
|
|
4
4
|
import { rowToAgent } from "./mappers.ts";
|
|
5
|
+
import { unpromotedBlackboardEntries, type UnpromotedBlackboardSummary } from "./project-entries.ts";
|
|
5
6
|
import type { AgentCard, Team } from "../types";
|
|
6
7
|
|
|
7
8
|
interface TeamRow {
|
|
@@ -273,10 +274,20 @@ export function getTeamOutcomeAggregates(input: { recentLimit?: number } = {}):
|
|
|
273
274
|
};
|
|
274
275
|
}
|
|
275
276
|
|
|
276
|
-
export
|
|
277
|
+
export interface TeamDissolveResult {
|
|
278
|
+
team: Team;
|
|
279
|
+
/** Active blackboard findings not yet lifted into a project — surfaced (not auto-promoted)
|
|
280
|
+
* so the operator/agent can decide what graduates to durable project knowledge (#404). */
|
|
281
|
+
unpromotedBlackboard: UnpromotedBlackboardSummary;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function dissolveTeam(teamId: string, input: TeamDissolveInput = {}): TeamDissolveResult {
|
|
277
285
|
const existing = getTeam(teamId);
|
|
278
286
|
if (!existing) throw new ValidationError(`team ${teamId} not found`);
|
|
279
287
|
const members = getTeamRoster(teamId);
|
|
288
|
+
// Capture unpromoted findings BEFORE the row flips to dissolved — the blackboard rows
|
|
289
|
+
// survive dissolve, but reading the summary here keeps the surface atomic with the event.
|
|
290
|
+
const unpromotedBlackboard = unpromotedBlackboardEntries(teamId);
|
|
280
291
|
const now = Date.now();
|
|
281
292
|
getDb().query(`
|
|
282
293
|
UPDATE teams
|
|
@@ -299,5 +310,5 @@ export function dissolveTeam(teamId: string, input: TeamDissolveInput = {}): Tea
|
|
|
299
310
|
now,
|
|
300
311
|
teamId,
|
|
301
312
|
);
|
|
302
|
-
return getTeam(teamId)
|
|
313
|
+
return { team: getTeam(teamId)!, unpromotedBlackboard };
|
|
303
314
|
}
|
package/src/maintenance.ts
CHANGED
|
@@ -39,7 +39,7 @@ import type { WorkspaceMergePreview, WorkspaceRecord, WorkspaceStatus } from "./
|
|
|
39
39
|
import { requestWorkspaceMerge } from "./workspace-merge";
|
|
40
40
|
import { reconcileLandedWorkspace } from "./branch-landed";
|
|
41
41
|
import { isAwaitingReviewerApproval } from "./reviewer-pipeline";
|
|
42
|
-
import { preparePrCompletionScan } from "./workspace-pr-completion";
|
|
42
|
+
import { preparePrCompletionScan, reconcileLandedPr } from "./workspace-pr-completion";
|
|
43
43
|
import { notifyAgentOffline } from "./agent-lifecycle-events";
|
|
44
44
|
import { workspaceActiveClaim } from "./workspace-claim";
|
|
45
45
|
import { reapOrphanedWorktrees } from "./workspace-orphans";
|
|
@@ -655,6 +655,7 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
|
|
|
655
655
|
);
|
|
656
656
|
const flagged: string[] = [], cleared: string[] = [], merged: string[] = [], notifiedStewards: string[] = [];
|
|
657
657
|
const reviewerArmed: string[] = [], reviewerChangesRequested: string[] = [], relayPrMerged: string[] = [], relayPrWaiting: string[] = [];
|
|
658
|
+
const prRecycled: string[] = [];
|
|
658
659
|
|
|
659
660
|
for (const ws of candidates) {
|
|
660
661
|
const orch = orchestrators.find((candidate) => workspacePathWithinBase(ws.sourceCwd, candidate.baseDir));
|
|
@@ -687,6 +688,12 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
|
|
|
687
688
|
// branch-landed.ts (#304).
|
|
688
689
|
const landed = p.landed === true || p.prMerged === true;
|
|
689
690
|
if (landed && LANDED_RECONCILE_STATUSES.has(ws.status)) {
|
|
691
|
+
// A PR-land merges asynchronously on the remote, outside the relay's land path. For a
|
|
692
|
+
// LIVE owner that means recycle (land-and-continue, #423); for a gone owner, finalize
|
|
693
|
+
// terminal `merged` (#304). `reconcileLandedPr` owns that split — see workspace-pr-completion.
|
|
694
|
+
const outcome = reconcileLandedPr(ws, p);
|
|
695
|
+
if (outcome.kind === "recycle") { emitCommand(outcome.command); prRecycled.push(ws.id); continue; }
|
|
696
|
+
if (outcome.kind === "wait") { relayPrWaiting.push(`${ws.id}:${outcome.reason}`); continue; }
|
|
690
697
|
reconcileLandedWorkspace(ws, p);
|
|
691
698
|
merged.push(ws.id);
|
|
692
699
|
continue;
|
|
@@ -752,7 +759,7 @@ async function scanWorkspaceConflicts(): Promise<Record<string, unknown>> {
|
|
|
752
759
|
}
|
|
753
760
|
}
|
|
754
761
|
|
|
755
|
-
return { scanned: candidates.length, flagged, cleared, merged, notifiedStewards, reviewerArmed, reviewerChangesRequested, relayPrMerged, relayPrWaiting };
|
|
762
|
+
return { scanned: candidates.length, flagged, cleared, merged, prRecycled, notifiedStewards, reviewerArmed, reviewerChangesRequested, relayPrMerged, relayPrWaiting };
|
|
756
763
|
}
|
|
757
764
|
|
|
758
765
|
// Deterministic auto-land (Layer 0, #167/#207/#242): land conflict-free ready
|