# How Nodeterm Maintains the Waiting State for Codex's `request_user_input`

> Discover how Nodeterm maintains the waiting state for Codex request_user_input. Learn how normalize.ts and the agent-status-mirror reducer ensure a seamless user experience.

- Repository: [eneskirca/nodeterm](https://github.com/eneskirca/nodeterm)
- Tags: internals
- Published: 2026-08-23

---

**TLDR: Nodeterm keeps a Codex node in the `waiting` state across turn boundaries by special-casing `PreToolUse` and `PostToolUse` events for the `request_user_input` tool in [`normalize.ts`](https://github.com/eneskirca/nodeterm/blob/main/normalize.ts), then preserving the hold in the `agent-status-mirror` reducer so the UI continues to show an awaiting-input badge.**

When a Codex session invokes the `request_user_input` tool (the "ask-the-user" operation), the question stays open until the human responds. If Nodeterm treated that as a normal tool call, the node would flip to `done` the moment the turn finished — losing the visual waiting state. To prevent that, the open-source project [eneskirca/nodeterm](https://github.com/eneskirca/nodeterm) implements a two-phase mechanism across its normalization and state-reduction layers. Here's exactly how it works, straight from the source.

## Where the Waiting State Is Enforced

The logic lives in two complementary files, each responsible for a different stage of the event pipeline:

- **[`src/shared/agents/normalize.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/normalize.ts)** — converts raw Codex hook events into normalized internal events.
- **[`src/core/agent-status-mirror.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/agent-status-mirror.ts)** — merges those normalized events into the node's persistent state.

Both files work together to guarantee that a node stays in the `waiting` state for as long as the user's answer is pending.

## Normalizing Codex Events for `request_user_input`

The `normalizeCodex` function in [`src/shared/agents/normalize.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/normalize.ts) inspects every incoming hook event and applies special handling when the tool is `request_user_input`. The critical logic appears around lines 311–318 of that file.

The behavior is deliberately asymmetric between the two event types:

- **`PreToolUse` events** — when the tool is about to execute, Nodeterm sets the agent's state to `waiting`, adds an `awaitingInput` flag, and stores the question text in a `question` field. This tells the UI to render the node as "waiting for input" rather than "processing."

- **`PostToolUse` events** — the code intentionally does nothing. This is the single most important detail, because Codex's turn ends with the question still unanswered. If the `PostToolUse` handler cleared the waiting flag, the node would transition to `done` prematurely. By returning `null` and doing nothing, the waiting state survives the turn boundary.

Here's the actual normalization logic:

```ts
// 1. Normalization – keep waiting for the question
if (p.tool_name === 'request_user_input' && (ev === 'PreToolUse' || ev === 'PostToolUse')) {
  // PreToolUse → mark waiting + store the question
  if (ev === 'PreToolUse') {
    return {
      state: 'waiting',
      awaitingInput: true,
      question: p.args?.question ?? '',
    };
  }
  // PostToolUse → do nothing (preserve waiting state)
  return null;
}

```

The unit tests in [`src/shared/agents/normalize.test.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/normalize.test.ts) (lines 372–398) explicitly confirm that both event types are handled this way, so regressions in this behavior get caught automatically.

## Preserving the Hold in the State Reducer

Once normalized, the events flow into the `reduceEntry` function in [`src/core/agent-status-mirror.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/agent-status-mirror.ts). This reducer merges events into the node's persistent state, and it's the second line of defense for the waiting state.

The reducer checks whether the entry's hold is an `request_user_input` hold with `awaitingInput` set to true. If so, it forces the state back to `waiting`, re-applies the `awaitingInput` flag, and carries the stored question text forward — even if other events in the same batch would otherwise transition the node to `done`.

```ts
// 2. State reduction – preserve the waiting hold across turn end
function reduceEntry(entry: AgentEntry) {
  // If we have an unanswered request_user_input hold, keep it
  if (entry.hold?.tool_name === 'request_user_input' && entry.hold.awaitingInput) {
    entry.state = 'waiting';
    entry.awaitingInput = true;
    entry.question = entry.hold.question;
  }
  // Normal transition handling …
  return entry;
}

```

This reducer behavior is exercised in [`src/core/agent-status-mirror.test.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/agent-status-mirror.test.ts) under the sections titled **"codex request_user_input hold (awaitingInput)"** and **"the request_user_input hold clears it — it commits `waiting`"**.

## Ending the Waiting State

The waiting state doesn't last forever. Once the user supplies their answer through a separate `UserPromptSubmit` event, the reducer clears the hold. At that point the `awaitingInput` flag is removed, and the node follows the normal transition — likely moving to `done` or whatever state is appropriate for the next step in the agent's workflow.

This means the lifetime of the waiting state is precisely bound:

```text
Codex sends PreToolUse(request_user_input)
  → node enters `waiting` + awaitingInput=true + question stored
Codex sends PostToolUse(request_user_input)
  → normalize returns null; reducer preserves hold; node stays `waiting`
User submits a response (UserPromptSubmit)
  → hold is cleared; awaitingInput removed; node transitions normally

```

## The UI Side: Visualizing the Waiting State

The `awaitingInput` flag and `question` field don't just serve internal bookkeeping. The UI layer reads those fields to render a visible badge on the node, so users always know a question is pending.

```tsx
// 3. UI component – render a waiting badge when awaiting input
function WaitingBadge({ node }: { node: NodeState }) {
  if (node.state === 'waiting' && node.awaitingInput) {
    return <Badge title={node.question}>Waiting for input…</Badge>;
  }
  return null;
}

```

This conditional check — `node.state === 'waiting' && node.awaitingInput` — is what makes the badge appear only for genuinely unanswered questions, not for other arbitrary waiting conditions.

## Key Files and Their Roles

| File | Purpose |
|------|---------|
| [[`src/shared/agents/normalize.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/normalize.ts)](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/normalize.ts#L311-L318) | Normalizes Codex hook events; contains the special case for `request_user_input` that differentiates `PreToolUse` from `PostToolUse` behavior. |
| [[`src/core/agent-status-mirror.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/agent-status-mirror.ts)](https://github.com/eneskirca/nodeterm/blob/main/src/core/agent-status-mirror.ts#L370-L410) | Merges normalized events into the node's persisted state; preserves the waiting hold across turn boundaries. |
| [[`src/shared/agents/normalize.test.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/normalize.test.ts)](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/normalize.test.ts#L372-L398) | Unit tests that confirm `PreToolUse` marks waiting and `PostToolUse` does not. |
| [[`src/core/agent-status-mirror.test.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/agent-status-mirror.test.ts)](https://github.com/eneskirca/nodeterm/blob/main/src/core/agent-status-mirror.test.ts#L1796-L2050) | Tests that ensure the waiting hold persists across normalized event merges. |

## Summary

- The waiting state is preserved by **not acting on `PostToolUse`** for `request_user_input` — doing nothing is the deliberate design choice that stops the node from moving to `done`.
- **`PreToolUse`** marks the state as `waiting`, sets `awaitingInput`, and records the user's question for UI rendering.
- The **reducer in [`agent-status-mirror.ts`](https://github.com/eneskirca/nodeterm/blob/main/agent-status-mirror.ts)** re-applies the waiting hold on every merge, acting as a safety net even if other events in the turn would conflict.
- Waiting ends only when a separate `UserPromptSubmit` event carries the user's answer, which then clears the hold.
- The UI's `WaitingBadge` component renders an explicit badge pulled straight from the `question` field when both `waiting` and `awaitingInput` are set.

## Frequently Asked Questions

### Why does Nodeterm ignore the `PostToolUse` event for `request_user_input`?

Because Codex's turn ends immediately after a `request_user_input` PostToolUse fires, and the question is still pending. If the handler cleared the waiting flag there, the node would immediately flip to `done`, which would mislead the user into thinking the agent had finished. Ignoring the event is the intentional design that lets the waiting state survive the turn boundary.

### What exactly does the `awaitingInput` flag do in the agent state?

`awaitingInput` is a boolean metadata flag that sits alongside `state: 'waiting'`. It signals to the UI layer that this particular waiting state derives from an unanswered user question rather than from any other reason (like a tool call in progress). The UI checks it alongside `state === 'waiting'` before rendering the "Waiting for input…" badge.

### How does the waiting state get cleared once the user replies?

A separate `UserPromptSubmit` event arrives once the user sends their answer. The reducer in [`src/core/agent-status-mirror.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/core/agent-status-mirror.ts) recognizes this event, removes the `awaitingInput` hold, and lets the node transition through its normal lifecycle — effectively moving from `waiting` to a next step like `done` or `running`.

### Is the `request_user_input` behavior specific to Codex, or does it apply to other agents?

It is Codex-specific. The special case lives inside `normalizeCodex` in [`src/shared/agents/normalize.ts`](https://github.com/eneskirca/nodeterm/blob/main/src/shared/agents/normalize.ts), not in the generic normalizer. Other agents (like OpenAI's function-calling tool use) have their own normalization paths and do not inherit this special behavior unless they implement similar logic.