Skip to content

Commit 5c66538

Browse files
committed
Release Dhee 7.2.5 lifecycle repair
1 parent bb9c6c7 commit 5c66538

19 files changed

Lines changed: 662 additions & 29 deletions

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
44

55
The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/).
66

7+
## [7.2.5] - 2026-07-03 - Clear-water lifecycle repair
8+
9+
- Updated the default NVIDIA extraction/generation model to
10+
`moonshotai/kimi-k2.6` and added a live provider model ping in Dhee doctor.
11+
- Enabled scene summarization by default and wired lifecycle enrichment to fill
12+
missing scene summaries with deterministic fallback summaries.
13+
- Added scene-noise audit and repair for operational/test prompt scenes,
14+
tombstoning active noise while preserving it as episodic events.
15+
- Expanded `DHEE_DATA_DIR` tildes consistently and hardened pytest data-dir
16+
isolation so fixture users do not write into the live vault by accident.
17+
718
## [7.2.4] - 2026-07-02 - Clear-water memory enrichment
819

920
- Deferred enrichment now runs structured engram extraction and marks honest

dhee/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
# Default import remains model-free for backwards compatibility.
5151
Memory = CoreMemory
5252

53-
__version__ = "7.2.4"
53+
__version__ = "7.2.5"
5454
__all__ = [
5555
# Memory classes
5656
"Engram",

dhee/configs/base.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from pathlib import Path
23
from typing import Any, Dict, List, Optional
34

45
from pydantic import BaseModel, Field, field_validator
@@ -12,12 +13,17 @@
1213
)
1314

1415

16+
def resolve_dhee_data_dir(path: str | os.PathLike[str]) -> str:
17+
"""Return an absolute Dhee data directory with user markers expanded."""
18+
return os.path.abspath(os.path.expanduser(os.fspath(path)))
19+
20+
1521
def _dhee_data_dir() -> str:
1622
"""Resolve data directory: DHEE_DATA_DIR > ~/.dhee."""
1723
env = os.environ.get("DHEE_DATA_DIR")
1824
if env:
19-
return env
20-
return os.path.join(os.path.expanduser("~"), ".dhee")
25+
return resolve_dhee_data_dir(env)
26+
return resolve_dhee_data_dir(Path.home() / ".dhee")
2127

2228

2329
_VALID_VECTOR_PROVIDERS = {"memory", "sqlite_vec", "zvec"}
@@ -180,7 +186,7 @@ class SceneConfig(BaseModel):
180186
scene_topic_threshold: float = 0.55 # cosine sim below this = topic shift
181187
auto_close_inactive_minutes: int = 120
182188
max_scene_memories: int = 50
183-
use_llm_summarization: bool = False
189+
use_llm_summarization: bool = True
184190
summary_regenerate_threshold: int = 5
185191

186192

dhee/contract_runtime.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -741,17 +741,14 @@ def guard_router_call(tool_name: str, arguments: Dict[str, Any]) -> Dict[str, An
741741
"repo": str(repo_root),
742742
})
743743
if mode == "deny":
744-
# Even in deny mode a supervisor outage must leave a recovery
745-
# path open: read-only tools cannot violate a contract, and the
746-
# dhee CLI is how the agent repairs the runtime state.
747744
command = str(arguments.get("command") or "").strip()
748-
recoverable = tool_name in _READ_TOOL_NAMES or tool_name in _GREP_TOOL_NAMES or (
749-
tool_name in _BASH_TOOL_NAMES and command.startswith("dhee ")
745+
recoverable = tool_name in {"Read", "Grep"} or (
746+
tool_name == "Bash" and command.startswith("dhee ")
750747
)
751748
if recoverable:
752749
warning = (
753750
"Contract supervisor unavailable; deny mode allowed this "
754-
"read-only/remediation call."
751+
"native read-only/remediation call."
755752
)
756753
_record_enforcement_warning(
757754
repo_root,

dhee/core/scene.py

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,16 +229,10 @@ def close_scene(self, scene_id: str, timestamp: Optional[str] = None) -> None:
229229

230230
# Generate summary (LLM when enabled, otherwise deterministic extractive fallback).
231231
memories = self.db.get_scene_memories(scene_id)
232-
summary = None
233-
if self.use_llm_summarization and self.llm:
234-
summary = self._summarize_scene(scene, memories)
235-
if not summary:
236-
summary = self._extractive_scene_summary(memories)
232+
summary = self._generate_scene_summary(scene, memories)
237233
if summary:
238234
updates["summary"] = summary
239-
# Derive title from summary
240-
title = summary.split(".")[0][:120]
241-
updates["title"] = title
235+
updates["title"] = self._title_from_summary(summary)
242236

243237
if updates:
244238
self.db.update_scene(scene_id, updates)
@@ -270,6 +264,77 @@ def auto_close_stale(self, user_id: str) -> List[str]:
270264
# Summarization
271265
# ------------------------------------------------------------------
272266

267+
def summarize_scene(self, scene_id: str, *, force: bool = False) -> Optional[str]:
268+
"""Generate and persist a summary for an existing scene."""
269+
scene = self.db.get_scene(scene_id)
270+
if not scene:
271+
return None
272+
existing = str(scene.get("summary") or "").strip()
273+
if existing and not force:
274+
return existing
275+
276+
memories = self.db.get_scene_memories(scene_id)
277+
summary = self._generate_scene_summary(scene, memories)
278+
if not summary:
279+
return None
280+
self.db.update_scene(
281+
scene_id,
282+
{
283+
"summary": summary,
284+
"title": self._title_from_summary(summary),
285+
},
286+
)
287+
return summary
288+
289+
def summarize_unsummarized(
290+
self,
291+
*,
292+
user_id: Optional[str] = None,
293+
limit: int = 100,
294+
) -> Dict[str, Any]:
295+
"""Fill missing scene summaries for a bounded maintenance slice."""
296+
bounded_limit = max(1, min(int(limit), 2_000))
297+
scenes = self.db.get_scenes(user_id=user_id, limit=bounded_limit)
298+
summarized: List[Dict[str, Any]] = []
299+
skipped_existing = 0
300+
skipped_empty = 0
301+
302+
for scene in scenes:
303+
if str(scene.get("summary") or "").strip():
304+
skipped_existing += 1
305+
continue
306+
summary = self.summarize_scene(str(scene["id"]), force=True)
307+
if not summary:
308+
skipped_empty += 1
309+
continue
310+
summarized.append(
311+
{
312+
"id": scene.get("id"),
313+
"title": self._title_from_summary(summary),
314+
}
315+
)
316+
317+
return {
318+
"scanned_count": len(scenes),
319+
"summarized_count": len(summarized),
320+
"skipped_existing": skipped_existing,
321+
"skipped_empty": skipped_empty,
322+
"summaries": summarized[:50],
323+
"truncated_summaries": max(0, len(summarized) - 50),
324+
}
325+
326+
def _generate_scene_summary(
327+
self,
328+
scene: Dict[str, Any],
329+
memories: List[Dict[str, Any]],
330+
) -> Optional[str]:
331+
summary = None
332+
if self.use_llm_summarization and self.llm:
333+
summary = self._summarize_scene(scene, memories)
334+
if not summary:
335+
summary = self._extractive_scene_summary(memories)
336+
return summary
337+
273338
def _summarize_scene(
274339
self, scene: Dict[str, Any], memories: List[Dict[str, Any]]
275340
) -> Optional[str]:
@@ -319,6 +384,14 @@ def _extractive_scene_summary(memories: List[Dict[str, Any]], max_lines: int = 4
319384
return None
320385
return " | ".join(snippets)
321386

387+
@staticmethod
388+
def _title_from_summary(summary: str) -> str:
389+
cleaned = " ".join(str(summary or "").split())
390+
if not cleaned:
391+
return "Untitled scene"
392+
first_sentence = cleaned.split(".")[0].strip()
393+
return (first_sentence or cleaned)[:120]
394+
322395
# ------------------------------------------------------------------
323396
# Search
324397
# ------------------------------------------------------------------

dhee/doctor.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class DoctorReport:
4949
generated_at: float = 0.0
5050
core: dict[str, Any] = field(default_factory=dict)
5151
runtime: dict[str, Any] = field(default_factory=dict)
52+
provider_health: dict[str, Any] = field(default_factory=dict)
5253
router: dict[str, Any] = field(default_factory=dict)
5354
context: dict[str, Any] = field(default_factory=dict)
5455
cognition: dict[str, Any] = field(default_factory=dict)
@@ -62,6 +63,7 @@ def to_dict(self) -> dict[str, Any]:
6263
"generated_at": self.generated_at,
6364
"core": self.core,
6465
"runtime": self.runtime,
66+
"provider_health": self.provider_health,
6567
"router": self.router,
6668
"context": self.context,
6769
"cognition": self.cognition,
@@ -123,6 +125,61 @@ def _runtime_section() -> dict[str, Any]:
123125
return {"error": f"{type(exc).__name__}: {exc}"}
124126

125127

128+
def _provider_health_section() -> dict[str, Any]:
129+
try:
130+
from dhee.cli_config import get_api_key, load_config
131+
from dhee.provider_defaults import provider_defaults
132+
except Exception as exc:
133+
return {"ok": False, "status": "config_error", "error": f"{type(exc).__name__}: {exc}"}
134+
135+
config = load_config()
136+
provider = str(config.get("provider") or "nvidia").strip().lower()
137+
defaults = provider_defaults(provider)
138+
model = str(config.get("llm_model") or defaults.get("llm_model") or "").strip()
139+
if provider != "nvidia":
140+
return {
141+
"ok": None,
142+
"status": "not_implemented",
143+
"provider": provider,
144+
"model": model,
145+
}
146+
147+
api_key = get_api_key(provider)
148+
if not api_key:
149+
return {
150+
"ok": False,
151+
"status": "missing_api_key",
152+
"provider": provider,
153+
"model": model,
154+
"required_env": defaults.get("env_var"),
155+
}
156+
157+
try:
158+
from dhee.llms.nvidia import NvidiaLLM
159+
160+
llm = NvidiaLLM(
161+
{
162+
"api_key": api_key,
163+
"model": model,
164+
"temperature": 0,
165+
"max_tokens": 2,
166+
"timeout": 8,
167+
"max_retries": 0,
168+
"app_retries": 1,
169+
}
170+
)
171+
return llm.ping()
172+
except Exception as exc:
173+
return {
174+
"ok": False,
175+
"status": "unavailable",
176+
"provider": provider,
177+
"model": model,
178+
"error_type": type(exc).__name__,
179+
"error": str(exc),
180+
}
181+
182+
126183
def _context_section() -> dict[str, Any]:
127184
out: dict[str, Any] = {}
128185
try:
@@ -597,6 +654,7 @@ def build_report() -> DoctorReport:
597654

598655
core = _core_section()
599656
runtime = _runtime_section()
657+
provider_health = _provider_health_section()
600658
router = _router_section()
601659
context = _context_section()
602660
cognition = _cognition_section()
@@ -609,6 +667,7 @@ def build_report() -> DoctorReport:
609667
generated_at=time.time(),
610668
core=core,
611669
runtime=runtime,
670+
provider_health=provider_health,
612671
router=router,
613672
context=context,
614673
cognition=cognition,
@@ -627,6 +686,7 @@ def format_human(report: DoctorReport) -> str:
627686
lines: list[str] = []
628687
core = report.core
629688
runtime = report.runtime
689+
provider_health = report.provider_health
630690
router = report.router
631691
context = report.context
632692
cog = report.cognition
@@ -674,6 +734,17 @@ def format_human(report: DoctorReport) -> str:
674734
lines.append(f" runtime dir: {paths.get('runtime_dir')}")
675735
lines.append("")
676736

737+
# Provider health
738+
lines.append("[ provider health ]")
739+
lines.append(f" provider: {provider_health.get('provider', '?')}")
740+
lines.append(f" model: {provider_health.get('model', '?')}")
741+
lines.append(f" status: {provider_health.get('status', '?')}")
742+
if provider_health.get("latency_ms") is not None:
743+
lines.append(f" latency: {provider_health.get('latency_ms')} ms")
744+
if provider_health.get("error_type"):
745+
lines.append(f" error: {provider_health.get('error_type')}: {provider_health.get('error')}")
746+
lines.append("")
747+
677748
# Router
678749
lines.append("[ router ]")
679750
if "error" in router:

dhee/llms/nvidia.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22
import os
33
import time
4-
from typing import Optional
4+
from typing import Any, Dict, Optional
55

66
from dhee.llms.base import BaseLLM
77
from dhee.provider_defaults import DEFAULT_NVIDIA_LLM_MODEL
@@ -59,6 +59,40 @@ def __init__(self, config: Optional[dict] = None):
5959
self.top_p = self.config.get("top_p", 0.7)
6060
self.enable_thinking = self.config.get("enable_thinking", False)
6161

62+
def ping(self) -> Dict[str, Any]:
63+
"""Check that the configured NVIDIA model accepts a minimal chat call."""
64+
started = time.perf_counter()
65+
try:
66+
response = self.client.chat.completions.create(
67+
model=self.model,
68+
messages=[{"role": "user", "content": "Reply with OK."}],
69+
temperature=0,
70+
top_p=1,
71+
max_tokens=2,
72+
stream=False,
73+
)
74+
content = ""
75+
if getattr(response, "choices", None):
76+
content = str(response.choices[0].message.content or "").strip()
77+
return {
78+
"ok": True,
79+
"status": "ready",
80+
"provider": "nvidia",
81+
"model": self.model,
82+
"latency_ms": round((time.perf_counter() - started) * 1000, 2),
83+
"response_preview": content[:20],
84+
}
85+
except Exception as exc:
86+
return {
87+
"ok": False,
88+
"status": "unavailable",
89+
"provider": "nvidia",
90+
"model": self.model,
91+
"latency_ms": round((time.perf_counter() - started) * 1000, 2),
92+
"error_type": type(exc).__name__,
93+
"error": str(exc),
94+
}
95+
6296
def generate(self, prompt: str) -> str:
6397
from openai import APITimeoutError, APIConnectionError
6498

0 commit comments

Comments
 (0)