agent-relay-server 0.92.0 → 0.93.1
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 +28 -10
- package/src/mcp/tools-scheduler.ts +10 -4
- 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
|
|
|
@@ -46,7 +46,7 @@ const VALID_ARTIFACT_ENTITY_TYPES = ["message", "task", "recipeRun", "recipeStep
|
|
|
46
46
|
export const MESSAGING_TOOLS: ToolDefinition[] = [
|
|
47
47
|
{
|
|
48
48
|
name: "relay_send_message",
|
|
49
|
-
description: "Send an Agent Relay message. Returns a delivery receipt (delivered/expectReply/recipients): if expectReply is false, no live recipient exists — don't wait for a reply. An ambiguous target is rejected up front. An unknown target is stored for delivery once that id registers (the receipt reports delivered:false with empty recipients and a reason) — never silently dropped.",
|
|
49
|
+
description: "Send an Agent Relay message. Accepts message as an alias for body. Returns a delivery receipt (delivered/expectReply/recipients): if expectReply is false, no live recipient exists — don't wait for a reply. An ambiguous target is rejected up front. An unknown target is stored for delivery once that id registers (the receipt reports delivered:false with empty recipients and a reason) — never silently dropped.",
|
|
50
50
|
requiredScopes: ["messages:write", "message:send"],
|
|
51
51
|
inputSchema: {
|
|
52
52
|
type: "object",
|
|
@@ -54,6 +54,8 @@ 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
|
+
message: { type: "string", description: "Alias for body." },
|
|
58
|
+
kind: { type: "string", enum: ["chat", "channel.event", "task", "pair", "control", "system", "session"] },
|
|
57
59
|
subject: { type: "string" },
|
|
58
60
|
channel: { type: "string" },
|
|
59
61
|
claimable: { type: "boolean" },
|
|
@@ -63,13 +65,13 @@ export const MESSAGING_TOOLS: ToolDefinition[] = [
|
|
|
63
65
|
payload: { type: "object" },
|
|
64
66
|
meta: { type: "object" },
|
|
65
67
|
},
|
|
66
|
-
required: ["to"
|
|
68
|
+
required: ["to"],
|
|
67
69
|
additionalProperties: false,
|
|
68
70
|
},
|
|
69
71
|
},
|
|
70
72
|
{
|
|
71
73
|
name: "relay_reply",
|
|
72
|
-
description: "Reply to an Agent Relay message by id, preserving reply/thread/channel context. Returns a delivery receipt; expectReply:false means the original sender is no longer reachable.",
|
|
74
|
+
description: "Reply to an Agent Relay message by id, preserving reply/thread/channel context. Accepts message as an alias for body. Returns a delivery receipt; expectReply:false means the original sender is no longer reachable.",
|
|
73
75
|
requiredScopes: ["messages:write", "message:send"],
|
|
74
76
|
inputSchema: {
|
|
75
77
|
type: "object",
|
|
@@ -77,13 +79,14 @@ export const MESSAGING_TOOLS: ToolDefinition[] = [
|
|
|
77
79
|
from: { type: "string", description: "Deprecated/optional. Your identity is taken from your auth token; ignored for managed agents. You never need to set this." },
|
|
78
80
|
messageId: { type: "integer", minimum: 1 },
|
|
79
81
|
body: { type: "string" },
|
|
82
|
+
message: { type: "string", description: "Alias for body." },
|
|
80
83
|
subject: { type: "string" },
|
|
81
84
|
format: { type: "string", enum: ["text", "markdown", "markdownv2"] },
|
|
82
85
|
attachments: { type: "array", items: { type: "object" } },
|
|
83
86
|
payload: { type: "object" },
|
|
84
87
|
meta: { type: "object" },
|
|
85
88
|
},
|
|
86
|
-
required: ["messageId"
|
|
89
|
+
required: ["messageId"],
|
|
87
90
|
additionalProperties: false,
|
|
88
91
|
},
|
|
89
92
|
},
|
|
@@ -226,6 +229,10 @@ function runMcpSend(auth: McpAuthContext, input: SendMessageInput): Message & {
|
|
|
226
229
|
export function relaySendMessage(auth: McpAuthContext, args: Record<string, unknown>): (Message & { delivery: DeliveryReceipt }) | Record<string, unknown> {
|
|
227
230
|
const attachments = optionalAttachments(args.attachments);
|
|
228
231
|
const payload = payloadWithAttachments(optionalRecord(args.payload, "payload"), attachments);
|
|
232
|
+
const kind = optionalEnum(args.kind, "kind", ["chat", "channel.event", "task", "pair", "control", "system", "session"] as const);
|
|
233
|
+
const channel = optionalString(args.channel, "channel", 120);
|
|
234
|
+
const claimable = optionalBoolean(args.claimable, "claimable");
|
|
235
|
+
const meta = optionalRecord(args.meta, "meta");
|
|
229
236
|
const deliveryTime = optionalString(args.deliveryTime, "deliveryTime", 80);
|
|
230
237
|
if (deliveryTime) {
|
|
231
238
|
if (!hasScope(auth, "schedule:write")) throw new McpAuthError("deliveryTime requires schedule:write");
|
|
@@ -233,9 +240,13 @@ export function relaySendMessage(auth: McpAuthContext, args: Record<string, unkn
|
|
|
233
240
|
const timer = scheduleTimer({
|
|
234
241
|
target: stringField(args.to, "to", { required: true, max: 200 }),
|
|
235
242
|
at: deliveryTime,
|
|
236
|
-
|
|
243
|
+
kind,
|
|
244
|
+
channel,
|
|
245
|
+
body: messageBody(args),
|
|
237
246
|
subject: optionalString(args.subject, "subject", 200),
|
|
247
|
+
claimable,
|
|
238
248
|
payload,
|
|
249
|
+
meta,
|
|
239
250
|
idempotencyKey: optionalString(args.idempotencyKey, "idempotencyKey", 240),
|
|
240
251
|
}, authContextFromMcp({ kind: auth.kind, actor: auth.actor, scopes: auth.scopes, component: auth.component }));
|
|
241
252
|
return { scheduled: true, timer };
|
|
@@ -247,14 +258,15 @@ export function relaySendMessage(auth: McpAuthContext, args: Record<string, unkn
|
|
|
247
258
|
const input: SendMessageInput = {
|
|
248
259
|
from: optionalString(args.from, "from", 200) ?? "",
|
|
249
260
|
to: stringField(args.to, "to", { required: true, max: 200 }),
|
|
250
|
-
|
|
261
|
+
kind,
|
|
262
|
+
channel,
|
|
263
|
+
body: messageBody(args),
|
|
251
264
|
subject: optionalString(args.subject, "subject", 200),
|
|
252
|
-
|
|
253
|
-
claimable: optionalBoolean(args.claimable, "claimable"),
|
|
265
|
+
claimable,
|
|
254
266
|
idempotencyKey: optionalString(args.idempotencyKey, "idempotencyKey", 240),
|
|
255
267
|
attachments,
|
|
256
268
|
payload,
|
|
257
|
-
meta
|
|
269
|
+
meta,
|
|
258
270
|
};
|
|
259
271
|
return runMcpSend(auth, input);
|
|
260
272
|
}
|
|
@@ -273,7 +285,7 @@ export function relayReply(auth: McpAuthContext, args: Record<string, unknown>):
|
|
|
273
285
|
return runMcpSend(auth, {
|
|
274
286
|
from: optionalString(args.from, "from", 200) ?? "",
|
|
275
287
|
to: "",
|
|
276
|
-
body:
|
|
288
|
+
body: messageBody(args),
|
|
277
289
|
subject: optionalString(args.subject, "subject", 200),
|
|
278
290
|
replyTo: parent.id,
|
|
279
291
|
attachments,
|
|
@@ -282,6 +294,12 @@ export function relayReply(auth: McpAuthContext, args: Record<string, unknown>):
|
|
|
282
294
|
});
|
|
283
295
|
}
|
|
284
296
|
|
|
297
|
+
function messageBody(args: Record<string, unknown>): string {
|
|
298
|
+
const value = args.body ?? args.message;
|
|
299
|
+
if (value === undefined || value === null) throw new ValidationError("body (or its alias message) is required");
|
|
300
|
+
return stringField(value, "body", { required: true, maxBytes: MAX_BODY_BYTES });
|
|
301
|
+
}
|
|
302
|
+
|
|
285
303
|
export function relayGetMessage(args: Record<string, unknown>): Record<string, unknown> {
|
|
286
304
|
const messageId = positiveId(args.messageId, "messageId");
|
|
287
305
|
const message = getMessage(messageId);
|
|
@@ -2,7 +2,7 @@ import { McpAuthError } from "../mcp-errors";
|
|
|
2
2
|
import { authContextFromMcp } from "../services/auth-context";
|
|
3
3
|
import { ServiceAuthError } from "../services/errors";
|
|
4
4
|
import { cancelTimer, listTimers, scheduleTimer, type ScheduleTimerInput } from "../services/scheduler";
|
|
5
|
-
import type
|
|
5
|
+
import { ValidationError, type ScheduledTimer } from "../db";
|
|
6
6
|
import type { McpAuthContext, ToolDefinition } from "./types";
|
|
7
7
|
import { optionalBoolean, optionalPositiveInt, optionalRecord, optionalString, stringField } from "./validation";
|
|
8
8
|
|
|
@@ -45,14 +45,14 @@ export const SCHEDULER_TOOLS: ToolDefinition[] = [
|
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
name: "relay_cancel_timer",
|
|
48
|
-
description: "Cancel one of your durable Relay timers. Admin callers may cancel any timer.",
|
|
48
|
+
description: "Cancel one of your durable Relay timers. Accepts id as an alias for timerId, matching the id returned by relay_schedule_timer. Admin callers may cancel any timer.",
|
|
49
49
|
requiredScopes: ["schedule:write"],
|
|
50
50
|
inputSchema: {
|
|
51
51
|
type: "object",
|
|
52
52
|
properties: {
|
|
53
53
|
timerId: { type: "string" },
|
|
54
|
+
id: { type: "string", description: "Alias for timerId (matches the `id` field returned by relay_schedule_timer)." },
|
|
54
55
|
},
|
|
55
|
-
required: ["timerId"],
|
|
56
56
|
additionalProperties: false,
|
|
57
57
|
},
|
|
58
58
|
},
|
|
@@ -77,10 +77,16 @@ export function relayListTimers(auth: McpAuthContext, args: Record<string, unkno
|
|
|
77
77
|
|
|
78
78
|
export function relayCancelTimer(auth: McpAuthContext, args: Record<string, unknown>): Record<string, unknown> {
|
|
79
79
|
return withAuthErrors(() => ({
|
|
80
|
-
timer: serializeTimer(cancelTimer(
|
|
80
|
+
timer: serializeTimer(cancelTimer(timerIdArg(args), authContextFromMcp(authContextPayload(auth)))),
|
|
81
81
|
}));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
function timerIdArg(args: Record<string, unknown>): string {
|
|
85
|
+
const value = args.timerId ?? args.id;
|
|
86
|
+
if (value === undefined || value === null) throw new ValidationError("timerId (or its alias id) is required");
|
|
87
|
+
return stringField(value, "timerId", { required: true, max: 120 });
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
function cleanScheduleInput(args: Record<string, unknown>): ScheduleTimerInput {
|
|
85
91
|
return {
|
|
86
92
|
target: optionalString(args.target, "target", 200),
|
|
@@ -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))];
|