From 784d5d7ce4a6b7bdf4e0ec401be61699512a7288 Mon Sep 17 00:00:00 2001 From: Steve Dudenhoeffer Date: Sat, 27 Jun 2026 18:33:41 -0400 Subject: [PATCH] run: PostRun detached ctx + panic-isolated Cleanup (gadfly #12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two convergent gadfly refinements on the PostRun wiring: - PostRun now runs on detach(ctx), not the caller's ctx — a finished/cancelled caller no longer aborts artifact production (3-model: glm-5.2/minimax/deepseek). - Cleanup is panic-isolated via safeCleanup (recover+log), matching runPostRun, so a misbehaving teardown can't clobber an otherwise-successful run (deepseek). Co-Authored-By: Claude Opus 4.8 (1M context) --- run/executor.go | 6 ++++-- run/session.go | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/run/executor.go b/run/executor.go index 32f76ad..6a57330 100644 --- a/run/executor.go +++ b/run/executor.go @@ -218,7 +218,7 @@ func (e *Executor) Run(ctx context.Context, ra RunnableAgent, inv tool.Invocatio if inv.SessionToolFactory != nil { st := inv.SessionToolFactory(&runSession{mailbox: mailbox}) if st.Cleanup != nil { - defer st.Cleanup() + defer safeCleanup(st.Cleanup) // panic-isolated, like runPostRun } for _, t := range st.Tools { if err := toolbox.Add(t); err != nil { @@ -338,7 +338,9 @@ func (e *Executor) Run(ctx context.Context, ra RunnableAgent, inv tool.Invocatio if runRes != nil { transcript = runRes.Messages } - res.PostRunResult = runPostRun(ctx, postRun, transcript, res.Output, runErr) + // Detach from the caller's ctx: a finished/cancelled caller must not abort + // artifact production (the hook owns its own bounding, per its contract). + res.PostRunResult = runPostRun(detach(ctx), postRun, transcript, res.Output, runErr) } e.finishAudit(ctx, rec, status, res, started, runErr) diff --git a/run/session.go b/run/session.go index 21b7a04..a756da4 100644 --- a/run/session.go +++ b/run/session.go @@ -74,3 +74,15 @@ func (s *runSession) AttachImages(text string, images ...llm.ImagePart) { } s.mailbox.push(llm.UserParts(parts...)) } + +// safeCleanup runs a SessionTools.Cleanup with panic isolation, so a misbehaving +// teardown (temp-dir removal, handle close) can't clobber an otherwise-successful +// run via the executor's top-level recover. +func safeCleanup(fn func()) { + defer func() { + if r := recover(); r != nil { + slog.Error("run: session Cleanup panicked", "panic", r) + } + }() + fn() +}