Refactor to stateless graph-backed architecture#81
Merged
Okm165 merged 204 commits intoJun 10, 2026
Conversation
Batch 1 (iter 1): belief direction reversal threshold, batch token budget, reflect gate log format, ingest knowledge text, STM MERGE fix, health diagnostics. Batch 2 (iter 2): saturation rule in belief prompt, topic LLM-hint threshold 0.80→0.70, subtopic suffix collapse, ingest content dedup, cumulative shift recording, O(N)→ANN knowledge dedup. Batch 3 (iter 3): move episode fetch before forgetting cycle in reflection, BatchEntrenchmentResponse numeric index guard, entrenchment prompt rewrite, Segment coalesce for nullable properties, FeatureConsolidation max_tokens 512→1024.
- ReflectionGateResponse max_tokens 128→256: gate JSON was truncating mid-reasoning,
causing parse failure and always-SKIP fallback (snapshot frozen at 1486 chars)
- BeliefDecayDecision.new_confidence Optional[float]: LLM wrote 'null' for FORGET
action, crashing validation; fall back to 80% decay when None on DECAY action
- get_forgetting_candidates min_age_minutes=60: cycle was archiving brand-new episodes
(e.g. just-stored 13149f06) because access_count=0 looks identical to stale ones
- ExtractionResponse.normalize_response filters {propositions:[{}]}: empty-dict props
from very-short content (e.g. 3-word titles) caused spurious validation retries
- MAX_BELIEFS=35 + SpongeState._prune_weak_beliefs: 21 beliefs by interaction 25
including transient one-offs (juliana stratton, minnesota, fraud); lowest
value=conf*ev*abs(pos) beliefs evicted when cap exceeded
Made-with: Cursor
- _decay_beliefs_with_llm: process top 20 stale per cycle (was 10); with 52+ stale beliefs in Docker, 10/cycle was far too slow to converge - Add max_tokens=1024 to decay LLM call to prevent JSON truncation for 20 decisions - BATCH_BELIEF_DECAY_PROMPT: add explicit guidance to FORGET low-evidence (≤2-3) one-off entity beliefs with gap>10; previously LLM retained everything because the prompt only asked about "foundational" vs "stale" without entity-type heuristic Made-with: Cursor
_prune_weak_beliefs() was guarded behind the 'if not due: return []' early exit in apply_due_staged_updates(). On fresh container start with an old sponge loaded from disk (126 beliefs), no legacy staged updates were immediately due, so the capacity cap never fired and beliefs stayed at 120+. Call _prune_weak_beliefs() before the early return so capacity is enforced on every apply_due_staged_updates() call, regardless of whether any staged updates are ripe for commitment. Made-with: Cursor
…points() qdrant-client 1.17.x removed the .search() method; the correct async API is .query_points(). _find_nearest_knowledge() was introduced with the wrong method call causing AttributeError on every knowledge extraction ingest, silently breaking knowledge deduplication and falling back to the old O(N) scroll path. Switch to .query_points() consistent with the rest of the codebase. Made-with: Cursor
- BatchBeliefDecayResponse: filter non-dict (boolean) items from decisions
validator; prompt/schema mismatch caused LLM to return [True,False,...];
also remove assistant_prefix and align prompt to output {"decisions":[...]}
- ChunkingResponse: filter empty {} items in normalize_chunks validator
- KnowledgeConsolidation: filter null entries in contradictions/merges/weak_uids
- Snapshot truncation: truncate at last sentence boundary instead of mid-word
- Health assessment: include ev=N in beliefs_summary so LLM doesn't falsely
infer "only 1 supporting episode" for well-evidenced beliefs; update prompt
to use ev= field for ossification checks
- Entrenchment prompt: scope to broad worldview topics only; exclude specific
news events/elections/places that are naturally one-directional
Made-with: Cursor
- Health sycophancy false positive: track respond_count separately in BehavioralSignature; fix disagreement_rate denominator to use respond_count (not interaction_count); update health prompt to only flag sycophancy when respond_count > 10 - Health "identity drift" false positive: add March 2026 date context to health prompt so LLM does not flag current-date news events as fabricated - Snapshot overconfidence: add calibrated language scale to reflection prompt (conf >= 0.90 = "certain", 0.70-0.89 = "believe", < 0.50 = "uncertain") - Neo4j notification noise: raise logger to ERROR to suppress benign property-not-found warnings (queries already use coalesce()) Made-with: Cursor
threading.Lock() in async FastAPI endpoints blocks the entire asyncio event loop during long ingest operations (18-130s), causing /health to timeout and the container to be marked unhealthy. Replace with asyncio.Lock + asyncio.to_thread so the event loop stays responsive for health checks while agent processing runs in a thread pool. Made-with: Cursor
- Decay prompt: add confidence-decay-over-time rules so high-confidence stale beliefs are DECAY'd, not just retained unchanged. New rules: gap > 20 always decays, gap > 10 with confidence > 0.7 and low sources decays, saturated positions (>=0.95) with gap > 8 decay. - BELIEF_SATURATION log: show full saturation list instead of [:5] truncation so the displayed count matches the listed items Made-with: Cursor
Delete 7 modules replaced by Neo4j graph-backed architecture: - sponge.py: JSON-based personality state (→ PersonalitySnapshot in graph.py) - stm.py, stm_consolidator.py: short-term memory buffer (→ per-request loading) - updater.py: insight extraction (→ two-tier reflection in agent) - context_format.py: episode formatting helper (→ format_episode_line in graph.py) - health.py, health_trace.py: memory health assessment (removed, not needed)
Delete 6 test files that tested removed functionality: - test_sponge.py: SpongeState persistence - test_agent_health.py: memory health assessment - test_post_process_ordering.py: reflection ordering - test_agent_belief_llm.py: LLM belief extraction - test_consistency.py: sponge consistency checks - test_updater.py: insight extraction
Delete 5 benchmark files that depended on SpongeState architecture: - knowledge_scenarios.py: knowledge acquisition scenarios - psych_scenarios.py: psychological stability scenarios - test_knowledge_acquisition_live.py: live knowledge tests - test_psych_reflection.py: reflection behavior tests - test_psych_stability_live.py: personality stability tests The harness files remain for future use with the new architecture.
Delete sonality/server.py — functionality merged into sonality/api.py. The serve() function now lives in api.py and pyproject.toml entry point updated accordingly.
Remove configuration no longer needed: - SPONGE_FILE, SPONGE_HISTORY_DIR, ESS_AUDIT_LOG_FILE paths - Per-task LLM token limits (unified to LLM_MAX_TOKENS) - EMBEDDING_API_KEY, EMBEDDING_BASE_URL, EMBEDDING_SEND_DIMENSIONS - STM_* buffer settings - MAX_BELIEFS, EPISODIC_RETRIEVAL_COUNT, SEMANTIC_RETRIEVAL_COUNT - OPINION_COOLING_PERIOD, MAX_CONVERSATION_CHARS, REFLECTION_EVERY The graph-backed architecture loads state per-request from Neo4j.
- Collection: Qdrant collection names (derivatives, semantic_features) - SemanticCategory: personality, preferences, knowledge, relationships - ChatRole: system, user, assistant - DENSE_VECTOR constant and shared HNSW/quantization configs Centralizes magic strings into typed enums for safer usage.
Introduce in sonality/memory/graph.py: - SEED_SNAPSHOT: initial personality text - PersonalitySnapshot: dataclass for identity state - BeliefNode: dataclass for individual beliefs with valence/confidence - format_episode_line(): moved from deleted context_format.py - Graph queries: get_personality(), save_personality(), get_all_beliefs(), upsert_belief(), get_top_beliefs() This replaces SpongeState with Neo4j-backed persistent identity.
Move all LLM prompt templates to single canonical location: - REFLECTION_TRIAGE_PROMPT: cheap check if update warranted - REFLECTION_DEEP_PROMPT: full belief/snapshot update - Updated build_system_prompt() for snapshot_text + beliefs_text params - All routing, chunking, reranking, knowledge extraction prompts Delete sonality/llm/prompts.py. Minimize sonality/llm/__init__.py exports.
- Add StreamChunk NamedTuple for typed streaming deltas - Replace interaction_active Event with context manager pattern - Add interaction_in_progress() query function - Remove embedding_provider (use default_provider for all) - Update stream_chat_completion to yield StreamChunk
Update __all__ exports: - Add: SEED_SNAPSHOT, BeliefNode, PersonalitySnapshot, chain_retrieve, route_query, split_retrieve - Remove: deleted module exports (SpongeState, ShortTermMemory, BackgroundSummarizer, HealthReport, etc.) - Remove: internal implementation details (ContractionAction, UpdateMagnitude, AggregationStrategy, etc.)
Replace class-based retrieval with simpler functions: - chain.py: ChainOfQueryAgent → chain_retrieve() - router.py: QueryRouter → route_query() - split.py: SplitQueryAgent → split_retrieve() - reranker.py: simplify rerank_episodes() interface Update retrieval/__init__.py exports accordingly.
belief_provenance.py: - Remove ContractionAction, ProvenanceUpdate, UpdateMagnitude classes - Keep only assess_belief_evidence_batch() for bulk assessment consolidation.py: - Replace ConsolidationEngine class with maybe_consolidate_segment() - Remove ConsolidationReadinessDecision export - Simplify to on-demand segment consolidation
knowledge_extract.py: - Remove consolidate_knowledge, prune_stale_knowledge, detect_correlations - Keep extract_and_store_knowledge, retrieve_relevant_knowledge forgetting.py: - Replace ForgettingEngine class with assess_and_forget() function derivatives.py, dual_store.py, embedder.py, segmentation.py, semantic_features.py: - Remove unused classes and exports - Simplify interfaces for new architecture
- Add module docstring explaining LLM-only classification approach - Use _build_aliases() generic helper for enum alias tables - Remove OpinionDirection.sign property (not needed) - Rename sponge_snapshot param to snapshot_text - Import ChatRole from schema - Clean up type annotations
- Import decode_llm_json from provider (renamed from extract_last_json_object) - Import ChatRole from schema module - Use ChatRole enum for message roles - Remove duplicate _parse_json (use provider's decode_llm_json)
Major architectural change: - Each request starts with zero in-memory state - Identity (PersonalitySnapshot + beliefs) loaded from Neo4j per request - Conversation context managed by caller (chat client / API) - Two-tier reflection: cheap triage → deep update only when warranted Removed: - SpongeState integration - ReflectionGate, ReflectionTrigger classes - Complex belief update machinery - In-memory state persistence New: - respond(messages) accepts full conversation history - respond_stream(messages) for streaming responses - _run_reflection_triage() and _run_reflection_deep() - Load/save personality via MemoryGraph
- Pass full messages[] to agent.respond() instead of last user message - Import ChatRole, BeliefNode from local modules - Simplify /health endpoint response (belief_count, snapshot_version) - Update /beliefs endpoint for BeliefNode format - Add MODEL_ID constant - Use package __version__ for API version
- CLI now maintains conversation history and passes to agent - Remove _show_health(), _show_diff() commands (no longer applicable) - Remove SpongeState imports - Import ChatRole from schema - Use package __version__ for banner - Simplify REPL loop
chat/client.py: - SonalityClient now owns conversation history list - clear_history() method for conversation reset - Chat requests send full history with each call - Update HealthStatus: belief_count, snapshot_version (remove version, interaction_count, topic_count, staged_updates) - Update Belief: valence instead of position, add belief_text chat/telegram.py, chat/terminal.py: - Adjust for new client API
Replace web_search and web_extract with unified web_research at all depth presets (glance through exhaustive). Add ResearchArgs and parse_research_args(). merge_facts() dedupes streamed vs final. format_facts() groups by source with confidence tags. Progress via ctx.progress(). STM injected into research goal. HTTP 429 handling.
Delete evidence synthesis tool. Research now returns structured facts — agent reasons over facts and calls integrate_knowledge.
Add ctx.progress() at recall/integrate stages. Deterministic episode identity via deterministic_id. Partial-store degradation messages. Add ctx.short_term_memory in recall output. Use ctx.research_transcript for reflect. Richer section headers with counts.
Replace LLM-based belief reranking with rank_beliefs_by_similarity (embedding cosine) and rank_beliefs_algorithmically (RRF fusion of embedding + graph confidence * log evidence). Single reasoning model LLM call. Web enrichment via start_research depth=glance. Remove consensus_call, _merge_reflections, forgetting cycle.
Use shared RequestIDMiddleware, HealthResponse, check_health_ dependencies. Constant-time bearer token auth via hmac.compare_digest. Stream wrapped in try/except yielding SSE error event. Max tokens upper bound 131072. Remove /chat endpoint. Ingest response: CredibilitySignals dict + float urgency. Pydantic range constraints on belief fields.
LoopState per-request instead of instance fields. THINKING/ACTING phase machine: _phase_thinking (LLM + compose_guarded, stall/extend handling), _phase_acting (dispatch + MemoryUpdate consolidation, raw tool output never re-enters THINKING). Async bookkeeping queue. _pre_seed_memory for episodic LTM retrieval. _flush_ltm_to_episode. _rank_beliefs with embedding cosine. Streaming Fathom research via thread-safe queue. check_dependencies health probe. Shutdown drain with 10s timeout. Remove context folding, message compression, loop handoff, boundary detection, segment consolidation.
Config: add tts_optimize_concurrency, pydantic validator for TTS fallback defaults, set tts_optimize_max_tokens to -1, default log level DEBUG. Package init: entry-point only, logging from settings.
Replace local llm_call wrapper with compose_guarded() + provider chat_completion. LLMProvider gets concurrency from settings. _TTS_SYSTEM as Final system prompt. Context char limit derived from max_tokens.
Per-request trace_id with X-Trace-ID header and structlog binding. Rename web_search/web_extract tool labels to web_research. Add goal to arg summary scan. Harden SSE parsing with safe empty choices. Fix empty assistant response history corruption. Add tool_progress logging.
Add tool_progress handling with intermediate status updates. Scrolling status window (last 8 steps, max 10 lines). Remove reviewing progress, ESS score/reasoning_type/topics from done footer. Structured elapsed_s.
Stdlib-only HuggingFace GGUF downloader. Default output to .models/. Models: llm (Qwen3.6-35B Q4_K_XL), embed (Qwen3-Embedding-4B Q4_K_M). Selective --all, --list, --force flags. Resumable downloads with .part files. Optional --verify SHA-256.
Feed: strip HTML tags from article description before MIN_DESC_LEN check. Diagnostics: migrate config access to pydantic settings API, remove neo4j_derivatives count and derivative density health rules.
Delete tests/containers.py (Qdrant/Neo4j testcontainers lifecycle). Remove --use-containers option, session db_containers fixture, autouse clear_db_between_tests, multi-target mock_llm_call fixture. Add minimal env defaults. Root conftest: drop benches scope, suppress_expected_logs.
Regression tests for clean_completion, decode_llm_json, extract_tool_calls, strip_markdown, message_content_text, normalize_llm_list_response, coerce_string_fields.
Fathom: remove URLScoring tests, split PageAnalysisResult tests for dict and bare list input. Derivatives: patch async_llm_call and async_embed_documents with AsyncMock. API: update _make_ess() for CredibilitySignals + float urgency, mock check_dependencies and http_dependency_status.
Update chat clients, scripts, and test suite
Rewrite Sonality agent: two-phase automaton, unified tools, async bookkeeping
Refactor Sonality memory: graph schema, ESS storage, unified retrieval
Refactor Sonality core: config, ESS signals, prompts, caller
Refactor Fathom: fact-centric research, embedding ranking, source memory
Feat/shared module refactor
Remove benchmark suite and rewrite documentation
Infrastructure: AGPL license, local llama.cpp stack, CI updates
Restructure into monorepo with shared infrastructure and fathom service
…wledge Structured agentic loop with pipeline enforcement and integrate_knowledge
feat: Unified agentic tool system with web search
…t-caching feat(sonality): token budgets, request caching, API auth, and Telegram isolation
…cumentation docs: comprehensive architecture and system documentation
feat(speaches): improve model preloading reliability
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
Major architectural refactor replacing JSON-based SpongeState with Neo4j graph-backed identity storage:
chain_retrieve,route_query,split_retrieve)sonality/prompts.pyscripts/x_feed.pyfor Twitter/X content ingestionRemoved (20 files)
sponge.py,stm.py,stm_consolidator.py,updater.py,context_format.py,health.py,health_trace.pysonality/server.py,sonality/llm/prompts.pyAdded (2 files)
scripts/x_feed.py: X/Twitter feed ingestionscripts/_helpers.py: Shared display utilitiesModified (51 files)
Core, memory, retrieval, chat, tests, benches, and infrastructure files updated for new architecture.
Test plan
make test— full test suite passesmake db-setup— Neo4j schema migrations workmake chat— interactive terminal works with historymake beliefs— shows beliefs from graphmake reset— clears graph state correctlypython scripts/feed.py --dry-run— RSS feed worksmake bench-ess— ESS calibration passes