agent-relay-runner 0.14.0 → 0.15.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
|
@@ -96,7 +96,10 @@ relay_pending_reply_stop_decision() {
|
|
|
96
96
|
local port="${AGENT_RELAY_RUNNER_PORT:-}"
|
|
97
97
|
[ -z "$port" ] && return 0
|
|
98
98
|
local response
|
|
99
|
-
|
|
99
|
+
# --max-time guards the Claude Stop hook's 5s budget: a slow runner/server (e.g. an
|
|
100
|
+
# un-indexed obligation query) must never block past the timeout, or Claude SIGKILLs
|
|
101
|
+
# the hook before it clears the turn -> stuck "busy" (#199). On timeout: no block.
|
|
102
|
+
response="$(curl -fsS --max-time 2 "http://127.0.0.1:${port}/reply-obligations/claude-stop" 2>/dev/null || true)"
|
|
100
103
|
case "$response" in
|
|
101
104
|
*'"decision":"block"'*|*'"decision": "block"'*) ;;
|
|
102
105
|
*) return 0 ;;
|
|
@@ -7,7 +7,9 @@ stop_hook_active="$(relay_json_bool_field stop_hook_active "$payload")"
|
|
|
7
7
|
if [ "$stop_hook_active" != "true" ]; then
|
|
8
8
|
last_assistant_msg="$(echo "$payload" | jq -c '.last_assistant_message // empty' 2>/dev/null || true)"
|
|
9
9
|
relay_post_session_turn "$(relay_json_string_field transcript_path "$payload")" "$last_assistant_msg"
|
|
10
|
-
|
|
10
|
+
# `|| true`: under `set -e`, a non-zero from the obligation check must never abort
|
|
11
|
+
# the hook before the idle-clear below — clearing the turn is the critical path (#199).
|
|
12
|
+
stop_decision="$(relay_pending_reply_stop_decision || true)"
|
|
11
13
|
if [ -n "$stop_decision" ]; then
|
|
12
14
|
printf '%s\n' "$stop_decision"
|
|
13
15
|
exit 0
|
package/src/runner.ts
CHANGED
|
@@ -76,12 +76,18 @@ const LOG_TAIL_BYTES = 128 * 1024;
|
|
|
76
76
|
const PROMPT_ECHO_DEDUP_MS = 30_000;
|
|
77
77
|
// Busy reconciler: a conservative LAST-RESORT backstop for a turn that ended
|
|
78
78
|
// without the provider's Stop hook clearing busy (e.g. ESC straight into the web
|
|
79
|
-
// terminal). It must never fire during a live turn, so it
|
|
80
|
-
// after it has actually observed the provider busy, and (b) requires a long,
|
|
79
|
+
// terminal). It must never fire during a live turn, so it requires a long,
|
|
81
80
|
// unbroken idle streak — an active turn shows its working spinner well within
|
|
82
81
|
// this window, which resets the streak. ~32s of uninterrupted idle = really done.
|
|
83
82
|
const BUSY_RECONCILE_POLL_MS = 4_000;
|
|
84
83
|
const BUSY_RECONCILE_IDLE_CONFIRM = 8;
|
|
84
|
+
// When the reconciler never observed the provider busy this turn (a turn faster
|
|
85
|
+
// than the 4s poll — common for short voice/autosend replies), it can't trust a
|
|
86
|
+
// quick idle the way it does after seeing the spinner. But refusing forever wedged
|
|
87
|
+
// fast turns in "busy" when the Stop hook's idle was lost (#199). So we still
|
|
88
|
+
// force-clear, just after a much longer unbroken-idle window — an active turn would
|
|
89
|
+
// have flashed its spinner into at least one of these probes and reset the streak.
|
|
90
|
+
const BUSY_RECONCILE_IDLE_CONFIRM_NO_BUSY = 15;
|
|
85
91
|
// After a dashboard interrupt, give the provider a moment to drop out of its turn,
|
|
86
92
|
// then reconcile immediately so the user sees "stopped" without waiting for the backstop.
|
|
87
93
|
const INTERRUPT_RECONCILE_DELAY_MS = 1_500;
|
|
@@ -1146,18 +1152,21 @@ export class AgentRunner {
|
|
|
1146
1152
|
let activity: "busy" | "idle" | "unknown";
|
|
1147
1153
|
try { activity = await this.options.adapter.probeActivity(this.process); } catch { return; }
|
|
1148
1154
|
if (activity === "busy") this.busyReconcileSawBusy = true;
|
|
1149
|
-
// Reset the streak on anything that isn't a confident idle
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
if (activity !== "idle") this.busyReconcileIdleStreak = 0;
|
|
1155
|
+
// Reset the streak on anything that isn't a confident idle.
|
|
1156
|
+
if (activity !== "idle") {
|
|
1157
|
+
this.busyReconcileIdleStreak = 0;
|
|
1153
1158
|
this.sessionDebug(`reconcile probe=${activity} sawBusy=${this.busyReconcileSawBusy} streak=${this.busyReconcileIdleStreak}`);
|
|
1154
1159
|
return;
|
|
1155
1160
|
}
|
|
1156
1161
|
this.busyReconcileIdleStreak += 1;
|
|
1157
|
-
|
|
1158
|
-
|
|
1162
|
+
// Confirm faster once we've seen the spinner this turn; otherwise demand a much
|
|
1163
|
+
// longer all-idle window before trusting it (rescues fast turns without
|
|
1164
|
+
// false-clearing a live turn that simply hasn't flashed busy into a probe yet).
|
|
1165
|
+
const confirm = this.busyReconcileSawBusy ? BUSY_RECONCILE_IDLE_CONFIRM : BUSY_RECONCILE_IDLE_CONFIRM_NO_BUSY;
|
|
1166
|
+
this.sessionDebug(`reconcile probe=idle sawBusy=${this.busyReconcileSawBusy} streak=${this.busyReconcileIdleStreak}/${confirm}`);
|
|
1167
|
+
if (this.busyReconcileIdleStreak < confirm) return;
|
|
1159
1168
|
this.disarmBusyReconciler();
|
|
1160
|
-
this.forceClearProviderTurn("backstop reconciler");
|
|
1169
|
+
this.forceClearProviderTurn(this.busyReconcileSawBusy ? "backstop reconciler" : "backstop reconciler (no-busy-observed)");
|
|
1161
1170
|
}
|
|
1162
1171
|
|
|
1163
1172
|
// Force-clear a stuck provider-turn claim directly. Unlike the idle status path
|