From c38cb5726efc45334eafe1eb2905a9b485122161 Mon Sep 17 00:00:00 2001 From: defangdevs Date: Thu, 10 Sep 2026 00:09:04 +0000 Subject: [PATCH 1/2] fix(sessions): codex opens the TUI by default, not the pairing daemon (#623) Both writers of a codex session (settings-daemon.py's /sessions/add and session-cli.sh's `add`) hardcoded remoteControl: true regardless of harness. For codex that is not a flag on the ordinary TUI, as it is for claude -- it replaces the process outright with the app-server pairing daemon (supervisor.sh) -- so picking a codex profile from Add Session, or running `agent-box-session add --harness codex`, always opened the daemon and never a second interactive session. Per lionello's answers on #623: a real codex profile should open the TUI, and the daemon gets its own explicit entry instead. Add-session now offers "codex remote control (pairing daemon, no profile)" as a pseudo-profile alongside `shell`, and both writers default codex sessions to the TUI unless that daemon is explicitly requested (a new --remote-control flag on the CLI side, for parity). ensure_harness_session (the auto-add after install+login, #504) now also references the profile agent-box-profile seed (#508) already creates per installed harness, instead of always leaving profile: None -- so a second codex session added afterwards has a profile to pick. Checks run (aarch64-linux, native): assemble-module-escaping, backend-parity, connect-card, golden-snapshot, module-generated-up-to-date, one-spec-both-backends, profile-panel, runtime-profile, session-route, sessions-registry, plus the full native --keep-going set. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_018SPDtQA4fdkFGcqC1YdjFD --- modules/agent-box.nix | 73 +++++++++++++++++-- modules/src/session-cli.sh | 30 +++++++- modules/src/settings-daemon.py | 43 ++++++++++- .../agent-box-session/bin/agent-box-session | 30 +++++++- .../agent-box-settings/bin/agent-box-settings | 43 ++++++++++- 5 files changed, 201 insertions(+), 18 deletions(-) diff --git a/modules/agent-box.nix b/modules/agent-box.nix index b7bbc9e8..36556fdd 100644 --- a/modules/agent-box.nix +++ b/modules/agent-box.nix @@ -4347,7 +4347,7 @@ usage() { echo "Stop a session to free a slot. A refused start exits 75; retry is safe." echo " agent-box-session peers" echo " agent-box-session add [NAME] [--harness HARNESS] [--profile PROFILE]" - echo " [--cwd DIR]" + echo " [--cwd DIR] [--remote-control true|false]" echo " [--prompt TEXT] [--resume-prompt TEXT] [--ephemeral]" echo " [-- EXTRA_ARGS...]" echo " agent-box-session rm NAME" @@ -4376,6 +4376,11 @@ usage() { echo "fail to start." echo "--prompt kicks the session off with a task (first spawn only); a later" echo "respawn resumes the prior transcript instead of redoing it." + echo "--remote-control defaults to true, except for codex where it defaults to" + echo "false: claude's remote control is a flag on its ordinary TUI, but codex's" + echo "replaces the TUI outright with the app-server pairing daemon, so a plain" + echo "codex session (a profile, or --harness codex with none) now opens the" + echo "TUI unless this is passed explicitly." echo "--ephemeral marks a ONE-SHOT session: parking it (a clean agent exit, or" echo "'stop') delists it outright, because nobody is going to resume it. A" echo "CRASH still parks nothing and leaves the post-mortem shell attachable." @@ -4741,7 +4746,7 @@ case "$cmd" in *) name="$1"; shift; valid_new_name "$name" || { usage >&2; exit 2; } ;; esac harness="$DEFAULT_AGENT"; cwd=""; prompt=""; rprompt=""; has_prompt=0; has_rprompt=0 - profile=""; has_harness=0; ephemeral=0 + profile=""; has_harness=0; ephemeral=0; remote_control="" while [ $# -gt 0 ]; do case "$1" in --harness) harness="''${2:?--harness needs a value}"; has_harness=1; shift 2 ;; @@ -4757,6 +4762,13 @@ case "$cmd" in ;; --profile) profile="''${2:?--profile needs a value}"; shift 2 ;; --cwd) cwd="''${2:?--cwd needs a value}"; shift 2 ;; + --remote-control) + case "''${2:?--remote-control needs 'true' or 'false'}" in + (true|false) remote_control="$2" ;; + (*) echo "agent-box-session: --remote-control must be 'true' or 'false'" >&2 + exit 2 ;; + esac + shift 2 ;; --prompt) prompt="''${2?--prompt needs a value}"; has_prompt=1; shift 2 ;; --resume-prompt) rprompt="''${2?--resume-prompt needs a value}"; has_rprompt=1; shift 2 ;; --ephemeral) ephemeral=1; shift ;; @@ -4846,6 +4858,16 @@ case "$cmd" in pargs+=("$parg") done < <("$JQ" -j '.args[] + "\u0000"' <<<"$pjson") fi + # codex's remote control is a different PROGRAM entirely - the app-server + # pairing daemon, not a TUI flag (see supervisor.sh) - so an unqualified + # codex session now opens the TUI by default (issue #623); claude's own + # remote control is just a flag on its ordinary TUI and keeps its old + # default. Resolved AFTER the profile above, since a profile can change + # $harness. An explicit --remote-control always wins. + if [ -z "$remote_control" ]; then + remote_control=true + [ "$harness" = codex ] && remote_control=false + fi case " $AGENTS " in (*" $harness "*) ;; (*) echo "harness '$harness' is not available (available: $AGENTS)" >&2; exit 2 ;; @@ -4882,8 +4904,8 @@ case "$cmd" in registry_edit --arg n "$name" --arg a "$harness" --arg c "$cwd" \ --arg p "$prompt" --arg pp "$has_prompt" \ --arg rp "$rprompt" --arg rpp "$has_rprompt" --arg bid "$bid" \ - --arg prof "$profile" --arg eph "$ephemeral" \ - '.sessions[$n] = ({agent: $a, skipPermissions: true, remoteControl: true, + --arg prof "$profile" --arg eph "$ephemeral" --argjson rc "$remote_control" \ + '.sessions[$n] = ({agent: $a, skipPermissions: true, remoteControl: $rc, remoteControlName: null, workingDirectory: (if $c == "" then null else $c end), extraArgs: $ARGS.positional, @@ -16644,7 +16666,13 @@ def ensure_harness_session(agent, remote_control): sign-in (a token refresh, "Sign in again") must not mint a second one every time the card cycles through "connected". Claude gets a reusable starter profile, with its launch arguments resolved just like Add - session. Codex retains its bare interactive session. + session. Every other harness (codex included) gets a PROFILE only when + one already exists (agent-box-profile seed creates one per installed + harness, named after it, at every supervisor start -- issue #508): + referencing it here is what leaves the add-session picker something to + pick for a SECOND session afterwards (issue #623). A box whose + supervisor has not restarted since #508 landed has no such profile yet, + so this falls back to none rather than naming a file that is not there. `remote_control` is the one thing that differs by harness. claude's rc is a flag on the ordinary TUI (supervisor.sh appends --remote-control), @@ -16668,6 +16696,8 @@ def ensure_harness_session(agent, remote_control): # Profile storage failure must not undo a successful login or # prevent its worker from starting. pass + elif os.path.exists(profile_path(agent)): + profile = agent try: with sessions_lock(): sessions, version = load_sessions() @@ -20958,6 +20988,16 @@ def render_keys(keys): # #493 refuses is HARNESS=shell, not the name). SHELL_PSEUDO_PROFILE = ":shell" +# The value the profile picker posts to start codex's remote-control pairing +# daemon instead of a profile's interactive TUI (issue #623). Same shape as +# SHELL_PSEUDO_PROFILE and for the same reason: a profile NAME can only hold +# [A-Za-z0-9_-] (PROFILE_NAME_RE), so a leading ":" cannot collide with one. +# The daemon is a different PROGRAM entirely (supervisor.sh's codex+rc +# branch), not a codex profile with a flag flipped -- it has no model, +# effort or profile of its own, since the paired client decides those -- so +# it gets its own pseudo-profile rather than overloading a real one. +CODEX_RC_PSEUDO_PROFILE = ":codex-remote-control" + def render_profile_options(profiles): """The session row's profile - now the ONLY control on it @@ -3742,6 +3760,10 @@ def render_profile_options(profiles): harness = html.escape(profiles[name]["reserved"].get("HARNESS") or "") label = f"{safe} ({harness})" if harness else safe items.append(f'') + if "codex" in AGENTS: + # A real codex profile always opens the interactive TUI now (issue + # #623); the daemon is offered here instead, as its own entry. + items.append('' % CODEX_RC_PSEUDO_PROFILE) items.append(shell_opt) return "".join(items) @@ -7090,10 +7112,19 @@ def do_POST(self): # to say the other thing from a shell. profile = (form.get("profile", [""])[0]).strip() agent = (form.get("agent", [""])[0]).strip() + # Captured before profile is cleared below: the one signal that + # this add asked for codex's pairing daemon rather than a TUI + # (issue #623), read again where remoteControl is decided. + codex_daemon = profile == CODEX_RC_PSEUDO_PROFILE if profile == SHELL_PSEUDO_PROFILE: # The one entry that is not a profile file: a bare shell has # no model, reasoning level or instructions to carry. agent, profile = "shell", "" + elif codex_daemon: + # Also not a profile file: the daemon has no model, effort + # or profile of its own, since the paired client decides + # those once it connects. + agent, profile = "codex", "" elif profile: agent = "" # resolved from the profile below elif not agent: @@ -7207,7 +7238,15 @@ def do_POST(self): sessions[name] = { "agent": agent, "skipPermissions": True, - "remoteControl": True, + # codex's remote control is a different PROGRAM + # entirely (the pairing daemon, not a TUI flag - + # see ensure_harness_session), so only the explicit + # pseudo-profile above asks for it; a real codex + # profile, or --harness codex with none, now opens + # the interactive TUI instead (issue #623). claude's + # own remote control is just a flag on its ordinary + # TUI and keeps the old default. + "remoteControl": codex_daemon or agent != "codex", "remoteControlName": None, "workingDirectory": cwd, "extraArgs": pargs, diff --git a/tests/golden/vm/payloads/agent-box-session/bin/agent-box-session b/tests/golden/vm/payloads/agent-box-session/bin/agent-box-session index e0f9a511..a8c37189 100644 --- a/tests/golden/vm/payloads/agent-box-session/bin/agent-box-session +++ b/tests/golden/vm/payloads/agent-box-session/bin/agent-box-session @@ -657,7 +657,7 @@ usage() { echo "Stop a session to free a slot. A refused start exits 75; retry is safe." echo " agent-box-session peers" echo " agent-box-session add [NAME] [--harness HARNESS] [--profile PROFILE]" - echo " [--cwd DIR]" + echo " [--cwd DIR] [--remote-control true|false]" echo " [--prompt TEXT] [--resume-prompt TEXT] [--ephemeral]" echo " [-- EXTRA_ARGS...]" echo " agent-box-session rm NAME" @@ -686,6 +686,11 @@ usage() { echo "fail to start." echo "--prompt kicks the session off with a task (first spawn only); a later" echo "respawn resumes the prior transcript instead of redoing it." + echo "--remote-control defaults to true, except for codex where it defaults to" + echo "false: claude's remote control is a flag on its ordinary TUI, but codex's" + echo "replaces the TUI outright with the app-server pairing daemon, so a plain" + echo "codex session (a profile, or --harness codex with none) now opens the" + echo "TUI unless this is passed explicitly." echo "--ephemeral marks a ONE-SHOT session: parking it (a clean agent exit, or" echo "'stop') delists it outright, because nobody is going to resume it. A" echo "CRASH still parks nothing and leaves the post-mortem shell attachable." @@ -1051,7 +1056,7 @@ case "$cmd" in *) name="$1"; shift; valid_new_name "$name" || { usage >&2; exit 2; } ;; esac harness="$DEFAULT_AGENT"; cwd=""; prompt=""; rprompt=""; has_prompt=0; has_rprompt=0 - profile=""; has_harness=0; ephemeral=0 + profile=""; has_harness=0; ephemeral=0; remote_control="" while [ $# -gt 0 ]; do case "$1" in --harness) harness="${2:?--harness needs a value}"; has_harness=1; shift 2 ;; @@ -1067,6 +1072,13 @@ case "$cmd" in ;; --profile) profile="${2:?--profile needs a value}"; shift 2 ;; --cwd) cwd="${2:?--cwd needs a value}"; shift 2 ;; + --remote-control) + case "${2:?--remote-control needs 'true' or 'false'}" in + (true|false) remote_control="$2" ;; + (*) echo "agent-box-session: --remote-control must be 'true' or 'false'" >&2 + exit 2 ;; + esac + shift 2 ;; --prompt) prompt="${2?--prompt needs a value}"; has_prompt=1; shift 2 ;; --resume-prompt) rprompt="${2?--resume-prompt needs a value}"; has_rprompt=1; shift 2 ;; --ephemeral) ephemeral=1; shift ;; @@ -1156,6 +1168,16 @@ case "$cmd" in pargs+=("$parg") done < <("$JQ" -j '.args[] + "\u0000"' <<<"$pjson") fi + # codex's remote control is a different PROGRAM entirely - the app-server + # pairing daemon, not a TUI flag (see supervisor.sh) - so an unqualified + # codex session now opens the TUI by default (issue #623); claude's own + # remote control is just a flag on its ordinary TUI and keeps its old + # default. Resolved AFTER the profile above, since a profile can change + # $harness. An explicit --remote-control always wins. + if [ -z "$remote_control" ]; then + remote_control=true + [ "$harness" = codex ] && remote_control=false + fi case " $AGENTS " in (*" $harness "*) ;; (*) echo "harness '$harness' is not available (available: $AGENTS)" >&2; exit 2 ;; @@ -1192,8 +1214,8 @@ case "$cmd" in registry_edit --arg n "$name" --arg a "$harness" --arg c "$cwd" \ --arg p "$prompt" --arg pp "$has_prompt" \ --arg rp "$rprompt" --arg rpp "$has_rprompt" --arg bid "$bid" \ - --arg prof "$profile" --arg eph "$ephemeral" \ - '.sessions[$n] = ({agent: $a, skipPermissions: true, remoteControl: true, + --arg prof "$profile" --arg eph "$ephemeral" --argjson rc "$remote_control" \ + '.sessions[$n] = ({agent: $a, skipPermissions: true, remoteControl: $rc, remoteControlName: null, workingDirectory: (if $c == "" then null else $c end), extraArgs: $ARGS.positional, diff --git a/tests/golden/web/payloads/agent-box-settings/bin/agent-box-settings b/tests/golden/web/payloads/agent-box-settings/bin/agent-box-settings index 43b0df59..97d45d68 100644 --- a/tests/golden/web/payloads/agent-box-settings/bin/agent-box-settings +++ b/tests/golden/web/payloads/agent-box-settings/bin/agent-box-settings @@ -1358,7 +1358,13 @@ def ensure_harness_session(agent, remote_control): sign-in (a token refresh, "Sign in again") must not mint a second one every time the card cycles through "connected". Claude gets a reusable starter profile, with its launch arguments resolved just like Add - session. Codex retains its bare interactive session. + session. Every other harness (codex included) gets a PROFILE only when + one already exists (agent-box-profile seed creates one per installed + harness, named after it, at every supervisor start -- issue #508): + referencing it here is what leaves the add-session picker something to + pick for a SECOND session afterwards (issue #623). A box whose + supervisor has not restarted since #508 landed has no such profile yet, + so this falls back to none rather than naming a file that is not there. `remote_control` is the one thing that differs by harness. claude's rc is a flag on the ordinary TUI (supervisor.sh appends --remote-control), @@ -1382,6 +1388,8 @@ def ensure_harness_session(agent, remote_control): # Profile storage failure must not undo a successful login or # prevent its worker from starting. pass + elif os.path.exists(profile_path(agent)): + profile = agent try: with sessions_lock(): sessions, version = load_sessions() @@ -5672,6 +5680,16 @@ def render_keys(keys): # #493 refuses is HARNESS=shell, not the name). SHELL_PSEUDO_PROFILE = ":shell" +# The value the profile picker posts to start codex's remote-control pairing +# daemon instead of a profile's interactive TUI (issue #623). Same shape as +# SHELL_PSEUDO_PROFILE and for the same reason: a profile NAME can only hold +# [A-Za-z0-9_-] (PROFILE_NAME_RE), so a leading ":" cannot collide with one. +# The daemon is a different PROGRAM entirely (supervisor.sh's codex+rc +# branch), not a codex profile with a flag flipped -- it has no model, +# effort or profile of its own, since the paired client decides those -- so +# it gets its own pseudo-profile rather than overloading a real one. +CODEX_RC_PSEUDO_PROFILE = ":codex-remote-control" + def render_profile_options(profiles): """The session row's profile