Skip to content

feat: add turn_end observer hook#2578

Closed
AresNing wants to merge 11 commits into
Hmbown:mainfrom
AresNing:1364-phase2-turn-end
Closed

feat: add turn_end observer hook#2578
AresNing wants to merge 11 commits into
Hmbown:mainfrom
AresNing:1364-phase2-turn-end

Conversation

@AresNing

@AresNing AresNing commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the Phase 2 turn_end lifecycle hook from #1364 as an observer-only post-turn event.

Scope:

  • Adds HookEvent::TurnEnd / event = "turn_end" discovery via /hooks events.
  • Adds structured observer hook execution that writes JSON to stdin, ignores stdout, logs failures, and never blocks caller state.
  • Fires turn_end from the TUI TurnComplete branch after post-turn state updates and before queued-message dispatch.
  • Documents the config, payload, stdout ignored behavior, continue_on_error observer semantics, and updates the Hooks need mutation rights on user submit and a turn-end event #1364 RFC baseline.
  • Syncs web/package-lock.json so Web Frontend CI can run npm ci after the web docs change.

Not in this slice:

  • No transcript, user text, model response, tool argument, or tool result mutation.
  • No subagent lifecycle hooks; those remain Phase 3.
  • No gating/blocking behavior for turn_end.

Builds on: #2434
Issues: Refs #1364 (partial)

Testing

  • cargo fmt --check
  • cargo check
  • cargo clippy --workspace --all-targets --all-features
  • cargo test -p codewhale-tui hooks::
  • env HOME=/private/tmp/codewhale-test-home-turnend CARGO_HOME=/Users/aresning/.cargo RUSTUP_HOME=/Users/aresning/.rustup cargo test -p codewhale-tui mcp::tests::legacy_sse_closed_stream_reconnects_and_retries_tool_call --all-features
  • env HOME=/private/tmp/codewhale-test-home-turnend CARGO_HOME=/Users/aresning/.cargo RUSTUP_HOME=/Users/aresning/.rustup RUST_TEST_THREADS=1 cargo test --workspace --all-features
  • npm install --package-lock-only --ignore-scripts --registry=https://registry.npmjs.org/ in web/
  • npm ci --ignore-scripts --registry=https://registry.npmjs.org/ in web/
  • npm run lint in web/
  • npx tsc --noEmit in web/
  • git diff --check

Notes:

  • The un-isolated full Rust test hit a readonly default ~/.deepseek/state.db; final full run used isolated HOME.
  • Parallel full Rust runs exposed a local MCP SSE reconnect flake (connection closed before message completed); the exact test passed, and the final single-thread full workspace run passed.
  • Local full npm ci without --ignore-scripts hung in a macOS postinstall path, so dependency/lockfile validation used --ignore-scripts; GitHub Actions runs full npm ci on Ubuntu.
  • Greptile's observer continue_on_error documentation feedback was addressed and the review thread is resolved.

Checklist

  • Updated docs or comments as needed
  • Added or updated tests where relevant
  • Verified TUI behavior manually if UI changes (no visual UI behavior; dispatch behavior covered by hook/unit tests)

Greptile Summary

This PR implements the Phase 2 turn_end observer hook from the #1364 lifecycle RFC. The hook fires after each terminal turn outcome (completed, interrupted, failed), runs asynchronously via tokio::task::spawn_blocking to keep the event loop non-blocking, and delivers a structured JSON payload to hook scripts on stdin while ignoring stdout.

  • Adds HookEvent::TurnEnd, execute_structured_observer, and turn_end_payload in hooks.rs; wires the hook into the EngineEvent::TurnComplete branch of ui.rs after all post-turn state updates and before queued-message dispatch.
  • Documents observer semantics (continue_on_error does not gate later hooks, stdout is ignored, failures are warn-only) in both docs/CONFIGURATION.md and the RFC; surface-level web doc update adds turn_end to the quick-reference comment.
  • Syncs web/package-lock.json to resolve a vitest esbuild version split so Web CI can run npm ci cleanly.

Confidence Score: 5/5

Safe to merge. The observer hook is strictly additive, fire-and-forget, and cannot mutate or block any caller state.

The implementation is architecturally sound: spawn_blocking keeps the Tokio event loop unblocked, has_hooks_for_event correctly gates on the enabled flag, the payload snapshot is taken before queued-message pop so counts are accurate, and the unconditional-iteration / warn-only failure model is both intentional and clearly documented. Tests cover stdin delivery, stdout-ignored contract, and the continue_on_error=false non-propagation case explicitly. No behavioral regressions to existing hook events are possible because the new code path only activates when TurnEnd hooks are explicitly configured.

No files require special attention.

Important Files Changed

