apache / apache/maka

bug(cli): TUI interrupt waits behind queue RPCs and terminal cleanup while still rendering Working

Open
#3,698 2 comments 0 reactions 0 assignees View on GitHub
bug help wanted
Dominant language
TypeScript
Stars
5.4k
Forks
502
Avg merge
1d 2h
Merged PRs (30d)
715

Description

## Problem

The TUI interrupt path is not cancellation-first. Once the double-Escape gesture is recognized, `requestTurnInterrupt()` marks local state but serializes the actual stop behind queue convergence:

```ts
await settlePendingEnqueues();
const retracted = (await input.driver.retractQueued?.()) ?? '';
const fallback = await takePendingFallbackSettled();
// ...refill editor...
await input.driver.stop();
```

This is in `packages/cli/src/pi-tui-runner.ts` around the `requestTurnInterrupt` implementation.

`settlePendingEnqueues()` does **not** wait for the model provider response. It waits for outstanding Runtime Host `turn.message.submit` RPCs used by Steer/Queue. Those operations normally only mutate queue/admission/receipt state, but an RPC delayed by transport, Session admission, storage, or a fallback retry becomes an unbounded barrier in front of stop. `queue.retract` then adds a second Host round trip before cancellation is even requested.

After stop is finally sent, the TUI driver uses `turn.stop`. Runtime Host `requestStop()` awaits `stopRoot()`, and `stopRoot()` awaits `active.done`, so the control response includes backend abort, active tool cleanup, process termination grace, I/O drain, and durable terminal publication. During that entire interval the TUI continues to render `Working…`; there is no `Cancelling…` state.

This combines two distinct delays into one apparently ignored keypress:

1. pre-stop delay: pending enqueue settlement + queue retract;
2. post-stop delay: tool/backend cleanup + durable terminal convergence.

The Host already exposes the atomic `turn.interrupt` operation, which commits the queue stop fence, retracts queued entries, and owns the matching root Turn. Desktop already uses it in `createRuntimeHostSessionStop`; the TUI still composes `queue.retract` and `turn.stop` client-side.

## User-visible trigger

The interrupt binding is itself difficult to discover: a running Turn requires two Escapes within 600 ms. The first Escape has no visible effect or hint. The binding appears only in `/help`, not in the startup guidance or running footer. This overlaps the discoverability discussion in #3538; this issue focuses on cancellation ordering, acknowledgement, and feedback rather than keybinding selection.

`Ctrl+C` is not a safe discoverability fallback because it also owns process-exit behavior: a second press while interruption is converging exits, and two presses while idle exit the TUI.

## Reproduction and measurements

Tested with the installed Maka 0.2.0 TUI in a fresh tmux session on macOS, using `qwen3.8-max` through `alibaba-token-plan-cn`.

### Provider stream

1. Start a prompt and interrupt while the first model response is pending.
2. Send Esc twice within 600 ms.

The durable `aborted` event was committed within the measurement resolution of the second Escape, and the TUI returned to idle in under 170 ms. This confirms the basic provider abort path can be fast.

### Cooperative Bash process

1. Ask the agent to execute `sleep 120`.
2. Wait for the Bash tool to show `running`.
3. Press one Escape: after one second the process is still alive and no stop was issued.
4. Press Escape twice within 600 ms.

Observed after the second Escape:

- child process exit: approximately 63 ms;
- TUI idle: approximately 132 ms;
- `abort_requested` to `tool_completed`: 20 ms;
- `abort_requested` to durable `aborted`: 27 ms.

### Process ignoring SIGTERM

Repeat with:

```sh
trap '' TERM; sleep 119
```

Observed after the second Escape:

- child process group exit: approximately 2.13 s;
- TUI idle: approximately 2.20 s.

This matches `DEFAULT_PROCESS_TERMINATION_GRACE_MS = 2000`. The grace may be intentional, but rendering `Working…` throughout makes it look as though interrupt was not accepted.

### Missing deterministic regression case

A focused TUI test can expose the pre-stop barrier without timing or a real provider:

1. use a driver whose `steer()`/`queueMessage()` returns a Promise that does not settle;
2. submit a mid-Turn message so it enters `pendingEnqueueTasks`;
3. trigger interrupt;
4. assert stop/interrupt authority is invoked promptly.

Today step 4 never occurs because `settlePendingEnqueues()` waits first.

## Expected behavior

- User interrupt has control-plane priority over pending message submission.
- Acceptance is visible immediately, independently of backend/tool cleanup.
- Queue contents are not lost or duplicated when enqueue and interrupt race.
- The running footer changes from `Working…` to `Cancelling…` as soon as the gesture is accepted.
- Terminal durability remains authoritative; fast acknowledgement must not fake completion.

## Proposed direction

1. Give the TUI driver a first-class interrupt method backed by Host `turn.interrupt`, rather than composing `queue.retract` followed by `turn.stop`.
2. Commit the stop/queue fence before waiting for client-side enqueue Promises. Host serialization can determine whether an enqueue committed before the fence and is returned as retracted, or lost the race and must be restored from its failed client request.
3. Split interrupt acceptance from terminal convergence if needed: return/emit an accepted or stopping snapshot once the stop fence is durable and abort delivery has begun; observe the final terminal through the existing subscription.
4. Render local `Cancelling…` immediately from `interruptRequested`, with elapsed cleanup time if convergence is slow.
5. Keep graceful process cleanup, but consider a shorter user-stop grace or an explicit force-stop escalation separately; it should not be required to fix acknowledgement and feedback.

## Acceptance criteria

- A never-settling enqueue request cannot prevent interrupt dispatch.
- Interrupt + enqueue races preserve each queued message exactly once, either in Host retraction output or restored client-side.
- TUI shows `Cancelling…` in the same render tick as interrupt recognition.
- Cooperative provider and Bash cancellation remain sub-second in integration coverage.
- A SIGTERM-resistant process may take the configured grace, but the UI truthfully reports cancellation in progress.
- TUI and Desktop use the same Runtime Host interrupt authority instead of divergent client-composed sequences.

Related: #3538 (composer reachability/discoverability), #3556 (queue-code simplification).

_Investigated with AI assistance. Runtime behavior was verified against local source and a real tmux TUI run._

Contributor guide

Open the contributing guide

Research direction

Start in packages/cli/src/pi-tui-runner.ts at requestTurnInterrupt(), then compare the TUI flow with Desktop’s createRuntimeHostSessionStop and the Host turn.interrupt operation. Add or update a focused TUI regression test using a never-settling steer() or queueMessage() promise. Done means interrupt dispatch is not blocked, Cancelling… renders immediately, enqueue races preserve messages, and cancellation remains truthful during cleanup.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
backend-api-design, cli, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
56/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.