Skip to content

Inline demo video in README - #3

Closed
sonichi wants to merge 2 commits into
mainfrom
inline-demo-video
Closed

sonichi wants to merge 2 commits into
mainfrom
inline-demo-video

Conversation

@sonichi

@sonichi sonichi commented Mar 28, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replace YouTube thumbnail with GitHub-hosted MP4 for inline playback
  • Video plays directly in the README without leaving the page

Test plan

  • Video renders and plays on GitHub README page

🤖 Generated with Claude Code

sonichi and others added 2 commits March 28, 2026 09:57
Replace YouTube thumbnail with GitHub-hosted MP4 for inline playback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sonichi sonichi closed this Mar 28, 2026
@sonichi
sonichi deleted the inline-demo-video branch March 28, 2026 17:00
sonichi added a commit that referenced this pull request Apr 10, 2026
Scripts the post-merge verification after the 3-PR Gemini 3.1 compat
stack lands:
  - bodhi fork #1 (sendClientContent → sendRealtimeInput text path)
  - bodhi fork #2 (Susan: sendAudio media → audio)
  - bodhi fork #3 (me: sendFile mimeType branching)
  - sutando #259 (Chi: deduplicate tool declarations + SDK bump)

Checks the installed bodhi dist for each wire-format fix using
awk-scoped extraction (simpler grep would false-positive on the
OpenAI realtime transport's sendAudio which also uses `audio:` as a
flat field, so the 3.1 gemini-specific check has to be scoped to the
Gemini transport's function body).

Checks sutando src for the phone conversation-server's tool dedup
comment as a PR #259 marker.

Checks .env for the 2.5 pin (exits green on 2.5, warns on 3.1 — so
the script is safe to run repeatedly before, during, and after the
rollout).

Exits non-zero with "NOT READY for 3.1 rollout" if any wire-format
fix is missing. On success, prints the manual next-steps checklist
for unpinning .env, restarting voice-agent, and running the real
3.1 session test.

Usage:
  bash src/verify-gemini-31.sh              # check current state
  bash src/verify-gemini-31.sh --install    # npm install + check

Tested on the current state: correctly reports PR #1 applied,
PR #2 + #3 + #259 not yet. Will flip to "ready" once those merge
and an `npm install` pulls the updated bodhi SHA.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
sonichi added a commit that referenced this pull request Apr 10, 2026
Isolates the second voice-agent-specific 3.1 compat issue discovered
while landing the Gemini 3.1 stack today. Chi's #259 (dedup) +
Susan's bodhi #2 (sendAudio) + my bodhi #3 (sendFile) all merged,
npm installed, and 2.5 continued to work fine — but flipping .env to
gemini-3.1-flash-live-preview still hit 1011 "exceeded your current
quota" within 200ms of WebSocket setup. Misleading error text; the
actual cause turned out to be the googleSearch + native-audio combo.

Experiment: flipped googleSearch: true → false on an investigation
branch, kept everything else identical (.env to 3.1, same dist,
same tool list). Result: setup complete in 315ms, no 1011. Flipped
back to googleSearch: true + 3.1 and the 1011 returned. Reproducible.

So: Gemini 3.1 native audio rejects the googleSearch grounding
tool entry. 2.5 silently accepted it. The rejection manifests as
the same misleading "quota" close code that caught us on #259's
duplicate-declaration bug earlier.

Fix: env-var gate. VOICE_GOOGLE_SEARCH defaults to 'true' to
preserve existing 2.5 behavior. Users unpinning VOICE_NATIVE_AUDIO_MODEL
to 3.1 set VOICE_GOOGLE_SEARCH=false in .env at the same time.
Comment at the declaration explains the constraint and points at
today's investigation.

Voice-agent LOSES google-search grounding on 3.1. Google search was
used for "quick factual lookups" per the system instructions; 3.1
users will need to route those queries through the work tool
(sutando-core) instead. Acceptable trade-off.

Test plan:
- ✓ tsc clean
- ✓ Default (VOICE_GOOGLE_SEARCH unset, 2.5 pinned): setup complete,
  googleSearch: true as before
- ✓ Investigation: VOICE_GOOGLE_SEARCH=false + 3.1 pinned → Gemini
  setup complete in 315ms, no 1011
- Live: user needs to test the full 3.1 session (greeting, tool
  call, voice goodbye, reconnect) before we can call 3.1 "ready"

Follow-up: consider auto-inferring googleSearch=false when
VOICE_NATIVE_AUDIO_MODEL contains "3.1" — would remove the need
for users to set two env vars together. Not doing it here because
the explicit config is more discoverable and easier to revert.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
sonichi added a commit that referenced this pull request Apr 10, 2026
Scripts the post-merge verification after the 3-PR Gemini 3.1 compat
stack lands:
  - bodhi fork #1 (sendClientContent → sendRealtimeInput text path)
  - bodhi fork #2 (Susan: sendAudio media → audio)
  - bodhi fork #3 (me: sendFile mimeType branching)
  - sutando #259 (Chi: deduplicate tool declarations + SDK bump)

Checks the installed bodhi dist for each wire-format fix using
awk-scoped extraction (simpler grep would false-positive on the
OpenAI realtime transport's sendAudio which also uses `audio:` as a
flat field, so the 3.1 gemini-specific check has to be scoped to the
Gemini transport's function body).

Checks sutando src for the phone conversation-server's tool dedup
comment as a PR #259 marker.

Checks .env for the 2.5 pin (exits green on 2.5, warns on 3.1 — so
the script is safe to run repeatedly before, during, and after the
rollout).

Exits non-zero with "NOT READY for 3.1 rollout" if any wire-format
fix is missing. On success, prints the manual next-steps checklist
for unpinning .env, restarting voice-agent, and running the real
3.1 session test.

Usage:
  bash src/verify-gemini-31.sh              # check current state
  bash src/verify-gemini-31.sh --install    # npm install + check

Tested on the current state: correctly reports PR #1 applied,
PR #2 + #3 + #259 not yet. Will flip to "ready" once those merge
and an `npm install` pulls the updated bodhi SHA.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
sonichi added a commit that referenced this pull request Apr 10, 2026
…mpat) (#262)

* voice-agent: gate googleSearch on VOICE_GOOGLE_SEARCH env var

Isolates the second voice-agent-specific 3.1 compat issue discovered
while landing the Gemini 3.1 stack today. Chi's #259 (dedup) +
Susan's bodhi #2 (sendAudio) + my bodhi #3 (sendFile) all merged,
npm installed, and 2.5 continued to work fine — but flipping .env to
gemini-3.1-flash-live-preview still hit 1011 "exceeded your current
quota" within 200ms of WebSocket setup. Misleading error text; the
actual cause turned out to be the googleSearch + native-audio combo.

Experiment: flipped googleSearch: true → false on an investigation
branch, kept everything else identical (.env to 3.1, same dist,
same tool list). Result: setup complete in 315ms, no 1011. Flipped
back to googleSearch: true + 3.1 and the 1011 returned. Reproducible.

So: Gemini 3.1 native audio rejects the googleSearch grounding
tool entry. 2.5 silently accepted it. The rejection manifests as
the same misleading "quota" close code that caught us on #259's
duplicate-declaration bug earlier.

Fix: env-var gate. VOICE_GOOGLE_SEARCH defaults to 'true' to
preserve existing 2.5 behavior. Users unpinning VOICE_NATIVE_AUDIO_MODEL
to 3.1 set VOICE_GOOGLE_SEARCH=false in .env at the same time.
Comment at the declaration explains the constraint and points at
today's investigation.

Voice-agent LOSES google-search grounding on 3.1. Google search was
used for "quick factual lookups" per the system instructions; 3.1
users will need to route those queries through the work tool
(sutando-core) instead. Acceptable trade-off.

Test plan:
- ✓ tsc clean
- ✓ Default (VOICE_GOOGLE_SEARCH unset, 2.5 pinned): setup complete,
  googleSearch: true as before
- ✓ Investigation: VOICE_GOOGLE_SEARCH=false + 3.1 pinned → Gemini
  setup complete in 315ms, no 1011
- Live: user needs to test the full 3.1 session (greeting, tool
  call, voice goodbye, reconnect) before we can call 3.1 "ready"

Follow-up: consider auto-inferring googleSearch=false when
VOICE_NATIVE_AUDIO_MODEL contains "3.1" — would remove the need
for users to set two env vars together. Not doing it here because
the explicit config is more discoverable and easier to revert.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* sync package-lock.json to @google/genai 1.49 from #259

PR #262 branched before #259 merged, so the branch's package-lock
still had @google/genai@1.48 while package.json had 1.49 (inherited
from the rebase pull). CI failed with:

  npm error Invalid: lock file's @google/genai@1.48.0 does not
  satisfy @google/genai@1.49.0

Regenerated lockfile via `npm install` (no args). No other changes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
sonichi added a commit that referenced this pull request Apr 10, 2026
…261)

* Add src/verify-gemini-31.sh — pre-rollout gate for Gemini 3.1 stack

Scripts the post-merge verification after the 3-PR Gemini 3.1 compat
stack lands:
  - bodhi fork #1 (sendClientContent → sendRealtimeInput text path)
  - bodhi fork #2 (Susan: sendAudio media → audio)
  - bodhi fork #3 (me: sendFile mimeType branching)
  - sutando #259 (Chi: deduplicate tool declarations + SDK bump)

Checks the installed bodhi dist for each wire-format fix using
awk-scoped extraction (simpler grep would false-positive on the
OpenAI realtime transport's sendAudio which also uses `audio:` as a
flat field, so the 3.1 gemini-specific check has to be scoped to the
Gemini transport's function body).

Checks sutando src for the phone conversation-server's tool dedup
comment as a PR #259 marker.

Checks .env for the 2.5 pin (exits green on 2.5, warns on 3.1 — so
the script is safe to run repeatedly before, during, and after the
rollout).

Exits non-zero with "NOT READY for 3.1 rollout" if any wire-format
fix is missing. On success, prints the manual next-steps checklist
for unpinning .env, restarting voice-agent, and running the real
3.1 session test.

Usage:
  bash src/verify-gemini-31.sh              # check current state
  bash src/verify-gemini-31.sh --install    # npm install + check

Tested on the current state: correctly reports PR #1 applied,
PR #2 + #3 + #259 not yet. Will flip to "ready" once those merge
and an `npm install` pulls the updated bodhi SHA.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* verify-gemini-31: check VOICE_GOOGLE_SEARCH pin in .env

Extends the script to enforce the second 3.1 compat requirement
discovered in #262: VOICE_GOOGLE_SEARCH must be 'false' when
VOICE_NATIVE_AUDIO_MODEL is pinned to 3.1. Without it, voice-agent
hits a misleading 1011 close on connect.

Check matrix:
  2.5 + default (googleSearch=true)   → pass
  2.5 + VOICE_GOOGLE_SEARCH=false     → warn (unnecessarily losing search)
  3.1 + VOICE_GOOGLE_SEARCH=false     → pass
  3.1 + default (googleSearch=true)   → fail (1011 on connect)
  3.1 + unset VOICE_GOOGLE_SEARCH     → fail (defaults to true → 1011)

Also updates the 'Manual next steps' tail to tell users to set BOTH
env vars together when flipping to 3.1.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
sonichi added a commit that referenced this pull request Apr 10, 2026
… fixes)

Lockfile was pinned at 26992e3 (bodhi PR #1 only — sendClientContent
text path migration). Bodhi PR #2 (sendAudio media→audio) and PR #3
(sendFile mimeType branching) have been merged on the fork for some
time, but sutando was still running the old dist.

This caused tonight's 1007 regression: client connects, mic audio
frame → `realtime_input.media_chunks is deprecated` → session dies on
every reconnect. The bodhi-dist probe added in this PR catches this
case automatically.

Advance to 33a08c0 (bodhi PR #3 HEAD), which is the current HEAD of
the fork and contains all three compat fixes.
sonichi added a commit that referenced this pull request Apr 10, 2026
* health-check: add bodhi-dist probe to catch stale wire-format dist

The Gemini 3.1 migration's sendAudio/sendFile fixes live in the bodhi
fork. If `package-lock.json` advances to a post-fix commit via `git pull`
but `npm install` is not re-run, the dist on disk stays stale. voice-agent
boots cleanly because sendAudio isn't exercised until a client connects,
and existing probes (voice-agent port, voice-watchers log scan,
voice-transport close-code scan) all pass. The regression is invisible
until a real mic stream triggers 1007 "realtime_input.media_chunks is
deprecated" and the session dies on every reconnect.

The new probe scans node_modules/bodhi-realtime-agent/dist/index.js for
the Gemini transport's sendAudio and sendFile bodies and fails if either
still uses the deprecated `media: { data: ... }` shape. Uses a matched-
brace extractor (_extract_body) to isolate each function body, so it
ignores the OpenAI realtime transport which also defines sendAudio but
with a different shape.

Also fixes a leftover path in check_voice_transport from PR #251's
logs/ refactor: src/voice-agent.log → logs/voice-agent.log.

Repro verified: corrupting the dist to reintroduce `media: { data` in
the Gemini sendAudio body makes the probe fail with the expected detail
message and a clear fix instruction.

* chore: bump bodhi-realtime-agent to 33a08c0 (incl. sendAudio/sendFile fixes)

Lockfile was pinned at 26992e3 (bodhi PR #1 only — sendClientContent
text path migration). Bodhi PR #2 (sendAudio media→audio) and PR #3
(sendFile mimeType branching) have been merged on the fork for some
time, but sutando was still running the old dist.

This caused tonight's 1007 regression: client connects, mic audio
frame → `realtime_input.media_chunks is deprecated` → session dies on
every reconnect. The bodhi-dist probe added in this PR catches this
case automatically.

Advance to 33a08c0 (bodhi PR #3 HEAD), which is the current HEAD of
the fork and contains all three compat fixes.
sonichi pushed a commit that referenced this pull request Apr 11, 2026
* Add voice agent observability: events + tool calls + transcript

Track voice agent sessions in data/voice-metrics.jsonl with the same
format as phone agent's call-metrics.jsonl:
- Events: session start/end, tool calls/results, user/assistant speech, errors
- Tool calls: name, duration, timestamp
- Transcript: user and assistant turns

Writes on session end and on shutdown. Diagnosis skill updated to
read both phone and voice metrics for unified analysis.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Voice observability: full utterances + 7s user timestamp shift

- Remove 60-char truncation on user/assistant speech in events
- Add 7s backward shift for user timestamps (measured STT pipeline lag
  for voice agent is ~7s vs 12s for phone agent via Twilio)
- Update diagnosis skill: --metrics flag selects input file, source-aware
  labels (Voice Session vs Call), separate tracker HTML per source

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add call-diagnostics skill to repo

Copy the diagnosis skill from local ~/.claude/skills/ into the repo
so it's tracked. Includes --metrics flag for voice vs phone analysis,
source-aware labels, and unified tracker HTML generation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Refactor diagnose.py: 1057 → 737 lines (30% reduction)

- Extract CSS/chart JS into module constants
- Consolidate repair recommendations with _make_repair helper
- Compact issue detection (single-line dicts, _ts_short helper)
- Use list append + join for HTML generation
- Remove redundant whitespace in HTML output

No change to CLI behavior or output appearance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix duplicate metrics: guard writeVoiceMetrics with metricsWritten flag

onSessionEnd and shutdown() both call writeVoiceMetrics(). Flag prevents
duplicate entries. Reset on session start for reconnect support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove 60-char truncation from phone agent event data collection

Full utterances stored in call-metrics.jsonl — matches voice agent.
Diagnosis HTML can truncate for display if needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Diagnosis: add -c flag for timeline context around each issue

Shows ±2 surrounding events with >>> marking the issue point.
Helps cross-reference issues with the observability timeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix narration drift, diagnostics false positives, get_task_status, test cleanup

- Narration guardrail: "ONLY describe what you SEE" prevents Gemini from
  searching Google during record_screen_with_narration
- Diagnostics: exclude work tool from "failed N times" check (async delegate
  returns 0ms by design), exclude file paths from inline-keyword match
- get_task_status: use in-memory pendingTasks counter instead of scanning
  filesystem (task files get deleted after processing)
- Remove 5 incompatible test stubs (node:test instead of vitest)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Restore test stubs removed in previous commit

These test files use node:test (not vitest) and need migration,
not deletion. Keeping them as placeholders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix CI: update bodhi dependency to liususan091219 fork

sonichi GitHub account is gone (404). CI fails on npm ci because
it can't clone github:sonichi/bodhi_realtime_agent. Updated to
github:liususan091219/bodhi_realtime_agent (same code, forked
before account went down).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Apr 11, 2026
* Add voice agent observability + call diagnostics skill (#3)

* Add voice agent observability: events + tool calls + transcript

Track voice agent sessions in data/voice-metrics.jsonl with the same
format as phone agent's call-metrics.jsonl:
- Events: session start/end, tool calls/results, user/assistant speech, errors
- Tool calls: name, duration, timestamp
- Transcript: user and assistant turns

Writes on session end and on shutdown. Diagnosis skill updated to
read both phone and voice metrics for unified analysis.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Voice observability: full utterances + 7s user timestamp shift

- Remove 60-char truncation on user/assistant speech in events
- Add 7s backward shift for user timestamps (measured STT pipeline lag
  for voice agent is ~7s vs 12s for phone agent via Twilio)
- Update diagnosis skill: --metrics flag selects input file, source-aware
  labels (Voice Session vs Call), separate tracker HTML per source

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add call-diagnostics skill to repo

Copy the diagnosis skill from local ~/.claude/skills/ into the repo
so it's tracked. Includes --metrics flag for voice vs phone analysis,
source-aware labels, and unified tracker HTML generation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Refactor diagnose.py: 1057 → 737 lines (30% reduction)

- Extract CSS/chart JS into module constants
- Consolidate repair recommendations with _make_repair helper
- Compact issue detection (single-line dicts, _ts_short helper)
- Use list append + join for HTML generation
- Remove redundant whitespace in HTML output

No change to CLI behavior or output appearance.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix duplicate metrics: guard writeVoiceMetrics with metricsWritten flag

onSessionEnd and shutdown() both call writeVoiceMetrics(). Flag prevents
duplicate entries. Reset on session start for reconnect support.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove 60-char truncation from phone agent event data collection

Full utterances stored in call-metrics.jsonl — matches voice agent.
Diagnosis HTML can truncate for display if needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Diagnosis: add -c flag for timeline context around each issue

Shows ±2 surrounding events with >>> marking the issue point.
Helps cross-reference issues with the observability timeline.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix narration drift, diagnostics false positives, get_task_status, test cleanup

- Narration guardrail: "ONLY describe what you SEE" prevents Gemini from
  searching Google during record_screen_with_narration
- Diagnostics: exclude work tool from "failed N times" check (async delegate
  returns 0ms by design), exclude file paths from inline-keyword match
- get_task_status: use in-memory pendingTasks counter instead of scanning
  filesystem (task files get deleted after processing)
- Remove 5 incompatible test stubs (node:test instead of vitest)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Restore test stubs removed in previous commit

These test files use node:test (not vitest) and need migration,
not deletion. Keeping them as placeholders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix CI: update bodhi dependency to liususan091219 fork

sonichi GitHub account is gone (404). CI fails on npm ci because
it can't clone github:sonichi/bodhi_realtime_agent. Updated to
github:liususan091219/bodhi_realtime_agent (same code, forked
before account went down).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Migrate repo references from sonichi to liususan091219 (#4)

sonichi GitHub account is currently down (404). Update all
references to point to liususan091219/sutando and
liususan091219/bodhi_realtime_agent.

This PR can be reverted once the sonichi account is back.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* startup: auto-install fswatch via Homebrew when missing

Instead of just reporting "fswatch not found", startup.sh now
auto-installs it via `brew install fswatch` if Homebrew is available.
Falls back to the original error message if brew is not installed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Susan Xueqing Liu <xliu127@stevens.edu>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Chi <wangchi@Chis-Mac-mini.hsd1.wa.comcast.net>
sonichi pushed a commit that referenced this pull request Apr 17, 2026
Mini overnight deliverable #3 of 4. Runs ~8 checks in <20s and prints
PASS/WARN/FAIL for each. Exits 1 on any FAIL, 2 on any WARN, 0 on all-PASS
so it integrates cleanly with other scripts / CI.

Checks:
  voice-agent          port 9900 listening + log mtime <60s
  bodhi state machine  0× CLOSED→RECONNECTING FATALs in current log (post-#409)
  conversation-server  port 3100 /health = ok
  ngrok tunnel         curl public URL, expect 200-range
  presenter-mode       sentinel present + future expiry
  Claude quota         >=10% remaining
  disk space           >5 GB free
  memory-sync          last sync <6h

Designed for the 5-10 min pre-talk window. Owner runs it once, sees the
summary, triages the WARNs before going on screen.

Local test: 5 pass, 3 warn, 0 fail. Warnings are all actionable:
- ngrok tunnel unreachable (SSL handshake fails — separate bug, non-blocking
  for remote ICLR talk which uses Zoom audio routing, not phone)
- presenter-mode INACTIVE — expected, run before talk
- memory-sync 9h — run `bash src/sync-memory.sh` before talk

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Apr 17, 2026
* feat: stage-readiness.sh — single-command pre-flight check for ICLR talk

Mini overnight deliverable #3 of 4. Runs ~8 checks in <20s and prints
PASS/WARN/FAIL for each. Exits 1 on any FAIL, 2 on any WARN, 0 on all-PASS
so it integrates cleanly with other scripts / CI.

Checks:
  voice-agent          port 9900 listening + log mtime <60s
  bodhi state machine  0× CLOSED→RECONNECTING FATALs in current log (post-#409)
  conversation-server  port 3100 /health = ok
  ngrok tunnel         curl public URL, expect 200-range
  presenter-mode       sentinel present + future expiry
  Claude quota         >=10% remaining
  disk space           >5 GB free
  memory-sync          last sync <6h

Designed for the 5-10 min pre-talk window. Owner runs it once, sees the
summary, triages the WARNs before going on screen.

Local test: 5 pass, 3 warn, 0 fail. Warnings are all actionable:
- ngrok tunnel unreachable (SSL handshake fails — separate bug, non-blocking
  for remote ICLR talk which uses Zoom audio routing, not phone)
- presenter-mode INACTIVE — expected, run before talk
- memory-sync 9h — run `bash src/sync-memory.sh` before talk

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(stage-readiness): portable stat + df + remove dead ngrok conditional

Three real bugs surfaced by MacBook's cold-review on a Mac-with-Homebrew-
coreutils dev environment (the common setup — MacBook's Apr 26 pre-flight
will run under this shell).

1. `stat -f %m` BSD syntax fails silently on GNU coreutils: GNU emits
   filesystem info to stdout AND exits non-zero. The original
   `BSD || GNU` fallback in `$(...)` concatenated both outputs, then
   `set -u` arithmetic expansion died with "File: unbound variable"
   and silently aborted remaining checks. Fix: new `stat_mtime()` helper
   tries GNU first (works on Linux + coreutils-on-Mac), BSD as fallback,
   explicit `echo 0` as second fallback. Order matters — GNU first is
   correct because GNU's presence is the bug trigger; pure BSD Macs
   without coreutils work cleanly either way.

2. `df -g` is BSD-only. GNU df doesn't support `-g`, so the disk check
   silently printed nothing and was skipped entirely. Fix: POSIX
   `df -k` returns KB universally; convert to GB via arithmetic.

3. Dead `[[ "$probe_url" != */* ]]` conditional on the ngrok probe:
   the pattern matches any URL containing `/` — i.e., every full URL,
   because `https://` has slashes — so the branch always triggers
   regardless of path. The intended "append /health only if no path"
   semantic doesn't work. Fix: just unconditionally append
   `/health` since all Twilio-bound ngrok webhooks need it.

Tested on this Mac: `bash scripts/stage-readiness.sh` → 6 pass, 2 warn
(ngrok SSL handshake + presenter-mode INACTIVE are expected pre-talk),
exit 0.

Credit: MacBook's cold-review-log.md entry on #412 caught (1) and (2);
(3) I'd already flagged in build_log pass 435 but hadn't fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Chi <wangchi@Chis-Mac-mini.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 pushed a commit that referenced this pull request Apr 18, 2026
Per Susan's 'general edit to correct outdated descriptions' ask. Applied
Maddy's review hits that I could verify locally:

- pkill pattern: `-f "src/Sutando"` → `-x Sutando` (hit #8: safer, matches
  the actual process name regardless of launch path).
- Uninstall step 5: softened from `brew uninstall imsg wacli` to a
  generic 'remove via the package manager you used' (hit #3: wacli isn't
  necessarily brew).
- Capabilities table:
  - Task delegation: `task-bridge.ts` → `task-bridge.ts + watch-tasks.sh + tasks/`
    (hit #4: reflects the actual file-based flow).
  - Notes: 'via CLAUDE.md' → '`notes/` directory (YAML-frontmatter markdown)'
    (hit #5: notes/ is the real store).
  - Added rows for cross-node sync, info-radar, menu-bar avatar states
    (hit #6: recent features missing from the matrix).
- Services table: added 7845 (screen-capture), 3100 (conversation-server),
  4040 (ngrok admin UI) (hit #7).
- Removed stray duplicate `---` separator after the services table
  (hit #9).

Skipped:
- Hit #1 (L77 subscription line) — already removed earlier.
- Hit #2 (screencapture note) — Maddy confirmed still accurate.
- Hit #10 (swiftc missing `-framework AVFoundation`) — main.swift does NOT
  import AVFoundation (verified: import Cocoa, Carbon, UserNotifications
  only). Current compile line is correct.
- L101 permission rewrite (Maddy's 4-bullet proposal) — substantive
  rewrite, deferred to Susan's specific approval before applying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Apr 19, 2026
- `.env.example`: fix stale `GEMINI_API_KEY` description — it said "STT
  fallback" but after the expansion commit STT routes through
  `GEMINI_VOICE_API_KEY`. Rewritten to reflect current behavior.
- `voice-agent.ts`: expand the comment above `createGoogleGenerativeAI`
  to acknowledge the deliberate tradeoff Mini flagged (subagents lose
  paid-tier quota; revisit if throughput becomes a concern).

Follow-up #3 from the review (fail-fast on wrong voice key → silent
voice break) deferred to a separate PR; current PR is unchanged
behaviorally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Apr 19, 2026
…#452)

* feat(voice-agent): support GEMINI_VOICE_API_KEY env for key split

Lets users put the Gemini Live voice session on a separate API key from
the rest of Sutando (text LLM, vision, STT fallback). Motivation is
Susan's $99/28d billing incident: her paid-tier-with-billing project was
metering long voice sessions. Gemini Live voice is free on the free tier
(rate-limited, no metered charges), so a free-tier key for voice isolates
it from any paid-tier spend on the shared GEMINI_API_KEY.

Change:
- `src/voice-agent.ts`: new `GEMINI_VOICE_API_KEY` const with
  `process.env.GEMINI_VOICE_API_KEY || GEMINI_API_KEY` fallback; pass to
  `VoiceSession.apiKey`. Comment block at top updated.
- `.env.example`: documented the new env var + the free/paid tier
  rationale users care about.

Scope is deliberately narrow: only the VoiceSession apiKey switches.
Text LLM (line 103 `createGoogleGenerativeAI`), GeminiBatchSTTProvider
fallback (line 597), and browser-tools vision (`src/browser-tools.ts:323`)
stay on GEMINI_API_KEY — those paths don't have the "free tier unlimited"
billing asymmetry that voice does.

Backward compatible: unset → voice uses GEMINI_API_KEY as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(voice-agent): route subagent LLM + Gemini STT through voice key too

Expands #452's scope from "voice session Live only" to "all voice-session-
triggered Gemini traffic" per Chi's refinement in DM.

Two more apiKey swaps:
- `createGoogleGenerativeAI(...)` at line 110 (was GEMINI_API_KEY). Only
  consumer is VoiceSession.model, which bodhi uses for voice-session
  subagent text LLM calls (generateText inside bodhi's Subagent class).
- `GeminiBatchSTTProvider(...)` at line 608 (was GEMINI_API_KEY). Handles
  user-side webUI transcripts every voice turn when STT_PROVIDER=gemini
  (default when CARTESIA_API_KEY is unset).

Result: every Gemini call triggered by a voice session (Live + STT +
subagents) routes to GEMINI_VOICE_API_KEY. Non-voice paths
(image/video generation, vision describe) stay on GEMINI_API_KEY.

Backward compatible: unset → all routes use GEMINI_API_KEY as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: address Mini's cold-review on #452

- `.env.example`: fix stale `GEMINI_API_KEY` description — it said "STT
  fallback" but after the expansion commit STT routes through
  `GEMINI_VOICE_API_KEY`. Rewritten to reflect current behavior.
- `voice-agent.ts`: expand the comment above `createGoogleGenerativeAI`
  to acknowledge the deliberate tradeoff Mini flagged (subagents lose
  paid-tier quota; revisit if throughput becomes a concern).

Follow-up #3 from the review (fail-fast on wrong voice key → silent
voice break) deferred to a separate PR; current PR is unchanged
behaviorally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 added a commit that referenced this pull request Apr 19, 2026
* docs(README): update 'Summon' voice trigger example to 'Summon my computer to zoom'

The short form 'Summon' is false-positive-prone in voice transcripts. Owner
uses the longer 'Summon my computer to zoom' phrase in practice. This docs
update matches what actually triggers the Zoom summon reliably.

Code-side prompts are NOT changed in this PR (per CLAUDE.md's 'prompts tuned
through testing' rule) — the underlying summonTool already matches both
phrases via the 'summon' keyword.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(README): add two troubleshooting entries (Claude context drift + phone-server error)

Per Susan's Discord msg: two common failure modes worth surfacing in
Troubleshooting:
1. Claude hallucinates more as context fills → tell users to restart the
   Claude Code session periodically.
2. Phone call answers 'We are sorry, an error has occurred' → the
   conversation-server on port 3100 isn't running; relaunch via
   startup.sh or restart.sh.

Verified the correct server path: skills/phone-conversation/scripts/
conversation-server.ts (not a standalone start-phone-server.sh, which
doesn't exist).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(README): apply Maddy's verified hits — general cleanup

Per Susan's 'general edit to correct outdated descriptions' ask. Applied
Maddy's review hits that I could verify locally:

- pkill pattern: `-f "src/Sutando"` → `-x Sutando` (hit #8: safer, matches
  the actual process name regardless of launch path).
- Uninstall step 5: softened from `brew uninstall imsg wacli` to a
  generic 'remove via the package manager you used' (hit #3: wacli isn't
  necessarily brew).
- Capabilities table:
  - Task delegation: `task-bridge.ts` → `task-bridge.ts + watch-tasks.sh + tasks/`
    (hit #4: reflects the actual file-based flow).
  - Notes: 'via CLAUDE.md' → '`notes/` directory (YAML-frontmatter markdown)'
    (hit #5: notes/ is the real store).
  - Added rows for cross-node sync, info-radar, menu-bar avatar states
    (hit #6: recent features missing from the matrix).
- Services table: added 7845 (screen-capture), 3100 (conversation-server),
  4040 (ngrok admin UI) (hit #7).
- Removed stray duplicate `---` separator after the services table
  (hit #9).

Skipped:
- Hit #1 (L77 subscription line) — already removed earlier.
- Hit #2 (screencapture note) — Maddy confirmed still accurate.
- Hit #10 (swiftc missing `-framework AVFoundation`) — main.swift does NOT
  import AVFoundation (verified: import Cocoa, Carbon, UserNotifications
  only). Current compile line is correct.
- L101 permission rewrite (Maddy's 4-bullet proposal) — substantive
  rewrite, deferred to Susan's specific approval before applying.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(README): revise 'How it works' diagram — phone agent + accurate process topology

Per Susan's ask ('diagram isn't quite right', 'phone agent rarely covered')
and Maddy's accuracy verification:

Diagram changes:
- Web client → Voice agent is now WebSocket on :9900 (was implicit file bridge)
- Phone (Twilio) → Conversation server added as peer entry path
- results/ flow annotated as per-channel (speak via voice/phone vs text
  via Telegram/Discord), not one unified speak path

Process topology changes:
- "Two processes work together" → "Three processes" (voice-agent,
  conversation-server, core-agent) — verified against
  conversation-server.ts by Maddy (writes to tasks/ directly at lines
  289, 964, 1315; not hierarchically downstream of voice-agent).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(README): surface phone-agent in prereqs + services list

Per Susan's ask ('phone agent is rarely covered in the README, it should be
mentioned when appropriate'):
- Prereqs: added optional Twilio + ngrok bullet so readers know the phone
  path exists and what they need to enable it.
- 'This starts all services' sentence: added 'phone conversation server' to
  the list so readers see it's a default-started daemon, not an opt-in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(README): rewrite permission section (4-bullet 'why elevated perms') per Maddy's proposal

Replaces the conservative CYA note ('you should review what it does. All
actions are logged') with Maddy's framing that tells the reader:
1. It's local (no remote control plane)
2. You control the audience (3-tier access gating + VERIFIED_CALLERS)
3. Actions are auditable (build_log + tasks/ + results/ + logs/)
4. Hooks are your brake pedal (git-rules-guard.sh Discord 👍/👎)

Susan's direction was 'lead the work and Maddy please review iteratively'.
Applying Maddy's already-drafted proposal; she reviews next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(README): add inline-tools path to 'How it works' diagram

Per Susan's ask — voice agent and conversation server have two
response paths (inline tools for instant in-process calls;
tasks/ for anything the core agent needs to execute), while
Telegram/Discord bridges only use tasks/.

— Lucy (Mac Studio bot)

* docs(README): swap 'mute' for real inline tool per Maddy's review

mute is a menu-bar ⌃M shortcut, not an inline tool. Replace with
get_current_time (src/inline-tools.ts:376) and broaden the prose
sentence to list only genuine inline tools.

— Lucy (Mac Studio bot)

* docs(README): add running-costs section + price-aware prereqs

Per Susan's in-#dev review (consolidated 4-edit bundle, 👍'd):
- Line 96: drop unqualified "free" on API key — usage-billed per
  Running costs
- Line 97: add ~$0.01–0.03/min Twilio cost reference
- New "## Running costs" section: Gemini Live $0.30–$1.30/hr,
  Twilio $0.0125/min (US local), free for browser/Telegram/Discord
- New ffmpeg bullet in prereqs (optional, for video/audio)

Sources: Gemini pricing docs, Twilio voice pricing docs.

— Lucy (Mac Studio bot)

* docs(README): bundle 4 follow-ups from #susan iteration

Per Susan's 4× 👍 iterative review on PR #450:
- Diagram: add `↻` on Core agent + legend explaining the cron-driven
  proactive loop (5-min fires for tasks, health, build-log work).
- Running costs table: Twilio row now bidirectional — ~$0.013–$0.018/min
  covers inbound ($0.0085) + outbound ($0.014) + Media Streams ($0.004).
- Prereq bullet: rename "Google AI Studio API key" → "Gemini API key"
  and inline the ~$0.30–$1.30/hour voice cost.
- macOS permissions: moved from inline Quick start bullets to the
  Security section with a per-permission "why it's needed" expansion;
  Quick start keeps a one-line pointer.

— Lucy (Mac Studio bot)

* docs(README): hedge Gemini price claims — free tier vs paid unclear

Susan and Chi are still sorting out the free-tier vs paid-tier
picture; pull back the specific $0.30-$1.30/hr claim since it
misrepresents the free-tier case:

- Prereq bullet: "may be free on the free tier or usage-billed
  depending on your Google Cloud account" instead of a rate
- Running costs table Gemini row: "Free tier available - paid
  usage varies"; notes keep the published paid rates as reference
  but point readers at their billing dashboard for ground truth
- Drop the personal $13/602-min datapoint from the typical-call
  example (may conflate with non-voice Gemini spend)

Twilio row unchanged - that math is from published rates.

— Lucy (Mac Studio bot)

* docs(README): Option B — remove Running costs section + price mentions

Per Susan's "B" direction in #dev: the free-tier vs paid-tier
picture for Gemini isn't settled, and leaving any price language
in the PR risks misleading first-time readers.

- Delete the entire "## Running costs" section (table + example +
  sources line).
- Revert the Gemini API key prereq bullet to `(click "Get API key")`
  — drop the usage-billed caveat entirely.
- Revert the Twilio prereq bullet to drop the `~$0.01–0.03/min`
  price reference.

ffmpeg bullet, diagram rewrite, macOS permissions in Security
section, `↻` cron legend, phone-agent surfacing — all stay.

— Lucy (Mac Studio bot)

---------

Co-authored-by: Xueqing Liu <xueqingliu@Xueqings-Mac-Studio.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Apr 19, 2026
1. Audio-RMS s-speaking never cleared on audio-drop (Mini's #1, real
   bug). Mirror legacy `classList.toggle('speaking', speaking)` behavior:
   when speaking=false, remove `s-speaking` and fall back to `s-idle`.
   Server SSE will overwrite with the true state (listening/working/etc.)
   on the next poll. Without this the SVG visor stayed green until the
   next SSE event.

2. Unused `visorSweepHero` gradient removed from hero SVG (Mini's #2).
   CSS only references `#visorSweep` (defined in header); hero now reuses
   that definition. Saved ~9 lines of dead markup.

Mini's #3 (extract duplicated SVG into a helper) deferred to a follow-up
per the review.

Typecheck clean. 93/93 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request Apr 19, 2026
…tions (#457)

* feat(web-client): wire inline-SVG default avatar with per-state animations

Chi: "I need to test on macbook". PR #443 shipped the 5-state inline-SVG
mockup at docs/avatar-default.html (282 lines, CSS keyframes, 5 states
side-by-side) but the live web-client still only rendered the PNG
`<img>` — hidden entirely for users without a custom avatar
(avatarGenerated=false, i.e. most new users).

This wires the mockup into the live UI.

Changes:
- Add `.avatar-svg-default` CSS block: per-state selectors via `.s-*`
  modifier on parent containers (#avatar-wrap, #hero). Five states with
  distinct animations: idle (indigo breathe), listening (violet halo
  pulse), speaking (green visor sweep + glow), working (blue orbiting
  dots + swell), seeing (amber visor scan).
- Add inline SVG to both header `#avatar-wrap` (44px) and `.hero`
  (80px). Hidden by default via `display:none`.
- Identity-fetch handler: when `!avatarGenerated`, show the SVG
  containers. When generated, the existing PNG path takes over.
- State class propagation:
  - Server-side SSE 'agent-state' events → toggle `.s-{state}` on
    #avatar-wrap + #hero (5-way exclusive, defaults to `s-idle`).
  - Local audio-RMS speaking detection → also sets `.s-speaking` on
    the parent (prevents ~500ms lag vs the PNG `.speaking` class).

Backward compatible:
- Custom-avatar users see the PNG exactly as before (SVG stays hidden).
- `.working`/`.seeing`/`.speaking` classes on the PNG still work.
- No server-side changes.

Test:
- `npx tsc --noEmit` clean.
- `bun test tests/` → 93/93 pass.
- Manual: `launchctl kickstart -k gui/$(id -u)/com.sutando.web-client`,
  open `http://localhost:8080`, observe the SVG avatar breathe (idle);
  connect voice to trigger listening/speaking/working/seeing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(web-client): address Mini's #457 cold-review nits

1. Audio-RMS s-speaking never cleared on audio-drop (Mini's #1, real
   bug). Mirror legacy `classList.toggle('speaking', speaking)` behavior:
   when speaking=false, remove `s-speaking` and fall back to `s-idle`.
   Server SSE will overwrite with the true state (listening/working/etc.)
   on the next poll. Without this the SVG visor stayed green until the
   next SSE event.

2. Unused `visorSweepHero` gradient removed from hero SVG (Mini's #2).
   CSS only references `#visorSweep` (defined in header); hero now reuses
   that definition. Saved ~9 lines of dead markup.

Mini's #3 (extract duplicated SVG into a helper) deferred to a follow-up
per the review.

Typecheck clean. 93/93 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(web-client): strip backticks from embedded-JS comment

Previous commit's comment body had backticks (`classList.toggle(...)`)
inside a <script> block that lives in a TS template literal — the
backticks terminated the outer template early and broke tsc (3 errors
at line 1140, pre-merge CI would have caught it).

Typecheck now clean. 93/93 tests pass. Per feedback_web_client_embedded
_js_no_ts.md — the <script> block is strict browser JS inside a
backtick-delimited TS string, so ALL backticks in the embedded JS
(including inside comments) must be escaped or avoided.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219 added a commit that referenced this pull request May 3, 2026
…, timeout label

Builds on the silent-timeout + nudge work. Susan's follow-ups today:

1. The yellow ⏱ icon was visible but no "TIMEOUT" word — easy to miss.
   web-client.ts: render a small uppercase "TIMEOUT" pill next to the
   icon when status === 'timeout', styled as `.task-status-label.timeout`
   (yellow on dark-yellow, parallel to the existing badge color).

2. "Just one nudge then nothing" was too quiet for a genuinely-stuck
   core. New behavior: nudges fire at 1×, 2×, 3× NUDGE_AT_MS (= 3×, 6×,
   9× TASK_TIMEOUT_MS). Each carries a "[Sutando-status-check #N]"
   prefix and the original task text. After the 3rd nudge fires, the
   task is escalated via Discord DM to the owner — task-bridge writes
   `results/proactive-stuck-{ts}.txt`, which the existing Discord
   bridge picks up and DMs the owner. Per-task dedupe means escalation
   fires at most once per task per result-cycle (cleared when the
   real result arrives).

State change: `_nudgeFired: Set<string>` → `_nudgeCount: Map<string,
number>` to support periodic firing. New module-level `_escalated`
guards the once-per-task DM. Both reset in `_processResult`.

Test "nudges fire periodically up to MAX_NUDGES, then escalate via
Discord DM" walks the full ladder: timeout-only at age < NUDGE_AT_MS,
nudge #1 at 1×, nudge #2 at 2×, nudge #3 + escalation at 3×, then
asserts capping (no nudge #4) and escalation dedupe (no second DM)
at 4×. Still silent throughout — `onResultCalls.length === 0`.

All 6 timeout tests pass; full suite still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi added a commit that referenced this pull request May 5, 2026
…do.app

Per Mini's #600 review nit #3 + Chi's confirmation: after PR #600 lands,
Sutando.app is the canonical writer for contextual-chips.json. Removing
step 11 from the proactive-loop SKILL.md establishes the single-writer
invariant immediately, before /build-loop is split out.

Step 12 (heartbeat) renumbered to 11 with a note explaining where chip
refresh moved + why future cases that need decision-state in chips
should surface via a different file Sutando.app reads, not by competing
as a writer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant