Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,13 @@ MEMORY_ENABLED=false
# Models with different dims are rejected at startup. Supporting other
# dimensions requires changing embedding_model_dims in memory.py.
#MEMORY_EMBEDDING_MODEL=all-MiniLM-L6-v2
# Minimum Mem0 similarity score for a memory to be returned. Float in
# [0.0, 1.0]; default 0.3 matches Mem0's built-in default. Governs both
# the context-injection path (format_context) and the /memory search UI
# - one knob, two paths. Raise toward 0.5+ to reduce false positives at
# the cost of recall; lower toward 0.0 if recall feels missing. Tunable
# at runtime by editing this file and restarting the service.
#MEMORY_SEARCH_FLOOR=0.3

# Track 2 Haiku extraction. Requires MEMORY_ENABLED=true and a Claude
# backend. Off by default at Phase 2 ship (self-reinforcing loop needs
Expand Down
10 changes: 9 additions & 1 deletion src/kai/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
filters,
)

from kai import github_api, services, sessions, webhook
from kai import github_api, memory_command, services, sessions, webhook
from kai.config import (
DATA_DIR,
MAX_CONTEXT_CEILING,
Expand Down Expand Up @@ -2845,6 +2845,12 @@ async def handle_help(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
"/github add <repo> - Watch a repo\n"
"/github remove <repo> - Unwatch a repo\n"
"\n"
"/memory - Browse remembered facts by tag\n"
"/memory search <q> - Semantic search over memories\n"
"/memory stats - Counts and confidence distribution\n"
"/memory forget <tag> - Delete all memories with a tag\n"
"/memory help - /memory subcommand reference\n"
"\n"
"/voice - Toggle voice on/off\n"
"/voice only - Voice only (no text)\n"
"/voice on - Text + voice\n"
Expand Down Expand Up @@ -3705,12 +3711,14 @@ def create_bot(config: Config, *, use_webhook: bool = True) -> Application:
app.add_handler(CommandHandler("voices", handle_voices))
app.add_handler(CommandHandler("webhooks", handle_webhooks))
app.add_handler(CommandHandler("github", handle_github))
app.add_handler(CommandHandler("memory", memory_command.handle_memory_command))
app.add_handler(CommandHandler("stop", handle_stop))

# Callback query handlers for inline keyboards (pattern-matched)
app.add_handler(CallbackQueryHandler(handle_model_callback, pattern=r"^model:"))
app.add_handler(CallbackQueryHandler(handle_voice_callback, pattern=r"^voice:"))
app.add_handler(CallbackQueryHandler(handle_workspace_callback, pattern=r"^ws:"))
app.add_handler(CallbackQueryHandler(memory_command.handle_memory_callback, pattern=r"^mem:"))

# Media handlers (must be before the catch-all text handler)
app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
Expand Down
29 changes: 29 additions & 0 deletions src/kai/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,19 @@ class Config:
# an executor thread on a hung subprocess.
memory_extraction_timeout_s: int = 10

# Minimum Mem0 similarity score for a memory to be returned by
# search-driven paths: both `format_context` (context injection
# at session start) and the `/memory search` UI surface in
# `memory_command.py`. Values below the floor are dropped before
# any ranking. Default 0.3 matches Mem0's built-in default and
# the prior hard-coded constant; raise toward 0.5+ to reduce
# false positives at the cost of recall. Spec 310 §7.5 documents
# the "one knob, two paths" decision: keeping the UI floor and
# the context-injection floor in lockstep prevents silent
# divergence between "what the user sees in /memory search" and
# "what Kai pulls into context" after a config change.
memory_search_floor: float = 0.3

def get_workspace_config(self, workspace: Path) -> WorkspaceConfig | None:
"""
Get per-workspace config for a path, or None for global defaults.
Expand Down Expand Up @@ -1400,6 +1413,21 @@ def load_config() -> Config:
except ValueError:
raise SystemExit("MEMORY_EXTRACTION_TIMEOUT_S must be an integer") from None

# Search relevance floor. Float in [0.0, 1.0]; default 0.3 matches
# Mem0's built-in default and the prior hard-coded constant. Same
# try/except pattern as the other memory_* numeric vars: bad input
# exits at startup rather than surfacing as a divide-by-zero or
# mysterious "no results" symptom at first query. Range is bounded
# at both ends because Mem0 cosine similarity is normalized to
# [0.0, 1.0]; a floor outside that range silently filters
# everything (>1.0) or nothing (<0.0), both of which are footguns.
try:
memory_search_floor = float(os.environ.get("MEMORY_SEARCH_FLOOR", "0.3"))
if memory_search_floor < 0.0 or memory_search_floor > 1.0:
raise SystemExit("MEMORY_SEARCH_FLOOR must be between 0.0 and 1.0")
except ValueError:
raise SystemExit("MEMORY_SEARCH_FLOOR must be a number") from None

# Per-workspace configuration. Loaded after ALLOWED_WORKSPACES so
# YAML-defined workspaces can be merged into the allowed set.
workspace_configs = _load_workspace_configs()
Expand Down Expand Up @@ -1557,4 +1585,5 @@ def load_config() -> Config:
memory_extraction_model=memory_extraction_model,
memory_extraction_budget_usd=memory_extraction_budget_usd,
memory_extraction_timeout_s=memory_extraction_timeout_s,
memory_search_floor=memory_search_floor,
)
Loading
Loading