Skip to content

Refactor to stateless graph-backed architecture#81

Merged
Okm165 merged 204 commits into
chore/format-long-linesfrom
refactor/stateless-graph-architecture
Jun 10, 2026
Merged

Refactor to stateless graph-backed architecture#81
Okm165 merged 204 commits into
chore/format-long-linesfrom
refactor/stateless-graph-architecture

Conversation

@Okm165

@Okm165 Okm165 commented Apr 26, 2026

Copy link
Copy Markdown
Owner

Summary

Major architectural refactor replacing JSON-based SpongeState with Neo4j graph-backed identity storage:

  • Stateless design: Each request loads identity (PersonalitySnapshot + beliefs) from Neo4j; no in-memory state persists between requests
  • Two-tier reflection: Cheap triage determines if deep belief/snapshot update is warranted, reducing LLM calls
  • Client-side history: Chat clients now own conversation context and send full history with each request
  • Simplified retrieval: Replace class-based agents with function-based API (chain_retrieve, route_query, split_retrieve)
  • Consolidated prompts: All LLM templates in single sonality/prompts.py
  • X feed support: New scripts/x_feed.py for Twitter/X content ingestion

Removed (20 files)

  • Memory: sponge.py, stm.py, stm_consolidator.py, updater.py, context_format.py, health.py, health_trace.py
  • Tests: 6 test files for removed functionality
  • Benches: 5 scenario files dependent on SpongeState
  • Other: sonality/server.py, sonality/llm/prompts.py

Added (2 files)

  • scripts/x_feed.py: X/Twitter feed ingestion
  • scripts/_helpers.py: Shared display utilities

Modified (51 files)

Core, memory, retrieval, chat, tests, benches, and infrastructure files updated for new architecture.

Test plan

  • make test — full test suite passes
  • make db-setup — Neo4j schema migrations work
  • make chat — interactive terminal works with history
  • make beliefs — shows beliefs from graph
  • make reset — clears graph state correctly
  • python scripts/feed.py --dry-run — RSS feed works
  • Verify beliefs persist across agent restarts
  • Run make bench-ess — ESS calibration passes

Okm165 added 30 commits March 18, 2026 09:51
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
Okm165 added 29 commits June 10, 2026 15:24
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
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
@Okm165
Okm165 merged commit 2613bf3 into chore/format-long-lines Jun 10, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant