agent-relay-runner 0.68.1 → 0.69.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.68.1",
3
+ "version": "0.69.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.68.1",
4
+ "version": "0.69.1",
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 {
@@ -0,0 +1,59 @@
1
+ import type { MessageSessionMeta, SendMessageInput } from "agent-relay-sdk";
2
+
3
+ interface ReportOutbox {
4
+ enqueue(record: { kind: string; idempotencyKey?: string; payload: SendMessageInput }): void;
5
+ }
6
+
7
+ type SessionPublisher = (input: {
8
+ from: string;
9
+ to: string;
10
+ body: string;
11
+ session: MessageSessionMeta;
12
+ replyTo?: number;
13
+ }) => void | Promise<void>;
14
+
15
+ export async function publishCapturedResponse(input: {
16
+ publishSessionEvent: SessionPublisher;
17
+ outbox: ReportOutbox;
18
+ provider: string;
19
+ from: string;
20
+ to: string;
21
+ body: string;
22
+ session: MessageSessionMeta;
23
+ replyTo?: number;
24
+ }): Promise<void> {
25
+ await input.publishSessionEvent({
26
+ from: input.from,
27
+ to: input.to,
28
+ body: input.body,
29
+ ...(input.replyTo ? { replyTo: input.replyTo } : {}),
30
+ session: input.session,
31
+ });
32
+ enqueueCapturedResponseReport(input);
33
+ }
34
+
35
+ export function enqueueCapturedResponseReport(input: {
36
+ outbox: ReportOutbox;
37
+ provider: string;
38
+ from: string;
39
+ to: string;
40
+ body: string;
41
+ session: MessageSessionMeta;
42
+ replyTo?: number;
43
+ }): void {
44
+ if (!input.replyTo || input.to === "user") return;
45
+ const turnId = input.session.turnId ?? "";
46
+ input.outbox.enqueue({
47
+ kind: "session-message",
48
+ idempotencyKey: `response-report:${input.from}:${input.replyTo}:${turnId}`,
49
+ payload: {
50
+ from: input.from,
51
+ to: input.to,
52
+ replyTo: input.replyTo,
53
+ kind: "chat",
54
+ body: input.body,
55
+ replyExpected: false,
56
+ payload: { responseCapture: { provider: input.provider, ...input.session } },
57
+ },
58
+ });
59
+ }
@@ -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";
@@ -22,6 +22,7 @@ import { ensureSessionScratch, reapSessionScratch, sweepStaleSessions, type Sess
22
22
  import { deliverBufferedMcpCall as deliverBufferedMcpOutboxCall, deliverRunnerOutboxEvent } from "./mcp-outbox";
23
23
  import { RunnerInsights } from "./runner-insights";
24
24
  import { BusyReconciler } from "./busy-reconciler";
25
+ import { publishCapturedResponse } from "./response-capture-report";
25
26
  import { capsFromEnv, contextStateDirFromEnv, logLevelFromEnv, mcpProxyEnabledFromEnv, orchestratorBaseDirFromEnv, orchestratorUrlFromEnv, registrationTimeoutMsFromEnv, runnerInfoFileFromEnv, runnerLogFileFromEnv, runnerOutboxDirWithInfoFallback, sessionDebugEnabled, tagsFromEnv, workspaceModeFromEnv } from "./config";
26
27
  import {
27
28
  appliedAgentProfileMetadata,
@@ -725,6 +726,7 @@ export class AgentRunner {
725
726
  });
726
727
  await this.options.adapter.deliver(this.process, prepared);
727
728
  for (const message of deliverable) {
729
+ await this.http.recordMessageDeliveryAttempt(message.id, { agentId: this.agentId, status: "delivered" }).catch(() => {});
728
730
  await this.http.markRead(message.id, this.agentId).catch(() => {});
729
731
  if (!taskIdFromMessage(message)) this.claims.finishClaim("message", String(message.id));
730
732
  }
@@ -1336,7 +1338,7 @@ export class AgentRunner {
1336
1338
  const turnId = this.currentTurnId;
1337
1339
  this.stopReasoningTail();
1338
1340
  // Optional correlation for threading + obligation clearing — never a capture gate.
1339
- let replyToMessageId: number | undefined;
1341
+ let replyToMessageId: number | undefined, replyTarget = "user";
1340
1342
  const pendingPrompt = this.pendingPromptMessageId;
1341
1343
  if (pendingPrompt) {
1342
1344
  replyToMessageId = pendingPrompt;
@@ -1344,8 +1346,8 @@ export class AgentRunner {
1344
1346
  } else {
1345
1347
  // Correlation-only (threading + obligation clearing) — the local snapshot is fresh
1346
1348
  // 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;
1349
+ const route = responseCaptureReplyRoute(this.obligationCache.get(), this.currentTurnStartedAt);
1350
+ replyToMessageId = route?.replyToMessageId; replyTarget = route?.to ?? "user";
1349
1351
  }
1350
1352
 
1351
1353
  // Grab and reset the pre-flush cursor before extraction: the cursor may be
@@ -1389,13 +1391,7 @@ export class AgentRunner {
1389
1391
  }
1390
1392
 
1391
1393
  this.sessionLog(`response captured for turn ${turnId ?? "?"} (${body.length} chars${replyToMessageId ? `, replyTo #${replyToMessageId}` : ", no replyTo"})`);
1392
- await this.publishSessionEvent({
1393
- from: this.agentId,
1394
- to: "user",
1395
- body,
1396
- ...(replyToMessageId ? { replyTo: replyToMessageId } : {}),
1397
- session: { type: "response", origin: "provider", ...(turnId ? { turnId } : {}) },
1398
- });
1394
+ await publishCapturedResponse({ publishSessionEvent: (event) => this.publishSessionEvent(event), outbox: this.sessionOutbox, provider: this.options.provider, from: this.agentId, to: replyTarget, body, replyTo: replyToMessageId, session: { type: "response", origin: "provider", ...(turnId ? { turnId } : {}) } });
1399
1395
  // The agent's reply may have cleared an obligation — refresh the snapshot so the next
1400
1396
  // turn-end doesn't re-prompt for a message already answered (#196).
1401
1397
  if (replyToMessageId) this.obligationCache.markDirty();
@@ -1600,24 +1596,19 @@ export class AgentRunner {
1600
1596
  }
1601
1597
  if (event.type === "response") {
1602
1598
  // 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;
1599
+ // response. User obligations still belong to the session mirror; spawner
1600
+ // obligations (#413) route to the spawn-parent with replyTo to clear the debt.
1601
+ let replyToMessageId: number | undefined, replyTarget = "user";
1606
1602
  const pendingPrompt = this.pendingPromptMessageId;
1607
1603
  if (pendingPrompt) {
1608
1604
  replyToMessageId = pendingPrompt;
1609
1605
  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;
1606
+ } else {
1607
+ const route = responseCaptureReplyRoute(this.obligationCache.get(), this.currentTurnStartedAt, { suppressUser: true });
1608
+ if (!route) return; // The session mirror will satisfy the user obligation; don't double-post (#196).
1609
+ replyToMessageId = route.replyToMessageId; replyTarget = route.to;
1613
1610
  }
1614
- await this.publishSessionEvent({
1615
- from: this.agentId,
1616
- to: "user",
1617
- body,
1618
- ...(replyToMessageId ? { replyTo: replyToMessageId } : {}),
1619
- session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}) },
1620
- });
1611
+ await publishCapturedResponse({ publishSessionEvent: (sessionEvent) => this.publishSessionEvent(sessionEvent), outbox: this.sessionOutbox, provider: this.options.provider, from: this.agentId, to: replyTarget, body, replyTo: replyToMessageId, session: { type: "response", origin: event.origin ?? "provider", ...(turnId ? { turnId } : {}) } });
1621
1612
  if (replyToMessageId) this.obligationCache.markDirty();
1622
1613
  return;
1623
1614
  }
@@ -1660,10 +1651,6 @@ export class AgentRunner {
1660
1651
  return false;
1661
1652
  }
1662
1653
 
1663
- private obligationPredatesCurrentTurn(obligation: { createdAt: number }): boolean {
1664
- return this.currentTurnStartedAt === undefined || obligation.createdAt <= this.currentTurnStartedAt;
1665
- }
1666
-
1667
1654
  // Force-clear a stuck provider-turn claim directly. Unlike the idle status path
1668
1655
  // it does NOT depend on a matching claim id (the Stop hook keys busy as
1669
1656
  // provider-turn:provider-turn, but reconciliation has no specific id), and it