# Runtime troubleshooting

> Non-API runtime issues: duplicate InkIt bundle instances and Accessibility grants, Bluetooth mic profile delay, debugLoggingEnabled trace file at ~/Library/Logs/InkIt-debug.log, SwiftData persistence fallback, and notch HUD positioning on non-notch displays.

- Repository: cartesia-ai/InkIt
- GitHub: https://github.com/cartesia-ai/InkIt
- Human docs: https://grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b
- Complete Markdown: https://grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b/llms-full.txt

## Source Files

- `InkIt/AppCoordinator.swift`
- `InkIt/DebugLog.swift`
- `InkIt/AudioCaptureService.swift`
- `InkIt/TranscriptHistoryStore.swift`
- `InkIt/NotchHUD.swift`
- `InkItTests/DebugLogFormatterTests.swift`

---

---
title: "Runtime troubleshooting"
description: "Non-API runtime issues: duplicate InkIt bundle instances and Accessibility grants, Bluetooth mic profile delay, debugLoggingEnabled trace file at ~/Library/Logs/InkIt-debug.log, SwiftData persistence fallback, and notch HUD positioning on non-notch displays."
---

`AppCoordinator` coordinates the main runtime failure surfaces outside Cartesia STT and LLM polish: it detects duplicate bundle instances at launch, gates dictation on `PermissionsService.hasAccessibility`, drives `audioReady` from `AudioCaptureService`, routes trace output through `DebugLog`, and hosts `NotchHUDController` after onboarding completes. `TranscriptHistoryStore` owns SwiftData persistence with an in-memory fallback when the on-disk store cannot open.

## Symptom map

| Symptom | Likely runtime cause | Primary signal |
| --- | --- | --- |
| Hotkey fires but paste never works | Accessibility granted to a different `.app` bundle than the running copy | `setError("Accessibility needed")` in notch HUD; `AXIsProcessTrusted()` false |
| Two menu-bar icons or conflicting hotkeys | Multiple processes share `Bundle.main.bundleIdentifier` but different `bundlePath` values | `detectDuplicateRunningCopies()` sets `lastError` with tilde-shortened paths |
| First words missing on AirPods / Bluetooth headset | A2DP→HFP profile switch emits digital silence for ~200–500 ms | `audioReady` stays false; `HUDPreparingDot` instead of waveform |
| History rows vanish after quit | SwiftData fell back to in-memory store | Transcripts visible this session only; `isPersistent == false` internally |
| HUD looks like a black blob on menu bar | Machine has no physical notch | `NotchGeometry.hasPhysicalNotch == false` → `floatingIsland` capsule |

## Duplicate InkIt bundle instances

At `AppCoordinator` init, `detectDuplicateRunningCopies()` enumerates `NSWorkspace.shared.runningApplications` for other processes with the same `bundleIdentifier` and a different PID. When matches exist, it records:

```
Multiple InkIt copies are running. Current: <path>. Also running: <paths>. Quit the duplicate copy and grant Accessibility to only one app bundle.
```

in `lastError`. Startup is not blocked; dictation may still register, but two copies compete for the global hotkey and paste injection.

<Warning>
macOS TCC grants Accessibility per app **bundle path**, not per bundle ID alone. A release build at `/Applications/InkIt.app` and a Debug build under `~/Library/Developer/Xcode/DerivedData/.../InkIt.app` are separate entries in System Settings → Privacy & Security → Accessibility.
</Warning>

<Steps>
<Step title="Identify running copies">

Open Activity Monitor and filter for **InkIt**, or run:

```bash
pgrep -lf InkIt
```

Note each distinct `.app` path.

</Step>
<Step title="Quit duplicates">

Leave only the bundle you intend to use (typically `/Applications/InkIt.app` after a release install, or a single local build during development). Quit extras from the menu bar or Activity Monitor.

</Step>
<Step title="Re-grant Accessibility to the surviving bundle">

In System Settings → Privacy & Security → Accessibility, enable **only** the InkIt entry that matches the path you kept. Remove stale entries for old DerivedData builds if present.

</Step>
<Step title="Verify">

Relaunch the single copy. Press the dictation hotkey in a focused text field. The notch HUD should show the live island without an immediate `Accessibility needed` error.

