A2 — Durable Work Session workflow (Mastra + PostgreSQL)
Project:70160c56-cf2f-4d94-81a0-ee9000eee76b (Commercial Intelligence — Agentic Workbench)
Card: A2. Hacer durable el workflow de Mastra
Scope: the versioned work-session-flow-v1 workflow + the recoverable-SSE surface. Dark: registered as a provider, wired to nothing user-visible; the opportunity copy is untouched.
What ships
apps/api/src/chat/mastra-workflow.ts—MastraWorkSessionWorkflowFactory, the same posture as the market-intelligence factory: lazy ESM loaders (importEsm, never a static@mastraimport — the runtime-contract candado holds), a singleton workflow, PostgresStore keyeddriftless-work-session-workflowsoverSUPABASE_URLwith the sharedOPPORTUNITY_WORKFLOW_POOL_MAXconnection budget.- Steps:
compile-contract(A3 compiler + bootstrap ExecutionPlan v1) →clarify(suspend on a typedHumanCheckpoint; resume revalidates workspace, principal, session, thread, checkpoint id and contract version against the PERSISTED suspension) →finalize(contract_ready | needs_clarification | stopped | went_back). - Cancel is structural: it resumes through the checkpoint’s stop affordance, so it is durable, restart-safe, preserves the compiled contract as the partial, and a second cancel/resume hits the consumed-suspension rejection.
- Steering messages queue on the caller and ride the resume payload (bounded: 5 × 500 chars), drained in order.
- Bounded retries (
withBoundedRetries) wrap ONLY transient infrastructure (loader/storage init); aDomainExceptionnever retries. - Snapshots hold small, closed payloads (contract + plan + checkpoint — proven < 8 KB canonical in the spec); no datasets/HTML.
- SSE recoverability:
WorkSessionStore.listEventsAfter(workspace, session, afterSeq)serves the persisted tail; replaying it through the idempotent reducer/projection cannot duplicate a delta.ChatStreamHubremains an in-memory projection, never an authority.
Migration / grants / RLS
- No new Driftless migration: Mastra’s
PostgresStore.init()creates and owns itsmastra_*tables on first use — the same mechanism the already-shipped opportunity workflow uses. Both stores write the SAMEmastra_*tables in the same schema (a storeidis not a table namespace); run-state isolation between the two workflows comes from the distinct workflow id (work-session-flow-v1vscommercial-opportunity-flow-v1) in every snapshot key, plus the turn-scoped runId derivation. - Grants/RLS: the store connects with the API’s service-role connection string (
SUPABASE_URL), the same trust level as TypeORM’s pool;mastra_*tables are not exposed through PostgREST and inherit the service-role-only posture of every other API-owned table. Tenant isolation is enforced ABOVE the storage:resourceId = workspace_idon every run, and resume revalidates the full tenant scope against the persisted suspension — a cross-tenant resume fails closed even with direct storage access. - Connection budget: this store is a SEPARATE pool (one connection) from the opportunity workflow’s —
DATABASE_CONNECTION_BUDGETgains an explicitworkSessionWorkflow: 1line (total 7 → 8, still well under the staging session-pool limit; its spec pins the new total).
Rollback (P1)
The factory is additive and dark. Rollback = revert the commit: no schema change of ours to unwind (Mastra tables are inert without a caller), no consumer to migrate, no flag needed. If a partial deploy leaves suspendedwork-session-flow-v1 runs, they expire unused — nothing reads them after revert.
Evidence
pnpm --filter @driftless/api exec vitest run src/chat/mastra-workflow.spec.ts — 12 tests: straight-through compile, typed suspension with bounded snapshot, cross-turn resume with directed second round, restart survival over shared storage, cancel preserving partials, cross-tenant/principal/checkpoint/stale rejections, consumed-suspension rejection, fail-closed answers, ordered steering + bounded queue, recoverable-SSE tail replay with duplicate-free reapplication, bounded-retry semantics.