Fix re-summarize running one voice behind: commit the job row before poking the inline queue.

The reprocess endpoint poked transport_enqueue while its request session
was still uncommitted; the inline worker woke instantly, read the job row
with its own session, and summarized with the PREVIOUS click's tone
(TRACE: click Funny -> worker read 'dry, witty'; click Neutral -> ran
'funny'). The UI disabled the voice chips for the whole wrong-voice run,
which read as 'spinner does nothing until I leave and come back'.

Rows are the real queue: commit before the poke. Regression test pins
worker tone to the requested tone and Neutral clearing a prior voice;
verified red without the fix, green with it.
This commit is contained in:
avi 2026-09-18 17:03:37 -05:00
commit 11bb13cb3d
3 changed files with 54 additions and 0 deletions

View file

@ -441,6 +441,14 @@ async def reprocess_recording(
rec.processing_status = ProcessingStatus.processing
rec.processing_error = None
await session.flush()
# Commit BEFORE poking the queue: the inline worker wakes the moment
# the item lands in the asyncio queue and reads the job row with its
# OWN session, while this request session only commits after the
# response completes. Without this the worker summarizes with the
# PREVIOUS click's tone (TRACE showed every click running one voice
# behind: click Funny -> worker read 'dry, witty'). The rows are the
# real queue; they must be durable before the poke.
await session.commit()
await proc.transport_enqueue(job_type, rec.id)
rows = await session.scalars(
select(ProcessingJob)

View file

@ -94,10 +94,12 @@ class FakeLlm:
def __init__(self, fail=None):
self.fail = fail
self.seen = []
self.seen_tones = []
async def summarize(self, transcript, *, title=None, tone=None,
on_progress=None):
self.seen.append(transcript)
self.seen_tones.append(tone)
if self.fail is not None:
raise self.fail
return SummaryResult(

View file

@ -132,6 +132,50 @@ async def test_reprocess_reruns_in_place_with_model(client, monkeypatch):
await inline_queue.stop()
async def test_reprocess_summarize_uses_requested_tone(client, monkeypatch):
"""Regression: the inline worker used to read the job row BEFORE the
reprocess request committed, so every re-summarize ran with the
PREVIOUS click's tone (click Funny -> 'dry, witty' summary; click
Neutral -> 'funny' summary). The endpoint now commits before poking
the queue; this pins the worker's tone to the tone in the request."""
llm = FakeLlm()
use_fakes(monkeypatch, FakeTranscriber(), llm)
use_inline(monkeypatch)
await inline_queue.start()
try:
token = await user_tokens(client, email="reproc-tone@shonar.dev")
h = {"Authorization": f"Bearer {token}"}
rec = await upload_recording(client, token, client_id="reproc-tone-1")
await _wait_terminal(rec["id"], want=2)
# Re-summarize asking for a NEW voice.
r = await client.post(
f"/api/v1/recordings/{rec['id']}/reprocess"
"?job=summarize&tone=sarcastic",
headers=h,
)
assert r.status_code == 200, r.text
await _wait_terminal(rec["id"], want=2)
# The LLM call for this job must have carried 'sarcastic', not
# whatever the first (auto) pass used.
assert llm.seen_tones[-1] == "sarcastic", llm.seen_tones
s = await client.get(f"/api/v1/recordings/{rec['id']}/summary", headers=h)
assert s.json()["tone"] == "sarcastic"
# And Neutral (tone=None) must CLEAR the voice, not inherit it.
r = await client.post(
f"/api/v1/recordings/{rec['id']}/reprocess?job=summarize",
headers=h,
)
assert r.status_code == 200, r.text
await _wait_terminal(rec["id"], want=2)
assert llm.seen_tones[-1] is None, llm.seen_tones
s = await client.get(f"/api/v1/recordings/{rec['id']}/summary", headers=h)
assert s.json()["tone"] is None
finally:
await inline_queue.stop()
async def test_reprocess_validation(client, monkeypatch):
token = await user_tokens(client, email="reproc2@shonar.dev")
h = {"Authorization": f"Bearer {token}"}