Investigating 'apps blank while a bg task runs': measured serving latency
through a real scheduled bg-task run — 1-3ms on both loopbacks throughout,
so the server is not the bottleneck. Found and fixed the likely renderer-era
culprit plus made the failure observable:
- agent sync (apps:list, polled every 4s) rewrote each bundled agent's
task.yaml unconditionally — racing the bg runner's own patches mid-run and
spamming watcher events; now writes only when the definition changed
- AppFrame load watchdog: if the iframe hasn't loaded in 6s, show a visible
'taking too long' state with Retry instead of a silent blank pane
- main event-loop lag monitor: stalls >300ms are logged so future blank
reports can be tied to the offending work
- checkCapability choke point (D7): tools/llm/copilot access requires the
capability in the app's manifest; 403 capability_not_declared otherwise
- POST /_rowboat/tools/search|execute: Composio pass-through with the same
request shape as the builtin, typed error mapping
- POST /_rowboat/fetch: SSRF-guarded proxy (loopback/RFC1918/link-local/ULA/
*.localhost rejection re-checked per redirect hop, 5MB cap + truncated flag,
30s timeout, Host/Cookie stripped)
- POST /_rowboat/llm/generate: default-model plumbing, override validated
against the allowed set, token/concurrency caps, usage attributed
app_llm_generate/<folder>
- POST /_rowboat/copilot/run: headless run (bg-task tool profile, no shell),
app-attributed audit turn with origin context, 10-min timeout, 1-per-app cap
- registered M2 routes are POST-only (GETs are D17-exempt and must not reach them)
- bundled agents (§8): strict AppAgentDefinitionSchema, deterministic
app--<folder>--<name> slugs materialized disabled with sourceApp, manifest
removals deactivate, app delete removes owned tasks; sourceApp in summaries
- new use cases app_llm_generate / app_copilot_run in runs + analytics enums
macOS's resolver maps *.apps.localhost to ::1 only, so Electron's iframe
connects to [::1]:3210 while the server listened on 127.0.0.1 only —
connection refused, blank app, while external browsers succeeded via their
own IPv4 fallback. Listen on both loopback addresses (::1 best-effort for
IPv6-disabled machines).
- watch ~/.rowboat/apps: data.json changes push into open apps (shim
re-fetches, onData fires); installs/updates re-list the gallery live and
reload an open app once the write settles
- atomic installs (temp->rename) so an app opened mid-install never reads a
partial file
- serve app assets by direct disk read instead of net.fetch(file://) —
Chromium's network service could stall and blank every open app at once
New bridge commands: "model" lists the available models (same catalog as
the desktop picker), "model N" / "model <name>" sets a per-sender
override passed as agent.overrides.model on every turn, and
"model default" resets to the app default. In-memory scope: resets on
app restart, never sticks a session to a bad model.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds a transport-agnostic messaging bridge in @x/core that drives the
session/turn runtime from your phone: WhatsApp links as a companion
device via Baileys QR pairing (self-chat is the command channel),
Telegram long-polls the user's own bot token. Commands: list, resume N,
new, status, stop; anything else runs a turn in the current session and
replies with the final assistant text. Ask-human questions are relayed
and answerable from the phone.
- packages/core/src/channels/: bridge (commands, settle watcher,
ask-human relay), WhatsApp + Telegram transports, config repo, service
(lifecycle, status fan-out, QR rendering, current-transport reply
routing, dynamic baileys import)
- Settings → Mobile tab: enable toggles, QR pairing, bot token,
sender allowlists
- Strict sender authorization: WhatsApp self-chat + allowlisted numbers
(LID-aware), Telegram allowlisted chat IDs only
- Telegram offset persisted across restarts; generation-guarded
WhatsApp socket lifecycle; vitest coverage for the bridge
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Remove the "Mark as read" toggle for Everything else end-to-end (UI, IPC,
config, sync auto-read, markSectionRead) — it could bulk-mark backlog and
archived mail read on the server during cache-recovery syncs
- Sending an edited draft now reuses the draft's stored In-Reply-To/References
instead of self-referencing its own Message-ID (external clients thread
correctly); standalone drafts send as fresh messages
- Keep draft autosaves out of the sync pipeline: skip DRAFT history messages
(no more md/knowledge-event leaks, phantom "New email" notifications, or
per-autosave LLM reclassification) and mirror the draft body onto the cached
snapshot surgically instead of waking the sync loop
- Only recreate a Gmail draft when update fails with 404/410 — transient
errors no longer pile up duplicates
- Emptying a draft and closing now deletes it from Gmail (matches Gmail)
- Forward keydown out of message iframes so j/k/e/r etc. keep working after
clicking into an email body; restore expanded quotes across theme reloads
- Composer footer wraps on narrow panes: formatting toolbar + Discard move as
one unit instead of overflowing into each other
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Welcome step's "Use Next or your arrow keys" hint stays in the bubble
but is dropped from the voice clip (its tail kept synthesizing with
gibberish). Steps gain an optional voiceText override, honored by both
the clip generator and the live-TTS fallback; the generator also takes
step ids as args to re-roll individual clips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundle the 11 tour narration clips as renderer assets so the tour never
calls ElevenLabs (works offline and signed-out). useVoiceTTS gains
speakUrl() which plays a ready URL through the same queue/analyser path,
keeping lip-sync and cancellation intact. Clips are regenerated with
scripts/generate-tour-audio.mjs, which parses TOUR_STEPS from
product-tour.tsx and synthesizes via @x/core. Default TTS voice is now
s3TPKV1kjDlVtZbl4Ksh.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>