agent-relay-server 0.62.3 → 0.63.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.
Files changed (181) hide show
  1. package/docs/openapi.json +1 -1
  2. package/package.json +4 -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/runner/src/adapter.ts +2 -1
  71. package/runner/src/config.ts +143 -5
  72. package/src/agent-ref.ts +8 -6
  73. package/src/artifact-storage.ts +3 -5
  74. package/src/automations/constants.ts +12 -0
  75. package/src/automations/cron.ts +129 -0
  76. package/src/automations/crud.ts +84 -0
  77. package/src/automations/dispatch.ts +275 -0
  78. package/src/automations/index.ts +5 -0
  79. package/src/automations/reconcile.ts +245 -0
  80. package/src/automations/storage.ts +181 -0
  81. package/src/automations/types.ts +16 -0
  82. package/src/automations/validation.ts +140 -0
  83. package/src/automations.ts +1 -1045
  84. package/src/bus.ts +3 -4
  85. package/src/cli/_shared.ts +5 -4
  86. package/src/cli/agent-detect.ts +6 -4
  87. package/src/cli/introspect.ts +3 -2
  88. package/src/cli/token.ts +2 -1
  89. package/src/cli/workspace.ts +2 -1
  90. package/src/config.ts +176 -1
  91. package/src/connectors.ts +3 -3
  92. package/src/db/activity.ts +1 -1
  93. package/src/db/agent-predicates.ts +9 -0
  94. package/src/db/agent-search.ts +2 -2
  95. package/src/db/agents.ts +13 -41
  96. package/src/db/artifacts.ts +8 -8
  97. package/src/db/channel-identity.ts +38 -0
  98. package/src/db/channels.ts +37 -61
  99. package/src/db/connection.ts +2 -1
  100. package/src/db/continuation-archives.ts +7 -7
  101. package/src/db/delivery.ts +7 -12
  102. package/src/db/inbox.ts +1 -1
  103. package/src/db/integrations.ts +4 -4
  104. package/src/db/issue-lifecycle.ts +6 -6
  105. package/src/db/mappers.ts +1 -2
  106. package/src/db/merge-lease.ts +3 -3
  107. package/src/db/message-claims.ts +16 -0
  108. package/src/db/message-queries.ts +8 -0
  109. package/src/db/message-reads.ts +26 -9
  110. package/src/db/messages.ts +17 -41
  111. package/src/db/notification-subscriptions.ts +2 -2
  112. package/src/db/orchestrators.ts +4 -4
  113. package/src/db/pair-maintenance.ts +9 -0
  114. package/src/db/pairs.ts +10 -18
  115. package/src/db/plans.ts +1 -1
  116. package/src/db/policy-runtime.ts +14 -0
  117. package/src/db/project-entries.ts +1 -1
  118. package/src/db/projects.ts +2 -2
  119. package/src/db/shared.ts +7 -0
  120. package/src/db/stats.ts +2 -2
  121. package/src/db/task-core.ts +18 -0
  122. package/src/db/task-maintenance.ts +24 -0
  123. package/src/db/tasks.ts +5 -17
  124. package/src/db/teams.ts +3 -3
  125. package/src/db/workspaces.ts +6 -6
  126. package/src/index.ts +6 -5
  127. package/src/issue-tracker/github.ts +3 -3
  128. package/src/maintenance/constants.ts +52 -0
  129. package/src/maintenance/events.ts +32 -0
  130. package/src/maintenance/index.ts +7 -0
  131. package/src/maintenance/jobs.ts +340 -0
  132. package/src/maintenance/orphaned-sessions.ts +86 -0
  133. package/src/maintenance/scheduler.ts +231 -0
  134. package/src/maintenance/teams.ts +65 -0
  135. package/src/maintenance/types.ts +31 -0
  136. package/src/maintenance/workspaces.ts +251 -0
  137. package/src/maintenance.ts +1 -1099
  138. package/src/mcp/identity.ts +131 -0
  139. package/src/mcp/index.ts +208 -0
  140. package/src/mcp/tools-messaging.ts +459 -0
  141. package/src/mcp/tools-spawn.ts +145 -0
  142. package/src/mcp/tools-workspace.ts +227 -0
  143. package/src/mcp/types.ts +24 -0
  144. package/src/mcp/validation.ts +104 -0
  145. package/src/mcp.ts +1 -1319
  146. package/src/notification-router.ts +2 -2
  147. package/src/notification-rules.ts +2 -2
  148. package/src/project-ingest.ts +3 -3
  149. package/src/project-instructions.ts +1 -1
  150. package/src/provider-config-store.ts +5 -4
  151. package/src/provider-state.ts +2 -4
  152. package/src/reviewer-pipeline.ts +1 -1
  153. package/src/routes/agent-sessions.ts +5 -4
  154. package/src/routes/orchestrator-proxy.ts +5 -4
  155. package/src/routes/workspaces.ts +5 -4
  156. package/src/security.ts +13 -7
  157. package/src/self-resume.ts +3 -7
  158. package/src/services/agent-action.ts +2 -2
  159. package/src/services/issue-activity.ts +0 -4
  160. package/src/services/issue-associations.ts +2 -2
  161. package/src/services/issue-lifecycle.ts +8 -11
  162. package/src/services/managed-running.ts +1 -1
  163. package/src/services/parity-harness.ts +3 -3
  164. package/src/services/plan-graph.ts +4 -7
  165. package/src/services/send-message.ts +16 -12
  166. package/src/services/shutdown-agent.ts +2 -2
  167. package/src/services/team-templates.ts +2 -2
  168. package/src/services/team.ts +2 -2
  169. package/src/settings/modules.ts +1 -1
  170. package/src/settings/registry.ts +3 -3
  171. package/src/spawn-command.ts +1 -1
  172. package/src/sse.ts +0 -4
  173. package/src/upgrade.ts +7 -9
  174. package/src/workspace-actions.ts +1 -1
  175. package/src/workspace-auto-merge.ts +4 -5
  176. package/src/workspace-claim.ts +1 -2
  177. package/src/workspace-orphans.ts +2 -7
  178. package/src/workspace-probe.ts +5 -8
  179. package/public/assets/MessageBubble-DRrxfjyT.js.map +0 -1
  180. package/public/assets/display-DzNvOu1j.js.map +0 -1
  181. package/public/assets/store-iTiheuTD.js.map +0 -1
