agent-relay-server 0.91.1 → 0.91.3
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/docs/openapi.json +1 -1
- package/package.json +1 -1
- package/runner/src/config.ts +5 -0
- package/src/agent-death-diagnosis.ts +4 -0
- package/src/bus.ts +2 -37
- package/src/notification-types.ts +2 -0
- package/src/provider-state.ts +21 -0
- package/src/services/provider-state-audit.ts +52 -0
- package/src/services/rate-limit-resume.ts +68 -8
- package/src/services/terminal-provider-exit.ts +24 -1
package/docs/openapi.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"openapi": "3.1.0",
|
|
3
3
|
"info": {
|
|
4
4
|
"title": "Agent Relay API",
|
|
5
|
-
"version": "0.91.
|
|
5
|
+
"version": "0.91.3",
|
|
6
6
|
"description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
|
|
7
7
|
"license": {
|
|
8
8
|
"name": "MIT",
|
package/package.json
CHANGED
package/runner/src/config.ts
CHANGED
|
@@ -99,6 +99,11 @@ export function registrationTimeoutMsFromEnv(): number {
|
|
|
99
99
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 60_000;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
export function nativeSelfResumeTimeoutMsFromEnv(): number {
|
|
103
|
+
const parsed = Number(process.env.AGENT_RELAY_NATIVE_SELF_RESUME_TIMEOUT_MS);
|
|
104
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 120_000;
|
|
105
|
+
}
|
|
106
|
+
|
|
102
107
|
export function agentProfileNameFromEnv(): string | undefined {
|
|
103
108
|
return process.env.AGENT_RELAY_AGENT_PROFILE;
|
|
104
109
|
}
|
|
@@ -173,6 +173,10 @@ export function classifyAgentDeath(ev: DeathEvidence): AgentDeathDiagnosis {
|
|
|
173
173
|
pushUnique(evidence, "log/error text matched a crash pattern (panic/segfault/traceback)");
|
|
174
174
|
return verdict("provider-crash", "medium", "Likely provider crash (log pattern)");
|
|
175
175
|
}
|
|
176
|
+
if (/self-resume-(?:exit-before-resume|relaunch-failed)/i.test(text)) {
|
|
177
|
+
pushUnique(evidence, "native self-resume exited before Relay observed a resumed provider");
|
|
178
|
+
return verdict("provider-crash", "high", "Self-resume failed before relaunch completed");
|
|
179
|
+
}
|
|
176
180
|
|
|
177
181
|
// 5) killed — external SIGKILL/SIGTERM that isn't OOM and isn't a parent-initiated shutdown.
|
|
178
182
|
if (signal && /KILL|TERM|INT|HUP|QUIT/i.test(signal)) {
|
package/src/bus.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { reconcileTerminalProviderExit } from "./services/terminal-provider-exit
|
|
|
11
11
|
import { registerAgent } from "./services/register-agent";
|
|
12
12
|
import { authContextFromBus } from "./services/auth-context";
|
|
13
13
|
import { commandAuthorizationResource, dispatchCommand } from "./services/dispatch-command";
|
|
14
|
+
import { auditProviderStateTransition } from "./services/provider-state-audit";
|
|
14
15
|
import { ServiceAuthError } from "./services/errors";
|
|
15
16
|
import { ShutdownAuthError, ShutdownTargetError, shutdownAgent, shutdownInputFromBusFrame } from "./services/shutdown-agent";
|
|
16
17
|
import { applyCommandToRecipe } from "./recipe-runner";
|
|
@@ -27,9 +28,8 @@ import {
|
|
|
27
28
|
type RegisterFrame,
|
|
28
29
|
} from "agent-relay-sdk/protocol";
|
|
29
30
|
import { errMessage, isRecord, stringValue } from "agent-relay-sdk";
|
|
30
|
-
import { agentProviderState } from "./provider-state";
|
|
31
31
|
import { getComponentAuth, isComponentAuthorizedFor, isAuthorized, isOriginAllowed, unauthorized } from "./security";
|
|
32
|
-
import type {
|
|
32
|
+
import type { Command, ComponentToken, ContextState, Message, ProviderCapabilities, QuotaState, RegisterAgentInput, Task } from "./types";
|
|
33
33
|
|
|
34
34
|
interface BusSocketData {
|
|
35
35
|
kind: "bus";
|
|
@@ -562,36 +562,6 @@ function emitAgentStatusEvent(agentId: string): void {
|
|
|
562
562
|
});
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
-
function auditProviderStateTransition(agentId: string, before: AgentCard | null | undefined, after: AgentCard | null | undefined): void {
|
|
566
|
-
const previous = agentProviderState(before);
|
|
567
|
-
const next = agentProviderState(after);
|
|
568
|
-
const previousKey = providerStateKey(previous);
|
|
569
|
-
const nextKey = providerStateKey(next);
|
|
570
|
-
if (previousKey === nextKey) return;
|
|
571
|
-
if (next?.state !== "blocked" && previous?.state !== "blocked") return;
|
|
572
|
-
|
|
573
|
-
const blocked = next?.state === "blocked";
|
|
574
|
-
const label = typeof next?.label === "string" ? next.label : typeof previous?.label === "string" ? previous.label : "provider blocked";
|
|
575
|
-
try {
|
|
576
|
-
createActivityEvent({
|
|
577
|
-
clientId: `server-agent-${agentId}-provider-state-${nextKey ?? "cleared"}-${Date.now()}`,
|
|
578
|
-
kind: "state",
|
|
579
|
-
title: blocked ? `Agent blocked: ${label}` : "Agent provider unblocked",
|
|
580
|
-
meta: agentId,
|
|
581
|
-
icon: blocked ? "ti-alert-circle" : "ti-circle-check",
|
|
582
|
-
view: "agents",
|
|
583
|
-
agentId,
|
|
584
|
-
metadata: {
|
|
585
|
-
source: "server",
|
|
586
|
-
previousProviderState: previous ?? null,
|
|
587
|
-
providerState: next ?? null,
|
|
588
|
-
},
|
|
589
|
-
});
|
|
590
|
-
} catch {
|
|
591
|
-
// Audit trail writes must never block bus status updates.
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
|
|
595
565
|
function auditRunnerTimelineEvent(agentId: string, timelineEvent: unknown): void {
|
|
596
566
|
if (!isRecord(timelineEvent)) return;
|
|
597
567
|
const metadata = isRecord(timelineEvent.metadata) ? timelineEvent.metadata : {};
|
|
@@ -633,11 +603,6 @@ function auditRunnerTimelineEvent(agentId: string, timelineEvent: unknown): void
|
|
|
633
603
|
}
|
|
634
604
|
}
|
|
635
605
|
|
|
636
|
-
function providerStateKey(state: Record<string, unknown> | null): string | null {
|
|
637
|
-
if (!state) return null;
|
|
638
|
-
return [state.state, typeof state.reason === "string" ? state.reason : ""].join(":");
|
|
639
|
-
}
|
|
640
|
-
|
|
641
606
|
function sendCommandResult(
|
|
642
607
|
ws: BusWebSocket,
|
|
643
608
|
commandId: string,
|
|
@@ -12,6 +12,7 @@ export type NotificationType =
|
|
|
12
12
|
| "agent.resume_budget_exhausted"
|
|
13
13
|
| "agent.transient_land_hold"
|
|
14
14
|
| "agent.rate_limit_resume"
|
|
15
|
+
| "agent.provider_blocked"
|
|
15
16
|
| "team.idle"
|
|
16
17
|
| "team.member_silent"
|
|
17
18
|
| "team.coordinator_gone"
|
|
@@ -68,6 +69,7 @@ export const DEFAULT_TTL_MS: Record<NotificationType, number | null> = {
|
|
|
68
69
|
"agent.resume_budget_exhausted": null,
|
|
69
70
|
"agent.transient_land_hold": null,
|
|
70
71
|
"agent.rate_limit_resume": 15 * MINUTES,
|
|
72
|
+
"agent.provider_blocked": null,
|
|
71
73
|
"team.idle": null,
|
|
72
74
|
"team.member_silent": null,
|
|
73
75
|
"team.coordinator_gone": null,
|
package/src/provider-state.ts
CHANGED
|
@@ -38,6 +38,8 @@ export function isProviderBlocked(agent: HasMeta | null | undefined): boolean {
|
|
|
38
38
|
|
|
39
39
|
/** The `reason` value a rate-limit hold carries (matches the runner's control-server seam). */
|
|
40
40
|
export const RATE_LIMIT_BLOCK_REASON = "rate_limit";
|
|
41
|
+
export const TRANSIENT_RATE_LIMIT_KIND = "transient_overload";
|
|
42
|
+
export const TRANSIENT_RATE_LIMIT_BACKOFF_MS = [30_000, 2 * 60_000, 5 * 60_000] as const;
|
|
41
43
|
|
|
42
44
|
/** True when the agent is specifically held on a provider usage/rate limit. */
|
|
43
45
|
export function isRateLimitHold(agent: HasMeta | null | undefined): boolean {
|
|
@@ -45,6 +47,19 @@ export function isRateLimitHold(agent: HasMeta | null | undefined): boolean {
|
|
|
45
47
|
return state?.state === "blocked" && state.reason === RATE_LIMIT_BLOCK_REASON;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
export function isTransientRateLimitHold(state: ProviderStateView | null): boolean {
|
|
51
|
+
return state?.kind === TRANSIENT_RATE_LIMIT_KIND;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function transientRateLimitAttempt(state: ProviderStateView | null): number {
|
|
55
|
+
const raw = state?.resumeAttempt;
|
|
56
|
+
return typeof raw === "number" && Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function transientRateLimitExhausted(state: ProviderStateView | null): boolean {
|
|
60
|
+
return isTransientRateLimitHold(state) && transientRateLimitAttempt(state) >= TRANSIENT_RATE_LIMIT_BACKOFF_MS.length;
|
|
61
|
+
}
|
|
62
|
+
|
|
48
63
|
// Fallback window when the provider gave no reset time: retry after this long
|
|
49
64
|
// rather than waiting the full (unknown) limit window. Re-entering the hold on a
|
|
50
65
|
// still-limited retry is cheap, and a subscription limit almost always carries a
|
|
@@ -56,6 +71,12 @@ export function isRateLimitHold(agent: HasMeta | null | undefined): boolean {
|
|
|
56
71
|
*/
|
|
57
72
|
export function rateLimitResumeAt(state: ProviderStateView | null): number | null {
|
|
58
73
|
if (!state) return null;
|
|
74
|
+
if (isTransientRateLimitHold(state)) {
|
|
75
|
+
if (typeof state.nextResumeAt === "number" && Number.isFinite(state.nextResumeAt)) return state.nextResumeAt;
|
|
76
|
+
if (transientRateLimitExhausted(state)) return null;
|
|
77
|
+
const base = typeof state.enteredAt === "number" && Number.isFinite(state.enteredAt) ? state.enteredAt : Date.now();
|
|
78
|
+
return base + TRANSIENT_RATE_LIMIT_BACKOFF_MS[transientRateLimitAttempt(state)]!;
|
|
79
|
+
}
|
|
59
80
|
if (typeof state.resetAt === "number" && Number.isFinite(state.resetAt)) return state.resetAt;
|
|
60
81
|
if (typeof state.enteredAt === "number" && Number.isFinite(state.enteredAt)) {
|
|
61
82
|
return state.enteredAt + rateLimitFallbackMs();
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createActivityEvent } from "../db";
|
|
2
|
+
import { routeNotification } from "../notification-router";
|
|
3
|
+
import { agentProviderState } from "../provider-state";
|
|
4
|
+
import type { AgentCard } from "../types";
|
|
5
|
+
|
|
6
|
+
export function auditProviderStateTransition(agentId: string, before: AgentCard | null | undefined, after: AgentCard | null | undefined): void {
|
|
7
|
+
const previous = agentProviderState(before);
|
|
8
|
+
const next = agentProviderState(after);
|
|
9
|
+
const previousKey = providerStateKey(previous);
|
|
10
|
+
const nextKey = providerStateKey(next);
|
|
11
|
+
if (previousKey === nextKey) return;
|
|
12
|
+
if (next?.state !== "blocked" && previous?.state !== "blocked") return;
|
|
13
|
+
|
|
14
|
+
const blocked = next?.state === "blocked";
|
|
15
|
+
const label = typeof next?.label === "string" ? next.label : typeof previous?.label === "string" ? previous.label : "provider blocked";
|
|
16
|
+
try {
|
|
17
|
+
createActivityEvent({
|
|
18
|
+
clientId: `server-agent-${agentId}-provider-state-${nextKey ?? "cleared"}-${Date.now()}`,
|
|
19
|
+
kind: "state",
|
|
20
|
+
title: blocked ? `Agent blocked: ${label}` : "Agent provider unblocked",
|
|
21
|
+
meta: agentId,
|
|
22
|
+
icon: blocked ? "ti-alert-circle" : "ti-circle-check",
|
|
23
|
+
view: "agents",
|
|
24
|
+
agentId,
|
|
25
|
+
metadata: {
|
|
26
|
+
source: "server",
|
|
27
|
+
previousProviderState: previous ?? null,
|
|
28
|
+
providerState: next ?? null,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
if (!blocked || !after?.spawnedBy) return;
|
|
32
|
+
routeNotification({
|
|
33
|
+
type: "agent.provider_blocked",
|
|
34
|
+
scope: { agentId, parent: after.spawnedBy },
|
|
35
|
+
recipients: [after.spawnedBy],
|
|
36
|
+
message: {
|
|
37
|
+
subject: "Child held on provider block",
|
|
38
|
+
body: `Child agent ${after.label ?? after.name ?? agentId} is held: ${label}.`,
|
|
39
|
+
payload: { kind: "agent.provider_blocked", agentId, providerState: next ?? null },
|
|
40
|
+
replyExpected: false,
|
|
41
|
+
idempotencyKey: `provider-blocked:${agentId}:${nextKey ?? "blocked"}`,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
} catch {
|
|
45
|
+
// Audit trail writes must never block bus status updates.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function providerStateKey(state: Record<string, unknown> | null): string | null {
|
|
50
|
+
if (!state) return null;
|
|
51
|
+
return [state.state, typeof state.reason === "string" ? state.reason : ""].join(":");
|
|
52
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { createActivityEvent, listAgents } from "../db";
|
|
1
|
+
import { createActivityEvent, listAgents, mergeAgentMeta } from "../db";
|
|
2
2
|
import { routeNotification } from "../notification-router";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
TRANSIENT_RATE_LIMIT_BACKOFF_MS,
|
|
5
|
+
agentProviderState,
|
|
6
|
+
isRateLimitHold,
|
|
7
|
+
isTransientRateLimitHold,
|
|
8
|
+
rateLimitResumeAt,
|
|
9
|
+
transientRateLimitAttempt,
|
|
10
|
+
transientRateLimitExhausted,
|
|
11
|
+
} from "../provider-state";
|
|
4
12
|
|
|
5
13
|
// #286 Phase 2: auto-resume agents held on a usage/rate-limit stall once their
|
|
6
14
|
// window resets. The runner reports the hold as `providerState: blocked` (reason
|
|
@@ -22,14 +30,49 @@ const envMsOrDefault = (name: string, fallback: number): number => {
|
|
|
22
30
|
// between delivery and the agent flipping to busy (which clears the hold).
|
|
23
31
|
const resumeCooldownMs = () => envMsOrDefault("AGENT_RELAY_RATE_LIMIT_RESUME_COOLDOWN_MS", 5 * 60 * 1000);
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
interface ResumeTrackerEntry {
|
|
34
|
+
lastResumeAt?: number;
|
|
35
|
+
attempts?: number;
|
|
36
|
+
exhaustedNotified?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const resumeTracker = new Map<string, ResumeTrackerEntry>();
|
|
26
40
|
|
|
27
41
|
export function resetRateLimitResumeTrackerForTests(): void {
|
|
28
42
|
resumeTracker.clear();
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
const RESUME_BODY =
|
|
32
|
-
"
|
|
46
|
+
"Your provider limit/overload hold is ready to retry — resuming. Continue the task you were working on when the provider interrupted you. If you had already finished, no action is needed.";
|
|
47
|
+
|
|
48
|
+
function nextTransientState(state: Record<string, unknown>, now: number, attempts: number): Record<string, unknown> {
|
|
49
|
+
const nextAttempts = attempts + 1;
|
|
50
|
+
const nextDelay = TRANSIENT_RATE_LIMIT_BACKOFF_MS[nextAttempts];
|
|
51
|
+
return {
|
|
52
|
+
...state,
|
|
53
|
+
resumeAttempt: nextAttempts,
|
|
54
|
+
lastResumeAt: now,
|
|
55
|
+
...(nextDelay !== undefined ? { nextResumeAt: now + nextDelay } : {}),
|
|
56
|
+
updatedAt: now,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function notifyTransientExhausted(agent: { id: string; label?: string | null; spawnedBy?: string | null }, state: Record<string, unknown>, now: number): void {
|
|
61
|
+
mergeAgentMeta(agent.id, { providerState: { ...state, exhaustedAt: now, updatedAt: now } });
|
|
62
|
+
if (!agent.spawnedBy) return;
|
|
63
|
+
routeNotification({
|
|
64
|
+
type: "agent.provider_blocked",
|
|
65
|
+
scope: { agentId: agent.id, parent: agent.spawnedBy },
|
|
66
|
+
recipients: [agent.spawnedBy],
|
|
67
|
+
message: {
|
|
68
|
+
subject: "Child held on provider overload",
|
|
69
|
+
body: `Child agent ${agent.label ?? agent.id} is still held on transient provider overload after bounded auto-resume attempts. Manual check or restart may be needed.`,
|
|
70
|
+
payload: { kind: "agent.provider_blocked", agentId: agent.id, providerState: state, exhausted: true },
|
|
71
|
+
replyExpected: false,
|
|
72
|
+
idempotencyKey: `provider-overload-exhausted:${agent.id}:${state.enteredAt ?? "unknown"}`,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
33
76
|
|
|
34
77
|
export function resumeRateLimitedAgents(): Record<string, unknown> {
|
|
35
78
|
const now = Date.now();
|
|
@@ -47,9 +90,21 @@ export function resumeRateLimitedAgents(): Record<string, unknown> {
|
|
|
47
90
|
const resumeAt = rateLimitResumeAt(state);
|
|
48
91
|
if (resumeAt !== null && now < resumeAt) { waiting.push(agent.id); continue; }
|
|
49
92
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
93
|
+
const transient = isTransientRateLimitHold(state);
|
|
94
|
+
const tracked = resumeTracker.get(agent.id) ?? {};
|
|
95
|
+
const attempts = transient ? Math.max(transientRateLimitAttempt(state), tracked.attempts ?? 0) : 0;
|
|
96
|
+
if (transient && (attempts >= TRANSIENT_RATE_LIMIT_BACKOFF_MS.length || transientRateLimitExhausted(state))) {
|
|
97
|
+
if (!tracked.exhaustedNotified) {
|
|
98
|
+
notifyTransientExhausted(agent, state as Record<string, unknown>, now);
|
|
99
|
+
resumeTracker.set(agent.id, { ...tracked, attempts, exhaustedNotified: true });
|
|
100
|
+
}
|
|
101
|
+
waiting.push(agent.id);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const lastResume = tracked.lastResumeAt;
|
|
106
|
+
if (!transient && lastResume !== undefined && now - lastResume < cooldown) { cooling.push(agent.id); continue; }
|
|
107
|
+
resumeTracker.set(agent.id, { ...tracked, attempts: transient ? attempts + 1 : undefined, lastResumeAt: now });
|
|
53
108
|
|
|
54
109
|
try {
|
|
55
110
|
// Short default TTL (per-type): a "limit reset, resume" push is pointless once stale — the
|
|
@@ -77,6 +132,8 @@ export function resumeRateLimitedAgents(): Record<string, unknown> {
|
|
|
77
132
|
continue;
|
|
78
133
|
}
|
|
79
134
|
|
|
135
|
+
if (transient) mergeAgentMeta(agent.id, { providerState: nextTransientState(state as Record<string, unknown>, now, attempts) });
|
|
136
|
+
|
|
80
137
|
resumed.push(agent.id);
|
|
81
138
|
createActivityEvent({
|
|
82
139
|
clientId: `rate-limit-resume-${agent.id}-${now}`,
|
|
@@ -97,7 +154,10 @@ export function resumeRateLimitedAgents(): Record<string, unknown> {
|
|
|
97
154
|
}
|
|
98
155
|
|
|
99
156
|
// Forget agents no longer held so a future hold resumes immediately.
|
|
100
|
-
for (const id of resumeTracker.
|
|
157
|
+
for (const [id, tracked] of resumeTracker.entries()) {
|
|
158
|
+
if (seen.has(id)) continue;
|
|
159
|
+
if (tracked.lastResumeAt === undefined || now - tracked.lastResumeAt > 15 * 60_000) resumeTracker.delete(id);
|
|
160
|
+
}
|
|
101
161
|
|
|
102
162
|
return { held: seen.size, resumed, waiting, cooling, tracked: resumeTracker.size };
|
|
103
163
|
}
|
|
@@ -8,6 +8,7 @@ import { isRecord } from "agent-relay-sdk";
|
|
|
8
8
|
import { clearAgentMetaKeys } from "../db";
|
|
9
9
|
import { notifyAgentOffline } from "../agent-lifecycle-events";
|
|
10
10
|
import { diagnoseAgentDeath } from "../agent-death-diagnosis";
|
|
11
|
+
import { routeNotification } from "../notification-router";
|
|
11
12
|
import type { AgentCard } from "../types";
|
|
12
13
|
|
|
13
14
|
/**
|
|
@@ -29,5 +30,27 @@ export function reconcileTerminalProviderExit(agentId: string, before: AgentCard
|
|
|
29
30
|
if (!isRecord(marker)) return;
|
|
30
31
|
const reasonText = typeof marker.reason === "string" ? marker.reason : "provider exited";
|
|
31
32
|
const reason = `provider exited (${reasonText})`;
|
|
32
|
-
|
|
33
|
+
const diagnosis = diagnoseAgentDeath({ agent: after, reason });
|
|
34
|
+
notifyAgentOffline(agentId, reason, diagnosis);
|
|
35
|
+
if (!after.spawnedBy && reasonText.startsWith("self-resume-")) {
|
|
36
|
+
routeNotification({
|
|
37
|
+
type: "agent.exited",
|
|
38
|
+
scope: { agentId },
|
|
39
|
+
recipients: ["user"],
|
|
40
|
+
message: {
|
|
41
|
+
subject: "Self-resume failed",
|
|
42
|
+
body: `Agent \`${agentId}\` exited during self-resume before the resumed session was confirmed. Manual recovery may need the provider resume id from the runner log.`,
|
|
43
|
+
payload: {
|
|
44
|
+
kind: "agent.self_resume_failed",
|
|
45
|
+
agentId,
|
|
46
|
+
reason,
|
|
47
|
+
diagnosis,
|
|
48
|
+
...(typeof marker.claudeResumeId === "string" ? { claudeResumeId: marker.claudeResumeId } : {}),
|
|
49
|
+
...(typeof marker.selfResumeCommandId === "string" ? { commandId: marker.selfResumeCommandId } : {}),
|
|
50
|
+
...(typeof marker.selfResumeGeneration === "number" ? { generation: marker.selfResumeGeneration } : {}),
|
|
51
|
+
},
|
|
52
|
+
replyExpected: false,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
}
|
|
33
56
|
}
|