Filename Overview
crates/tui/src/hooks.rs Core hook implementation: adds TurnEnd enum variant, TurnEndPayloadInput/TurnEndTotals structs, execute_structured_observer (unconditional iteration, warn-on-failure, observer-only), and turn_end_payload builder. Tests cover payload shape, stdin delivery, stdout-ignored contract, and failure non-propagation including continue_on_error=false.
crates/tui/src/tui/ui.rs Wires turn_end into TurnComplete branch: payload is built from post-update app state with queued_message_count sampled before pop_queued_message, then dispatched via spawn_blocking. Also extracts turn_outcome_status_label helper and fixes error borrow to as_deref.
crates/tui/src/commands/hooks.rs Adds turn_end to /hooks events discovery listing, event_label mapping, and corresponding unit tests. Changes are mechanical and complete.
docs/CONFIGURATION.md Adds a clear observer turn_end hooks section with TOML config example, full JSON payload sample, and explicit prose that continue_on_error does not stop later hooks in observer mode.
docs/rfcs/1364-hooks-lifecycle.md RFC updated from Draft to Phase 1 landed / Phase 2+3 spec; restructured to reflect current state, shared design rules, Phase 2 technical spec with payload rules, and Phase 3 subagent spec. Documentation only, no runtime impact.
web/app/[locale]/docs/page.tsx Adds turn_end to both the Chinese and English quick-reference comment strings and appends a one-sentence description of turn_end observer semantics to each hooks paragraph.
web/package-lock.json Lockfile sync: marks esbuild platform packages dev:true and adds vitest-scoped esbuild 0.28.0 entries; generated mechanically via npm install --package-lock-only.

Sequence Diagram

sequenceDiagram
    participant Engine
    participant EventLoop as UI Event Loop
    participant App
    participant BlockingPool as Tokio Blocking Pool
    participant HookScript as Hook Script

    Engine->>EventLoop: EngineEvent::TurnComplete
    EventLoop->>App: Clear loading/streaming state
    EventLoop->>App: Set runtime_turn_status
    EventLoop->>App: Update token counters and cost
    EventLoop->>App: Schedule session snapshot
    EventLoop->>App: has_hooks_for_event(TurnEnd)?
    alt hooks configured and enabled
        EventLoop->>App: Snapshot post-update state into payload
        EventLoop->>App: Clone HookExecutor
        EventLoop->>BlockingPool: spawn_blocking fire-and-forget
        EventLoop->>App: pop_queued_message and dispatch next turn
        BlockingPool->>HookScript: exec with JSON on stdin
        HookScript-->>BlockingPool: exit code and stderr
        Note over BlockingPool: stdout ignored, failures warn-only
    else no hooks configured
        EventLoop->>App: pop_queued_message and dispatch next turn
    end
Loading

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (8): Last reviewed commit: "merge origin/main into phase2 branch" | Re-trigger Greptile

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown

Thanks @AresNing for taking the time to contribute.

This repository is currently observing a maintainer-managed contribution gate in dry-run mode, so this pull request is staying open. When enforcement is enabled, pull requests from contributors who are not listed in .github/APPROVED_CONTRIBUTORS will be closed automatically.

Please read CONTRIBUTING.md for the expected contribution shape. A maintainer can grant PR access by commenting /lgtm on a pull request.

Comment thread crates/tui/src/hooks.rs
@AresNing AresNing force-pushed the 1364-phase2-turn-end branch from 774217e to 74d69c3 Compare June 2, 2026 06:30
@Hmbown

Hmbown commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Thanks @AresNing. I’m closing this as harvested and credited rather than leaving the original branch open after the v0.9 stewardship branch took the focused turn-end observer hook slice.

The current stewardship branch contains the turn_end observer hook in 0d66ef34d:

  • HookEvent::TurnEnd and structured turn_end payload support in crates/tui/src/hooks.rs
  • /hooks events discovery coverage in crates/tui/src/commands/hooks.rs
  • TurnComplete observer dispatch after post-turn TUI state updates in crates/tui/src/tui/ui.rs
  • docs in docs/CONFIGURATION.md and docs/rfcs/1364-hooks-lifecycle.md
  • changelog credit for @AresNing and the Hooks need mutation rights on user submit and a turn-end event #1364 lifecycle work

Verified locally on the stewardship branch with:

cargo test -p codewhale-tui --bin codewhale-tui --locked turn_end -- --nocapture
cargo test -p codewhale-tui --bin codewhale-tui --locked hooks:: -- --nocapture

The focused turn_end run passed 2/2 tests, and the broader hooks run passed 43/43 tests. The payload test now asserts both totals.input_tokens and totals.output_tokens, so the final review gap is covered as well.

Thank you for pushing the lifecycle-hook surface forward.

@Hmbown Hmbown closed this Jun 5, 2026
timothybrush pushed a commit to timothybrush/DeepSeek-TUI that referenced this pull request Jun 8, 2026
Harvested the narrow Rust/docs slice of PR Hmbown#2578 by @AresNing for Hmbown#1364. The event uses the maintained structured observer path: JSON stdin, stdout ignored, warn-only failures, and no ability to block or mutate the turn.

The hook fires after post-turn app state, usage totals, cost, notification, receipt, and queue-recovery state are updated, before queued follow-up dispatch. Docs, RFC notes, /hooks discovery, and v0.9 tracking now describe the observer-only contract.

Co-authored-by: AresNing <49557311+AresNing@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants