@agent-native/core 0.133.3 → 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 (221) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +62 -0
  3. package/corpus/core/docs/content/observability.mdx +41 -1
  4. package/corpus/core/docs/content/template-content-developers.mdx +1 -1
  5. package/corpus/core/docs/content/tracking.mdx +25 -0
  6. package/corpus/core/package.json +1 -1
  7. package/corpus/core/src/agent/run-manager.ts +7 -0
  8. package/corpus/core/src/client/agent-chat-adapter.ts +10 -0
  9. package/corpus/core/src/client/analytics.ts +105 -1
  10. package/corpus/core/src/deploy/build.ts +38 -1
  11. package/corpus/core/src/observability/posthog-ai.ts +294 -0
  12. package/corpus/core/src/observability/routes.ts +29 -6
  13. package/corpus/core/src/observability/traces.ts +287 -12
  14. package/corpus/core/src/observability/types.ts +11 -0
  15. package/corpus/core/src/secrets/register-framework-secrets.ts +16 -0
  16. package/corpus/core/src/server/agent-run-context.ts +21 -0
  17. package/corpus/core/src/server/capture-error.ts +8 -0
  18. package/corpus/core/src/server/core-routes-plugin.ts +75 -4
  19. package/corpus/core/src/server/deploy-environment.ts +57 -0
  20. package/corpus/core/src/server/error-noise-filter.ts +268 -0
  21. package/corpus/core/src/server/posthog-config.ts +75 -0
  22. package/corpus/core/src/server/request-context.ts +6 -0
  23. package/corpus/core/src/server/sentry-config.ts +4 -8
  24. package/corpus/core/src/server/sentry-plugin.ts +13 -29
  25. package/corpus/core/src/server/sentry.ts +13 -156
  26. package/corpus/core/src/server/ssr-handler.ts +6 -1
  27. package/corpus/core/src/shared/mcp-embed-headers.ts +1 -1
  28. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  29. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  30. package/corpus/core/src/tracking/error-capture.ts +44 -109
  31. package/corpus/core/src/tracking/index.ts +11 -0
  32. package/corpus/core/src/tracking/posthog-exception.ts +305 -0
  33. package/corpus/core/src/tracking/providers.ts +91 -14
  34. package/corpus/core/src/tracking/redaction.ts +105 -0
  35. package/corpus/templates/clips/changelog/2026-07-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.md +6 -0
  36. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +37 -48
  37. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
  38. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
  39. package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
  40. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
  41. package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
  42. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  43. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +125 -16
  44. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -4
  45. package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
  46. package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
  47. package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
  48. package/corpus/templates/content/actions/_database-utils.ts +12 -3
  49. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
  50. package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
  51. package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
  52. package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
  53. package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
  54. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
  55. package/corpus/templates/content/actions/remove-database-items.ts +183 -0
  56. package/corpus/templates/content/actions/set-document-property.ts +57 -39
  57. package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -0
  58. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +471 -329
  59. package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
  60. package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
  61. package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
  62. package/corpus/templates/content/app/i18n-data.ts +130 -0
  63. package/corpus/templates/content/changelog/2026-07-30-database-rows-can-now-be-removed-without-deleting-their-page.md +6 -0
  64. package/corpus/templates/content/parity/eval-scenarios.ts +2 -2
  65. package/corpus/templates/content/parity/matrix.md +1 -1
  66. package/corpus/templates/content/parity/matrix.ts +4 -5
  67. package/corpus/templates/content/shared/api.ts +4 -0
  68. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -11
  69. package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
  70. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
  71. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
  72. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
  73. package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
  74. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
  75. package/corpus/templates/design/app/pages/Index.tsx +3 -1
  76. package/corpus/templates/design/app/pages/Present.tsx +2 -1
  77. package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
  78. package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
  79. package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
  80. package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
  81. package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
  82. package/corpus/templates/design/package.json +4 -0
  83. package/corpus/templates/design/shared/html-integrity.ts +802 -556
  84. package/dist/agent/run-manager.d.ts.map +1 -1
  85. package/dist/agent/run-manager.js +7 -0
  86. package/dist/agent/run-manager.js.map +1 -1
  87. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  88. package/dist/client/agent-chat-adapter.js +12 -0
  89. package/dist/client/agent-chat-adapter.js.map +1 -1
  90. package/dist/client/analytics.d.ts +8 -0
  91. package/dist/client/analytics.d.ts.map +1 -1
  92. package/dist/client/analytics.js +95 -1
  93. package/dist/client/analytics.js.map +1 -1
  94. package/dist/collab/routes.d.ts +1 -1
  95. package/dist/collab/struct-routes.d.ts +1 -1
  96. package/dist/deploy/build.d.ts.map +1 -1
  97. package/dist/deploy/build.js +38 -1
  98. package/dist/deploy/build.js.map +1 -1
  99. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  100. package/dist/notifications/routes.d.ts +1 -1
  101. package/dist/observability/posthog-ai.d.ts +126 -0
  102. package/dist/observability/posthog-ai.d.ts.map +1 -0
  103. package/dist/observability/posthog-ai.js +190 -0
  104. package/dist/observability/posthog-ai.js.map +1 -0
  105. package/dist/observability/routes.d.ts +3 -3
  106. package/dist/observability/routes.d.ts.map +1 -1
  107. package/dist/observability/routes.js +26 -6
  108. package/dist/observability/routes.js.map +1 -1
  109. package/dist/observability/traces.d.ts +9 -0
  110. package/dist/observability/traces.d.ts.map +1 -1
  111. package/dist/observability/traces.js +219 -10
  112. package/dist/observability/traces.js.map +1 -1
  113. package/dist/observability/types.d.ts +10 -0
  114. package/dist/observability/types.d.ts.map +1 -1
  115. package/dist/observability/types.js +1 -0
  116. package/dist/observability/types.js.map +1 -1
  117. package/dist/progress/routes.d.ts +1 -1
  118. package/dist/resources/handlers.d.ts +1 -1
  119. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  120. package/dist/secrets/register-framework-secrets.js +14 -0
  121. package/dist/secrets/register-framework-secrets.js.map +1 -1
  122. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  123. package/dist/server/agent-run-context.d.ts +8 -0
  124. package/dist/server/agent-run-context.d.ts.map +1 -1
  125. package/dist/server/agent-run-context.js +18 -0
  126. package/dist/server/agent-run-context.js.map +1 -1
  127. package/dist/server/capture-error.d.ts +8 -0
  128. package/dist/server/capture-error.d.ts.map +1 -1
  129. package/dist/server/capture-error.js.map +1 -1
  130. package/dist/server/core-routes-plugin.d.ts +0 -23
  131. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  132. package/dist/server/core-routes-plugin.js +71 -7
  133. package/dist/server/core-routes-plugin.js.map +1 -1
  134. package/dist/server/deploy-environment.d.ts +17 -0
  135. package/dist/server/deploy-environment.d.ts.map +1 -0
  136. package/dist/server/deploy-environment.js +47 -0
  137. package/dist/server/deploy-environment.js.map +1 -0
  138. package/dist/server/error-noise-filter.d.ts +80 -0
  139. package/dist/server/error-noise-filter.d.ts.map +1 -0
  140. package/dist/server/error-noise-filter.js +173 -0
  141. package/dist/server/error-noise-filter.js.map +1 -0
  142. package/dist/server/posthog-config.d.ts +28 -0
  143. package/dist/server/posthog-config.d.ts.map +1 -0
  144. package/dist/server/posthog-config.js +53 -0
  145. package/dist/server/posthog-config.js.map +1 -0
  146. package/dist/server/request-context.d.ts +6 -0
  147. package/dist/server/request-context.d.ts.map +1 -1
  148. package/dist/server/request-context.js.map +1 -1
  149. package/dist/server/sentry-config.d.ts +1 -0
  150. package/dist/server/sentry-config.d.ts.map +1 -1
  151. package/dist/server/sentry-config.js +3 -1
  152. package/dist/server/sentry-config.js.map +1 -1
  153. package/dist/server/sentry-plugin.d.ts.map +1 -1
  154. package/dist/server/sentry-plugin.js +12 -27
  155. package/dist/server/sentry-plugin.js.map +1 -1
  156. package/dist/server/sentry.d.ts.map +1 -1
  157. package/dist/server/sentry.js +8 -124
  158. package/dist/server/sentry.js.map +1 -1
  159. package/dist/server/ssr-handler.d.ts.map +1 -1
  160. package/dist/server/ssr-handler.js +6 -1
  161. package/dist/server/ssr-handler.js.map +1 -1
  162. package/dist/server/transcribe-voice.d.ts +1 -1
  163. package/dist/shared/mcp-embed-headers.d.ts +1 -1
  164. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  165. package/dist/shared/mcp-embed-headers.js +1 -1
  166. package/dist/shared/mcp-embed-headers.js.map +1 -1
  167. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  168. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  169. package/dist/tracking/error-capture.d.ts +7 -0
  170. package/dist/tracking/error-capture.d.ts.map +1 -1
  171. package/dist/tracking/error-capture.js +10 -73
  172. package/dist/tracking/error-capture.js.map +1 -1
  173. package/dist/tracking/index.d.ts +1 -0
  174. package/dist/tracking/index.d.ts.map +1 -1
  175. package/dist/tracking/index.js +1 -0
  176. package/dist/tracking/index.js.map +1 -1
  177. package/dist/tracking/posthog-exception.d.ts +103 -0
  178. package/dist/tracking/posthog-exception.d.ts.map +1 -0
  179. package/dist/tracking/posthog-exception.js +181 -0
  180. package/dist/tracking/posthog-exception.js.map +1 -0
  181. package/dist/tracking/providers.d.ts +14 -0
  182. package/dist/tracking/providers.d.ts.map +1 -1
  183. package/dist/tracking/providers.js +63 -11
  184. package/dist/tracking/providers.js.map +1 -1
  185. package/dist/tracking/redaction.d.ts +25 -0
  186. package/dist/tracking/redaction.d.ts.map +1 -0
  187. package/dist/tracking/redaction.js +85 -0
  188. package/dist/tracking/redaction.js.map +1 -0
  189. package/docs/content/observability.mdx +41 -1
  190. package/docs/content/template-content-developers.mdx +1 -1
  191. package/docs/content/tracking.mdx +25 -0
  192. package/package.json +1 -1
  193. package/src/agent/run-manager.ts +7 -0
  194. package/src/client/agent-chat-adapter.ts +10 -0
  195. package/src/client/analytics.ts +105 -1
  196. package/src/deploy/build.ts +38 -1
  197. package/src/observability/posthog-ai.ts +294 -0
  198. package/src/observability/routes.ts +29 -6
  199. package/src/observability/traces.ts +287 -12
  200. package/src/observability/types.ts +11 -0
  201. package/src/secrets/register-framework-secrets.ts +16 -0
  202. package/src/server/agent-run-context.ts +21 -0
  203. package/src/server/capture-error.ts +8 -0
  204. package/src/server/core-routes-plugin.ts +75 -4
  205. package/src/server/deploy-environment.ts +57 -0
  206. package/src/server/error-noise-filter.ts +268 -0
  207. package/src/server/posthog-config.ts +75 -0
  208. package/src/server/request-context.ts +6 -0
  209. package/src/server/sentry-config.ts +4 -8
  210. package/src/server/sentry-plugin.ts +13 -29
  211. package/src/server/sentry.ts +13 -156
  212. package/src/server/ssr-handler.ts +6 -1
  213. package/src/shared/mcp-embed-headers.ts +1 -1
  214. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +36 -10
  215. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +55 -1
  216. package/src/tracking/error-capture.ts +44 -109
  217. package/src/tracking/index.ts +11 -0
  218. package/src/tracking/posthog-exception.ts +305 -0
  219. package/src/tracking/providers.ts +91 -14
  220. package/src/tracking/redaction.ts +105 -0
  221. package/corpus/templates/content/actions/delete-database-items.ts +0 -89
@@ -13,6 +13,7 @@
13
13
  * automatically by the core-routes plugin).
14
14
  */
15
15
 
16
+ import { reshapeTrackedExceptionProperties } from "./posthog-exception.js";
16
17
  import { registerTrackingProvider } from "./registry.js";
17
18
  import type { TrackingProvider, TrackingEvent } from "./types.js";
18
19
 
@@ -162,27 +163,61 @@ function isPostHogAiObservabilityEvent(eventName: string): boolean {
162
163
  return eventName.startsWith("$ai_");
163
164
  }
164
165
 
165
- function createPostHogProvider(apiKey: string, host: string): TrackingProvider {
166
+ /**
167
+ * `$ai_*` and `$exception` are ingested through PostHog's dedicated endpoint
168
+ * rather than `/capture/`, and `$exception` additionally has to carry
169
+ * `$exception_list` — the framework's own `captureException()` emits camelCase
170
+ * fields that PostHog would otherwise render as an empty, ungroupable issue.
171
+ */
172
+ function createPostHogProvider(
173
+ apiKey: string,
174
+ host: string,
175
+ errorTracking: boolean,
176
+ ): TrackingProvider {
177
+ const sendToEventsEndpoint = (
178
+ event: TrackingEvent,
179
+ properties: Record<string, unknown> | undefined,
180
+ distinctId: string,
181
+ ): void => {
182
+ enqueue(
183
+ `${host}/i/v0/e/`,
184
+ JSON.stringify({
185
+ api_key: apiKey,
186
+ event: event.name,
187
+ properties: {
188
+ distinct_id: distinctId,
189
+ ...properties,
190
+ timestamp: event.timestamp,
191
+ },
192
+ }),
193
+ );
194
+ };
195
+
166
196
  return {
167
197
  name: "posthog",
168
198
  track(event: TrackingEvent) {
169
199
  const distinctId = event.userId || "anonymous";
170
200
  if (isPostHogAiObservabilityEvent(event.name)) {
171
- enqueue(
172
- `${host}/i/v0/e/`,
173
- JSON.stringify({
174
- api_key: apiKey,
175
- event: event.name,
176
- properties: {
177
- distinct_id: distinctId,
178
- ...event.properties,
179
- timestamp: event.timestamp,
180
- },
181
- }),
182
- );
201
+ sendToEventsEndpoint(event, event.properties, distinctId);
183
202
  return;
184
203
  }
185
204
 
205
+ if (event.name === "$exception") {
206
+ // `POSTHOG_ERROR_TRACKING=false` keeps product analytics flowing while
207
+ // another backend owns crashes. Drop rather than downgrade to
208
+ // `/capture/`: a malformed exception is what this branch exists to
209
+ // prevent.
210
+ if (!errorTracking) return;
211
+ const reshaped = reshapeTrackedExceptionProperties(event.properties);
212
+ if (reshaped) {
213
+ sendToEventsEndpoint(event, reshaped, distinctId);
214
+ return;
215
+ }
216
+ // No recognizable exception fields. Fall through to `/capture/` so the
217
+ // event is still recorded as-is rather than becoming an issue with
218
+ // nothing in it.
219
+ }
220
+
186
221
  enqueue(
187
222
  `${host}/capture/`,
188
223
  JSON.stringify({
@@ -213,6 +248,42 @@ function createPostHogProvider(apiKey: string, host: string): TrackingProvider {
213
248
  };
214
249
  }
215
250
 
251
+ /**
252
+ * Send one event to PostHog only, bypassing the provider fan-out.
253
+ *
254
+ * For payloads whose *shape* is PostHog-specific and whose *content* other
255
+ * backends must not receive. `track()` broadcasts to every configured provider,
256
+ * so a PostHog-only integration (e.g. enabling a survey id) would otherwise
257
+ * start exporting that integration's content — including user-authored text —
258
+ * to Mixpanel, Amplitude, webhooks, and Agent Native Analytics as a side
259
+ * effect nobody opted into.
260
+ *
261
+ * Returns `false` when PostHog is not configured, so callers can tell "not
262
+ * sent" from "sent".
263
+ */
264
+ export function sendPostHogEvent(
265
+ name: string,
266
+ properties: Record<string, unknown>,
267
+ distinctId: string,
268
+ ): boolean {
269
+ const apiKey = process.env.POSTHOG_API_KEY;
270
+ if (!apiKey) return false;
271
+ const host = (process.env.POSTHOG_HOST || POSTHOG_DEFAULT_HOST).replace(
272
+ /\/+$/,
273
+ "",
274
+ );
275
+ enqueue(
276
+ `${host}/capture/`,
277
+ JSON.stringify({
278
+ api_key: apiKey,
279
+ event: name,
280
+ distinct_id: distinctId,
281
+ properties: { ...properties, timestamp: new Date().toISOString() },
282
+ }),
283
+ );
284
+ return true;
285
+ }
286
+
216
287
  // ─── Mixpanel ──────────────────────────────────────────────────────────────
217
288
 
218
289
  function createMixpanelProvider(token: string): TrackingProvider {
@@ -383,7 +454,13 @@ export function registerBuiltinProviders(): void {
383
454
  /\/+$/,
384
455
  "",
385
456
  );
386
- registerTrackingProvider(createPostHogProvider(posthogKey, host));
457
+ registerTrackingProvider(
458
+ createPostHogProvider(
459
+ posthogKey,
460
+ host,
461
+ process.env.POSTHOG_ERROR_TRACKING?.trim().toLowerCase() !== "false",
462
+ ),
463
+ );
387
464
  }
388
465
 
389
466
  const mixpanelToken = process.env.MIXPANEL_TOKEN;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Bounding and redaction helpers shared by every exception emitter.
3
+ *
4
+ * Kept free of `process.env` and Node built-ins so the browser bundle can use
5
+ * the same rules — an exception shaped one way on the server and another way
6
+ * in the client is how a leak ships in only one of them.
7
+ */
8
+
9
+ export const MAX_MESSAGE_LENGTH = 1000;
10
+ export const MAX_STACK_LENGTH = 8000;
11
+ export const MAX_TAGS = 30;
12
+ export const MAX_EXTRA_KEYS = 30;
13
+ export const MAX_EXTRA_VALUE_LENGTH = 1000;
14
+
15
+ const SECRET_RE = /\b(?:bearer|basic)\s+[^\s]+/gi;
16
+
17
+ export const SECRET_KEY_RE =
18
+ /(?:authorization|cookie|set[-_]?cookie|token|secret|password|passwd|pwd|api[-_]?key|apikey|credential)/i;
19
+
20
+ export function redact(value: string): string {
21
+ return value
22
+ .replace(SECRET_RE, (match) => `${match.split(/\s+/, 1)[0]} <redacted>`)
23
+ .replace(
24
+ /([A-Za-z0-9_$.-]*(?:authorization|cookie|token|secret|password|passwd|pwd|api[-_]?key|apikey|credential)[A-Za-z0-9_$.-]*\s*[:=]\s*)([^\s,;}]+)/gi,
25
+ "$1<redacted>",
26
+ );
27
+ }
28
+
29
+ export function boundedText(value: unknown, max: number): string {
30
+ const text = typeof value === "string" ? value : String(value ?? "");
31
+ const safe = redact(text);
32
+ return safe.length > max ? safe.slice(0, max) : safe;
33
+ }
34
+
35
+ export function safeValue(value: unknown, depth = 2): unknown {
36
+ if (
37
+ value == null ||
38
+ typeof value === "boolean" ||
39
+ typeof value === "number"
40
+ ) {
41
+ return value;
42
+ }
43
+ if (typeof value === "string")
44
+ return boundedText(value, MAX_EXTRA_VALUE_LENGTH);
45
+ if (depth <= 0) return boundedText(value, MAX_EXTRA_VALUE_LENGTH);
46
+ if (Array.isArray(value)) {
47
+ return value.slice(0, 20).map((item) => safeValue(item, depth - 1));
48
+ }
49
+ if (typeof value === "object") {
50
+ const out: Record<string, unknown> = {};
51
+ for (const [key, child] of Object.entries(value)) {
52
+ if (Object.keys(out).length >= MAX_EXTRA_KEYS) break;
53
+ const safeKey = boundedText(key, 100);
54
+ out[safeKey] = SECRET_KEY_RE.test(safeKey)
55
+ ? "<redacted>"
56
+ : safeValue(child, depth - 1);
57
+ }
58
+ return out;
59
+ }
60
+ return boundedText(value, MAX_EXTRA_VALUE_LENGTH);
61
+ }
62
+
63
+ export function safeTags(
64
+ tags: Record<string, string | undefined> | undefined,
65
+ ): Record<string, string> {
66
+ const out: Record<string, string> = {};
67
+ for (const [key, value] of Object.entries(tags ?? {})) {
68
+ if (Object.keys(out).length >= MAX_TAGS) break;
69
+ // Skip, don't stop: callers build tag objects with optional entries
70
+ // (`{ ...tags, route, method, userAgent }`), so breaking on the first
71
+ // undefined dropped every tag after it depending on key order.
72
+ if (value == null) continue;
73
+ const safeKey = boundedText(key, 100);
74
+ out[safeKey] = SECRET_KEY_RE.test(safeKey)
75
+ ? "<redacted>"
76
+ : boundedText(value, 200);
77
+ }
78
+ return out;
79
+ }
80
+
81
+ export interface ExceptionParts {
82
+ type: string;
83
+ message: string;
84
+ stack?: string;
85
+ }
86
+
87
+ /** Split an unknown thrown value into bounded, redacted type/message/stack. */
88
+ export function exceptionParts(error: unknown): ExceptionParts {
89
+ if (error instanceof Error) {
90
+ return {
91
+ type: boundedText(error.name || "Error", 200),
92
+ message: boundedText(
93
+ error.message || error.name || "Error",
94
+ MAX_MESSAGE_LENGTH,
95
+ ),
96
+ ...(error.stack
97
+ ? { stack: boundedText(error.stack, MAX_STACK_LENGTH) }
98
+ : {}),
99
+ };
100
+ }
101
+ return {
102
+ type: "Error",
103
+ message: boundedText(error, MAX_MESSAGE_LENGTH),
104
+ };
105
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-29
4
+ ---
5
+
6
+ Meeting notes no longer transcribe the other side twice when you are on speakers instead of headphones
@@ -8,11 +8,14 @@ import {
8
8
  appendFinalTranscript,
9
9
  onFinalTranscript,
10
10
  restartTranscriptionEngine,
11
- speakerFor,
12
11
  startTranscriptionEngine,
13
12
  stopTranscriptionEngine,
13
+ transcriptFullText,
14
+ transcriptLineFromSegment,
15
+ transcriptSegments,
14
16
  type SourcedTranscriptSegment,
15
17
  type TranscriptionEngine,
18
+ type TranscriptLine,
16
19
  } from "../lib/transcription-engine";
17
20
  import { normalizeServerUrl } from "../lib/url";
18
21
 
@@ -31,8 +34,7 @@ export interface MeetingTranscriptionPayload {
31
34
  interface MeetingTranscriptionSession {
32
35
  meetingId: string;
33
36
  recordingId: string;
34
- lines: string[];
35
- segments: SourcedTranscriptSegment[];
37
+ lines: TranscriptLine[];
36
38
  unlisten: Array<() => void>;
37
39
  flushTimer: ReturnType<typeof setTimeout> | null;
38
40
  stopping: boolean;
@@ -52,6 +54,22 @@ interface MeetingTranscriptionSession {
52
54
  dirtySeq: number;
53
55
  }
54
56
 
57
+ /** What the pill overlay needs to render a line: text, side, and timestamp.
58
+ * The verbatim segments stay behind in the session. */
59
+ interface PillTranscriptLine {
60
+ text: string;
61
+ source: "mic" | "system";
62
+ startMs?: number;
63
+ }
64
+
65
+ function pillTranscriptLines(lines: TranscriptLine[]): PillTranscriptLine[] {
66
+ return lines.map((line) => ({
67
+ text: line.text,
68
+ source: line.source,
69
+ startMs: line.startMs ?? undefined,
70
+ }));
71
+ }
72
+
55
73
  type CallClipsAction = <T>(
56
74
  name: string,
57
75
  body: Record<string, unknown>,
@@ -79,11 +97,7 @@ export function useMeetingTranscription({
79
97
  const pendingPillInitRef = useRef<{
80
98
  meetingId: string;
81
99
  initialNotes: string;
82
- preloadedLines?: Array<{
83
- text: string;
84
- source: "mic" | "system";
85
- startMs?: number;
86
- }>;
100
+ preloadedLines?: PillTranscriptLine[];
87
101
  } | null>(null);
88
102
 
89
103
  const normalizedServerUrl = useMemo(
@@ -119,8 +133,8 @@ export function useMeetingTranscription({
119
133
  const run = (async () => {
120
134
  await callClipsAction("save-browser-transcript", {
121
135
  recordingId: session.recordingId,
122
- fullText: session.lines.join("\n\n"),
123
- segments: session.segments,
136
+ fullText: transcriptFullText(session.lines),
137
+ segments: transcriptSegments(session.lines),
124
138
  source: session.engine,
125
139
  overwriteReady: true,
126
140
  });
@@ -314,7 +328,6 @@ export function useMeetingTranscription({
314
328
  meetingId: resolvedMeetingId,
315
329
  recordingId,
316
330
  lines: [],
317
- segments: [],
318
331
  unlisten: [],
319
332
  flushTimer: null,
320
333
  stopping: false,
@@ -363,13 +376,7 @@ export function useMeetingTranscription({
363
376
  })),
364
377
  }
365
378
  : event;
366
- if (
367
- appendFinalTranscript(
368
- timelineEvent,
369
- session.lines,
370
- session.segments,
371
- )
372
- ) {
379
+ if (appendFinalTranscript(timelineEvent, session.lines)) {
373
380
  scheduleFlush();
374
381
  }
375
382
  }),
@@ -539,11 +546,7 @@ export function useMeetingTranscription({
539
546
  pendingPillInitRef.current = {
540
547
  meetingId: resolvedMeetingId,
541
548
  initialNotes,
542
- preloadedLines: session.segments.map((segment) => ({
543
- text: segment.text,
544
- source: segment.source,
545
- startMs: segment.startMs,
546
- })),
549
+ preloadedLines: pillTranscriptLines(session.lines),
547
550
  };
548
551
  emit("clips:meeting-notes-init", {
549
552
  meetingId: resolvedMeetingId,
@@ -561,25 +564,16 @@ export function useMeetingTranscription({
561
564
  source?: "mic" | "system";
562
565
  }>;
563
566
  if (segs.length > 0) {
564
- const preloadedLineStrings = segs.map(
565
- (s) => `${speakerFor(s.source)}: ${s.text}`,
567
+ const storedLines = segs.map((s) =>
568
+ transcriptLineFromSegment({
569
+ startMs: s.startMs ?? 0,
570
+ endMs: s.endMs ?? 0,
571
+ text: s.text,
572
+ source: s.source ?? "mic",
573
+ }),
566
574
  );
567
- const preloadedSegments = segs.map((s) => ({
568
- startMs: s.startMs ?? 0,
569
- endMs: s.endMs ?? 0,
570
- text: s.text,
571
- source: s.source ?? ("mic" as const),
572
- }));
573
- session.lines = [...preloadedLineStrings, ...session.lines];
574
- session.segments = [
575
- ...preloadedSegments,
576
- ...session.segments,
577
- ];
578
- const preloadedLines = session.segments.map((s) => ({
579
- text: s.text,
580
- source: s.source,
581
- startMs: s.startMs,
582
- }));
575
+ session.lines = [...storedLines, ...session.lines];
576
+ const preloadedLines = pillTranscriptLines(session.lines);
583
577
  // Store in ref so clips:pill-ready can re-emit if the
584
578
  // pill window mounts after this fetch resolves.
585
579
  if (
@@ -648,15 +642,10 @@ export function useMeetingTranscription({
648
642
  .then((history) => {
649
643
  if (sessionRef.current !== session) return;
650
644
  const historyLines = history.segments.map(
651
- (segment) => `${speakerFor(segment.source)}: ${segment.text}`,
645
+ transcriptLineFromSegment,
652
646
  );
653
647
  session.lines = [...historyLines, ...session.lines];
654
- session.segments = [...history.segments, ...session.segments];
655
- const preloadedLines = session.segments.map((segment) => ({
656
- text: segment.text,
657
- source: segment.source,
658
- startMs: segment.startMs,
659
- }));
648
+ const preloadedLines = pillTranscriptLines(session.lines);
660
649
  if (pendingPillInitRef.current?.meetingId === resolvedMeetingId) {
661
650
  pendingPillInitRef.current = {
662
651
  ...pendingPillInitRef.current,
@@ -19,7 +19,10 @@ import {
19
19
  startTranscriptionEngine,
20
20
  stopTranscriptionEngine,
21
21
  TranscriptionEngine,
22
+ transcriptFullText,
23
+ transcriptSegments,
22
24
  type SourcedTranscriptSegment,
25
+ type TranscriptLine,
23
26
  } from "./transcription-engine";
24
27
 
25
28
  /** Grace period after stop for whisper to emit any flushed trailing finals. */
@@ -307,8 +310,7 @@ export async function startTranscriptionCapture(
307
310
  voiceProcessing?: boolean;
308
311
  },
309
312
  ): Promise<TranscriptionCapture | null> {
310
- const lines: string[] = [];
311
- const segments: SourcedTranscriptSegment[] = [];
313
+ const lines: TranscriptLine[] = [];
312
314
  let disposed = false;
313
315
  let paused = false;
314
316
  let desiredPaused = false;
@@ -331,8 +333,8 @@ export async function startTranscriptionCapture(
331
333
  };
332
334
 
333
335
  const captured = (): CapturedTranscript => ({
334
- text: lines.join("\n\n").trim(),
335
- segments,
336
+ text: transcriptFullText(lines),
337
+ segments: transcriptSegments(lines),
336
338
  });
337
339
 
338
340
  let engine: TranscriptionEngine;
@@ -340,7 +342,7 @@ export async function startTranscriptionCapture(
340
342
  unlistens.push(
341
343
  await onFinalTranscript((event) => {
342
344
  if (disposed) return;
343
- appendFinalTranscript(event, lines, segments);
345
+ appendFinalTranscript(event, lines);
344
346
  }),
345
347
  );
346
348