chat: /clear round-trip is 3-5s since the #149 clear-hook — pull + double transcript parse block the forward #177

Open
opened 2026-07-18 21:39:07 +02:00 by dominik.polakovics · 0 comments

Symptom

Sending /clear (or New conversation) used to feel sub-second; since the clear-hook shipped (issue #149, ~Jul 12) it takes 3-5 seconds before the chat visibly resets. Perceived as gradual degradation, but it is a discrete regression amplified by forge latency (issue #176).

Mechanism (code-confirmed)

handleClearWithPull (internal/httpapi/chat.go) runs, serially, before the clear is forwarded:

  1. Busy-gate chat.Read — a full transcript parse (~20ms/MB measured with the real parser; 30-75ms at current sizes, grows within a conversation).
  2. PullBase when idle — git fetch against the forge + merge (~1-3s; forge RTT is 0.3-0.5s and rising with forge load).
  3. chat.Reply -> dialogPending -> a second full transcript parse.
  4. tmux paste + 250ms keyDelay pacing + Enter.

Then the UI waits up to one tailer tick (1s poll) for the rotation to be noticed and the stream to reset. Total: ~3-5s of blocking work for what reads as an instant local action.

Fix direction

The clear is already sacrosanct ("always goes through"); make it fast too:

  • Don't let the pull block the forward: forward the clear immediately, run the pull concurrently (or right after the forward, before the agent gets its first prompt — the fresh conversation has no context to invalidate). If a strict pull-before-clear ordering must survive, at least drop the redundant work below so only the fetch remains.
  • Drop one of the two full parses on the reply path: the busy-gate read and dialogPending read the same state within milliseconds — a single read can serve both gates. (The tailer's last derived state is also available via chat.State(session) and may be fresh enough for the busy gate.)
  • Consider surfacing the pull result as the existing notice mechanism after the fact instead of pre-blocking.

Constraints: keep the #149/#151 semantics — arg-bearing clears still pull, a failed pull is loud but never blocks the clear, a working agent skips the pull. The rotation-detection path (locateActive, issue #34) is fine and not part of this.

Verification

Reply-route histogram (lab_http_request_duration_seconds{route="/api/v1/runs/{id}/reply"}): clear sends currently land in the 1-10s buckets; after the fix a clear POST should return in the same ~400ms band as an ordinary reply, and the chat should reset within ~1 tailer tick.

## Symptom Sending `/clear` (or New conversation) used to feel sub-second; since the clear-hook shipped (issue #149, ~Jul 12) it takes 3-5 seconds before the chat visibly resets. Perceived as gradual degradation, but it is a discrete regression amplified by forge latency (issue #176). ## Mechanism (code-confirmed) `handleClearWithPull` (`internal/httpapi/chat.go`) runs, serially, before the clear is forwarded: 1. Busy-gate `chat.Read` — a **full transcript parse** (~20ms/MB measured with the real parser; 30-75ms at current sizes, grows within a conversation). 2. `PullBase` when idle — **git fetch against the forge + merge** (~1-3s; forge RTT is 0.3-0.5s and rising with forge load). 3. `chat.Reply` -> `dialogPending` -> a **second full transcript parse**. 4. tmux paste + 250ms keyDelay pacing + Enter. Then the UI waits up to one tailer tick (1s poll) for the rotation to be noticed and the stream to reset. Total: ~3-5s of blocking work for what reads as an instant local action. ## Fix direction The clear is already sacrosanct ("always goes through"); make it *fast* too: - Don't let the pull block the forward: forward the clear immediately, run the pull concurrently (or right after the forward, before the agent gets its first prompt — the fresh conversation has no context to invalidate). If a strict pull-before-clear ordering must survive, at least drop the redundant work below so only the fetch remains. - Drop one of the two full parses on the reply path: the busy-gate read and `dialogPending` read the same state within milliseconds — a single read can serve both gates. (The tailer's last derived state is also available via `chat.State(session)` and may be fresh enough for the busy gate.) - Consider surfacing the pull result as the existing notice mechanism after the fact instead of pre-blocking. Constraints: keep the #149/#151 semantics — arg-bearing clears still pull, a failed pull is loud but never blocks the clear, a working agent skips the pull. The rotation-detection path (locateActive, issue #34) is fine and not part of this. ## Verification Reply-route histogram (`lab_http_request_duration_seconds{route="/api/v1/runs/{id}/reply"}`): clear sends currently land in the 1-10s buckets; after the fix a clear POST should return in the same ~400ms band as an ordinary reply, and the chat should reset within ~1 tailer tick.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#177
No description provided.