codex-autorunner 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codex_autorunner/bootstrap.py +26 -5
- codex_autorunner/core/about_car.py +12 -12
- codex_autorunner/core/config.py +178 -61
- codex_autorunner/core/context_awareness.py +1 -0
- codex_autorunner/core/filesystem.py +24 -0
- codex_autorunner/core/flows/controller.py +50 -12
- codex_autorunner/core/flows/runtime.py +8 -3
- codex_autorunner/core/hub.py +293 -16
- codex_autorunner/core/lifecycle_events.py +44 -5
- codex_autorunner/core/pma_context.py +188 -1
- codex_autorunner/core/pma_delivery.py +81 -0
- codex_autorunner/core/pma_dispatches.py +224 -0
- codex_autorunner/core/pma_lane_worker.py +122 -0
- codex_autorunner/core/pma_queue.py +167 -18
- codex_autorunner/core/pma_reactive.py +91 -0
- codex_autorunner/core/pma_safety.py +58 -0
- codex_autorunner/core/pma_sink.py +104 -0
- codex_autorunner/core/pma_transcripts.py +183 -0
- codex_autorunner/core/safe_paths.py +117 -0
- codex_autorunner/housekeeping.py +77 -23
- codex_autorunner/integrations/agents/codex_backend.py +18 -12
- codex_autorunner/integrations/agents/wiring.py +2 -0
- codex_autorunner/integrations/app_server/client.py +31 -0
- codex_autorunner/integrations/app_server/supervisor.py +3 -0
- codex_autorunner/integrations/telegram/adapter.py +1 -1
- codex_autorunner/integrations/telegram/config.py +1 -1
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +16 -15
- codex_autorunner/integrations/telegram/handlers/commands/files.py +5 -8
- codex_autorunner/integrations/telegram/handlers/commands/github.py +10 -6
- codex_autorunner/integrations/telegram/handlers/commands/shared.py +9 -8
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +85 -2
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +29 -8
- codex_autorunner/integrations/telegram/handlers/messages.py +8 -2
- codex_autorunner/integrations/telegram/helpers.py +30 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +54 -3
- codex_autorunner/static/archive.js +274 -81
- codex_autorunner/static/archiveApi.js +21 -0
- codex_autorunner/static/constants.js +1 -1
- codex_autorunner/static/docChatCore.js +2 -0
- codex_autorunner/static/hub.js +59 -0
- codex_autorunner/static/index.html +70 -54
- codex_autorunner/static/notificationBell.js +173 -0
- codex_autorunner/static/notifications.js +187 -36
- codex_autorunner/static/pma.js +96 -35
- codex_autorunner/static/styles.css +431 -4
- codex_autorunner/static/terminalManager.js +22 -3
- codex_autorunner/static/utils.js +5 -1
- codex_autorunner/surfaces/cli/cli.py +206 -129
- codex_autorunner/surfaces/cli/template_repos.py +157 -0
- codex_autorunner/surfaces/web/app.py +193 -5
- codex_autorunner/surfaces/web/routes/archive.py +197 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +115 -87
- codex_autorunner/surfaces/web/routes/flows.py +125 -67
- codex_autorunner/surfaces/web/routes/pma.py +638 -57
- codex_autorunner/surfaces/web/schemas.py +11 -0
- codex_autorunner/tickets/agent_pool.py +6 -1
- codex_autorunner/tickets/outbox.py +27 -14
- codex_autorunner/tickets/replies.py +4 -10
- codex_autorunner/tickets/runner.py +1 -0
- codex_autorunner/workspace/paths.py +8 -3
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/METADATA +1 -1
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/RECORD +67 -57
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.2.0.dist-info → codex_autorunner-1.3.0.dist-info}/top_level.txt +0 -0
|
@@ -21,6 +21,7 @@ from fastapi.responses import StreamingResponse
|
|
|
21
21
|
|
|
22
22
|
from ....agents.registry import validate_agent_id
|
|
23
23
|
from ....core import drafts as draft_utils
|
|
24
|
+
from ....core.context_awareness import CAR_AWARENESS_BLOCK, format_file_role_addendum
|
|
24
25
|
from ....core.state import now_iso
|
|
25
26
|
from ....core.utils import atomic_write, find_repo_root
|
|
26
27
|
from ....integrations.app_server.event_buffer import format_sse
|
|
@@ -50,6 +51,24 @@ class _Target:
|
|
|
50
51
|
state_key: str
|
|
51
52
|
|
|
52
53
|
|
|
54
|
+
@dataclass
|
|
55
|
+
class FileChatRoutesState:
|
|
56
|
+
active_chats: Dict[str, asyncio.Event]
|
|
57
|
+
chat_lock: asyncio.Lock
|
|
58
|
+
turn_lock: asyncio.Lock
|
|
59
|
+
current_by_target: Dict[str, Dict[str, Any]]
|
|
60
|
+
current_by_client: Dict[str, Dict[str, Any]]
|
|
61
|
+
last_by_client: Dict[str, Dict[str, Any]]
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
self.active_chats = {}
|
|
65
|
+
self.chat_lock = asyncio.Lock()
|
|
66
|
+
self.turn_lock = asyncio.Lock()
|
|
67
|
+
self.current_by_target = {}
|
|
68
|
+
self.current_by_client = {}
|
|
69
|
+
self.last_by_client = {}
|
|
70
|
+
|
|
71
|
+
|
|
53
72
|
def _state_path(repo_root: Path) -> Path:
|
|
54
73
|
return draft_utils.state_path(repo_root)
|
|
55
74
|
|
|
@@ -146,41 +165,20 @@ def _parse_target(repo_root: Path, raw: str) -> _Target:
|
|
|
146
165
|
def _build_file_chat_prompt(*, target: _Target, message: str, before: str) -> str:
|
|
147
166
|
if target.kind == "ticket":
|
|
148
167
|
file_role_context = (
|
|
149
|
-
"
|
|
150
|
-
"`.codex-autorunner/tickets/TICKET-###*.md` in numeric order.\n"
|
|
168
|
+
f"{format_file_role_addendum('ticket', target.rel_path)}\n"
|
|
151
169
|
"Edits here change what the ticket flow agent will do; keep YAML "
|
|
152
170
|
"frontmatter valid."
|
|
153
171
|
)
|
|
154
172
|
elif target.kind == "workspace":
|
|
155
173
|
file_role_context = (
|
|
156
|
-
"
|
|
157
|
-
"
|
|
174
|
+
f"{format_file_role_addendum('workspace', target.rel_path)}\n"
|
|
175
|
+
"These docs act as shared memory across ticket turns."
|
|
158
176
|
)
|
|
159
177
|
else:
|
|
160
|
-
file_role_context = (
|
|
161
|
-
"This file is a normal repo file (not a CAR ticket/workspace doc)."
|
|
162
|
-
)
|
|
178
|
+
file_role_context = format_file_role_addendum("other", target.rel_path)
|
|
163
179
|
|
|
164
180
|
return (
|
|
165
|
-
"
|
|
166
|
-
"You are operating inside a Codex Autorunner (CAR) managed repo.\n\n"
|
|
167
|
-
"CAR’s durable control-plane lives under `.codex-autorunner/`:\n"
|
|
168
|
-
"- `.codex-autorunner/ABOUT_CAR.md` — short repo-local briefing "
|
|
169
|
-
"(ticket/workspace conventions + helper scripts).\n"
|
|
170
|
-
"- `.codex-autorunner/tickets/` — ordered ticket queue "
|
|
171
|
-
"(`TICKET-###*.md`) used by the ticket flow runner.\n"
|
|
172
|
-
"- `.codex-autorunner/workspace/` — shared context docs:\n"
|
|
173
|
-
" - `active_context.md` — current “north star” context; kept fresh "
|
|
174
|
-
"for ongoing work.\n"
|
|
175
|
-
" - `spec.md` — longer spec / acceptance criteria when needed.\n"
|
|
176
|
-
" - `decisions.md` — prior decisions / tradeoffs when relevant.\n\n"
|
|
177
|
-
"Intent signals: if the user mentions tickets, “dispatch”, “resume”, "
|
|
178
|
-
"workspace docs, or `.codex-autorunner/`, they are likely referring "
|
|
179
|
-
"to CAR artifacts/workflow rather than generic repo files.\n\n"
|
|
180
|
-
"Use the above as orientation. If you need the operational details "
|
|
181
|
-
"(exact helper commands, what CAR auto-generates), read "
|
|
182
|
-
"`.codex-autorunner/ABOUT_CAR.md`.\n"
|
|
183
|
-
"</injected context>\n\n"
|
|
181
|
+
f"{CAR_AWARENESS_BLOCK}\n\n"
|
|
184
182
|
"<file_role_context>\n"
|
|
185
183
|
f"{file_role_context}\n"
|
|
186
184
|
"</file_role_context>\n\n"
|
|
@@ -225,25 +223,32 @@ def _build_patch(rel_path: str, before: str, after: str) -> str:
|
|
|
225
223
|
|
|
226
224
|
def build_file_chat_routes() -> APIRouter:
|
|
227
225
|
router = APIRouter(prefix="/api", tags=["file-chat"])
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
async def _get_or_create_interrupt_event(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
async with
|
|
226
|
+
state = FileChatRoutesState()
|
|
227
|
+
|
|
228
|
+
def _get_state(request: Request) -> FileChatRoutesState:
|
|
229
|
+
if not hasattr(request.app.state, "file_chat_routes_state"):
|
|
230
|
+
request.app.state.file_chat_routes_state = state
|
|
231
|
+
return request.app.state.file_chat_routes_state
|
|
232
|
+
|
|
233
|
+
async def _get_or_create_interrupt_event(
|
|
234
|
+
request: Request, key: str
|
|
235
|
+
) -> asyncio.Event:
|
|
236
|
+
s = _get_state(request)
|
|
237
|
+
async with s.chat_lock:
|
|
238
|
+
if key not in s.active_chats:
|
|
239
|
+
s.active_chats[key] = asyncio.Event()
|
|
240
|
+
return s.active_chats[key]
|
|
241
|
+
|
|
242
|
+
async def _clear_interrupt_event(request: Request, key: str) -> None:
|
|
243
|
+
s = _get_state(request)
|
|
244
|
+
async with s.chat_lock:
|
|
245
|
+
s.active_chats.pop(key, None)
|
|
246
|
+
|
|
247
|
+
async def _begin_turn_state(
|
|
248
|
+
request: Request, target: _Target, client_turn_id: Optional[str]
|
|
249
|
+
) -> None:
|
|
250
|
+
s = _get_state(request)
|
|
251
|
+
async with s.turn_lock:
|
|
247
252
|
state: Dict[str, Any] = {
|
|
248
253
|
"client_turn_id": client_turn_id or "",
|
|
249
254
|
"target": target.target,
|
|
@@ -252,13 +257,16 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
252
257
|
"thread_id": None,
|
|
253
258
|
"turn_id": None,
|
|
254
259
|
}
|
|
255
|
-
|
|
260
|
+
s.current_by_target[target.state_key] = state
|
|
256
261
|
if client_turn_id:
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
async def _update_turn_state(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
+
s.current_by_client[client_turn_id] = state
|
|
263
|
+
|
|
264
|
+
async def _update_turn_state(
|
|
265
|
+
request: Request, target: _Target, **updates: Any
|
|
266
|
+
) -> None:
|
|
267
|
+
s = _get_state(request)
|
|
268
|
+
async with s.turn_lock:
|
|
269
|
+
state = s.current_by_target.get(target.state_key)
|
|
262
270
|
if not state:
|
|
263
271
|
return
|
|
264
272
|
for key, value in updates.items():
|
|
@@ -267,34 +275,45 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
267
275
|
state[key] = value
|
|
268
276
|
cid = state.get("client_turn_id") or ""
|
|
269
277
|
if cid:
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
async def _finalize_turn_state(
|
|
273
|
-
|
|
274
|
-
|
|
278
|
+
s.current_by_client[cid] = state
|
|
279
|
+
|
|
280
|
+
async def _finalize_turn_state(
|
|
281
|
+
request: Request, target: _Target, result: Dict[str, Any]
|
|
282
|
+
) -> None:
|
|
283
|
+
s = _get_state(request)
|
|
284
|
+
async with s.turn_lock:
|
|
285
|
+
state = s.current_by_target.pop(target.state_key, None)
|
|
275
286
|
cid = ""
|
|
276
287
|
if state:
|
|
277
288
|
cid = state.get("client_turn_id", "") or ""
|
|
278
289
|
if cid:
|
|
279
|
-
|
|
280
|
-
|
|
290
|
+
s.current_by_client.pop(cid, None)
|
|
291
|
+
s.last_by_client[cid] = dict(result or {})
|
|
281
292
|
|
|
282
|
-
async def _active_for_client(
|
|
293
|
+
async def _active_for_client(
|
|
294
|
+
request: Request, client_turn_id: Optional[str]
|
|
295
|
+
) -> Dict[str, Any]:
|
|
283
296
|
if not client_turn_id:
|
|
284
297
|
return {}
|
|
285
|
-
|
|
286
|
-
|
|
298
|
+
s = _get_state(request)
|
|
299
|
+
async with s.turn_lock:
|
|
300
|
+
return dict(s.current_by_client.get(client_turn_id, {}))
|
|
287
301
|
|
|
288
|
-
async def _last_for_client(
|
|
302
|
+
async def _last_for_client(
|
|
303
|
+
request: Request, client_turn_id: Optional[str]
|
|
304
|
+
) -> Dict[str, Any]:
|
|
289
305
|
if not client_turn_id:
|
|
290
306
|
return {}
|
|
291
|
-
|
|
292
|
-
|
|
307
|
+
s = _get_state(request)
|
|
308
|
+
async with s.turn_lock:
|
|
309
|
+
return dict(s.last_by_client.get(client_turn_id, {}))
|
|
293
310
|
|
|
294
311
|
@router.get("/file-chat/active")
|
|
295
|
-
async def file_chat_active(
|
|
296
|
-
|
|
297
|
-
|
|
312
|
+
async def file_chat_active(
|
|
313
|
+
request: Request, client_turn_id: Optional[str] = None
|
|
314
|
+
) -> Dict[str, Any]:
|
|
315
|
+
current = await _active_for_client(request, client_turn_id)
|
|
316
|
+
last = await _last_for_client(request, client_turn_id)
|
|
298
317
|
return {"active": bool(current), "current": current, "last_result": last}
|
|
299
318
|
|
|
300
319
|
@router.post("/file-chat")
|
|
@@ -320,13 +339,14 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
320
339
|
target.path.parent.mkdir(parents=True, exist_ok=True)
|
|
321
340
|
|
|
322
341
|
# Concurrency guard per target
|
|
323
|
-
|
|
324
|
-
|
|
342
|
+
s = _get_state(request)
|
|
343
|
+
async with s.chat_lock:
|
|
344
|
+
existing = s.active_chats.get(target.state_key)
|
|
325
345
|
if existing is not None and not existing.is_set():
|
|
326
346
|
raise HTTPException(status_code=409, detail="File chat already running")
|
|
327
|
-
|
|
347
|
+
s.active_chats[target.state_key] = asyncio.Event()
|
|
328
348
|
|
|
329
|
-
await _begin_turn_state(target, client_turn_id)
|
|
349
|
+
await _begin_turn_state(request, target, client_turn_id)
|
|
330
350
|
|
|
331
351
|
if stream:
|
|
332
352
|
return StreamingResponse(
|
|
@@ -349,6 +369,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
349
369
|
|
|
350
370
|
async def _on_meta(agent_id: str, thread_id: str, turn_id: str) -> None:
|
|
351
371
|
await _update_turn_state(
|
|
372
|
+
request,
|
|
352
373
|
target,
|
|
353
374
|
agent=agent_id,
|
|
354
375
|
thread_id=thread_id,
|
|
@@ -368,6 +389,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
368
389
|
)
|
|
369
390
|
except Exception as exc:
|
|
370
391
|
await _finalize_turn_state(
|
|
392
|
+
request,
|
|
371
393
|
target,
|
|
372
394
|
{
|
|
373
395
|
"status": "error",
|
|
@@ -378,10 +400,10 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
378
400
|
raise
|
|
379
401
|
result = dict(result or {})
|
|
380
402
|
result["client_turn_id"] = client_turn_id or ""
|
|
381
|
-
await _finalize_turn_state(target, result)
|
|
403
|
+
await _finalize_turn_state(request, target, result)
|
|
382
404
|
return result
|
|
383
405
|
finally:
|
|
384
|
-
await _clear_interrupt_event(target.state_key)
|
|
406
|
+
await _clear_interrupt_event(request, target.state_key)
|
|
385
407
|
|
|
386
408
|
async def _stream_file_chat(
|
|
387
409
|
request: Request,
|
|
@@ -399,6 +421,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
399
421
|
|
|
400
422
|
async def _on_meta(agent_id: str, thread_id: str, turn_id: str) -> None:
|
|
401
423
|
await _update_turn_state(
|
|
424
|
+
request,
|
|
402
425
|
target,
|
|
403
426
|
agent=agent_id,
|
|
404
427
|
thread_id=thread_id,
|
|
@@ -430,7 +453,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
430
453
|
}
|
|
431
454
|
result = dict(result or {})
|
|
432
455
|
result["client_turn_id"] = client_turn_id or ""
|
|
433
|
-
await _finalize_turn_state(target, result)
|
|
456
|
+
await _finalize_turn_state(request, target, result)
|
|
434
457
|
|
|
435
458
|
asyncio.create_task(_finalize())
|
|
436
459
|
|
|
@@ -463,7 +486,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
463
486
|
logger.exception("file chat stream failed")
|
|
464
487
|
yield format_sse("error", {"detail": "File chat failed"})
|
|
465
488
|
finally:
|
|
466
|
-
await _clear_interrupt_event(target.state_key)
|
|
489
|
+
await _clear_interrupt_event(request, target.state_key)
|
|
467
490
|
|
|
468
491
|
async def _execute_file_chat(
|
|
469
492
|
request: Request,
|
|
@@ -499,7 +522,9 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
499
522
|
|
|
500
523
|
prompt = _build_file_chat_prompt(target=target, message=message, before=before)
|
|
501
524
|
|
|
502
|
-
interrupt_event = await _get_or_create_interrupt_event(
|
|
525
|
+
interrupt_event = await _get_or_create_interrupt_event(
|
|
526
|
+
request, target.state_key
|
|
527
|
+
)
|
|
503
528
|
if interrupt_event.is_set():
|
|
504
529
|
return {"status": "interrupted", "detail": "File chat interrupted"}
|
|
505
530
|
|
|
@@ -509,7 +534,7 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
509
534
|
agent_id = "codex"
|
|
510
535
|
|
|
511
536
|
thread_key = f"file_chat.{target.state_key}"
|
|
512
|
-
await _update_turn_state(target, status="running", agent=agent_id)
|
|
537
|
+
await _update_turn_state(request, target, status="running", agent=agent_id)
|
|
513
538
|
|
|
514
539
|
if agent_id == "opencode":
|
|
515
540
|
if opencode is None:
|
|
@@ -972,8 +997,9 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
972
997
|
body = await request.json()
|
|
973
998
|
repo_root = _resolve_repo_root(request)
|
|
974
999
|
resolved = _parse_target(repo_root, str(body.get("target") or ""))
|
|
975
|
-
|
|
976
|
-
|
|
1000
|
+
s = _get_state(request)
|
|
1001
|
+
async with s.chat_lock:
|
|
1002
|
+
ev = s.active_chats.get(resolved.state_key)
|
|
977
1003
|
if ev is None:
|
|
978
1004
|
return {"status": "ok", "detail": "No active chat to interrupt"}
|
|
979
1005
|
ev.set()
|
|
@@ -997,14 +1023,15 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
997
1023
|
repo_root = _resolve_repo_root(request)
|
|
998
1024
|
target = _parse_target(repo_root, f"ticket:{int(index)}")
|
|
999
1025
|
|
|
1000
|
-
|
|
1001
|
-
|
|
1026
|
+
s = _get_state(request)
|
|
1027
|
+
async with s.chat_lock:
|
|
1028
|
+
existing = s.active_chats.get(target.state_key)
|
|
1002
1029
|
if existing is not None and not existing.is_set():
|
|
1003
1030
|
raise HTTPException(
|
|
1004
1031
|
status_code=409, detail="Ticket chat already running"
|
|
1005
1032
|
)
|
|
1006
|
-
|
|
1007
|
-
await _begin_turn_state(target, client_turn_id)
|
|
1033
|
+
s.active_chats[target.state_key] = asyncio.Event()
|
|
1034
|
+
await _begin_turn_state(request, target, client_turn_id)
|
|
1008
1035
|
|
|
1009
1036
|
if stream:
|
|
1010
1037
|
return StreamingResponse(
|
|
@@ -1034,10 +1061,10 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
1034
1061
|
)
|
|
1035
1062
|
result = dict(result or {})
|
|
1036
1063
|
result["client_turn_id"] = client_turn_id or ""
|
|
1037
|
-
await _finalize_turn_state(target, result)
|
|
1064
|
+
await _finalize_turn_state(request, target, result)
|
|
1038
1065
|
return result
|
|
1039
1066
|
finally:
|
|
1040
|
-
await _clear_interrupt_event(target.state_key)
|
|
1067
|
+
await _clear_interrupt_event(request, target.state_key)
|
|
1041
1068
|
|
|
1042
1069
|
@router.get("/tickets/{index}/chat/pending")
|
|
1043
1070
|
async def pending_ticket_patch(index: int, request: Request):
|
|
@@ -1107,8 +1134,9 @@ def build_file_chat_routes() -> APIRouter:
|
|
|
1107
1134
|
async def interrupt_ticket_chat(index: int, request: Request):
|
|
1108
1135
|
repo_root = _resolve_repo_root(request)
|
|
1109
1136
|
target = _parse_target(repo_root, f"ticket:{int(index)}")
|
|
1110
|
-
|
|
1111
|
-
|
|
1137
|
+
s = _get_state(request)
|
|
1138
|
+
async with s.chat_lock:
|
|
1139
|
+
ev = s.active_chats.get(target.state_key)
|
|
1112
1140
|
if ev is None:
|
|
1113
1141
|
return {"status": "ok", "detail": "No active chat to interrupt"}
|
|
1114
1142
|
ev.set()
|