agent-relay-runner 0.91.2 → 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/package.json +1 -1
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/config.ts +5 -0
- package/src/native-self-resume.ts +202 -0
- package/src/runner-core.ts +29 -30
- package/src/runner-helpers.ts +33 -0
package/package.json
CHANGED
package/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
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { errMessage } from "agent-relay-sdk";
|
|
2
|
+
import type { ProviderStatusEvent, SemanticStatus } from "./adapter";
|
|
3
|
+
import { nativeSelfResumeTimeoutMsFromEnv } from "./config";
|
|
4
|
+
|
|
5
|
+
interface RunnerTimelineEvent {
|
|
6
|
+
status: string;
|
|
7
|
+
id?: string;
|
|
8
|
+
timestamp: number;
|
|
9
|
+
title?: string;
|
|
10
|
+
body?: string;
|
|
11
|
+
icon?: string;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NativeSelfResumeHandoff {
|
|
16
|
+
commandId: string;
|
|
17
|
+
generation?: number;
|
|
18
|
+
compactedAt?: number;
|
|
19
|
+
timer: ReturnType<typeof setTimeout>;
|
|
20
|
+
resolve: (result: Record<string, unknown>) => void;
|
|
21
|
+
reject: (error: Error) => void;
|
|
22
|
+
promise: Promise<Record<string, unknown>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isNativeSelfResumeCompact(type: string, params: Record<string, unknown>): boolean {
|
|
26
|
+
if (type !== "agent.compact") return false;
|
|
27
|
+
const selfResume = params.selfResume;
|
|
28
|
+
return Boolean(selfResume && typeof selfResume === "object" && !Array.isArray(selfResume) && (selfResume as Record<string, unknown>).mode === "native");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class NativeSelfResumeTracker {
|
|
32
|
+
private current?: NativeSelfResumeHandoff;
|
|
33
|
+
|
|
34
|
+
get active(): NativeSelfResumeHandoff | undefined {
|
|
35
|
+
return this.current;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
begin(commandId: string, params: Record<string, unknown>): NativeSelfResumeHandoff {
|
|
39
|
+
this.finish(this.current);
|
|
40
|
+
let resolve!: (result: Record<string, unknown>) => void;
|
|
41
|
+
let reject!: (error: Error) => void;
|
|
42
|
+
const promise = new Promise<Record<string, unknown>>((res, rej) => {
|
|
43
|
+
resolve = res;
|
|
44
|
+
reject = rej;
|
|
45
|
+
});
|
|
46
|
+
const selfResume = params.selfResume && typeof params.selfResume === "object" && !Array.isArray(params.selfResume)
|
|
47
|
+
? params.selfResume as Record<string, unknown>
|
|
48
|
+
: undefined;
|
|
49
|
+
const generation = typeof selfResume?.generation === "number" ? selfResume.generation : undefined;
|
|
50
|
+
const timeoutMs = nativeSelfResumeTimeoutMsFromEnv();
|
|
51
|
+
const handoff: NativeSelfResumeHandoff = {
|
|
52
|
+
commandId,
|
|
53
|
+
generation,
|
|
54
|
+
promise,
|
|
55
|
+
resolve,
|
|
56
|
+
reject,
|
|
57
|
+
timer: setTimeout(() => {
|
|
58
|
+
if (this.current !== handoff) return;
|
|
59
|
+
this.current = undefined;
|
|
60
|
+
reject(new Error(`native self-resume timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
61
|
+
}, timeoutMs),
|
|
62
|
+
};
|
|
63
|
+
this.current = handoff;
|
|
64
|
+
return handoff;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
finish(handoff?: NativeSelfResumeHandoff): void {
|
|
68
|
+
if (!handoff) return;
|
|
69
|
+
clearTimeout(handoff.timer);
|
|
70
|
+
if (this.current === handoff) this.current = undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resolve(handoff: NativeSelfResumeHandoff, result: Record<string, unknown>): void {
|
|
74
|
+
this.finish(handoff);
|
|
75
|
+
handoff.resolve(result);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
reject(handoff: NativeSelfResumeHandoff, error: Error): void {
|
|
79
|
+
this.finish(handoff);
|
|
80
|
+
handoff.reject(error);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
noteCompacted(timestamp: number = Date.now()): void {
|
|
84
|
+
if (!this.current) return;
|
|
85
|
+
this.current.compactedAt = timestamp;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface NativeSelfResumeRecoveryOptions {
|
|
90
|
+
handoff: NativeSelfResumeHandoff;
|
|
91
|
+
tracker: NativeSelfResumeTracker;
|
|
92
|
+
providerSessionId: string;
|
|
93
|
+
status: SemanticStatus;
|
|
94
|
+
diagnostics: Record<string, unknown>;
|
|
95
|
+
now: number;
|
|
96
|
+
publishTimeline(event: RunnerTimelineEvent): void;
|
|
97
|
+
setTerminalProviderExit(marker: Record<string, unknown>): void;
|
|
98
|
+
setProviderStatus(update: ProviderStatusEvent): void;
|
|
99
|
+
restartProvider(resumeId?: string): Promise<void>;
|
|
100
|
+
publishStatus(): void;
|
|
101
|
+
scheduleDrain(): void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function recoverNativeSelfResumeExit(opts: NativeSelfResumeRecoveryOptions): Promise<void> {
|
|
105
|
+
const resumeId = typeof opts.diagnostics.claudeResumeId === "string" && opts.diagnostics.claudeResumeId.length > 0
|
|
106
|
+
? opts.diagnostics.claudeResumeId
|
|
107
|
+
: undefined;
|
|
108
|
+
if (opts.handoff.compactedAt && resumeId) {
|
|
109
|
+
await recoverWithClaudeResume(opts, resumeId);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await recoverWithFreshLaunch(opts, resumeId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function recoverWithClaudeResume(opts: NativeSelfResumeRecoveryOptions, resumeId: string): Promise<void> {
|
|
116
|
+
opts.publishTimeline({
|
|
117
|
+
status: "provider.restart_decision",
|
|
118
|
+
id: `provider-self-resume-relaunch-${opts.providerSessionId}-${opts.now}`,
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
title: "Self-resume relaunching",
|
|
121
|
+
body: `Claude exited during native self-resume; runner is relaunching with claude --resume ${resumeId}.`,
|
|
122
|
+
icon: "ti-refresh",
|
|
123
|
+
metadata: selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
124
|
+
decision: "resume-native-self-resume",
|
|
125
|
+
reason: "self-resume-exit-after-compact",
|
|
126
|
+
claudeResumeId: resumeId,
|
|
127
|
+
compactedAt: opts.handoff.compactedAt,
|
|
128
|
+
}),
|
|
129
|
+
});
|
|
130
|
+
try {
|
|
131
|
+
await opts.restartProvider(resumeId);
|
|
132
|
+
opts.publishStatus();
|
|
133
|
+
opts.scheduleDrain();
|
|
134
|
+
opts.tracker.resolve(opts.handoff, { recovered: true, via: "claude-resume", claudeResumeId: resumeId });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const message = errMessage(error);
|
|
137
|
+
opts.setTerminalProviderExit(selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
138
|
+
reason: "self-resume-relaunch-failed",
|
|
139
|
+
claudeResumeId: resumeId,
|
|
140
|
+
compactedAt: opts.handoff.compactedAt,
|
|
141
|
+
lastError: message,
|
|
142
|
+
}));
|
|
143
|
+
opts.setProviderStatus({
|
|
144
|
+
status: "offline",
|
|
145
|
+
reason: "provider-turn",
|
|
146
|
+
id: `provider-self-resume-relaunch-failed-${opts.providerSessionId}`,
|
|
147
|
+
clear: ["provider-turn", "subagent", "background-script"],
|
|
148
|
+
});
|
|
149
|
+
opts.tracker.reject(opts.handoff, new Error(`self-resume relaunch failed: ${message}`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function recoverWithFreshLaunch(opts: NativeSelfResumeRecoveryOptions, resumeId?: string): Promise<void> {
|
|
154
|
+
opts.publishTimeline({
|
|
155
|
+
status: "provider.restart_decision",
|
|
156
|
+
id: `provider-self-resume-fresh-launch-${opts.providerSessionId}-${opts.now}`,
|
|
157
|
+
timestamp: Date.now(),
|
|
158
|
+
title: "Self-resume fresh-launching",
|
|
159
|
+
body: "Claude exited during native self-resume before a compacted marker; runner is fresh-launching so Relay can inject the persisted continuation.",
|
|
160
|
+
icon: "ti-refresh",
|
|
161
|
+
metadata: selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
162
|
+
decision: "fresh-launch-native-self-resume",
|
|
163
|
+
reason: "self-resume-exit-before-compact",
|
|
164
|
+
...(resumeId ? { claudeResumeId: resumeId } : {}),
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
try {
|
|
168
|
+
await opts.restartProvider();
|
|
169
|
+
opts.publishStatus();
|
|
170
|
+
opts.scheduleDrain();
|
|
171
|
+
opts.tracker.resolve(opts.handoff, {
|
|
172
|
+
recovered: true,
|
|
173
|
+
via: "fresh-launch",
|
|
174
|
+
...(resumeId ? { claudeResumeId: resumeId } : {}),
|
|
175
|
+
});
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const message = errMessage(error);
|
|
178
|
+
opts.setTerminalProviderExit(selfResumeMetadata(opts.handoff, opts.diagnostics, {
|
|
179
|
+
reason: "self-resume-fresh-launch-failed",
|
|
180
|
+
...(resumeId ? { claudeResumeId: resumeId } : {}),
|
|
181
|
+
lastError: message,
|
|
182
|
+
}));
|
|
183
|
+
opts.setProviderStatus({
|
|
184
|
+
status: "offline",
|
|
185
|
+
reason: "provider-turn",
|
|
186
|
+
id: `provider-self-resume-fresh-launch-failed-${opts.providerSessionId}`,
|
|
187
|
+
clear: ["provider-turn", "subagent", "background-script"],
|
|
188
|
+
});
|
|
189
|
+
opts.tracker.reject(opts.handoff, new Error(`self-resume fresh launch failed: ${message}`));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function selfResumeMetadata(handoff: NativeSelfResumeHandoff, diagnostics: Record<string, unknown>, extra: Record<string, unknown>): Record<string, unknown> {
|
|
194
|
+
return {
|
|
195
|
+
eventType: "provider.restart_decision",
|
|
196
|
+
selfResume: true,
|
|
197
|
+
selfResumeCommandId: handoff.commandId,
|
|
198
|
+
selfResumeGeneration: handoff.generation ?? null,
|
|
199
|
+
...extra,
|
|
200
|
+
...diagnostics,
|
|
201
|
+
};
|
|
202
|
+
}
|
package/src/runner-core.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { runnerLaunchInjectionSessionEvents, type RunnerRelayInjectionEvent } fr
|
|
|
30
30
|
import { providerTerminalSession, providerTerminalSocket } from "./process-meta";
|
|
31
31
|
import { capsFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
|
|
32
32
|
import { boundaryReasonForCommand, PRE_DESTROY_TIMEOUT_MS, reasonExitsRunner, type LifecycleAction, type SessionDestroyReason } from "./session-destroy";
|
|
33
|
+
import { isNativeSelfResumeCompact, NativeSelfResumeTracker, recoverNativeSelfResumeExit } from "./native-self-resume";
|
|
33
34
|
import {
|
|
34
35
|
appliedAgentProfileMetadata,
|
|
35
36
|
commandTimeoutMs,
|
|
@@ -38,8 +39,8 @@ import {
|
|
|
38
39
|
isContextState,
|
|
39
40
|
isHttpAuthError,
|
|
40
41
|
isHttpStatusError,
|
|
41
|
-
latestClaudeResumeIdFromLogFile,
|
|
42
42
|
lifecycleCapabilities,
|
|
43
|
+
providerExitDiagnostics,
|
|
43
44
|
providerStateFromActiveWork,
|
|
44
45
|
registerWithinDeadline,
|
|
45
46
|
relayBusUrl,
|
|
@@ -198,6 +199,7 @@ export class AgentRunner {
|
|
|
198
199
|
// terminal `agent.exited` event with a #636 diagnosis instead of the death going silent. Cleared
|
|
199
200
|
// on any genuine restart back to busy/idle.
|
|
200
201
|
private terminalProviderExit?: Record<string, unknown>;
|
|
202
|
+
private nativeSelfResume = new NativeSelfResumeTracker();
|
|
201
203
|
private exitCommandInProgress = false;
|
|
202
204
|
private restartInProgress = false;
|
|
203
205
|
// Set for the whole unexpected-exit restart, including the pre-restart backoff
|
|
@@ -521,7 +523,7 @@ export class AgentRunner {
|
|
|
521
523
|
return mode === "isolated";
|
|
522
524
|
}
|
|
523
525
|
|
|
524
|
-
private async spawnProvider(): Promise<ManagedProcess> {
|
|
526
|
+
private async spawnProvider(opts: { resumeId?: string; suppressPrompt?: boolean } = {}): Promise<ManagedProcess> {
|
|
525
527
|
this.providerSessionId = crypto.randomUUID();
|
|
526
528
|
this.lastTranscriptPath = undefined;
|
|
527
529
|
const includeProviderGlobals = profileUsesHostProviderGlobals(this.options);
|
|
@@ -571,10 +573,10 @@ export class AgentRunner {
|
|
|
571
573
|
...(this.options.rig ? { rig: this.options.rig } : {}),
|
|
572
574
|
...(this.options.profile ? { profile: this.options.profile } : {}),
|
|
573
575
|
...(this.options.agentProfile ? { agentProfile: this.options.agentProfile } : {}),
|
|
574
|
-
...(this.options.prompt ? { prompt: this.options.prompt } : {}),
|
|
576
|
+
...(this.options.prompt && !opts.resumeId && !opts.suppressPrompt ? { prompt: this.options.prompt } : {}),
|
|
575
577
|
...(this.options.systemPromptAppend ? { systemPromptAppend: this.options.systemPromptAppend } : {}),
|
|
576
578
|
...(this.options.tmuxSession ? { tmuxSession: this.options.tmuxSession } : {}),
|
|
577
|
-
providerArgs: this.options.providerArgs,
|
|
579
|
+
providerArgs: opts.resumeId ? [...this.options.providerArgs, "--resume", opts.resumeId] : this.options.providerArgs,
|
|
578
580
|
providerConfig: this.options.providerConfig,
|
|
579
581
|
env,
|
|
580
582
|
controlPort: this.control!.port,
|
|
@@ -588,7 +590,7 @@ export class AgentRunner {
|
|
|
588
590
|
deliver: (messages) => this.control!.deliverToMonitor(messages),
|
|
589
591
|
},
|
|
590
592
|
};
|
|
591
|
-
const launchSeededPrompt = this.options.adapter.seedsInitialPromptAtLaunch ? this.options.prompt?.trim() : ""; if (launchSeededPrompt) this.recordInjectedPrompt(launchSeededPrompt);
|
|
593
|
+
const launchSeededPrompt = this.options.adapter.seedsInitialPromptAtLaunch && !opts.resumeId && !opts.suppressPrompt ? this.options.prompt?.trim() : ""; if (launchSeededPrompt) this.recordInjectedPrompt(launchSeededPrompt);
|
|
592
594
|
const managedProcess = await this.options.adapter.spawn(config);
|
|
593
595
|
const injectionEvents = runnerLaunchInjectionSessionEvents({ carried: this.options.relayInjectionEvents, provider: this.options.provider, config, providerConfig: this.options.providerConfig, agentId: this.agentId, providerSessionId: this.providerSessionId });
|
|
594
596
|
for (const event of injectionEvents) {
|
|
@@ -744,6 +746,9 @@ export class AgentRunner {
|
|
|
744
746
|
|
|
745
747
|
const exitAfterCommand = type === "agent.shutdown" || type === "agent.kill";
|
|
746
748
|
if (exitAfterCommand) this.exitCommandInProgress = true;
|
|
749
|
+
const nativeSelfResume = isNativeSelfResumeCompact(type, params)
|
|
750
|
+
? this.nativeSelfResume.begin(commandId, params)
|
|
751
|
+
: undefined;
|
|
747
752
|
this.claims.startClaim("command", commandId);
|
|
748
753
|
try {
|
|
749
754
|
await this.updateCommand(commandId, "accepted");
|
|
@@ -767,6 +772,10 @@ export class AgentRunner {
|
|
|
767
772
|
providerResult = await this.options.adapter.compact(this.process, {
|
|
768
773
|
instructions: typeof params.instructions === "string" ? params.instructions : undefined,
|
|
769
774
|
});
|
|
775
|
+
if (nativeSelfResume) {
|
|
776
|
+
const handoff = await nativeSelfResume.promise;
|
|
777
|
+
providerResult = { ...(providerResult ?? {}), nativeSelfResume: handoff };
|
|
778
|
+
}
|
|
770
779
|
} else if (type === "agent.clearContext") {
|
|
771
780
|
if (!this.options.adapter.clearContext || !this.process) throw new Error("provider does not support clearContext");
|
|
772
781
|
providerResult = await this.options.adapter.clearContext(this.process);
|
|
@@ -793,6 +802,7 @@ export class AgentRunner {
|
|
|
793
802
|
...(providerResult ? { providerResult } : {}),
|
|
794
803
|
});
|
|
795
804
|
} catch (error) {
|
|
805
|
+
if (nativeSelfResume) this.nativeSelfResume.finish(nativeSelfResume);
|
|
796
806
|
await this.updateCommand(commandId, "failed", undefined, errMessage(error)).catch(() => {});
|
|
797
807
|
} finally {
|
|
798
808
|
this.claims.finishClaim("command", commandId);
|
|
@@ -808,6 +818,7 @@ export class AgentRunner {
|
|
|
808
818
|
}).finally(() => process.exit(0)), 10);
|
|
809
819
|
}
|
|
810
820
|
} else if (!this.stopped) {
|
|
821
|
+
if (nativeSelfResume) this.nativeSelfResume.finish(nativeSelfResume);
|
|
811
822
|
this.lifecycleAction = undefined;
|
|
812
823
|
this.publishStatus();
|
|
813
824
|
}
|
|
@@ -904,7 +915,7 @@ export class AgentRunner {
|
|
|
904
915
|
return attachmentText ? `${body}\n\n${attachmentText}` : body;
|
|
905
916
|
}
|
|
906
917
|
|
|
907
|
-
private async restartProvider(): Promise<void> {
|
|
918
|
+
private async restartProvider(opts: { resumeId?: string; suppressPrompt?: boolean } = {}): Promise<void> {
|
|
908
919
|
this.restartInProgress = true;
|
|
909
920
|
try {
|
|
910
921
|
if (this.process) {
|
|
@@ -918,7 +929,7 @@ export class AgentRunner {
|
|
|
918
929
|
this.claims.clearWorkKind("subagent");
|
|
919
930
|
this.claims.clearWorkKind("background-script");
|
|
920
931
|
if (this.stopped) return;
|
|
921
|
-
this.process = await this.spawnProvider();
|
|
932
|
+
this.process = await this.spawnProvider(opts);
|
|
922
933
|
this.processStartedAt = Date.now();
|
|
923
934
|
} finally {
|
|
924
935
|
this.restartInProgress = false;
|
|
@@ -956,7 +967,7 @@ export class AgentRunner {
|
|
|
956
967
|
const recent = this.unexpectedExitTimes.filter((time) => now - time <= UNEXPECTED_EXIT_WINDOW_MS);
|
|
957
968
|
recent.push(now);
|
|
958
969
|
this.unexpectedExitTimes.splice(0, this.unexpectedExitTimes.length, ...recent);
|
|
959
|
-
const diagnostics =
|
|
970
|
+
const diagnostics = providerExitDiagnostics({ status, runtimeMs, processMeta: this.process?.meta, hasProcess: Boolean(this.process?.process), exitCommandInProgress: this.exitCommandInProgress, stopped: this.stopped, restartInProgress: this.restartInProgress, restartPending: this.restartPending, headless: this.options.headless, provider: this.options.provider, logFile: runnerLogFileFromEnv() });
|
|
960
971
|
|
|
961
972
|
this.publishRunnerTimelineEvent({
|
|
962
973
|
status: "provider.exit_detected",
|
|
@@ -971,6 +982,12 @@ export class AgentRunner {
|
|
|
971
982
|
},
|
|
972
983
|
});
|
|
973
984
|
|
|
985
|
+
const nativeSelfResume = this.nativeSelfResume.active;
|
|
986
|
+
if (nativeSelfResume && this.options.provider === "claude") {
|
|
987
|
+
await recoverNativeSelfResumeExit({ handoff: nativeSelfResume, tracker: this.nativeSelfResume, providerSessionId: this.providerSessionId, status, diagnostics, now, publishTimeline: (event) => this.publishRunnerTimelineEvent(event), setTerminalProviderExit: (marker) => { this.terminalProviderExit = marker; }, setProviderStatus: (update) => this.setProviderStatus(update), restartProvider: (resumeId) => this.restartProvider(resumeId ? { resumeId } : { suppressPrompt: true }), publishStatus: () => this.publishStatus(), scheduleDrain: () => this.scheduleDrain() });
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
974
991
|
if (this.shouldStopUnexpectedProviderExit(diagnostics)) {
|
|
975
992
|
const hasResumeId = typeof diagnostics.claudeResumeId === "string" && diagnostics.claudeResumeId.length > 0;
|
|
976
993
|
logger.warn("lifecycle", `${this.options.provider} exited; leaving agent offline for manual recovery`);
|
|
@@ -1104,28 +1121,6 @@ export class AgentRunner {
|
|
|
1104
1121
|
this.publishStatus();
|
|
1105
1122
|
}
|
|
1106
1123
|
|
|
1107
|
-
private providerExitDiagnostics(status: SemanticStatus, runtimeMs: number): Record<string, unknown> {
|
|
1108
|
-
const tmuxSession = typeof this.process?.meta?.tmuxSession === "string" ? this.process.meta.tmuxSession : undefined;
|
|
1109
|
-
const tmuxSocket = typeof this.process?.meta?.tmuxSocket === "string" ? this.process.meta.tmuxSocket : undefined;
|
|
1110
|
-
const exitSource = tmuxSession ? "tmux-session-ended" : this.process?.process ? "process-exit" : "provider-status";
|
|
1111
|
-
const logFile = runnerLogFileFromEnv();
|
|
1112
|
-
const claudeResumeId = this.options.provider === "claude" && logFile ? latestClaudeResumeIdFromLogFile(logFile) : undefined;
|
|
1113
|
-
return {
|
|
1114
|
-
status,
|
|
1115
|
-
runtimeMs: Number.isFinite(runtimeMs) ? runtimeMs : null,
|
|
1116
|
-
exitSource,
|
|
1117
|
-
exitCommandInProgress: this.exitCommandInProgress,
|
|
1118
|
-
stopped: this.stopped,
|
|
1119
|
-
restartInProgress: this.restartInProgress,
|
|
1120
|
-
restartPending: this.restartPending,
|
|
1121
|
-
headless: this.options.headless,
|
|
1122
|
-
hasTerminalSession: Boolean(tmuxSession),
|
|
1123
|
-
tmuxSession: tmuxSession ?? null,
|
|
1124
|
-
tmuxSocket: tmuxSocket ?? null,
|
|
1125
|
-
claudeResumeId: claudeResumeId ?? null,
|
|
1126
|
-
};
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
1124
|
private async updateCommand(commandId: string, status: string, result?: Record<string, unknown>, error?: string): Promise<void> {
|
|
1130
1125
|
await this.bus.updateCommand(commandId, { status, ...(result ? { result } : {}), ...(error ? { error } : {}) });
|
|
1131
1126
|
}
|
|
@@ -1222,6 +1217,7 @@ export class AgentRunner {
|
|
|
1222
1217
|
if (timelineStatus === "compacting") {
|
|
1223
1218
|
this.compactionMidTurn = this.currentTurnId !== undefined;
|
|
1224
1219
|
} else if (timelineStatus === "compacted") {
|
|
1220
|
+
this.nativeSelfResume.noteCompacted(typeof update !== "string" && typeof update.timeline?.timestamp === "number" ? update.timeline.timestamp : Date.now());
|
|
1225
1221
|
this.publishCompactionNotice();
|
|
1226
1222
|
if (this.compactionMidTurn) {
|
|
1227
1223
|
// Keep the turn (and its live reasoning tail) alive; the genuine Stop hook ends it.
|
|
@@ -1304,6 +1300,9 @@ export class AgentRunner {
|
|
|
1304
1300
|
this.rateLimitHold = hold;
|
|
1305
1301
|
if (fresh) this.publishRateLimitNotice(hold);
|
|
1306
1302
|
}
|
|
1303
|
+
if (status === "idle" && this.nativeSelfResume.active) {
|
|
1304
|
+
this.nativeSelfResume.resolve(this.nativeSelfResume.active, { resumed: true, via: "provider-idle" });
|
|
1305
|
+
}
|
|
1307
1306
|
this.publishStatus();
|
|
1308
1307
|
}
|
|
1309
1308
|
|
package/src/runner-helpers.ts
CHANGED
|
@@ -157,6 +157,39 @@ export function latestClaudeResumeIdFromLogFile(path: string): string | undefine
|
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
+
export function providerExitDiagnostics(input: {
|
|
161
|
+
status: SemanticStatus;
|
|
162
|
+
runtimeMs: number;
|
|
163
|
+
processMeta?: Record<string, unknown>;
|
|
164
|
+
hasProcess: boolean;
|
|
165
|
+
exitCommandInProgress: boolean;
|
|
166
|
+
stopped: boolean;
|
|
167
|
+
restartInProgress: boolean;
|
|
168
|
+
restartPending: boolean;
|
|
169
|
+
headless: boolean;
|
|
170
|
+
provider: string;
|
|
171
|
+
logFile?: string;
|
|
172
|
+
}): Record<string, unknown> {
|
|
173
|
+
const tmuxSession = typeof input.processMeta?.tmuxSession === "string" ? input.processMeta.tmuxSession : undefined;
|
|
174
|
+
const tmuxSocket = typeof input.processMeta?.tmuxSocket === "string" ? input.processMeta.tmuxSocket : undefined;
|
|
175
|
+
const exitSource = tmuxSession ? "tmux-session-ended" : input.hasProcess ? "process-exit" : "provider-status";
|
|
176
|
+
const claudeResumeId = input.provider === "claude" && input.logFile ? latestClaudeResumeIdFromLogFile(input.logFile) : undefined;
|
|
177
|
+
return {
|
|
178
|
+
status: input.status,
|
|
179
|
+
runtimeMs: Number.isFinite(input.runtimeMs) ? input.runtimeMs : null,
|
|
180
|
+
exitSource,
|
|
181
|
+
exitCommandInProgress: input.exitCommandInProgress,
|
|
182
|
+
stopped: input.stopped,
|
|
183
|
+
restartInProgress: input.restartInProgress,
|
|
184
|
+
restartPending: input.restartPending,
|
|
185
|
+
headless: input.headless,
|
|
186
|
+
hasTerminalSession: Boolean(tmuxSession),
|
|
187
|
+
tmuxSession: tmuxSession ?? null,
|
|
188
|
+
tmuxSocket: tmuxSocket ?? null,
|
|
189
|
+
claudeResumeId: claudeResumeId ?? null,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
160
193
|
export function commandTimeoutMs(params: Record<string, unknown>, fallback = 10_000): number {
|
|
161
194
|
const raw = params.timeoutMs;
|
|
162
195
|
if (typeof raw !== "number" || !Number.isSafeInteger(raw) || raw <= 0) return fallback;
|