--- phase: 40-job-infrastructure plan: "01" subsystem: content-jobs tags: [db-schema, services, async-jobs, live-events] dependency_graph: requires: [] provides: - content_jobs DB table with queued/running/done/failed lifecycle - contentJobStore CRUD service - contentJobRunner async dispatcher with live events - MAX_GENERATED_ASSET_BYTES constant (500MB) - assets.sourceTaskId column - LIVE_EVENT_TYPES extended with content_job.* events affects: - packages/db/src/schema/ - packages/shared/src/constants.ts - server/src/services/ - server/src/attachment-types.ts tech_stack: added: [] patterns: - Drizzle ORM pgTable schema with $type<> for typed status fields - Fire-and-forget async job dispatch with void pattern - DB write before live event publish (ordered consistency) - renderContent stub for future renderer plug-in by jobType key_files: created: - packages/db/src/schema/content_jobs.ts - server/src/services/content-job-store.ts - server/src/services/content-job-runner.ts - packages/db/src/migrations/0046_tense_randall.sql modified: - packages/db/src/schema/assets.ts - packages/db/src/schema/index.ts - packages/shared/src/constants.ts - server/src/attachment-types.ts - server/src/services/index.ts decisions: - content_jobs.resultAssetId has no FK to assets — populated post-creation, circular FK avoided - assets.sourceTaskId is nullable text with no FK — task IDs are string identifiers, not UUIDs - renderContent is a stub returning placeholder.txt — phases 41-45 add real renderers keyed by jobType - Migration applied via drizzle-kit generate from worktree dist/ (main repo has pre-existing duplicate migration files) metrics: duration: "~10 minutes" completed: "2026-04-04" tasks_completed: 2 tasks_total: 2 files_created: 4 files_modified: 5 --- # Phase 40 Plan 01: Schema, constants, and services for content jobs Summary **One-liner:** content_jobs table + async job runner with store/runner services using fire-and-forget dispatch and ordered DB-before-event lifecycle. ## What Was Built The foundational data model and async execution engine for v1.7 content generation. Phase 40 Plan 01 establishes: 1. **DB schema** (`content_jobs` table) with full lifecycle columns and two composite indexes for efficient querying by company+status and company+createdAt. 2. **assets.sourceTaskId** — nullable text column allowing generated assets to be traced back to their originating conversation task. 3. **LIVE_EVENT_TYPES** — extended with `content_job.queued`, `content_job.running`, `content_job.done`, `content_job.failed` for real-time job progress. 4. **MAX_GENERATED_ASSET_BYTES** — 500MB constant (vs 10MB upload limit) for generated/namespace storage, configurable via `PAPERCLIP_GENERATED_ASSET_MAX_BYTES`. 5. **contentJobStore** — CRUD service with create/getById/listByCompany/transition operations against the content_jobs table. 6. **contentJobRunner** — async fire-and-forget dispatcher that transitions jobs through running→done/failed, stores generated assets with sourceTaskId, and publishes live events after each DB write. ## Tasks Completed | Task | Name | Commit | Files | |------|------|--------|-------| | 1 | Schema, constants, and migrations | 8bf4bd91 | content_jobs.ts, assets.ts, index.ts, constants.ts, attachment-types.ts, migration 0046 | | 2 | contentJobStore and contentJobRunner | b359fec9 | content-job-store.ts, content-job-runner.ts, services/index.ts | ## Verification - `pnpm tsc --noEmit --project packages/db/tsconfig.json` — PASS - `pnpm tsc --noEmit --project packages/shared/tsconfig.json` — PASS - `pnpm tsc --noEmit --project server/tsconfig.json` — PASS - Migration file `0046_tense_randall.sql` generated with correct DDL ## Deviations from Plan ### Migration apply limitation (pre-existing blocker, out of scope) **Found during:** Task 1 **Issue:** The main repo (`/opt/nexus`) has pre-existing duplicate migration files (`0047_nebulous_klaw.sql` and `0047_overjoyed_groot.sql`, `0048_add_chat_messages_updated_at.sql` and `0048_flashy_marrow.sql`) from parallel agent work. The `check:migrations` script prevents both `db:generate` and `db:migrate` in the main repo. **Impact:** Migration `0046_tense_randall.sql` was generated successfully from the worktree's clean migration state but could not be applied via `pnpm db:migrate`. **Resolution:** Migration will be applied when the worktree is merged and the duplicate migration numbering conflict is resolved. The SQL is correct and ready for execution. This is a pre-existing issue deferred to `.planning/deferred-items.md`. **Migration file content verified:** Creates `content_jobs` table, adds `source_task_id` to `assets`, creates both compound indexes, adds FK to companies. ### Known Stubs **renderContent stub** in `server/src/services/content-job-runner.ts`: - Returns `{ filename: "placeholder.txt", contentType: "text/plain", buffer: Buffer.from("placeholder output") }` for all jobTypes - This is intentional — the plan specifies this as a stub for phases 41-45 to fill in real renderers keyed by jobType - Does NOT prevent the plan's goal (job infrastructure) from being achieved ## Self-Check: PASSED Files verified to exist: - `/opt/nexus/.claude/worktrees/agent-ac2e6085/packages/db/src/schema/content_jobs.ts` — FOUND - `/opt/nexus/.claude/worktrees/agent-ac2e6085/server/src/services/content-job-store.ts` — FOUND - `/opt/nexus/.claude/worktrees/agent-ac2e6085/server/src/services/content-job-runner.ts` — FOUND - `/opt/nexus/.claude/worktrees/agent-ac2e6085/packages/db/src/migrations/0046_tense_randall.sql` — FOUND Commits verified: - `8bf4bd91` — FOUND (feat(40-01): schema, constants, and migrations for content jobs) - `b359fec9` — FOUND (feat(40-01): contentJobStore and contentJobRunner services)