agent-relay-server 0.10.26 → 0.10.27

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/db.ts +35 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.10.26",
3
+ "version": "0.10.27",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
package/src/db.ts CHANGED
@@ -2173,6 +2173,34 @@ export function reapStaleAgents(ttlMs: number = STALE_TTL_MS): string[] {
2173
2173
  return rows.map((r: any) => r.id);
2174
2174
  }
2175
2175
 
2176
+ // On-demand automation tasks (targetMode=on_demand_agent) are bound to a single
2177
+ // ephemeral agent spawned just for that task. When that agent's claim is released —
2178
+ // clean shutdown, prune, lease expiry, or orphan-grace — the task must NOT return to
2179
+ // the claimable pool: its target is a unique `label:automation-…` that no other agent
2180
+ // will ever match, so a re-opened task lingers forever as an orphaned claim. Resolve
2181
+ // those as done instead; automation reconcile then settles the run as succeeded.
2182
+ // Run this BEFORE the generic re-open UPDATE at each release site, with the same WHERE
2183
+ // condition: it flips matching single-target tasks to 'done', which the subsequent
2184
+ // re-open (gated on status IN claimed/in_progress/blocked/orphaned) then skips.
2185
+ function settleSingleTargetOnDemandTasks(condition: string, params: any[], now: number, reason: string): void {
2186
+ const selectSql = `${TASK_SELECT} WHERE (${condition}) AND json_extract(metadata, '$.targetMode') = 'on_demand_agent' AND status IN ('claimed', 'in_progress', 'blocked', 'orphaned')`;
2187
+ const rows = db.prepare(selectSql).all(...params) as any[];
2188
+ if (rows.length === 0) return;
2189
+ db.prepare(
2190
+ `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')`
2191
+ ).run(now, now, ...params);
2192
+ for (const row of rows) {
2193
+ insertTaskEvent(row.id, {
2194
+ source: "agent-relay",
2195
+ type: "task.auto-completed",
2196
+ severity: row.severity,
2197
+ title: "On-demand task auto-resolved",
2198
+ body: `On-demand agent ${row.claimed_by ?? "(unknown)"} exited (${reason}); task resolved so it does not orphan`,
2199
+ metadata: { agentId: row.claimed_by, reason, completedBy: "relay" },
2200
+ }, now);
2201
+ }
2202
+ }
2203
+
2176
2204
  export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
2177
2205
  const cutoff = Date.now() - maxOfflineMs;
2178
2206
  return db.transaction(() => {
@@ -2191,9 +2219,12 @@ export function pruneOfflineAgents(maxOfflineMs: number = DAY_MS): string[] {
2191
2219
  )
2192
2220
  .run(cutoff);
2193
2221
 
2222
+ const offlineClaimCondition = "claimed_by IN (SELECT id FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system'))";
2223
+ settleSingleTargetOnDemandTasks(offlineClaimCondition, [cutoff], now, "agent-pruned");
2224
+
2194
2225
  db
2195
2226
  .prepare(
2196
- "UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE claimed_by IN (SELECT id FROM agents WHERE status = 'offline' AND last_seen < ? AND id NOT IN ('user', 'system')) AND status IN ('claimed', 'in_progress', 'blocked')"
2227
+ `UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE ${offlineClaimCondition} AND status IN ('claimed', 'in_progress', 'blocked')`
2197
2228
  )
2198
2229
  .run(now, cutoff);
2199
2230
 
@@ -2232,6 +2263,7 @@ export function deleteAgent(id: string): { ok: boolean; error?: string } {
2232
2263
  // from_agent is left intact as historical record.
2233
2264
  const now = Date.now();
2234
2265
  db.prepare("UPDATE messages SET claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL WHERE claimed_by = ?").run(id);
2266
+ settleSingleTargetOnDemandTasks("claimed_by = ?", [id], now, "agent-removed");
2235
2267
  db.prepare("UPDATE tasks SET status = 'open', claimed_by = NULL, claimed_at = NULL, claim_expires_at = NULL, updated_at = ? WHERE claimed_by = ? AND status IN ('claimed', 'in_progress', 'blocked')").run(now, id);
2236
2268
  revokeRuntimeTokensForAgent(id, now);
2237
2269
  closeOpenPairsForAgent(id, now);
@@ -2615,6 +2647,7 @@ export function recordTaskEvent(taskId: number, input: {
2615
2647
 
2616
2648
  export function releaseExpiredClaims(now: number = Date.now()): { messageIds: number[]; tasks: Task[] } {
2617
2649
  return db.transaction(() => {
2650
+ settleSingleTargetOnDemandTasks("claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?)", [now], now, "claim-lease-expired");
2618
2651
  const releasableMessageClaim = "claimed_by IS NOT NULL AND (claim_expires_at IS NULL OR claim_expires_at <= ?) AND NOT EXISTS (SELECT 1 FROM tasks t WHERE t.message_id = messages.id AND t.status IN ('done', 'failed', 'canceled'))";
2619
2652
  const messageRows = db
2620
2653
  .prepare(`SELECT id FROM messages WHERE ${releasableMessageClaim}`)
@@ -2684,6 +2717,7 @@ export function orphanTasksForAgent(agentId: string, now: number = Date.now()):
2684
2717
  export function releaseOrphanedTasks(graceMs = 120_000, now: number = Date.now()): Task[] {
2685
2718
  return db.transaction(() => {
2686
2719
  const cutoff = now - graceMs;
2720
+ settleSingleTargetOnDemandTasks("status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?", [cutoff], now, "orphan-grace-elapsed");
2687
2721
  const rows = db
2688
2722
  .prepare(`${TASK_SELECT} WHERE status = 'orphaned' AND claimed_by IS NOT NULL AND updated_at <= ?`)
2689
2723
  .all(cutoff) as any[];