agent-relay-runner 0.68.1 → 0.69.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.68.1",
3
+ "version": "0.69.0",
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.68.1",
4
+ "version": "0.69.0",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -27,6 +27,20 @@ export function obligationRequiresExplicitReply(obligation: ReplyObligation): bo
27
27
  return obligation.from !== "user";
28
28
  }
29
29
 
30
+ export function responseCaptureReplyRoute(
31
+ obligations: ReplyObligation[],
32
+ currentTurnStartedAt?: number,
33
+ opts: { suppressUser?: boolean } = {},
34
+ ): { replyToMessageId?: number; to: string } | null {
35
+ const eligible = [...obligations].reverse()
36
+ .filter((o) => currentTurnStartedAt === undefined || o.createdAt <= currentTurnStartedAt);
37
+ const explicit = eligible.find(obligationRequiresExplicitReply);
38
+ if (explicit) return { replyToMessageId: explicit.messageId, to: explicit.from };
39
+ const user = eligible.find((o) => o.from === "user");
40
+ if (!user) return { to: "user" };
41
+ return opts.suppressUser ? null : { replyToMessageId: user.messageId, to: "user" };
42
+ }
43
+
30
44
  type ReplyObligationFetch = () => Promise<ReplyObligation[]>;
31
45
 
32
46
  interface ReplyObligationCacheOptions {
@@ -10,7 +10,7 @@ import type { ManagedProcess, ProviderAdapter, ProviderConfig, ProviderPermissio
10
10
  import { messagesWithCachedAttachments } from "./attachment-cache";
11
11
  import { ClaimTracker } from "./claim-tracker";
12
12
  import { startControlServer, type ControlServer } from "./control-server";
13
- import { ReplyObligationCache, obligationRequiresExplicitReply } from "./reply-obligation-cache";
13
+ import { ReplyObligationCache, obligationRequiresExplicitReply, responseCaptureReplyRoute } from "./reply-obligation-cache";
14
14
  import { Outbox, type OutboxRecord } from "./outbox";
15
15
  import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractFinalAssistantMessageAfterEntry, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
16
16
  import { profileUsesHostProviderGlobals } from "./profile-home";
@@ -1336,7 +1336,7 @@ export class AgentRunner {
1336
1336
  const turnId = this.currentTurnId;
1337
1337
  this.stopReasoningTail();
1338
1338
  // Optional correlation for threading + obligation clearing — never a capture gate.
1339
- let replyToMessageId: number | undefined;
1339
+ let replyToMessageId: number | undefined, replyTarget = "user";
1340
1340
  const pendingPrompt = this.pendingPromptMessageId;
1341
1341
  if (pendingPrompt) {
1342
1342
  replyToMessageId = pendingPrompt;
@@ -1344,8 +1344,8 @@ export class AgentRunner {
1344
1344
  } else {
1345
1345
  // Correlation-only (threading + obligation clearing) — the local snapshot is fresh
1346
1346
  // enough and never blocks the response-capture path (#196).
1347
- const obligation = [...this.obligationCache.get()].reverse().find((o) => o.from === "user");
1348
- replyToMessageId = obligation?.messageId;
1347
+ const route = responseCaptureReplyRoute(this.obligationCache.get(), this.currentTurnStartedAt);
1348
+ replyToMessageId = route?.replyToMessageId; replyTarget = route?.to ?? "user";
1349
1349
  }
1350
1350
 
1351
1351
  // Grab and reset the pre-flush cursor before extraction: the cursor may be
@@ -1391,7 +1391,7 @@ export class AgentRunner {
1391
1391
  this.sessionLog(`response captured for turn ${turnId ?? "?"} (${body.length} chars${replyToMessageId ? `, replyTo #${replyToMessageId}` : ", no replyTo"})`);
1392
1392
  await this.publishSessionEvent({
1393
1393
  from: this.agentId,
1394
- to: "user",
1394
+ to: replyTarget,
1395
1395
  body,
1396
1396
  ...(replyToMessageId ? { replyTo: replyToMessageId } : {}),
1397
1397
  session: { type: "response", origin: "provider", ...(turnId ? { turnId } : {}) },
@@ -1600,20 +1600,21 @@ export class AgentRunner {
1600
1600
  }
1601
1601
  if (event.type === "response") {
1602
1602
  // Dashboard prompt injection is already answered by this captured App Server
1603
- // response. Other Relay inbox obligations still belong to the agent's explicit
1604
- // reply path, so those keep suppressing auto-capture to avoid duplicates.
1605
- let replyToMessageId: number | undefined;
1603
+ // response. User obligations still belong to the session mirror; spawner
1604
+ // obligations (#413) route to the spawn-parent with replyTo to clear the debt.
1605
+ let replyToMessageId: number | undefined, replyTarget = "user";
1606
1606
  const pendingPrompt = this.pendingPromptMessageId;
1607
1607
  if (pendingPrompt) {
1608
1608
  replyToMessageId = pendingPrompt;
1609
1609
  this.pendingPromptMessageId = undefined;
1610
- } else if (this.obligationCache.get().some((o) => o.from === "user" && this.obligationPredatesCurrentTurn(o))) {
1611
- // The agent will answer the relay obligation itself — don't double-post (#196).
1612
- return;
1610
+ } else {
1611
+ const route = responseCaptureReplyRoute(this.obligationCache.get(), this.currentTurnStartedAt, { suppressUser: true });
1612
+ if (!route) return; // The session mirror will satisfy the user obligation; don't double-post (#196).
1613
+ replyToMessageId = route.replyToMessageId; replyTarget = route.to;
1613
1614
  }
1614
1615
  await this.publishSessionEvent({
1615
1616
  from: this.agentId,
1616
- to: "user",
1617
+ to: replyTarget,
1617
1618
  body,
1618
1619
  ...(replyToMessageId ? { replyTo: replyToMessageId } : {}),
1619
1620
  session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}) },
@@ -1660,10 +1661,6 @@ export class AgentRunner {
1660
1661
  return false;
1661
1662
  }
1662
1663
 
1663
- private obligationPredatesCurrentTurn(obligation: { createdAt: number }): boolean {
1664
- return this.currentTurnStartedAt === undefined || obligation.createdAt <= this.currentTurnStartedAt;
1665
- }
1666
-
1667
1664
  // Force-clear a stuck provider-turn claim directly. Unlike the idle status path
1668
1665
  // it does NOT depend on a matching claim id (the Stop hook keys busy as
1669
1666
  // provider-turn:provider-turn, but reconciliation has no specific id), and it