personal memory agent
0
fork

Configure Feed

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

convey/chat: count redispatch hops through skip-through events

The previous back-walker broke early when chat_error or any other non-terminal event sat between counted sol_messages, defeating the loop cap. Rewrite as a single reverse pass that pairs each requested-target sol_message with the nearest earlier talent terminal event while skipping bookkeeping events.

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

+144 -10
+19 -10
convey/chat.py
··· 867 867 868 868 869 869 def _talent_loop_count_locked() -> int: 870 + """Count trailing redispatch hops for the current owner turn. 871 + 872 + Each hop is a requested-target sol_message paired with the nearest earlier 873 + talent_finished or talent_errored event. Bookkeeping events between them do 874 + not break the chain or satisfy a pending hop. 875 + """ 870 876 events = read_chat_events(_today_day()) 871 877 count = 0 872 - for index in range(len(events) - 1, -1, -1): 873 - event = events[index] 878 + pending_redispatch = False 879 + 880 + for event in reversed(events): 874 881 kind = event.get("kind") 875 882 if kind == "owner_message": 876 883 break 877 - if kind != "sol_message": 884 + if kind == "sol_message": 885 + if not event.get("requested_target"): 886 + continue 887 + if pending_redispatch: 888 + break 889 + pending_redispatch = True 878 890 continue 879 - if not event.get("requested_target"): 891 + if kind in {"talent_finished", "talent_errored"}: 892 + if pending_redispatch: 893 + count += 1 894 + pending_redispatch = False 880 895 continue 881 - 882 - previous = events[index - 1] if index > 0 else None 883 - if previous and previous.get("kind") in {"talent_finished", "talent_errored"}: 884 - count += 1 885 - else: 886 - break 887 896 return count 888 897 889 898
+125
tests/test_chat_runtime.py
··· 235 235 assert errors[-1]["reason"] == "chat had trouble — try again" 236 236 237 237 238 + def test_talent_loop_count_skips_chat_error_between_retry_hops(tmp_path, monkeypatch): 239 + import convey.chat as chat 240 + 241 + _setup_journal(tmp_path, monkeypatch) 242 + _reset_chat_state(chat) 243 + 244 + append_chat_event( 245 + "owner_message", 246 + text="dig deeper", 247 + app="sol", 248 + path="/app/sol", 249 + facet="work", 250 + ) 251 + append_chat_event( 252 + "sol_message", 253 + use_id="1713622100000", 254 + text="follow up 0", 255 + notes="retrying", 256 + requested_target="exec", 257 + requested_task="task 0", 258 + ) 259 + append_chat_event( 260 + "talent_finished", 261 + use_id="1713622100001", 262 + name="exec", 263 + summary="summary 0", 264 + ) 265 + append_chat_event( 266 + "chat_error", 267 + use_id="1713622100000", 268 + reason="transient trouble", 269 + ) 270 + append_chat_event( 271 + "sol_message", 272 + use_id="1713622100000", 273 + text="follow up 1", 274 + notes="retrying", 275 + requested_target="exec", 276 + requested_task="task 1", 277 + ) 278 + append_chat_event( 279 + "talent_finished", 280 + use_id="1713622100002", 281 + name="exec", 282 + summary="summary 1", 283 + ) 284 + append_chat_event( 285 + "sol_message", 286 + use_id="1713622100000", 287 + text="follow up 2", 288 + notes="retrying", 289 + requested_target="exec", 290 + requested_task="task 2", 291 + ) 292 + 293 + with chat._state_lock: 294 + assert chat._talent_loop_count_locked() == 2 295 + 296 + 297 + def test_talent_loop_count_counts_through_talent_errored_and_reflection_ready( 298 + tmp_path, monkeypatch 299 + ): 300 + import convey.chat as chat 301 + 302 + _setup_journal(tmp_path, monkeypatch) 303 + _reset_chat_state(chat) 304 + 305 + append_chat_event( 306 + "owner_message", 307 + text="dig deeper", 308 + app="sol", 309 + path="/app/sol", 310 + facet="work", 311 + ) 312 + append_chat_event( 313 + "sol_message", 314 + use_id="1713622200000", 315 + text="follow up 0", 316 + notes="retrying", 317 + requested_target="exec", 318 + requested_task="task 0", 319 + ) 320 + append_chat_event( 321 + "talent_errored", 322 + use_id="1713622200001", 323 + name="exec", 324 + reason="needs clarification", 325 + ) 326 + append_chat_event( 327 + "reflection_ready", 328 + day=chat._today_day(), 329 + url="/app/chat/today", 330 + ) 331 + append_chat_event( 332 + "sol_message", 333 + use_id="1713622200000", 334 + text="follow up 1", 335 + notes="retrying", 336 + requested_target="exec", 337 + requested_task="task 1", 338 + ) 339 + append_chat_event( 340 + "talent_finished", 341 + use_id="1713622200002", 342 + name="exec", 343 + summary="summary 1", 344 + ) 345 + append_chat_event( 346 + "reflection_ready", 347 + day=chat._today_day(), 348 + url="/app/chat/today#latest", 349 + ) 350 + append_chat_event( 351 + "sol_message", 352 + use_id="1713622200000", 353 + text="follow up 2", 354 + notes="retrying", 355 + requested_target="exec", 356 + requested_task="task 2", 357 + ) 358 + 359 + with chat._state_lock: 360 + assert chat._talent_loop_count_locked() == 2 361 + 362 + 238 363 def test_cortex_finish_and_error_append_exec_terminal_events_by_use_id( 239 364 tmp_path, monkeypatch 240 365 ):