v1.17.0: reliability pass: 14 bug fixes, strtobool env parsing, whitelist match modes, fault-tolerant inspectTrace
Reliability pass. v1.16 closed the macOS 26.x recording gap with recordViaInstrumentsApp. v1.17 sweeps the audit punch list that surfaced after dogfooding: 14 bugs across three tiers, 3 known limitations documented, 3 tech-debt items deferred. The headlines are env-var truthy parsing (every MEMORYDETECTIVE_* boolean now accepts 1 / true / yes / on, was 1-only), a verifyFix whitelist that supports exact / substring / regex modes (was substring-only), the recordViaInstrumentsApp watcher catching saves outside the watch dir, and the inspectTrace fault-tolerant fallback so a wedged 52K bundle no longer throws. 41 MCP tools, 701 tests (+24 vs v1.16).
Fixed
Tier 1 (user-visible)
recordViaInstrumentsAppwatcher misses saves outsidewatchDir(B-01). Pre-v1.17 the tool only watched the filesystem path; users who hit Save in Instruments.app and accepted the default Desktop location would time out even though the trace was on disk. Now queries the running Instruments.app via AppleScript every poll for anydocumentwhose file path was set after the tool started; on first match outsidewatchDirreturns the path withsavedOutsideWatchDir: true. Uses the documentedInstruments.sdeffile of documentaccessor only (no unsupported verbs).verifyFix.expectedAliveClassessupports per-entry mode (B-02). Pre-v1.17 every whitelist entry was matched as a case-insensitive substring. Caller wanting "match exactlyUIRemoteKeyboardWindow, do not matchMyUIRemoteKeyboardWindowWrapper" had no way to express it. Schema now acceptsstring(defaults to substring for backwards compat) or{ pattern: string, mode: "exact" | "substring" | "regex" }. ESLint and Jest both use single-mode matching, but their users have asked for the inverse in tracked issues; we picked the opposite trade-off.MEMORYDETECTIVE_*env booleans accept the strtobool truthy set (B-03). Pre-v1.17 only the literal string1turned a gate on. Users exportingMEMORYDETECTIVE_ALLOW_LAUNCH=trueor=yessaw silent no-op behavior. v1.17 normalizes the five booleans throughparseBooleanEnv, which accepts1 / true / t / yes / y / on(truthy) and0 / false / f / no / n / off(falsy), case-insensitive. Unrecognized values emit a one-time stderr warning per var so the operator knows the setting was ignored. Mirrorsenvalid'sbool()semantics without taking the dependency.maybeOpenInInstrumentschecks bundle viability before opening (B-04). Pre-v1.17 the auto-open path would launch a wedged 52K macOS 26.x stub bundle in Instruments.app, where it presents a Document Missing Template Error dialog the operator has to dismiss. Now probesTrace1.run/MANIFEST.plistbefore callingopen; on missing manifest, skips the open and surfaces the bundle status to the caller. The exportedclassifyBundleOnDisk(tracePath)helper returns"unknown" | "salvageable" | "wedged"for reuse.
Tier 2 (silently sub-optimal)
inspectTracefault-tolerant fallback whenxctrace export --tocfails (B-05). Pre-v1.17 a failed TOC export threw, which broke the entire MCP call. Now returnsok: truewithschemas: [],rowCounts: {},suggestedNextCalls: [], and a diagnosis string naming the 52K macOS 26.x stub pattern so callers can self-diagnose. Throwing is reserved for missing trace paths.schemaDiscovery.fetchDiscoveredSchemasWithStatussurfaces failures (B-06). New sibling offetchDiscoveredSchemasreturns{ schemas, status: "ok" | "failed", reason? }so trace analyzers can include a discovery-failure entry insupportStatus[]instead of silently using canonical schema names against a renamed-schema trace. Emits a one-time stderr warning pertracePath:reason, gated onMEMORYDETECTIVE_SUPPRESS_PLATFORM_ADVISORY. LegacyfetchDiscoveredSchemaskeeps its silent-fallback contract.countByClassWithBytesreports min/max/median for variable-size classes (B-07). Pre-v1.17 we returned the first observedinstanceSizeas canonical for every class. Misled callers inspectingNSData,NSString,CFDatawhose per-instance size is payload-dependent. Now: fixed-size classes return a singleinstanceSizeBytesvalue as before; variable-size classes return{ instanceSizeBytes: median, instanceSizeBytesMin, instanceSizeBytesMax, instanceSizeBytesMedian }.totalBytesunchanged.CountAliveEntrypropagates the spread through per-class and topN paths.analyzeHangs.supportStatus[]always includes both schema entries (B-08). Pre-v1.17 a caller withouthangRisksXmlsaw a single entry, a caller with it saw two. Agent code branching onsupportStatus.find(s => s.kind === "hang-risks")could not distinguish "I did not ask" from "schema absent". Both entries are now always present; the missing-XML path returnsstatus: "not_present"with reason"caller did not provide hangRisksXml".recordTimeProfile.bundleStatusreflects on-disk reality (B-09). NewbundleStatus: "unknown" | "salvageable" | "wedged"field on the response. The timeout path now probesMANIFEST.plistviaclassifyBundleOnDiskso callers branching ontracePath ? "have trace" : "no trace"get an honest signal.countAliveframework-noise filter is configurable (B-10). New inputs:excludeFrameworkNoise: boolean(default true),additionalNoisePatterns?: string[],unsuppressClassPatterns?: string[],noiseAuditMode: boolean. The curated noise list (calibrated for the v1.5 notelet investigation) remains the default. Audit mode returns anoiseAudit[]array listing every filtered class with reason'default-list','additional-pattern', or'kept-by-unsuppress'. User-supplied regex strings compile with a literal-escape fallback on syntax errors.
Tier 3 (edge case / cosmetic)
extractHostparses IPv6 bracket form (B-11).analyzeNetworkActivitywas misreporting hosts like[::1]:443as the empty string. Now strips brackets and splits the port correctly.normalizeBucketpriority fix (B-12).analyzeEnergyImpactwas matching"high"before"foreground"in xctrace's varying-case bucket strings. Reordered to check the named buckets first.- Redundant
t.schema === "memory-footprint"equality removed (B-13).analyzeMemoryFootprintwas double-checking afterdiscoverSchemasalready resolved the family name. analyzeLeakTimelinehandles column-drift gracefully (B-14). When every row is skipped due to missingclassName, returnsstatus: "partial"with a reason naming the column drift instead of silently emitting an empty timeline.
Behavior changes (informational)
- The
MEMORYDETECTIVE_*boolean env vars are now case-insensitively recognized for the strtobool set. Setting=yesor=truenow turns gates on; pre-v1.17 it did not. Operators who deliberately set unrecognized values will see a stderr warning but no behavioral change (unrecognized falls back to default). verifyFix.expectedAliveClassesstring entries continue to behave as case-insensitive substring matches for backwards compat. The new object form{ pattern, mode }is opt-in.- The deprecated
noticeandstatusaliases on trace analyzers stay (will be removed in a future major bump).