Decouple transcripts from note text; end-to-end watch transfer acks

Schema v3: transcription attempts accumulate on AudioInfo (text, locale,
confidence, forced flag) — latest attempt is displayed, payload.text is
purely user writing, v2 notes migrate at read time. Editor shows the
transcript in its own live-updating section with insert-into-note; search
matches transcripts weighted by confidence. Watch recordings are now
deleted only on the phone's durable-ingest ack (transferUserInfo), closing
every phone-side loss window; failed sidecar writes unstage instead of
orphaning, and unreadable inbox recordings are surfaced in logs.

Claude-Session: https://claude.ai/code/session_014esDWi42URLEC6Cj17hGQ3
This commit is contained in:
2026-07-16 12:29:49 -04:00
parent c29d25748a
commit c5c90e9441
15 changed files with 546 additions and 55 deletions
+22 -2
View File
@@ -37,6 +37,15 @@ final class PhoneWatchReceiver: NSObject {
session.activate()
self.session = session
}
/// Tell the watch a recording reached a terminal state here, so it can
/// delete its local copy. `transferUserInfo` queues durably the ack
/// arrives even if the watch app is closed right now. Wired to
/// `SyncEngine.onWatchRecordingHandled` by `NotesApp`.
func acknowledgeIngest(id: String) {
guard let session, session.activationState == .activated else { return }
session.transferUserInfo(IngestAck(id: id).userInfo())
}
}
// MARK: - WCSessionDelegate
@@ -95,6 +104,9 @@ extension PhoneWatchReceiver: WCSessionDelegate {
do {
try fm.moveItem(at: file.fileURL, to: audioDest)
} catch {
// Bytes lost on this delivery safe because the watch keeps its
// copy until the ingest ack, which this recording never gets; the
// next watch reconcile redelivers it.
log.error("failed to stage recording \(base, privacy: .public): \(error, privacy: .public)")
return
}
@@ -105,8 +117,16 @@ extension PhoneWatchReceiver: WCSessionDelegate {
"duration": payload.duration,
]
if let tz = payload.timeZoneID { meta["timeZoneID"] = tz }
if let data = try? PropertyListSerialization.data(fromPropertyList: meta, format: .binary, options: 0) {
try? data.write(to: metaDest)
do {
let data = try PropertyListSerialization.data(fromPropertyList: meta, format: .binary, options: 0)
try data.write(to: metaDest)
} catch {
// A sidecar-less m4a would sit in the inbox forever (the drain
// can't ingest it). Remove the audio too and let the un-acked
// watch copy drive a clean redelivery instead.
try? fm.removeItem(at: audioDest)
log.error("failed to write sidecar for \(base, privacy: .public), unstaging: \(error, privacy: .public)")
return
}
log.info("staged watch recording \(base, privacy: .public) for ingest")
}