Skip to content

Add background mode to run_shell_command (fix Termux timeout on servers) - #7

Merged
aciderix merged 1 commit into
mainfrom
claude/gemini-cli-ui-port-9gzwX-bg
Apr 21, 2026
Merged

aciderix merged 1 commit into
mainfrom
claude/gemini-cli-ui-port-9gzwX-bg

Conversation

@aciderix

@aciderix aciderix commented Apr 21, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixe le faux-fail exit=-1 — Termux did not reply qu'on voit quand
Gemini lance un serveur (Python http.server, live-server, npm run dev, uvicorn…). Le process démarrait bien côté Termux mais le bridge
attendait la fin de la commande, abandonnait à 12 s et renvoyait une
erreur — alors que le serveur tournait toujours.

Fix

run_shell_command expose un nouveau paramètre background: boolean.
Quand il vaut true, le bridge enveloppe la commande avec :

mkdir -p "$HOME/.gemini-bg"
LOG="$HOME/.gemini-bg/run-<id>.log"
nohup bash -lc '<user command>' > "$LOG" 2>&1 &
PID=$!
disown
sleep 0.2
kill -0 $PID    # confirme que le process n'a pas crashé dans la demi-seconde
echo "[background] started pid=$PID log=$LOG"

Bash termine immédiatement → Termux broadcast exit=0 dans la
milliseconde → le bridge renvoie un résultat propre avec PID + chemin
du log. Si le process crashe instantanément, on tail les 30 dernières
lignes du log et on retourne exit=1 pour que Gemini sache qu'il y a
eu un problème.

Usage depuis Gemini

run_shell_command(command="python -m http.server 8000", background=true)
→ [background] started pid=12345 log=/data/.../.gemini-bg/run-a1b2c3d4.log

run_shell_command(command="tail -n 80 /data/.../.gemini-bg/run-a1b2c3d4.log")
→ (serveur vivant, logs HTTP)

run_shell_command(command="kill 12345")

Le tool description explique explicitement au modèle quand utiliser
background: true (serveurs / watchers / daemons) et comment
inspecter / arrêter le process ensuite.

Escapes

Les single quotes dans la commande utilisateur sont correctement
échappées (' → '\'') pour éviter toute injection quand on ré-enveloppe
dans bash -lc '…'.

Test plan

  • Compile.
  • Lancer un serveur via la chat : background: true → message
    [background] started pid=… log=…, serveur accessible sur
    http://localhost:8000 depuis le navigateur.
  • tail du log depuis Gemini → voit les requêtes HTTP arriver.
  • kill <pid> via Gemini → process stoppé.
  • Une commande foregound classique reste inchangée (pas de
    régression pour ls, echo, etc.).

🤖 Generated with Claude Code

Summary by Sourcery

Add background execution support to Termux shell commands to avoid timeouts for long-running processes and document how to use it from the shell tool.

New Features:

  • Introduce a background flag to Termux shell command execution to detach long-running processes, redirect output to per-run log files, and return immediately with PID and log path.

Enhancements:

  • Extend the shell tool description and parameters to guide models on when and how to use background mode and how to inspect or stop background processes.
  • Include explicit metadata in shell tool results to indicate when a command was run in background mode.

Summary by CodeRabbit

  • New Features
    • Added support for background command execution, allowing commands to run asynchronously without blocking.
    • Background processes now detach automatically and return immediately with process information and log location.
    • Command output and errors are captured and logged for later review.

Long-running commands (servers, watchers, daemons) blocked the Termux
bridge's 12 s reply window, surfacing as "Termux did not reply" even
when the process was in fact running. Add a `background: true` flag
that wraps the command with nohup + output redirection to
~/.gemini-bg/run-<id>.log, disowns it, and returns within ~200 ms with
the PID and log path once the process is confirmed alive.

Gemini can then poll progress via `tail -n 80 <log>` and stop the
process with `kill <pid>` — both regular foreground shell calls.
@sourcery-ai

sourcery-ai Bot commented Apr 21, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Adds a background execution mode to Termux shell commands so long-running servers can be started without hitting the 12s timeout, including safe bash wrapping and updated tool metadata for the Gemini shell tool.

Sequence diagram for background Termux shell command execution

