agent-relay-runner 0.63.0 → 0.63.2
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 +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/src/adapter.ts +2 -1
- package/src/adapters/claude-transcript.ts +34 -0
- package/src/attachment-cache.ts +4 -3
- package/src/config.ts +143 -5
- package/src/index.ts +30 -13
- package/src/logger.ts +2 -1
- package/src/outbox.ts +2 -1
- package/src/profile-home.ts +2 -1
- package/src/runner-core.ts +30 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.63.
|
|
3
|
+
"version": "0.63.2",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"directory": "runner"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"agent-relay-sdk": "0.2.
|
|
23
|
+
"agent-relay-sdk": "0.2.42"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/adapter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentProfile, Message } from "agent-relay-sdk";
|
|
2
2
|
import { isRecord } from "agent-relay-sdk";
|
|
3
3
|
import type { SessionEvent } from "./session-insights";
|
|
4
|
+
import { messageBodyMaxCharsFromEnv } from "./config";
|
|
4
5
|
|
|
5
6
|
export type SemanticStatus = "idle" | "busy" | "offline" | "error";
|
|
6
7
|
type ProviderWorkKind = "provider-turn" | "subagent";
|
|
@@ -202,7 +203,7 @@ export const DEFAULT_PROVIDER_MESSAGE_BODY_MAX_CHARS = 24_000;
|
|
|
202
203
|
// Resolve the delivered-body cap. Deployment-dependent (the right ceiling tracks the host's
|
|
203
204
|
// context budget), so it's overridable via env — set on the orchestrator/host that spawns runners.
|
|
204
205
|
export function providerMessageBodyMaxChars(): number {
|
|
205
|
-
const raw =
|
|
206
|
+
const raw = messageBodyMaxCharsFromEnv();
|
|
206
207
|
if (raw !== undefined) {
|
|
207
208
|
const parsed = Number.parseInt(raw.trim(), 10);
|
|
208
209
|
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
@@ -202,6 +202,40 @@ export function extractFinalAssistantMessage(jsonl: string): string {
|
|
|
202
202
|
return lastText.trim();
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Like extractFinalAssistantMessage but skips the first `afterEntry` valid JSON
|
|
207
|
+
* entries. Used when a PreToolUse pre-flush already emitted the text before a
|
|
208
|
+
* blocking control, so final-mode Stop capture only sees later assistant text.
|
|
209
|
+
*/
|
|
210
|
+
export function extractFinalAssistantMessageAfterEntry(jsonl: string, afterEntry: number): string {
|
|
211
|
+
const lines = jsonl.split("\n");
|
|
212
|
+
let lastText = "";
|
|
213
|
+
let pastLastUserPrompt = afterEntry > 0;
|
|
214
|
+
let entryIndex = 0;
|
|
215
|
+
for (const line of lines) {
|
|
216
|
+
const trimmed = line.trim();
|
|
217
|
+
if (!trimmed) continue;
|
|
218
|
+
let entry: TranscriptEntry;
|
|
219
|
+
try {
|
|
220
|
+
entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
221
|
+
} catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
entryIndex++;
|
|
225
|
+
if (isSidechainEntry(entry)) continue;
|
|
226
|
+
if (entryIndex <= afterEntry) continue;
|
|
227
|
+
if (isRealUserPrompt(entry)) {
|
|
228
|
+
pastLastUserPrompt = true;
|
|
229
|
+
lastText = "";
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (!pastLastUserPrompt) continue;
|
|
233
|
+
const text = assistantText(entry);
|
|
234
|
+
if (text) lastText = text;
|
|
235
|
+
}
|
|
236
|
+
return lastText.trim();
|
|
237
|
+
}
|
|
238
|
+
|
|
205
239
|
/**
|
|
206
240
|
* Extract text from the `last_assistant_message` field in the Stop hook
|
|
207
241
|
* payload. This is the content of the final assistant message — either a plain
|
package/src/attachment-cache.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { basename, join } from "node:path";
|
|
|
4
4
|
import type { Artifact, Message } from "agent-relay-sdk";
|
|
5
5
|
import { errMessage, isRecord } from "agent-relay-sdk";
|
|
6
6
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
7
|
+
import { attachmentCacheDirFromEnv } from "./config";
|
|
7
8
|
|
|
8
9
|
const DEFAULT_CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
10
|
|
|
@@ -34,11 +35,11 @@ function attachmentRefs(message: Message): Record<string, unknown>[] {
|
|
|
34
35
|
return refs.filter(isRecord);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
function attachmentCacheRoot(agentId: string, rootDir =
|
|
38
|
+
function attachmentCacheRoot(agentId: string, rootDir = attachmentCacheDirFromEnv()): string {
|
|
38
39
|
return join(attachmentCacheBase(rootDir), safePathPart(agentId));
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function attachmentCacheBase(rootDir =
|
|
42
|
+
function attachmentCacheBase(rootDir = attachmentCacheDirFromEnv()): string {
|
|
42
43
|
return rootDir || join(homedir(), ".agent-relay", "attachments");
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -147,7 +148,7 @@ function withCacheError(ref: Record<string, unknown>, error: unknown): Record<st
|
|
|
147
148
|
|
|
148
149
|
export function pruneAttachmentCache(
|
|
149
150
|
agentId: string,
|
|
150
|
-
rootDir =
|
|
151
|
+
rootDir = attachmentCacheDirFromEnv(),
|
|
151
152
|
maxAgeMs = DEFAULT_CACHE_MAX_AGE_MS,
|
|
152
153
|
now = Date.now(),
|
|
153
154
|
): void {
|
package/src/config.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, hostname } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { DEFAULT_RELAY_URL, RELAY_TOKEN_HEADER, errMessage, stringValue } from "agent-relay-sdk";
|
|
6
6
|
import type { SettingEntry, WorkspaceMetadata } from "agent-relay-sdk";
|
|
7
7
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
interface ProviderConfig {
|
|
10
|
+
command: string;
|
|
11
|
+
defaultArgs: string[];
|
|
12
|
+
env: Record<string, string>;
|
|
13
|
+
pluginDirs: string[];
|
|
14
|
+
defaultCapabilities: string[];
|
|
15
|
+
defaultApprovalMode: string;
|
|
16
|
+
defaultTags: string[];
|
|
17
|
+
chatCaptureMode: "final" | "full";
|
|
18
|
+
reasoningCapture?: boolean;
|
|
19
|
+
headless: {
|
|
20
|
+
tmuxPrefix: string;
|
|
21
|
+
shutdownTimeoutMs: number;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
9
24
|
|
|
10
25
|
interface GlobalRunnerConfig {
|
|
11
26
|
relayUrl: string;
|
|
@@ -17,7 +32,7 @@ interface LoadedProviderConfig extends ProviderConfig {
|
|
|
17
32
|
path: string;
|
|
18
33
|
}
|
|
19
34
|
|
|
20
|
-
function agentRelayHome(): string {
|
|
35
|
+
export function agentRelayHome(): string {
|
|
21
36
|
return process.env.AGENT_RELAY_HOME || join(homedir(), ".agent-relay");
|
|
22
37
|
}
|
|
23
38
|
|
|
@@ -48,12 +63,135 @@ export function loadGlobalConfig(home = agentRelayHome()): GlobalRunnerConfig {
|
|
|
48
63
|
const path = join(home, "config.json");
|
|
49
64
|
const parsed = readJson(path);
|
|
50
65
|
return {
|
|
51
|
-
relayUrl: stringValue(parsed.relayUrl) ??
|
|
52
|
-
token: stringValue(parsed.token) ??
|
|
66
|
+
relayUrl: stringValue(parsed.relayUrl) ?? relayUrlFromEnv() ?? DEFAULT_RELAY_URL,
|
|
67
|
+
token: stringValue(parsed.token) ?? relayTokenFromEnv(),
|
|
53
68
|
defaultCwd: stringValue(parsed.defaultCwd) ?? process.cwd(),
|
|
54
69
|
};
|
|
55
70
|
}
|
|
56
71
|
|
|
72
|
+
function relayUrlFromEnv(): string | undefined {
|
|
73
|
+
return process.env.AGENT_RELAY_URL;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function relayTokenFromEnv(): string | undefined {
|
|
77
|
+
return process.env.AGENT_RELAY_TOKEN;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function runtimeTokenProfileFromEnv(): string | undefined {
|
|
81
|
+
return process.env.AGENT_RELAY_TOKEN_PROFILE;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function runtimeTokenJtiFromEnv(): string | undefined {
|
|
85
|
+
return process.env.AGENT_RELAY_TOKEN_JTI;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function runtimeTokenExpiresAtFromEnv(): string | undefined {
|
|
89
|
+
return process.env.AGENT_RELAY_TOKEN_EXPIRES_AT;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function agentProfileNameFromEnv(): string | undefined {
|
|
93
|
+
return process.env.AGENT_RELAY_AGENT_PROFILE;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function agentProfileJsonFromEnv(): string | undefined {
|
|
97
|
+
return process.env.AGENT_RELAY_AGENT_PROFILE_JSON;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function workspaceJsonFromEnv(): string | undefined {
|
|
101
|
+
return process.env.AGENT_RELAY_WORKSPACE_JSON;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function tmuxSessionFromEnv(): string | undefined {
|
|
105
|
+
return process.env.AGENT_RELAY_TMUX_SESSION;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function policyNameFromEnv(): string | undefined {
|
|
109
|
+
return process.env.AGENT_RELAY_POLICY;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function spawnRequestIdFromEnv(): string | undefined {
|
|
113
|
+
return process.env.AGENT_RELAY_SPAWN_REQUEST_ID;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function automationIdFromEnv(): string | undefined {
|
|
117
|
+
return process.env.AGENT_RELAY_AUTOMATION_ID;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function automationRunIdFromEnv(): string | undefined {
|
|
121
|
+
return process.env.AGENT_RELAY_AUTOMATION_RUN_ID;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function lifecycleFromEnv(): string | undefined {
|
|
125
|
+
return process.env.AGENT_RELAY_LIFECYCLE;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function sessionDebugEnabled(): boolean {
|
|
129
|
+
return process.env.AGENT_RELAY_SESSION_DEBUG === "1";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function logLevelFromEnv(): string | undefined {
|
|
133
|
+
return process.env.AGENT_RELAY_LOG_LEVEL;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function mcpProxyEnabledFromEnv(): boolean {
|
|
137
|
+
return !["0", "false", "off"].includes((process.env.AGENT_RELAY_MCP_PROXY ?? "").trim().toLowerCase());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function runnerInfoFileFromEnv(): string | undefined {
|
|
141
|
+
return process.env.AGENT_RELAY_RUNNER_INFO_FILE;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function runnerOutboxDirFromEnv(): string | undefined {
|
|
145
|
+
return process.env.AGENT_RELAY_RUNNER_OUTBOX_DIR;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function runnerOutboxDirWithInfoFallback(): string | undefined {
|
|
149
|
+
const dir = runnerOutboxDirFromEnv();
|
|
150
|
+
if (dir) return dir;
|
|
151
|
+
const infoFile = runnerInfoFileFromEnv();
|
|
152
|
+
return infoFile ? join(dirname(infoFile), "outbox") : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function capsFromEnv(): string | undefined {
|
|
156
|
+
return process.env.AGENT_RELAY_CAPS;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function tagsFromEnv(): string | undefined {
|
|
160
|
+
return process.env.AGENT_RELAY_TAGS;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function workspaceModeFromEnv(): string | undefined {
|
|
164
|
+
return process.env.AGENT_RELAY_WORKSPACE_MODE;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function orchestratorBaseDirFromEnv(): string | undefined {
|
|
168
|
+
return process.env.AGENT_RELAY_ORCHESTRATOR_BASE_DIR;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function runnerLogFileFromEnv(): string | undefined {
|
|
172
|
+
return typeof process.env.AGENT_RELAY_LOG_FILE === "string" ? process.env.AGENT_RELAY_LOG_FILE : undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function orchestratorUrlFromEnv(): string | undefined {
|
|
176
|
+
return process.env.AGENT_RELAY_ORCHESTRATOR_URL;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function contextStateDirFromEnv(): string | undefined {
|
|
180
|
+
return process.env.AGENT_RELAY_CONTEXT_STATE_DIR;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function attachmentCacheDirFromEnv(): string | undefined {
|
|
184
|
+
return process.env.AGENT_RELAY_ATTACHMENT_CACHE_DIR;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function providerHomeRootFromEnv(): string {
|
|
188
|
+
return process.env.AGENT_RELAY_PROVIDER_HOME_ROOT || join(homedir(), ".agent-relay", "provider-homes");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function messageBodyMaxCharsFromEnv(): string | undefined {
|
|
192
|
+
return process.env.AGENT_RELAY_MESSAGE_BODY_MAX_CHARS;
|
|
193
|
+
}
|
|
194
|
+
|
|
57
195
|
export function loadProviderConfig(provider: string, home = agentRelayHome()): LoadedProviderConfig {
|
|
58
196
|
const path = join(providersDir(home), `${provider}.json`);
|
|
59
197
|
const raw = readJson(path);
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,24 @@ import { hostname } from "node:os";
|
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { isatty } from "node:tty";
|
|
5
5
|
import { AgentRunner } from "./runner";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
agentProfileJsonFromEnv,
|
|
8
|
+
agentProfileNameFromEnv,
|
|
9
|
+
automationIdFromEnv,
|
|
10
|
+
automationRunIdFromEnv,
|
|
11
|
+
lifecycleFromEnv,
|
|
12
|
+
loadGlobalConfig,
|
|
13
|
+
loadProviderConfigFromRelay,
|
|
14
|
+
policyNameFromEnv,
|
|
15
|
+
resolveCwd,
|
|
16
|
+
runnerId,
|
|
17
|
+
runtimeTokenExpiresAtFromEnv,
|
|
18
|
+
runtimeTokenJtiFromEnv,
|
|
19
|
+
runtimeTokenProfileFromEnv,
|
|
20
|
+
spawnRequestIdFromEnv,
|
|
21
|
+
tmuxSessionFromEnv,
|
|
22
|
+
workspaceJsonFromEnv,
|
|
23
|
+
} from "./config";
|
|
7
24
|
import { createProviderAdapter } from "./providers";
|
|
8
25
|
import { VERSION } from "./version";
|
|
9
26
|
import type { AgentProfile, SpawnProvider, WorkspaceMetadata } from "agent-relay-sdk";
|
|
@@ -49,9 +66,9 @@ export async function main(argv = process.argv): Promise<void> {
|
|
|
49
66
|
interactive: opts.interactive,
|
|
50
67
|
relayUrl,
|
|
51
68
|
token: opts.token ?? globalConfig.token,
|
|
52
|
-
runtimeTokenProfile:
|
|
53
|
-
runtimeTokenJti:
|
|
54
|
-
runtimeTokenExpiresAt: parseTokenExpiresAt(
|
|
69
|
+
runtimeTokenProfile: runtimeTokenProfileFromEnv(),
|
|
70
|
+
runtimeTokenJti: runtimeTokenJtiFromEnv(),
|
|
71
|
+
runtimeTokenExpiresAt: parseTokenExpiresAt(runtimeTokenExpiresAtFromEnv()),
|
|
55
72
|
provider: opts.provider,
|
|
56
73
|
cwd,
|
|
57
74
|
runnerId: id,
|
|
@@ -84,20 +101,20 @@ export async function main(argv = process.argv): Promise<void> {
|
|
|
84
101
|
approvalMode,
|
|
85
102
|
label: opts.label,
|
|
86
103
|
rig: opts.rig,
|
|
87
|
-
profile: opts.profile ??
|
|
88
|
-
agentProfile: parseAgentProfileEnv(
|
|
89
|
-
workspace: parseWorkspaceEnv(
|
|
104
|
+
profile: opts.profile ?? agentProfileNameFromEnv(),
|
|
105
|
+
agentProfile: parseAgentProfileEnv(agentProfileJsonFromEnv()),
|
|
106
|
+
workspace: parseWorkspaceEnv(workspaceJsonFromEnv()),
|
|
90
107
|
prompt: opts.prompt,
|
|
91
108
|
systemPromptAppend: opts.systemPromptAppend,
|
|
92
|
-
tmuxSession:
|
|
109
|
+
tmuxSession: tmuxSessionFromEnv(),
|
|
93
110
|
tags: opts.tags,
|
|
94
111
|
capabilities: opts.caps,
|
|
95
112
|
providerArgs: opts.providerArgs,
|
|
96
|
-
policyName:
|
|
97
|
-
spawnRequestId:
|
|
98
|
-
automationId:
|
|
99
|
-
automationRunId:
|
|
100
|
-
lifecycle: normalizeAgentLifecycle(
|
|
113
|
+
policyName: policyNameFromEnv(),
|
|
114
|
+
spawnRequestId: spawnRequestIdFromEnv(),
|
|
115
|
+
automationId: automationIdFromEnv(),
|
|
116
|
+
automationRunId: automationRunIdFromEnv(),
|
|
117
|
+
lifecycle: normalizeAgentLifecycle(lifecycleFromEnv()) ?? "persistent",
|
|
101
118
|
startedAt: Date.now(),
|
|
102
119
|
providerConfig,
|
|
103
120
|
adapter,
|
package/src/logger.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { appendFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
4
|
+
import { logLevelFromEnv } from "./config";
|
|
4
5
|
|
|
5
6
|
// Phase 1 observability (#198): one leveled, runtime-togglable logger for the
|
|
6
7
|
// Runner and the provider adapters below it. Replaces the ad-hoc scatter of
|
|
@@ -47,7 +48,7 @@ export class Logger {
|
|
|
47
48
|
private logDir: string;
|
|
48
49
|
|
|
49
50
|
constructor(config: LoggerConfig = {}) {
|
|
50
|
-
this.level = config.level ?? parseLogLevel(
|
|
51
|
+
this.level = config.level ?? parseLogLevel(logLevelFromEnv()) ?? "info";
|
|
51
52
|
this.agentId = config.agentId ?? "runner";
|
|
52
53
|
this.headless = config.headless ?? false;
|
|
53
54
|
this.logDir = config.logDir ?? join(process.env.HOME || ".", ".agent-relay", "logs");
|
package/src/outbox.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { tmpdir } from "node:os";
|
|
|
5
5
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
6
6
|
import { errMessage } from "agent-relay-sdk";
|
|
7
7
|
import { logger } from "./logger";
|
|
8
|
+
import { runnerOutboxDirFromEnv } from "./config";
|
|
8
9
|
|
|
9
10
|
// Phase 2 (#196) — the "nothing is ever lost" half. Runner→server events that used to be
|
|
10
11
|
// fire-and-forget over HTTP (session turns, reasoning/tool traces, prompt echoes, insights,
|
|
@@ -112,7 +113,7 @@ export class Outbox {
|
|
|
112
113
|
this.maxBackoffMs = options.maxBackoffMs ?? DEFAULTS.maxBackoffMs;
|
|
113
114
|
this.pollMs = options.pollMs ?? DEFAULTS.pollMs;
|
|
114
115
|
|
|
115
|
-
const dir = options.dir ??
|
|
116
|
+
const dir = options.dir ?? runnerOutboxDirFromEnv() ?? join(tmpdir(), "agent-relay-outbox");
|
|
116
117
|
this.path = options.dir === ":memory:" ? ":memory:" : join(dir, `outbox-${safeName(this.agentId)}.sqlite`);
|
|
117
118
|
if (this.path !== ":memory:") mkdirSync(dirname(this.path), { recursive: true });
|
|
118
119
|
|
package/src/profile-home.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { join, resolve } from "node:path";
|
|
|
4
4
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
5
5
|
import { profileAllowsRelayFeature, type RunnerSpawnConfig } from "./adapter";
|
|
6
6
|
import { CLAUDE_RELAY_MANUAL } from "./relay-instructions";
|
|
7
|
+
import { providerHomeRootFromEnv } from "./config";
|
|
7
8
|
|
|
8
9
|
type ProviderHome = {
|
|
9
10
|
path: string;
|
|
@@ -105,7 +106,7 @@ function readHostClaudeConfig(): Record<string, unknown> | undefined {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
function providerHomePath(provider: "claude" | "codex", config: RunnerSpawnConfig): string {
|
|
108
|
-
const root =
|
|
109
|
+
const root = providerHomeRootFromEnv();
|
|
109
110
|
const profileName = sanitizePathPart(config.agentProfile?.name || config.profile || "profile");
|
|
110
111
|
const instance = sanitizePathPart(config.instanceId || config.runnerId);
|
|
111
112
|
return join(root, provider, profileName, instance);
|
package/src/runner-core.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { ClaimTracker } from "./claim-tracker";
|
|
|
12
12
|
import { startControlServer, type ControlServer } from "./control-server";
|
|
13
13
|
import { ReplyObligationCache, obligationRequiresExplicitReply } from "./reply-obligation-cache";
|
|
14
14
|
import { Outbox, type OutboxRecord } from "./outbox";
|
|
15
|
-
import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
|
|
15
|
+
import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractFinalAssistantMessageAfterEntry, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
|
|
16
16
|
import { profileUsesHostProviderGlobals } from "./profile-home";
|
|
17
17
|
import { RELAY_MCP_TOKEN_ENV, relayMcpEndpoint } from "./relay-mcp";
|
|
18
18
|
import { RelayMcpProxy } from "./relay-mcp-proxy";
|
|
@@ -22,6 +22,7 @@ import { ensureSessionScratch, reapSessionScratch, sweepStaleSessions, type Sess
|
|
|
22
22
|
import { deliverBufferedMcpCall as deliverBufferedMcpOutboxCall, deliverRunnerOutboxEvent } from "./mcp-outbox";
|
|
23
23
|
import { RunnerInsights } from "./runner-insights";
|
|
24
24
|
import { BusyReconciler } from "./busy-reconciler";
|
|
25
|
+
import { capsFromEnv, contextStateDirFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
|
|
25
26
|
import {
|
|
26
27
|
appliedAgentProfileMetadata,
|
|
27
28
|
commandTimeoutMs,
|
|
@@ -272,7 +273,7 @@ export class AgentRunner {
|
|
|
272
273
|
private injectedPrompts: Array<{ text: string; at: number }> = [];
|
|
273
274
|
// Verbose session-mirror diagnostics (turn lifecycle, reconciler probes, tail
|
|
274
275
|
// emits) → a dedicated clean log. Always on for key transitions; AGENT_RELAY_SESSION_DEBUG=1 adds the high-frequency probe/emit lines.
|
|
275
|
-
private readonly sessionDebugVerbose =
|
|
276
|
+
private readonly sessionDebugVerbose = sessionDebugEnabled();
|
|
276
277
|
// Tracks whether the provider is in a legitimate blocked/approval state, so the
|
|
277
278
|
// busy reconciler doesn't mistake a permission prompt for a stuck-busy turn.
|
|
278
279
|
private providerBlocked = false;
|
|
@@ -299,21 +300,20 @@ export class AgentRunner {
|
|
|
299
300
|
logger.configure({
|
|
300
301
|
agentId: this.agentId,
|
|
301
302
|
headless: options.headless,
|
|
302
|
-
...(this.sessionDebugVerbose && !parseLogLevel(
|
|
303
|
+
...(this.sessionDebugVerbose && !parseLogLevel(logLevelFromEnv()) ? { level: "debug" as const } : {}),
|
|
303
304
|
});
|
|
304
305
|
this.currentToken = options.token;
|
|
305
306
|
this.currentTokenJti = options.tokenJti;
|
|
306
307
|
this.currentTokenProfileId = options.tokenProfileId;
|
|
307
308
|
this.currentTokenExpiresAt = options.tokenExpiresAt;
|
|
308
|
-
this.mcpProxyEnabled =
|
|
309
|
+
this.mcpProxyEnabled = mcpProxyEnabledFromEnv();
|
|
309
310
|
this.mcpProxySecret = crypto.randomUUID();
|
|
310
311
|
const runtime = runtimeMetadata(options.provider);
|
|
311
312
|
this.http = new RelayHttpClient({ baseUrl: options.relayUrl, token: this.currentToken });
|
|
312
313
|
this.obligationCache = new ReplyObligationCache({ fetch: () => this.http.listReplyObligations(this.agentId) });
|
|
313
314
|
// Co-locate the durable outbox with the runner's runtime state (survives reboot) when the
|
|
314
315
|
// orchestrator told us where that is; otherwise the Outbox falls back to a temp dir.
|
|
315
|
-
const outboxDir =
|
|
316
|
-
?? (process.env.AGENT_RELAY_RUNNER_INFO_FILE ? join(dirname(process.env.AGENT_RELAY_RUNNER_INFO_FILE), "outbox") : undefined);
|
|
316
|
+
const outboxDir = runnerOutboxDirWithInfoFallback();
|
|
317
317
|
this.outbox = new Outbox({ agentId: this.agentId, dir: outboxDir, send: (record) => this.deliverOutboxEvent(record) });
|
|
318
318
|
this.sessionOutbox = new Outbox({
|
|
319
319
|
agentId: `${this.agentId}-session`,
|
|
@@ -359,7 +359,7 @@ export class AgentRunner {
|
|
|
359
359
|
...new Set([
|
|
360
360
|
...options.providerConfig.defaultCapabilities,
|
|
361
361
|
...options.capabilities,
|
|
362
|
-
...csvTags(
|
|
362
|
+
...csvTags(capsFromEnv()),
|
|
363
363
|
"lifecycle.shutdown.hard",
|
|
364
364
|
"lifecycle.restart.hard",
|
|
365
365
|
"lifecycle.status.semantic",
|
|
@@ -369,7 +369,7 @@ export class AgentRunner {
|
|
|
369
369
|
"capabilities.report",
|
|
370
370
|
]),
|
|
371
371
|
],
|
|
372
|
-
tags: [...new Set([options.provider, ...csvTags(
|
|
372
|
+
tags: [...new Set([options.provider, ...csvTags(tagsFromEnv()), ...options.tags, ...options.providerConfig.defaultTags, ...(options.headless ? ["headless"] : [])])],
|
|
373
373
|
meta: {
|
|
374
374
|
...runtime,
|
|
375
375
|
version: runtime.package.version,
|
|
@@ -388,7 +388,7 @@ export class AgentRunner {
|
|
|
388
388
|
automationRunId: options.automationRunId ?? null,
|
|
389
389
|
workspace: options.workspace ?? null,
|
|
390
390
|
lifecycle: options.lifecycle ?? "persistent",
|
|
391
|
-
workspaceMode: options.workspace?.requestedMode ?? options.workspace?.mode ??
|
|
391
|
+
workspaceMode: options.workspace?.requestedMode ?? options.workspace?.mode ?? workspaceModeFromEnv() ?? null,
|
|
392
392
|
runnerManaged: true,
|
|
393
393
|
cwd: options.cwd,
|
|
394
394
|
approvalMode: options.approvalMode,
|
|
@@ -471,7 +471,7 @@ export class AgentRunner {
|
|
|
471
471
|
reapSessionScratch({
|
|
472
472
|
agentId: this.agentId,
|
|
473
473
|
cwd: this.options.cwd,
|
|
474
|
-
fallbackBaseDir:
|
|
474
|
+
fallbackBaseDir: orchestratorBaseDirFromEnv(),
|
|
475
475
|
});
|
|
476
476
|
if (!alreadyStopped) await this.bus.statusAsync({ agentStatus: "offline", ready: false });
|
|
477
477
|
if (this.process && !alreadyStopped) {
|
|
@@ -528,7 +528,7 @@ export class AgentRunner {
|
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
private ownsIsolatedWorktree(): boolean {
|
|
531
|
-
const mode = this.options.workspace?.requestedMode ?? this.options.workspace?.mode ??
|
|
531
|
+
const mode = this.options.workspace?.requestedMode ?? this.options.workspace?.mode ?? workspaceModeFromEnv();
|
|
532
532
|
return mode === "isolated";
|
|
533
533
|
}
|
|
534
534
|
|
|
@@ -607,7 +607,7 @@ export class AgentRunner {
|
|
|
607
607
|
}
|
|
608
608
|
|
|
609
609
|
private writeRunnerInfoFile(): void {
|
|
610
|
-
const file =
|
|
610
|
+
const file = runnerInfoFileFromEnv();
|
|
611
611
|
if (!file || !this.control) return;
|
|
612
612
|
try {
|
|
613
613
|
mkdirSync(dirname(file), { recursive: true });
|
|
@@ -1101,7 +1101,7 @@ export class AgentRunner {
|
|
|
1101
1101
|
const tmuxSession = typeof this.process?.meta?.tmuxSession === "string" ? this.process.meta.tmuxSession : undefined;
|
|
1102
1102
|
const tmuxSocket = typeof this.process?.meta?.tmuxSocket === "string" ? this.process.meta.tmuxSocket : undefined;
|
|
1103
1103
|
const exitSource = tmuxSession ? "tmux-session-ended" : this.process?.process ? "process-exit" : "provider-status";
|
|
1104
|
-
const logFile =
|
|
1104
|
+
const logFile = runnerLogFileFromEnv();
|
|
1105
1105
|
const claudeResumeId = this.options.provider === "claude" && logFile ? latestClaudeResumeIdFromLogFile(logFile) : undefined;
|
|
1106
1106
|
return {
|
|
1107
1107
|
status,
|
|
@@ -1306,10 +1306,9 @@ export class AgentRunner {
|
|
|
1306
1306
|
// no relay message) are mirrored too. A reply obligation, when present, is still
|
|
1307
1307
|
// used as replyTo so the Stop hook stops nagging the agent to /reply.
|
|
1308
1308
|
//
|
|
1309
|
-
// isPreFlush: true (#435) — mid-turn
|
|
1310
|
-
//
|
|
1311
|
-
//
|
|
1312
|
-
// entry-count cursor so the eventual Stop-hook call (full mode) skips it.
|
|
1309
|
+
// isPreFlush: true (#435/#499) — mid-turn PreToolUse boundary. Drain the live
|
|
1310
|
+
// tail and force-flush session messages before showing the blocking control;
|
|
1311
|
+
// the entry cursor lets Stop capture skip already-emitted text.
|
|
1313
1312
|
private async publishSessionTurn(input: { transcriptPath: string; lastAssistantMessage?: unknown; isPreFlush?: boolean }): Promise<void> {
|
|
1314
1313
|
if (input.transcriptPath) this.lastTranscriptPath = input.transcriptPath;
|
|
1315
1314
|
|
|
@@ -1321,15 +1320,12 @@ export class AgentRunner {
|
|
|
1321
1320
|
const body = extractLastAssistantTurnAfterEntry(jsonl, this.narrativeFlushEntryCount);
|
|
1322
1321
|
await this.drainReasoningTail();
|
|
1323
1322
|
this.narrativeFlushEntryCount = countTranscriptEntries(jsonl);
|
|
1324
|
-
if (
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
body,
|
|
1331
|
-
session: { type: "response", origin: "provider", ...(turnId ? { turnId } : {}) },
|
|
1332
|
-
});
|
|
1323
|
+
if (body && !this.preFlushBodyAlreadyMirroredByReasoningTail(jsonl, body)) {
|
|
1324
|
+
const turnId = this.currentTurnId;
|
|
1325
|
+
this.sessionLog(`pre-flush narrative for turn ${turnId ?? "?"} (${body.length} chars)`);
|
|
1326
|
+
await this.publishSessionEvent({ from: this.agentId, to: "user", body, session: { type: "response", origin: "provider", ...(turnId ? { turnId } : {}) } });
|
|
1327
|
+
}
|
|
1328
|
+
await this.sessionOutbox.flush(2_000).then((ok) => { if (!ok) this.sessionLog(`pre-flush session outbox incomplete before blocking control (${this.sessionOutbox.pendingCount()} pending)`); }, (error) => this.sessionLog(`pre-flush session outbox failed before blocking control: ${errMessage(error)}`));
|
|
1333
1329
|
return;
|
|
1334
1330
|
}
|
|
1335
1331
|
|
|
@@ -1368,11 +1364,10 @@ export class AgentRunner {
|
|
|
1368
1364
|
try { jsonl = await readFile(input.transcriptPath, "utf8"); } catch { break; }
|
|
1369
1365
|
}
|
|
1370
1366
|
if (!transcriptLooksComplete(jsonl)) continue;
|
|
1371
|
-
//
|
|
1372
|
-
// collect text from entries after that mark to avoid double-emit (#435).
|
|
1367
|
+
// If a pre-flush already emitted entries 1..flushCount, resume after that mark to avoid double-emit (#435/#499).
|
|
1373
1368
|
const extract = this.options.providerConfig.chatCaptureMode === "full"
|
|
1374
1369
|
? (j: string) => (flushCount > 0 ? extractLastAssistantTurnAfterEntry(j, flushCount) : extractLastAssistantTurn(j))
|
|
1375
|
-
: extractFinalAssistantMessage;
|
|
1370
|
+
: (j: string) => (flushCount > 0 ? extractFinalAssistantMessageAfterEntry(j, flushCount) : extractFinalAssistantMessage(j));
|
|
1376
1371
|
body = extract(jsonl);
|
|
1377
1372
|
if (body) break;
|
|
1378
1373
|
}
|
|
@@ -1820,7 +1815,7 @@ export class AgentRunner {
|
|
|
1820
1815
|
automationId: this.options.automationId ?? null,
|
|
1821
1816
|
automationRunId: this.options.automationRunId ?? null,
|
|
1822
1817
|
workspace: this.options.workspace ?? null,
|
|
1823
|
-
lifecycle: this.options.lifecycle ?? "persistent", workspaceMode: this.options.workspace?.requestedMode ?? this.options.workspace?.mode ??
|
|
1818
|
+
lifecycle: this.options.lifecycle ?? "persistent", workspaceMode: this.options.workspace?.requestedMode ?? this.options.workspace?.mode ?? workspaceModeFromEnv() ?? null,
|
|
1824
1819
|
lifecycleAction: this.lifecycleAction ?? null,
|
|
1825
1820
|
profile: this.options.profile ?? null,
|
|
1826
1821
|
...(status === "error" ? { terminalStatus: "error" } : {}),
|
|
@@ -1938,7 +1933,7 @@ export class AgentRunner {
|
|
|
1938
1933
|
this.scratch = ensureSessionScratch({
|
|
1939
1934
|
agentId: this.agentId,
|
|
1940
1935
|
cwd: this.options.cwd,
|
|
1941
|
-
fallbackBaseDir:
|
|
1936
|
+
fallbackBaseDir: orchestratorBaseDirFromEnv(),
|
|
1942
1937
|
});
|
|
1943
1938
|
} catch (error) {
|
|
1944
1939
|
this.logRunnerDiagnostic(`session scratch setup failed: ${errMessage(error)}`);
|
|
@@ -1952,7 +1947,7 @@ export class AgentRunner {
|
|
|
1952
1947
|
keep.add(this.agentId);
|
|
1953
1948
|
const removed = sweepStaleSessions({
|
|
1954
1949
|
cwd: this.options.cwd,
|
|
1955
|
-
fallbackBaseDir:
|
|
1950
|
+
fallbackBaseDir: orchestratorBaseDirFromEnv(),
|
|
1956
1951
|
keepAgentIds: keep,
|
|
1957
1952
|
now: Date.now(),
|
|
1958
1953
|
});
|
|
@@ -2004,7 +1999,7 @@ export class AgentRunner {
|
|
|
2004
1999
|
// is already expired — the orchestrator's standing credential is the authority.
|
|
2005
2000
|
private canRemintViaOrchestrator(): boolean {
|
|
2006
2001
|
return Boolean(
|
|
2007
|
-
|
|
2002
|
+
orchestratorUrlFromEnv() &&
|
|
2008
2003
|
this.currentToken &&
|
|
2009
2004
|
(this.currentTokenProfileId === "provider-agent" || this.currentTokenProfileId === "provider-interactive"),
|
|
2010
2005
|
);
|
|
@@ -2079,7 +2074,7 @@ export class AgentRunner {
|
|
|
2079
2074
|
// (possibly expired) token; the orchestrator re-mints it via the relay using its
|
|
2080
2075
|
// standing credential. Returns true on success.
|
|
2081
2076
|
private async remintViaOrchestrator(): Promise<boolean> {
|
|
2082
|
-
const orchUrl =
|
|
2077
|
+
const orchUrl = orchestratorUrlFromEnv();
|
|
2083
2078
|
if (!orchUrl || !this.currentToken) return false;
|
|
2084
2079
|
try {
|
|
2085
2080
|
const res = await fetch(`${orchUrl.replace(/\/+$/, "")}/api/runtime-tokens/runner-renew`, {
|
|
@@ -2153,7 +2148,7 @@ export class AgentRunner {
|
|
|
2153
2148
|
}
|
|
2154
2149
|
|
|
2155
2150
|
private latestProbeMetrics() {
|
|
2156
|
-
const stateDir =
|
|
2151
|
+
const stateDir = contextStateDirFromEnv();
|
|
2157
2152
|
return readContextProbeState(this.agentId, stateDir);
|
|
2158
2153
|
}
|
|
2159
2154
|
|