Circe app review
Dead code, conflicting behavior, and incomplete integrations after the rebrand. Reviewed 13 September 2026.
Changes required
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)
- apps/mobile/modules/circe-local-asr/expo-module.config.json
- apps/mobile/src/features/circe/CirceMobileProvider.tsx
- apps/mobile/src/features/voice-input/useVoiceInputController.ts
- apps/mobile/src/native/circeLocalAsr.ts
- apps/mobile/src/native/voiceTranscription.android.ts
- apps/server/src/circe/Layers/CirceCodexSupervisor.ts
- apps/server/src/circe/Layers/CirceController.ts
- apps/server/src/circe/Layers/CirceFastSupervisor.ts
- apps/server/src/circe/Layers/CirceGrokSupervisor.ts
- apps/server/src/circe/Layers/CirceOpencodeSupervisor.ts
- apps/server/src/circe/Layers/CirceProjectLexicon.ts
- apps/server/src/circe/Layers/CirceWsRpc.ts
- apps/server/src/circe/Services/CirceFastSupervisor.ts
- apps/server/src/circe/Services/CirceLiveVoice.ts
- apps/server/src/circe/controllerHelpers.ts
- apps/server/src/circe/conversationsProject.ts
- apps/server/src/persistence/Migrations.ts
- apps/server/src/persistence/Migrations/041_CirceTaskDesks.ts
- apps/server/src/serverRuntimeStartup.ts
- apps/web/src/circePreferences.ts
- apps/web/src/components/circe/CirceControlCenter.tsx
- apps/web/src/components/circe/CirceLiveVoice.bridge.ts
- apps/web/src/components/circe/CirceLiveVoice.logic.ts
- apps/web/src/components/circe/CirceLiveVoiceRuntime.tsx
- apps/web/src/components/circe/CirceManager.logic.test.ts
- apps/web/src/components/circe/CirceManager.logic.ts
- apps/web/src/components/circe/CirceVoiceReporter.logic.ts
- apps/web/src/components/circe/CirceVoiceReporter.tsx
- apps/web/src/components/circe/CirceVoiceRuntime.tsx
- docs/user/circe.md
- infra/relay/.env.example
- infra/relay/src/http/Api.ts
- infra/relay/src/voice/LiveVoiceSessions.ts
- packages/circe-client-runtime/src/circe/mesh.ts
- packages/contracts/src/providerInstance.ts
- scripts/build-desktop-artifact.ts
- scripts/circe-fx-packaging.ts
Issues table
Prompt for all actionable findings · Ctrl+`
Prompt to rerun · Ctrl+Space
Review comments
5 high-priority defects · 5 smaller repairs and cleanups · 1 staged integration note
packages/circe-client-runtime/src/circe/mesh.ts:356-366Select 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 commitRelated: 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
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-1264Resolve 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 commitRelated: apps/server/src/circe/conversationsProject.ts · apps/server/src/serverRuntimeStartup.ts
Prompt for AI agents
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-496Preserve 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 commitRelated: 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
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-46Keep 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 commitRelated: 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
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-916Refresh 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 commitRelated: apps/web/src/components/circe/CirceLiveVoiceRuntime.tsx · apps/server/src/circe/Services/CirceLiveVoice.ts
Prompt for AI agents
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-10Restore 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 commitRelated: apps/web/src/components/circe/CirceControlCenter.tsx · apps/web/src/components/circe/CirceVoiceReporter.tsx · docs/user/circe.md
Prompt for AI agents
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-4101Stop 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 commitRelated: 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
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-67Remove 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 commitRelated: 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
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-481Delete 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 commitRelated: apps/web/src/components/circe/CirceVoiceReporter.logic.ts · apps/web/src/components/circe/CirceManager.logic.test.ts
Prompt for AI agents
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-38Relay-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=marinOpen 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
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.
apps/server/src/persistence/Migrations.ts:137-154
Add a forward migration for installed Jarvis databases
P1 logic data-integrity actionableThe 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
Open source at reviewed commitRelated: apps/server/src/persistence/Migrations/041_CirceTaskDesks.ts · apps/server/src/circe/Layers/CirceProjectLexicon.ts
Prompt for AI agents