mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:30:48 +08:00
fix(scheduler): reuse one execution session per scheduled job
Reuse channel_session_v2 per job_id so cron runs do not create a new oclaw session each time; isolate scheduled turn context to the active turn_uuid so shared sessions do not leak prior run history. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
146d83c336
commit
4f36f4a36f
4 changed files with 53 additions and 24 deletions
|
|
@ -588,6 +588,14 @@ def _build_model_context(
|
||||||
active_turn_uuid: str | None = None,
|
active_turn_uuid: str | None = None,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
rows = store.get_messages(session_id=session_id, limit=int(max_messages))
|
rows = store.get_messages(session_id=session_id, limit=int(max_messages))
|
||||||
|
pb_ctx = prompt_build_context if isinstance(prompt_build_context, dict) else {}
|
||||||
|
if bool(pb_ctx.get("scheduled_proactive")) and str(active_turn_uuid or "").strip():
|
||||||
|
turn_key = str(active_turn_uuid).strip()
|
||||||
|
rows = [
|
||||||
|
r
|
||||||
|
for r in rows
|
||||||
|
if str(getattr(r, "turn_uuid", "") or "").strip() == turn_key
|
||||||
|
]
|
||||||
rows = _guard_tool_results_for_llm_context(
|
rows = _guard_tool_results_for_llm_context(
|
||||||
store=store,
|
store=store,
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
|
|
@ -621,7 +629,6 @@ def _build_model_context(
|
||||||
# Hook integration: wiki-auto-inject can prepend retrieval snippets
|
# Hook integration: wiki-auto-inject can prepend retrieval snippets
|
||||||
# before prompt build when query/topic hints indicate supplemental lookup.
|
# before prompt build when query/topic hints indicate supplemental lookup.
|
||||||
try:
|
try:
|
||||||
pb_ctx = prompt_build_context if isinstance(prompt_build_context, dict) else {}
|
|
||||||
user_text_final = str(user_text or "").strip()
|
user_text_final = str(user_text or "").strip()
|
||||||
wiki_query = str(pb_ctx.get("wiki_query") or "").strip()
|
wiki_query = str(pb_ctx.get("wiki_query") or "").strip()
|
||||||
hook_ctx = {
|
hook_ctx = {
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,6 @@ def enqueue_scheduled_job_run(
|
||||||
store,
|
store,
|
||||||
job=job,
|
job=job,
|
||||||
created_by_user_id=str(getattr(job, "created_by_user_id", "") or ""),
|
created_by_user_id=str(getattr(job, "created_by_user_id", "") or ""),
|
||||||
run_id=str(run.id),
|
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
store.scheduled_job_run_update(
|
store.scheduled_job_run_update(
|
||||||
|
|
|
||||||
|
|
@ -51,24 +51,28 @@ def _ensure_administrator_owner(store: Any, *, tenant_id: str) -> dict[str, Any]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _create_scheduled_execution_session(
|
def _get_or_create_scheduled_execution_session(
|
||||||
store: Any,
|
store: Any,
|
||||||
*,
|
*,
|
||||||
tenant_id: str,
|
tenant_id: str,
|
||||||
user_id: str,
|
job_id: str,
|
||||||
job_name: str,
|
job_name: str,
|
||||||
run_id: str = "",
|
|
||||||
) -> str:
|
) -> str:
|
||||||
rid = str(run_id or "").strip()
|
"""One execution session per scheduled job (not per run); visible for ops cleanup."""
|
||||||
title = f"Scheduled · {job_name}"
|
title = f"Scheduled · {job_name}"
|
||||||
if rid:
|
jid = str(job_id or "").strip()
|
||||||
title = f"{title} · {rid[:8]}"
|
|
||||||
tid = str(tenant_id or "").strip()
|
tid = str(tenant_id or "").strip()
|
||||||
uid = str(user_id or "").strip()
|
getter = getattr(store, "get_or_create_channel_session_v2", None)
|
||||||
if uid and tid:
|
if jid and tid and callable(getter):
|
||||||
sess = store.create_session_for_user(title=title, tenant_id=tid, user_id=uid)
|
return getter(
|
||||||
else:
|
tenant_id=tid,
|
||||||
sess = store.create_session(title)
|
channel="scheduled_job",
|
||||||
|
account_id="job",
|
||||||
|
external_chat_id=jid,
|
||||||
|
external_user_id=jid,
|
||||||
|
session_title=title,
|
||||||
|
)
|
||||||
|
sess = store.create_session(title)
|
||||||
return str(sess.id)
|
return str(sess.id)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -255,9 +259,9 @@ def resolve_scheduled_session(
|
||||||
*,
|
*,
|
||||||
job: Any,
|
job: Any,
|
||||||
created_by_user_id: str = "",
|
created_by_user_id: str = "",
|
||||||
run_id: str = "",
|
|
||||||
) -> ResolvedSession:
|
) -> ResolvedSession:
|
||||||
job_name = str(getattr(job, "name", "") or "Scheduled task")
|
job_name = str(getattr(job, "name", "") or "Scheduled task")
|
||||||
|
job_id = str(getattr(job, "id", "") or "").strip()
|
||||||
(
|
(
|
||||||
tenant_id,
|
tenant_id,
|
||||||
user_id,
|
user_id,
|
||||||
|
|
@ -272,12 +276,11 @@ def resolve_scheduled_session(
|
||||||
job=job,
|
job=job,
|
||||||
created_by_user_id=created_by_user_id,
|
created_by_user_id=created_by_user_id,
|
||||||
)
|
)
|
||||||
execution_session_id = _create_scheduled_execution_session(
|
execution_session_id = _get_or_create_scheduled_execution_session(
|
||||||
store,
|
store,
|
||||||
tenant_id=tenant_id,
|
tenant_id=tenant_id,
|
||||||
user_id=user_id,
|
job_id=job_id,
|
||||||
job_name=job_name,
|
job_name=job_name,
|
||||||
run_id=run_id,
|
|
||||||
)
|
)
|
||||||
return ResolvedSession(
|
return ResolvedSession(
|
||||||
session_id=execution_session_id,
|
session_id=execution_session_id,
|
||||||
|
|
|
||||||
|
|
@ -64,8 +64,9 @@ class ScheduledSessionIsolationTests(unittest.TestCase):
|
||||||
def tearDown(self) -> None:
|
def tearDown(self) -> None:
|
||||||
self._tmp.cleanup()
|
self._tmp.cleanup()
|
||||||
|
|
||||||
def test_resolve_scheduled_session_uses_fresh_execution_session(self) -> None:
|
def test_resolve_scheduled_session_isolates_from_interactive_and_reuses_per_job(self) -> None:
|
||||||
job = mock.MagicMock()
|
job = mock.MagicMock()
|
||||||
|
job.id = "job-water-reminder"
|
||||||
job.tenant_id = self.tenant_id
|
job.tenant_id = self.tenant_id
|
||||||
job.source_session_id = self.interactive_session_id
|
job.source_session_id = self.interactive_session_id
|
||||||
job.delivery_json = json.dumps(
|
job.delivery_json = json.dumps(
|
||||||
|
|
@ -81,18 +82,37 @@ class ScheduledSessionIsolationTests(unittest.TestCase):
|
||||||
)
|
)
|
||||||
job.name = "喝水提醒"
|
job.name = "喝水提醒"
|
||||||
|
|
||||||
resolved = resolve_scheduled_session(
|
resolved_first = resolve_scheduled_session(
|
||||||
self.store,
|
self.store,
|
||||||
job=job,
|
job=job,
|
||||||
created_by_user_id=self.admin_id,
|
created_by_user_id=self.admin_id,
|
||||||
run_id="run-abc12345",
|
|
||||||
)
|
)
|
||||||
self.assertNotEqual(resolved.session_id, self.interactive_session_id)
|
resolved_second = resolve_scheduled_session(
|
||||||
self.assertEqual(resolved.source_session_id, self.interactive_session_id)
|
self.store,
|
||||||
self.assertEqual(resolved.external_chat_id, "120363012345678@g.us")
|
job=job,
|
||||||
rows = self.store.get_messages(session_id=resolved.session_id, limit=10)
|
created_by_user_id=self.admin_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotEqual(resolved_first.session_id, self.interactive_session_id)
|
||||||
|
self.assertEqual(resolved_first.session_id, resolved_second.session_id)
|
||||||
|
self.assertEqual(resolved_first.source_session_id, self.interactive_session_id)
|
||||||
|
self.assertEqual(resolved_first.external_chat_id, "120363012345678@g.us")
|
||||||
|
rows = self.store.get_messages(session_id=resolved_first.session_id, limit=10)
|
||||||
self.assertEqual(len(rows), 0)
|
self.assertEqual(len(rows), 0)
|
||||||
|
|
||||||
|
other_job = mock.MagicMock()
|
||||||
|
other_job.id = "job-other"
|
||||||
|
other_job.tenant_id = self.tenant_id
|
||||||
|
other_job.source_session_id = self.interactive_session_id
|
||||||
|
other_job.delivery_json = job.delivery_json
|
||||||
|
other_job.name = "Other task"
|
||||||
|
resolved_other = resolve_scheduled_session(
|
||||||
|
self.store,
|
||||||
|
job=other_job,
|
||||||
|
created_by_user_id=self.admin_id,
|
||||||
|
)
|
||||||
|
self.assertNotEqual(resolved_other.session_id, resolved_first.session_id)
|
||||||
|
|
||||||
|
|
||||||
class ScheduledMentionCreatorTests(unittest.TestCase):
|
class ScheduledMentionCreatorTests(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue