feat(runtime): add user-controlled turn recovery

This commit is contained in:
Xubin Ren
2026-08-24 00:58:04 +08:00
parent ffa58aa5ef
commit 12029f8812
60 changed files with 4027 additions and 169 deletions
+58
View File
@@ -36,6 +36,7 @@ from nanobot.session.keys import (
UNIFIED_SESSION_KEY,
)
from nanobot.session.manager import Session
from nanobot.session.recovery import PENDING_FOLLOWUP_ID_KEY, PENDING_FOLLOWUPS_KEY
from nanobot.session.turn_continuation import (
INTERNAL_CONTINUATION_META,
INTERNAL_CONTINUATION_RUN_STARTED_AT_META,
@@ -161,6 +162,35 @@ def test_persist_cron_turn_uses_distinct_history_marker(tmp_path: Path) -> None:
assert message["cron_prompt_ref"] == prompt_ref
def test_persist_user_message_acknowledges_durable_followup(tmp_path: Path) -> None:
loop = _make_full_loop(tmp_path)
session = loop.sessions.get_or_create("websocket:chat")
session.metadata[PENDING_FOLLOWUPS_KEY] = [
{
"id": "followup-1",
"sender_id": "user",
"chat_id": "chat",
"content": "queued while busy",
"media": [],
"metadata": {},
}
]
persisted = loop._persist_user_message_early(
InboundMessage(
channel="websocket",
sender_id="user",
chat_id="chat",
content="queued while busy",
metadata={PENDING_FOLLOWUP_ID_KEY: "followup-1"},
),
session,
)
assert persisted is True
assert PENDING_FOLLOWUPS_KEY not in session.metadata
def test_persist_local_trigger_turn_uses_hidden_automation_marker(tmp_path: Path) -> None:
loop = _make_full_loop(tmp_path)
session = loop.sessions.get_or_create("websocket:auto")
@@ -381,6 +411,34 @@ def test_save_turn_keeps_multimodal_runtime_context_for_model_replay() -> None:
assert public_history_message(session.messages[0])["content"] == []
def test_save_turn_acknowledges_every_merged_recovery_followup() -> None:
"""Persisting a merged injected row retires every durable follow-up ID."""
loop = _mk_loop()
session = Session(
key="test:recovery-followups",
metadata={
PENDING_FOLLOWUPS_KEY: [
{"id": "first"},
{"id": "second"},
]
},
)
loop._save_turn(
session,
[
{
"role": "user",
"content": "first\n\nsecond",
PENDING_FOLLOWUP_ID_KEY: ["first", "second"],
}
],
skip=0,
)
assert PENDING_FOLLOWUPS_KEY not in session.metadata
def test_save_turn_keeps_image_placeholder_and_runtime_context() -> None:
loop = _mk_loop()
session = Session(key="test:image")