Skip to content

feat: /rewind — jump back to a prior user message and re-prompt (#21910)#23445

Closed
SaguaroDev wants to merge 8 commits into
NousResearch:mainfrom
SaguaroDev:feat/rewind-21910
Closed

feat: /rewind — jump back to a prior user message and re-prompt (#21910)#23445
SaguaroDev wants to merge 8 commits into
NousResearch:mainfrom
SaguaroDev:feat/rewind-21910

Conversation

@SaguaroDev

Copy link
Copy Markdown
Contributor

Closes #21910

Adds a first-class /rewind affordance — jump back to a prior user message in the current session, re-prompt from there with the original text pre-filled and editable, while keeping the truncated rows on disk for audit. Equivalent to Claude Code's double-Esc behavior, scoped per the issue to CLI + TUI only (gateway is a v2 follow-up).

What ships

Schema (v11 → v12)

  • messages.active INTEGER NOT NULL DEFAULT 1 — soft-delete flag
  • sessions.rewind_count INTEGER NOT NULL DEFAULT 0 — audit counter
  • Deferred idx_messages_session_active index (created after _reconcile_columns so legacy DBs don't trip on the missing column)

Declarative reconcile + a belt-and-suspenders UPDATE messages SET active = 1 WHERE active IS NULL covers existing databases.

SessionDB API

  • rewind_to_message(session_id, target_message_id) → {rewound_count, target_message, new_head_id} — refuses non-user targets and cross-session targets with ValueError; idempotent
  • restore_rewound(session_id, since_message_id) → int — reverse op for a future /unrewind
  • list_recent_user_messages(session_id, limit=20) — picker source
  • get_messages, get_messages_as_conversation, search_messages gain include_inactive: bool = False. By default rewound rows are excluded everywhere — including session_search — so re-prompts and search hits see a clean transcript. Opt in for audit / debug tooling.

CLI (/rewind)

Slash command registered in hermes_cli/commands.py; handler in cli.py mirroring _handle_branch_command:

  1. Picker (existing _run_curses_picker) shows the last 10 user messages
  2. Soft-truncate via SessionDB.rewind_to_message
  3. Reload active-only history into conversation_history
  4. Mirror /branch's agent surgery (reset_session_state, _invalidate_system_prompt, _last_flushed_db_idx)
  5. Notify _memory_manager.on_session_switch(..., rewound=True) so per-turn caches invalidate (Hindsight etc.)
  6. Pre-fill the prompt_toolkit buffer with the selected message text — edit and resubmit

TUI (Ink + tui_gateway)

  • command.dispatch gets a name == "rewind" branch returning the new {"type": "prefill", "message": <text>, "notice": "↶ Rewound N message(s)…"} payload
  • /rewind added to _PENDING_INPUT_COMMANDS so slash.exec routes it correctly
  • New prefill variant on CommandDispatchResponse + asCommandDispatch validator; createSlashHandler handles it by calling ctx.composer.setInput(d.message) (no auto-submit, unlike send)
  • v1 auto-picks the most recent user turn in the TUI — a dedicated Ink picker overlay for multi-step rewind is tracked as a follow-up

Memory hook (#6672 extension)

MemoryProvider.on_session_switch documents rewound: bool = False. MemoryManager.on_session_switch keeps it in **kwargs (not an explicit named kwarg) so existing tests that assert exact extra dict equality stay green. Existing callers (/branch, compression, etc.) are untouched.

Test coverage

Suite Tests Status
tests/test_hermes_state.py +10 new (rewind primitives, migration v11→v12, FTS filter on/off, ValueError paths)
tests/cli/test_rewind_command.py +6 (handler, mocked picker, memory notify, guard paths)
tests/tui_gateway/test_rewind_command.py +7 (prefill payload, in-memory truncation, DB soft-delete, memory notify, busy-session refusal, registry placement)
ui-tui/src/__tests__/asCommandDispatch.test.ts +3 (prefill variant + malformed rejection)

All existing suites in the blast radius (memory, cli, branch, hermes_state, tui_gateway) — 253 / 253 passing post-rebase.

Pre-existing failures on origin/main that are unrelated and unaffected by this PR:

  • test_anthropic_adapter.py::test_returns_token_from_credential_files — mock setup issue
  • test_cli_save_config_value.py::TestSaveConfigValueAtomic::* — missing ruamel module in CI env

Acceptance checklist (from #21910)

  • /rewind command in CLI and TUI
  • Truncated transcript no longer shown to the model on the next turn (active-only filter in get_messages_as_conversation)
  • Rewound rows preserved in SessionDB (active=0), not deleted
  • Memory providers notified on rewind (on_session_switch(rewound=True))
  • Tests covering: rewind to first user message, rewind mid-session, rewind then continue normally, rewind across tool-call boundaries
  • Double-Esc keybind — deferred per design discussion (/rewind slash command is the canonical entry; keybind can land in a follow-up after we audit Esc Esc for conflicts with prompt_toolkit's existing keymap)

Out of scope for v1 (tracked as follow-ups to #21910)

  • Dedicated Ink picker overlay for multi-step rewind. The TUI's v1 auto-picks the most recent user turn, which covers the common case. The CLI ships the full picker.
  • Gateway platforms (Telegram, Discord, etc.) — issue body explicitly scopes v1 to CLI/TUI. xavierchoi's comment on the issue outlines the v2 work (best-effort message deletion, permission probe, thread-scoped boundaries).
  • Compression-fork boundary handling — rewinding across a context-compression session split would need the parent-chain logic in resolve_resume_session_id extended; not addressed here.

@SaguaroDev

Copy link
Copy Markdown
Contributor Author

@xavierchoi — you volunteered to help test on the original issue (#21910). v1 is up here when you have a moment. A few notes specifically on the gateway concerns you raised:

  • This PR is intentionally scoped to CLI + TUI only, matching the issue body. Your v2 ideas (best-effort message deletion, per-platform message ID tracking, permission probes, thread-scoped boundaries) are not addressed here — they're the natural follow-up.
  • The memory-provider notification path is in place (on_session_switch(..., rewound=True)), so when v2 lands on gateway it can hook the same seam without rework.
  • In the TUI, v1 auto-picks the most recent user turn rather than rendering a dedicated Ink picker overlay. The CLI ships the full curses picker (last 10 user messages). I'd value a second opinion on whether the auto-pick TUI behavior is acceptable or whether the picker overlay should block v1.

What would help most:

  1. Smoke-test /rewind in the CLI — pick a mid-session message, verify the prompt prefill is correct and that the next turn sees the truncated transcript.
  2. Same in the TUI (hermes --tui) — confirm the prefill lands in the composer and that you can edit before submitting.
  3. Sanity-check that session_search no longer surfaces rewound content (rewound rows are excluded by default; opt-in via include_inactive=True for audit).

Happy to iterate on anything that feels off. Thanks for the detailed v2 scope on the issue — it shaped how the memory hook signature is laid out so this PR doesn't paint v2 into a corner.

@xavierchoi

Copy link
Copy Markdown

Nice, glad to see this moving. The active-flag soft-delete approach is cleaner than I expected — keeping rewound rows queryable for audit while excluding them from search by default is the right call. Will keep an eye on the v2 gateway scope.

@alt-glitch alt-glitch added type/feature New feature or request comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have labels May 10, 2026
@SaguaroDev

Copy link
Copy Markdown
Contributor Author

v2 plan is mapped out and committed to a separate branch (feat/rewind-v2-gateway-21910) — will open the follow-up PR once this one merges. Plan doc: docs/plans/2026-05-11-rewind-v2-gateway.md on that branch.

Quick scouting summary in case it's useful for this review:

  • SendResult.message_id already returns from every send → no new contract needed
  • base.delete_message(chat_id, message_id) -> bool already exists (Telegram implements; ephemeral-TTL path uses it) → Discord/Slack/Matrix just need adapter overrides
  • /rewind is already in GATEWAY_KNOWN_COMMANDS via the central registry → handler is the only gateway-side gap
  • v1's on_session_switch(rewound=True) hook is what v2 reuses for memory-provider notification on gateway rewinds

v2 scope (9 TDD-shaped tasks):

  1. outbound_messages table in hermes_state.py linking (session, seq) → (platform, chat, thread, msg_id)
  2. Wire record_outbound_message into the gateway send-finalisation path
    3-5. delete_message overrides for Discord / Slack / Matrix
  3. Gateway /rewind handler — calls rewind_to then iterates outbound rows in the rewound range and best-effort deletes
  4. Ephemeral summary reply (Removed N, kept M (missing Manage Messages on Discord))
  5. Docs + hindsight regression in gateway mode
  6. Changelog + PR

Thread-scoped by design ((chat_id, thread_id) filter on the outbound query), so the rewind blast radius is the same as the session boundary — matches @xavierchoi's v2 suggestion.

Holding the v2 branch until this one lands so I can rebase cleanly if anything shifts in review.

@SaguaroDev SaguaroDev force-pushed the feat/rewind-21910 branch from ec6835c to 3f1f5db Compare May 12, 2026 01:41
@SaguaroDev

Copy link
Copy Markdown
Contributor Author

Review in 5 minutes

Quick verification recipe if you want to smoke-test before reading the diff:

gh pr checkout 23445
python -m pytest tests/test_hermes_state.py tests/cli/test_rewind_command.py tests/tui_gateway/test_rewind_command.py
# expect: 241 passed

Then in two terminals, one CLI and one TUI:

CLI (hermes):

  1. Send 3-4 user messages, get responses
  2. /rewind → curses picker shows last N user turns
  3. Pick one → transcript truncates, picked message lands prefilled in the prompt, edit and submit
  4. session_search "..." should NOT surface rewound content (opt-in via include_inactive=True for audit)

TUI (hermes --tui):

  1. Same setup, then /rewind
  2. v1 auto-picks the most recent user turn (single-step undo, Claude-Code-style)
  3. The picked message lands in the composer for editing — does NOT auto-submit
  4. Dedicated picker overlay = v1 follow-up

The one open design call I'd value a second opinion on: the TUI auto-picks the most recent user turn rather than rendering a dedicated Ink picker overlay. My read is this matches the most common use case (single-step undo) and the picker overlay is a clean v1.1 follow-up; happy to block on the overlay if you'd rather ship them together.

Scope: CLI + TUI only, as the issue body specifies. Gateway (Discord/Telegram/Slack message deletion) is v2, mapped out separately and reuses the on_session_switch(rewound=True) seam this PR adds.

Size note: +1026 / −10 across 13 files but the diff splits cleanly along the 6 commits — the schema/primitives commit is independently mergeable as a no-op-from-user-perspective base if you'd prefer to land it first and layer CLI + TUI as follow-ups. Happy to split.

@teknium1 — could I get a look at this when you have a window? Most of the surface area is hermes_state.py + cli.py + tui_gateway/server.py; CI is green locally on 241 rewind-specific tests and the branch is rebased clean on main.

@xavierchoi

Copy link
Copy Markdown

Appreciate the ping, and honestly impressed at how fast this came together.

Have to be upfront: I can't smoke-test on my end right now — I'm running Hermes gateway-only on Discord, no local CLI/TUI session set up. So no signal from me on (1) and (2). For (3), I'll trust the test coverage — the include_inactive default-off behavior on search_messages reads correctly in the diff.

On the TUI auto-pick vs dedicated picker overlay design call: my vote is ship v1 with auto-pick, track the overlay as a follow-up. Single-step undo covers the muscle-memory case (it's what double-Esc trains you to expect), and the CLI already has the full picker for the multi-step case. Blocking v1 on the overlay feels like scope creep for a marginal UX gain.

Also — thanks for mapping the v2 gateway branch already. outbound_messages keyed by (session, seq) → (platform, chat, thread, msg_id) and the thread-scoped boundary are exactly the shape I had in mind, and reusing the on_session_switch(rewound=True) seam means v2 won't need to retrofit anything. Looking forward to that PR.

@SaguaroDev

Copy link
Copy Markdown
Contributor Author

@teknium1 @OutThisLife — could I get a review when you have a window? Diff splits cleanly:

  • CLI + SessionDB half (cli.py, hermes_state.py, hermes_cli/commands.py, agent/memory hook) — @teknium1 if you're the right set of eyes
  • TUI half (tui_gateway/, ui-tui/src/, asCommandDispatch validator) — @OutThisLife since you own most of ui-tui/

253/253 tests green in the blast radius. Acceptance checklist 5/6, with the deferred Esc-Esc keybind explained in the PR body. Happy to split into two PRs if that's easier to review.

…ch#21910)

Schema v12 adds:
- messages.active (default 1) — soft-delete flag for /rewind
- sessions.rewind_count (default 0) — audit counter
- idx_messages_session_active deferred index

New SessionDB methods:
- rewind_to_message(session_id, target_message_id) — soft-deletes rows
  >= target_id, refuses non-user targets, increments rewind_count
- restore_rewound(session_id, since_message_id) — undo for stretch goal
- list_recent_user_messages — picker source

Existing methods get include_inactive kwarg (default False):
- get_messages, get_messages_as_conversation, search_messages.
  Rewound rows excluded from session_search by default — opt-in for audit.

The deferred index pattern (DEFERRED_INDEX_SQL run after _reconcile_columns)
avoids 'no such column: active' on legacy pre-v12 databases, since
executescript(SCHEMA_SQL) runs before column reconciliation.
Adding rewound as an explicit named kwarg on MemoryManager.on_session_switch
broke existing tests that assert exact equality on the extra kwargs dict
captured by providers (test_manager_fans_out_to_all_providers,
test_manager_reset_flag_preserved).

The MemoryProvider base class still documents rewound as a known kwarg,
but the manager now passes it through **kwargs like any other forwarded
flag. The CLI rewind handler already passes rewound=True keyword-style,
so behavior is identical.
…ousResearch#21910)

Adds the TUI half of the /rewind feature so the Ink terminal UI gets
the same affordance as the prompt_toolkit CLI.

Python side (tui_gateway/server.py):
- /rewind added to _PENDING_INPUT_COMMANDS so slash.exec rejects it
  and the TUI falls through to command.dispatch (the only path with
  access to live session state + memory hooks).
- New command.dispatch branch for name == "rewind":
  v1 auto-picks the most recent user turn (Claude-Code-style single-
  step undo), calls SessionDB.rewind_to_message, refreshes the
  in-memory history, fires _memory_manager.on_session_switch with
  rewound=True, and returns the new "prefill" payload.
- A dedicated picker overlay (multi-step rewind) is tracked as a
  follow-up to NousResearch#21910.

TS side (ui-tui/src/):
- New "prefill" variant on CommandDispatchResponse + asCommandDispatch
  validator. Mirrors "send" but does NOT auto-submit; the client drops
  the message into the composer for editing.
- createSlashHandler renders the optional notice via sys() and calls
  ctx.composer.setInput(d.message), letting the user edit-and-resubmit
  the rewound turn — the core UX promised by the issue.

Tests:
- 7 new tui_gateway tests covering prefill payload shape, in-memory
  history truncation, DB soft-delete, memory-provider notification
  (rewound=True), busy-session refusal, missing-session error, and
  registry placement in _PENDING_INPUT_COMMANDS.
- Extended asCommandDispatch vitest covering the new prefill variant
  (with + without notice, and rejection of malformed payloads).

Out of scope for v1 (tracked as NousResearch#21910 follow-up):
- Dedicated picker overlay in Ink (the multi-step rewind UI). v1 auto-
  picks the most recent user turn, matching the most common case.
- Gateway platforms (Telegram, Discord, etc.) — issue scopes v1 to
  CLI + TUI only.
@SaguaroDev

Copy link
Copy Markdown
Contributor Author

Rebased on main and resolved the conflict in hermes_state.py — entirely against c03acca (the AUTOINCREMENT-id ordering fix). Four hunks:

  • get_messages() — kept the new include_inactive parameter and soft-delete filter; switched ORDER BY timestamp, idORDER BY id to match main's clock-regression-safe ordering.
  • get_messages_as_conversation() inner query — same shape: preserved active_clause, dropped timestamp from the ORDER BY.
  • search_messages() signature — merged both new params: sort (main) + include_inactive (this PR).
  • search_messages() docstring — merged both blocks.

Drive-by: the new recent_user_messages() (rewind picker) was using ORDER BY timestamp DESC, id DESC — switched to ORDER BY id DESC for consistency. Same WSL2 clock-regression risk would have applied to the picker.

241/241 in the rewind blast radius after the rebase, plus 67/67 in adjacent session/search/memory tests.

@alt-glitch — would you be able to route this to a reviewer? I keep having to rebuild as main moves; a second pair of eyes on the SessionDB half (the include_inactive + ordering interaction in particular) would unblock the v2 gateway follow-up branch as well.

@SaguaroDev SaguaroDev force-pushed the feat/rewind-21910 branch from 3f1f5db to 05e33f3 Compare May 19, 2026 00:25
@SaguaroDev

Copy link
Copy Markdown
Contributor Author

@alt-glitch is this change not wanted? I will stop dev work on it, but I keep having to update it while there are no responses. That's fine but please tell me to drop it so I can move on with my life. Please

Resolve conflicts in hermes_state.py and tests/test_hermes_state.py with the
platform_message_id/observed work that landed on main (PRs NousResearch#27166, #31a01001,
#4a91e364).

- SCHEMA_VERSION 12 (HEAD) vs 13 (main) -> 14, accommodating both feature
  bumps (rewind 'active' flag + platform_message_id/observed).
- messages table: keep both new columns ('active', 'platform_message_id',
  'observed') side by side.
- DEFERRED_INDEX_SQL: fold main's idx_messages_platform_msg_id into the same
  deferred block as idx_messages_session_active (consistent legacy-DB
  reconciliation path).
- _read_messages_for_sessions SELECT: union both column sets and preserve
  the active-flag filter from the rewind branch.
- test_schema_version assertions: switch the three == 12 literals to
  == SCHEMA_VERSION so the test tracks the constant.

Verified with: pytest tests/test_hermes_state.py (227 passed) and pytest -k
'rewind or platform_message or observed or active' (348 passed).
# Conflicts:
#	hermes_state.py
#	tests/test_hermes_state.py
@SaguaroDev SaguaroDev force-pushed the feat/rewind-21910 branch from b73fc67 to a737a3b Compare May 31, 2026 20:44
@SaguaroDev

Copy link
Copy Markdown
Contributor Author

@teknium1 This PR is rebased clean on latest main and conflict-free (mergeable: MERGEABLE). The only thing blocking it is the fork-PR workflow gate — all 5 checks (Tests, Lint, Docker, Nix, Supply Chain Audit) show action_required and need a maintainer to click Approve and run workflows before CI can run.

Once CI is unblocked it should go green: the /rewind primitives and migration logic are covered by tests in tests/test_hermes_state.py (243 passing locally), and the merge resolved two conflicts — in hermes_state.py (kept main's FTS5-availability-aware v11 migration plus the new v12 messages.active block) and tests/test_hermes_state.py (kept both new test classes).

Would appreciate the workflow approval and a look when you have a moment. Thanks!

@teknium1

teknium1 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Superseded by #36229. We folded your /rewind work into the existing /undo command instead of a separate command + picker: /undo [N] backs up N user turns (default 1), prefills the backed-up message for editing, and soft-deletes the truncated rows on disk.

Your substantive infrastructure was cherry-picked onto current main with your authorship preserved in git log — the SessionDB rewind primitives (messages.active, rewind_to_message, restore_rewound, list_recent_user_messages), the on_session_switch(rewound=True) memory hook, and the TUI command.dispatch prefill payload are all your commits. Thanks for the clean, well-tested groundwork — the schema reconcile + deferred-index ordering made the rebase onto the v14 schema painless.

@teknium1 teknium1 closed this Jun 1, 2026
@SaguaroDev SaguaroDev deleted the feat/rewind-21910 branch June 1, 2026 06:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/tui Terminal UI (ui-tui/ + tui_gateway/) P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: rewind/edit-and-resubmit (like Claude Code's double-Esc)

4 participants