Skip to content

fix(ls): check executor state for non-tmux (SDK/omni) agents - #1167

Merged
namastex888 merged 1 commit into
devfrom
fix/genie-ls-sdk-liveness
Apr 17, 2026
Merged

namastex888 merged 1 commit into
devfrom
fix/genie-ls-sdk-liveness

Conversation

@namastex888

Copy link
Copy Markdown
Contributor

Summary

  • genie ls was rendering SDK/omni-bridge/process agents as offline (or the misleading error (0/3 resumes, auto-resume: on)) while they were actively running.
  • Root cause: buildWorkerStatusMap (src/term-commands/agents.ts:2138) gated liveness entirely on isPaneAlive(w.paneId), which requires a %N tmux pane ID. Non-tmux transports carry synthetic tokens ('sdk', 'inline', '') that fail the regex, so they were silently dropped from the status map.
  • Fix: gate on transport. Tmux agents still use isPaneAlive (legacy behavior preserved exactly). Non-tmux agents consult the new getLiveExecutorState(agentId) helper — executors.state is the authoritative signal (claude-sdk updates it on every message) and is also used for display since agents.state isn't kept in sync for these transports.

Changes

  • src/lib/executor-registry.ts — new getLiveExecutorState(agentId): Promise<ExecutorState | null> + boolean wrapper isExecutorAlive.
  • src/term-commands/agents.ts — buildWorkerStatusMap delegates to a new resolveWorkerLiveness helper that picks the right liveness source.
  • src/lib/executor-registry.test.ts — 14 new tests covering both functions across all executor states, missing-executor, and terminal transitions.

Scope

Intentionally narrow. The tracer flagged parallel isPaneAlive(w.paneId) sites in agents.ts:1291/1835/1886, scheduler-daemon.ts:595/667, team-auto-spawn.ts:122, auto-spawn.ts:84 — those are out of scope and belong in a follow-up wish. This PR is a surgical UX visibility fix, not a lifecycle refactor.

Evidence

Live against a running daemon, same agent (trace) in both runs:

# BEFORE (dev)
$ genie ls --json | jq '.[] | select(.name=="trace")'
{ "name": "trace", "status": "error (0/3 resumes, auto-resume: on)", ... }

# AFTER (this branch)
$ ./dist/genie.js ls --json | jq '.[] | select(.name=="trace")'
{ "name": "trace", "status": "spawning", ... }

Status distribution shift (same daemon, same instant):

status before after
offline 48 48
error (0/3 resumes, auto-resume: on) 19 18
idle 2 2
spawning 0 1

Tmux agents (felipe-3, fix-genie-ls-status) unchanged — still idle — confirming the legacy path is preserved.

Test plan

  • bun test src/lib/executor-registry.test.ts → 79 pass, 0 fail (14 new)
  • bun run check → EXIT=0, 2495/0 across 128 files
  • Pre-push hook full suite → 2501/0
  • Live before/after on running daemon (shown above)
  • bunx biome check on changed files → no new warnings (one pre-existing processMessage complexity warning at agents.ts:1073 unrelated)

🤖 Generated with Claude Code

genie ls was rendering SDK/omni-bridge/process agents as "offline" (or
a misleading "error (0/3 resumes, auto-resume: on)") while they were
actively running. buildWorkerStatusMap gated liveness entirely on
isPaneAlive(w.paneId), which requires paneId to match /^%\d+$/.
Non-tmux transports carry synthetic pane tokens ('sdk', 'inline', '')
that fail the regex, so they were silently dropped from statusMap and
their stale agents.state field was all that survived for display.

Gate on transport type: tmux panes still use isPaneAlive (preserving
legacy behavior exactly), while non-tmux transports consult
executors.state via the new getLiveExecutorState helper. That state is
also used directly for display, since agents.state is not kept in sync
for SDK/omni agents — the claude-sdk transport updates executors.state
on every message, which is the authoritative signal.

Scope: intentionally narrow. Parallel-pattern sites flagged by tracer
(agents.ts:1291, 1835, 1886; scheduler-daemon.ts:595,667;
team-auto-spawn.ts:122; auto-spawn.ts:84) are out of scope — separate
wish.

Evidence (genie ls --json | jq '.[] | select(.name=="trace")'):
  BEFORE: status = "error (0/3 resumes, auto-resume: on)"
  AFTER:  status = "spawning"

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c8117e52-cfae-4fa8-9070-380d3db41d31

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/genie-ls-sdk-liveness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces getLiveExecutorState and isExecutorAlive to accurately track the liveness of agents using non-tmux transports, ensuring their status is correctly reported in genie ls. Review feedback suggests parallelizing the liveness checks in buildWorkerStatusMap to improve performance and adjusting getLiveExecutorState to return the authoritative state for terminal executors to prevent the display of stale information.

