From 20d54088e1b378a2bda8cd91a1775cab9bf08ceb Mon Sep 17 00:00:00 2001 From: Beforerr Date: Sun, 26 Jul 2026 21:01:46 +0900 Subject: [PATCH] feat: auto-close Codex-owned sessions --- README.md | 2 +- docs/architecture.md | 2 -- go/owner.go | 12 ++++++------ go/owner_test.go | 27 +++++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index eb1ba88..dbb551f 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ repld stop # shutdown daemon `` is `julia`, `python3`, `R`, `wolframscript`, an absolute/relative interpreter path, etc. repld's own flags (`--session`/`--lang`/`--trace`/`--fresh`) go before ``; after it, every flag forwards verbatim to the interpreter (e.g. Julia's `--project=DIR`, `+1.11` for juliaup) except native eval/print flags (`-e`/`-c`/`-E`). Each call routes to a persistent session keyed by language + interpreter + `--session`/cwd. Forwarded interpreter flags configure a session only when it starts. -A session auto-closes once the agent process that created it exits. +A session created from Claude Code or Codex auto-closes once its agent process exits. `repld free ` pins a session against auto-closing; `repld --owner-pid 0 ...` opts out at creation. ## Architecture diff --git a/docs/architecture.md b/docs/architecture.md index fb6732b..4fdd8ae 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -37,8 +37,6 @@ client-side (`absExe`); a bare name is looked up in PATH on the daemon. (`go/daemon.go`). The per-session `Adapter` is chosen from each request's `lang` (`SessionManager.getOrCreate`). Runs until `stop` (or optional `daemon --idle-timeout SECS`; default 0 = never). -**Owner lease**: the daemon closes a session after its owning process exits. -Ownerless sessions persist until closed; `repld free` removes an existing lease. **Session** (`go/session.go`): wraps one interpreter subprocess. The adapter supplies the launch argv, the embedded runtime source + its load statement, the diff --git a/go/owner.go b/go/owner.go index 52785d1..dbce302 100644 --- a/go/owner.go +++ b/go/owner.go @@ -25,11 +25,10 @@ func resolveOwner(explicit string) (int, int64) { if env := os.Getenv("REPLD_OWNER_PID"); env != "" { return ownerFrom(env) } - if os.Getenv("CLAUDECODE") != "" { + if os.Getenv("CLAUDECODE") != "" || os.Getenv("CODEX_THREAD_ID") != "" { // The immediate parent is the per-call shell; the harness sits some - // variable number of levels up (Claude Code wraps Bash as a compound - // `zsh -c`, and users add timeout/env/xargs wrappers), so walk the - // ancestry rather than assuming a fixed depth. + // variable number of levels up, so walk the ancestry rather than + // assuming a fixed depth. if h := harnessPID(os.Getppid(), ppidOf, procIdent); h > 0 { st, _ := procInfo(h) return h, st @@ -55,11 +54,12 @@ func harnessPID(start int, parent func(int) (int, bool), ident func(int) string) return 0 } -// Shell argv may mention ~/.claude; match executable basenames only. +// Shell argv may mention agent config paths; match executable basenames only. func identifiesHarness(ident string) bool { sep := func(r rune) bool { return r == 0 || r == ' ' || r == '\t' || r == '\n' || r == '\r' } for _, tok := range strings.FieldsFunc(ident, sep) { - if filepath.Base(tok) == "claude" { + switch filepath.Base(tok) { + case "claude", "codex": return true } } diff --git a/go/owner_test.go b/go/owner_test.go index 0c517a1..843e5df 100644 --- a/go/owner_test.go +++ b/go/owner_test.go @@ -39,3 +39,30 @@ func TestOwnerLease(t *testing.T) { require.NotContains(t, m.sessions, owned) require.Contains(t, m.sessions, freed) } + +func TestIdentifiesHarness(t *testing.T) { + for _, tc := range []struct { + ident string + want bool + }{ + {"claude\x00/usr/local/bin/claude\x00", true}, + {"codex\x00/opt/homebrew/bin/codex\x00--yolo\x00", true}, + {"zsh\x00/bin/zsh\x00-c\x00repld julia -e 1\x00", false}, + {"node\x00/usr/bin/node\x00/Users/me/.claude/cli.js\x00", false}, + {"codex-wrapper\x00/usr/local/bin/codex-wrapper\x00", false}, + } { + t.Run(tc.ident, func(t *testing.T) { + require.Equal(t, tc.want, identifiesHarness(tc.ident)) + }) + } +} + +func TestHarnessPID(t *testing.T) { + parents := map[int]int{40: 30, 30: 20, 20: 1} + idents := map[int]string{40: "zsh", 30: "codex", 20: "login"} + parent := func(pid int) (int, bool) { + ppid, ok := parents[pid] + return ppid, ok + } + require.Equal(t, 30, harnessPID(40, parent, func(pid int) string { return idents[pid] })) +}