agent-relay-server 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/public/index.html CHANGED
@@ -305,7 +305,7 @@
305
305
  </div>
306
306
 
307
307
  <!-- Main content -->
308
- <main class="ar-main flex-grow-1 overflow-auto">
308
+ <main class="ar-main flex-grow-1" style="overflow-y:scroll;overflow-x:hidden">
309
309
  <div class="container-xl py-3">
310
310
  <template x-if="authNeeded">
311
311
  <div class="alert alert-warning d-flex align-items-center gap-2 mb-3">
package/src/config.ts CHANGED
@@ -22,6 +22,7 @@ export const STALE_TTL_MS = envPositiveInt("STALE_TTL_MS", 120_000); // 2min wit
22
22
  export const OFFLINE_PRUNE_MS = envPositiveInt("OFFLINE_PRUNE_MS", DAY_MS); // 24h offline → delete
23
23
  export const REAP_INTERVAL_MS = envPositiveInt("REAP_INTERVAL_MS", 60_000); // reaper cadence
24
24
  export const CLAIM_LEASE_MS = envPositiveInt("AGENT_RELAY_CLAIM_LEASE_MS", 1_800_000); // 30min claim lease
25
+ export const POOL_CLAIM_LEASE_MS = envPositiveInt("AGENT_RELAY_POOL_CLAIM_LEASE_MS", STALE_TTL_MS * 3); // pool binding lease
25
26
 
26
27
  // Max body size for any POST/PATCH request (64 KiB).
27
28
  export const MAX_BODY_BYTES = 64 * 1024;
package/src/db.ts CHANGED
@@ -41,7 +41,7 @@ import type {
41
41
  InboxThreadState,
42
42
  TaskStatusInput,
43
43
  } from "./types";
44
- import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS } from "./config";
44
+ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS } from "./config";
45
45
 
46
46
  let db: Database;
47
47
 
