From d17b5df7dc3c16ce54d35fe868674d50a4e7349a Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 29 Jul 2026 22:33:03 +0200 Subject: [PATCH] context: thread daemon ctx through sessions to agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Daemon creates a root context, cancelled on SIGTERM. Sessions derive their context from it. Agent turns derive from session ctx. Tools get the turn ctx. Signal propagation: - SIGTERM → daemonCancel() → all session ctxs cancelled → agent turns exit - User interrupt → agent.Interrupt() (SIGINT equivalent, turn-level only) - Session kill → sess.Cancel() → that session's agents stop Previously sessions used context.Background() — daemon shutdown had no automatic propagation to running agents. --- main.go | 5 +++++ main_test.go | 5 +++++ mgr/create.go | 4 ++-- mgr/manager.go | 1 + mgr/persist.go | 4 ++-- 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 7b685f5..5e77998 100644 --- a/main.go +++ b/main.go @@ -178,7 +178,11 @@ func runServer(sockPath string) { // Elevate broker (initialized after manager; closures capture the pointer). var elevateBroker *elevate.Broker + daemonCtx, daemonCancel := context.WithCancel(context.Background()) + defer daemonCancel() + mgr := mgr.NewManager(mgr.ManagerConfig{ + Ctx: daemonCtx, ToolRegistry: toolRegistry, SkillsRegistry: skillsRegistry, AgentsDir: agentsDirs[0], @@ -332,6 +336,7 @@ func runServer(sockPath string) { <-sigChan fmt.Println("shutting down") + daemonCancel() // signal all sessions via context propagation if dbusAdapter != nil { dbusAdapter.Close() } diff --git a/main_test.go b/main_test.go index 199de30..c2cbcd5 100644 --- a/main_test.go +++ b/main_test.go @@ -101,6 +101,7 @@ func newTestSessionManager(t *testing.T) *mgr.Manager { t.Helper() sink := testSink() return mgr.NewManager(mgr.ManagerConfig{ + Ctx: context.Background(), Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, @@ -112,6 +113,7 @@ func newTestSessionManagerWithCore(t *testing.T) *mgr.Manager { t.Helper() sink := testSink() return mgr.NewManager(mgr.ManagerConfig{ + Ctx: context.Background(), Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, @@ -347,6 +349,7 @@ func TestSessionManagerShutdown(t *testing.T) { func TestSessionManagerRename(t *testing.T) { sink := testSink() s := mgr.NewManager(mgr.ManagerConfig{ + Ctx: context.Background(), Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return nil, nil }, @@ -1145,6 +1148,7 @@ func TestSessionManagerCreateSessionEnvExpansion(t *testing.T) { t.Setenv("TEST_CWD", "/expanded/path") sink := testSink() s := mgr.NewManager(mgr.ManagerConfig{ + Ctx: context.Background(), Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, @@ -1169,6 +1173,7 @@ func TestSessionManagerCreateSessionEnvExpansion(t *testing.T) { func TestSessionManagerCreateSessionTildeExpansion(t *testing.T) { sink := testSink() s := mgr.NewManager(mgr.ManagerConfig{ + Ctx: context.Background(), Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, diff --git a/mgr/create.go b/mgr/create.go index f989f45..16543fd 100644 --- a/mgr/create.go +++ b/mgr/create.go @@ -137,7 +137,7 @@ func (s *Manager) CreateSession(args []string) (string, error) { // FIXME: Eager dial means session creation blocks on SSH connect. // Eventually make this async or move host info into the prompt // scripts themselves (running remotely via the RPC). - rsrv, dialErr := execute.RemoteDial(context.Background(), execute.RemoteConfig{ + rsrv, dialErr := execute.RemoteDial(s.cfg.Ctx, execute.RemoteConfig{ SSHTarget: remoteTarget, CWD: cwd, }) @@ -228,7 +228,7 @@ func (s *Manager) CreateSession(args []string) (string, error) { }) } - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(s.cfg.Ctx) sess := NewSession(sessID, core, ctx, cancel) sessPtr = sess sess.remote = remoteTarget diff --git a/mgr/manager.go b/mgr/manager.go index 931c88e..1373731 100644 --- a/mgr/manager.go +++ b/mgr/manager.go @@ -28,6 +28,7 @@ func FileMode(name string) (os.FileMode, bool) { // ManagerConfig holds the dependencies for a Manager. type ManagerConfig struct { + Ctx context.Context // daemon context — sessions derive from this AgentsDir string SessionsDir string diff --git a/mgr/persist.go b/mgr/persist.go index 1209ff3..022f2b5 100644 --- a/mgr/persist.go +++ b/mgr/persist.go @@ -137,7 +137,7 @@ func (s *Manager) restoreSession(ps *agent.PersistedAgent) error { remoteTarget := ps.Remote if remoteTarget != "" { - rsrv, dialErr := execute.RemoteDial(context.Background(), execute.RemoteConfig{ + rsrv, dialErr := execute.RemoteDial(s.cfg.Ctx, execute.RemoteConfig{ SSHTarget: remoteTarget, CWD: cwd, }) @@ -237,7 +237,7 @@ func (s *Manager) restoreSession(ps *agent.PersistedAgent) error { }, }) - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(s.cfg.Ctx) sess := NewSession(sessID, core, ctx, cancel) sessPtr = sess sess.uname = uname