Skip to content

feat(log-capture): Durable OSLog session tracking and executor settlement fixes#336

Open
cameroncooke wants to merge 6 commits intomainfrom
cameroncooke/fix/mcp-cli-snapshot-parity
Open

feat(log-capture): Durable OSLog session tracking and executor settlement fixes#336
cameroncooke wants to merge 6 commits intomainfrom
cameroncooke/fix/mcp-cli-snapshot-parity

Conversation

@cameroncooke
Copy link
Copy Markdown
Collaborator

Add a filesystem-backed registry for tracking detached simctl spawn ... log stream helper processes across MCP server restarts, plus fixes for command executor settlement and test pipeline error handling.

OSLog session tracking architecture

The core problem: every build_run_sim launch spawns a detached simctl spawn <uuid> log stream process to capture app logs. These helpers were fire-and-forget -- no tracking, no cleanup on re-launch, no cleanup on app stop, and invisible to other MCP server instances. Over time they accumulate silently.

The solution is a durable, filesystem-backed registry under ~/Library/Developer/XcodeBuildMCP/state/simulator-launch-oslog/. Each helper gets a JSON record written atomically (write-to-temp, rename) containing the helper PID, owner instance identity, simulator UUID, bundle ID, and expected command signature.

Key design decisions:

  • Cross-process visibility: A RuntimeInstance identity (UUID + PID) tags each record's owner. Any MCP server process can read all records, but only stops its own sessions during shutdown. This prevents one server from killing another's active log streams.
  • PID-recycling safety: Liveness checks shell out to ps -p <pid> -o pid=,command= and verify the running command contains expected parts (simctl, spawn, <uuid>, log, stream, <bundleId>). A matching PID with a different command is treated as stale and pruned.
  • Self-healing reads: Every list call prunes malformed JSON files and stale records automatically, so the registry never grows unboundedly even if a process crashes without cleanup.
  • Two-phase termination: Stop sends SIGTERM, polls for exit, then escalates to SIGKILL. The stop result aggregates { stoppedSessionCount, errorCount, errors } for structured reporting.

Lifecycle integration points:

  • Launch: cleans up existing sessions for the same simulator+bundleId before spawning a new helper; registers the new helper atomically (kills it if registration fails)
  • Stop app: stop_app_sim terminates tracked OSLog sessions alongside the simctl terminate call
  • Shutdown: only owned sessions are stopped, respecting multi-process coexistence
  • Status/lifecycle snapshots: expose tracked session counts for observability

Command executor settlement fix

The defaultExecutor in command.ts resolved on the close event alone. When a spawned process exits but a detached grandchild inherits stdout/stderr FDs, close is delayed indefinitely -- hanging the promise. Replaced with a state machine that settles when both exit is observed and all streams are drained, with a 100ms safety timer as fallback. The close event remains a final authority for immediate settlement.

Test pipeline finalization fix

handleTestLogic in test-common.ts could leave the xcodebuild pipeline response in a pending state if the executor threw after startBuildPipeline. The started variable is now hoisted so the catch handler can call finalizeInlineXcodebuild and produce a proper error response.

cameroncooke and others added 4 commits April 11, 2026 09:11
Route shared xcodebuild snapshot scenarios through canonical CLI fixtures and make MCP durable text rendering use the same non-interactive transcript formatter. This removes MCP-specific newline drift and keeps test discovery formatting consistent across success and failure paths.

Also surface selective test targeting in test headers and limit discovery previews to the first six tests so the canonical fixtures stay readable while preserving parity across runtimes.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
…e alone

The close event can be delayed indefinitely when a detached grandchild
inherits stdout/stderr file descriptors. Replace the single close listener
with a state machine that tracks open streams, observes exit, and settles
when both conditions are met. A 100ms safety timer after exit handles the
case where streams never drain. The close event remains a final authority
that forces immediate settlement.
…ions

When the executor throws after startBuildPipeline has been called, the
pipeline response was left in a pending state with no output. Hoist the
started variable above the try block so the catch handler can call
finalizeInlineXcodebuild and produce a proper error response with build
log links.
…ess registry

Detached simctl log stream helper processes were spawned fire-and-forget
with no tracking, accumulating silently across server restarts. Add a
filesystem-backed registry under ~/Library/Developer/XcodeBuildMCP/state/
that records each helper's PID, owner instance, and expected command
signature. Liveness is verified via ps command matching to handle PID
recycling.

On app launch, existing sessions for the same simulator+bundleId are
cleaned up before spawning a new helper. On app stop, tracked sessions
are terminated alongside the simctl terminate call. On server shutdown,
only sessions owned by the current process are stopped. The lifecycle
snapshot and session status resource expose tracked session counts for
observability.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 11, 2026

Open in StackBlitz

npm i https://pkg.pr.new/xcodebuildmcp@336

commit: 281f4d2

return contentParts.join('');
return renderCliTextTranscript(events, {
suppressWarnings: suppressWarnings ?? false,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Text strategy now always renders next-steps as CLI format

Medium Severity

The createTextRenderSession (the 'text' strategy used for MCP tool responses) now delegates to renderCliTextTranscript, which internally uses createCliTextProcessor. That processor always passes 'cli' to formatNextStepsEvent(event, 'cli'), ignoring the event's runtime property. Previously, the old inline rendering in createTextRenderSession honored it: const effectiveRuntime = event.runtime === 'cli' ? 'cli' : 'mcp'. This means MCP tool responses now show CLI-style commands (e.g. xcodebuildmcp macos get-app-path) instead of MCP tool invocations (e.g. get_mac_app_path({ ... })), which are what AI agents using the MCP protocol can actually call.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit afa79db. Configure here.

Track simulator launch OSLog helpers in a durable registry so stop and shutdown can
clean them up across independent CLI and MCP process lifetimes.

Keep lifecycle visibility accurate for parallel runs, prune stale or corrupt
registry entries, and tighten the related command and rendering refactors that
support the new ownership model.

Refs GH-273
Co-Authored-By: OpenAI Codex <noreply@openai.com>
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f3bbe56. Configure here.

}) as ChildProcess['kill'];

return child;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Duplicated createTrackedChild helper across three test files

Low Severity

The createTrackedChild helper function is independently re-implemented in three test files (stop_app_sim.test.ts, session-status.test.ts, and mcp-lifecycle.test.ts) with slightly different signatures and behaviors. This increases maintenance burden — future changes to the mock ChildProcess shape need to be synchronized across all three copies. A shared test utility (e.g., in test-utils/) would reduce duplication.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f3bbe56. Configure here.

Batch registry PID sampling so durable OSLog cleanup does less process churn while keeping stale-entry pruning behavior unchanged.

Make the test-only session reset helper explicit, guard simulator device parsing against malformed simctl payloads, and avoid duplicate local cleanup when both child exit and close fire.

These changes keep the durable registry design the same while tightening a few rough edges called out in follow-up review.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
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