Skip to content

fix(launch): count launch depth instead of refusing the first nested agent - #119

Merged
NagyVikt merged 1 commit into
mainfrom
fix/launch-depth-guard
Aug 5, 2026
Merged

fix(launch): count launch depth instead of refusing the first nested agent#119
NagyVikt merged 1 commit into
mainfrom
fix/launch-depth-guard

Conversation

@NagyVikt

@NagyVikt NagyVikt commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Unblocks any tool that shells out to claude from inside a cue-launched session — an AI code-review provider, most immediately, which was refused mid-merge-gate today.

The conflation

CUE_LAUNCHING was the string "1" and the guard tripped on it. Re-entry can mean two different things:

shape verdict
shim loop — cue resolves the agent binary back to its own shim re-enters without bound must be stopped
nested launch — an agent, or a tool it runs, invokes claude for a subtask re-enters once legitimate

The flag is inherited by the whole process tree under a launched agent, so the second case was refused too — with a message pointing at a fault that isn't there:

cue: shim recursion detected — check PATH ordering (cue's shim dir must precede the real claude/codex location)

Callers that knew worked around it by hand: failures.ts deletes the variable before spawning, and the headless-gif-demo skill instructs unset CUE_LAUNCHING …. A caller that didn't know just failed.

The fix

Count instead of flag. A loop climbs to the cap in milliseconds; honest nesting stays shallow.

  • CUE_LAUNCHING carries a depth; each launch writes depth + 1 into the child env
  • guard trips at MAX_LAUNCH_DEPTH = 3 — one nested agent is fine, a loop still dies almost instantly
  • the message names both causes instead of guessing
  • a non-numeric value from an older cue still reads as depth 1

Verification

From inside a cue-launched session, where CUE_LAUNCHING=1 is inherited and the shipped cue refuses:

result
installed cue shim recursion detected
patched cue launches — "profile": "core", "agent": "claude-code"
patched cue at CUE_LAUNCHING=3 still refuses, both causes named

Three e2e cases replace the single old one: trips at the cap, allows one nested launch, and reads a legacy non-numeric marker as depth 1.

Full suite 3019 pass, 1 skip, 0 fail. tsc --noEmit exit 0. biome lint clean.

🤖 Generated with Claude Code

…agent

`CUE_LAUNCHING` was the string "1" and the guard tripped on it, which
conflates the two things re-entry can mean.

A shim LOOP — cue resolving the agent binary back to its own shim — re-
enters without bound and has to be stopped. A NESTED launch — an agent,
or a tool it runs, invoking `claude` for a subtask such as an AI code
review — re-enters exactly once and is legitimate. The flag is inherited
by the entire process tree under a launched agent, so the old guard
killed the second case too, with a message blaming PATH ordering:

  cue: shim recursion detected — check PATH ordering (...)

That message sends you hunting through PATH for a fault that is not
there. It cost a merge gate today: the AI-review provider shelled out to
`claude` from inside a cue-launched session and was refused.

Callers that knew about it worked around it by hand — `failures.ts`
deletes the variable before spawning, and the headless-gif-demo skill
tells you to `unset` it. A caller that did not know simply failed.

Counting separates the cases: a loop climbs to the cap in milliseconds,
honest nesting stays shallow. `CUE_LAUNCHING` now carries a depth, the
guard trips at 3, and the message names both causes. A non-numeric value
from an older cue still reads as depth 1.

Verified from inside a cue-launched session, where CUE_LAUNCHING=1 is
inherited and the shipped cue refuses:

  installed cue → "shim recursion detected"
  patched cue   → launches ("profile": "core", "agent": "claude-code")
  patched cue at CUE_LAUNCHING=3 → still refuses, with both causes named

Full suite 3019 pass / 1 skip / 0 fail, tsc --noEmit exit 0, biome clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@NagyVikt
NagyVikt merged commit 71b7314 into main Aug 5, 2026
6 checks passed
@NagyVikt
NagyVikt deleted the fix/launch-depth-guard branch August 5, 2026 11:16
NagyVikt added a commit that referenced this pull request Aug 5, 2026
announceTmuxProfile() gated only on being inside tmux, so every cue launch
reached it — including the short-lived `--print` skill-selector cue spawns on
each session start. That helper inherits TMUX_PANE from the session it is
helping, so it set @cue_* on that pane and then, seconds later, its exit
handler unset all eight. The interactive session kept running with its badge
wiped from the pane border: the profile name and icon simply vanished.

Measured on this machine: 1 of 12 live panes still had @cue_profile_name set.

Two guards, because they cover different cases:

  - ownsPaneBadge() adds a TTY requirement. Verified against live processes:
    all seven interactive launchers write to /dev/pts/N, while the classifier
    is spawned with stdio ["ignore", "pipe", "ignore"], so a pipe is exactly
    what distinguishes a helper from the session the user is looking at.

  - The exit sweep now reads the options back and unsets only those still
    holding the values it wrote. A nested launch — supported since #119 — has
    a TTY too, so the TTY guard alone would not stop it from clearing an outer
    session's badge on exit. One extra spawn, batched like the set path.

A failed read-back leaves the options in place: leaking a stale badge is
recoverable on the next launch, clearing a live one is not.

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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