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.
- package/docs/openapi.json +1 -1
- package/package.json +2 -2
- package/public/assets/MessageBubble-BWdM9UCM.js +3 -0
- package/public/assets/MessageBubble-BWdM9UCM.js.map +1 -0
- package/public/assets/{PlanGraphPanel-CubzRNGK.js → PlanGraphPanel-icESxd82.js} +2 -2
- package/public/assets/{PlanGraphPanel-CubzRNGK.js.map → PlanGraphPanel-icESxd82.js.map} +1 -1
- package/public/assets/{activity-83lDNDEp.js → activity-BR9I23hc.js} +2 -2
- package/public/assets/{activity-83lDNDEp.js.map → activity-BR9I23hc.js.map} +1 -1
- package/public/assets/{agent-profiles-BzjoH-NX.js → agent-profiles-BB0V-Tg1.js} +2 -2
- package/public/assets/{agent-profiles-BzjoH-NX.js.map → agent-profiles-BB0V-Tg1.js.map} +1 -1
- package/public/assets/{agents-BQhoABy8.js → agents-Clkboz93.js} +2 -2
- package/public/assets/{agents-BQhoABy8.js.map → agents-Clkboz93.js.map} +1 -1
- package/public/assets/{analytics-BrsfrNFw.js → analytics-DRlHs5ho.js} +3 -3
- package/public/assets/{analytics-BrsfrNFw.js.map → analytics-DRlHs5ho.js.map} +1 -1
- package/public/assets/{automation-VYuix2Iu.js → automation-DlnH_MPy.js} +2 -2
- package/public/assets/{automation-VYuix2Iu.js.map → automation-DlnH_MPy.js.map} +1 -1
- package/public/assets/{badge-lljplQxm.js → badge-YQhT30dy.js} +2 -2
- package/public/assets/{badge-lljplQxm.js.map → badge-YQhT30dy.js.map} +1 -1
- package/public/assets/branch-state-badge-Dg1qnmF5.js +2 -0
- package/public/assets/{branch-state-badge-DjdzycOv.js.map → branch-state-badge-Dg1qnmF5.js.map} +1 -1
- package/public/assets/{button-BwkKot02.js → button-CaFI76Ja.js} +2 -2
- package/public/assets/{button-BwkKot02.js.map → button-CaFI76Ja.js.map} +1 -1
- package/public/assets/card-B_fQtKce.js +2 -0
- package/public/assets/{card-ByAPej7n.js.map → card-B_fQtKce.js.map} +1 -1
- package/public/assets/{channels-n4vJBF5t.js → channels-BVYLrKUL.js} +2 -2
- package/public/assets/{channels-n4vJBF5t.js.map → channels-BVYLrKUL.js.map} +1 -1
- package/public/assets/chat-ml_YzbK4.js +2 -0
- package/public/assets/chat-ml_YzbK4.js.map +1 -0
- package/public/assets/{connectors-B8cKMHKY.js → connectors-BOyk5hC3.js} +2 -2
- package/public/assets/{connectors-B8cKMHKY.js.map → connectors-BOyk5hC3.js.map} +1 -1
- package/public/assets/copy-button-m00JmflM.js +2 -0
- package/public/assets/{copy-button-Pha61-a_.js.map → copy-button-m00JmflM.js.map} +1 -1
- package/public/assets/{dist-B2WRWI7O.js → dist-B_yrHbsb.js} +2 -2
- package/public/assets/{dist-B2WRWI7O.js.map → dist-B_yrHbsb.js.map} +1 -1
- package/public/assets/{dist-BXI214fV.js → dist-C-viWls-.js} +2 -2
- package/public/assets/{dist-BXI214fV.js.map → dist-C-viWls-.js.map} +1 -1
- package/public/assets/dist-CYS2puT9.js +2 -0
- package/public/assets/{dist-CXzYZzCR.js.map → dist-CYS2puT9.js.map} +1 -1
- package/public/assets/{dist-BowDWFij.js → dist-ChP4KvKi.js} +2 -2
- package/public/assets/{dist-BowDWFij.js.map → dist-ChP4KvKi.js.map} +1 -1
- package/public/assets/{dist-0mCQ5WMT.js → dist-Cnlh_NpI.js} +2 -2
- package/public/assets/{dist-0mCQ5WMT.js.map → dist-Cnlh_NpI.js.map} +1 -1
- package/public/assets/dist-CydxJz3m.js +2 -0
- package/public/assets/{dist-DTgYq8HB.js.map → dist-CydxJz3m.js.map} +1 -1
- package/public/assets/{dist-CXLRXIMd.js → dist-D7K3qgp7.js} +2 -2
- package/public/assets/{dist-CXLRXIMd.js.map → dist-D7K3qgp7.js.map} +1 -1
- package/public/assets/{dist-CcBB6z4x.js → dist-D8Yr1AIJ.js} +2 -2
- package/public/assets/{dist-CcBB6z4x.js.map → dist-D8Yr1AIJ.js.map} +1 -1
- package/public/assets/{dist-CT_Zr-w5.js → dist-DeAdFM5_.js} +2 -2
- package/public/assets/{dist-CT_Zr-w5.js.map → dist-DeAdFM5_.js.map} +1 -1
- package/public/assets/{dist-BBMeJabC.js → dist-DeM72J6a.js} +2 -2
- package/public/assets/{dist-BBMeJabC.js.map → dist-DeM72J6a.js.map} +1 -1
- package/public/assets/dist-DfQP9-VC.js +2 -0
- package/public/assets/{dist-CgbWbsmS.js.map → dist-DfQP9-VC.js.map} +1 -1
- package/public/assets/{es2015-CkzQKfKp.js → es2015-C3DnXBqu.js} +2 -2
- package/public/assets/{es2015-CkzQKfKp.js.map → es2015-C3DnXBqu.js.map} +1 -1
- package/public/assets/{formatted-body-impl-Drko1nzi.js → formatted-body-impl-D2F76gPY.js} +2 -2
- package/public/assets/{formatted-body-impl-Drko1nzi.js.map → formatted-body-impl-D2F76gPY.js.map} +1 -1
- package/public/assets/index-CQQTCZxg.css +2 -0
- package/public/assets/index-DYmgI4s4.js +22 -0
- package/public/assets/{index-ChQVgQyf.js.map → index-DYmgI4s4.js.map} +1 -1
- package/public/assets/{input-sp_EWS3J.js → input-Cqqq0OFl.js} +2 -2
- package/public/assets/{input-sp_EWS3J.js.map → input-Cqqq0OFl.js.map} +1 -1
- package/public/assets/{insights-BRclOzwJ.js → insights-DylYp2q4.js} +2 -2
- package/public/assets/{insights-BRclOzwJ.js.map → insights-DylYp2q4.js.map} +1 -1
- package/public/assets/{integrations-CfaOLw9i.js → integrations-B9ViWoJy.js} +2 -2
- package/public/assets/{integrations-CfaOLw9i.js.map → integrations-B9ViWoJy.js.map} +1 -1
- package/public/assets/{lib-DNMJa4Hh.js → lib-6L4m0E3M.js} +3 -3
- package/public/assets/{lib-DNMJa4Hh.js.map → lib-6L4m0E3M.js.map} +1 -1
- package/public/assets/{lucide-react--HSX8ykl.js → lucide-react-DI8kTavp.js} +2 -2
- package/public/assets/{lucide-react--HSX8ykl.js.map → lucide-react-DI8kTavp.js.map} +1 -1
- package/public/assets/{maintenance-BWbsnscm.js → maintenance-inyZGLys.js} +2 -2
- package/public/assets/{maintenance-BWbsnscm.js.map → maintenance-inyZGLys.js.map} +1 -1
- package/public/assets/{managed-agents-D6yncoFy.js → managed-agents-CEFvMPN-.js} +2 -2
- package/public/assets/{managed-agents-D6yncoFy.js.map → managed-agents-CEFvMPN-.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-o59KaM_T.js → markdown-preview-impl-CniyG4_g.js} +2 -2
- package/public/assets/{markdown-preview-impl-o59KaM_T.js.map → markdown-preview-impl-CniyG4_g.js.map} +1 -1
- package/public/assets/memory-DDYGuRzr.js +2 -0
- package/public/assets/{memory-CXoJ1LvP.js.map → memory-DDYGuRzr.js.map} +1 -1
- package/public/assets/{messages-EqiGQvcb.js → messages-BMFcsMMa.js} +2 -2
- package/public/assets/{messages-EqiGQvcb.js.map → messages-BMFcsMMa.js.map} +1 -1
- package/public/assets/{orchestrators-Ds_vwaEy.js → orchestrators-D5nL4a52.js} +2 -2
- package/public/assets/{orchestrators-Ds_vwaEy.js.map → orchestrators-D5nL4a52.js.map} +1 -1
- package/public/assets/{overview-DSe3b-6K.js → overview-DywDrPKE.js} +2 -2
- package/public/assets/{overview-DSe3b-6K.js.map → overview-DywDrPKE.js.map} +1 -1
- package/public/assets/{pairs-C6RT1uqI.js → pairs-UvIyBk4w.js} +2 -2
- package/public/assets/{pairs-C6RT1uqI.js.map → pairs-UvIyBk4w.js.map} +1 -1
- package/public/assets/{projects-QriMCcqU.js → projects-DXBjwPU5.js} +2 -2
- package/public/assets/{projects-QriMCcqU.js.map → projects-DXBjwPU5.js.map} +1 -1
- package/public/assets/{react-dom-c3AoN3Om.js → react-dom-Ct8qXivQ.js} +2 -2
- package/public/assets/{react-dom-c3AoN3Om.js.map → react-dom-Ct8qXivQ.js.map} +1 -1
- package/public/assets/{security-CbpLH77r.js → security-Boe7MEIz.js} +2 -2
- package/public/assets/{security-CbpLH77r.js.map → security-Boe7MEIz.js.map} +1 -1
- package/public/assets/{select-B50dx5dg.js → select-Dh5Q8rJ8.js} +2 -2
- package/public/assets/{select-B50dx5dg.js.map → select-Dh5Q8rJ8.js.map} +1 -1
- package/public/assets/settings-Ch4jMDrE.js +4 -0
- package/public/assets/{settings-mBwuRyo3.js.map → settings-Ch4jMDrE.js.map} +1 -1
- package/public/assets/store-BRvMMH5T.js +9 -0
- package/public/assets/{store-C_mEOgkp.js.map → store-BRvMMH5T.js.map} +1 -1
- package/public/assets/{tasks-XzhOn6rE.js → tasks-DrW8R52c.js} +2 -2
- package/public/assets/{tasks-XzhOn6rE.js.map → tasks-DrW8R52c.js.map} +1 -1
- package/public/assets/teams-CJqpAwmO.js +2 -0
- package/public/assets/{teams-Dm6ToSMX.js.map → teams-CJqpAwmO.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-BRu1_SEu.js → terminal-viewer-impl-1_7v8GfU.js} +3 -3
- package/public/assets/{terminal-viewer-impl-BRu1_SEu.js.map → terminal-viewer-impl-1_7v8GfU.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-NLJzBfhI.js → use-keyboard-viewport-DxmCAbQi.js} +2 -2
- package/public/assets/{use-keyboard-viewport-NLJzBfhI.js.map → use-keyboard-viewport-DxmCAbQi.js.map} +1 -1
- package/public/assets/work-queue-BnIi7evb.js +2 -0
- package/public/assets/{work-queue-CjnV15xQ.js.map → work-queue-BnIi7evb.js.map} +1 -1
- package/public/assets/{workspaces-DgeUNiwv.js → workspaces-u5j6mOlA.js} +3 -3
- package/public/assets/{workspaces-DgeUNiwv.js.map → workspaces-u5j6mOlA.js.map} +1 -1
- package/public/index.html +21 -21
- package/runner/src/config.ts +4 -0
- package/src/db/messages.ts +56 -24
- package/src/db/notification-subscriptions.ts +68 -12
- package/src/index.ts +1 -1
- package/src/mcp-notification-subscription-tools.ts +90 -4
- package/src/memory-service.ts +14 -0
- package/src/notification-router.ts +9 -75
- package/src/notification-rules.ts +3 -3
- package/src/notification-subscription-rules.ts +11 -0
- package/src/notification-types.ts +70 -0
- package/src/project-instructions.ts +72 -10
- package/src/services/send-message.ts +2 -0
- package/src/services/spawn-agent.ts +5 -2
- package/src/spawn-command.ts +2 -0
- package/src/sse.ts +39 -2
- package/public/assets/MessageBubble-ak21YMQE.js +0 -3
- package/public/assets/MessageBubble-ak21YMQE.js.map +0 -1
- package/public/assets/branch-state-badge-DjdzycOv.js +0 -2
- package/public/assets/card-ByAPej7n.js +0 -2
- package/public/assets/chat-Dh-3tqyP.js +0 -2
- package/public/assets/chat-Dh-3tqyP.js.map +0 -1
- package/public/assets/copy-button-Pha61-a_.js +0 -2
- package/public/assets/dist-CXzYZzCR.js +0 -2
- package/public/assets/dist-CgbWbsmS.js +0 -2
- package/public/assets/dist-DTgYq8HB.js +0 -2
- package/public/assets/index-Cczea_lj.css +0 -2
- package/public/assets/index-ChQVgQyf.js +0 -22
- package/public/assets/memory-CXoJ1LvP.js +0 -2
- package/public/assets/settings-mBwuRyo3.js +0 -4
- package/public/assets/store-C_mEOgkp.js +0 -9
- package/public/assets/teams-Dm6ToSMX.js +0 -2
- package/public/assets/work-queue-CjnV15xQ.js +0 -2
package/src/db/messages.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
366
|
+
body: message.body,
|
|
329
367
|
since: message.createdAt - LINEAGE_REPORT_DUPLICATE_WINDOW_MS,
|
|
330
368
|
});
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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:
|
|
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-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
125
|
-
|
|
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 "./
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|
package/src/memory-service.ts
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
21
|
+
type RuleDecision = "deliver" | "suppress" | "abstain";
|
|
22
22
|
|
|
23
|
-
|
|
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;
|