Comment on lines 2158 to +2160
for (const w of workers) {
const name = w.role || w.id;
const alive = await isPaneAlive(w.paneId);
const { alive, state } = await resolveWorkerLiveness(w);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The loop performs sequential asynchronous calls to resolveWorkerLiveness, which involves external process executions (tmux) and database queries. This can lead to significant performance degradation when listing a large number of agents. Parallelizing these checks using Promise.all would improve responsiveness.

Suggested change
for (const w of workers) {
const name = w.role || w.id;
const alive = await isPaneAlive(w.paneId);
const { alive, state } = await resolveWorkerLiveness(w);
const results = await Promise.all(workers.map(async (w) => ({ w, ...(await resolveWorkerLiveness(w)) })));
for (const { w, alive, state } of results) {
const name = w.role || w.id;

Comment on lines +271 to +281
export async function getLiveExecutorState(agentId: string): Promise<ExecutorState | null> {
const sql = await getConnection();
const rows = await sql<{ state: ExecutorState }[]>`
SELECT e.state FROM executors e
JOIN agents a ON a.current_executor_id = e.id
WHERE a.id = ${agentId}
AND e.state IN ('spawning', 'running', 'working', 'idle', 'permission', 'question')
LIMIT 1
`;
return rows.length > 0 ? rows[0].state : null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The getLiveExecutorState function returns null for terminal states (done, error, terminated). However, as noted in the function's documentation, the agents.state column is stale for non-tmux transports. By returning null for dead executors, you force the caller to fall back to this stale data, which can result in genie ls showing an incorrect status (e.g., offline instead of error) for crashed agents. Consider returning the authoritative executor state regardless of liveness and letting the caller determine how to handle terminal states.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99d8ea67ca

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2148 to +2152
if (/^%\d+$/.test(w.paneId)) {
return { alive: await isPaneAlive(w.paneId), state: w.state };
}
const execState = await executorRegistry.getLiveExecutorState(w.id);
return { alive: execState !== null, state: execState ?? w.state };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude identity rows when resolving worker liveness

registry.list() includes identity-only agent rows (from findOrCreateAgent) that have empty paneId but a live current_executor_id; with this new branch, those rows are treated as non-tmux and marked alive from executors.state. For tmux-backed workers this creates a duplicate entry keyed by the same role, and because buildWorkerStatusMap overwrites by name without stable ordering, genie ls can nondeterministically show the identity row’s stale executor state (often running) instead of the runtime row’s agents.state (idle/working).

Useful? React with 👍 / 👎.

@namastex888
namastex888 merged commit 40df308 into dev Apr 17, 2026
8 of 9 checks passed
namastex888 pushed a commit that referenced this pull request Apr 18, 2026
Shared helper that dispatches on paneId shape: tmux (%N) → isPaneAlive,
synthetic (sdk/inline/'') → isExecutorAlive. Mirrors the regex-guard pattern
from scheduler-daemon.ts:countActiveWorkers (PR #1181) and unifies the
call-sites that previously called isPaneAlive blindly (PR #1167 + this sweep).
namastex888 pushed a commit that referenced this pull request Apr 18, 2026
Site 1 findDeadResumable: safe — provider filter excludes SDK (documented).
Site 2 rejectDuplicateRole: dispatch via resolveWorkerLivenessByTransport so
  live SDK/omni/inline duplicates are detected instead of being silently
  cleaned. Session-recycle check gated on tmux paneId (synthetic paneIds
  never collide with a recycled pane).
Site 3 resolveSpawnIdentity: widen isAliveFn signature to {id, paneId};
  default routes through the shared helper. Prevents a live SDK canonical
  from being wrongly re-canonicalized and clobbering its session UUID via
  ON CONFLICT UPDATE. Updates test stubs in agents.test.ts and
  tui-spawn-dx.integration.test.ts to match signature.

Complements #1167.
namastex888 pushed a commit that referenced this pull request Apr 18, 2026
Site 4 checkWorkerAlive (scheduler-daemon.ts): documented safe — runs.worker_id
  only carries real PIDs or a UUID daemonId fallback from handleTrigger; never
  a tmux pane or synthetic id. SDK agents live in agents/executors, not runs.
Site 5 runAgentRecoveryPass (scheduler-daemon.ts): documented safe —
  claudeSessionId filter excludes SDK agents (SDK lacks a Claude-CLI JSONL).
Site 6 isAgentAlive (team-auto-spawn.ts): dispatch via shared helper so
  inbox-watcher correctly routes messages to live SDK recipients instead of
  treating them as dead and dropping/misrouting.
Site 7 executeAutoSpawn (hooks/handlers/auto-spawn.ts): dispatch via shared
  helper so the PreToolUse SendMessage handler stops triggering duplicate
  spawns on every message to a live SDK recipient.

Complements #1167.
automagik-genie added a commit that referenced this pull request Apr 18, 2026
fix(v0.2): transport-aware liveness at 5 callers of isPaneAlive (complements #1167)
@automagik-genie
automagik-genie deleted the fix/genie-ls-sdk-liveness branch September 25, 2026 04:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant