feat: persist ConversationContext across sessions (closes #164)#168
Open
luceinaltis wants to merge 1 commit into
Open
feat: persist ConversationContext across sessions (closes #164)#168luceinaltis wants to merge 1 commit into
luceinaltis wants to merge 1 commit into
Conversation
Closes #164. ConversationContext was previously rebuilt from the session log on every query and lost on process exit. This adds a durable layer so returning users keep their topic stack, last intent, and activity timestamp. ### What changed - **`qracer/conversation/context_store.py`** (new) — `save_context` / `load_context` (JSON, malformed-input tolerant), `decay_stale` that clears the topic_stack when the gap exceeds `DEFAULT_STALE_DAYS` (7), `merge_with_theses` that folds open FactStore theses into the stack up to `MAX_TOPIC_STACK`, and `merge_contexts` that combines an in-session context with the persisted one (session wins on scalars, persisted supplies trailing topics). - **`qracer/conversation/engine.py`** — new `context_path` kwarg; on init the persisted context is loaded, decayed, and enriched with open theses, then exposed via `self._context`. After each query the engine merges the session-log context with the persisted one and writes the result back to disk. Load/save are try/except-guarded so a bad file never breaks the query loop. - **`qracer/cli.py`** — `repl` wires `context_path` to `~/.qracer/context.json`. ### Tests - `tests/conversation/test_context_store.py` (new, 23 tests) covers save/load round-trip, missing/malformed/non-dict fallbacks, bad timestamps, non-string topic filtering, decay (fresh/stale/custom threshold/empty fast-path), and both merge helpers including dedup and `MAX_TOPIC_STACK` bounds. - `tests/conversation/test_engine.py` — 5 new integration tests: context file written after query, topics resumed in a fresh engine, stale context decayed on load, open theses merged into topic_stack, no-crash when `context_path` is unset. Full suite: 806 passed, 14 skipped. Ruff + pyright clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #164.
ConversationContextwas previously rebuilt from the session log on every query and lost on process exit. This PR adds a durable layer so returning users keep their topic stack, last intent, and activity timestamp across restarts — the "welcome back" experience flagged as 구현 예정 indocs/user-experience.md.What changed
qracer/conversation/context_store.py(new) — pure helpers:save_context/load_context— JSON serialization, tolerant of missing files, malformed JSON, non-dict payloads, bad ISO timestamps, and non-string topic entries.decay_stale(ctx, max_age_days=7)— clearstopic_stack/current_topiconce the gap sincelast_activityexceeds the threshold. Because the data model carries a singlelast_activityshared by all topics, "decay stale topics" is implemented as "the whole stack is abandoned once the threshold is crossed" — documented in the module docstring.merge_with_theses(ctx, thesis_tickers)— folds openFactStoretheses into the stack (deduped, bounded byMAX_TOPIC_STACK), seedingcurrent_topicfrom the stack if unset.merge_contexts(session, persisted)— combines an in-session context with the persisted one: session wins on every "right now" scalar (current_topic,intent,depth,last_activity); the persistedtopic_stacksupplies trailing entries.qracer/conversation/engine.py— newcontext_path: Path | None = Nonekwarg. On init the persisted context is loaded, decayed, and enriched withFactStore.get_open_theses(). After each successful query the engine mergesextract_context(turns)with the persisted context and writes the merged state back to disk. Load and save are both try/except-guarded so a bad file can never break the query loop.qracer/cli.py—replwirescontext_path=_user_dir() / "context.json"so real users get persistence for free.Scope mapping (issue #164)
context.jsonin~/.qracer/context.json_load_initial_contextdecay_stale,DEFAULT_STALE_DAYS=7Test plan
uv run pytest tests/conversation/test_context_store.py tests/conversation/test_engine.py— 68 passed (23 newtest_context_storecases + 5 newTestPersistentContextengine cases).uv run pytestfull suite — 806 passed, 14 skipped.uv run ruff checkon changed files — clean.uv run pyright qracer/conversation/context_store.py qracer/conversation/engine.py— 0 errors.New tests cover
current_topic, stack, intent, depth,last_activity).decay_stale; stale context clears stack but keepslast_activity; custommax_age_days; empty-context fast-path.merge_with_thesesappends, dedupes, respectsMAX_TOPIC_STACK, seedscurrent_topic, preserves existing one.merge_contextssession-wins semantics, persisted fallback for missing scalars, topic-stack merge order (session first),MAX_TOPIC_STACKbound,last_activitycarries session's value.context_path, stale context decayed on load,FactStoreopen theses merged into the stack, engine withoutcontext_pathis a silent no-op.Manual verification