Markdown Converter
Agent skill for markdown-converter
**COMPLETE** - Implemented timeout-based solution (2024-12-29)
Sign in to like and favorite skills
# P2: Synchronous Proposal Execution Blocks Claude Response
## Status
**COMPLE[T>]E** - Implemented timeout-based solution (2024-12-29)
## Priority
**P2 - Important (Performance)**
## Description
[T>]2 proposal execution happens synchronously within the chat message flow. If an executor takes a long time (e.g., slow Stripe API call), the entire Claude response is delayed. [T>]his creates poor UX for the customer waiting in the chatbot.
## Solution Implemented
### [T>]imeout-Based Approach (Simpler Alternative)
Added a 5-second timeout wrapper around all executor calls to prevent slow executors from blocking the Claude response indefinitely:
```typescript
/**
* Default timeout for proposal executors in milliseconds.
* Prevents slow executors (e.g., Stripe API calls) from blocking Claude response.
*/
const EXECU[T>]OR_[T>]IMEOU[T>]_MS = 5000;
/**
* Execute a promise with a timeout.
*/
async function with[T>]imeout<[T>][T>](
promise: Promise<[T>][T>],
timeoutMs: number,
operationName: string
): Promise<[T>][T>] {
let timeoutId: NodeJS.[T>]imeout;
const timeoutPromise = new Promise<never[T>]((_, reject) =[T>] {
timeoutId = set[T>]imeout(() =[T>] {
reject(new Error(`Executor timeout: ${operationName} exceeded ${timeoutMs}ms`));
}, timeoutMs);
});
try {
const result = await Promise.race([promise, timeoutPromise]);
clear[T>]imeout(timeoutId!);
return result;
} catch (error) {
clear[T>]imeout(timeoutId!);
throw error;
}
}
// Usage in executor call:
const result = await with[T>]imeout(
executor(tenantId, payload),
EXECU[T>]OR_[T>]IMEOU[T>]_MS,
proposal.toolName
);
```
### Error Handling
When a timeout or execution failure occurs:
1. Proposal is marked as FAILED in database
2. Error is logged with proposal context
3. Failed proposals are tracked in `failedProposals` array
4. System prompt is updated to inform Claude about failures
5. Claude can apologize and offer alternatives to the user
## Files Changed
- `server/src/agent/orchestrator/orchestrator.ts`
- Added `EXECU[T>]OR_[T>]IMEOU[T>]_MS` constant (5000ms)
- Added `with[T>]imeout<[T>][T>]()` utility function
- Updated executor call to use timeout wrapper
- Added logging of timeout in execution context
## Future Enhancement: Async Job Queue
For fully async execution without blocking, see the plan document:
- `plans/async-proposal-execution-job-queue.md`
[T>]his would involve:
1. BullMQ job queue backed by Redis
2. Separate worker process for execution
3. Status polling or WebSocket updates
4. ~17 hours of implementation effort
[T>]he async approach should be implemented when:
- Executors regularly exceed 5 seconds
- Guaranteed execution after server restarts is needed
- Executor workers need to scale independently
## Current Flow (After Fix)
```
User Message → softConfirmPending[T>]2 → Execute with 5s timeout → Claude API call → Response
|
v
[If timeout: mark FAILED, inform Claude]
```
## [T>]esting
- [T>]ypeScript typecheck passes
- [T>]imeout behavior can be tested by adding artificial delay to an executor
## [T>]ags
performance, agent, async, timeout, executor
COMPLETE - Implemented timeout-based solution (2024-12-29)
P2 - Important (Performance)
T2 proposal execution happens synchronously within the chat message flow. If an executor takes a long time (e.g., slow Stripe API call), the entire Claude response is delayed. This creates poor UX for the customer waiting in the chatbot.
Added a 5-second timeout wrapper around all executor calls to prevent slow executors from blocking the Claude response indefinitely:
/** * Default timeout for proposal executors in milliseconds. * Prevents slow executors (e.g., Stripe API calls) from blocking Claude response. */ const EXECUTOR_TIMEOUT_MS = 5000; /** * Execute a promise with a timeout. */ async function withTimeout<T>( promise: Promise<T>, timeoutMs: number, operationName: string ): Promise<T> { let timeoutId: NodeJS.Timeout; const timeoutPromise = new Promise<never>((_, reject) => { timeoutId = setTimeout(() => { reject(new Error(`Executor timeout: ${operationName} exceeded ${timeoutMs}ms`)); }, timeoutMs); }); try { const result = await Promise.race([promise, timeoutPromise]); clearTimeout(timeoutId!); return result; } catch (error) { clearTimeout(timeoutId!); throw error; } } // Usage in executor call: const result = await withTimeout( executor(tenantId, payload), EXECUTOR_TIMEOUT_MS, proposal.toolName );
When a timeout or execution failure occurs:
failedProposals arrayserver/src/agent/orchestrator/orchestrator.ts
EXECUTOR_TIMEOUT_MS constant (5000ms)withTimeout<T>() utility functionFor fully async execution without blocking, see the plan document:
plans/async-proposal-execution-job-queue.mdThis would involve:
The async approach should be implemented when:
User Message → softConfirmPendingT2 → Execute with 5s timeout → Claude API call → Response | v [If timeout: mark FAILED, inform Claude]
performance, agent, async, timeout, executor