sequenceDiagram
    actor GeminiModel
    participant RunShellCommandTool
    participant TermuxBridge
    participant TermuxRunCommandService as Termux_RunCommandService
    participant Bash
    participant BackgroundProcess

    GeminiModel->>RunShellCommandTool: run_shell_command(command, background=true)
    RunShellCommandTool->>RunShellCommandTool: parse arguments
    RunShellCommandTool->>TermuxBridge: run(command, workdir, timeoutMs=12000, background=true)

    TermuxBridge->>TermuxBridge: wrapBackground(command)
    TermuxBridge->>TermuxRunCommandService: dispatch(wrappedCommand, workdir, timeoutMs)
    TermuxRunCommandService->>Bash: execute wrappedCommand

    Bash->>Bash: mkdir -p ~/.gemini-bg
    Bash->>Bash: LOG=~/.gemini-bg/run-<id>.log
    Bash->>BackgroundProcess: nohup bash -lc <user command> &
    Bash->>Bash: PID=$!
    Bash->>Bash: disown
    Bash->>Bash: sleep 0.2
    Bash->>BackgroundProcess: kill -0 $PID

    alt process alive
        Bash-->>TermuxRunCommandService: echo [background] started pid=<pid> log=<log>
        TermuxRunCommandService-->>TermuxBridge: exit=0, stdout
        TermuxBridge-->>RunShellCommandTool: Result(exitCode=0, stdout)
        RunShellCommandTool-->>GeminiModel: exit=0 mode=background cwd=...
    else process crashed immediately
        Bash-->>TermuxRunCommandService: echo error + tail last log lines
        TermuxRunCommandService-->>TermuxBridge: exit=1, stdout
        TermuxBridge-->>RunShellCommandTool: Result(exitCode=1, stdout)
        RunShellCommandTool-->>GeminiModel: exit=1 mode=background cwd=...
    end

    BackgroundProcess-->>BackgroundProcess: keeps running in Termux
Loading

Class diagram for updated TermuxBridge and RunShellCommandTool

classDiagram
    class TermuxBridge {
        -Context appContext
        -boolean autoSetupAttempted
        +suspend run(command: String, workdir: String?, timeoutMs: Long, background: Boolean): Result
        -suspend dispatch(command: String, workdir: String?, timeoutMs: Long): Result
        -boolean isInstalled()
        -boolean isWorkdirDenied(result: Result)
        -void triggerStorageBootstrap()
        -String wrapBackground(command: String)
        -String bashSingleQuote(s: String)
    }

    class RunShellCommandTool {
        -Workspace workspace
        -TermuxBridge termux
        +RunShellCommandTool(workspace: Workspace, termux: TermuxBridge)
        +suspend execute(call: ToolCall): ToolCallResult
    }

    class ToolCall {
        +Map~String, Any?~ arguments
    }

    class ToolCallResult

    class Workspace {
        +String absolutePath()
        +String? unreachableReason()
    }

    class Result {
        +boolean ok
        +int exitCode
        +String stdout
        +String stderr
    }

    TermuxBridge --> Result
    RunShellCommandTool --> TermuxBridge
    RunShellCommandTool --> Workspace
    RunShellCommandTool --> ToolCall
    RunShellCommandTool --> ToolCallResult
Loading

File-Level Changes

Change Details Files
Add background-mode wrapping for Termux shell commands so long-running processes detach cleanly while logging to per-run files.
  • Extend TermuxBridge.run to accept a background flag and choose between the raw command and a wrapped background version before dispatching.
  • Ensure both primary and fallback Termux dispatch paths reuse the same effective command, honoring the background mode even when the working directory is not usable.
  • Implement wrapBackground to build a nohup-based bash wrapper that creates ~/.gemini-bg, spools output to a run-specific log file, launches the user command via bash -lc in the background, disowns the child, then checks with kill -0 and either reports the PID/log or tails recent log lines and exits 1 on immediate crash.
  • Add bashSingleQuote helper to safely escape user commands for embedding inside a single-quoted bash -lc invocation, correctly handling embedded single quotes.
core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt
Expose the background execution capability through the RunShellCommandTool API and update its documentation and output format.
  • Update the RunShellCommandTool description to document foreground timeouts, when to use background mode, how logs are stored under ~/.gemini-bg, and how to tail logs or kill background processes.
  • Add a background boolean parameter to the tool schema with guidance on intended use for servers/daemons/watchers.
  • Plumb the background argument from the tool call into TermuxBridge.run so background requests use the new wrapper.
  • Annotate the tool's textual result with a mode=background marker when background execution is used, alongside exit code and cwd.
core-bridge/src/main/kotlin/com/gemini/bridge/tools/ShellTool.kt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Apr 21, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The changes introduce a background parameter to enable detached command execution in Termux. When enabled, commands are wrapped with nohup and disown, with output redirected to log files in $HOME/.gemini-bg/, allowing operations to proceed asynchronously while returning immediately to the caller.

Changes

Cohort / File(s) Summary
Core Termux Bridge
core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt
Added background: Boolean = false parameter to run(). Implements wrapBackground() to detach processes with nohup and disown, redirecting logs to $HOME/.gemini-bg/run-<id>.log. Added bashSingleQuote() helper for safe command quoting.
Shell Tool Wrapper
core-bridge/src/main/kotlin/com/gemini/bridge/tools/ShellTool.kt
Added background parameter to run_shell_command tool spec. Propagates flag to termux.run() and enriches result with mode=background indicator. Updated documentation with foreground timeout (~12s) and background execution semantics.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hark! With nohup and disown we go,
Commands vanish to the logs below,
Background whispers, detached and free—
Async magic as it should be!
~Run in silence, return with speed! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a background mode parameter to run_shell_command to address Termux timeout issues on servers.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/gemini-cli-ui-port-9gzwX-bg

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.

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt" line_range="221-223" />
<code_context>
+        val quoted = bashSingleQuote(command)
+        return buildString {
+            append("mkdir -p \"\$HOME/.gemini-bg\" && ")
+            append("LOG=\"\$HOME/.gemini-bg/run-$id.log\" && ")
+            append("nohup bash -lc $quoted > \"\$LOG\" 2>&1 & ")
+            append("PID=\$! && ")
</code_context>
<issue_to_address>
**suggestion (performance):** Background log files under ~/.gemini-bg can grow without bound; consider a retention/cleanup strategy.

Each background run writes to a new file in `~/.gemini-bg/` with no rotation or pruning, so logs can grow without limit. Please consider adding a simple retention mechanism (e.g., keep only the last N logs, cap total directory size, or provide a cleanup command) to avoid long‑term disk usage issues.

```suggestion
        return buildString {
            append("mkdir -p \"\$HOME/.gemini-bg\" && ")
            // simple retention: keep only the 20 most recent background logs
            append("{ ls -1t \"\$HOME/.gemini-bg\"/run-*.log 2>/dev/null | tail -n +21 | xargs -r rm -- 2>/dev/null || true ; } && ")
            append("LOG=\"\$HOME/.gemini-bg/run-$id.log\" && ")
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +221 to +223
return buildString {
append("mkdir -p \"\$HOME/.gemini-bg\" && ")
append("LOG=\"\$HOME/.gemini-bg/run-$id.log\" && ")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (performance): Background log files under ~/.gemini-bg can grow without bound; consider a retention/cleanup strategy.

Each background run writes to a new file in ~/.gemini-bg/ with no rotation or pruning, so logs can grow without limit. Please consider adding a simple retention mechanism (e.g., keep only the last N logs, cap total directory size, or provide a cleanup command) to avoid long‑term disk usage issues.

Suggested change
return buildString {
append("mkdir -p \"\$HOME/.gemini-bg\" && ")
append("LOG=\"\$HOME/.gemini-bg/run-$id.log\" && ")
return buildString {
append("mkdir -p \"\$HOME/.gemini-bg\" && ")
// simple retention: keep only the 20 most recent background logs
append("{ ls -1t \"\$HOME/.gemini-bg\"/run-*.log 2>/dev/null | tail -n +21 | xargs -r rm -- 2>/dev/null || true ; } && ")
append("LOG=\"\$HOME/.gemini-bg/run-$id.log\" && ")

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (2)
core-bridge/src/main/kotlin/com/gemini/bridge/tools/ShellTool.kt (1)

39-43: Consider documenting the failure mode and log-retrieval contract in the background param description.

The current description tells the model the happy path but not:

  • that if the process dies within ~200 ms the call returns exit=1 with the tail of the log (so the model should read stdout on failure rather than blindly retrying), and
  • that the log=<path> line in stdout is the canonical handle — stable wording helps the model parse it reliably for the follow-up tail/kill.

Purely a description tweak, no behavior change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-bridge/src/main/kotlin/com/gemini/bridge/tools/ShellTool.kt` around
lines 39 - 43, Update the "background" booleanProp description in ShellTool (the
"background" to booleanProp(...) entry) to document the failure mode and
log-retrieval contract: state that if the backgrounded process dies within ~200
ms the call returns exit=1 and prints the tail of the log to stdout (so callers
should read stdout on failure), and explicitly declare that a line of the form
"log=<path>" in stdout is the canonical, stable handle to use for subsequent
tail/kill operations. Keep the rest of the behavior text but append these two
clarifying sentences.
core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt (1)

218-236: Minor: ~/.gemini-bg/ log files are never rotated or cleaned up.

Each background invocation creates a new run-<id>.log and nothing ever prunes them. For a long-lived install this grows unbounded. Consider a best-effort cleanup of logs older than N days at the top of wrapBackground, e.g.:

+            append("find \"\$HOME/.gemini-bg\" -type f -name 'run-*.log' -mtime +7 -delete 2>/dev/null ; ")
             append("mkdir -p \"\$HOME/.gemini-bg\" || exit 1 ; ")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt` around
lines 218 - 236, In wrapBackground, add a best-effort cleanup step before
creating the new log so old run-<id>.log files under $HOME/.gemini-bg are pruned
(e.g. use a find ... -mtime +N -type f -name "run-*.log" -delete or move to
rotated names) and silence failures; update the buildString to prepend this
command (or a configurable retention N) so LOG creation and nohup still proceed
if cleanup fails, referencing wrapBackground and the
LOG="\$HOME/.gemini-bg/run-$id.log" variable to locate where to run the prune.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt`:
- Around line 218-236: In TermuxBridge.wrapBackground, the current single-line
chain lets the entire chain be backgrounded so PID=$! captures the subshell, not
the actual nohup bash process; split the commands so you run mkdir and export
LOG, then run nohup bash -lc <quoted> > "$LOG" 2>&1 </dev/null & and immediately
capture PID=$! (then run disown 2>/dev/null || true and the kill-check logic
against that PID); also add the stdin redirection </dev/null to the nohup
invocation and ensure the subsequent tail/exit logic still references the same
LOG and PID.

In `@core-bridge/src/main/kotlin/com/gemini/bridge/tools/ShellTool.kt`:
- Line 50: The current silent cast of call.arguments["background"] to Boolean in
ShellTool.kt drops non-Boolean JSON values (e.g., "true") and defaults to false;
change the extraction to coerce/validate multiple types instead: read the raw
value from call.arguments["background"], then handle Boolean (use as-is), String
(parse case-insensitive "true"/"false"), and Number (treat 1 as true, 0 as
false), and for any other or unparseable value either log a warning (include the
raw value) and default to a safe choice or throw a clear validation exception;
update the binding where background is defined (the val background assignment
inside ShellTool) and include a short log/validation message referencing the raw
argument so mis-typed inputs are visible at runtime.

---

Nitpick comments:
In `@core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt`:
- Around line 218-236: In wrapBackground, add a best-effort cleanup step before
creating the new log so old run-<id>.log files under $HOME/.gemini-bg are pruned
(e.g. use a find ... -mtime +N -type f -name "run-*.log" -delete or move to
rotated names) and silence failures; update the buildString to prepend this
command (or a configurable retention N) so LOG creation and nohup still proceed
if cleanup fails, referencing wrapBackground and the
LOG="\$HOME/.gemini-bg/run-$id.log" variable to locate where to run the prune.

In `@core-bridge/src/main/kotlin/com/gemini/bridge/tools/ShellTool.kt`:
- Around line 39-43: Update the "background" booleanProp description in
ShellTool (the "background" to booleanProp(...) entry) to document the failure
mode and log-retrieval contract: state that if the backgrounded process dies
within ~200 ms the call returns exit=1 and prints the tail of the log to stdout
(so callers should read stdout on failure), and explicitly declare that a line
of the form "log=<path>" in stdout is the canonical, stable handle to use for
subsequent tail/kill operations. Keep the rest of the behavior text but append
these two clarifying sentences.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f7d15b03-2797-44f4-a400-62b21f20bb05

📥 Commits

Reviewing files that changed from the base of the PR and between c74cbe1 and 5213bc7.

📒 Files selected for processing (2)
  • core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt
  • core-bridge/src/main/kotlin/com/gemini/bridge/tools/ShellTool.kt

Comment on lines +218 to +236
private fun wrapBackground(command: String): String {
val id = UUID.randomUUID().toString().take(8)
val quoted = bashSingleQuote(command)
return buildString {
append("mkdir -p \"\$HOME/.gemini-bg\" && ")
append("LOG=\"\$HOME/.gemini-bg/run-$id.log\" && ")
append("nohup bash -lc $quoted > \"\$LOG\" 2>&1 & ")
append("PID=\$! && ")
append("disown 2>/dev/null || true ; ")
append("sleep 0.2 ; ")
append("if kill -0 \$PID 2>/dev/null ; then ")
append(" echo \"[background] started pid=\$PID log=\$LOG\" ; ")
append("else ")
append(" echo \"[background] process died immediately; last log lines:\" ; ")
append(" tail -n 30 \"\$LOG\" 2>/dev/null || true ; ")
append(" exit 1 ; ")
append("fi")
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm bash precedence: is `A && B & C` parsed as `(A && B) & ; C` ?
# Expected: $! is the PID of the (A && B) group, not of B alone.
bash -c '
  true && sleep 3 & PID=$!
  echo "captured PID=$PID"
  ps -o pid,ppid,cmd -p $PID 2>/dev/null || echo "PID $PID not a direct sleep process"
  # Show children — if PID is a subshell wrapping nohup, sleep will be its child
  pgrep -P $PID | xargs -r ps -o pid,ppid,cmd -p
  wait
'

Repository: aciderix/gemini-android-app

Length of output: 174


🏁 Script executed:

#!/bin/bash

# Test 1: Verify bash operator precedence for `A && B & C`
# Does it parse as `(A && B) & ; C` or `(A && B & C)` ?
echo "=== Test 1: Operator Precedence ==="
bash -c '
  # If parsed as (A && B) & ; C then PID=$! captures the group
  # If parsed as (A && B & C) then the entire sequence is backgrounded
  true && (echo "B executed") & PID=$!
  echo "Captured PID=$PID"
  # Wait a moment for background job to complete
  sleep 0.5
  wait $PID 2>/dev/null && echo "Successfully waited for $PID"
'

# Test 2: More explicit test with process tree
echo -e "\n=== Test 2: What is $! when using && and & ? ==="
bash -c '
  # Create a marker file to track execution order
  true && sleep 10 & 
  PID=$!
  echo "Backgrounded PID=$PID"
  # Check if PID still exists
  if ps -p $PID > /dev/null 2>&1; then
    echo "PID $PID still running"
    ps -o pid,ppid,cmd -p $PID 2>/dev/null | head -5
  fi
  kill $PID 2>/dev/null
  wait 2>/dev/null
'

# Test 3: Verify the exact TermuxBridge pattern
echo -e "\n=== Test 3: Simulating TermuxBridge nohup pattern ==="
tmpdir=$(mktemp -d)
bash -c "
  mkdir -p \"$tmpdir\" && LOG=\"$tmpdir/test.log\" && nohup sleep 10 > \"\$LOG\" 2>&1 & PID=\$!
  echo \"Captured PID=\$PID\"
  # What process is $PID?
  ps -o pid,ppid,cmd -p \$PID 2>/dev/null | head -2
  # Cleanup
  kill \$PID 2>/dev/null || true
  wait 2>/dev/null || true
"
rm -rf "$tmpdir"

Repository: aciderix/gemini-android-app

Length of output: 296


🏁 Script executed:

#!/bin/bash
# Check if the code in the repository matches the review snippet
cat -n core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt | sed -n '218,236p'

Repository: aciderix/gemini-android-app

Length of output: 1095


🏁 Script executed:

#!/bin/bash

echo "=== Detailed Test: Process Tree Analysis ==="

tmpdir=$(mktemp -d)

# Test the actual pattern with detailed process inspection
bash -c "
  # This simulates the exact TermuxBridge pattern
  mkdir -p \"$tmpdir\" && LOG=\"$tmpdir/test.log\" && nohup bash -c 'sleep 30' > \"\$LOG\" 2>&1 & PID=\$!
  echo \"Captured PID=\$PID\"
  
  # Get details about the captured PID
  echo \"Process info for captured PID \$PID:\"
  ps -o pid,ppid,sid,cmd -p \$PID 2>/dev/null || echo \"PID \$PID not found\"
  
  # Show all processes in this shell's process group
  echo \"All processes in current shell environment:\"
  ps -o pid,ppid,sid,cmd 2>/dev/null | grep -E '(bash|sleep|PID)' | head -20
  
  # Wait briefly and check again
  sleep 1
  echo \"After 1 second - checking if sleep is still under PID \$PID:\"
  pgrep -P \$PID 2>/dev/null || echo \"No children under PID \$PID\"
  
  # Cleanup
  kill -9 \$PID 2>/dev/null || true
  wait 2>/dev/null || true
" 2>&1

# Also test: what is the difference between capturing on the same line vs different lines?
echo -e "\n=== Comparison: Same line vs different line ==="
bash -c "
  mkdir -p \"$tmpdir\" && LOG=\"$tmpdir/test2.log\" && nohup bash -c 'sleep 30' > \"\$LOG\" 2>&1 &
  BASHPID_LINE=\$BASHPID
  PID_SAME=\$!
  
  # Separate capture on next line
  mkdir -p \"$tmpdir\" && LOG=\"$tmpdir/test3.log\" && nohup bash -c 'sleep 30' > \"\$LOG\" 2>&1 &
  PID_NEXT=\$!
  
  echo \"PID captured on same line (after &): \$PID_SAME\"
  echo \"PID captured on next line: \$PID_NEXT\"
  
  # Cleanup
  kill -9 \$PID_SAME \$PID_NEXT 2>/dev/null || true
  wait 2>/dev/null || true
"

rm -rf "$tmpdir"

Repository: aciderix/gemini-android-app

Length of output: 423


🏁 Script executed:

#!/bin/bash
# Search for related code and comments
rg -B5 -A10 "wrapBackground" core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt

Repository: aciderix/gemini-android-app

Length of output: 1888


PID=$! captures the backgrounded subshell, not the nohup bash process.

In bash, & has lower precedence than &&. The line:

mkdir -p "$HOME/.gemini-bg" && LOG="…" && nohup bash -lc '…' > "$LOG" 2>&1 & PID=$! && disown …

parses as (mkdir && LOG=… && nohup bash … > "$LOG" 2>&1) & ; PID=$! && disown … — the entire chain runs in one backgrounded subshell. $! captures that subshell's PID, not the inner nohup bash process. Consequences:

  • nohup bash -lc '…' runs inside the subshell (not backgrounded again), so the subshell waits for it to complete before exiting.
  • $! is the temporary subshell's PID, which exits after launching nohup bash. The actual daemon process becomes a child of init.
  • The PID printed to the model ([background] started pid=$PID) is incorrect. When the model later runs kill <pid>, it targets a defunct subshell while the actual server continues running.
  • disown 2>/dev/null targets $! (the subshell), having no effect on the nohup child.

Suggested fix: don't chain with && across the &, and capture $! immediately after the nohup line:

🛠️ Proposed fix
     private fun wrapBackground(command: String): String {
         val id = UUID.randomUUID().toString().take(8)
         val quoted = bashSingleQuote(command)
         return buildString {
-            append("mkdir -p \"\$HOME/.gemini-bg\" && ")
-            append("LOG=\"\$HOME/.gemini-bg/run-$id.log\" && ")
-            append("nohup bash -lc $quoted > \"\$LOG\" 2>&1 & ")
-            append("PID=\$! && ")
-            append("disown 2>/dev/null || true ; ")
-            append("sleep 0.2 ; ")
+            append("mkdir -p \"\$HOME/.gemini-bg\" || exit 1 ; ")
+            append("LOG=\"\$HOME/.gemini-bg/run-$id.log\" ; ")
+            append("nohup bash -lc $quoted > \"\$LOG\" 2>&1 </dev/null & ")
+            append("PID=\$! ; ")
+            append("disown \$PID 2>/dev/null || true ; ")
+            append("sleep 0.2 ; ")
             append("if kill -0 \$PID 2>/dev/null ; then ")
             append("  echo \"[background] started pid=\$PID log=\$LOG\" ; ")
             append("else ")
             append("  echo \"[background] process died immediately; last log lines:\" ; ")
             append("  tail -n 30 \"\$LOG\" 2>/dev/null || true ; ")
             append("  exit 1 ; ")
             append("fi")
         }
     }

Also add </dev/null to prevent the backgrounded bash inheriting closed stdin from RunCommandService and receiving SIGTTIN on accidental reads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-bridge/src/main/kotlin/com/gemini/bridge/termux/TermuxBridge.kt` around
lines 218 - 236, In TermuxBridge.wrapBackground, the current single-line chain
lets the entire chain be backgrounded so PID=$! captures the subshell, not the
actual nohup bash process; split the commands so you run mkdir and export LOG,
then run nohup bash -lc <quoted> > "$LOG" 2>&1 </dev/null & and immediately
capture PID=$! (then run disown 2>/dev/null || true and the kill-check logic
against that PID); also add the stdin redirection </dev/null to the nohup
invocation and ensure the subsequent tail/exit logic still references the same
LOG and PID.


override suspend fun execute(call: ToolCall): ToolCallResult = runCatching {
val command = call.arguments["command"] as? String ?: error("command is required")
val background = call.arguments["background"] as? Boolean ?: false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find how tool arguments are deserialized — do they land as typed JSON (Boolean/Number/String)
# or everything as String? Look at the JSON parser feeding ToolCall.arguments.
rg -nP --type=kotlin -C4 '\bToolCall\s*\(' 
rg -nP --type=kotlin -C3 'arguments\s*[:=]\s*Map'
rg -nP --type=kotlin -C5 'fun\s+\w*[Pp]arse\w*Tool|fromJson|decodeFromString.*Tool'

Repository: aciderix/gemini-android-app

Length of output: 4089


🏁 Script executed:

rg -nP --type=kotlin 'fun\s+jsonToMap|jsonToMap\s*\(' -A 10

Repository: aciderix/gemini-android-app

Length of output: 5582


🏁 Script executed:

cat -n core-bridge/src/main/kotlin/com/gemini/bridge/tools/ShellTool.kt | head -70 | tail -30

Repository: aciderix/gemini-android-app

Length of output: 1952


🏁 Script executed:

rg -nP --type=kotlin 'fun\s+booleanProp|class.*Prop' -A 5 | head -60

Repository: aciderix/gemini-android-app

Length of output: 688


background parameter silently defaults to false if the model sends a non-Boolean value (e.g., string "true").

The schema declares "type": "boolean", but booleanProp() is only a schema builder—it doesn't enforce runtime type validation. The jsonToMap() function preserves JSON types as-is, so call.arguments["background"] could be a Boolean, String, or Number depending on what the API returns. The current cast as? Boolean ?: false treats all non-Boolean values as false, creating a silent failure: a model sending "background": "true" would execute in foreground mode and hit the 12 s Termux timeout, making the failure harder to diagnose.

Consider accepting multiple types:

Suggested fix
-        val background = call.arguments["background"] as? Boolean ?: false
+        val background = when (val v = call.arguments["background"]) {
+            is Boolean -> v
+            is String -> v.equals("true", ignoreCase = true)
+            is Number -> v.toInt() != 0
+            else -> false
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val background = call.arguments["background"] as? Boolean ?: false
val background = when (val v = call.arguments["background"]) {
is Boolean -> v
is String -> v.equals("true", ignoreCase = true)
is Number -> v.toInt() != 0
else -> false
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core-bridge/src/main/kotlin/com/gemini/bridge/tools/ShellTool.kt` at line 50,
The current silent cast of call.arguments["background"] to Boolean in
ShellTool.kt drops non-Boolean JSON values (e.g., "true") and defaults to false;
change the extraction to coerce/validate multiple types instead: read the raw
value from call.arguments["background"], then handle Boolean (use as-is), String
(parse case-insensitive "true"/"false"), and Number (treat 1 as true, 0 as
false), and for any other or unparseable value either log a warning (include the
raw value) and default to a safe choice or throw a clear validation exception;
update the binding where background is defined (the val background assignment
inside ShellTool) and include a short log/validation message referencing the raw
argument so mis-typed inputs are visible at runtime.

@aciderix
aciderix merged commit 86edda0 into main Apr 21, 2026
4 checks passed
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.

2 participants