Zero-terminal first boot. Previously the bootstrap_ceo invite had to be
created via a CLI command (paperclipai auth bootstrap-ceo) and the UI
showed a code block instructing the user to run it. Nexus is meant to
be zero-terminal, so the server now auto-creates the invite on startup
when no instance admin exists and exposes its relative path through
/api/health. BootstrapPendingPage redirects straight to /invite/{token}.
The CLI command is left intact for headless/SSH-only setups.
Invite flow fixes that surfaced during testing:
- InviteLanding's invite query had default React Query refetch
behavior. After a successful bootstrap accept, the invite is marked
accepted server-side, so the refetch returned "not available" and
shadowed the success screen, making it look like the bootstrap had
failed when it actually succeeded. Set staleTime: Infinity +
refetchOnWindowFocus/Mount/Reconnect: false so the first fetch is a
one-shot snapshot.
- Reordered the render checks so result?.kind === "bootstrap" / "join"
are evaluated before the invite-availability error check — defensive
against any stray refetch that still leaks through.
- On bootstrap success, window.location.replace("/") lands the new
admin directly on the board; the "Bootstrap complete" confirmation
screen is now an unreachable safety net.
Vite onnxruntime middleware replaces the earlier public/ dump. The
previous commit put ort-wasm-simd-threaded.{mjs,wasm} in ui/public/ so
VAD's onnxWASMBasePath: "/" would find them. That works at runtime but
trips vite's dep optimizer: it scans onnxruntime-web, resolves the
dynamic import string to the public asset, and errors with "files in
/public should not be imported from source code." Remove the files and
add a vite plugin (configureServer middleware) that serves the two URLs
straight from node_modules/.pnpm/onnxruntime-web@*/. Runtime keeps
working and the files never enter vite's module graph.
Production build caveat: the middleware only runs in dev. When building
a static dist for production, the wasm files will need a different
mechanism (e.g. generateBundle hook). Not addressed here.
Also bundled (load-bearing for LAN browser testing):
- ui/src/lib/queryKeys.ts: add missing 'nexus' group. useNexusMode
referenced queryKeys.nexus.settings since commit 7bb72a5a (Phase
33-02) but the key was never added. Caused a blank screen crash on
any page that mounts Sidebar.
- ctl.sh: read PORT from .env instead of hardcoding 3100, and read it
once at the top so every subcommand honors it. Fixes the Version /
Mode showing '?' in status output after the port move to 6100.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HEAD had 3 pairs of drizzle-generated migrations colliding on indices 46-48
(chat set vs doc/feedback/routines set) with a journal that only referenced
one of each pair. Migrations 0047-0055 (chat_conversations, chat_messages,
bookmarks, chat_files, push_subscriptions, etc.) were committed as files on
disk but never added to _journal.json, so drizzle never applied them.
Rename the three non-chat ghost migrations out of the conflict zone
(0046/0047/0048 -> 0056/0057/0058) and extend the journal with entries for
all 12 previously-orphaned migrations so drizzle applies them in order on
fresh DB init.
Also mount chatRoutes() in app.ts — the router was defined in routes/chat.ts
but never wired up, so /api/companies/:id/{conversations,bookmarks} 404'd
even when tables existed.
Ship ort-wasm-simd-threaded.mjs + .wasm in ui/public so VAD can load the
onnxruntime module at /ort-wasm-simd-threaded.mjs instead of getting the
SPA HTML fallback.
Bundles pre-existing LAN-testing hunks in app.ts: conditional COOP/COEP
headers (only on secure/localhost origins) and Vite HMR host fix for
0.0.0.0 binding so the HMR client connects back to whatever hostname the
browser used. These are load-bearing for LAN browser testing on plain HTTP.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add VoiceCapability interface with whisperAvailable, piperAvailable, voiceTierSufficient
- Extend HardwareInfo with voiceCapability field
- Add detectVoiceCapability() probing whisper-cpp/whisper and piper with 2s timeout each
- voiceTierSufficient: true for apple_silicon/gpu, or cpu_only with >= 4GB free RAM
- Wrap voice probe in 3s timeout to avoid slowing hardware detection
- Route automatically includes voiceCapability via existing HardwareInfo return
- Add streaming prop (default true) to ChatVoicePlayerProps
- Connect to POST /api/synthesize/stream via fetch + ReadableStream
- Parse SSE lines manually from response body stream
- First sentence audio begins playing as soon as first chunk arrives
- Subsequent sentences auto-play in sequence from audioQueue
- Show 'Sentence N of M' progress indicator during streaming playback
- Dot progress bar shows completed vs pending sentences
- Falls back to full-fetch mode on stream error or streaming=false
- Clean up all object URLs on unmount or new text
- Export splitSentences() with title-abbreviation protection (Dr., Mr. etc.)
- Add synthesizeSentenceStream() AsyncGenerator yielding per-sentence audio chunks
- Add synthesizeMultiLang() synthesizing same text in N voices via Promise.all
- Add POST /api/synthesize/stream SSE endpoint with base64 audio per sentence
- Add POST /api/synthesize/multi-lang returning array of voiceId+audio pairs
- Existing POST /api/synthesize unchanged (backward compatible)
- Refactor text relay into shared relayToAgent() used by both text/voice handlers
- Add bot.on('message:voice') handler: send 'Transcribing...' immediately, process async
- Download OGG from Telegram CDN via ctx.getFile() + fetch, transcribe via voicePipelineService
- Synthesize agent responses to OGG Opus via transcodeToOggOpus() and ctx.replyWithVoice()
- TTS failure degrades gracefully (text reply already sent, voice is bonus)
- telegram.ts stays at 322 lines (under 500-line TGRAM-06 constraint)
- Import telegramService, telegramRoutes, nexusSettingsService
- Mount /telegram routes under /api prefix
- Conditionally start Telegram bot on boot if telegramToken is configured
- Token route restarts bot after saving new token
- Install grammy v2 for long polling Telegram bot
- telegramService: text relay handler, agent prefix, session map, deleteWebhook lifecycle
- telegramRoutes: POST /telegram/token (getMe validation), GET /telegram/status
- telegram.ts under 500 lines (187 lines)
- BotFather numbered instructions (4-step setup guide)
- Token input with live validation via POST /api/telegram/token
- Success state showing connected bot username
- Error state with descriptive message
- Skip/Back/Next navigation; Next enabled only after validation
- postMessageAndStream data type extended with optional voiceMode field
- startStream signature updated: (userMessage, agentId?, voiceMode?)
- voiceMode forwarded into fetch body via postMessageAndStream call
- VoiceModeToggle: Text / Voice In / Full Voice pills with active/inactive styling
- Auto-play checkbox in full_voice mode, persists to nexus:voice:autoplay in localStorage
- useVoiceMode: reads/writes voiceMode via PATCH /api/nexus/settings with loading state
(deviation Rule 3: created missing blocking dependency for VoiceModeToggle)
- VoiceWaveform: 80x32 canvas with Web Audio AnalyserNode (fftSize=64), 20 animated bars drawn from frequency data using --primary color
- VoiceMicButton: three visual states — idle (Mic icon), recording (VoiceWaveform + ring-2 ring-primary), processing (Loader2 animate-spin)
- All three states have correct aria-labels per UI spec copywriting contract