agent-relay-server 0.71.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-BYmE8ldg.js → PlanGraphPanel-icESxd82.js} +2 -2
- package/public/assets/{PlanGraphPanel-BYmE8ldg.js.map → PlanGraphPanel-icESxd82.js.map} +1 -1
- package/public/assets/{activity-BzU196gP.js → activity-BR9I23hc.js} +2 -2
- package/public/assets/{activity-BzU196gP.js.map → activity-BR9I23hc.js.map} +1 -1
- package/public/assets/{agent-profiles-CQuoP8Nr.js → agent-profiles-BB0V-Tg1.js} +2 -2
- package/public/assets/{agent-profiles-CQuoP8Nr.js.map → agent-profiles-BB0V-Tg1.js.map} +1 -1
- package/public/assets/{agents-qH6q44nK.js → agents-Clkboz93.js} +2 -2
- package/public/assets/{agents-qH6q44nK.js.map → agents-Clkboz93.js.map} +1 -1
- package/public/assets/{analytics-EJH9GFID.js → analytics-DRlHs5ho.js} +3 -3
- package/public/assets/{analytics-EJH9GFID.js.map → analytics-DRlHs5ho.js.map} +1 -1
- package/public/assets/{automation-Dahmxg07.js → automation-DlnH_MPy.js} +2 -2
- package/public/assets/{automation-Dahmxg07.js.map → automation-DlnH_MPy.js.map} +1 -1
- package/public/assets/{badge-C6NJQ60B.js → badge-YQhT30dy.js} +2 -2
- package/public/assets/{badge-C6NJQ60B.js.map → badge-YQhT30dy.js.map} +1 -1
- package/public/assets/branch-state-badge-Dg1qnmF5.js +2 -0
- package/public/assets/{branch-state-badge-C7H8HY22.js.map → branch-state-badge-Dg1qnmF5.js.map} +1 -1
- package/public/assets/{button-DD2CCM3t.js → button-CaFI76Ja.js} +2 -2
- package/public/assets/{button-DD2CCM3t.js.map → button-CaFI76Ja.js.map} +1 -1
- package/public/assets/{card-GZqj3rMm.js → card-B_fQtKce.js} +2 -2
- package/public/assets/{card-GZqj3rMm.js.map → card-B_fQtKce.js.map} +1 -1
- package/public/assets/{channels-VhlwES_A.js → channels-BVYLrKUL.js} +2 -2
- package/public/assets/{channels-VhlwES_A.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-C03pRoXx.js → connectors-BOyk5hC3.js} +2 -2
- package/public/assets/{connectors-C03pRoXx.js.map → connectors-BOyk5hC3.js.map} +1 -1
- package/public/assets/copy-button-m00JmflM.js +2 -0
- package/public/assets/{copy-button-DqAoeiPC.js.map → copy-button-m00JmflM.js.map} +1 -1
- package/public/assets/display-LAWaAboe.js.map +1 -1
- package/public/assets/{dist-DByqf8W0.js → dist-B_yrHbsb.js} +2 -2
- package/public/assets/{dist-DByqf8W0.js.map → dist-B_yrHbsb.js.map} +1 -1
- package/public/assets/{dist-C_XTOHbv.js → dist-C-viWls-.js} +2 -2
- package/public/assets/{dist-C_XTOHbv.js.map → dist-C-viWls-.js.map} +1 -1
- package/public/assets/{dist-DMD9fD99.js → dist-CYS2puT9.js} +2 -2
- package/public/assets/{dist-DMD9fD99.js.map → dist-CYS2puT9.js.map} +1 -1
- package/public/assets/{dist-CpugiXtB.js → dist-ChP4KvKi.js} +2 -2
- package/public/assets/{dist-CpugiXtB.js.map → dist-ChP4KvKi.js.map} +1 -1
- package/public/assets/{dist-CePvvSwo.js → dist-Cnlh_NpI.js} +2 -2
- package/public/assets/{dist-CePvvSwo.js.map → dist-Cnlh_NpI.js.map} +1 -1
- package/public/assets/{dist-DJ6ZVqRl.js → dist-CydxJz3m.js} +2 -2
- package/public/assets/{dist-DJ6ZVqRl.js.map → dist-CydxJz3m.js.map} +1 -1
- package/public/assets/{dist-Djf_WfhW.js → dist-D7K3qgp7.js} +2 -2
- package/public/assets/{dist-Djf_WfhW.js.map → dist-D7K3qgp7.js.map} +1 -1
- package/public/assets/{dist-otKoCUDq.js → dist-D8Yr1AIJ.js} +2 -2
- package/public/assets/{dist-otKoCUDq.js.map → dist-D8Yr1AIJ.js.map} +1 -1
- package/public/assets/{dist-BauUE17c.js → dist-DeAdFM5_.js} +2 -2
- package/public/assets/{dist-BauUE17c.js.map → dist-DeAdFM5_.js.map} +1 -1
- package/public/assets/{dist-CROpCfON.js → dist-DeM72J6a.js} +2 -2
- package/public/assets/{dist-CROpCfON.js.map → dist-DeM72J6a.js.map} +1 -1
- package/public/assets/{dist-CW_EpVCQ.js → dist-DfQP9-VC.js} +2 -2
- package/public/assets/{dist-CW_EpVCQ.js.map → dist-DfQP9-VC.js.map} +1 -1
- package/public/assets/{es2015-t3pwjvKi.js → es2015-C3DnXBqu.js} +2 -2
- package/public/assets/{es2015-t3pwjvKi.js.map → es2015-C3DnXBqu.js.map} +1 -1
- package/public/assets/{formatted-body-impl-DhvR8Z-A.js → formatted-body-impl-D2F76gPY.js} +2 -2
- package/public/assets/{formatted-body-impl-DhvR8Z-A.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-VGRP16ua.js.map → index-DYmgI4s4.js.map} +1 -1
- package/public/assets/{input-08ItMIki.js → input-Cqqq0OFl.js} +2 -2
- package/public/assets/{input-08ItMIki.js.map → input-Cqqq0OFl.js.map} +1 -1
- package/public/assets/{insights-v9noub5E.js → insights-DylYp2q4.js} +2 -2
- package/public/assets/{insights-v9noub5E.js.map → insights-DylYp2q4.js.map} +1 -1
- package/public/assets/{integrations-BEa3ArUw.js → integrations-B9ViWoJy.js} +2 -2
- package/public/assets/{integrations-BEa3ArUw.js.map → integrations-B9ViWoJy.js.map} +1 -1
- package/public/assets/{lib-CMqRPJG-.js → lib-6L4m0E3M.js} +2 -2
- package/public/assets/{lib-CMqRPJG-.js.map → lib-6L4m0E3M.js.map} +1 -1
- package/public/assets/{lucide-react-BbMYCUlW.js → lucide-react-DI8kTavp.js} +2 -2
- package/public/assets/{lucide-react-BbMYCUlW.js.map → lucide-react-DI8kTavp.js.map} +1 -1
- package/public/assets/{maintenance-DyRfVkhp.js → maintenance-inyZGLys.js} +2 -2
- package/public/assets/{maintenance-DyRfVkhp.js.map → maintenance-inyZGLys.js.map} +1 -1
- package/public/assets/{managed-agents-BSdMoBgG.js → managed-agents-CEFvMPN-.js} +2 -2
- package/public/assets/{managed-agents-BSdMoBgG.js.map → managed-agents-CEFvMPN-.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DidsrMmn.js → markdown-preview-impl-CniyG4_g.js} +2 -2
- package/public/assets/{markdown-preview-impl-DidsrMmn.js.map → markdown-preview-impl-CniyG4_g.js.map} +1 -1
- package/public/assets/memory-DDYGuRzr.js +2 -0
- package/public/assets/{memory-DM8muGVW.js.map → memory-DDYGuRzr.js.map} +1 -1
- package/public/assets/{messages-BqjAkcBF.js → messages-BMFcsMMa.js} +2 -2
- package/public/assets/{messages-BqjAkcBF.js.map → messages-BMFcsMMa.js.map} +1 -1
- package/public/assets/{orchestrators-C_mjUDZg.js → orchestrators-D5nL4a52.js} +2 -2
- package/public/assets/{orchestrators-C_mjUDZg.js.map → orchestrators-D5nL4a52.js.map} +1 -1
- package/public/assets/{overview-Cpu2plX-.js → overview-DywDrPKE.js} +2 -2
- package/public/assets/{overview-Cpu2plX-.js.map → overview-DywDrPKE.js.map} +1 -1
- package/public/assets/{pairs--8_0A0lq.js → pairs-UvIyBk4w.js} +2 -2
- package/public/assets/{pairs--8_0A0lq.js.map → pairs-UvIyBk4w.js.map} +1 -1
- package/public/assets/{projects-nCww9PG1.js → projects-DXBjwPU5.js} +2 -2
- package/public/assets/{projects-nCww9PG1.js.map → projects-DXBjwPU5.js.map} +1 -1
- package/public/assets/{react-dom-HJwa4sZ-.js → react-dom-Ct8qXivQ.js} +2 -2
- package/public/assets/{react-dom-HJwa4sZ-.js.map → react-dom-Ct8qXivQ.js.map} +1 -1
- package/public/assets/{security-CwgoEvJT.js → security-Boe7MEIz.js} +2 -2
- package/public/assets/{security-CwgoEvJT.js.map → security-Boe7MEIz.js.map} +1 -1
- package/public/assets/{select-Bhx6zSZr.js → select-Dh5Q8rJ8.js} +2 -2
- package/public/assets/{select-Bhx6zSZr.js.map → select-Dh5Q8rJ8.js.map} +1 -1
- package/public/assets/{settings-BNQBnKXg.js → settings-Ch4jMDrE.js} +3 -3
- package/public/assets/{settings-BNQBnKXg.js.map → settings-Ch4jMDrE.js.map} +1 -1
- package/public/assets/{store-BB-YW24l.js → store-BRvMMH5T.js} +2 -2
- package/public/assets/{store-BB-YW24l.js.map → store-BRvMMH5T.js.map} +1 -1
- package/public/assets/{tasks-C5Kv18N7.js → tasks-DrW8R52c.js} +2 -2
- package/public/assets/{tasks-C5Kv18N7.js.map → tasks-DrW8R52c.js.map} +1 -1
- package/public/assets/{teams-D_Ttv7qD.js → teams-CJqpAwmO.js} +2 -2
- package/public/assets/{teams-D_Ttv7qD.js.map → teams-CJqpAwmO.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-5y8v9jNx.js → terminal-viewer-impl-1_7v8GfU.js} +3 -3
- package/public/assets/{terminal-viewer-impl-5y8v9jNx.js.map → terminal-viewer-impl-1_7v8GfU.js.map} +1 -1
- package/public/assets/{use-keyboard-viewport-CEmOxaNm.js → use-keyboard-viewport-DxmCAbQi.js} +2 -2
- package/public/assets/{use-keyboard-viewport-CEmOxaNm.js.map → use-keyboard-viewport-DxmCAbQi.js.map} +1 -1
- package/public/assets/work-queue-BnIi7evb.js +2 -0
- package/public/assets/{work-queue-BB7bYLIK.js.map → work-queue-BnIi7evb.js.map} +1 -1
- package/public/assets/workspaces-u5j6mOlA.js +3 -0
- package/public/assets/{workspaces-CrtAeVnv.js.map → workspaces-u5j6mOlA.js.map} +1 -1
- package/public/index.html +21 -21
- package/runner/src/adapter.ts +7 -1
- package/runner/src/config.ts +4 -0
- package/src/db/message-reads.ts +8 -46
- package/src/db/messages.ts +208 -0
- package/src/db/notification-subscriptions.ts +68 -12
- package/src/index.ts +1 -1
- package/src/mcp/tools-spawn.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 +82 -9
- package/src/services/send-message.ts +5 -0
- package/src/services/spawn-agent.ts +13 -12
- package/src/spawn-command.ts +2 -0
- package/src/sse.ts +39 -2
- package/public/assets/MessageBubble-DPcJxTJj.js +0 -3
- package/public/assets/MessageBubble-DPcJxTJj.js.map +0 -1
- package/public/assets/branch-state-badge-C7H8HY22.js +0 -2
- package/public/assets/chat-DJqvtoFT.js +0 -2
- package/public/assets/chat-DJqvtoFT.js.map +0 -1
- package/public/assets/copy-button-DqAoeiPC.js +0 -2
- package/public/assets/index-Dh7NjR-l.css +0 -2
- package/public/assets/index-VGRP16ua.js +0 -22
- package/public/assets/memory-DM8muGVW.js +0 -2
- package/public/assets/work-queue-BB7bYLIK.js +0 -2
- package/public/assets/workspaces-CrtAeVnv.js +0 -3
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";
|
|
@@ -157,6 +158,7 @@ function inferMessageKind(input: SendMessageInput): Message["kind"] {
|
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
const REPLY_DUPLICATE_WINDOW_MS = 2 * 60 * 1000;
|
|
161
|
+
const LINEAGE_REPORT_DUPLICATE_WINDOW_MS = 2 * 60 * 1000;
|
|
160
162
|
|
|
161
163
|
function findRecentDuplicateReply(input: SendMessageInput, threadId: number | null, now: number, hasAttachments: boolean): Message | null {
|
|
162
164
|
if (!input.replyTo || threadId === null || hasAttachments) return null;
|
|
@@ -186,6 +188,209 @@ function findRecentDuplicateReply(input: SendMessageInput, threadId: number | nu
|
|
|
186
188
|
return row ? rowToMessage(row) : null;
|
|
187
189
|
}
|
|
188
190
|
|
|
191
|
+
function sessionMeta(input: Message | SendMessageInput): Record<string, unknown> | undefined {
|
|
192
|
+
return isRecord(input.payload?.session) ? input.payload.session : undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function responseCaptureMeta(input: Message | SendMessageInput): Record<string, unknown> | undefined {
|
|
196
|
+
return isRecord(input.payload?.responseCapture) ? input.payload.responseCapture : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isFinalSessionResponse(session: Record<string, unknown>): boolean {
|
|
200
|
+
return session.type === "response" && session.final === true;
|
|
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
|
+
|
|
211
|
+
function captureTurnId(capture: Record<string, unknown>): string | undefined {
|
|
212
|
+
return stringValue(capture.turnId);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function lineageResponseTurnId(message: Message, session: Record<string, unknown>): string {
|
|
216
|
+
return stringValue(session.turnId) ?? `message-${message.id}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function lineageResponseReportKeyForTurn(childId: string, turnId: string): string {
|
|
220
|
+
return `response-report:${childId}:${turnId}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function lineageResponseReportKey(message: Message, session: Record<string, unknown>): string {
|
|
224
|
+
return lineageResponseReportKeyForTurn(message.from, lineageResponseTurnId(message, session));
|
|
225
|
+
}
|
|
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
|
+
|
|
242
|
+
function findLineageReportByTurn(input: {
|
|
243
|
+
from: string;
|
|
244
|
+
to: string;
|
|
245
|
+
turnId: string;
|
|
246
|
+
since: number;
|
|
247
|
+
}): Message | null {
|
|
248
|
+
const lineageKey = lineageResponseReportKeyForTurn(input.from, input.turnId);
|
|
249
|
+
const row = getDb().query(`
|
|
250
|
+
${MSG_SELECT}
|
|
251
|
+
WHERE m.from_agent = ?
|
|
252
|
+
AND m.to_target = ?
|
|
253
|
+
AND m.kind = 'chat'
|
|
254
|
+
AND m.created_at >= ?
|
|
255
|
+
AND (
|
|
256
|
+
m.idempotency_key = ?
|
|
257
|
+
OR m.idempotency_key LIKE ?
|
|
258
|
+
OR (
|
|
259
|
+
json_extract(m.payload, '$.responseCapture.type') = 'response'
|
|
260
|
+
AND json_extract(m.payload, '$.responseCapture.turnId') = ?
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
ORDER BY m.id DESC
|
|
264
|
+
LIMIT 1
|
|
265
|
+
`).get(
|
|
266
|
+
input.from,
|
|
267
|
+
input.to,
|
|
268
|
+
input.since,
|
|
269
|
+
lineageKey,
|
|
270
|
+
`response-report:${input.from}:%:${input.turnId}`,
|
|
271
|
+
input.turnId,
|
|
272
|
+
) as any;
|
|
273
|
+
return row ? rowToMessage(row) : null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function findRecentLineageReport(input: SendMessageInput, now: number): Message | null {
|
|
277
|
+
if ((input.kind ?? "chat") !== "chat") return null;
|
|
278
|
+
const from = input.from;
|
|
279
|
+
const to = input.to;
|
|
280
|
+
if (!from || !to) return null;
|
|
281
|
+
const child = getAgent(from);
|
|
282
|
+
if (!child?.spawnedBy || child.spawnedBy !== to) return null;
|
|
283
|
+
const responseCapture = responseCaptureMeta(input);
|
|
284
|
+
const turnId = responseCapture ? captureTurnId(responseCapture) : undefined;
|
|
285
|
+
if (turnId) {
|
|
286
|
+
const duplicate = findLineageReportByTurn({
|
|
287
|
+
from,
|
|
288
|
+
to,
|
|
289
|
+
turnId,
|
|
290
|
+
since: now - LINEAGE_REPORT_DUPLICATE_WINDOW_MS,
|
|
291
|
+
});
|
|
292
|
+
if (duplicate) return duplicate;
|
|
293
|
+
}
|
|
294
|
+
if (responseCapture) return null;
|
|
295
|
+
const row = getDb().query(`
|
|
296
|
+
${MSG_SELECT}
|
|
297
|
+
WHERE m.from_agent = ?
|
|
298
|
+
AND m.to_target = ?
|
|
299
|
+
AND m.kind = 'chat'
|
|
300
|
+
AND m.body = ?
|
|
301
|
+
AND m.created_at >= ?
|
|
302
|
+
AND (
|
|
303
|
+
m.idempotency_key LIKE ?
|
|
304
|
+
OR json_extract(m.payload, '$.responseCapture.type') = 'response'
|
|
305
|
+
)
|
|
306
|
+
ORDER BY m.id DESC
|
|
307
|
+
LIMIT 1
|
|
308
|
+
`).get(from, to, input.body, now - LINEAGE_REPORT_DUPLICATE_WINDOW_MS, `response-report:${from}:%`) as any;
|
|
309
|
+
return row ? rowToMessage(row) : null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function findRecentExplicitLineageDuplicate(input: {
|
|
313
|
+
from: string;
|
|
314
|
+
to: string;
|
|
315
|
+
body: string;
|
|
316
|
+
since: number;
|
|
317
|
+
}): Message | null {
|
|
318
|
+
const row = getDb().query(`
|
|
319
|
+
${MSG_SELECT}
|
|
320
|
+
WHERE m.from_agent = ?
|
|
321
|
+
AND m.to_target = ?
|
|
322
|
+
AND m.kind = 'chat'
|
|
323
|
+
AND m.body = ?
|
|
324
|
+
AND m.created_at >= ?
|
|
325
|
+
ORDER BY m.id DESC
|
|
326
|
+
LIMIT 1
|
|
327
|
+
`).get(
|
|
328
|
+
input.from,
|
|
329
|
+
input.to,
|
|
330
|
+
input.body,
|
|
331
|
+
input.since,
|
|
332
|
+
) as any;
|
|
333
|
+
return row ? rowToMessage(row) : null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function promoteLineageCapturedResponse(message: Message): { message: Message; created: boolean } | null {
|
|
337
|
+
if (message.kind !== "session") return null;
|
|
338
|
+
const session = sessionMeta(message);
|
|
339
|
+
if (!session) return null;
|
|
340
|
+
const stream = childStreamForSession(session);
|
|
341
|
+
if (!stream) return null;
|
|
342
|
+
if (stream === "responses" && !isFinalSessionResponse(session)) return null;
|
|
343
|
+
const child = getAgent(message.from);
|
|
344
|
+
const parent = child?.spawnedBy;
|
|
345
|
+
if (!parent) return null;
|
|
346
|
+
if (stream !== "responses" && !childStreamSubscribed({ parentId: parent, child, stream })) return null;
|
|
347
|
+
|
|
348
|
+
const idempotencyKey = lineageStreamReportKey(message, session, stream);
|
|
349
|
+
const existing = findMessageByIdempotencyKey(message.from, idempotencyKey);
|
|
350
|
+
if (existing) return { message: existing, created: false };
|
|
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({
|
|
364
|
+
from: message.from,
|
|
365
|
+
to: parent,
|
|
366
|
+
body: message.body,
|
|
367
|
+
since: message.createdAt - LINEAGE_REPORT_DUPLICATE_WINDOW_MS,
|
|
368
|
+
});
|
|
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
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return sendMessageWithResult({
|
|
381
|
+
from: message.from,
|
|
382
|
+
to: parent,
|
|
383
|
+
kind: "chat",
|
|
384
|
+
body: message.body,
|
|
385
|
+
replyExpected: false,
|
|
386
|
+
idempotencyKey,
|
|
387
|
+
payload: stream === "responses"
|
|
388
|
+
? { responseCapture: { ...session } }
|
|
389
|
+
: { childStreamCapture: { stream, ...session } },
|
|
390
|
+
occurredAt: message.occurredAt ?? message.createdAt,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
189
394
|
// Event time may be queued-then-backfilled, so it can legitimately be older than the
|
|
190
395
|
// receive time — but it must be a sane epoch-ms value. Returns null (column stays NULL, so
|
|
191
396
|
// readers fall back to created_at) for absent/invalid values or anything more than a minute
|
|
@@ -206,6 +411,9 @@ export function sendMessageWithResult(input: SendMessageInput): { message: Messa
|
|
|
206
411
|
}
|
|
207
412
|
validateAttachmentRefs(attachmentRefs);
|
|
208
413
|
|
|
414
|
+
const lineageDuplicate = findRecentLineageReport(input, now);
|
|
415
|
+
if (lineageDuplicate) return { message: lineageDuplicate, created: false };
|
|
416
|
+
|
|
209
417
|
if (input.idempotencyKey) {
|
|
210
418
|
const existing = findMessageByIdempotencyKey(input.from, input.idempotencyKey);
|
|
211
419
|
if (existing) {
|
|
@@ -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";
|
package/src/mcp/tools-spawn.ts
CHANGED
|
@@ -30,7 +30,7 @@ export const SPAWN_TOOLS: ToolDefinition[] = [
|
|
|
30
30
|
effort: { type: "string", enum: VALID_EFFORTS },
|
|
31
31
|
approvalMode: { type: "string", enum: APPROVAL_MODES, description: "Permission posture for the worker. Defaults to YOUR OWN approval mode when omitted (a headless `guarded` worker wedges on the first approval prompt), so a coordinator running `open` spawns workers that can act autonomously. Pass an explicit value to narrow a child (e.g. `read-only` reviewer)." },
|
|
32
32
|
prompt: { type: "string", description: "Initial task/message delivered to the agent on launch — spawn and hand it its first instruction in one call (no separate follow-up message needed)." },
|
|
33
|
-
expectReply: { type: "boolean", description: "
|
|
33
|
+
expectReply: { type: "boolean", description: "Legacy compatibility flag. Spawned workers now report up automatically: each turn-final response is routed to the spawner by lineage, without a reply obligation or idle nag." },
|
|
34
34
|
systemPromptAppend: { type: "string" },
|
|
35
35
|
projectInstructions: { type: "object", description: "#409 — project instruction projection (default OFF). When set, relay composes a knowledge preamble from the spawn cwd's resolved project (own + inherited entries) and prepends it to systemPromptAppend, instead of relying on host CLAUDE.md. `policy: \"all\"` includes ingested host content; `\"vanilla\"` excludes it (manual+promoted only). Omit to keep current behavior.", properties: { policy: { type: "string", enum: PROJECT_INSTRUCTION_POLICY_NAMES } }, additionalProperties: false },
|
|
36
36
|
profile: { type: "string", description: `Agent profile name to apply (env, instructions, permissions, MCP/skills, spawn quota). Omit to use \`${DEFAULT_SPAWN_PROFILE_NAME}\`.` },
|
|
@@ -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. */
|