Skip to content

[copilot-finds] Bug: waitForState timeout handler fails to remove stale waiter due to reference mismatch #201

@github-actions

Description

@github-actions

Problem

In InMemoryOrchestrationBackend.waitForState() (in-memory-backend.ts), the timeout handler attempts to clean up the stale waiter by comparing w.resolve === resolve, but this comparison always fails because w.resolve is a wrapper function — not the original Promise resolve.

Code (before fix):

const timer = setTimeout(() => {
  const waiters = this.stateWaiters.get(instanceId);
  if (waiters) {
    const index = waiters.findIndex((w) => w.resolve === resolve);
    //                                     ^^^^^^^^^^^^^^^^^^^^^^^^
    // BUG: w.resolve is a wrapper (result) => { clearTimeout(timer); resolve(result); }
    // so it will never === resolve. findIndex always returns -1.
    if (index >= 0) {
      waiters.splice(index, 1); // Never executed
    }
  }
  reject(new Error(...));
}, timeoutMs);

const waiter: StateWaiter = {
  resolve: (result) => {
    clearTimeout(timer);
    resolve(result);   // ← wraps resolve, so waiter.resolve !== resolve
  },
  ...
};

Root Cause

The StateWaiter.resolve property is set to a wrapper function that calls clearTimeout(timer) before calling the original Promise resolve. The timeout handler then tries to find the waiter by comparing w.resolve === resolve — but w.resolve is the wrapper, not the original resolve, so the identity check always fails.

Impact

  • Memory leak: Timed-out waiters permanently accumulate in the stateWaiters map.
  • Wasted computation: notifyWaiters() continues evaluating predicates on stale waiters.
  • Timer leak: The waitForState timeout timer is not tracked in pendingTimers, so it is not cleaned up by reset().
  • Affects any test using waitForState (indirectly via TestOrchestrationClient.waitForOrchestrationCompletion, waitForOrchestrationStart, etc.) where the wait times out.
  • No data corruption since calling resolve() on a settled Promise is a no-op, but the stale waiter and its closure remain in memory.

Proposed Fix

  1. Move the waiter declaration before the timer so the timeout callback can use waiters.indexOf(waiter) (object identity) instead of findIndex with a broken reference comparison.
  2. Track the waitForState timeout timer in pendingTimers so reset() cleans it up.
  3. Remove the timer from pendingTimers when the waiter resolves, rejects, or times out.
  4. Clean up the stateWaiters map entry when the last waiter for an instance is removed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    copilot-findsFindings from daily automated code review agent

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions