Skip to content

Commit 5b8e278

Browse files
cameroncookeclaude
andcommitted
fix(session): Refresh simulator defaults in background
Run simulatorId/simulatorName resolution and platform inference asynchronously during MCP startup and session-set-defaults so tool execution is not blocked. Cache simulatorPlatform, clear stale selector fields when the selector changes, and apply async refresh results only when the session revision still matches. Persist startup hydration refresh results to self-heal stale config defaults. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5109a7b commit 5b8e278

17 files changed

Lines changed: 1018 additions & 443 deletions

docs/TOOLS-CLI.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,4 @@ XcodeBuildMCP provides 71 canonical tools organized into 13 workflow groups.
187187

188188
---
189189

190-
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-07T22:29:44.282Z UTC*
190+
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-08T11:11:49.328Z UTC*

docs/TOOLS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,4 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov
202202

203203
---
204204

205-
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-07T22:29:44.282Z UTC*
205+
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-02-08T11:11:49.328Z UTC*
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# Investigation + Plan: Simulator Selector Normalization, CLI Determinism, and Platform Inference
2+
3+
## Purpose
4+
5+
This document separates three related but distinct problem domains and defines a concrete implementation plan for each.
6+
7+
## Problem Domains
8+
9+
1. Inconsistent handling of `simulatorId` and `simulatorName` across logic paths.
10+
2. CLI hydrates session defaults, which makes CLI behavior non-deterministic.
11+
3. Non-iOS simulator targets (watchOS/tvOS/visionOS) can use incorrect platform names and fail builds.
12+
13+
## Scope
14+
15+
1. Keep existing CLI argument UX and command surface.
16+
2. Keep existing runtime validation behavior (`oneOf`/`allOf`/XOR checks).
17+
3. Change only session-default hydration behavior, selector normalization behavior, and platform inference behavior.
18+
4. Stateful CLI behavior (daemon, logs, debug sessions) remains by design and is out of scope.
19+
20+
## Decision Snapshot
21+
22+
1. Platform inference must support non-iOS simulators using simulator runtime metadata, with build-settings only as fallback.
23+
2. Session defaults must be MCP-only runtime behavior (no CLI/daemon hydration into `sessionStore`).
24+
3. Store both `simulatorId` and `simulatorName` in session defaults/config and disambiguate at tool boundary via shared helper logic.
25+
4. Persist only `simulatorPlatform` as platform cache; do not persist `simulatorRuntime` or timestamp fields.
26+
27+
---
28+
29+
## Domain 1: `simulatorId` / `simulatorName` Normalization
30+
31+
## Current State (verified)
32+
33+
1. `session_set_defaults` can keep both `simulatorId` and `simulatorName`.
34+
2. Config normalization drops `simulatorName` when both are present.
35+
3. Session-aware factory prunes exclusive pairs at merge-time and prefers first key when both come from defaults.
36+
4. Some tools still enforce schema-level XOR, creating layered/duplicated enforcement.
37+
38+
Net effect: behavior is usually correct, but inconsistent and hard to reason about.
39+
40+
## Decision
41+
42+
Use a single normalized model everywhere:
43+
44+
1. Store both values in session defaults/config.
45+
2. `simulatorId` is authoritative for tools that require UUID.
46+
3. `simulatorName` is preserved for portability and tools that can use name.
47+
4. Explicit user args that provide both remain invalid.
48+
5. Each tool receives exactly one effective selector value at execution boundary.
49+
50+
## Why this model
51+
52+
1. `simulatorName` survives simulator resets better than UUIDs.
53+
2. Some operations fundamentally require UUID; others can run with name.
54+
3. Keeping both in storage avoids repeated lossy conversion and supports both execution modes.
55+
56+
## Required Invariants
57+
58+
1. Storage layer may contain both selector fields.
59+
2. Explicit invocation args may not contain both selector fields.
60+
3. Tool execution input must be disambiguated to one selector by a shared helper.
61+
4. Disambiguation precedence must be deterministic and test-covered.
62+
63+
## Implementation Plan
64+
65+
1. Add `inferSimulatorSelectorForTool(...)` helper (or equivalent) used by simulator tools.
66+
2. Normalize config/session behavior to stop dropping `simulatorName` when both exist.
67+
3. Keep factory-level explicit XOR validation.
68+
4. Keep per-tool requirement checks (`oneOf`/`allOf`) unchanged.
69+
5. Reduce duplicated tool-local selector branching by centralizing selector choice.
70+
71+
## Validation Plan
72+
73+
1. Unit tests for helper precedence across explicit args, stored defaults, and missing values.
74+
2. Regression tests for config-load behavior preserving both fields.
75+
3. Integration tests for tools that require UUID vs tools that accept name.
76+
4. Real-world MCP run validating both selector paths.
77+
78+
---
79+
80+
## Domain 2: CLI Determinism and Session Defaults
81+
82+
## Current State (verified)
83+
84+
1. `session-management` workflow is not exposed in CLI.
85+
2. CLI runtime still bootstraps config and hydrates `config.sessionDefaults` into `sessionStore`.
86+
3. Result: CLI can pick up hidden persisted defaults that the user cannot inspect/mutate via CLI commands.
87+
88+
Net effect: CLI behavior can vary based on hidden config state.
89+
90+
## Decision
91+
92+
Make session-default hydration MCP-only.
93+
94+
1. MCP runtime hydrates `config.sessionDefaults` into `sessionStore`.
95+
2. CLI and daemon runtimes do not hydrate `sessionDefaults` into `sessionStore`.
96+
3. CLI/daemon still read non-session config (workflow filters, debug flags, timeouts, etc.).
97+
4. No CLI command/flag redesign is required.
98+
99+
## Required Invariants
100+
101+
1. CLI tool behavior must not depend on persisted `sessionDefaults`.
102+
2. CLI behavior remains explicit-argument driven.
103+
3. Existing runtime validation for required parameters remains intact.
104+
4. `disableSessionDefaults=true` behavior for MCP tools remains consistent with current expectations.
105+
106+
## Implementation Plan
107+
108+
1. Gate session-default hydration by runtime in `bootstrapRuntime`.
109+
2. Ensure daemon startup path also does not hydrate selector defaults.
110+
3. Add tests that verify:
111+
- MCP hydrates session defaults.
112+
- CLI and daemon do not.
113+
4. Keep all existing CLI validation paths and error messages unless a bug is found.
114+
115+
## Validation Plan
116+
117+
1. Unit/integration tests for runtime hydration boundaries.
118+
2. CLI real-world test with persisted `sessionDefaults` present in config:
119+
- missing required args should still fail.
120+
- explicit args should succeed.
121+
3. MCP real-world test confirming session-default convenience still works.
122+
123+
---
124+
125+
## Domain 3: Non-iOS Simulator Platform Inference
126+
127+
## Current State (verified)
128+
129+
1. iOS paths are generally reliable.
130+
2. Non-iOS simulator targets can infer wrong platform and fail destination matching.
131+
3. Build settings lookups are slower and should not be the first-line source for simulator platform inference.
132+
133+
## Decision
134+
135+
Platform inference for simulator tools must use simulator metadata first, then fallback.
136+
137+
1. Primary source: simulator runtime metadata (via `simctl` resolution).
138+
2. Derived output: correct simulator platform string (`iOS/watchOS/tvOS/visionOS Simulator`).
139+
3. Secondary source: build settings only when simulator metadata cannot resolve.
140+
4. Final fallback: explicit warning + `iOS Simulator` only when no better signal exists.
141+
142+
Cache policy:
143+
144+
1. Persist `simulatorPlatform` as the cached output.
145+
2. Recompute `simulatorPlatform` during MCP startup hydration.
146+
3. Recompute `simulatorPlatform` whenever simulator selector (`simulatorId`/`simulatorName`) changes.
147+
4. Do not persist `simulatorRuntime` or timestamp fields.
148+
149+
## Required Invariants
150+
151+
1. If runtime indicates non-iOS simulator, platform must not default to iOS.
152+
2. Platform inference source should be logged for observability.
153+
3. Selector normalization and platform inference should be reusable utilities, not tool-local variants.
154+
155+
## Implementation Plan
156+
157+
1. Introduce/standardize `inferPlatform(...)` utility contract around selector + runtime metadata.
158+
2. Ensure simulator-name and simulator-id paths both resolve runtime/platform deterministically.
159+
3. Add normalized mapping from CoreSimulator runtime to xcodebuild destination platform.
160+
4. Use build-settings only as fallback path.
161+
162+
## Validation Plan
163+
164+
1. Unit tests for runtime-to-platform mapping.
165+
2. Integration tests for iOS + non-iOS simulator selectors.
166+
3. Real-world CLI/MCP checks for watchOS/tvOS/visionOS flows where available.
167+
168+
---
169+
170+
## Cross-Cutting Architecture
171+
172+
## Shared Helpers
173+
174+
1. `inferSimulatorSelectorForTool(...)`
175+
- Input: explicit params + stored defaults + tool capability (`requiresId` vs `acceptsNameOrId`).
176+
- Output: exactly one effective selector (or deterministic validation error).
177+
2. `inferPlatform(...)`
178+
- Input: resolved selector + scheme/path context.
179+
- Output: `{ platform, source, runtime? }`.
180+
181+
## Data Model
182+
183+
Keep/add simulator metadata fields in session/config:
184+
185+
1. `simulatorId`
186+
2. `simulatorName`
187+
3. `simulatorPlatform` (optional cache)
188+
189+
`simulatorRuntime` can still be used transiently inside resolver/helper logic, but is not persisted.
190+
191+
## Runtime Boundary Rules
192+
193+
1. MCP: may hydrate/use session defaults.
194+
2. CLI: no session-default hydration; explicit invocation only.
195+
3. Daemon (CLI backend): same as CLI for hydration semantics.
196+
197+
---
198+
199+
## Delivery Plan
200+
201+
## Phase 0: Lock Decisions
202+
203+
1. Approve this document’s three-domain decisions.
204+
2. Confirm no CLI UX changes are in scope.
205+
206+
## Phase 1: Runtime Boundary Fix (Domain 2)
207+
208+
1. Implement MCP-only session-default hydration.
209+
2. Add runtime boundary tests.
210+
211+
## Phase 2: Selector Normalization (Domain 1)
212+
213+
1. Implement shared selector helper.
214+
2. Align config/session behavior to preserve both selector fields.
215+
3. Remove path-specific inconsistencies.
216+
217+
## Phase 3: Platform Inference Hardening (Domain 3)
218+
219+
1. Consolidate non-iOS platform inference on simulator metadata.
220+
2. Add mapping tests and fallback tests.
221+
222+
## Phase 4: Regression + Real-World Validation
223+
224+
1. Run project test/lint/typecheck/build pipeline.
225+
2. Run real-world MCP and CLI smoke tests for selector + platform behavior.
226+
3. Document outcomes in PR notes.
227+
228+
---
229+
230+
## Risks and Mitigations
231+
232+
1. Risk: duplicate validation behavior drifts across tools.
233+
- Mitigation: central helper + contract tests.
234+
2. Risk: config backward-compat surprises.
235+
- Mitigation: additive fields and migration-safe parsing.
236+
3. Risk: non-iOS paths regress silently.
237+
- Mitigation: explicit non-iOS test coverage and runtime-source logging.
238+
239+
## Out of Scope
240+
241+
1. Redesigning CLI command structure or argument names.
242+
2. Changing general daemon stateful behavior.
243+
3. Introducing auto-retry heuristics on command failure.

