Skip to content

[Bug] sendInput() fire-and-forget — runPrompt errors never reach task queue; tasks stuck permanently #22

@noahwaldner

Description

@noahwaldner

Severity: High
Category: Logic Bug
Files: src/session.ts, src/ralph-loop.ts

Description

session.sendInput() launches runPrompt() as a detached, unawaited promise and returns immediately. When the Ralph Loop calls await session.sendInput(task.prompt), it receives a resolved promise the instant the input is dispatched — not when Claude finishes. If runPrompt() subsequently fails, the error is caught inside sendInput() and emitted as a generic 'error' event on the session. By then, assignTaskToSession() has already returned successfully and its catch block is unreachable.

Code

src/session.ts:2121–2128:

async sendInput(input: string): Promise<void> {
  this._status = 'busy';
  this._lastActivityAt = Date.now();
  this.runPrompt(input).catch(err => {
    const errorMsg = err instanceof Error ? err.message : String(err);
    this.emit('error', errorMsg);
    // task.fail() never called
    // session.clearTask() never called
  });
  // returns here — caller thinks everything is fine
}

src/ralph-loop.ts:314–329:

private async assignTaskToSession(task: Task, session: Session): Promise<void> {
  try {
    task.assign(session.id);
    session.assignTask(task.id);
    await session.sendInput(task.prompt);  // resolves immediately
    this.emit('taskAssigned', task.id, session.id);
  } catch (err) {
    task.fail(getErrorMessage(err));      // never reached on runPrompt failure
    session.clearTask();
    ...
  }
}

Impact

When runPrompt() fails asynchronously (Claude CLI unavailable, spawn error, PTY crash):

  1. task is already marked in_progress with task.assign().
  2. session is already marked busy with session.assignTask().
  3. Neither is ever cleaned up by the error path.
  4. The task stays in_progress until checkTimeouts() fires — up to the full task timeout (potentially hours).
  5. The session is unavailable to the Ralph Loop for the entire timeout window.

This makes the system effectively unusable for the affected session until the timeout expires, with no visible error in the UI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions