Status: planned; source audit complete, implementation not started
Date: 2026-07-27
Audit snapshot: dirty working tree on feature/aiden-assistant-plan-777723 at 7299340282f84fb816f1615f54a27bf97390f6fe; findings refer to the current filesystem, not only HEAD
Scope: Electron main/preload, React renderer, native helpers, storage, IPC, networking, background services, packaging, and macOS lifecycle
Aiden has several good foundations: renderer sandboxing and typed IPC boundaries, bounded updater cadence, generation abort bookkeeping, terminal cleanup on renderer destruction, error boundaries, and a broad passing test suite. The performance problem is not one bad framework choice. It is a set of unbounded or always-on paths that compound:
- Release-blocking durability and memory risks: chat JSON is overwritten in place, unreadable state can silently fall back to defaults, attachments and recordings lack aggregate bounds, and chat history repeatedly carries base64 attachment payloads.
- Proven idle battery work: a completed streaming message can retain a perpetual animation-frame loop, while a normal folder-backed chat can launch approximately six Git subprocesses every five seconds.
- Main-thread responsiveness risks: local transcription loads and runs a roughly 600 MB native recognizer synchronously in Electron main; repository search and some helper/network paths lack complete cancellation and deadlines.
- Renderer scaling costs: streaming repeatedly copies and reparses growing strings, transcript scrolling has overlapping layout triggers, and closed or high-cardinality surfaces still do catalog/list work.
- Lifecycle gaps: MCP clients, renderer crash recovery, scheduled catch-up, model installation, and shutdown do not yet share bounded ownership and recovery contracts.
- Startup and distribution overhead: the rebuilt renderer entry is 2.54 MB minified / 764 KB gzip, the full Highlight.js catalog is eager, and production globs include about 9.8 MB of generated source maps.
- No trustworthy performance feedback loop: there are no packaged-runtime budgets for event-loop delay, wakeups, helper/process counts, IPC volume, disk bytes, heap growth, crash loops, or energy impact.
The first implementation milestone should therefore be data safety and bounded memory, not cosmetic micro-optimization. The next should remove work that continues when the user is idle. Heavy native and repository work should then move behind owned, cancellable workers. Renderer and bundle work follows once the app can measure the result.
- Never lose acknowledged chat or settings data because a write was interrupted.
- Never allocate memory proportional to an arbitrary file, recording, tool result, or renderer-supplied history.
- Keep Electron main responsive during local voice, repository search, downloads, MCP activity, and shutdown.
- Reach zero app-owned animation frames and zero periodic Git subprocesses when idle or hidden.
- Preserve immediate chat/composer availability and smooth perceived streaming.
- Bound background work on battery, while minimized, while locked, and across suspend/resume.
- Make long chats, large workspaces, model catalogs, diffs, terminals, and attachments scale predictably.
- Enforce startup, package, memory, cancellation, and idle-energy budgets before release.
- Rewriting Aiden away from Electron or React.
- Replacing every JSON store with a database before measurement shows that atomic JSON plus an append journal is insufficient.
- Reducing visual quality speculatively. Transparency, vibrancy, blur, and motion should change only after packaged traces prove their cost.
- Lazy-loading the primary chat shell or composer. Secondary routes and heavy optional surfaces are the split candidates.
- Polling models.dev from normal runtime paths; the existing explicit refresh/release boundary remains intact.
| Priority | Confirmed issue | Evidence | User-visible impact |
|---|---|---|---|
| P0 | Chat and index files are overwritten directly; parse failure becomes an empty index or missing chat | main/services/chat-store-core.ts |
A crash, power loss, disk-full event, or partial write can hide or destroy history |
| P0 | Attachments have no count or aggregate budget; text files are fully read before truncation; files are read concurrently | main/services/attachments.ts, main/handlers/attachments.ts, main/handlers/chat-params.ts |
A huge file or many images can freeze/crash main and duplicate hundreds of MB across IPC/history |
| P0 | Several device-local stores treat non-missing read failures as defaults without universal quarantine/recovery | main/services/data-store.ts, schedule/settings/usage store construction |
Recoverable state can be silently replaced after corruption, permissions, or transient I/O failure |
| P1 | Streaming reveal schedules requestAnimationFrame forever, including when complete or reduced motion is active |
renderer/components/streaming-markdown-reveal.tsx |
Persistent renderer wakeups and battery use after streaming |
| P1 | Git info polls every 5 s, with review at 4 s; an uncached info call launches about six Git commands | renderer/lib/queries.ts, main/services/git.ts |
Approximately 72 child processes/minute in a normal open chat, plus heavier review work |
| P1 | Local speech recognition constructs and invokes the native recognizer synchronously in main and caches by model | main/handlers/local-voice.ts, main/services/parakeet.ts |
Whole-app stalls and potentially roughly 1.2 GB retained after using both voice models |
| P1 | Streaming copies the full accumulated string per frame, reparses growing Markdown, and has overlapping scroll observers | renderer/main/chat-pane.tsx, renderer/components/streaming-markdown-reveal.tsx, renderer/components/ui.tsx |
Long-task, layout, and frame-drop risk on long/code-heavy responses |
| P1 | Stop does not consistently stop recursive repository work or network tools | main/services/coding-tools.ts, main/services/tools.ts |
CPU, disk, and network activity can continue after the user presses Stop |
| P1 | MCP connect is not single-flight; schemas are fetched per generation; clients have no idle expiry; close is not awaited | main/services/mcp.ts, main/index.ts |
Duplicate/leaked helpers, hangs, stale connections, and incomplete quit |
| P1 | Renderer exit immediately reloads without reason-aware backoff or a retry ceiling | main/index.ts |
A deterministic crash can become a hot relaunch loop |
| P1 | Model installation deletes the usable version before validating a staged replacement and does not own extraction cancellation | main/services/local-models.ts |
Interrupted installation can remove a working local voice model |
| P1 | Scheduled catch-up has per-task overlap protection but no global budget or battery/lock policy | main/services/schedule-service-core.ts |
Multiple missed tasks can stampede after startup/resume |
| P2 | The closed model picker queries every provider and rebuilds/sorts catalog structures during parent renders | renderer/components/model-picker.tsx, renderer/lib/queries.ts |
Stream-frame work scales with provider/model count |
| P2 | Startup waits for provider enumeration/auth state before the first React render | renderer/main/index.tsx, main/services/provider-registry.ts |
A slow keychain/provider probe delays visible app chrome |
| P2 | Routes, settings, xterm, KaTeX, and full Highlight.js are in an eager startup graph | renderer/main/router.tsx, renderer/main/settings-view.tsx, renderer/components/code-block.tsx |
Larger parse/compile/startup and update payload |
| P2 | Terminal output and LLM deltas cross IPC at source cadence; terminal buffers repeatedly copy large strings | main/services/terminal.ts, main/services/llm-client.ts |
Excess wakeups, IPC allocations, and resize churn |
| P2 | Several realistic lists are unwindowed | files, review, chat/model palettes, transcript | Large workspaces and histories degrade nonlinearly |
The following remain measurement gaps, not validated regressions: the GPU/energy cost of the transparent vibrant window, the hidden dictation pill's disabled background throttling, real packaged startup time, KaTeX font subset value, updater cost on battery, and title-generation contention. Phase 0 decides these from traces.
- Acknowledge only durable state. An IPC mutation resolves after its recoverable write boundary, not after only an in-memory update.
- Bound at every trust boundary. Renderer input, filesystem input, network/MCP output, and native helper output each get count, byte, time, and concurrency limits.
- One owner per expensive resource. Workers, recognizers, child processes, PTYs, MCP clients, downloads, timers, and watchers have explicit start, abort, idle, and shutdown ownership.
- Event-driven by default. App mutations invalidate state immediately; focused surfaces may use a slow fallback. Hidden/minimized surfaces do not poll.
- Zero idle scheduling. No RAF, short timer, filesystem scan, child process, or helper probe remains live without pending user-visible work.
- Isolate the active turn. Streaming state should not force the persisted transcript, model catalog, composer, and unrelated panels to rerender.
- Optimize from packaged evidence. Development memory and CPU snapshots are directional only. Release gates use a signed/unpacked production-equivalent build on a named reference Mac.
- Preserve perceived speed. Render the chat shell first, keep the composer immediately interactive once provider identity is authoritative, and idle-preload secondary chunks where useful.
flowchart LR
P0["Phase 0: measure and instrument"] --> P1["Phase 1: durability and hard bounds"]
P0 --> P2["Phase 2: eliminate idle work"]
P1 --> P3["Phase 3: isolate heavy work and own lifecycles"]
P2 --> P3
P3 --> P4["Phase 4: renderer scale and startup"]
P4 --> P5["Phase 5: power policy, soak, and release gates"]
Deliverables:
- Add privacy-safe, device-local diagnostic events with explicit export:
- startup milestones: main ready, window created, navigation started, first shell paint, providers ready, composer ready;
- event-loop delay/utilization and long main tasks;
- renderer long tasks, React commits, live RAF/timer counts, and scroll writes;
- child/helper launches, duration, exit reason, and owner;
- IPC messages/bytes by channel, filesystem read/write bytes, Git commands, MCP clients, PTYs, and recognizers;
- renderer
unresponsive/responsive,render-process-gonedetails,child-process-gone, process errors, shutdown timeouts, and crash-loop state.
- Store a bounded redacted ring buffer. Never record prompts, response text, file paths, credentials, environment values, or tool payloads.
- Add a scripted benchmark fixture and a manual Instruments runbook for:
- cold/warm launch;
- visible idle, blurred idle, minimized, window closed/background;
- 100- and 500-turn chats;
- 2k/10k-character Markdown streams with code, math, reasoning, and tool phases;
- clean and dirty monorepos, Review closed/open, and external file churn;
- one/many/oversized attachments and long voice recordings;
- four terminals with idle and high-output workloads;
- MCP offline/hung/duplicate-connect cases;
- 20 missed schedules, suspend/resume, lock/unlock, and timezone change.
- Record AC and battery runs using Instruments Time Profiler, Energy Log, Core Animation, Chrome Performance, and React Profiler. Use
powermetricsonly when permissions are explicitly available. - Stamp benchmark output with commit, dirty-state hash, build mode, app/Electron versions, hardware, macOS version, power source, and scenario.
Exit gate:
- One reproducible production-equivalent baseline exists for every scenario.
- CI can fail on deterministic counters and bundle/package budgets.
- Hardware energy thresholds are documented as lab gates rather than pretending CI can measure them.
- Introduce one tested atomic writer: sibling temp file, bounded serialization, flush, rename, and parent-directory sync where macOS semantics require it.
- Keep the last known good generation; quarantine malformed/schema-invalid files with a timestamp and reason.
- Distinguish
ENOENT, malformed JSON, schema failure,EACCES, disk full, and transient I/O. Do not turn non-missing I/O failures into writable defaults. - Convert chat/index writes first. Rebuild a missing/corrupt index from valid chat bodies.
- Replace the global chat queue with per-chat mutation ownership plus a serialized index owner.
- Stop pretty-print rewriting unbounded history on every append. First introduce cached/atomic JSON; then benchmark an append journal with compaction. Adopt SQLite only if the journal cannot meet the measured gate.
- Apply the same recovery contract to settings, workspaces, schedules, usage, and cache stores according to whether data is authoritative or regenerable.
- Preserve backward reads and keep recovery tooling available across at least one release rollback window.
Tests:
- Fault injection before/after temp write, flush, rename, index update, and acknowledgement.
- Kill/relaunch, disk-full, read-only directory, corrupt/truncated JSON, stale temp, and interrupted migration.
- Concurrent mutation of two large chats; index rebuild; recovery of the last acknowledged message.
- Reject attachment count, per-file bytes, aggregate raw bytes, and aggregate encoded bytes before allocation.
- Read only a bounded text prefix; detect binary data in the prefix; use concurrency 2–4 instead of
Promise.allover arbitrary selection. - Validate the attachment DTO again in main before persistence or generation.
- Store attachment bodies as content-addressed files under
userData; store references and metadata in chat JSON. Use bounded thumbnails/blob URLs in the renderer and garbage-collect unreferenced blobs transactionally. - Send
chatId + current turnto main and load authoritative history there. Do not clone renderer-supplied base64 history before compaction. - Add recording duration/sample/encoded-byte limits. Prefer transferable binary buffers over base64 for voice IPC.
- Bound MCP/tool/terminal results before JSON/string conversion.
Tests:
- Sparse 10 GB text file, many near-limit images, base64 expansion, duplicate attachments, binary-prefix detection, deleted source file, and cleanup after failed persistence.
- Repeated attach/remove/chat-switch cycles with heap recovery.
- Oversized renderer-crafted history/attachment payloads rejected at IPC.
Exit gate:
- Zero lost acknowledged chat mutations at every injected write boundary.
- Corruption or permissions failure never silently overwrites recoverable state.
- No file, recording, message history, or tool result can allocate outside a documented aggregate budget.
- Cache repository identity by real path/common Git directory and invalidate only on path/repository changes.
- Coalesce app-owned mutations, file watcher signals, focus, and explicit refresh into one versioned Git snapshot shared by branch, Review, comparison, push, and environment consumers.
- Ignore high-churn/generated directories and debounce event storms. Watchers are hints; preserve a slow visible-only fallback.
- Do not run Review hashing/diff snapshots unless its surface is visible or the user requests refresh.
- Pause fallback refresh while hidden, minimized, locked, suspended, or in low-power mode.
- Replace per-hook 4–5 second intervals with invalidation/version subscriptions and an adaptive 30–60 second focused fallback only if measurement proves necessary.
- Replace the perpetual reveal loop with a single scheduled callback only while unrevealed units are due.
- Stop immediately when caught up, complete, reduced motion is active, the document is hidden, or the component unmounts.
- Use a single active-stream scheduler for main chat and Assistant; cap active visual commits around 30 Hz unless traces justify more.
- Coalesce streamed deltas and terminal chunks by time/byte threshold, while flushing state transitions and completion immediately.
- Replace full-subtree scroll mutation observation with a tail sentinel and one coalesced scroll measurement/write. Preserve the user's away-from-bottom state.
- Configure a query freshness matrix: event-owned local state gets long/infinite stale time and no focus retry storm; network/runtime state gets explicit TTL, retry, visibility, and reconnect behavior.
Tests:
- Fake-clock/RAF proof that caught-up, complete, reduced-motion, hidden, and unmounted streams schedule no frames.
- One mutation batch produces at most one scroll write; a detached reader is never pulled to bottom.
- Process-spawn counters prove no periodic Git children when idle/hidden and immediate refresh after a known mutation.
Exit gate:
- Zero live Aiden-owned RAF loops and zero periodic Git subprocesses when idle or hidden.
- Git state appears within two seconds after a known Aiden mutation without polling.
- Visible external changes meet the fallback freshness budget established in Phase 0.
- Move Sherpa recognizer creation and decoding to one owned utility process or worker. Prefer process isolation if a native crash can otherwise take down Electron main.
- Use one bounded transcription queue, cancellation, progress state, and one-recognizer LRU/idle eviction. Release on memory pressure and battery policy.
- Replace recursive in-process JavaScript grep with owned
rgor a worker. Bound traversal, output, duration, and binary/ignored directories. - Make
AbortSignal, deadline, byte limit, and result limit mandatory parts of every tool contract, including Exa/network work. - Define stop-to-settled behavior and assert that filesystem, network, native, and child activity ends.
- Use a single-flight MCP connection record keyed by server ID plus config fingerprint.
- Cache tool schemas with explicit config/health invalidation; connect independent servers with bounded concurrency.
- Add connect/list/call/close deadlines, health state, last-used/refcount idle expiry, and bounded tool results.
- Await MCP closure in the shutdown barrier and report stragglers without hanging quit forever.
- Download local models into a resumable partial artifact with size/checksum/deadline; extract into staging with an owned cancellable child; validate the complete manifest; atomically swap while preserving the previous valid model.
- Serialize model download/delete and make cancellation settle only after owned children exit.
- Replace terminal string slicing with a bounded chunk ring. Coalesce IPC output, dedupe resize calls, and fit after the open/close animation settles.
- Keep hidden PTYs only when a real job is running or the user chose persistence; expose status rather than silently killing work.
- Make renderer recovery reason-aware with exponential backoff, a bounded rolling retry window, and a safe-mode/recovery screen after repeated exits.
- Add a global scheduled-task concurrency budget, missed-run coalescing, catch-up ceiling, jitter, and explicit battery/locked/suspend/resume semantics.
- Reconcile schedule state on resume and timezone/DST change; make run history/runtime state one durable transaction.
Exit gate:
- Local transcription does not push main event-loop p99 above the Phase 0 budget.
- One MCP client/helper exists under concurrent requests; hung operations meet deadlines; shutdown leaves zero owned children.
- Stop reaches quiescence within a documented per-operation deadline.
- Interrupted model install always leaves the previous valid model usable.
- Repeated renderer failure exits the hot loop into bounded recovery.
- Separate the active streaming turn from memoized persisted transcript rows and composer/model controls.
- Parse only the append-only tail; cache finalized Markdown blocks. Render unfinished fences as plain text and highlight once stable.
- Replace full Highlight.js with
coreplus an evidence-based language allowlist; lazy-load uncommon languages or highlight in a worker. - Keep the model picker trigger cheap while closed. Batch provider metadata into one revisioned snapshot, memoize indexes/layout, and construct/virtualize the full picker only when open.
- Replace per-provider model-info IPC with one all-provider snapshot. Read/decrypt the Artificial Analysis cache once per cache generation rather than once for each provider.
- Window/paginate transcript, files, diff lines, chat/model palettes, and other measured high-cardinality lists while preserving keyboard navigation, screen-reader context, search, selection, copy, and scroll anchors.
- Stop recreating historical image data URLs; use stable blob URLs/thumbnails and release them with reference ownership.
- Render the lightweight shell before provider enumeration. Show a truthful provider-hydration state and enable composer selection only after alias/identity migration is authoritative.
- Dynamically import Settings/Profile/Scheduled routes, individual heavy Settings sections, terminal/xterm on first open, and other secondary panels.
- Keep ChatLayout, transcript shell, composer, selected-model trigger, and generation bridge in the initial graph. Optionally idle-preload the next likely surface after first input readiness.
- Use package-content allowlists. Bundle pure-JS main dependencies when safe; externalize/unpack only native/runtime-required modules.
- Disable packaged source maps or publish hidden maps outside the app artifact. Add a test that no
.mapis shipped. - Add bundle visualizer output and initial/async chunk budgets to CI.
Exit gate:
- Initial renderer JS meets the Phase 0-derived budget; proposed first target is at most 1.5 MB minified with no startup chunk over 500 KB.
- Packaged artifacts contain no source maps and only allowlisted unpacked dependencies.
- A 500-turn chat, 4,000-file workspace, large diff, and full model catalog remain responsive within the long-task/heap gates.
- The shell appears before provider discovery completes without allowing a stale provider/model mutation.
- Introduce a central lifecycle/power signal: visible, focused, minimized, locked, suspended, on battery, and low-power mode.
- Each background owner declares behavior for every state: updater, scheduler, MCP, recognizer cache, terminal, Git/workspace watcher, Foundation Models probe, title generation, and optional Assistant work.
- Respect the Foundation Models cache on activation; force only explicit retry or stale resume recovery.
- Measure transparency/vibrancy, blur, dictation-pill
backgroundThrottling, and animations as controlled A/B variants. Change them only when the energy/frame result is material and the visual/accessibility tradeoff is accepted. - Run 30–60 minute AC/battery soaks for foreground, blurred, minimized, background, streaming, voice, terminals, MCP, and sleep/wake.
- Add crash, disk-full, offline, hung-helper, process-exit, and quit-under-load chaos scenarios to release verification.
Exit gate:
- No monotonic heap/RSS growth across repeated chat, attachment, voice, terminal, MCP, and model-switch cycles after a defined settling window.
- Idle/minimized app launches no periodic Git or helper probes and improves wakeups/CPU time by at least 50% versus the Phase 0 baseline.
- No owned child process, PTY, recognizer, watcher, or network request remains after its shutdown deadline.
- Packaged startup, first-input readiness, first-token overhead, idle energy, streaming long tasks, package/update size, and bytes written per turn all have recorded release budgets.
Phase 0 may tighten these numbers. Invariants are hard requirements now; relative performance targets become hard after the reference baseline is recorded.
| Area | Release gate |
|---|---|
| Durability | Zero acknowledged-message loss across fault injection; recoverable corruption is quarantined, surfaced, and never overwritten |
| Input bounds | Count, raw-byte, encoded-byte, duration, concurrency, and result caps enforced in main regardless of renderer behavior |
| Idle renderer | Zero continuous RAF/timer loops after work settles; zero scroll writes without content/viewport change |
| Idle Git | Zero periodic Git children while hidden/minimized; target zero while visible idle, with at most one slow fallback refresh/minute if watchers prove insufficient |
| Main responsiveness | Proposed p99 event-loop delay below 50 ms during voice/tool workloads after isolation |
| Cancellation | Every owned local operation reaches quiescence within a measured deadline; network/helper calls have explicit timeouts |
| MCP | One client/helper under 100 concurrent connect attempts; a hung server is isolated; zero clients/helpers after quit |
| Renderer scale | No task over 50 ms at p95 during the reference stream; at most one scroll write/frame; bounded parser/highlighter calls |
| Memory | Heap/RSS returns within a Phase 0-defined tolerance after repeated attachment, voice, terminal, chat, and model cycles |
| Startup | Shell first, provider-ready second; proposed p95 shell target at most 1.5 s on the reference Apple Silicon Mac |
| Bundle/package | Proposed initial JS at most 1.5 MB minified; no initial chunk over 500 KB; no production maps; package allowlist enforced |
| Battery | At least 50% fewer idle wakeups/CPU time than baseline and no material minimized/background energy impact |
Every behavioral change includes narrow unit/contract tests and the relevant existing suites. New test files must be registered in package.json.
| Workstream | Automated verification | Runtime verification |
|---|---|---|
| Storage | fault-injected atomic writer, recovery, concurrent chat/index, migration, disk/I/O errors | kill during writes, relaunch/recovery, large-history append |
| Bounds | sparse files, many images, renderer-crafted IPC, oversized history/audio/results | heap trace while adding/removing realistic media |
| Streaming/scroll | fake RAF/clock, parser/highlighter counters, scroll sentinel contract | Chrome + React profile of long Markdown/code/math stream |
| Git/query | watcher/debounce/invalidation, child-count assertions, visibility/power state | dirty monorepo Instruments trace, Review closed/open |
| Voice/tools | worker protocol, cancellation, crash, memory eviction, timeout | cold/warm dictation and stop-under-load trace |
| MCP/download | concurrent connect, idle expiry, stale config, timeout, atomic install | hung server, offline install, cancel extraction, quit |
| Startup/package | chunk/map/package manifest budgets, provider hydration contract | cold/warm packaged launch and first-input timing |
| Scheduler/power | missed-run policy, global queue, resume/DST/timezone, lifecycle matrix | sleep/wake, locked, battery/AC soak |
Required commands at each applicable milestone:
npm run type-checknpm run lint- the narrow registered test script for the changed area
npm testnpm run build- package verification and the Phase 0 benchmark scenarios for release-facing changes
Safe parallel lanes after Phase 0:
- Atomic persistence/recovery.
- Attachment/history/voice input bounds.
- Git freshness coordinator and query policy.
- Streaming scheduler and scroll ownership.
Follow-on lanes:
- Voice/search worker isolation depends on bounds and diagnostics.
- MCP/download/shutdown ownership depends on common deadline/child tracking.
- Transcript/catalog virtualization depends on stable streaming boundaries.
- Startup/package splitting depends on measured module/chunk attribution.
- Central power policy depends on every background service exposing explicit lifecycle controls.
Each lane should land as a small reversible milestone with its own before/after trace. Do not combine persistence-format migration, worker isolation, and UI virtualization in one release.
- Attachment blob retention and cleanup policy, including behavior when a chat is deleted.
- Whether Sherpa runs in an Electron utility process or a Node worker; choose from crash containment and packaged native-module compatibility tests.
- Git watcher implementation and ignored-directory policy after an event-storm prototype.
- Scheduled-task behavior while on battery/locked and the maximum catch-up count after sleep.
- Source-map publication destination and package allowlist.
- Exact reference hardware and hard p95/energy budgets after Phase 0.
This plan is complete only when:
- every P0/P1 item has a landed fix or an evidence-backed rejection;
- P2 items have either met their budget, moved to a separately indexed plan, or been documented as intentionally accepted;
- release verification contains durability, bounds, idle, cancellation, lifecycle, package, and soak gates;
- a packaged before/after report shows the measured change in startup, idle wakeups/CPU, streaming long tasks, memory settling, child/helper counts, disk bytes, and artifact size;
docs/plans/README.mdmoves this plan to the completed archive with the remaining deferrals named explicitly.