agent-relay-runner 0.12.2 → 0.12.4
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
package/src/adapters/claude.ts
CHANGED
|
@@ -355,6 +355,13 @@ export function sessionStatusLineSettingsArgs(...argLists: string[][]): string[]
|
|
|
355
355
|
command: "agent-relay context-probe --wrap",
|
|
356
356
|
refreshInterval: 30,
|
|
357
357
|
},
|
|
358
|
+
// Force readable thinking text for managed sessions so the session-mirror can
|
|
359
|
+
// surface reasoning in the dashboard. With showThinkingSummaries:false the API
|
|
360
|
+
// redacts thinking to a signature-only stub (empty text), leaving the transcript
|
|
361
|
+
// tail nothing to mirror. --settings merges per-key, so this overrides only this
|
|
362
|
+
// key for managed sessions — a host rig default of false still governs the
|
|
363
|
+
// operator's own interactive TUI sessions.
|
|
364
|
+
showThinkingSummaries: true,
|
|
358
365
|
})];
|
|
359
366
|
}
|
|
360
367
|
|
|
@@ -448,12 +455,19 @@ export function claudePaneLooksReady(text: string): boolean {
|
|
|
448
455
|
|| text.includes("Claude Code");
|
|
449
456
|
}
|
|
450
457
|
|
|
458
|
+
// The working-spinner footer carries a live elapsed-time counter while a turn is in
|
|
459
|
+
// flight, e.g. "✶ Perambulating… (2m 17s · ↓ 8.7k tokens)" — gerund, "… (", then
|
|
460
|
+
// "[Nh ][Nm ]Ns". Anchored on the gerund ellipsis so it can't match the "… +N lines
|
|
461
|
+
// (ctrl+o to expand)" truncation marker, the idle input box, or the persistent
|
|
462
|
+
// "/btw … without interrupting Claude's current work" queue hint.
|
|
463
|
+
const CLAUDE_BUSY_SPINNER_RE = /…\s*\((?:\d+h\s+)?(?:\d+m\s+)?\d+s\b/;
|
|
464
|
+
|
|
451
465
|
export function claudePaneIsBusy(text: string): boolean {
|
|
452
|
-
// Claude
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
456
|
-
return text.includes("esc to interrupt");
|
|
466
|
+
// Claude Code <2.1.x rendered "(esc to interrupt)" in the spinner footer; 2.1.x
|
|
467
|
+
// dropped that hint but kept the "(<elapsed>" counter, which is the stable busy
|
|
468
|
+
// signal across versions. Match either so the busy probe (and the reconciler
|
|
469
|
+
// backstop that depends on it) keep working as the footer wording changes.
|
|
470
|
+
return CLAUDE_BUSY_SPINNER_RE.test(text) || text.includes("esc to interrupt");
|
|
457
471
|
}
|
|
458
472
|
|
|
459
473
|
async function waitForClaudeInputReady(sessionName: string, timeoutMs = CLAUDE_TMUX_READY_TIMEOUT_MS, socketName?: string): Promise<void> {
|
package/src/adapters/codex.ts
CHANGED
|
@@ -453,6 +453,18 @@ export function codexToolSummary(type: string | undefined, item: Record<string,
|
|
|
453
453
|
if (type === "webSearch") {
|
|
454
454
|
return { label: "Search", body: clip(oneLine(item.query) || "web search") };
|
|
455
455
|
}
|
|
456
|
+
if (type === "plan") {
|
|
457
|
+
return { label: "Plan", body: clip(oneLine(item.text) || "updated plan") };
|
|
458
|
+
}
|
|
459
|
+
if (type === "collabAgentToolCall") {
|
|
460
|
+
const tool = stringValue(item.tool) ?? "collab";
|
|
461
|
+
const prompt = oneLine(item.prompt);
|
|
462
|
+
const targets = Array.isArray(item.receiverThreadIds)
|
|
463
|
+
? item.receiverThreadIds.filter((t): t is string => typeof t === "string").length
|
|
464
|
+
: 0;
|
|
465
|
+
const detail = prompt || (targets ? `${targets} agent${targets === 1 ? "" : "s"}` : tool);
|
|
466
|
+
return { label: `Collab/${tool}`, body: clip(detail) };
|
|
467
|
+
}
|
|
456
468
|
return null;
|
|
457
469
|
}
|
|
458
470
|
|
package/src/runner.ts
CHANGED
|
@@ -63,6 +63,9 @@ const CLAIM_RENEW_INTERVAL_MS = 5 * 60 * 1000;
|
|
|
63
63
|
const HTTP_LIVENESS_INTERVAL_MS = 20_000;
|
|
64
64
|
const HTTP_LIVENESS_LOG_INTERVAL_MS = 5 * 60 * 1000;
|
|
65
65
|
const TOKEN_RENEW_RETRY_MS = 60_000;
|
|
66
|
+
// Debounce reactive token recovery so a burst of 401-ing calls in the same window
|
|
67
|
+
// triggers a single re-mint attempt, not one per failing request.
|
|
68
|
+
const REACTIVE_TOKEN_RECOVERY_DEBOUNCE_MS = 10_000;
|
|
66
69
|
const UNEXPECTED_EXIT_WINDOW_MS = 2 * 60 * 1000;
|
|
67
70
|
const RAPID_EXIT_MS = 30 * 1000;
|
|
68
71
|
const MAX_RAPID_UNEXPECTED_EXITS = 3;
|
|
@@ -128,6 +131,7 @@ export class AgentRunner {
|
|
|
128
131
|
private tokenRenewTimer?: Timer;
|
|
129
132
|
private tokenRenewInFlight = false;
|
|
130
133
|
private tokenRenewLastLog?: { key: string; at: number };
|
|
134
|
+
private reactiveTokenRecoveryAt?: number;
|
|
131
135
|
private processStartedAt = 0;
|
|
132
136
|
private providerSessionId = crypto.randomUUID();
|
|
133
137
|
private lifecycleAction?: "shutting-down" | "killing" | "restarting";
|
|
@@ -972,6 +976,7 @@ export class AgentRunner {
|
|
|
972
976
|
});
|
|
973
977
|
} catch (error) {
|
|
974
978
|
this.logRunnerDiagnostic(`session ${input.session.type} capture failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
979
|
+
if (isHttpAuthError(error)) this.recoverRuntimeTokenAfterAuthFailure("session-capture");
|
|
975
980
|
}
|
|
976
981
|
}
|
|
977
982
|
|
|
@@ -1139,15 +1144,29 @@ export class AgentRunner {
|
|
|
1139
1144
|
// and drop the rest of the turn. A seen-set is idempotent under any reshuffle.
|
|
1140
1145
|
const seen = new Set<string>();
|
|
1141
1146
|
const turnIdAtStart = this.currentTurnId;
|
|
1147
|
+
// On the first poll the new prompt usually hasn't landed in the transcript yet,
|
|
1148
|
+
// so extractLatestTurnSteps still returns the PRIOR (completed) turn. Seed those
|
|
1149
|
+
// signatures as already-seen so we don't replay last turn's reasoning/tools as
|
|
1150
|
+
// this turn's activity. Once our prompt lands the window resets at the new user
|
|
1151
|
+
// boundary and genuinely-new steps emit normally. Only seed when the transcript
|
|
1152
|
+
// is complete (last entry is an end_turn assistant) — otherwise we're already
|
|
1153
|
+
// inside the new turn and those steps are legitimately ours.
|
|
1154
|
+
let seeded = false;
|
|
1142
1155
|
const poll = async (): Promise<void> => {
|
|
1143
1156
|
let jsonl: string;
|
|
1144
1157
|
try { jsonl = await readFile(transcriptPath, "utf8"); } catch { return; }
|
|
1145
1158
|
let steps: ReturnType<typeof extractLatestTurnSteps>;
|
|
1146
1159
|
try { steps = extractLatestTurnSteps(jsonl); } catch { return; }
|
|
1160
|
+
if (!seeded) {
|
|
1161
|
+
seeded = true;
|
|
1162
|
+
if (transcriptLooksComplete(jsonl)) {
|
|
1163
|
+
for (const s of steps) seen.add(JSON.stringify([s.type, s.label ?? "", s.text]));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1147
1166
|
const turnId = this.currentTurnId ?? turnIdAtStart;
|
|
1148
1167
|
let emitted = 0;
|
|
1149
1168
|
for (const step of steps) {
|
|
1150
|
-
const sig =
|
|
1169
|
+
const sig = JSON.stringify([step.type, step.label ?? "", step.text]);
|
|
1151
1170
|
if (seen.has(sig)) continue;
|
|
1152
1171
|
seen.add(sig);
|
|
1153
1172
|
emitted += 1;
|
|
@@ -1245,6 +1264,25 @@ export class AgentRunner {
|
|
|
1245
1264
|
this.httpLivenessAuthFailed = true;
|
|
1246
1265
|
if (this.httpLivenessTimer) clearInterval(this.httpLivenessTimer);
|
|
1247
1266
|
this.httpLivenessTimer = undefined;
|
|
1267
|
+
// A 401/403 here is the only timely signal that the token died — stopping the
|
|
1268
|
+
// liveness timer means there is no second chance, so recover from THIS failure.
|
|
1269
|
+
this.recoverRuntimeTokenAfterAuthFailure("http-liveness");
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// A definitive relay auth failure (401/403) means the runtime token is dead right
|
|
1273
|
+
// now — expired, or (the common case) revoked when the relay marked this agent
|
|
1274
|
+
// stale across its own restart/reconnect. The proactive renew timer is keyed to
|
|
1275
|
+
// TTL and structurally cannot catch a revocation, so the auth failure itself must
|
|
1276
|
+
// drive recovery. renewRuntimeToken() prefers an orchestrator re-mint, which heals
|
|
1277
|
+
// even a revoked token. Debounced so a burst of failing calls re-mints once.
|
|
1278
|
+
private recoverRuntimeTokenAfterAuthFailure(source: string): void {
|
|
1279
|
+
if (this.stopped || this.tokenRenewInFlight) return;
|
|
1280
|
+
if (!this.isRuntimeTokenRenewable() && !this.canRemintViaOrchestrator()) return;
|
|
1281
|
+
const now = Date.now();
|
|
1282
|
+
if (this.reactiveTokenRecoveryAt && now - this.reactiveTokenRecoveryAt < REACTIVE_TOKEN_RECOVERY_DEBOUNCE_MS) return;
|
|
1283
|
+
this.reactiveTokenRecoveryAt = now;
|
|
1284
|
+
this.logRunnerDiagnostic(`[runner] relay auth failure on ${source}; recovering runtime token`);
|
|
1285
|
+
void this.renewRuntimeToken();
|
|
1248
1286
|
}
|
|
1249
1287
|
|
|
1250
1288
|
private logHttpLivenessFailure(error: unknown, authFailed: boolean): void {
|
|
@@ -1418,6 +1456,11 @@ export class AgentRunner {
|
|
|
1418
1456
|
this.http.setToken(token);
|
|
1419
1457
|
this.bus.setToken(token);
|
|
1420
1458
|
this.httpLivenessAuthFailed = false;
|
|
1459
|
+
this.reactiveTokenRecoveryAt = undefined;
|
|
1460
|
+
// An earlier auth failure may have stopped the liveness loop; restart it so the
|
|
1461
|
+
// agent reports live again on the fresh token. startHttpLiveness clears any
|
|
1462
|
+
// existing timer first, so this is safe on the normal (proactive) renew path too.
|
|
1463
|
+
this.startHttpLiveness();
|
|
1421
1464
|
this.pendingTimelineEvent = { status, id: record.jti, timestamp: Date.now() };
|
|
1422
1465
|
this.bus.reconnectTransport(status === "runtime-token-reminted" ? "runtime token re-minted" : "runtime token renewed");
|
|
1423
1466
|
this.publishStatus();
|