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.responses import HTMLResponse, RedirectResponse
|
||||||
from fastapi.templating import Jinja2Templates
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from chat.eventlog.log import append_event
|
from chat.eventlog.log import append_and_apply
|
||||||
from chat.eventlog.projector import project
|
|
||||||
from chat.llm.client import LLMClient
|
from chat.llm.client import LLMClient
|
||||||
from chat.services.kickoff import parse_kickoff
|
from chat.services.kickoff import parse_kickoff
|
||||||
from chat.state.entities import get_bot, get_you
|
from chat.state.entities import get_bot, get_you
|
||||||
@@ -263,8 +262,14 @@ async def kickoff_post(
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
container_id = next_container_row[0]
|
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
|
# 1. chat_created
|
||||||
append_event(
|
append_and_apply(
|
||||||
conn,
|
conn,
|
||||||
kind="chat_created",
|
kind="chat_created",
|
||||||
payload={
|
payload={
|
||||||
@@ -277,7 +282,7 @@ async def kickoff_post(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 2. container_created
|
# 2. container_created
|
||||||
append_event(
|
append_and_apply(
|
||||||
conn,
|
conn,
|
||||||
kind="container_created",
|
kind="container_created",
|
||||||
payload={
|
payload={
|
||||||
@@ -293,7 +298,7 @@ async def kickoff_post(
|
|||||||
bot_interruptible = bool(bot_activity_action_interruptible)
|
bot_interruptible = bool(bot_activity_action_interruptible)
|
||||||
|
|
||||||
# 3. activity_change for "you"
|
# 3. activity_change for "you"
|
||||||
append_event(
|
append_and_apply(
|
||||||
conn,
|
conn,
|
||||||
kind="activity_change",
|
kind="activity_change",
|
||||||
payload={
|
payload={
|
||||||
@@ -314,7 +319,7 @@ async def kickoff_post(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 4. activity_change for bot
|
# 4. activity_change for bot
|
||||||
append_event(
|
append_and_apply(
|
||||||
conn,
|
conn,
|
||||||
kind="activity_change",
|
kind="activity_change",
|
||||||
payload={
|
payload={
|
||||||
@@ -335,7 +340,7 @@ async def kickoff_post(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 5. scene_opened
|
# 5. scene_opened
|
||||||
append_event(
|
append_and_apply(
|
||||||
conn,
|
conn,
|
||||||
kind="scene_opened",
|
kind="scene_opened",
|
||||||
payload={
|
payload={
|
||||||
@@ -352,7 +357,7 @@ async def kickoff_post(
|
|||||||
facts = _parse_facts(edge_seed_knowledge_facts)
|
facts = _parse_facts(edge_seed_knowledge_facts)
|
||||||
if edge_seed_summary.strip():
|
if edge_seed_summary.strip():
|
||||||
facts.insert(0, f"[summary] {edge_seed_summary.strip()}")
|
facts.insert(0, f"[summary] {edge_seed_summary.strip()}")
|
||||||
append_event(
|
append_and_apply(
|
||||||
conn,
|
conn,
|
||||||
kind="edge_update",
|
kind="edge_update",
|
||||||
payload={
|
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)
|
return RedirectResponse(url=f"/chats/{chat_id}", status_code=303)
|
||||||
|
|||||||
Reference in New Issue
Block a user