Commit graph

372 commits

Author SHA1 Message Date
Gagan
2bbb502661 chore(apps): drop unused STEPS array (lint) 2026-07-06 16:17:58 +05:30
Gagan
beaf6ebf69 fix(apps): bootstrap empty repos before the Git Data push
GitHub's Git Data API (blobs/trees/commits) returns 409 'Git Repository is
empty' on a repo with no commits — it cannot create the first commit. Ensure
main exists via a Contents API bootstrap commit (the generated README), then
chain the publish commit onto it.
2026-07-06 16:17:26 +05:30
Gagan
684ec0b390 fix(apps): device-flow diagnostics + bounded identity retries
Verified the wire calls are correct (both endpoints behave with this client
id from a plain node run), so instrument the runtime path instead of
guessing:

- log every poll outcome ([GitHubAuth] lines) at the token exchange, the
  identity fetch, and the IPC handler
- identity failures now retry at most 3 polls then fail loudly with the HTTP
  status — never silently degrade to 'pending' forever
- explicit User-Agent on all GitHub calls (defensive)
2026-07-06 16:12:53 +05:30
Gagan
0ca3606be5 fix(apps): device-flow poll stuck on 'Waiting for authorization'
GitHub's OAuth endpoints take form-encoded params; the JSON token request
produced an unrecognized error that fell through to 'pending' forever, so the
dialog never advanced after the user authorized.

- form-encode device/code and access_token requests
- unknown token-endpoint errors now fail loudly instead of spinning;
  incorrect_device_code maps to expired (restart offered)
- issued-token is remembered so a transient identity-fetch failure retries on
  the next poll instead of burning the consumed device code
- dialog catches hard poll failures and surfaces them
2026-07-06 15:54:06 +05:30
Gagan
35c781b8e4 feat(apps): M3 publisher — guided publish, updates, register-existing
- publisher (§11): resumable state machine persisted in .rowboat-publish.json
  (packaged → repo_created → source_pushed → release_created →
  assets_uploaded → registered → published); Git Data API single-commit
  source push with generated README/LICENSE/.gitignore when absent; both
  release assets uploaded (bundle + standalone manifest); fork+PR registry
  registration with rejected:<code> parsing and rename-retry surface
- publish update (§11.3): semver bump, package, push, release — no registry
- register existing release (§11.5) with client-side asset probe
- IPC: apps:publish/publishUpdate/registerExisting + apps:progress pushes
2026-07-06 15:08:36 +05:30
Gagan
6dea528fb4 feat(apps): M3 installer — catalog/URL installs, updates, rollback, uninstall
- installer (§12): streaming download with size cap, extraction guards
  (zip-slip, symlink entries, entry-count, uncompressed cap), bundle-identity
  check, D18 capability-mismatch check against the previewed manifest,
  defaults->data on first install, pinned per-file sha256, folder suffixing
- URL installs (§12.5): two-phase preview from the bundle's own manifest,
  10-min retained staging, GitHub-provenance detection for later updates
- update (§12.3): D18 scoped to the diff (new_capabilities), modified-file
  warning against pinned hashes, .previous/ swap with one-step rollback
- uninstall (§12.4) deletes app-owned bg-tasks; startup tmp cleanup
- IPC: apps:catalogIndex/Search/Detail, install, installFromUrl, uninstall,
  checkUpdate, update, rollback + analytics events
2026-07-06 15:02:00 +05:30
Gagan
d7af51c10b feat(apps): M3 groundwork — packager, GitHub device-flow auth, registry client
- env: GITHUB_OAUTH_CLIENT_ID (device flow enabled on the Rowboat OAuth app;
  overridable via ROWBOAT_GITHUB_CLIENT_ID)
- packager (§4.4): allowlist-only .rowboat-app ZIP (yazl), sorted entries,
  symlink skip, sha256
- github-auth (§10): device-code start/poll, identity fetch, token stored
  0600 with safeStorage encryption injected from main (core stays
  electron-free); githubAuth:* IPC + external open of the verification page
- registry client (§9.2): unauthenticated tarball index with 5-min cache and
  stale fallback, raw-record resolve, substring search, quota-free
  latestManifest via release-asset redirect with name-mismatch guard
- registry repo contents (docs/apps-registry): record JSON schema +
  validate-and-merge Action implementing §9.3 checks 1-7 with rejected:<code>
  comments and per-name concurrency
- fix: host-api copilot-run adapted to dev's ModelSelection {provider,model}
  (this is the same fix dev needs for Ramnique's packaging break)
- pnpm 11: blockExoticSubdeps=false (electron-forge has a git subdep)
2026-07-06 14:49:52 +05:30
gagan
b7d1019538
Merge pull request #665 from rowboatlabs/feature/apps-v1
Rowboat Apps V1 — M1 + M2 (spec implementation)
2026-07-06 14:18:11 +05:30
Ramnique Singh
1050614295
Merge pull request #674 from rowboatlabs/code-mode-fixes
Code mode fixes
2026-07-06 14:06:41 +05:30
Ramnique Singh
3bbed55735 feat(x): ephemeral CodeRunFeed for codex streaming + settle-time durable batch
The runs->turns migration broke codex live streaming in copilot chat: the
turns bridge's publish shim forwarded only tool-output-stream and silently
dropped code-run-event / code-run-permission-request (the latter would
deadlock a turn under policy 'ask'). An interim fix persisted every stream
event as durable tool_progress, but that wrote each text chunk to the turn
file — unsustainable.

Final architecture — live and durable paths split:

- Live (ephemeral, bypasses the turn runtime): code_agent_run broadcasts
  each ACP event on a new CodeRunFeed (core DI singleton), forwarded by
  main over a dedicated codeRun:events channel, buffered per toolCallId in
  a module-level renderer store and rendered by CodingRunBlock. The buffer
  survives session switches; nothing is persisted.

- Durable (one line per run): when the run settles (success, error, or
  cancel), code_agent_run publishes a single code-run-events-batch with
  the whole ordered timeline, consecutive same-role message chunks
  coalesced (display-lossless — the timeline concatenates them anyway).
  The turns bridge maps it to tool_progress {kind:'code-run-events'};
  turn-view derives the replay timeline from it, so reloads keep history.

- Permissions stay durable per-ask (request + resolved marker): the
  renderer overlay resets on session switch, so an ephemeral-only ask
  would strand a blocked turn with no card to answer. Pending = requests
  minus resolutions (handles concurrent asks), cleared on tool result.

The legacy code-section path (runs bus per-event) is untouched; its
per-event ctx.publish remains and is a no-op under the turns shim.

Also: repaired the two code_agent_run tests broken by the earlier cwd
existence check (they used a nonexistent /repo), and added coverage for
feed broadcast, batch coalescing, partial-batch-on-failure, bridge
durability routing, and pending-permission derivation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:04:33 +05:30
Arjun
fd49334c33 move local-model concurrency to a turn-level defer lock
Review feedback: drop the model-layer scheduler (per-call-site
interactive/classifier/background priorities, local-provider hostname
heuristic) in favor of app-level orchestration:

- deferBackgroundTasks flag in models.json, surfaced as a settings
  toggle; auto-enabled once (UI logic) when the user connects Ollama
- ChatActivity counter marked by both chat runtimes (sessions layer and
  legacy AgentRuntime.trigger)
- startWhenPossible/runWhenPossible wrappers around the headless agent
  runner; all background invocations (knowledge pipeline, live notes,
  background tasks, scheduled + prebuilt agents) go through them and
  wait for chat-idle when the flag is set

createLanguageModel keeps only the Ollama context-window middleware;
the LM Studio capability probe now keys off the provider flavor instead
of hostname sniffing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:38:39 +05:30
Arjun
c1fd4e6221 allow byok even when logged in 2026-07-06 13:32:13 +05:30
Arjun
0b55dc3300 fix ollama model effort level 2026-07-06 13:31:13 +05:30
Arjun
799d7584b8 make local models work well 2026-07-06 13:31:13 +05:30
Ramnique Singh
7c06af04a7 fix(x): unblock codex code-mode (bad cwd + revoked engine cert)
code_agent_run failed with a misleading "spawn <Electron> ENOENT" that
read as "Codex isn't installed". Two stacked causes:

1. cwd wasn't expanded/validated: a `~` or non-existent path was passed
   straight to child_process.spawn, which reports ENOENT against the
   command (Electron) rather than the missing directory. Now expandHome +
   resolve + existence-check with a clear error.

2. Apple revoked the signing cert on the pinned @openai/codex@0.128.0, so
   macOS Gatekeeper trashed the binary on launch and the ACP adapter died
   mid-handshake. Bump the ACP stack off the revoked build:
   codex-acp 0.0.44 -> 1.1.0, claude-agent-acp 0.39 -> 0.55, sdk 0.22 ->
   1.1, regen engine-manifest (codex 0.142.5 / claude 0.3.198). Removed the
   two now-obsolete patches (contextCompaction upstreamed; the codex
   windowsHide patch targeted bin/codex.js, which we bypass via CODEX_PATH).

Bump fallout handled:
- client.ts setModel: sdk 1.x dropped unstable_setSessionModel; model is now
  a config option -> setSessionConfigOption({configId:'model'}).
- engine-provisioner: codex 0.142 moved its binary (codex/ -> bin/) and rg
  (path/ -> codex-path/); probe both layouts.
- pnpm override vscode-jsonrpc to 8.2.0: codex-acp 1.1 pulls jsonrpc 9.x
  whose restrictive exports break langium's deep import in the renderer
  (blank screen). codex-acp is the only 9.x consumer and works on 8.x
  (handshake verified), so pin the workspace to 8.x.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 12:57:02 +05:30
Ramnique Singh
4a2fe30153 fix(x): surface coding tool errors 2026-07-06 10:56:23 +05:30
Arjun
adb8b16a3c Add in-call mic mute: pauses all input (mic audio + frame capture)
A mute button on both call surfaces (full-screen call and floating
popout) that pauses everything going to the assistant while keeping the
call alive — for stepping away to talk to someone in the room without
ending and restarting the call.

- Mic audio stops reaching Deepgram (user mute OR'd into the existing
  thinking/speaking setPaused; KeepAlives keep the socket warm so
  unmute is instant)
- Camera/screen frames stop being sampled (new setCapturePaused in
  useVideoMode); collectFrames() returns nothing while muted, so typed
  messages during a mute carry no frames either
- Devices stay acquired for instant resume; mute resets at call
  start/end; assistant output is unaffected (Stop handles that)
- Honest UI: "Muted" status chip replaces the green "Listening" pulse,
  muted badge on the user tile, pill's share badge flips to "Sharing
  paused"; new toggle-mic popout action + micMuted in popout state

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 00:28:34 +05:30
Arjun
b493baf689 use native gmail in copilot 2026-07-05 22:44:06 +05:30
arkml
6b2120d376
Merge pull request #666 from rowboatlabs/graph2
Improvements to graph and email labeling
2026-07-05 11:38:31 +05:30
Arjun
0d14220239 labeling improvements 2026-07-05 11:09:18 +05:30
Arjun
c88a45ee7e fix issue with sent emails showing up 2026-07-05 03:14:45 +05:30
Arjun
9d31d25046 learns email importance from preference actions 2026-07-05 02:36:01 +05:30
Arjun
258f157b39 improvements to graph building 2026-07-05 02:10:46 +05:30
Gagan
dc28b0b868 merge dev into feature/apps-v1
Union resolutions — dev's changes kept, apps-v1 additions appended:
- use_case.ts: dev's meeting_prep + our app_llm_generate/app_copilot_run
- ipc.ts: dev's HttpAuthRequestSchema import + our AppSummarySchema
- shared/index.ts: dev's channels export + our rowboatApp export
- builtin-tools app-navigation: dev's read-view/open-item actions + our open-app
- chat-conversation: dev's read-view/open-item labels + our open-app labels
- sidebar: dev's nav-workspaces tour id + our Apps entry
- App.tsx: dev's open-item handler + our open-app handler
2026-07-05 01:08:35 +05:30
Gagan
7c9fe7214d fix(apps): stop task.yaml rewrite races + blank-load watchdog + loop-lag monitor
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
2026-07-05 00:45:30 +05:30
Gagan
810fd2da1a feat(apps): detail panel, skill trigger, analytics, real apps:get
- app detail panel (toolbar Info toggle): manifest info, capabilities,
  provenance/publish state, bundled agents with enable toggles + run-now,
  rollback affordance, README
- copilot instructions: apps-skill trigger line (build/make/create an app →
  load the apps skill first)
- analytics: app_created / app_deleted (main), app_opened (renderer)
- apps:get returns real readme + rollbackAvailable (.previous/ check)
2026-07-05 00:17:55 +05:30
Gagan
3e34cbe1af feat(apps): M2 host integration — tools, fetch proxy, LLM, copilot, bundled agents
- 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
2026-07-04 18:16:43 +05:30
Gagan
c81739f9f7 fix(apps): bind IPv6 loopback too — blank apps in-app
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).
2026-07-04 17:30:55 +05:30
Gagan
7f15f67273 feat(apps): Rowboat Apps M1 — per-app origins, Host API core, apps skill
Implements M1 of the Apps V1 spec, replacing the mini-apps prototype:

