summaryrefslogtreecommitdiff
path: root/subagents/jobs.lua
diff options
context:
space:
mode:
Diffstat (limited to 'subagents/jobs.lua')
-rw-r--r--subagents/jobs.lua54
1 files changed, 43 insertions, 11 deletions
diff --git a/subagents/jobs.lua b/subagents/jobs.lua
index a8cde6d..bab8f30 100644
--- a/subagents/jobs.lua
+++ b/subagents/jobs.lua
@@ -22,6 +22,14 @@
-- A settled result is read and cached the moment it appears, because
-- `job:close()` frees it. Jobs are otherwise left open until close_all() ends
-- the turn, so a result can still be read after its coroutine resumed.
+--
+-- `job:close()` also JOINS the pump, and a pump parked in a tool batch is
+-- waiting on the owner thread to come back to the uv loop — the very thread
+-- every entry point here runs on. Closing an unsettled job would therefore
+-- deadlock, so nothing does: only settle() closes a job, once its pump has
+-- exited. close_all() asks what is still running to cancel, marks it
+-- close-on-settle, and lets the wake byte that carries the settle drive the
+-- close.
local ok_uv, uv = pcall(require, "luv")
@@ -75,6 +83,16 @@ local function close_pipe(handle)
end
end
+-- Free the binding job. Safe only after the result is cached (close() frees it)
+-- and the pump has exited (close() joins it), which together mean: after settle.
+local function close_job(handle)
+ if handle.job then
+ pcall(handle.job.close, handle.job)
+ handle.job = nil
+ end
+ handle.state = "closed"
+end
+
local function drain_pipe(handle)
if not handle.fds then
return
@@ -114,6 +132,11 @@ local function settle(handle, raw)
end
handle.state = "settled"
close_pipe(handle)
+ -- The turn ended while this one was still running: close_all() could not
+ -- join the pump then, but the pump is gone now, so the join is free.
+ if handle.close_on_settle then
+ close_job(handle)
+ end
pump_queue()
end
@@ -283,13 +306,15 @@ function M.start(spec)
return handle
end
--- True while a child with this id has a turn in flight.
+-- True while a child with this id has a turn in flight. A child cancelled by
+-- close_all is not one: its turn belongs to the turn that ended, and the next
+-- one must not be refused because that pump has not finished exiting yet.
function M.active(id)
if id == nil then
return false
end
for _, handle in ipairs(live) do
- if handle.id == id and handle.settled == nil then
+ if handle.id == id and handle.settled == nil and handle.state ~= "closing" then
return true
end
end
@@ -353,7 +378,7 @@ local function can_park(handles)
return false
end
for _, handle in ipairs(handles) do
- if handle.state == "running" and handle.poll == nil then
+ if handle.job ~= nil and handle.settled == nil and handle.poll == nil then
return false
end
end
@@ -404,23 +429,30 @@ function M.cancel_all()
end
end
--- The turn is over: join every pump and drop the state. Requests go out first
--- so the joins overlap instead of running one child's teardown at a time.
+-- The turn is over: cancel every child and drop the state. Never blocks — a
+-- child whose pump is still up is left open and closed by its own settle, so
+-- the owner thread stays free for the tool batches those pumps are waiting on.
function M.close_all()
for _, handle in ipairs(live) do
if handle.job and handle.settled == nil then
pcall(handle.job.request_cancel, handle.job)
end
end
+
+ -- Whatever is still unsettled stays live so its poll (and any fallback
+ -- drain) still reaches it; the next close_all sweeps up what settled since.
+ local closing = {}
for _, handle in ipairs(live) do
- if handle.job then
- pcall(handle.job.close, handle.job)
- handle.job = nil
+ if handle.job and handle.settled == nil then
+ handle.close_on_settle = true
+ handle.state = "closing"
+ closing[#closing + 1] = handle
+ else
+ close_pipe(handle)
+ close_job(handle)
end
- close_pipe(handle)
- handle.state = "closed"
end
- live, queued, waiters = {}, {}, {}
+ live, queued, waiters = closing, {}, {}
running = 0
end