Skip to content

Commit dee7610

Browse files
chore(security): scrub production Call SID and competitor lineage from public files (#129)
- CHANGELOG.md: remove a live Twilio Call SID and two private acceptance-repo path crumbs from the 0.6.1 release notes. A Call SID is production-call PII that security.md forbids in the public repo. - stream-handler.ts: rewrite a pacing comment to drop a competitor lineage reference (TwilioFrameSerializer / call-gpt sample) and two non-English user-complaint quotes, restating the rationale in neutral English. Comment-only; no behaviour change. The Python stream_handler twin was already clean. These strings remain in git history (working-tree scrub only) — the Call SID is an opaque, non-credential resource ID, accepted as low-risk per maintainer review. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8f14e62 commit dee7610

2 files changed

Lines changed: 7 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1214,9 +1214,9 @@ A new optional field `LatencyBreakdown.user_speech_duration_ms` (`userSpeechDura
12141214

12151215
- **Cartesia STT + AssemblyAI STT warmup: API key no longer leaks into logs on handshake failure** (Python + TypeScript parity, security). Both providers authenticate via a query-string parameter on the WS upgrade URL (Cartesia: `?api_key=...`, AssemblyAI optional: `?token=...`). When the WS handshake failed (e.g. 401 from a rotated key), `aiohttp.WSServerHandshakeError.__str__` (Py) and the `ws` library `Error.message` (TS) typically include the full request URL — and `logger.debug("warmup failed: %s", exc)` therefore wrote the API key straight into application logs. The fix catches the handshake-error class specifically before the generic `Exception` handler and logs only the HTTP status code (or, for non-handshake errors, just the exception class name) — never the full message or URL. New regression tests in both SDKs install a custom logger / `caplog` capture, force a 401 handshake error during warmup, and assert (a) the API key never appears in any captured log message, (b) the URL with `?api_key=` / `?token=` never appears either, and (c) the status code is still surfaced so operators see why the warmup failed. Files: `libraries/python/getpatter/providers/{cartesia_stt,assemblyai_stt}.py`, `libraries/typescript/src/providers/{cartesia-stt,assemblyai-stt}.ts`.
12161216

1217-
- **Dashboard hydrate: hydrated calls no longer lose `cost` and `latency`** (Python + TypeScript parity, fully backward-compatible). `CallLogger.log_call_end` writes `cost`, `latency`, `duration_ms`, and `telephony_provider` as **top-level keys** of `metadata.json`, but `MetricsStore.hydrate` (`libraries/python/getpatter/dashboard/store.py:535`, `libraries/typescript/src/dashboard/store.ts:421-424`) read them only from `meta.metrics.cost` / `meta.metrics.latency`. Result: every call rebuilt from disk landed in the store with `metrics=null`, so the local dashboard rendered `$0.00` and `—` for cost/latency on all hydrated rows; only the in-flight call (which never goes through hydrate) showed real numbers. Caught during 0.6.0 acceptance against `releases/0.6.0/typescript/matrix/outbound-cartesia-cerebras-elevenlabs.ts` — 48 of 49 calls in the dashboard had blank P95/cost columns. Fix promotes the top-level fields into a synthesized `metrics` dict (`metrics_from_top_level` / `metricsFromTopLevel`) when `meta.metrics` is missing, mapping `latency.p95_ms` → `metrics.latency_avg.total_ms` so the existing UI fields populate. Explicit `meta.metrics` (legacy/future shape) is preserved untouched. New regressions: `tests/unit/test_metrics_store_hydrate.py::test_hydrate_lifts_top_level_cost_and_latency_into_metrics` + `test_hydrate_preserves_explicit_metrics_when_present` (Py); two `MetricsStore.hydrate` cases in `tests/dashboard-store.test.ts` (TS). Files: `libraries/python/getpatter/dashboard/store.py`, `libraries/typescript/src/dashboard/store.ts`.
1217+
- **Dashboard hydrate: hydrated calls no longer lose `cost` and `latency`** (Python + TypeScript parity, fully backward-compatible). `CallLogger.log_call_end` writes `cost`, `latency`, `duration_ms`, and `telephony_provider` as **top-level keys** of `metadata.json`, but `MetricsStore.hydrate` (`libraries/python/getpatter/dashboard/store.py:535`, `libraries/typescript/src/dashboard/store.ts:421-424`) read them only from `meta.metrics.cost` / `meta.metrics.latency`. Result: every call rebuilt from disk landed in the store with `metrics=null`, so the local dashboard rendered `$0.00` and `—` for cost/latency on all hydrated rows; only the in-flight call (which never goes through hydrate) showed real numbers. Caught during 0.6.0 acceptance testing — 48 of 49 calls in the dashboard had blank P95/cost columns. Fix promotes the top-level fields into a synthesized `metrics` dict (`metrics_from_top_level` / `metricsFromTopLevel`) when `meta.metrics` is missing, mapping `latency.p95_ms` → `metrics.latency_avg.total_ms` so the existing UI fields populate. Explicit `meta.metrics` (legacy/future shape) is preserved untouched. New regressions: `tests/unit/test_metrics_store_hydrate.py::test_hydrate_lifts_top_level_cost_and_latency_into_metrics` + `test_hydrate_preserves_explicit_metrics_when_present` (Py); two `MetricsStore.hydrate` cases in `tests/dashboard-store.test.ts` (TS). Files: `libraries/python/getpatter/dashboard/store.py`, `libraries/typescript/src/dashboard/store.ts`.
12181218

1219-
- **Pipeline early barge-in: VAD self-cancellation before TTS first byte arrived** (Python + TypeScript parity, behavioural change for pipeline mode). Cloud TTS providers (ElevenLabs, Cartesia, …) take 200–700 ms to emit the first audio byte. The barge-in anti-flicker gate was anchored on `_speaking_started_at` / `speakingStartedAt` (set inside `_begin_speaking` / `beginSpeaking`), so a 250 ms gate without AEC expired BEFORE TTS produced any audio. VAD then picked up background noise, room ambience, or a "hello?" from the operator and triggered `[VAD] speech_start during TTS → BARGE-IN` → `cancelSpeaking` → `isSpeaking=false` → the `for await (chunk of tts.synthesizeStream(...))` loop exited at `if (!this.isSpeaking) break`, emitting **zero bytes**. From the SDK's perspective the agent "spoke" the first message; from the caller's perspective the line went silent until the next turn. Reproduced on `outbound-cartesia-cerebras-elevenlabs.ts` (call CAfca9c23b22144d4b1bb8ee737dd24016, 47 s — first message never reached the wire). Fix introduces `_first_audio_sent_at` / `firstAudioSentAt`, set in a new `_mark_first_audio_sent` / `markFirstAudioSent` helper invoked AFTER `audio_sender.send_audio` / `bridge.sendAudio` succeeds at all four pipeline emit sites (firstMessage, streaming response, regular response, WebSocket remote). `_can_barge_in` / `canBargeIn` now refuses to open the gate while `_first_audio_sent_at` is null — VAD speech_start before the first wire-time byte is suppressed regardless of how much wall-clock has elapsed since `_begin_speaking`. The 250 ms / 1000 ms gate values are unchanged — only the anchor moves. New regressions: `tests/unit/test_stream_handler_unit.py::test_barge_in_suppressed_before_first_audio_emitted` (Py); `canBargeIn() false before the first TTS chunk has hit the wire` in `tests/unit/stream-handler.test.ts` (TS). Existing `_handle_barge_in` / `handleBargeIn` tests updated to set both timestamps to reflect the new contract. Files: `libraries/python/getpatter/stream_handler.py`, `libraries/typescript/src/stream-handler.ts`.
1219+
- **Pipeline early barge-in: VAD self-cancellation before TTS first byte arrived** (Python + TypeScript parity, behavioural change for pipeline mode). Cloud TTS providers (ElevenLabs, Cartesia, …) take 200–700 ms to emit the first audio byte. The barge-in anti-flicker gate was anchored on `_speaking_started_at` / `speakingStartedAt` (set inside `_begin_speaking` / `beginSpeaking`), so a 250 ms gate without AEC expired BEFORE TTS produced any audio. VAD then picked up background noise, room ambience, or a "hello?" from the operator and triggered `[VAD] speech_start during TTS → BARGE-IN` → `cancelSpeaking` → `isSpeaking=false` → the `for await (chunk of tts.synthesizeStream(...))` loop exited at `if (!this.isSpeaking) break`, emitting **zero bytes**. From the SDK's perspective the agent "spoke" the first message; from the caller's perspective the line went silent until the next turn. Reproduced on a pipeline outbound call (~47 s — first message never reached the wire). Fix introduces `_first_audio_sent_at` / `firstAudioSentAt`, set in a new `_mark_first_audio_sent` / `markFirstAudioSent` helper invoked AFTER `audio_sender.send_audio` / `bridge.sendAudio` succeeds at all four pipeline emit sites (firstMessage, streaming response, regular response, WebSocket remote). `_can_barge_in` / `canBargeIn` now refuses to open the gate while `_first_audio_sent_at` is null — VAD speech_start before the first wire-time byte is suppressed regardless of how much wall-clock has elapsed since `_begin_speaking`. The 250 ms / 1000 ms gate values are unchanged — only the anchor moves. New regressions: `tests/unit/test_stream_handler_unit.py::test_barge_in_suppressed_before_first_audio_emitted` (Py); `canBargeIn() false before the first TTS chunk has hit the wire` in `tests/unit/stream-handler.test.ts` (TS). Existing `_handle_barge_in` / `handleBargeIn` tests updated to set both timestamps to reflect the new contract. Files: `libraries/python/getpatter/stream_handler.py`, `libraries/typescript/src/stream-handler.ts`.
12201220

12211221
## 0.6.0 (2026-05-08)
12221222

libraries/typescript/src/stream-handler.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,13 +1635,12 @@ export class StreamHandler {
16351635
// we've made to "help" Twilio (per-chunk sleep, mark back-pressure,
16361636
// initial-fill burst, absolute-clock scheduling) introduced its own
16371637
// jitter source: setTimeout drift, mark-echo RTT > playout window,
1638-
// or burst-then-stall patterns. The result the user heard as
1639-
// "scatti" / "differenza di frequenza" was the side effect of our
1640-
// pacing fighting the carrier clock, not the carrier itself.
1638+
// or burst-then-stall patterns — audible as choppy or warbled
1639+
// playout caused by our pacing fighting the carrier clock, not the
1640+
// carrier itself.
16411641
//
1642-
// Mirror the pattern used by Twilio's own call-gpt reference sample
1643-
// and Pipecat's TwilioFrameSerializer: dump every 20 ms slice into
1644-
// the WebSocket back-to-back, return, let Twilio drain. For prewarm
1642+
// The stable approach: dump every 20 ms slice into the WebSocket
1643+
// back-to-back, return, and let Twilio drain. For prewarm
16451644
// this is ~250 sendAudio calls in <50 ms for a 5 s greeting; the
16461645
// WebSocket buffer absorbs them and the carrier plays at exactly
16471646
// 50 frames/s with no further intervention from us. Barge-in still

0 commit comments

Comments
 (0)