agent-relay-runner 0.119.0 → 0.119.1
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-runner",
|
|
3
|
-
"version": "0.119.
|
|
3
|
+
"version": "0.119.1",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"directory": "runner"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"agent-relay-providers": "0.104.
|
|
24
|
-
"agent-relay-sdk": "0.2.
|
|
23
|
+
"agent-relay-providers": "0.104.2",
|
|
24
|
+
"agent-relay-sdk": "0.2.103",
|
|
25
25
|
"callmux": "0.23.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const { mkdirSync, renameSync, writeFileSync } = require("node:fs");
|
|
5
|
+
const { tmpdir } = require("node:os");
|
|
6
|
+
const { basename, dirname, join } = require("node:path");
|
|
7
|
+
|
|
8
|
+
let input = "";
|
|
9
|
+
process.stdin.setEncoding("utf8");
|
|
10
|
+
process.stdin.on("data", (chunk) => { input += chunk; });
|
|
11
|
+
process.stdin.on("end", () => {
|
|
12
|
+
const metrics = parseClaudeStatusLineInput(input);
|
|
13
|
+
if (metrics) writeState(metrics);
|
|
14
|
+
process.stdout.write(`${standaloneOutput(metrics)}\n`);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function parseClaudeStatusLineInput(raw) {
|
|
18
|
+
let data;
|
|
19
|
+
try {
|
|
20
|
+
data = JSON.parse(raw);
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
if (!isRecord(data) || !isRecord(data.context_window)) return null;
|
|
25
|
+
const context = data.context_window;
|
|
26
|
+
const timestamp = Date.now();
|
|
27
|
+
const contextPercent = numberValue(context.used_percentage);
|
|
28
|
+
const tokensMax = numberValue(context.context_window_size);
|
|
29
|
+
const usage = isRecord(context.current_usage) ? context.current_usage : undefined;
|
|
30
|
+
const tokensUsed = sumNumbers(
|
|
31
|
+
usage && usage.input_tokens,
|
|
32
|
+
usage && usage.cache_creation_input_tokens,
|
|
33
|
+
usage && usage.cache_read_input_tokens,
|
|
34
|
+
);
|
|
35
|
+
if (contextPercent === undefined && tokensUsed === undefined) return null;
|
|
36
|
+
|
|
37
|
+
const rateLimits = isRecord(data.rate_limits) ? data.rate_limits : undefined;
|
|
38
|
+
const fiveHour = isRecord(rateLimits && rateLimits.five_hour) ? rateLimits.five_hour : undefined;
|
|
39
|
+
const quotaWindows = quotaWindowsFromRateLimits(rateLimits);
|
|
40
|
+
const model = stringValue(data.model) || stringValue(process.env.CLAUDE_MODEL);
|
|
41
|
+
const effort = isRecord(data.effort) ? stringValue(data.effort.level) : stringValue(data.effort);
|
|
42
|
+
const resolvedEffort = effort || stringValue(process.env.CLAUDE_EFFORT) || stringValue(process.env.CLAUDE_CODE_EFFORT_LEVEL);
|
|
43
|
+
|
|
44
|
+
const metrics = {
|
|
45
|
+
agentId: stringValue(process.env.AGENT_RELAY_AGENT_ID) || stringValue(process.env.AGENT_RELAY_ID) || stringValue(data.session_id) || "unknown",
|
|
46
|
+
contextPercent: contextPercent !== undefined ? contextPercent : ((tokensUsed || 0) / (tokensMax || 1)) * 100,
|
|
47
|
+
source: "statusline",
|
|
48
|
+
confidence: "exact",
|
|
49
|
+
timestamp,
|
|
50
|
+
};
|
|
51
|
+
if (tokensUsed !== undefined) metrics.tokensUsed = tokensUsed;
|
|
52
|
+
if (tokensMax !== undefined) metrics.tokensMax = tokensMax;
|
|
53
|
+
const quotaUsed = numberValue(fiveHour && fiveHour.used_percentage);
|
|
54
|
+
if (quotaUsed !== undefined) metrics.quotaUsed = quotaUsed;
|
|
55
|
+
const quotaResetIn = secondsUntil(numberValue(fiveHour && fiveHour.resets_at), timestamp);
|
|
56
|
+
if (quotaResetIn !== undefined) metrics.quotaResetIn = quotaResetIn;
|
|
57
|
+
if (quotaWindows.length > 0) metrics.quotaWindows = quotaWindows;
|
|
58
|
+
if (model) metrics.model = model;
|
|
59
|
+
if (resolvedEffort) metrics.effort = resolvedEffort;
|
|
60
|
+
metrics.contextPercent = Math.max(0, Math.min(100, metrics.contextPercent));
|
|
61
|
+
return metrics;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeState(metrics) {
|
|
65
|
+
const file = join(process.env.AGENT_RELAY_CONTEXT_STATE_DIR || tmpdir(), `agent-relay-context-${safeStateId(metrics.agentId)}.json`);
|
|
66
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
67
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
68
|
+
writeFileSync(tmp, JSON.stringify(metrics, null, 2));
|
|
69
|
+
renameSync(tmp, file);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function quotaWindowsFromRateLimits(rateLimits) {
|
|
73
|
+
const windows = [];
|
|
74
|
+
for (const name of ["five_hour", "seven_day", "seven_day_sonnet", "seven_day_opus"]) {
|
|
75
|
+
const raw = isRecord(rateLimits && rateLimits[name]) ? rateLimits[name] : undefined;
|
|
76
|
+
const usedPercentage = numberValue(raw && raw.used_percentage);
|
|
77
|
+
if (usedPercentage === undefined) continue;
|
|
78
|
+
const window = { name, utilization: usedPercentage / 100, unit: "%" };
|
|
79
|
+
const resetsAt = epochSecondsToMs(numberValue(raw && raw.resets_at));
|
|
80
|
+
if (resetsAt !== undefined) window.resetsAt = resetsAt;
|
|
81
|
+
windows.push(window);
|
|
82
|
+
}
|
|
83
|
+
return windows;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function standaloneOutput(metrics) {
|
|
87
|
+
if (!metrics) return "agent-relay context: unavailable";
|
|
88
|
+
const pct = Math.round(metrics.contextPercent);
|
|
89
|
+
const tokens = metrics.tokensUsed && metrics.tokensMax ? ` ${formatTokens(metrics.tokensUsed)}/${formatTokens(metrics.tokensMax)}` : "";
|
|
90
|
+
return `agent-relay context: ${pct}%${tokens}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatTokens(tokens) {
|
|
94
|
+
if (tokens >= 1000000) return `${(tokens / 1000000).toFixed(1)}M`;
|
|
95
|
+
if (tokens >= 1000) return `${Math.round(tokens / 1000)}K`;
|
|
96
|
+
return String(tokens);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function safeStateId(agentId) {
|
|
100
|
+
const safe = basename(agentId).replace(/[^\w.-]/g, "-");
|
|
101
|
+
return safe || "unknown";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isRecord(value) {
|
|
105
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function stringValue(value) {
|
|
109
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function numberValue(value) {
|
|
113
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sumNumbers(...values) {
|
|
117
|
+
let total = 0;
|
|
118
|
+
let seen = false;
|
|
119
|
+
for (const value of values) {
|
|
120
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
121
|
+
total += value;
|
|
122
|
+
seen = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return seen ? total : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function secondsUntil(epochSeconds, nowMs) {
|
|
129
|
+
return epochSeconds === undefined ? undefined : Math.max(0, Math.round(epochSeconds - nowMs / 1000));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function epochSecondsToMs(epochSeconds) {
|
|
133
|
+
return epochSeconds === undefined ? undefined : epochSeconds * 1000;
|
|
134
|
+
}
|
|
@@ -71,7 +71,7 @@ var claudeProviderManifest = {
|
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
73
|
catalog: {
|
|
74
|
-
defaultModel: "sonnet-
|
|
74
|
+
defaultModel: "sonnet-5",
|
|
75
75
|
models: [
|
|
76
76
|
{ alias: "fable-5", label: "Fable 5", providerModel: "claude-fable-5", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", unavailable: "suspended by export-control directive, 2026-06-12", ...codeModel({ contextWindowTokens: CONTEXT_1M }) },
|
|
77
77
|
{ alias: "opus-4.8", label: "Opus 4.8", providerModel: "claude-opus-4-8", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_200K }) },
|
|
@@ -80,8 +80,8 @@ var claudeProviderManifest = {
|
|
|
80
80
|
{ alias: "opus-4.7[1m]", label: "Opus 4.7 1M", providerModel: "claude-opus-4-7[1m]", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_1M }) },
|
|
81
81
|
{ alias: "opus-4.6", label: "Opus 4.6", providerModel: "claude-opus-4-6", efforts: CLAUDE_LOW_TO_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_200K }) },
|
|
82
82
|
{ alias: "opus-4.6[1m]", label: "Opus 4.6 1M", providerModel: "claude-opus-4-6[1m]", efforts: CLAUDE_LOW_TO_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_1M }) },
|
|
83
|
-
{ alias: "sonnet-
|
|
84
|
-
{ alias: "sonnet-
|
|
83
|
+
{ alias: "sonnet-5", label: "Sonnet 5", providerModel: "claude-sonnet-5", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_200K }) },
|
|
84
|
+
{ alias: "sonnet-5[1m]", label: "Sonnet 5 1M", providerModel: "claude-sonnet-5[1m]", efforts: CLAUDE_LOW_TO_XHIGH_MAX, defaultEffort: "medium", ...codeModel({ contextWindowTokens: CONTEXT_1M }) },
|
|
85
85
|
{ alias: "haiku", label: "Haiku", providerModel: "haiku", efforts: [], ...codeModel() }
|
|
86
86
|
]
|
|
87
87
|
},
|
package/src/launch-assembly.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "node:fs";
|
|
2
2
|
import { resolve, join } from "node:path";
|
|
3
3
|
import type { AgentProfile, AgentProfileBase, ProvisioningMcpServer, SpawnProvider } from "agent-relay-sdk";
|
|
4
|
+
import { shellQuote } from "agent-relay-sdk/shell-utils";
|
|
4
5
|
import {
|
|
5
6
|
profileAllowsRelayFeature,
|
|
6
7
|
profileIsVanillaBase,
|
|
@@ -186,11 +187,15 @@ export function sessionStatusLineSettingsArgs(...argLists: string[][]): string[]
|
|
|
186
187
|
|
|
187
188
|
function relayStatusLineSettings(): Record<string, unknown> {
|
|
188
189
|
return {
|
|
189
|
-
statusLine: { type: "command", command:
|
|
190
|
+
statusLine: { type: "command", command: `node ${shellQuote(claudeStatusLineProbePath())}`, refreshInterval: 30 },
|
|
190
191
|
showThinkingSummaries: true,
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
194
|
|
|
195
|
+
function claudeStatusLineProbePath(): string {
|
|
196
|
+
return resolve(import.meta.dir, "../plugins/claude/claude-statusline-probe.cjs");
|
|
197
|
+
}
|
|
198
|
+
|
|
194
199
|
function claudeSettingsArgs(
|
|
195
200
|
projectSettings: Record<string, unknown>,
|
|
196
201
|
includeStatusLine: boolean,
|
package/src/runner-core.ts
CHANGED
|
@@ -125,6 +125,8 @@ const REACTIVE_TOKEN_RECOVERY_DEBOUNCE_MS = 10_000;
|
|
|
125
125
|
const UNEXPECTED_EXIT_WINDOW_MS = 2 * 60 * 1000;
|
|
126
126
|
const RAPID_EXIT_MS = 30 * 1000;
|
|
127
127
|
const MAX_RAPID_UNEXPECTED_EXITS = 3;
|
|
128
|
+
const UNDETERMINED_PROVIDER_EXIT_CONFIRM_MS = 1_500;
|
|
129
|
+
const PROVIDER_EXIT_CONFIRM_POLL_MS = 250;
|
|
128
130
|
// A UserPromptSubmit echo matching a runner-injected prompt within this window is
|
|
129
131
|
// the same prompt arriving back from the provider — drop it to avoid a duplicate.
|
|
130
132
|
const PROMPT_ECHO_DEDUP_MS = 30_000;
|
|
@@ -996,39 +998,67 @@ export class AgentRunner {
|
|
|
996
998
|
|
|
997
999
|
if (this.shouldStopUnexpectedProviderExit(diagnostics)) {
|
|
998
1000
|
const hasResumeId = typeof diagnostics.claudeResumeId === "string" && diagnostics.claudeResumeId.length > 0;
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1001
|
+
if (!hasResumeId) {
|
|
1002
|
+
const recoveredStatus = await this.recoveredStatusAfterUndeterminedProviderExit();
|
|
1003
|
+
if (recoveredStatus) {
|
|
1004
|
+
logger.warn("lifecycle", `${this.options.provider} exit signal recovered as ${recoveredStatus}; suppressing terminal exit`);
|
|
1005
|
+
this.publishRunnerTimelineEvent({
|
|
1006
|
+
status: "provider.restart_decision",
|
|
1007
|
+
id: `provider-restart-decision-${this.providerSessionId}-${now}`,
|
|
1008
|
+
timestamp: Date.now(),
|
|
1009
|
+
title: "Provider exit suppressed",
|
|
1010
|
+
body: `${this.options.provider} reported ${recoveredStatus} during exit confirmation; runner kept the provider live.`,
|
|
1011
|
+
icon: "ti-player-play",
|
|
1012
|
+
metadata: {
|
|
1013
|
+
eventType: "provider.restart_decision",
|
|
1014
|
+
decision: "suppress-recovered",
|
|
1015
|
+
reason: "undetermined-provider-exit-recovered",
|
|
1016
|
+
recoveredStatus,
|
|
1017
|
+
confirmWindowMs: UNDETERMINED_PROVIDER_EXIT_CONFIRM_MS,
|
|
1018
|
+
...diagnostics,
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
this.setProviderStatus(recoveredStatus);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
if (!hasResumeId && this.shouldRestartRecoverableProviderExit(diagnostics)) {
|
|
1026
|
+
logger.warn("lifecycle", `${this.options.provider} exit signal is early/recoverable; allowing fresh restart`);
|
|
1027
|
+
} else {
|
|
1028
|
+
logger.warn("lifecycle", `${this.options.provider} exited; leaving agent offline for manual recovery`);
|
|
1029
|
+
this.publishRunnerTimelineEvent({
|
|
1030
|
+
status: "provider.restart_decision",
|
|
1031
|
+
id: `provider-restart-decision-${this.providerSessionId}-${now}`,
|
|
1032
|
+
timestamp: Date.now(),
|
|
1033
|
+
title: "Provider restart skipped",
|
|
1034
|
+
body: hasResumeId
|
|
1035
|
+
? "Claude exited; runner will not auto-resume. Resume id captured for manual recovery."
|
|
1036
|
+
: "Claude exited; runner will not restart automatically.",
|
|
1037
|
+
icon: "ti-player-stop",
|
|
1038
|
+
metadata: {
|
|
1039
|
+
eventType: "provider.restart_decision",
|
|
1040
|
+
decision: "stop-surface",
|
|
1041
|
+
reason: hasResumeId ? "claude-exit-manual-resume-available" : "claude-exit-manual-intervention-required",
|
|
1042
|
+
...diagnostics,
|
|
1043
|
+
},
|
|
1044
|
+
});
|
|
1045
|
+
this.process = undefined;
|
|
1046
|
+
// #633: the runner stays alive (manual-resume hold), so neither the orchestrator tmux-gone
|
|
1047
|
+
// path nor the heartbeat reaper will ever fire. Stamp the terminal marker so the SAME offline
|
|
1048
|
+
// status frame (published by setProviderStatus below) carries it; the server's bus status
|
|
1049
|
+
// handler turns that edge into a terminal `agent.exited` event with a #636 diagnosis.
|
|
1050
|
+
this.terminalProviderExit = {
|
|
1012
1051
|
reason: hasResumeId ? "claude-exit-manual-resume-available" : "claude-exit-manual-intervention-required",
|
|
1013
1052
|
...diagnostics,
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
...diagnostics,
|
|
1024
|
-
};
|
|
1025
|
-
this.setProviderStatus({
|
|
1026
|
-
status,
|
|
1027
|
-
reason: "provider-turn",
|
|
1028
|
-
id: `provider-exit-${this.providerSessionId}`,
|
|
1029
|
-
clear: ["provider-turn", "subagent", "background-script"],
|
|
1030
|
-
});
|
|
1031
|
-
return;
|
|
1053
|
+
};
|
|
1054
|
+
this.setProviderStatus({
|
|
1055
|
+
status,
|
|
1056
|
+
reason: "provider-turn",
|
|
1057
|
+
id: `provider-exit-${this.providerSessionId}`,
|
|
1058
|
+
clear: ["provider-turn", "subagent", "background-script"],
|
|
1059
|
+
});
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1032
1062
|
}
|
|
1033
1063
|
|
|
1034
1064
|
if (runtimeMs < RAPID_EXIT_MS && recent.length > MAX_RAPID_UNEXPECTED_EXITS) {
|
|
@@ -1094,6 +1124,28 @@ export class AgentRunner {
|
|
|
1094
1124
|
return getManifest(this.options.provider)?.launch?.managedPolicyPrompt?.alwaysOn === "append-system-prompt" && diagnostics.exitCommandInProgress !== true;
|
|
1095
1125
|
}
|
|
1096
1126
|
|
|
1127
|
+
private shouldRestartRecoverableProviderExit(diagnostics: Record<string, unknown>): boolean {
|
|
1128
|
+
return diagnostics.exitSource === "tmux-session-ended"
|
|
1129
|
+
&& typeof diagnostics.runtimeMs === "number"
|
|
1130
|
+
&& diagnostics.runtimeMs >= 0
|
|
1131
|
+
&& diagnostics.runtimeMs < RAPID_EXIT_MS;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private async recoveredStatusAfterUndeterminedProviderExit(): Promise<"busy" | "idle" | undefined> {
|
|
1135
|
+
if (!this.process || !this.options.adapter.probeActivity) return undefined;
|
|
1136
|
+
const deadline = Date.now() + UNDETERMINED_PROVIDER_EXIT_CONFIRM_MS;
|
|
1137
|
+
do {
|
|
1138
|
+
const activity = await this.options.adapter.probeActivity(this.process).catch((error) => {
|
|
1139
|
+
this.sessionLog(`provider-exit confirmation probe failed: ${errMessage(error)}`);
|
|
1140
|
+
return "unknown" as const;
|
|
1141
|
+
});
|
|
1142
|
+
if (activity === "busy" || activity === "idle") return activity;
|
|
1143
|
+
if (Date.now() >= deadline) return undefined;
|
|
1144
|
+
await Bun.sleep(PROVIDER_EXIT_CONFIRM_POLL_MS);
|
|
1145
|
+
} while (!this.stopped && !this.exitCommandInProgress);
|
|
1146
|
+
return undefined;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1097
1149
|
private async shutdownProvider(hard: boolean, timeoutMs = this.options.providerConfig.headless.shutdownTimeoutMs): Promise<void> {
|
|
1098
1150
|
this.lifecycleAction = hard ? "killing" : "shutting-down";
|
|
1099
1151
|
this.publishStatus();
|