feat: lifecycle events carry triggered_by_assistant_turn_id back-reference (T114.1)

Phase 3.5 T83.4 surfaced un-rolled-back lifecycle transitions on
regenerate; T114 wires up the actual rollback. Step 1 is the back-
reference: every event_started / event_completed / event_cancelled
emitted by post_turn (chat/web/turns.py) and regenerate
(chat/services/regenerate.py) now carries
``triggered_by_assistant_turn_id`` in its payload, set to the id of
the assistant_turn event that produced the transition.

Schema decision (Option A from the plan): no migration. The field is
a payload convention only — older event_log rows lack it and rollback
will skip them with a debug log when T114.3 lands. Forward-only.

The post_turn lifecycle block already runs AFTER the assistant_turn
event is appended (step 8a vs step 7), so ``primary_assistant_event_id``
is in scope. Same for regenerate: the lifecycle classification (step 9a)
runs after step 6's append. **No emission-order reorder was needed**
in either flow.

Updates ``test_turn_with_event_transition_appends_started_event`` to
assert the new field is present in the emitted event_started payload
and points at the assistant_turn id.
This commit is contained in:
Joseph Doherty
2026-04-27 06:38:48 -04:00
parent f863cf0158
commit 7370f68bdf
3 changed files with 42 additions and 0 deletions
+14
View File
@@ -738,6 +738,12 @@ async def regenerate_assistant_turn(
payload={
"event_id": transition.event_id,
"started_at": chat.get("time"),
# T114.1: back-reference to the assistant_turn
# that triggered this transition (see turns.py
# for rationale).
"triggered_by_assistant_turn_id": (
new_assistant_event_id
),
},
)
elif transition.new_status == "completed":
@@ -747,6 +753,10 @@ async def regenerate_assistant_turn(
payload={
"event_id": transition.event_id,
"completed_at": chat.get("time"),
# T114.1: back-reference (see above).
"triggered_by_assistant_turn_id": (
new_assistant_event_id
),
},
)
promote_completed_event(
@@ -762,6 +772,10 @@ async def regenerate_assistant_turn(
payload={
"event_id": transition.event_id,
"completed_at": chat.get("time"),
# T114.1: back-reference (see above).
"triggered_by_assistant_turn_id": (
new_assistant_event_id
),
},
)
+16
View File
@@ -812,6 +812,14 @@ async def post_turn(
payload={
"event_id": transition.event_id,
"started_at": chat.get("time"),
# T114.1: back-reference to the assistant_turn that
# triggered this transition. Regenerate uses this
# to roll back lifecycle transitions when the turn
# is superseded. Forward-only — older events
# without this field are skipped by rollback.
"triggered_by_assistant_turn_id": (
primary_assistant_event_id
),
},
)
elif transition.new_status == "completed":
@@ -821,6 +829,10 @@ async def post_turn(
payload={
"event_id": transition.event_id,
"completed_at": chat.get("time"),
# T114.1: back-reference (see above).
"triggered_by_assistant_turn_id": (
primary_assistant_event_id
),
},
)
# Run promotion inline so the artifact-emitting events
@@ -842,6 +854,10 @@ async def post_turn(
payload={
"event_id": transition.event_id,
"completed_at": chat.get("time"),
# T114.1: back-reference (see above).
"triggered_by_assistant_turn_id": (
primary_assistant_event_id
),
},
)
# Any other ``new_status`` value falls through silently —
+12
View File
@@ -1023,6 +1023,18 @@ def test_turn_with_event_transition_appends_started_event(
assert started_payload["event_id"] == "evt_1"
assert started_payload["started_at"] == "2026-04-26T20:00:00+00:00"
# T114.1: payload carries the back-reference to the assistant_turn
# that triggered the transition. The assistant_turn lands in
# event_log immediately before the event_started, so its id is
# the largest assistant_turn id in the chat at this point.
at_id = conn.execute(
"SELECT id FROM event_log "
"WHERE kind = 'assistant_turn' "
" AND json_extract(payload_json, '$.chat_id') = 'chat_bot_a' "
"ORDER BY id DESC LIMIT 1"
).fetchone()[0]
assert started_payload["triggered_by_assistant_turn_id"] == at_id
# The events projection row reflects the active status.
ev_row = conn.execute(
"SELECT status, started_at FROM events WHERE event_id = ?",