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:
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user