src/mcp/tools/session-management/__tests__/session_set_defaults.test.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ describe('session-set-defaults tool', () => {
7171
const current = sessionStore.getAll();
7272
expect(current.scheme).toBe('MyScheme');
7373
expect(current.simulatorName).toBe('iPhone 16');
74-
// simulatorId should be auto-resolved from simulatorName
75-
expect(current.simulatorId).toBe('RESOLVED-SIM-UUID');
74+
// simulatorId resolution happens in background; immediate update keeps explicit inputs only
75+
expect(current.simulatorId).toBeUndefined();
7676
expect(current.useLatestOS).toBe(true);
7777
expect(current.arch).toBe('arm64');
7878
});
@@ -115,28 +115,33 @@ describe('session-set-defaults tool', () => {
115115
);
116116
});
117117

118-
it('should clear simulatorName when simulatorId is explicitly set', async () => {
119-
sessionStore.setDefaults({ simulatorName: 'iPhone 16' });
120-
const result = await sessionSetDefaultsLogic({ simulatorId: 'SIM-UUID' }, createContext());
118+
it('should clear stale simulatorName when simulatorId is explicitly set', async () => {
119+
sessionStore.setDefaults({ simulatorName: 'Old Name' });
120+
const result = await sessionSetDefaultsLogic(
121+
{ simulatorId: 'RESOLVED-SIM-UUID' },
122+
createContext(),
123+
);
121124
const current = sessionStore.getAll();
122-
expect(current.simulatorId).toBe('SIM-UUID');
125+
expect(current.simulatorId).toBe('RESOLVED-SIM-UUID');
123126
expect(current.simulatorName).toBeUndefined();
124127
expect(result.content[0].text).toContain(
125-
'Cleared simulatorName because simulatorId was explicitly set.',
128+
'Cleared simulatorName because simulatorId changed; background resolution will repopulate it.',
126129
);
127130
});
128131

129-
it('should auto-resolve simulatorName to simulatorId when only simulatorName is set', async () => {
132+
it('should clear stale simulatorId when only simulatorName is set', async () => {
130133
sessionStore.setDefaults({ simulatorId: 'OLD-SIM-UUID' });
131134
const result = await sessionSetDefaultsLogic({ simulatorName: 'iPhone 16' }, createContext());
132135
const current = sessionStore.getAll();
133-
// Both should be set now - name provided, id resolved
136+
// simulatorId resolution happens in background; stale id is cleared immediately
134137
expect(current.simulatorName).toBe('iPhone 16');
135-
expect(current.simulatorId).toBe('RESOLVED-SIM-UUID');
136-
expect(result.content[0].text).toContain('Resolved simulatorName');
138+
expect(current.simulatorId).toBeUndefined();
139+
expect(result.content[0].text).toContain(
140+
'Cleared simulatorId because simulatorName changed; background resolution will repopulate it.',
141+
);
137142
});
138143

139-
it('should return error when simulatorName cannot be resolved', async () => {
144+
it('should not fail when simulatorName cannot be resolved immediately', async () => {
140145
const contextWithFailingExecutor = {
141146
executor: vi.fn().mockImplementation(async (command: string[]) => {
142147
if (command.includes('simctl') && command.includes('list')) {
@@ -159,8 +164,8 @@ describe('session-set-defaults tool', () => {
159164
{ simulatorName: 'NonExistentSimulator' },
160165
contextWithFailingExecutor,
161166
);
162-
expect(result.isError).toBe(true);
163-
expect(result.content[0].text).toContain('Failed to resolve simulator name');
167+
expect(result.isError).toBe(false);
168+
expect(sessionStore.getAll().simulatorName).toBe('NonExistentSimulator');
164169
});
165170

166171
it('should prefer workspacePath when both projectPath and workspacePath are provided', async () => {
@@ -222,7 +227,11 @@ describe('session-set-defaults tool', () => {
222227
await initConfigStore({ cwd, fs });
223228

224229
const result = await sessionSetDefaultsLogic(
225-
{ workspacePath: '/new/App.xcworkspace', simulatorId: 'SIM-1', persist: true },
230+
{
231+
workspacePath: '/new/App.xcworkspace',
232+
simulatorId: 'RESOLVED-SIM-UUID',
233+
persist: true,
234+
},
226235
createContext(),
227236
);
228237

@@ -235,8 +244,7 @@ describe('session-set-defaults tool', () => {
235244
};
236245
expect(parsed.sessionDefaults?.workspacePath).toBe('/new/App.xcworkspace');
237246
expect(parsed.sessionDefaults?.projectPath).toBeUndefined();
238-
expect(parsed.sessionDefaults?.simulatorId).toBe('SIM-1');
239-
// simulatorName is cleared because simulatorId was explicitly set
247+
expect(parsed.sessionDefaults?.simulatorId).toBe('RESOLVED-SIM-UUID');
240248
expect(parsed.sessionDefaults?.simulatorName).toBeUndefined();
241249
});
242250

0 commit comments

Comments
 (0)