- rowboat-app.json manifest + AppSummary/install/publish/registry schemas;
  sourceApp on background tasks
- apps server on 127.0.0.1:3210 (absorbs local-sites): Host-header routing to
  <slug>.apps.localhost origins, 421 rebinding guard, static from dist/ with
  entry/SPA fallback + realpath confinement, control host /health, chokidar
  watcher + per-app SSE with dist/data areas, embed-opt-in autosize bootstrap
  and cancelable rowboat:data-change event
- Host API: D17 anti-CSRF (X-Rowboat-App + strict Origin on non-GET),
  GET /_rowboat/app (manifest + theme), /_rowboat/events, data API under
  data/ with atomic writes and dataContracts enforcement
- indexer: list (ok/invalid surfaced) / create scaffold / delete
- IPC: apps:list/get/create/delete/setTheme; renderer reports theme live
- renderer: Apps home (My apps grid + catalog placeholder, server banner,
  invalid/kind badges) and full-height app view on the app origin
- apps copilot skill (replaces build-mini-app); app-set-data builtin replaces
  mini-app-install/set-data; deleted the app://miniapp protocol, postMessage
  bridge, mini-apps handler/channels, and local-sites
2026-07-04 17:12:54 +05:30
Gagan
2c038fe518 feat(mini-apps): live data refresh + install reliability
- 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
2026-07-04 15:59:13 +05:30
arkml
51c289ba36
Merge pull request #663 from rowboatlabs/video1
Video mode with screenshare and app navigation
2026-07-04 14:27:18 +05:30
Arjun
82abfe9a04 add better app navigation on call 2026-07-04 14:20:13 +05:30
Arjun
5b2ecc6720 added stop button in video call 2026-07-04 13:24:32 +05:30
Arjun
af24596591 phase 1 improvements to reduce latency: smart endpointing, streaming tts, early clause speech, ack 2026-07-04 12:51:06 +05:30
Arjun
e9ee8fd975 popout and full screen work as expected 2026-07-04 12:06:32 +05:30
Arjun
d2501c4f4d video mode popout over the app 2026-07-04 00:25:55 +05:30
Arjun
1edc1b17f1 screen share pop out when I switch to a different app 2026-07-04 00:09:46 +05:30
Arjun
48a3ff6a36 added screen share 2026-07-03 23:47:16 +05:30
Arjun
6f901095ff initial commit of video mode 2026-07-03 22:29:10 +05:30
Arjun
930e377f34 feat(channels): model selection from WhatsApp/Telegram
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>
2026-07-03 20:23:04 +05:30
Arjun
c80a09daba feat(channels): mobile bridge — chat with Rowboat from WhatsApp/Telegram
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>
2026-07-03 20:17:31 +05:30
Arjun
fc9a76e1cb add whatsapp and telegram support 2026-07-03 19:25:06 +05:30
arkml
3ba94402d3
Merge pull request #650 from rowboatlabs/mail-enhancements
feat(email): drafts, search, read-state controls & Superhuman-style shortcuts
2026-07-03 01:12:40 +05:30
Arjun
080b8625c6 fix(email): draft lifecycle, keyboard nav, and composer layout fixes from review
- 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>
2026-07-03 01:08:55 +05:30
Arjun
b028e56e44 ship pre-recorded tour narration + new default voice
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>
2026-07-02 22:20:11 +05:30
Ramnique Singh
17f18a35a1 feat(migration): port legacy runs into the turn/session runtime at startup
One-time startup migration that scans ~/.rowboat/runs/*.jsonl and converts
each legacy run into the new event-sourced runtime, so existing chats and
agent history are visible and continuable after the runtime rewrite.

Mapping (a run is one whole conversation; turn boundaries are user messages):
- copilot_chat run  -> 1 session (sessionId = original runId) + one turn per
                       user message (turn ids <runId>-tNNN).
- every other run   -> a single standalone turn whose id IS the original runId,
                       so live-note (lastRunId) and background-task (runs.log)
                       history views resolve it via sessions:getTurn with no
                       renderer change.
- code_session runs are skipped (Code mode still uses the old runtime).

- convert.ts: pure convertRun(); synthesizes a reduceTurn/reduceSession-legal
  event log (exact request refs, permission replay, denials -> runtime isError
  results, reasoning parts preserved) and validates via the reducers before
  returning.
- migrate.ts: defensive IO runner. Successful runs are moved to runs-archive/
  (that move is the idempotency guard); failures are left in place (still
  served by the runs:fetch fallback) and retried next launch. Per-run
  try/catch never blocks boot. Writes config/runs-migration.json.
- main.ts: runs before sessions.initialize() and logs an [runs-migration]
  summary (N turns across M sessions, skipped/failed counts).
- Tests: 13 cases over 4 real (redacted) run fixtures — conversion, session
  chaining, deny->error, reasoning preservation, transcript fidelity,
  archive-on-success, skip, quarantine, idempotency, and read-back through the
  real FSTurnRepo/FSSessionRepo.

Dry-run on real data: 19 runs -> 3 sessions + 21 turns, 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:56:04 +05:30
Arjun
7f3987094f fix browser issues 2026-07-02 19:39:21 +05:30
Ramnique Singh
0e316d1273 refactor(x/core): inject headless agent dependencies 2026-07-02 15:18:30 +05:30
Ramnique Singh
627451cd94 docs(x): stage 7 resolved — document the code-mode runs carve-out
Decision: defer code-mode unification; no deletions. The legacy runs
runtime (runs/, AgentRuntime/streamAgent, runs:* IPC, App.tsx legacy
tab state) remains solely for code-mode sessions. AGENTS.md gets the
authoritative carve-out section: what stays and why, the no-new-callers
rule (headless work uses agents/headless.ts), the temporary fallbacks
that die with unification, and the scoped future project (rowboat-mode
code prompts as composition turns; direct-mode ACP streams as a
delegated turn kind). Turn-runtime design doc status updated to
implemented-and-live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:20:44 +05:30
Ramnique Singh
fa6943e905 feat(x): llm_usage analytics for turn model calls + voice-output parity
Analytics: the new runtime emitted usage into durable events but never
reported it to PostHog (only the permission classifier self-reported).
New IUsageReporter seam on the turn loop — invoked once per completed
model call, after the durable append, failure-isolated. The real bridge
emits the same llm_usage event as the old loop; useCase/subUseCase come
from the AsyncLocalStorage context the stage-6 headless runners already
establish via withUseCase, defaulting to copilot_chat for UI-driven
session turns (matching the old createRun default). Capturing at the
loop covers ALL turns — the session bus never sees headless ones.

Voice: the new renderer path rendered <voice> tags literally and never
fired TTS. The live overlay now extracts completed <voice> blocks
(robust to tags split across deltas) into chatState.voiceSegments;
App speaks new segments via the existing useVoiceTTS when voice output
is on, skipping segments streamed before a session became active. Tags
are stripped from both the streaming message and persisted assistant
messages, mirroring the legacy display-time strip.

Tests: usage report assertion in the runtime suite; four voice tests
(split-delta extraction, per-call scan reset, state stripping+segments,
persisted-message stripping).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:20:44 +05:30