Skip to content

fix(statusline): concurrent session creates no longer fall out of tmux - #531

Merged
Ark0N merged 1 commit into
masterfrom
fix/statusline-exporter-tmp-race
Oct 4, 2026
Merged

Ark0N merged 1 commit into
masterfrom
fix/statusline-exporter-tmp-race

Conversation

@Ark0N

@Ark0N Ark0N commented Oct 4, 2026

Copy link
Copy Markdown
Owner

When Claude sessions are created at the same moment (the codeman skill's spawn_workers, or a multi-tab Run), some of them can silently end up outside tmux: they run on Codeman's direct-PTY fallback, so they don't survive a server restart, while the create call still reports success. This PR fixes the cause, a temp-file name that wasn't unique, with a one-line change plus a regression test. No API, UI or behavior changes otherwise.

Cause

Every claude launch goes through ensureStatusLineExporterScript() (src/hooks-config.ts), which keeps ~/.codeman/statusline-exporter.sh up to date for the plan-usage chip. When the script's content needs changing (a fresh data dir, or a release that changes the script), it writes a temp file and renames it into place, so live sessions never execute a half-written file.

The temp name was statusline-exporter.sh.<pid>.<Date.now()>.tmp. Within one server process, two creates in the same millisecond produce the same temp path. The first writer renames it away, and the others then fail with ENOENT on chmod or rename. TmuxManager.createSession() treats that as a mux failure, and Session falls back to a direct PTY:

[Session] Failed to create mux session, falling back to direct PTY: Error: ENOENT: no such file or directory, rename '/home/…/.codeman/statusline-exporter.sh.2505760.1791133404959.tmp' -> '/home/…/.codeman/statusline-exporter.sh'

Fix

The temp suffix now comes from randomBytes(6), the same pattern already used by the skill-file writer in the same file and by user-store.ts. Concurrent writers each rename their own complete, executable copy into place; the content is identical, so whichever lands last is correct.

Testing

  • New test in test/hooks-config.test.ts: freezes Date.now() (which is what two creates in one millisecond amount to), runs eight refreshes at once, and checks they all succeed, the script is fresh and executable, and no temp files are left. On master it fails with the same ENOENT ... rename error as the log line above; with the fix it passes. hooks-config and statusline-cli-flag suites pass (104 tests), and tsc, eslint and prettier are clean.

  • End to end on a fresh isolated instance (CODEMAN_INSTANCE, its own tmux socket, data dir and cases dir), firing four concurrent claude POST /api/quick-start calls with real Claude Code 2.1.289:

    round 1 round 2
    master 2 of 4 in tmux, 2 direct-PTY fallbacks 2 of 4 in tmux, 2 fallbacks
    this PR 4 of 4 in tmux, 0 fallbacks 4 of 4 in tmux, 0 fallbacks

    On master, quick-start returned success for all four in both rounds.

Notes

  • How often this happens in practice: only while the script needs rewriting, so on a long-running install it's the first concurrent creates after an upgrade that changes the exporter script, or on a fresh install. When it hits, the affected sessions lose tmux persistence without any visible error.
  • src/custom-model-hosts.ts has a similar fixed temp name (<file>.<pid>.tmp) on an async write path, but its writers are user-driven route handlers and it also has a separate read-modify-write question, so it's left out of this PR.
  • No changeset here; it goes in with the release.

… creates stay in tmux

ensureStatusLineExporterScript() rewrites ~/.codeman/statusline-exporter.sh
via a temp file + rename whenever the script content changes (a fresh data
dir, or a release that changes it). The temp name was pid + Date.now(), so
claude sessions created in the same millisecond (spawn_workers, a multi-tab
Run) shared one temp path: the first rename consumed it and every other
writer failed with ENOENT on chmod or rename. createSession() treats that as
a mux failure and falls back to a direct PTY, so those sessions silently ran
outside tmux (no reattach after a server restart) while quick-start still
reported success.

Measured on a fresh isolated instance, 4 concurrent claude quick-starts:
master put 2 of 4 in tmux in both rounds; with this change 4 of 4, both
rounds. The temp suffix now comes from randomBytes, like the skill writer in
the same file and user-store.ts already do. The new test freezes Date.now()
and runs eight refreshes at once; it fails on master with the same ENOENT.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N
Ark0N merged commit 574f3db into master Oct 4, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Oct 4, 2026
…#520, #521, #522, #523, #524, #530, #531)

One consolidated minor changeset with the Thanks block first; the four contributor changesets (#520, #521, #522, #523) are folded into it.

Co-Authored-By: Claude Opus 5.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.

2 participants