Skip to content

Windows (Git Bash): codex bridge respawns every few seconds and old bridges accumulate — launcher and bridge record the pidfile in different PID namespaces #458

Description

@chemica-tan

Environment

  • agmsg v1.1.10, Windows 11, Git for Windows 2.53 (MSYS2 runtime), Node 22, codex CLI 0.144.5 (npm)
  • Repro'd with a single TUI: delivery.sh set monitor codex <project> → shim-launched
    codex$agmsg actas <role> as the first turn
  • Mechanism re-verified against current main (3f87d60) — all code paths cited below are present there

Observed

Within ~2 minutes of the bridge arming:

  • a new codex-bridge.js process was spawned every ~7–15 s (launcher backoff cadence),
  • none of the old ones were ever killed — 13 live bridges at the 2-minute mark,
    every one of them subscribed (duplicate-delivery risk),
  • run/codex-bridge.<team>.<role>.log shows one resumed thread … / armed … pair per
    respawn,
  • the pidfile value alternates between two kinds of numbers (large MSYS pids and
    native Windows pids — see below),
  • delivery.sh status reports the armed bridge as stale,
  • delivery.sh set off cannot kill the live bridges (it believes they are already
    dead and only removes their run files).

The only reason this stays bounded in practice is the bridge's own exit-on-close:
when the TUI quits and the app-server goes away, every accumulated bridge exits on
websocket close. During a long session, the count grows without limit.

(Full respawn timeline / bridge-log excerpts available on request — happy to attach.)

Root cause

The bridge pidfile is written in two different PID namespaces:

writer value namespace
launcher, right after spawn (codex-bridge-launcher.sh:324-325launched_pid=$! → pidfile) nohup's $! MSYS
bridge, on startup (codex-bridge.js writeMeta()process.pid) node's pid native Windows

Every reader then checks liveness in its own namespace only:

  • the launcher's reuse check does kill -0 "$bridge_pid" — Git Bash kill only sees
    MSYS pids, so once writeMeta() has overwritten the pidfile with the native pid,
    the check is permanently false. The launcher concludes the bridge is dead,
    removes the pidfile (without killing anything — it "knows" the process is gone)
    and spawns a replacement. Repeat forever: that is the accumulation.
  • the bridge's ensureSingleInstance() does process.kill(existing, 0) — node uses
    native pids, so the MSYS pid the launcher recorded raises ESRCH, the guard clears
    the "stale" files and proceeds. The single-instance defence is bypassed in the
    other direction, which is why the replacements never refuse to start. (The Fix Codex bridge self-PID restart loop #444
    self-PID clause doesn't engage here: on Windows the launcher's $! is an MSYS pid
    and never equals the bridge's own process.pid, so the guard always lands in the
    ESRCH branch.)
  • stop_codex_bridge() in delivery.sh and the status probe in codex _delivery.sh
    use the same bare kill -0, hence the wrong status output and the no-op set off.

On Linux/macOS $! and process.pid live in the same namespace, so the double write
is harmless — this is Windows-only.

Relation to known issues:

Suggested fix (happy to send a PR)

Unify the pidfile on the native namespace and reuse the existing helpers:

  1. launcher: record _compat_get_winpid "$!" (from lib/compat.sh) instead of $!
    on MSYS; keep $! for wait. Measured on Git for Windows 2.53 with the default
    native Node: the WINPID of $! is the bridge's own process.pid (the MSYS
    "exec" of a native binary maps the MSYS pid to the exec'd child), so the record
    converges with what writeMeta() publishes and ensureSingleInstance()'s
    existing self-PID clause keeps working unchanged. (Non-default wrappers/shims
    would need their own verification.) If the WINPID mapping cannot be resolved,
    fail closed — never publish a raw MSYS pid, since every reader would judge it
    permanently dead.
  2. liveness via _agmsg_pid_alive; teardown via taskkill /PID <pid> /T /F on
    MSYS (tree kill also reaps the leftover MSYS exec stub), plain kill elsewhere.
  3. stop_codex_bridge() / the launcher reuse check / codex _delivery.sh status:
    same helpers, plus a cmdline identity check before killing (matching the idiom
    the app-server teardown already uses). A live pid whose cmdline cannot be read
    right now (transient CIM failure) must be treated conservatively — neither
    killed nor forgotten — or the failure path quietly re-creates the accumulation.

Off Windows, the pid-conversion paths are MSYS-gated and _agmsg_pid_alive
degrades to kill -0, so the default node …/codex-bridge.js path behaves as
before; the added cmdline identity guard is the one behavioral addition there
(it declines to kill a live pid it cannot attribute to the bridge).

Validation of the fix (we run it downstream)

We have been running this fix on the same Windows machine since 2026-07-20:

Happy to turn this into a PR if that's welcome. It should also resolve the
kill -0 side of #415, assuming no further publication-order edge cases surface
in that flow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions