- greeting waits only on instant local context (time/memory/edition);
recall, calendar, alerts inject into chat context after the greeting
- turn_detection=EnglishModel() (semantic EOU), min_endpointing 0.35s,
VAD silence floor 0.7s→0.4s — the detector guards mid-thought pauses
- preemptive_generation=True: LLM starts on partial transcript
- METRIC log lines per turn: llm ttft, tts ttfb, eou delay
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No suggestions, no unprompted calendar/alerts, no questions the opener
can't answer itself. Joel sets direction.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Slang density down (no rhyming slang, one light Aussie-ism max), attitude
unchanged; two new registers explicitly license taking the piss.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- _gather_context: 5 sequential shell-outs (10s timeouts each) ran to 11.5s
observed; now parallel with 4s per-probe ceilings — 4.0s standalone — and
kicked off at ctx.connect() so it overlaps SIP setup instead of blocking
the greeting after the caller is live
- prewarm_fnc loads Silero VAD at process spawn, not on the caller's clock
- transcript save was dead since the livekit-agents ChatContext change
('ChatContext has no len()') — no voice/call.completed events were firing;
new _history_lines uses history.items + text_content
- canary create-and-delete rooms no longer dump RuntimeError tracebacks
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Random register seed (12 flavors) + time of day; the model invents a fresh
opener every call instead of parroting the config greeting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the wayfinder ticket 'Build Fail-Loud Canaries and Boot Verification'.
Proven live: bootout of the voice agent paged Joel in 4m05s (Telnyx call+SMS
+ gateway urgent); synthetic self-call answered in 3s; verify-voice.sh 21/21.
- voice-worker-canary (cron */5): bare-room dispatch probe, cause-tagged,
30-min cooldown, recovery notice
- voice-synthetic-call (daily 12:05 PT): outbound trunk dials own DID;
main.py speaks a canary token to its own number, never guest mode
- voice-telnyx-balance (6h): warn <$25, page <$10 (how the old number died)
- voice-missed-call + telnyx webhook provider (Ed25519): FQDN connection now
posts call events to hooks.joelclaw.com/webhooks/telnyx — verified
delivering; dark until telnyx_public_key lands in agent-secrets
- verify-voice.sh + com.joel.verify-voice LaunchAgent (boot + 30 min):
plist/GUI-session/secrets/dispatch-probe/Typesense/balance/allowlist
- serve.ts leases TELNYX_PUBLIC_KEY; run.sh leases TELNYX_PHONE_NUMBER
Built by a pi worker in a herdr pane; spec receipt in .brain/data/tasks/.
Surfaces new ticket: add-graceful-drain-voice-worker.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Continuation surface for fresh sessions working the voice workroom map:
live state, invariants, frontier order, gotchas.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- slack_search / slack_thread / slack_reply tools on Joel's user token
(search + read, reply posts AS Joel — prompt requires reading the exact
message back and getting a yes before sending)
- call-joel.sh: any fleet agent rings Joel via the LiveKit outbound trunk
with a call_reason participant attribute; ShitRat opens with the reason.
Symlinked to ~/.local/bin/call-joel; dark-wizard skill documents ssh use
from other machines
- run.sh leases slack_user_token
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kids' verdict: ShitRat talks too much. One sentence default, two when
needed, three rare; answer first, no preamble, no restating, top-1 for
lists. Guest mode tightened the same way. Speed handled via local config
(tts.speed 1.15 — ElevenLabs caps at 1.2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Background noise was triggering ghost turns and cutting the agent off
mid-reply. Krisp BVCTelephony strips background voices before VAD/STT on
every session (guest included); interrupting now requires 0.8s of sustained
speech and 2 transcribed words, and false barge-ins resume the reply after
2s. All knobs overridable via the local config's audio: section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Map + ten tickets for the voice workroom effort: phone-in ShitRat on flagg
via LiveKit Cloud, wired to the wiki, wayfinder, and pi-discord-threads
sessions — resumable as voice or text, recallable, discoverable.
Restore Dial Tone via LiveKit Cloud is already resolved (closed with
receipts): live both directions on +1 360 389 4321. Recorded traps: lapsed
Telnyx balance silently released the old DID and killed outbound too;
LiveKit SIP URIs must come from the dashboard (wildcard DNS makes guessed
hostnames resolve then 404); worker judged callers before the phone leg
joined.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 0/1 of the voice workroom map (dial tone + tools), plus the rename:
- LiveKit Cloud media plane: LIVEKIT_URL leased from agent-secrets instead of
hardcoded local k8s URL; run.sh hard-fails when a lease returns an error
envelope (a failed lease was being exported as a Bearer credential)
- Judge callers by SIP participant attribute after waiting for the phone leg
to join; room-name parse is fallback only (outbound rooms embed no caller,
so the agent was rejecting its own calls before the callee answered)
- Panda -> ShitRat: persona from shitrat-cli defaults (sharp, loyal, skeptical,
receipt-first, sewer energy), Australian idioms kept
- open_loops + loop_detail tools over the joelclaw-wiki daily edition, loops
in pre-call context, explicit grilling posture in the prompt
- Guest mode for unrecognized callers: bare Agent with zero tools and zero
private context, 10-minute cap, transcript quarantined to voice/guests and
never sent to the memory pipeline (prompt-injection containment); callers
with no caller ID still get the fail-closed hangup
- recall tool docstring: Qdrant -> Typesense (matches actual plumbing)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three chunks looped deterministically (6-14x of backchannel phrases on
quiet mic stretches) even with the anti-hallucination flags, exhausting
retries and failing the run. At whisper segment granularity a run of
identical segments is decoder looping, not speech — the honest
transcript keeps one occurrence.
screenWithCollapse() collapses runs of normalized-identical consecutive
segments, then screens what remains. A decode that was mostly loop
padding still fails via maxCollapsedFraction (0.5): the 2,768x
"thank you" whole-file collapses to almost nothing, which is a failed
decode, not a quiet meeting.
Aggregation stitches the collapsed segments (also across chunk
boundaries); buildPlan rewrites adopted whole-track files with the
collapsed form since asrDone tracks bypass aggregation; actors and
claim checks screen-with-collapse for accept/reject only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The dev-mode Inngest server delivers actor.finished events to
waitForEvent calls whose `if` match expression should exclude them.
Trusting the payload made waiting chunk runs throw on OTHER chunks'
repetitive_output failures, reap their own healthy actors, and burn
retries on someone else's error (observed live: 6 healthy actors
cancelled_by_signal clustered around 2 legitimate failures).
Only an exact spawned-actorId match is trusted now; foreign events fall
through to the status-file check like a timeout. Iteration budgets
widened (24 ASR / 64 diarize) since foreign events consume iterations.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Real recovery data exposed two decoder-level failures the pipeline has
to absorb:
- mlx_whisper emits bare NaN tokens ("avg_logprob": NaN) on low-speech
audio; strict JSON.parse rejects the file, so a NaN-bearing chunk
failed validation forever across retries. parseAsrJson() nulls bare
NaN/Infinity value tokens (never inside strings) and now backs every
ASR claim-check reader.
- 16 of 22 recovery chunks hit hallucination loops ("yeah" x144,
"i dont know" x275) even at 600s chunk length. ASR actors now pass
--condition-on-previous-text False and
--hallucination-silence-threshold 2.
transcript-rig (separate checkout) got the same decoder flags, MPS
diarization with CPU fallback (~5-10x faster on Apple Silicon), and a
doctor check for torch MPS availability.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Inngest orchestrates; MLX Whisper and pyannote run as detached actor
processes (own process group, heartbeat status files, actor.finished
events) so no step ever holds a request open across inference.
- ASR chunked at 600s with per-chunk repetition screening and
time-offset aggregation into the rig's whole-track claim check;
adopts existing chunk layouts and valid whole-track outputs, so the
in-flight manual recovery resumes with near-zero recompute
- diarization stays whole-file but detached, with a waitForEvent +
status-file watchdog loop (event loss tolerated)
- retries reap the previous attempt's process group first (ps-verified
identity, PID-reuse safe); supervisor step ids parameterized by
attempt so Inngest memoization cannot replay a stale attempt
- cancellation fans out to transcription-cleanup-v1: SIGTERM -> 10s ->
SIGKILL on live actor groups plus a cancelled flag the spawner
refuses to spawn past; actors treat un-initiated child signal deaths
(130/137/143) as cancelled to survive Bun's signal-dispatch race
- transcript-rig (separate checkout) gained --no-inference guards so
fast stages can never relaunch multi-hour inference in-request
- Pi monitor step-id contract (NN- prefixes) preserved
Incident receipts and design decisions in
.brain/projects/transcription-pipeline-v2-2026-07-11.svx.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- summarizeRollupCost: OpenRouter-benchmark cost for rows without provider
cost, wired into joelclaw usage and the daily token report
- clickhouse-usage-query: source option (router|agents|all) so agent_usage.turn
tailer events roll up alongside model_router.result; coverage % is router-only
- claude tailer parser: dedupe by message.id (Claude Code repeats usage per
content-block line; counts inflated ~2x) — inflated rows purged, offsets reset
- inference: non-zero pi exit with empty output now fails for json callers too
(was recorded as circuit success); --mode json event-stream soup no longer
leaks into result.text; caller metadata can't clobber usage fields
- daily report: rollup widened to 5000 groups; agent-sessions line added
- k8s worker manifest published with SLOG_SYSTEM_ID (pod blocked by
pre-existing flannel failure, see brain note follow-ups)
- brain notes + skills updated: langfuse decommission receipts, usage receipts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>