personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(sense): require facets array minItems: 1; hydrator drops constraint on empty journals

Tightens talent/sense.schema.json so the sense talent must attribute every
active segment to at least one configured facet. The runtime hydrator drops
the minItems constraint when the journal has no valid facets so the schema
stays satisfiable on fresh journals. Prompt copy updated to require closest-
match attribution; no behavior change to the activity state machine's no-
facet sentinel branch (kept as defense-in-depth).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+85 -3
+3 -3
talent/sense.md
··· 84 84 - **other**: Use only when the entity is grounded in another clear signal that does not fit the categories above. 85 85 86 86 ### facets 87 - Classify into the owner's configured facets. Only include facets with clear, direct evidence of activity. Be precise — assign exactly ONE primary facet in most cases. Only add a second facet if there is genuinely distinct secondary activity. For each: 87 + Classify into the owner's configured facets. Always include at least one facet — pick the closest configured facet. If multiple facets fit, include the dominant one as `level: high` and others at `level: medium` or `level: low`. For each: 88 88 - `facet`: The facet ID slug — MUST be one of the configured facets listed in the input 89 89 - `activity`: 1-sentence description of what was observed for this facet 90 90 - `level`: "high" (primary focus), "medium" (significant), "low" (brief/peripheral) 91 91 92 - **Facet assignment rules:** Do not invent facet IDs that are not in the configured journal facet list. Empty array is acceptable when no configured facet matches. 92 + **Facet assignment rules:** Do not invent facet IDs that are not in the configured journal facet list. The array always has at least one entry — pick the closest configured facet even when the match is loose, and use `level: low` to signal weak fit. 93 93 94 94 ### meeting_detected 95 95 `true` ONLY if you can identify distinct, named participants in a live multi-person interaction: ··· 128 128 129 129 1. Every field is required. Never omit a field. 130 130 2. `entities` and `speakers` may be empty arrays `[]`. 131 - 3. `facets` may be empty array `[]` if no configured facets match. 131 + 3. `facets` always has at least one entry — the closest configured facet for the activity. Empty array is not allowed. 132 132 4. Be precise with density — misclassifying active segments as idle is the worst error. 133 133 5. For `content_type`, choose the single best match — the dominant activity in the segment. If two activities are roughly equal, pick the one with more durable continuation evidence (entities, repeated screen content); the `facets[]` array's `level` field already encodes secondary activity. 134 134 6. Activity summary must describe observable actions, not inferred states.
+1
talent/sense.schema.json
··· 24 24 }, 25 25 "facets": { 26 26 "type": "array", 27 + "minItems": 1, 27 28 "items": { 28 29 "type": "object", 29 30 "additionalProperties": false,
+77
tests/test_sense_schema.py
··· 93 93 assert hydrated["properties"]["facet"]["enum"] == ["alpha", "valid_one"] 94 94 95 95 96 + def test_sense_schema_facets_array_requires_minItems_one(): 97 + schema = json.loads(SENSE_SCHEMA_PATH.read_text(encoding="utf-8")) 98 + facets_node = schema["properties"]["facets"] 99 + 100 + assert facets_node["type"] == "array" 101 + assert facets_node.get("minItems") == 1 102 + Draft202012Validator.check_schema(schema) 103 + 104 + 105 + def test_hydrate_runtime_enums_preserves_facet_minItems_when_facets_exist( 106 + monkeypatch, 107 + ): 108 + monkeypatch.setattr( 109 + "think.talent.get_facets", 110 + lambda: {"alpha": {}, "Beta": {}, "weird,name": {}, "valid_one": {}}, 111 + ) 112 + schema = { 113 + "type": "object", 114 + "properties": { 115 + "facets": { 116 + "type": "array", 117 + "minItems": 1, 118 + "items": { 119 + "type": "object", 120 + "properties": { 121 + "facet": { 122 + "type": "string", 123 + "enum": [RUNTIME_FACETS_SENTINEL], 124 + } 125 + }, 126 + }, 127 + } 128 + }, 129 + } 130 + 131 + hydrated = hydrate_runtime_enums(schema) 132 + facets_node = hydrated["properties"]["facets"] 133 + facet_schema = facets_node["items"]["properties"]["facet"] 134 + 135 + assert facets_node["minItems"] == 1 136 + assert facet_schema["enum"] == ["alpha", "valid_one"] 137 + Draft202012Validator.check_schema(hydrated) 138 + 139 + 96 140 def test_hydrate_runtime_enums_empty_facets_fallback(monkeypatch): 97 141 monkeypatch.setattr("think.talent.get_facets", lambda: {}) 98 142 schema = { ··· 104 148 facet_schema = hydrated["properties"]["facet"] 105 149 106 150 assert facet_schema == {"type": "string", "minLength": 1} 151 + Draft202012Validator.check_schema(hydrated) 152 + 153 + 154 + def test_hydrate_runtime_enums_drops_facet_minItems_on_empty_facets_fallback( 155 + monkeypatch, 156 + ): 157 + monkeypatch.setattr("think.talent.get_facets", lambda: {}) 158 + schema = { 159 + "type": "object", 160 + "properties": { 161 + "facets": { 162 + "type": "array", 163 + "minItems": 1, 164 + "items": { 165 + "type": "object", 166 + "properties": { 167 + "facet": { 168 + "type": "string", 169 + "enum": [RUNTIME_FACETS_SENTINEL], 170 + } 171 + }, 172 + }, 173 + } 174 + }, 175 + } 176 + 177 + hydrated = hydrate_runtime_enums(schema) 178 + facets_node = hydrated["properties"]["facets"] 179 + facet_schema = facets_node["items"]["properties"]["facet"] 180 + 181 + assert "minItems" not in facets_node 182 + assert facet_schema["minLength"] == 1 183 + assert "enum" not in facet_schema 107 184 Draft202012Validator.check_schema(hydrated) 108 185 109 186
+4
think/talent.py
··· 460 460 replaces it with the sorted list of valid runtime facet slugs. If no 461 461 valid facets exist, drops the `enum` key and sets `minLength: 1` on 462 462 that node so the schema remains satisfiable. 463 + Also removes the parent facets array `minItems` constraint in that case. 463 464 464 465 Returns None when given None. Deep-copies non-None input. Idempotent 465 466 for already-hydrated schemas (sentinel is gone after first call). ··· 490 491 _walk(hydrated) 491 492 492 493 if used_empty_fallback: 494 + facets_node = hydrated.get("properties", {}).get("facets") 495 + if isinstance(facets_node, dict): 496 + facets_node.pop("minItems", None) 493 497 LOG.info( 494 498 "hydrate_runtime_enums: no valid runtime facets; using minLength fallback" 495 499 )