feat(clustering): 1h TTL + skip recompute when tasks unchanged

focus-area now recomputes at most once per hour, and only if the task list
actually changed since the last compute.

- focus-area TTL: 43200s → 3600s; version bumped to 2.1.0
- computeAndStore hashes sorted task contents (MD5) and checks the stored
  _task_hash in the existing snapshot; skips the ml-serving call when the
  hash matches and the output isn't expired
- ml-serving injects _task_hash into the snapshot so the next cycle can compare

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 14:45:15 +00:00
parent 9ddeea6cac
commit d12f11d29d
4 changed files with 38 additions and 5 deletions

View File

@@ -20,7 +20,7 @@ def _infer_preferred_areas(history: UserHistory) -> list[str]:
MANIFEST = AgentManifest(
id="focus-area",
version="2.0.0", # semantic clustering via nomic-embed-text (#97, #113)
version="2.1.0", # 1h TTL + task-change detection (#129)
description="Identifies the most congested semantic focus area in the user's task list.",
pref_schema={
"type": "object",
@@ -37,7 +37,7 @@ MANIFEST = AgentManifest(
context_schema=["todoist.tasks"],
required_consents=["data:core", "data:todoist"],
output_contract={"type": "snippet", "format": "free_text"},
ttl_sec=43_200,
ttl_sec=3_600,
inferred_params=[
InferredParam(
key="preferred_areas",
@@ -54,7 +54,7 @@ class FocusAreaAgent(BaseAgent):
"""Identifies the most congested semantic focus area in the user's task list."""
agent_id: ClassVar[str] = MANIFEST.id
ttl_seconds: ClassVar[int] = MANIFEST.ttl_sec
version: ClassVar[str] = MANIFEST.version
version: ClassVar[str] = MANIFEST.version # 2.1.0
def compute(self, inp: AgentInput) -> AgentOutput:
preferred: list[str] = inp.agent_prefs.get("preferred_areas", [])

View File

@@ -662,7 +662,7 @@ class TestFocusAreaPreferredAreas:
def test_version_bumped(self):
from ml.agents.focus_area import MANIFEST as FA_MANIFEST
assert FA_MANIFEST.version == "2.0.0"
assert FA_MANIFEST.version == "2.1.0"
def test_snapshot_uses_cluster_keys(self):
tasks = [self._task("T", "work")]