agent-relay-runner 0.63.1 → 0.63.2

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.63.1",
3
+ "version": "0.63.2",
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.63.1",
4
+ "version": "0.63.2",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -202,6 +202,40 @@ export function extractFinalAssistantMessage(jsonl: string): string {
202
202
  return lastText.trim();
203
203
  }
204
204
 
205
+ /**
206
+ * Like extractFinalAssistantMessage but skips the first `afterEntry` valid JSON
207
+ * entries. Used when a PreToolUse pre-flush already emitted the text before a
208
+ * blocking control, so final-mode Stop capture only sees later assistant text.
209
+ */
210
+ export function extractFinalAssistantMessageAfterEntry(jsonl: string, afterEntry: number): string {
211
+ const lines = jsonl.split("\n");
212
+ let lastText = "";
213
+ let pastLastUserPrompt = afterEntry > 0;
214
+ let entryIndex = 0;
215
+ for (const line of lines) {
216
+ const trimmed = line.trim();
217
+ if (!trimmed) continue;
218
+ let entry: TranscriptEntry;
219
+ try {
220
+ entry = JSON.parse(trimmed) as TranscriptEntry;
221
+ } catch {
222
+ continue;
223
+ }
224
+ entryIndex++;
225
+ if (isSidechainEntry(entry)) continue;
226
+ if (entryIndex <= afterEntry) continue;
227
+ if (isRealUserPrompt(entry)) {
228
+ pastLastUserPrompt = true;
229
+ lastText = "";
230
+ continue;
231
+ }
232
+ if (!pastLastUserPrompt) continue;
233
+ const text = assistantText(entry);
234
+ if (text) lastText = text;
235
+ }
236
+ return lastText.trim();
237
+ }
238
+
205
239
  /**
206
240
  * Extract text from the `last_assistant_message` field in the Stop hook
207
241
  * payload. This is the content of the final assistant message — either a plain
@@ -12,7 +12,7 @@ import { ClaimTracker } from "./claim-tracker";
12
12
  import { startControlServer, type ControlServer } from "./control-server";
13
13
  import { ReplyObligationCache, obligationRequiresExplicitReply } from "./reply-obligation-cache";
14
14
  import { Outbox, type OutboxRecord } from "./outbox";
15
- import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
15
+ import { extractLastAssistantTurn, extractLastAssistantTurnAfterEntry, countTranscriptEntries, extractFinalAssistantMessage, extractFinalAssistantMessageAfterEntry, extractHookAssistantMessage, extractLatestTurnSteps, stepDedupKeys, transcriptLooksComplete } from "./adapters/claude-transcript";
16
16
  import { profileUsesHostProviderGlobals } from "./profile-home";
17
17
  import { RELAY_MCP_TOKEN_ENV, relayMcpEndpoint } from "./relay-mcp";
18
18
  import { RelayMcpProxy } from "./relay-mcp-proxy";
@@ -1306,10 +1306,9 @@ export class AgentRunner {
1306
1306
  // no relay message) are mirrored too. A reply obligation, when present, is still
1307
1307
  // used as replyTo so the Stop hook stops nagging the agent to /reply.
1308
1308
  //
1309
- // isPreFlush: true (#435) — mid-turn call from a PreToolUse hook (AskUserQuestion /
1310
- // ExitPlanMode / permission). Emits any un-mirrored narrative from the transcript
1311
- // tail immediately, before the interactive form appears in chat, then updates the
1312
- // entry-count cursor so the eventual Stop-hook call (full mode) skips it.
1309
+ // isPreFlush: true (#435/#499) — mid-turn PreToolUse boundary. Drain the live
1310
+ // tail and force-flush session messages before showing the blocking control;
1311
+ // the entry cursor lets Stop capture skip already-emitted text.
1313
1312
  private async publishSessionTurn(input: { transcriptPath: string; lastAssistantMessage?: unknown; isPreFlush?: boolean }): Promise<void> {
1314
1313
  if (input.transcriptPath) this.lastTranscriptPath = input.transcriptPath;
1315
1314
 
@@ -1321,15 +1320,12 @@ export class AgentRunner {
1321
1320
  const body = extractLastAssistantTurnAfterEntry(jsonl, this.narrativeFlushEntryCount);
1322
1321
  await this.drainReasoningTail();
1323
1322
  this.narrativeFlushEntryCount = countTranscriptEntries(jsonl);
1324
- if (!body || this.preFlushBodyAlreadyMirroredByReasoningTail(jsonl, body)) return;
1325
- const turnId = this.currentTurnId;
1326
- this.sessionLog(`pre-flush narrative for turn ${turnId ?? "?"} (${body.length} chars)`);
1327
- await this.publishSessionEvent({
1328
- from: this.agentId,
1329
- to: "user",
1330
- body,
1331
- session: { type: "response", origin: "provider", ...(turnId ? { turnId } : {}) },
1332
- });
1323
+ if (body && !this.preFlushBodyAlreadyMirroredByReasoningTail(jsonl, body)) {
1324
+ const turnId = this.currentTurnId;
1325
+ this.sessionLog(`pre-flush narrative for turn ${turnId ?? "?"} (${body.length} chars)`);
1326
+ await this.publishSessionEvent({ from: this.agentId, to: "user", body, session: { type: "response", origin: "provider", ...(turnId ? { turnId } : {}) } });
1327
+ }
1328
+ await this.sessionOutbox.flush(2_000).then((ok) => { if (!ok) this.sessionLog(`pre-flush session outbox incomplete before blocking control (${this.sessionOutbox.pendingCount()} pending)`); }, (error) => this.sessionLog(`pre-flush session outbox failed before blocking control: ${errMessage(error)}`));
1333
1329
  return;
1334
1330
  }
1335
1331
 
@@ -1368,11 +1364,10 @@ export class AgentRunner {
1368
1364
  try { jsonl = await readFile(input.transcriptPath, "utf8"); } catch { break; }
1369
1365
  }
1370
1366
  if (!transcriptLooksComplete(jsonl)) continue;
1371
- // Full mode: if a pre-flush already emitted entries 1..flushCount, only
1372
- // collect text from entries after that mark to avoid double-emit (#435).
1367
+ // If a pre-flush already emitted entries 1..flushCount, resume after that mark to avoid double-emit (#435/#499).
1373
1368
  const extract = this.options.providerConfig.chatCaptureMode === "full"
1374
1369
  ? (j: string) => (flushCount > 0 ? extractLastAssistantTurnAfterEntry(j, flushCount) : extractLastAssistantTurn(j))
1375
- : extractFinalAssistantMessage;
1370
+ : (j: string) => (flushCount > 0 ? extractFinalAssistantMessageAfterEntry(j, flushCount) : extractFinalAssistantMessage(j));
1376
1371
  body = extract(jsonl);
1377
1372
  if (body) break;
1378
1373
  }