Severity: Medium — found during a deep project review (2026-07-06). Crash-window bugs: better-sqlite3 is synchronous so there is no in-process interleaving, but a crash/OOM/docker stop mid-sequence persists partial state with no rollback.
Non-transactional multi-step writes
autoReschedule mass date cascade (highest impact) — server/src/services/schedulingEngine.ts:698-1050: per-row db.update(workItems) (:885) and db.update(householdItems) (:1031) loops; each statement autocommits. Triggered from routes/workItems.ts:139 on work-item create/update and from the daily reschedule. A crash mid-run persists a half-applied schedule (some items cascaded, dependents not), undetectable afterwards.
reorderSubtasks — server/src/services/subtaskService.ts:252-262: per-subtask sortOrder updates autocommit individually; kill mid-loop persists mixed old/new orders (duplicate sortOrder values).
createMilestone parent + links — server/src/services/milestoneService.ts:253-278: milestone insert then milestoneWorkItems link loop; crash in between persists the milestone with partial/no links while the client gets an error and retries → duplicate milestone.
- Two-step deletes —
server/src/services/workItemService.ts:597-609 and server/src/services/milestoneService.ts:373-382: householdItemDeps rows deleted first, then the parent entity; crash in between silently destroys dependency rows while the parent survives.
Suggested fix
Wrap each sequence in a Drizzle/better-sqlite3 transaction (db.transaction(...)). The scheduling engine cascade is the priority — it does the largest multi-table write in the app on a routine trigger.
Severity: Medium — found during a deep project review (2026-07-06). Crash-window bugs: better-sqlite3 is synchronous so there is no in-process interleaving, but a crash/OOM/
docker stopmid-sequence persists partial state with no rollback.Non-transactional multi-step writes
autoReschedulemass date cascade (highest impact) —server/src/services/schedulingEngine.ts:698-1050: per-rowdb.update(workItems)(:885) anddb.update(householdItems)(:1031) loops; each statement autocommits. Triggered fromroutes/workItems.ts:139on work-item create/update and from the daily reschedule. A crash mid-run persists a half-applied schedule (some items cascaded, dependents not), undetectable afterwards.reorderSubtasks—server/src/services/subtaskService.ts:252-262: per-subtasksortOrderupdates autocommit individually; kill mid-loop persists mixed old/new orders (duplicatesortOrdervalues).createMilestoneparent + links —server/src/services/milestoneService.ts:253-278: milestone insert thenmilestoneWorkItemslink loop; crash in between persists the milestone with partial/no links while the client gets an error and retries → duplicate milestone.server/src/services/workItemService.ts:597-609andserver/src/services/milestoneService.ts:373-382:householdItemDepsrows deleted first, then the parent entity; crash in between silently destroys dependency rows while the parent survives.Suggested fix
Wrap each sequence in a Drizzle/better-sqlite3 transaction (
db.transaction(...)). The scheduling engine cascade is the priority — it does the largest multi-table write in the app on a routine trigger.