agent-relay-server 0.62.2 → 0.63.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +2 -2
  3. package/public/assets/{MessageBubble-DRrxfjyT.js → MessageBubble-Cl6ufPHw.js} +2 -2
  4. package/public/assets/MessageBubble-Cl6ufPHw.js.map +1 -0
  5. package/public/assets/{PlanGraphPanel-Dp2eYazz.js → PlanGraphPanel-BnfjmkQ8.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-Dp2eYazz.js.map → PlanGraphPanel-BnfjmkQ8.js.map} +1 -1
  7. package/public/assets/{activity--t0U2yzq.js → activity-aFxvGLtd.js} +2 -2
  8. package/public/assets/{activity--t0U2yzq.js.map → activity-aFxvGLtd.js.map} +1 -1
  9. package/public/assets/{agent-profiles-BMtJYzxn.js → agent-profiles-B4mleUDz.js} +2 -2
  10. package/public/assets/{agent-profiles-BMtJYzxn.js.map → agent-profiles-B4mleUDz.js.map} +1 -1
  11. package/public/assets/{agents-Ck9AePlc.js → agents-DW5djToQ.js} +2 -2
  12. package/public/assets/{agents-Ck9AePlc.js.map → agents-DW5djToQ.js.map} +1 -1
  13. package/public/assets/{analytics-CzfK387L.js → analytics-CRi6thtg.js} +2 -2
  14. package/public/assets/{analytics-CzfK387L.js.map → analytics-CRi6thtg.js.map} +1 -1
  15. package/public/assets/{automation-Cgw8vtE8.js → automation-Do4OpauI.js} +2 -2
  16. package/public/assets/{automation-Cgw8vtE8.js.map → automation-Do4OpauI.js.map} +1 -1
  17. package/public/assets/{branch-state-badge-DnEEPtOP.js → branch-state-badge-D3W3w7zz.js} +2 -2
  18. package/public/assets/{branch-state-badge-DnEEPtOP.js.map → branch-state-badge-D3W3w7zz.js.map} +1 -1
  19. package/public/assets/{channels-Dc3L2MQs.js → channels-BytdR9NS.js} +2 -2
  20. package/public/assets/{channels-Dc3L2MQs.js.map → channels-BytdR9NS.js.map} +1 -1
  21. package/public/assets/{chat-Fc6iJ7sa.js → chat-CvaQ2czL.js} +2 -2
  22. package/public/assets/{chat-Fc6iJ7sa.js.map → chat-CvaQ2czL.js.map} +1 -1
  23. package/public/assets/{connectors-Dc2ttGWs.js → connectors-BLQDp8Bc.js} +2 -2
  24. package/public/assets/{connectors-Dc2ttGWs.js.map → connectors-BLQDp8Bc.js.map} +1 -1
  25. package/public/assets/{display-DzNvOu1j.js → display-B06I5sqa.js} +3 -3
  26. package/public/assets/display-B06I5sqa.js.map +1 -0
  27. package/public/assets/{formatted-body-impl-Bg-gx7rG.js → formatted-body-impl-0Lf8e1Ic.js} +2 -2
  28. package/public/assets/{formatted-body-impl-Bg-gx7rG.js.map → formatted-body-impl-0Lf8e1Ic.js.map} +1 -1
  29. package/public/assets/{index-BeDncjN4.js → index-C3su8ezq.js} +6 -6
  30. package/public/assets/{index-BeDncjN4.js.map → index-C3su8ezq.js.map} +1 -1
  31. package/public/assets/{insights-c08aW-7n.js → insights-64uExH1E.js} +2 -2
  32. package/public/assets/{insights-c08aW-7n.js.map → insights-64uExH1E.js.map} +1 -1
  33. package/public/assets/{integrations-BwpgcqnF.js → integrations-HyC1EaIp.js} +2 -2
  34. package/public/assets/{integrations-BwpgcqnF.js.map → integrations-HyC1EaIp.js.map} +1 -1
  35. package/public/assets/{maintenance-CTKP0kWB.js → maintenance-EVBpKH07.js} +2 -2
  36. package/public/assets/{maintenance-CTKP0kWB.js.map → maintenance-EVBpKH07.js.map} +1 -1
  37. package/public/assets/{managed-agents-BofCxo8d.js → managed-agents-BLZPxGuw.js} +2 -2
  38. package/public/assets/{managed-agents-BofCxo8d.js.map → managed-agents-BLZPxGuw.js.map} +1 -1
  39. package/public/assets/{markdown-preview-impl-CQ-6BIb8.js → markdown-preview-impl-Bg6rCWYN.js} +2 -2
  40. package/public/assets/{markdown-preview-impl-CQ-6BIb8.js.map → markdown-preview-impl-Bg6rCWYN.js.map} +1 -1
  41. package/public/assets/{memory-DvfXjMoi.js → memory-Bjr8bgjj.js} +2 -2
  42. package/public/assets/{memory-DvfXjMoi.js.map → memory-Bjr8bgjj.js.map} +1 -1
  43. package/public/assets/{messages-C9VsPzZ_.js → messages-CRYw1F4z.js} +2 -2
  44. package/public/assets/{messages-C9VsPzZ_.js.map → messages-CRYw1F4z.js.map} +1 -1
  45. package/public/assets/{orchestrators-MffVueeE.js → orchestrators-D-co1vdp.js} +2 -2
  46. package/public/assets/{orchestrators-MffVueeE.js.map → orchestrators-D-co1vdp.js.map} +1 -1
  47. package/public/assets/{overview-BSVajefM.js → overview-Ddy5czri.js} +2 -2
  48. package/public/assets/{overview-BSVajefM.js.map → overview-Ddy5czri.js.map} +1 -1
  49. package/public/assets/{pairs-B5711-Cs.js → pairs-CFVM8uHL.js} +2 -2
  50. package/public/assets/{pairs-B5711-Cs.js.map → pairs-CFVM8uHL.js.map} +1 -1
  51. package/public/assets/{security-DkUwK9Vj.js → security-Df4_76bh.js} +2 -2
  52. package/public/assets/{security-DkUwK9Vj.js.map → security-Df4_76bh.js.map} +1 -1
  53. package/public/assets/{select-DrM2RRiT.js → select-Cq3BBnL9.js} +2 -2
  54. package/public/assets/{select-DrM2RRiT.js.map → select-Cq3BBnL9.js.map} +1 -1
  55. package/public/assets/{settings-DdKCb-vy.js → settings-DOLEYXZL.js} +2 -2
  56. package/public/assets/{settings-DdKCb-vy.js.map → settings-DOLEYXZL.js.map} +1 -1
  57. package/public/assets/{store-iTiheuTD.js → store-C1E8jmNE.js} +4 -4
  58. package/public/assets/store-C1E8jmNE.js.map +1 -0
  59. package/public/assets/{tasks-e1y1PFfO.js → tasks-BcsmPi9_.js} +2 -2
  60. package/public/assets/{tasks-e1y1PFfO.js.map → tasks-BcsmPi9_.js.map} +1 -1
  61. package/public/assets/{teams-BUcxoGC_.js → teams-BQrV3eKI.js} +2 -2
  62. package/public/assets/{teams-BUcxoGC_.js.map → teams-BQrV3eKI.js.map} +1 -1
  63. package/public/assets/{terminal-viewer-impl-CmZjQOB2.js → terminal-viewer-impl-Dle04qpH.js} +2 -2
  64. package/public/assets/{terminal-viewer-impl-CmZjQOB2.js.map → terminal-viewer-impl-Dle04qpH.js.map} +1 -1
  65. package/public/assets/{work-queue-CXBQx43n.js → work-queue-BLhTLg4o.js} +2 -2
  66. package/public/assets/{work-queue-CXBQx43n.js.map → work-queue-BLhTLg4o.js.map} +1 -1
  67. package/public/assets/{workspaces-D2EfZh9a.js → workspaces-4NCNTmRQ.js} +3 -3
  68. package/public/assets/{workspaces-D2EfZh9a.js.map → workspaces-4NCNTmRQ.js.map} +1 -1
  69. package/public/index.html +3 -3
  70. package/scripts/orchestrator-spawn-smoke.ts +87 -67
  71. package/src/agent-ref.ts +8 -6
  72. package/src/automations/constants.ts +12 -0
  73. package/src/automations/cron.ts +129 -0
  74. package/src/automations/crud.ts +84 -0
  75. package/src/automations/dispatch.ts +275 -0
  76. package/src/automations/index.ts +5 -0
  77. package/src/automations/reconcile.ts +245 -0
  78. package/src/automations/storage.ts +181 -0
  79. package/src/automations/types.ts +16 -0
  80. package/src/automations/validation.ts +140 -0
  81. package/src/automations.ts +1 -1045
  82. package/src/db/activity.ts +1 -1
  83. package/src/db/agent-predicates.ts +9 -0
  84. package/src/db/agent-search.ts +2 -2
  85. package/src/db/agents.ts +13 -41
  86. package/src/db/artifacts.ts +8 -8
  87. package/src/db/channel-identity.ts +38 -0
  88. package/src/db/channels.ts +37 -61
  89. package/src/db/continuation-archives.ts +4 -4
  90. package/src/db/delivery.ts +75 -30
  91. package/src/db/inbox.ts +1 -1
  92. package/src/db/integrations.ts +4 -4
  93. package/src/db/issue-lifecycle.ts +6 -6
  94. package/src/db/mappers.ts +1 -2
  95. package/src/db/merge-lease.ts +3 -3
  96. package/src/db/message-claims.ts +16 -0
  97. package/src/db/message-queries.ts +8 -0
  98. package/src/db/message-reads.ts +157 -18
  99. package/src/db/messages.ts +23 -48
  100. package/src/db/notification-subscriptions.ts +2 -2
  101. package/src/db/orchestrators.ts +4 -4
  102. package/src/db/pair-maintenance.ts +9 -0
  103. package/src/db/pairs.ts +10 -18
  104. package/src/db/plans.ts +1 -1
  105. package/src/db/policy-runtime.ts +14 -0
  106. package/src/db/project-entries.ts +1 -1
  107. package/src/db/projects.ts +2 -2
  108. package/src/db/shared.ts +7 -0
  109. package/src/db/stats.ts +2 -2
  110. package/src/db/task-core.ts +18 -0
  111. package/src/db/task-maintenance.ts +24 -0
  112. package/src/db/tasks.ts +5 -17
  113. package/src/db/teams.ts +3 -3
  114. package/src/db/workspaces.ts +6 -6
  115. package/src/issue-tracker/github.ts +3 -3
  116. package/src/maintenance/constants.ts +35 -0
  117. package/src/maintenance/events.ts +32 -0
  118. package/src/maintenance/index.ts +7 -0
  119. package/src/maintenance/jobs.ts +340 -0
  120. package/src/maintenance/orphaned-sessions.ts +86 -0
  121. package/src/maintenance/scheduler.ts +231 -0
  122. package/src/maintenance/teams.ts +65 -0
  123. package/src/maintenance/types.ts +31 -0
  124. package/src/maintenance/workspaces.ts +251 -0
  125. package/src/maintenance.ts +1 -1099
  126. package/src/mcp/identity.ts +131 -0
  127. package/src/mcp/index.ts +208 -0
  128. package/src/mcp/tools-messaging.ts +459 -0
  129. package/src/mcp/tools-spawn.ts +145 -0
  130. package/src/mcp/tools-workspace.ts +227 -0
  131. package/src/mcp/types.ts +24 -0
  132. package/src/mcp/validation.ts +104 -0
  133. package/src/mcp.ts +1 -1319
  134. package/src/notification-router.ts +2 -2
  135. package/src/notification-rules.ts +2 -2
  136. package/src/project-ingest.ts +3 -3
  137. package/src/project-instructions.ts +1 -1
  138. package/src/provider-config-store.ts +3 -3
  139. package/src/provider-state.ts +1 -1
  140. package/src/reviewer-pipeline.ts +1 -1
  141. package/src/self-resume.ts +3 -7
  142. package/src/services/agent-action.ts +2 -2
  143. package/src/services/issue-activity.ts +0 -4
  144. package/src/services/issue-associations.ts +2 -2
  145. package/src/services/issue-lifecycle.ts +7 -7
  146. package/src/services/managed-running.ts +1 -1
  147. package/src/services/parity-harness.ts +3 -3
  148. package/src/services/plan-graph.ts +3 -3
  149. package/src/services/send-message.ts +16 -12
  150. package/src/services/shutdown-agent.ts +2 -2
  151. package/src/services/team-templates.ts +2 -2
  152. package/src/services/team.ts +2 -2
  153. package/src/settings/modules.ts +1 -1
  154. package/src/settings/registry.ts +3 -3
  155. package/src/spawn-command.ts +1 -1
  156. package/src/sse.ts +0 -4
  157. package/src/workspace-actions.ts +1 -1
  158. package/src/workspace-probe.ts +3 -7
  159. package/public/assets/MessageBubble-DRrxfjyT.js.map +0 -1
  160. package/public/assets/display-DzNvOu1j.js.map +0 -1
  161. package/public/assets/store-iTiheuTD.js.map +0 -1
