Skip to content

feat: add turn_end observer hook#2578

Open
AresNing wants to merge 5 commits into
Hmbown:mainfrom
AresNing:1364-phase2-turn-end
Open

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

Conversation

@AresNing
Copy link
Copy Markdown

@AresNing AresNing commented Jun 2, 2026

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 RFC #1364, adding a fire-and-forget post-turn lifecycle event that delivers a structured JSON payload to user-configured shell hooks after every terminal turn outcome.

  • Adds HookEvent::TurnEnd with a new execute_structured_observer helper that iterates all matching hooks unconditionally (ignoring continue_on_error for sequencing), logs failures as warnings, and never blocks the UI — behaviour explicitly documented and covered by a dedicated test.
  • Wires the hook in ui.rs via tokio::task::spawn_blocking after all post-turn state (token counters, cost, receipts, persistence scheduling) has been updated and before any queued-message dispatch, so the payload reflects the fully settled turn state.
  • Updates /hooks events, docs/CONFIGURATION.md, the web docs, and the RFC baseline; syncs web/package-lock.json for CI.

Confidence Score: 5/5

Safe to merge — the hook is purely observer-only, dispatched off the UI thread, and can never affect turn state or block the next user action.

The turn_end observer is entirely fire-and-forget: the JoinHandle from spawn_blocking is dropped, hook failures are logged and discarded, and the payload is built from already-settled app state. The intentional deviation from continue_on_error sequencing is tested and explicitly called out in the documentation. No existing hook contract is changed.

No files require special attention.

Important Files Changed

Filename Overview
crates/tui/src/hooks.rs Adds HookEvent::TurnEnd, TurnEndTotals, TurnEndPayloadInput, execute_structured_observer, and turn_end_payload; observer loop intentionally ignores continue_on_error, which is tested and documented
crates/tui/src/tui/ui.rs Wires TurnEnd hook dispatch via spawn_blocking after all post-turn state updates; refactors status label into a shared helper and fixes error borrow to keep the value available for the payload
crates/tui/src/commands/hooks.rs Registers TurnEnd in the /hooks events listing and adds the corresponding label and unit-test assertions
docs/CONFIGURATION.md Adds a clear turn_end observer section with TOML config, full JSON payload example, status values, and explicit note that continue_on_error does not stop later observer hooks
docs/rfcs/1364-hooks-lifecycle.md Major RFC update: promotes document from Draft to Phase 1 landed / Phase 2-3 spec, with detailed trigger points, payload rules, and Phase 3 subagent lifecycle spec
web/app/[locale]/docs/page.tsx Adds turn_end to the hook event comment and appends a one-sentence description in both the Chinese and English docs sections
web/package-lock.json Lockfile sync: marks several native binary packages as dev: true and adds vitest/esbuild 0.28.0 nested packages to satisfy CI npm ci

Sequence Diagram

sequenceDiagram
    participant Engine
    participant UI as run_event_loop (ui.rs)
    participant App as App State
    participant Blocking as spawn_blocking thread
    participant Hook as turn_end shell hook

    Engine->>UI: "EngineEvent::TurnComplete { status, usage, error }"
    UI->>App: Clear loading/streaming state
    UI->>App: Set runtime_turn_status
    UI->>App: Update token counters and cost
    UI->>App: Update notifications and receipts
    UI->>App: Schedule session persistence

    alt has_hooks_for_event(TurnEnd)
        UI->>UI: Build turn_end_payload(TurnEndPayloadInput)
        UI->>UI: Clone HookExecutor
        UI->>Blocking: spawn_blocking (fire-and-forget)
        Note over UI,Blocking: JoinHandle dropped immediately
    end

    UI->>App: pop_queued_message() dispatch next queued message

    Blocking->>Hook: stdin JSON payload
    Hook-->>Blocking: stdout ignored stderr logged on failure
    Blocking-->>Blocking: warn on non-zero exit no caller impact
Loading

Reviews (4): Last reviewed commit: "fix: gate hook test fs import" | 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
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

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
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.

1 participant