Skip to content

fix(agy): pure SpawnArgs, no temp-file leak, forward signals to agy#54

Merged
dmora merged 1 commit into
mainfrom
fix/agy-temp-file-leak-and-orphan
Jun 7, 2026
Merged

fix(agy): pure SpawnArgs, no temp-file leak, forward signals to agy#54
dmora merged 1 commit into
mainfrom
fix/agy-temp-file-leak-and-orphan

Conversation

@dmora

@dmora dmora commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Addresses post-merge review feedback on #53 from Gemini Code Assist and Codex.

Issues fixed

# Reviewer Issue
Temp-file leak Gemini (medium), Codex P2 SpawnArgs created a temp log file → leaked on every Engine.Validate() (zero session), leaked again because Start re-calls SpawnArgs and overwrites the path, leaked on single-turn runs (no ResumeArgs), and violated the "SpawnArgs is a pure builder" contract.
Test temp-file leak Gemini (medium) The test left agy-*.log files in the temp dir.
Orphaned agy on Stop Codex P1 The sh -c wrapper means cmd.Process is sh, not agy. The engine signals cmd.Process on Stop/replacement, and a plain sh -c doesn't forward signals to its child → agy left running orphaned.

Approach

Rather than papering over with a finalizer, the backend now matches the codex/opencode capture pattern and hardens the wrapper:

  • SpawnArgs is pure — no file I/O. The shell wrapper allocates its own temp log via mktemp and rms it on exit, so nothing leaks (Validate, single-turn, or test).
  • Conversation ID via stdout — the wrapper extracts Created conversation <id> from the log and emits a {"type":"agy_session","id":"..."} sentinel; ParseLine captures it write-once into an atomic pointer. ResumeArgs no longer reads/removes a file side channel.
  • Signal forwarding — the wrapper runs agy in the background and traps SIGTERM/SIGINT, forwarding to the agy child so Stop/replacement actually terminates agy.

Verification

  • make qa + make mcp-qa green.
  • Live (real agy): turn-1 ID capture + turn-2 resume passed 2/2; SIGTERM to the wrapper killed agy with no orphan; no leftover temp files.

🤖 Generated with Claude Code

Addresses review feedback on #53 (Gemini Code Assist, Codex):

- SpawnArgs is now a pure, I/O-free argument builder. Previously it created a
  temp log file, which leaked on every Engine.Validate() call (zero Session),
  leaked again because Start re-calls SpawnArgs and overwrites the path, and
  leaked on single-turn runs (run_turn/probe) where ResumeArgs never runs to
  delete it — and it violated the Spawner pure-builder contract.

- The shell wrapper now allocates its OWN temp log via mktemp and removes it on
  exit, extracts the new conversation ID from the log and emits it as an
  {"type":"agy_session",...} stdout sentinel. ParseLine captures that ID
  (write-once atomic), so ResumeArgs no longer reads/removes a file side channel
  (matches the codex/opencode stdout-capture pattern). No temp file can leak.

- The wrapper runs agy in the background and forwards SIGTERM/SIGINT to it. The
  CLI engine signals cmd.Process (= sh, not agy) on Stop/replacement; a plain
  `sh -c` does not forward signals to its child, so timeouts/session_stop could
  orphan the agy agent. Forwarding ensures agy is terminated.

Verified live: turn-1 ID capture + turn-2 resume (2/2), no leftover temp files,
and SIGTERM to the wrapper kills agy (no orphan). make qa + make mcp-qa green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dmora dmora merged commit c84233d into main Jun 7, 2026
6 checks passed
@dmora dmora deleted the fix/agy-temp-file-leak-and-orphan branch June 7, 2026 19:14

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5b3dbca343

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread engine/cli/agy/agy.go
Comment on lines +130 to +132
uuid := b.resolveResumeID(session)
if uuid == "" {
uuid = session.Options[agentrun.OptionResumeID]
}

if uuid == "" {
return "", nil, errors.New("agy: no conversation ID available (not captured from log and OptionResumeID not set)")
return "", nil, errors.New("agy: no conversation ID available (not captured from output and OptionResumeID not set)")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve resume IDs before interrupting agy

When a caller sends a replacement message while the first agy turn is still running, process.replaceSubprocess calls ResumeArgs before terminating the current subprocess (engine/cli/process.go:611-614). This code now resolves only the ID already captured from the wrapper's stdout sentinel, but that sentinel is emitted only after a clean exit, so interrupted turns return agy: no conversation ID available even if agy has already written the conversation ID to its log. The previous log-side read allowed these mid-turn replacements to resume; keep an early side channel or emit/capture the ID before ResumeArgs is needed.

Useful? React with 👍 / 👎.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the agy CLI backend to make SpawnArgs a pure argument builder with no side effects. Temporary log file allocation, signal forwarding, and conversation ID extraction are moved into the shell wrapper script, which emits an agy_session sentinel captured by ParseLine. Feedback was provided to address a potential race condition in the shell wrapper's signal handling, to use a more precise POSIX BRE pattern in sed for robust UUID extraction, and to prioritize root options over backend-specific options in SpawnArgs.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread engine/cli/agy/agy.go
if [ -n "$__log" ]; then set -- --log-file "$__log" "$@"; fi
"$0" "$@" &
__pid=$!
trap 'kill -TERM "$__pid" 2>/dev/null' TERM INT

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The trap command kill -TERM "$__pid" is asynchronous. When the shell wrapper receives SIGTERM or SIGINT, it sends the signal to agy and immediately proceeds to delete the log file and exit, without waiting for agy to actually terminate. This can leave agy running as an orphaned process and causes a race condition where the log file is deleted while agy is still shutting down. Waiting for the process to exit inside the trap ensures a clean and graceful shutdown.

Suggested change
trap 'kill -TERM "$__pid" 2>/dev/null' TERM INT
trap 'kill -TERM "$__pid" 2>/dev/null; wait "$__pid" 2>/dev/null' TERM INT

Comment thread engine/cli/agy/agy.go
Comment on lines 109 to +110
func (b *Backend) SpawnArgs(session agentrun.Session) (string, []string) {
// Create a temp log file so agy records the new conversation ID.
// If creation fails we omit --log-file; ResumeArgs falls back to OptionResumeID.
if f, err := os.CreateTemp("", "agy-*.log"); err == nil {
f.Close()
b.logFile = f.Name()
}

var agyArgs []string
if b.logFile != "" {
agyArgs = append(agyArgs, "--log-file", b.logFile)
}
agyArgs = appendSessionArgs(agyArgs, session)
agyArgs := appendSessionArgs(nil, session)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In SpawnArgs, if root options (such as the session's conversation ID) are set, they must take precedence over backend-specific options like resumeID. If these root options are invalid, they should be silently skipped rather than falling back to backend-specific settings to avoid unexpected behavior and prevent sequential session pollution.

Suggested change
func (b *Backend) SpawnArgs(session agentrun.Session) (string, []string) {
// Create a temp log file so agy records the new conversation ID.
// If creation fails we omit --log-file; ResumeArgs falls back to OptionResumeID.
if f, err := os.CreateTemp("", "agy-*.log"); err == nil {
f.Close()
b.logFile = f.Name()
}
var agyArgs []string
if b.logFile != "" {
agyArgs = append(agyArgs, "--log-file", b.logFile)
}
agyArgs = appendSessionArgs(agyArgs, session)
agyArgs := appendSessionArgs(nil, session)
func (b *Backend) SpawnArgs(session agentrun.Session) (string, []string) {
var resumeID string
if session.ConversationID() != "" {
if isValidUUID(session.ConversationID()) {
resumeID = session.ConversationID()
}
} else if val := b.resumeID.Load(); val != nil {
resumeID = val.(string)
}
References
  1. When designing an API with independent control surfaces (e.g., root options and backend-specific options), if root options are set, they should take precedence over backend-specific options. If root options are invalid, they should be silently skipped in lenient contexts (like SpawnArgs in cli.Spawner contract) to avoid unexpected behavior for the consumer, rather than falling back to backend-specific settings which could be more surprising and harder to debug.

Comment thread engine/cli/agy/agy.go
trap - TERM INT
if [ "$__e" -eq 0 ]; then
if [ -n "$__log" ]; then
__cid=$(sed -n 's/.*Created conversation \([0-9a-f-][0-9a-f-]*\).*/\1/p' "$__log" 2>/dev/null | head -n 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The sed pattern [0-9a-f-][0-9a-f-]* is very loose and can match any sequence of hex characters and hyphens of any length. If the log output contains trailing characters or other hex/hyphen sequences, it could extract an invalid or malformed UUID. Since the Go parser regex expects an exact 36-character UUID, any mismatch will cause the conversation ID capture to fail. Using a precise POSIX BRE pattern for UUIDs ensures robust extraction.

Suggested change
__cid=$(sed -n 's/.*Created conversation \([0-9a-f-][0-9a-f-]*\).*/\1/p' "$__log" 2>/dev/null | head -n 1)
__cid=$(sed -n 's/.*Created conversation \\([0-9a-f]\\{8\\}-[0-9a-f]\\{4\\}-[0-9a-f]\\{4\\}-[0-9a-f]\\{4\\}-[0-9a-f]\\{12\\}\\).*/\\1/p' "$__log" 2>/dev/null | head -n 1)

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.

1 participant