Skip to content

An agent that carries its own checkpointer silently gets NO cross-run persistence, yet --show-config reports persist = True + a .sqlite store path (never created) and --list-sessions lists the "session" as resumable — resuming restores nothing #128

Description

@dkedar7

Summary

Session persistence (#102) intentionally lets a user-supplied checkpointer win: "A user-supplied checkpointer still wins, untouched", and the CLI supplies its durable SQLite store only when the graph has none. That part is correct. The bug is that the persistence diagnostics lie for such an agent:

  • --show-config reports persist = True and prints a concrete sessions_store = .../<key>.sqlite path — but that .sqlite is never created or written, so there is zero cross-run state.
  • --list-sessions lists the run as a resumable session (id + first message + timestamp).
  • --continue / --resume happily "resume" that thread — but restore nothing: /history is empty and the graph never sees prior turns.

So a user is told, by the CLI's own primary config diagnostic, that persistence is on and even where it is stored, while in reality every run is amnesiac. This directly violates #102's own acceptance criterion #3"Surface it in --show-config//status … so it's discoverable and honored-as-advertised." /status does distinguish the two modes (on (graph checkpointer) vs on (durable)), but --show-config (the non-interactive diagnostic) and --list-sessions do not.

This is reachable straight from the README's own "Creating Your Own Agent → full deep agent" example, which compiles with checkpointer=MemorySaver().

Severity: major — silent loss of conversation continuity on the headline feature, with the diagnostics actively reassuring the user that persistence is on and naming a store file that does not exist.

Environment

  • langstage-cli 0.6.30 (clean pip install langstage-cli from PyPI)
  • langstage-core 1.0.34, langgraph 1.2.11, langgraph-checkpoint-sqlite 3.1.1, click 8.4.2
  • Python 3.11.15, Linux, fresh venv

Minimal repro

mkdir demo && cd demo
export LANGSTAGE_CLI_SESSIONS_DIR="$PWD/sess"     # isolate the store for the repro

cat > mem_agent.py <<'PY'
# Same shape as the README's "full deep agent" example: the graph owns a checkpointer.
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import MessagesState
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.messages import AIMessage
def respond(state):
    return {"messages": [AIMessage(content=f"turn sees {len(state['messages'])} msgs")]}
g = StateGraph(MessagesState); g.add_node("respond", respond)
g.add_edge(START, "respond"); g.add_edge("respond", END)
graph = g.compile(checkpointer=MemorySaver())     # <-- user-supplied checkpointer
PY
export LANGSTAGE_AGENT_SPEC="mem_agent.py:graph"

langstage-cli --show-config | sed -n '/Session persistence/,$p'
langstage-cli -q "first message"        # run 1
langstage-cli -q -c "second message"    # run 2 (continue)
langstage-cli --list-sessions
printf '/history\n/quit\n' | langstage-cli -c    # resume + inspect history
ls sess/

Actual

  Session persistence:
  persist          = True                       [default]   (env: LANGSTAGE_PERSIST, toml: session.persist)
  sessions_store   = .../sess/b6b30a6936f792a7.sqlite [env:LANGSTAGE_CLI_SESSIONS_DIR]

# run 1:  turn sees 1 msgs
# run 2:  turn sees 1 msgs          <-- no continuity; prior turn invisible

Sessions (.../demo)
  9b0ba186  2026-08-18 07:41  first message      <-- listed as resumable

# /history after -c:  (completely empty — no You/Agent lines at all)

# store dir contents:
b6b30a6936f792a7.index.json         <-- ONLY the index sidecar; NO .sqlite was ever written

Expected

For an agent that owns its own (non-durable) checkpointer, one of:

  1. Honest diagnostics (minimum fix). --show-config should report the same distinction /status already makes — e.g. persist = on (graph checkpointer; cross-run history not stored by the CLI) and not print a sessions_store .sqlite path that is never created. --list-sessions should either not list index-only entries as resumable, or mark them "(no stored state)".
  2. A one-line warning when --continue/--resume targets a thread that has only an index entry and no durable checkpoint, so the silent empty resume is at least visible.

The current output tells the user persistence is on and points at a .sqlite file, while every run is actually amnesiac — the exact "honored-as-advertised" gap #102 set out to avoid.

Control (no own checkpointer — works correctly)

Same agent compiled with g.compile() (no checkpointer): run 2 reports turn sees 3 msgs, --list-sessions shows the session, /history shows both turns, and the store dir contains a real <key>.sqlite (~28 KB) alongside the index. So the divergence is entirely driven by whether the graph carries its own checkpointer.

Root cause (pointer)

main() computes had_user_checkpointer = getattr(graph, "checkpointer", None) is not None and use_durable = persist and not had_user_checkpointer; when use_durable is false, persist_sqlite_path stays None so no .sqlite is ever opened — but sessions.record_session(...) still writes the index entry, and the --show-config / _persist_diagnostic path reports persist=True with the computed db_path(...) regardless of use_durable.


Filed by the nightly dogfood routine (dogfooding 0.6.30, clean PyPI install).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingroutineFiled by the daily power-user dogfooding routine

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions