agent-relay-server 0.72.0 → 0.73.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.
Files changed (144) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +2 -2
  3. package/public/assets/MessageBubble-BWdM9UCM.js +3 -0
  4. package/public/assets/MessageBubble-BWdM9UCM.js.map +1 -0
  5. package/public/assets/{PlanGraphPanel-CubzRNGK.js → PlanGraphPanel-icESxd82.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-CubzRNGK.js.map → PlanGraphPanel-icESxd82.js.map} +1 -1
  7. package/public/assets/{activity-83lDNDEp.js → activity-BR9I23hc.js} +2 -2
  8. package/public/assets/{activity-83lDNDEp.js.map → activity-BR9I23hc.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BzjoH-NX.js → agent-profiles-BB0V-Tg1.js} +2 -2
  10. package/public/assets/{agent-profiles-BzjoH-NX.js.map → agent-profiles-BB0V-Tg1.js.map} +1 -1
  11. package/public/assets/{agents-BQhoABy8.js → agents-Clkboz93.js} +2 -2
  12. package/public/assets/{agents-BQhoABy8.js.map → agents-Clkboz93.js.map} +1 -1
  13. package/public/assets/{analytics-BrsfrNFw.js → analytics-DRlHs5ho.js} +3 -3
  14. package/public/assets/{analytics-BrsfrNFw.js.map → analytics-DRlHs5ho.js.map} +1 -1
  15. package/public/assets/{automation-VYuix2Iu.js → automation-DlnH_MPy.js} +2 -2
  16. package/public/assets/{automation-VYuix2Iu.js.map → automation-DlnH_MPy.js.map} +1 -1
  17. package/public/assets/{badge-lljplQxm.js → badge-YQhT30dy.js} +2 -2
  18. package/public/assets/{badge-lljplQxm.js.map → badge-YQhT30dy.js.map} +1 -1
  19. package/public/assets/branch-state-badge-Dg1qnmF5.js +2 -0
  20. package/public/assets/{branch-state-badge-DjdzycOv.js.map → branch-state-badge-Dg1qnmF5.js.map} +1 -1
  21. package/public/assets/{button-BwkKot02.js → button-CaFI76Ja.js} +2 -2
  22. package/public/assets/{button-BwkKot02.js.map → button-CaFI76Ja.js.map} +1 -1
  23. package/public/assets/card-B_fQtKce.js +2 -0
  24. package/public/assets/{card-ByAPej7n.js.map → card-B_fQtKce.js.map} +1 -1
  25. package/public/assets/{channels-n4vJBF5t.js → channels-BVYLrKUL.js} +2 -2
  26. package/public/assets/{channels-n4vJBF5t.js.map → channels-BVYLrKUL.js.map} +1 -1
  27. package/public/assets/chat-ml_YzbK4.js +2 -0
  28. package/public/assets/chat-ml_YzbK4.js.map +1 -0
  29. package/public/assets/{connectors-B8cKMHKY.js → connectors-BOyk5hC3.js} +2 -2
  30. package/public/assets/{connectors-B8cKMHKY.js.map → connectors-BOyk5hC3.js.map} +1 -1
  31. package/public/assets/copy-button-m00JmflM.js +2 -0
  32. package/public/assets/{copy-button-Pha61-a_.js.map → copy-button-m00JmflM.js.map} +1 -1
  33. package/public/assets/{dist-B2WRWI7O.js → dist-B_yrHbsb.js} +2 -2
  34. package/public/assets/{dist-B2WRWI7O.js.map → dist-B_yrHbsb.js.map} +1 -1
  35. package/public/assets/{dist-BXI214fV.js → dist-C-viWls-.js} +2 -2
  36. package/public/assets/{dist-BXI214fV.js.map → dist-C-viWls-.js.map} +1 -1
  37. package/public/assets/dist-CYS2puT9.js +2 -0
  38. package/public/assets/{dist-CXzYZzCR.js.map → dist-CYS2puT9.js.map} +1 -1
  39. package/public/assets/{dist-BowDWFij.js → dist-ChP4KvKi.js} +2 -2
  40. package/public/assets/{dist-BowDWFij.js.map → dist-ChP4KvKi.js.map} +1 -1
  41. package/public/assets/{dist-0mCQ5WMT.js → dist-Cnlh_NpI.js} +2 -2
  42. package/public/assets/{dist-0mCQ5WMT.js.map → dist-Cnlh_NpI.js.map} +1 -1
  43. package/public/assets/dist-CydxJz3m.js +2 -0
  44. package/public/assets/{dist-DTgYq8HB.js.map → dist-CydxJz3m.js.map} +1 -1
  45. package/public/assets/{dist-CXLRXIMd.js → dist-D7K3qgp7.js} +2 -2
  46. package/public/assets/{dist-CXLRXIMd.js.map → dist-D7K3qgp7.js.map} +1 -1
  47. package/public/assets/{dist-CcBB6z4x.js → dist-D8Yr1AIJ.js} +2 -2
  48. package/public/assets/{dist-CcBB6z4x.js.map → dist-D8Yr1AIJ.js.map} +1 -1
  49. package/public/assets/{dist-CT_Zr-w5.js → dist-DeAdFM5_.js} +2 -2
  50. package/public/assets/{dist-CT_Zr-w5.js.map → dist-DeAdFM5_.js.map} +1 -1
  51. package/public/assets/{dist-BBMeJabC.js → dist-DeM72J6a.js} +2 -2
  52. package/public/assets/{dist-BBMeJabC.js.map → dist-DeM72J6a.js.map} +1 -1
  53. package/public/assets/dist-DfQP9-VC.js +2 -0
  54. package/public/assets/{dist-CgbWbsmS.js.map → dist-DfQP9-VC.js.map} +1 -1
  55. package/public/assets/{es2015-CkzQKfKp.js → es2015-C3DnXBqu.js} +2 -2
  56. package/public/assets/{es2015-CkzQKfKp.js.map → es2015-C3DnXBqu.js.map} +1 -1
  57. package/public/assets/{formatted-body-impl-Drko1nzi.js → formatted-body-impl-D2F76gPY.js} +2 -2
  58. package/public/assets/{formatted-body-impl-Drko1nzi.js.map → formatted-body-impl-D2F76gPY.js.map} +1 -1
  59. package/public/assets/index-CQQTCZxg.css +2 -0
  60. package/public/assets/index-DYmgI4s4.js +22 -0
  61. package/public/assets/{index-ChQVgQyf.js.map → index-DYmgI4s4.js.map} +1 -1
  62. package/public/assets/{input-sp_EWS3J.js → input-Cqqq0OFl.js} +2 -2
  63. package/public/assets/{input-sp_EWS3J.js.map → input-Cqqq0OFl.js.map} +1 -1
  64. package/public/assets/{insights-BRclOzwJ.js → insights-DylYp2q4.js} +2 -2
  65. package/public/assets/{insights-BRclOzwJ.js.map → insights-DylYp2q4.js.map} +1 -1
  66. package/public/assets/{integrations-CfaOLw9i.js → integrations-B9ViWoJy.js} +2 -2
  67. package/public/assets/{integrations-CfaOLw9i.js.map → integrations-B9ViWoJy.js.map} +1 -1
  68. package/public/assets/{lib-DNMJa4Hh.js → lib-6L4m0E3M.js} +3 -3
  69. package/public/assets/{lib-DNMJa4Hh.js.map → lib-6L4m0E3M.js.map} +1 -1
  70. package/public/assets/{lucide-react--HSX8ykl.js → lucide-react-DI8kTavp.js} +2 -2
  71. package/public/assets/{lucide-react--HSX8ykl.js.map → lucide-react-DI8kTavp.js.map} +1 -1
  72. package/public/assets/{maintenance-BWbsnscm.js → maintenance-inyZGLys.js} +2 -2
  73. package/public/assets/{maintenance-BWbsnscm.js.map → maintenance-inyZGLys.js.map} +1 -1
  74. package/public/assets/{managed-agents-D6yncoFy.js → managed-agents-CEFvMPN-.js} +2 -2
  75. package/public/assets/{managed-agents-D6yncoFy.js.map → managed-agents-CEFvMPN-.js.map} +1 -1
  76. package/public/assets/{markdown-preview-impl-o59KaM_T.js → markdown-preview-impl-CniyG4_g.js} +2 -2
  77. package/public/assets/{markdown-preview-impl-o59KaM_T.js.map → markdown-preview-impl-CniyG4_g.js.map} +1 -1
  78. package/public/assets/memory-DDYGuRzr.js +2 -0
  79. package/public/assets/{memory-CXoJ1LvP.js.map → memory-DDYGuRzr.js.map} +1 -1
  80. package/public/assets/{messages-EqiGQvcb.js → messages-BMFcsMMa.js} +2 -2
  81. package/public/assets/{messages-EqiGQvcb.js.map → messages-BMFcsMMa.js.map} +1 -1
  82. package/public/assets/{orchestrators-Ds_vwaEy.js → orchestrators-D5nL4a52.js} +2 -2
  83. package/public/assets/{orchestrators-Ds_vwaEy.js.map → orchestrators-D5nL4a52.js.map} +1 -1
  84. package/public/assets/{overview-DSe3b-6K.js → overview-DywDrPKE.js} +2 -2
  85. package/public/assets/{overview-DSe3b-6K.js.map → overview-DywDrPKE.js.map} +1 -1
  86. package/public/assets/{pairs-C6RT1uqI.js → pairs-UvIyBk4w.js} +2 -2
  87. package/public/assets/{pairs-C6RT1uqI.js.map → pairs-UvIyBk4w.js.map} +1 -1
  88. package/public/assets/{projects-QriMCcqU.js → projects-DXBjwPU5.js} +2 -2
  89. package/public/assets/{projects-QriMCcqU.js.map → projects-DXBjwPU5.js.map} +1 -1
  90. package/public/assets/{react-dom-c3AoN3Om.js → react-dom-Ct8qXivQ.js} +2 -2
  91. package/public/assets/{react-dom-c3AoN3Om.js.map → react-dom-Ct8qXivQ.js.map} +1 -1
  92. package/public/assets/{security-CbpLH77r.js → security-Boe7MEIz.js} +2 -2
  93. package/public/assets/{security-CbpLH77r.js.map → security-Boe7MEIz.js.map} +1 -1
  94. package/public/assets/{select-B50dx5dg.js → select-Dh5Q8rJ8.js} +2 -2
  95. package/public/assets/{select-B50dx5dg.js.map → select-Dh5Q8rJ8.js.map} +1 -1
  96. package/public/assets/settings-Ch4jMDrE.js +4 -0
  97. package/public/assets/{settings-mBwuRyo3.js.map → settings-Ch4jMDrE.js.map} +1 -1
  98. package/public/assets/store-BRvMMH5T.js +9 -0
  99. package/public/assets/{store-C_mEOgkp.js.map → store-BRvMMH5T.js.map} +1 -1
  100. package/public/assets/{tasks-XzhOn6rE.js → tasks-DrW8R52c.js} +2 -2
  101. package/public/assets/{tasks-XzhOn6rE.js.map → tasks-DrW8R52c.js.map} +1 -1
  102. package/public/assets/teams-CJqpAwmO.js +2 -0
  103. package/public/assets/{teams-Dm6ToSMX.js.map → teams-CJqpAwmO.js.map} +1 -1
  104. package/public/assets/{terminal-viewer-impl-BRu1_SEu.js → terminal-viewer-impl-1_7v8GfU.js} +3 -3
  105. package/public/assets/{terminal-viewer-impl-BRu1_SEu.js.map → terminal-viewer-impl-1_7v8GfU.js.map} +1 -1
  106. package/public/assets/{use-keyboard-viewport-NLJzBfhI.js → use-keyboard-viewport-DxmCAbQi.js} +2 -2
  107. package/public/assets/{use-keyboard-viewport-NLJzBfhI.js.map → use-keyboard-viewport-DxmCAbQi.js.map} +1 -1
  108. package/public/assets/work-queue-BnIi7evb.js +2 -0
  109. package/public/assets/{work-queue-CjnV15xQ.js.map → work-queue-BnIi7evb.js.map} +1 -1
  110. package/public/assets/{workspaces-DgeUNiwv.js → workspaces-u5j6mOlA.js} +3 -3
  111. package/public/assets/{workspaces-DgeUNiwv.js.map → workspaces-u5j6mOlA.js.map} +1 -1
  112. package/public/index.html +21 -21
  113. package/runner/src/config.ts +4 -0
  114. package/src/db/messages.ts +56 -24
  115. package/src/db/notification-subscriptions.ts +68 -12
  116. package/src/index.ts +1 -1
  117. package/src/mcp-notification-subscription-tools.ts +90 -4
  118. package/src/memory-service.ts +14 -0
  119. package/src/notification-router.ts +9 -75
  120. package/src/notification-rules.ts +3 -3
  121. package/src/notification-subscription-rules.ts +11 -0
  122. package/src/notification-types.ts +70 -0
  123. package/src/project-instructions.ts +72 -10
  124. package/src/services/send-message.ts +2 -0
  125. package/src/services/spawn-agent.ts +5 -2
  126. package/src/spawn-command.ts +2 -0
  127. package/src/sse.ts +39 -2
  128. package/public/assets/MessageBubble-ak21YMQE.js +0 -3
  129. package/public/assets/MessageBubble-ak21YMQE.js.map +0 -1
  130. package/public/assets/branch-state-badge-DjdzycOv.js +0 -2
  131. package/public/assets/card-ByAPej7n.js +0 -2
  132. package/public/assets/chat-Dh-3tqyP.js +0 -2
  133. package/public/assets/chat-Dh-3tqyP.js.map +0 -1
  134. package/public/assets/copy-button-Pha61-a_.js +0 -2
  135. package/public/assets/dist-CXzYZzCR.js +0 -2
  136. package/public/assets/dist-CgbWbsmS.js +0 -2
  137. package/public/assets/dist-DTgYq8HB.js +0 -2
  138. package/public/assets/index-Cczea_lj.css +0 -2
  139. package/public/assets/index-ChQVgQyf.js +0 -22
  140. package/public/assets/memory-CXoJ1LvP.js +0 -2
  141. package/public/assets/settings-mBwuRyo3.js +0 -4
  142. package/public/assets/store-C_mEOgkp.js +0 -9
  143. package/public/assets/teams-Dm6ToSMX.js +0 -2
  144. package/public/assets/work-queue-CjnV15xQ.js +0 -2
@@ -22,6 +22,7 @@ import { enforceQueueLimit, isChannelAgentId, matchingDeliveryAgents } from "./d
22
22
  import { MSG_SELECT, normalizeReactionEmoji, rowToMessage } from "./mappers.ts";
23
23
  import { claimMessageRow } from "./message-claims.ts";
24
24
  import { getMessage } from "./message-queries.ts";
25
+ import { childStreamSubscribed, type ChildStream } from "./notification-subscriptions.ts";
25
26
  import { runningAgentForPolicy } from "./policy-runtime.ts";
26
27
  import { getTask, releaseExpiredClaims } from "./tasks.ts";
27
28
  import { insertTaskEvent } from "./task-core.ts";
@@ -199,6 +200,14 @@ function isFinalSessionResponse(session: Record<string, unknown>): boolean {
199
200
  return session.type === "response" && session.final === true;
200
201
  }
201
202
 
203
+ function childStreamForSession(session: Record<string, unknown>): ChildStream | null {
204
+ if (session.type === "response") return "responses";
205
+ if (session.type === "tool") return "toolCalls";
206
+ if (session.type === "reasoning") return "reasoning";
207
+ if (session.type === "narration" || session.type === "narrative") return "narrative";
208
+ return null;
209
+ }
210
+
202
211
  function captureTurnId(capture: Record<string, unknown>): string | undefined {
203
212
  return stringValue(capture.turnId);
204
213
  }
