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:
- Uploaded input binaries (via `sessionInitializer.js:38-120`)
- P0 extraction artifacts (via `document-processing-analyst` inside the pre-created directory)
- Memo converter output exports (via `documentConverter.js:650` → `path.join(sessionPath, 'documents')`)
- 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
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
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:
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:
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:
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:
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:
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
Related
Files Changed
Commit
`3485e2d` — `fix(enhancement): PDF/DOCX download "File not found" + DB artifact persistence (v5.8.3)`
🤖 Generated with Claude Code