agent-relay-server 0.121.2 → 0.121.3

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/docs/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Relay API",
5
- "version": "0.121.2",
5
+ "version": "0.121.3",
6
6
  "description": "Real-time message bus for inter-agent communication. Agent-first: this spec is designed for machine consumption — agents can self-discover the full API surface via GET /api/spec.",
7
7
  "license": {
8
8
  "name": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.121.2",
3
+ "version": "0.121.3",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -37,7 +37,7 @@
37
37
  "CONTRIBUTING.md"
38
38
  ],
39
39
  "dependencies": {
40
- "agent-relay-channels-host": "0.121.2",
40
+ "agent-relay-channels-host": "0.121.3",
41
41
  "agent-relay-providers": "0.104.3",
42
42
  "agent-relay-sdk": "0.2.113",
43
43
  "ajv": "^8.20.0"
@@ -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.121.2",
4
+ "version": "0.121.3",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
package/src/bus.ts CHANGED
@@ -174,7 +174,12 @@ function handleFrame(ws: BusWebSocket, frame: ReturnType<typeof validateClientFr
174
174
  // frame. Flush the held report-up on THIS edge too, else `before.status` is already
175
175
  // non-busy at the next status frame → the flush never fires → the child deadlocks idle
176
176
  // with its verdict held and the coordinator waits forever.
177
- if (current?.status === "busy" && frame.payload.status !== "busy") flushIdleLineageReport(conn.agentId);
177
+ // #1037 (re-regression) gate on a genuine at-rest `idle`, NOT merely "not busy". A
178
+ // real turn-end always reports `idle`; a transient non-idle status (a stray `online`,
179
+ // a malformed frame) is NOT the end of the child's work and must not flush its held
180
+ // mid-work capture upstream. Terminal exit edges (WS-close/stale sweep) keep their
181
+ // unconditional flush for the #1040 delivery guarantee — this only tightens the LIVE edge.
182
+ if (current?.status === "busy" && frame.payload.status === "idle") flushIdleLineageReport(conn.agentId);
178
183
  }
179
184
  emitAgentStatusEvent(conn.agentId);
180
185
  // #416 — flush queued messages if the agent transitioned from unavailable to available.
@@ -220,8 +225,11 @@ function handleFrame(ws: BusWebSocket, frame: ReturnType<typeof validateClientFr
220
225
  // #237 stop-hook → instant ⚪↔🟡 changes detection on the turn-end edge.
221
226
  probeWorkspaceOnTurnEnd(before, after);
222
227
  // #1037 — the same busy→idle turn-end edge flushes a quiet child's deferred turn-final
223
- // report-up to its spawner (held back while the child was mid-work).
224
- if (before?.status === "busy" && after && after.status !== "busy") flushIdleLineageReport(conn.agentId);
228
+ // report-up to its spawner (held back while the child was mid-work). Gate on a genuine
229
+ // at-rest `idle`: a real turn-end reports `idle`, whereas a transient non-idle status
230
+ // (stray `online`, reconnect) is not the child going to rest and must not flush its held
231
+ // mid-work capture. Terminal exit edges keep the #1040 unconditional flush.
232
+ if (before?.status === "busy" && after && after.status === "idle") flushIdleLineageReport(conn.agentId);
225
233
  // #633 — silent clean-exit: a runner that stays alive after its provider terminally exited
226
234
  // marks the offline edge with `terminalProviderExit`; the owning service turns that into a
227
235
  // terminal event + #636 diagnosis (and clears the hold on a genuine resume).
@@ -347,6 +347,40 @@ function childIsBusy(child: AgentCard): boolean {
347
347
  return child.status === "busy";
348
348
  }
349
349
 
350
+ // #1037 (re-regression) — the report-up must reach the spawner as a CONCISE result, not the full
351
+ // turn transcript. In `chatCaptureMode: "full"` (and for rambling multi-block recaps) the captured
352
+ // turn-final body is the ENTIRE turn's narration — every "Let me read X… now Y… now Z…" segment
353
+ // joined by the canonical `\n\n` block boundary (runner extractLastAssistantTurn). Forwarding it
354
+ // verbatim floods the coordinator with a wall of mid-work text (the exact bug #1037 keeps
355
+ // re-shipping). Idle-gating alone can't help: this is a genuine turn-final at a genuine idle, so
356
+ // the payload itself must be distilled. Short bodies pass through untouched (concise finals are
357
+ // unaffected); a long body is trimmed to its trailing result block(s) — a turn's conclusion lives
358
+ // at its end — with an elision marker so the coordinator knows narration was dropped. The actual
359
+ // result is never lost. Pure function of the body → deterministic, so re-promotion (the idle flush)
360
+ // yields the identical distilled body + idempotency key and dedups cleanly.
361
+ const REPORT_UP_DISTILL_THRESHOLD = 800;
362
+ const REPORT_UP_TAIL_MAX = 800;
363
+ const REPORT_UP_ELISION = "[…mid-work narration trimmed by report-up…]";
364
+
365
+ function distillLineageReportBody(body: string): string {
366
+ const trimmed = body.trim();
367
+ if (trimmed.length <= REPORT_UP_DISTILL_THRESHOLD) return body;
368
+ const blocks = trimmed.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
369
+ const kept: string[] = [];
370
+ let size = 0;
371
+ for (let i = blocks.length - 1; i >= 0; i--) {
372
+ const block = blocks[i]!;
373
+ if (kept.length > 0 && size + block.length > REPORT_UP_TAIL_MAX) break;
374
+ kept.unshift(block);
375
+ size += block.length + 2;
376
+ }
377
+ let tail = kept.join("\n\n");
378
+ // A single oversized trailing block (unsegmented narration) — hard-cap to its own tail.
379
+ if (tail.length > REPORT_UP_TAIL_MAX) tail = tail.slice(tail.length - REPORT_UP_TAIL_MAX).replace(/^\S*\s/, "").trimStart();
380
+ if (tail.length >= trimmed.length) return body; // nothing meaningful trimmed
381
+ return `${REPORT_UP_ELISION}\n\n${tail}`;
382
+ }
383
+
350
384
  // #1037 — a quiet child's turn-final response that arrives while the child is still busy is a
351
385
  // mid-task yield, not the end of its work. Instead of shipping it upstream (mid-work narration) or
352
386
  // dropping it (a genuine last turn whose idle status simply hasn't landed yet), we HOLD the latest
@@ -493,7 +527,9 @@ export function promoteLineageCapturedResponse(message: Message): { message: Mes
493
527
  from: message.from,
494
528
  to: parent,
495
529
  kind: "chat",
496
- body: message.body,
530
+ // #1037 — distill the turn-final report-up to a concise result; verbose child-stream captures
531
+ // (an explicit subscriber that asked for the raw stream) are forwarded untouched.
532
+ body: stream === "responses" ? distillLineageReportBody(message.body) : message.body,
497
533
  replyExpected: false,
498
534
  idempotencyKey,
499
535
  payload: stream === "responses"