@@ -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)
@@ -1,4 +1,5 @@
1
1
  import { Database } from "bun:sqlite";
2
+ import { DB_SLOW_QUERY_MS } from "../config";
2
3
 
3
4
  // The shared SQLite handle. Set once by initDb() (schema.ts) and read everywhere
4
5
  // else through getDb(). Kept module-private so the only mutation path is setDb().
@@ -33,7 +34,7 @@ export function applyConnectionPragmas(conn: Database): void {
33
34
  // Slow-query log. Off unless AGENT_RELAY_DB_SLOW_MS is set (>0). Wraps the
34
35
  // execution of the heavy dynamic queries so the next slow query surfaces before
35
36
  // it wedges something (see issue #197 — the 8.6s reply-obligation query).
36
- const SLOW_QUERY_MS = Number(process.env.AGENT_RELAY_DB_SLOW_MS) || 0;
37
+ const SLOW_QUERY_MS = DB_SLOW_QUERY_MS;
37
38
  export function timedQuery<T>(label: string, run: () => T): T {
38
39
  if (SLOW_QUERY_MS <= 0) return run();
39
40
  const start = performance.now();
@@ -1,8 +1,8 @@
1
- import { MAX_BODY_BYTES } from "../config";
1
+ import { MAX_BODY_BYTES, continuationArchiveFtsEnabled } 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
  }
@@ -28,7 +28,7 @@ interface ContinuationArchiveRow {
28
28
  const MAX_RECALL_LIMIT = 20;
29
29
 
30
30
  export function ensureContinuationArchiveSearchSchema(): void {
31
- if (process.env.AGENT_RELAY_DISABLE_CONTINUATION_ARCHIVE_FTS === "1") return;
31
+ if (!continuationArchiveFtsEnabled()) return;
32
32
  try {
33
33
  getDb().query(`
34
34
  CREATE VIRTUAL TABLE IF NOT EXISTS continuation_archive_segments
@@ -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,8 +115,8 @@ export function searchContinuationArchives(input: {
115
115
  return searchContinuationArchivesLike(agentId, query, limit);
116
116
  }
117
117
 
118
- export function continuationArchiveSearchMode(): "fts5" | "like" {
119
- if (process.env.AGENT_RELAY_DISABLE_CONTINUATION_ARCHIVE_FTS === "1") return "like";
118
+ function continuationArchiveSearchMode(): "fts5" | "like" {
119
+ if (!continuationArchiveFtsEnabled()) return "like";
120
120
  const row = getDb().query(`
121
121
  SELECT 1 FROM sqlite_master
122
122
  WHERE type = 'table' AND name = 'continuation_archive_segments'
@@ -16,10 +16,11 @@ import {
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
18
  import { getAgent, listAgents } from "./agents.ts";
19
- import { channelProviderForAgent } from "./channels.ts";
19
+ import { isDeliveryAgent } from "./agent-predicates.ts";
20
+ import { channelProviderForAgent } from "./channel-identity.ts";
20
21
  import { getDb } from "./connection.ts";
21
22
  import { MSG_SELECT, rowToMessage, rowToMessageDeliveryAttempt } from "./mappers.ts";
22
- import { getMessage } from "./messages.ts";
23
+ import { getMessage } from "./message-queries.ts";
23
24
  import type {
24
25
  AgentCard,
25
26
  ActivityEvent,
@@ -83,7 +84,7 @@ import type {
83
84
  WorkspaceStatus,
84
85
  } from "../types";
85
86
 
86
- export function queueDepthLimit(target: string): number {
87
+ function queueDepthLimit(target: string): number {
87
88
  const row = getDb().query("SELECT value FROM config WHERE namespace = 'system' AND key = 'message-queue'").get() as { value?: string } | undefined;
88
89
  const parsed = row?.value ? parseJson<Record<string, unknown>>(row.value, {}) : {};
89
90
  const perTarget = parsed?.maxDepthPerTarget;
@@ -124,13 +125,7 @@ export function enforceQueueLimit(target: string): void {
124
125
  })();
125
126
  }
126
127
 
127
- export function isDeliveryAgent(agent: AgentCard): boolean {
128
- return agent.status !== "offline" &&
129
- agent.id !== "user" &&
130
- agent.id !== "system" &&
131
- agent.kind !== "channel" &&
132
- agent.meta?.kind !== "channel";
133
- }
128
+ export { isDeliveryAgent } from "./agent-predicates.ts";
134
129
 
135
130
  export function isChannelAgentId(agentId: string): boolean {
136
131
  const agent = getAgent(agentId);
@@ -242,7 +237,7 @@ export function getMessageDeliveryAttempts(messageId: number, limit = 50): Messa
242
237
  `).all(messageId, safeLimit) as any[]).map(rowToMessageDeliveryAttempt);
243
238
  }
244
239
 
245
- export function insertMessageDeliveryAttempt(
240
+ function insertMessageDeliveryAttempt(
246
241
  messageId: number,
247
242
  input: {
248
243
  agentId?: string;
@@ -269,7 +264,7 @@ export function insertMessageDeliveryAttempt(
269
264
  );
270
265
  }
271
266
 
272
- export function setMessageDeliveryState(
267
+ function setMessageDeliveryState(
273
268
  messageId: number,
274
269
  input: {
275
270
  status: MessageDeliveryStatus;
package/src/db/inbox.ts CHANGED
@@ -212,7 +212,7 @@ export function createChatHistoryImport(input: {
212
212
  return getChatHistoryImport(id)!;
213
213
  }
214
214
 
215
- export function getChatHistoryImport(id: string): ChatHistoryImport | null {
215
+ function getChatHistoryImport(id: string): ChatHistoryImport | null {
216
216
  const row = getDb().query("SELECT * FROM chat_history_imports WHERE id = ?").get(id) as any | undefined;
217
217
  if (!row) return null;
218
218
  const entries = (getDb().query(
@@ -212,7 +212,7 @@ export function listIntegrationTaskStats(): IntegrationTaskStats[] {
212
212
  }));
213
213
  }
214
214
 
215
- export type IntegrationRegistryInput = {
215
+ type IntegrationRegistryInput = {
216
216
  name: string;
217
217
  displayName?: string;
218
218
  description?: string;
@@ -231,7 +231,7 @@ export type IntegrationRegistryInput = {
231
231
  source?: "api" | "env" | "observed";
232
232
  };
233
233
 
234
- export function rowToIntegrationRegistry(row: any): Omit<IntegrationSummary, "configured" | "observed" | "callbackConfigured" | "rateLimit" | "taskStats"> & {
234
+ function rowToIntegrationRegistry(row: any): Omit<IntegrationSummary, "configured" | "observed" | "callbackConfigured" | "rateLimit" | "taskStats"> & {
235
235
  source: "api" | "env" | "observed";
236
236
  lastEventAt?: number;
237
237
  lastTaskAt?: number;
@@ -312,7 +312,7 @@ export function upsertIntegrationRegistry(input: IntegrationRegistryInput): Retu
312
312
  return getIntegrationRegistry(name)!;
313
313
  }
314
314
 
315
- export function getIntegrationRegistry(name: string): ReturnType<typeof rowToIntegrationRegistry> | null {
315
+ function getIntegrationRegistry(name: string): ReturnType<typeof rowToIntegrationRegistry> | null {
316
316
  const row = getDb().query("SELECT * FROM integration_registry WHERE name = ?").get(name) as any;
317
317
  return row ? rowToIntegrationRegistry(row) : null;
318
318
  }
@@ -321,7 +321,7 @@ export function listIntegrationRegistry(): ReturnType<typeof rowToIntegrationReg
321
321
  return (getDb().query("SELECT * FROM integration_registry ORDER BY display_name COLLATE NOCASE, name COLLATE NOCASE").all() as any[]).map(rowToIntegrationRegistry);
322
322
  }
323
323
 
324
- export function updateIntegrationObserved(name: string, eventAt: number): void {
324
+ function updateIntegrationObserved(name: string, eventAt: number): void {
325
325
  const now = Date.now();
326
326
  getDb().query(`
327
327
  INSERT INTO integration_registry (name, enabled, scopes, targets, channels, tags, manifest, source, created_at, updated_at, last_event_at, last_task_at)
@@ -1,11 +1,11 @@
1
1
  import { getDb, ValidationError } from "./connection.ts";
2
2
 
3
- export const ISSUE_LIFECYCLE_ACTIONS = ["close", "reopen"] as const;
4
- export const ISSUE_LIFECYCLE_STATUSES = ["succeeded", "failed"] as const;
5
- export type IssueLifecycleAction = (typeof ISSUE_LIFECYCLE_ACTIONS)[number];
6
- export type IssueLifecycleStatus = (typeof ISSUE_LIFECYCLE_STATUSES)[number];
3
+ const ISSUE_LIFECYCLE_ACTIONS = ["close", "reopen"] as const;
4
+ const ISSUE_LIFECYCLE_STATUSES = ["succeeded", "failed"] as const;
5
+ type IssueLifecycleAction = (typeof ISSUE_LIFECYCLE_ACTIONS)[number];
6
+ type IssueLifecycleStatus = (typeof ISSUE_LIFECYCLE_STATUSES)[number];
7
7
 
8
- export interface IssueLifecycleActionRecord {
8
+ interface IssueLifecycleActionRecord {
9
9
  id: number;
10
10
  issueRefId: string;
11
11
  action: IssueLifecycleAction;
@@ -24,7 +24,7 @@ export interface IssueLifecycleActionRecord {
24
24
  updatedAt: number;
25
25
  }
26
26
 
27
- export interface UpsertIssueLifecycleActionInput {
27
+ interface UpsertIssueLifecycleActionInput {
28
28
  issueRefId: string;
29
29
  action: IssueLifecycleAction;
30
30
  landKey: string;
package/src/db/mappers.ts CHANGED
@@ -14,8 +14,7 @@ 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 { parseStringArray } from "./agents.ts";
18
- import { artifactId } from "./artifacts.ts";
17
+ import { parseStringArray } from "./shared.ts";
19
18
  import type {
20
19
  AgentCard,
21
20
  ActivityEvent,
@@ -80,7 +80,7 @@ import type {
80
80
 
81
81
  // --- Per-repo merge serialization lease (issue #157) -----------------------
82
82
 
83
- export interface MergeLeaseRecord {
83
+ interface MergeLeaseRecord {
84
84
  repoRoot: string;
85
85
  workspaceId: string;
86
86
  commandId?: string;
@@ -89,7 +89,7 @@ export interface MergeLeaseRecord {
89
89
  expiresAt: number;
90
90
  }
91
91
 
92
- export function rowToMergeLease(row: any): MergeLeaseRecord {
92
+ function rowToMergeLease(row: any): MergeLeaseRecord {
93
93
  return {
94
94
  repoRoot: row.repo_root,
95
95
  workspaceId: row.workspace_id,
@@ -171,7 +171,7 @@ export interface MergePauseRecord {
171
171
  expiresAt: number;
172
172
  }
173
173
 
174
- export function rowToMergePause(row: any): MergePauseRecord {
174
+ function rowToMergePause(row: any): MergePauseRecord {
175
175
  return {
176
176
  id: row.id,
177
177
  repoRoot: row.repo_root,
@@ -0,0 +1,16 @@
1
+ import { CLAIM_LEASE_MS } from "../config";
2
+ import { getDb } from "./connection.ts";
3
+
4
+ export function claimMessageRow(messageId: number, agentId: string, now: number): { ok: false; error: string } | { ok: true } {
5
+ const expiresAt = now + CLAIM_LEASE_MS;
6
+ // Idempotent: a same-agent re-claim refreshes the lease (preserving the original claimed_at).
7
+ // Without this, a delivery failure that locally drops the claim strands the message until the
8
+ // lease expires (#261) — the holder's retry would otherwise 409 against its own claim.
9
+ const result = getDb().query(
10
+ "UPDATE messages SET claimed_by = ?, claimed_at = COALESCE(claimed_at, ?), claim_expires_at = ? WHERE id = ? AND (claimed_by IS NULL OR claimed_by = ?)"
11
+ ).run(agentId, now, expiresAt, messageId, agentId);
12
+
13
+ // Atomic: if changes === 0, someone else claimed it between our read and write
14
+ if (result.changes === 0) return { ok: false, error: "claim race — already claimed" };
15
+ return { ok: true };
16
+ }
@@ -0,0 +1,8 @@
1
+ import { getDb } from "./connection.ts";
2
+ import { MSG_SELECT, rowToMessage } from "./mappers.ts";
3
+ import type { Message } from "../types";
4
+
5
+ export function getMessage(id: number): Message | null {
6
+ const row = getDb().query(`${MSG_SELECT} WHERE m.id = ?`).get(id) as any;
7
+ return row ? rowToMessage(row) : null;
8
+ }
@@ -15,7 +15,7 @@ import {
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
17
  import { getAgent } from "./agents.ts";
18
- import { sendMessageWithResult } from "./messages.ts";
18
+ import { getMessage, sendMessageWithResult } from "./messages.ts";
19
19
  import { deleteArtifactLinksForEntity } from "./artifacts.ts";
20
20
  import { getDb, timedQuery } from "./connection.ts";
21
21
  import { legacyChannelTargets, isChannelAgentId } from "./delivery.ts";
@@ -369,7 +369,7 @@ export function pollMessages(query: PollQuery): Message[] {
369
369
  return annotateAnsweredMessages(rows.map(rowToMessage), query.for);
370
370
  }
371
371
 
372
- export function messageRequiresReply(message: Message): boolean {
372
+ function messageRequiresReply(message: Message): boolean {
373
373
  // Server-owned notification flag (#283) wins over every kind/sender heuristic below: an
374
374
  // explicit replyExpected:false is a fire-and-forget message that must never become an obligation.
375
375
  if (message.replyExpected === false) return false;
@@ -387,13 +387,13 @@ export function recordField(value: unknown): Record<string, unknown> | undefined
387
387
  return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : undefined;
388
388
  }
389
389
 
390
- export function relayConversationId(message: Message): string | undefined {
390
+ function relayConversationId(message: Message): string | undefined {
391
391
  const conversation = recordField(message.payload?.conversation);
392
392
  const replyContext = recordField(message.payload?.replyContext);
393
393
  return stringField(replyContext?.conversationId) ?? stringField(conversation?.id);
394
394
  }
395
395
 
396
- export function isCoveredByLaterAgentResponse(message: Message, agentId: string): boolean {
396
+ function isCoveredByLaterAgentResponse(message: Message, agentId: string): boolean {
397
397
  // Order by id, not created_at: ids are monotonic insertion order, so this is
398
398
  // robust when a reply lands in the same millisecond as the message it covers
399
399
  // (created_at > … strictly would miss it, leaving the message wrongly pending).
@@ -414,7 +414,7 @@ export function isCoveredByLaterAgentResponse(message: Message, agentId: string)
414
414
  });
415
415
  }
416
416
 
417
- export function replyObligationFromMessage(message: Message, agentId: string): ReplyObligation {
417
+ function replyObligationFromMessage(message: Message, agentId: string): ReplyObligation {
418
418
  return {
419
419
  messageId: message.id,
420
420
  agentId,
@@ -482,6 +482,25 @@ export function channelObligationAnsweredBy(
482
482
  return open.length === 1 ? open[0]!.messageId : null;
483
483
  }
484
484
 
485
+ /**
486
+ * Spawn report-back linking (#492): a spawned worker's actual task is delivered as the provider
487
+ * prompt, while `seedSpawnReplyObligation` creates a short Stop-hook contract message so the worker
488
+ * cannot go idle without reporting back. Workers commonly send their natural completion report
489
+ * directly to the spawning coordinator; when there is exactly one open spawn obligation from that
490
+ * coordinator, that send is the answer. Link it to the obligation at send time so the Stop hook does
491
+ * not nag the worker into re-sending the same verdict through `/reply`.
492
+ */
493
+ export function spawnObligationAnsweredBy(
494
+ agentId: string,
495
+ spawnerAgentId: string,
496
+ ): number | null {
497
+ const open = listPendingReplyObligations(agentId, 50).filter((obligation) => {
498
+ if (obligation.from !== spawnerAgentId) return false;
499
+ return getMessage(obligation.messageId)?.payload?.source === "spawn-obligation";
500
+ });
501
+ return open.length === 1 ? open[0]!.messageId : null;
502
+ }
503
+
485
504
  /**
486
505
  * #413 / vent #121 — seed a pending reply obligation for a freshly-registered spawned worker so
487
506
  * it cannot strand its verdict. A spawned agent gets its task as the positional spawn prompt, NOT
@@ -490,7 +509,7 @@ export function channelObligationAnsweredBy(
490
509
  * spawn command by its `spawnRequestId` (same lookup as `seedContinuationObjectiveFromSpawnPrompt`),
491
510
  * and — only when the spawn carried `expectReply` — posts a message FROM the spawner TO the worker
492
511
  * and marks it read, making it an obligation immediately. The Stop hook then refuses to let the
493
- * worker idle until it `/reply`s, which auto-routes back to the spawner and wakes it.
512
+ * worker idle until it reports back; a natural direct send to the spawner is linked at send time.
494
513
  *
495
514
  * Call only on first registration; idempotent regardless (idempotencyKey dedups a double-fire).
496
515
  * Never throws — a failure here must not abort registration. Returns the obligation message id,
@@ -515,15 +534,13 @@ export function seedSpawnReplyObligation(
515
534
  const params = row ? parseJson<unknown>(row.params, {}) : {};
516
535
  if (!isRecord(params) || params.expectReply !== true) return null;
517
536
 
518
- const objective = typeof params.prompt === "string" ? params.prompt.trim() : "";
519
537
  const contract = `Report-back requested by ${spawnedBy}. When you finish your spawned task, reply here with your result so the coordinator is notified — you should not go idle before delivering it.`;
520
- const body = objective ? `${contract}\n\nYour task was:\n${objective}` : contract;
521
538
 
522
539
  const { message } = sendMessageWithResult({
523
540
  from: spawnedBy,
524
541
  to: agentId,
525
542
  subject: "Report your result when done",
526
- body,
543
+ body: contract,
527
544
  replyExpected: true,
528
545
  // `payload.source` is the server-owned "this requires a reply" signal (see messageRequiresReply).
529
546
  payload: { source: "spawn-obligation", spawnRequestId },