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
package/src/bus.ts CHANGED
@@ -15,6 +15,7 @@ import { applyCommandToRecipe } from "./recipe-runner";
15
15
  import { enqueueContinuationInjectionAfterClear } from "./self-resume";
16
16
  import { messageMatchesAgent, targetMatchesAgent } from "./agent-ref";
17
17
  import { probeWorkspaceOnTurnEnd } from "./workspace-probe";
18
+ import { busStaleGraceMs } from "./config";
18
19
  import {
19
20
  BusProtocolError,
20
21
  parseBusFrame,
@@ -107,11 +108,9 @@ export function getBusConnectionCount(): number {
107
108
  return busConnections.size;
108
109
  }
109
110
 
110
- export function expireStaleBusAgents(graceMs = Number(process.env.AGENT_RELAY_STALE_GRACE_MS) || 120_000): { agentIds: string[]; orphanedTasks: Task[] } {
111
+ export function expireStaleBusAgents(graceMs = busStaleGraceMs()): { agentIds: string[]; orphanedTasks: Task[] } {
111
112
  const cutoff = Date.now() - graceMs;
112
- const rows = getDb()
113
- .query("SELECT id FROM agents WHERE status = 'stale' AND last_seen < ? AND id NOT IN ('user', 'system')")
114
- .all(cutoff) as Array<{ id: string }>;
113
+ const rows = getDb().query("SELECT id FROM agents WHERE status = 'stale' AND last_seen < ? AND id NOT IN ('user', 'system')").all(cutoff) as Array<{ id: string }>;
115
114
  const orphanedTasks: Task[] = [];
116
115
  for (const row of rows) {
117
116
  // Bus-grace staleness across a relay restart/reconnect — the process may be alive.
@@ -5,11 +5,12 @@
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { stdin as input, stdout as output } from "node:process";
7
7
  import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
8
+ import { cliRelayToken, cliRelayUrl } from "../config";
8
9
 
9
10
  export async function apiRequest(method: string, path: string, body?: unknown): Promise<unknown> {
10
- const baseUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
11
+ const baseUrl = cliRelayUrl();
11
12
  const headers: Record<string, string> = {};
12
- const token = process.env.AGENT_RELAY_TOKEN;
13
+ const token = cliRelayToken();
13
14
  if (token) headers[RELAY_TOKEN_HEADER] = token;
14
15
  if (body !== undefined) headers["Content-Type"] = "application/json";
15
16
  const response = await fetch(new URL(path, baseUrl), {
@@ -27,9 +28,9 @@ export async function apiRequest(method: string, path: string, body?: unknown):
27
28
  }
28
29
 
29
30
  export async function apiRawRequest(method: string, path: string, body: BodyInit, extraHeaders: Record<string, string> = {}): Promise<unknown> {
30
- const baseUrl = process.env.AGENT_RELAY_URL || "http://127.0.0.1:4850";
31
+ const baseUrl = cliRelayUrl();
31
32
  const headers: Record<string, string> = { ...extraHeaders };
32
- const token = process.env.AGENT_RELAY_TOKEN;
33
+ const token = cliRelayToken();
33
34
  if (token) headers[RELAY_TOKEN_HEADER] = token;
34
35
  const response = await fetch(new URL(path, baseUrl), { method, headers, body });
35
36
  const text = await response.text();
@@ -5,6 +5,7 @@
5
5
  import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
6
6
  import { join, resolve } from "node:path";
7
7
  import { apiRequest, uniqueStrings } from "./_shared";
8
+ import { agentIdFromEnv, codexStatePathFromEnv, contextPathFromEnv } from "../config";
8
9
 
9
10
  export async function detectActivePairId(agentId: string): Promise<string | undefined> {
10
11
  const pairs = await apiRequest("GET", `/api/pairs?agent=${encodeURIComponent(agentId)}&status=active`) as Array<{ id?: string }>;
@@ -12,15 +13,16 @@ export async function detectActivePairId(agentId: string): Promise<string | unde
12
13
  }
13
14
 
14
15
  export async function detectAgentId(): Promise<string | undefined> {
15
- const explicit = process.env.AGENT_RELAY_ID;
16
+ const explicit = agentIdFromEnv();
16
17
  if (explicit) return explicit;
17
18
 
18
19
  const contextMatch = currentAgentContextId();
19
20
  if (contextMatch) return contextMatch;
20
21
 
21
22
  const cwd = process.cwd();
22
- const explicitCodexState = process.env.AGENT_RELAY_CODEX_STATE_PATH
23
- ? readCodexState(process.env.AGENT_RELAY_CODEX_STATE_PATH)
23
+ const explicitCodexStatePath = codexStatePathFromEnv();
24
+ const explicitCodexState = explicitCodexStatePath
25
+ ? readCodexState(explicitCodexStatePath)
24
26
  : null;
25
27
  if (explicitCodexState?.agentId) return explicitCodexState.agentId;
26
28
 
@@ -48,7 +50,7 @@ export async function detectAgentId(): Promise<string | undefined> {
48
50
  }
49
51
 
50
52
  function currentAgentContextId(): string | undefined {
51
- const explicitPath = process.env.AGENT_RELAY_CONTEXT_PATH;
53
+ const explicitPath = contextPathFromEnv();
52
54
  if (explicitPath) {
53
55
  const explicit = readAgentContext(explicitPath);
54
56
  if (explicit?.agentId && contextMatchesCurrentProcess(explicit)) return explicit.agentId;
@@ -2,6 +2,7 @@
2
2
  // end-of-session self-view as a bounded, structured Insights observation (#185).
3
3
  import { apiRequest, readStdin } from "./_shared";
4
4
  import { detectAgentId } from "./agent-detect";
5
+ import { projectFromEnv, providerSessionIdFromEnv } from "../config";
5
6
 
6
7
  // Insights #185: capture the agent's end-of-session self-view as a bounded, structured
7
8
  // artifact (epic #183, docs/self-improvement.md). Manual rail in v0 — the agent or
@@ -61,8 +62,8 @@ export async function handleIntrospectCommand(args: string[]): Promise<void> {
61
62
  if (!from) throw new Error("Could not detect current Agent Relay ID. Pass --from AGENT_ID or set AGENT_RELAY_ID.");
62
63
 
63
64
  const observation = await apiRequest("POST", "/api/insights/observations", {
64
- sessionId: sessionId || process.env.AGENT_RELAY_PROVIDER_SESSION_ID || `manual-${from}`,
65
- project: project || process.env.AGENT_RELAY_PROJECT || process.cwd().split("/").filter(Boolean).at(-1) || process.cwd(),
65
+ sessionId: sessionId || providerSessionIdFromEnv() || `manual-${from}`,
66
+ project: project || projectFromEnv() || process.cwd().split("/").filter(Boolean).at(-1) || process.cwd(),
66
67
  agentId: from,
67
68
  signal: "introspection",
68
69
  source: "agent",
package/src/cli/token.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  // Token commands — auto-split from cli.ts (#294). create/list/revoke/verify for
2
2
  // component JWTs.
3
3
  import { apiRequest, splitTagArgs } from "./_shared";
4
+ import { cliRelayToken } from "../config";
4
5
 
5
6
  export async function handleTokenCommand(args: string[]): Promise<void> {
6
7
  const action = args[0] ?? "list";
@@ -45,7 +46,7 @@ export async function handleTokenCommand(args: string[]): Promise<void> {
45
46
  return;
46
47
  }
47
48
  if (action === "verify") {
48
- const token = rest.find((arg) => !arg.startsWith("--")) ?? process.env.AGENT_RELAY_TOKEN;
49
+ const token = rest.find((arg) => !arg.startsWith("--")) ?? cliRelayToken();
49
50
  if (!token) throw new Error("Usage: agent-relay token verify TOKEN");
50
51
  const payload = decodeJwtPayload(token);
51
52
  if (!payload) throw new Error("not a component JWT");
@@ -7,13 +7,14 @@ import { describeWorkspacePhase, readyContract, type WorkspacePhaseView } from "
7
7
  import type { WorkspaceDepsRefreshResult } from "agent-relay-sdk";
8
8
  import { AUTO_MERGE_POLICIES } from "../workspace-merge";
9
9
  import type { WorkspaceAutoMergePolicy } from "../types";
10
+ import { workspaceJsonFromEnv } from "../config";
10
11
 
11
12
  export const WORKSPACE_USAGE = "Usage: agent-relay workspace <status|diagnostics|ready|land|claim|release|list|cleanup-stale|deps> [--id ID] [--strategy ...] [--auto-merge on-green|on-approval|manual] [--reviewer HANDLE] [--purpose TEXT] [--repo PATH] [--wait] [--timeout SECONDS] [--check] [--execute] [--json]";
12
13
 
13
14
  // The agent's own isolated-workspace id, published in AGENT_RELAY_WORKSPACE_JSON
14
15
  // by the orchestrator at spawn. Undefined for shared-workspace / non-managed agents.
15
16
  function currentWorkspaceId(): string | undefined {
16
- const json = process.env.AGENT_RELAY_WORKSPACE_JSON;
17
+ const json = workspaceJsonFromEnv();
17
18
  if (!json) return undefined;
18
19
  try {
19
20
  const parsed = JSON.parse(json) as { id?: string };
package/src/config.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  // Shared runtime constants. Import from here rather than redefining.
2
2
 
3
3
  import { readFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
4
5
  import { join, dirname } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
7
+ import { DEFAULT_RELAY_URL, RELAY_TOKEN_HEADER } from "agent-relay-sdk";
6
8
  import { CONTRACT_VERSIONS } from "./contracts";
7
9
 
8
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -19,6 +21,31 @@ function envPositiveInt(name: string, fallback: number): number {
19
21
  return Math.floor(parsed);
20
22
  }
21
23
 
24
+ function envNumberOrDefault(name: string, fallback: number): number {
25
+ return Number(process.env[name]) || fallback;
26
+ }
27
+
28
+ function envNonNegativeNumber(name: string, fallback: number): number {
29
+ const parsed = Number(process.env[name]);
30
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
31
+ }
32
+
33
+ function envSafePositiveInteger(name: string, fallback: number): number {
34
+ const raw = process.env[name];
35
+ if (!raw) return fallback;
36
+ const parsed = Number(raw);
37
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
38
+ }
39
+
40
+ function envTrimmed(name: string): string {
41
+ return (process.env[name] || "").trim();
42
+ }
43
+
44
+ function envTrimmedOrUndefined(name: string): string | undefined {
45
+ const value = process.env[name]?.trim();
46
+ return value || undefined;
47
+ }
48
+
22
49
  export const STALE_TTL_MS = envPositiveInt("STALE_TTL_MS", 120_000); // 2min without heartbeat → offline
23
50
  // #451 — default TTL for fire-and-forget FAN-OUT messages (broadcast/tag/cap/label/team) that the
24
51
  // sender didn't bound. A stale fan-out push must not wake an agent that matches the target hours
@@ -42,12 +69,160 @@ export const MAIN_MERGE_PAUSE_TTL_MS = envPositiveInt("AGENT_RELAY_MAIN_MERGE_PA
42
69
  export const MAX_BODY_BYTES = 64 * 1024;
43
70
  export const INTEGRATION_RATE_LIMIT_PER_MINUTE = envPositiveInt("AGENT_RELAY_INTEGRATION_RATE_LIMIT_PER_MINUTE", 120);
44
71
 
45
- export const AUTH_TOKEN = process.env.AGENT_RELAY_TOKEN || "";
72
+ export const AUTOMATION_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_AUTOMATION_INTERVAL_MS", 30 * 1000);
73
+ export const IDLE_TIMEOUT_SECONDS = envNumberOrDefault("AGENT_RELAY_IDLE_TIMEOUT_SECONDS", 255);
74
+ export const LOG_REQUESTS = process.env.AGENT_RELAY_LOG_REQUESTS === "1";
75
+
76
+ const AUTH_TOKEN = process.env.AGENT_RELAY_TOKEN || "";
46
77
  export const CORS_ORIGINS = (process.env.AGENT_RELAY_CORS_ORIGINS || "")
47
78
  .split(",")
48
79
  .map((origin) => origin.trim())
49
80
  .filter(Boolean);
50
81
 
82
+ export function relayToken(): string {
83
+ return process.env.AGENT_RELAY_TOKEN ?? AUTH_TOKEN;
84
+ }
85
+
86
+ export function relayAuthHeaders(base: Record<string, string> = {}): Record<string, string> {
87
+ const token = relayToken();
88
+ return token ? { ...base, [RELAY_TOKEN_HEADER]: token } : base;
89
+ }
90
+
91
+ export function allowUnauthenticated(): boolean {
92
+ return process.env.AGENT_RELAY_ALLOW_UNAUTH === "1";
93
+ }
94
+
95
+ export function tokenSecretFromEnv(): string | undefined {
96
+ return process.env.AGENT_RELAY_TOKEN_SECRET;
97
+ }
98
+
99
+ export function corsOriginsFromEnv(): string[] | undefined {
100
+ const raw = process.env.AGENT_RELAY_CORS_ORIGINS;
101
+ if (raw === undefined) return undefined;
102
+ return raw.split(",").map((origin) => origin.trim()).filter(Boolean);
103
+ }
104
+
105
+ export function connectorsDirFromEnv(): string | undefined {
106
+ return process.env.AGENT_RELAY_CONNECTORS_DIR;
107
+ }
108
+
109
+ export function connectorActionTimeoutMs(fallback: number): number {
110
+ const raw = Number(process.env.AGENT_RELAY_CONNECTOR_ACTION_TIMEOUT_MS);
111
+ return Number.isFinite(raw) && raw > 0 ? raw : fallback;
112
+ }
113
+
114
+ export function rateLimitFallbackMs(): number {
115
+ return envNumberOrDefault("AGENT_RELAY_RATE_LIMIT_FALLBACK_MS", 60 * 60 * 1000);
116
+ }
117
+
118
+ export function artifactsDir(): string {
119
+ return process.env.AGENT_RELAY_ARTIFACTS_DIR || "~/.agent-relay/artifacts";
120
+ }
121
+
122
+ export function maxArtifactBytesFromEnv(fallback: number): number {
123
+ return envSafePositiveInteger("AGENT_RELAY_MAX_ARTIFACT_BYTES", fallback);
124
+ }
125
+
126
+ export const OUTBOX_RETENTION_MS = envNumberOrDefault("AGENT_RELAY_OUTBOX_RETENTION_MS", 60 * 60 * 1000);
127
+ export const TOKEN_RECORD_RETENTION_SECONDS = envNumberOrDefault("AGENT_RELAY_TOKEN_RECORD_RETENTION_SECONDS", 7 * 24 * 60 * 60);
128
+ export const CONFLICT_SCAN_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_CONFLICT_SCAN_INTERVAL_MS", 10 * 60 * 1000);
129
+ export const WORKSPACE_RETENTION_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_RETENTION_MS", DAY_MS);
130
+ export const DB_MAINTENANCE_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_DB_MAINTENANCE_INTERVAL_MS", DAY_MS);
131
+ export const DB_VACUUM_EVERY = envNumberOrDefault("AGENT_RELAY_DB_VACUUM_EVERY", 7);
132
+ export const WORKSPACE_REVIEW_TTL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_REVIEW_TTL_MS", 3 * DAY_MS);
133
+ export const WORKSPACE_GC_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_GC_INTERVAL_MS", 60 * 60 * 1000);
134
+ export const WORKSPACE_ORPHAN_REAPER_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_ORPHAN_REAPER_INTERVAL_MS", 30 * 60 * 1000);
135
+ export const WORKSPACE_AUTO_MERGE_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_AUTO_MERGE_INTERVAL_MS", CONFLICT_SCAN_INTERVAL_MS);
136
+ export const ORPHAN_REAPER_INTERVAL_MS = envNumberOrDefault("AGENT_RELAY_ORPHAN_REAPER_INTERVAL_MS", 5 * 60 * 1000);
137
+ export const STEWARD_WAKE_COOLDOWN_MS = envNumberOrDefault("AGENT_RELAY_STEWARD_WAKE_COOLDOWN_MS", 10 * 60 * 1000);
138
+ export const UNLANDED_FLAG_COOLDOWN_MS = envNumberOrDefault("AGENT_RELAY_ORPHAN_FLAG_COOLDOWN_MS", 6 * 60 * 60 * 1000);
139
+ export const STEWARD_CLAIM_TTL_MS = envNumberOrDefault("AGENT_RELAY_WORKSPACE_CLAIM_TTL_MS", 15 * 60_000);
140
+ export const DB_SLOW_QUERY_MS = envNumberOrDefault("AGENT_RELAY_DB_SLOW_MS", 0);
141
+
142
+ export function stewardEscalationMs(): number {
143
+ return envNumberOrDefault("AGENT_RELAY_WORKSPACE_STEWARD_ESCALATION_MS", 60 * 60 * 1000);
144
+ }
145
+
146
+ export function stewardFallbackTarget(): string {
147
+ return envTrimmed("AGENT_RELAY_WORKSPACE_STEWARD_FALLBACK");
148
+ }
149
+
150
+ export function envMsOrDefault(name: string, fallback: number): number {
151
+ return envNonNegativeNumber(name, fallback);
152
+ }
153
+
154
+ export function orphanGraceMs(): number {
155
+ return envMsOrDefault("AGENT_RELAY_ORPHAN_GRACE_MS", 30 * 60 * 1000);
156
+ }
157
+
158
+ export function orphanReapCooldownMs(): number {
159
+ return envMsOrDefault("AGENT_RELAY_ORPHAN_REAP_COOLDOWN_MS", 5 * 60 * 1000);
160
+ }
161
+
162
+ export function orphanReapEnabled(): boolean {
163
+ return process.env.AGENT_RELAY_ORPHAN_REAP !== "0";
164
+ }
165
+
166
+ export function orphanWorktreeReapEnabled(): boolean {
167
+ return process.env.AGENT_RELAY_ORPHAN_WORKTREE_REAP !== "0";
168
+ }
169
+
170
+ export function workspaceAutoMergeEnabled(): boolean {
171
+ return process.env.AGENT_RELAY_WORKSPACE_AUTO_MERGE !== "0";
172
+ }
173
+
174
+ export function continuationArchiveFtsEnabled(): boolean {
175
+ return process.env.AGENT_RELAY_DISABLE_CONTINUATION_ARCHIVE_FTS !== "1";
176
+ }
177
+
178
+ export function busStaleGraceMs(): number {
179
+ return envNumberOrDefault("AGENT_RELAY_STALE_GRACE_MS", 120_000);
180
+ }
181
+
182
+ export function defaultIssueRepo(): string | undefined {
183
+ return process.env.AGENT_RELAY_DEFAULT_ISSUE_REPO || process.env.GITHUB_REPOSITORY || undefined;
184
+ }
185
+
186
+ export function cliRelayUrl(): string {
187
+ return process.env.AGENT_RELAY_URL || DEFAULT_RELAY_URL;
188
+ }
189
+
190
+ export function cliRelayToken(): string | undefined {
191
+ return process.env.AGENT_RELAY_TOKEN;
192
+ }
193
+
194
+ export function workspaceJsonFromEnv(): string | undefined {
195
+ return process.env.AGENT_RELAY_WORKSPACE_JSON;
196
+ }
197
+
198
+ export function providerSessionIdFromEnv(): string | undefined {
199
+ return process.env.AGENT_RELAY_PROVIDER_SESSION_ID;
200
+ }
201
+
202
+ export function projectFromEnv(): string | undefined {
203
+ return process.env.AGENT_RELAY_PROJECT;
204
+ }
205
+
206
+ export function agentIdFromEnv(): string | undefined {
207
+ return process.env.AGENT_RELAY_ID;
208
+ }
209
+
210
+ export function codexStatePathFromEnv(): string | undefined {
211
+ return process.env.AGENT_RELAY_CODEX_STATE_PATH;
212
+ }
213
+
214
+ export function contextPathFromEnv(): string | undefined {
215
+ return process.env.AGENT_RELAY_CONTEXT_PATH;
216
+ }
217
+
218
+ export function localOrchestratorIdFromEnv(): string | undefined {
219
+ return envTrimmedOrUndefined("AGENT_RELAY_ORCHESTRATOR_ID");
220
+ }
221
+
222
+ export function providerConfigHomeFromEnv(): string {
223
+ return process.env.AGENT_RELAY_HOME || join(process.env.HOME || ".", ".agent-relay");
224
+ }
225
+
51
226
  export type IntegrationTokenConfig = {
52
227
  name: string;
53
228
  token: string;
package/src/connectors.ts CHANGED
@@ -5,6 +5,7 @@ import type { ConnectorAction, ConnectorActionResult, ConnectorManifest, Connect
5
5
  import { ValidationError } from "./db";
6
6
  import { isRecord } from "agent-relay-sdk";
7
7
  import { registerConnectorConfigSetting, validateConnectorConfig } from "./settings/modules";
8
+ import { connectorActionTimeoutMs as configuredConnectorActionTimeoutMs, connectorsDirFromEnv } from "./config";
8
9
 
9
10
  const CONNECTOR_SCHEMA = "agent-relay.connector.v1";
10
11
  const VALID_KINDS = new Set(["channel", "event", "provider", "orchestrator"]);
@@ -12,7 +13,7 @@ const VALID_ACTIONS = new Set(["install", "uninstall", "enable", "disable", "sta
12
13
  const DEFAULT_ACTION_TIMEOUT_MS = 30_000;
13
14
 
14
15
  function connectorRegistryDir(): string {
15
- const configured = process.env.AGENT_RELAY_CONNECTORS_DIR;
16
+ const configured = connectorsDirFromEnv();
16
17
  if (configured) return resolveHome(configured);
17
18
  return join(homedir(), ".agent-relay", "connectors");
18
19
  }
@@ -177,8 +178,7 @@ export function writeConnectorConfig(id: string, config: Record<string, unknown>
177
178
  }
178
179
 
179
180
  function connectorActionTimeoutMs(): number {
180
- const raw = Number(process.env.AGENT_RELAY_CONNECTOR_ACTION_TIMEOUT_MS);
181
- return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_ACTION_TIMEOUT_MS;
181
+ return configuredConnectorActionTimeoutMs(DEFAULT_ACTION_TIMEOUT_MS);
182
182
  }
183
183
 
184
184
  const LIFECYCLE_ACTIONS = new Set(["install", "uninstall", "start", "stop", "restart", "enable", "disable"]);
@@ -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
+ }