agent-relay-runner 0.78.6 → 0.78.8

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.78.6",
3
+ "version": "0.78.8",
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.56"
23
+ "agent-relay-sdk": "0.2.58"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "latest",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.78.6",
4
+ "version": "0.78.8",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -1,16 +1,20 @@
1
1
  type EmitAgentMessage = (body: string) => void;
2
+ type ResponseEntry = { itemId?: string; body: string };
3
+ type CompleteItemOptions = { includeInResponse?: boolean; phase?: unknown };
2
4
 
3
5
  export class CodexAgentMessageCapture {
4
- private segments: string[][] = [];
5
- private currentSegment: string[] = [];
6
+ private segments: ResponseEntry[][] = [];
7
+ private currentSegment: ResponseEntry[] = [];
6
8
  private readonly buffers = new Map<string, string>();
7
9
  private readonly flushedLengths = new Map<string, number>();
10
+ private readonly responseRecordedLengths = new Map<string, number>();
8
11
 
9
12
  reset(): void {
10
13
  this.segments = [];
11
14
  this.currentSegment = [];
12
15
  this.buffers.clear();
13
16
  this.flushedLengths.clear();
17
+ this.responseRecordedLengths.clear();
14
18
  }
15
19
 
16
20
  appendDelta(itemId: string | undefined, delta: string): void {
@@ -18,8 +22,14 @@ export class CodexAgentMessageCapture {
18
22
  this.buffers.set(itemId, `${this.buffers.get(itemId) ?? ""}${delta}`);
19
23
  }
20
24
 
21
- completeItem(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage): void {
22
- this.flushText(itemId, text ?? (itemId ? this.buffers.get(itemId) : undefined), emit);
25
+ completeItem(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage, options: CompleteItemOptions = {}): void {
26
+ const fullText = text ?? (itemId ? this.buffers.get(itemId) : undefined);
27
+ this.flushText(itemId, fullText, emit);
28
+ if (options.includeInResponse === false || options.phase === "commentary") {
29
+ this.forgetResponseItem(itemId);
30
+ } else {
31
+ this.recordResponseText(itemId, fullText);
32
+ }
23
33
  if (!itemId) return;
24
34
  this.buffers.delete(itemId);
25
35
  this.flushedLengths.delete(itemId);
@@ -36,11 +46,14 @@ export class CodexAgentMessageCapture {
36
46
  this.closeSegment(emit);
37
47
  const finalSegment = this.segments.at(-1);
38
48
  this.segments = [];
39
- return finalSegment?.length ? finalSegment.join("\n\n").trim() : undefined;
49
+ return finalSegment?.length ? finalSegment.map((entry) => entry.body).join("\n\n").trim() : undefined;
40
50
  }
41
51
 
42
52
  private flushPending(emit: EmitAgentMessage): void {
43
- for (const [itemId, text] of [...this.buffers.entries()]) this.flushText(itemId, text, emit);
53
+ for (const [itemId, text] of [...this.buffers.entries()]) {
54
+ this.flushText(itemId, text, emit);
55
+ this.recordResponseText(itemId, text);
56
+ }
44
57
  }
45
58
 
46
59
  private flushText(itemId: string | undefined, text: string | undefined, emit: EmitAgentMessage): void {
@@ -50,7 +63,30 @@ export class CodexAgentMessageCapture {
50
63
  if (itemId) this.flushedLengths.set(itemId, text.length);
51
64
  const body = chunk.trim();
52
65
  if (!body) return;
53
- this.currentSegment.push(body);
54
66
  emit(body);
55
67
  }
68
+
69
+ private recordResponseText(itemId: string | undefined, text: string | undefined): void {
70
+ if (!text) return;
71
+ const body = text.trim();
72
+ if (!body) return;
73
+ if (itemId) {
74
+ const offset = this.responseRecordedLengths.get(itemId) ?? 0;
75
+ const chunk = text.slice(offset).trim();
76
+ this.responseRecordedLengths.set(itemId, text.length);
77
+ if (!chunk) return;
78
+ this.currentSegment.push({ itemId, body: chunk });
79
+ return;
80
+ }
81
+ this.currentSegment.push({ body });
82
+ }
83
+
84
+ private forgetResponseItem(itemId: string | undefined): void {
85
+ if (!itemId) return;
86
+ this.responseRecordedLengths.delete(itemId);
87
+ this.currentSegment = this.currentSegment.filter((entry) => entry.itemId !== itemId);
88
+ this.segments = this.segments
89
+ .map((segment) => segment.filter((entry) => entry.itemId !== itemId))
90
+ .filter((segment) => segment.length > 0);
91
+ }
56
92
  }
@@ -504,7 +504,7 @@ export class CodexAdapter implements ProviderAdapter {
504
504
  // buffered reasoning first so it lands in transcript order, ahead of this tool/message.
505
505
  if (type !== "reasoning") this.flushBufferedReasoning();
506
506
  if (type === "agentMessage") {
507
- this.agentMessageCapture.completeItem(itemId, stringValue(item.text), (body) => this.emitAgentNarration(body));
507
+ this.agentMessageCapture.completeItem(itemId, stringValue(item.text), (body) => this.emitAgentNarration(body), { phase: item.phase });
508
508
  return;
509
509
  }
510
510
  if (type === "userMessage") {
@@ -0,0 +1,57 @@
1
+ import type { RelayHttpClient } from "agent-relay-sdk";
2
+ import { quotaStateFromProbeMetrics } from "agent-relay-sdk/context-probe";
3
+ import { readRunnerContextProbeState } from "./context-probe-state";
4
+ import { DEFAULT_PROVIDER_QUOTA_CONFIG, normalizeProviderQuotaConfig, resolveClaudeStatuslineQuotaIdentity } from "./quota";
5
+
6
+ const PASSIVE_QUOTA_CONFIG_REFRESH_MS = 60_000;
7
+
8
+ interface ClaudeQuotaHarvestOptions {
9
+ agentId: string;
10
+ provider: string;
11
+ http: () => Pick<RelayHttpClient, "getProviderQuotaConfig" | "upsertProviderQuota">;
12
+ }
13
+
14
+ export class ClaudeQuotaHarvest {
15
+ private config = { ...DEFAULT_PROVIDER_QUOTA_CONFIG };
16
+ private configRefreshAt = 0;
17
+ private lastReportAt?: number;
18
+
19
+ constructor(private readonly options: ClaudeQuotaHarvestOptions) {}
20
+
21
+ async publish(): Promise<void> {
22
+ if (this.options.provider !== "claude") return;
23
+ const probeMetrics = readRunnerContextProbeState(this.options.agentId);
24
+ const quota = probeMetrics ? quotaStateFromProbeMetrics(probeMetrics) : undefined;
25
+ if (!quota) return;
26
+ const config = await this.refreshConfig();
27
+ if (!config.enabled) return;
28
+ const now = Date.now();
29
+ if (this.lastReportAt !== undefined && now - this.lastReportAt < config.pollIntervalMs) return;
30
+ const identity = await resolveClaudeStatuslineQuotaIdentity({ env: process.env });
31
+ if (!identity) return;
32
+ await this.options.http().upsertProviderQuota({
33
+ provider: "claude",
34
+ accountKey: identity.accountKey,
35
+ quota,
36
+ lastAttemptAt: quota.updatedAt,
37
+ sourceAgentId: this.options.agentId,
38
+ });
39
+ this.lastReportAt = now;
40
+ }
41
+
42
+ private async refreshConfig() {
43
+ const now = Date.now();
44
+ if (now < this.configRefreshAt) return this.config;
45
+ this.configRefreshAt = now + PASSIVE_QUOTA_CONFIG_REFRESH_MS;
46
+ try {
47
+ const map = await this.options.http().getProviderQuotaConfig();
48
+ this.config = map.claude
49
+ ? normalizeProviderQuotaConfig(map.claude)
50
+ : { ...DEFAULT_PROVIDER_QUOTA_CONFIG };
51
+ } catch {
52
+ // Keep the previous config; defaults preserve collection if the relay is older
53
+ // or temporarily unreachable during a liveness tick.
54
+ }
55
+ return this.config;
56
+ }
57
+ }
@@ -0,0 +1,6 @@
1
+ import { readContextProbeState } from "agent-relay-sdk/context-probe";
2
+ import { contextStateDirFromEnv } from "./config";
3
+
4
+ export function readRunnerContextProbeState(agentId: string) {
5
+ return readContextProbeState(agentId, contextStateDirFromEnv());
6
+ }
@@ -0,0 +1,9 @@
1
+ import type { ManagedProcess } from "./adapter";
2
+
3
+ export function providerTerminalSession(process?: ManagedProcess): string | undefined {
4
+ return typeof process?.meta?.tmuxSession === "string" ? process.meta.tmuxSession : undefined;
5
+ }
6
+
7
+ export function providerTerminalSocket(process?: ManagedProcess): string | undefined {
8
+ return typeof process?.meta?.tmuxSocket === "string" ? process.meta.tmuxSocket : undefined;
9
+ }
package/src/quota.ts CHANGED
@@ -4,6 +4,7 @@ export {
4
4
  CLAUDE_MESSAGES_URL,
5
5
  CLAUDE_QUOTA_PROBE_MODEL,
6
6
  CLAUDE_USAGE_URL,
7
+ DEFAULT_PROVIDER_QUOTA_CONFIG,
7
8
  QUOTA_FAILURE_LOG_INTERVAL_MS,
8
9
  QUOTA_FAST_RETRY_MS,
9
10
  QUOTA_POLL_INTERVAL_MS,
@@ -20,11 +21,13 @@ export {
20
21
  defaultClaudeConfigDir,
21
22
  defaultClaudeCredentialsPath,
22
23
  hostProviderAccountKey,
24
+ normalizeProviderQuotaConfig,
23
25
  providerPayloadAccountKey,
24
26
  providerQuotaErrorFromCollectorError,
25
27
  quotaRetryAfterMs,
26
28
  readClaudeOAuthAccessToken,
27
29
  resolveClaudeQuotaIdentity,
30
+ resolveClaudeStatuslineQuotaIdentity,
28
31
  resolveCodexQuotaIdentity,
29
32
  resolveCodexQuotaIdentityFromHome,
30
33
  resolveStableClaudeQuotaIdentity,
@@ -4,11 +4,13 @@ import { readFile } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
5
5
  import type { AgentLifecycle, AgentProfile, ContextState, Message, MessageSessionMeta, SendMessageInput, TaskStatusInput, WorkspaceMetadata } from "agent-relay-sdk";
6
6
  import { errMessage, RelayBusClient, RelayHttpClient } from "agent-relay-sdk";
7
- import { contextStateFromProbeMetrics, quotaStateFromProbeMetrics, readContextProbeState } from "agent-relay-sdk/context-probe";
7
+ import { contextStateFromProbeMetrics, quotaStateFromProbeMetrics } from "agent-relay-sdk/context-probe";
8
8
  import { providerAttachmentText, providerMessageText } from "./adapter";
9
9
  import type { ManagedProcess, ProviderAdapter, ProviderConfig, ProviderPermissionDecision, ProviderPermissionDecisionInput, ProviderSessionEvent, ProviderStatusUpdate, RunnerSpawnConfig, SemanticStatus, TerminalAttachSpec } from "./adapter";
10
10
  import { messagesWithCachedAttachments } from "./attachment-cache";
11
11
  import { ClaimTracker } from "./claim-tracker";
12
+ import { ClaudeQuotaHarvest } from "./claude-quota-harvest";
13
+ import { readRunnerContextProbeState } from "./context-probe-state";
12
14
  import { startControlServer, type ControlServer } from "./control-server";
13
15
  import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
14
16
  import { Outbox, type OutboxRecord } from "./outbox";
@@ -24,7 +26,8 @@ import { RunnerInsights } from "./runner-insights";
24
26
  import { BusyReconciler } from "./busy-reconciler";
25
27
  import { publishCapturedResponse } from "./response-capture-report";
26
28
  import { runnerLaunchInjectionSessionEvents, type RunnerRelayInjectionEvent } from "./relay-injection-events";
27
- import { capsFromEnv, contextStateDirFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
29
+ import { providerTerminalSession, providerTerminalSocket } from "./process-meta";
30
+ import { capsFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
28
31
  import {
29
32
  appliedAgentProfileMetadata,
30
33
  commandTimeoutMs,
@@ -204,6 +207,7 @@ export class AgentRunner {
204
207
  private readonly sessionOutbox: Outbox;
205
208
  private readonly insights: RunnerInsights;
206
209
  private readonly busyReconciler: BusyReconciler;
210
+ private readonly claudeQuotaHarvest: ClaudeQuotaHarvest;
207
211
  private currentToken?: string;
208
212
  private currentTokenJti?: string;
209
213
  private currentTokenProfileId?: string;
@@ -314,6 +318,7 @@ export class AgentRunner {
314
318
  this.mcpProxySecret = crypto.randomUUID();
315
319
  const runtime = runtimeMetadata(options.provider);
316
320
  this.http = new RelayHttpClient({ baseUrl: options.relayUrl, token: this.currentToken });
321
+ this.claudeQuotaHarvest = new ClaudeQuotaHarvest({ agentId: this.agentId, provider: options.provider, http: () => this.http });
317
322
  this.obligationCache = new ReplyObligationCache({ fetch: () => this.http.listReplyObligations(this.agentId) });
318
323
  // Co-locate the durable outbox with the runner's runtime state (survives reboot) when the
319
324
  // orchestrator told us where that is; otherwise the Outbox falls back to a temp dir.
@@ -386,8 +391,8 @@ export class AgentRunner {
386
391
  agentProfile: options.agentProfile ? appliedAgentProfileMetadata(options.provider, options.agentProfile, { ...options, agentId: this.agentId, env: {}, controlPort: 0 } as RunnerSpawnConfig, options.providerConfig) : null,
387
392
  runnerId: options.runnerId,
388
393
  startedAt: options.startedAt,
389
- tmuxSession: this.providerTerminalSession() ?? options.tmuxSession ?? null,
390
- tmuxSocket: this.providerTerminalSocket() ?? null,
394
+ tmuxSession: providerTerminalSession(this.process) ?? options.tmuxSession ?? null,
395
+ tmuxSocket: providerTerminalSocket(this.process) ?? null,
391
396
  policyName: options.policyName ?? null,
392
397
  spawnRequestId: options.spawnRequestId ?? null,
393
398
  automationId: options.automationId ?? null,
@@ -628,8 +633,8 @@ export class AgentRunner {
628
633
  provider: this.options.provider,
629
634
  controlUrl: this.control.url,
630
635
  pid: process.pid,
631
- ...(this.providerTerminalSession() ? { tmuxSession: this.providerTerminalSession() } : {}),
632
- ...(this.providerTerminalSocket() ? { tmuxSocket: this.providerTerminalSocket() } : {}),
636
+ ...(providerTerminalSession(this.process) ? { tmuxSession: providerTerminalSession(this.process) } : {}),
637
+ ...(providerTerminalSocket(this.process) ? { tmuxSocket: providerTerminalSocket(this.process) } : {}),
633
638
  startedAt: this.options.startedAt,
634
639
  }, null, 2) + "\n", { mode: 0o600 });
635
640
  } catch (error) {
@@ -1807,8 +1812,8 @@ export class AgentRunner {
1807
1812
  meta: {
1808
1813
  runnerId: this.options.runnerId,
1809
1814
  startedAt: this.options.startedAt,
1810
- tmuxSession: this.providerTerminalSession() ?? this.options.tmuxSession ?? null,
1811
- tmuxSocket: this.providerTerminalSocket() ?? null,
1815
+ tmuxSession: providerTerminalSession(this.process) ?? this.options.tmuxSession ?? null,
1816
+ tmuxSocket: providerTerminalSocket(this.process) ?? null,
1812
1817
  policyName: this.options.policyName ?? null,
1813
1818
  spawnRequestId: this.options.spawnRequestId ?? null,
1814
1819
  automationId: this.options.automationId ?? null,
@@ -1857,6 +1862,7 @@ export class AgentRunner {
1857
1862
  await this.http.setStatus(this.agentId, agentStatus, this.options.instanceId);
1858
1863
  await this.http.setReady(this.agentId, agentStatus !== "offline", this.options.instanceId);
1859
1864
  await this.http.heartbeat(this.agentId, this.options.instanceId);
1865
+ await this.claudeQuotaHarvest.publish().catch((error) => this.logRunnerDiagnostic(`passive Claude quota publish failed: ${errMessage(error)}`));
1860
1866
  await this.bootstrapUnreadMessages();
1861
1867
  } catch (error) {
1862
1868
  if (!this.stopped) this.handleHttpLivenessFailure(error);
@@ -2111,8 +2117,8 @@ export class AgentRunner {
2111
2117
  const probeContext = probeMetrics ? contextStateFromProbeMetrics(probeMetrics) : undefined;
2112
2118
  const context = processContext ?? probeContext;
2113
2119
  const quota = probeMetrics ? quotaStateFromProbeMetrics(probeMetrics) : undefined;
2114
- const terminalSession = this.providerTerminalSession();
2115
- const terminalSocket = this.providerTerminalSocket();
2120
+ const terminalSession = providerTerminalSession(this.process);
2121
+ const terminalSocket = providerTerminalSocket(this.process);
2116
2122
  const probeModel: ProbeModelInfo | undefined = probeMetrics?.model || probeMetrics?.effort
2117
2123
  ? { model: probeMetrics.model, effort: probeMetrics.effort }
2118
2124
  : undefined;
@@ -2125,14 +2131,6 @@ export class AgentRunner {
2125
2131
  return meta;
2126
2132
  }
2127
2133
 
2128
- private providerTerminalSession(): string | undefined {
2129
- return typeof this.process?.meta?.tmuxSession === "string" ? this.process.meta.tmuxSession : undefined;
2130
- }
2131
-
2132
- private providerTerminalSocket(): string | undefined {
2133
- return typeof this.process?.meta?.tmuxSocket === "string" ? this.process.meta.tmuxSocket : undefined;
2134
- }
2135
-
2136
2134
  private latestProcessContext(): ContextState | undefined {
2137
2135
  const getContext = this.process?.meta?.getContext;
2138
2136
  if (typeof getContext === "function") {
@@ -2144,8 +2142,7 @@ export class AgentRunner {
2144
2142
  }
2145
2143
 
2146
2144
  private latestProbeMetrics() {
2147
- const stateDir = contextStateDirFromEnv();
2148
- return readContextProbeState(this.agentId, stateDir);
2145
+ return readRunnerContextProbeState(this.agentId);
2149
2146
  }
2150
2147
 
2151
2148
  private matchesMessage(message: Message): boolean {