Skip to content

refactor: split core, app and desktop with dependency-injected services - #1538

Merged
ocavue merged 596 commits into
masterfrom
service
Oct 13, 2026
Merged

ocavue merged 596 commits into
masterfrom
service

Conversation

@ocavuebot

@ocavuebot ocavuebot commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Replaces the process-global IPC bridge and module-level singletons on the TypeScript side with dependency-injected services, and splits the TypeScript side into three layers: @reflect/core (business logic and platform ports), @reflect/app (the app layer: windows, routes, commands, menus, update, theme, boot flows) and apps/desktop (adapters and React). No Rust logic changes (two test fixture paths moved).

What changed

  • Three packages. @reflect/core keeps business logic and the interfaces of platform primitives; it knows nothing about windows, routes, menus, pixels, React or Tauri (lint-enforced: core cannot import @reflect/app, and the role port is the only window fact core reads). New @reflect/app (packages/app) holds the app layer over core; it imports no React, TanStack or Tauri either, so it runs in the app-node vitest project. apps/desktop keeps the platform adapters (apps/desktop/src/services/) and React.
  • Two composition roots. createCoreServices(adapters: CoreAdapters) (packages/core/src/create-core-services.ts) builds CoreServices and CoreInternalServices; createAppServices(core, host: AppHost) (packages/app/src/create-app-services.ts) builds AppServices. apps/desktop/src/boot.ts runs them in order: the shared primitives (logger, clock, the IpcBridgeService), then createDesktopHost (window, lifecycle, operations and the other host-side services), then core with the host's services as its ports, then the app layer. Core layout: the two roots sit in packages/core/src/, the host ports and their default implementations in packages/core/src/ports/, everything else in a domain directory, and a file that exports a service is named after it. Pure computations are plain functions, not services: sha256Hex lives in @reflect/utils, pkceChallenge is called where it is used, and the mobile sync status is the function mobileSyncStatus.
  • Three ports. Business services reach the shell only through ProgressPort (where visible background work is reported; the app's OperationsService implements it), ActivityPort (resume, focus, visibility and background-task signals; the app's AppLifecycleService implements it) and role: 'main' | 'note-window'. packages/core/src/ports/activity-port.ts and progress-port.ts. ProgressPort.fail(label, message) is the one-shot form; start() returns a handle for long operations. A note window's TaskService follows the Rust index:written broadcast itself through ipc.listen.
  • Services are classes behind interfaces. Each capability is XxxServiceDeps (readonly fields typed with the full service type; the deps interface lists only the services the class uses), XxxService (the documented contract), an unexported XxxServiceImpl class (readonly #deps, # state, plain methods, no accessors, no private) and createXxxService(deps) returning new XxxServiceImpl(deps). Files are packages/<core|app>/src/<domain>/<name>-service.ts. Only getState / subscribe and the few members React passes unbound are function properties and arrow fields. Test doubles follow the same shape; a wrapper holds #inner and forwards each member, nothing spreads a service instance.
  • Per-graph lifecycles. createGraphServices(session, deps, flags) (core) starts backup, iCloud sync, capture, asset description, the chat session, iCloud downloads and the attachment catalog watch when a graph opens and disposes them when GraphSession.signal aborts; it takes booleans and does no branching. createAppGraphServices (packages/app/src/graph/graph-lifecycles.ts) decides the flags from the window role, the platform and two live settings, and the desktop passes it to core through CoreAdapters.createGraphServices.
  • Graph open and boot split. Core GraphSessionService is the state machine: attach, openRecent, createGraphAt, close, adopt, recents, deleteGraph, refreshIndex, park. App GraphBootService holds the flows that know windows and dialogs: boot (main window, note window adoption, mobile), pickAndOpen, chooseGraph. IndexLifecycleService.followActivity() (core, started by GraphSessionService.attach()) suspends the index while a mobile main window is hidden. MobileOnboardingService is app layer.
  • Deep links split. Core DeepLinkIntakeService owns the reflect:// protocol: the OS source, the pending buffer, onUrl, and resolve(url) into a target (note, daily, search, tasks, capture spooled, none). App DeepLinkRouter maps a target to a Route and navigates. Route and the navigation history (NavigationService), link activation (LinkNavigationService), commands, keymap, native menus, update, theme, telemetry, the palette and autocomplete data layers, the calendar events store, the daily view models and the mobile keyboard and share services are all @reflect/app.
  • Shell steps moved to callers. NoteActionsService.deepLinkFor / absolutePathFor return data and the desktop writes the clipboard (lib/clipboard-actions.ts); GistPublishService.publish returns the URL; attachFilesToNote takes the files the desktop picked. ClipboardService, OpenerService and DialogService are app-layer services (core has no caller left).
  • generation. Rust-boundary wrappers (NoteFileService, AssetStoreService, IndexStoreService, GitRepositoryService, ...) take an explicit generation, reads included. User-facing services capture the open GraphSession once at each public method's entry (requireGraphSession, writeGenerationFor). Desktop code passes no generations and has no "No graph is open" guards.
  • React. CoreServicesProvider / useCoreServices() and AppServicesProvider / useAppServices() (apps/desktop/src/services/services-provider.tsx); useServiceState(service, read) reads a service's state. useMaybeGraph(), useMaybeGraphServices() and useMaybeChatSession() are the versions that return null without a graph. CommandContext carries services (core) and app. Providers build their context from the service state and the service; there is no SettingsProvider. Hook signatures (useGraph, useSettings, useSync, useChatSession, useRouter) did not change.
  • Logic moved from desktop into core or app. Graph session and boot, index lifecycle, note sessions and open documents, rename and move, note actions, tasks (including the optimistic task rows: TaskService.getState() / subscribe / refresh() and ten write methods, so lib/tasks/ keeps only UI orchestration), navigation and deep links, link navigation, capture, audio memos, asset descriptions, chat, embeddings and semantic search, backup and iCloud sync, calendar and contacts, monetization, the Classic access recheck policy, the per-graph lifecycles above.
  • One optimistic mechanism. NoteRowService is the overlay for pin, pin order, private, gist and trash; NoteActionsService writes it and readers merge it. The app only invalidates React Query caches (apps/desktop/src/lib/query-invalidation.ts).
  • Removed. setBridge / getBridge / hasBridge / useBridgeReady, every free function that delegated to a global, the desktop lib/ modules that only forwarded calls, useMainWindowEffect, the desktop query-cache patches, GitHostCredentialService, IpcBridgeService.transport / hasPluginEvents, withAppReviewDemo / withAppReviewDemoTranscription, withContactsLookups, the *ServiceFactory layers, the React Query task cache, packages/core/scripts/check-exports.mjs (a whole-repo check does not belong in lint; the barrels were trimmed by hand), eslint.architecture.js (folded into eslint.config.js), the core browser vitest project and playwright.
  • Renamed. WikiLinkSuggestionService is AutocompleteSuggestionService; type Unlisten lives in packages/core/src/unlisten.ts; PlatformFacts.buildPlatform carries the Tauri build target; the core container is CoreServices (not AppServices, which is now the app layer's).

Tests

  • @reflect/core/testing provides createTestCoreServices(overrides) over the core root with test ports (createTestProgressPort(), createTestActivityPort()), plus hand-written doubles for the Rust, SQLite and plugin boundaries and recording wrappers over real services; where a real service's dependencies all have doubles, tests build the real service. @reflect/app/testing provides createTestAppServices(core, overrides) over the app root with a test host (createTestAppHost()); its lifecycle double drives the same activity port the core services subscribe to, so one emitResume() reaches both layers.
  • Desktop tests render through renderInApp / renderHookInApp (apps/desktop/src/test-utils/render-in-app.tsx): both providers, the real GraphProvider and the real per-graph lifecycles over test services. The app handle is the core container, appServices the app layer.
  • vitest-fail-on-console covers every package. restoreMocks is on.
Before After
Test files that install a fake bridge (setBridge() 85 0
vi.mock( calls 552 about 80
hasBridge() / useBridgeReady() call sites 61 0
Entries in the console allowlist 17 1
Function overloads in packages/ and apps/ 13 0
Services and doubles returning an object literal all 0
"No graph is open" guards in desktop 47 0
Core services that read the window, the DOM lifecycle or the operations list 37 0 (three ports)
Exported names of @reflect/core 1200 about 380

The remaining vi.mock calls are pure UI children, heavy children, Tauri adapter tests, and the browser extension. A lint rule holds the allowlist.

Lint rules that keep the shape (eslint.config.js)

  • packages/core cannot import @tauri-apps/*, react, @tanstack/*, @reflect/app, or the apps; packages/app cannot import @tauri-apps/*, react, @tanstack/*, or the apps.
  • Core source cannot mention isMainWindow, openNoteWindow, closeSecondaryWindows or the Route type.
  • Neither package exports default… / setDefault… instances or uses module-level let.
  • @tauri-apps/* is importable only from apps/desktop/src/services/, boot.ts, and main.tsx.
  • No console in app or package source; logging goes through LoggerService.
  • vi.mock only for an explicit allowlist of module specifiers.
  • No function overloads, no accessors, no private / protected / public or parameter properties (# instead).

Behavior changes to review

The migration moved code and changed how dependencies are obtained. These are the places where behavior is not identical:

  • Mobile boot: when the settings load fails, the app retries the load when it comes back to the foreground. Before, it stayed on "Couldn’t load your settings." (the query that used to re-run was removed with the global bridge).
  • A failed step in the quit or background flush, a failed GitHub keychain read, a failed calendar permission read, and a failed note:moved / import-progress listener registration are now logged. Before, they were swallowed.
  • UpdateService.supported is false on iOS and Android. Before, it was true in any native shell.
  • The hasBridge() / useBridgeReady() gates are gone. The IPC transport is installed before the first render, and the browser-dev transport queues early calls.
  • Index query invalidation also fires on watcher batches that changed no row and after an asset-description re-index (throttled); embedding and asset-description followers also hear applyChanges batches (hash-gated).
  • Contacts permission is read at boot, on focus while the integrations section is open, after a prompt, and before a meeting is added. Before, every consumer mount re-read it.
  • A pin made before the sidebar shelf loaded now reads the index and writes a numbered pin order. Before, it wrote a bare pinned: true.
  • A non-GitHub HTTPS remote no longer reads the keychain for a host credential (nothing could store one); its error text now suggests the SSH remote form.
  • GraphSession.indexGeneration can be null: a graph whose index failed to open still runs backup, iCloud sync, capture and chat, as before, and index-dependent actions no-op.
  • Index, graph, navigation, and chat state are one instance per webview, not one per provider mount. Subscriptions attach one microtask earlier.
  • Task rows: a failed read is not retried once after a second (React Query did); rows reset on any graph session change (before: on a root change only); the optimistic patch lands synchronously (before: a few microtasks later); overlapping writes of one kind stay pending until all finish; an asset-description pass no longer re-reads the task rows.
  • noteDocuments.readSource, templates.templateBody and backlinks.getBacklinksWithContext throw "No graph is open." without an open graph. The command palette note preview reads the open editor buffer first, then disk.
  • Deep links: the handler is a subscription (onUrl) instead of a single setHandler; the first subscriber drains the buffer.
  • Opening or creating a graph, and forgetting the open graph, close the note windows before the core call instead of after; a failed folder picker reports the error without parking on the chooser. When a graph closes, its own lifecycles are disposed before the asset-description and iCloud-download ones (the order used to be reversed).
  • Attaching files: the graph generation is read after the file picker closes, not before.
  • Fixed on the way (present on master): asset-description re-indexing and deepLinkFor passed the file generation where the index generation was expected, so those index writes could be rejected.
  • packages/core gained a date-fns dependency; packages/app depends on @reflect/core, @reflect/utils, @ocavue/utils, date-fns and zod.

Fixes found on the way

  • Mobile surface flags were wrong on the phone for code that read the platform from the provider; the boot facts now derive them from the app platform.
  • asset-description-service.test.ts and note-editor.test.tsx had timing races; month-picker-drawer.test.tsx depended on state left by another file.
  • Browser tests: @tauri-apps/api/core was optimized by Vite in the middle of a run, which reloaded the page. It is pre-optimized now.
  • CI: Chromium on Linux did not free finished test iframes, and the browser process died near 2.5 GB. The CI test step now makes Vitest collect garbage after every file.

Not in this PR

  • One-line JSDoc comments that predate this PR (about 1200 lines on master) are unchanged; the lines this PR adds are all multi-line.

Docs

  • docs/contributing/services.md (with a "Core, app, or desktop?" section), testing.md, adding-a-command.md, adding-a-setting.md, editor-architecture.md, multi-window.md, deep-links.md, docs/plans/architecture-conventions.md, AGENTS.md, packages/core/README.md, and the new packages/app/README.md.

Written by Claude Fable 5.1, Claude Opus 5.5 and Claude Sonnet 5.5.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by Sourcery

Introduce replaceable core services for time, logging, identifiers, and hashing, and migrate affected application code and tests to use them.

New Features:

  • Add clock, logger, identifier, and hashing service interfaces with replaceable default implementations.
  • Expose deterministic test service factories through the core testing package.
  • Allow Classic sign-in to use an injected identifier service for OAuth state and PKCE values.

Enhancements:

  • Centralize local date parsing and current-date handling in core clock utilities.
  • Route core diagnostics through the logger service instead of direct console calls.
  • Update desktop date state and tests to use the clock service and controllable test clocks.
  • Route content hashing and note ID generation through replaceable services.
  • Expand service-level and test-double coverage for time, logging, IDs, and hashing.

Tests:

  • Replace console spies and browser timer manipulation with recording logger and deterministic clock test services.

Chores:

  • Export the new platform services and testing entry point from @reflect/core.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Walkthrough

Warning

Review details and warnings were omitted to fit the comment limit.

@sourcery-ai

sourcery-ai Bot commented Oct 12, 2026

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

This draft step establishes the package export and API location for service-specific test doubles, but does not yet implement the dependency-injected services or migrate existing global bridges, singletons, or tests.

File-Level Changes

Change Details Files
Added a public testing entry point for future dependency-injected service fakes.
  • Added the @reflect/core/testing package export.
  • Created a placeholder testing module documenting the planned createTestXxxService() API; no test doubles are implemented yet.
packages/core/package.json
packages/core/src/testing/index.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@ocavue
ocavue marked this pull request as ready for review October 12, 2026 22:53

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, we are unable to review this pull request

The GitHub API does not allow us to fetch diffs exceeding 300 files, and this pull request has 1143

@qodo-code-review

qodo-code-review Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

PR Summary by Qodo

Replace global TypeScript services with injected core and app layers

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Split TypeScript business logic, app orchestration, and platform adapters into explicit layers.
• Replace global IPC and singleton state with injected, lifecycle-scoped services.
• Migrate React consumers and tests; enforce package boundaries through lint and CI.
Diagram

graph TD
  B["Desktop boot"] --> H["Platform adapters"] --> C["Core services"] --> G["Graph services"] --> R["React providers"]
  B --> A["App services"] --> R
  H --> A
  C --> A
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Keep a single package with internal module boundaries
  • ➕ Fewer package exports and less workspace configuration.
  • ➕ Allows a smaller initial migration.
  • ➖ Package boundaries cannot independently protect core from app-layer dependencies.
  • ➖ Retains more opportunities for UI and business logic to become coupled.
2. Migrate one domain at a time behind compatibility adapters
  • ➕ Smaller reviewable changes and easier regression isolation.
  • ➕ Can preserve existing consumers while services move.
  • ➖ Temporarily maintains two service models and their wiring.
  • ➖ Delays removal of global IPC and module singletons.

Recommendation: The three-layer dependency-injection design is a sound destination: it gives core explicit host ports and makes services testable without React or Tauri. Given the exceptionally broad migration, domain-by-domain review and validation are preferable even if the final integration lands together; retaining a single package would weaken the intended boundary.

Files changed (120) +26576 / -3735

Refactor (86) +21014 / -3345
create-app-services.tsCompose application services from core and host ports +335/-0

Compose application services from core and host ports

• Defines AppHost and constructs app-wide services for windows, navigation, commands, menus, updates, theme, and mobile flows.

packages/app/src/create-app-services.ts

index.tsExpose the app-layer public API +174/-0

Expose the app-layer public API

• Exports the application services, contracts, factories, and types consumed by desktop.

packages/app/src/index.ts

lifecycle-service.tsMove activity and background-task signals into a service +190/-0

Move activity and background-task signals into a service

• Implements the activity port used by core for resume, focus, visibility, and background-task handling.

packages/app/src/app/lifecycle-service.ts

operations-service.tsProvide injected background-operation status +175/-0

Provide injected background-operation status

• Implements the progress port for visible running operations and lingering warnings or failures.

packages/app/src/app/operations-service.ts

window-service.tsCentralize window behavior behind an app service +262/-0

Centralize window behavior behind an app service

• Moves window identity, opening, closing, and related host interactions out of core and desktop globals.

packages/app/src/app/window-service.ts

quit-policy-service.tsOwn quit-time policy in the app layer +155/-0

Own quit-time policy in the app layer

• Coordinates lifecycle-driven persistence and shutdown behavior through injected dependencies.

packages/app/src/app/quit-policy-service.ts

update-service.tsExtract updater orchestration +305/-0

Extract updater orchestration

• Wraps update availability and installation behind an app service and optional native updater adapter.

packages/app/src/app/update-service.ts

platform-service.tsModel app-facing platform capabilities +105/-0

Model app-facing platform capabilities

• Supplies application-level platform and media state without introducing Tauri dependencies into the package.

packages/app/src/app/platform-service.ts

exception-telemetry-service.tsDefine an injectable telemetry contract +46/-0

Define an injectable telemetry contract

• Lets the host install exception reporting or supply a disabled implementation.

packages/app/src/app/exception-telemetry-service.ts

dialog-service.tsDefine the app's dialog port +35/-0

Define the app's dialog port

• Describes native file-selection operations without depending on a desktop implementation.

packages/app/src/app/dialog-service.ts

clipboard-service.tsDefine the clipboard service contract +6/-0

Define the clipboard service contract

• Exposes clipboard capabilities to app logic through a host-provided service.

packages/app/src/app/clipboard-service.ts

opener-service.tsExtract URL and file opening into an app service +77/-0

Extract URL and file opening into an app service

• Routes opening behavior through an injected browser or native transport.

packages/app/src/app/opener-service.ts

command-service.tsReplace the global command registry +209/-0

Replace the global command registry

• Owns command registration and execution within the injected app graph.

packages/app/src/commands/command-service.ts

keymap-service.tsProvide an injected keyboard mapping service +84/-0

Provide an injected keyboard mapping service

• Moves keybinding lookup and dispatch out of module-level desktop state.

packages/app/src/commands/keymap-service.ts

native-menu-service.tsCompose native menus using host ports +321/-0

Compose native menus using host ports

• Separates menu behavior from Tauri calls and supplies an unsupported-host implementation.

packages/app/src/commands/native-menu-service.ts

graph-boot-service.tsMove graph boot orchestration into the app layer +272/-0

Move graph boot orchestration into the app layer

• Coordinates graph startup and surface-specific boot behavior using injected services.

packages/app/src/graph/graph-boot-service.ts

graph-lifecycles.tsSelect graph lifecycles by app context +109/-0

Select graph lifecycles by app context

• Defines the app-side selection and startup of per-graph background work.

packages/app/src/graph/graph-lifecycles.ts

mobile-onboarding-service.tsExtract mobile onboarding state and flows +438/-0

Extract mobile onboarding state and flows

• Moves onboarding orchestration out of React providers into an app-layer service.

packages/app/src/graph/mobile-onboarding-service.ts

navigation-service.tsMake application routing injectable +350/-0

Make application routing injectable

• Owns route transitions and navigation state independently of React.

packages/app/src/navigation/navigation-service.ts

link-navigation-service.tsCentralize link navigation decisions +470/-0

Centralize link navigation decisions

• Moves link routing and window-opening behavior from editor and UI hooks into a testable app service.

packages/app/src/navigation/link-navigation-service.ts

deep-link-router.tsRoute incoming deep links through app services +149/-0

Route incoming deep links through app services

• Connects parsed links to navigation actions without depending on React or Tauri.

packages/app/src/navigation/deep-link-router.ts

route.tsMove route definitions into @reflect/app +139/-0

Move route definitions into @reflect/app

• Keeps app-specific route types out of the business-logic package.

packages/app/src/navigation/route.ts

autocomplete-suggestion-service.tsExtract editor suggestion orchestration +274/-0

Extract editor suggestion orchestration

• Builds autocomplete suggestions from injected search and navigation capabilities.

packages/app/src/search/autocomplete-suggestion-service.ts

palette-search-service.tsMove command-palette search into an app service +132/-0

Move command-palette search into an app service

• Combines search results and app navigation for palette consumers.

packages/app/src/search/palette-search-service.ts

palette-entries.tsRelocate command-palette entry logic +181/-0

Relocate command-palette entry logic

• Keeps palette-specific presentation and command entries in the app layer.

packages/app/src/search/palette-entries.ts

theme-service.tsMove theme ownership into an app service +152/-0

Move theme ownership into an app service

• Centralizes theme state and persistence for desktop and mobile consumers.

packages/app/src/settings/theme-service.ts

connect-github-wizard-service.tsExtract GitHub connection wizard flow +550/-0

Extract GitHub connection wizard flow

• Moves multi-step GitHub connection state out of React hooks and into an injected app service.

packages/app/src/sync/connect-github-wizard-service.ts

sync-status-service.tsProvide app-level sync presentation state +123/-0

Provide app-level sync presentation state

• Derives sync status for UI surfaces from injected core services.

packages/app/src/sync/sync-status-service.ts

calendar-events-service.tsExtract calendar event presentation flow +234/-0

Extract calendar event presentation flow

• Supplies application-facing calendar events without placing UI concerns in core.

packages/app/src/calendar/calendar-events-service.ts

keyboard-service.tsMove mobile keyboard signals into a service +56/-0

Move mobile keyboard signals into a service

• Exposes keyboard state to mobile UI through the app graph.

packages/app/src/mobile/keyboard-service.ts

share-service.tsMove mobile share handling into a service +65/-0

Move mobile share handling into a service

• Coordinates share actions without keeping their logic in a React hook.

packages/app/src/mobile/share-service.ts

create-core-services.tsCreate the injected core composition root +1458/-0

Create the injected core composition root

• Wires app-scoped business services from typed host adapters and separates public from internal capabilities.

packages/core/src/app/create-core-services.ts

create-graph-services.tsOwn per-graph service lifecycles +224/-0

Own per-graph service lifecycles

• Creates backup, iCloud, capture, asset-description, chat, and attachment work for a graph session, selected by lifecycle flags.

packages/core/src/app/create-graph-services.ts

graph-session.tsDefine graph-session ownership and roles +87/-0

Define graph-session ownership and roles

• Adds session state and role contracts used to scope graph services to main and note windows.

packages/core/src/app/graph-session.ts

ports.tsDefine business-to-host activity and progress ports +99/-0

Define business-to-host activity and progress ports

• Introduces platform-independent lifecycle, background-task, and visible-operation contracts for core services.

packages/core/src/app/ports.ts

persistence-flush-service.tsCoordinate registered persistence flushers +91/-0

Coordinate registered persistence flushers

• Centralizes flush ownership, including the documents flusher, for lifecycle and quit-time work.

packages/core/src/app/persistence-flush-service.ts

ipc-service.tsReplace global IPC with a validated bridge service +373/-0

Replace global IPC with a validated bridge service

• Wraps injected native transports with schema-validated commands, typed errors, binary calls, and event subscriptions.

packages/core/src/ipc/ipc-service.ts

open-documents-service.tsOwn open documents and external file changes +172/-0

Own open documents and external file changes

• Moves document-session ownership and external change routing out of React and module-level desktop state.

packages/core/src/documents/open-documents-service.ts

note-session.tsMove note-session state into core +688/-0

Move note-session state into core

• Owns editing-session state and document binding independently of the desktop editor.

packages/core/src/documents/note-session.ts

note-actions-service.tsConsolidate note actions in core +536/-0

Consolidate note actions in core

• Provides injected operations for note changes, including trash behavior used by desktop and mobile.

packages/core/src/documents/note-actions-service.ts

attachment-service.tsExtract attachment workflows into core +724/-0

Extract attachment workflows into core

• Moves attachment persistence and graph-session catalog watching behind a business service.

packages/core/src/documents/attachment-service.ts

title-rename-service.tsExtract title-rename coordination +338/-0

Extract title-rename coordination

• Moves cross-document rename work into a testable core service.

packages/core/src/documents/title-rename-service.ts

index-lifecycle-service.tsOwn indexing lifecycle and applied-change handling +702/-0

Own indexing lifecycle and applied-change handling

• Coordinates index updates, file changes, and post-apply behavior through injected services.

packages/core/src/indexing/index-lifecycle-service.ts

index-query-service.tsExpose index reads through a service +351/-0

Expose index reads through a service

• Replaces module-level index query helpers with graph-aware injected queries.

packages/core/src/indexing/index-query-service.ts

index-store-service.tsOwn index storage in a core service +411/-0

Own index storage in a core service

• Makes index persistence and associated state available through explicit dependencies.

packages/core/src/indexing/index-store-service.ts

search-service.tsMove indexed search into core services +375/-0

Move indexed search into core services

• Routes search requests through injected index and database capabilities.

packages/core/src/indexing/search-service.ts

graph-session-service.tsOwn graph opening and switching +553/-0

Own graph opening and switching

• Moves graph-session state and refresh orchestration from React providers into core.

packages/core/src/graph/graph-session-service.ts

note-creation-service.tsExtract note creation into core +231/-0

Extract note creation into core

• Makes note creation a reusable operation with explicit graph and storage dependencies.

packages/core/src/graph/note-creation-service.ts

task-service.tsCentralize task reads and mutations +1195/-0

Centralize task reads and mutations

• Moves task operations and related state out of desktop hooks into an injected core service.

packages/core/src/tasks/task-service.ts

backup-service.tsMove backup lifecycle into a graph service +564/-0

Move backup lifecycle into a graph service

• Replaces a desktop controller with a session-owned backup service and injected dependencies.

packages/core/src/sync/backup-service.ts

icloud-sync-service.tsInject iCloud synchronization dependencies +293/-240

Inject iCloud synchronization dependencies

• Adapts iCloud conflict and synchronization work to the graph service lifecycle.

packages/core/src/sync/icloud-sync-service.ts

github-auth-service.tsMake GitHub authentication service-owned +526/-0

Make GitHub authentication service-owned

• Replaces module-level authentication state and commands with an injected core service.

packages/core/src/sync/github-auth-service.ts

settings-service.tsOwn settings loading and persistence +288/-0

Own settings loading and persistence

• Provides settings state through an injected service and supports retrying a failed mobile load on app return.

packages/core/src/settings/settings-service.ts

audio-memo-service.tsMove audio memo orchestration into core +1020/-0

Move audio memo orchestration into core

• Consolidates recording and memo workflows previously distributed across actions, hooks, and providers.

packages/core/src/audio/audio-memo-service.ts

mobile-audio-memo-service.tsExtract mobile memo recording flow +608/-0

Extract mobile memo recording flow

• Owns mobile audio-memo state and processing independently of React provider lifecycle.

packages/core/src/audio/mobile-audio-memo-service.ts

capture-service.tsProvide an injected capture service +365/-0

Provide an injected capture service

• Moves capture handling and session setup from standalone actions into core services.

packages/core/src/capture/capture-service.ts

chat-session-service.tsOwn chat sessions per graph +622/-0

Own chat sessions per graph

• Moves chat state and interactions out of the React chat provider into graph-scoped core logic.

packages/core/src/ai/chat/chat-session-service.ts

embedding-index-service.tsExtract embedding index operations +325/-0

Extract embedding index operations

• Encapsulates embedding index state and writes behind an injected service.

packages/core/src/embeddings/embedding-index-service.ts

semantic-search-service.tsExpose semantic search through core services +237/-0

Expose semantic search through core services

• Replaces standalone retrieval orchestration with an injected semantic search service.

packages/core/src/embeddings/semantic-search-service.ts

boot.tsCompose host, core, and app services at startup +56/-15

Compose host, core, and app services at startup

• Constructs logger, clock, IPC transport, host ports, core services, and app services in dependency order; installs telemetry and query invalidation.

apps/desktop/src/boot.ts

create-desktop-services.tsConstruct browser and Tauri host adapters +129/-0

Construct browser and Tauri host adapters

• Selects platform transports and creates the window, lifecycle, operations, clipboard, and telemetry host supplied to the service graphs.

apps/desktop/src/services/create-desktop-services.ts

services-provider.tsxExpose injected service graphs through React +78/-0

Expose injected service graphs through React

• Adds core, app, and graph contexts with hooks that require their respective providers.

apps/desktop/src/services/services-provider.tsx

deferred-ipc.tsDefer plain-browser IPC connection +48/-0

Defer plain-browser IPC connection

• Lets browser development construct services before its in-memory IPC transport becomes available.

apps/desktop/src/services/deferred-ipc.ts

tauri-ipc.tsAdapt Tauri IPC to the core bridge +62/-0

Adapt Tauri IPC to the core bridge

• Implements native command, event, binary, and plugin transport operations behind the injected IPC interface.

apps/desktop/src/services/tauri-ipc.ts

detect-platform.tsDetect platform facts at the desktop boundary +86/-0

Detect platform facts at the desktop boundary

• Builds the platform facts used to select native or browser adapters and UI behavior.

apps/desktop/src/services/detect-platform.ts

dom-lifecycle.tsAdapt DOM lifecycle events +47/-0

Adapt DOM lifecycle events

• Provides browser visibility and activity signals to the app lifecycle service.

apps/desktop/src/services/dom-lifecycle.ts

tauri-window.tsImplement native window host operations +80/-0

Implement native window host operations

• Connects the app window service to Tauri windows while retaining window-role behavior.

apps/desktop/src/services/tauri-window.ts

native-menu.tsAdapt Tauri native menus +75/-0

Adapt Tauri native menus

• Implements the host interface consumed by the app-layer menu service.

apps/desktop/src/services/native-menu.ts

query-invalidation.tsInstall query invalidation at the desktop boundary +68/-0

Install query invalidation at the desktop boundary

• Connects injected service changes to TanStack query invalidation without importing query code into core or app.

apps/desktop/src/lib/query-invalidation.ts

query-client.tsRemove business orchestration from the query client +30/-137

Remove business orchestration from the query client

• Leaves TanStack integration in desktop while moving domain behavior into injected services.

apps/desktop/src/lib/query-client.ts

main.tsxBoot React with injected service providers +18/-16

Boot React with injected service providers

• Connects application startup and root rendering to the new service graphs.

apps/desktop/src/main.tsx

app.tsxConsume the app service graph at the root +6/-7

Consume the app service graph at the root

• Adjusts root composition to use injected app and core services.

apps/desktop/src/app.tsx

graph-provider.tsxMake graph provider a service boundary +70/-548

Make graph provider a service boundary

• Replaces provider-owned graph business logic with subscriptions to the core graph-session service and graph-scoped services.

apps/desktop/src/providers/graph-provider.tsx

settings-provider.tsxRead settings from the core service +25/-205

Read settings from the core service

• Removes settings ownership from React in favor of the injected settings service.

apps/desktop/src/providers/settings-provider.tsx

chat-provider.tsxReplace chat provider business state +48/-450

Replace chat provider business state

• Delegates chat-session behavior to injected core services while retaining UI integration.

apps/desktop/src/providers/chat-provider.tsx

audio-memo-provider.tsxDelegate desktop memo state to core +77/-283

Delegate desktop memo state to core

• Reduces provider orchestration by consuming the audio memo service.

apps/desktop/src/providers/audio-memo-provider.tsx

sync-provider.tsxSubscribe to injected synchronization services +29/-108

Subscribe to injected synchronization services

• Replaces provider-owned sync behavior with service-backed state.

apps/desktop/src/providers/sync-provider.tsx

theme-provider.tsxConnect React theme state to the app service +29/-94

Connect React theme state to the app service

• Uses app-owned theme state instead of a provider-owned singleton.

apps/desktop/src/providers/theme-provider.tsx

router.tsxBind React routing to navigation services +72/-245

Bind React routing to navigation services

• Moves route state and navigation decisions into @reflect/app while retaining React rendering.

apps/desktop/src/routing/router.tsx

app-shortcuts.tsDispatch shortcuts through app commands +40/-140

Dispatch shortcuts through app commands

• Connects UI keyboard handling to the injected command and navigation services.

apps/desktop/src/routing/app-shortcuts.ts

use-note-document.tsBind editor documents to core sessions +59/-81

Bind editor documents to core sessions

• Replaces hook-owned document orchestration with the core open-document and note-session services.

apps/desktop/src/editor/use-note-document.ts

use-asset-persistence.tsDelegate editor asset persistence +50/-194

Delegate editor asset persistence

• Uses injected attachment and asset services instead of implementing persistence in a React hook.

apps/desktop/src/editor/use-asset-persistence.ts

use-editor-autocomplete.tsUse app-owned editor suggestions +10/-192

Use app-owned editor suggestions

• Moves suggestion generation into the app layer and keeps the hook focused on editor integration.

apps/desktop/src/editor/use-editor-autocomplete.ts

audio-memo-provider.tsxDelegate mobile recording state to core +63/-351

Delegate mobile recording state to core

• Replaces React-managed audio-memo orchestration with the mobile audio memo service.

apps/desktop/src/mobile/audio-memo-provider.tsx

mobile-services.tsExpose mobile-specific service access +18/-0

Expose mobile-specific service access

• Provides a typed path for mobile UI to consume services grouped under the app's mobile capabilities.

apps/desktop/src/mobile/mobile-services.ts

use-paywall-gate.tsRead paywall decisions from injected services +12/-39

Read paywall decisions from injected services

• Shrinks the hook to UI wiring around mobile subscription and paywall services.

apps/desktop/src/mobile/use-paywall-gate.ts

Tests (13) +3486 / -0
create-app-services.test.tsVerify application service composition +47/-0

Verify application service composition

• Exercises construction of the app-layer service graph with supplied core and host services.

packages/app/src/create-app-services.test.ts

services.tsConstruct test app-service graphs +165/-0

Construct test app-service graphs

• Provides independently configurable app services for unit and rendered UI tests.

packages/app/src/testing/services.ts

index.tsExpose app-layer test doubles +58/-0

Expose app-layer test doubles

• Collects test factories for app services behind a dedicated testing entry point.

packages/app/src/testing/index.ts

create-core-services.test.tsTest core graph construction +243/-0

Test core graph construction

• Verifies the dependencies and service relationships created by the core composition root.

packages/core/src/app/create-core-services.test.ts

create-graph-services.test.tsTest per-session service lifecycles +325/-0

Test per-session service lifecycles

• Checks graph-service construction and conditional lifecycle execution.

packages/core/src/app/create-graph-services.test.ts

ipc-service.test.tsVerify injected IPC validation and events +286/-0

Verify injected IPC validation and events

• Tests command results, transport failures, and service-owned subscriptions against a fake bridge.

packages/core/src/ipc/ipc-service.test.ts

task-service.test.tsCover task service behavior +951/-0

Cover task service behavior

• Tests task reads and mutations directly against the new service.

packages/core/src/tasks/task-service.test.ts

services.tsBuild isolated test core-service graphs +363/-0

Build isolated test core-service graphs

• Supplies configurable service doubles and overrides for unit and rendered tests.

packages/core/src/testing/services.ts

graph-session.tsProvide a test graph-session service +307/-0

Provide a test graph-session service

• Lets tests drive graph opening and session state without a real shell.

packages/core/src/testing/graph-session.ts

ports.tsAdd test activity and progress ports +129/-0

Add test activity and progress ports

• Provides fakes for lifecycle signals and background-operation reporting.

packages/core/src/testing/ports.ts

query-invalidation.test.tsTest service-driven query invalidation +193/-0

Test service-driven query invalidation

• Covers query refresh behavior triggered by changes in the new service graph.

apps/desktop/src/lib/query-invalidation.test.ts

render-in-app.tsxRender UI with complete injectable test graphs +279/-0

Render UI with complete injectable test graphs

• Builds test core and app services, graph state, routing, and a query client for browser component tests.

apps/desktop/src/test-utils/render-in-app.tsx

render-in-app.test.tsxVerify the shared app render harness +140/-0

Verify the shared app render harness

• Tests the service-backed rendering setup used by migrated browser tests.

apps/desktop/src/test-utils/render-in-app.test.tsx

Documentation (10) +1190 / -363
AGENTS.mdDocument the new service architecture for agents +44/-11

Document the new service architecture for agents

• Updates contributor guidance around package boundaries, injected services, and testing conventions.

AGENTS.md

CONTRIBUTING.mdPoint contributors to the new service conventions +12/-6

Point contributors to the new service conventions

• Refreshes development guidance to reflect the core, app, and desktop split.

CONTRIBUTING.md

README.mdDescribe the application layer +53/-0

Describe the application layer

• Documents the responsibilities and boundaries of @reflect/app.

packages/app/README.md

README.mdDescribe core's business and port boundary +89/-43

Describe core's business and port boundary

• Updates package guidance to explain injected services and separation from application and desktop concerns.

packages/core/README.md

services.mdDocument service contracts and composition +350/-0

Document service contracts and composition

• Introduces contributor guidance for service dependencies, lifetimes, ports, test doubles, and architectural boundaries.

docs/contributing/services.md

testing.mdDocument service-backed testing +137/-75

Document service-backed testing

• Explains app and core Node tests and the shared desktop rendering harness.

docs/contributing/testing.md

editor-architecture.mdUpdate editor architecture guidance +97/-61

Update editor architecture guidance

• Describes editor integration with the moved document and autocomplete services.

docs/contributing/editor-architecture.md

adding-a-command.mdExplain service-backed command registration +105/-42

Explain service-backed command registration

• Updates the command authoring guide for app-layer command services and desktop wiring.

docs/contributing/adding-a-command.md

adding-a-setting.mdExplain settings in the new layers +55/-45

Explain settings in the new layers

• Refreshes the setting authoring workflow around injected settings and app services.

docs/contributing/adding-a-setting.md

architecture-conventions.mdRecord three-layer architecture conventions +248/-80

Record three-layer architecture conventions

• Updates architectural conventions to distinguish core business services, app orchestration, and desktop adapters.

docs/plans/architecture-conventions.md

Other (11) +886 / -27
ci.ymlPrevent Chromium test-worker memory exhaustion +9/-0

Prevent Chromium test-worker memory exhaustion

• Forces Vitest's Chromium browser pool to collect garbage between test files in CI.

.github/workflows/ci.yml

eslint.config.jsEnforce service and package boundaries +446/-0

Enforce service and package boundaries

• Adds architecture-focused restrictions, including an allowlist for module mocks and checks protecting the new service layering.

eslint.config.js

package.jsonIntroduce the @reflect/app package +27/-0

Introduce the @reflect/app package

• Declares the application-layer package and its exports and dependencies.

packages/app/package.json

vitest.node.config.tsRun app-layer tests without a browser +15/-0

Run app-layer tests without a browser

• Creates the Node Vitest project for React-free and Tauri-free application services.

packages/app/vitest.node.config.ts

vitest.setup.tsConfigure app service tests +13/-0

Configure app service tests

• Establishes setup for the new app-layer Vitest project.

packages/app/vitest.setup.ts

tsconfig.jsonConfigure application-layer TypeScript compilation +19/-0

Configure application-layer TypeScript compilation

• Adds TypeScript configuration for the new workspace package.

packages/app/tsconfig.json

package.jsonAdjust core package exports and scripts +8/-6

Adjust core package exports and scripts

• Aligns core package configuration with service APIs and Node-side tests.

packages/core/package.json

vitest.node.config.tsConfigure core's Node test project +4/-0

Configure core's Node test project

• Supports testing core business services without a desktop browser environment.

packages/core/vitest.node.config.ts

vitest.shared.tsAlign desktop tests with injected services +10/-1

Align desktop tests with injected services

• Updates shared Vitest configuration for the new testing setup.

apps/desktop/vitest.shared.ts

vitest.config.tsRegister the application-layer test project +2/-1

Register the application-layer test project

• Includes the new app Node tests alongside core and desktop test projects.

vitest.config.ts

pnpm-lock.yamlResolve the new workspace package +333/-19

Resolve the new workspace package

• Updates lockfile workspace references and package resolutions for the @reflect/app split.

pnpm-lock.yaml

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

7 issues found across 1143 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/desktop/src/services/browser-opener.ts">

<violation number="1" location="apps/desktop/src/services/browser-opener.ts:7">
P2: Browsers that drop transient activation across the clipboard await can block this popup, but `openUrl` still resolves and the GitHub flow shows no fallback URL. Keep the open in the click gesture or expose the verification link as a fallback.</violation>
</file>

<file name="CONTRIBUTING.md">

<violation number="1" location="CONTRIBUTING.md:51">
P3: This guidance does not apply to the extension's React components, which have no `useAppServices()` provider. Qualify it as desktop-specific so extension contributors are not directed to an unavailable hook.</violation>
</file>

<file name="apps/desktop/src/services/services-provider.tsx">

<violation number="1" location="apps/desktop/src/services/services-provider.tsx:41">
P3: This also throws beneath a valid provider when no graph session is open, so the error incorrectly blames missing provider context. Change it to identify the missing active graph.</violation>
</file>

<file name="apps/desktop/src/hooks/use-link-navigation-host.ts">

<violation number="1" location="apps/desktop/src/hooks/use-link-navigation-host.ts:30">
P2: A scope change away and back leaves the original key equal, so the old async activation can still navigate after its host context changed. Track a scope-change revision and compare the captured revision instead.</violation>
</file>

<file name="apps/desktop/src/mobile/classic-sign-in-progress.tsx">

<violation number="1" location="apps/desktop/src/mobile/classic-sign-in-progress.tsx:102">
P2: A transient keychain-read failure is logged, but `mutation.reset()` still closes the successful sign-in without publishing the new access. Keep this dialog open on read failure so the user can retry; `useClassicAccess` will not retry while its state has no token.</violation>
</file>

<file name="apps/desktop/src/services/browser-image-codec.ts">

<violation number="1" location="apps/desktop/src/services/browser-image-codec.ts:27">
P2: JPEG encoding drops transparency, so transparent areas in downscaled PNG or GIF attachments become black. Fill the canvas with an explicit background before drawing when `options.type` is JPEG.</violation>
</file>

<file name="apps/desktop/src/lib/shortcuts.ts">

<violation number="1" location="apps/desktop/src/lib/shortcuts.ts:40">
P2: This conversion silently drops earlier commands that share an exact binding before `KeymapService` can reject the duplicate. Build the record with an explicit duplicate check so conflicting app shortcuts fail during registration.</violation>
</file>

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.
This PR moves logic across many files. Ultrareviews find 2.4x more serious bugs than standard reviews. Comment @cubic-dev-ai ultrareview to run one.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread apps/desktop/src/services/browser-opener.ts
Comment thread apps/desktop/src/hooks/use-link-navigation-host.ts
Comment thread apps/desktop/src/mobile/classic-sign-in-progress.tsx
Comment thread apps/desktop/src/services/browser-image-codec.ts Outdated
Comment thread apps/desktop/src/lib/shortcuts.ts
Comment thread CONTRIBUTING.md Outdated
Comment thread apps/desktop/src/services/services-provider.tsx Outdated
@qodo-code-review

qodo-code-review Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (8) 📘 Rule violations (4)

⚠️ 3 lower-priority findings omitted to fit the comment size limit; re-run the review or view the findings in the Qodo portal.

Grey Divider


Remediation recommended

1. Credential read failures stall connection 🐞 Bug
Description
onAuthed starts githubAuth.load().then(...) without handling rejection, so a failed credential
read never calls #finish. When the secret-store read rejects after authentication, the wizard
remains at its finish step without passing the error through its normal failure handler.
Code

packages/core/src/sync/connect-github-wizard-service.ts[R245-250]

+  onAuthed(authedUser: GithubUser): void {
+    this.#patch({ user: authedUser, step: 'finish' })
+    void this.#deps.githubAuth.load().then((auth) => {
+      const kind = auth?.kind ?? null
+      this.#patch({ authKind: kind })
+      return this.#finish(authedUser, { kind })
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A credential reload rejection leaves the GitHub connection wizard at its finish step without an error.
## Fix Focus Areas
- packages/core/src/sync/connect-github-wizard-service.ts[245-251]
- packages/core/src/sync/connect-github-wizard-service.ts[456-465]
## Recommended Fix
Handle rejection from `githubAuth.load()` through the wizard's existing error-state flow, and test a rejected secret-store read after authentication.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


View medium (11)
2. Closed graphs can retain sync listeners 🐞 Bug
Description
ICloudSyncService.start awaits watchStart without checking #disposed again before registering
its listeners. If a graph closes during that await, dispose removes the listeners available at the
time, but the resumed start registers new ones after teardown.
Code

packages/core/src/sync/icloud-sync-service.ts[R184-187]

+  async start(): Promise<void> {
+    const { icloudNative, fileChanges, lifecycle, clock, logger } = this.#deps
+    const { root, metadataWatch } = this.#options
+    if (this.#disposed) {
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An iCloud watch start that finishes after graph teardown can register listeners on a disposed service.
## Fix Focus Areas
- packages/core/src/sync/icloud-sync-service.ts[184-198]
- packages/core/src/sync/icloud-sync-service.ts[262-288]
- packages/core/src/app/create-graph-services.ts[155-161]
## Recommended Fix
Recheck disposal after the watch-start await and avoid registering subscriptions or scheduling work on a disposed service. Cover disposal while watch startup is pending.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


3. External move scans stall on many files 📘 Rule violation
Description
detectExternalMoves awaits noteFile.read inside its loop over every arrival path instead of
starting independent reads together. A graph with many candidate files therefore waits for each
filesystem read in sequence, while the existing per-file catch could preserve failures after
concurrent reads.
Code

packages/core/src/indexing/move-healing.ts[R85-86]

+      try {
+        const source = await noteFile.read(path, generation)
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`detectExternalMoves` performs independent arrival-file reads sequentially, increasing external move detection latency for graphs with many candidate paths.

## Fix Focus Areas
- packages/core/src/indexing/move-healing.ts[81-92]

## Recommended Fix
Map arrival paths to promises and await them with `Promise.all` or `Promise.allSettled`, retaining each path's content and parsed identifier while converting individual read failures into skipped candidates and preserving the abort behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


4. Moved notes linger in the pinned shelf 🐞 Bug
Description
usePinnedNotes synthesizes a shelf entry for every pinned overlay path missing from the index, but
note moves change the file path without transferring or clearing that overlay. If a pin or reorder
is still awaiting index reconciliation when a title change moves the note, the indexed entry appears
at the new path while the old-path overlay continues to produce a stale shelf entry.
Code

apps/desktop/src/hooks/use-pinned-notes.ts[R68-69]

+  return useMemo(
+    () => applyPinnedNoteOverlays(data ?? EMPTY, overlays, preview),
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A pending pin or reorder overlay remains keyed to a note's old path after a title-driven move, and the pinned shelf renders that path as an additional entry.

## Fix Focus Areas
- apps/desktop/src/hooks/use-pinned-notes.ts[54-69]
- packages/core/src/documents/note-row-service.ts[234-260]
- packages/core/src/documents/title-rename-service.ts[125-140]

## Recommended Fix
Transfer or retire path-keyed pin overlays when a note move succeeds. Ensure the shelf does not synthesize an entry for an old path once its note has moved, and test a move before the index catches up.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


5. Pinned-note fallback derives titles twice 📘 Rule violation
Description
titleFromPath repeats the readable-filename derivation already implemented by noteTitle in the
sharing service. When filename-title rules change, the pinned-note fallback and share subject can
diverge because each path strips the Markdown suffix independently.
Code

apps/desktop/src/hooks/use-pinned-notes.ts[R16-18]

+function titleFromPath(path: string): string {
+  const name = path.split('/').at(-1) ?? path
+  return name.endsWith('.md') ? name.slice(0, -3) : name
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The pinned-note fallback duplicates the existing filename-to-title derivation in the sharing service, allowing the two behaviors to diverge when title normalization changes.

## Fix Focus Areas
- apps/desktop/src/hooks/use-pinned-notes.ts[16-18]
- packages/core/src/mobile/share-service.ts[11-15]

## Recommended Fix
Export the existing note-title helper from the shared core module, import it in the pinned-notes hook, and remove the local `titleFromPath` implementation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


6. A recreated note stays hidden from lists 🐞 Bug
Description
reconcileEntry retains a trashed overlay when a row exists at the same path, while
usePinnedNotes reconciles only returned pinned rows and cannot establish that a deleted note has
disappeared from the index. If the note is recreated or restored before an unfiltered All Notes read
observes the missing path, the retained overlay filters it from All Notes and the pinned shelf until
the graph is reopened.
Code

packages/core/src/documents/note-row-service.ts[R304-309]

+  function store(path: string, generation: number, overlay: MutableNoteRowOverlay): void {
+    if (Object.keys(overlay).length === 0) {
+      overlays.delete(path)
+    } else {
+      overlays.set(path, { generation, overlay })
+    }
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A retained `trashed` overlay hides a note recreated or restored at the same path when no reader observed its absence from the index first. The pinned query cannot establish that absence on its own because it also omits existing unpinned notes.

## Fix Focus Areas
- packages/core/src/documents/note-row-service.ts[293-317]
- packages/core/src/documents/note-row-service.ts[313-324]
- packages/core/src/documents/note-row-service.ts[404-418]
- packages/core/src/documents/note-actions-service.ts[325-349]
- apps/desktop/src/hooks/use-pinned-notes.ts[41-52]

## Recommended Fix
Arrange an authoritative absence check for trashed paths after index updates, or record a tombstone time or sequence so reconciliation can distinguish a later note at the same path. Retire `trashed` when the index has dropped the original note and a row subsequently appears; alternatively, have note creation and file-change upserts clear the overlay through `NoteRowService`. Do not mark the pinned query itself as a complete note list. Add a test that trashes a note, drops it from the index, recreates or restores it without opening All Notes, and verifies that both `applyNoteRowOverlays` and the pinned shelf show it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


7. Unpinned notes can stay hidden from the pinned shelf 🐞 Bug
Description
usePinnedNotes reconciles only the notes the index lists as pinned (always with isPinned: true),
so an { isPinned: false } overlay from an unpin can never be retired by the shelf reader. When the
unpinned note is not open and All Notes is not mounted, that overlay lasts for the whole graph
session. A later re-pin from outside the app (git or iCloud sync, an agent or CLI edit) is then
hidden from the shelf and from the note's sidebar toggle, because retireAgreeing only drops fields
the index agrees with.
Code

apps/desktop/src/hooks/use-pinned-notes.ts[R41-51]

+  useEffect(() => {
+    if (generation !== undefined && data !== undefined && overlays.size > 0) {
+      noteRows.reconcileRows(
+        generation,
+        data.map((note) => ({
+          path: note.path,
+          isPinned: true,
+          pinnedOrder: note.pinnedOrder ?? null,
+        })),
+      )
+    }
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The pinned-shelf reconcile only sees pinned rows, so an `isPinned: false` overlay is never retired by it. A later external re-pin stays hidden for the rest of the graph session.

## Fix Focus Areas
- apps/desktop/src/hooks/use-pinned-notes.ts[41-52]
- packages/core/src/documents/note-row-service.ts[361-376]

## Recommended Fix
The pinned query lists every pinned note, so treat it as complete for the pin fields. In the effect, also reconcile every overlay path with `isPinned === false` that is missing from `data`, passing `{ path, isPinned: false }`. Alternatively, add a `completeFor: 'pinned'` option to `reconcileRows` that does the same. Add a test: unpin a note that is not open, the index drops it from the shelf, the overlay retires, then an external re-pin shows up on the shelf.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


8. Semantic search silently stops indexing on failure 🐞 Bug
Description
backfill turns any error from the note-list query into the result 'failed' without logging it,
and follow throws that result away with .then(() => {}). When the backfill can't read the note
list, notes that already existed never get embeddings and nothing appears in the logs; the .catch
that logs 'embedding backfill failed' is unreachable because backfill never rejects.
Code

packages/core/src/embeddings/embedding-index-service.ts[R252-255]

+        if (stopped.signal.aborted) {
+          return
+        }
+        return backfill({ generation, modelId, signal: stopped.signal }).then(() => {})
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`EmbeddingIndexService.backfill` catches every error and returns `'failed'` without logging it. `follow` drops that result, so a backfill that can't read the note list fails with no log, and the `.catch` logger in `follow` never runs.

## Fix Focus Areas
- packages/core/src/embeddings/embedding-index-service.ts[238-261]

## Recommended Fix
In `backfill`, catch the cause and call `logger.error('embedding backfill failed:', cause)` before returning `'failed'`. Alternatively, have `follow` inspect the result and log when it is `'failed'`. Then remove or reword the now-unreachable `.catch` comment.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


9. Platform overrides leave mobile disabled 🐞 Bug
Description
composeAppServices gives the mobile factory adapters.platform rather than the platform produced
by the replaceable platform factory. If a caller replaces that factory with mobile platform facts
while supplying desktop adapter facts, the returned platform reports mobile but the default mobile
factory returns null; file-change echo and recorder selection also use the adapter facts.
Code

packages/core/src/app/create-app-services.ts[R904-907]

+  const mobileServices = create.mobile({
+    platform: adapters.platform,
+    build: createMobileServices,
+  })
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Some composition decisions use raw adapter platform facts instead of the replaceable composed platform service.

## Fix Focus Areas
- packages/core/src/app/create-app-services.ts[697-710]
- packages/core/src/app/create-app-services.ts[728-741]
- packages/core/src/app/create-app-services.ts[904-910]

## Recommended Fix
Feed the composed platform into mobile gating, recorder selection, and file-change configuration. Test a platform-factory replacement whose facts differ from the adapter.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


10. New images miss automatic descriptions 🐞 Bug
Description
IndexLifecycleService.applyChanges filters asset paths out of its applied event, although the
asset-description session looks for asset upserts in that event. For an asset-only watcher batch,
liveIndex.apply also returns zero mutations, so no event is emitted and the image is not queued
for description until another wake or backfill.
Code

packages/core/src/indexing/index-lifecycle-service.ts[567]

+          changes: changes.filter((change) => isNotePath(change.path)),
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The post-apply event excludes asset paths and is suppressed for asset-only batches, so the asset-description listener never queues newly added images.

## Fix Focus Areas
- packages/core/src/indexing/index-lifecycle-service.ts[562-569]
- packages/core/src/assets/asset-description-service.ts[619-640]

## Recommended Fix
After the applicable note-index work settles, emit the full relevant batch, including eligible asset changes. Do not require a note mutation for an asset-only batch; add a test that sends an asset-only watcher event and checks that description work is scheduled.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


11. Asset updates read notes one by one 📘 Rule violation
Description
markAssetsFromNotes awaits noteFile.read inside its loop over changed note paths. When several
notes change together, each filesystem read delays the next even though failures are already
converted into a per-note skip and the collected asset paths can be merged after concurrent reads.
Code

packages/core/src/assets/asset-description-service.ts[R591-601]

+    async function markAssetsFromNotes(notePaths: readonly string[]): Promise<void> {
+      const referenced = new Set<string>()
+      for (const notePath of notePaths) {
+        if (loop.isStale()) {
+          return
+        }
+        let source: string
+        try {
+          source = await noteFile.read(notePath, generation)
+        } catch {
+          continue // deleted/unreadable since the change — nothing to re-evaluate
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Asset-description refreshes process changed note files with sequential awaits, making batches of independent note reads unnecessarily slow.

## Fix Focus Areas
- packages/core/src/assets/asset-description-service.ts[591-601]

## Recommended Fix
Create one promise per note path and await the reads with `Promise.allSettled` or a `Promise.all` wrapper that returns a skipped result for read failures. Preserve stale-loop checks and the current behavior of ignoring deleted or unreadable notes, then parse successful results and merge their eligible asset paths.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


12. Link resolution reads candidates one by one 📘 Rule violation
Description
diskTitleMatches awaits noteFile.read inside the candidate loop instead of starting the
independent reads together. A title with many slug-family candidates therefore waits for each
filesystem read in sequence, while one missing candidate is already handled locally and can be
preserved with concurrent reads.
Code

packages/core/src/graph/note-link-resolver-service.ts[R133-146]

+    for (const candidate of candidates) {
+      if (candidate.placeholder === true) {
+        unavailablePaths.push(candidate.path)
+        continue
+      }
+      let source: string
+      try {
+        source = await noteFile.read(candidate.path, generation)
+      } catch {
+        // A candidate that vanishes after listing is not proven absent: sync
+        // may restore it before an atomic claim. Preserve the creation guard.
+        unavailablePaths.push(candidate.path)
+        continue
+      }
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Disk fallback link resolution performs independent candidate-file reads sequentially, increasing latency for titles with many possible matches.

## Fix Focus Areas
- packages/core/src/graph/note-link-resolver-service.ts[133-146]

## Recommended Fix
Map candidates to read promises and await them with `Promise.all` or `Promise.allSettled`, preserving candidate order and the existing per-candidate handling for placeholders, missing files, and read failures. Keep the classification of unavailable candidates unchanged while allowing independent reads to run concurrently.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


Grey Divider

Resolved findings
1. Concurrent note edits can lose content ✗ Dismissed
Description
commitFrontmatter reads and writes a note outside the per-path chain used by commitSourceEdit.
When a closed note receives a frontmatter change and a source edit concurrently, both can read the
old file and the later write discards the earlier change.
Code

packages/core/src/documents/note-document-service.ts[R165-168]

+    const onDisk = await this.#readOrEmpty(path, generation)
+    const patched = upsertFrontmatter(onDisk, frontmatterPatchToYaml(patch))
+    if (patched !== onDisk) {
+      await noteFile.write(path, patched, generation)
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Concurrent frontmatter and source edits can overwrite each other because only source edits enter the per-path write chain.
## Fix Focus Areas
- packages/core/src/documents/note-document-service.ts[155-195]
- packages/core/src/documents/note-document-service.ts[221-237]
## Recommended Fix
Run frontmatter commits through the same per-path serialization as source edits, including the fallback when an open session declines the patch. Test concurrent writes to a closed note.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Failed backup assertions leak live watchers ✗ Dismissed
Description
The backup-service tests call backup.dispose() only after their assertions instead of placing it
in a finally block. If any expectation or awaited probe fails, the started backup service keeps
its file-change subscription and background work alive for later tests.
Code

packages/core/src/sync/backup-service.test.ts[R269-272]

+  return createBackupService(backupDeps, {
+    generation: GENERATION,
+    indexGeneration: options.indexGeneration,
+  })
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Backup-service tests start a resource-owning service and dispose it only after assertions, so failed assertions can leak subscriptions and background work into subsequent tests.

## Fix Focus Areas
- packages/core/src/sync/backup-service.test.ts[269-272]

## Recommended Fix
Wrap each test's started backup service and assertions in a `try/finally` block, calling `backup.dispose()` from the `finally` clause so cleanup runs on both passing and failing paths.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Gist failures trigger needless reconnects ✗ Dismissed
Description
The gist create, update, and delete methods classify every 403 as an authentication failure, while
create also classifies every 404 as authentication. Rate limits, SSO or access restrictions, missing
gists, and other non-credential failures can therefore prompt a reconnect instead of reporting a
retryable or operation-specific error.
Code

packages/core/src/sync/gist-service.ts[R98-105]

+    if (response.status === 404) {
+      throw new ReflectError(
+        'auth',
+        'GitHub refused gist access (404) — reconnect GitHub to grant it',
+      )
+    }
+    if (response.status === 401 || response.status === 403) {
+      throw new ReflectError('auth', `GitHub rejected the token (${response.status})`)
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Gist operations map broad 403 responses and create-time 404 responses to authentication errors, although those statuses do not prove that credentials were rejected.

## Fix Focus Areas
- packages/core/src/sync/gist-service.ts[98-109]
- packages/core/src/sync/gist-service.ts[128-136]
- packages/core/src/sync/gist-service.ts[147-153]

## Recommended Fix
Map only 401 and explicitly confirmed bad-credentials 403 responses to auth. Preserve 404 as a missing-resource or access result according to the operation, map rate-limit and SSO responses to retryable/network errors, and add tests for ambiguous 403 and 404 responses.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Repo rate limits look like bad tokens ✗ Dismissed
Description
createRepo maps every 403 except a body containing not accessible, and getRepo maps every 403,
to ReflectError('auth', ...). Rate limits, SSO restrictions, and other non-credential 403
responses therefore reach callers as invalid-token failures instead of retryable or general I/O
errors.
Code

packages/core/src/sync/github-repo-service.ts[R156-164]

+    if (response.status === 403) {
+      const body = (await response.text()).toLowerCase()
+      if (body.includes('not accessible')) {
+        return null
+      }
+      throw new ReflectError('auth', 'GitHub rejected the token (403)')
+    }
+    if (response.status === 401) {
+      throw new ReflectError('auth', 'GitHub rejected the token (401)')
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
GitHub repository operations treat broad 403 responses as authentication failures, even when GitHub is rate-limiting or enforcing SSO or another permission restriction.

## Fix Focus Areas
- packages/core/src/sync/github-repo-service.ts[156-164]
- packages/core/src/sync/github-repo-service.ts[178-185]

## Recommended Fix
Inspect the 403 response body and map only explicit bad-credentials responses to the auth category. Map rate limits, SSO restrictions, and other 403 responses to the retryable/network or general I/O category, and add tests for both bad-credentials and non-auth 403 responses.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Dev index tests leave database handles open ✗ Dismissed
Description
dev-index-db.test.ts opens a DevIndexDb for each scenario and connects services to it without
ever calling its newly added close() method. Each test that reaches connectIndex retains the
SQLite handle after assertions, and a failed assertion skips any later cleanup just as a successful
test does.
Code

apps/desktop/src/dev/dev-index-db.test.ts[R112-123]

+  const ipc = createIpcBridgeService({
+    transport: {
+      invoke: async (command, args) => {
+        if (command !== 'db_query') {
+          throw new Error(`Unexpected command: ${command}`)
+        }
+        const sql = String(args['sql'])
+        const params = (args['params'] as readonly unknown[]) ?? []
+        captured.push({ sql, params })
+        return db.query(sql, params)
+      },
+      listen: async () => () => {},
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The dev index database tests open a database but do not release it, despite `DevIndexDb` now exposing `close()`. Test failures and normal completion both leave SQLite handles alive.

## Fix Focus Areas
- apps/desktop/src/dev/dev-index-db.test.ts[92-93]
- apps/desktop/src/dev/dev-index-db.test.ts[108-123]
- apps/desktop/src/dev/dev-index-db.ts[38-40]

## Recommended Fix
Wrap each test's database usage in `try/finally` and call `db.close()` in the `finally` block, or register equivalent per-test cleanup that runs after failures. Keep the cleanup paired with every `openDb()` call and add a failure-path regression test if the test harness supports one.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Asset tests leak live sessions ✗ Dismissed
Description
The describable test helper opens an asset-description session, but most tests never call
session.dispose(). After an assertion failure or normal test completion, the session remains
subscribed to index events and can retain background work for later tests.
Code

packages/core/src/assets/asset-description-service.test.ts[R539-542]

+    const session = context.service.openSession({
+      generation: GENERATION,
+      indexGeneration: INDEX_GENERATION,
+    })
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The asset-description tests open lifecycle sessions without reliably disposing them, so failed assertions can leave event listeners and background work active across the test suite.

## Fix Focus Areas
- packages/core/src/assets/asset-description-service.test.ts[539-542]
- packages/core/src/assets/asset-description-service.test.ts[546-746]

## Recommended Fix
Wrap each test that receives an opened session in a `try`/`finally` block and call `session.dispose()` in the `finally` block, including tests that await background work and tests that assert failures. Keep the explicit disposal test, and add a failure-path assertion if needed to verify listeners are removed when an expectation or asynchronous operation fails.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Task edits can alter another graph ✓ Resolved
Description
writeTask resolves the current graph generation when the write executes, rather than retaining the
graph that supplied the task and its relative note path. If a Tasks-view mutation waits for
cache.snapshot() while the user switches graphs, its old task can be toggled, edited, or removed
in a matching note in the new graph; inserting a task can also create a note there.
Code

packages/core/src/tasks/task-service.ts[R378-379]

+    async writeTask(task, actions) {
+      return await writeOne(requireGraphSession(graphSession).generation, task, actions)
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Tasks-view mutations can start with a task from one graph but execute after another graph becomes active. The service then uses the replacement graph's generation with the original task's relative note path.

## Fix Focus Areas
- packages/core/src/tasks/task-service.ts[370-419]
- apps/desktop/src/lib/tasks/use-task-actions.ts[114-120]
- apps/desktop/src/lib/tasks/use-task-actions.ts[283-290]

## Recommended Fix
Carry the initiating graph's identity through each asynchronous mutation and reject the write if the active session no longer matches it before reading or writing the note. Preserve that identity across the whole batch, and test a graph switch while mutation setup is pending.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Task tests leak database handles ✗ Dismissed
Description
The task-service test setup creates a fresh TestDatabaseService for every test but never calls its
close() method. Tests that query or seed the database open an in-memory SQLite handle, and any
failed assertion or test completion leaves that handle alive for the rest of the suite.
Code

packages/core/src/tasks/task-service.test.ts[R78-92]

+beforeEach(() => {
+  database = createTestDatabaseService()
+  noteFile = createTestNoteFileService()
+  openDocuments = createOpenDocumentsService({ fileChanges: createTestFileChangeService() })
+  operations = createTestOperationsService()
+  clock = createTestClockService()
+  logger = createTestLoggerService()
+  tasks = createTaskService({
+    database,
+    noteDocuments: createNoteDocumentService({ openDocuments, noteFile }),
+    operations,
+    clock,
+    logger,
+    graphSession,
+  })
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The task-service tests create a database in `beforeEach` but do not release it, so opened SQLite resources remain alive after each test and are not cleaned up when assertions fail.

## Fix Focus Areas
- packages/core/src/tasks/task-service.test.ts[78-92]

## Recommended Fix
Add an `afterEach` cleanup that closes the current `database` in a `finally`-safe cleanup path, and add a regression test or setup coverage that verifies the database is closed when a test operation fails.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Graph tests leave watchers running ✗ Dismissed
Description
The starts every lifecycle of the main window test calls setup(), which starts backup, chat,
attachment, capture, and asset-description lifecycles without aborting the returned controller. When
the assertions finish or fail, those live watchers and sessions are not disposed before the test
exits.
Code

packages/core/src/app/create-graph-services.test.ts[R98-105]

+  it('starts every lifecycle of the main window over the session generations', async () => {
+    const context = setup()
+    await context.app.settings.whenLoaded()
+
+    expect(context.graph.backup).toBe(context.backups.backup)
+    expect(context.backups.creations()).toEqual([{ generation: 3, indexGeneration: 5 }])
+    expect(context.backups.backup.calls()).toEqual([{ method: 'start' }])
+    expect(context.chats).toEqual([{ attached: 1, detached: 0 }])
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
This test starts multiple graph-scoped lifecycles but never disposes them, and assertion failures would also bypass any teardown placed after the assertions.

## Fix Focus Areas
- packages/core/src/app/create-graph-services.test.ts[98-115]

## Recommended Fix
Wrap the setup and assertions in a `try/finally` block and call `context.controller.abort()` in `finally`. Apply the same cleanup pattern to every test that calls `setup()` and starts graph-scoped services.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Failed assertions leave listeners active ✗ Dismissed
Description
installQueryInvalidation registers event listeners and the test calls stop() only after its
assertions. If an expectation throws, the listener remains subscribed and can affect later tests or
retain service state.
Code

apps/desktop/src/lib/query-invalidation.test.ts[R97-99]

+    await app.indexLifecycle.rebuildVisibly(5)
+    expect(invalidate).toHaveBeenCalledWith({ queryKey: queryKeys.index.all })
+    stop()
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The test stops the query-invalidation listeners only after assertions, so a failed assertion skips cleanup and leaves subscriptions active.

## Fix Focus Areas
- apps/desktop/src/lib/query-invalidation.test.ts[90-99]

## Recommended Fix
Wrap each test's resource-owning setup and assertions in `try/finally`, calling the returned `stop()` from the `finally` block. Ensure the failure path is covered so listener cleanup is verified even when an assertion or operation rejects.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Classic sign-in leaves the paywall up ✓ Resolved
Description
useClassicAccess now relies on classic.start() and observable service state, but a successful
classic.signIn() saves access without publishing it or initiating another read. When verification
finishes after the initial read, the paywall continues to see classic.active === false, and the
mounted sign-in flow does not refresh that state.
Code

apps/desktop/src/mobile/use-classic-access.ts[22]

+  useEffect(() => classic.start(), [classic])
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Successful Classic sign-in stores access but leaves the observable access state unchanged, so the paywall remains visible.

## Fix Focus Areas
- packages/core/src/classic/classic-access-service.ts[354-361]
- apps/desktop/src/mobile/use-classic-access.ts[18-22]

## Recommended Fix
After the access record is successfully saved, publish the new access through the service's existing state-update path. Add a test that completes sign-in after the initial read and checks that the paywall gate updates.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Timeouts escape as raw fetch errors ✗ Dismissed
Description
getAuthenticatedUser awaits http.fetch and maps only HTTP status codes, so transport and timeout
exceptions are not converted to the service's network error type. When the request is aborted or
cannot connect, getSignedInUser and callers receive the raw exception instead of the retryable
failure expected for network problems.
Code

packages/core/src/sync/github-auth-service.ts[R389-407]

+  async function getAuthenticatedUser(token: string): Promise<GithubUser> {
+    const response = await http.fetch('https://api.github.com/user', {
+      headers: apiHeaders(token),
+    })
+    if (response.status === 401) {
+      throw new ReflectError('auth', 'GitHub rejected the token (401)')
+    }
+    if (response.status === 403) {
+      const body = (await response.text()).toLowerCase()
+      if (body.includes('bad credentials')) {
+        throw new ReflectError('auth', 'GitHub rejected the token (403)')
+      }
+      throw new ReflectError(
+        'network',
+        'GitHub temporarily refused the signed-in user lookup (403, likely rate limiting)',
+      )
+    }
+    if (!response.ok) {
+      throw new ReflectError('network', `looking up the signed-in user failed (${response.status})`)
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
GitHub authentication requests do not convert timeout or transport exceptions into the service's network error type, so callers cannot reliably classify them as retryable failures.

## Fix Focus Areas
- packages/core/src/sync/github-auth-service.ts[389-407]

## Recommended Fix
Wrap the `http.fetch` call and response parsing in error handling that converts timeout, abort, and connection failures into `ReflectError('network', ...)`, while preserving the existing precise mappings for authentication and HTTP status failures. Add tests covering an aborted or timed-out request and verify that `getAuthenticatedUser` and `getSignedInUser` reject with a network error without clearing valid credentials.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Old task edits can alter the new list ✓ Resolved
Description
#writeEach checks the original graph’s abort signal before writeTask, but applies the returned
task relocations without checking it again after the await. If the user switches graphs while a
write is pending, the new graph’s task cache can be re-keyed using the old graph’s result when the
graphs contain matching note paths.
Code

packages/core/src/tasks/task-service.ts[R1106-1107]

+      const result = await this.writeTask(task, actionsFor(task))
+      this.#relocate(root, task.notePath, result.moved)
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An in-flight task write can apply its old graph’s relocation result to the task cache after a graph switch.

## Fix Focus Areas
- packages/core/src/tasks/task-service.ts[1094-1111]
- packages/core/src/tasks/task-service.ts[1078-1088]

## Recommended Fix
After each awaited write, verify that the captured graph session is still active before patching task rows. Keep the write’s graph identity attached to any subsequent cache reconciliation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. Task changes can revert on screen ✓ Resolved
Description
completeTasks, reopenTasks, and scheduleTasks patch cached rows without invalidating a pending
read, unlike mutations that first call #snapshot. If that older read resolves after the optimistic
patch, #read replaces it with pre-write rows, which both desktop and mobile screens render until
another refresh.
Code

packages/core/src/tasks/task-service.ts[R618-623]

+  async completeTasks(tasks: readonly OpenTask[]): Promise<void> {
+    const { root } = this.#begin()
+    this.#patch(
+      (rows) => withoutTasks(rows, tasks),
+      (rows) => asCompleted(rows, tasks),
+    )
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Pending task reads can overwrite optimistic bulk changes with older rows.
## Fix Focus Areas
- packages/core/src/tasks/task-service.ts[618-675]
- packages/core/src/tasks/task-service.ts[1009-1033]
- packages/core/src/tasks/task-service.ts[1095-1103]
- packages/core/src/tasks/task-service.ts[1155-1160]
## Recommended Fix
Invalidate pending read tokens before bulk optimistic patches, including deletion and conversion, and test a read resolving after a successful write.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. Closed graphs restart local history ✓ Resolved
Description
createGraphServices starts the backup asynchronously and disposes it on session abort, but
startLocalHistory can resume after its git.setup await and create an engine despite that
disposal. If a graph is switched while setup is pending, the ended session can acquire a new
file-change subscription and launch a local-history cycle that its teardown never sees.
Code

packages/core/src/app/create-graph-services.ts[111]

+    void mainBackup.start()
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A graph session can abort while its backup service awaits repository setup. Startup then creates an engine after disposal, leaving a local-history lifecycle running for the ended session.

## Fix Focus Areas
- packages/core/src/app/create-graph-services.ts[107-114]
- packages/core/src/sync/backup-service.ts[273-294]
- packages/core/src/sync/backup-service.ts[515-519]

## Recommended Fix
Check `disposed` immediately after the awaited setup and before creating or adopting the local-history engine. Add a test that holds setup pending, disposes the service, then resolves setup and verifies that no engine or commit cycle starts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. A substituted resolver misses link searches ✓ Resolved
Description
withOverrides(createIndexReadServices(deps)) replaces wikiResolver only after the group has
constructed search with the original resolver. When a test overrides wikiResolver, link-filter
searches still resolve targets through the original service even though services.wikiResolver is
the substitute.
Code

packages/core/src/testing/services.ts[178]

+    indexReads: (deps) => withOverrides(createIndexReadServices(deps)),
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Replacing a grouped resolver leaves other services in the group bound to the original resolver.

## Fix Focus Areas
- packages/core/src/testing/services.ts[143-153]
- packages/core/src/testing/services.ts[175-183]
- packages/core/src/app/create-app-services.ts[437-454]

## Recommended Fix
Construct grouped services using overridden members before creating their dependents, so an overridden wiki resolver reaches search. Cover a link-filter search with an overridden resolver.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. Custom test clocks miss operation timers ✓ Resolved
Description
createTestAppServices passes defaults.clock to the operations factory even when
overrides.clock supplies the clock exposed by the app. When a test advances its supplied clock,
operation visibility and linger timers remain scheduled on the separate default clock.
Code

packages/core/src/testing/services.ts[293]

+    ...createTestServiceFactories({ ipc, platform, clock: defaults.clock }, overrides),
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A clock override does not control the test operations service's timers.

## Fix Focus Areas
- packages/core/src/testing/services.ts[139-160]
- packages/core/src/testing/services.ts[266-295]

## Recommended Fix
Pass the effective test clock to the operations factory, or have that factory use the clock dependency supplied by composition. Add a test that advances an overridden clock and observes an operation expire.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can turn these tips off under Display > Comment parts

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread packages/core/src/sync/github-auth-service.ts Outdated
@greptile-apps

greptile-apps Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

[High impact] Refactors service architecture across the entire desktop app.

Fix indexing after graph creation and task rollback across graph switches before merging.

Findings

  1. P1 New graphs stop indexing ▶
  2. P1 Old tasks replace current tasks ▶

Summary

Moves TypeScript logic into injected services across @reflect/core, @reflect/app, and desktop adapters.

  • Each webview builds separate core and app services around its host adapters.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Desktop["Desktop adapters"] --> Host["Window, lifecycle, operations"]
  Desktop --> Core["createCoreServices"]
  Host --> Core
  Core --> App["createAppServices"]
  Host --> App
  Core --> Session["GraphSession"]
  Session --> Graph["Per-graph services"]
  App --> React["React providers"]
  Core --> React
Loading

Reviews (2) · Last reviewed commit: "Merge branch 'service_r4_h6' into servic..." · Reviewed by Greptile

Comment thread apps/desktop/src/mobile/use-classic-access.ts
Comment thread apps/desktop/src/lib/query-invalidation.test.ts Outdated
Comment thread packages/core/src/app/create-graph-services.test.ts Outdated
Comment thread packages/core/src/tasks/task-service.test.ts Outdated
Comment thread packages/core/src/tasks/task-service.ts Outdated
Comment thread packages/core/src/assets/asset-description-service.test.ts
@ocavuebot

Copy link
Copy Markdown
Collaborator Author

Thanks for the walkthroughs. The CodeRabbit, Sourcery and Greptile summaries describe the change and report no blocking finding, so there is nothing to act on from them. Every qodo and cubic inline thread has a reply and is resolved; the qodo summary findings without an inline thread are answered in a separate comment.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/core/src/app/platform-service.ts Outdated
Comment thread packages/core/src/app/create-core-services.ts Outdated
Comment thread packages/core/src/app/hash-service.ts Outdated
ocavue added 20 commits October 13, 2026 20:42
@ocavuebot ocavuebot changed the title refactor: dependency-injected services for @reflect/core refactor: split core, app and desktop with dependency-injected services Oct 13, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 1 file (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread packages/core/src/assets/asset-description-service.test.ts
@ocavue
ocavue merged commit b094504 into master Oct 13, 2026
12 checks passed
@ocavue
ocavue deleted the service branch October 13, 2026 11:11
@ocavuebot
ocavuebot restored the service branch October 14, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants