fix: strict attach does not replay log for dead sessions#26
Open
rmorgans wants to merge 28 commits intomobydeck:mainfrom
Open
fix: strict attach does not replay log for dead sessions#26rmorgans wants to merge 28 commits intomobydeck:mainfrom
rmorgans wants to merge 28 commits intomobydeck:mainfrom
Conversation
- openpty fallback now closes master fd on grantpt/unlockpt/ptsname failure instead of leaking it - Add _Static_assert for SCROLLBACK_SIZE power-of-two requirement and packet buffer fitting in uint8_t length field Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- socket_with_chdir: always restore cwd, even on socket failure - expand_sockname: check malloc return before use - rotate_log/pty_activity: disable logging on write failure instead of silently losing data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract write_all() and use it everywhere a socket write must be complete-or-fail. Fixes spurious disconnects under memory pressure or signal interruption that returns a short count. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
send_kill() still used a bare write() for the kill packet. Apply the same write_all() retry loop as push and attach paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- write_all: return -1 with errno=EIO when write() returns 0 (no progress), preventing an infinite retry loop - Include fault injection tests (preload_short_write.c) that force short socket writes to deterministically verify the retry logic in push and kill paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Zero passes the power-of-two bitmask check but is not a valid ring buffer size. Add SCROLLBACK_SIZE > 0 guard. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Once logging is disabled (log_fd = -1), no code path should increment log_written, call rotate_log, or lseek/write the fd. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace printf/exit in signal handlers with volatile sig_atomic_t flags. Use sigaction() instead of signal() to control SA_RESTART explicitly: SA_RESTART for SIGWINCH (benign), no SA_RESTART for fatal signals so select() returns EINTR promptly. Handle EINTR in read paths. write_all() stops retrying EINTR when die_signal is set. When stdout itself is wedged (blocked write to PTY), the deferred-signal exit path uses TCSANOW + _exit() to avoid hanging in printf or atexit handlers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
C harness using forkpty() for exact PID targeting — no pkill races or heuristic process identification. Tests SIGWINCH survival, SIGTERM exit, SIGHUP exit, SIGTERM during blocked stdout write, and detach character. Wire harness into test.sh with TAP folding that preserves diagnostic lines for debuggability. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
create_socket restored the original umask before calling bind(2).
With a typical shell umask of 022, bind created the socket file with
mode 0755 (S_IXUSR set). chmod(0600) was called right after, but the
tiny window between bind and chmod was enough for a concurrent
`atch list` to see the stale execute bit and report a freshly started
session as [attached] — even though no client had ever connected.
The fix switches to umask(0177) before bind so the kernel creates the
socket file directly at mode 0600 (0777 & ~0177). S_IXUSR is never
present on the socket path during creation, closing the TOCTOU window
entirely. The subsequent chmod(0600) is kept for explicitness and to
guard against any platform-specific deviations.
Adds regression test 24 (start-inside-session) that:
- starts an outer session and attaches a python client to it
- starts an inner session with ATCH_SESSION set (simulating being
inside the outer session)
- asserts the inner session socket has no execute bit immediately
after creation
- asserts `atch list` does not show the inner session as [attached]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When Ctrl+\ was pressed, process_kbd called exit(0) without notifying the master via MSG_DETACH. The master only learned about the detach when it received EOF on the closed fd, introducing a window where a concurrent `atch list` could read the stale S_IXUSR bit and display "[attached]" for a session that was already detached. Send MSG_DETACH synchronously before calling exit(0), mirroring the existing suspend-key (VSUSP) path. This ensures the master clears the S_IXUSR mode bit within one select cycle — before the client exits. Adds regression tests (test 23) exercising the MSG_DETACH → list flow across single and multi-session attach/detach cycles. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Large session logs (>1MB) caused an apparent infinite scroll on reattach. Seek to the last SCROLLBACK_SIZE bytes before replaying, consistent with the in-memory ring buffer size. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…estry check The anti-recursion guard in attach_main relied solely on ATCH_SESSION to detect self-attach attempts. Because ATCH_SESSION is an inherited env var it could be stale (e.g. the variable set inside a session that has since been detached-from, or a shell-config export), causing every subsequent attach to be incorrectly blocked with "cannot attach from within itself". Fix: the master now writes the pty-child PID to <sockname>.ppid on session start and removes it on cleanup. attach_main reads this file and walks the process-ancestry tree (via libproc on macOS, /proc on Linux) to verify that the calling process is genuinely a descendant of the session shell before blocking the attach. If the .ppid file is absent or the recorded PID is not an ancestor, ATCH_SESSION is treated as stale and the attach proceeds. The ancestry check is also hoisted before require_tty() in cmd_attach and the legacy -a/-A/-c routes so the correct diagnostic fires even without a controlling terminal. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
log_written tracked bytes since last rotation, not actual file size. After open_log(), the file could already hold log_max_size bytes but log_written started at 0, so another log_max_size was written before rotation triggered. Initialize log_written from the fd position after rotate_log() so the counter reflects reality.
Documents fork identity, architectural principles (raw PTY passthrough, no emulation, no deps), C style conventions, upstream policy, build instructions, and test process. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents all commands (attach, new, start, run, push, kill, clear, list, current), all options (-e, -E, -r, -R, -z, -q, -t, -C, -f), FILES, ENVIRONMENT (ATCH_SESSION), EXIT STATUS, and EXAMPLES sections. Includes tests/test_man.sh with 33 TAP assertions verified via mandoc. Also removes the *.1 gitignore rule so that the committed man page is tracked directly rather than generated by pandoc. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds PREFIX ?= /usr/local and an install target that copies: - the atch binary to $(PREFIX)/bin/atch - the man page to $(PREFIX)/share/man/man1/atch.1 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… push paths # Conflicts: # tests/test.sh
… blocked-write fix
…and log writes # Conflicts: # master.c
Cycles 50 sessions under ulimit -n 64 to verify that the openpty fallback properly closes fds on failure paths. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Verifies that a failed socket operation via socket_with_chdir does not leave the process in the wrong working directory. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts: # tests/test.sh
# Conflicts: # tests/test.sh
attach_main() with noerror=0 (strict attach) no longer calls replay_session_log() when the session is not running. It just prints the appropriate error message and exits. The smart-default path (atch <session>) still replays the log via its own explicit call before creating a new session. Use 'atch tail <session>' to view logs for dead sessions. Closes mobydeck#20 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This was referenced Mar 15, 2026
Verifies that 'atch attach' does not replay the session log when the session has exited. The log content must not appear in the command output. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
rmorgans
added a commit
to rmorgans/atch
that referenced
this pull request
Mar 15, 2026
…ot replay log # Conflicts: # tests/test.sh
rmorgans
added a commit
to rmorgans/atch
that referenced
this pull request
Mar 15, 2026
replay-log: use smart-open path instead of strict attach, which no longer replays dead-session logs after PR mobydeck#26. dash-binary env var: resolve symlink source to absolute path (fixes macOS), replace fixed sleep with poll loop (fixes CI timing). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
atch attach <session>no longer replays the session log when the session is dead. It prints the appropriate error message ("does not exist" / "is not running") and exits with code 1, matching the documented contract ("strict attach: fail if session missing").atch <session>) still replays the log before creating a new session — that behavior is unchanged.atch tail <session>to explicitly view logs for dead sessions.Closes #20
Test plan
atch start foo sleep 5 && sleep 6 && atch attach foo→ prints error, no log replayatch foo(smart default, dead session with log) → still replays log before creatingatch tail foo→ shows log contents as expected🤖 Generated with Claude Code