agent-relay-server 0.4.23 → 0.4.24

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/src/db.ts CHANGED
@@ -3,6 +3,8 @@ import { randomUUID } from "node:crypto";
3
3
  import { VERSION } from "./config.ts";
4
4
  import type {
5
5
  AgentCard,
6
+ ActivityEvent,
7
+ ActivityEventInput,
6
8
  AgentSessionGuard,
7
9
  CreatePairInput,
8
10
  HealthCheck,
@@ -168,6 +170,27 @@ export function initDb(path: string = "agent-relay.db"): Database {
168
170
  PRIMARY KEY (operator_id, peer_id)
169
171
  );
170
172
  CREATE INDEX IF NOT EXISTS idx_inbox_drafts_operator ON inbox_drafts(operator_id);
173
+
174
+ CREATE TABLE IF NOT EXISTS activity_events (
175
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
176
+ operator_id TEXT,
177
+ client_id TEXT UNIQUE,
178
+ kind TEXT NOT NULL,
179
+ title TEXT NOT NULL,
180
+ body TEXT,
181
+ meta_text TEXT,
182
+ icon TEXT,
183
+ view TEXT,
184
+ peer_id TEXT,
185
+ message_id INTEGER,
186
+ pair_id TEXT,
187
+ task_id INTEGER,
188
+ agent_id TEXT,
189
+ metadata TEXT NOT NULL DEFAULT '{}',
190
+ created_at INTEGER NOT NULL
191
+ );
192
+ CREATE INDEX IF NOT EXISTS idx_activity_operator ON activity_events(operator_id, created_at);
193
+ CREATE INDEX IF NOT EXISTS idx_activity_created ON activity_events(created_at);
171
194
  `);
172
195
 
173
196
  // Migrations
@@ -438,6 +461,27 @@ function rowToInboxDraft(row: any): InboxDraft {
438
461
  };
439
462
  }
440
463
 
464
+ function rowToActivityEvent(row: any): ActivityEvent {
465
+ return {
466
+ id: row.id,
467
+ operatorId: row.operator_id ?? undefined,
468
+ clientId: row.client_id ?? undefined,
469
+ kind: row.kind,
470
+ title: row.title,
471
+ body: row.body ?? undefined,
472
+ meta: row.meta_text ?? undefined,
473
+ icon: row.icon ?? undefined,
474
+ view: row.view ?? undefined,
475
+ peer: row.peer_id ?? undefined,
476
+ messageId: row.message_id ?? undefined,
477
+ pairId: row.pair_id ?? undefined,
478
+ taskId: row.task_id ?? undefined,
479
+ agentId: row.agent_id ?? undefined,
480
+ metadata: parseJson(row.metadata, {}),
481
+ createdAt: row.created_at,
482
+ };
483
+ }
484
+
441
485
  const MSG_SELECT = `SELECT m.*, (
442
486
  SELECT json_group_array(agent_id) FROM message_reads WHERE message_id = m.id
443
487
  ) AS read_by_agents FROM messages m`;
@@ -1576,6 +1620,60 @@ export function deleteInboxDraft(operatorId: string, peerId: string): boolean {
1576
1620
  return db.prepare("DELETE FROM inbox_drafts WHERE operator_id = ? AND peer_id = ?").run(operatorId, peerId).changes > 0;
1577
1621
  }
1578
1622
 
1623
+ export function listActivityEvents(input: {
1624
+ operatorId?: string;
1625
+ limit?: number;
1626
+ since?: number;
1627
+ } = {}): ActivityEvent[] {
1628
+ const conditions: string[] = [];
1629
+ const params: any[] = [];
1630
+ if (input.operatorId) {
1631
+ conditions.push("operator_id = ?");
1632
+ params.push(input.operatorId);
1633
+ }
1634
+ if (input.since !== undefined) {
1635
+ conditions.push("created_at >= ?");
1636
+ params.push(input.since);
1637
+ }
1638
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1639
+ params.push(input.limit ?? 200);
1640
+ return (db.prepare(
1641
+ `SELECT * FROM activity_events ${where} ORDER BY created_at DESC, id DESC LIMIT ?`,
1642
+ ).all(...params) as any[]).map(rowToActivityEvent);
1643
+ }
1644
+
1645
+ export function createActivityEvent(input: ActivityEventInput): ActivityEvent {
1646
+ if (input.clientId) {
1647
+ const existing = db.prepare("SELECT * FROM activity_events WHERE client_id = ?").get(input.clientId) as any | undefined;
1648
+ if (existing) return rowToActivityEvent(existing);
1649
+ }
1650
+ const now = Date.now();
1651
+ const result = db.prepare(`
1652
+ INSERT INTO activity_events (
1653
+ operator_id, client_id, kind, title, body, meta_text, icon, view, peer_id,
1654
+ message_id, pair_id, task_id, agent_id, metadata, created_at
1655
+ )
1656
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1657
+ `).run(
1658
+ input.operatorId ?? null,
1659
+ input.clientId ?? null,
1660
+ input.kind,
1661
+ input.title,
1662
+ input.body ?? null,
1663
+ input.meta ?? null,
1664
+ input.icon ?? null,
1665
+ input.view ?? null,
1666
+ input.peer ?? null,
1667
+ input.messageId ?? null,
1668
+ input.pairId ?? null,
1669
+ input.taskId ?? null,
1670
+ input.agentId ?? null,
1671
+ JSON.stringify(input.metadata ?? {}),
1672
+ now,
1673
+ );
1674
+ return rowToActivityEvent(db.prepare("SELECT * FROM activity_events WHERE id = ?").get(Number(result.lastInsertRowid)));
1675
+ }
1676
+
1579
1677
  export function deleteMessage(id: number): boolean {
1580
1678
  return db.transaction(() => {
1581
1679
  // Break reply_to references from children so the FK doesn't block delete.
package/src/routes.ts CHANGED
@@ -40,12 +40,16 @@ import {
40
40
  setInboxThreadState,
41
41
  setInboxDraft,
42
42
  deleteInboxDraft,
43
+ listActivityEvents,
44
+ createActivityEvent,
43
45
  createCallbackDelivery,
44
46
  finishCallbackDelivery,
47
+ reapStaleAgents,
48
+ releaseExpiredClaims,
45
49
  validateAgentSession,
46
50
  ValidationError,
47
51
  } from "./db";
48
- import type { AgentSessionGuard, CreatePairInput, IntegrationEventInput, PairActionInput, PairMessageInput, PairStatus, RegisterAgentInput, SendMessageInput, TaskStatus, TaskStatusInput } from "./types";
52
+ import type { ActivityEventInput, ActivityKind, AgentSessionGuard, CreatePairInput, IntegrationEventInput, PairActionInput, PairMessageInput, PairStatus, RegisterAgentInput, SendMessageInput, TaskStatus, TaskStatusInput } from "./types";
49
53
  import { getIntegrationTokens, INTEGRATION_RATE_LIMIT_PER_MINUTE, MAX_BODY_BYTES } from "./config";
50
54
  import {
51
55
  getIntegrationAuth,
@@ -58,6 +62,7 @@ import {
58
62
  emitAgentStatus,
59
63
  emitAgentRemoved,
60
64
  emitMessageClaimed,
65
+ emitMessageClaimReleased,
61
66
  emitMessageDeleted,
62
67
  emitTaskChanged,
63
68
  } from "./sse";
@@ -147,6 +152,7 @@ const VALID_AGENT_STATUSES = ["online", "idle", "busy", "offline"] as const;
147
152
  const VALID_TASK_SEVERITIES = ["info", "warning", "critical"] as const;
148
153
  const VALID_TASK_STATUSES = ["open", "claimed", "in_progress", "blocked", "done", "failed", "canceled"] as const;
149
154
  const VALID_PAIR_STATUSES = ["pending", "active", "ended", "rejected", "expired"] as const;
155
+ const VALID_ACTIVITY_KINDS = ["message", "reply", "question", "operator", "pair", "task", "state"] as const;
150
156
  const integrationRateBuckets = new Map<string, { windowStart: number; count: number }>();
151
157
 
152
158
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -427,6 +433,26 @@ function normalizeInboxDraftInput(body: unknown): {
427
433
  };
428
434
  }
429
435
 
436
+ function normalizeActivityInput(body: unknown): ActivityEventInput {
437
+ if (!isRecord(body)) throw new ValidationError("JSON object body required");
438
+ return {
439
+ operatorId: cleanOperatorId(body.operatorId),
440
+ clientId: cleanString(body.clientId, "clientId", { max: 240 }),
441
+ kind: cleanEnum(body.kind, "kind", VALID_ACTIVITY_KINDS, "operator") as ActivityKind,
442
+ title: cleanString(body.title, "title", { required: true, max: 200 })!,
443
+ body: cleanString(body.body, "body", { max: 1000 }),
444
+ meta: cleanString(body.meta, "meta", { max: 500 }),
445
+ icon: cleanString(body.icon, "icon", { max: 80 }),
446
+ view: cleanString(body.view, "view", { max: 80 }),
447
+ peer: cleanString(body.peer, "peer", { max: 200 }),
448
+ messageId: cleanPositiveId(body.messageId, "messageId"),
449
+ pairId: cleanString(body.pairId, "pairId", { max: 120 }),
450
+ taskId: cleanPositiveId(body.taskId, "taskId"),
451
+ agentId: cleanString(body.agentId, "agentId", { max: 200 }),
452
+ metadata: cleanMeta(body.metadata),
453
+ };
454
+ }
455
+
430
456
  function pairErrorStatus(code: string): number {
431
457
  if (code === "not_found") return 404;
432
458
  if (code === "busy") return 409;
@@ -489,14 +515,39 @@ async function postCallback(deliveryId: number, url: string, payload: unknown):
489
515
  }
490
516
  }
491
517
 
518
+ function auditEvent(input: ActivityEventInput): void {
519
+ try {
520
+ createActivityEvent({
521
+ ...input,
522
+ metadata: { source: "server", ...(input.metadata ?? {}) },
523
+ });
524
+ } catch {
525
+ // Audit trail writes must never block relay behavior.
526
+ }
527
+ }
528
+
492
529
  // --- Agent routes ---
493
530
 
494
531
  const postAgent: Handler = async (req) => {
495
532
  const parsed = await parseBody<unknown>(req);
496
533
  if (!parsed.ok) return error(parsed.error, parsed.status);
497
534
  try {
498
- const agent = upsertAgent(normalizeAgentInput(parsed.body));
535
+ const input = normalizeAgentInput(parsed.body);
536
+ const existing = getAgent(input.id);
537
+ const agent = upsertAgent(input);
499
538
  emitAgentStatus(agent.id);
539
+ if (!existing) {
540
+ auditEvent({
541
+ clientId: "server-agent-" + agent.id + "-registered",
542
+ kind: "state",
543
+ title: "Agent registered",
544
+ body: agent.name,
545
+ meta: agent.id,
546
+ icon: "ti-robot",
547
+ view: "agents",
548
+ agentId: agent.id,
549
+ });
550
+ }
500
551
  return json(agent, 201);
501
552
  } catch (e) {
502
553
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -542,6 +593,15 @@ const patchAgentStatus: Handler = async (req, params) => {
542
593
  throw e;
543
594
  }
544
595
  emitAgentStatus(params.id!);
596
+ auditEvent({
597
+ clientId: "server-agent-" + params.id! + "-status-" + body.status + "-" + Date.now(),
598
+ kind: "state",
599
+ title: "Agent " + body.status,
600
+ meta: params.id!,
601
+ icon: body.status === "offline" ? "ti-plug-off" : "ti-activity",
602
+ view: "agents",
603
+ agentId: params.id!,
604
+ });
545
605
  return json({ ok: true });
546
606
  };
547
607
 
@@ -600,6 +660,15 @@ const patchAgentReady: Handler = async (req, params) => {
600
660
  throw e;
601
661
  }
602
662
  emitAgentStatus(params.id!);
663
+ auditEvent({
664
+ clientId: "server-agent-" + params.id! + "-ready-" + body.ready + "-" + Date.now(),
665
+ kind: "state",
666
+ title: body.ready ? "Agent ready" : "Agent not ready",
667
+ meta: params.id!,
668
+ icon: body.ready ? "ti-circle-check" : "ti-loader",
669
+ view: "agents",
670
+ agentId: params.id!,
671
+ });
603
672
  return json({ ok: true });
604
673
  };
605
674
 
@@ -626,7 +695,20 @@ const postMessage: Handler = async (req) => {
626
695
  input.idempotencyKey = cleanString(req.headers.get("Idempotency-Key") ?? undefined, "idempotencyKey", { max: 240 });
627
696
  }
628
697
  const result = sendMessageWithResult(input);
629
- if (result.created) emitNewMessage(result.message);
698
+ if (result.created) {
699
+ emitNewMessage(result.message);
700
+ auditEvent({
701
+ clientId: "server-message-" + result.message.id,
702
+ kind: result.message.claimable ? "task" : "message",
703
+ title: result.message.claimable ? "Claimable message sent" : "Message sent",
704
+ body: result.message.subject || result.message.body,
705
+ meta: `${result.message.from} -> ${result.message.to}`,
706
+ icon: result.message.claimable ? "ti-hand-grab" : "ti-send",
707
+ view: "messages",
708
+ messageId: result.message.id,
709
+ agentId: result.message.from,
710
+ });
711
+ }
630
712
  return json(result.message, result.created ? 201 : 200);
631
713
  } catch (e) {
632
714
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -725,6 +807,17 @@ const postClaimMessage: Handler = async (req, params) => {
725
807
  const result = claimMessage(id, body.agentId, guard);
726
808
  if (result.ok) {
727
809
  emitMessageClaimed(id, body.agentId, getMessage(id)?.claimExpiresAt);
810
+ auditEvent({
811
+ clientId: "server-message-" + id + "-claimed-" + body.agentId,
812
+ kind: "task",
813
+ title: "Message claimed",
814
+ body: "Message #" + id,
815
+ meta: "by " + body.agentId,
816
+ icon: "ti-user-check",
817
+ view: "work",
818
+ messageId: id,
819
+ agentId: body.agentId,
820
+ });
728
821
  if (result.task) {
729
822
  emitTaskChanged(result.task, "task.claimed");
730
823
  void dispatchTaskCallbacks(result.task.id, "task.claimed");
@@ -830,6 +923,37 @@ const deleteInboxDraftRoute: Handler = (req) => {
830
923
  }
831
924
  };
832
925
 
926
+ // --- Shared activity audit trail ---
927
+
928
+ const getActivityEvents: Handler = (req) => {
929
+ const url = new URL(req.url);
930
+ try {
931
+ const limitRaw = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500 });
932
+ if (Number.isNaN(limitRaw)) return error("limit must be an integer between 1 and 500");
933
+ const sinceRaw = parseQueryInt(url.searchParams.get("since"), { min: 0, max: Number.MAX_SAFE_INTEGER });
934
+ if (Number.isNaN(sinceRaw)) return error("since must be a non-negative integer");
935
+ return json(listActivityEvents({
936
+ operatorId: cleanString(url.searchParams.get("operatorId") ?? undefined, "operatorId", { max: 200 }),
937
+ limit: limitRaw ?? 200,
938
+ since: sinceRaw ?? undefined,
939
+ }));
940
+ } catch (e) {
941
+ if (e instanceof ValidationError) return error(e.message, 400);
942
+ throw e;
943
+ }
944
+ };
945
+
946
+ const postActivityEvent: Handler = async (req) => {
947
+ const parsed = await parseBody<unknown>(req);
948
+ if (!parsed.ok) return error(parsed.error, parsed.status);
949
+ try {
950
+ return json(createActivityEvent(normalizeActivityInput(parsed.body)), 201);
951
+ } catch (e) {
952
+ if (e instanceof ValidationError) return error(e.message, 400);
953
+ throw e;
954
+ }
955
+ };
956
+
833
957
  // --- Tasks and integrations ---
834
958
 
835
959
  const postIntegrationEvent: Handler = async (req) => {
@@ -904,6 +1028,17 @@ const postClaimTask: Handler = async (req, params) => {
904
1028
  return error(result.error!, status);
905
1029
  }
906
1030
  emitTaskChanged(result.task!, "task.claimed");
1031
+ auditEvent({
1032
+ clientId: "server-task-" + id + "-claimed-" + agentId,
1033
+ kind: "task",
1034
+ title: "Task claimed",
1035
+ body: result.task!.title,
1036
+ meta: "by " + agentId,
1037
+ icon: "ti-user-check",
1038
+ view: "work",
1039
+ taskId: id,
1040
+ agentId,
1041
+ });
907
1042
  if (result.task!.messageId) {
908
1043
  emitMessageClaimed(result.task!.messageId, agentId, getMessage(result.task!.messageId)?.claimExpiresAt);
909
1044
  }
@@ -946,6 +1081,17 @@ const patchTaskStatus: Handler = async (req, params) => {
946
1081
  const result = updateTaskStatus(id, normalizeTaskStatusInput(parsed.body));
947
1082
  if (!result.ok) return error(result.error!, result.error === "task not found" ? 404 : agentSessionStatus(result.error));
948
1083
  emitTaskChanged(result.task!, "task.status");
1084
+ auditEvent({
1085
+ clientId: "server-task-" + id + "-status-" + result.task!.status + "-" + Date.now(),
1086
+ kind: "task",
1087
+ title: "Task " + result.task!.status,
1088
+ body: result.task!.title,
1089
+ meta: result.task!.claimedBy ? "by " + result.task!.claimedBy : result.task!.target,
1090
+ icon: "ti-arrows-exchange",
1091
+ view: "work",
1092
+ taskId: id,
1093
+ agentId: result.task!.claimedBy,
1094
+ });
949
1095
  void dispatchTaskCallbacks(id, "task.status");
950
1096
  return json({ task: result.task, event: result.event });
951
1097
  } catch (e) {
@@ -963,6 +1109,17 @@ const postPair: Handler = async (req) => {
963
1109
  const result = createPair(normalizeCreatePairInput(parsed.body));
964
1110
  if (!result.ok) return pairError(result);
965
1111
  emitNewMessage(result.invite);
1112
+ auditEvent({
1113
+ clientId: "server-pair-" + result.pair.id + "-invited",
1114
+ kind: "pair",
1115
+ title: "Pair invited",
1116
+ body: result.pair.objective,
1117
+ meta: `${result.pair.requesterId} <-> ${result.pair.targetId}`,
1118
+ icon: "ti-link-plus",
1119
+ view: "pairs",
1120
+ pairId: result.pair.id,
1121
+ agentId: result.pair.requesterId,
1122
+ });
966
1123
  return json({ pair: result.pair, invite: result.invite }, 201);
967
1124
  } catch (e) {
968
1125
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -994,6 +1151,17 @@ const postAcceptPair: Handler = async (req, params) => {
994
1151
  const result = acceptPair(params.id!, normalizePairActionInput(parsed.body));
995
1152
  if (!result.ok) return pairError(result);
996
1153
  for (const notice of result.notices) emitNewMessage(notice);
1154
+ auditEvent({
1155
+ clientId: "server-pair-" + result.pair.id + "-accepted",
1156
+ kind: "pair",
1157
+ title: "Pair accepted",
1158
+ body: result.pair.objective,
1159
+ meta: `${result.pair.requesterId} <-> ${result.pair.targetId}`,
1160
+ icon: "ti-link",
1161
+ view: "pairs",
1162
+ pairId: result.pair.id,
1163
+ agentId: result.pair.targetId,
1164
+ });
997
1165
  return json(result.pair);
998
1166
  } catch (e) {
999
1167
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -1008,6 +1176,17 @@ const postRejectPair: Handler = async (req, params) => {
1008
1176
  const result = rejectPair(params.id!, normalizePairActionInput(parsed.body));
1009
1177
  if (!result.ok) return pairError(result);
1010
1178
  emitNewMessage(result.notice);
1179
+ auditEvent({
1180
+ clientId: "server-pair-" + result.pair.id + "-rejected",
1181
+ kind: "pair",
1182
+ title: "Pair rejected",
1183
+ body: result.pair.objective,
1184
+ meta: `${result.pair.requesterId} <-> ${result.pair.targetId}`,
1185
+ icon: "ti-x",
1186
+ view: "pairs",
1187
+ pairId: result.pair.id,
1188
+ agentId: result.pair.targetId,
1189
+ });
1011
1190
  return json(result.pair);
1012
1191
  } catch (e) {
1013
1192
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -1022,6 +1201,17 @@ const postHangupPair: Handler = async (req, params) => {
1022
1201
  const result = endPair(params.id!, normalizePairActionInput(parsed.body));
1023
1202
  if (!result.ok) return pairError(result);
1024
1203
  if (result.notice) emitNewMessage(result.notice);
1204
+ auditEvent({
1205
+ clientId: "server-pair-" + result.pair.id + "-ended-" + Date.now(),
1206
+ kind: "pair",
1207
+ title: "Pair ended",
1208
+ body: result.pair.objective,
1209
+ meta: result.pair.endedBy ? "by " + result.pair.endedBy : `${result.pair.requesterId} <-> ${result.pair.targetId}`,
1210
+ icon: "ti-phone-off",
1211
+ view: "pairs",
1212
+ pairId: result.pair.id,
1213
+ agentId: result.pair.endedBy,
1214
+ });
1025
1215
  return json(result.pair);
1026
1216
  } catch (e) {
1027
1217
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -1036,6 +1226,18 @@ const postPairMessage: Handler = async (req, params) => {
1036
1226
  const result = sendPairMessage(params.id!, normalizePairMessageInput(parsed.body));
1037
1227
  if (!result.ok) return pairError(result);
1038
1228
  emitNewMessage(result.message);
1229
+ auditEvent({
1230
+ clientId: "server-pair-message-" + result.message.id,
1231
+ kind: "pair",
1232
+ title: "Pair message sent",
1233
+ body: result.message.subject || result.message.body,
1234
+ meta: "from " + result.message.from,
1235
+ icon: "ti-messages",
1236
+ view: "pairs",
1237
+ messageId: result.message.id,
1238
+ pairId: result.pair.id,
1239
+ agentId: result.message.from,
1240
+ });
1039
1241
  return json({ pair: result.pair, message: result.message }, 201);
1040
1242
  } catch (e) {
1041
1243
  if (e instanceof ValidationError) return error(e.message, 400);
@@ -1056,6 +1258,28 @@ const getEvents: Handler = (req) => {
1056
1258
  const getStatsRoute: Handler = () => json(getStats());
1057
1259
  const getHealthRoute: Handler = () => json(getHealth());
1058
1260
 
1261
+ const postSystemReap: Handler = () => {
1262
+ const released = releaseExpiredClaims();
1263
+ const reapedAgentIds = reapStaleAgents();
1264
+ for (const id of released.messageIds) emitMessageClaimReleased(id);
1265
+ for (const task of released.tasks) emitTaskChanged(task, "task.updated");
1266
+ for (const id of reapedAgentIds) emitAgentStatus(id);
1267
+ auditEvent({
1268
+ clientId: "server-system-reap-" + Date.now(),
1269
+ kind: "state",
1270
+ title: "Maintenance reaper run",
1271
+ body: `${reapedAgentIds.length} stale agent(s), ${released.messageIds.length} message claim(s), ${released.tasks.length} task claim(s)`,
1272
+ icon: "ti-broom",
1273
+ view: "activity",
1274
+ });
1275
+ return json({
1276
+ ok: true,
1277
+ reapedAgentIds,
1278
+ releasedMessageIds: released.messageIds,
1279
+ releasedTaskIds: released.tasks.map((task) => task.id),
1280
+ });
1281
+ };
1282
+
1059
1283
  // --- Router ---
1060
1284
 
1061
1285
  interface Route {
@@ -1106,6 +1330,9 @@ const routes: Route[] = [
1106
1330
  route("PUT", "/api/inbox/drafts", putInboxDraft),
1107
1331
  route("DELETE", "/api/inbox/drafts", deleteInboxDraftRoute),
1108
1332
 
1333
+ route("GET", "/api/activity", getActivityEvents),
1334
+ route("POST", "/api/activity", postActivityEvent),
1335
+
1109
1336
  route("POST", "/api/integrations/events", postIntegrationEvent),
1110
1337
  route("GET", "/api/tasks", getTasks),
1111
1338
  route("GET", "/api/tasks/:id", getTaskById),
@@ -1125,6 +1352,7 @@ const routes: Route[] = [
1125
1352
  route("GET", "/api/events", getEvents),
1126
1353
  route("GET", "/api/stats", getStatsRoute),
1127
1354
  route("GET", "/api/health", getHealthRoute),
1355
+ route("POST", "/api/system/reap", postSystemReap),
1128
1356
  ];
1129
1357
 
1130
1358
  export function matchRoute(
package/src/security.ts CHANGED
@@ -97,6 +97,7 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
97
97
  if (pathname === "/api/events") return "events:read";
98
98
  if (pathname.startsWith("/api/integrations/")) return method === "GET" ? "integrations:read" : "integrations:write";
99
99
  if (pathname.startsWith("/api/agents")) return method === "GET" ? "agents:read" : "agents:write";
100
+ if (pathname.startsWith("/api/activity")) return method === "GET" ? "activity:read" : "activity:write";
100
101
  if (pathname.startsWith("/api/inbox")) return method === "GET" ? "messages:read" : "messages:write";
101
102
  if (pathname.startsWith("/api/messages")) return method === "GET" ? "messages:read" : "messages:write";
102
103
  if (pathname.startsWith("/api/tasks")) return method === "GET" ? "tasks:read" : "tasks:write";
package/src/types.ts CHANGED
@@ -207,6 +207,44 @@ export interface InboxState {
207
207
  drafts: InboxDraft[];
208
208
  }
209
209
 
210
+ export type ActivityKind = "message" | "reply" | "question" | "operator" | "pair" | "task" | "state";
211
+
212
+ export interface ActivityEvent {
213
+ id: number;
214
+ operatorId?: string;
215
+ clientId?: string;
216
+ kind: ActivityKind;
217
+ title: string;
218
+ body?: string;
219
+ meta?: string;
220
+ icon?: string;
221
+ view?: string;
222
+ peer?: string;
223
+ messageId?: number;
224
+ pairId?: string;
225
+ taskId?: number;
226
+ agentId?: string;
227
+ metadata: Record<string, unknown>;
228
+ createdAt: number;
229
+ }
230
+
231
+ export interface ActivityEventInput {
232
+ operatorId?: string;
233
+ clientId?: string;
234
+ kind: ActivityKind;
235
+ title: string;
236
+ body?: string;
237
+ meta?: string;
238
+ icon?: string;
239
+ view?: string;
240
+ peer?: string;
241
+ messageId?: number;
242
+ pairId?: string;
243
+ taskId?: number;
244
+ agentId?: string;
245
+ metadata?: Record<string, unknown>;
246
+ }
247
+
210
248
  export interface HealthCheck {
211
249
  name: string;
212
250
  status: "ok" | "warn" | "error";