agent-relay-runner 0.37.0 → 0.39.0
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.
|
|
3
|
+
"version": "0.39.0",
|
|
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.24"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
package/src/adapters/claude.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
|
-
import type
|
|
5
|
+
import { extractClaudeModelUnavailableMessage, type Message } from "agent-relay-sdk";
|
|
6
6
|
import { shellEscape as shellQuote } from "agent-relay-sdk/shell-utils";
|
|
7
7
|
import { tmuxCommand, tmuxHasSession } from "agent-relay-sdk/tmux-utils";
|
|
8
8
|
import { sanitizeFsName } from "agent-relay-sdk/fs-name";
|
|
@@ -21,6 +21,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
21
21
|
private statusCb: (status: ProviderStatusUpdate) => void = () => {};
|
|
22
22
|
private tmuxWatcher?: Timer;
|
|
23
23
|
private turnWatcher?: Timer;
|
|
24
|
+
private modelUnavailableReported = false;
|
|
24
25
|
|
|
25
26
|
onStatusChange(cb: (status: ProviderStatusUpdate) => void): void {
|
|
26
27
|
this.statusCb = cb;
|
|
@@ -285,6 +286,7 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
285
286
|
|
|
286
287
|
private async spawnHeadless(config: RunnerSpawnConfig, spawnArgs: SpawnArgs): Promise<ManagedProcess> {
|
|
287
288
|
const { sessionName, socketName, args: tmuxArgs } = this.buildTmuxArgs(config, spawnArgs);
|
|
289
|
+
this.modelUnavailableReported = false;
|
|
288
290
|
|
|
289
291
|
Bun.spawnSync(tmuxCommand(socketName, "kill-session", "-t", sessionName), {
|
|
290
292
|
stdin: "ignore", stdout: "ignore", stderr: "ignore",
|
|
@@ -332,6 +334,19 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
332
334
|
clearInterval(this.tmuxWatcher!);
|
|
333
335
|
this.tmuxWatcher = undefined;
|
|
334
336
|
this.statusCb("offline");
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (this.modelUnavailableReported) return;
|
|
340
|
+
let pane = "";
|
|
341
|
+
try {
|
|
342
|
+
pane = captureTmuxPane(sessionName, socketName);
|
|
343
|
+
} catch {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const status = claudeModelUnavailableStatus(pane, sessionName);
|
|
347
|
+
if (status) {
|
|
348
|
+
this.modelUnavailableReported = true;
|
|
349
|
+
this.statusCb(status);
|
|
335
350
|
}
|
|
336
351
|
}, 2000);
|
|
337
352
|
}
|
|
@@ -505,6 +520,43 @@ export function claudePaneIsBusy(text: string): boolean {
|
|
|
505
520
|
return CLAUDE_BUSY_SPINNER_RE.test(text) || text.includes("esc to interrupt");
|
|
506
521
|
}
|
|
507
522
|
|
|
523
|
+
export function claudeModelUnavailableStatus(text: string, sessionName?: string): ProviderStatusUpdate | null {
|
|
524
|
+
const message = extractClaudeModelUnavailableMessage(text);
|
|
525
|
+
if (!message) return null;
|
|
526
|
+
return {
|
|
527
|
+
status: "error",
|
|
528
|
+
clear: ["provider-turn", "subagent"],
|
|
529
|
+
providerState: {
|
|
530
|
+
state: "failed",
|
|
531
|
+
reason: "model-unavailable",
|
|
532
|
+
message,
|
|
533
|
+
source: "claude-pane",
|
|
534
|
+
terminal: true,
|
|
535
|
+
...(sessionName ? { sessionName } : {}),
|
|
536
|
+
recommendedAction: "Choose a different Claude model before restarting this agent.",
|
|
537
|
+
},
|
|
538
|
+
metadata: {
|
|
539
|
+
terminalFailureReason: "model-unavailable",
|
|
540
|
+
terminalFailureMessage: message,
|
|
541
|
+
},
|
|
542
|
+
timeline: {
|
|
543
|
+
status: "provider.restart_decision",
|
|
544
|
+
id: `provider-model-unavailable-${Date.now()}`,
|
|
545
|
+
timestamp: Date.now(),
|
|
546
|
+
title: "Provider restart skipped",
|
|
547
|
+
body: message,
|
|
548
|
+
icon: "ti-player-stop",
|
|
549
|
+
metadata: {
|
|
550
|
+
eventType: "provider.restart_decision",
|
|
551
|
+
decision: "stop-surface",
|
|
552
|
+
reason: "model-unavailable",
|
|
553
|
+
modelUnavailable: true,
|
|
554
|
+
modelUnavailableMessage: message,
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
508
560
|
async function waitForClaudeInputReady(sessionName: string, timeoutMs = CLAUDE_TMUX_READY_TIMEOUT_MS, socketName?: string): Promise<void> {
|
|
509
561
|
const deadline = Date.now() + timeoutMs;
|
|
510
562
|
while (Date.now() < deadline) {
|
package/src/runner.ts
CHANGED
|
@@ -250,6 +250,7 @@ export class AgentRunner {
|
|
|
250
250
|
// Session-mirror: a synthesized id grouping a turn's reasoning/tool steps and
|
|
251
251
|
// its final response. Set when a provider-turn starts, cleared when it ends.
|
|
252
252
|
private currentTurnId?: string;
|
|
253
|
+
private currentTurnStartedAt?: number;
|
|
253
254
|
// Prompt-echo dedup: a short, time-bounded queue of prompts the runner itself
|
|
254
255
|
// injected (chat box or initial prompt) that are still awaiting their matching
|
|
255
256
|
// UserPromptSubmit echo. A single slot dropped earlier entries when several prompts
|
|
@@ -268,6 +269,7 @@ export class AgentRunner {
|
|
|
268
269
|
// Tracks whether the provider is in a legitimate blocked/approval state, so the
|
|
269
270
|
// busy reconciler doesn't mistake a permission prompt for a stuck-busy turn.
|
|
270
271
|
private providerBlocked = false;
|
|
272
|
+
private terminalFailure?: { reason: string; message: string; providerState?: Record<string, unknown> };
|
|
271
273
|
// Reasoning tailer (item 5): streams the in-flight turn's reasoning/tool steps
|
|
272
274
|
// from the Claude transcript into chat as discreet session events.
|
|
273
275
|
private reasoningTail?: { timer: ReturnType<typeof setInterval>; seen: Set<string> };
|
|
@@ -1091,15 +1093,36 @@ export class AgentRunner {
|
|
|
1091
1093
|
} else if (status === "idle") {
|
|
1092
1094
|
this.providerBlocked = false;
|
|
1093
1095
|
}
|
|
1096
|
+
if (typeof update !== "string" && status === "error") {
|
|
1097
|
+
const terminalReason = typeof update.metadata?.terminalFailureReason === "string"
|
|
1098
|
+
? update.metadata.terminalFailureReason
|
|
1099
|
+
: typeof update.providerState?.reason === "string"
|
|
1100
|
+
? update.providerState.reason
|
|
1101
|
+
: "provider-error";
|
|
1102
|
+
const terminalMessage = typeof update.metadata?.terminalFailureMessage === "string"
|
|
1103
|
+
? update.metadata.terminalFailureMessage
|
|
1104
|
+
: typeof update.providerState?.message === "string"
|
|
1105
|
+
? update.providerState.message
|
|
1106
|
+
: "Provider reported an unrecoverable error.";
|
|
1107
|
+
this.terminalFailure = {
|
|
1108
|
+
reason: terminalReason,
|
|
1109
|
+
message: terminalMessage,
|
|
1110
|
+
...(update.providerState ? { providerState: update.providerState } : {}),
|
|
1111
|
+
};
|
|
1112
|
+
} else if (status === "idle" || status === "busy") {
|
|
1113
|
+
this.terminalFailure = undefined;
|
|
1114
|
+
}
|
|
1094
1115
|
if (status === "busy" && reason === "provider-turn") {
|
|
1095
1116
|
if (!this.currentTurnId) {
|
|
1096
1117
|
this.currentTurnId = typeof update !== "string" && update.id ? update.id : crypto.randomUUID();
|
|
1118
|
+
this.currentTurnStartedAt = Date.now();
|
|
1097
1119
|
this.sessionLog(`turn started (turn ${this.currentTurnId})`);
|
|
1098
1120
|
}
|
|
1099
1121
|
this.armBusyReconciler();
|
|
1100
1122
|
} else if (status === "idle" && reason === "provider-turn") {
|
|
1101
1123
|
if (this.currentTurnId) this.sessionLog(`turn ended via provider idle (turn ${this.currentTurnId})`);
|
|
1102
1124
|
this.currentTurnId = undefined;
|
|
1125
|
+
this.currentTurnStartedAt = undefined;
|
|
1103
1126
|
this.disarmBusyReconciler();
|
|
1104
1127
|
this.stopReasoningTail();
|
|
1105
1128
|
}
|
|
@@ -1324,7 +1347,10 @@ export class AgentRunner {
|
|
|
1324
1347
|
// deliveries) so those aren't double-posted.
|
|
1325
1348
|
private async handleUserPrompt(input: { prompt: string; transcriptPath?: string }): Promise<void> {
|
|
1326
1349
|
if (input.transcriptPath) this.lastTranscriptPath = input.transcriptPath;
|
|
1327
|
-
if (!this.currentTurnId)
|
|
1350
|
+
if (!this.currentTurnId) {
|
|
1351
|
+
this.currentTurnId = crypto.randomUUID();
|
|
1352
|
+
this.currentTurnStartedAt = Date.now();
|
|
1353
|
+
}
|
|
1328
1354
|
const text = input.prompt.trim();
|
|
1329
1355
|
if (text && !this.isRunnerInjectedPrompt(text)) {
|
|
1330
1356
|
this.sessionLog(`prompt echoed from terminal (${text.length} chars)`);
|
|
@@ -1482,7 +1508,7 @@ export class AgentRunner {
|
|
|
1482
1508
|
if (pendingPrompt) {
|
|
1483
1509
|
replyToMessageId = pendingPrompt;
|
|
1484
1510
|
this.pendingPromptMessageId = undefined;
|
|
1485
|
-
} else if (this.obligationCache.get().some((o) => o.from === "user")) {
|
|
1511
|
+
} else if (this.obligationCache.get().some((o) => o.from === "user" && this.obligationPredatesCurrentTurn(o))) {
|
|
1486
1512
|
// The agent will answer the relay obligation itself — don't double-post (#196).
|
|
1487
1513
|
return;
|
|
1488
1514
|
}
|
|
@@ -1534,6 +1560,10 @@ export class AgentRunner {
|
|
|
1534
1560
|
return false;
|
|
1535
1561
|
}
|
|
1536
1562
|
|
|
1563
|
+
private obligationPredatesCurrentTurn(obligation: { createdAt: number }): boolean {
|
|
1564
|
+
return this.currentTurnStartedAt === undefined || obligation.createdAt <= this.currentTurnStartedAt;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1537
1567
|
// --- Busy-state reconciler (item 2) -------------------------------------------------
|
|
1538
1568
|
// A safety net for turns that end out of band (interrupted from the web terminal,
|
|
1539
1569
|
// a hook that never fired) where the runner would otherwise stay stuck "busy".
|
|
@@ -1668,11 +1698,13 @@ export class AgentRunner {
|
|
|
1668
1698
|
}
|
|
1669
1699
|
|
|
1670
1700
|
private publishStatus(): void {
|
|
1701
|
+
this.claims.expire();
|
|
1671
1702
|
const status = this.claims.currentStatus();
|
|
1672
1703
|
const agentStatus = runnerAgentStatus(status);
|
|
1673
1704
|
const activeWork = this.claims.activeWork();
|
|
1674
1705
|
const activeSubagents = activeWork.filter((item) => item.kind === "subagent");
|
|
1675
|
-
const
|
|
1706
|
+
const terminalFailure = this.terminalFailure;
|
|
1707
|
+
const providerState = terminalFailure?.providerState ?? providerStateFromActiveWork(activeWork);
|
|
1676
1708
|
this.bus.setSemanticStatus(status === "offline" || status === "error" ? "idle" : status);
|
|
1677
1709
|
const timelineEvent = this.pendingTimelineEvent;
|
|
1678
1710
|
this.pendingTimelineEvent = undefined;
|
|
@@ -1693,6 +1725,11 @@ export class AgentRunner {
|
|
|
1693
1725
|
lifecycleAction: this.lifecycleAction ?? null,
|
|
1694
1726
|
profile: this.options.profile ?? null,
|
|
1695
1727
|
...(status === "error" ? { terminalStatus: "error" } : {}),
|
|
1728
|
+
...(terminalFailure ? {
|
|
1729
|
+
lastError: terminalFailure.message,
|
|
1730
|
+
terminalFailureReason: terminalFailure.reason,
|
|
1731
|
+
terminalFailureMessage: terminalFailure.message,
|
|
1732
|
+
} : {}),
|
|
1696
1733
|
busyReasons: this.claims.reasons(),
|
|
1697
1734
|
activeWork,
|
|
1698
1735
|
activeSubagents,
|