fix: kickoff_post uses append_and_apply per-event (no full replay)
The route appended 6 events with append_event() and then called project(conn) to apply them all at once. project() replays the *entire* event log, but _apply_chat_created in chat/state/world.py uses raw INSERT (not INSERT OR REPLACE), so the moment a second bot got kicked off, projecting hit the existing chat_<bot_id> row from the first kickoff and 500'd with sqlite3.IntegrityError: UNIQUE constraint failed: chats.id. Switch to append_and_apply (the live-path pattern in chat/eventlog/log.py) which appends and applies only the new event through its registered handler — leaving prior state untouched. project() / append_event() imports are now unused in this file and removed. Suite: 470 passed in 11.8s. Verified manually: a second bot's kickoff now redirects to /chats/chat_<bot_id> instead of 500ing.
This commit is contained in:
+13
-13
@@ -17,8 +17,7 @@ from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from chat.eventlog.log import append_event
|
||||
from chat.eventlog.projector import project
|
||||
from chat.eventlog.log import append_and_apply
|
||||
from chat.llm.client import LLMClient
|
||||
from chat.services.kickoff import parse_kickoff
|
||||
from chat.state.entities import get_bot, get_you
|
||||
@@ -263,8 +262,14 @@ async def kickoff_post(
|
||||
).fetchone()
|
||||
container_id = next_container_row[0]
|
||||
|
||||
# Use ``append_and_apply`` per event (live-path pattern) rather than
|
||||
# appending all-then-project. ``project()`` replays the *entire*
|
||||
# event log; non-idempotent handlers like ``_apply_chat_created``
|
||||
# (raw INSERT into chats) then 500 with UNIQUE constraint failures
|
||||
# for any chats that already exist from prior kickoffs.
|
||||
|
||||
# 1. chat_created
|
||||
append_event(
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="chat_created",
|
||||
payload={
|
||||
@@ -277,7 +282,7 @@ async def kickoff_post(
|
||||
)
|
||||
|
||||
# 2. container_created
|
||||
append_event(
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="container_created",
|
||||
payload={
|
||||
@@ -293,7 +298,7 @@ async def kickoff_post(
|
||||
bot_interruptible = bool(bot_activity_action_interruptible)
|
||||
|
||||
# 3. activity_change for "you"
|
||||
append_event(
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="activity_change",
|
||||
payload={
|
||||
@@ -314,7 +319,7 @@ async def kickoff_post(
|
||||
)
|
||||
|
||||
# 4. activity_change for bot
|
||||
append_event(
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="activity_change",
|
||||
payload={
|
||||
@@ -335,7 +340,7 @@ async def kickoff_post(
|
||||
)
|
||||
|
||||
# 5. scene_opened
|
||||
append_event(
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="scene_opened",
|
||||
payload={
|
||||
@@ -352,7 +357,7 @@ async def kickoff_post(
|
||||
facts = _parse_facts(edge_seed_knowledge_facts)
|
||||
if edge_seed_summary.strip():
|
||||
facts.insert(0, f"[summary] {edge_seed_summary.strip()}")
|
||||
append_event(
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="edge_update",
|
||||
payload={
|
||||
@@ -363,9 +368,4 @@ async def kickoff_post(
|
||||
},
|
||||
)
|
||||
|
||||
# Project all events at once. ``bot_authored`` (already in log from prior
|
||||
# POST) is idempotent (INSERT OR REPLACE); the new events project cleanly
|
||||
# because they're being applied for the first time.
|
||||
project(conn)
|
||||
|
||||
return RedirectResponse(url=f"/chats/{chat_id}", status_code=303)
|
||||
|
||||
Reference in New Issue
Block a user