docs: add macOS setup gotchas (npm prefix, ffmpeg, Groq key format)#45
Open
impactworks-dev wants to merge 146 commits into
Open
docs: add macOS setup gotchas (npm prefix, ffmpeg, Groq key format)#45impactworks-dev wants to merge 146 commits into
impactworks-dev wants to merge 146 commits into
Conversation
Captures real issues encountered during a fresh macOS setup walkthrough: - npm -g EACCES workaround via user-writable prefix - brew install ffmpeg required for local say + ffmpeg TTS fallback - Groq API keys are gsk_* (not org_*) to avoid 401 invalid_api_key - Clarify secrets live in .env, not the shell - launchd PATH must include Claude CLI dir to spawn subprocesses Also refreshes package-lock.json to match npm install. Co-Authored-By: Oz <oz-agent@warp.dev>
Document the one-time local setup for: - Obsidian Brain vault integration (path, registry, cleanup) - GitHub SSH ed25519 key with Keychain-backed auto-load - kepano/obsidian-skills install layout and user-level symlinks - Agent template vault configuration - launchd PATH requirements for npm-global binaries No secrets are recorded; machine-specific values stay in .env. Co-Authored-By: Oz <oz-agent@warp.dev>
Add a new chat_settings table to store per-chat preferences keyed by chat_id and key. Use it to persist the voice-reply toggle so the bot remembers each chat's voice mode across restarts. Changes: - db.ts: add chat_settings table and get/set/delete/getAllByKey helpers - bot.ts: load voice-enabled chats on startup; persist /voice toggle - index.ts: invoke loadVoiceEnabledChats() at startup and log result Co-Authored-By: Oz <oz-agent@warp.dev>
Add .gitignore entries for files that are either machine-specific or managed by tooling: - .agents/ and matching .claude/skills/* symlinks (managed by `skills`) - cli/ (third-party repo cloned for reference) - memory/ (personal knowledge base) - dashboard.html (generated from src/dashboard-html.ts) - TASKS.md (personal task tracker) - launchd/com.claudeclaw.tunnel.plist (absolute home paths) Also track skills-lock.json so the skill install set is reproducible. Co-Authored-By: Oz <oz-agent@warp.dev>
Introduces the warroom/ Python package: a WebSocket voice server that bridges browser audio to ClaudeClaw agents via either Gemini Live (default) or Deepgram STT + Cartesia TTS. Built against pipecat-ai 1.0.0: - WebsocketServerTransport owns the WebSocket server (host/port). - SileroVADAnalyzer is attached via WebsocketServerParams.vad_analyzer. - PipelineTask accepts params as a keyword-only argument. - LLMContext + LLMContextAggregatorPair replace the old OpenAILLMContext + llm.create_context_aggregator() pattern. - GeminiLiveLLMService replaces GeminiMultimodalLiveLLMService. - Cartesia/Deepgram service imports use their .tts / .stt submodules. - CartesiaTTSService uses Settings(voice=...) instead of deprecated voice_id kwarg. Also ignores warroom/.venv/ and warroom/__pycache__/ from VCS. Co-Authored-By: Oz <oz-agent@warp.dev>
The test 'truncates last_result to 500 chars' asserted toHaveLength(500) against a 1000-char input, but updateTaskAfterRun in src/db.ts truncates via result.slice(0, 4000). The input was therefore never truncated and the length check was unreachable. Align the test with the current runtime behavior: feed 5000 chars and assert the stored value is exactly 4000. Co-Authored-By: Oz <oz-agent@warp.dev>
Add War Room voice server on pipecat 1.0
rubening
reviewed
May 19, 2026
rubening
left a comment
There was a problem hiding this comment.
Review (Overnight Agent, Night #97)
Thanks for the setup docs content -- the macOS gotchas section is genuinely useful. However, this PR has several issues that make it unmergeable as-is:
- CLAUDE.md deletion -- This PR replaces the entire production CLAUDE.md with a generic template. CLAUDE.md contains the bot's personality, environment config, skills, special commands, and all operational instructions. Deleting it would break the bot.
- Unrelated code changes --
src/bot.ts,src/db.ts,src/index.tsmodifications for voice persistence, and an entirewarroom/Python package, are bundled with what's labeled as a docs PR. - Mixed commits -- 7 unrelated commits covering docs, code features, config changes, and a merge commit from your fork.
Recommendation: Please submit a clean PR with only the setup docs changes (a new docs/SETUP_NOTES.md and the relevant README additions). The voice persistence feature and warroom package are interesting but should be separate PRs with their own review.
The headRefName being main suggests these were committed directly to your fork's main -- consider using feature branches for future PRs to keep changes isolated.
- Add interactive dashboard with HTML UI and TypeScript backend - Add War Room client bundle, avatars, and HTML interface - Add Google OAuth authentication flow (scripts/google-auth.ts) - Update package dependencies (package.json, package-lock.json) - Add launchd service plists and restart-main.sh helper - Various src/ module updates and new TypeScript sources Co-Authored-By: Oz <oz-agent@warp.dev>
Ports five osrepo PRs onto the manual-port snapshot: - PR earlyaidopters#51 (security/dashboard): timing-safe token comparison + CORS Origin allowlist + War Room WebSocket upgrade auth gate - PR earlyaidopters#52 (security/dashboard-html): escape agent.name on 3 innerHTML render sites - PR earlyaidopters#57 (fix/scheduler): pass agentDefaultModel to runAgent for scheduled tasks (mission tasks already correct) - PR #67 (fix/orchestrator): pass target agent's model to runAgent in delegateToAgent (was undefined, ignored per-agent model) - PR #69 (fix/scheduler): wire memory ingestion into mission and scheduled task completion Skipped from PR earlyaidopters#51: CSRF middleware block (not present in snapshot), requireToken helper (not present in snapshot). Files: src/dashboard.ts, src/dashboard-html.ts, src/scheduler.ts, src/orchestrator.ts. +77 / -10.
Adds the full v2 dashboard frontend from osrepo/main:
- web/ (55 files): index.html, main.tsx, App.tsx, 14 pages, 19 components,
16 lib helpers, public/brain.glb (1.6MB)
- vite.config.ts: Vite root=web/, outDir=dist/web/, proxies /api+/ws+
/warroom-* to backend in dev, vendor chunk split
- package.json + package-lock.json: adds preact, vite, three,
monaco-editor, tailwindcss v4, dompurify, marked, lucide-preact,
wouter-preact, @preact/signals, @preact/preset-vite, @types/three,
@types/dompurify, @tailwindcss/vite
Backend wiring in src/dashboard.ts:
- Token middleware now gates only /api/*. SPA shell at / is public so
token-stripped URLs still load the app and the SPA reads ?token= from
window.location itself
- requireToken() helper for legacy HTML routes that embed DASHBOARD_TOKEN
- / route serves dist/web/index.html when present, falls back to legacy
inline HTML if DASHBOARD_LEGACY=true or bundle is missing
- /assets/* serves Vite-built JS/CSS/source-maps with immutable cache
- /:filename.{glb|gltf|bin|ktx2|wasm|svg|png|webp|ico} serves top-level
static files (e.g. /brain.glb for 3D Hive Mind)
- /warroom now calls requireToken() inline (was global middleware)
Known gaps vs osrepo/main (SPA pages will 404 on these endpoints):
- /api/dashboard/settings PATCH (Settings page)
- /api/security/kill-switch POST (Settings -> Security)
- /api/warroom/unpin POST (War Room)
These depend on src/kill-switches.ts and other osrepo modules not yet
ported. UI loads, most pages work, three buttons fail. Iterate later.
Files: src/dashboard.ts, package.json, package-lock.json, web/* (55),
vite.config.ts.
…, war room text) Dashboard fixes (earlier today): - token persisted in localStorage (no more 401 lockouts on fresh tabs/bookmarks) - SPA history fallback for deep routes (no more 404 on refresh of /mission etc.) - War Room voice WebSocket token fix (warroom-html.ts) Feature parity ported from cached osrepo/main: - /api/dashboard/settings (GET+PATCH), /api/security/kill-switch (POST) - /api/agents/suggestions (4 routes), /api/warroom/text/* (11 routes) + /warroom/text page - new modules: kill-switches, env-write, warroom-tool-policy, warroom-text-* - security.ts getScrubbedSdkEnv, state.ts abortByPrefix, memory-ingest extractViaClaude - buildMemoryContext opts param, AgentConfig.warroomTools (additive, backward compatible) db.ts: - new tables: dashboard_settings, agent_suggestions, agent_file_history - warroom_meetings meeting_type + chat_id migrations - 26 parity helper functions; getWarRoomTranscript/addWarRoomTranscript upgraded (back-compat) chore: harden .gitignore against untracked OAuth client_secret + personal working files
- Multi-stage Dockerfile with Python venv + Claude Code CLI baked in - docker-entrypoint.sh bridges Fly secrets to /app/.env, restores Claude creds - fly.toml: app config, persistent volume, single machine, no HTTP healthcheck - Run as non-root node user (Claude Code refuses --dangerously-skip-permissions as root) - agents/*/agent.yaml: obsidian vault path → /app/store/obsidian-brain (syncthing target) - .github/workflows/fly-deploy.yml: auto-deploy on push to main - scripts/fly-*.sh: setup, data migration, cutover, syncthing helpers - FLY-MIGRATION.md: end-to-end runbook
Includes daily brief, memory-to-tasks dispatcher, sidebar rebrand, dashboard updates that were running locally on Mac before cutover but missed the first CI commit.
Without this, every deploy wipes /home/node/.claude/projects/ and stored session IDs become orphans. Symlink the dir onto the Fly volume so sessions survive container restarts. No more manual DELETE FROM sessions needed.
Bumps machine to 2GB to fit 5 node processes + Gemini bursts. Each sub-agent uses its own Telegram bot token from /app/.env. When main exits, sub-agents die with the bash shell, supervisor restarts.
Mobile Safari ITP clears localStorage after 7 days of inactivity, causing all API calls to return 401 (dashboardToken becomes empty string). Frontend (api.ts): - On every load, persist token to BOTH localStorage AND a 30-day cookie (claw_token; Secure; SameSite=Lax). Cookie is also read as final fallback so cleared localStorage or private browsing never breaks auth. - All fetch helpers now send Authorization: Bearer <token> header in addition to the existing ?token= query param. Backend (dashboard.ts): - API auth middleware now accepts the token from (in order): 1. ?token= query param (original, always present when token is known) 2. Authorization: Bearer header (new - sent by every SPA fetch) 3. claw_token cookie (new - set by SPA on first authenticated load) This means once a mobile browser visits with ?token= once, subsequent visits work indefinitely without re-injecting the token.
Static token entry page at /auth.html. On any api 401/403, the SPA hops there with a return param. Form verifies the token via /api/health and persists to localStorage + cookie. Token already cached → silently redirects back. Force re-entry with ?forceLogin=1.
Adds refreshing state to useFetch so pages can distinguish first-load (loading) from background revalidation. Button now disables, swaps copy to Refreshing..., and spins the RefreshCw icon (Tailwinds animate-spin) while a fetch is in flight.
Claude Code SDK with OAuth (Pro/Max subscription) only populates usage on the final result event, not on per-assistant-message events. Without a fallback, lastCallCacheRead and lastCallInputTokens stayed 0 so the token_usage table got context_tokens=0 every turn, breaking /convolife percent calculation and the dashboard usage view. Fall back to the result event aggregate when per-message events were empty.
SPA calls /api/tokens, /api/health, /api/memories without ever sending a chatId. Backend filtered by empty string, returning zeros even though the DB has 254 rows for the real chat. Default to ALLOWED_CHAT_ID so the page shows the primary authorized chats data when no chatId is explicitly provided.
Order intake forms have business_name often blank, literal "null", or stale. Pull the canonical name from /businessLocations resource (same source the Vendasta Partner Center UI uses) and override any order-form name with it. Also filter literal "null" / "undefined" / empty strings as not-a-name. Recovers ~47 previously-unnamed customers including Reesource Pest, Data Check Systems, ZAGG Phone Repair, NEATCap Medical, etc.
Adds optional full per-account customer breakdown (retail MRR, wholesale, margin) to the revenue endpoint. Default response unchanged — only top 5 to keep dashboard payload slim. ?full=1 returns the whole list for report generation.
Phase A+B of the QBO Class-based attribution build. - revenueByAccount now joins each AG-ID against the CRM list_records endpoint to attach a marketSlug (pwps = ImpactWorks, default = Rocket Local). Surfaces both byAccount[].marketSlug AND a pre-aggregated byMarket totals block. - CleanedRevenue now exposes a brands[] array with per-brand customerRetailMRR / wholesaleMonthly / grossMargin / marginPct / customerCount / retailShare. Internal accounts (Pest WebPros + Rocket Local internal) are excluded just like the top-line numbers. This is the data layer that powers the upcoming Mission Control tile, monthly Settlement Slip generator, and Telegram-approved QBO journal entry write.
Slim view (id, name, type, balance, active) used by the upcoming Settlement Slip generator to resolve the 5 QBO account IDs needed for the per-brand Vendasta journal entry.
…(Phases D+E)
Phase D — scripts/settlement-slip.mjs:
Pulls per-brand split from /api/vendasta/revenue, resolves the 5 QBO
account IDs from /api/qb/accounts, builds a balanced journal entry,
saves it as a pending slip in store/pending-slips.json, generates a
Google Doc, prints a Telegram-ready summary with a slip ID.
Phase E — connectors/quickbooks/server.mjs + scripts/post-settlement-slip.mjs:
- New qbo_create_journal_entry tool: POSTs JournalEntry to QBO with
balance validation (rejects unbalanced entries before sending).
- New qboPost helper alongside qboGet.
- post-settlement-slip.mjs: loads pending slip by ID (or latest), calls
qbo_create_journal_entry, stores QBO entry ID + link, updates slip
status to posted. Called by Nikki when Dante replies post slip <id>
in Telegram.
Books structure (no Classes, separate accounts per brand):
DR Bank
DR ImpactWorks - Vendasta Wholesale (COGS)
DR Rocket Local - Vendasta Wholesale (COGS)
CR Income: ImpactWorks
CR Income: Rocket Local
Tomorrows test fire is scheduled (task 59407a3d, June 9 9am EST). After it fires, Nikki deletes the one-shot task and the monthly recurring task (a1f38408, 5th of month at 9am EST) remains.
…nt cap Added the people-map.json entries Nikki uses to interpret incoming iMessages with proper context (family vs client vs friend). Roster: family (wife, son, stepson, daughter, stepdaughter, mother, father, mother-in-law, uncle, aunt, cousin), close circle (best friend since high school, mentor, ex-roommate + NEW founder), and clients (Reesource Pest, ZAGG, John Fenrich, Ralph Caparotti). Also bumped /recent cap from 200 to 5000 so future tagging passes see more of the message history.
…disambiguation - Auto-added 25 email handles from contacts.json to people-map for all previously-tagged contacts (Jennifer, Ralph, Emily, John, Jim, Bill Warner, Nicole, Jeffrey, Cam, etc.) - Disambiguated Vincent Sr (father, name Silvio Vincent Crescenzi) from Vincent Jr (son, same full name, goes by Vincent). - Re-tagged vincent@impactworks.com from father to son (its actually the sons work email). - Re-tagged dante+ai@impactworks.com from business line to self. - Added dante@impactworks.com + finance@impactworks.com as self.
src/people-resolver.ts:
- Loads relay/people-map.json (priority) + relay/contacts.json (gaps)
- resolvePerson(handle) returns { name, relationship?, org?, notes?, source }
- categorize(person) returns family|inner-circle|self|client|vendor|...
- mtime watcher reloads files on change (no restart needed)
- 78 manually-tagged entries + 2248 contact-only handles indexed
Wired into:
- email-data.ts buildBriefEmailBlock: senders now show
name (relationship) with a category badge (👪 ⭐ 💼 🔄)
- New /api/people/stats + /api/people/resolve?handle=...
admin endpoints for ad-hoc lookups
Smoke tested against 12 sample handles — all resolve to expected
category. Father/son disambiguation works (vincent@impactworks.com →
son, svc@cbmcpa.com → father).
…Inbox tile
Server enrichment: shapeRow() now resolves the sender via people-resolver
and attaches senderPerson { name, relationship, org, category } to every
EmailRow. Single in-memory cache lookup per row, no extra latency.
Frontend: InboxCard.tsx EmailRowView renders
👪 Audra Crescenzi (wife)
⭐ Jim Roberts (close friend / business community)
💼 Ralph Caparotti (client / business partner)
🔄 Dante Crescenzi (self)
instead of raw email addresses.
Badge legend:
👪 family · ⭐ inner circle · 💼 client · 🔄 self/business
🏷️ vendor · 🎓 professional · (none) other/unknown
People-resolver was returning 0 entries on Fly because the Dockerfile did not COPY the relay/ data files. The TS code was deployed but the data files were not. Also adding contacts.json to git (was created locally from vCard import but never committed). Repo is private — these are committed alongside all other personal data files already in store/ etc. After this deploy: /api/people/stats should return 78 peopleMapEntries + 2248 contactHandles
STOCKS: Stooq added a JavaScript proof-of-work browser-verification challenge to their CSV endpoint — every server-side fetch returns 404. Switched the quote fetch from Stooq to Twelvedata /quote endpoint. We were already using Twelvedata for chart history so this consolidates on one provider. Free tier (800/day) + 5min cache = safe headroom. AI NEWS: Each NewsItem now carries iconUrl (Googles s2/favicons proxy keyed to the publishers domain) and sourceDomain (extracted from the <source url=> attribute in the RSS feed). Frontend renders a 28px circular badge next to each story with the publisher logo, falling back to the source name initial if the favicon fails to load.
CONNECTOR: new plaid_get_holdings tool that calls /investments/holdings/get across every linked Plaid item, aggregating accounts + securities + holdings. Items without the investments product attached are skipped quietly via per-item error capture rather than throwing. LINK FLOW: plaid_create_link_token now requests both transactions AND investments products. Plaid auto-disables products an institution does not support, so this is safe for banks like Novo while unlocking brokerage support for Schwab / Vanguard / Stash / Robinhood / etc. DATA LAYER: src/investments-data.ts. Aggregates per-account current value + day change, top-10 holdings sorted by value with weight %, gross totals. 30-min cache on Fly volume. ENDPOINT: /api/investments (?force=1 bypasses cache). DASHBOARD: Investments tile rendered between cash-pulse and cash-pipeline. Shows hero portfolio value + day delta, per-institution list with mask + day change, top-5 holdings with ticker / value / weight. Empty state nudges user to /cash/connect to link their first brokerage. Section order bumped to v4 to add investments slot.
… brokerage CONNECTOR: plaid_get_holdings now resolves institution_name from Plaid when missing. Calls /institutions/get_by_id with the institution_id returned on the item, persists the name back to items.json so subsequent calls hit the cached value. Fixes accounts linked tonight that came through without an institution_name (Plaid Link didnt forward it). FRONTEND: Per-account list on the Investments tile now GROUPS by institution. Each group shows VANGUARD / STASH / etc. as the bold header with the brokerages subtotal, then the individual accounts indented underneath with their name + subtype + mask + value. Much easier to tell which accounts came from where.
…oint.sh helper
Lets Mac/Cowork sessions push a high-salience pinned session checkpoint
directly into Nikki's memory table on Fly. Closes the gap where the
local save-nikki-checkpoint.py only wrote to the Mac copy.
POST /api/memory/import-checkpoint
Auth: DASHBOARD_TOKEN (Bearer or ?token=)
Body: { summary, raw_text, chat_id?, agent_id?, entities?, topics?, importance? }
Defaults: chat_id=ALLOWED_CHAT_ID, agent_id=main, importance=0.95
Always source='session_checkpoint', pinned=1.
Root cause: all 5 agents (main + 4 sub) called initDatabase() simultaneously on container boot with no SQLite busy_timeout, causing SQLITE_BUSY crashes. Fixes: 1. db.ts: busy_timeout=30000 before journal_mode=WAL so SQLite retries on BUSY 2. docker-entrypoint.sh: WAL checkpoint at boot + main starts first, sub-agents after 5s delay + wait MAIN_PID instead of exec 3. scripts/settlement-slip.mjs: fix hostname + slim revenue endpoint
Completes the WAL-contention fix (55a6853). The entrypoint reorder + busy_timeout stopped the simultaneous-open crash; this closes the remaining hole in the single-instance lock itself. Before: on boot, acquireLock SIGTERM'd whatever PID was in the lock file then proceeded after a fixed 1s sleep — opening the DB even if the previous instance was still alive and holding the WAL (the contention window), and wasting that 1s on an already-dead PID after a crash. After: - isAlive(pid) probe (kill -0) distinguishes a live holder from a stale PID. - Live holder: SIGTERM, then poll until it actually exits (10s grace) before reclaiming, escalating to SIGKILL if it ignores SIGTERM. We never open the DB while the prior instance still holds it. - Stale/crashed PID: reclaimed instantly — the real crash-recovery path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move apiGet call from render body into useEffect with cleanup (prevents multiple concurrent fetches on parent re-renders and ensures state updates fire correctly) - Pass e.id (message ID) to the API instead of e.threadId — the gmail-cli read command requires a message ID, not a thread ID; for multi-message threads these differ and the old code would 500 - Keep threadId on the modal for the Gmail deep-link URL (correct) - Add cancellation flag so stale callbacks don't stomp state after the modal closes
1) Inbox modal showed blank email content because getThread() read j.from / j.bodyText but gmail-cli wraps the payload in j.message. Now reads from j.message.* and treats missing message as an error. 2) Latest brief header showed the Sun at night because it used the stored brief_kind from the most recent brief regardless of how old it was. Now uses the briefs kind ONLY if it matches the current time-of-day bucket, otherwise falls back to themeFromCurrentHour().
- Prefer bodyHtml over bodyText so newsletters and marketing emails render with their original layout and images (was showing the bland plain-text version even when HTML existed). - Add sanitizeEmailHtml() helper: strip script tags, event handlers, javascript: URLs, form/input elements. Cap at 2MB so a malicious large email cant lock up the browser. - Add scoped .email-html-body CSS in main.css to undo Tailwind preflight inside the email body: images max-width 100%, links use accent color, tables/lists/blockquotes/headings render with sensible defaults.
… memory Journal: - New SQLite table journal_entries (date PK, gratitude_1/2/3, great_today_1/2/3, affirmation, highlight_1/2/3, learned, completion timestamps) - 4 API endpoints: /api/journal/today, /journal/entry/:date GET+POST, /journal/recent - Daily rotating gratitude quote (30 quotes, deterministic by date) - Streak tracking (current + longest consecutive morning entries) - JournalCard component: compact morning gratitude entry on Founder Dashboard with sunrise gradient + quote + streak indicator - Journal full page at /journal: morning section (gratitude, what would make today great, daily affirmation) + evening section (highlights, what I learned), date navigation, sun/moon decorations - Whimsical aesthetic via Caveat (handwritten cursive) + Cormorant Garamond (italic serif), parchment tones, dashed dividers - Light + dark mode variants Brief mark feedback wiring: - /api/brief/:id/mark now writes a memory note when action is set - acted: low-importance memory tagging which brief drove activity - ignored: high-importance pinned memory so Nikki notices format may need rethink
…mode - Removed all [data-mode=dark] overrides on .journal-* selectors - Morning section: peach/apricot wash, sunrise hero, gold accents - Evening section: lavender/rose wash, dusk colors (purple text + caret), distinct from morning - Sun icon stays gold, moon icon goes lavender for visual identity - Used !important on the journal-specific properties so future theme tokens cant bleed in - Page background stays a consistent warm cream canvas under the cards
Tone: - Replaced bright peach/lavender with calm warm-linen + dusty-sage + dusty-mauve palette inspired by The Way app (Henry Shukman) - Single CSS token set as :root vars on .journal-card and .journal-page - Toned-down radial gradients, lower saturation Meditation: - Added meditation_minutes / meditation_sessions / meditation_last_at to journal_entries with backward-compat migration - POST /api/journal/meditation/:date increments todays sit count - MeditationSession modal: 5 duration chips (3/5/10/15/20), breath orb expanding 4s, holding 2s, contracting 4s, with cycling cue text - Soft sine-wave bell on session start + end via Web Audio API - Done-early button logs partial sit - 15 Henry Shukman / Thich Nhat Hanh / Zen quotes added to rotation Practice rings + gamification: - New PracticeRings component: three thin SVG progress rings (Gratitude / Sit / Reflect) - 28-day path-of-stones grid on the Journal page - Streak math expanded to journal, meditation, and practice tracks (practice = lenient combined) - Removed fire emoji in favor of italic Cormorant Garamond - New Meditation section between Morning and Evening on the Journal page
Two causes of 'can't access Nikki' after successful boot: 1. Telegram 409 crash loop: When the container restarts, the previous instance's 30-second getUpdates long-poll is still open. The new instance immediately tries to poll → 409 Conflict → GrammyError propagates to main().catch → process.exit(1) → restart → 409 again → hits Fly max_retries=10 → machine permanently stopped. Fix: wrap bot.start() in a retry loop. On 409, log a warning and wait 35s (> the 30s poll window) before retrying. Up to 6 retries (3.5 min total). Any other error still crashes as before. 2. Fly default restart policy stops machine after 10 failures: fly.toml had no [restart] block so Fly used 'on-failure / max=10'. Fix: add [restart] policy = 'always' so Fly keeps trying forever with exponential backoff, same as any production service should.
flyctl Config.restart is typed as []appconfig.Restart (a Go slice), so TOML requires array-of-tables [[restart]] not a plain [restart] object. Previous commit broke CI with: json: cannot unmarshal object into Go struct field Config.restart of type []appconfig.Restart
When Plaid credentials expire, the connector was hanging for 60-150s before throwing, blocking /api/founder and making the dashboard appear blank on refresh. Two fixes: 1. Add timeout: 10_000 to execFileAsync in plaidCall() — credentials errors now surface in ~10s instead of 60-150s. 2. readStaleCache() fallback — if the live call fails and there's any prior cache on disk (even expired), return it rather than an empty cash section. Stale data is better than a blank dashboard.
The pre-flight pipecat import check used a 10s timeout, but pipecat pulls in heavy ML/audio deps and can take 20-30s to import on a cold container. When the timeout fires, the code thought deps were missing and sent a Telegram message every time. Combined with the 409-conflict retry loop (triggered by a second Telegram poller), this produced periodic spam. - Bump timeout to 60s - Distinguish a real ModuleNotFoundError (worth alerting on) from a cold- import timeout (silent log + skip War Room this boot, next boot will likely succeed) - Only Telegram-spam on genuine missing deps
Connector extension (read-only): - vendasta_platform_list_campaigns - vendasta_platform_get_campaign - vendasta_platform_list_email_templates - vendasta_platform_list_automation_runs Resource paths are best-guess from naming conventions (marketingCampaigns, emailTemplates, automationRuns). Marked PROBING in the code — verify on first call and adjust if Platform API returns 404. Data layer (src/vendasta-campaigns.ts): - 10 min cache on Fly volume - Aggregates totalRecipients, emailsDelivered, weighted avg open rate - Counts published/ongoing/draft - Surfaces top 5 by emailsDelivered API: GET /api/vendasta/campaigns Founder Dashboard CampaignsCard: - Inserted between attention and real-mrr in the section order - Hero stats: reach, weighted open rate, recipients - Top 3 performing campaigns with delivered + open rate - Open-in-Vendasta link to partners.vendasta.com/marketing/campaigns/all - Section order bumped to v6
…ting Verified at developers.vendasta.com that the Partner Platform REST API exposes: Platform / Business / CRM / Advertising Intelligence / CalendarHero / Customer Voice / Local SEO / Reputation AI / Social Marketing / SCIM — but NOT Marketing Campaigns. The /marketing/campaigns/all UI uses Vendasta internal/private API that is not part of the documented partner integration surface. Changes: - Connector: list_campaigns / get_campaign / list_email_templates now return a documented unsupported_by_vendasta_public_api error instead of attempting 404 calls. list_automation_runs kept as a probe since /automations itself works. - Data layer: passes the unsupported state through cleanly. - Founder Dashboard: removed campaigns from DEFAULT_SECTION_ORDER (v7). The CampaignsCard component stays in the repo in case the API ever opens up. - Memory: vendasta-campaigns-api-limitation.md documents the dead-end so future sessions dont retry. Workaround if Dante asks again: infer campaign activity from the CRM activity feed via vendasta_list_records on activities.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a short "macOS setup notes" subsection in Step 1 of the README capturing the real issues a fresh Mac setup hits that aren't obvious from the current docs.
What's documented
npm i -gfails withEACCESon/usr/local/...— macOS ships an npm prefix owned by root. Docs the user-writable$HOME/.npm-globalworkaround so@anthropic-ai/claude-codeinstalls without sudo.say+ ffmpeg TTS fallback silently falls back to text without it. Addsbrew install ffmpeg.gsk_...values work; anorg_...identifier returnsHTTP 401 invalid_api_key..env, not the shell — pasting a key at the zsh prompt producescommand not found, easy to misread as a setup failure.claudelives in$HOME/.npm-global/bin).Also refreshes
package-lock.jsonto match a currentnpm install.Why
These were encountered during a real end-to-end setup on a clean Mac. Writing them in Step 1 up front saves newcomers from the same debug loop.
Testing
npm install,npm run build,npm run setupequivalent config generation, launchd install, andnpm run status— reports "All systems go."transcribeAudio()insrc/voice.ts(HTTP 200, correct transcript).synthesizeSpeech()(produces a valid OGG Opus buffer; round-trips back to text through Groq).Conversation
Generated with assistance from Warp (conversation: https://app.warp.dev/conversation/7f78a63a-b2fe-461c-b0e0-794ba9f23ae3)