Skip to content

feat(ios27): real token accounting and interruption-safe voice capture - #10

Merged
asmuelle merged 1 commit into
mainfrom
claude/ios-27-api-improvements-09e939
Sep 16, 2026
Merged

asmuelle merged 1 commit into
mainfrom
claude/ios-27-api-improvements-09e939

Conversation

@asmuelle

Copy link
Copy Markdown
Owner

Summary

First two items from the iOS 27 adoption review, both fixing real failure modes in shipped paths rather than adding surface. Deployment target stays iOS 26.0; every iOS 27 API is behind #available.

Model accounting (ReflectKit)

  • TokenAccounting.swift: prompt budget from the model's real context size, overflow rebudget scaled by the reported overshoot, and a bounded TokenUsageLedger actor that calibrates the characters-per-token estimate from what the model actually counted. Counts only, never text; calibration can only make the estimate stricter.
  • ModelAccounting.swift: reads SystemLanguageModel.default.contextSize (exact on 27, back-deployed 4096 on 26), maps LanguageModelError.contextSizeExceeded and the iOS 26 equivalent to a typed ReflectionError.contextExceeded, records response.usage on 27.
  • Both FoundationModels providers budget the prompt and retry exactly once with a shrunk budget on overflow. The weekly review trims each entry summary to fit instead of sending the week unbudgeted; entries are never dropped so the 1-based citation numbering stays in lockstep with resolve(numbers:in:).

Audio interruptions (CaptureKit)

  • TranscriptionEngine.interruptions() channel and CaptureInterruption event, opened inside start() so an event right after start is buffered rather than missed.
  • CaptureState.interrupted(draft:) parks the words captured so far. Coordinator resumes and appends when the system recommends it, hands the draft to the editor otherwise, and exposes continueAfterInterruption() for the new "Keep going" stage. Re-entrancy between a tap and a system-driven resume is guarded; the interruption subscription is dropped before the engine drains on stop.
  • SpeechTranscriberEngine observes iOS 27 AVAudioSession.DidBecomeInactiveMessage (system deactivation only) and ResumptionRecommendationMessage, falling back to the legacy interruptionNotification on iOS 26. Observers are removed when the stream terminates.
  • Copy localized in all nine tables.

Test plan

  • New tests: 6 accounting, 3 week trimming, 6 interruption state machine
  • swift test --parallel full core package
  • iOS Simulator build against the iOS 27 SDK
  • App test bundle on iPhone 17 Pro simulator (StoreKit suite skipped as on CI)
  • Code review pass; two coordinator races found and fixed
  • On-device: take a phone call mid-recording, confirm words are kept and recording resumes (interruptions cannot be exercised in the Simulator)

Notes

  • Copy enum now sits one line over SwiftLint's 400-line type-body warning; splitting it into extensions would clear it.
  • LocalizedOverrideTests can flake under --parallel because of a process-global override; unrelated to this change and tracked separately.
  • CaptureSummaryProvider and the InsightKit extractor still create sessions without accounting; natural next step.

🤖 Generated with Claude Code

ReflectKit budgets prompts against SystemLanguageModel.contextSize (exact on
iOS 27, back-deployed 4096 on 26), maps LanguageModelError.contextSizeExceeded
to a typed ReflectionError.contextExceeded, retries once with a budget shrunk
by the reported overshoot, and records response.usage into a bounded
TokenUsageLedger that only ever tightens the characters-per-token estimate.
The weekly review trims each entry to fit instead of sending the week
unbudgeted; entries are never dropped so citation numbering stays in lockstep.

CaptureKit adds a CaptureInterruption channel opened in TranscriptionEngine
.start(), a CaptureState.interrupted(draft:) that parks the words so far, and
coordinator handling: resume and append when the system recommends it, hand
the draft to the editor otherwise, or let the writer tap "Keep going". The
real engine observes iOS 27's AVAudioSession didBecomeInactive /
resumptionRecommendation messages and falls back to the legacy interruption
notification on iOS 26. New copy is localized in all nine tables.
@asmuelle
asmuelle merged commit 7c021c3 into main Sep 16, 2026
2 of 4 checks passed
@asmuelle
asmuelle deleted the claude/ios-27-api-improvements-09e939 branch September 16, 2026 19:32
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