Files
wristivo/Adapters/WatchBridge/INTEGRATION-PROPOSAL.md
T

6.0 KiB

Historical agent handoff. The integrated composition and current test status are documented in docs/FIRST-DEVICE-PROTOTYPE.md and docs/VALIDATION.md. Proposed wiring below has been incorporated or superseded.

WatchBridge integration proposal

Status: accepted with integration decisions below. This proposal is retained as design history; docs/CONTRACTS.md and RemoteCore/WatchSession.swift are the authoritative final API. The original suggested signatures below are not the implementation contract.

Integration decisions

  • Shared DTOs are implemented, including RelayFailure conforming to Error.
  • RelayJSON centrally enforces millisecond dates and the 4096-byte payload bound.
  • PhoneCommandCoordinating is an Actor protocol with explicit request values and a local WatchPeerGeneration argument on every operation, plus activation and deactivation callbacks. This resolves stale counterpart/lifecycle events.
  • The bounded three-key container remains adapter-owned; RelayMessageKind freezes its v1 routing vocabulary. Only A's single actor consumes command admission.
  • An adjacent local Swift package provides Common/iPhone/Watch source and test targets. Xcode links its platform products instead of duplicating source files directly in the app targets. New B source files require no project regeneration.
  • B may use a fake injected actor now. A's production conformance and physical device behavior remain later integration gates, not blockers to B's coding.

Original proposal (historical)

Shared RemoteCore additions requested

Add typed, Codable, Sendable, Equatable control-plane values to RemoteCore. Names are suggestions; the wire values and semantics need to be frozen by integration before B implements them.

public struct BeginRemoteSessionRequest: Codable, Sendable, Equatable {
    public let schemaVersion: Int
}

public struct RemoteSessionGrant: Codable, Sendable, Equatable {
    public let schemaVersion: Int
    public let sessionID: UUID
    public let televisionID: UUID
    public let issuedAt: Date
    public let maximumCommandLifetimeMilliseconds: UInt64
}

public struct EndRemoteSessionRequest: Codable, Sendable, Equatable {
    public let schemaVersion: Int
    public let sessionID: UUID
}

public struct RemoteSessionEnded: Codable, Sendable, Equatable {
    public let schemaVersion: Int
    public let sessionID: UUID
}

public struct RelayFailure: Codable, Sendable, Equatable {
    public enum Code: String, Codable, Sendable {
        case unsupportedSchema
        case malformedRequest
        case noSelectedTelevision
        case unavailable
        case sessionInvalidated
    }

    public let schemaVersion: Int
    public let code: Code
}

maximumCommandLifetimeMilliseconds must be positive and no greater than CommandAdmission.maximumLifetime * 1_000. issuedAt is encoded as JSON milliseconds since Unix epoch. A grant authorizes no command by itself and must be bound by the phone actor to the currently activated paired WCSession and the phone's currently selected TV.

The bridge container remains adapter-owned and has exactly three keys:

schemaVersion: Int == 1
kind: String
payload: Data (maximum 4 KiB)

Freeze these v1 kind values: beginSession, session, endSession, ended, command, receipt, and error. Every request receives one reply container. Unsupported versions, unknown kinds, missing/extra or incorrectly typed keys, oversized payloads, and structural decode failures fail closed with an error reply. Semantic command rejection remains solely in the phone command actor.

Injected phone actor seam requested

Integration/A should provide one actor conforming to a narrow protocol such as:

public protocol PhoneCommandCoordinating: Sendable {
    func beginWatchSession() async -> Result<RemoteSessionGrant, RelayFailure>
    func endWatchSession(_ request: EndRemoteSessionRequest) async
        -> Result<RemoteSessionEnded, RelayFailure>
    func submitWatchCommand(_ envelope: CommandEnvelope) async -> CommandReceipt
    func watchBridgeDidDeactivate() async
}

That actor remains the only owner of the authorized session and CommandAdmission. WatchBridge will retain only the injected protocol reference on iPhone. It will not instantiate CommandAdmission, perform semantic admission, select a TV, or issue session UUIDs.

Please confirm whether a changed paired-watch identity can be surfaced to the actor as watchBridgeDidDeactivate() or needs an explicit opaque peer generation parameter. No persistent hardware identifier should cross this seam.

Project/test wiring requested

Update the integrator-owned project generator and generated project to:

  • compile Adapters/WatchBridge/Sources/Common into both app targets;
  • compile Sources/iPhone only into the iPhone target and Sources/Watch only into the watchOS target;
  • link WatchConnectivity through normal SDK imports (no third-party runtime);
  • add adjacent fake-driven test targets for common, iPhone, and watch behavior;
  • keep the adapter outside the pure algorithm package.

The test target must be able to inject a session facade rather than subclassing or manufacturing WCSession. Planned coverage: reachable/unreachable delivery, activation/deactivation, phone/watch reinstall or counterpart switch, duplicate callback suppression, reply timeout, lost reply, invalid containers, 4 KiB payload boundary, millisecond date round trips, and application-context sanitization.

Migration and impact

This is additive to contract v1 but introduces the first control-plane wire format. Existing demo apps remain demos until integration explicitly injects the live adapter. Mixed installations fail closed on schema mismatch and expose an update-required/unavailable UI state; they do not fall back to queued commands.

No permission string, background mode, credential path, analytics, raw motion, or third-party dependency is requested. Physical iPhone/Watch testing remains required for locked-phone wake, relaunch, counterpart switching, and actual reachability behavior without Xcode attached.