Skip to content

release: prepare v7.2.6#76

Merged
gaelic-ghost merged 2 commits intomainfrom
plan/issue-45
May 8, 2026
Merged

release: prepare v7.2.6#76
gaelic-ghost merged 2 commits intomainfrom
plan/issue-45

Conversation

@gaelic-ghost
Copy link
Copy Markdown
Owner

@gaelic-ghost gaelic-ghost commented May 8, 2026

Release

  • prepares v7.2.6 from branch plan/issue-45
  • keeps protected main updates behind pull request review and CI
  • release tag v7.2.6 will be created after CI and the review-comment gate pass, so failed or still-discussed release candidates do not get tagged

Review Loop

Before merge and tagging, scripts/repo-maintenance/release.sh watches CI and stops on review comments unless the maintainer has already addressed or resolved them and reruns with --review-comments-addressed.

Summary by CodeRabbit

  • New Features

    • Granular playback events added (start, first-chunk, preroll, rebuffer start/resume, completed), queue/active-request milestones, device and interruption changes.
    • New lifecycle hooks invoked at playback start and completion.
    • Playback updates now include event payloads with timing details.
  • Documentation

    • Guidance updated to highlight observing playback milestones and update structure.
  • Tests

    • Added coverage and helpers validating playback milestones, queue handoff, timing, and environment events.
  • Chores

    • CI iOS smoke job made opt-in via manual trigger.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 8, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c628183-aea8-4287-947f-9ff7cc5048ea

📥 Commits

Reviewing files that changed from the base of the PR and between c5e47c0 and 7930545.

📒 Files selected for processing (5)
  • .github/workflows/swift.yml
  • Sources/SpeakSwiftly/Runtime/WorkerRuntime+ControlRequests.swift
  • Sources/SpeakSwiftly/Runtime/WorkerRuntime+RequestObservation.swift
  • Tests/SpeakSwiftlyTests/Support/TestSupport.swift
  • docs/maintainers/validation-lanes.md
✅ Files skipped from review due to trivial changes (1)
  • docs/maintainers/validation-lanes.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/SpeakSwiftly/Runtime/WorkerRuntime+RequestObservation.swift

📝 Walkthrough

Walkthrough

This pull request expands the public PlaybackEvent vocabulary, adds lifecycle hooks and event-aware publishing APIs, wires event emission throughout the playback queue and runtime, updates tests and test helpers to assert milestone semantics, and updates documentation and CI docs.

Changes

Typed Playback Event System

Layer / File(s) Summary
Public Event Type
Sources/SpeakSwiftly/API/PlaybackObservation.swift
PlaybackEvent enum expanded with 10 new cases covering state changes, request lifecycle (started, completed), queue/active request changes, buffer readiness (prerollReady, rebufferStarted/resumed), and environment changes (outputDeviceChanged, interruptionChanged).
Hook Infrastructure
Sources/SpeakSwiftly/Playback/PlaybackHooks.swift
Added four new @Sendable async closure properties: playbackStarted(String), playbackCompleted(String), activeRequestChanged(), and queueChanged().
Publishing API Refactor
Sources/SpeakSwiftly/Runtime/WorkerRuntime+RequestObservation.swift
Replaced parameterless publishPlaybackUpdate() with two overloads: one accepting optional PlaybackEvent, another accepting a closure mapping PlaybackSnapshot to PlaybackEvent. Refactored makePlaybackUpdate(snapshot:event:advanceSequence:) helper to use snapshot-based construction.
Queue Lifecycle Wiring
Sources/SpeakSwiftly/Playback/PlaybackQueue+Execution.swift
startNextIfPossible() invokes playbackStarted, activeRequestChanged, queueChanged after scheduling. finishPlayback() invokes playbackCompleted, activeRequestChanged, queueChanged before queue resumption.
Event Publishing
Sources/SpeakSwiftly/Playback/RuntimePlaybackEvents.swift
Added publishPlaybackUpdate calls for .firstChunk, .prerollReady (with buffer targets), .rebufferStarted/resumed (with metrics), .outputDeviceChanged, and .interruptionChanged. Removed redundant call from completePlaybackJob.
Runtime Integration
Sources/SpeakSwiftly/Runtime/WorkerRuntimeLifecycle.swift, Sources/SpeakSwiftly/Runtime/WorkerRuntime+ControlRequests.swift
installPlaybackHooks() wires lifecycle event publishing. Control requests (clearQueue, cancelRequest) now emit snapshot-derived .queueChanged events via publishPlaybackUpdate(eventFromSnapshot:).
Test Support
Tests/SpeakSwiftlyTests/Support/TestSupport.swift
PlaybackSpy now maintains playbackState and mutates it through play, stop, pause, resume, state callbacks. Added PlaybackUpdateRecorder helper for recording and querying PlaybackUpdate values.
Test Coverage
Tests/SpeakSwiftlyTests/Runtime/WorkerRuntimeControlSurfaceTests.swift, Tests/SpeakSwiftlyTests/API/LibrarySurfaceTests.swift
Three new integration tests validate playback milestone ordering with timing constraints, pause/resume state changes with queue handoff, and environment event reporting. Metadata stability test added for PlaybackUpdate.event. Event enumeration test validates 13 event cases.
Documentation
Sources/SpeakSwiftly/SpeakSwiftly.docc/*, docs/maintainers/typed-observation-api.md
RuntimeQuickStart directs users to Playback.updates() and PlaybackUpdate.event for stable milestones. Topic lists updated to include PlaybackEvent and PlaybackUpdate. Maintainers guide documents PlaybackEvent enum and clarifies public domain events vs. internal diagnostics.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly Related Issues

Suggested Labels

enhancement

🐰 Swift playback events now take center stage,
Milestones and queue changes on the public stage!
With hooks that hop and snapshots that bind,
Richer observation, a treasure to find. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'release: prepare v7.2.6' is a standard release preparation commit, but the changeset includes substantial feature additions (PlaybackEvent expansion, new hooks, test coverage) beyond typical version bump changes. Update the title to reflect the primary feature changes, e.g., 'feat: expand PlaybackEvent with lifecycle and environment milestones' or include a more descriptive subtitle if the release is the primary intent.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch plan/issue-45

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Tests/SpeakSwiftlyTests/Support/TestSupport.swift (1)

321-514: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

PlaybackSpy.play can leave playbackState stuck at .playing on error/cancel

Line 509 can throw before the Line 512 reset runs, so state() may report stale .playing after failed playback. Move the reset into defer right after setting .playing.

Suggested fix
             play: { [self] _, text, tuningProfile, stream, onEvent in
                 lock.withLock {
                     playCount += 1
                     playbackState = .playing
                 }
+                defer {
+                    lock.withLock {
+                        playbackState = .idle
+                    }
+                }
                 let thresholds = PlaybackThresholdController(text: text, tuningProfile: tuningProfile).thresholds
@@
-                lock.withLock {
-                    playbackState = .idle
-                }
-
                 return PlaybackSummary(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/SpeakSwiftlyTests/Support/TestSupport.swift` around lines 321 - 514,
PlaybackSpy.play can throw/cancel after setting playbackState = .playing and
before it is reset back to .idle, leaving state() stuck at .playing; move the
reset into a defer immediately after you set playbackState = .playing so it's
always executed (wrap the existing lock.withLock { playbackState = .playing }
with a defer that resets playbackState = .idle via lock.withLock), ensuring the
rest of the logic in play (the async chunk loop, behavior switch, and possible
throws) still runs but the state is reliably restored on exit or error.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/SpeakSwiftly/Runtime/WorkerRuntime`+ControlRequests.swift:
- Around line 380-385: cancelRequest is unconditionally calling
publishPlaybackUpdate(.queueChanged(...)) which emits false-positive playback
events when only generation-side state changes; update cancelRequest to take the
current playback snapshot (via the same snapshot type used by
publishPlaybackUpdate) before performing cancellation, perform the
cancellation/mutation, then compute the new playback snapshot and only call
publishPlaybackUpdate(eventFromSnapshot: { .queueChanged(...) }) if the
activeRequest or queuedRequests differ (compare snapshot.activeRequest and
snapshot.queuedRequests with !=) so that queueChanged is emitted only when the
playback queue truly changed.

---

Outside diff comments:
In `@Tests/SpeakSwiftlyTests/Support/TestSupport.swift`:
- Around line 321-514: PlaybackSpy.play can throw/cancel after setting
playbackState = .playing and before it is reset back to .idle, leaving state()
stuck at .playing; move the reset into a defer immediately after you set
playbackState = .playing so it's always executed (wrap the existing
lock.withLock { playbackState = .playing } with a defer that resets
playbackState = .idle via lock.withLock), ensuring the rest of the logic in play
(the async chunk loop, behavior switch, and possible throws) still runs but the
state is reliably restored on exit or error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4a8f3918-2cf7-4625-badc-4a5ae6d0364b

📥 Commits

Reviewing files that changed from the base of the PR and between 8431d35 and c5e47c0.

📒 Files selected for processing (14)
  • Sources/SpeakSwiftly/API/PlaybackObservation.swift
  • Sources/SpeakSwiftly/Playback/PlaybackHooks.swift
  • Sources/SpeakSwiftly/Playback/PlaybackQueue+Execution.swift
  • Sources/SpeakSwiftly/Playback/RuntimePlaybackEvents.swift
  • Sources/SpeakSwiftly/Playback/RuntimePlaybackQueueCommands.swift
  • Sources/SpeakSwiftly/Runtime/WorkerRuntime+ControlRequests.swift
  • Sources/SpeakSwiftly/Runtime/WorkerRuntime+RequestObservation.swift
  • Sources/SpeakSwiftly/Runtime/WorkerRuntimeLifecycle.swift
  • Sources/SpeakSwiftly/SpeakSwiftly.docc/RuntimeQuickStart.md
  • Sources/SpeakSwiftly/SpeakSwiftly.docc/SpeakSwiftly.md
  • Tests/SpeakSwiftlyTests/API/LibrarySurfaceTests.swift
  • Tests/SpeakSwiftlyTests/Runtime/WorkerRuntimeControlSurfaceTests.swift
  • Tests/SpeakSwiftlyTests/Support/TestSupport.swift
  • docs/maintainers/typed-observation-api.md
💤 Files with no reviewable changes (1)
  • Sources/SpeakSwiftly/Playback/RuntimePlaybackQueueCommands.swift

Comment thread Sources/SpeakSwiftly/Runtime/WorkerRuntime+ControlRequests.swift Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c5e47c0976

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 277 to 279
let latestUpdate = if let latestPlaybackUpdate = playbackObservationBroker.latestUpdate {
latestPlaybackUpdate
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit snapshot state on new playback subscriptions

playbackUpdates() replays playbackObservationBroker.latestUpdate to every new subscriber. After this commit, updates can carry one-shot milestones like .started, .firstChunk, or .completed, so a subscriber that attaches later can receive a stale milestone and treat it as a fresh live event (for example, re-running completion/start handlers on subscribe). For milestone-style events, the initial emission should be synthesized from the current snapshot (typically .stateChanged(snapshot.state)) instead of replaying the last arbitrary event.

Useful? React with 👍 / 👎.

Comment on lines +380 to +384
await publishPlaybackUpdate(eventFromSnapshot: { snapshot in
.queueChanged(
activeRequest: snapshot.activeRequest,
queuedRequests: snapshot.queuedRequests,
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Publish active-request milestone after request cancellation

In the cancelRequest control path, playback observers are only sent .queueChanged(...). When the cancelled request is the active playback and no successor starts immediately, the active request transitions to nil without an .activeRequestChanged(nil) milestone, so consumers that key off the dedicated active-request event can miss this transition. Emit an activeRequestChanged update here (or alongside this queue update) to keep milestone coverage consistent for cancellations.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79305454bf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +219 to +221
await publishPlaybackUpdate(
event: .outputDeviceChanged(previousDevice: previousDevice, currentDevice: currentDevice),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Deduplicate output-device milestone emissions

When an output route changes during an active playback request, AudioPlaybackDriver.handleObservedOutputDeviceChange emits .outputDeviceChanged to both environmentEventSink and activeEventSink (Sources/SpeakSwiftly/Playback/AudioPlaybackDriver+EnvironmentRecovery.swift lines 38-51). This commit now publishes a public playback update in both handlers (handlePlaybackEvent and handlePlaybackEnvironmentEvent in RuntimePlaybackEvents.swift), so subscribers can receive the same PlaybackEvent.outputDeviceChanged twice for one hardware change and run side effects twice (for example, duplicate UI/device-change handling). Consider publishing from only one path or adding deduplication.

Useful? React with 👍 / 👎.

@gaelic-ghost gaelic-ghost merged commit 1b9913e into main May 8, 2026
5 checks passed
@gaelic-ghost gaelic-ghost deleted the plan/issue-45 branch May 8, 2026 13:51
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