Skip to content

Commit 1cbebcb

Browse files
emooreatxclaude
andauthored
Release 2.4.2: Context enrichment cache auto-population (#669)
* Bump version to 2.4.2 Known issues to fix in this release: 1. Wallet badge does not recalculate after trust finalizes at 5/5 (stays 1/5 amber) 2. Music Assistant devices not detected - needs robust detection + dedicated UI page for troubleshooting/configuring HA/MA when HA adapter is loaded 3. Depth limit violated via repeated pondering (7 round limit not enforced) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix wallet badge, Music Assistant detection, and depth limit enforcement Bug fixes for v2.4.2: 1. Wallet badge stays amber after trust level change - InteractViewModel.kt: Call fetchWalletStatus() when trust level changes - Badge color depends on spending authority which changes with trust level 2. Music Assistant tools not detected - tool_service.py: Add notify_ha_initialized() method for post-init MA detection - adapter.py: Call notify_ha_initialized() after background HA initialization - Fixes race condition where tool discovery ran before HA entities loaded 3. Depth limit (7 rounds) not enforced - component_builder.py: Add bypass_exemption=True to ThoughtDepthGuardrail - Guardrail must always run to enforce hard safety limit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add Environment Info page and fix lat/long passthrough in setup Features: 1. New Environment Information page under Adapters & Tools menu - Shows user location from setup (lat/long, timezone, city) - Displays context enrichment results from all adapters - Shows cache statistics for debugging - Highlights missing coordinates that break weather/nav 2. Fix lat/long not passed to backend during first-run setup - Add location_latitude/longitude fields to CompleteSetupRequest model - Extract coordinates from selectedLocation in SetupViewModel - Include timezone from selected location 3. Backend API endpoint: GET /v1/system/environment - Returns location info from env vars - Returns context enrichment cache data - Returns cache statistics Files changed: - services.py: Add /v1/system/environment endpoint - system_snapshot_helpers.py: Add get_all_entries() to cache - Setup.kt: Add location_latitude/longitude to CompleteSetupRequest - SetupViewModel.kt: Extract lat/long/timezone from selectedLocation - CIRISApp.kt: Add EnvironmentInfo screen and menu item - CIRISApiClient.kt: Add getEnvironmentInfo() method - CIRISApiClientProtocol.kt: Add interface and data classes - EnvironmentInfoScreen.kt: New UI screen (created) - EnvironmentInfoViewModel.kt: New ViewModel (created) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add Environment Items shopping list UI with CRUD Backend changes: - Add GET /v1/system/adapters/context-enrichment endpoint for adapter cache - Add location fields to SetupConfigResponse (from .env) - Remove redundant /v1/system/environment endpoint Mobile changes: - Shopping list style Environment Info screen with category filters - Categories: Want, Need, Have, Can Borrow, Can Barter - Item cards with quantity, condition, and notes - Add/Delete item dialogs - Grayed-out community share toggles (coming soon) - Context enrichment display with cache stats - Uses existing /memory/query API for ENVIRONMENT scope nodes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add context enrichment cache auto-population and unit tests - Auto-populate enrichment cache at startup and when adapters load - Fix 404 on /adapters/context-enrichment route ordering - Add comprehensive tests for startup cache and adapter refresh - Update changelog for 2.4.1 and 2.4.2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Address Codex feedback on environment items and config - Fix NodeType: use "concept" instead of invalid "object" - Fix delete scope: add scope query param to forget endpoint, pass scope=environment from mobile - Fix location parsing: wrap float() in try/except to handle invalid env vars 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add unit tests for Codex feedback fixes - Test forget endpoint with environment/community/local scope params - Test location env var parsing with valid/invalid/empty values - 9 new tests covering all three Codex-identified issues 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix SonarCloud S5765: Store asyncio task to prevent GC Store background task in _background_tasks set and add done callback to remove completed tasks. Prevents premature garbage collection. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix conscience prompt localization when user changes language - Add set_conscience_prompt_language() call in sync_language_preference() to update conscience prompts when user changes preferred_language via API - Update streaming localization test to check DMA prompts instead of conscience prompts (conscience prompts aren't streamed in SSE events) - Add unit test for conscience prompt loader sync 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix SonarCloud issues: FastAPI Annotated hints and cognitive complexity - memory.py: Use Annotated type hints for Path/Query parameters (S5765) - config.py: Extract helper functions to reduce cognitive complexity from 19 to ~10: _require_auth_if_configured, _get_template_id, _parse_float_env 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 58debb7 commit 1cbebcb

File tree

33 files changed

+2352
-99
lines changed

33 files changed

+2352
-99
lines changed

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ All notable changes to CIRIS Agent will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.4.2] - 2026-04-10
9+
10+
### Added
11+
12+
- **Context Enrichment Cache Auto-Population** - Enrichment cache now auto-populates at startup and when adapters load dynamically, eliminating first-thought latency
13+
- **Unit Tests for Enrichment Cache** - Added comprehensive tests for startup cache population and adapter cache refresh
14+
15+
### Fixed
16+
17+
- **Context Enrichment Route** - Fixed 404 on `/adapters/context-enrichment` endpoint by moving it before wildcard route
18+
19+
## [2.4.1] - 2026-04-09
20+
21+
### Added
22+
23+
- **WA Key Auto-Rotation** - User Wise Authority keys now auto-rotate with unit test coverage
24+
- **WA Signing via CIRISVerify** - Named key signing capability through CIRISVerify integration
25+
- **Play Integrity Reporting** - CIRISVerify v1.5.3 with Play Integrity failure reporting
26+
27+
### Fixed
28+
29+
- **Wallet Badge Display** - Fixed trust badge and wallet race conditions at startup
30+
- **Attestation Lights** - Parse CIRISVerify v1.5.x unified attestation format correctly
31+
- **Domain Filtering** - Fixed domain filtering and deterministic trace IDs
32+
833
## [2.4.0] - 2026-04-07
934

1035
### Added

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
**A type-safe, auditable AI agent framework with built-in ethical reasoning**
1919

20-
**BETA RELEASE 2.4.1-stable** | [Release Notes](CHANGELOG.md) | [Documentation Hub](docs/README.md)
20+
**BETA RELEASE 2.4.2-stable** | [Release Notes](CHANGELOG.md) | [Documentation Hub](docs/README.md)
2121

2222
CIRIS lets you run AI agents that explain their decisions, defer to humans when uncertain, and maintain complete audit trails. Currently powering Discord community moderation, designed to scale to healthcare and education.
2323

android/app/src/main/python/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
# Static version - updated at build time by the Android build process
99
# This avoids file-system hashing logic that doesn't work in the Android package
10-
__version__ = "android-2.4.1"
10+
__version__ = "android-2.4.2"
1111

1212

1313
def get_version() -> str:

ciris_adapters/home_assistant/adapter.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ async def _background_initialize(self) -> None:
140140
f"[HA DISCOVERY] Found {len(entities)} entities across {len(domains)} domains: "
141141
+ ", ".join(f"{d}={c}" for d, c in sorted(domains.items(), key=lambda x: -x[1])[:15])
142142
)
143+
# Notify tool service that HA is now initialized so it can detect Music Assistant
144+
# This is critical - without this, MA tools won't appear if tool discovery
145+
# happened before HA finished loading entities
146+
await self.tool_service.notify_ha_initialized()
143147
except Exception as e:
144148
logger.warning(f"[HA DISCOVERY] Entity discovery failed: {e}")
145149
else:

ciris_adapters/home_assistant/tool_service.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,20 @@ def get_service_metadata(self) -> Dict[str, Any]:
12981298
"""Return service metadata for DSAR and data source discovery."""
12991299
return {"data_source": False, "service_type": "device_control"}
13001300

1301+
async def notify_ha_initialized(self) -> None:
1302+
"""Called after HA background initialization completes.
1303+
1304+
This triggers Music Assistant detection now that entities are available.
1305+
Without this, MA tools might not appear if tool discovery happened
1306+
before HA finished loading entities.
1307+
"""
1308+
logger.info("[HA TOOLS] HA initialized - checking for Music Assistant")
1309+
ma_detected = await self._check_ma_available()
1310+
if ma_detected:
1311+
logger.info("[HA TOOLS] Music Assistant tools now available")
1312+
else:
1313+
logger.debug("[HA TOOLS] Music Assistant not detected after HA init")
1314+
13011315
async def get_available_tools(self) -> List[str]:
13021316
"""Get available tool names. Used by system snapshot tool collection."""
13031317
tools = list(self.TOOL_DEFINITIONS.keys())

ciris_engine/constants.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
from pathlib import Path
44

55
# Version information
6-
CIRIS_VERSION = "2.4.1-stable"
6+
CIRIS_VERSION = "2.4.2-stable"
77
ACCORD_VERSION = "1.2-Beta"
88
CIRIS_VERSION_MAJOR = 2
99
CIRIS_VERSION_MINOR = 4
10-
CIRIS_VERSION_PATCH = 1
10+
CIRIS_VERSION_PATCH = 2
1111
CIRIS_VERSION_BUILD = 0
1212
CIRIS_VERSION_STAGE = "stable"
1313
CIRIS_CODENAME = "Context Engineering" # Codename for this release

ciris_engine/logic/adapters/api/routes/memory.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,8 @@ async def forget_memory(
434434
request: Request,
435435
auth: AuthAdminDep,
436436
memory_service: MemoryServiceDep,
437-
node_id: str = Path(..., description="Node ID to forget"),
437+
node_id: Annotated[str, Path(description="Node ID to forget")],
438+
scope: Annotated[Optional[str], Query(description="Graph scope (local, environment, community)")] = None,
438439
) -> SuccessResponse[MemoryOpResult[GraphNode]]:
439440
"""
440441
Forget a specific memory node (FORGET).
@@ -447,10 +448,19 @@ async def forget_memory(
447448
# The forget method will look up the full node internally
448449
from ciris_engine.schemas.services.graph_core import GraphNode, GraphScope, NodeType
449450

451+
# Parse scope from query param, default to LOCAL for backwards compatibility
452+
graph_scope = GraphScope.LOCAL
453+
if scope:
454+
scope_lower = scope.lower()
455+
if scope_lower == "environment":
456+
graph_scope = GraphScope.ENVIRONMENT
457+
elif scope_lower == "community":
458+
graph_scope = GraphScope.COMMUNITY
459+
450460
node_to_forget = GraphNode(
451461
id=node_id,
452462
type=NodeType.CONCEPT, # Default type, will be looked up by forget method
453-
scope=GraphScope.LOCAL, # Default scope
463+
scope=graph_scope,
454464
attributes={},
455465
)
456466

ciris_engine/logic/adapters/api/routes/setup/config.py

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -264,43 +264,60 @@ def _detect_api_key_set(provider: str) -> bool:
264264
router = APIRouter()
265265

266266

267-
@router.get("/config", responses=RESPONSES_401_500)
268-
async def get_current_config(request: Request) -> SuccessResponse[SetupConfigResponse]:
269-
"""Get current configuration.
267+
async def _require_auth_if_configured(request: Request) -> None:
268+
"""Require authentication if setup is already completed."""
269+
if _is_setup_allowed_without_auth():
270+
return
270271

