Make document lifecycle fan-out durable and atomic
Pre-v47 `createDocumentWithLifecycle` wrapped each hook in its own try/catch and always returned success, so a failure in any of the downstream hooks (thread event, checkpoint approval, deliverable review, automation dispatch, reviewer wake) left a live document row behind with a silently incomplete lifecycle. Two invariants now hold: - DB-writing hooks (thread integration, checkpoint approval, deliverable review) run inside the same `BEGIN IMMEDIATE` transaction as `saveDocument`. A throw rolls back the document row along with every partial write, so an operator never sees a persisted document with a missing thread or skipped approval. - Cross-process side effects (automation dispatch, reviewer wake) are enqueued as `document_lifecycle_events` rows inside the same transaction and processed post-commit. Success stamps `status = 'succeeded'`; failure stamps `status = 'failed'` with `last_error`, so partial lifecycle state is observable via `listFailedForCompany` instead of disappearing into the log tail. Migration v47 installs the ledger; `buildDocumentLifecycleEventMethods` exposes the enqueue/running/succeeded/failed transitions; the lifecycle service drives both.
This commit is contained in:
1 parent
a36884360a
commit
edbebe6109
5 files changed
+326
-117
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import log from "electron-log/main.js";
|
||||
import type { Migration, MigrationDeps } from "./types";
|
||||
|
||||
export function createMigrationsV47V47(deps: MigrationDeps): Migration[] {
|
||||
const { db } = deps;
|
||||
return [
|
||||
{
|
||||
// Durable fan-out ledger for document-creation side effects.
|
||||
//
|
||||
// Before v47 `createDocumentWithLifecycle` wrapped every downstream
|
||||
// hook (automation dispatch, thread integration, checkpoint engine,
|
||||
// reviewer wake) in an individual `try/catch` and always returned
|
||||
// success. A failure in any single hook left the document row live
|
||||
// with a silently incomplete lifecycle — thread views, approvals,
|
||||
// or reviewer wakes disappeared without trace.
|
||||
//
|
||||
// The ledger is the tracked-state surface the operator reads to
|
||||
// reason about lifecycle coverage: one row per hook per document
|
||||
// is inserted inside the same transaction as the document write.
|
||||
// The processor advances `status` from `pending` to `succeeded` or
|
||||
// `failed`, stamps `last_error` on failure, and `attempts` lets the
|
||||
// sweeper retry within a bounded policy.
|
||||
version: 47,
|
||||
name: "document_lifecycle_events_ledger",
|
||||
fn: () => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS document_lifecycle_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
company_id TEXT NOT NULL,
|
||||
document_id TEXT NOT NULL,
|
||||
hook_type TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
last_error TEXT,
|
||||
payload_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
completed_at TEXT
|
||||
)
|
||||
`);
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_document_lifecycle_events_company
|
||||
ON document_lifecycle_events(company_id, status, updated_at)
|
||||
`);
|
||||
db.exec(`
|
||||
CREATE INDEX IF NOT EXISTS idx_document_lifecycle_events_document
|
||||
ON document_lifecycle_events(document_id, hook_type)
|
||||
`);
|
||||
log.info("[migration v47] document_lifecycle_events ledger installed");
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
Reference in new issue
Block a user