* feat(worldline): integrate checkpoint-tree rewind into tui_v2
- tuiapp_v2.py: /rewind durable inline picker + /worldline three-pane
checkpoint tree (tree UI inlined; colors follow the v2 theme) with a
per-node diff viewport; conv-only /rewind restores via the persistent store.
- worldline.py (new): UI-agnostic backend — RewindStore (persistent
checkpoint tree + content-addressed blobs), reconcile, restore_plan,
node_diff, native-log projection.
- continue_cmd.py: gated worldline support — list_sessions(rewind_root=),
continue_inplace/copy(allow_empty=); default-off, other UIs byte-identical.
- tui_v3.py: status line shows concrete model id (unrelated minor tweak).
* feat(worldline): /rewind reuses the restore-mode picker (conv/code/both)
After picking a turn, /rewind now opens the same RestoreModeScreen as
/worldline to choose conversation / code / both, then restores via the
persistent store — making /rewind a true lite view of /worldline (linear
list, no tree/diff) over the identical restore backend.
* feat(worldline): sync working memory (history_info/key_info) on rewind
树节点除 conv 外,再存 history_info 增量(与 conv 同构)+ key_info 快照。
restore_plan 重建二者,_rw_sync_working_memory 在 rewind 时就地写回
handler.history_info / working['key_info'],使注入的 WORKING MEMORY 与
回退后的对话一致,消除旧 rewind「历史回退、纪要串味」缺陷。
向后兼容:老树无 WM 记录 → path_has_wm=False → 恢复时跳过 WM 同步,
不动现场纪要。单测 21/21 + 真机(363580)端到端验证(call_013→014
v2 痕迹 ×3→×0)通过。
* fix(worldline): reconcile against the log, only on /continue; rebuild working memory on absorb
- reconcile now takes the log-parsed full history, never the compressed
backend.history. Compressed memory is a lossy derivative; reconciling the
full tree against it made the tree adopt a front-trimmed suffix as a new
trunk (false divergence). Contract pinned in the docstring.
- drop in-session reconcile from /rewind and /worldline: the tree is kept
current every turn by _rw_commit, and live history there is compressed.
reconcile now runs only on /continue, where backend.history is the freshly
loaded full log.
- reconcile rebuilds history_info/key_info from the absorbed log so resumed
and external-UI sessions also carry working memory on rewind.
* fix(worldline): feed commit the full log; derive deltas from the tree's real length
_rw_commit fed the compressed backend.history, so once a long session front-trims,
commit's absolute hist_len delta drops turns. Now:
- _rw_commit feeds the log-parsed full history (fallback to backend.history).
- commit derives the delta start from len(rebuild_history(parent)) /
len(rebuild_hist_info(parent)) instead of the stored hist_len, so a poisoned
count can't drop turns and a previously-lost turn self-heals on the next commit
(the log is append-only, always a superset of the tree).
* chore(worldline): mark in-memory _do_rewind as the non-persistent degraded fallback
It chops the compressed live history (can't reach front-trimmed turns, keeps
truncated remnants, restores neither files nor working memory). Only used when
there is no checkpoint tree; the durable tree-rebuild path is always preferred.
* fix(worldline): derive tree working memory from the log, not live handler state
/continue resets agent.history (handler.history_info) to empty. _rw_commit
previously fed that live list as hist_info, so after a continue the per-turn WM
delta misaligned with the tree's log-derived WM and new turns' summaries were
dropped. commit now derives hist_info/key_info from the history it stores (the
full log) when not given, matching reconcile; _rw_commit stops passing the live
list. Tree WM is uniformly log-derived and survives the continue reset.
* feat(worldline): restore working memory on /continue (TUI-only, derived from the log)
continue_cmd clears handler.history_info on restore; the TUI's continue path now
derives history_info from the loaded log (same _derive_hist_info as the tree) and
writes it back to agent.history, so a continued session keeps its working memory,
consistent with the tree and live sessions. Scoped to tuiapp_v2 only — shared
continue_cmd is untouched, so telegram/discord/streamlit frontends are unaffected.
* refactor(continue): restore working memory on /continue via opt-in restore_wm
Move the WM restore from the TUI into continue_cmd as an opt-in. _load_history_into
derives history_info from the loaded log and writes it back to agent.history when
restore_wm=True; continue_inplace/continue_copy thread the flag and the worldline
TUI passes True. Default False keeps every other frontend (telegram/discord/
streamlit/shared chat) byte-for-byte unchanged. _derive_hist_info is self-contained
(no worldline dependency), same extraction口径 as the tree.
* perf(worldline): only re-parse the log in _rw_commit when front-trim is detected
_rw_commit re-parsed the whole log every segment to feed commit a full history;
for very long sessions (multi-MB logs) that adds a multi-second lag per turn. Now
it uses the fast in-memory backend.history by default and only falls back to
parsing the log when front-trim is detected — RewindStore.first_question() vs the
live history's first question (front-trim pops the first question, so they diverge).
Common case (no trim) pays nothing; correctness under trim is preserved.
* feat(worldline): conv/code bridge nodes + align _rw_commit to native log
- worldline: commit_bridge() + restore_plan() three-way (both/conv/code) with a new rw_tag field. conv bridges fork at parent(target); code bridges mount at parent(old_head) as a sibling of the current conversation node (same conversation, older code).
- tuiapp_v2: _rw_commit now reads the native log (parse_native_log) instead of the compressible live backend.history; stat-cached _rw_log_history; title from the real log delta; skip commit when there is no new complete pair. Cursor points at the bridge for conv/code; rw_tag rendered after the title.
- continue_cmd: add parse_native_log() public wrapper (complete Prompt->Response pairs only; [] for native-but-empty, None for non-native).
* fix(worldline): realpath 去重 key + 节点行数增删 + 剥离 project-mode
- key()/cwd 改用 realpath 解析 junction/symlink,同一物理文件只产生一个 tracked
key(修 workspace 下 @提及绝对路径与相对写入产生双 key、回退误覆写当前内容)
- 新增 node_line_delta:按 node_diff 计每节点相对父节点的 +新增/-删除 行数,
供 /rewind、worldline 显示代码变动量
- 重建历史时剥离 project-mode 注入块与自动注入标记(worldline._msg_user_text +
continue_cmd._derive_hist_info),标题/提问只留用户原话
* feat(worldline/rewind): 面板与 rewind UI 精简重设计
worldline 面板:
- 右栏改用原生 OptionList(整项高亮/滑窗跟踪/点击映射,取代手写 ClickableList)
- 左右栏比例 3:2;详情面板精简(删掉与状态条/diff 重复的操作提示、时间、回退说明)
- 状态条重写为完整动宾短语,Enter 随聚焦切文案;右栏聚焦高亮改用更亮的 alt_bg
(对齐左树,修此前聚焦态反而更淡的问题)
/rewind:
- 三列:用户 prompt · 上次时间 · 代码 +新增/-删除(绿/红,无改动显示「(无改动)」);
倒序、聚焦底部、超长 prompt 省略号截断
- 延迟折叠(defer_collapse):选中先弹模式窗,确认才回退并清卡片;Esc 取消保留列表、
焦点回到列表,不再留误导性「✓ 已选中」
其他:
- Ctrl+D 关会话时释放日志锁(abort + release_current),之后可原地 /continue
* fix(worldline): 回填输入框的 prefill 剥离 project-mode 注入
restore_plan 的 prefill/_prompt_of 走未清洗的 user_msg_text(树里的 user 消息取自
含注入的 native 日志),把 --- [PROJECT MODE:...] --- 一起塞进输入框。统一过
_strip_project_mode(幂等)。回归:temp/test_rewind_prefill_no_projectmode.py 9/9。
* refactor(tui): 删除被取代的旧 rewind 面板与死代码
- 删旧 /rewind 整簇:RewindScreen 类 + _on_rewind_pick + _rw_* 9 个 helper
(已被内联 choice 卡 + RestoreModeScreen / RewindTreeScreen 取代,无 live 入口)
- 删未用 import next_same_depth/CheckpointTree、只写不读的 _diff_for/assistant_len
- 净 -276 行;import tuiapp_v2 通过、worldline 回归全绿
* fix(worldline): apply_code 回退失败不再静默,回传错误由 UI 提示
apply_code 直接写用户工作区文件,中途失败(权限/磁盘)会留下半回退状态。原先
restore_plan 吞成 changed=[],UI 显示「无变更」,用户毫不知情——全 PR 里失败半径
最大却唯一被静默的点。改为 _apply_code_safe 回传 (changed, code_error),
_rw_restore_node 据此追加「⚠️ 代码回退未完成…请检查」告警。
* fix(continue): preserve session workspace on in-place restore
In-place /continue retargets agent.log_path to the restored file, so the
reset bind `_bind_workspace(None)` ran during restore was persisting
session_ws_set(path, "") — erasing that session's own workspace mapping to
"" (read back as "explicitly off") before we read it, which also short-
circuited the log-scan fallback. The continued session never re-entered its
workspace.
Add a `persist` flag to `_bind_workspace`; the restore-time reset passes
persist=False so it only refreshes in-memory state. The session→workspace
map is now written solely by explicit /workspace, /workspace off, and a
successful restore.
* fix(tui_v3): support Home/End keys for line start/end
PTK delivers Home/End as raw VT sequences in KeyPress.data, which
_esc_repl swallowed (only arrows were decoded). Map \x1b[H/[F/[1~/[4~
and SS3 \x1bOH/\x1bOF to internal bytes 0x07/0x14, extend _ESC_RE to
match SS3 as whole sequences, and add jump-to-line-start/end handlers
in _keys. Home no longer collides with Ctrl+A select-all.
* feat(worldline): integrate checkpoint-tree rewind into tui_v2
- tuiapp_v2.py: /rewind durable inline picker + /worldline three-pane
checkpoint tree (tree UI inlined; colors follow the v2 theme) with a
per-node diff viewport; conv-only /rewind restores via the persistent store.
- worldline.py (new): UI-agnostic backend — RewindStore (persistent
checkpoint tree + content-addressed blobs), reconcile, restore_plan,
node_diff, native-log projection.
- continue_cmd.py: gated worldline support — list_sessions(rewind_root=),
continue_inplace/copy(allow_empty=); default-off, other UIs byte-identical.
- tui_v3.py: status line shows concrete model id (unrelated minor tweak).
* feat(worldline): /rewind reuses the restore-mode picker (conv/code/both)
After picking a turn, /rewind now opens the same RestoreModeScreen as
/worldline to choose conversation / code / both, then restores via the
persistent store — making /rewind a true lite view of /worldline (linear
list, no tree/diff) over the identical restore backend.
/continue list previews showed JSON debris (e.g. `{`, `" } ]} === Response ===`)
for some sessions. Two causes in _preview_from_file:
1. Models sometimes emit an unclosed <summary>, so the non-greedy DOTALL regex
pairs it with a far-away </summary> and captures === block headers / JSON
across rounds. Now only the latest <summary> is used, and it is rejected if
it looks like such a cross-segment capture (contains `=== `/`"role"` or
>200 chars) — falling through instead of digging for older summaries.
2. The fallback scanned for the first non-=== line, returning JSON structure
lines. It now uses the last real user prompt via a new _last_user(), which
scans Prompt blocks directly (so response-less/aborted sessions still
preview) and reuses _user_text() to skip tool_result continuations and all
_INJECT_MARKERS (WORKING MEMORY / SYSTEM TIPS / DANGER ...).
Scanned 261 local logs: garbage previews 26 -> 0.
_user_text was misclassifying any prompt whose text block didn't start
with '### [WORKING MEMORY]' as a real user message — but auto-continue
prompts can interleave [SYSTEM TIPS], [System] regenerate triggers,
[DANGER] guard injections, and similar. Treat any prompt carrying a
tool_result block as auto-continue (the natural signal that this is
the next round of an in-flight LLM call), and extend the injection
marker list as a secondary guard for the rare tool_result-less
synthetic prompts.
extract_ui_messages now renders tool_use headers and tool_result fences
into the assistant content using the same string format that agent_loop
yields live, so fold_turns folds restored sessions identically to live
chat. Tool-only response rounds no longer drop the user message, and
Turn 1 also carries a marker so the first turn folds like the rest.
When the user runs '/continue N' in stapp, the agent's in-memory context
is restored, but the UI previously showed only a single '✅ restored' line
— all prior chat bubbles were missing.
This change parses the target session log and reconstructs the
user/assistant message pairs into st.session_state.messages, so reopening
a session feels like the conversation was never interrupted.
* continue_cmd.py: add extract_ui_messages(path)
- parses model_responses log into [{role, content}, ...]
- groups multi-turn LLM calls (prompts whose text starts with the
'### [WORKING MEMORY]' header) into a single assistant bubble,
inserting the existing '**LLM Running (Turn N) ...**' marker so
fold_turns() renders them as collapsible segments.
- two small helpers (_user_text / _assistant_text) keep parsing local.
* stapp.py: in the /continue branch, resolve the target log path BEFORE
calling handle_frontend_command (which snapshots the current log and
would otherwise shift list_sessions indices), then replace
session_state.messages with the reconstructed history on success.
Falls back to the previous behavior for bare /continue or failure.
Co-authored-by: wjl2023 <wjl2023@users.noreply.github.com>