agent-relay-server 0.92.0 → 0.93.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 +29 -1
- package/package.json +2 -2
- package/public/assets/MessageBubble-DX16Hbb5.js +3 -0
- package/public/assets/MessageBubble-DX16Hbb5.js.map +1 -0
- package/public/assets/{PlanGraphPanel-CpIBMvnu.js → PlanGraphPanel-BuWerKhs.js} +2 -2
- package/public/assets/{PlanGraphPanel-CpIBMvnu.js.map → PlanGraphPanel-BuWerKhs.js.map} +1 -1
- package/public/assets/activity-tuTEn3vn.js +2 -0
- package/public/assets/{activity-B6WPWNsX.js.map → activity-tuTEn3vn.js.map} +1 -1
- package/public/assets/{agent-profiles-CLJNeVxc.js → agent-profiles-CAwmjdDx.js} +2 -2
- package/public/assets/{agent-profiles-CLJNeVxc.js.map → agent-profiles-CAwmjdDx.js.map} +1 -1
- package/public/assets/agents-B-BSBrM5.js +2 -0
- package/public/assets/{agents-BebF-yQr.js.map → agents-B-BSBrM5.js.map} +1 -1
- package/public/assets/{analytics-C1F28oqr.js → analytics-aI_oGHtR.js} +3 -3
- package/public/assets/{analytics-C1F28oqr.js.map → analytics-aI_oGHtR.js.map} +1 -1
- package/public/assets/{automation-CyPXC1A8.js → automation-BAQM1dBh.js} +2 -2
- package/public/assets/{automation-CyPXC1A8.js.map → automation-BAQM1dBh.js.map} +1 -1
- package/public/assets/{badge-CK6ejiOh.js → badge-fjSgjnDc.js} +2 -2
- package/public/assets/{badge-CK6ejiOh.js.map → badge-fjSgjnDc.js.map} +1 -1
- package/public/assets/branch-state-badge-DaafW7sN.js +2 -0
- package/public/assets/{branch-state-badge-Q4W68jxj.js.map → branch-state-badge-DaafW7sN.js.map} +1 -1
- package/public/assets/{button-BAwKuRIq.js → button-DidMOp8n.js} +2 -2
- package/public/assets/{button-BAwKuRIq.js.map → button-DidMOp8n.js.map} +1 -1
- package/public/assets/card-CpLnx1JJ.js +2 -0
- package/public/assets/{card-DDxxo-Fo.js.map → card-CpLnx1JJ.js.map} +1 -1
- package/public/assets/{channels-BQbNtrtF.js → channels-DHjTCcE8.js} +2 -2
- package/public/assets/{channels-BQbNtrtF.js.map → channels-DHjTCcE8.js.map} +1 -1
- package/public/assets/chat-uWKl3MpS.js +2 -0
- package/public/assets/{chat-DfqC3kFF.js.map → chat-uWKl3MpS.js.map} +1 -1
- package/public/assets/{connectors-CmARSKoV.js → connectors-CI97PYOl.js} +2 -2
- package/public/assets/{connectors-CmARSKoV.js.map → connectors-CI97PYOl.js.map} +1 -1
- package/public/assets/copy-button-CKAwTG9g.js +2 -0
- package/public/assets/{copy-button-D3B_s7J7.js.map → copy-button-CKAwTG9g.js.map} +1 -1
- package/public/assets/{dist-Dmiv1tSO.js → dist-BEpGArlE.js} +2 -2
- package/public/assets/{dist-Dmiv1tSO.js.map → dist-BEpGArlE.js.map} +1 -1
- package/public/assets/dist-BZ-CpDXy.js +2 -0
- package/public/assets/{dist-CIbvPDQV.js.map → dist-BZ-CpDXy.js.map} +1 -1
- package/public/assets/{dist-DLsSqk6h.js → dist-BaDWyAAG.js} +2 -2
- package/public/assets/{dist-DLsSqk6h.js.map → dist-BaDWyAAG.js.map} +1 -1
- package/public/assets/{dist-CAFUEpK2.js → dist-BivRSJg7.js} +2 -2
- package/public/assets/{dist-CAFUEpK2.js.map → dist-BivRSJg7.js.map} +1 -1
- package/public/assets/{dist-CL2npVDe.js → dist-C9ntDbUp.js} +2 -2
- package/public/assets/{dist-CL2npVDe.js.map → dist-C9ntDbUp.js.map} +1 -1
- package/public/assets/dist-CEb4q1gZ.js +2 -0
- package/public/assets/{dist-DyqEeyBI.js.map → dist-CEb4q1gZ.js.map} +1 -1
- package/public/assets/dist-DBDYIgS5.js +2 -0
- package/public/assets/{dist-CoRABSO3.js.map → dist-DBDYIgS5.js.map} +1 -1
- package/public/assets/{dist-ERYvDMdt.js → dist-DNxK5Ibj.js} +2 -2
- package/public/assets/{dist-ERYvDMdt.js.map → dist-DNxK5Ibj.js.map} +1 -1
- package/public/assets/{dist-YjP8Gd1C.js → dist-DayffsX1.js} +2 -2
- package/public/assets/{dist-YjP8Gd1C.js.map → dist-DayffsX1.js.map} +1 -1
- package/public/assets/{dist-YQVQrtnc.js → dist-L3vQoV3T.js} +2 -2
- package/public/assets/{dist-YQVQrtnc.js.map → dist-L3vQoV3T.js.map} +1 -1
- package/public/assets/{dist-CnRv0y2x.js → dist-uFOP-AfS.js} +2 -2
- package/public/assets/{dist-CnRv0y2x.js.map → dist-uFOP-AfS.js.map} +1 -1
- package/public/assets/{es2015-Yto-4EBw.js → es2015-CJr6bLcU.js} +2 -2
- package/public/assets/{es2015-Yto-4EBw.js.map → es2015-CJr6bLcU.js.map} +1 -1
- package/public/assets/{formatted-body-impl-CrE-hFvy.js → formatted-body-impl-DuqZunxL.js} +2 -2
- package/public/assets/{formatted-body-impl-CrE-hFvy.js.map → formatted-body-impl-DuqZunxL.js.map} +1 -1
- package/public/assets/index-DLjkMB-b.css +2 -0
- package/public/assets/index-dMB8klzy.js +23 -0
- package/public/assets/index-dMB8klzy.js.map +1 -0
- package/public/assets/{input-CLW2gDUM.js → input-BbH2Jq-f.js} +2 -2
- package/public/assets/{input-CLW2gDUM.js.map → input-BbH2Jq-f.js.map} +1 -1
- package/public/assets/{insights-BribQHXe.js → insights-C0lZ7n8D.js} +2 -2
- package/public/assets/{insights-BribQHXe.js.map → insights-C0lZ7n8D.js.map} +1 -1
- package/public/assets/integrations-DXh00Ss-.js +2 -0
- package/public/assets/{integrations-D9Cx-7ma.js.map → integrations-DXh00Ss-.js.map} +1 -1
- package/public/assets/{lib-DdCM7GfN.js → lib-B0r9alsu.js} +2 -2
- package/public/assets/{lib-DdCM7GfN.js.map → lib-B0r9alsu.js.map} +1 -1
- package/public/assets/{lucide-react-CsPXvXMC.js → lucide-react-BCarrGVa.js} +2 -2
- package/public/assets/{lucide-react-CsPXvXMC.js.map → lucide-react-BCarrGVa.js.map} +1 -1
- package/public/assets/{maintenance-Cjol3fS_.js → maintenance-Dh9nzjYV.js} +2 -2
- package/public/assets/{maintenance-Cjol3fS_.js.map → maintenance-Dh9nzjYV.js.map} +1 -1
- package/public/assets/{managed-agents-1lZwQNHl.js → managed-agents-Bsveljn4.js} +4 -4
- package/public/assets/{managed-agents-1lZwQNHl.js.map → managed-agents-Bsveljn4.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-C2WeUNFR.js → markdown-preview-impl-CYpMFis9.js} +2 -2
- package/public/assets/{markdown-preview-impl-C2WeUNFR.js.map → markdown-preview-impl-CYpMFis9.js.map} +1 -1
- package/public/assets/{memory-B7mcELEW.js → memory-DMFQMXrp.js} +2 -2
- package/public/assets/{memory-B7mcELEW.js.map → memory-DMFQMXrp.js.map} +1 -1
- package/public/assets/messages-Cnzc7j50.js +2 -0
- package/public/assets/{messages-B8jF9p2l.js.map → messages-Cnzc7j50.js.map} +1 -1
- package/public/assets/{orchestrators-CQjvBXDx.js → orchestrators-DSoDPNVS.js} +2 -2
- package/public/assets/{orchestrators-CQjvBXDx.js.map → orchestrators-DSoDPNVS.js.map} +1 -1
- package/public/assets/{overview-CgDUJOG-.js → overview-OsRxo5XQ.js} +2 -2
- package/public/assets/{overview-CgDUJOG-.js.map → overview-OsRxo5XQ.js.map} +1 -1
- package/public/assets/{pairs-BdPKQyzE.js → pairs-D4UiUhyW.js} +2 -2
- package/public/assets/{pairs-BdPKQyzE.js.map → pairs-D4UiUhyW.js.map} +1 -1
- package/public/assets/{projects-hU5KGW_T.js → projects-CYzU-Cf3.js} +2 -2
- package/public/assets/{projects-hU5KGW_T.js.map → projects-CYzU-Cf3.js.map} +1 -1
- package/public/assets/{react-dom-CEl58oxP.js → react-dom-CYpnckci.js} +2 -2
- package/public/assets/{react-dom-CEl58oxP.js.map → react-dom-CYpnckci.js.map} +1 -1
- package/public/assets/{security-BlZQ0ECI.js → security-CC2F6xdd.js} +2 -2
- package/public/assets/{security-BlZQ0ECI.js.map → security-CC2F6xdd.js.map} +1 -1
- package/public/assets/{select-CjpI0JtN.js → select-CD38f1WQ.js} +2 -2
- package/public/assets/{select-CjpI0JtN.js.map → select-CD38f1WQ.js.map} +1 -1
- package/public/assets/settings-T3B40Amb.js +6 -0
- package/public/assets/settings-T3B40Amb.js.map +1 -0
- package/public/assets/{store-DsqUEZOf.js → store-CJeafbP1.js} +3 -3
- package/public/assets/{store-DsqUEZOf.js.map → store-CJeafbP1.js.map} +1 -1
- package/public/assets/{tasks-BcHWptn4.js → tasks-BkrhWMw7.js} +2 -2
- package/public/assets/{tasks-BcHWptn4.js.map → tasks-BkrhWMw7.js.map} +1 -1
- package/public/assets/{teams-BGL-VpqZ.js → teams-DyzaREAg.js} +2 -2
- package/public/assets/{teams-BGL-VpqZ.js.map → teams-DyzaREAg.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-D8bnx7Z7.js → terminal-viewer-impl-B5JeHof3.js} +3 -3
- package/public/assets/{terminal-viewer-impl-D8bnx7Z7.js.map → terminal-viewer-impl-B5JeHof3.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-DABeIMEJ.js → use-keyboard-viewport-BYCJkZCN.js} +2 -2
- package/public/assets/{use-keyboard-viewport-DABeIMEJ.js.map → use-keyboard-viewport-BYCJkZCN.js.map} +1 -1
- package/public/assets/user-avatar-C7i11_ju.js +2 -0
- package/public/assets/{user-avatar-DZ1g4X7l.js.map → user-avatar-C7i11_ju.js.map} +1 -1
- package/public/assets/{work-queue-CIy6fe6y.js → work-queue-DHp4p1Zk.js} +2 -2
- package/public/assets/{work-queue-CIy6fe6y.js.map → work-queue-DHp4p1Zk.js.map} +1 -1
- package/public/assets/workspaces-BzwZvbsq.js +3 -0
- package/public/assets/{workspaces-C740m1ys.js.map → workspaces-BzwZvbsq.js.map} +1 -1
- package/public/index.html +21 -21
- package/src/agent-lifecycle-events.ts +0 -10
- package/src/branch-landed.ts +17 -29
- package/src/config-store.ts +13 -26
- package/src/context-advisory.ts +20 -10
- package/src/db/migrations.ts +17 -0
- package/src/db/scheduled-timers.ts +33 -12
- package/src/db/schema.ts +4 -0
- package/src/mcp/index.ts +1 -1
- package/src/mcp/tools-messaging.ts +13 -3
- package/src/mcp-notification-subscription-tools.ts +55 -2
- package/src/notification-router.ts +163 -9
- package/src/notification-rules.ts +7 -1
- package/src/notification-settings-store.ts +47 -0
- package/src/notification-settings.ts +155 -0
- package/src/notification-types.ts +2 -0
- package/src/quota-advisory.ts +0 -3
- package/src/routes/index.ts +2 -0
- package/src/routes/notifications.ts +36 -0
- package/src/routes/settings.ts +6 -0
- package/src/security.ts +1 -0
- package/src/services/scheduler.ts +29 -1
- package/src/settings/registry.ts +30 -44
- package/src/sse.ts +1 -0
- package/public/assets/MessageBubble-CUddRP1E.js +0 -3
- package/public/assets/MessageBubble-CUddRP1E.js.map +0 -1
- package/public/assets/activity-B6WPWNsX.js +0 -2
- package/public/assets/agents-BebF-yQr.js +0 -2
- package/public/assets/branch-state-badge-Q4W68jxj.js +0 -2
- package/public/assets/card-DDxxo-Fo.js +0 -2
- package/public/assets/chat-DfqC3kFF.js +0 -2
- package/public/assets/copy-button-D3B_s7J7.js +0 -2
- package/public/assets/dist-CIbvPDQV.js +0 -2
- package/public/assets/dist-CoRABSO3.js +0 -2
- package/public/assets/dist-DyqEeyBI.js +0 -2
- package/public/assets/index-CdDX5NR1.js +0 -23
- package/public/assets/index-CdDX5NR1.js.map +0 -1
- package/public/assets/index-umexz3cs.css +0 -2
- package/public/assets/integrations-D9Cx-7ma.js +0 -2
- package/public/assets/messages-B8jF9p2l.js +0 -2
- package/public/assets/settings-CMgqpUY-.js +0 -4
- package/public/assets/settings-CMgqpUY-.js.map +0 -1
- package/public/assets/user-avatar-DZ1g4X7l.js +0 -2
- package/public/assets/workspaces-C740m1ys.js +0 -3
package/src/branch-landed.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { errMessage } from "agent-relay-sdk";
|
|
2
2
|
import { emitRelayEvent } from "./events";
|
|
3
|
-
import { getNotificationsConfig } from "./config-store";
|
|
4
3
|
import { routeNotification, INFORMATIONAL_TTL_MS } from "./notification-router";
|
|
5
|
-
import { registerDefaultAudienceRules,
|
|
4
|
+
import { registerDefaultAudienceRules, audiencePredicates } from "./notification-rules";
|
|
6
5
|
import { createActivityEvent, getAgent, updateWorkspaceStatus } from "./db";
|
|
7
6
|
import { handleLandedIssueLifecycle } from "./services/issue-lifecycle";
|
|
8
7
|
import type { WorkspaceMergePreview, WorkspaceRecord } from "./types";
|
|
@@ -135,9 +134,6 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
135
134
|
console.warn(`issue lifecycle land hook failed for ${workspace.id}: ${errMessage(error)}`);
|
|
136
135
|
});
|
|
137
136
|
|
|
138
|
-
const config = getNotificationsConfig();
|
|
139
|
-
if (!config.enabled || !config.branchLanded) return;
|
|
140
|
-
|
|
141
137
|
const author = workspace.ownerAgentId;
|
|
142
138
|
const shaLabel = shortSha ? ` as \`${shortSha}\`` : "";
|
|
143
139
|
const subjectLabel = input.subject ? ` — "${input.subject}"` : "";
|
|
@@ -186,25 +182,22 @@ export function notifyBranchLanded(input: BranchLandedInput): void {
|
|
|
186
182
|
// online agents whose cwd is the main checkout, author-excluded, team-scoped. Empty for a solo
|
|
187
183
|
// land with no other main agents → no-op.
|
|
188
184
|
const mainScope = { author, repoRoot: workspace.repoRoot, workspaceId: workspace.id, teamId: author ? getAgent(author)?.teamId : undefined };
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
}
|
|
185
|
+
const branchLabel = landedBranch ? `\`${landedBranch}\`` : "A branch";
|
|
186
|
+
const authorLabel = author ? ` by \`${author}\`` : "";
|
|
187
|
+
// Bystander fan-out (ttl informational): a "merged" notice is only useful while fresh — a
|
|
188
|
+
// late-joining main agent shouldn't be woken by an hours-old land. Omit explicit recipients so
|
|
189
|
+
// the notification router records the full #442 rule decision for #446's routed-because trace.
|
|
190
|
+
routeNotification({
|
|
191
|
+
type: "branch.landed",
|
|
192
|
+
scope: mainScope,
|
|
193
|
+
ttlMs: INFORMATIONAL_TTL_MS,
|
|
194
|
+
message: {
|
|
195
|
+
subject: `Merged to ${base}`,
|
|
196
|
+
body: `🔀 ${branchLabel}${authorLabel} merged to \`${base}\`${shaLabel}${subjectLabel}.${publishLabel}`,
|
|
197
|
+
payload: { ...payload, notification: "landed-success" },
|
|
198
|
+
replyExpected: false,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
208
201
|
}
|
|
209
202
|
|
|
210
203
|
/**
|
|
@@ -240,8 +233,6 @@ export function notifyBranchLandFailed(input: {
|
|
|
240
233
|
});
|
|
241
234
|
|
|
242
235
|
if (!author) return;
|
|
243
|
-
const config = getNotificationsConfig();
|
|
244
|
-
if (!config.enabled || !config.branchLanded) return;
|
|
245
236
|
const branchLabel = workspace.branch ? `\`${workspace.branch}\`` : "Your branch";
|
|
246
237
|
routeNotification({
|
|
247
238
|
type: "landed-failure",
|
|
@@ -298,9 +289,6 @@ export function notifyStewardHandoff(input: {
|
|
|
298
289
|
|
|
299
290
|
emitRelayEvent({ type: "workspace.steward_handoff", source: "server", subject: workspace.id, data: payload });
|
|
300
291
|
|
|
301
|
-
const config = getNotificationsConfig();
|
|
302
|
-
if (!config.enabled) return;
|
|
303
|
-
|
|
304
292
|
// #641 — tell the owner branch agent which steward now holds its merge, so it stops
|
|
305
293
|
// thinking the branch silently failed. Durable (ttl null): the owner must learn this
|
|
306
294
|
// even if it went idle. Mirror the "don't touch main" guardrail of the landed-failure wake.
|
package/src/config-store.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { getDb, ValidationError } from "./db";
|
|
2
2
|
import { cleanEnum, cleanString, cleanStringArray } from "./validation";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
NOTIFICATIONS_CONFIG_DEFAULTS,
|
|
5
|
+
NOTIFICATIONS_DEFAULT_KEY,
|
|
6
|
+
NOTIFICATIONS_NAMESPACE,
|
|
7
|
+
parseNotificationSettingsKey,
|
|
8
|
+
validateNotificationSettingsLayer,
|
|
9
|
+
validateNotificationsConfig,
|
|
10
|
+
} from "./notification-settings";
|
|
5
11
|
import { DEFAULT_SELF_RESUME_CONFIG, validateSelfResumeConfig } from "./self-resume-config";
|
|
6
12
|
import { TEAM_TEMPLATE_NAMESPACE, validateTeamTemplate } from "./team-template-config";
|
|
7
13
|
import { PROJECT_INSTRUCTION_POLICY_NAMES } from "./project-instructions";
|
|
@@ -36,8 +42,6 @@ const STEWARD_NAMESPACE = "steward";
|
|
|
36
42
|
const STEWARD_KEY = "default";
|
|
37
43
|
const INSIGHTS_NAMESPACE = "insights";
|
|
38
44
|
const INSIGHTS_KEY = "default";
|
|
39
|
-
const NOTIFICATIONS_NAMESPACE = "notifications";
|
|
40
|
-
const NOTIFICATIONS_KEY = "default";
|
|
41
45
|
const SELF_RESUME_NAMESPACE = "self-resume";
|
|
42
46
|
const SELF_RESUME_KEY = "default";
|
|
43
47
|
const WORKSPACE_NAMESPACE = "workspace";
|
|
@@ -549,26 +553,6 @@ function validateInsightsConfig(value: unknown): InsightsConfig {
|
|
|
549
553
|
};
|
|
550
554
|
}
|
|
551
555
|
|
|
552
|
-
// Relay-driven lifecycle push notifications (#239 event bus). Default-on; the
|
|
553
|
-
// operator can flip the master switch or individual events off via the generic
|
|
554
|
-
// config route. Push messages wake recipients, so they must be suppressible.
|
|
555
|
-
const NOTIFICATIONS_CONFIG_DEFAULTS: NotificationsConfig = { enabled: true, branchLanded: true, agentReady: true, agentExited: true, agentSpawnFailed: true, contextThreshold: DEFAULT_CONTEXT_THRESHOLD_CONFIG, quotaThreshold: DEFAULT_QUOTA_THRESHOLD_CONFIG };
|
|
556
|
-
|
|
557
|
-
function validateNotificationsConfig(value: unknown): NotificationsConfig {
|
|
558
|
-
if (!isRecord(value)) throw new ValidationError("notifications config value must be an object");
|
|
559
|
-
const bool = (key: "enabled" | "branchLanded" | "agentReady" | "agentExited" | "agentSpawnFailed"): boolean =>
|
|
560
|
-
value[key] === undefined ? NOTIFICATIONS_CONFIG_DEFAULTS[key] : cleanBoolean(value[key], key);
|
|
561
|
-
return {
|
|
562
|
-
enabled: bool("enabled"),
|
|
563
|
-
branchLanded: bool("branchLanded"),
|
|
564
|
-
agentReady: bool("agentReady"),
|
|
565
|
-
agentExited: bool("agentExited"),
|
|
566
|
-
agentSpawnFailed: bool("agentSpawnFailed"),
|
|
567
|
-
contextThreshold: validateContextThresholdConfig(value.contextThreshold),
|
|
568
|
-
quotaThreshold: validateQuotaThresholdConfig(value.quotaThreshold),
|
|
569
|
-
};
|
|
570
|
-
}
|
|
571
|
-
|
|
572
556
|
// Global workspace provisioning config for isolated worktrees (#159 follow-up).
|
|
573
557
|
// Defaults seed the two untracked paths an isolated agent almost always needs:
|
|
574
558
|
// the agent guide and the rig config, both gitignored so a fresh worktree lacks them.
|
|
@@ -609,7 +593,10 @@ function normalizeValue(namespace: string, key: string, value: unknown): unknown
|
|
|
609
593
|
if (namespace === AGENT_PROFILE_NAMESPACE) return validateAgentProfile(key, value);
|
|
610
594
|
if (namespace === STEWARD_NAMESPACE) return validateStewardConfig(value);
|
|
611
595
|
if (namespace === INSIGHTS_NAMESPACE) return validateInsightsConfig(value);
|
|
612
|
-
if (namespace === NOTIFICATIONS_NAMESPACE)
|
|
596
|
+
if (namespace === NOTIFICATIONS_NAMESPACE) {
|
|
597
|
+
if (!parseNotificationSettingsKey(key)) throw new ValidationError("notifications config key must be default, team:<teamId>, or agent:<agentId>");
|
|
598
|
+
return key === NOTIFICATIONS_DEFAULT_KEY ? validateNotificationsConfig(value) : validateNotificationSettingsLayer(value);
|
|
599
|
+
}
|
|
613
600
|
if (namespace === SELF_RESUME_NAMESPACE) return validateSelfResumeConfig(value);
|
|
614
601
|
if (namespace === WORKSPACE_NAMESPACE) return validateWorkspaceConfig(value);
|
|
615
602
|
if (namespace === LANDING_NAMESPACE) return validateLandingConfig(value);
|
|
@@ -748,7 +735,7 @@ export function getInsightsConfigEntry(): ConfigEntry<InsightsConfig> {
|
|
|
748
735
|
|
|
749
736
|
/** Lifecycle-notification config (#239), merged over defaults (always usable). */
|
|
750
737
|
export function getNotificationsConfig(): NotificationsConfig {
|
|
751
|
-
const entry = getConfig<Partial<NotificationsConfig>>(NOTIFICATIONS_NAMESPACE,
|
|
738
|
+
const entry = getConfig<Partial<NotificationsConfig>>(NOTIFICATIONS_NAMESPACE, NOTIFICATIONS_DEFAULT_KEY);
|
|
752
739
|
if (!entry) return { ...NOTIFICATIONS_CONFIG_DEFAULTS };
|
|
753
740
|
return validateNotificationsConfig({ ...NOTIFICATIONS_CONFIG_DEFAULTS, ...entry.value });
|
|
754
741
|
}
|
package/src/context-advisory.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { deriveContextThresholdFromWindowTokens, findProviderCatalogModel, resolveProviderContextThreshold, type ProviderModelCatalogEntry, type ResolvedProviderContextThreshold } from "agent-relay-sdk/provider-catalog";
|
|
2
2
|
import { isRecord, SPAWN_PROVIDERS } from "agent-relay-sdk";
|
|
3
|
-
import { getConfig
|
|
4
|
-
import {
|
|
3
|
+
import { getConfig } from "./config-store";
|
|
4
|
+
import { validateContextThresholdConfig } from "./context-threshold-config";
|
|
5
|
+
import { getResolvedNotificationsConfig } from "./notification-settings-store";
|
|
6
|
+
import { NOTIFICATIONS_DEFAULT_KEY, NOTIFICATIONS_NAMESPACE, notificationAgentKey, notificationTeamKey } from "./notification-settings";
|
|
5
7
|
import { getDb } from "./db/connection.ts";
|
|
6
8
|
import { emitRelayEvent } from "./events";
|
|
7
9
|
import { routeNotification } from "./notification-router";
|
|
@@ -53,9 +55,6 @@ function notifyContextAdvisory(input: ContextAdvisoryInput): void {
|
|
|
53
55
|
data: payload,
|
|
54
56
|
});
|
|
55
57
|
|
|
56
|
-
const config = getNotificationsConfig();
|
|
57
|
-
if (!config.enabled || !config.contextThreshold.enabled) return;
|
|
58
|
-
|
|
59
58
|
// Short default TTL (per-type): a stale context advisory is misleading — the band may have moved
|
|
60
59
|
// by the time the agent next polls, so old advice shouldn't wake it.
|
|
61
60
|
routeNotification({
|
|
@@ -140,15 +139,14 @@ function effectiveContextThreshold(agent: AgentCard): EffectiveContextThreshold
|
|
|
140
139
|
if (model?.contextThreshold) {
|
|
141
140
|
return { ...resolveProviderContextThreshold(model), source: "model" };
|
|
142
141
|
}
|
|
143
|
-
const global = explicitContextThresholdConfig();
|
|
142
|
+
const global = explicitContextThresholdConfig(agent);
|
|
144
143
|
if (global) return global;
|
|
145
144
|
return { ...deriveContextThresholdFromWindowTokens(windowTokens), source: "window" };
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
function explicitContextThresholdConfig(): EffectiveContextThreshold | undefined {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const global = validateContextThresholdConfig({ ...DEFAULT_CONTEXT_THRESHOLD_CONFIG, ...entry.value.contextThreshold });
|
|
147
|
+
function explicitContextThresholdConfig(agent: AgentCard): EffectiveContextThreshold | undefined {
|
|
148
|
+
if (!hasExplicitContextThreshold(agent)) return undefined;
|
|
149
|
+
const global = validateContextThresholdConfig(getResolvedNotificationsConfig({ agentId: agent.id, teamId: agent.teamId }).contextThreshold);
|
|
152
150
|
const selfCompactUtilization = global.selfCompactUtilization ?? global.advisoryUtilization;
|
|
153
151
|
return {
|
|
154
152
|
selfCompactUtilization,
|
|
@@ -158,6 +156,18 @@ function explicitContextThresholdConfig(): EffectiveContextThreshold | undefined
|
|
|
158
156
|
};
|
|
159
157
|
}
|
|
160
158
|
|
|
159
|
+
function hasExplicitContextThreshold(agent: AgentCard): boolean {
|
|
160
|
+
const keys = [
|
|
161
|
+
NOTIFICATIONS_DEFAULT_KEY,
|
|
162
|
+
...(agent.teamId ? [notificationTeamKey(agent.teamId)] : []),
|
|
163
|
+
notificationAgentKey(agent.id),
|
|
164
|
+
];
|
|
165
|
+
return keys.some((key) => {
|
|
166
|
+
const entry = getConfig(NOTIFICATIONS_NAMESPACE, key);
|
|
167
|
+
return isRecord(entry?.value) && isRecord(entry.value.contextThreshold);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
161
171
|
function findAgentCatalogModel(agent: AgentCard, observedWindowTokens: number | undefined): ProviderModelCatalogEntry | undefined {
|
|
162
172
|
const runtime = agent.providerCapabilities?.model;
|
|
163
173
|
const provider = runtime?.provider;
|
package/src/db/migrations.ts
CHANGED
|
@@ -896,9 +896,13 @@ export function applyMigrations(): void {
|
|
|
896
896
|
next_run_at INTEGER NOT NULL,
|
|
897
897
|
schedule_kind TEXT NOT NULL,
|
|
898
898
|
schedule_def TEXT NOT NULL,
|
|
899
|
+
message_kind TEXT,
|
|
900
|
+
channel TEXT,
|
|
899
901
|
subject TEXT,
|
|
900
902
|
body TEXT NOT NULL,
|
|
903
|
+
claimable INTEGER NOT NULL DEFAULT 0,
|
|
901
904
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
905
|
+
meta TEXT NOT NULL DEFAULT '{}',
|
|
902
906
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
903
907
|
run_count INTEGER NOT NULL DEFAULT 0,
|
|
904
908
|
max_runs INTEGER,
|
|
@@ -913,6 +917,19 @@ export function applyMigrations(): void {
|
|
|
913
917
|
claim_expires_at INTEGER
|
|
914
918
|
)
|
|
915
919
|
`);
|
|
920
|
+
const scheduledTimerCols = (getDb().query("PRAGMA table_info(scheduled_timers)").all() as any[]).map((c: any) => c.name);
|
|
921
|
+
if (!scheduledTimerCols.includes("message_kind")) {
|
|
922
|
+
getDb().run("ALTER TABLE scheduled_timers ADD COLUMN message_kind TEXT");
|
|
923
|
+
}
|
|
924
|
+
if (!scheduledTimerCols.includes("channel")) {
|
|
925
|
+
getDb().run("ALTER TABLE scheduled_timers ADD COLUMN channel TEXT");
|
|
926
|
+
}
|
|
927
|
+
if (!scheduledTimerCols.includes("claimable")) {
|
|
928
|
+
getDb().run("ALTER TABLE scheduled_timers ADD COLUMN claimable INTEGER NOT NULL DEFAULT 0");
|
|
929
|
+
}
|
|
930
|
+
if (!scheduledTimerCols.includes("meta")) {
|
|
931
|
+
getDb().run("ALTER TABLE scheduled_timers ADD COLUMN meta TEXT NOT NULL DEFAULT '{}'");
|
|
932
|
+
}
|
|
916
933
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_scheduled_timers_due ON scheduled_timers(enabled, next_run_at, claim_expires_at)");
|
|
917
934
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_scheduled_timers_creator ON scheduled_timers(created_by, enabled, next_run_at)");
|
|
918
935
|
getDb().run("CREATE INDEX IF NOT EXISTS idx_scheduled_timers_correlation ON scheduled_timers(correlation_id)");
|
|
@@ -22,9 +22,13 @@ export interface ScheduledTimer {
|
|
|
22
22
|
nextRunAt: number;
|
|
23
23
|
scheduleKind: TimerScheduleKind;
|
|
24
24
|
scheduleDef: TimerScheduleDef;
|
|
25
|
+
messageKind?: string;
|
|
26
|
+
channel?: string;
|
|
25
27
|
subject?: string;
|
|
26
28
|
body: string;
|
|
29
|
+
claimable?: boolean;
|
|
27
30
|
payload: Record<string, unknown>;
|
|
31
|
+
meta: Record<string, unknown>;
|
|
28
32
|
enabled: boolean;
|
|
29
33
|
runCount: number;
|
|
30
34
|
maxRuns?: number;
|
|
@@ -51,9 +55,13 @@ interface ScheduledTimerRow {
|
|
|
51
55
|
next_run_at: number;
|
|
52
56
|
schedule_kind: string;
|
|
53
57
|
schedule_def: string;
|
|
58
|
+
message_kind: string | null;
|
|
59
|
+
channel: string | null;
|
|
54
60
|
subject: string | null;
|
|
55
61
|
body: string;
|
|
62
|
+
claimable: number | null;
|
|
56
63
|
payload: string;
|
|
64
|
+
meta: string | null;
|
|
57
65
|
enabled: number;
|
|
58
66
|
run_count: number;
|
|
59
67
|
max_runs: number | null;
|
|
@@ -78,9 +86,13 @@ export interface CreateScheduledTimerRowInput {
|
|
|
78
86
|
nextRunAt: number;
|
|
79
87
|
scheduleKind: TimerScheduleKind;
|
|
80
88
|
scheduleDef: TimerScheduleDef;
|
|
89
|
+
messageKind?: string;
|
|
90
|
+
channel?: string;
|
|
81
91
|
subject?: string;
|
|
82
92
|
body: string;
|
|
93
|
+
claimable?: boolean;
|
|
83
94
|
payload?: Record<string, unknown>;
|
|
95
|
+
meta?: Record<string, unknown>;
|
|
84
96
|
maxRuns?: number;
|
|
85
97
|
expiresAt?: number;
|
|
86
98
|
idempotencyKey?: string;
|
|
@@ -92,10 +104,10 @@ export function insertScheduledTimer(input: CreateScheduledTimerRowInput, now =
|
|
|
92
104
|
getDb().query(`
|
|
93
105
|
INSERT INTO scheduled_timers (
|
|
94
106
|
id, target, created_by, created_by_kind, created_by_scopes, created_by_constraints, delivery_from,
|
|
95
|
-
created_at, next_run_at, schedule_kind, schedule_def, subject, body, payload, enabled,
|
|
107
|
+
created_at, next_run_at, schedule_kind, schedule_def, message_kind, channel, subject, body, claimable, payload, meta, enabled,
|
|
96
108
|
run_count, max_runs, expires_at, idempotency_key, correlation_id
|
|
97
109
|
)
|
|
98
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, ?, ?, ?, ?)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 0, ?, ?, ?, ?)
|
|
99
111
|
`).run(
|
|
100
112
|
id,
|
|
101
113
|
input.target,
|
|
@@ -108,9 +120,13 @@ export function insertScheduledTimer(input: CreateScheduledTimerRowInput, now =
|
|
|
108
120
|
input.nextRunAt,
|
|
109
121
|
input.scheduleKind,
|
|
110
122
|
JSON.stringify(input.scheduleDef),
|
|
123
|
+
input.messageKind ?? null,
|
|
124
|
+
input.channel ?? null,
|
|
111
125
|
input.subject ?? null,
|
|
112
126
|
input.body,
|
|
127
|
+
input.claimable ? 1 : 0,
|
|
113
128
|
JSON.stringify(input.payload ?? {}),
|
|
129
|
+
JSON.stringify(input.meta ?? {}),
|
|
114
130
|
input.maxRuns ?? null,
|
|
115
131
|
input.expiresAt ?? null,
|
|
116
132
|
input.idempotencyKey ?? null,
|
|
@@ -199,20 +215,20 @@ export function completeScheduledTimerRun(input: {
|
|
|
199
215
|
claimed_by = NULL,
|
|
200
216
|
claim_expires_at = NULL
|
|
201
217
|
WHERE id = ?
|
|
202
|
-
|
|
218
|
+
AND claimed_by = ?
|
|
219
|
+
AND enabled = 1
|
|
220
|
+
`).run(runCount, input.now, input.messageId, candidateNextRunAt, enabled ? 1 : 0, input.timer.id, input.timer.claimedBy ?? "");
|
|
203
221
|
return getScheduledTimer(input.timer.id)!;
|
|
204
222
|
}
|
|
205
223
|
|
|
206
224
|
export function recordScheduledTimerFailure(input: {
|
|
207
|
-
|
|
225
|
+
timer: ScheduledTimer;
|
|
208
226
|
error: string;
|
|
209
227
|
now: number;
|
|
210
228
|
retryMs: number;
|
|
211
229
|
maxFailures: number;
|
|
212
230
|
}): ScheduledTimer | null {
|
|
213
|
-
const
|
|
214
|
-
if (!current) return null;
|
|
215
|
-
const failures = current.consecutiveFailures + 1;
|
|
231
|
+
const failures = input.timer.consecutiveFailures + 1;
|
|
216
232
|
const enabled = failures < input.maxFailures;
|
|
217
233
|
getDb().query(`
|
|
218
234
|
UPDATE scheduled_timers
|
|
@@ -223,17 +239,18 @@ export function recordScheduledTimerFailure(input: {
|
|
|
223
239
|
claimed_by = NULL,
|
|
224
240
|
claim_expires_at = NULL
|
|
225
241
|
WHERE id = ?
|
|
226
|
-
|
|
227
|
-
|
|
242
|
+
AND claimed_by = ?
|
|
243
|
+
AND enabled = 1
|
|
244
|
+
`).run(input.error, failures, input.now + input.retryMs, enabled ? 1 : 0, input.timer.id, input.timer.claimedBy ?? "");
|
|
245
|
+
return getScheduledTimer(input.timer.id);
|
|
228
246
|
}
|
|
229
247
|
|
|
230
|
-
export function cancelScheduledTimer(id: string
|
|
231
|
-
|
|
248
|
+
export function cancelScheduledTimer(id: string): ScheduledTimer | null {
|
|
249
|
+
getDb().query(`
|
|
232
250
|
UPDATE scheduled_timers
|
|
233
251
|
SET enabled = 0, claimed_by = NULL, claim_expires_at = NULL, last_error = COALESCE(last_error, 'cancelled')
|
|
234
252
|
WHERE id = ? AND enabled = 1
|
|
235
253
|
`).run(id);
|
|
236
|
-
if (result.changes === 0) return getScheduledTimer(id);
|
|
237
254
|
return getScheduledTimer(id);
|
|
238
255
|
}
|
|
239
256
|
|
|
@@ -259,9 +276,13 @@ function rowToTimer(row: ScheduledTimerRow): ScheduledTimer {
|
|
|
259
276
|
nextRunAt: row.next_run_at,
|
|
260
277
|
scheduleKind: row.schedule_kind === "interval" ? "interval" : "once",
|
|
261
278
|
scheduleDef: parseRecord(row.schedule_def) as TimerScheduleDef,
|
|
279
|
+
messageKind: row.message_kind ?? undefined,
|
|
280
|
+
channel: row.channel ?? undefined,
|
|
262
281
|
subject: row.subject ?? undefined,
|
|
263
282
|
body: row.body,
|
|
283
|
+
claimable: row.claimable === 1 ? true : undefined,
|
|
264
284
|
payload: parseRecord(row.payload) ?? {},
|
|
285
|
+
meta: parseRecord(row.meta) ?? {},
|
|
265
286
|
enabled: row.enabled === 1,
|
|
266
287
|
runCount: row.run_count,
|
|
267
288
|
maxRuns: row.max_runs ?? undefined,
|
package/src/db/schema.ts
CHANGED
|
@@ -176,9 +176,13 @@ export function initDb(path: string = "agent-relay.db"): Database {
|
|
|
176
176
|
next_run_at INTEGER NOT NULL,
|
|
177
177
|
schedule_kind TEXT NOT NULL,
|
|
178
178
|
schedule_def TEXT NOT NULL,
|
|
179
|
+
message_kind TEXT,
|
|
180
|
+
channel TEXT,
|
|
179
181
|
subject TEXT,
|
|
180
182
|
body TEXT NOT NULL,
|
|
183
|
+
claimable INTEGER NOT NULL DEFAULT 0,
|
|
181
184
|
payload TEXT NOT NULL DEFAULT '{}',
|
|
185
|
+
meta TEXT NOT NULL DEFAULT '{}',
|
|
182
186
|
enabled INTEGER NOT NULL DEFAULT 1,
|
|
183
187
|
run_count INTEGER NOT NULL DEFAULT 0,
|
|
184
188
|
max_runs INTEGER,
|
package/src/mcp/index.ts
CHANGED
|
@@ -150,7 +150,7 @@ async function callTool(auth: McpAuthContext, params: unknown): Promise<Record<s
|
|
|
150
150
|
else if (name.startsWith("relay_plan_")) result = relayPlanTool(auth, name, args);
|
|
151
151
|
else if (name.startsWith("relay_blackboard_")) result = relayBlackboardTool(auth, name, args);
|
|
152
152
|
else if (name.startsWith("relay_project_")) result = relayProjectTool(auth, name, args); else if (name.startsWith("relay_issue_ref_")) result = relayIssueRefTool(auth, name, args); else if (name.startsWith("relay_issue_association_")) result = relayIssueAssociationTool(auth, name, args); else if (name.startsWith("relay_issue_activity_")) result = relayIssueActivityTool(auth, name, args); else if (name.startsWith("relay_issue_lifecycle_")) result = await relayIssueLifecycleTool(auth, name, args);
|
|
153
|
-
else if (name === "relay_subscribe" || name === "relay_unsubscribe" || name === "relay_list_subscriptions") result = relayNotificationSubscriptionTool(auth, name, args);
|
|
153
|
+
else if (name === "relay_subscribe" || name === "relay_unsubscribe" || name === "relay_list_subscriptions" || name === "relay_why_notified" || name === "relay_notification_preview") result = relayNotificationSubscriptionTool(auth, name, args);
|
|
154
154
|
else if (name === "relay_notify_ntfy") result = await relayNotifyNtfy(args);
|
|
155
155
|
else throw new ValidationError(`unknown tool: ${name}`);
|
|
156
156
|
|
|
@@ -54,6 +54,7 @@ export const MESSAGING_TOOLS: ToolDefinition[] = [
|
|
|
54
54
|
from: { type: "string", description: "Deprecated/optional. Your identity is taken from your auth token; ignored for managed agents. You never need to set this." },
|
|
55
55
|
to: { type: "string", description: "Target: an agent id, label, name, or a unique id segment (e.g. the number part) — resolved automatically. Or a fan-out selector: tag:, cap:, label:, team:, policy:, or broadcast." },
|
|
56
56
|
body: { type: "string" },
|
|
57
|
+
kind: { type: "string", enum: ["chat", "channel.event", "task", "pair", "control", "system", "session"] },
|
|
57
58
|
subject: { type: "string" },
|
|
58
59
|
channel: { type: "string" },
|
|
59
60
|
claimable: { type: "boolean" },
|
|
@@ -226,6 +227,10 @@ function runMcpSend(auth: McpAuthContext, input: SendMessageInput): Message & {
|
|
|
226
227
|
export function relaySendMessage(auth: McpAuthContext, args: Record<string, unknown>): (Message & { delivery: DeliveryReceipt }) | Record<string, unknown> {
|
|
227
228
|
const attachments = optionalAttachments(args.attachments);
|
|
228
229
|
const payload = payloadWithAttachments(optionalRecord(args.payload, "payload"), attachments);
|
|
230
|
+
const kind = optionalEnum(args.kind, "kind", ["chat", "channel.event", "task", "pair", "control", "system", "session"] as const);
|
|
231
|
+
const channel = optionalString(args.channel, "channel", 120);
|
|
232
|
+
const claimable = optionalBoolean(args.claimable, "claimable");
|
|
233
|
+
const meta = optionalRecord(args.meta, "meta");
|
|
229
234
|
const deliveryTime = optionalString(args.deliveryTime, "deliveryTime", 80);
|
|
230
235
|
if (deliveryTime) {
|
|
231
236
|
if (!hasScope(auth, "schedule:write")) throw new McpAuthError("deliveryTime requires schedule:write");
|
|
@@ -233,9 +238,13 @@ export function relaySendMessage(auth: McpAuthContext, args: Record<string, unkn
|
|
|
233
238
|
const timer = scheduleTimer({
|
|
234
239
|
target: stringField(args.to, "to", { required: true, max: 200 }),
|
|
235
240
|
at: deliveryTime,
|
|
241
|
+
kind,
|
|
242
|
+
channel,
|
|
236
243
|
body: stringField(args.body, "body", { required: true, maxBytes: MAX_BODY_BYTES }),
|
|
237
244
|
subject: optionalString(args.subject, "subject", 200),
|
|
245
|
+
claimable,
|
|
238
246
|
payload,
|
|
247
|
+
meta,
|
|
239
248
|
idempotencyKey: optionalString(args.idempotencyKey, "idempotencyKey", 240),
|
|
240
249
|
}, authContextFromMcp({ kind: auth.kind, actor: auth.actor, scopes: auth.scopes, component: auth.component }));
|
|
241
250
|
return { scheduled: true, timer };
|
|
@@ -247,14 +256,15 @@ export function relaySendMessage(auth: McpAuthContext, args: Record<string, unkn
|
|
|
247
256
|
const input: SendMessageInput = {
|
|
248
257
|
from: optionalString(args.from, "from", 200) ?? "",
|
|
249
258
|
to: stringField(args.to, "to", { required: true, max: 200 }),
|
|
259
|
+
kind,
|
|
260
|
+
channel,
|
|
250
261
|
body: stringField(args.body, "body", { required: true, maxBytes: MAX_BODY_BYTES }),
|
|
251
262
|
subject: optionalString(args.subject, "subject", 200),
|
|
252
|
-
|
|
253
|
-
claimable: optionalBoolean(args.claimable, "claimable"),
|
|
263
|
+
claimable,
|
|
254
264
|
idempotencyKey: optionalString(args.idempotencyKey, "idempotencyKey", 240),
|
|
255
265
|
attachments,
|
|
256
266
|
payload,
|
|
257
|
-
meta
|
|
267
|
+
meta,
|
|
258
268
|
};
|
|
259
269
|
return runMcpSend(auth, input);
|
|
260
270
|
}
|
|
@@ -15,10 +15,11 @@ import {
|
|
|
15
15
|
type NotificationSubscriptionMode,
|
|
16
16
|
} from "./db/notification-subscriptions";
|
|
17
17
|
import { ValidationError } from "./db";
|
|
18
|
-
import { McpAuthError } from "./mcp-errors";
|
|
18
|
+
import { McpAuthError, McpNotFoundError } from "./mcp-errors";
|
|
19
19
|
import { authContextFromMcp } from "./services/auth-context";
|
|
20
20
|
import { cleanString } from "./validation";
|
|
21
|
-
import type
|
|
21
|
+
import { NOTIFICATION_TYPES, type NotificationScope, type NotificationType } from "./notification-types";
|
|
22
|
+
import { previewNotificationRouting, whyNotified } from "./notification-router";
|
|
22
23
|
|
|
23
24
|
export const NOTIFICATION_SUBSCRIPTION_TOOLS = [
|
|
24
25
|
{
|
|
@@ -68,6 +69,33 @@ export const NOTIFICATION_SUBSCRIPTION_TOOLS = [
|
|
|
68
69
|
additionalProperties: false,
|
|
69
70
|
},
|
|
70
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: "relay_why_notified",
|
|
74
|
+
description: "Explain why this caller agent received a delivered notification message.",
|
|
75
|
+
requiredScopes: ["notifications:read"],
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
properties: {
|
|
79
|
+
messageId: { type: "number", description: "Delivered Relay message id." },
|
|
80
|
+
},
|
|
81
|
+
required: ["messageId"],
|
|
82
|
+
additionalProperties: false,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "relay_notification_preview",
|
|
87
|
+
description: "Dry-run notification routing for a type and scope, returning recipients and matched rules without sending.",
|
|
88
|
+
requiredScopes: ["notifications:read"],
|
|
89
|
+
inputSchema: {
|
|
90
|
+
type: "object",
|
|
91
|
+
properties: {
|
|
92
|
+
type: { type: "string", enum: NOTIFICATION_TYPES },
|
|
93
|
+
scope: { type: "object", description: "Notification scope, such as { author, teamId, repoRoot, workspaceId }." },
|
|
94
|
+
},
|
|
95
|
+
required: ["type", "scope"],
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
71
99
|
] satisfies Array<{ name: string; description: string; requiredScopes: string[]; inputSchema: Record<string, unknown> }>;
|
|
72
100
|
|
|
73
101
|
type McpAuthLike = Parameters<typeof authContextFromMcp>[0];
|
|
@@ -149,6 +177,17 @@ export function relayNotificationSubscriptionTool(
|
|
|
149
177
|
},
|
|
150
178
|
};
|
|
151
179
|
}
|
|
180
|
+
if (name === "relay_why_notified") {
|
|
181
|
+
const explanation = whyNotified(cleanMessageId(args.messageId), agentId);
|
|
182
|
+
if (!explanation) throw new McpNotFoundError("notification routing explanation not found for this agent/message");
|
|
183
|
+
return { ...explanation };
|
|
184
|
+
}
|
|
185
|
+
if (name === "relay_notification_preview") {
|
|
186
|
+
return { ...previewNotificationRouting({
|
|
187
|
+
type: cleanNotificationType(args.type),
|
|
188
|
+
scope: cleanScope(args.scope) ?? {},
|
|
189
|
+
}) };
|
|
190
|
+
}
|
|
152
191
|
throw new ValidationError(`unknown tool: ${name}`);
|
|
153
192
|
}
|
|
154
193
|
|
|
@@ -169,6 +208,20 @@ function cleanScope(value: unknown): Partial<NotificationScope> | null {
|
|
|
169
208
|
return value as Partial<NotificationScope>;
|
|
170
209
|
}
|
|
171
210
|
|
|
211
|
+
function cleanNotificationType(value: unknown): NotificationType {
|
|
212
|
+
if (typeof value !== "string" || !NOTIFICATION_TYPES.includes(value as NotificationType)) {
|
|
213
|
+
throw new ValidationError(`type must be one of: ${NOTIFICATION_TYPES.join(", ")}`);
|
|
214
|
+
}
|
|
215
|
+
return value as NotificationType;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function cleanMessageId(value: unknown): number {
|
|
219
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
220
|
+
throw new ValidationError("messageId must be a positive integer");
|
|
221
|
+
}
|
|
222
|
+
return value;
|
|
223
|
+
}
|
|
224
|
+
|
|
172
225
|
function cleanChildStreams(value: unknown): ChildStream[] {
|
|
173
226
|
if (!Array.isArray(value) || value.length === 0) throw new ValidationError("streams must be a non-empty array");
|
|
174
227
|
return [...new Set(value.map(cleanChildStream))];
|