@@ -215,6 +224,21 @@ function lineageResponseReportKey(message: Message, session: Record<string, unkn
215
224
  return lineageResponseReportKeyForTurn(message.from, lineageResponseTurnId(message, session));
216
225
  }
217
226
 
227
+ function lineageStreamTurnId(message: Message, session: Record<string, unknown>): string {
228
+ return stringValue(session.turnId) ?? `message-${message.id}`;
229
+ }
230
+
231
+ function lineageStreamEventId(message: Message, session: Record<string, unknown>): string {
232
+ const occurredAt = message.occurredAt ?? message.createdAt;
233
+ const stepId = stringValue(session.stepId);
234
+ return stepId ? `${stepId}:${occurredAt}` : `message:${occurredAt}`;
235
+ }
236
+
237
+ function lineageStreamReportKey(message: Message, session: Record<string, unknown>, stream: ChildStream): string {
238
+ if (stream === "responses") return lineageResponseReportKey(message, session);
239
+ return `${stream}-report:${message.from}:${lineageStreamTurnId(message, session)}:${lineageStreamEventId(message, session)}`;
240
+ }
241
+
218
242
  function findLineageReportByTurn(input: {
219
243
  from: string;
220
244
  to: string;
@@ -312,39 +336,45 @@ function findRecentExplicitLineageDuplicate(input: {
312
336
  export function promoteLineageCapturedResponse(message: Message): { message: Message; created: boolean } | null {
313
337
  if (message.kind !== "session") return null;
314
338
  const session = sessionMeta(message);
315
- if (!session || !isFinalSessionResponse(session)) return null;
339
+ if (!session) return null;
340
+ const stream = childStreamForSession(session);
341
+ if (!stream) return null;
342
+ if (stream === "responses" && !isFinalSessionResponse(session)) return null;
316
343
  const child = getAgent(message.from);
317
344
  const parent = child?.spawnedBy;
318
345
  if (!parent) return null;
346
+ if (stream !== "responses" && !childStreamSubscribed({ parentId: parent, child, stream })) return null;
319
347
 
320
- const idempotencyKey = lineageResponseReportKey(message, session);
348
+ const idempotencyKey = lineageStreamReportKey(message, session, stream);
321
349
  const existing = findMessageByIdempotencyKey(message.from, idempotencyKey);
322
350
  if (existing) return { message: existing, created: false };
323
- const turnId = captureTurnId(session);
324
- if (turnId) {
325
- const crossKeyDuplicate = findLineageReportByTurn({
351
+ if (stream === "responses") {
352
+ const turnId = captureTurnId(session);
353
+ if (turnId) {
354
+ const crossKeyDuplicate = findLineageReportByTurn({
355
+ from: message.from,
356
+ to: parent,
357
+ turnId,
358
+ since: message.createdAt - LINEAGE_REPORT_DUPLICATE_WINDOW_MS,
359
+ });
360
+ if (crossKeyDuplicate) return { message: crossKeyDuplicate, created: false };
361
+ }
362
+
363
+ const duplicate = findRecentExplicitLineageDuplicate({
326
364
  from: message.from,
327
365
  to: parent,
328
- turnId,
366
+ body: message.body,
329
367
  since: message.createdAt - LINEAGE_REPORT_DUPLICATE_WINDOW_MS,
330
368
  });
331
- if (crossKeyDuplicate) return { message: crossKeyDuplicate, created: false };
332
- }
333
-
334
- const duplicate = findRecentExplicitLineageDuplicate({
335
- from: message.from,
336
- to: parent,
337
- body: message.body,
338
- since: message.createdAt - LINEAGE_REPORT_DUPLICATE_WINDOW_MS,
339
- });
340
- if (duplicate && !responseCaptureMeta(duplicate)) {
341
- getDb().query(`
342
- UPDATE messages
343
- SET idempotency_key = coalesce(idempotency_key, ?),
344
- payload = ?
345
- WHERE id = ?
346
- `).run(idempotencyKey, JSON.stringify({ ...(duplicate.payload ?? {}), responseCapture: { ...session } }), duplicate.id);
347
- return { message: getMessage(duplicate.id) ?? duplicate, created: false };
369
+ if (duplicate && !responseCaptureMeta(duplicate)) {
370
+ getDb().query(`
371
+ UPDATE messages
372
+ SET idempotency_key = coalesce(idempotency_key, ?),
373
+ payload = ?
374
+ WHERE id = ?
375
+ `).run(idempotencyKey, JSON.stringify({ ...(duplicate.payload ?? {}), responseCapture: { ...session } }), duplicate.id);
376
+ return { message: getMessage(duplicate.id) ?? duplicate, created: false };
377
+ }
348
378
  }
349
379
 
350
380
  return sendMessageWithResult({
@@ -354,7 +384,9 @@ export function promoteLineageCapturedResponse(message: Message): { message: Mes
354
384
  body: message.body,
355
385
  replyExpected: false,
356
386
  idempotencyKey,
357
- payload: { responseCapture: { ...session } },
387
+ payload: stream === "responses"
388
+ ? { responseCapture: { ...session } }
389
+ : { childStreamCapture: { stream, ...session } },
358
390
  occurredAt: message.occurredAt ?? message.createdAt,
359
391
  });
360
392
  }
@@ -1,16 +1,34 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { isRecord } from "agent-relay-sdk";
3
- import { DEFAULT_TTL_MS, type NotificationScope, type NotificationType } from "../notification-router";
4
- import { setSubscriptionRules, type NotificationRule, type NotificationRuleContext, type RuleDecision } from "../notification-rules";
3
+ import { DEFAULT_TTL_MS, type NotificationScope, type NotificationType } from "../notification-types";
5
4
  import type { AgentCard } from "../types";
6
5
  import { parseJson } from "../utils";
7
6
  import { getDb, ValidationError } from "./connection";
8
7
 
9
- export const NOTIFICATION_SUBSCRIPTION_TYPES = ["*", ...Object.keys(DEFAULT_TTL_MS)] as Array<NotificationType | "*">;
8
+ export const CHILD_STREAMS = ["responses", "toolCalls", "reasoning", "narrative"] as const;
9
+ const CONFIGURABLE_CHILD_STREAMS = ["toolCalls", "reasoning", "narrative"] as const;
10
+ export type ChildStream = (typeof CHILD_STREAMS)[number];
11
+ export type ConfigurableChildStream = (typeof CONFIGURABLE_CHILD_STREAMS)[number];
12
+ type ChildStreamSubscriptionType = `child_stream.${ConfigurableChildStream}`;
13
+
14
+ const CHILD_STREAM_SUBSCRIPTION_TYPES = CONFIGURABLE_CHILD_STREAMS.map(
15
+ (stream) => `child_stream.${stream}` as ChildStreamSubscriptionType,
16
+ );
17
+ export const NOTIFICATION_SUBSCRIPTION_TYPES = [
18
+ "*",
19
+ ...Object.keys(DEFAULT_TTL_MS),
20
+ ...CHILD_STREAM_SUBSCRIPTION_TYPES,
21
+ ] as Array<NotificationSubscriptionType>;
10
22
  export const NOTIFICATION_SUBSCRIPTION_MODES = ["opt_in", "opt_out", "mute"] as const;
11
23
 
12
- type NotificationSubscriptionType = NotificationType | "*";
24
+ type NotificationSubscriptionType = NotificationType | "*" | ChildStreamSubscriptionType;
13
25
  export type NotificationSubscriptionMode = (typeof NOTIFICATION_SUBSCRIPTION_MODES)[number];
26
+ type SubscriptionRuleDecision = "deliver" | "suppress" | "abstain";
27
+ interface SubscriptionRuleContext {
28
+ type: NotificationType;
29
+ scope: NotificationScope;
30
+ now: number;
31
+ }
14
32
 
15
33
  interface NotificationSubscription {
16
34
  id: string;
@@ -100,14 +118,14 @@ export function listSubscriptions(agentId: string): NotificationSubscription[] {
100
118
  return rows.map(rowToSubscription);
101
119
  }
102
120
 
103
- export function subscriptionDecisionFor(agent: AgentCard, ctx: NotificationRuleContext): RuleDecision {
121
+ export function subscriptionDecisionFor(agent: AgentCard, ctx: SubscriptionRuleContext): SubscriptionRuleDecision {
104
122
  const rows = getDb().query(`
105
123
  SELECT * FROM notification_subscriptions
106
124
  WHERE agent_id = ? AND (type = ? OR type = '*')
107
125
  ORDER BY created_at ASC, id ASC
108
126
  `).all(agent.id, ctx.type) as NotificationSubscriptionRow[];
109
127
 
110
- let decision: RuleDecision = "abstain";
128
+ let decision: SubscriptionRuleDecision = "abstain";
111
129
  for (const row of rows) {
112
130
  const subscription = rowToSubscription(row);
113
131
  if (!scopeMatches(subscription.scopeMatch, ctx.scope)) continue;
@@ -116,13 +134,51 @@ export function subscriptionDecisionFor(agent: AgentCard, ctx: NotificationRuleC
116
134
  return decision;
117
135
  }
118
136
 
119
- const subscriptionRule: NotificationRule = {
120
- name: "subscription:store",
121
- evaluate: subscriptionDecisionFor,
122
- };
137
+ export function childStreamSubscriptionType(stream: ConfigurableChildStream): ChildStreamSubscriptionType {
138
+ return `child_stream.${stream}`;
139
+ }
140
+
141
+ export function childStreamFromSubscriptionType(type: NotificationSubscriptionType): ConfigurableChildStream | null {
142
+ if (!type.startsWith("child_stream.")) return null;
143
+ const stream = type.slice("child_stream.".length);
144
+ return CONFIGURABLE_CHILD_STREAMS.includes(stream as ConfigurableChildStream)
145
+ ? stream as ConfigurableChildStream
146
+ : null;
147
+ }
148
+
149
+ export function cleanChildStream(value: unknown): ChildStream {
150
+ if (typeof value !== "string" || !CHILD_STREAMS.includes(value as ChildStream)) {
151
+ throw new ValidationError(`stream must be one of: ${CHILD_STREAMS.join(", ")}`);
152
+ }
153
+ return value as ChildStream;
154
+ }
123
155
 
124
- export function installNotificationSubscriptionRule(): void {
125
- setSubscriptionRules([subscriptionRule]);
156
+ export function childStreamSubscribed(input: {
157
+ parentId: string;
158
+ child: Pick<AgentCard, "id" | "teamId">;
159
+ stream: ChildStream;
160
+ }): boolean {
161
+ if (input.stream === "responses") return true;
162
+ const parentId = cleanAgentId(input.parentId);
163
+ const type = childStreamSubscriptionType(input.stream);
164
+ const scope: NotificationScope = {
165
+ parent: parentId,
166
+ agentId: input.child.id,
167
+ ...(input.child.teamId ? { teamId: input.child.teamId } : {}),
168
+ };
169
+ const rows = getDb().query(`
170
+ SELECT * FROM notification_subscriptions
171
+ WHERE agent_id = ? AND type = ?
172
+ ORDER BY created_at ASC, id ASC
173
+ `).all(parentId, type) as NotificationSubscriptionRow[];
174
+
175
+ let subscribed = false;
176
+ for (const row of rows) {
177
+ const subscription = rowToSubscription(row);
178
+ if (!scopeMatches(subscription.scopeMatch, scope)) continue;
179
+ subscribed = subscription.mode === "opt_in";
180
+ }
181
+ return subscribed;
126
182
  }
127
183
 
128
184
  function getSubscription(id: string): NotificationSubscription | null {
package/src/index.ts CHANGED
@@ -10,7 +10,7 @@ import { getLifecycleManager } from "./lifecycle-manager";
10
10
  import { getCompactionWatch } from "./compaction-watch";
11
11
  import { getConfig, setConfig } from "./config-store";
12
12
  import { startConnectorStatusPoller } from "./connectors";
13
- import { installNotificationSubscriptionRule } from "./db/notification-subscriptions";
13
+ import { installNotificationSubscriptionRule } from "./notification-subscription-rules";
14
14
  import { startPlanLiveBindings } from "./services/plan-live-bindings";
15
15
  import { resolve, sep } from "path";
16
16
  import { gzipSync, brotliCompressSync, constants as zlibConstants } from "node:zlib";
@@ -1,23 +1,29 @@
1
1
  import {
2
2
  addSubscription,
3
+ childStreamFromSubscriptionType,
4
+ childStreamSubscriptionType,
5
+ cleanChildStream,
3
6
  cleanSubscriptionMode,
4
7
  cleanSubscriptionType,
8
+ CHILD_STREAMS,
5
9
  listSubscriptions,
6
10
  NOTIFICATION_SUBSCRIPTION_MODES,
7
11
  NOTIFICATION_SUBSCRIPTION_TYPES,
8
12
  removeSubscription,
13
+ type ConfigurableChildStream,
14
+ type ChildStream,
9
15
  type NotificationSubscriptionMode,
10
16
  } from "./db/notification-subscriptions";
11
17
  import { ValidationError } from "./db";
12
18
  import { McpAuthError } from "./mcp-errors";
13
19
  import { authContextFromMcp } from "./services/auth-context";
14
20
  import { cleanString } from "./validation";
15
- import type { NotificationScope } from "./notification-router";
21
+ import type { NotificationScope } from "./notification-types";
16
22
 
17
23
  export const NOTIFICATION_SUBSCRIPTION_TOOLS = [
18
24
  {
19
25
  name: "relay_subscribe",
20
- description: "Subscribe, opt out, or mute this caller agent for a notification type. The caller's token identity is the subscription owner.",
26
+ description: "Subscribe, opt out, or mute this caller agent for a notification type, or opt into child streams. Child response streams are always on; toolCalls/reasoning/narrative are opt-in.",
21
27
  requiredScopes: ["notifications:write"],
22
28
  inputSchema: {
23
29
  type: "object",
@@ -25,8 +31,11 @@ export const NOTIFICATION_SUBSCRIPTION_TOOLS = [
25
31
  type: { type: "string", enum: NOTIFICATION_SUBSCRIPTION_TYPES },
26
32
  mode: { type: "string", enum: NOTIFICATION_SUBSCRIPTION_MODES, description: "Default opt_in. mute and opt_out suppress matching default deliveries." },
27
33
  scope: { type: "object", description: "Optional partial notification scope match, such as { teamId } or { repoRoot }." },
34
+ streams: { type: "array", items: { type: "string", enum: CHILD_STREAMS }, description: "Child streams to opt into. responses is always on and is not persisted." },
35
+ childAgentId: { type: "string", description: "Limit child stream subscriptions to one spawned child." },
36
+ teamId: { type: "string", description: "Limit child stream subscriptions to children on a team." },
37
+ allChildren: { type: "boolean", description: "Subscribe for all children spawned by this caller. This is the default when childAgentId/teamId are omitted." },
28
38
  },
29
- required: ["type"],
30
39
  additionalProperties: false,
31
40
  },
32
41
  },
@@ -41,6 +50,10 @@ export const NOTIFICATION_SUBSCRIPTION_TOOLS = [
41
50
  type: { type: "string", enum: NOTIFICATION_SUBSCRIPTION_TYPES },
42
51
  mode: { type: "string", enum: NOTIFICATION_SUBSCRIPTION_MODES },
43
52
  scope: { type: "object" },
53
+ streams: { type: "array", items: { type: "string", enum: CHILD_STREAMS } },
54
+ childAgentId: { type: "string" },
55
+ teamId: { type: "string" },
56
+ allChildren: { type: "boolean" },
44
57
  },
45
58
  additionalProperties: false,
46
59
  },
@@ -66,6 +79,27 @@ export function relayNotificationSubscriptionTool(
66
79
  ): Record<string, unknown> {
67
80
  const agentId = callerAgentId(auth);
68
81
  if (name === "relay_subscribe") {
82
+ if (Object.prototype.hasOwnProperty.call(args, "streams")) {
83
+ const scope = cleanChildStreamScope(args, agentId);
84
+ const streams = cleanChildStreams(args.streams);
85
+ const subscriptions = streams
86
+ .filter((stream): stream is ConfigurableChildStream => stream !== "responses")
87
+ .map((stream) => addSubscription({
88
+ agentId,
89
+ type: childStreamSubscriptionType(stream),
90
+ mode: cleanModeOrDefault(args.mode),
91
+ scopeMatch: scope,
92
+ }));
93
+ return {
94
+ subscriptions,
95
+ childStreamFilter: {
96
+ scope,
97
+ active: ["responses", ...streams.filter((stream) => stream !== "responses")],
98
+ responsesAlwaysOn: true,
99
+ },
100
+ };
101
+ }
102
+ if (args.type === undefined || args.type === null) throw new ValidationError("type or streams required");
69
103
  const subscription = addSubscription({
70
104
  agentId,
71
105
  type: cleanSubscriptionType(args.type),
@@ -76,6 +110,21 @@ export function relayNotificationSubscriptionTool(
76
110
  }
77
111
  if (name === "relay_unsubscribe") {
78
112
  const id = cleanString(args.id, "id", { max: 120 });
113
+ if (Object.prototype.hasOwnProperty.call(args, "streams")) {
114
+ const scope = cleanChildStreamScope(args, agentId);
115
+ const streams = cleanChildStreams(args.streams);
116
+ let removed = 0;
117
+ for (const stream of streams) {
118
+ if (stream === "responses") continue;
119
+ removed += removeSubscription({
120
+ agentId,
121
+ type: childStreamSubscriptionType(stream),
122
+ mode: args.mode === undefined || args.mode === null ? undefined : cleanSubscriptionMode(args.mode),
123
+ scopeMatch: scope,
124
+ });
125
+ }
126
+ return { removed, responsesAlwaysOn: true };
127
+ }
79
128
  const type = args.type === undefined || args.type === null ? undefined : cleanSubscriptionType(args.type);
80
129
  if (!id && !type) throw new ValidationError("id or type required");
81
130
  const removed = removeSubscription({
@@ -88,7 +137,17 @@ export function relayNotificationSubscriptionTool(
88
137
  return { removed };
89
138
  }
90
139
  if (name === "relay_list_subscriptions") {
91
- return { subscriptions: listSubscriptions(agentId) };
140
+ const subscriptions = listSubscriptions(agentId);
141
+ return {
142
+ subscriptions,
143
+ childStreamFilter: {
144
+ default: ["responses"],
145
+ responsesAlwaysOn: true,
146
+ subscriptions: subscriptions
147
+ .map((subscription) => ({ ...subscription, stream: childStreamFromSubscriptionType(subscription.type) }))
148
+ .filter((subscription) => subscription.stream !== null),
149
+ },
150
+ };
92
151
  }
93
152
  throw new ValidationError(`unknown tool: ${name}`);
94
153
  }
@@ -109,3 +168,30 @@ function cleanScope(value: unknown): Partial<NotificationScope> | null {
109
168
  if (typeof value !== "object" || Array.isArray(value)) throw new ValidationError("scope must be an object");
110
169
  return value as Partial<NotificationScope>;
111
170
  }
171
+
172
+ function cleanChildStreams(value: unknown): ChildStream[] {
173
+ if (!Array.isArray(value) || value.length === 0) throw new ValidationError("streams must be a non-empty array");
174
+ return [...new Set(value.map(cleanChildStream))];
175
+ }
176
+
177
+ function cleanChildStreamScope(args: Record<string, unknown>, parentId: string): Partial<NotificationScope> {
178
+ const childAgentId = cleanString(args.childAgentId, "childAgentId", { max: 160 });
179
+ const teamId = cleanString(args.teamId, "teamId", { max: 160 });
180
+ if (childAgentId && teamId) throw new ValidationError("childAgentId and teamId are mutually exclusive");
181
+ const allChildren = args.allChildren === undefined || args.allChildren === null ? false : args.allChildren === true;
182
+ if (args.allChildren !== undefined && args.allChildren !== null && typeof args.allChildren !== "boolean") {
183
+ throw new ValidationError("allChildren must be a boolean");
184
+ }
185
+ if (allChildren && (childAgentId || teamId)) throw new ValidationError("allChildren cannot be combined with childAgentId or teamId");
186
+ const explicitScope = cleanScope(args.scope);
187
+ if (explicitScope) {
188
+ if (explicitScope.parent && explicitScope.parent !== parentId) throw new ValidationError("child stream scope.parent must match the caller");
189
+ if (childAgentId || teamId || allChildren) throw new ValidationError("scope cannot be combined with childAgentId, teamId, or allChildren");
190
+ return { ...explicitScope, parent: parentId };
191
+ }
192
+ return {
193
+ parent: parentId,
194
+ ...(childAgentId ? { agentId: childAgentId } : {}),
195
+ ...(teamId ? { teamId } : {}),
196
+ };
197
+ }
@@ -5,6 +5,7 @@ import { assembleContextPackage, contextBudgetForAgent, estimateMemoryTokens } f
5
5
  import { createMemoryBrokerRegistry, memoryBrokerConfigFromEnv } from "./memory-broker-registry";
6
6
  import { filterMemoriesForInjection, formatUntrustedMemoryBlock } from "./memory-broker-contract";
7
7
  import { assertMemoryCreateAllowed } from "./memory-security";
8
+ import { emitRelayInjectionEvent } from "./sse";
8
9
  import { basename } from "node:path";
9
10
  import type {
10
11
  ActiveMemoryClearReason,
@@ -88,6 +89,19 @@ export async function injectMemoryContext(input: MemoryInjectionInput): Promise<
88
89
  package: pkg,
89
90
  },
90
91
  });
92
+ emitRelayInjectionEvent({
93
+ agentId: input.agent.id,
94
+ category: "memory",
95
+ summary: `injected ${loadedMemoryIds.length} memor${loadedMemoryIds.length === 1 ? "y" : "ies"}: ${input.reason}`,
96
+ detail: {
97
+ source: input.source ?? "system",
98
+ reason: input.reason,
99
+ memoryIds: loadedMemoryIds,
100
+ content: formatUntrustedMemoryBlock(pkg, { generatedAt: input.ctx.now }),
101
+ package: pkg,
102
+ },
103
+ timestamp: input.ctx.now,
104
+ });
91
105
  return { command, package: pkg };
92
106
  }
93
107
 
@@ -1,5 +1,14 @@
1
1
  import { notifySystemMessage } from "./notify";
2
2
  import type { Message, MessageKind } from "./types";
3
+ import { DEFAULT_TTL_MS } from "./notification-types";
4
+ import type { NotificationScope, NotificationType } from "./notification-types";
5
+
6
+ export {
7
+ DEFAULT_TTL_MS,
8
+ INFORMATIONAL_TTL_MS,
9
+ type NotificationScope,
10
+ type NotificationType,
11
+ } from "./notification-types";
3
12
 
4
13
  /**
5
14
  * #440 — the single seam every agent-facing "wake" notification flows through. Before this,
@@ -16,48 +25,6 @@ import type { Message, MessageKind } from "./types";
16
25
  * `scope` (the entities a notification is ABOUT) is captured now even though #441 doesn't route on
17
26
  * it, so emitters are converted exactly once and the rule engine has its inputs the day it lands.
18
27
  */
19
- export type NotificationType =
20
- | "landed-failure"
21
- | "branch.landed"
22
- | "agent.ready"
23
- | "agent.exited"
24
- | "agent.spawn_failed"
25
- | "agent.context_threshold"
26
- | "agent.resume_budget_exhausted"
27
- | "agent.transient_land_hold"
28
- | "agent.rate_limit_resume"
29
- | "plan.bound_agent_exited"
30
- | "workspace.steward_task"
31
- | "workspace.conflict"
32
- | "workspace.steward_reassigned"
33
- | "workspace.stranded_escalation"
34
- | "workspace.auto_abandoned"
35
- | "workspace.review_request"
36
- | "workspace.review_changes_requested";
37
-
38
- /**
39
- * The entities a notification is ABOUT — the future routing inputs (#442) and the explainability
40
- * subject (#446). All optional: each emitter fills the fields it has in scope at the call site.
41
- */
42
- export interface NotificationScope {
43
- /** The agent whose work the notification concerns (branch author, workspace owner). */
44
- author?: string;
45
- /** The agent the notification is primarily about (lifecycle subject, advisory target). */
46
- agentId?: string;
47
- /** The spawn-parent that should hear about a child's lifecycle. */
48
- parent?: string;
49
- /** Team the notification belongs to (plan/coordination scope). */
50
- teamId?: string;
51
- /** Plan the notification concerns. */
52
- planId?: string;
53
- /** Repo checkout the notification concerns. */
54
- repoRoot?: string;
55
- /** Workspace the notification concerns. */
56
- workspaceId?: string;
57
- /** Files the notification concerns (path-scoped routing, #442). */
58
- paths?: string[];
59
- }
60
-
61
28
  interface NotificationMessage {
62
29
  subject?: string;
63
30
  body: string;
@@ -89,39 +56,6 @@ interface RouteNotificationInput {
89
56
  emittedAt?: number;
90
57
  }
91
58
 
92
- const MINUTES = 60 * 1000;
93
-
94
- /**
95
- * Per-type default TTL (#451). Default-SAFE: anything about the recipient's own work or any
96
- * obligation is `null` (never auto-expires — store-ahead #234 must still deliver it). Only the
97
- * genuinely now-or-never pings get a short default; a stale one of those is actively misleading:
98
- * - `agent.context_threshold` — the advisory band may have changed; old advice is wrong.
99
- * - `agent.rate_limit_resume` — "your hold cleared, resume" is pointless once the window moved.
100
- * Emitters opt INTO a shorter TTL per-call for the bystander/informational cases.
101
- */
102
- export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
103
- "landed-failure": null,
104
- "branch.landed": null,
105
- "agent.ready": null,
106
- "agent.exited": null,
107
- "agent.spawn_failed": null,
108
- "agent.context_threshold": 15 * MINUTES,
109
- "agent.resume_budget_exhausted": null,
110
- "agent.transient_land_hold": null,
111
- "agent.rate_limit_resume": 15 * MINUTES,
112
- "plan.bound_agent_exited": null,
113
- "workspace.steward_task": null,
114
- "workspace.conflict": null,
115
- "workspace.steward_reassigned": null,
116
- "workspace.stranded_escalation": null,
117
- "workspace.auto_abandoned": null,
118
- "workspace.review_request": null,
119
- "workspace.review_changes_requested": null,
120
- };
121
-
122
- /** Convenience TTL for emitter-supplied overrides on bystander/informational notifications. */
123
- export const INFORMATIONAL_TTL_MS = 30 * MINUTES;
124
-
125
59
  /**
126
60
  * Resolve the effective TTL: an explicit per-call `ttlMs` wins (including an explicit `null` =
127
61
  * never); `undefined` falls back to the per-type default.
@@ -1,7 +1,7 @@
1
1
  import type { AgentCard } from "./types";
2
2
  import { listAgents } from "./db";
3
3
  import { isAgentOnline } from "./agent-ref";
4
- import type { NotificationType, NotificationScope } from "./notification-router";
4
+ import type { NotificationType, NotificationScope } from "./notification-types";
5
5
 
6
6
  /**
7
7
  * #442 — the declarative rule engine that computes a notification's recipients FROM its `scope`,
@@ -18,9 +18,9 @@ import type { NotificationType, NotificationScope } from "./notification-router"
18
18
  * #441 explicit-recipients path. Nothing here is wired into production delivery yet — #444 does that.
19
19
  */
20
20
 
21
- export type RuleDecision = "deliver" | "suppress" | "abstain";
21
+ type RuleDecision = "deliver" | "suppress" | "abstain";
22
22
 
23
- export interface NotificationRuleContext {
23
+ interface NotificationRuleContext {
24
24
  type: NotificationType;
25
25
  scope: NotificationScope;
26
26
  /** Evaluation clock — passed in (never read via Date.now() inside a rule) so eval is testable. */
@@ -0,0 +1,11 @@
1
+ import { subscriptionDecisionFor } from "./db/notification-subscriptions";
2
+ import { setSubscriptionRules, type NotificationRule } from "./notification-rules";
3
+
4
+ const subscriptionRule: NotificationRule = {
5
+ name: "subscription:store",
6
+ evaluate: subscriptionDecisionFor,
7
+ };
8
+
9
+ export function installNotificationSubscriptionRule(): void {
10
+ setSubscriptionRules([subscriptionRule]);
11
+ }
@@ -0,0 +1,70 @@
1
+ export type NotificationType =
2
+ | "landed-failure"
3
+ | "branch.landed"
4
+ | "agent.ready"
5
+ | "agent.exited"
6
+ | "agent.spawn_failed"
7
+ | "agent.context_threshold"
8
+ | "agent.resume_budget_exhausted"
9
+ | "agent.transient_land_hold"
10
+ | "agent.rate_limit_resume"
11
+ | "plan.bound_agent_exited"
12
+ | "workspace.steward_task"
13
+ | "workspace.conflict"
14
+ | "workspace.steward_reassigned"
15
+ | "workspace.stranded_escalation"
16
+ | "workspace.auto_abandoned"
17
+ | "workspace.review_request"
18
+ | "workspace.review_changes_requested";
19
+
20
+ /**
21
+ * The entities a notification is ABOUT: routing inputs (#442) and explainability subjects (#446).
22
+ * All optional because each emitter fills the fields it has in scope at the call site.
23
+ */
24
+ export interface NotificationScope {
25
+ /** The agent whose work the notification concerns (branch author, workspace owner). */
26
+ author?: string;
27
+ /** The agent the notification is primarily about (lifecycle subject, advisory target). */
28
+ agentId?: string;
29
+ /** The spawn-parent that should hear about a child's lifecycle. */
30
+ parent?: string;
31
+ /** Team the notification belongs to (plan/coordination scope). */
32
+ teamId?: string;
33
+ /** Plan the notification concerns. */
34
+ planId?: string;
35
+ /** Repo checkout the notification concerns. */
36
+ repoRoot?: string;
37
+ /** Workspace the notification concerns. */
38
+ workspaceId?: string;
39
+ /** Files the notification concerns (path-scoped routing, #442). */
40
+ paths?: string[];
41
+ }
42
+
43
+ const MINUTES = 60 * 1000;
44
+
45
+ /**
46
+ * Per-type default TTL (#451). Default-SAFE: anything about the recipient's own work or any
47
+ * obligation is `null` (never auto-expires). Only genuinely now-or-never pings get a short default.
48
+ */
49
+ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
50
+ "landed-failure": null,
51
+ "branch.landed": null,
52
+ "agent.ready": null,
53
+ "agent.exited": null,
54
+ "agent.spawn_failed": null,
55
+ "agent.context_threshold": 15 * MINUTES,
56
+ "agent.resume_budget_exhausted": null,
57
+ "agent.transient_land_hold": null,
58
+ "agent.rate_limit_resume": 15 * MINUTES,
59
+ "plan.bound_agent_exited": null,
60
+ "workspace.steward_task": null,
61
+ "workspace.conflict": null,
62
+ "workspace.steward_reassigned": null,
63
+ "workspace.stranded_escalation": null,
64
+ "workspace.auto_abandoned": null,
65
+ "workspace.review_request": null,
66
+ "workspace.review_changes_requested": null,
67
+ };
68
+
69
+ /** Convenience TTL for emitter-supplied overrides on bystander/informational notifications. */
70
+ export const INFORMATIONAL_TTL_MS = 30 * MINUTES;