fix(tui): reliable large-prompt paste + version-robust turn detection (#130) (#131)

TUI mode hung ("stuck typing") on the OpenClaw agent's real (large, multi-line) prompts.
Three root causes, all fixed:

1. transcript.mjs — terminal detection only recognized {system, turn_duration}, which
   claude 2.1.114 (the cloud host) does not emit → readTuiTranscript never saw completion
   and ran to the 120s wallclock even though claude had answered. Now also treats an
   {assistant} line with message.stop_reason ∈ {end_turn, stop_sequence, max_tokens} as
   terminal (version-robust; stop_reason "tool_use" stays non-terminal, so tool turns are
   not truncated — preserves the v3.17.1 narrowing).

2. session.mjs paste — send-keys -l "$(cat file)" delivers a large multi-line prompt's
   newlines as separate key events (≈repeated Enter), so the prompt never lands. Replaced
   with tmux load-buffer + paste-buffer -p (bracketed paste): atomic, no shell arg limit,
   claude ingests it as one "[Pasted text]".

3. session.mjs verify — the old "placeholder-gone" heuristic false-positived on claude's
   empty curly-quote placeholder, so Enter fired into an empty box (THE root cause of the
   hang). tuiPromptLanded now trusts only positive signals ([Pasted text] indicator or the
   prompt's own text); readiness-poll + paste-verify-poll + fast-fail (tui_paste_not_landed)
   turns a 120s wallclock hang into a deterministic ~5s error.

Also: env delivered to the pane via an `env`-prefix on the pane command (tmux does NOT
forward spawnSync's env, and new-session -e needs tmux ≥3.2 while the cloud host runs 2.7),
carrying DISABLE_AUTOUPDATER (version pin) + CLAUDE_CODE_ENTRYPOINT labeling.

Validated live on both hosts: a 300-line / ~30KB prompt that previously hung now returns
in ~5-8s; small prompts ~4-5s. 192 tests pass (new: terminal-detection across schemas,
positive-signal verify incl. the curly-quote false-positive guard, paste predicates).

Scope discipline: an attempt to also strip claude's CLAUDE.md/auto-memory injection (context
reduction) was REVERTED — it didn't measurably reduce context through OCP, caused an
off-response, and is a separate concern. It will be its own measured branch.

ALIGNMENT.md: TUI is the ADR-0007 proxy-side execution bridge, not a forwarded Anthropic
operation, so a cli.js citation is N/A under Rule 2. No blacklisted tokens / port literals.

Independent fresh-context reviewer (opus): APPROVE WITH CHANGES (Iron Rule 10) — validated
the three fixes, caught that a per-session tmux socket would break the startup stale-session
reaper, and flagged the context-suppression scope-creep; both were reverted per the review.

Closes #130.

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
dtzp555-max
2026-06-01 21:26:25 +10:00
committed by GitHub
co-authored by taodeng Claude Opus 4.8
parent 1f577c075f
commit 9568411bcb
3 changed files with 187 additions and 34 deletions
+20 -10
View File
@@ -51,19 +51,29 @@ export function parseTranscriptLines(text) {
return out;
}
// A line marks the assistant turn complete when it is the turn_duration system
// event. That is the ONLY reliable terminal marker in interactive TUI mode.
// A line marks the assistant turn complete when EITHER:
// (a) {type:"system", subtype:"turn_duration"} — emitted by newer claude builds
// (e.g. 2.1.159), OR
// (b) {type:"assistant"} whose message.stop_reason is a FINAL reason
// ("end_turn" / "stop_sequence" / "max_tokens"). This is the API-level
// end-of-turn signal, present across claude builds whose transcripts do NOT
// emit turn_duration (e.g. 2.1.114 — verified live on the cloud host). Without
// it OCP can't detect completion on those builds and hangs to the wallclock,
// then returns only partial text (issue #130, cloud/server-side symptom).
//
// Why tool_use is NOT a terminal marker:
// In interactive claude, when the model decides to call a tool (stop_reason=
// "tool_use"), claude handles the tool call internally and then continues
// generating — the turn is NOT complete. The transcript advances to another
// assistant entry after the tool result. Only {type:"system",
// subtype:"turn_duration"} signals that claude has fully finished the turn.
// Treating tool_use as terminal would truncate tool-using turns mid-flight.
// stop_reason "tool_use" is deliberately NOT terminal: the model is mid-turn (it will
// run a tool and continue with a later assistant entry). Matching on a FINAL
// stop_reason — not on the mere presence of a tool_use — keeps tool-using turns intact.
// (The v3.17.1 narrowing dropped a buggy "tool_use is terminal" rule; this restores
// cross-version completion detection without bringing that bug back.)
const TERMINAL_STOP_REASONS = new Set(["end_turn", "stop_sequence", "max_tokens"]);
export function isTerminalLine(obj) {
if (!obj || typeof obj !== "object") return false;
return obj.type === "system" && obj.subtype === "turn_duration";
if (obj.type === "system" && obj.subtype === "turn_duration") return true;
if (obj.type === "assistant" && obj.message && typeof obj.message === "object") {
return TERMINAL_STOP_REASONS.has(obj.message.stop_reason);
}
return false;
}
// Text of the LAST assistant turn: concatenate its text content blocks