fix(lineage): keep tool children separate across readers
This commit is contained in:
1 parent
a41bfe8690
commit
f3545da572
3 files changed
+76
No files matched your search
@@ -278,6 +278,26 @@ visibility stage decides whether recovered background rows are shown. In
|
|||||||
are merged; cross-profile scoping, visibility, deduplication, and final route
|
are merged; cross-profile scoping, visibility, deduplication, and final route
|
||||||
limits remain downstream responsibilities.
|
limits remain downstream responsibilities.
|
||||||
|
|
||||||
|
#### Compression lineage and session-list invalidation
|
||||||
|
|
||||||
|
`api.agent_sessions._is_continuation_session()` is the shared classifier for
|
||||||
|
sidebar projection, lineage metadata/reporting, and `state.db` transcript
|
||||||
|
stitching. It uses the direct parent link, no conflicting non-empty source,
|
||||||
|
`compression` or `cli_close` parent end reason, and the existing two-second
|
||||||
|
`started_at` overlap allowance. A `source="tool"` child is always a separate
|
||||||
|
conversation, even when its parent is also a tool session or has no source.
|
||||||
|
A direct `_branched_from`, `_delegate_from`, or `_reset_from` marker in the
|
||||||
|
child's `model_config` also makes a boundary; inherited ancestor markers do
|
||||||
|
not. Malformed or unverifiable marker evidence fails closed as a boundary.
|
||||||
|
The overlap allowance is the existing master policy, not a new window set by
|
||||||
|
this change.
|
||||||
|
|
||||||
|
The gateway watcher's cheap database fingerprint includes `model_config`, so
|
||||||
|
a marker-only update causes a fresh projection. Its published-payload hash
|
||||||
|
covers every emitted session field, not just ID, activity time and message
|
||||||
|
count; a changed projected title or lineage field can therefore emit
|
||||||
|
`sessions_changed` even without message-row churn.
|
||||||
|
|
||||||
### 4.3 SSE Streaming Engine
|
### 4.3 SSE Streaming Engine
|
||||||
|
|
||||||
This is the most architecturally interesting part. Two endpoints cooperate:
|
This is the most architecturally interesting part. Two endpoints cooperate:
|
||||||
|
|||||||
@@ -462,6 +462,10 @@ def _is_continuation_session(parent: dict | None, child: dict | None) -> bool:
|
|||||||
return False
|
return False
|
||||||
parent_source = str(parent.get('source') or '').strip().lower()
|
parent_source = str(parent.get('source') or '').strip().lower()
|
||||||
child_source = str(child.get('source') or '').strip().lower()
|
child_source = str(child.get('source') or '').strip().lower()
|
||||||
|
# Agent lineage excludes tool children even when the parent has the same
|
||||||
|
# source (or no source); a timestamp overlap cannot override that boundary.
|
||||||
|
if child_source == 'tool':
|
||||||
|
return False
|
||||||
if parent_source and child_source and parent_source != child_source:
|
if parent_source and child_source and parent_source != child_source:
|
||||||
return False
|
return False
|
||||||
if parent.get('end_reason') not in {'compression', 'cli_close'}:
|
if parent.get('end_reason') not in {'compression', 'cli_close'}:
|
||||||
|
|||||||
@@ -981,6 +981,58 @@ def test_continuation_classification_timestamp_tolerance_and_guards():
|
|||||||
assert not _is_continuation_session(make_parent(), make_child(started_at='not-a-number'))
|
assert not _is_continuation_session(make_parent(), make_child(started_at='not-a-number'))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("parent_source", ["tool", ""])
|
||||||
|
def test_tool_child_is_not_stitched_into_parent_lineage(_isolate, parent_source):
|
||||||
|
"""A marker-less tool child stays independent even with a matching/empty source."""
|
||||||
|
from api.agent_sessions import (
|
||||||
|
read_importable_agent_session_rows,
|
||||||
|
read_session_lineage_metadata,
|
||||||
|
read_session_lineage_report,
|
||||||
|
)
|
||||||
|
|
||||||
|
conn = _ensure_state_db(_isolate)
|
||||||
|
_ensure_messages_table(conn)
|
||||||
|
t0 = time.time() - 100
|
||||||
|
parent_id, child_id = "tool_boundary_parent", "tool_boundary_child"
|
||||||
|
try:
|
||||||
|
_insert_state_row(
|
||||||
|
conn, parent_id, source=parent_source, title="Parent conversation",
|
||||||
|
started_at=t0, ended_at=t0 + 5, end_reason="compression",
|
||||||
|
)
|
||||||
|
_insert_state_row(
|
||||||
|
conn, child_id, source="tool", title="Tool conversation",
|
||||||
|
parent=parent_id, started_at=t0 + 4.5,
|
||||||
|
)
|
||||||
|
_insert_state_message(
|
||||||
|
conn, parent_id, role="user", content="parent request", timestamp=t0 + 1,
|
||||||
|
)
|
||||||
|
_insert_state_message(
|
||||||
|
conn, child_id, role="user", content="tool request", timestamp=t0 + 6,
|
||||||
|
)
|
||||||
|
|
||||||
|
projected = read_importable_agent_session_rows(
|
||||||
|
_isolate, limit=None, exclude_sources=None,
|
||||||
|
)
|
||||||
|
rows = {row["id"]: row for row in projected}
|
||||||
|
assert set(rows) == {parent_id, child_id}
|
||||||
|
assert rows[child_id]["relationship_type"] == "child_session"
|
||||||
|
assert "_lineage_root_id" not in rows[child_id]
|
||||||
|
assert "_lineage_tip_id" not in rows[parent_id]
|
||||||
|
|
||||||
|
metadata = read_session_lineage_metadata(_isolate, {parent_id, child_id})
|
||||||
|
assert metadata[child_id]["relationship_type"] == "child_session"
|
||||||
|
assert "_lineage_root_id" not in metadata[child_id]
|
||||||
|
assert read_session_lineage_report(_isolate, parent_id)["total_segments"] == 1
|
||||||
|
assert read_session_lineage_report(_isolate, child_id)["total_segments"] == 1
|
||||||
|
|
||||||
|
messages = models.get_state_db_session_messages(
|
||||||
|
child_id, stitch_continuations=True,
|
||||||
|
)
|
||||||
|
assert [message["content"] for message in messages] == ["tool request"]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def test_state_db_stitch_keeps_branched_child_out_of_parent_transcript(_isolate):
|
def test_state_db_stitch_keeps_branched_child_out_of_parent_transcript(_isolate):
|
||||||
"""#7021 r2: the open/import transcript stitcher (get_state_db_session_messages
|
"""#7021 r2: the open/import transcript stitcher (get_state_db_session_messages
|
||||||
with stitch_continuations=True) must apply the same model_config branch-marker
|
with stitch_continuations=True) must apply the same model_config branch-marker
|
||||||
|
|||||||
Reference in new issue
Block a user