agent-relay-runner 0.65.0 → 0.65.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.65.0",
3
+ "version": "0.65.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.65.0",
4
+ "version": "0.65.2",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -31,6 +31,44 @@ relay_post_status_clearing_subagents() {
31
31
  relay_post_status "$1" "${2:-}" "${3:-}" "${4:-}" "${5:-}" "${6:-}" subagent
32
32
  }
33
33
 
34
+ # #502: count the background shells still running at the moment this turn ends.
35
+ # Claude's Stop payload carries a `background_tasks` array (CC v2.1.157+) listing
36
+ # the shells launched with run_in_background that have not yet finished. It is a
37
+ # full live snapshot, so reporting it each turn-end can never wedge: a completed
38
+ # task wakes Claude, the next Stop re-reports the (now smaller/empty) set. Older
39
+ # Claude builds omit the field → count 0 → graceful no-op (no false busy).
40
+ relay_background_task_count() {
41
+ local input="${1:-}"
42
+ [ -z "$input" ] && { printf '0'; return 0; }
43
+ if command -v jq >/dev/null 2>&1; then
44
+ local n
45
+ n="$(printf '%s' "$input" | jq -r '(.background_tasks // []) | length' 2>/dev/null)"
46
+ case "$n" in ''|*[!0-9]*) printf '0' ;; *) printf '%s' "$n" ;; esac
47
+ return 0
48
+ fi
49
+ # jq-less fallback: a non-empty array literal means at least one is running.
50
+ if printf '%s' "$input" | tr -d '[:space:]' | grep -q '"background_tasks":\[{'; then
51
+ printf '1'
52
+ else
53
+ printf '0'
54
+ fi
55
+ }
56
+
57
+ # #502: end the provider turn (idle) clearing any stale subagent work — the
58
+ # critical path (#199) — then report the live background-script snapshot so an
59
+ # agent whose turn ended while a `run_in_background` shell keeps running stays
60
+ # busy instead of falsely reading idle. A single full-snapshot post per turn-end:
61
+ # >0 → busy(background-script); 0 → idle(background-script) which clears any prior.
62
+ relay_post_turn_end_status() {
63
+ local bg_count="${1:-0}"
64
+ relay_post_status_clearing_subagents idle
65
+ case "$bg_count" in
66
+ ''|0|*[!0-9]*) relay_post_status idle background-script background-tasks ;;
67
+ 1) relay_post_status busy background-script background-tasks "1 background task" ;;
68
+ *) relay_post_status busy background-script background-tasks "${bg_count} background tasks" ;;
69
+ esac
70
+ }
71
+
34
72
  # Report a provider usage/rate-limit hold to the runner (#286). The runner turns
35
73
  # this into a `providerState: blocked` (reason rate_limit), posts a chat notice,
36
74
  # and auto-resumes the agent once the window resets. reset_time (unix seconds,
@@ -8,10 +8,16 @@ relay_install_hook_guard stop
8
8
  # exception is the reply-obligation block path, which deliberately keeps the agent
9
9
  # busy to answer — it opts out via the flag before exiting.
10
10
  _relay_clear_idle_on_exit=1
11
- trap '[ "${_relay_clear_idle_on_exit:-0}" = "1" ] && relay_post_status_clearing_subagents idle' EXIT
11
+ # bg_count is read from the payload below and consumed by the trap at exit time
12
+ # (#502): the turn-end status carries the live background-script snapshot so an
13
+ # agent whose turn ended while a run_in_background shell keeps running stays busy
14
+ # instead of falsely reading idle.
15
+ bg_count=0
16
+ trap '[ "${_relay_clear_idle_on_exit:-0}" = "1" ] && relay_post_turn_end_status "${bg_count:-0}"' EXIT
12
17
 
13
18
  payload="$(cat || true)"
14
19
  stop_hook_active="$(relay_json_bool_field stop_hook_active "$payload")"
20
+ bg_count="$(relay_background_task_count "$payload")"
15
21
 
16
22
  # Session-mirror capture is orthogonal to reply-obligation gating (#332): a recursive Stop
17
23
  # (stop_hook_active=true) must NOT suppress capturing this turn's response, or the live mirror
package/src/adapter.ts CHANGED
@@ -4,7 +4,7 @@ import type { SessionEvent } from "./session-insights";
4
4
  import { messageBodyMaxCharsFromEnv } from "./config";
5
5
 
6
6
  export type SemanticStatus = "idle" | "busy" | "offline" | "error";
7
- type ProviderWorkKind = "provider-turn" | "subagent";
7
+ type ProviderWorkKind = "provider-turn" | "subagent" | "background-script";
8
8
 
9
9
  export interface ProviderStatusEvent {
10
10
  status: SemanticStatus;
@@ -1,5 +1,5 @@
1
1
  type ClaimKind = "message" | "task" | "command";
2
- type RunnerWorkKind = "provider-turn" | "subagent";
2
+ type RunnerWorkKind = "provider-turn" | "subagent" | "background-script";
3
3
  type RunnerBusyReason = RunnerWorkKind | ClaimKind;
4
4
  type RunnerSemanticStatus = "idle" | "busy" | "offline" | "error";
5
5
 
@@ -439,12 +439,12 @@ async function handleStatus(req: Request, options: ControlServerOptions): Promis
439
439
  return Response.json({ error: "status must be idle, busy, offline, or error" }, { status: 400 });
440
440
  }
441
441
  const reason = body?.reason;
442
- if (reason !== undefined && reason !== "provider-turn" && reason !== "subagent") {
443
- return Response.json({ error: "reason must be provider-turn or subagent" }, { status: 400 });
442
+ if (reason !== undefined && reason !== "provider-turn" && reason !== "subagent" && reason !== "background-script") {
443
+ return Response.json({ error: "reason must be provider-turn, subagent, or background-script" }, { status: 400 });
444
444
  }
445
- const clear = Array.isArray(body?.clear)
446
- ? body.clear.filter((item): item is "provider-turn" | "subagent" => item === "provider-turn" || item === "subagent")
447
- : undefined;
445
+ const isWorkKind = (item: unknown): item is "provider-turn" | "subagent" | "background-script" =>
446
+ item === "provider-turn" || item === "subagent" || item === "background-script";
447
+ const clear = Array.isArray(body?.clear) ? body.clear.filter(isWorkKind) : undefined;
448
448
  const timeline = statusTimelineEvent(body);
449
449
  const update: ProviderStatusEvent = {
450
450
  status,
@@ -919,6 +919,7 @@ export class AgentRunner {
919
919
  this.claims.clearTerminalStatus();
920
920
  this.claims.clearWorkKind("provider-turn");
921
921
  this.claims.clearWorkKind("subagent");
922
+ this.claims.clearWorkKind("background-script");
922
923
  if (this.stopped) return;
923
924
  this.process = await this.spawnProvider();
924
925
  this.processStartedAt = Date.now();
@@ -997,7 +998,7 @@ export class AgentRunner {
997
998
  status,
998
999
  reason: "provider-turn",
999
1000
  id: `provider-exit-${this.providerSessionId}`,
1000
- clear: ["provider-turn", "subagent"],
1001
+ clear: ["provider-turn", "subagent", "background-script"],
1001
1002
  });
1002
1003
  return;
1003
1004
  }