271-
Returns current setup configuration for editing.
272-
Requires authentication if setup is already completed.
273-
"""
274-
# If not first-run, require authentication
275-
if not _is_setup_allowed_without_auth():
276-
# Manually get auth context from request
277-
try:
278-
from ...dependencies.auth import get_auth_context, get_auth_service
279-
280-
# Extract authorization header and auth service manually since we're not using Depends()
281-
authorization = request.headers.get("Authorization")
282-
auth_service = get_auth_service(request)
283-
auth = await get_auth_context(request, authorization, auth_service)
284-
if auth is None:
285-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
286-
except HTTPException:
287-
raise
288-
except Exception as e:
289-
logger.warning(f"Authentication failed for /setup/config: {e}")
272+
from ...dependencies.auth import get_auth_context, get_auth_service
273+
274+
try:
275+
authorization = request.headers.get("Authorization")
276+
auth_service = get_auth_service(request)
277+
auth = await get_auth_context(request, authorization, auth_service)
278+
if auth is None:
290279
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
280+
except HTTPException:
281+
raise
282+
except Exception as e:
283+
logger.warning(f"Authentication failed for /setup/config: {e}")
284+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required")
285+
291286

292-
# Get template from CLI flag (via runtime config) or environment variable
293-
# CLI --template flag takes precedence on first-run before .env exists
287+
def _get_template_id(request: Request) -> str:
288+
"""Get template ID from CLI flag or environment variable."""
294289
template_id = os.getenv("CIRIS_TEMPLATE")
295290
if not template_id:
296291
runtime = getattr(request.app.state, "runtime", None)
297292
if runtime and hasattr(runtime, "essential_config") and runtime.essential_config:
298293
template_id = getattr(runtime.essential_config, "default_template", None)
299-
if not template_id:
300-
template_id = "default"
294+
return template_id or "default"
295+
296+
297+
def _parse_float_env(env_var: str) -> Optional[float]:
298+
"""Parse a float from environment variable, returning None if invalid."""
299+
value = os.getenv(env_var)
300+
if not value:
301+
return None
302+
try:
303+
return float(value)
304+
except ValueError:
305+
return None
306+
307+
308+
@router.get("/config", responses=RESPONSES_401_500)
309+
async def get_current_config(request: Request) -> SuccessResponse[SetupConfigResponse]:
310+
"""Get current configuration.
311+
312+
Returns current setup configuration for editing.
313+
Requires authentication if setup is already completed.
314+
"""
315+
await _require_auth_if_configured(request)
301316

302-
# Detect LLM provider using same logic as LLM service
317+
template_id = _get_template_id(request)
303318
llm_provider = _detect_llm_provider(request)
319+
latitude = _parse_float_env("CIRIS_USER_LATITUDE")
320+
longitude = _parse_float_env("CIRIS_USER_LONGITUDE")
304321

305322
config = SetupConfigResponse(
306323
llm_provider=llm_provider,
@@ -313,6 +330,13 @@ async def get_current_config(request: Request) -> SuccessResponse[SetupConfigRes
313330
template_id=template_id,
314331
enabled_adapters=os.getenv("CIRIS_ADAPTER", "api").split(","),
315332
agent_port=int(os.getenv("CIRIS_API_PORT", "8080")),
333+
location_country=os.getenv("CIRIS_USER_COUNTRY"),
334+
location_region=os.getenv("CIRIS_USER_REGION"),
335+
location_city=os.getenv("CIRIS_USER_CITY"),
336+
location_latitude=latitude,
337+
location_longitude=longitude,
338+
timezone=os.getenv("CIRIS_USER_TIMEZONE"),
339+
has_coordinates=latitude is not None and longitude is not None,
316340
)
317341

318342
return SuccessResponse(data=config)

ciris_engine/logic/adapters/api/routes/setup/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,15 @@ class SetupConfigResponse(BaseModel):
378378
# Application
379379
agent_port: int = Field(default=8080, description="Current agent port")
380380

381+
# User Location (from setup)
382+
location_country: Optional[str] = Field(None, description="User's country")
383+
location_region: Optional[str] = Field(None, description="User's region/state")
384+
location_city: Optional[str] = Field(None, description="User's city")
385+
location_latitude: Optional[float] = Field(None, description="Latitude in decimal degrees")
386+
location_longitude: Optional[float] = Field(None, description="Longitude in decimal degrees")
387+
timezone: Optional[str] = Field(None, description="IANA timezone")
388+
has_coordinates: bool = Field(False, description="Whether lat/long are available")
389+
381390

382391
class CreateUserRequest(BaseModel):
383392
"""Request to create initial admin user."""

ciris_engine/logic/adapters/api/routes/system/adapters.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,45 @@ def _get_loaded_count(module_id: str) -> int:
713713
raise HTTPException(status_code=500, detail=str(e))
714714

715715

716+
@router.get(
717+
"/adapters/context-enrichment",
718+
responses={500: {"description": "Failed to get context enrichment data"}},
719+
)
720+
async def get_context_enrichment_cache(
721+
request: Request,
722+
auth: Annotated[AuthContext, Depends(require_observer)],
723+
) -> SuccessResponse[Dict[str, Any]]:
724+
"""
725+
Get context enrichment cache data from adapters.
726+
727+
Returns cached results from adapter tools that have context_enrichment=True,
728+
along with cache statistics. This data is ephemeral and refreshed periodically.
729+
"""
730+
from ciris_engine.logic.context.system_snapshot_helpers import get_enrichment_cache
731+
732+
try:
733+
cache = get_enrichment_cache()
734+
enrichment_data = cache.get_all_entries()
735+
cache_stats = cache.stats
736+
737+
return SuccessResponse(
738+
data={
739+
"entries": enrichment_data,
740+
"stats": {
741+
"entry_count": cache_stats.get("entries", 0),
742+
"hits": cache_stats.get("hits", 0),
743+
"misses": cache_stats.get("misses", 0),
744+
"hit_rate_pct": cache_stats.get("hit_rate_pct", 0.0),
745+
"startup_populated": cache_stats.get("startup_populated", False),
746+
},
747+
}
748+
)
749+
750+
except Exception as e:
751+
logger.error(f"Error getting context enrichment cache: {e}")
752+
raise HTTPException(status_code=500, detail=str(e))
753+
754+
716755
@router.get("/adapters/{adapter_id}", responses=RESPONSES_ADAPTER_STATUS)
717756
async def get_adapter_status(
718757
adapter_id: str,
@@ -964,3 +1003,5 @@ async def reload_adapter(
9641003
except Exception as e:
9651004
logger.error(f"Error reloading adapter: {e}")
9661005
raise HTTPException(status_code=500, detail=str(e))
1006+
1007+

0 commit comments

Comments
 (0)