Skip to content

Commit 90d0591

Browse files
authored
Merge branch 'master' into fix/tool-call-stream-cpu
2 parents e289c35 + fd62958 commit 90d0591

4 files changed

Lines changed: 95 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44

55
- Improve CPU usage while streaming tool-call arguments by reusing the prompt's tool list.
66

7+
## 0.133.4
8+
9+
- Bugfix: stop the infinite "Cannot run program 'kill'" liveness-probe log loop for sandboxed environments.
10+
711
## 0.133.3
812

913
- Add unit and integration tests covering parent↔subagent end-to-end communication so regressions like the v0.133.1 spawn-agent breakage are caught automatically.

resources/ECA_VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.133.3
1+
0.133.4

src/eca/server.clj

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@
1313
[eca.remote.server :as remote.server]
1414
[eca.shared :as shared :refer [assoc-some]]
1515
[jsonrpc4clj.io-server :as io-server]
16-
[jsonrpc4clj.liveness-probe :as liveness-probe]
1716
[jsonrpc4clj.server :as jsonrpc.server]
18-
[promesa.core :as p]))
17+
[promesa.core :as p])
18+
(:import
19+
[java.lang ProcessHandle]
20+
[java.util Optional]
21+
[java.util.concurrent CompletableFuture]
22+
[java.util.function BiConsumer]))
1923

2024
(set! *warn-on-reflection* true)
2125

@@ -56,9 +60,39 @@
5660
(catch Throwable e#
5761
(logger/error e# "[server] Error in async notification handler")))))
5862

63+
(defn ^:private start-liveness-probe!
64+
"Monitor parent process `ppid`; invoke `on-exit` once when the parent
65+
disappears. Event-driven via `ProcessHandle.onExit` so we don't shell out
66+
to `kill -0` (which loops forever when the binary is missing)."
67+
[ppid on-exit]
68+
(let [fire! (fn []
69+
(try (on-exit)
70+
(catch Throwable t
71+
(logger/error t "[server] Liveness probe - on-exit threw"))))]
72+
(try
73+
(let [opt ^Optional (ProcessHandle/of (long ppid))]
74+
(if (.isPresent opt)
75+
(let [^ProcessHandle handle (.get opt)
76+
^CompletableFuture fut (.onExit handle)]
77+
(.whenComplete fut
78+
(reify BiConsumer
79+
(accept [_ _result ex]
80+
(if (some? ex)
81+
(logger/warn ex "[server] Liveness probe - failed waiting for parent" ppid "- monitor disabled")
82+
(do
83+
(logger/info "[server] Liveness probe - parent" ppid "exited - exiting server")
84+
(fire!))))))
85+
nil)
86+
(do
87+
(logger/info "[server] Liveness probe - parent" ppid "is not running - exiting server")
88+
(fire!))))
89+
(catch Throwable t
90+
(logger/error t "[server] Liveness probe - failed to start; parent monitoring disabled")
91+
nil))))
92+
5993
(defmethod jsonrpc.server/receive-request "initialize" [_ {:keys [server] :as components} params]
6094
(when-let [parent-process-id (:process-id params)]
61-
(liveness-probe/start! parent-process-id log-wrapper-fn #(exit server)))
95+
(start-liveness-probe! parent-process-id #(exit server)))
6296
(handlers/initialize components params))
6397

6498
(defmethod jsonrpc.server/receive-notification "initialized" [_ components _params]

test/eca/server_test.clj

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
(ns eca.server-test
2+
(:require
3+
[babashka.process :as p]
4+
[clojure.test :refer [deftest is testing]]
5+
[eca.server :as server]
6+
[eca.test-helper :as h]))
7+
8+
(defn ^:private spawn-blocking-process []
9+
;; Long-running child whose pid we own. `sleep 600` is fine on Linux/macOS;
10+
;; subprocess-based tests are skipped on Windows.
11+
(p/process {:cmd ["sleep" "600"]
12+
:shutdown p/destroy-tree}))
13+
14+
(defn ^:private pid-of [proc]
15+
(.pid ^java.lang.Process (:proc proc)))
16+
17+
(deftest start-liveness-probe-with-missing-pid-test
18+
(testing "an absent parent triggers on-exit at start"
19+
(let [exited? (promise)]
20+
(#'server/start-liveness-probe! Long/MAX_VALUE
21+
#(deliver exited? true))
22+
(is (= true (deref exited? 200 :timeout))
23+
"on-exit must fire when the parent is not present"))))
24+
25+
(deftest start-liveness-probe-survives-on-exit-throwing-test
26+
(testing "an exception in on-exit does not propagate out of start!"
27+
(is (nil? (#'server/start-liveness-probe! Long/MAX_VALUE
28+
#(throw (ex-info "boom" {}))))
29+
"start! must not raise even when on-exit throws")))
30+
31+
(deftest start-liveness-probe-with-alive-parent-test
32+
(when-not h/windows?
33+
(testing "an alive parent does not trigger on-exit"
34+
(let [proc (spawn-blocking-process)
35+
exited? (promise)]
36+
(try
37+
(#'server/start-liveness-probe! (pid-of proc)
38+
#(deliver exited? true))
39+
(is (= :still-alive (deref exited? 100 :still-alive))
40+
"on-exit must not fire while the parent is alive")
41+
(finally
42+
(p/destroy-tree proc)))))))
43+
44+
(deftest start-liveness-probe-fires-when-parent-dies-test
45+
(when-not h/windows?
46+
(testing "killing the parent triggers on-exit"
47+
(let [proc (spawn-blocking-process)
48+
exited? (promise)]
49+
(#'server/start-liveness-probe! (pid-of proc)
50+
#(deliver exited? :fired))
51+
(p/destroy-tree proc)
52+
(is (= :fired (deref exited? 2000 :timeout))
53+
"on-exit must fire shortly after the parent dies")))))

0 commit comments

Comments
 (0)