feat(undo): /undo [N] backs up N user turns with prefill + soft-delete (#21910)#36229
Conversation
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
1 |
First entries
tests/tui_gateway/test_undo_command.py:21: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
✅ Fixed issues: none
Unchanged: 4959 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
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.
…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 #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 #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.
Extends the existing /undo command from a single in-memory exchange removal into a full rewind: back up N user turns (default 1), soft-delete the truncated rows in SessionDB (active=0, kept for audit, hidden from re-prompts and search), notify memory providers, and prefill the composer with the backed-up message text for editing — CLI and TUI. Reuses the SessionDB rewind primitives, the on_session_switch(rewound=True) memory hook, and the TUI command.dispatch prefill payload from SaguaroDev's #21910 work, wired to /undo [N] instead of a separate /rewind picker. - cli.py: undo_last(n, prefill) — in-memory truncate + SQLite soft-delete + agent surgery (system-prompt invalidate, flush-index reset) + memory notify + editable buffer prefill; /undo dispatch parses optional count; checkpoint-rollback caller passes prefill=False - tui_gateway/server.py: command.dispatch undo branch (was rewind) parses count, picks Nth-from-last user turn, clamps to oldest - commands.py: /undo gains [N] args_hint - tests: rename + expand TUI suite (multi-turn, clamp, invalid-count) - release.py: AUTHOR_MAP entry for SaguaroDev Co-authored-by: SaguaroDev <74339271+SaguaroDev@users.noreply.github.com>
The on_session_switch fan-out passed rewound=rewound unconditionally, injecting rewound=False into every provider's **kwargs on the common /resume, /branch, /new, and compression paths. Providers that capture extra kwargs into an 'extra' dict (and the exact-dict-equality tests guarding them) broke. Forward rewound only when truthy; /undo sets it explicitly, everyone else stays clean.
b3016a8 to
6f80976
Compare
|
Thank you @teknium1, this honestly made my week!! Seeing the rewind primitives land on main with authorship preserved means a lot, and folding it into /undo [N] is cleaner than the separate command + picker I had. Glad the schema reconcile and deferred-index ordering made the rebase painless, that was the part I was most worried about. One thing in case it's useful, not a blocker: #25074 (stacked on this) carries the messaging platform gateway half of the #21910 v2 scope that /undo doesn't touch yet, best-effort cross-platform message deletion with per-platform outbound-id tracking (schema v13 columns + partial index) and thread-scoped rewind behind a permission probe. If gateway-side rewind is ever on the roadmap, the outbound-tracking groundwork is already there to cherry-pick. Either way, thank you so much for taking a look and a screenshot worthy visual. I'm touched |
Summary
/undo [N]now backs up N user turns (default 1), soft-deletes the truncated rows on disk, and prefills the composer with the backed-up message for editing — the in-place "undo and re-prompt" primitive requested in #21910, folded into the existing/undocommand instead of a separate/rewind.Salvages @SaguaroDev's #21910 infrastructure (SessionDB rewind primitives,
on_session_switch(rewound=True)memory hook, TUI prefill payload) onto currentmain, wired to/undo [N].Changes
undo_last(n, prefill)— in-memory truncate + SQLite soft-delete (active=0) + agent surgery (system-prompt invalidate, flush-index reset) + memory notify + editable buffer prefill./undodispatch parses optional count; checkpoint-rollback caller passesprefill=False.command.dispatchundobranch parses count, picks Nth-from-last user turn, clamps to oldest.rewoundkwarg (SaguaroDev's commits, rebased onto v14 schema)./undogains[N]args_hint.Behavior
/undo/undo 3/undo 99Rewound rows stay on disk (
active=0) for audit, hidden from re-prompts andsession_search;rewind_countis bumped per operation.Validation
test_hermes_state.py+ 11 new TUI/undotests).ValueErroron non-user target.undo_lastpath: in-memory + disk stay in sync across sequential undos.Replaces #21910 / #23445 (separate
/rewindcommand). Closes #21910.Co-authored-by: SaguaroDev 74339271+SaguaroDev@users.noreply.github.com
Infographic