- Add improvements.md with a system-wide fragility and reliability assessment - Prioritize 16 hardening recommendations by severity with estimated effort - Document high-risk hotspots across persistence, migration, runtime lifecycle, and scoping - Capture cross-cutting themes, test coverage gaps, and complexity metrics for follow-up
166 lines
14 KiB
Markdown
166 lines
14 KiB
Markdown
# Fusion System Improvements
|
|
|
|
Generated: 2026-04-08
|
|
Task: FN-1161
|
|
|
|
## Summary
|
|
|
|
Fusion is functionally rich, but fragility is concentrated in a small number of orchestration-heavy files where persistence, process control, API translation, and recovery logic are tightly coupled. The highest-risk paths are where state is written to multiple backends without a single atomic boundary, and where recovery logic is best-effort rather than explicit/stateful.
|
|
|
|
The most urgent issues are in task persistence (`TaskStore` dual-write), migration safety (`db.ts` + `db-migrate.ts` partial-apply semantics), and parallel step execution (`StepSessionExecutor` fallback behavior). These can produce split-brain state, hard-to-reconcile migration outcomes, or concurrent git writes to the same worktree.
|
|
|
|
Operationally, multi-project correctness and runtime lifecycle cleanup are the next tier of risk: several dashboard routes bypass scoped stores, child runtime restart/kill timers are not fully coordinated, and process/timer listeners can leak across repeated startups (observed during `pnpm test` as `MaxListenersExceededWarning`).
|
|
|
|
## Priority 1: Critical — Risk of Data Loss or Corruption
|
|
|
|
### 1) Non-atomic dual persistence for task state
|
|
- **File(s):** `packages/core/src/store.ts:459-485` (`readTaskJson`), `packages/core/src/store.ts:488-500` (`atomicWriteTaskJson`), `packages/core/src/store.ts:1054-1139` (`moveTask`)
|
|
- **Issue:** Task state is written to SQLite (`upsertTask`) and then mirrored to `task.json`. If the filesystem write/rename fails after DB write, the two sources diverge.
|
|
- **Impact:** Recovery/fallback paths can read stale `task.json`, causing inconsistent task columns/status/steps and hard-to-debug behavior after partial failures.
|
|
- **Suggestion:** Introduce explicit single-source mode for runtime operations (SQLite-only reads/writes), and move JSON mirror writes to an async reconciliation queue with health markers. Add a consistency check endpoint/CLI command that reports DB↔file drift.
|
|
- **Estimated effort:** M
|
|
|
|
### 2) Migration steps can partially apply without rollback journal
|
|
- **File(s):** `packages/core/src/db.ts:375-537` (`migrate`, `applyMigration`), `packages/core/src/db-migrate.ts:69-121` (`migrateFromLegacy`)
|
|
- **Issue:** `applyMigration()` explicitly avoids transaction wrapping for ALTER paths, and legacy migration catches per-step failures and continues.
|
|
- **Impact:** A project can end in a mixed schema/data state with no checkpoint table describing which pieces are authoritative.
|
|
- **Suggestion:** Add migration journal tables (`schema_migrations`, `legacy_migration_runs`, per-entity status), require explicit success/failure summary persistence, and add resumable migration commands (`--resume`, `--verify`) before backup cleanup.
|
|
- **Estimated effort:** L
|
|
|
|
### 3) Parallel-step fallback can run multiple steps in the same worktree concurrently
|
|
- **File(s):** `packages/engine/src/step-session-executor.ts:776-795` (`executeParallelWave`)
|
|
- **Issue:** When per-step worktree creation fails, code currently falls back to `this.options.worktreePath` and still executes the wave concurrently.
|
|
- **Impact:** Concurrent step sessions may write to the same git worktree, risking index conflicts, bad cherry-picks, or invalid step outcomes.
|
|
- **Suggestion:** On any worktree creation failure, degrade the entire wave to sequential mode (or hard-fail the wave) instead of sharing primary worktree paths concurrently.
|
|
- **Estimated effort:** S
|
|
|
|
## Priority 2: High — Risk of Operational Failure
|
|
|
|
### 4) Child-process runtime kill/restart lifecycle has timer races
|
|
- **File(s):** `packages/engine/src/runtimes/child-process-runtime.ts:347-361` (`killChild`), `packages/engine/src/runtimes/child-process-runtime.ts:436-459` (`handleUnhealthy`)
|
|
- **Issue:** Force-kill and restart timers are fire-and-forget and not tracked/cancelled; `this.child` is nulled immediately after scheduling SIGKILL.
|
|
- **Impact:** Delayed timer callbacks can target a new child process or run after shutdown transitions, producing restart flapping or accidental kill of replacement workers.
|
|
- **Suggestion:** Introduce explicit runtime lifecycle state machine + tracked timer handles. Keep per-child generation IDs so delayed callbacks only act on the intended child.
|
|
- **Estimated effort:** M
|
|
|
|
### 5) Global limit refresh timer leak and dead semaphore path
|
|
- **File(s):** `packages/engine/src/project-manager.ts:95-123` (constructor, `refreshGlobalLimit`), `packages/engine/src/project-manager.ts:459-470` (`stopAll`)
|
|
- **Issue:** `setInterval` for limit refresh is never cleared; `globalSemaphore` is recreated but never used for admission control.
|
|
- **Impact:** Long-lived processes can accumulate timers; intended global coordination is partially implemented and misleading.
|
|
- **Suggestion:** Either wire `globalSemaphore` into project run admission or remove it. Store interval handle and clear it in `stopAll()`.
|
|
- **Estimated effort:** S
|
|
|
|
### 6) Multi-project scoping bypass in dashboard mutation routes
|
|
- **File(s):** `packages/dashboard/src/routes.ts:1305-1311` (`getScopedStore`), plus unscoped handlers at `3525-3949` (`/github/issues/import`, `/github/issues/batch-import`, `/github/pulls/import`) and `5312-5828` (subtasks/planning create flows)
|
|
- **Issue:** Many routes correctly call `getScopedStore(req)`, but several import/planning/subtask endpoints still use root `store` directly.
|
|
- **Impact:** Requests with `projectId` can mutate the wrong project store and emit events on unexpected channels.
|
|
- **Suggestion:** Add a required `resolveStore(req)` middleware and ban direct outer `store` usage in route handlers via ESLint rule or route wrapper.
|
|
- **Estimated effort:** M
|
|
|
|
### 7) Realtime channels are not uniformly project-scoped
|
|
- **File(s):** `packages/dashboard/src/server.ts:134-145` (`/api/events`), `packages/dashboard/src/server.ts:544-644` (`setupBadgeWebSocket`), `packages/dashboard/app/hooks/useTasks.ts:34-108`, `packages/dashboard/app/hooks/useBadgeWebSocket.ts:104`, `packages/dashboard/app/hooks/useAgents.ts:39-50`
|
|
- **Issue:** SSE has scoped store support, but badge WS subscriptions are tied to the root store; client hooks also note unfiltered SSE behavior and open independent EventSources.
|
|
- **Impact:** Stale/cross-project updates, duplicate sockets, and reconciliation complexity under multi-project usage.
|
|
- **Suggestion:** Add `projectId` to badge WS subscribe protocol, normalize all event payloads with explicit project attribution, and centralize one shared app-level SSE/WS transport.
|
|
- **Estimated effort:** M
|
|
|
|
### 8) CLI extension mutates global console for tool output capture
|
|
- **File(s):** `packages/cli/src/extension.ts:681-696` (`kb_task_import_github`), `packages/cli/src/extension.ts:1015-1034` (`kb_task_plan`)
|
|
- **Issue:** Tools monkey-patch `console.log/error` globally to capture output.
|
|
- **Impact:** Concurrent tool executions can interleave logs, lose output, or restore wrong handlers.
|
|
- **Suggestion:** Refactor command APIs to return structured results (`{ summary, createdTasks, logs }`) and stop global console mutation.
|
|
- **Estimated effort:** S
|
|
|
|
### 9) Dashboard command lifecycle leaks signal listeners across repeated startups
|
|
- **File(s):** `packages/cli/src/commands/dashboard.ts:791-803` (`process.on` handlers)
|
|
- **Issue:** SIGINT/SIGTERM listeners are added on each run without paired teardown in test/runtime reuse scenarios.
|
|
- **Impact:** Observed in `pnpm test` as repeated `MaxListenersExceededWarning`; indicates lifecycle cleanup fragility.
|
|
- **Suggestion:** Use a listener registrar utility and always remove listeners in shutdown paths (or `once` + scoped cleanup).
|
|
- **Estimated effort:** S
|
|
|
|
### 10) AI automation timeout does not cancel underlying work
|
|
- **File(s):** `packages/engine/src/cron-runner.ts:371-439` (`executeAiPromptStep`)
|
|
- **Issue:** Timeout uses `Promise.race` with `setTimeout` but does not abort the running AI session/executor.
|
|
- **Impact:** Timed-out automation steps can keep consuming resources and finish out-of-band.
|
|
- **Suggestion:** Add AbortSignal plumbing to `AiPromptExecutor`, cancel timed-out runs explicitly, and clear timeout handles deterministically.
|
|
- **Estimated effort:** M
|
|
|
|
## Priority 3: Medium — Maintainability & Technical Debt
|
|
|
|
### 11) Oversized orchestration files with mixed responsibilities
|
|
- **File(s):** `packages/dashboard/src/routes.ts` (10,037 LOC), `packages/core/src/store.ts` (3,278 LOC), `packages/engine/src/executor.ts` (3,114 LOC), `packages/cli/src/extension.ts` (1,747 LOC)
|
|
- **Issue:** API wiring, domain logic, persistence, git operations, and recovery are combined in single files.
|
|
- **Impact:** Higher regression risk and slower change velocity; difficult targeted testing.
|
|
- **Suggestion:** Extract service boundaries with compatibility facades (e.g., `TaskPersistence`, `TaskTransitions`, `GitOps`, `PlanningRoutes`, `GitRoutes`, `ToolRegistry`).
|
|
- **Estimated effort:** L
|
|
|
|
### 12) CLI dispatch is a giant manual switch tree
|
|
- **File(s):** `packages/cli/src/bin.ts:267-900` (`main` command dispatch)
|
|
- **Issue:** 70+ case branches with repeated usage checks and `process.exit` branches.
|
|
- **Impact:** Easy to introduce inconsistent UX/error behavior; hard to verify command coverage.
|
|
- **Suggestion:** Move to declarative command table with argument schemas + shared runner/error mapping.
|
|
- **Estimated effort:** M
|
|
|
|
### 13) Error handling policy is inconsistent and frequently best-effort
|
|
- **File(s):** `packages/dashboard/src/routes.ts` (289 `catch` occurrences), `packages/engine/src/executor.ts` (73), `packages/engine/src/merger.ts` (36), `packages/core/src/db-migrate.ts:69-121`
|
|
- **Issue:** Frequent silent catches and warning-only branches without structured error classes.
|
|
- **Impact:** Operational failures are harder to classify/retry/alert; behavior differs by module.
|
|
- **Suggestion:** Introduce shared error taxonomy (`UserInput`, `RetryableInfra`, `InvariantViolation`) and mandatory structured logging fields (`area`, `operation`, `retryable`, `cleanupAttempted`).
|
|
- **Estimated effort:** M
|
|
|
|
### 14) Event payload typing is weak at SSE boundaries
|
|
- **File(s):** `packages/dashboard/src/sse.ts:47-109`
|
|
- **Issue:** SSE handlers are predominantly `any`-typed payloads.
|
|
- **Impact:** Runtime-only breakage for payload shape drift; weaker refactor safety.
|
|
- **Suggestion:** Define shared event contracts in `@fusion/core` and type SSE/WS emitters and client handlers against those contracts.
|
|
- **Estimated effort:** S
|
|
|
|
## Priority 4: Low — Nice-to-Have Improvements
|
|
|
|
### 15) Add disposal for cached TaskStore instances in extension
|
|
- **File(s):** `packages/cli/src/extension.ts:34-42` (`storeCache`/`getStore`), `packages/cli/src/extension.ts:1737-1746` (`session_shutdown`)
|
|
- **Issue:** Session shutdown clears cache keys but does not `close()` each cached `TaskStore` DB handle.
|
|
- **Impact:** Usually minor for short-lived sessions, but can keep descriptors open longer than needed.
|
|
- **Suggestion:** Iterate cached stores on shutdown and call `store.close()` before clearing the map.
|
|
- **Estimated effort:** S
|
|
|
|
### 16) Formalize route-level validation schemas
|
|
- **File(s):** `packages/dashboard/src/routes.ts` (many manual `if (!field || typeof field !== ...)` blocks)
|
|
- **Issue:** Validation logic is repetitive and hand-rolled per endpoint.
|
|
- **Impact:** Subtle behavior drift across similar routes and harder API contract evolution.
|
|
- **Suggestion:** Adopt shared schema validation middleware (e.g., Zod/TypeBox) and generate request/response typings from one source.
|
|
- **Estimated effort:** M
|
|
|
|
## Cross-Cutting Themes
|
|
|
|
1. **Split-brain state risk from hybrid persistence:** runtime writes often touch SQLite + filesystem mirrors without a single transactional boundary.
|
|
2. **Best-effort recovery over explicit state machines:** many critical cleanup/retry paths rely on ad-hoc catches/timers instead of explicit lifecycle ownership.
|
|
3. **Project-context propagation is incomplete:** most routes are scoped correctly, but exceptions create high-severity multi-project correctness risk.
|
|
4. **Operational side effects are under-tested:** signal handlers, timers, and child-process restart behavior are not comprehensively asserted.
|
|
5. **Complexity concentration in a few mega-files:** defects and regressions cluster in the same hotspots (`routes.ts`, `store.ts`, `executor.ts`).
|
|
|
|
## Test Coverage Gaps
|
|
|
|
- **No direct tests for `useAgents` hook:** `packages/dashboard/app/hooks/useAgents.ts` has no matching test file.
|
|
- **Missing project-scoping assertions for import/planning routes:** route tests exist (`packages/dashboard/src/routes.test.ts` around lines `3603+`, `5679+`) but do not exercise `projectId` behavior for `/github/issues/import`, `/github/issues/batch-import`, `/github/pulls/import`, `/planning/*`, `/subtasks/*`.
|
|
- **Child runtime lifecycle edge cases are lightly tested:** `packages/engine/src/runtimes/child-process-runtime.test.ts` currently validates basic status/getter behavior, but not restart timer race conditions (`killChild` + `handleUnhealthy`).
|
|
- **Parallel-step failure path lacks coverage:** `packages/engine/src/step-session-executor.test.ts` focuses on parsing/planning utilities; it does not test worktree-creation-failure behavior inside `executeParallelWave`.
|
|
- **Process listener cleanup is not asserted:** CLI dashboard tests surfaced `MaxListenersExceededWarning`, but tests do not currently fail on listener leaks.
|
|
|
|
## Appendix: Key Metrics
|
|
|
|
- **Largest source files reviewed:**
|
|
- `packages/dashboard/src/routes.ts` — **10,037** lines
|
|
- `packages/core/src/store.ts` — **3,278** lines
|
|
- `packages/engine/src/executor.ts` — **3,114** lines
|
|
- `packages/cli/src/extension.ts` — **1,747** lines
|
|
- **Complexity signals:**
|
|
- Route handlers in `routes.ts`: **210**
|
|
- Tool registrations in `extension.ts`: **28**
|
|
- `execSync` usage in `routes.ts`: **69**
|
|
- `catch` occurrences in `routes.ts`: **289**
|
|
- **Large tests (complexity proxy):**
|
|
- `packages/engine/src/executor.test.ts` — 8,659 lines
|
|
- `packages/dashboard/src/routes.test.ts` — 8,277 lines
|
|
- `packages/core/src/store.test.ts` — 5,991 lines
|
|
- **Full test run result:** `pnpm test` passed; however, repeated `MaxListenersExceededWarning` occurred during CLI dashboard tests, indicating listener lifecycle debt. |