Skip to content

Enhanced prompt PDF/DOCX download: "File not found" — path resolution bug + DB artifact persistence (v5.8.3) #67

Description

@Number531

Summary

Clicking the PDF (or DOCX) download link in the Prompt Enhancement approval modal returned `{"error":"File not found"}`. This is a distinct second bug that was masked by the earlier "Invalid session ID format" bug (fixed in `cb7e8a2`). Once the URL was syntactically valid, the backend path resolution exposed a lower-layer mismatch.

Status

FIXED in commit `3485e2d` (v5.8.3) — deployed to GCE staging at 2026-04-09 17:24 UTC, user confirmed: PDF link works.

Root Cause

The download route at `documentConversionRouter.js:182` hardcodes `documents/` as the base subdirectory for all served files:

```javascript
const absPath = path.join(REPORTS_DIR, sessionId, 'documents', filePath);
```

This makes `{session}/documents/` the canonical "exports and working files" directory, shared by:

  1. Uploaded input binaries (via `sessionInitializer.js:38-120`)
  2. P0 extraction artifacts (via `document-processing-analyst` inside the pre-created directory)
  3. Memo converter output exports (via `documentConverter.js:650` → `path.join(sessionPath, 'documents')`)
  4. All files served by the download router — both fresh downloads and backfill reconstructions

The `promptEnhancer.js` conversion block at line 317-318 was the only code path writing exports to the session root instead of `{session}/documents/`:

```javascript
const docxPath = enhancedPath.replace('.md', '.docx'); // session root — WRONG
const pdfPath = enhancedPath.replace('.md', '.pdf'); // session root — WRONG
```

The frontend's `doc_convert` event with `category: 'root'` means "root of the documents/ subdirectory" (frontend computes `catPath = category === 'root' ? '' : category + '/'` at `app.js:3191`), NOT "session root" as the old enhancer code assumed.

The URL the frontend constructed (`/api/convert/download/{session}/enhanced-prompt.pdf`) was correct, but the backend joined `documents/` to it and looked in a directory where the file didn't exist:

  • Frontend URL: `/api/convert/download/2026-04-09-xxxx/enhanced-prompt.pdf`
  • Backend lookup: `reports/2026-04-09-xxxx/documents/enhanced-prompt.pdf`
  • Actual file: `reports/2026-04-09-xxxx/enhanced-prompt.pdf` ← wrong location
  • Result: `res.sendFile()` fails → 404 `{"error":"File not found"}`

Why Enhancement Must Create `documents/` On-Demand

`initializeSession()` (`sessionInitializer.js:38`) creates the full directory structure (`initial-query-docs/`, `documents/`, `specialist-reports/`) — but it is only called when documents are uploaded (`agentStreamHandler.js:140`):

```javascript
ctx.sessionInfo = await initializeSession(ctx.userQuery, ctx.documents, ctx.sessionDir);
```

And enhancement ONLY runs when documents are NOT uploaded (`promptEnhancer.js:106`):

```javascript
if (ctx.documents.length > 0) return null;
```

These two code paths are mutually exclusive. In an enhancement-only flow:

  • `initializeSession` is skipped → `documents/` subdir never created
  • P0 is also skipped (`p0Orchestrator.js:90`: `if (!deps.featureFlags.DOCUMENT_PROCESSING || !ctx.sessionInfo) return null`)
  • Only `{session}/` root exists, with `enhanced-prompt.md` inside it

P0 can assume `documents/` exists because it runs after `initializeSession` in document-upload sessions. Enhancement must create the subdirectory itself because `initializeSession` was skipped in its code path.

The Fix — Dual-Part

Part 1: Path fix

```javascript
const exportsDir = path.join(fullSessionPath, 'documents');
fs.mkdirSync(exportsDir, { recursive: true }); // ← on-demand creation, idempotent
const docxPath = path.join(exportsDir, 'enhanced-prompt.docx');
const pdfPath = path.join(exportsDir, 'enhanced-prompt.pdf');
```

Part 2: DB artifact persistence (closes pre-existing gap)

Before this release, enhancement PDF/DOCX files lived only on the container's ephemeral filesystem. If the container was replaced between conversion and user download, the files were gone even though the `.md` source was safely in the `reports` table.

This release persists both artifacts to `report_artifacts` with:

  • `file_path = 'documents/enhanced-prompt.{docx,pdf}'` (matches backfill convention)
  • `category = 'root'` (matches frontend `doc_convert` event)
  • `source = 'prompt_enhancement'` (new free-text value)
  • Linked to `reports.id` via a SELECT lookup (no modification to the existing `reports` INSERT)

Session replay after container replacement now finds the enhancement PDFs via the DB fallback download route (`claude-sdk-server.js:570-617` → `/api/db/artifacts/:id/download`).

Zero-Break Guarantees

All changes are additive. Verified:

Concern Status
Existing `reports` INSERT modified NO — artifact persistence uses separate SELECT for `report_id` lookup
Existing `sessions` query modified NO — new SELECT only
Schema changes required NO — `report_artifacts` table already exists
`persistArtifact()` function exists YES — `src/utils/artifactPersistence.js:39`
DB unavailable behavior `getPool()` returns null → block skipped
`sessions` row missing `sessRes.rows[0]?.id` undefined → block skipped
`reports` row missing `reportId` falls back to `null` — column is nullable
File read failure Inner try/catch per file — DOCX failure doesn't block PDF attempt
`persistArtifact` throws Caught by inner and outer try
Race with concurrent downloads Filesystem file written before DB persist — immediate clicks work via filesystem path
`source = 'prompt_enhancement'` new enum value `source` is free-text, not enum. Other values: `'document_conversion'`, `'backfill'`, `'code_execution'`
Breaking `persistSessionArtifacts()` NO — different `file_path` namespace, no conflicts

Filesystem Layout (after fix)

For enhancement-only session:
```
reports/
└── 2026-04-09-XXXX/
├── enhanced-prompt.md ← session root (.md source, unchanged)
├── intake-enhancement-state.json ← session root (state file)
└── documents/ ← created on-demand by v5.8.3
├── enhanced-prompt.docx ← ← moved here
└── enhanced-prompt.pdf ← ← moved here
```

Verification

  • Module smoke test: `promptEnhancer.js` imports cleanly
  • Path fix deployed to GCE staging
  • User confirmed: PDF download link works (2026-04-09 17:30 UTC)
  • Verify `report_artifacts` rows appear with `source='prompt_enhancement'` after a full enhancement session
  • Verify session replay after container restart retrieves PDFs from DB fallback

Related

Files Changed

  • `src/server/promptEnhancer.js` — path fix + on-demand mkdirSync + DB artifact persistence block (+66/-2 lines)

Commit

`3485e2d` — `fix(enhancement): PDF/DOCX download "File not found" + DB artifact persistence (v5.8.3)`

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions