agent-relay-runner 0.57.0 → 0.57.1

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.57.0",
3
+ "version": "0.57.1",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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.57.0",
4
+ "version": "0.57.1",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/runner.ts CHANGED
@@ -5,6 +5,7 @@ import { dirname, join } from "node:path";
5
5
  import type { AgentLifecycle, AgentProfile, ContextState, Message, MessageSessionMeta, ProviderCapabilities, SendMessageInput, TaskStatusInput, WorkspaceMetadata } from "agent-relay-sdk";
6
6
  import { errMessage, RelayBusClient, RelayHttpClient } from "agent-relay-sdk";
7
7
  import { contextStateFromProbeMetrics, readContextProbeState } from "agent-relay-sdk/context-probe";
8
+ import { providerMessageText } from "./adapter";
8
9
  import type { ManagedProcess, ProviderAdapter, ProviderConfig, ProviderPermissionDecision, ProviderPermissionDecisionInput, ProviderSessionEvent, ProviderStatusUpdate, RunnerSpawnConfig, SemanticStatus, TerminalAttachSpec } from "./adapter";
9
10
  import { messagesWithCachedAttachments } from "./attachment-cache";
10
11
  import { resolveProjectName } from "./config";
@@ -288,6 +289,9 @@ export class AgentRunner {
288
289
  // from the Claude transcript into chat as discreet session events.
289
290
  private reasoningTail?: { timer: ReturnType<typeof setInterval>; seen: Set<string> };
290
291
  private scratch?: SessionScratchLayout;
292
+ // #417: for non-claude providers, tracks how many times each spawner obligation has
293
+ // been re-injected so a non-responsive agent can't spin in a re-injection loop.
294
+ private readonly pendingObligationInjections = new Map<number, number>();
291
295
 
292
296
  constructor(private readonly options: RunnerOptions) {
293
297
  this.agentId = options.agentId ?? options.runnerId;
@@ -1170,6 +1174,9 @@ export class AgentRunner {
1170
1174
  this.compactionMidTurn = false;
1171
1175
  this.disarmBusyReconciler();
1172
1176
  this.stopReasoningTail();
1177
+ // #417: claude enforces obligations via the stop.sh hook; for every other provider
1178
+ // re-inject any pending spawner obligation so it surfaces the /reply command.
1179
+ if (this.options.provider !== "claude") void this.reInjectPendingObligation();
1173
1180
  }
1174
1181
  if (status === "busy") {
1175
1182
  this.claims.clearTerminalStatus();
@@ -1202,6 +1209,49 @@ export class AgentRunner {
1202
1209
  this.publishStatus();
1203
1210
  }
1204
1211
 
1212
+ // #417: when a non-claude provider finishes a turn with a pending spawner obligation
1213
+ // still unresolved, re-inject the obligation as a synthetic message so providerMessageText
1214
+ // surfaces the specific `agent-relay /reply <id>` command. Claude handles this via the
1215
+ // stop.sh hook; every other provider needs this in-process nudge.
1216
+ private async reInjectPendingObligation(): Promise<void> {
1217
+ if (!this.process) return;
1218
+ // Filter to spawner obligations only (from ≠ "user") — user obligations go through
1219
+ // the existing codex prompt-capture path (publishProviderSessionEvent).
1220
+ const obligations = this.obligationCache.get().filter((o) => o.from !== "user");
1221
+ if (!obligations.length) return;
1222
+ const MAX_REINJECTIONS = 3;
1223
+ for (const obligation of obligations) {
1224
+ const count = this.pendingObligationInjections.get(obligation.messageId) ?? 0;
1225
+ if (count >= MAX_REINJECTIONS) continue;
1226
+ this.pendingObligationInjections.set(obligation.messageId, count + 1);
1227
+ this.sessionLog(`re-injecting pending reply obligation #${obligation.messageId} from ${obligation.from} (attempt ${count + 1}/${MAX_REINJECTIONS})`);
1228
+ const replayMessage = {
1229
+ id: obligation.messageId,
1230
+ from: obligation.from,
1231
+ to: this.agentId,
1232
+ kind: obligation.kind,
1233
+ ...(obligation.subject ? { subject: obligation.subject } : {}),
1234
+ body: obligation.bodyPreview,
1235
+ payload: {},
1236
+ readBy: [] as string[],
1237
+ createdAt: obligation.createdAt,
1238
+ };
1239
+ // Suppress the userMessage echo that providers (e.g. Codex) reflect back when
1240
+ // a turn is started with relay-formatted content — the echo body is exactly what
1241
+ // providerMessageText produces, which starts with "[relay message #" and is caught
1242
+ // by the RELAY_INJECTION_MARKERS prefix check, but record it explicitly as
1243
+ // defense-in-depth for adapters that might echo only the unformatted body text.
1244
+ this.recordInjectedPrompt(providerMessageText([replayMessage]));
1245
+ await this.options.adapter.deliver(this.process, [replayMessage]);
1246
+ // Kick off a cache refresh so a cleared obligation (agent replied) is detected
1247
+ // before the next idle fires — without this, the stale snapshot re-injects up to
1248
+ // MAX_REINJECTIONS times even after the agent has already /reply-ed.
1249
+ this.obligationCache.markDirty();
1250
+ // One obligation at a time — let the provider reply before nudging again.
1251
+ break;
1252
+ }
1253
+ }
1254
+
1205
1255
  // Session-mirror lane: capture the assistant turn from the Claude transcript and
1206
1256
  // post it as a "session" message so it shows in the dashboard chat with zero
1207
1257
  // agent tokens. Capture is UNCONDITIONAL — it no longer depends on a triggering