agent-relay-runner 0.57.1 → 0.58.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.57.1",
3
+ "version": "0.58.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.33"
23
+ "agent-relay-sdk": "0.2.34"
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.57.1",
4
+ "version": "0.58.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -2,6 +2,7 @@ import type { Server, ServerWebSocket } from "bun";
2
2
  import type { Message, ReplyObligation } from "agent-relay-sdk";
3
3
  import { errMessage, isRecord } from "agent-relay-sdk";
4
4
  import type { ProviderPermissionDecisionInput, ProviderStatusEvent, SemanticStatus, TerminalAttachSpec } from "./adapter";
5
+ import { obligationRequiresExplicitReply } from "./reply-obligation-cache";
5
6
  import { logger, parseLogLevel, LOG_LEVELS } from "./logger";
6
7
  import { buildRateLimitProviderState } from "./rate-limit";
7
8
 
@@ -199,7 +200,13 @@ function replyObligationSummary(obligations: ReplyObligation[]): Record<string,
199
200
  }
200
201
 
201
202
  function replyObligationStopDecision(obligations: ReplyObligation[]): Record<string, unknown> {
202
- const summary = replyObligationSummary(obligations);
203
+ // #418 never block the turn to nag for a `user` obligation: the operator already receives
204
+ // this turn via the session mirror, so an explicit /reply double-delivers. Only agent↔agent /
205
+ // spawner obligations (#413) gate the turn here; the mirror's replyTo-linking clears the user
206
+ // one. (The /reply-obligations summary endpoint is left unfiltered — the obligation IS pending
207
+ // until the mirror lands, so observability stays accurate.)
208
+ const enforceable = obligations.filter(obligationRequiresExplicitReply);
209
+ const summary = replyObligationSummary(enforceable);
203
210
  if (summary.pending !== true) return {};
204
211
  return {
205
212
  decision: "block",
@@ -16,6 +16,17 @@ import { logger } from "./logger";
16
16
  // - A background interval keeps the snapshot warm; `markDirty()` requests an extra,
17
17
  // debounced refresh when state likely just changed (a message arrived, a turn ended).
18
18
 
19
+ // #418 — the human `user` sink is NOT delivered via an explicit relay reply: the agent's
20
+ // turn is mirrored to the dashboard chat by the session-mirror lane (always, for managed
21
+ // agents). So an explicit `/reply` to `user` double-delivers — the operator sees the mirrored
22
+ // turn AND the explicit copy. Only agent↔agent / spawner obligations (#413) need an explicit
23
+ // reply to clear and reach their recipient. This is the single home for that distinction —
24
+ // both the Claude Stop-hook nag (control-server) and the non-claude re-injection (runner)
25
+ // gate on it, so the rule must not drift between them.
26
+ export function obligationRequiresExplicitReply(obligation: ReplyObligation): boolean {
27
+ return obligation.from !== "user";
28
+ }
29
+
19
30
  type ReplyObligationFetch = () => Promise<ReplyObligation[]>;
20
31
 
21
32
  interface ReplyObligationCacheOptions {
package/src/runner.ts CHANGED
@@ -11,7 +11,7 @@ import { messagesWithCachedAttachments } from "./attachment-cache";
11
11
  import { resolveProjectName } from "./config";
12
12
  import { ClaimTracker } from "./claim-tracker";
13
13
  import { startControlServer, type ControlServer } from "./control-server";
14
- import { ReplyObligationCache } from "./reply-obligation-cache";
14
+ import { ReplyObligationCache, obligationRequiresExplicitReply } from "./reply-obligation-cache";
15
15
  import { Outbox, type OutboxRecord } from "./outbox";
16
16
  import { extractLastAssistantTurn, extractFinalAssistantMessage, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
17
17
  import { computeContextRatio } from "./session-insights";
@@ -1215,9 +1215,11 @@ export class AgentRunner {
1215
1215
  // stop.sh hook; every other provider needs this in-process nudge.
1216
1216
  private async reInjectPendingObligation(): Promise<void> {
1217
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");
1218
+ // Filter to spawner obligations only — user obligations go through the existing codex
1219
+ // prompt-capture path (publishProviderSessionEvent) and the session mirror (#418), never
1220
+ // an explicit /reply. Shared predicate so this rule stays in lockstep with the Claude
1221
+ // Stop-hook nag exemption (control-server.replyObligationStopDecision).
1222
+ const obligations = this.obligationCache.get().filter(obligationRequiresExplicitReply);
1221
1223
  if (!obligations.length) return;
1222
1224
  const MAX_REINJECTIONS = 3;
1223
1225
  for (const obligation of obligations) {