agent: the managed daemon's subcommand helpers are no stale Codex copy (#1461)
Codex keeps a long-lived helper next to the managed app-server (codex
app-server daemon pid-update-loop). It matches the codex startsWith
pattern, carries no --managed-daemon, sits under no app bundle or
Application Support folder, so codexCopyKind called it "cli": a helper
that started before magpie's last change to what Codex reads counted as
"A Codex in a terminal" forever, a daemon restart didn't end it, and the
only way to clear the row was killing a process the user can't name.
codexCopyKind now answers "helper" for an app-server daemon <sub>
command line, and StaleCopies skips it: it reads no list and there is no
Codex to quit. The daemon itself (--managed-daemon) is told apart
before, and a terminal Codex still counts.
Red/green: TestCodexDaemonHelperIsNoStaleCopy runs the helper alone from
before magpie's change — FAIL on the old code (one stale "cli" copy),
passes now; a terminal codex from before still counts 1.
Generated-by: GLM-5.3-Flash (ZCode)
Reviewed and landed by the maintainer (cherry-pick of b40a1dd0 from
ggbdpq:fix/codex-daemon-helper-1461, lychjck's report). Run on main at
c98160c0 with this commit on top, macOS arm64:
- On this Mac (ps, read only): the installed codex-cli 0.162.0 binary has
the subcommand `app-server daemon pid-update-loop` ("[internal] Run the
detached pid-backed standalone updater loop"): an updater, it reads no
model list. The managed daemons running here are
`codex app-server --listen unix:// --managed-daemon` and stay "daemon".
No pid-update-loop helper was running on this Mac at review time, so the
reporter's live row was not reproduced here; the fixture is the command
line from the report. provider's isManagedDaemon already names the same
helper as no daemon.
- TestCodexDaemonHelperIsNoStaleCopy with main's connectinfo.go: FAIL,
"the daemon's helper counted a stale copy: [{Kind:cli ...}], want none";
passes with the change.
- gofmt -l: clean. go vet -tags nogui ./internal/agent for darwin, linux
and windows; GOOS=linux go vet -tags nogui ./... and GOOS=windows go vet
./...: clean. go build, GOOS=linux go build -tags nogui, GOOS=windows
go build: ok.
- go test -tags nogui -count=1 ./... under a scratch HOME with GOPATH,
GOMODCACHE and GOCACHE pinned: 44 packages ok, none failed.
- Not run: GUI tests (no GUI change), Windows or Linux hardware (the
stale-copy check doesn't run on Windows; Linux uses the same ps path).
This commit is contained in:
1 parent
c98160c045
commit
74c48ac060
2 files changed
+71
-3
No files matched your search
@@ -128,3 +128,46 @@ func TestCodexStaleCopiesSayWhichIsLeft(t *testing.T) {
|
||||
t.Fatalf("Stale: %d, want 6 (one app, not one per process)", n)
|
||||
}
|
||||
}
|
||||
|
||||
// lychjck's #1461: the managed daemon keeps a long-lived helper next to it
|
||||
// (codex app-server daemon pid-update-loop) that a daemon restart doesn't
|
||||
// end and that reads no list. It matches the codex startsWith pattern and
|
||||
// no --managed-daemon, so it was counted as a "cli" copy from before
|
||||
// magpie's change: "A Codex in a terminal" that doesn't exist, forever,
|
||||
// and only killing the process cleared the row. A daemon helper is no
|
||||
// copy to reopen.
|
||||
func TestCodexDaemonHelperIsNoStaleCopy(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
t.Setenv("USERPROFILE", home)
|
||||
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
||||
t.Setenv("XDG_CACHE_HOME", filepath.Join(home, ".cache"))
|
||||
t.Setenv("CODEX_HOME", "")
|
||||
dir := filepath.Join(home, ".codex")
|
||||
os.MkdirAll(dir, 0o755)
|
||||
cfg := filepath.Join(dir, "config.toml")
|
||||
os.WriteFile(cfg, []byte("model = \"gpt-6.1-sol\"\nopenai_base_url = \"http://127.0.0.1:3425/backend-api/codex\"\n"), 0o600)
|
||||
then := time.Now().Add(-2 * time.Hour)
|
||||
os.Chtimes(cfg, then, then)
|
||||
|
||||
was := running
|
||||
t.Cleanup(func() { running = was })
|
||||
running = func(string) []runningProc {
|
||||
return []runningProc{
|
||||
// the helper, from before magpie's change
|
||||
{3 * time.Hour, "/opt/homebrew/bin/codex app-server daemon pid-update-loop"},
|
||||
}
|
||||
}
|
||||
if got := codex(home).StaleCopies(); len(got) != 0 {
|
||||
t.Fatalf("the daemon's helper counted a stale copy: %+v, want none", got)
|
||||
}
|
||||
// a terminal Codex from before is still one
|
||||
running = func(string) []runningProc {
|
||||
return []runningProc{
|
||||
{3 * time.Hour, "/opt/homebrew/bin/codex"},
|
||||
}
|
||||
}
|
||||
if n := codex(home).Stale(); n != 1 {
|
||||
t.Fatalf("a terminal codex: %d stale, want 1", n)
|
||||
}
|
||||
}
|
||||
@@ -187,9 +187,10 @@ func (a *Agent) Stale() int { return len(a.StaleCopies()) }
|
||||
type StaleCopy struct {
|
||||
// Kind, for Codex: "app" (the ChatGPT or Codex desktop app, its
|
||||
// app-server and helpers), "ide" (an editor extension's), "daemon"
|
||||
// (the background app-server the CLI leaves running) or "cli"; ""
|
||||
// for other agents; "embedded" is another app's own Codex, named by
|
||||
// App
|
||||
// (the background app-server the CLI leaves running), "helper" (a
|
||||
// subcommand the daemon spawns beside itself, no copy to reopen) or
|
||||
// "cli"; "" for other agents; "embedded" is another app's own Codex,
|
||||
// named by App
|
||||
Kind string `json:"kind,omitempty"`
|
||||
App string `json:"app,omitempty"`
|
||||
Since time.Time `json:"since"`
|
||||
@@ -236,6 +237,12 @@ func (a *Agent) StaleCopies() []StaleCopy {
|
||||
if a.ID == "codex" {
|
||||
var app string
|
||||
c.Kind, app = codexCopyKind(p.cmd)
|
||||
if c.Kind == "helper" {
|
||||
// a daemon subcommand's helper reads no list, a
|
||||
// restart of the daemon doesn't end it, and there
|
||||
// is no Codex to quit (#1461)
|
||||
continue
|
||||
}
|
||||
if c.Kind == "embedded" {
|
||||
c.App = app
|
||||
}
|
||||
@@ -273,6 +280,11 @@ func codexCopyKind(cmd string) (kind, app string) {
|
||||
// the outermost bundle: ChatGPT.app's codex runs from a
|
||||
// CodexCLI.app inside it
|
||||
return "app", exe[:strings.Index(exe, ".app/")+len(".app")]
|
||||
case daemonHelper(cmd):
|
||||
// a subcommand the managed daemon spawns beside itself (codex
|
||||
// app-server daemon pid-update-loop): long-lived, reads no list,
|
||||
// and outlives a daemon restart (#1461)
|
||||
return "helper", ""
|
||||
case slices.Contains(strings.Fields(cmd), "app-server"):
|
||||
// an app that ships a Codex of its own and talks to its
|
||||
// app-server (Agents Anywhere's connector, from its folder in
|
||||
@@ -290,6 +302,19 @@ func codexCopyKind(cmd string) (kind, app string) {
|
||||
return "cli", ""
|
||||
}
|
||||
|
||||
// daemonHelper says the command line is a subcommand the managed daemon
|
||||
// spawns beside itself: app-server daemon <sub> (pid-update-loop, …). The
|
||||
// daemon itself carries --managed-daemon and is told apart above.
|
||||
func daemonHelper(cmd string) bool {
|
||||
f := strings.Fields(cmd)
|
||||
for i, s := range f {
|
||||
if s == "app-server" && i+2 < len(f) && f[i+1] == "daemon" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// elapsed reads ps's etime, [[dd-]hh:]mm:ss.
|
||||
func elapsed(s string) (time.Duration, bool) {
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
Reference in new issue
Block a user