@@ -307,6 +307,14 @@ export function initDb(path: string = "agent-relay.db"): Database {
307
307
  db.run("CREATE INDEX IF NOT EXISTS idx_channel_bindings_channel ON channel_bindings(channel_id, priority)");
308
308
  db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_channel_bindings_target ON channel_bindings(channel_id, conversation_key, target_type, target_id)");
309
309
 
310
+ const bindingColNames = (db.prepare("PRAGMA table_info(channel_bindings)").all() as any[]).map((c: any) => c.name);
311
+ if (!bindingColNames.includes("pool_selector")) {
312
+ db.run("ALTER TABLE channel_bindings ADD COLUMN pool_selector TEXT");
313
+ db.run("ALTER TABLE channel_bindings ADD COLUMN pool_agent_id TEXT");
314
+ db.run("ALTER TABLE channel_bindings ADD COLUMN pool_agent_epoch INTEGER");
315
+ db.run("ALTER TABLE channel_bindings ADD COLUMN pool_claim_expires_at INTEGER");
316
+ }
317
+
310
318
  if (!colNames.includes("kind")) {
311
319
  db.run("ALTER TABLE messages ADD COLUMN kind TEXT NOT NULL DEFAULT 'chat'");
312
320
  }
@@ -608,7 +616,7 @@ function rowToChannelBinding(row: any): ChannelBinding {
608
616
  const target = row.target_type === "broadcast"
609
617
  ? { type: "broadcast" } as ChannelRouteTarget
610
618
  : { type: row.target_type, id: row.target_id } as ChannelRouteTarget;
611
- return {
619
+ const binding: ChannelBinding = {
612
620
  id: row.id,
613
621
  channelId: row.channel_id,
614
622
  conversationId: row.conversation_id ?? undefined,
@@ -618,6 +626,13 @@ function rowToChannelBinding(row: any): ChannelBinding {
618
626
  createdAt: row.created_at,
619
627
  updatedAt: row.updated_at,
620
628
  };
629
+ if (row.pool_selector) {
630
+ binding.poolSelector = row.pool_selector;
631
+ binding.poolAgentId = row.pool_agent_id ?? undefined;
632
+ binding.poolAgentEpoch = row.pool_agent_epoch ?? undefined;
633
+ binding.poolClaimExpiresAt = row.pool_claim_expires_at ?? undefined;
634
+ }
635
+ return binding;
621
636
  }
622
637
 
623
638
  function bindingTargetToLegacyTarget(target: ChannelRouteTarget): string {
@@ -627,6 +642,7 @@ function bindingTargetToLegacyTarget(target: ChannelRouteTarget): string {
627
642
  if (target.type === "capability") return `cap:${target.id}`;
628
643
  if (target.type === "broadcast") return "broadcast";
629
644
  if (target.type === "orchestrator") return `orchestrator:${target.id}`;
645
+ if (target.type === "pool") return `pool:${target.id}`;
630
646
  return "";
631
647
  }
632
648
 
@@ -641,6 +657,7 @@ function channelTargetMatches(target: ChannelRouteTarget): AgentCard[] {
641
657
  const agent = getAgent(target.id);
642
658
  return agent ? [agent] : [];
643
659
  }
660
+ if (target.type === "pool") return poolSelectorMatches(target.id, candidates);
644
661
  if (target.type === "label") return candidates.filter((agent) => agent.label === target.id);
645
662
  if (target.type === "tag") return candidates.filter((agent) => agent.tags.includes(target.id));
646
663
  if (target.type === "capability") return candidates.filter((agent) => agent.capabilities.includes(target.id));
@@ -648,6 +665,14 @@ function channelTargetMatches(target: ChannelRouteTarget): AgentCard[] {
648
665
  return [];
649
666
  }
650
667
 
668
+ function poolSelectorMatches(selector: string, candidates: AgentCard[]): AgentCard[] {
669
+ if (selector.startsWith("label:")) return candidates.filter((a) => a.label === selector.slice("label:".length));
670
+ if (selector.startsWith("tag:")) return candidates.filter((a) => a.tags.includes(selector.slice("tag:".length)));
671
+ if (selector.startsWith("cap:")) return candidates.filter((a) => a.capabilities.includes(selector.slice("cap:".length)));
672
+ const agent = getAgent(selector);
673
+ return agent ? [agent] : [];
674
+ }
675
+
651
676
  function channelTargetMatchSnapshot(agent: AgentCard): ChannelTargetHealth["matches"][number] {
652
677
  return {
653
678
  id: agent.id,
@@ -675,6 +700,25 @@ function channelTargetHealth(binding: ChannelBinding, now: number = Date.now()):
675
700
  const matches = channelTargetMatches(target);
676
701
  const snapshots = matches.map(channelTargetMatchSnapshot);
677
702
 
703
+ if (target.type === "pool") {
704
+ const healthyEligibles = matches.filter((a) => isHealthyChannelTarget(a, now));
705
+ if (!binding.poolAgentId) {
706
+ if (matches.length === 0) return { status: "error", detail: `Pool ${targetLabel}: no eligible agents`, target, matches: [] };
707
+ return { status: "error", detail: `Pool ${targetLabel}: ${healthyEligibles.length} eligible but slot unclaimed`, target, matches: snapshots };
708
+ }
709
+ const holder = getAgent(binding.poolAgentId);
710
+ if (!holder || holder.status === "offline") {
711
+ return { status: "error", detail: `Pool ${targetLabel}: holder ${binding.poolAgentId} is offline`, target, matches: snapshots };
712
+ }
713
+ if (!holder.ready) {
714
+ return { status: "warning", detail: `Pool ${targetLabel}: holder ${binding.poolAgentId} online but not ready`, target, matches: snapshots };
715
+ }
716
+ if (binding.poolAgentEpoch !== undefined && holder.epoch !== binding.poolAgentEpoch) {
717
+ return { status: "warning", detail: `Pool ${targetLabel}: holder epoch changed (stale claim)`, target, matches: snapshots };
718
+ }
719
+ return { status: "ok", detail: `Pool ${targetLabel}: claimed by ${binding.poolAgentId}`, target, matches: snapshots };
720
+ }
721
+
678
722
  if (target.type === "agent" || target.type === "orchestrator") {
679
723
  const agent = matches[0];
680
724
  if (!agent) {
@@ -729,6 +773,10 @@ function rowToChannelSummary(row: any): ChannelSummary {
729
773
  priority: row.binding_priority,
730
774
  created_at: row.binding_created_at,
731
775
  updated_at: row.binding_updated_at,
776
+ pool_selector: row.binding_pool_selector,
777
+ pool_agent_id: row.binding_pool_agent_id,
778
+ pool_agent_epoch: row.binding_pool_agent_epoch,
779
+ pool_claim_expires_at: row.binding_pool_claim_expires_at,
732
780
  }) : undefined;
733
781
 
734
782
  return {
@@ -817,6 +865,7 @@ export function upsertAgent(input: RegisterAgentInput): AgentCard {
817
865
 
818
866
  const agent = getAgent(input.id)!;
819
867
  if (agent.kind === "channel") upsertChannelForAgent(agent);
868
+ evaluatePoolBindings();
820
869
  return agent;
821
870
  }
822
871
 
@@ -949,6 +998,7 @@ function channelDirectionForAgent(agent: AgentCard): ChannelSummary["direction"]
949
998
 
950
999
  function routeTargetFromLegacyTarget(target: string | undefined): ChannelRouteTarget | undefined {
951
1000
  if (!target) return undefined;
1001
+ if (target.startsWith("pool:")) return { type: "pool", id: target.slice("pool:".length) };
952
1002
  if (target.startsWith("label:")) return { type: "label", id: target.slice("label:".length) };
953
1003
  if (target.startsWith("tag:")) return { type: "tag", id: target.slice("tag:".length) };
954
1004
  if (target.startsWith("cap:")) return { type: "capability", id: target.slice("cap:".length) };
@@ -1002,7 +1052,7 @@ function upsertChannelForAgent(agent: AgentCard): void {
1002
1052
  upsertChannelBinding({
1003
1053
  channelId,
1004
1054
  target: defaultTarget,
1005
- mode: defaultTarget.type === "agent" ? "exclusive" : defaultTarget.type === "broadcast" ? "broadcast" : "claimable",
1055
+ mode: (defaultTarget.type === "agent" || defaultTarget.type === "pool") ? "exclusive" : defaultTarget.type === "broadcast" ? "broadcast" : "claimable",
1006
1056
  });
1007
1057
  }
1008
1058
  }
@@ -1024,7 +1074,11 @@ export function listChannels(): ChannelSummary[] {
1024
1074
  b.mode AS binding_mode,
1025
1075
  b.priority AS binding_priority,
1026
1076
  b.created_at AS binding_created_at,
1027
- b.updated_at AS binding_updated_at
1077
+ b.updated_at AS binding_updated_at,
1078
+ b.pool_selector AS binding_pool_selector,
1079
+ b.pool_agent_id AS binding_pool_agent_id,
1080
+ b.pool_agent_epoch AS binding_pool_agent_epoch,
1081
+ b.pool_claim_expires_at AS binding_pool_claim_expires_at
1028
1082
  FROM channels c
1029
1083
  JOIN agents a ON a.id = c.agent_id
1030
1084
  LEFT JOIN channel_bindings b ON b.channel_id = c.id AND b.conversation_key = ''
@@ -1057,17 +1111,20 @@ export function upsertChannelBinding(input: {
1057
1111
  const targetKey = input.target.type === "broadcast" ? "broadcast" : `${input.target.type}:${targetId}`;
1058
1112
  const id = `${input.channelId}:${conversationKey || "default"}:${targetKey}`;
1059
1113
  const mode = input.mode ?? "exclusive";
1114
+ const isPool = input.target.type === "pool";
1115
+ const poolSelector = isPool ? targetId : null;
1060
1116
  const now = Date.now();
1061
1117
  db.transaction(() => {
1062
1118
  if (mode === "exclusive") {
1063
1119
  db.prepare("DELETE FROM channel_bindings WHERE channel_id = ? AND conversation_key = ?").run(input.channelId, conversationKey);
1064
1120
  }
1065
1121
  db.prepare(`
1066
- INSERT INTO channel_bindings (id, channel_id, conversation_key, conversation_id, target_type, target_id, mode, priority, created_at, updated_at)
1067
- VALUES ($id, $channelId, $conversationKey, $conversationId, $targetType, $targetId, $mode, $priority, $now, $now)
1122
+ INSERT INTO channel_bindings (id, channel_id, conversation_key, conversation_id, target_type, target_id, mode, priority, pool_selector, created_at, updated_at)
1123
+ VALUES ($id, $channelId, $conversationKey, $conversationId, $targetType, $targetId, $mode, $priority, $poolSelector, $now, $now)
1068
1124
  ON CONFLICT(channel_id, conversation_key, target_type, target_id) DO UPDATE SET
1069
1125
  mode = $mode,
1070
1126
  priority = $priority,
1127
+ pool_selector = $poolSelector,
1071
1128
  updated_at = $now
1072
1129
  `).run({
1073
1130
  $id: id,
@@ -1078,6 +1135,7 @@ export function upsertChannelBinding(input: {
1078
1135
  $targetId: targetId,
1079
1136
  $mode: mode,
1080
1137
  $priority: input.priority ?? 0,
1138
+ $poolSelector: poolSelector,
1081
1139
  $now: now,
1082
1140
  });
1083
1141
  })();
@@ -1086,6 +1144,67 @@ export function upsertChannelBinding(input: {
1086
1144
  return rowToChannelBinding(row);
1087
1145
  }
1088
1146
 
1147
+ export interface PoolBindingChange {
1148
+ bindingId: string;
1149
+ channelId: string;
1150
+ previousAgentId: string | null;
1151
+ newAgentId: string | null;
1152
+ }
1153
+
1154
+ export function evaluatePoolBindings(now: number = Date.now()): PoolBindingChange[] {
1155
+ const rows = db.prepare("SELECT * FROM channel_bindings WHERE target_type = 'pool'").all() as any[];
1156
+ const changes: PoolBindingChange[] = [];
1157
+
1158
+ for (const row of rows) {
1159
+ const bindingId = row.id as string;
1160
+ const channelId = row.channel_id as string;
1161
+ const selector = row.pool_selector ?? row.target_id;
1162
+ const currentAgentId: string | null = row.pool_agent_id;
1163
+ const currentEpoch: number | null = row.pool_agent_epoch;
1164
+
1165
+ let holderValid = false;
1166
+ if (currentAgentId) {
1167
+ const holder = getAgent(currentAgentId);
1168
+ if (holder && holder.status !== "offline" && holder.ready && holder.lastSeen > now - STALE_TTL_MS) {
1169
+ if (currentEpoch === null || holder.epoch === currentEpoch) {
1170
+ db.prepare("UPDATE channel_bindings SET pool_claim_expires_at = ? WHERE id = ?")
1171
+ .run(now + POOL_CLAIM_LEASE_MS, bindingId);
1172
+ holderValid = true;
1173
+ }
1174
+ }
1175
+ }
1176
+
1177
+ if (!holderValid && currentAgentId) {
1178
+ db.prepare("UPDATE channel_bindings SET pool_agent_id = NULL, pool_agent_epoch = NULL, pool_claim_expires_at = NULL WHERE id = ?")
1179
+ .run(bindingId);
1180
+ changes.push({ bindingId, channelId, previousAgentId: currentAgentId, newAgentId: null });
1181
+ }
1182
+
1183
+ if (!holderValid) {
1184
+ const candidates = listAgents().filter((a) =>
1185
+ a.id !== "user" && a.id !== "system" && a.kind !== "channel" && a.meta?.kind !== "channel"
1186
+ );
1187
+ const eligible = poolSelectorMatches(selector, candidates)
1188
+ .filter((a) => a.status !== "offline" && a.ready && a.lastSeen > now - STALE_TTL_MS)
1189
+ .sort((a, b) => b.lastSeen - a.lastSeen);
1190
+
1191
+ if (eligible.length > 0) {
1192
+ const picked = eligible[0]!;
1193
+ db.prepare("UPDATE channel_bindings SET pool_agent_id = ?, pool_agent_epoch = ?, pool_claim_expires_at = ? WHERE id = ?")
1194
+ .run(picked.id, picked.epoch, now + POOL_CLAIM_LEASE_MS, bindingId);
1195
+ const lastChange = changes[changes.length - 1];
1196
+ if (lastChange && lastChange.bindingId === bindingId && lastChange.newAgentId === null) {
1197
+ lastChange.newAgentId = picked.id;
1198
+ } else {
1199
+ changes.push({ bindingId, channelId, previousAgentId: currentAgentId, newAgentId: picked.id });
1200
+ }
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ return changes;
1206
+ }
1207
+
1089
1208
  export function resolveChannelRoutes(channelId: string, conversationId?: string): ChannelBinding[] {
1090
1209
  const rows = conversationId
1091
1210
  ? db.prepare(`
@@ -2503,6 +2622,7 @@ export function upsertOrchestrator(input: RegisterOrchestratorInput): Orchestrat
2503
2622
  machine: input.hostname,
2504
2623
  capabilities: ["orchestrator", "spawn"],
2505
2624
  status: "online",
2625
+ ready: true,
2506
2626
  meta: {
2507
2627
  orchestratorId: input.id,
2508
2628
  builtin: true,
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
- import { initDb, reapStaleAgents, pruneOfflineAgents, pruneOldMessages, releaseExpiredClaims } from "./db";
2
+ import { initDb, reapStaleAgents, pruneOfflineAgents, pruneOldMessages, releaseExpiredClaims, evaluatePoolBindings } from "./db";
3
3
  import { matchRoute } from "./routes";
4
- import { emitAgentStatus, emitAgentRemoved, emitMessageClaimReleased, emitTaskChanged } from "./sse";
4
+ import { emitAgentStatus, emitAgentRemoved, emitMessageClaimReleased, emitTaskChanged, emitPoolBindingChanged } from "./sse";
5
5
  import { resolve, sep } from "path";
6
6
  import {
7
7
  REAP_INTERVAL_MS,
@@ -57,6 +57,12 @@ function startServer(): void {
57
57
  console.log(`pruned ${pruned.length} offline agent(s)`);
58
58
  for (const id of pruned) emitAgentRemoved(id);
59
59
  }
60
+
61
+ const poolChanges = evaluatePoolBindings();
62
+ for (const change of poolChanges) {
63
+ console.log(`pool binding ${change.bindingId}: ${change.previousAgentId ?? "none"} → ${change.newAgentId ?? "none"}`);
64
+ emitPoolBindingChanged(change.bindingId, change.channelId, change.previousAgentId, change.newAgentId);
65
+ }
60
66
  }, REAP_INTERVAL_MS);
61
67
 
62
68
  // Daily message prune
package/src/routes.ts CHANGED
@@ -60,6 +60,7 @@ import {
60
60
  orchestratorHeartbeat,
61
61
  updateManagedAgents,
62
62
  deleteOrchestrator,
63
+ evaluatePoolBindings,
63
64
  ValidationError,
64
65
  } from "./db";
65
66
  import {
@@ -90,6 +91,7 @@ import {
90
91
  emitChannelActivity,
91
92
  emitOrchestratorStatus,
92
93
  emitOrchestratorRemoved,
94
+ emitPoolBindingChanged,
93
95
  } from "./sse";
94
96
 
95
97
  type Handler = (
@@ -175,7 +177,7 @@ function parseQueryInt(
175
177
 
176
178
  const VALID_AGENT_STATUSES = ["online", "idle", "busy", "offline"] as const;
177
179
  const VALID_AGENT_KINDS = ["provider", "channel", "orchestrator", "system", "user"] as const;
178
- const VALID_CHANNEL_BINDING_TARGET_TYPES = ["agent", "label", "tag", "capability", "broadcast", "orchestrator"] as const;
180
+ const VALID_CHANNEL_BINDING_TARGET_TYPES = ["agent", "label", "tag", "capability", "broadcast", "orchestrator", "pool"] as const;
179
181
  const VALID_CHANNEL_BINDING_MODES = ["exclusive", "claimable", "broadcast"] as const;
180
182
  const VALID_AGENT_ACTIONS = ["restart", "shutdown"] as const;
181
183
  const VALID_AGENT_SPAWN_PROVIDERS = ["codex"] as const;
@@ -404,6 +406,7 @@ function normalizeChannelBindingInput(body: unknown): {
404
406
  }
405
407
 
406
408
  function routeTargetFromAddress(target: string): ChannelRouteTarget {
409
+ if (target.startsWith("pool:")) return { type: "pool", id: target.slice("pool:".length) };
407
410
  if (target.startsWith("label:")) return { type: "label", id: target.slice("label:".length) };
408
411
  if (target.startsWith("tag:")) return { type: "tag", id: target.slice("tag:".length) };
409
412
  if (target.startsWith("cap:")) return { type: "capability", id: target.slice("cap:".length) };
@@ -412,8 +415,12 @@ function routeTargetFromAddress(target: string): ChannelRouteTarget {
412
415
  return { type: "agent", id: target };
413
416
  }
414
417
 
415
- function messageTargetForChannelTarget(target: ChannelRouteTarget): string {
418
+ function messageTargetForChannelTarget(target: ChannelRouteTarget, binding?: ChannelBinding): string {
416
419
  if (target.type === "orchestrator") throw new ValidationError("orchestrator channel targets are not supported yet");
420
+ if (target.type === "pool") {
421
+ if (!binding?.poolAgentId) throw new ValidationError("pool slot is unclaimed — no eligible agent available");
422
+ return binding.poolAgentId;
423
+ }
417
424
  if (target.type === "label") return `label:${target.id}`;
418
425
  if (target.type === "tag") return `tag:${target.id}`;
419
426
  if (target.type === "capability") return `cap:${target.id}`;
@@ -1762,7 +1769,7 @@ const postChannelEvent: Handler = async (req, params) => {
1762
1769
  if (!bindings.length) return error("channel has no binding", 409);
1763
1770
  const results = bindings.map((binding) => sendMessageWithResult({
1764
1771
  from: channel.agentId,
1765
- to: messageTargetForChannelTarget(binding.target),
1772
+ to: messageTargetForChannelTarget(binding.target, binding),
1766
1773
  kind: "channel.event",
1767
1774
  channel: channel.id,
1768
1775
  body: input.body,
@@ -2123,6 +2130,8 @@ const postSystemReap: Handler = () => {
2123
2130
  for (const task of released.tasks) emitTaskChanged(task, "task.updated");
2124
2131
  for (const id of reapedAgentIds) emitAgentStatus(id);
2125
2132
  for (const id of reapedOrchestratorIds) emitOrchestratorStatus(id);
2133
+ const poolChanges = evaluatePoolBindings();
2134
+ for (const change of poolChanges) emitPoolBindingChanged(change.bindingId, change.channelId, change.previousAgentId, change.newAgentId);
2126
2135
  auditEvent({
2127
2136
  clientId: "server-system-reap-" + Date.now(),
2128
2137
  kind: "state",
package/src/sse.ts CHANGED
@@ -155,3 +155,9 @@ export function emitOrchestratorRemoved(orchestratorId: string) {
155
155
  send(conn, "orchestrator.removed", { id: orchestratorId });
156
156
  }
157
157
  }
158
+
159
+ export function emitPoolBindingChanged(bindingId: string, channelId: string, previousAgentId: string | null, newAgentId: string | null) {
160
+ for (const conn of connections.values()) {
161
+ send(conn, "channel.pool.changed", { bindingId, channelId, previousAgentId, newAgentId, at: Date.now() });
162
+ }
163
+ }
package/src/types.ts CHANGED
@@ -275,7 +275,8 @@ export type ChannelRouteTarget =
275
275
  | { type: "tag"; id: string }
276
276
  | { type: "capability"; id: string }
277
277
  | { type: "broadcast" }
278
- | { type: "orchestrator"; id: string };
278
+ | { type: "orchestrator"; id: string }
279
+ | { type: "pool"; id: string };
279
280
 
280
281
  export type ChannelBindingMode = "exclusive" | "claimable" | "broadcast";
281
282
 
@@ -288,6 +289,10 @@ export interface ChannelBinding {
288
289
  priority: number;
289
290
  createdAt: number;
290
291
  updatedAt: number;
292
+ poolSelector?: string;
293
+ poolAgentId?: string;
294
+ poolAgentEpoch?: number;
295
+ poolClaimExpiresAt?: number;
291
296
  }
292
297
 
293
298
  export interface ChannelTargetHealth {