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.
- package/package.json +1 -1
- package/src/db.ts +35 -1
package/package.json
CHANGED
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
|
-
|
|
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[];
|