Circe app review

Dead code, conflicting behavior, and incomplete integrations after the rebrand. Reviewed 13 September 2026.

10 actionable · 11 total+0 −0 source editsmain 69e66477f0round 1

Changes required

0/5

5 major findings, 5 minor findings, and 1 integration note. Score is the review rubric’s readiness total, not confidence in every claim.

The pull merged cleanly into feat/circe-rebrand at 69e66477f0. Its tracked tree matches origin/main. This review includes existing issues across the Circe layer.

Walkthrough
  • Preserve old installs with a new forward migration before further branding cleanup.
  • Unify semantic-host, Conversations-project, and provider-account selection around existing typed identities.
  • Make live speech own cancellation and current context, then restore the report mute control.
  • Remove confirmed fx, Android-recognizer and clarification leftovers only after preserving their still-used pieces.
Evidence and limits

384 existing tests passed across 19 focused files. Both Circe packages typecheck. Four new regression tests fail: old-database upgrade, Controller semantic selection, duplicate Conversations identity, and report cancellation before live startup.

Download the four regression tests as a patch. They were removed from the working tree after capture. The patch reproduces findings; it does not fix them.

Snapshot audit of Circe ownership at origin/main 69e66477f0. Inherited T3 provider, session, Git, terminal, persistence-engine and coding-UI internals were treated as dependencies. This is not a line-by-line certification of every source file. No app browser, simulator, microphone, live install, or real provider was used. No Circe source fixes, PR, or push were made.

The first broad test invocation also discovered nested worktree copies of a packaging test. That run was discarded; the reported 19-file result explicitly excludes .worktrees.

Coverage

Core and shared runtime. Command grounding, clarification, task identity, node choice, catalog readiness, provider selection, reverse transitions. Focused core and runtime tests plus both package typechecks.

Server composition. Preset enforcement, controller interpretation and execution, Conversations identity, follow-up ownership, presentation fanout, push and voice contracts, desktop-use boundary, and migration upgrade behavior.

Web and desktop. Voice owner, report lifecycle, interruption and reconnect, session context, preferences, lazy composition, tray/global-shortcut dynamic imports, and release resources.

Mobile. Circe node selection, retained focus and reconnect helpers, native ASR consumers, platform variants, and Expo module reachability. No simulator or device verification.

Packaging and relay. fx staging and native dependencies, Headless packaging, shipped SQL migrations, relay voice handlers and node integration, deployment configuration. No full release build or remote deployment of Circe.

Dead-code scan: what to keep
  • The ordinary Knip pass reported one unused declaration file, two dependencies, and 157 exported symbols. Those numbers are candidates, not deletion counts.
  • uiohook-napi is a false positive: DesktopCirceShell.ts dynamically imports it by name. It owns the native shortcut.
  • Keep the shared fast-supervisor prompt, credential-format compatibility helpers, internally used constants, and deliberate test seams.
  • Keep shipped historical migrations. “No live consumer” is not permission to erase database history.
  • Production Knip marks development and release scripts unused because production entry selection omits those tasks. Release-tool callers were checked separately.
  • @circe/client-runtime in the desktop package and scripts/verify-windows-setup.d.mts are small cleanup candidates. The declaration may support tooling; verify type-resolution intent before deleting it.
  • Browser speech is live fallback behavior, not dead code. Its presence conflicts with the live-only architecture wording and belongs in the voice-owner cleanup.
Source references (unchanged)
Issues table
Prompt for all actionable findings · Ctrl+`
Complete fix handoff
Prompt to rerun · Ctrl+Space
Incremental review

Review comments

5 high-priority defects · 5 smaller repairs and cleanups · 1 staged integration note

apps/server/src/persistence/Migrations.ts:137-154

Add a forward migration for installed Jarvis databases

P1 logic data-integrity actionable

The rebrand changes historical migration SQL to create circe_* tables but keeps the already-shipped IDs. runMigrations runs only IDs above the database ledger. An installation already through migration 65 therefore runs no rename and retains jarvis_project_aliases, jarvis_task_desks, jarvis_follow_up_queue, and jarvis_push_registrations while the current services query circe_*.

The in-memory upgrade regression recreates the old table names at the existing ledger position, preserves an alias, then invokes the current migrator. Reading that alias fails with “no such table: circe_project_aliases”. Fresh database tests pass because they execute the rewritten migrations. This affects all presets that reuse an existing state directory; it does not delete the old data, but makes it inaccessible to the new services.

Reproduced: circeUpgrade.audit.test.ts fails against an in-memory database. No live data was read or written.

Repair direction. Preserve shipped migration history and add a new migration after 65 that handles old and fresh schemas transactionally. Audit persisted Circe payload names as part of the same upgrade, and preserve aliases, task desks, queue identities, and push registrations.

Current source
 137   [41, "CirceTaskDesks", Migration0041],
 138   [42, "CirceTaskDeskAttention", Migration0042],
 139   [43, "CirceTaskDeskClarification", Migration0043],
 140   [44, "CirceProjectClarification", Migration0044],
 141   [45, "CirceProjectAliases", Migration0045],
 142   [46, "CirceReportOutbox", Migration0046],
 143   [47, "AuthSessionClientConnection", Migration0047],
 144   [48, "ProjectionThreadLinkedPullRequest", Migration0048],
 145   [49, "ProjectionThreadsUnsettledAt", Migration0049],
 146   [50, "CirceWorkStartedCandidates", Migration0050],
 147   [51, "CirceFollowUpQueue", Migration0051],
 148   [52, "CirceProjectAliasCurrentState", Migration0052],
 149   [53, "CirceTaskDeskCurrentState", Migration0053],
 150   [54, "CircePresentation", Migration0054],
Open source at reviewed commit

Related: apps/server/src/persistence/Migrations/041_CirceTaskDesks.ts · apps/server/src/circe/Layers/CirceProjectLexicon.ts

Prompt for AI agents
Finding f1
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/server/src/persistence/Migrations.ts` around lines 137-154, The rebrand changes historical migration SQL to create circe_* tables but keeps the already-shipped IDs. runMigrations runs only IDs above the database ledger. An installation already through migration 65 therefore runs no rename and retains jarvis_project_aliases, jarvis_task_desks, jarvis_follow_up_queue, and jarvis_push_registrations while the current services query circe_*. Preserve shipped migration history and add a new migration after 65 that handles old and fresh schemas transactionally. Audit persisted Circe payload names as part of the same upgrade, and preserve aliases, task desks, queue identities, and push registrations. Reproduced: circeUpgrade.audit.test.ts fails against an in-memory database. No live data was read or written. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.
packages/circe-client-runtime/src/circe/mesh.ts:356-366

Select a node that can actually run semantic interpretation

P1 logic correctness actionable

selectCirceSemanticNode filters only reachability, then chooses the ambient node or first online entry. It ignores the advertised execution capability. The Circe RPC handler explicitly rejects circe.interpret on Controller nodes.

With an online Controller followed by an online Headless execution node, the regression selects the Controller. Web commands that need semantic interpretation and mobile submissions can fail even while a usable execution node is connected. Registry ordering should not decide whether the main product preset can direct work.

Reproduced: expected node-laptop (Headless), received node-desktop (Controller). Server rejection is at CirceWsRpc.ts lines 345-350.

Repair direction. Choose semantic nodes using the advertised capabilities and catalog readiness before applying the existing ambient preference. Keep execution targets separate from the interpretation host, and return a typed unavailable result when no eligible host exists.

Current source
 356 export function selectCirceSemanticNode(
 357   catalog: CirceMeshCatalog,
 358   ambientNodeId?: EnvironmentId,
 359 ): CirceMeshNode | undefined {
 360   const online = catalog.nodes.filter((node) => node.reachability === "online");
 361   if (online.length === 0) return undefined;
 362   if (ambientNodeId !== undefined) {
 363     const ambient = online.find((node) => node.nodeId === ambientNodeId);
 364     if (ambient !== undefined) return ambient;
 365   }
 366   return online[0];
Open source at reviewed commit

Related: apps/server/src/circe/Layers/CirceWsRpc.ts · apps/web/src/components/circe/CirceVoiceRuntime.tsx · apps/mobile/src/features/circe/CirceMobileProvider.tsx

Prompt for AI agents
Finding f2
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `packages/circe-client-runtime/src/circe/mesh.ts` around lines 356-366, selectCirceSemanticNode filters only reachability, then chooses the ambient node or first online entry. It ignores the advertised execution capability. The Circe RPC handler explicitly rejects circe.interpret on Controller nodes. Choose semantic nodes using the advertised capabilities and catalog readiness before applying the existing ambient preference. Keep execution targets separate from the interpretation host, and return a typed unavailable result when no eligible host exists. Reproduced: expected node-laptop (Headless), received node-desktop (Controller). Server rejection is at CirceWsRpc.ts lines 345-350. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.
apps/server/src/circe/Layers/CirceController.ts:1257-1264

Resolve the Conversations project by workspace identity

P1 logic correctness actionable

General-question execution searches the shell catalog for the first project titled Conversations. Startup creates the managed project by its canonical workspace root, so title matching is a different identity rule. A user project may have the same title, and the managed project may be renamed.

The regression puts a user-owned Conversations project before the managed Conversations project. The controller starts the provider thread in decoy-conversations instead of managed-conversations. This puts general-question history and tool execution in the wrong workspace. The existing resolveCirceConversationsProjectId helper already resolves by root, but has no production caller.

Reproduced through CirceController.execute: status started, projectId decoy-conversations instead of managed-conversations.

Repair direction. Use the managed workspace identity from conversationsProject.ts when selecting the destination. Fail or recover explicitly when that project is unavailable; do not retain an ambient coding project silently. Cover duplicate titles and renamed managed projects.

Current source
1257           const conversationsShell = yield* projections.getShellSnapshot();
1258           const conversations = conversationsShell.projects.find(
1259             (candidate) => candidate.title === CIRCE_CONVERSATIONS_PROJECT_TITLE,
1260           );
1261           if (conversations !== undefined) {
1262             interpretation = {
1263               ...interpretation,
1264               command: { ...interpretation.command, projectId: conversations.id },
Open source at reviewed commit

Related: apps/server/src/circe/conversationsProject.ts · apps/server/src/serverRuntimeStartup.ts

Prompt for AI agents
Finding f3
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/server/src/circe/Layers/CirceController.ts` around lines 1257-1264, General-question execution searches the shell catalog for the first project titled Conversations. Startup creates the managed project by its canonical workspace root, so title matching is a different identity rule. A user project may have the same title, and the managed project may be renamed. Use the managed workspace identity from conversationsProject.ts when selecting the destination. Fail or recover explicitly when that project is unavailable; do not retain an ambient coding project silently. Cover duplicate titles and renamed managed projects. Reproduced through CirceController.execute: status started, projectId decoy-conversations instead of managed-conversations. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.
apps/server/src/circe/Layers/CirceController.ts:482-496

Preserve provider-instance credentials in direct supervision

P1 logic security actionable

The direct supervisor is selected by driver family after resolving a provider instance, but receives only the prompt, and optionally the model. Its credential loader independently reads process-wide overrides or default home-directory credentials. The chosen instance ID and its configuration never reach that loader.

For a work Codex instance backed by a custom home and a personal login in ~/.codex, Circe can send the work utterance and catalog context to the personal account. OpenCode and Grok use the same family-level approach. Checking that the driver is Codex does not prove that it is the selected Codex account. Existing tests check family separation, not separation between two instances of the same driver.

Source trace: Codex auth candidates at CirceCodexSupervisor.ts lines 93-106; OpenCode uses ~/.local/share/opencode/auth.json. No real credential files or external provider APIs were accessed.

Repair direction. Use the ordinary selected-instance adapter for supervision unless a public contract can establish that the direct path belongs to that exact instance. Do not infer credentials from the family or add instance-specific branches inside T3 providers. Add same-driver, different-account regression coverage.

Current source
 482           const activeDriver = resolveCirceActiveDriver(plan, providers);
 483           if (activeDriver === "codex") {
 484             const codexProposal = yield* tryCodexSupervisor(fastPrompt);
 485             if (codexProposal !== null) {
 486               return interpretCirceCommand(input, prepared, codexProposal);
 487             }
 488           }
 489           if (activeDriver === "opencode") {
 490             const opencodeProposal = yield* tryOpencodeSupervisor(fastPrompt, plan?.provider.model);
 491             if (opencodeProposal !== null) {
 492               return interpretCirceCommand(input, prepared, opencodeProposal);
 493             }
 494           }
 495           if (activeDriver === "grok") {
Open source at reviewed commit

Related: apps/server/src/circe/Layers/CirceCodexSupervisor.ts · apps/server/src/circe/Layers/CirceOpencodeSupervisor.ts · apps/server/src/circe/Layers/CirceGrokSupervisor.ts · packages/contracts/src/providerInstance.ts

Prompt for AI agents
Finding f4
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/server/src/circe/Layers/CirceController.ts` around lines 482-496, The direct supervisor is selected by driver family after resolving a provider instance, but receives only the prompt, and optionally the model. Its credential loader independently reads process-wide overrides or default home-directory credentials. The chosen instance ID and its configuration never reach that loader. Use the ordinary selected-instance adapter for supervision unless a public contract can establish that the direct path belongs to that exact instance. Do not infer credentials from the family or add instance-specific branches inside T3 providers. Add same-driver, different-account regression coverage. Source trace: Codex auth candidates at CirceCodexSupervisor.ts lines 93-106; OpenCode uses ~/.local/share/opencode/auth.json. No real credential files or external provider APIs were accessed. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.
apps/web/src/components/circe/CirceVoiceReporter.tsx:36-46

Keep report identity through live speech cancellation

P1 logic correctness actionable

The live branch converts a presentation to plain text, calls speak, and immediately reports played. The playback queue loses ownership even when the live session is still connecting. Its cancel callback only cancels browser speech; it cannot remove the live controller’s pending append.

The composed regression uses the actual speakPresentation function and live controller, enqueues a report, clears the report queue as disconnect does, then delivers session.started. The supposedly cleared report is still sent as session.commentary.append. The same loss of identity prevents terminal events from retracting obsolete approval prompts. Successful queuing also cannot establish that audio played.

Reproduced: “Obsolete report from disconnected node” is sent after queue.clear(). Regression patch includes the real reporter-to-controller composition.

Repair direction. Carry presentation and turn identity into the one live speech owner and give it bounded cancellation and honest local outcome semantics. Route disconnect, new-input interruption, terminal supersession, and teardown through that owner. Keep this ephemeral; do not add a durable delivery ledger.

Current source
  36   const liveSink = getCirceLiveVoiceSink();
  37   if (liveSink !== null) {
  38     liveSink.speak(text);
  39     return Promise.resolve({ status: "played" });
  40   }
  41   // No session is live. On a node with a live voice key, open a muted
  42   // announcement session so the finished work still speaks without leaving
  43   // the voice channel open while the task ran.
  44   if (getCirceLiveVoiceEnabled()) {
  45     requestCirceLiveVoiceAnnouncement(text);
  46     return Promise.resolve({ status: "played" });
Open source at reviewed commit

Related: apps/web/src/components/circe/CirceLiveVoice.logic.ts · apps/web/src/components/circe/CirceLiveVoice.bridge.ts · apps/web/src/components/circe/CirceVoiceRuntime.tsx

Prompt for AI agents
Finding f5
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/web/src/components/circe/CirceVoiceReporter.tsx` around lines 36-46, The live branch converts a presentation to plain text, calls speak, and immediately reports played. The playback queue loses ownership even when the live session is still connecting. Its cancel callback only cancels browser speech; it cannot remove the live controller’s pending append. Carry presentation and turn identity into the one live speech owner and give it bounded cancellation and honest local outcome semantics. Route disconnect, new-input interruption, terminal supersession, and teardown through that owner. Keep this ephemeral; do not add a durable delivery ledger. Reproduced: “Obsolete report from disconnected node” is sent after queue.clear(). Regression patch includes the real reporter-to-controller composition. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.
apps/web/src/components/circe/CirceLiveVoice.logic.ts:910-916

Refresh app facts used by an ongoing live conversation

P2 logic correctness actionable

The context callback is evaluated once during session startup. It includes current project, current task, running-task count, provider names, and default model. No subsequent catalog, target, or settings update sends replacement context to the live model.

The server instructions tell the model to answer status and current-model questions directly from that reference data. If the user switches tasks, a task starts on another device, or the default changes during the session, those answers can describe the startup snapshot for up to the ten-minute session limit. A completion report does not refresh the whole catalog.

Verified call sites: options.context is read only at startup; CirceLiveVoiceRuntime.tsx supplies its snapshot; CirceLiveVoice.ts directs overview answers to reference data.

Repair direction. Either supply bounded event-driven context updates through the live owner or delegate questions requiring current state to the existing deterministic backend. Do not restart the voice session on every UI update or treat startup data as current.

Current source
 910       if (readStatus() !== "requesting") return;
 911       setStatus("connecting");
 912       const context = options.context?.()?.trim();
 913       const started = await cancellable(
 914         options.start({
 915           sdpOffer: nextPeer.localDescription?.sdp ?? offer.sdp,
 916           ...(context === undefined || context.length === 0 ? {} : { context }),
Open source at reviewed commit

Related: apps/web/src/components/circe/CirceLiveVoiceRuntime.tsx · apps/server/src/circe/Services/CirceLiveVoice.ts

Prompt for AI agents
Finding f6
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/web/src/components/circe/CirceLiveVoice.logic.ts` around lines 910-916, The context callback is evaluated once during session startup. It includes current project, current task, running-task count, provider names, and default model. No subsequent catalog, target, or settings update sends replacement context to the live model. Either supply bounded event-driven context updates through the live owner or delegate questions requiring current state to the existing deterministic backend. Do not restart the voice session on every UI update or treat startup data as current. Verified call sites: options.context is read only at startup; CirceLiveVoiceRuntime.tsx supplies its snapshot; CirceLiveVoice.ts directs overview answers to reference data. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.
apps/web/src/circePreferences.ts:4-10

Restore a reachable control for muting voice reports

P2 logic correctness actionable

Voice reports default to enabled, but setCirceVoiceReportsEnabled has no production caller. The current control-center button toggles a live session only. It does not change the report preference.

After the user stops a conversation, a later completion can start a new announcement session whenever an API key is configured. Without a key, reports can still use browser speech. There is no supported UI path to remain silent while leaving voice configured. Old false preferences can also remain stuck without a way to re-enable them.

Knip and a full web-source caller search find only the setter definition. Report auto-activation is in CirceVoiceReporter.tsx lines 44-46.

Repair direction. Expose the persisted report preference in the Circe settings or control center and wire it to the reporter lifetime. Keep session start/stop semantics clear, and make both mute and unmute reachable. Reconcile the browser fallback with the documented live-only voice policy.

Current source
   4 export function areCirceVoiceReportsEnabled(): boolean {
   5   return localStorage.getItem(VOICE_REPORTS_ENABLED_KEY) !== "false";
   6 }
   7 
   8 export function setCirceVoiceReportsEnabled(enabled: boolean): void {
   9   localStorage.setItem(VOICE_REPORTS_ENABLED_KEY, String(enabled));
  10   window.dispatchEvent(new Event(CIRCE_PREFERENCES_CHANGED_EVENT));
Open source at reviewed commit

Related: apps/web/src/components/circe/CirceControlCenter.tsx · apps/web/src/components/circe/CirceVoiceReporter.tsx · docs/user/circe.md

Prompt for AI agents
Finding f7
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/web/src/circePreferences.ts` around lines 4-10, Voice reports default to enabled, but setCirceVoiceReportsEnabled has no production caller. The current control-center button toggles a live session only. It does not change the report preference. Expose the persisted report preference in the Circe settings or control center and wire it to the reporter lifetime. Keep session start/stop semantics clear, and make both mute and unmute reachable. Reconcile the browser fallback with the documented live-only voice policy. Knip and a full web-source caller search find only the setter definition. Report auto-activation is in CirceVoiceReporter.tsx lines 44-46. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.
scripts/build-desktop-artifact.ts:4092-4101

Stop shipping the disconnected fx supervisor

P2 style maintainability additional

CirceFastSupervisorLive is absent from the production server composition and has no runtime consumer. Production Knip identifies the whole layer as unused. Desktop artifact construction still downloads, verifies, extracts, and bundles the pinned fx executable on supported targets.

The obsolete dependency therefore increases release contents and can fail a release build through Effect.orDie even though the app never invokes it. The old plan.fx field, fallback comments, and fx-specific parser code remain as well. The shared CIRCE_FAST_SUPERVISOR_SYSTEM_PROMPT is still imported by the direct supervisors and must survive removal of the obsolete service.

Verified by production Knip plus production composition and packaging searches. Existing fx packaging tests pass, but test a product path with no runtime consumer.

Repair direction. Remove the disconnected layer, fx-specific service code, plan field, binary staging, and resource packaging in one focused cleanup. Move or rename the still-used prompt without changing its consumers. Keep credential compatibility reads separate from binary execution.

Current source
4092   if (bundlesCirceFxResources(options)) {
4093     yield* Effect.tryPromise({
4094       try: () =>
4095         stageCirceFxResources({
4096           platform: options.platform,
4097           arch: options.arch === "arm64" ? "arm64" : "x64",
4098           stageProdResourcesDir,
4099         }),
4100       catch: (cause) => new CirceFxStagingError({ cause }),
4101     }).pipe(Effect.orDie);
Open source at reviewed commit

Related: apps/server/src/circe/Layers/CirceFastSupervisor.ts · apps/server/src/circe/Services/CirceFastSupervisor.ts · apps/server/src/circe/controllerHelpers.ts · scripts/circe-fx-packaging.ts

Prompt for AI agents
Finding f8
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `scripts/build-desktop-artifact.ts` around lines 4092-4101, CirceFastSupervisorLive is absent from the production server composition and has no runtime consumer. Production Knip identifies the whole layer as unused. Desktop artifact construction still downloads, verifies, extracts, and bundles the pinned fx executable on supported targets. Remove the disconnected layer, fx-specific service code, plan field, binary staging, and resource packaging in one focused cleanup. Move or rename the still-used prompt without changing its consumers. Keep credential compatibility reads separate from binary execution. Verified by production Knip plus production composition and packaging searches. Existing fx packaging tests pass, but test a product path with no runtime consumer. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.
apps/mobile/src/native/voiceTranscription.android.ts:55-67

Remove or connect the orphaned Android local recognizer

P2 style maintainability additional

The Android CirceLocalAsr module and getLocalLiveVoiceRecognizer implementation remain in the app, but the live recognizer has no non-test caller. The only voice-input UI consumes getLocalVoiceTranscriber, which returns null on Android.

Normal Android availability therefore stays false while the unused native recognizer, Expo module, support checks, and tests remain maintained. Metro platform variants explain why some static tools miss the gap. This is a disconnected feature subtree, not proof that all mobile voice code is dead; iOS file transcription is still used.

Caller trace: useVoiceInputController.ts lines 16, 107 and 209 use only getLocalVoiceTranscriber. All getLocalLiveVoiceRecognizer consumers are tests.

Repair direction. Decide the intended Android product capability and either connect the existing on-device recognizer through Circe-owned mobile composition or remove its native module and unused wrapper. Preserve the working iOS dictation path and avoid inventing file-URI sentinels.

Current source
  55 export function getLocalLiveVoiceRecognizer(): LocalLiveVoiceRecognizer | null {
  56   if (!isCirceLocalAsrAvailable()) return null;
  57   const locale = getDeviceLocale();
  58   return { prepare: (options) => prepareLiveRecognition(locale, options) };
  59 }
  60 
  61 /**
  62  * Android owns no file transcription path. The shared file transcriber stays
  63  * null here so callers cannot pass a fake URI; iOS provides the real file
  64  * transcriber through its own module.
  65  */
  66 export function getLocalVoiceTranscriber(): VoiceTranscriber | null {
  67   return null;
Open source at reviewed commit

Related: apps/mobile/src/features/voice-input/useVoiceInputController.ts · apps/mobile/modules/circe-local-asr/expo-module.config.json · apps/mobile/src/native/circeLocalAsr.ts

Prompt for AI agents
Finding f9
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/mobile/src/native/voiceTranscription.android.ts` around lines 55-67, The Android CirceLocalAsr module and getLocalLiveVoiceRecognizer implementation remain in the app, but the live recognizer has no non-test caller. The only voice-input UI consumes getLocalVoiceTranscriber, which returns null on Android. Decide the intended Android product capability and either connect the existing on-device recognizer through Circe-owned mobile composition or remove its native module and unused wrapper. Preserve the working iOS dictation path and avoid inventing file-URI sentinels. Caller trace: useVoiceInputController.ts lines 16, 107 and 209 use only getLocalVoiceTranscriber. All getLocalLiveVoiceRecognizer consumers are tests. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.
apps/web/src/components/circe/CirceManager.logic.ts:474-481

Delete superseded clarification and presentation helpers

P2 style maintainability additional

appendCirceChoice and applyCirceClarificationChoice implement the older string-rewriting approach to clarification. They are referenced only by each other and tests. circeManagerCatalogIsReady and presentationStatus likewise have no production consumers.

These tests exercise superseded behavior while the real runtime now uses typed model choices, exact frame IDs, and shared command context. Keeping both approaches makes it easier to reintroduce unguarded string rewriting during the next change. Unused exports that are internally used or are test seams are a different category and should remain.

Verified normal and production Knip output, then full web-source caller searches excluding tests.

Repair direction. Remove the proven orphan helpers and their obsolete tests, preserving current typed clarification logic and meaningful caller-level tests. Limit deletion to confirmed unused behavior rather than the entire Knip export list.

Current source
 474 export function appendCirceChoice(utterance: string, choice: string): string {
 475   const instruction = utterance.trim();
 476   const selection = choice.trim();
 477   if (instruction.length === 0) return selection;
 478   if (selection.length === 0) return instruction;
 479   return `${instruction}\n${selection}`;
 480 }
 481 
Open source at reviewed commit

Related: apps/web/src/components/circe/CirceVoiceReporter.logic.ts · apps/web/src/components/circe/CirceManager.logic.test.ts

Prompt for AI agents
Finding f10
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `apps/web/src/components/circe/CirceManager.logic.ts` around lines 474-481, appendCirceChoice and applyCirceClarificationChoice implement the older string-rewriting approach to clarification. They are referenced only by each other and tests. circeManagerCatalogIsReady and presentationStatus likewise have no production consumers. Remove the proven orphan helpers and their obsolete tests, preserving current typed clarification logic and meaningful caller-level tests. Limit deletion to confirmed unused behavior rather than the entire Knip export list. Verified normal and production Knip output, then full web-source caller searches excluding tests. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.
infra/relay/.env.example:34-38

Relay-funded voice exists without a node client

Info info maintainability additional

The relay exposes authenticated create/release live-voice APIs, usage limits, persistence, and an optional deployment API key. The source-built node’s CirceLiveVoice service always uses its own stored key and calls OpenAI directly. No server or client-runtime production call site invokes the relay create/release endpoints.

This is an integration gap to resolve before advertising the deployment-key option as available to app users. The relay endpoint itself is reachable, so it is not safe to label that service dead or delete its schema. It may be intentionally staged work; the current node does not select it.

Repository-wide createLiveVoiceSession/releaseLiveVoiceSession searches find relay handlers and contracts, but no shipped node caller. No relay was deployed or contacted.

Repair direction. Confirm whether relay-funded voice is intended to ship. If so, connect it through Circe-owned session composition with explicit authority and lifecycle handling; otherwise document the endpoint as staged and remove misleading setup claims.

Current source
  34 # When set, linked environments can mint speech-to-speech sessions with this
  35 # deployment key instead of a per-node key. The key never leaves the relay.
  36 # OPENAI_LIVE_API_KEY=sk-...
  37 # LIVE_VOICE_MODEL=gpt-live-1
  38 # LIVE_VOICE_VOICE=marin
Open source at reviewed commit

Related: infra/relay/src/voice/LiveVoiceSessions.ts · infra/relay/src/http/Api.ts · apps/server/src/circe/Services/CirceLiveVoice.ts

Prompt for AI agents
Finding f11
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate.

In `infra/relay/.env.example` around lines 34-38, The relay exposes authenticated create/release live-voice APIs, usage limits, persistence, and an optional deployment API key. The source-built node’s CirceLiveVoice service always uses its own stored key and calls OpenAI directly. No server or client-runtime production call site invokes the relay create/release endpoints. Confirm whether relay-funded voice is intended to ship. If so, connect it through Circe-owned session composition with explicit authority and lifecycle handling; otherwise document the endpoint as staged and remove misleading setup claims. Repository-wide createLiveVoiceSession/releaseLiveVoiceSession searches find relay handlers and contracts, but no shipped node caller. No relay was deployed or contacted. Keep the repair in Circe ownership. Check each affected client and preset, preserve node-qualified identities, and use focused tests. Do not modify inherited T3 implementations, delete shipped migration IDs, or operate on the live database. Update this review sidecar only after validating the fix.

Sidecar: review.json. Source audit finished 2026-09-13T15:00:14.570496+00:00. No findings have been fixed in this pass.