P2: Synchronous Proposal Execution Blocks Claude Response

**COMPLETE** - Implemented timeout-based solution (2024-12-29)

promptBeginner5 min to valuemarkdown
0 views
Feb 12, 2026

Sign in to like and favorite skills

Prompt Playground

1 Variables

Fill Variables

Preview

# 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
Share: