mirror of
https://github.com/andrewyng/openworker.git
synced 2026-08-30 22:53:41 +00:00
Auto-title at turn start, not turn completion
The title rides the user message the moment it lands — a long agentic turn no longer holds the session name hostage; an opener signature keeps the completion hook (background turns) from burning duplicate attempts.
This commit is contained in:
parent
5c8a60e7ee
commit
eae1ed315f
@ -2494,6 +2494,10 @@ def create_app(manager: SessionManager) -> FastAPI:
|
||||
)
|
||||
if event.type.value in _CHECKPOINTS:
|
||||
manager.save(session_id, engine)
|
||||
if event.type.value == "turn_start":
|
||||
# Title on the user's words the moment they land — never behind
|
||||
# a long agentic turn (owner catch 2026-08-24).
|
||||
manager._maybe_autotitle(session_id)
|
||||
finally:
|
||||
manager.mark_idle(session_id)
|
||||
manager.save(session_id, engine)
|
||||
|
||||
@ -223,6 +223,12 @@ class SessionManager:
|
||||
self._autotitle_inflight: set[str] = set()
|
||||
self._autotitle_tasks: set[asyncio.Task] = set()
|
||||
self._autotitle_attempts: dict[str, int] = {}
|
||||
# Opener-count signature of the last attempt: titling fires at TURN START (owner
|
||||
# catch 2026-08-24 — waiting for an agentic turn to COMPLETE left sessions
|
||||
# untitled for however long the scan ran), and the completion hook still covers
|
||||
# background turns; this guard keeps the two trigger points from burning
|
||||
# duplicate attempts on the same openers.
|
||||
self._autotitle_sig: dict[str, int] = {}
|
||||
self.workspace_trust = WorkspaceTrustStore()
|
||||
self.secrets = SecretStore()
|
||||
# No explicit provider injected → route by the model's `provider:` prefix (OpenAI default,
|
||||
@ -5093,6 +5099,11 @@ class SessionManager:
|
||||
][:2]
|
||||
if not openers:
|
||||
return
|
||||
# Same openers as the last attempt → nothing new to say; skip WITHOUT burning an
|
||||
# attempt (this is how the turn-start and turn-end triggers coexist).
|
||||
if self._autotitle_sig.get(session_id) == len(openers):
|
||||
return
|
||||
self._autotitle_sig[session_id] = len(openers)
|
||||
self._autotitle_attempts[session_id] = (
|
||||
self._autotitle_attempts.get(session_id, 0) + 1
|
||||
)
|
||||
|
||||
@ -146,3 +146,27 @@ async def test_sanitizes_and_rejects_absurd_output(tmp_path):
|
||||
mgr2, _ = _mgr(tmp_path / "b", [_text("ok")], ["x" * 90])
|
||||
await _turn(mgr2, "s8", "the login page 500s")
|
||||
assert mgr2.session_store.title_state("s8")["auto_title"] is None
|
||||
|
||||
|
||||
def test_turn_start_titles_before_the_turn_completes(tmp_path):
|
||||
# Owner catch 2026-08-24: titling used to ride ONLY turn completion, so a long
|
||||
# agentic turn left the session titled by its first message for the whole run. The
|
||||
# turn-start hook titles on the user's words the moment they land.
|
||||
async def go():
|
||||
mgr, provider = _mgr(tmp_path, [_text("working on it…")], ["Scan the API for secrets"])
|
||||
sid = "early1"
|
||||
engine = mgr.get_engine(sid, agent="chat")
|
||||
engine.messages.append({"role": "user", "content": "scan code for vuln and secrets"})
|
||||
mgr.save(sid, engine)
|
||||
mgr.mark_running(sid) # the turn is RUNNING — no completion in sight
|
||||
mgr._maybe_autotitle(sid) # the app's turn_start hook
|
||||
deadline = time.time() + 5.0
|
||||
while sid in mgr._autotitle_inflight and time.time() < deadline:
|
||||
await asyncio.sleep(0.005)
|
||||
state = mgr.session_store.title_state(sid)
|
||||
assert state["auto_title"] == "Scan the API for secrets"
|
||||
# The completion hook later, same openers: skipped WITHOUT burning attempt 2.
|
||||
mgr.mark_idle(sid)
|
||||
assert mgr._autotitle_attempts[sid] == 1
|
||||
|
||||
asyncio.run(go())
|
||||
|
||||
@ -423,6 +423,11 @@ def test_ws_allows_only_one_inflight_turn_per_session(tmp_path):
|
||||
self.max_active = 0
|
||||
|
||||
def complete(self, *, model, messages, tools=None, **settings):
|
||||
# The fire-and-forget auto-title completion legitimately runs CONCURRENTLY
|
||||
# with the chat turn (it fires at turn start, owner catch 2026-08-24) — the
|
||||
# invariant under test is one CHAT turn at a time, so exclude title calls.
|
||||
if messages and "title chat sessions" in str(messages[0].get("content", "")):
|
||||
return _text("A Title")
|
||||
with self._lock:
|
||||
self.active += 1
|
||||
self.max_active = max(self.max_active, self.active)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user