Meeting lifecycle recovery — design spec

Status: Planned
Author: Christophe
Date: 2026-07-21
Repo: north-os (packages/meetings, apps/worker-meetings, apps/web/lib/meetings)
Related: WS-4 of 2026-07-21-meeting-production-readiness · review docs/reviews/2026-07-21-meeting-system-review.md (findings 4, 9, 15, 16 + the best-effort-emit caveat).

Problem

The transcription lifecycle has two unrecoverable stuck states and a fire-and-forget event. After the pending→transcribing CAS (process.ts:68-83), a worker crash (OOM decrypting a large blob, deploy SIGKILL) leaves the row transcribing forever: BullMQ redelivers, the handler throws CasConflictError, and the consumer treats it as a benign race and ack-drops (consumer.ts:36-49). A failed bus.emit after a durable insert strands the row pending forever. And source.meeting.transcribed is emitted best-effort with no outbox (process.ts:130-151) — fine for the summary consumer today, load-bearing the moment a second consumer (RAG, WS-12) depends on it. The summary path already has the missing mechanism (15-min stale-generating reclaim, runtime.ts:386-395) — the asymmetry is the gap.

Design

Revision — 2026-07-22 (as implemented, NOS-193)

The per-workstream plan in Linear NOS-193 was adversarially verified against main and refined two of the design bullets above. The original text is kept for the record; this section is what shipped.

Invariant & acceptance

No meeting is ever stuck with no operator signal, and every ready meeting's transcribed-event is eventually delivered at least once (the second clause lands with WS-12 — see Revision; WS-4 ships the mechanics and the seam). Acceptance: a crash-after-CAS integration test (kill between the CAS and the catch, assert the row returns to pending and re-transcribes), a lost-emit test (assert pending-rescue re-drives), and an at-least-once test for the transcribed event (the last with WS-12).