@@ -98,7 +98,7 @@ export interface RecordIssueActivityInput {
98
98
  metadata?: Record<string, unknown>;
99
99
  }
100
100
 
101
- export interface IssueActivityAgentSummary {
101
+ interface IssueActivityAgentSummary {
102
102
  agentId: string;
103
103
  agentName?: string;
104
104
  agentKind?: string;
@@ -0,0 +1,9 @@
1
+ import type { AgentCard } from "../types";
2
+
3
+ export function isDeliveryAgent(agent: AgentCard): boolean {
4
+ return agent.status !== "offline" &&
5
+ agent.id !== "user" &&
6
+ agent.id !== "system" &&
7
+ agent.kind !== "channel" &&
8
+ agent.meta?.kind !== "channel";
9
+ }
@@ -105,9 +105,9 @@ export interface AgentSearchSort {
105
105
  limit?: number;
106
106
  }
107
107
 
108
- export const AGENT_SEARCH_MAX_LIMIT = 200;
108
+ const AGENT_SEARCH_MAX_LIMIT = 200;
109
109
 
110
- export function searchValues(value: string | string[] | undefined): string[] {
110
+ function searchValues(value: string | string[] | undefined): string[] {
111
111
  if (value == null) return [];
112
112
  return (Array.isArray(value) ? value : [value]).filter((v): v is string => typeof v === "string" && v.length > 0);
113
113
  }
package/src/db/agents.ts CHANGED
@@ -14,13 +14,15 @@ import {
14
14
  } from "../contracts";
15
15
  import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
16
16
  import { matchAgents } from "../agent-ref";
17
- import { evaluatePoolBindings, expectedChannelAgentId, upsertChannelForAgent } from "./channels.ts";
17
+ import { expectedChannelAgentId } from "./channel-identity.ts";
18
+ import { evaluatePoolBindings, upsertChannelForAgent } from "./channels.ts";
18
19
  import { seedContinuationObjectiveFromSpawnPrompt } from "./continuations.ts";
19
20
  import { messageTaskClosed } from "./claim-sql.ts";
20
21
  import { ValidationError, getDb } from "./connection.ts";
21
22
  import { rowToAgent, rowToContextSnapshot } from "./mappers.ts";
22
- import { closeOpenPairsForAgent } from "./pairs.ts";
23
- import { TASK_SELECT, insertTaskEvent } from "./tasks.ts";
23
+ import { closeOpenPairsForAgent } from "./pair-maintenance.ts";
24
+ import { parseStringArray } from "./shared.ts";
25
+ import { settleSingleTargetOnDemandTasks } from "./task-maintenance.ts";
24
26
  import { electWorkspaceStewards, electWorkspaceStewardsForAgent } from "./workspaces.ts";
25
27
  import { DEFAULT_SELF_RESUME_CONFIG } from "../self-resume-config";
26
28
  import type { TokenRevokeReason } from "../security";
@@ -89,17 +91,13 @@ import type {
89
91
 
90
92
  const CONTEXT_SNAPSHOT_DEBOUNCE_MS = 60_000;
91
93
 
92
- export function parseStringArray(raw: string): string[] {
93
- const parsed = parseJson<unknown>(raw, []);
94
- if (!Array.isArray(parsed)) return [];
95
- return parsed.filter((value): value is string => typeof value === "string");
96
- }
94
+ export { parseStringArray } from "./shared.ts";
97
95
 
98
96
  export function normalizeTags(tags: string[] | undefined): string[] {
99
97
  return [...new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))];
100
98
  }
101
99
 
102
- export function inferAgentKind(input: Pick<RegisterAgentInput, "id" | "kind" | "tags" | "capabilities" | "meta">): AgentKind {
100
+ function inferAgentKind(input: Pick<RegisterAgentInput, "id" | "kind" | "tags" | "capabilities" | "meta">): AgentKind {
103
101
  if (input.kind) return input.kind;
104
102
  if (input.id === "user") return "user";
105
103
  if (input.id === "system") return "system";
@@ -110,7 +108,7 @@ export function inferAgentKind(input: Pick<RegisterAgentInput, "id" | "kind" | "
110
108
  return "provider";
111
109
  }
112
110
 
113
- export function inferProviderTag(input: RegisterAgentInput): "claude" | "codex" | undefined {
111
+ function inferProviderTag(input: RegisterAgentInput): "claude" | "codex" | undefined {
114
112
  if (inferAgentKind(input) !== "provider") return undefined;
115
113
  const meta = input.meta ?? {};
116
114
  const values = [
@@ -134,7 +132,7 @@ export function inferProviderTag(input: RegisterAgentInput): "claude" | "codex"
134
132
  return undefined;
135
133
  }
136
134
 
137
- export function tagsWithProvider(input: RegisterAgentInput): string[] {
135
+ function tagsWithProvider(input: RegisterAgentInput): string[] {
138
136
  const tags = normalizeTags(input.tags);
139
137
  const provider = inferProviderTag(input);
140
138
  if (!provider || tags.includes(provider)) return tags;
@@ -392,14 +390,14 @@ export function listContextSnapshots(agentId: string, options: { since?: number;
392
390
  return (getDb().query(sql).all(...params) as any[]).map(rowToContextSnapshot);
393
391
  }
394
392
 
395
- export function pruneContextSnapshots(agentId?: string, olderThan = Date.now() - DAY_MS): number {
393
+ function pruneContextSnapshots(agentId?: string, olderThan = Date.now() - DAY_MS): number {
396
394
  const result = agentId
397
395
  ? getDb().query("DELETE FROM context_snapshots WHERE agent_id = ? AND captured_at < ?").run(agentId, olderThan)
398
396
  : getDb().query("DELETE FROM context_snapshots WHERE captured_at < ?").run(olderThan);
399
397
  return result.changes;
400
398
  }
401
399
 
402
- export function recordContextSnapshot(agentId: string, context: ContextState, now: number): void {
400
+ function recordContextSnapshot(agentId: string, context: ContextState, now: number): void {
403
401
  const latest = getDb()
404
402
  .query("SELECT captured_at FROM context_snapshots WHERE agent_id = ? ORDER BY captured_at DESC LIMIT 1")
405
403
  .get(agentId) as { captured_at: number } | undefined;
@@ -464,33 +462,7 @@ export function reapStaleAgents(ttlMs: number = STALE_TTL_MS): string[] {
464
462
  return rows.map((r: any) => r.id);
465
463
  }
466
464
 
467
- // On-demand automation tasks (targetMode=on_demand_agent) are bound to a single
468
- // ephemeral agent spawned just for that task. When that agent's claim is released —
469
- // clean shutdown, prune, lease expiry, or orphan-grace — the task must NOT return to
470
- // the claimable pool: its target is a unique `label:automation-…` that no other agent
471
- // will ever match, so a re-opened task lingers forever as an orphaned claim. Resolve
472
- // those as done instead; automation reconcile then settles the run as succeeded.
473
- // Run this BEFORE the generic re-open UPDATE at each release site, with the same WHERE
474
- // condition: it flips matching single-target tasks to 'done', which the subsequent
475
- // re-open (gated on status IN claimed/in_progress/blocked/orphaned) then skips.
476
- export function settleSingleTargetOnDemandTasks(condition: string, params: any[], now: number, reason: string): void {
477
- const selectSql = `${TASK_SELECT} WHERE (${condition}) AND json_extract(metadata, '$.targetMode') = 'on_demand_agent' AND status IN ('claimed', 'in_progress', 'blocked', 'orphaned')`;
478
- const rows = getDb().query(selectSql).all(...params) as any[];
479
- if (rows.length === 0) return;
480
- getDb().query(
481
- `UPDATE tasks SET status = 'done', claim_expires_at = NULL, updated_at = ?, last_seen_at = ? WHERE (${condition}) AND json_extract(metadata, '$.targetMode') = 'on_demand_agent' AND status IN ('claimed', 'in_progress', 'blocked', 'orphaned')`
482
- ).run(now, now, ...params);
483
- for (const row of rows) {
484
- insertTaskEvent(row.id, {
485
- source: "agent-relay",
486
- type: "task.auto-completed",
487
- severity: row.severity,
488
- title: "On-demand task auto-resolved",
489
- body: `On-demand agent ${row.claimed_by ?? "(unknown)"} exited (${reason}); task resolved so it does not orphan`,
490
- metadata: { agentId: row.claimed_by, reason, completedBy: "relay" },
491
- }, now);
492
- }
493
- }
465
+ export { settleSingleTargetOnDemandTasks } from "./task-maintenance.ts";
494
466
 
495
467
  export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
496
468
  const cutoff = Date.now() - maxOfflineMs;
@@ -578,7 +550,7 @@ export function revokeRuntimeTokensForAgent(agentId: string, now = Date.now(), r
578
550
  return rows.map((item) => item.jti);
579
551
  }
580
552
 
581
- export function runtimeTokenJtisFromMeta(meta: Record<string, unknown>): string[] {
553
+ function runtimeTokenJtisFromMeta(meta: Record<string, unknown>): string[] {
582
554
  const jtis = new Set<string>();
583
555
  const auth = isRecord(meta.auth) ? meta.auth : undefined;
584
556
  const direct = typeof auth?.jti === "string" ? auth.jti : typeof meta.runtimeTokenJti === "string" ? meta.runtimeTokenJti : undefined;
@@ -82,28 +82,28 @@ import type {
82
82
  // --- Artifacts ---
83
83
 
84
84
  export const VALID_ARTIFACT_KINDS = new Set<ArtifactKind>(["image", "audio", "video", "document", "archive", "other"]);
85
- export const VALID_ARTIFACT_VISIBILITIES = new Set<ArtifactVisibility>(["private", "project", "org"]);
85
+ const VALID_ARTIFACT_VISIBILITIES = new Set<ArtifactVisibility>(["private", "project", "org"]);
86
86
  export const VALID_ARTIFACT_SENSITIVITIES = new Set<ArtifactSensitivity>(["public", "normal", "sensitive", "secret"]);
87
- export const VALID_ARTIFACT_LINK_ENTITY_TYPES = new Set<ArtifactLink["entityType"]>(["message", "task", "recipeRun", "recipeStep", "channelEvent"]);
87
+ const VALID_ARTIFACT_LINK_ENTITY_TYPES = new Set<ArtifactLink["entityType"]>(["message", "task", "recipeRun", "recipeStep", "channelEvent"]);
88
88
  export const VALID_ARTIFACT_ROLES = new Set<NonNullable<ArtifactLink["role"]>>(["media", "patch", "report", "log", "output", "input"]);
89
89
 
90
90
  export function artifactId(): string {
91
91
  return `art_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
92
92
  }
93
93
 
94
- export function artifactLinkId(): string {
94
+ function artifactLinkId(): string {
95
95
  return `alink_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
96
96
  }
97
97
 
98
- export function normalizeArtifactKind(kind: unknown): ArtifactKind {
98
+ function normalizeArtifactKind(kind: unknown): ArtifactKind {
99
99
  return typeof kind === "string" && VALID_ARTIFACT_KINDS.has(kind as ArtifactKind) ? kind as ArtifactKind : "other";
100
100
  }
101
101
 
102
- export function normalizeArtifactVisibility(visibility: unknown): ArtifactVisibility {
102
+ function normalizeArtifactVisibility(visibility: unknown): ArtifactVisibility {
103
103
  return typeof visibility === "string" && VALID_ARTIFACT_VISIBILITIES.has(visibility as ArtifactVisibility) ? visibility as ArtifactVisibility : "project";
104
104
  }
105
105
 
106
- export function normalizeArtifactSensitivity(sensitivity: unknown): ArtifactSensitivity {
106
+ function normalizeArtifactSensitivity(sensitivity: unknown): ArtifactSensitivity {
107
107
  return typeof sensitivity === "string" && VALID_ARTIFACT_SENSITIVITIES.has(sensitivity as ArtifactSensitivity) ? sensitivity as ArtifactSensitivity : "normal";
108
108
  }
109
109
 
@@ -212,7 +212,7 @@ export function deleteArtifact(id: string): boolean {
212
212
  })();
213
213
  }
214
214
 
215
- export function listArtifactLinks(artifactId: string): ArtifactLink[] {
215
+ function listArtifactLinks(artifactId: string): ArtifactLink[] {
216
216
  return (getDb().query("SELECT * FROM artifact_links WHERE artifact_id = ? ORDER BY created_at ASC").all(artifactId) as any[]).map(rowToArtifactLink);
217
217
  }
218
218
 
@@ -271,7 +271,7 @@ export function artifactBlobReferenceCount(digest: string): number {
271
271
  return row.count ?? 0;
272
272
  }
273
273
 
274
- export function unreferencedArtifactBlobs(): ArtifactBlob[] {
274
+ function unreferencedArtifactBlobs(): ArtifactBlob[] {
275
275
  return (getDb().query(`
276
276
  SELECT b.*
277
277
  FROM artifact_blobs b
@@ -0,0 +1,38 @@
1
+ import { stringValue } from "agent-relay-sdk";
2
+ import type { AgentCard, RegisterAgentInput } from "../types";
3
+
4
+ export function channelProviderForAgent(agent: AgentCard): string {
5
+ return stringValue(agent.meta?.provider) ??
6
+ stringValue(agent.meta?.channelType) ??
7
+ stringValue(agent.meta?.transport) ??
8
+ agent.tags.find((tag) => tag.startsWith("channel:"))?.slice("channel:".length) ??
9
+ "custom";
10
+ }
11
+
12
+ function channelProviderForInput(input: Pick<RegisterAgentInput, "tags" | "meta">): string {
13
+ return stringValue(input.meta?.provider) ??
14
+ stringValue(input.meta?.channelType) ??
15
+ stringValue(input.meta?.transport) ??
16
+ (input.tags ?? []).find((tag) => tag.startsWith("channel:"))?.slice("channel:".length) ??
17
+ "custom";
18
+ }
19
+
20
+ export function channelAccountIdForAgent(agent: AgentCard, provider: string): string {
21
+ const accountId = stringValue(agent.meta?.accountId);
22
+ if (accountId) return accountId;
23
+ const prefix = `${provider}:`;
24
+ return agent.id.startsWith(prefix) ? agent.id.slice(prefix.length) : agent.id;
25
+ }
26
+
27
+ function channelAccountIdForInput(input: Pick<RegisterAgentInput, "id" | "meta">, provider: string): string {
28
+ const accountId = stringValue(input.meta?.accountId);
29
+ if (accountId) return accountId;
30
+ const prefix = `${provider}:`;
31
+ return input.id.startsWith(prefix) ? input.id.slice(prefix.length) : input.id;
32
+ }
33
+
34
+ export function expectedChannelAgentId(input: Pick<RegisterAgentInput, "id" | "tags" | "meta">): string {
35
+ const provider = channelProviderForInput(input);
36
+ const accountId = channelAccountIdForInput(input, provider);
37
+ return `${provider}:${accountId}`;
38
+ }
@@ -15,10 +15,12 @@ import {
15
15
  } from "../contracts";
16
16
  import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
17
17
  import { matchAgents } from "../agent-ref";
18
- import { getAgent, listAgents, parseStringArray } from "./agents.ts";
18
+ import { isDeliveryAgent } from "./agent-predicates.ts";
19
+ import { channelAccountIdForAgent, channelProviderForAgent, expectedChannelAgentId } from "./channel-identity.ts";
19
20
  import { ValidationError, getDb } from "./connection.ts";
20
- import { isDeliveryAgent } from "./delivery.ts";
21
- import { runningAgentForPolicy } from "./messages.ts";
21
+ import { rowToAgent } from "./mappers.ts";
22
+ import { runningAgentForPolicy } from "./policy-runtime.ts";
23
+ import { parseStringArray } from "./shared.ts";
22
24
  import type {
23
25
  AgentCard,
24
26
  ActivityEvent,
@@ -82,7 +84,9 @@ import type {
82
84
  WorkspaceStatus,
83
85
  } from "../types";
84
86
 
85
- export function rowToChannelBinding(row: any): ChannelBinding {
87
+ export { channelProviderForAgent, expectedChannelAgentId } from "./channel-identity.ts";
88
+
89
+ function rowToChannelBinding(row: any): ChannelBinding {
86
90
  const target = row.target_type === "broadcast"
87
91
  ? { type: "broadcast" } as ChannelRouteTarget
88
92
  : { type: row.target_type, id: row.target_id } as ChannelRouteTarget;
@@ -105,7 +109,7 @@ export function rowToChannelBinding(row: any): ChannelBinding {
105
109
  return binding;
106
110
  }
107
111
 
108
- export function bindingTargetToLegacyTarget(target: ChannelRouteTarget): string {
112
+ function bindingTargetToLegacyTarget(target: ChannelRouteTarget): string {
109
113
  if (target.type === "agent") return target.id;
110
114
  if (target.type === "label") return `label:${target.id}`;
111
115
  if (target.type === "team") return `team:${target.id}`;
@@ -118,7 +122,7 @@ export function bindingTargetToLegacyTarget(target: ChannelRouteTarget): string
118
122
  return "";
119
123
  }
120
124
 
121
- export function configuredChannelsForAgent(agent: AgentCard): string[] {
125
+ function configuredChannelsForAgent(agent: AgentCard): string[] {
122
126
  const channels = agent.meta?.channels;
123
127
  if (!Array.isArray(channels)) return [];
124
128
  return channels
@@ -127,7 +131,7 @@ export function configuredChannelsForAgent(agent: AgentCard): string[] {
127
131
  .filter((item) => item.length > 0);
128
132
  }
129
133
 
130
- export function channelEntryMatches(channelId: string, entry: string): boolean {
134
+ function channelEntryMatches(channelId: string, entry: string): boolean {
131
135
  const normalized = entry.trim();
132
136
  if (!normalized) return false;
133
137
  if (normalized === channelId) return true;
@@ -135,27 +139,36 @@ export function channelEntryMatches(channelId: string, entry: string): boolean {
135
139
  return channelId.startsWith(`${normalized}:`);
136
140
  }
137
141
 
138
- export function agentCanServeChannel(agent: AgentCard, channelId?: string): boolean {
142
+ function agentCanServeChannel(agent: AgentCard, channelId?: string): boolean {
139
143
  if (!channelId) return true;
140
144
  const channels = configuredChannelsForAgent(agent);
141
145
  if (channels.length === 0) return true;
142
146
  return channels.some((entry) => channelEntryMatches(channelId, entry));
143
147
  }
144
148
 
145
- export function channelTargetMatches(target: ChannelRouteTarget, channelId?: string): AgentCard[] {
146
- const candidates = listAgents().filter((agent) => (
149
+ function getChannelAgent(id: string): AgentCard | null {
150
+ const row = getDb().query("SELECT * FROM agents WHERE id = ?").get(id) as any;
151
+ return row ? rowToAgent(row) : null;
152
+ }
153
+
154
+ function listChannelAgents(): AgentCard[] {
155
+ return (getDb().query("SELECT * FROM agents ORDER BY last_seen DESC").all() as any[]).map(rowToAgent);
156
+ }
157
+
158
+ function channelTargetMatches(target: ChannelRouteTarget, channelId?: string): AgentCard[] {
159
+ const candidates = listChannelAgents().filter((agent) => (
147
160
  agent.id !== "user" &&
148
161
  agent.id !== "system" &&
149
162
  agent.kind !== "channel" &&
150
163
  agent.meta?.kind !== "channel"
151
164
  ));
152
165
  if (target.type === "agent" || target.type === "orchestrator") {
153
- const agent = getAgent(target.id);
166
+ const agent = getChannelAgent(target.id);
154
167
  return agent ? [agent] : [];
155
168
  }
156
169
  if (target.type === "policy") {
157
170
  const agentId = runningAgentForPolicy(target.id);
158
- const agent = agentId ? getAgent(agentId) : null;
171
+ const agent = agentId ? getChannelAgent(agentId) : null;
159
172
  return agent ? [agent] : [];
160
173
  }
161
174
  const channelCandidates = candidates.filter((agent) => agentCanServeChannel(agent, channelId));
@@ -168,15 +181,15 @@ export function channelTargetMatches(target: ChannelRouteTarget, channelId?: str
168
181
  return [];
169
182
  }
170
183
 
171
- export function poolSelectorMatches(selector: string, candidates: AgentCard[]): AgentCard[] {
184
+ function poolSelectorMatches(selector: string, candidates: AgentCard[]): AgentCard[] {
172
185
  if (selector.startsWith("label:")) return candidates.filter((a) => a.label === selector.slice("label:".length));
173
186
  if (selector.startsWith("tag:")) return candidates.filter((a) => a.tags.includes(selector.slice("tag:".length)));
174
187
  if (selector.startsWith("cap:")) return candidates.filter((a) => a.capabilities.includes(selector.slice("cap:".length)));
175
- const agent = getAgent(selector);
188
+ const agent = getChannelAgent(selector);
176
189
  return agent ? [agent] : [];
177
190
  }
178
191
 
179
- export function channelTargetMatchSnapshot(agent: AgentCard): ChannelTargetHealth["matches"][number] {
192
+ function channelTargetMatchSnapshot(agent: AgentCard): ChannelTargetHealth["matches"][number] {
180
193
  return {
181
194
  id: agent.id,
182
195
  name: agent.name,
@@ -189,15 +202,15 @@ export function channelTargetMatchSnapshot(agent: AgentCard): ChannelTargetHealt
189
202
  };
190
203
  }
191
204
 
192
- export function isHealthyChannelTarget(agent: AgentCard, now: number): boolean {
205
+ function isHealthyChannelTarget(agent: AgentCard, now: number): boolean {
193
206
  return isDeliveryAgent(agent) && agent.ready && agent.lastSeen > now - STALE_TTL_MS;
194
207
  }
195
208
 
196
- export function describeTarget(target: ChannelRouteTarget): string {
209
+ function describeTarget(target: ChannelRouteTarget): string {
197
210
  return bindingTargetToLegacyTarget(target);
198
211
  }
199
212
 
200
- export function channelTargetHealth(binding: ChannelBinding, now: number = Date.now()): ChannelTargetHealth {
213
+ function channelTargetHealth(binding: ChannelBinding, now: number = Date.now()): ChannelTargetHealth {
201
214
  const target = binding.target;
202
215
  const targetLabel = describeTarget(target);
203
216
  const matches = channelTargetMatches(target, binding.channelId);
@@ -209,7 +222,7 @@ export function channelTargetHealth(binding: ChannelBinding, now: number = Date.
209
222
  if (matches.length === 0) return { status: "error", detail: `Pool ${targetLabel}: no eligible agents`, target, matches: [] };
210
223
  return { status: "error", detail: `Pool ${targetLabel}: ${healthyEligibles.length} eligible but slot unclaimed`, target, matches: snapshots };
211
224
  }
212
- const holder = getAgent(binding.poolAgentId);
225
+ const holder = getChannelAgent(binding.poolAgentId);
213
226
  if (!holder || holder.status === "offline") {
214
227
  return { status: "error", detail: `Pool ${targetLabel}: holder ${binding.poolAgentId} is offline`, target, matches: snapshots };
215
228
  }
@@ -271,7 +284,7 @@ export function channelTargetHealth(binding: ChannelBinding, now: number = Date.
271
284
  };
272
285
  }
273
286
 
274
- export function rowToChannelSummary(row: any): ChannelSummary {
287
+ function rowToChannelSummary(row: any): ChannelSummary {
275
288
  const binding = row.binding_id ? rowToChannelBinding({
276
289
  id: row.binding_id,
277
290
  channel_id: row.id,
@@ -309,44 +322,7 @@ export function rowToChannelSummary(row: any): ChannelSummary {
309
322
  };
310
323
  }
311
324
 
312
-
313
- export function channelProviderForAgent(agent: AgentCard): string {
314
- return stringValue(agent.meta?.provider) ??
315
- stringValue(agent.meta?.channelType) ??
316
- stringValue(agent.meta?.transport) ??
317
- agent.tags.find((tag) => tag.startsWith("channel:"))?.slice("channel:".length) ??
318
- "custom";
319
- }
320
-
321
- export function channelProviderForInput(input: Pick<RegisterAgentInput, "tags" | "meta">): string {
322
- return stringValue(input.meta?.provider) ??
323
- stringValue(input.meta?.channelType) ??
324
- stringValue(input.meta?.transport) ??
325
- (input.tags ?? []).find((tag) => tag.startsWith("channel:"))?.slice("channel:".length) ??
326
- "custom";
327
- }
328
-
329
- export function channelAccountIdForAgent(agent: AgentCard, provider: string): string {
330
- const accountId = stringValue(agent.meta?.accountId);
331
- if (accountId) return accountId;
332
- const prefix = `${provider}:`;
333
- return agent.id.startsWith(prefix) ? agent.id.slice(prefix.length) : agent.id;
334
- }
335
-
336
- export function channelAccountIdForInput(input: Pick<RegisterAgentInput, "id" | "meta">, provider: string): string {
337
- const accountId = stringValue(input.meta?.accountId);
338
- if (accountId) return accountId;
339
- const prefix = `${provider}:`;
340
- return input.id.startsWith(prefix) ? input.id.slice(prefix.length) : input.id;
341
- }
342
-
343
- export function expectedChannelAgentId(input: Pick<RegisterAgentInput, "id" | "tags" | "meta">): string {
344
- const provider = channelProviderForInput(input);
345
- const accountId = channelAccountIdForInput(input, provider);
346
- return `${provider}:${accountId}`;
347
- }
348
-
349
- export function channelDirectionForAgent(agent: AgentCard): ChannelSummary["direction"] {
325
+ function channelDirectionForAgent(agent: AgentCard): ChannelSummary["direction"] {
350
326
  const direction = stringValue(agent.meta?.direction);
351
327
  return direction === "inbound" || direction === "outbound" || direction === "bidirectional" ? direction : "bidirectional";
352
328
  }
@@ -497,7 +473,7 @@ export function upsertChannelBinding(input: {
497
473
  return rowToChannelBinding(row);
498
474
  }
499
475
 
500
- export interface PoolBindingChange {
476
+ interface PoolBindingChange {
501
477
  bindingId: string;
502
478
  channelId: string;
503
479
  previousAgentId: string | null;
@@ -517,7 +493,7 @@ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChang
517
493
 
518
494
  let holderValid = false;
519
495
  if (currentAgentId) {
520
- const holder = getAgent(currentAgentId);
496
+ const holder = getChannelAgent(currentAgentId);
521
497
  if (holder && holder.status !== "offline" && holder.ready && holder.lastSeen > now - STALE_TTL_MS && agentCanServeChannel(holder, channelId)) {
522
498
  if (currentEpoch === null || holder.epoch === currentEpoch) {
523
499
  getDb().query("UPDATE channel_bindings SET pool_claim_expires_at = ? WHERE id = ?")
@@ -534,7 +510,7 @@ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChang
534
510
  }
535
511
 
536
512
  if (!holderValid) {
537
- const candidates = listAgents().filter((a) =>
513
+ const candidates = listChannelAgents().filter((a) =>
538
514
  a.id !== "user" && a.id !== "system" && a.kind !== "channel" && a.meta?.kind !== "channel"
539
515
  );
540
516
  const eligible = poolSelectorMatches(selector, candidates)
@@ -2,7 +2,7 @@ import { MAX_BODY_BYTES } from "../config";
2
2
  import { getContinuationEnvelope } from "./continuations.ts";
3
3
  import { getDb, ValidationError } from "./connection.ts";
4
4
 
5
- export interface ContinuationArchive {
5
+ interface ContinuationArchive {
6
6
  id: number;
7
7
  ref: string;
8
8
  agentId: string;
@@ -11,7 +11,7 @@ export interface ContinuationArchive {
11
11
  createdAt: number;
12
12
  }
13
13
 
14
- export interface ContinuationArchiveSearchResult extends ContinuationArchive {
14
+ interface ContinuationArchiveSearchResult extends ContinuationArchive {
15
15
  score: number | null;
16
16
  searchMode: "fts5" | "like";
17
17
  }
@@ -80,7 +80,7 @@ export function archiveContinuationSegment(input: {
80
80
  return getContinuationArchive(Number(info.lastInsertRowid))!;
81
81
  }
82
82
 
83
- export function getContinuationArchive(id: number): ContinuationArchive | null {
83
+ function getContinuationArchive(id: number): ContinuationArchive | null {
84
84
  const row = getDb().query("SELECT * FROM continuation_archives WHERE id = ?").get(id) as ContinuationArchiveRow | undefined;
85
85
  return row ? rowToArchive(row) : null;
86
86
  }
@@ -115,7 +115,7 @@ export function searchContinuationArchives(input: {
115
115
  return searchContinuationArchivesLike(agentId, query, limit);
116
116
  }
117
117
 
118
- export function continuationArchiveSearchMode(): "fts5" | "like" {
118
+ function continuationArchiveSearchMode(): "fts5" | "like" {
119
119
  if (process.env.AGENT_RELAY_DISABLE_CONTINUATION_ARCHIVE_FTS === "1") return "like";
120
120
  const row = getDb().query(`
121
121
  SELECT 1 FROM sqlite_master
@@ -4,6 +4,7 @@ import { isRecord, stringValue, isMechanicalMessageKind } from "agent-relay-sdk"
4
4
  import { ORCHESTRATOR_PROTOCOL_VERSION, VERSION } from "../config.ts";
5
5
  import { parseJson } from "../utils";
6
6
  import { isLiveIsolatedWorkspace } from "../workspace-phase";
7
+ import { parseChannelRouteTarget } from "../channel-target";
7
8
  import {
8
9
  CONTRACT_REQUIREMENTS,
9
10
  contractCompatibility,
@@ -15,10 +16,11 @@ import {
15
16
  import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_MERGE_LEASE_MS } from "../config";
16
17
  import { matchAgents } from "../agent-ref";
17
18
  import { getAgent, listAgents } from "./agents.ts";
18
- import { channelProviderForAgent } from "./channels.ts";
19
+ import { isDeliveryAgent } from "./agent-predicates.ts";
20
+ import { channelProviderForAgent } from "./channel-identity.ts";
19
21
  import { getDb } from "./connection.ts";
20
22
  import { MSG_SELECT, rowToMessage, rowToMessageDeliveryAttempt } from "./mappers.ts";
21
- import { getMessage } from "./messages.ts";
23
+ import { getMessage } from "./message-queries.ts";
22
24
  import type {
23
25
  AgentCard,
24
26
  ActivityEvent,
@@ -82,7 +84,7 @@ import type {
82
84
  WorkspaceStatus,
83
85
  } from "../types";
84
86
 
85
- export function queueDepthLimit(target: string): number {
87
+ function queueDepthLimit(target: string): number {
86
88
  const row = getDb().query("SELECT value FROM config WHERE namespace = 'system' AND key = 'message-queue'").get() as { value?: string } | undefined;
87
89
  const parsed = row?.value ? parseJson<Record<string, unknown>>(row.value, {}) : {};
88
90
  const perTarget = parsed?.maxDepthPerTarget;
@@ -123,13 +125,7 @@ export function enforceQueueLimit(target: string): void {
123
125
  })();
124
126
  }
125
127
 
126
- export function isDeliveryAgent(agent: AgentCard): boolean {
127
- return agent.status !== "offline" &&
128
- agent.id !== "user" &&
129
- agent.id !== "system" &&
130
- agent.kind !== "channel" &&
131
- agent.meta?.kind !== "channel";
132
- }
128
+ export { isDeliveryAgent } from "./agent-predicates.ts";
133
129
 
134
130
  export function isChannelAgentId(agentId: string): boolean {
135
131
  const agent = getAgent(agentId);
@@ -156,27 +152,76 @@ export function legacyChannelTargets(agent: AgentCard | null | undefined): strin
156
152
  return [...aliases];
157
153
  }
158
154
 
159
- export function matchingDeliveryAgents(target: string): AgentCard[] {
160
- if (!target) return [];
161
- const candidates = listAgents().filter(isDeliveryAgent);
162
- if (target === "broadcast") return candidates;
163
- const direct = getAgent(target);
164
- if (direct) return isDeliveryAgent(direct) ? [direct] : [];
165
- if (target.startsWith("tag:")) {
166
- const tag = target.slice(4);
167
- return candidates.filter((agent) => agent.tags.includes(tag));
155
+ type BroadcastDeliveryContext = Pick<SendMessageInput, "from" | "channel">;
156
+
157
+ function addTargetParticipants(target: string, scope: Set<string>, candidates: AgentCard[]): void {
158
+ const parsed = parseChannelRouteTarget(target);
159
+ switch (parsed.type) {
160
+ case "agent":
161
+ case "orchestrator":
162
+ if (candidates.some((agent) => agent.id === parsed.id)) scope.add(parsed.id);
163
+ return;
164
+ case "tag":
165
+ for (const agent of candidates) if (agent.tags.includes(parsed.id)) scope.add(agent.id);
166
+ return;
167
+ case "capability":
168
+ for (const agent of candidates) if (agent.capabilities.includes(parsed.id)) scope.add(agent.id);
169
+ return;
170
+ case "label":
171
+ for (const agent of candidates) if (agent.label === parsed.id) scope.add(agent.id);
172
+ return;
173
+ case "team":
174
+ for (const agent of candidates) if (agent.teamId === parsed.id) scope.add(agent.id);
175
+ return;
168
176
  }
169
- if (target.startsWith("cap:")) {
170
- const cap = target.slice(4);
171
- return candidates.filter((agent) => agent.capabilities.includes(cap));
177
+ }
178
+
179
+ function channelParticipantAgentIds(channel: string, candidates: AgentCard[], senderId?: string): Set<string> {
180
+ const candidateIds = new Set(candidates.map((agent) => agent.id));
181
+ const scope = new Set<string>();
182
+ if (senderId && candidateIds.has(senderId)) scope.add(senderId);
183
+ const rows = getDb().query(`
184
+ SELECT from_agent, to_target, resolved_to_agent
185
+ FROM messages
186
+ WHERE channel = ?
187
+ `).all(channel) as Array<{ from_agent: string; to_target: string; resolved_to_agent?: string | null }>;
188
+ for (const row of rows) {
189
+ if (candidateIds.has(row.from_agent)) scope.add(row.from_agent);
190
+ if (row.resolved_to_agent && candidateIds.has(row.resolved_to_agent)) scope.add(row.resolved_to_agent);
191
+ addTargetParticipants(row.to_target, scope, candidates);
172
192
  }
173
- if (target.startsWith("label:")) {
174
- const label = target.slice(6);
175
- return candidates.filter((agent) => agent.label === label);
193
+ return scope;
194
+ }
195
+
196
+ function scopedBroadcastCandidates(candidates: AgentCard[], context?: BroadcastDeliveryContext): AgentCard[] {
197
+ if (context?.channel) {
198
+ const scope = channelParticipantAgentIds(context.channel, candidates, context.from);
199
+ return candidates.filter((agent) => scope.has(agent.id));
176
200
  }
177
- if (target.startsWith("team:")) {
178
- const teamId = target.slice(5);
179
- return candidates.filter((agent) => agent.teamId === teamId);
201
+ const sender = context?.from ? getAgent(context.from) : null;
202
+ if (sender?.teamId) return candidates.filter((agent) => agent.teamId === sender.teamId);
203
+ return candidates;
204
+ }
205
+
206
+ export function matchingDeliveryAgents(target: string, context?: BroadcastDeliveryContext): AgentCard[] {
207
+ if (!target) return [];
208
+ const candidates = listAgents().filter(isDeliveryAgent);
209
+ const parsed = parseChannelRouteTarget(target);
210
+ switch (parsed.type) {
211
+ case "broadcast":
212
+ return scopedBroadcastCandidates(candidates, context);
213
+ case "agent": {
214
+ const direct = getAgent(parsed.id);
215
+ return direct && isDeliveryAgent(direct) ? [direct] : [];
216
+ }
217
+ case "tag":
218
+ return candidates.filter((agent) => agent.tags.includes(parsed.id));
219
+ case "capability":
220
+ return candidates.filter((agent) => agent.capabilities.includes(parsed.id));
221
+ case "label":
222
+ return candidates.filter((agent) => agent.label === parsed.id);
223
+ case "team":
224
+ return candidates.filter((agent) => agent.teamId === parsed.id);
180
225
  }
181
226
  return [];
182
227
  }
@@ -192,7 +237,7 @@ export function getMessageDeliveryAttempts(messageId: number, limit = 50): Messa
192
237
  `).all(messageId, safeLimit) as any[]).map(rowToMessageDeliveryAttempt);
193
238
  }
194
239
 
195
- export function insertMessageDeliveryAttempt(
240
+ function insertMessageDeliveryAttempt(
196
241
  messageId: number,
197
242
  input: {
198
243
  agentId?: string;
@@ -219,7 +264,7 @@ export function insertMessageDeliveryAttempt(
219
264
  );
220
265
  }
221
266
 
222
- export function setMessageDeliveryState(
267
+ function setMessageDeliveryState(
223
268
  messageId: number,
224
269
  input: {
225
270
  status: MessageDeliveryStatus;