@agent-native/core 0.134.0 → 0.134.1

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 (57) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/docs/content/template-content-developers.mdx +1 -1
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/templates/clips/changelog/2026-07-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.md +6 -0
  6. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +37 -48
  7. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
  8. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
  9. package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
  10. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
  11. package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
  12. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  13. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +125 -16
  14. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -4
  15. package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
  16. package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
  17. package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
  18. package/corpus/templates/content/actions/_database-utils.ts +12 -3
  19. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
  20. package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
  21. package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
  22. package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
  23. package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
  24. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
  25. package/corpus/templates/content/actions/remove-database-items.ts +183 -0
  26. package/corpus/templates/content/actions/set-document-property.ts +57 -39
  27. package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -0
  28. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +471 -329
  29. package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
  30. package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
  31. package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
  32. package/corpus/templates/content/app/i18n-data.ts +130 -0
  33. package/corpus/templates/content/changelog/2026-07-30-database-rows-can-now-be-removed-without-deleting-their-page.md +6 -0
  34. package/corpus/templates/content/parity/eval-scenarios.ts +2 -2
  35. package/corpus/templates/content/parity/matrix.md +1 -1
  36. package/corpus/templates/content/parity/matrix.ts +4 -5
  37. package/corpus/templates/content/shared/api.ts +4 -0
  38. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -11
  39. package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
  40. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
  41. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
  42. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
  43. package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
  44. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
  45. package/corpus/templates/design/app/pages/Index.tsx +3 -1
  46. package/corpus/templates/design/app/pages/Present.tsx +2 -1
  47. package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
  48. package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
  49. package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
  50. package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
  51. package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
  52. package/corpus/templates/design/package.json +4 -0
  53. package/corpus/templates/design/shared/html-integrity.ts +802 -556
  54. package/dist/notifications/routes.d.ts +3 -3
  55. package/docs/content/template-content-developers.mdx +1 -1
  56. package/package.json +1 -1
  57. package/corpus/templates/content/actions/delete-database-items.ts +0 -89
@@ -146,6 +146,7 @@ mod macos {
146
146
  use crate::capture_audio_bus::{
147
147
  try_subscribe, AudioSources, AudioSubscription, SubscriptionAttempt,
148
148
  };
149
+ use crate::echo_guard::EchoGuard;
149
150
  use crate::native_speech::macos::{
150
151
  start_raw_mic_capture, MicVoiceProcessingMode, RawMicCapture,
151
152
  };
@@ -441,6 +442,11 @@ mod macos {
441
442
  /// Recording capture only persists finals and disables this expensive
442
443
  /// repeated inference; meeting capture keeps it enabled.
443
444
  emit_partials: bool,
445
+ /// Shared speaker-bleed reference, present only while both streams are
446
+ /// capturing. The system stream records what the speakers played into
447
+ /// it; the mic stream reads it back to drop its own echo of that
448
+ /// playback before inference. See `echo_guard`.
449
+ echo_guard: Option<Arc<EchoGuard>>,
444
450
  }
445
451
 
446
452
  impl WhisperStream {
@@ -452,6 +458,7 @@ mod macos {
452
458
  ctx: Arc<WhisperContext>,
453
459
  stream_start: Instant,
454
460
  emit_partials: bool,
461
+ echo_guard: Option<Arc<EchoGuard>>,
455
462
  ) -> Arc<Self> {
456
463
  let done = Arc::new(AtomicBool::new(false));
457
464
  let stream = Arc::new(WhisperStream {
@@ -468,6 +475,7 @@ mod macos {
468
475
  }),
469
476
  reset_generation: AtomicU32::new(0),
470
477
  emit_partials,
478
+ echo_guard,
471
479
  });
472
480
  let worker_stream = stream.clone();
473
481
  std::thread::spawn(move || {
@@ -492,6 +500,11 @@ mod macos {
492
500
  if !self.running.load(Ordering::SeqCst) {
493
501
  return;
494
502
  }
503
+ if self.source == "system" {
504
+ if let Some(guard) = &self.echo_guard {
505
+ guard.note_playback(frames, self.src_rate.load(Ordering::SeqCst) as f64);
506
+ }
507
+ }
495
508
  if let Ok(mut buf) = self.buf.lock() {
496
509
  buf.extend_from_slice(frames);
497
510
  }
@@ -514,15 +527,48 @@ mod macos {
514
527
  .unwrap_or(0)
515
528
  }
516
529
 
517
- /// Mark the start of a fresh buffer (called when the buffer is cleared
530
+ /// Mark the start of a fresh buffer (called when the buffer is drained
518
531
  /// on finalize) so the next utterance's whisper timestamps offset
519
- /// correctly onto the meeting timeline.
520
- fn reset_buffer_start(&self) {
532
+ /// correctly onto the meeting timeline. `pending` is how much audio
533
+ /// survived the drain, which is how far before now the buffer begins.
534
+ fn reset_buffer_start(&self, pending: Duration) {
535
+ let now = Instant::now();
521
536
  if let Ok(mut timeline) = self.timeline.lock() {
522
- timeline.buffer_start = Instant::now();
537
+ timeline.buffer_start = buffer_start_after_drain(now, pending);
523
538
  }
524
539
  }
525
540
 
541
+ /// Wall-clock start of the audio currently sitting in `buf`, used to
542
+ /// line that audio up against the playback reference.
543
+ fn buffer_start(&self) -> Instant {
544
+ self.timeline
545
+ .lock()
546
+ .map(|timeline| timeline.buffer_start)
547
+ .unwrap_or_else(|_| Instant::now())
548
+ }
549
+
550
+ /// Whether `samples` (16 kHz mono) is the speakers bleeding into the
551
+ /// microphone rather than someone talking. Only the mic can be
552
+ /// contaminated: a call app never plays the local user back.
553
+ fn is_playback_echo(&self, samples: &[f32]) -> bool {
554
+ if self.source != "mic" {
555
+ return false;
556
+ }
557
+ self.echo_guard
558
+ .as_ref()
559
+ .is_some_and(|guard| guard.is_playback_echo(samples, self.buffer_start()))
560
+ }
561
+
562
+ /// Clear whichever live partial this stream last rendered. Suppressed
563
+ /// echo emits no final, so without this the overlay would keep showing
564
+ /// the partial that led up to it.
565
+ fn clear_partial(&self) {
566
+ let _ = self.app.emit(
567
+ "voice:partial-transcript",
568
+ serde_json::json!({ "text": "", "source": self.source }),
569
+ );
570
+ }
571
+
526
572
  /// Rebase timestamps to "now" and discard any audio captured while the
527
573
  /// recorder was warming up/counting down.
528
574
  fn reset_timeline(&self) {
@@ -682,6 +728,10 @@ mod macos {
682
728
  sum / count as f32
683
729
  }
684
730
 
731
+ fn buffer_start_after_drain(now: Instant, pending: Duration) -> Instant {
732
+ now.checked_sub(pending).unwrap_or(now)
733
+ }
734
+
685
735
  fn partial_inference_due(
686
736
  emit_partials: bool,
687
737
  had_voice: bool,
@@ -694,6 +744,18 @@ mod macos {
694
744
  && since_last_infer > Duration::from_millis(1200)
695
745
  }
696
746
 
747
+ fn partial_inference_timestamp(
748
+ previous: Instant,
749
+ inference_ran: bool,
750
+ now: Instant,
751
+ ) -> Instant {
752
+ if inference_ran {
753
+ now
754
+ } else {
755
+ previous
756
+ }
757
+ }
758
+
697
759
  fn utterance_finalize_due(have_secs: f32, silence: Duration) -> bool {
698
760
  (have_secs > 0.4 && silence > Duration::from_millis(800)) || have_secs > 25.0
699
761
  }
@@ -790,20 +852,32 @@ mod macos {
790
852
  if stream.reset_generation.load(Ordering::SeqCst) != seen_reset_generation {
791
853
  continue;
792
854
  }
793
- let segs = infer(&mut state, resample_state.samples(), lang);
794
- stream.emit_transcript("voice:final-transcript", &segs, stream.offset_ms());
855
+ if stream.is_playback_echo(resample_state.samples()) {
856
+ // A dropped utterance is indistinguishable from silence
857
+ // in the transcript, so say so here: this log is the
858
+ // only way a false positive is diagnosable afterwards.
859
+ eprintln!("[whisper-mic] suppressed {have_secs:.1}s of speaker bleed");
860
+ stream.clear_partial();
861
+ } else {
862
+ let segs = infer(&mut state, resample_state.samples(), lang);
863
+ stream.emit_transcript("voice:final-transcript", &segs, stream.offset_ms());
864
+ }
795
865
  }
866
+ let mut pending = 0usize;
796
867
  if let Ok(mut b) = stream.buf.lock() {
797
868
  let to_drain = n_processed.min(b.len());
798
869
  b.drain(..to_drain);
870
+ pending = b.len();
799
871
  }
800
872
  // Raw indices shift after the drain above (front-truncated),
801
873
  // so the resample cache is invalid regardless of whether this
802
874
  // utterance ran inference — rebuild fresh from whatever's left.
803
875
  resample_state.drop_all();
804
- // New buffer begins now — advance the timeline offset so the
805
- // next utterance's whisper timestamps map correctly.
806
- stream.reset_buffer_start();
876
+ // Advance the timeline offset so the next utterance's whisper
877
+ // timestamps map correctly. Inference can take seconds, and
878
+ // audio kept arriving throughout it, so the new buffer starts
879
+ // as far back as the audio it already holds — not at "now".
880
+ stream.reset_buffer_start(Duration::from_secs_f32(pending as f32 / src_rate));
807
881
  last_raw_len = 0;
808
882
  had_voice = false;
809
883
  last_infer = Instant::now();
@@ -825,9 +899,15 @@ mod macos {
825
899
  if stream.reset_generation.load(Ordering::SeqCst) != seen_reset_generation {
826
900
  continue;
827
901
  }
828
- let segs = infer(&mut state, resample_state.samples(), lang);
829
- stream.emit_transcript("voice:partial-transcript", &segs, stream.offset_ms());
830
- last_infer = Instant::now();
902
+ let inference_ran = if stream.is_playback_echo(resample_state.samples()) {
903
+ stream.clear_partial();
904
+ false
905
+ } else {
906
+ let segs = infer(&mut state, resample_state.samples(), lang);
907
+ stream.emit_transcript("voice:partial-transcript", &segs, stream.offset_ms());
908
+ true
909
+ };
910
+ last_infer = partial_inference_timestamp(last_infer, inference_ran, Instant::now());
831
911
  }
832
912
  }
833
913
 
@@ -835,7 +915,10 @@ mod macos {
835
915
  let raw = stream.buf.lock().map(|b| b.clone()).unwrap_or_default();
836
916
  let src_rate = stream.src_rate.load(Ordering::SeqCst) as f64;
837
917
  let samples = resample_to_16k(&raw, src_rate);
838
- if had_voice && samples.len() as f32 / SAMPLE_RATE_16K > 0.3 {
918
+ if had_voice
919
+ && samples.len() as f32 / SAMPLE_RATE_16K > 0.3
920
+ && !stream.is_playback_echo(&samples)
921
+ {
839
922
  let segs = infer(&mut state, &samples, lang);
840
923
  stream.emit_transcript("voice:final-transcript", &segs, stream.offset_ms());
841
924
  }
@@ -1028,6 +1111,9 @@ mod macos {
1028
1111
  // competing VoiceProcessingIO mic input. Older macOS versions (and a
1029
1112
  // failed SCK start) keep the existing split-capture fallback.
1030
1113
  let session_start = Instant::now();
1114
+ // Only a session that captures both streams can tell speaker bleed
1115
+ // from speech, so a mic-only session leaves the guard unarmed.
1116
+ let echo_guard = capture_system.then(|| Arc::new(EchoGuard::new()));
1031
1117
  let mic_stream = WhisperStream::new(
1032
1118
  app.clone(),
1033
1119
  "mic",
@@ -1036,6 +1122,7 @@ mod macos {
1036
1122
  ctx.clone(),
1037
1123
  session_start,
1038
1124
  emit_partials,
1125
+ echo_guard.clone(),
1039
1126
  );
1040
1127
  let sys_stream = capture_system.then(|| {
1041
1128
  WhisperStream::new(
@@ -1046,6 +1133,7 @@ mod macos {
1046
1133
  ctx.clone(),
1047
1134
  session_start,
1048
1135
  emit_partials,
1136
+ echo_guard.clone(),
1049
1137
  )
1050
1138
  });
1051
1139
  let mic_for_cb = mic_stream.clone();
@@ -1311,11 +1399,12 @@ mod macos {
1311
1399
 
1312
1400
  #[cfg(test)]
1313
1401
  mod tests {
1314
- use std::time::Duration;
1402
+ use std::time::{Duration, Instant};
1315
1403
 
1316
1404
  use super::{
1317
- partial_inference_due, resample_to_16k, should_use_combined_sck_capture,
1318
- split_mic_capture_options, utterance_finalize_due, IncrementalResample, SessionOwner,
1405
+ buffer_start_after_drain, partial_inference_due, partial_inference_timestamp,
1406
+ resample_to_16k, should_use_combined_sck_capture, split_mic_capture_options,
1407
+ utterance_finalize_due, IncrementalResample, SessionOwner,
1319
1408
  };
1320
1409
  use crate::native_speech::macos::MicVoiceProcessingMode;
1321
1410
 
@@ -1364,6 +1453,17 @@ mod macos {
1364
1453
  );
1365
1454
  }
1366
1455
 
1456
+ #[test]
1457
+ fn buffer_start_accounts_for_audio_captured_during_inference() {
1458
+ let now = Instant::now();
1459
+ let pending = Duration::from_millis(750);
1460
+
1461
+ assert_eq!(
1462
+ buffer_start_after_drain(now, pending),
1463
+ now.checked_sub(pending).unwrap()
1464
+ );
1465
+ }
1466
+
1367
1467
  #[test]
1368
1468
  fn recording_mode_never_runs_live_partial_inference() {
1369
1469
  assert!(!partial_inference_due(
@@ -1390,6 +1490,15 @@ mod macos {
1390
1490
  ));
1391
1491
  }
1392
1492
 
1493
+ #[test]
1494
+ fn echo_suppressed_partial_does_not_reset_retry_cadence() {
1495
+ let previous = Instant::now();
1496
+ let now = previous + Duration::from_secs(2);
1497
+
1498
+ assert_eq!(partial_inference_timestamp(previous, false, now), previous);
1499
+ assert_eq!(partial_inference_timestamp(previous, true, now), now);
1500
+ }
1501
+
1393
1502
  #[test]
1394
1503
  fn recording_mode_keeps_silence_and_long_utterance_finalization() {
1395
1504
  assert!(utterance_finalize_due(1.0, Duration::from_millis(801)));
@@ -132,7 +132,7 @@ filters for that column, hide property columns in the current view without
132
132
  changing other views, and resize column widths. Column headers show compact
133
133
  sort/filter indicators when that column has active view constraints. Table
134
134
  rows can be selected with row checkboxes, and the table shows a compact
135
- selected-row bar with clear, duplicate, confirmed delete, and bulk property
135
+ selected-row bar with clear, duplicate, confirmed membership removal, and bulk property
136
136
  set actions for editable non-computed fields. Empty table cells stay
137
137
  visually blank while remaining clickable for editing, and checkbox table
138
138
  cells render as compact checkbox glyphs instead of "Checked"/"Unchecked"
@@ -228,16 +228,18 @@ property definition.
228
228
  Use `create-content-database`, `create-inline-content-database`,
229
229
  `get-content-database`, `list-trashed-content-databases`,
230
230
  `restore-content-database`, `add-database-item`, `duplicate-database-item`,
231
- `duplicate-database-items`, `delete-database-items`, `move-database-item`,
231
+ `duplicate-database-items`, `remove-database-items`, `move-database-item`,
232
232
  `update-content-database-view`, `list-document-properties`,
233
233
  `configure-document-property`, `set-document-property`,
234
234
  `duplicate-document-property`, and `delete-document-property`; do not edit
235
235
  property rows or view config via raw SQL when an action can do it.
236
236
 
237
237
  When targeting more than one database row, call `duplicate-database-items` or
238
- `delete-database-items` once with a native JSON array of `itemIds` or
238
+ `remove-database-items` once with a native JSON array of `itemIds` or
239
239
  `documentIds`. Do not loop `duplicate-database-item` or `delete-document` for
240
- multi-row duplicate/delete requests.
240
+ multi-row duplicate or membership-removal requests. Removing a row from a
241
+ database preserves its Page, descendants, other database memberships, and
242
+ unrelated property values.
241
243
 
242
244
  Database views follow Notion-style tab labels. When creating or duplicating
243
245
  views in `viewConfig`, use unique default names (`Table 2`, `SEO copy 2`,
@@ -0,0 +1,25 @@
1
+ import { inArray, sql } from "drizzle-orm";
2
+
3
+ import { schema } from "../server/db/index.js";
4
+ import { chunks } from "./_batch-utils.js";
5
+
6
+ export async function lockDatabaseMemberships(db: any, itemIds: string[]) {
7
+ const uniqueItemIds = [...new Set(itemIds)].sort();
8
+ if (uniqueItemIds.length === 0) return;
9
+ const lockedIds = new Set<string>();
10
+ for (const itemIdChunk of chunks(uniqueItemIds, 90)) {
11
+ const lockedRows = await db
12
+ .update(schema.contentDatabaseItems)
13
+ .set({
14
+ updatedAt: sql`${schema.contentDatabaseItems.updatedAt}`,
15
+ })
16
+ .where(inArray(schema.contentDatabaseItems.id, itemIdChunk))
17
+ .returning({ id: schema.contentDatabaseItems.id });
18
+ for (const row of lockedRows) lockedIds.add(row.id);
19
+ }
20
+ if (lockedIds.size !== uniqueItemIds.length) {
21
+ throw new Error(
22
+ "Database memberships changed before the operation completed.",
23
+ );
24
+ }
25
+ }
@@ -79,6 +79,7 @@ function positionCaseSql(
79
79
 
80
80
  export async function resolveDatabaseRowsForBatch(
81
81
  input: DatabaseRowBatchInput,
82
+ options: { includeTrashed?: boolean } = {},
82
83
  ): Promise<{
83
84
  database: typeof schema.contentDatabases.$inferSelect;
84
85
  rows: DatabaseRowBatchRow[];
@@ -151,7 +152,7 @@ export async function resolveDatabaseRowsForBatch(
151
152
  eq(schema.contentDatabaseItems.databaseId, database.id),
152
153
  rowPredicates.length === 1 ? rowPredicates[0] : or(...rowPredicates),
153
154
  isNull(schema.contentDatabases.deletedAt),
154
- isNull(schema.documents.trashedAt),
155
+ options.includeTrashed ? undefined : isNull(schema.documents.trashedAt),
155
156
  ),
156
157
  )
157
158
  .orderBy(asc(schema.contentDatabaseItems.position));
@@ -177,7 +178,10 @@ export async function resolveDatabaseRowsForBatch(
177
178
  throw new Error("Duplicate database row references are not allowed.");
178
179
  }
179
180
 
180
- return { database, rows: requestedRows };
181
+ return {
182
+ database,
183
+ rows: requestedRows,
184
+ };
181
185
  }
182
186
 
183
187
  export async function renumberDatabaseRows(