personal memory agent
0
fork

Configure Feed

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

convey/chat: retry unresolved triggers on hydrate

Remove the one-shot recovery latch and rerun recovery from the session endpoint when chat is idle. Add the hydrate retry tests and update the shared chat runtime reset helper for the removed state.

+59 -7
+2 -5
convey/chat.py
··· 48 48 _queued_trigger: dict[str, Any] | None = None 49 49 _active_talents: dict[str, dict[str, Any]] = {} 50 50 _last_use_id = 0 51 - _recovery_day: str | None = None 52 51 _runtime: "ChatRuntimeState | None" = None 53 52 _atexit_registered = False 54 53 ··· 109 108 @chat_bp.route("/session", methods=["GET"]) 110 109 def chat_session() -> Any: 111 110 """Return reduced state for today's chat stream.""" 111 + _recover_chat_if_needed() 112 112 return jsonify(reduce_chat_state(_today_day())) 113 113 114 114 ··· 549 549 start_info: dict[str, Any] | None = None 550 550 551 551 with _state_lock: 552 - global _recovery_day 553 - if _recovery_day == day or _current_chat_use_id is not None: 552 + if _current_chat_use_id is not None: 554 553 return 555 554 unresolved = find_unresponded_trigger(day) 556 555 if unresolved is None: 557 - _recovery_day = day 558 556 return 559 557 location = _location_for_trigger(day, unresolved) 560 558 logical_use_id = _reserve_use_id_locked() 561 559 trigger = _trigger_from_stream_event(unresolved) 562 560 start_info = _activate_current_locked(logical_use_id, trigger, location) 563 - _recovery_day = day 564 561 565 562 if start_info is not None and not _spawn_chat_generate(start_info): 566 563 _handle_chat_failure(start_info["logical_use_id"], CHAT_TROUBLE_REASON)
-1
tests/test_chat_runtime.py
··· 16 16 chat_module._current_chat_state = None 17 17 chat_module._queued_trigger = None 18 18 chat_module._active_talents.clear() 19 - chat_module._recovery_day = None 20 19 chat_module._last_use_id = 0 21 20 22 21
+57 -1
tests/test_convey_chat.py
··· 27 27 chat_module._current_chat_state = None 28 28 chat_module._queued_trigger = None 29 29 chat_module._active_talents.clear() 30 - chat_module._recovery_day = None 31 30 chat_module._last_use_id = 0 32 31 33 32 ··· 131 130 } 132 131 ] 133 132 assert chat_client.get(f"/api/chat/stream/{day}").status_code == 200 133 + 134 + 135 + def test_chat_session_retries_unresolved_trigger_when_idle(chat_client, monkeypatch): 136 + day = "20260420" 137 + monkeypatch.setattr("convey.chat._today_day", lambda: day) 138 + append_chat_event( 139 + "owner_message", 140 + ts=_ms(2026, 4, 20, 12, 0, 0), 141 + text="retry me", 142 + app="sol", 143 + path="/app/sol", 144 + facet="work", 145 + ) 146 + 147 + starts: list[dict] = [] 148 + monkeypatch.setattr( 149 + "convey.chat._spawn_chat_generate", lambda action: starts.append(action) or True 150 + ) 151 + 152 + response = chat_client.get("/api/chat/session") 153 + 154 + assert response.status_code == 200 155 + assert len(starts) == 1 156 + assert starts[0]["trigger"]["type"] == "owner_message" 157 + 158 + 159 + def test_chat_session_retries_again_when_spawn_fails_and_trigger_remains_unresolved( 160 + chat_client, monkeypatch 161 + ): 162 + day = "20260420" 163 + monkeypatch.setattr("convey.chat._today_day", lambda: day) 164 + append_chat_event( 165 + "owner_message", 166 + ts=_ms(2026, 4, 20, 12, 0, 0), 167 + text="retry me again", 168 + app="sol", 169 + path="/app/sol", 170 + facet="work", 171 + ) 172 + 173 + starts: list[dict] = [] 174 + 175 + def fake_spawn(action): 176 + starts.append(action) 177 + return len(starts) > 1 178 + 179 + monkeypatch.setattr("convey.chat._spawn_chat_generate", fake_spawn) 180 + monkeypatch.setattr("convey.chat._emit_error", lambda *_args, **_kwargs: None) 181 + 182 + first = chat_client.get("/api/chat/session") 183 + second = chat_client.get("/api/chat/session") 184 + 185 + assert first.status_code == 200 186 + assert second.status_code == 200 187 + assert len(starts) == 2 188 + assert starts[0]["trigger"]["type"] == "owner_message" 189 + assert starts[1]["trigger"]["type"] == "owner_message" 134 190 135 191 136 192 def test_stream_endpoint_ordered_with_limit(chat_client):