agent-relay-runner 0.127.9 → 0.127.11
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/plugins/claude/monitors/relay-monitor.provisioned.mjs +2 -2
- package/src/adapters/claude-status-detectors.ts +15 -2
- package/src/adapters/claude.ts +7 -8
- package/src/control-server.ts +56 -26
- package/src/profile-home.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-relay-runner",
|
|
3
|
-
"version": "0.127.
|
|
3
|
+
"version": "0.127.11",
|
|
4
4
|
"description": "Unified provider lifecycle runner for Agent Relay",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"agent-relay-providers": "0.104.4",
|
|
26
|
-
"agent-relay-sdk": "0.2.
|
|
26
|
+
"agent-relay-sdk": "0.2.124",
|
|
27
27
|
"callmux": "0.23.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
@@ -47,8 +47,8 @@ var RUNNER_PROMPT_TEMPLATE_DEFAULTS = [
|
|
|
47
47
|
category: "runner",
|
|
48
48
|
title: "Codex Relay Context",
|
|
49
49
|
description: "Relay context note injected into Codex sessions.",
|
|
50
|
-
template: "[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id \u2014 relay fills it from your token; use relay_whoami only
|
|
51
|
-
defaultTemplate: "[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id \u2014 relay fills it from your token; use relay_whoami only
|
|
50
|
+
template: "[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id \u2014 relay fills it from your token. Your identity is injected at startup as the `## Agent Relay Identity` block; use relay_whoami only for the full verbose identity dump. relay_compact_and_resume is for clean-seam self-resume after a context advisory: pass workingState and optional ruledOut; Relay owns the objective envelope. relay_recall searches your own archived pre-compaction segments by keyword when a discarded detail is needed. relay_spawn_targets / relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present, call relay_spawn_targets FIRST for the live host/provider/model matrix + your quota, then stand up long-living child agents and shut down your own \u2014 find them later with relay_find_agents spawnedBy:me. For replies, prefer relay_reply or relay_send_message; CLI fallback: agent-relay /reply <messageId> \"<your reply>\". If a delivered message says it was truncated, fetch the full body with: agent-relay get-message <messageId>. For command details, run: agent-relay /guide",
|
|
51
|
+
defaultTemplate: "[agent-relay] You are connected to Agent Relay, a real-time message bus between agents and users. When you receive a relay message: read it, do what it asks, and reply through the relay when a text response is needed. Use agent-relay /react <messageId> <emoji> for lightweight acknowledgement or approval. If Relay MCP tools are available, prefer relay_reply, relay_get_message, relay_get_thread, relay_send_message, relay_upload_artifact, relay_attach_artifact, relay_agent_status, relay_find_agents, relay_compact_and_resume, relay_recall, relay_spawn_agent, and relay_shutdown_agent. You never need to know or pass your own agent id \u2014 relay fills it from your token. Your identity is injected at startup as the `## Agent Relay Identity` block; use relay_whoami only for the full verbose identity dump. relay_compact_and_resume is for clean-seam self-resume after a context advisory: pass workingState and optional ruledOut; Relay owns the objective envelope. relay_recall searches your own archived pre-compaction segments by keyword when a discarded detail is needed. relay_spawn_targets / relay_spawn_agent / relay_shutdown_agent only appear if your profile grants spawning (a live-children quota); when present, call relay_spawn_targets FIRST for the live host/provider/model matrix + your quota, then stand up long-living child agents and shut down your own \u2014 find them later with relay_find_agents spawnedBy:me. For replies, prefer relay_reply or relay_send_message; CLI fallback: agent-relay /reply <messageId> \"<your reply>\". If a delivered message says it was truncated, fetch the full body with: agent-relay get-message <messageId>. For command details, run: agent-relay /guide",
|
|
52
52
|
variables: []
|
|
53
53
|
},
|
|
54
54
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { extractClaudeModelUnavailableMessage } from "agent-relay-sdk";
|
|
2
2
|
import type { ProviderStatusUpdate } from "../adapter";
|
|
3
|
+
import type { ClaudeProbeActivity } from "./claude-session-probe";
|
|
3
4
|
import { buildRateLimitProviderState, parseClaudeRateLimitPane } from "../rate-limit";
|
|
4
5
|
|
|
5
6
|
// LEGACY PANE-SCRAPE FALLBACK (#766/#768) — busy/idle is now driven primarily by
|
|
@@ -19,8 +20,8 @@ import { buildRateLimitProviderState, parseClaudeRateLimitPane } from "../rate-l
|
|
|
19
20
|
// (the cross-version anchor; the "esc to interrupt" hint was already dropped in 2.1.x).
|
|
20
21
|
// FALSE POSITIVES: agent output that literally QUOTES any of these strings (e.g. a
|
|
21
22
|
// transcript discussing "esc to interrupt", or this very comment shown in a pane)
|
|
22
|
-
// reads as ready/busy.
|
|
23
|
-
//
|
|
23
|
+
// reads as ready/busy. The activity reconciler gates on the LAST N lines only (the
|
|
24
|
+
// live footer); direct raw predicates are still used by readiness/wait gates.
|
|
24
25
|
// History: 18067b5 (busy counter), and the readiness footer-vs-banner fix below.
|
|
25
26
|
export function claudePaneLooksReady(text: string): boolean {
|
|
26
27
|
// Claude's startup banner ("Claude Code" / "Welcome back") scrolls off the pane once the
|
|
@@ -43,6 +44,7 @@ export function claudePaneLooksReady(text: string): boolean {
|
|
|
43
44
|
// (ctrl+o to expand)" truncation marker, the idle input box, or the persistent
|
|
44
45
|
// "/btw … without interrupting Claude's current work" queue hint.
|
|
45
46
|
const CLAUDE_BUSY_SPINNER_RE = /…\s*\((?:\d+h\s+)?(?:\d+m\s+)?\d+s\b/;
|
|
47
|
+
const CLAUDE_ACTIVITY_FOOTER_LINES = 10;
|
|
46
48
|
|
|
47
49
|
export function claudePaneIsBusy(text: string): boolean {
|
|
48
50
|
// Claude Code <2.1.x rendered "(esc to interrupt)" in the spinner footer; 2.1.x
|
|
@@ -52,6 +54,17 @@ export function claudePaneIsBusy(text: string): boolean {
|
|
|
52
54
|
return CLAUDE_BUSY_SPINNER_RE.test(text) || text.includes("esc to interrupt");
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function claudeActivityFooter(text: string): string {
|
|
58
|
+
return text.split("\n").slice(-CLAUDE_ACTIVITY_FOOTER_LINES).join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function claudeActivityFromProbeAndPane(probe: ClaudeProbeActivity | undefined, pane: string): ClaudeProbeActivity {
|
|
62
|
+
const footer = claudeActivityFooter(pane);
|
|
63
|
+
if (probe === "busy" || claudePaneIsBusy(footer)) return "busy";
|
|
64
|
+
if (claudePaneLooksReady(footer)) return "idle";
|
|
65
|
+
return probe === "idle" ? "unknown" : probe ?? "unknown";
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
// #286: detect the subscription usage-limit modal in the pane and turn it into the
|
|
56
69
|
// provider-neutral rate-limit hold (idle + blocked) that the relay's resume sweep lifts
|
|
57
70
|
// at reset. Mirrors claudeModelUnavailableStatus, but non-terminal (idle, not error).
|
package/src/adapters/claude.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { assembleLaunch, materializeLaunchAssembly, sessionStatusLineSettingsArg
|
|
|
12
12
|
import { claudeProviderMessageText } from "./claude-delivery";
|
|
13
13
|
import { claudeProbeActivity, readManagedClaudeStatus, resolveClaudePid, type ClaudeProbeActivity } from "./claude-session-probe";
|
|
14
14
|
import { evaluateClaudePromptGatePane, initialClaudePromptGatePaneState, type ClaudePromptGatePaneState } from "./claude-prompt-gates";
|
|
15
|
-
import { claudePaneLooksReady, claudePaneIsBusy, claudeRateLimitStatus, claudeConnectionRetryStatus, claudeModelUnavailableStatus } from "./claude-status-detectors";
|
|
15
|
+
import { claudeActivityFromProbeAndPane, claudePaneLooksReady, claudePaneIsBusy, claudeRateLimitStatus, claudeConnectionRetryStatus, claudeModelUnavailableStatus } from "./claude-status-detectors";
|
|
16
16
|
import { ClaudeSessionCapture, collectClaudeTranscriptArchiveSegment, collectClaudeTranscriptSessionEvents } from "./claude-session-capture";
|
|
17
17
|
import { CLAUDE_TMUX_READY_TIMEOUT_MS, captureTmuxPane, submitTextToTmux, tmuxEnvKeys, tmuxSessionName, tmuxSocketName, waitForClaudeInputReady, writeLauncherScript } from "./claude-tmux";
|
|
18
18
|
import { claudePermissionPromptHandler } from "./claude-permission";
|
|
@@ -138,12 +138,11 @@ export class ClaudeAdapter implements ProviderAdapter {
|
|
|
138
138
|
tmuxSocket: socket,
|
|
139
139
|
configHomeEnv: (process.meta?.env as Record<string, string | undefined> | undefined)?.CLAUDE_CONFIG_DIR,
|
|
140
140
|
});
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return "unknown";
|
|
141
|
+
const probe = managed?.reading.available ? claudeProbeActivity(managed.reading.status) : undefined;
|
|
142
|
+
if (probe === "busy") return "busy";
|
|
143
|
+
let pane: string | undefined;
|
|
144
|
+
try { pane = captureTmuxPane(session, socket); } catch { return probe ?? "unknown"; }
|
|
145
|
+
return claudeActivityFromProbeAndPane(probe, pane);
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
// #769 P1-A: an independent pane-busy read, consumed ONLY as a veto on the busy
|
|
@@ -532,7 +531,7 @@ export { tmuxHasSession };
|
|
|
532
531
|
|
|
533
532
|
// Re-export pane-status detectors (moved to ./claude-status-detectors) so existing
|
|
534
533
|
// import paths (including claude.test.ts) continue to resolve via this module.
|
|
535
|
-
export { claudePaneLooksReady, claudePaneIsBusy, claudeRateLimitStatus, claudeConnectionRetryStatus, claudeModelUnavailableStatus } from "./claude-status-detectors";
|
|
534
|
+
export { claudeActivityFromProbeAndPane, claudePaneLooksReady, claudePaneIsBusy, claudeRateLimitStatus, claudeConnectionRetryStatus, claudeModelUnavailableStatus } from "./claude-status-detectors";
|
|
536
535
|
|
|
537
536
|
// Shared shell-quoting; re-exported so `./claude` consumers + tests resolve it.
|
|
538
537
|
export { shellQuote };
|
package/src/control-server.ts
CHANGED
|
@@ -323,34 +323,19 @@ async function handlePermissionRequest(
|
|
|
323
323
|
const toolInput = isRecord(body.tool_input) ? body.tool_input : undefined;
|
|
324
324
|
const approvalId = crypto.randomUUID();
|
|
325
325
|
|
|
326
|
-
// #1173: a PreToolUse "questions" prompt (the provider's AskUserQuestion) used to block this hook
|
|
327
|
-
// response for up to ~15 minutes waiting on the human — which meant the turn's reasoning that
|
|
328
|
-
// led to the question stayed buffered in the provider's process (never flushed to the transcript
|
|
329
|
-
// JSONL) for exactly as long as the human took to answer, so the dashboard could only ever
|
|
330
|
-
// show the question AFTER the reasoning that motivated it. Denying the tool call immediately
|
|
331
|
-
// is the ordinary, already-supported "rejected tool call" hook contract: the provider persists the
|
|
332
|
-
// buffered turn + a rejected tool_result right away, on its own, same as any other denied tool
|
|
333
|
-
// — so the settle-poll harvest below (previously doomed to time out for this one case) now
|
|
334
|
-
// finds the freshly-flushed anchor within moments. The human's eventual answer is delivered
|
|
335
|
-
// afterward as a normal injected prompt (see the `.then` below), not as this hook's response.
|
|
336
326
|
const probeView = handler.buildView(approvalId, body, true);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
327
|
+
const answerInput = permissionRequestQuestionAnswers(body);
|
|
328
|
+
if (body.hook_event_name === "PermissionRequest" && probeView.kind === "questions" && answerInput) {
|
|
329
|
+
return Response.json(handler.buildHookResponse({ approvalId, decision: "answer", answers: answerInput }, body));
|
|
330
|
+
}
|
|
331
|
+
if (
|
|
332
|
+
(body.hook_event_name === "PreToolUse" || body.hook_event_name === "PermissionRequest")
|
|
333
|
+
&& probeView.kind === "questions"
|
|
334
|
+
&& handler.buildAnswerInjectionPrompt
|
|
335
|
+
) {
|
|
336
|
+
return holdQuestionWithImmediateDeny({
|
|
343
337
|
options, pendingPermissionRequests, handler, approvalId, body, occurredAt, transcriptPath, toolName, toolInput,
|
|
344
|
-
|
|
345
|
-
recommendedAction: "Answer the question in Agent Relay.",
|
|
346
|
-
suppressResolvedStatus: true,
|
|
347
|
-
}).then(({ decision, view }) => {
|
|
348
|
-
const text = decision.decision === "answer" && decision.answers && Object.keys(decision.answers).length
|
|
349
|
-
? handler.buildAnswerInjectionPrompt!(view, decision, body)
|
|
350
|
-
: pendingQuestionDismissalPrompt(decision);
|
|
351
|
-
return options.onAnswerInject?.({ approvalId, text });
|
|
352
|
-
}).catch(() => {});
|
|
353
|
-
return response;
|
|
338
|
+
});
|
|
354
339
|
}
|
|
355
340
|
|
|
356
341
|
const { decision } = await awaitPermissionDecision({
|
|
@@ -361,6 +346,51 @@ async function handlePermissionRequest(
|
|
|
361
346
|
return Response.json(handler.buildHookResponse(decision, body));
|
|
362
347
|
}
|
|
363
348
|
|
|
349
|
+
function permissionRequestQuestionAnswers(body: Record<string, unknown>): Record<string, string> | undefined {
|
|
350
|
+
if (body.hook_event_name !== "PermissionRequest") return undefined;
|
|
351
|
+
const toolInput = isRecord(body.tool_input) ? body.tool_input : {};
|
|
352
|
+
if (!isRecord(toolInput.answers)) return undefined;
|
|
353
|
+
const answers = Object.fromEntries(
|
|
354
|
+
Object.entries(toolInput.answers)
|
|
355
|
+
.filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[1].trim().length > 0),
|
|
356
|
+
);
|
|
357
|
+
return Object.keys(answers).length ? answers : undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function holdQuestionWithImmediateDeny(params: {
|
|
361
|
+
options: ControlServerOptions;
|
|
362
|
+
pendingPermissionRequests: Map<string, PendingPermissionWaiter>;
|
|
363
|
+
handler: ProviderPermissionPromptHandler;
|
|
364
|
+
approvalId: string;
|
|
365
|
+
body: Record<string, unknown>;
|
|
366
|
+
occurredAt: number;
|
|
367
|
+
transcriptPath: string;
|
|
368
|
+
toolName?: string;
|
|
369
|
+
toolInput?: unknown;
|
|
370
|
+
}): Response {
|
|
371
|
+
const { options, pendingPermissionRequests, handler, approvalId, body, occurredAt, transcriptPath, toolName, toolInput } = params;
|
|
372
|
+
// #1173/#1194: a fresh "questions" prompt (the provider's AskUserQuestion) must not block
|
|
373
|
+
// its own hook response while the human answers. Denying immediately makes the provider persist
|
|
374
|
+
// the buffered turn plus rejected tool_result, so the pre-flush harvest below can publish the
|
|
375
|
+
// reasoning before the dashboard form appears. The eventual answer is injected as a normal prompt.
|
|
376
|
+
const response = Response.json(handler.buildHookResponse(
|
|
377
|
+
{ approvalId, decision: "deny", reason: PENDING_QUESTION_DENY_REASON },
|
|
378
|
+
body,
|
|
379
|
+
));
|
|
380
|
+
void awaitPermissionDecision({
|
|
381
|
+
options, pendingPermissionRequests, handler, approvalId, body, occurredAt, transcriptPath, toolName, toolInput,
|
|
382
|
+
blockedReason: "pendingQuestion",
|
|
383
|
+
recommendedAction: "Answer the question in Agent Relay.",
|
|
384
|
+
suppressResolvedStatus: true,
|
|
385
|
+
}).then(({ decision, view }) => {
|
|
386
|
+
const text = decision.decision === "answer" && decision.answers && Object.keys(decision.answers).length
|
|
387
|
+
? handler.buildAnswerInjectionPrompt!(view, decision, body)
|
|
388
|
+
: pendingQuestionDismissalPrompt(decision);
|
|
389
|
+
return options.onAnswerInject?.({ approvalId, text });
|
|
390
|
+
}).catch(() => {});
|
|
391
|
+
return response;
|
|
392
|
+
}
|
|
393
|
+
|
|
364
394
|
// Shared by both the traditional block-until-decided flow and the #1173 deny-then-hold flow:
|
|
365
395
|
// pre-flushes narrative (#435/#1116), builds+persists the interactive view, publishes the
|
|
366
396
|
// blocked providerState, and resolves once a decision arrives (or the wait times out). Callers
|
package/src/profile-home.ts
CHANGED
|
@@ -140,7 +140,7 @@ function seedClaudeConfigIfMissing(claudeHome: string, config: RunnerSpawnConfig
|
|
|
140
140
|
|
|
141
141
|
function readHostClaudeConfig(): Record<string, unknown> | undefined {
|
|
142
142
|
try {
|
|
143
|
-
const hostPath = join(
|
|
143
|
+
const hostPath = join(providerSourceHome("claude"), ".claude.json");
|
|
144
144
|
if (!existsSync(hostPath)) return undefined;
|
|
145
145
|
return JSON.parse(readFileSync(hostPath, "utf8")) as Record<string, unknown>;
|
|
146
146
|
} catch {
|