</Step>
</Steps>

`PermissionsService.appIdentityDescription` returns `CFBundleName`, `bundleIdentifier`, and `bundlePath` for confirming which binary macOS is evaluating.

## Accessibility grants and duplicate bundles

Dictation checks `permissions.hasAccessibility` on every `startDictation()` call. When false:

- `setError("Accessibility needed")` surfaces a brief notch error notice.
- `permissions.requestAccessibility()` runs at most once every `accessibilityPromptThrottle` (10 seconds) to avoid yanking System Settings on repeated hotkey mashes.
- The HUD error still appears on every blocked press.

`PermissionsService.requestAccessibility()` fires `AXIsProcessTrustedWithOptions(prompt: true)` once, then opens the Accessibility pane. After a Deny or repeat tap, later calls only re-open Settings (`needsManual` state). `confirmAccessibilityGrant()` may silently relaunch via `NSWorkspace.openApplication` with `createsNewApplicationInstance: true` when the in-process trust bit stays stale after the user toggled the switch.

When Accessibility flips on, `AppCoordinator.refreshHUD()` re-registers the hotkey so Fn/Globe bindings upgrade from passive monitor to suppressing `CGEventTap`.

## Bluetooth mic profile delay

`AudioCaptureService` converts input to 16 kHz PCM and signals readiness separately from hotkey press:

| Constant | Value | Role |
| --- | --- | --- |
| `readyLevelThreshold` | `0.03` | Normalized peak above noise floor → device is live |
| `readyFallbackDelay` | `0.6` s | Timer backstop when room is silent or threshold never crosses |

On `start()`, Bluetooth devices often emit digital silence while switching from output (A2DP) to input (HFP). The first buffer with `level > readyLevelThreshold` calls `signalReadyIfNeeded()`, which fires `onReady` once per take. If no signal arrives within 600 ms, the fallback timer signals ready anyway so the HUD never sticks on preparing.

`AppCoordinator` resets `audioReady = false` at each `startDictation()` and sets it true when `audio.onReady` fires. `NotchHUDView` shows `HUDPreparingDot` until `coordinator.audioReady`, then `HUDWaveform` — the intentional “start speaking” cue.

<Note>
Words spoken during the dead gap before HFP activation are lost at the hardware level; wait for the waveform before speaking on Bluetooth mics.
</Note>

See [Input device selection](/input-device-selection) for pinning a preferred CoreAudio UID and unplug fallback behavior.

## Debug trace file (`debugLoggingEnabled`)

`DebugLog` mirrors developer traces to unified logging (`os.Logger`, subsystem = bundle identifier, category `trace`) **and** a flat append-only file. Logging is **off by default** because traces can include raw transcripts and on-screen context.

<ParamField body="debugLoggingEnabled" type="boolean">
UserDefaults key shared by `SettingsStore.debugLoggingEnabled` and `DebugLog.isEnabledKey`. When false, `DebugLog.info` / `DebugLog.error` return immediately without writing.
</ParamField>

Enable via Settings → **Debug logging** (caption: writes to `~/Library/Logs/InkIt-debug.log`).

**Log path:** `~/Library/Logs/InkIt-debug.log`  
**Line format:** `[ISO8601-with-fractional-seconds] <message>\n`  
**Write model:** serial `DispatchQueue`; append via `FileHandle.seekToEndOfFile()`, atomic create on first write.

Utility helpers used across the dictation pipeline:

| API | Purpose |
| --- | --- |
| `boundedBlock(title:text:limit:)` | Metadata (`bytes`, FNV-style `hash`, `truncated` flag) plus body capped at 12 000 chars |
| `redacted(_:secrets:)` | Replaces key substrings with `<redacted>` |
| `prettyJSONString(_:)` | Sorted, pretty-printed JSON for request bodies |

Representative trace sites when enabled: `startDictation` target resolution, `correctedTranscript` polish runs, `setError`, `PasteService`, `CartesiaStreamingClient`, and `TranscriptHistoryStore` persistence errors.

<RequestExample>

```bash
# Tail the trace while reproducing an issue
defaults read com.cartesia.InkIt debugLoggingEnabled   # expect 1 when toggle is on
tail -f ~/Library/Logs/InkIt-debug.log
```

