Debugging Silent Failures: How I Fixed a Workflow Compression System
A deep dive into debugging a silent failure in our step digest compression system, where missing error logs turned a simple fix into a detective story.
Debugging Silent Failures: How I Fixed a Workflow Compression System
Last week, I encountered one of those bugs that make you question everything: a feature that appeared to be working but was silently failing in production. Our workflow engine's step digest compression system wasn't generating any digests, causing prompt sizes to balloon as workflows progressed. Here's the story of how I tracked down the issue and implemented a robust fix.
The Problem: Growing Pains
Our workflow engine processes complex multi-step tasks, where each step builds on the previous ones. Without compression, the context passed between steps grows exponentially—imagine trying to fit an entire conversation history into every new message. We built a digest system to compress completed steps, but it wasn't working.
The symptoms were clear: workflows were completing successfully, but prompt sizes were growing from reasonable 4KB chunks to unwieldy 32KB monsters. Something was preventing our digest generation, but there were no error messages to guide us.
The Hunt: Following Silent Breadcrumbs
The first breakthrough came when I added proper error logging to our digest service. The original code had a classic anti-pattern:
// Before: Silent failure
try {
const digest = await generateDigest(stepData);
} catch (error) {
// Error swallowed - no logging!
}
// After: Proper error visibility
try {
const digest = await generateDigest(stepData);
} catch (error) {
console.error('Digest generation failed:', error);
throw error; // Don't swallow the failure
}
Once I could see the actual errors, the root causes became clear. The digest system was failing at multiple points in the workflow lifecycle, but the application continued running as if nothing was wrong.
The Fixes: Comprehensive Coverage
1. Backfill Missing Digests
When workflows resume after interruption, some steps might have completed before digest generation was fixed. I added a backfill loop that checks for missing digests and generates them on-demand:
// After building chain context, backfill any missing digests
const completedSteps = steps.filter(s => s.status === 'completed' && !s.digest);
for (const step of completedSteps) {
await generateStepDigest(step.id);
}
This ensures no step gets left behind, regardless of when it was completed.
2. Handle Alternative Generation Paths
Our workflow engine supports generating multiple alternatives for a step, then selecting the best one. This code path was completely bypassing digest generation:
// Steps with alternatives need digests too
if (step.generateCount > 1) {
const selectedAlternative = alternatives[step.selectedIndex || 0];
await generateStepDigest(step.id, selectedAlternative.content);
}
3. Comprehensive Error Logging
I added error logging to every catch block in the digest pipeline. Silent failures are the enemy of maintainable systems—if something goes wrong, we need to know about it immediately.
The Results: Measurable Impact
After implementing the fixes, I ran a complete workflow to measure the compression effectiveness:
- Analyze Target Repo: 8KB → 4KB (50% compression)
- Design Features: Used compressed context, total prompt only 15.4KB instead of 25KB+
- Final Implementation: 32.2KB prompt with full context of all previous steps
The compression system was now working end-to-end, keeping prompt sizes manageable even for complex workflows.
Lessons Learned: The Pain Points That Teach Us
Database Tooling Gotchas
I needed to query the database to understand the workflow state, but ran into Prisma quirks:
# This doesn't work - Prisma uses model names, not table names
npx prisma db execute --sql "SELECT * FROM workflow_steps"
# This works - direct psql with proper column quoting
PGPASSWORD=mypass psql -h localhost -U user -d db
SELECT "workflowId", "selectedIndex" FROM workflow_steps;
Lesson: Prisma's db execute command has limitations. For complex queries, sometimes raw SQL with proper tooling is more reliable.
Authentication in Development Scripts
I wanted to trigger workflows from the command line for testing, but our API endpoints require browser session authentication. The solution was creating a bypass script:
// scripts/run-workflow.ts - bypasses API authentication
import { runWorkflow } from '../src/server/services/workflow-engine';
async function testWorkflow() {
const result = await runWorkflow(workflowId, {
skipAuth: true // Development only!
});
console.log('Workflow completed:', result);
}
Lesson: Always have a development path that bypasses authentication for testing. Just make sure it never reaches production!
Moving Forward: What's Next
With the digest system working reliably, the next steps are:
- Performance Analysis: Compare token costs before and after compression across multiple workflows
- Optimization: Make the backfill loop configurable to avoid unnecessary API calls
- Monitoring: Add metrics to track digest generation success rates
The Bigger Picture
This debugging session reinforced a fundamental principle: silent failures are worse than loud ones. When systems fail quietly, problems compound until they become much harder to diagnose and fix.
The key takeaways for any developer dealing with complex systems:
- Log errors explicitly - never swallow exceptions without visibility
- Test failure paths - ensure your error handling actually works
- Build debugging tools - sometimes you need to bypass normal flows to understand what's happening
- Measure impact - quantify the before and after to prove your fix works
Sometimes the most valuable debugging sessions aren't about clever algorithms or architectural decisions—they're about the unglamorous work of making sure errors are visible and systems fail gracefully. In this case, adding a few console.error statements turned an invisible problem into a solvable one.
What's your worst silent failure story? I'd love to hear about other debugging adventures in the comments below.