@agent-native/core 0.114.6 → 0.114.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/mcp-config-writers.ts +6 -4
  5. package/corpus/core/src/cli/mcp.ts +124 -0
  6. package/corpus/core/src/cli/skills-content/help.ts +4 -3
  7. package/corpus/core/src/cli/skills-content/index.ts +1 -0
  8. package/corpus/core/src/cli/skills-content/rewind-skill.ts +52 -0
  9. package/corpus/core/src/cli/skills.ts +115 -13
  10. package/corpus/core/src/mcp/screen-memory-stdio.ts +1360 -104
  11. package/corpus/core/src/mcp-client/index.ts +9 -0
  12. package/corpus/core/src/mcp-client/screen-memory-local.ts +847 -13
  13. package/corpus/templates/clips/actions/apply-rewind-extension.ts +195 -0
  14. package/corpus/templates/clips/actions/delete-agent-recording-if-unpromoted.ts +64 -0
  15. package/corpus/templates/clips/actions/get-rewind-extension-request.ts +22 -0
  16. package/corpus/templates/clips/actions/get-screen-memory-status.ts +18 -2
  17. package/corpus/templates/clips/actions/list-rewind-extension-requests.ts +52 -0
  18. package/corpus/templates/clips/actions/make-recording-private-for-rewind.ts +55 -0
  19. package/corpus/templates/clips/actions/query-screen-memory-context.ts +4 -4
  20. package/corpus/templates/clips/actions/request-rewind-extension.ts +81 -0
  21. package/corpus/templates/clips/actions/update-rewind-extension-request.ts +47 -0
  22. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +25 -0
  23. package/corpus/templates/clips/app/components/editor/editor-toolbar.tsx +36 -0
  24. package/corpus/templates/clips/app/components/editor/rewind-extension-dialog.tsx +338 -0
  25. package/corpus/templates/clips/app/components/editor/stitch-manager.tsx +1 -16
  26. package/corpus/templates/clips/app/components/editor/timeline.tsx +26 -0
  27. package/corpus/templates/clips/app/i18n/en-US.ts +18 -0
  28. package/corpus/templates/clips/app/lib/ffmpeg-export.ts +19 -12
  29. package/corpus/templates/clips/app/lib/rewind-visibility.ts +31 -0
  30. package/corpus/templates/clips/app/lib/timestamp-mapping.ts +7 -0
  31. package/corpus/templates/clips/app/lib/upload-file-client.ts +17 -0
  32. package/corpus/templates/clips/changelog/2026-07-14-rewind-keeps-recent-screen-context-local-and-turns-it-into-clips.md +6 -0
  33. package/corpus/templates/clips/changelog/2026-07-17-active-clips-keep-rewind-stable-and-settings-clear.md +12 -0
  34. package/corpus/templates/clips/changelog/2026-07-17-clicking-the-clips-icon-during-a-recording-now-opens-the-app.md +6 -0
  35. package/corpus/templates/clips/changelog/2026-07-17-default-mic-now-uses-the-mac-default-and-transcription-shares-one-capture.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-17-full-screen-clips-finalize-even-when-screencapturekit-stop-hangs.md +6 -0
  37. package/corpus/templates/clips/changelog/2026-07-17-normal-full-screen-clips-no-longer-stop-immediately-after-th.md +6 -0
  38. package/corpus/templates/clips/changelog/2026-07-17-rewind-can-hand-one-bounded-private-clip-to-your-agent.md +12 -0
  39. package/corpus/templates/clips/changelog/2026-07-17-rewind-home-can-copy-a-local-agent-prompt.md +6 -0
  40. package/corpus/templates/clips/changelog/2026-07-17-rewind-now-has-a-calm-home-status-focused-first-run-consent-.md +6 -0
  41. package/corpus/templates/clips/changelog/2026-07-17-rewind-settings-are-smaller-clearer-and-agent-first.md +6 -0
  42. package/corpus/templates/clips/changelog/2026-07-17-the-full-clips-interface-stays-available-while-recording.md +6 -0
  43. package/corpus/templates/clips/changelog/2026-07-18-rewind-organizes-recent-work-into-local-searchable-chapters.md +8 -0
  44. package/corpus/templates/clips/changelog/2026-07-18-searchable-rewind-chapters.md +6 -0
  45. package/corpus/templates/clips/changelog/2026-07-19-rewind-backed-recordings-now-start-immediately-after-the-cou.md +6 -0
  46. package/corpus/templates/clips/changelog/2026-07-20-rewind-can-set-up-your-local-agent-once-so-future-requests-w.md +6 -0
  47. package/corpus/templates/clips/desktop/package.json +1 -0
  48. package/corpus/templates/clips/desktop/scripts/sign-macos-local.ts +12 -1
  49. package/corpus/templates/clips/desktop/src/app.tsx +2498 -239
  50. package/corpus/templates/clips/desktop/src/components/ReadinessPanel.tsx +21 -1
  51. package/corpus/templates/clips/desktop/src/components/Switch.tsx +3 -0
  52. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +156 -24
  53. package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +6 -7
  54. package/corpus/templates/clips/desktop/src/lib/recorder.ts +1763 -1160
  55. package/corpus/templates/clips/desktop/src/lib/rewind-agent-prompt.ts +9 -0
  56. package/corpus/templates/clips/desktop/src/lib/rewind-recording-start.ts +20 -0
  57. package/corpus/templates/clips/desktop/src/main.tsx +3 -0
  58. package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +14 -14
  59. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +4 -4
  60. package/corpus/templates/clips/desktop/src/overlays/preparing.tsx +13 -0
  61. package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +27 -2
  62. package/corpus/templates/clips/desktop/src/shared/config.ts +16 -0
  63. package/corpus/templates/clips/desktop/src/styles.css +956 -134
  64. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +5 -0
  65. package/corpus/templates/clips/desktop/src-tauri/build.rs +69 -1
  66. package/corpus/templates/clips/desktop/src-tauri/src/accessibility.rs +386 -12
  67. package/corpus/templates/clips/desktop/src-tauri/src/capture_audio_bus.rs +382 -0
  68. package/corpus/templates/clips/desktop/src-tauri/src/capture_graph.rs +777 -0
  69. package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +249 -22
  70. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +148 -1
  71. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +58 -4
  72. package/corpus/templates/clips/desktop/src-tauri/src/logfile.rs +13 -1
  73. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +1505 -51
  74. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +124 -0
  75. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +1006 -95
  76. package/corpus/templates/clips/desktop/src-tauri/src/rewind_capture_suspension.rs +165 -0
  77. package/corpus/templates/clips/desktop/src-tauri/src/rewind_chapters.rs +2434 -0
  78. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +1650 -0
  79. package/corpus/templates/clips/desktop/src-tauri/src/rewind_egress.rs +643 -0
  80. package/corpus/templates/clips/desktop/src-tauri/src/rewind_local_ask.rs +803 -0
  81. package/corpus/templates/clips/desktop/src-tauri/src/rewind_meeting_history.rs +595 -0
  82. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory.rs +2900 -91
  83. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_ocr.rs +360 -0
  84. package/corpus/templates/clips/desktop/src-tauri/src/screen_memory_transcript.rs +408 -0
  85. package/corpus/templates/clips/desktop/src-tauri/src/shortcuts.rs +114 -33
  86. package/corpus/templates/clips/desktop/src-tauri/src/state.rs +2 -2
  87. package/corpus/templates/clips/desktop/src-tauri/src/system_audio.rs +13 -3
  88. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +4 -1
  89. package/corpus/templates/clips/desktop/src-tauri/src/util.rs +7 -13
  90. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +198 -11
  91. package/corpus/templates/clips/desktop/src-tauri/tauri.alpha.conf.json +17 -0
  92. package/dist/cli/mcp-config-writers.d.ts +2 -2
  93. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  94. package/dist/cli/mcp-config-writers.js +6 -6
  95. package/dist/cli/mcp-config-writers.js.map +1 -1
  96. package/dist/cli/mcp.d.ts +14 -0
  97. package/dist/cli/mcp.d.ts.map +1 -1
  98. package/dist/cli/mcp.js +79 -0
  99. package/dist/cli/mcp.js.map +1 -1
  100. package/dist/cli/skills-content/help.d.ts +1 -1
  101. package/dist/cli/skills-content/help.d.ts.map +1 -1
  102. package/dist/cli/skills-content/help.js +4 -3
  103. package/dist/cli/skills-content/help.js.map +1 -1
  104. package/dist/cli/skills-content/index.d.ts +1 -0
  105. package/dist/cli/skills-content/index.d.ts.map +1 -1
  106. package/dist/cli/skills-content/index.js +1 -0
  107. package/dist/cli/skills-content/index.js.map +1 -1
  108. package/dist/cli/skills-content/rewind-skill.d.ts +2 -0
  109. package/dist/cli/skills-content/rewind-skill.d.ts.map +1 -0
  110. package/dist/cli/skills-content/rewind-skill.js +53 -0
  111. package/dist/cli/skills-content/rewind-skill.js.map +1 -0
  112. package/dist/cli/skills.d.ts +6 -0
  113. package/dist/cli/skills.d.ts.map +1 -1
  114. package/dist/cli/skills.js +90 -14
  115. package/dist/cli/skills.js.map +1 -1
  116. package/dist/mcp/screen-memory-stdio.d.ts +365 -0
  117. package/dist/mcp/screen-memory-stdio.d.ts.map +1 -1
  118. package/dist/mcp/screen-memory-stdio.js +1014 -91
  119. package/dist/mcp/screen-memory-stdio.js.map +1 -1
  120. package/dist/mcp-client/index.d.ts +1 -1
  121. package/dist/mcp-client/index.d.ts.map +1 -1
  122. package/dist/mcp-client/index.js +1 -1
  123. package/dist/mcp-client/index.js.map +1 -1
  124. package/dist/mcp-client/screen-memory-local.d.ts +90 -0
  125. package/dist/mcp-client/screen-memory-local.d.ts.map +1 -1
  126. package/dist/mcp-client/screen-memory-local.js +590 -12
  127. package/dist/mcp-client/screen-memory-local.js.map +1 -1
  128. package/dist/notifications/routes.d.ts +3 -3
  129. package/dist/observability/routes.d.ts +1 -1
  130. package/dist/provider-api/actions/provider-api.d.ts +4 -4
  131. package/dist/resources/handlers.d.ts +1 -1
  132. package/package.json +1 -1
  133. package/src/cli/mcp-config-writers.ts +6 -4
  134. package/src/cli/mcp.ts +124 -0
  135. package/src/cli/skills-content/help.ts +4 -3
  136. package/src/cli/skills-content/index.ts +1 -0
  137. package/src/cli/skills-content/rewind-skill.ts +52 -0
  138. package/src/cli/skills.ts +115 -13
  139. package/src/mcp/screen-memory-stdio.ts +1360 -104
  140. package/src/mcp-client/index.ts +9 -0
  141. package/src/mcp-client/screen-memory-local.ts +847 -13
@@ -0,0 +1,595 @@
1
+ //! Explicit, local-only meeting transcript recovery from the rolling Rewind.
2
+ //!
3
+ //! The normal meeting workflow is still authoritative. This command only
4
+ //! prepends locally indexed transcript rows when a person deliberately asks to
5
+ //! include the scheduled portion they missed before pressing Start notes.
6
+
7
+ use crate::capture_graph::{CaptureConsumer, CaptureSource};
8
+ use crate::config::RewindCaptureMode;
9
+ use crate::screen_memory::{self, ScreenMemoryRuntimeState, ScreenMemorySegmentMetadata};
10
+ use crate::screen_memory_transcript::{
11
+ ScreenMemoryTranscriptRow, ScreenMemoryTranscriptState, ScreenMemoryTranscriptStatus,
12
+ };
13
+ use chrono::{DateTime, Duration as ChronoDuration, Utc};
14
+ use serde::{Deserialize, Serialize};
15
+ use std::collections::HashMap;
16
+ use std::fs::File;
17
+ use std::io::{BufRead, BufReader};
18
+ use std::path::{Path, PathBuf};
19
+ use std::sync::atomic::{AtomicU64, Ordering};
20
+ use std::sync::Mutex;
21
+ use std::time::{Duration, Instant};
22
+ use tauri::{AppHandle, Manager};
23
+
24
+ const COVERAGE_TOLERANCE_MS: i64 = 2_500;
25
+ const MAX_MEETING_HISTORY_HOURS: i64 = 8;
26
+ const INDEX_WAIT_TIMEOUT: Duration = Duration::from_secs(45);
27
+ const INDEX_POLL_INTERVAL: Duration = Duration::from_millis(200);
28
+ static HISTORY_REQUEST_COUNTER: AtomicU64 = AtomicU64::new(1);
29
+
30
+ #[derive(Default)]
31
+ pub struct RewindMeetingHistoryState {
32
+ requests: Mutex<HashMap<String, PreparedMeetingHistory>>,
33
+ }
34
+
35
+ #[derive(Clone)]
36
+ struct PreparedMeetingHistory {
37
+ scheduled_start: DateTime<Utc>,
38
+ captured_until: DateTime<Utc>,
39
+ segments: Vec<ScreenMemorySegmentMetadata>,
40
+ pin_id: String,
41
+ }
42
+
43
+ #[derive(Debug, Clone, Serialize)]
44
+ #[serde(rename_all = "camelCase")]
45
+ pub struct RewindMeetingHistoryAvailability {
46
+ pub available: bool,
47
+ pub reason: Option<String>,
48
+ pub covered_from: Option<String>,
49
+ }
50
+
51
+ #[derive(Debug, Clone, Serialize)]
52
+ #[serde(rename_all = "camelCase")]
53
+ pub struct RewindMeetingTranscriptSegment {
54
+ pub start_ms: u64,
55
+ pub end_ms: u64,
56
+ pub text: String,
57
+ pub source: String,
58
+ }
59
+
60
+ #[derive(Debug, Clone, Serialize)]
61
+ #[serde(rename_all = "camelCase")]
62
+ pub struct RewindMeetingHistoryResult {
63
+ pub scheduled_start: String,
64
+ pub captured_until: String,
65
+ pub segments: Vec<RewindMeetingTranscriptSegment>,
66
+ }
67
+
68
+ #[derive(Debug, Clone, Serialize)]
69
+ #[serde(rename_all = "camelCase")]
70
+ pub struct RewindMeetingHistoryPrepared {
71
+ pub token: String,
72
+ pub scheduled_start: String,
73
+ pub captured_until: String,
74
+ }
75
+
76
+ #[tauri::command]
77
+ pub async fn rewind_meeting_history_status(
78
+ app: AppHandle,
79
+ scheduled_start: String,
80
+ ) -> Result<RewindMeetingHistoryAvailability, String> {
81
+ let requested_start = parse_scheduled_start(&scheduled_start)?;
82
+ availability(&app, requested_start).await
83
+ }
84
+
85
+ #[tauri::command]
86
+ pub async fn rewind_meeting_history_prepare(
87
+ app: AppHandle,
88
+ scheduled_start: String,
89
+ ) -> Result<RewindMeetingHistoryPrepared, String> {
90
+ let requested_start = parse_scheduled_start(&scheduled_start)?;
91
+ let availability = availability(&app, requested_start).await?;
92
+ if !availability.available {
93
+ return Err(availability.reason.unwrap_or_else(|| {
94
+ "Rewind does not have continuous local audio coverage from this meeting's start."
95
+ .to_string()
96
+ }));
97
+ }
98
+
99
+ authorize_explicit_meeting_extension(&app, requested_start)?;
100
+
101
+ // Close the current logical fragment without stopping the one physical
102
+ // capture producer. Its transcript is queued locally by the normal segment
103
+ // finalization path; no raw media leaves the Mac.
104
+ screen_memory::fence_active_for_clip(&app)
105
+ .map_err(|error| format!("Could not include the earlier meeting: {error}"))?;
106
+ let captured_until = Utc::now();
107
+
108
+ let segments = relevant_segments(&app, requested_start, captured_until)?;
109
+ validate_continuous_audio_coverage(&segments, requested_start, captured_until)?;
110
+ reject_graph_gaps(&app, requested_start, captured_until)?;
111
+ let token = format!(
112
+ "meeting-history-{}-{}",
113
+ Utc::now().timestamp_millis(),
114
+ HISTORY_REQUEST_COUNTER.fetch_add(1, Ordering::Relaxed)
115
+ );
116
+ let pin_id = format!("{token}-segments");
117
+ for segment in &segments {
118
+ if let Err(error) = screen_memory::pin_segment(&app, &segment.id, &pin_id) {
119
+ for pinned in &segments {
120
+ let _ = screen_memory::unpin_segment(&app, &pinned.id, &pin_id);
121
+ }
122
+ return Err(error);
123
+ }
124
+ }
125
+ app.state::<RewindMeetingHistoryState>()
126
+ .requests
127
+ .lock()
128
+ .map_err(|error| error.to_string())?
129
+ .insert(
130
+ token.clone(),
131
+ PreparedMeetingHistory {
132
+ scheduled_start: requested_start,
133
+ captured_until,
134
+ segments,
135
+ pin_id,
136
+ },
137
+ );
138
+
139
+ Ok(RewindMeetingHistoryPrepared {
140
+ token,
141
+ scheduled_start: requested_start.to_rfc3339(),
142
+ captured_until: captured_until.to_rfc3339(),
143
+ })
144
+ }
145
+
146
+ #[tauri::command]
147
+ pub async fn rewind_meeting_history_collect(
148
+ app: AppHandle,
149
+ token: String,
150
+ ) -> Result<RewindMeetingHistoryResult, String> {
151
+ let prepared = app
152
+ .state::<RewindMeetingHistoryState>()
153
+ .requests
154
+ .lock()
155
+ .map_err(|error| error.to_string())?
156
+ .get(&token)
157
+ .cloned()
158
+ .ok_or_else(|| {
159
+ "This prepared meeting-history request is no longer available.".to_string()
160
+ })?;
161
+
162
+ let result = async {
163
+ wait_for_local_indexes(&app, &prepared.segments).await?;
164
+
165
+ let mut rows = Vec::new();
166
+ for segment in &prepared.segments {
167
+ rows.extend(read_transcript_rows(&app, &segment.id)?);
168
+ }
169
+ let mut mapped =
170
+ map_rows_to_meeting_timeline(rows, prepared.scheduled_start, prepared.captured_until);
171
+ mapped.sort_by_key(|row| (row.start_ms, row.end_ms, row.source.clone()));
172
+
173
+ Ok(RewindMeetingHistoryResult {
174
+ scheduled_start: prepared.scheduled_start.to_rfc3339(),
175
+ captured_until: prepared.captured_until.to_rfc3339(),
176
+ segments: mapped,
177
+ })
178
+ }
179
+ .await;
180
+
181
+ if let Ok(mut requests) = app.state::<RewindMeetingHistoryState>().requests.lock() {
182
+ requests.remove(&token);
183
+ }
184
+ for segment in &prepared.segments {
185
+ let _ = screen_memory::unpin_segment(&app, &segment.id, &prepared.pin_id);
186
+ }
187
+ result
188
+ }
189
+
190
+ #[tauri::command]
191
+ pub fn rewind_meeting_history_cancel(app: AppHandle, token: String) -> Result<(), String> {
192
+ let prepared = app
193
+ .state::<RewindMeetingHistoryState>()
194
+ .requests
195
+ .lock()
196
+ .map_err(|error| error.to_string())?
197
+ .remove(&token);
198
+ if let Some(prepared) = prepared {
199
+ for segment in &prepared.segments {
200
+ let _ = screen_memory::unpin_segment(&app, &segment.id, &prepared.pin_id);
201
+ }
202
+ }
203
+ Ok(())
204
+ }
205
+
206
+ fn parse_scheduled_start(value: &str) -> Result<DateTime<Utc>, String> {
207
+ let parsed = DateTime::parse_from_rfc3339(value)
208
+ .map_err(|_| "This meeting does not have a valid scheduled start time.".to_string())?
209
+ .with_timezone(&Utc);
210
+ let now = Utc::now();
211
+ if parsed > now + ChronoDuration::minutes(1) {
212
+ return Err("The meeting has not started yet.".to_string());
213
+ }
214
+ if now - parsed > ChronoDuration::hours(MAX_MEETING_HISTORY_HOURS) {
215
+ return Err(format!(
216
+ "Include from meeting start is limited to the previous {MAX_MEETING_HISTORY_HOURS} hours."
217
+ ));
218
+ }
219
+ Ok(parsed)
220
+ }
221
+
222
+ async fn availability(
223
+ app: &AppHandle,
224
+ requested_start: DateTime<Utc>,
225
+ ) -> Result<RewindMeetingHistoryAvailability, String> {
226
+ let status = screen_memory::screen_memory_status(app.clone()).await?;
227
+ let unavailable = |reason: &str| RewindMeetingHistoryAvailability {
228
+ available: false,
229
+ reason: Some(reason.to_string()),
230
+ covered_from: None,
231
+ };
232
+ if !status.available || !status.config.enabled || status.config.paused {
233
+ return Ok(unavailable(
234
+ "Turn on Rewind before using Include from meeting start.",
235
+ ));
236
+ }
237
+ if status.config.capture_mode != RewindCaptureMode::VisualsAudio {
238
+ return Ok(unavailable(
239
+ "Include from meeting start needs Rewind set to Visuals + audio.",
240
+ ));
241
+ }
242
+ if status.exclusion_active {
243
+ return Ok(unavailable(
244
+ "Rewind is currently stopped for an excluded app.",
245
+ ));
246
+ }
247
+ if !matches!(status.state, ScreenMemoryRuntimeState::Recording) {
248
+ return Ok(unavailable("Rewind is not currently recording."));
249
+ }
250
+
251
+ let now = Utc::now();
252
+ let mut segments = screen_memory::finalized_segments(app)?;
253
+ if let Some(active) = status.active_segment {
254
+ let active_start = parse_time(&active.started_at)?;
255
+ if active_start <= requested_start + ChronoDuration::milliseconds(COVERAGE_TOLERANCE_MS)
256
+ && validate_existing_prefix(&mut segments, requested_start, active_start).is_ok()
257
+ {
258
+ let covered_from = earliest_coverage_start(&segments)
259
+ .unwrap_or(active_start)
260
+ .to_rfc3339();
261
+ if reject_graph_gaps(app, requested_start, now).is_err() {
262
+ return Ok(unavailable(
263
+ "Rewind recorded a source coverage gap after this meeting started.",
264
+ ));
265
+ }
266
+ return Ok(RewindMeetingHistoryAvailability {
267
+ available: true,
268
+ reason: None,
269
+ covered_from: Some(covered_from),
270
+ });
271
+ }
272
+ if active_start <= now
273
+ && validate_existing_prefix(&mut segments, requested_start, active_start).is_ok()
274
+ {
275
+ let covered_from = earliest_coverage_start(&segments)
276
+ .unwrap_or(active_start)
277
+ .to_rfc3339();
278
+ if reject_graph_gaps(app, requested_start, now).is_err() {
279
+ return Ok(unavailable(
280
+ "Rewind recorded a source coverage gap after this meeting started.",
281
+ ));
282
+ }
283
+ return Ok(RewindMeetingHistoryAvailability {
284
+ available: true,
285
+ reason: None,
286
+ covered_from: Some(covered_from),
287
+ });
288
+ }
289
+ }
290
+ Ok(unavailable(
291
+ "Rewind does not have continuous local audio coverage from this meeting's start.",
292
+ ))
293
+ }
294
+
295
+ /// Records that this longer-than-a-normal-Clip retrospective range was an
296
+ /// explicit scheduled-meeting choice. The graph's dedicated meeting-start API
297
+ /// intentionally permits this while ordinary Rewind extensions remain capped
298
+ /// at five minutes.
299
+ fn authorize_explicit_meeting_extension(
300
+ app: &AppHandle,
301
+ requested_start: DateTime<Utc>,
302
+ ) -> Result<(), String> {
303
+ let age = (Utc::now() - requested_start)
304
+ .to_std()
305
+ .map_err(|_| "The scheduled meeting start is in the future.".to_string())?;
306
+ let now = Instant::now();
307
+ let requested_instant = now.checked_sub(age).unwrap_or(now);
308
+ let graph = app.state::<crate::capture_graph::CaptureGraphState>();
309
+ let mut graph = graph.0.lock().map_err(|error| error.to_string())?;
310
+ let lease = graph
311
+ .start_consumer(
312
+ CaptureConsumer::Meeting,
313
+ [CaptureSource::SystemAudio, CaptureSource::Microphone],
314
+ )
315
+ .map_err(|error| error.to_string())?;
316
+ if let Err(error) = graph.extend_retrospectively_to_start(&lease.id, requested_instant) {
317
+ let _ = graph.end_consumer(&lease.id);
318
+ return Err(error.to_string());
319
+ }
320
+ graph
321
+ .end_consumer(&lease.id)
322
+ .map_err(|error| error.to_string())?;
323
+ Ok(())
324
+ }
325
+
326
+ fn validate_existing_prefix(
327
+ segments: &mut Vec<ScreenMemorySegmentMetadata>,
328
+ requested_start: DateTime<Utc>,
329
+ active_start: DateTime<Utc>,
330
+ ) -> Result<(), String> {
331
+ let mut relevant = segments
332
+ .iter()
333
+ .filter_map(|segment| {
334
+ let start = parse_time(&segment.started_at).ok()?;
335
+ let end = parse_time(&segment.ended_at).ok()?;
336
+ (end >= requested_start && start <= active_start).then_some(segment.clone())
337
+ })
338
+ .collect::<Vec<_>>();
339
+ relevant.sort_by_key(|segment| segment.started_at.clone());
340
+ if relevant.is_empty() {
341
+ if active_start <= requested_start + ChronoDuration::milliseconds(COVERAGE_TOLERANCE_MS) {
342
+ return Ok(());
343
+ }
344
+ return Err("missing local coverage".to_string());
345
+ }
346
+ validate_continuous_audio_coverage(&relevant, requested_start, active_start)
347
+ }
348
+
349
+ fn earliest_coverage_start(segments: &[ScreenMemorySegmentMetadata]) -> Option<DateTime<Utc>> {
350
+ segments
351
+ .iter()
352
+ .filter_map(|segment| parse_time(&segment.started_at).ok())
353
+ .min()
354
+ }
355
+
356
+ fn relevant_segments(
357
+ app: &AppHandle,
358
+ requested_start: DateTime<Utc>,
359
+ captured_until: DateTime<Utc>,
360
+ ) -> Result<Vec<ScreenMemorySegmentMetadata>, String> {
361
+ let mut segments = screen_memory::finalized_segments(app)?
362
+ .into_iter()
363
+ .filter_map(|segment| {
364
+ let start = parse_time(&segment.started_at).ok()?;
365
+ let end = parse_time(&segment.ended_at).ok()?;
366
+ (end >= requested_start && start <= captured_until).then_some(segment)
367
+ })
368
+ .collect::<Vec<_>>();
369
+ segments.sort_by_key(|segment| segment.started_at.clone());
370
+ Ok(segments)
371
+ }
372
+
373
+ fn validate_continuous_audio_coverage(
374
+ segments: &[ScreenMemorySegmentMetadata],
375
+ requested_start: DateTime<Utc>,
376
+ captured_until: DateTime<Utc>,
377
+ ) -> Result<(), String> {
378
+ let first = segments.first().ok_or_else(|| {
379
+ "Rewind has no finalized local audio for the requested meeting range.".to_string()
380
+ })?;
381
+ let first_start = parse_time(&first.started_at)?;
382
+ if first_start > requested_start + ChronoDuration::milliseconds(COVERAGE_TOLERANCE_MS) {
383
+ return Err("Rewind coverage begins after the scheduled meeting start.".to_string());
384
+ }
385
+ let mut prior_end = requested_start;
386
+ for segment in segments {
387
+ if segment.corrupt || segment.exclusion_tainted || !segment.path.exists() {
388
+ return Err(
389
+ "Rewind found an incomplete or privacy-excluded segment in the requested range."
390
+ .to_string(),
391
+ );
392
+ }
393
+ if segment.capture_mode != RewindCaptureMode::VisualsAudio {
394
+ return Err("Part of this meeting was captured without audio.".to_string());
395
+ }
396
+ let start = parse_time(&segment.started_at)?;
397
+ let end = parse_time(&segment.ended_at)?;
398
+ if start > prior_end + ChronoDuration::milliseconds(COVERAGE_TOLERANCE_MS) {
399
+ return Err("Rewind has a coverage gap after the meeting started.".to_string());
400
+ }
401
+ if end > prior_end {
402
+ prior_end = end;
403
+ }
404
+ }
405
+ if prior_end + ChronoDuration::milliseconds(COVERAGE_TOLERANCE_MS) < captured_until {
406
+ return Err(
407
+ "Rewind could not finalize audio through the moment notes started.".to_string(),
408
+ );
409
+ }
410
+ Ok(())
411
+ }
412
+
413
+ fn reject_graph_gaps(
414
+ app: &AppHandle,
415
+ requested_start: DateTime<Utc>,
416
+ captured_until: DateTime<Utc>,
417
+ ) -> Result<(), String> {
418
+ let graph = app.state::<crate::capture_graph::CaptureGraphState>();
419
+ let status = graph
420
+ .0
421
+ .lock()
422
+ .map_err(|error| error.to_string())?
423
+ .status_at(Instant::now())
424
+ .map_err(|error| error.to_string())?;
425
+ for gap in status.coverage_gaps {
426
+ let start = parse_time(&gap.interval.started_at)?;
427
+ let end = parse_time(&gap.interval.ended_at)?;
428
+ if start < captured_until && end > requested_start {
429
+ return Err("Rewind recorded a source coverage gap during this meeting.".to_string());
430
+ }
431
+ }
432
+ Ok(())
433
+ }
434
+
435
+ async fn wait_for_local_indexes(
436
+ app: &AppHandle,
437
+ segments: &[ScreenMemorySegmentMetadata],
438
+ ) -> Result<(), String> {
439
+ let deadline = Instant::now() + INDEX_WAIT_TIMEOUT;
440
+ loop {
441
+ let mut pending = false;
442
+ for segment in segments {
443
+ let status = read_transcript_status(app, &segment.id)?;
444
+ match status.state {
445
+ ScreenMemoryTranscriptState::Ready => {}
446
+ ScreenMemoryTranscriptState::Pending
447
+ | ScreenMemoryTranscriptState::Transcribing => pending = true,
448
+ ScreenMemoryTranscriptState::Failed => {
449
+ return Err(status.error.unwrap_or_else(|| {
450
+ "Local transcription failed for part of this meeting.".to_string()
451
+ }))
452
+ }
453
+ ScreenMemoryTranscriptState::Skipped => {
454
+ return Err(
455
+ "Local transcription was unavailable for part of this meeting.".to_string(),
456
+ )
457
+ }
458
+ }
459
+ }
460
+ if !pending {
461
+ return Ok(());
462
+ }
463
+ if Instant::now() >= deadline {
464
+ return Err("The earlier meeting audio is still being indexed locally. Start notes without including history, or try again in a moment.".to_string());
465
+ }
466
+ tokio::time::sleep(INDEX_POLL_INTERVAL).await;
467
+ }
468
+ }
469
+
470
+ fn screen_memory_dir(app: &AppHandle) -> Result<PathBuf, String> {
471
+ app.path()
472
+ .app_data_dir()
473
+ .map(|path| path.join("screen-memory"))
474
+ .map_err(|error| format!("app data directory unavailable: {error}"))
475
+ }
476
+
477
+ fn read_transcript_status(
478
+ app: &AppHandle,
479
+ segment_id: &str,
480
+ ) -> Result<ScreenMemoryTranscriptStatus, String> {
481
+ let path = screen_memory_dir(app)?.join(format!("{segment_id}.transcript-status.json"));
482
+ let bytes = std::fs::read(&path).map_err(|_| {
483
+ "The earlier meeting audio has not finished indexing locally yet.".to_string()
484
+ })?;
485
+ serde_json::from_slice(&bytes)
486
+ .map_err(|_| "A local transcript status file is malformed.".to_string())
487
+ }
488
+
489
+ fn read_transcript_rows(
490
+ app: &AppHandle,
491
+ segment_id: &str,
492
+ ) -> Result<Vec<ScreenMemoryTranscriptRow>, String> {
493
+ let path = screen_memory_dir(app)?.join(format!("{segment_id}.transcript.jsonl"));
494
+ read_jsonl(&path)
495
+ }
496
+
497
+ fn read_jsonl<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<Vec<T>, String> {
498
+ let file = File::open(path)
499
+ .map_err(|_| "A ready local meeting transcript is missing its rows.".to_string())?;
500
+ BufReader::new(file)
501
+ .lines()
502
+ .filter_map(|line| match line {
503
+ Ok(line) if line.trim().is_empty() => None,
504
+ other => Some(other),
505
+ })
506
+ .map(|line| {
507
+ let line = line.map_err(|error| format!("local transcript read failed: {error}"))?;
508
+ serde_json::from_str(&line)
509
+ .map_err(|_| "A local meeting transcript row is malformed.".to_string())
510
+ })
511
+ .collect()
512
+ }
513
+
514
+ fn map_rows_to_meeting_timeline(
515
+ rows: Vec<ScreenMemoryTranscriptRow>,
516
+ scheduled_start: DateTime<Utc>,
517
+ captured_until: DateTime<Utc>,
518
+ ) -> Vec<RewindMeetingTranscriptSegment> {
519
+ rows.into_iter()
520
+ .filter_map(|row| {
521
+ let captured_at = parse_time(&row.captured_at).ok()?;
522
+ if captured_at > captured_until
523
+ || captured_at < scheduled_start - ChronoDuration::minutes(1)
524
+ {
525
+ return None;
526
+ }
527
+ let start_ms = (captured_at - scheduled_start).num_milliseconds().max(0) as u64;
528
+ let duration_ms = row.end_ms.saturating_sub(row.start_ms);
529
+ let end_ms = start_ms.saturating_add(duration_ms);
530
+ let source = match row.source.as_str() {
531
+ "system-audio" => "system",
532
+ "microphone" => "mic",
533
+ _ => return None,
534
+ };
535
+ Some(RewindMeetingTranscriptSegment {
536
+ start_ms,
537
+ end_ms,
538
+ text: row.text,
539
+ source: source.to_string(),
540
+ })
541
+ })
542
+ .collect()
543
+ }
544
+
545
+ fn parse_time(value: &str) -> Result<DateTime<Utc>, String> {
546
+ DateTime::parse_from_rfc3339(value)
547
+ .map(|time| time.with_timezone(&Utc))
548
+ .map_err(|_| "Rewind found malformed local capture timing metadata.".to_string())
549
+ }
550
+
551
+ #[cfg(test)]
552
+ mod tests {
553
+ use super::*;
554
+
555
+ #[test]
556
+ fn maps_rows_to_scheduled_meeting_timeline() {
557
+ let scheduled = DateTime::parse_from_rfc3339("2026-07-14T12:00:00Z")
558
+ .unwrap()
559
+ .with_timezone(&Utc);
560
+ let rows = vec![ScreenMemoryTranscriptRow {
561
+ schema_version: 1,
562
+ segment_id: "one".to_string(),
563
+ source: "system-audio".to_string(),
564
+ captured_at: "2026-07-14T12:00:03Z".to_string(),
565
+ start_ms: 3_000,
566
+ end_ms: 4_250,
567
+ text: "hello".to_string(),
568
+ }];
569
+ let mapped =
570
+ map_rows_to_meeting_timeline(rows, scheduled, scheduled + ChronoDuration::minutes(1));
571
+ assert_eq!(mapped.len(), 1);
572
+ assert_eq!(mapped[0].start_ms, 3_000);
573
+ assert_eq!(mapped[0].end_ms, 4_250);
574
+ assert_eq!(mapped[0].source, "system");
575
+ }
576
+
577
+ #[test]
578
+ fn prepared_history_never_absorbs_rows_after_its_fence() {
579
+ let scheduled = DateTime::parse_from_rfc3339("2026-07-14T12:00:00Z")
580
+ .unwrap()
581
+ .with_timezone(&Utc);
582
+ let rows = vec![ScreenMemoryTranscriptRow {
583
+ schema_version: 1,
584
+ segment_id: "replacement-segment".to_string(),
585
+ source: "microphone".to_string(),
586
+ captured_at: "2026-07-14T12:00:11Z".to_string(),
587
+ start_ms: 0,
588
+ end_ms: 1_000,
589
+ text: "already belongs to live capture".to_string(),
590
+ }];
591
+ let mapped =
592
+ map_rows_to_meeting_timeline(rows, scheduled, scheduled + ChronoDuration::seconds(10));
593
+ assert!(mapped.is_empty());
594
+ }
595
+ }