</RequestExample>

<Warning>
Disable debug logging after investigation. The file accumulates transcript text and context on disk.
</Warning>

## SwiftData persistence fallback

`TranscriptHistoryStore.makeContainer()` first opens a disk-backed `ModelContainer` for schema `[TranscriptRecord.self]`. On failure it logs (via `DebugLog.error` when tracing is on) and constructs an in-memory container with `isStoredInMemoryOnly: true`.

| `isPersistent` | Behavior |
| --- | --- |
| `true` | Rows survive relaunch; `lifetimeWords` advances in UserDefaults (`transcriptHistory.lifetimeWords.v1`) only after durable `saveContext()` |
| `false` | Session-only history; `entries` mirror updates in RAM but quit drops them; legacy UserDefaults migration (`transcriptHistory.v1`) is skipped to avoid deleting unmigrated data |

`add()` always inserts into the published `entries` array for the current session. `clear()` updates the UI only after a successful `context.delete` + `saveContext()`. A failed save returns `false` from `saveContext()` and leaves the legacy blob in place for retry on next launch.

Enable `debugLoggingEnabled` to capture `TranscriptHistoryStore: on-disk SwiftData store unavailable` or `save failed` lines when diagnosing missing history.

```text
TranscriptHistoryStore.init
        │
        ├─► ModelContainer (disk) ──success──► isPersistent = true
        │
        └─► catch ──► ModelContainer (in-memory) ──► isPersistent = false
                              │
                              └─► migrateLegacyHistoryIfNeeded() skipped
```

## Notch HUD on non-notch displays

`NotchHUDController` creates a borderless `NSPanel` at `CGWindowLevelForKey(.statusWindow) + 1`, click-through (`ignoresMouseEvents = true`), and repositions on `NSApplication.didChangeScreenParametersNotification`.

`NotchGeometry.detect(on:)` branches on `NSScreen.safeAreaInsets.top`:

| Display | `hasPhysicalNotch` | Horizontal anchor | HUD shape |
| --- | --- | --- | --- |
| MacBook with camera notch | `true` | Center of auxiliary top-left/right areas | `pill` merges with physical notch |
| External / non-notch Mac | `false` | `frame.midX` | `floatingIsland` — content-sized capsule below menu bar |

Non-notch layout uses `HUDMetrics.floatingTopGap` (3 pt), `floatingHeight` (22 pt), horizontal padding, and a soft capsule shadow. The island scales/fades in (`transition(.scale.combined(with: .opacity))`) because there is no notch to retract into. Window width stays `520` pt; `reposition()` sets `x = centerX - width/2`, `y = screen.frame.maxY - height`.

`SettingsStore` persists `notchHorizontalPosition` (UserDefaults, default `0.38`, clamped `0.04`…`0.96`), but `NotchHUDController.reposition()` currently derives `centerX` only from `NotchGeometry.detect` — not from that setting. On non-notch hardware the HUD is always centered on the main screen's top edge.

If the HUD appears off-screen after a display topology change, trigger a reposition by switching displays or toggling resolution; the screen-parameters observer calls `reposition()` automatically.

## Related pages

<CardGroup>
<Card title="Permissions model" href="/permissions-model">
Accessibility and microphone TCC flow, polling, `needsManual` state, and silent relaunch after grant.
</Card>
<Card title="Input device selection" href="/input-device-selection">
Preferred microphone UID, Bluetooth A2DP→HFP timing, and `audioReady` HUD coupling.
</Card>
<Card title="Dictation state machine" href="/dictation-state-machine">
`DictationState` transitions, `audioReady`, and error dismiss timing.
</Card>
<Card title="Settings reference" href="/settings-reference">
`debugLoggingEnabled`, `notchHorizontalPosition`, and other persisted keys.
</Card>
<Card title="STT troubleshooting" href="/stt-troubleshooting">
Cartesia API failures — separate from the runtime issues above.
</Card>
<Card title="Build from source" href="/build-from-source">
Why DerivedData builds create a second Accessibility TCC entry alongside `/Applications/InkIt.app`.
</Card>
</CardGroup>
