mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(client): count threads, not checkpoints, in list_threads checkpointer.list(None, limit=N) returns the newest N checkpoints across every thread, so one long conversation filled the limit and hid the rest: the TUI thread picker and --resume <title> only saw the latest one or two threads. List threads through their single step -1 root checkpoint instead, then read the latest checkpoint per returned thread. Add sort_by="updated_at" so --continue keeps resuming the most recently active thread rather than the most recently created one. * test(client): pin list_threads scan cost and namespace filtering; docs * docs(client): state list_threads cost as checkpoints loaded, not rows scanned * docs(changelog): reference #6426 * fix(client): list threads by checkpoint write order, including branches Discovering threads by their step -1 input checkpoint dropped Gateway branches, which are seeded through update_state (step 0), and ordering by checkpoint ts missed goal writes, which copy the previous ts under a fresh checkpoint id. Enumerate threads from the checkpoint primary-key index on SQLite and Postgres (unwrapping the delta-history cache), falling back to one list(None) walk for any other saver, and order by checkpoint_id, the key LangGraph itself uses for a thread's latest checkpoint.
60 lines
1.7 KiB
Python
60 lines
1.7 KiB
Python
"""Tests for Session thread resolution (id-or-title) and lifecycle."""
|
|
|
|
from deerflow.tui.cli import LaunchPlan
|
|
from deerflow.tui.session import Session
|
|
|
|
|
|
class _Client:
|
|
def __init__(self, threads):
|
|
self._threads = threads
|
|
|
|
def list_threads(self, limit=10, *, sort_by="created_at"):
|
|
return {"thread_list": self._threads}
|
|
|
|
|
|
def _session(threads):
|
|
return Session(client=_Client(threads))
|
|
|
|
|
|
THREADS = [
|
|
{"thread_id": "thread-aaaa", "title": "Bug triage"},
|
|
{"thread_id": "thread-bbbb", "title": "Write docs"},
|
|
]
|
|
|
|
|
|
def test_resolve_ref_matches_id():
|
|
assert _session(THREADS).resolve_ref("thread-bbbb") == "thread-bbbb"
|
|
|
|
|
|
def test_resolve_ref_matches_title():
|
|
assert _session(THREADS).resolve_ref("Bug triage") == "thread-aaaa"
|
|
|
|
|
|
def test_resolve_ref_unknown_falls_back_to_literal_id():
|
|
assert _session(THREADS).resolve_ref("brand-new-id") == "brand-new-id"
|
|
|
|
|
|
def test_resolve_thread_resolves_title_from_plan():
|
|
plan = LaunchPlan(mode="tui", thread_id="Write docs")
|
|
assert _session(THREADS).resolve_thread(plan) == "thread-bbbb"
|
|
|
|
|
|
def test_resolve_thread_continue_returns_most_recent():
|
|
plan = LaunchPlan(mode="tui", continue_recent=True)
|
|
assert _session(THREADS).resolve_thread(plan) == "thread-aaaa"
|
|
|
|
|
|
def test_close_is_a_noop_without_a_loop():
|
|
# Headless sessions have no persistence loop; close() must be safe.
|
|
_session(THREADS).close()
|
|
|
|
|
|
def test_close_stops_the_background_loop():
|
|
from deerflow.tui.persistence import ThreadMetaWriter, _LoopThread
|
|
|
|
loop = _LoopThread()
|
|
session = Session(client=_Client(THREADS), writer=ThreadMetaWriter(loop, None), _loop=loop)
|
|
session.close()
|
|
assert session._loop is None
|
|
session.close() # idempotent
|