Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ repld stop # shutdown daemon

`<exe>` is `julia`, `python3`, `R`, `wolframscript`, an absolute/relative interpreter path, etc. repld's own flags (`--session`/`--lang`/`--trace`/`--fresh`) go before `<exe>`; 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 <id | --session=LABEL>` pins a session against auto-closing; `repld --owner-pid 0 <exe> ...` opts out at creation.

## Architecture
Expand Down
2 changes: 0 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions go/owner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
}
Expand Down
27 changes: 27 additions & 0 deletions go/owner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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] }))
}
Loading