Native role launch refuses inside a launch, which contradicts the sentinel's own documented contract #348

Open
opened 2026-08-26 02:06:26 +00:00 by coilyco-ops · 1 comment
Member

Filed by Saiya (tpm seat), 2026-08-25. Kai raised it as a UX limit and thought it might already be in flight. It is not. No open issue or PR covers it, checked across the estate alongside the aterm and acompose work that landed tonight (#1243, #1246, #1249).

The observation

aterm eval claude from inside a composed session refuses:

native role launch cannot start inside another agent-compose launch

Why this is a defect rather than a policy

One sentinel, two call sites, two different behaviours, and only one of them matches what the sentinel says it is for.

internal/launch/launch.go:13 states the contract:

EnvSentinel marks a process launched by agent-compose. A nested launch sees it and skips refresh instead of recursing.

cmd/agent-compose/main.go:1096, in refreshThenExec, does exactly that. It sees the sentinel, prints nested launch detected; skipping refresh, and still execs:

if os.Getenv(launch.EnvSentinel) != "" {
    fmt.Fprintln(os.Stderr, "agent-compose: nested launch detected; skipping refresh")
    return execReal(command)
}

cmd/agent-compose/main.go:701, in runNativeLaunch, sees the same sentinel and refuses outright:

if os.Getenv(launch.EnvSentinel) != "" {
    return fmt.Errorf("native role launch cannot start inside another agent-compose launch")
}

The refusal sits immediately above the converge.Run call it is protecting. So the thing actually at risk of recursing is the converge, and refreshThenExec already demonstrates the correct handling: suppress the converge, proceed with the launch.

The fix

Make runNativeLaunch behave like refreshThenExec. Skip the converge when the sentinel is set, warn on stderr in the same shape, and continue into the launch. Do not delete the guard, because the recursion protection is the converge suppression rather than the refusal.

Acceptance criteria

  • aterm <role> <seat> from inside a composed session launches, and does not converge.
  • It warns on stderr that the nested converge was skipped, matching the wording style at main.go:1096.
  • Launching from a clean shell still converges, unchanged.
  • A test covers both branches of runNativeLaunch on the sentinel, in the shape of the existing coverage on refreshThenExec.
  • The EnvSentinel doc comment still describes what both call sites do.

Watch item

The nested session inherits AGENT_COMPOSE_LAUNCH and AGENT_GIT_ATTRIBUTION_ROLE from its parent. launch.go:18 notes attribution is per-session against a global host projection (coilysiren/inbox#362). Confirm the child's role attribution wins over the parent's rather than inheriting it, or a nested eval seat commits as the parent's role.

Not in scope

This does not make nested seats addressable by the harness's cross-agent messaging. Separate issue, filed alongside this one.

Filed by Saiya (tpm seat), 2026-08-25. Kai raised it as a UX limit and thought it might already be in flight. **It is not.** No open issue or PR covers it, checked across the estate alongside the aterm and acompose work that landed tonight (#1243, #1246, #1249). ## The observation `aterm eval claude` from inside a composed session refuses: ``` native role launch cannot start inside another agent-compose launch ``` ## Why this is a defect rather than a policy One sentinel, two call sites, two different behaviours, and only one of them matches what the sentinel says it is for. `internal/launch/launch.go:13` states the contract: > `EnvSentinel` marks a process launched by agent-compose. **A nested launch sees it and skips refresh instead of recursing.** `cmd/agent-compose/main.go:1096`, in `refreshThenExec`, does exactly that. It sees the sentinel, prints `nested launch detected; skipping refresh`, and **still execs**: ```go if os.Getenv(launch.EnvSentinel) != "" { fmt.Fprintln(os.Stderr, "agent-compose: nested launch detected; skipping refresh") return execReal(command) } ``` `cmd/agent-compose/main.go:701`, in `runNativeLaunch`, sees the same sentinel and **refuses outright**: ```go if os.Getenv(launch.EnvSentinel) != "" { return fmt.Errorf("native role launch cannot start inside another agent-compose launch") } ``` The refusal sits immediately above the `converge.Run` call it is protecting. So the thing actually at risk of recursing is the converge, and `refreshThenExec` already demonstrates the correct handling: suppress the converge, proceed with the launch. ## The fix Make `runNativeLaunch` behave like `refreshThenExec`. Skip the converge when the sentinel is set, warn on stderr in the same shape, and continue into the launch. **Do not delete the guard**, because the recursion protection is the converge suppression rather than the refusal. ## Acceptance criteria - [ ] `aterm <role> <seat>` from inside a composed session launches, and does not converge. - [ ] It warns on stderr that the nested converge was skipped, matching the wording style at `main.go:1096`. - [ ] Launching from a clean shell still converges, unchanged. - [ ] A test covers both branches of `runNativeLaunch` on the sentinel, in the shape of the existing coverage on `refreshThenExec`. - [ ] The `EnvSentinel` doc comment still describes what both call sites do. ## Watch item The nested session inherits `AGENT_COMPOSE_LAUNCH` and `AGENT_GIT_ATTRIBUTION_ROLE` from its parent. `launch.go:18` notes attribution is per-session against a global host projection (`coilysiren/inbox#362`). Confirm the child's role attribution wins over the parent's rather than inheriting it, or a nested `eval` seat commits as the parent's role. ## Not in scope This does not make nested seats addressable by the harness's cross-agent messaging. Separate issue, filed alongside this one.
Author
Member

Half landed, and I reopened this after closing it by mistake

e7605d7 on main does the converge half. I closed the issue on that and then re-read the acceptance criteria: the first one is still unmet, and it is the one carrying the user-visible symptom. Reopened.

What the code looked like when I got here, which is not what the body describes

The body was written against a runNativeLaunch that refused outright. --nested landed since, so main now reads:

childDepth, err := launch.NestedDepth(os.Getenv(launch.EnvSentinel), os.Getenv(launch.EnvDepth), nested)

NestedDepth refuses unless --nested is passed, then bounds the chain at one hop. That is a different resolution from the one the body proposed, and a reasonable one: it separates accidental nesting from deliberate nesting, which the sentinel alone cannot.

But the converge was still running on a nested launch, against a host the parent had just converged. That is the part the body is unambiguously right about, and the part nothing had done.

Landed in e7605d7

  • It warns on stderr that the nested converge was skipped, matching the wording style at the wrapper call site. Both notices now come from one function, nestedLaunchNotice, because the two saying different things is how they came to do different things.
  • Launching from a clean shell still converges, unchanged.
  • A test covers both branches, nestedLaunchSkipsConverge(0) and (1), plus both notice strings.
  • The EnvSentinel doc comment described only the refresh call site. It now describes what both do.
  • aterm <role> <seat> from inside a composed session launches. Still refuses, because aterm does not pass --nested.

The remaining question, which is a decision rather than code

Should aterm <role> <seat> pass --nested when it detects the sentinel?

  • Yes. Someone who types aterm eval claude inside a session is being deliberate by typing it. The flag exists to stop a harness re-invoking acompose by accident, and a human at a prompt is not that. This closes the symptom in about three lines of aterm/plan.go.
  • No. The flag's value is that deliberateness is spelled, and a launcher that always spells it for you deletes the distinction. Then this issue closes as "the refusal is correct, the converge was the bug", and the aterm-side answer is a clearer refusal that names --nested.

I did not pick, because both are defensible and the wrong one is annoying to walk back through two repos. Whichever way it goes, the aterm change is small.

Watch item from the body, unchecked

Role attribution on a nested seat is still unverified. AGENT_GIT_ATTRIBUTION_ROLE is inherited from the parent and I did not test whether the child's role wins. Recording that it remains open rather than implying this touched it.

## Half landed, and I reopened this after closing it by mistake `e7605d7` on `main` does the converge half. I closed the issue on that and then re-read the acceptance criteria: **the first one is still unmet**, and it is the one carrying the user-visible symptom. Reopened. ## What the code looked like when I got here, which is not what the body describes The body was written against a `runNativeLaunch` that refused outright. `--nested` landed since, so `main` now reads: ```go childDepth, err := launch.NestedDepth(os.Getenv(launch.EnvSentinel), os.Getenv(launch.EnvDepth), nested) ``` `NestedDepth` refuses unless `--nested` is passed, then bounds the chain at one hop. That is a different resolution from the one the body proposed, and a reasonable one: it separates *accidental* nesting from *deliberate* nesting, which the sentinel alone cannot. But the converge was still running on a nested launch, against a host the parent had just converged. That is the part the body is unambiguously right about, and the part nothing had done. ## Landed in e7605d7 - [x] It warns on stderr that the nested converge was skipped, matching the wording style at the wrapper call site. Both notices now come from one function, `nestedLaunchNotice`, because the two saying different things is how they came to *do* different things. - [x] Launching from a clean shell still converges, unchanged. - [x] A test covers both branches, `nestedLaunchSkipsConverge(0)` and `(1)`, plus both notice strings. - [x] The `EnvSentinel` doc comment described only the refresh call site. It now describes what both do. - [ ] **`aterm <role> <seat>` from inside a composed session launches.** Still refuses, because aterm does not pass `--nested`. ## The remaining question, which is a decision rather than code Should `aterm <role> <seat>` pass `--nested` when it detects the sentinel? * **Yes.** Someone who types `aterm eval claude` inside a session is being deliberate by typing it. The flag exists to stop a *harness* re-invoking acompose by accident, and a human at a prompt is not that. This closes the symptom in about three lines of `aterm/plan.go`. * **No.** The flag's value is that deliberateness is spelled, and a launcher that always spells it for you deletes the distinction. Then this issue closes as "the refusal is correct, the converge was the bug", and the aterm-side answer is a clearer refusal that names `--nested`. I did not pick, because both are defensible and the wrong one is annoying to walk back through two repos. Whichever way it goes, the aterm change is small. ## Watch item from the body, unchecked Role attribution on a nested seat is still unverified. `AGENT_GIT_ATTRIBUTION_ROLE` is inherited from the parent and I did not test whether the child's role wins. Recording that it remains open rather than implying this touched it.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/agent-compose#348
No description provided.