personal memory agent
0

Configure Feed

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

fix(chat): honest deterministic closer when an outbound support run fails

When the chat brain dispatches the outbound support talent and that run ends
in a cogitate runtime failure (a talent_errored whose reason_code is one of
DETERMINISTIC_FAILURE_REASON_CODES), the run never completed and nothing could
have been sent. The closer now states that plainly — "nothing was sent" to
solstone support — and offers to try again, instead of falling through to the
read-framed generic "couldn't finish that lookup … rephrase the question?"
copy that brushed the support trust rule.

Selection is deterministic: (talent ∈ OUTBOUND_TALENTS) × (reason_code ∈
DETERMINISTIC_FAILURE_REASON_CODES), never model output. The terminating
talent name and its reason_code are threaded from _on_cortex_error through
_handle_talent_terminal_locked / _talent_terminal_trigger, persisted on the
talent_errored chat event, and reconstructed in _trigger_from_stream_event so
a process restart yields the same honest closer. Spawn-failure and
watchdog-timeout talent errors carry no reason_code and keep the generic
closer; loop_exhausted and all non-support behavior are unchanged.

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

+180 -2
+3
solstone/apps/chat/copy.py
··· 29 29 CHAT_CLOSER_DIFFERENT_ANGLE_SUFFIX = "Want me to try a different angle?" 30 30 CHAT_CLOSER_TALENT_ERRORED_FORMAT = "I couldn't finish that lookup — {reason}. Want to try a different angle, or rephrase the question?" 31 31 CHAT_CLOSER_TALENT_ERRORED_GENERIC = "I couldn't finish that lookup. Want to try a different angle, or rephrase the question?" 32 + # Deterministic support-send-failure closer (backend-selected on an outbound 33 + # talent_errored carrying a runtime-failure reason_code). Brand: "solstone support". 34 + CHAT_CLOSER_SUPPORT_SEND_FAILED = "I couldn't finish reaching solstone support, so nothing was sent. Want me to try again?" 32 35 33 36 # T2.4 — thinking summary surfaces (CPO LOCKED) 34 37 CHAT_THINKING_EXPANDER_LABEL = "Show thinking"
+24 -1
solstone/convey/chat.py
··· 25 25 from solstone.apps.chat.copy import ( 26 26 CHAT_CLOSER_DIFFERENT_ANGLE_SUFFIX, 27 27 CHAT_CLOSER_LOOP_EXHAUSTED_PREFIX, 28 + CHAT_CLOSER_SUPPORT_SEND_FAILED, 28 29 CHAT_CLOSER_TALENT_ERRORED_FORMAT, 29 30 CHAT_CLOSER_TALENT_ERRORED_GENERIC, 30 31 CHAT_OFFER_SUPPORT_DECLINE, ··· 51 52 from solstone.convey.sol_initiated.copy import KIND_SOL_CHAT_REQUEST, SURFACE_CONVEY 52 53 from solstone.convey.utils import error_response 53 54 from solstone.think.callosum import CallosumConnection, callosum_send 55 + from solstone.think.cogitate_policy import DETERMINISTIC_FAILURE_REASON_CODES 54 56 from solstone.think.cortex_client import CortexSpawnUnavailable 55 57 from solstone.think.utils import get_journal, now_ms 56 58 ··· 470 472 message_text = _compose_terminal_closer( 471 473 exit_mode, 472 474 message_text, 475 + talent_name=trigger.get("name"), 473 476 talent_errored_reason=trigger.get("reason"), 477 + talent_errored_reason_code=trigger.get("reason_code"), 474 478 talent_finished_summary=trigger.get("summary"), 475 479 ) 476 480 requested_target = None ··· 630 634 next_info = _clear_current_locked() 631 635 elif use_id in _active_talents: 632 636 reason = str(message.get("error") or "unknown") 637 + reason_code = message.get("reason_code") or None 633 638 _evict_thinking_locked(use_id) 634 639 next_info = _handle_talent_terminal_locked( 635 640 use_id, 636 641 "talent_errored", 637 642 "reason", 638 643 reason, 644 + reason_code=reason_code, 639 645 ) 640 646 elif _is_superseded_raw_use_id_locked(use_id): 641 647 logger.debug( ··· 669 675 result_field_name: str, 670 676 result_value: str, 671 677 *, 678 + reason_code: str | None = None, 672 679 terminal_message: dict[str, Any] | None = None, 673 680 ) -> dict[str, Any] | None: 674 681 _cancel_watchdog_locked(use_id) ··· 681 688 talent_name, 682 689 result_field_name, 683 690 result_value, 691 + reason_code=reason_code, 684 692 ) 685 693 event_fields: dict[str, Any] = { 686 694 "use_id": use_id, 687 695 "name": talent_name, 688 696 result_field_name: result_value, 689 697 } 698 + if reason_code: 699 + event_fields["reason_code"] = reason_code 690 700 if kind == "talent_finished" and terminal_message is not None: 691 701 thinking = _drain_thinking_locked(use_id, terminal_message) 692 702 if thinking is not None: ··· 1373 1383 exit_mode: str, 1374 1384 raw_message: str | None, 1375 1385 *, 1386 + talent_name: str | None = None, 1376 1387 talent_errored_reason: str | None = None, 1388 + talent_errored_reason_code: str | None = None, 1377 1389 talent_finished_summary: str | None = None, 1378 1390 ) -> str: 1379 1391 if exit_mode == "loop_exhausted": ··· 1391 1403 ) 1392 1404 1393 1405 if exit_mode == "talent_errored": 1406 + if ( 1407 + talent_name in OUTBOUND_TALENTS 1408 + and talent_errored_reason_code in DETERMINISTIC_FAILURE_REASON_CODES 1409 + ): 1410 + return CHAT_CLOSER_SUPPORT_SEND_FAILED 1394 1411 reason = _clean_talent_errored_reason(talent_errored_reason) 1395 1412 if reason: 1396 1413 return CHAT_CLOSER_TALENT_ERRORED_FORMAT.format(reason=reason) ··· 1601 1618 event.get("name", "exec"), 1602 1619 "reason", 1603 1620 event.get("reason", ""), 1621 + reason_code=event.get("reason_code"), 1604 1622 ) 1605 1623 raise ValueError(f"unsupported trigger event: {kind}") 1606 1624 ··· 1611 1629 name: Any, 1612 1630 result_field_name: str, 1613 1631 result_value: Any, 1632 + *, 1633 + reason_code: str | None = None, 1614 1634 ) -> dict[str, Any]: 1615 - return { 1635 + trigger = { 1616 1636 "type": kind, 1617 1637 "use_id": use_id, 1618 1638 "name": name, 1619 1639 result_field_name: result_value, 1620 1640 } 1641 + if reason_code: 1642 + trigger["reason_code"] = reason_code 1643 + return trigger 1621 1644 1622 1645 1623 1646 def _read_talent_log(use_id: str) -> dict[str, Any] | None:
+1
solstone/convey/static/chat_copy.js
··· 46 46 CHAT_CLOSER_DIFFERENT_ANGLE_SUFFIX: "Want me to try a different angle?", 47 47 CHAT_CLOSER_TALENT_ERRORED_FORMAT: "I couldn't finish that lookup — {reason}. Want to try a different angle, or rephrase the question?", 48 48 CHAT_CLOSER_TALENT_ERRORED_GENERIC: "I couldn't finish that lookup. Want to try a different angle, or rephrase the question?", 49 + CHAT_CLOSER_SUPPORT_SEND_FAILED: "I couldn't finish reaching solstone support, so nothing was sent. Want me to try again?", 49 50 CHAT_THINKING_EXPANDER_LABEL: "Show thinking", 50 51 CHAT_THINKING_COLLAPSER_LABEL: "Hide thinking", 51 52 CHAT_ERROR_DETAIL_EXPANDER_LABEL: "Show details",
+52
tests/test_chat_closer.py
··· 3 3 4 4 import logging 5 5 6 + from solstone.apps.chat.copy import CHAT_CLOSER_SUPPORT_SEND_FAILED 6 7 from solstone.convey import chat 8 + from solstone.think.cogitate_policy import DETERMINISTIC_FAILURE_REASON_CODES 7 9 8 10 CHAT_LOGGER = "solstone.convey.chat" 9 11 FIXTURE_OPENERS = ( ··· 175 177 ) 176 178 == "I couldn't finish that lookup. Want to try a different angle, or rephrase the question?" 177 179 ) 180 + 181 + 182 + def test_support_talent_errored_send_failed_closer(): 183 + model_text = ( 184 + "<model text falsely claiming a ticket was drafted — file it via the live chat>" 185 + ) 186 + for reason_code in DETERMINISTIC_FAILURE_REASON_CODES: 187 + assert ( 188 + chat._compose_terminal_closer( 189 + "talent_errored", 190 + model_text, 191 + talent_name="support", 192 + talent_errored_reason="Traceback (most recent call last)", 193 + talent_errored_reason_code=reason_code, 194 + ) 195 + == CHAT_CLOSER_SUPPORT_SEND_FAILED 196 + ) 197 + 198 + support_unknown = chat._compose_terminal_closer( 199 + "talent_errored", 200 + model_text, 201 + talent_name="support", 202 + talent_errored_reason="provider gave up", 203 + talent_errored_reason_code=None, 204 + ) 205 + assert support_unknown != CHAT_CLOSER_SUPPORT_SEND_FAILED 206 + assert ( 207 + support_unknown 208 + == "I couldn't finish that lookup — provider gave up. Want to try a different angle, or rephrase the question?" 209 + ) 210 + 211 + support_unrecognized = chat._compose_terminal_closer( 212 + "talent_errored", 213 + model_text, 214 + talent_name="support", 215 + talent_errored_reason="provider gave up", 216 + talent_errored_reason_code="some_other_code", 217 + ) 218 + assert support_unrecognized != CHAT_CLOSER_SUPPORT_SEND_FAILED 219 + assert support_unrecognized == support_unknown 220 + 221 + non_support = chat._compose_terminal_closer( 222 + "talent_errored", 223 + model_text, 224 + talent_name="exec", 225 + talent_errored_reason="provider gave up", 226 + talent_errored_reason_code="wall_clock_exceeded", 227 + ) 228 + assert non_support != CHAT_CLOSER_SUPPORT_SEND_FAILED 229 + assert non_support == support_unknown 178 230 179 231 180 232 def test_loop_exhausted_empty_empty_fallback():
+6
tests/test_chat_copy.py
··· 162 162 "CHAT_CLOSER_DIFFERENT_ANGLE_SUFFIX": "Want me to try a different angle?", 163 163 "CHAT_CLOSER_TALENT_ERRORED_FORMAT": "I couldn't finish that lookup — {reason}. Want to try a different angle, or rephrase the question?", 164 164 "CHAT_CLOSER_TALENT_ERRORED_GENERIC": "I couldn't finish that lookup. Want to try a different angle, or rephrase the question?", 165 + "CHAT_CLOSER_SUPPORT_SEND_FAILED": "I couldn't finish reaching solstone support, so nothing was sent. Want me to try again?", 165 166 } 166 167 167 168 for name, literal in expected.items(): ··· 169 170 assert literal in text 170 171 171 172 assert "\u2014" in chat_copy.CHAT_CLOSER_TALENT_ERRORED_FORMAT 173 + assert "solstone support" in chat_copy.CHAT_CLOSER_SUPPORT_SEND_FAILED 174 + assert "sol pbc" not in chat_copy.CHAT_CLOSER_SUPPORT_SEND_FAILED 175 + assert "live chat" not in chat_copy.CHAT_CLOSER_SUPPORT_SEND_FAILED 176 + assert "lookup" not in chat_copy.CHAT_CLOSER_SUPPORT_SEND_FAILED 177 + assert "try again" in chat_copy.CHAT_CLOSER_SUPPORT_SEND_FAILED.lower() 172 178 173 179 174 180 def test_chat_placeholder_css_present():
+94 -1
tests/test_chat_runtime.py
··· 10 10 import pytest 11 11 from flask import Flask 12 12 13 + from solstone.apps.chat.copy import CHAT_CLOSER_SUPPORT_SEND_FAILED 13 14 from solstone.convey.chat_stream import append_chat_event, read_chat_events 14 15 15 16 ··· 311 312 assert chat._current_chat_use_id is None 312 313 313 314 315 + def test_post_support_talent_errored_request_uses_send_failed_closer( 316 + tmp_path, monkeypatch 317 + ): 318 + import solstone.convey.chat as chat 319 + 320 + _setup_journal(tmp_path, monkeypatch) 321 + _reset_chat_state(chat) 322 + 323 + actions: list[dict | None] = [] 324 + finishes: list[tuple[str, str]] = [] 325 + monkeypatch.setattr( 326 + "solstone.convey.chat._run_next_action", lambda action: actions.append(action) 327 + ) 328 + monkeypatch.setattr( 329 + "solstone.convey.chat._emit_finish", 330 + lambda use_id, message: finishes.append((use_id, message)), 331 + ) 332 + monkeypatch.setattr( 333 + "solstone.convey.chat._emit_error", 334 + lambda *args, **kwargs: None, 335 + ) 336 + 337 + with chat._state_lock: 338 + chat._current_chat_use_id = "1713622150000" 339 + chat._current_chat_state = { 340 + "raw_use_id": "1713622150001", 341 + "raw_use_ids_seen": {"1713622150001"}, 342 + "trigger": { 343 + "type": "talent_errored", 344 + "name": "support", 345 + "reason": "Traceback (most recent call last)", 346 + "reason_code": "wall_clock_exceeded", 347 + }, 348 + "location": {"app": "sol", "path": "/app/sol", "facet": "work"}, 349 + "retry_count": 0, 350 + } 351 + 352 + chat._on_cortex_finish( 353 + { 354 + "use_id": "1713622150001", 355 + "result": json.dumps( 356 + { 357 + "message": "I drafted a ticket and will file it via live chat.", 358 + "notes": "blocked redispatch", 359 + "talent_request": { 360 + "target": "exec", 361 + "task": "one more pass", 362 + "context": json.dumps({}), 363 + }, 364 + } 365 + ), 366 + } 367 + ) 368 + 369 + assert actions == [None] 370 + events = read_chat_events(chat._today_day()) 371 + sol_messages = [event for event in events if event["kind"] == "sol_message"] 372 + assert len(sol_messages) == 1 373 + assert sol_messages[-1]["text"] == CHAT_CLOSER_SUPPORT_SEND_FAILED 374 + assert sol_messages[-1]["requested_target"] is None 375 + assert sol_messages[-1]["requested_task"] is None 376 + assert finishes == [("1713622150000", CHAT_CLOSER_SUPPORT_SEND_FAILED)] 377 + with chat._state_lock: 378 + assert chat._current_chat_state is None 379 + assert chat._current_chat_use_id is None 380 + 381 + 314 382 def test_owner_message_direct_reply_keeps_raw_model_text(tmp_path, monkeypatch): 315 383 import solstone.convey.chat as chat 316 384 ··· 536 604 "location": {"app": "sol", "path": "/app/sol", "facet": "work"}, 537 605 } 538 606 539 - chat._on_cortex_error({"use_id": "1713624000001", "error": "boom"}) 607 + chat._on_cortex_error( 608 + { 609 + "use_id": "1713624000001", 610 + "error": "boom", 611 + "reason_code": "wall_clock_exceeded", 612 + } 613 + ) 540 614 errored_events = [ 541 615 e for e in read_chat_events(chat._today_day()) if e["kind"] == "talent_errored" 542 616 ] 543 617 assert errored_events[-1]["use_id"] == "1713624000001" 544 618 assert actions[-1]["trigger"]["type"] == "talent_errored" 545 619 assert actions[-1]["trigger"]["reason"] == "boom" 620 + assert actions[-1]["trigger"]["reason_code"] == "wall_clock_exceeded" 621 + 622 + 623 + def test_talent_errored_trigger_recovers_reason_code(): 624 + import solstone.convey.chat as chat 625 + 626 + trigger = chat._trigger_from_stream_event( 627 + { 628 + "kind": "talent_errored", 629 + "use_id": "1713624500001", 630 + "name": "support", 631 + "reason": "Traceback (most recent call last)", 632 + "reason_code": "wall_clock_exceeded", 633 + } 634 + ) 635 + 636 + assert trigger["type"] == "talent_errored" 637 + assert trigger["name"] == "support" 638 + assert trigger["reason_code"] == "wall_clock_exceeded" 546 639 547 640 548 641 @pytest.mark.parametrize(