fix(ssh): resolve session executor_id via TaskSession join

GET /api/v1/ssh/executors/:id/sessions was filtering ExecutorRunning rows by
their ExecutorID column, but the lifecycle manager's buildRunningFromExecution
only sets that column from a prior row — for fresh sessions it's left empty.
The endpoint therefore returned [] for every just-launched SSH session.

Look up the TaskSession by session_id and compare its ExecutorID instead.
Keep the row's own ExecutorID as a fast-path for resume/recovery flows
where it's already populated.
This commit is contained in:
José Almeida
2026-05-17 17:06:57 +01:00
parent b5991a8d82
commit e7933645fa
+31 -5
View File
@@ -24,9 +24,14 @@ import (
const errorJSONKey = "error"
// ExecutorRunningLister is the narrow repository slice we need to surface
// active SSH sessions. Implemented by the task repository.
// active SSH sessions. We list all running executors and resolve each row's
// executor_id by looking up the task_session — the lifecycle manager doesn't
// populate ExecutorRunning.ExecutorID directly (it's only carried forward from
// prior rows on resume), so joining through the session is the source of
// truth.
type ExecutorRunningLister interface {
ListExecutorsRunning(ctx context.Context) ([]*models.ExecutorRunning, error)
GetTaskSession(ctx context.Context, id string) (*models.TaskSession, error)
}
// Handler exposes /api/v1/ssh routes used by the settings UI.
@@ -322,8 +327,10 @@ func (h *Handler) testAgentctlCache(ctx context.Context, client *ssh.Client, res
// listSessions returns the active SSH-runtime sessions for the given executor.
// Backed by the ExecutorRunning table; each row carries the SSH metadata we
// persisted in CreateInstance. Filtered client-side because the repository
// does not yet expose a "list-by-executor" method.
// persisted in CreateInstance. The executor binding is resolved by looking up
// the TaskSession associated with each row — the row itself has an
// ExecutorID column but the lifecycle manager does not populate it for fresh
// executions, so it's unreliable as a filter.
func (h *Handler) listSessions(ctx context.Context, executorID string) ([]SessionRow, error) {
rows, err := h.repo.ListExecutorsRunning(ctx)
if err != nil {
@@ -335,10 +342,10 @@ func (h *Handler) listSessions(ctx context.Context, executorID string) ([]Sessio
if run == nil {
continue
}
if run.ExecutorID != executorID {
if run.Runtime != string(models.ExecutorTypeSSH) {
continue
}
if run.Runtime != string(models.ExecutorTypeSSH) {
if !h.sessionBelongsToExecutor(ctx, run, executorID) {
continue
}
row := SessionRow{
@@ -353,3 +360,22 @@ func (h *Handler) listSessions(ctx context.Context, executorID string) ([]Sessio
}
return out, nil
}
// sessionBelongsToExecutor returns true when the given ExecutorRunning row was
// launched under the given executor config. Resolves via the task_session
// row because ExecutorRunning.ExecutorID is unpopulated on fresh executions.
// Falls back to the row's own ExecutorID when the session lookup fails, so
// resume / recovery paths (where the row has it) still match.
func (h *Handler) sessionBelongsToExecutor(ctx context.Context, run *models.ExecutorRunning, executorID string) bool {
if run.ExecutorID == executorID && executorID != "" {
return true
}
if run.SessionID == "" {
return false
}
session, err := h.repo.GetTaskSession(ctx, run.SessionID)
if err != nil || session == nil {
return false
}
return session.ExecutorID == executorID
}