From 579c4fdbcabf1cbd5ce5f48764ca9b54bb81867f Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Sat, 14 Mar 2026 19:17:48 +0000 Subject: [PATCH 01/10] chore: remove __pycache__ from repo and add to .gitignore (#1177) Python bytecode cache files were accidentally committed. Remove them from tracking and prevent future occurrences via .gitignore. Co-authored-by: Claude Opus 4.6 (1M context) --- .gitignore | 4 ++++ .../conftest.cpython-313-pytest-8.4.0.pyc | Bin 14380 -> 0 bytes tests/e2e/__pycache__/helpers.cpython-313.pyc | Bin 9139 -> 0 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 201 -> 0 bytes .../test_chat.cpython-313-pytest-8.4.0.pyc | Bin 9147 -> 0 bytes ...est_connection.cpython-313-pytest-8.4.0.pyc | Bin 5072 -> 0 bytes .../test_csp.cpython-313-pytest-8.4.0.pyc | Bin 7373 -> 0 bytes ...xtension_oauth.cpython-313-pytest-8.4.0.pyc | Bin 35326 -> 0 bytes ...est_extensions.cpython-313-pytest-8.4.0.pyc | Bin 128293 -> 0 bytes ...html_injection.cpython-313-pytest-8.4.0.pyc | Bin 9543 -> 0 bytes ...ntial_fallback.cpython-313-pytest-8.4.0.pyc | Bin 9259 -> 0 bytes .../test_pairing.cpython-313-pytest-8.4.0.pyc | Bin 15874 -> 0 bytes ...tial_injection.cpython-313-pytest-8.4.0.pyc | Bin 12487 -> 0 bytes .../test_skills.cpython-313-pytest-8.4.0.pyc | Bin 7997 -> 0 bytes ..._sse_reconnect.cpython-313-pytest-8.4.0.pyc | Bin 7541 -> 0 bytes ..._tool_approval.cpython-313-pytest-8.4.0.pyc | Bin 11995 -> 0 bytes ...tool_execution.cpython-313-pytest-8.4.0.pyc | Bin 7772 -> 0 bytes ...wasm_lifecycle.cpython-313-pytest-8.4.0.pyc | Bin 89759 -> 0 bytes 18 files changed, 4 insertions(+) delete mode 100644 tests/e2e/__pycache__/conftest.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/__pycache__/helpers.cpython-313.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/__init__.cpython-313.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_chat.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_connection.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_csp.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_extension_oauth.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_extensions.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_html_injection.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_oauth_credential_fallback.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_pairing.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_routine_oauth_credential_injection.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_skills.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_sse_reconnect.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_tool_approval.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_tool_execution.cpython-313-pytest-8.4.0.pyc delete mode 100644 tests/e2e/scenarios/__pycache__/test_wasm_lifecycle.cpython-313-pytest-8.4.0.pyc diff --git a/.gitignore b/.gitignore index 51b461f2..ed64c242 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,10 @@ target/ +# Python +__pycache__/ +*.pyc + # Benchmark results (local runs, not committed) bench-results/ diff --git a/tests/e2e/__pycache__/conftest.cpython-313-pytest-8.4.0.pyc b/tests/e2e/__pycache__/conftest.cpython-313-pytest-8.4.0.pyc deleted file mode 100644 index 1dc7064b4c324ccfb7457e6dd664da565d56ebf3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14380 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 PisNG(;hid_C>8nw&E`0D diff --git a/tests/e2e/scenarios/__pycache__/test_chat.cpython-313-pytest-8.4.0.pyc b/tests/e2e/scenarios/__pycache__/test_chat.cpython-313-pytest-8.4.0.pyc deleted file mode 100644 index 61c1fca7f2786e369a4a869007f0f3558f91d044..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9147 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 K5QSz|$o~)UzDwc& diff --git a/tests/e2e/scenarios/__pycache__/test_extensions.cpython-313-pytest-8.4.0.pyc b/tests/e2e/scenarios/__pycache__/test_extensions.cpython-313-pytest-8.4.0.pyc deleted file mode 100644 index 76efd3111470c3d1d7327019c6a2be8ab64926c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128293 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%0D_uj6xIo73k|81wuvx>ml)Vdz}U6*P(pyaD} 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%0A6NQ%&l&mt>$h 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 z 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�ijvbvOp4fjz~4sw$2saJ^Ff3}R2N&# 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 CqySw2 diff --git a/tests/e2e/scenarios/__pycache__/test_routine_oauth_credential_injection.cpython-313-pytest-8.4.0.pyc b/tests/e2e/scenarios/__pycache__/test_routine_oauth_credential_injection.cpython-313-pytest-8.4.0.pyc deleted file mode 100644 index ea016e6a697730f78d1bccb5eb5f644e99d4cec5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12487 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 diff --git a/tests/e2e/scenarios/__pycache__/test_skills.cpython-313-pytest-8.4.0.pyc b/tests/e2e/scenarios/__pycache__/test_skills.cpython-313-pytest-8.4.0.pyc deleted file mode 100644 index b84d61cc315722aca4edc6393ecdd1dbb90658a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7997 zcmeHMU2GKB6`t9h-JSjQzt=X{9^>CNUfcKw$Hq1wZ1cbVnJ$64>UO+4##`32t2<*G z`w69efJzlXO@t#wN>t?mq(0=Kk!YHS0tu<0QfJwaai^)2rVkZwV5F)+s?>AmXLoHD z5u!-VLr3{+xWlS1o01CFdvJ-JUIf)n*>WBf+bnY zaS~ayxAhn$S{UM(4cQsJ#ep2qVm(eDb0R0yQ8ff|u{NM?)(+Hj))H{64YE;Q;7~lt z^bRrXxp*QWG9u5R=t2h*7et9mBsv&Qh%ty?OfK_GB0kSYSEC8O^C6y5C_t)KcKk$? zH0Fn2cKH7p{6DFISvL|4%r{KTFcxKI2!HjKCp21|>!&+GDAlYy9o^fnzhGM@_ zbrjaPU?qrU@=a^V=7DivII8WNjVh2~a>n`W$gG`pu=JqiTa4_)Bb%!dEMdzGeDUqi z^H!GZZ8Jn;)3A#kv)_!*xy4)N3Qf6QC&66x8SkF>45xNKeufzY`fOVb>c-LBt6DM(VjJS|o5gBQLH_jg% zKZ!^4T$JxrT_6ydTau)tpxQ<4lwOB}2el#9iQ7_~z$aA4a$JnhCHVM7nC^9pYJ>AD z@v1|LFY?JHNiEg7M)dO?i3_PEX{~V~ll-Q9XYAFoHmM~}gedaf%BD-*kf__<_)`)MjJF7m>;%(b(o8s?E`@7yhBKx~!|47C)s<_6|uCa`3 zLZK#PYGT7veEGzs6K|KT&!(#bN_BU-y8Hdpa&@;{J(}_Czc9YxD!SZxsdIhx{ichZ zvTH!024rgB%QFAf;VZ-MS>>iz(#&9{Z0N#?jgnfWq%B?2mMLjh-0d=T@`1Zup-#e| z+<~dVo0Zv`GrX=vd(ZHRK|8}8HLHonOwf9 zH|NGxZt1nll+C^Kdrgusr`$>kOJ`jITheLK9Ev@!Pg72_$-(MN&F{{ZX-=y=Y&V>d zW46_l%hO^`-)w?6%rkp>Ol{a1jgYAyV0Uzw3<(-NpwV`4OphCg*{n-+5FC6NhL|li zmCh7&d926O&JgujYw0{OVy!3UNlmX+b9NPLeXJH&F^9mA9$A`Wz#KEWHPiyEWgf-n zS0T<4mlBf5aH$lCcOra%5ss)3wgn-CUA#&!a_C$vxgw~xaqK14GEI@`*>oKUN0Sek z5Ol?{#nWDMV8ow|ixOI8mITCe(FG1%CIgc}ytIS_J_hd3oW?^OcU>o+T1H2%lWV2e z19k^QW^_bzhP~hnZPDbCAc3<5uh@-A?^?t7`4k_OKoKV~37!)rW{>IA^{GAG>Qwu6 zr`jK=KzQ>thl)xthj(73<{>NvCkK6Dy!kkS$>VrLb&j8p@+s^JRlDw3RSGsDg?8h< z6pf_-W^mndG#kC{t+Od8t<7!o?R+(zuz;!#*uAS?zW?an*)htuZ zkL;A~IJr?;d9~w8$J?UZ_)5BdFjG2o(XrvHxV(62QSmjUeNEQ}Z}76ON%oB?^q5SK zJ)()SnybMp!C!SLo(6ElYh!P4#8rYm}dsrV^#1;=)1Z&tjgN@6!=XsF@TQYQ-AT)3N zBL9^2r2vVo-!X(P+Y(_>@O`L)D=!>ll^2e&K*vk$gkWPUlYH2&#aJt5AP~bbRyMMl z$;M5&?S&}AA9g^DRdxDQUW`>en~khyn4Gl96^OBNqZE#RZ00#J=cawNT(5!=P&nHQ zVyx=Cl@yk?n`5vwMvO)0^=UC@pL@gVcVzqQ)MBjKFm1$G&e?z|nWv@QtjoP2#&Vik z%(oF^EkEZZ6utv>+8uC(T`%+H;SRh(K*YuUm>{4hj*zxJ&$~TZatJSa+r!qIt9bTK zzc8J&n5)oGk0tciyiqKnKY?a4bYG)4H2Q0e zu4rheb*=BHo?T`*9RxA+D7m;5C5Rn39VnQo=eSfD4IylFQ6nbUG-|L1HDeCL9)v&W z+K!dD0C=f0niK#(EHWcZTYwoEWhAu3t9B!^=rE=PIc<7@6C)CTUP5pyiM5tc(#(cy zV99`O^8kHf&|sn^wMd6yM&g{0#n*?R_W+ACmZkzG7RLxnVwO@d$WCe*&ujsi~qW$-!P_*OCfYi^ywwoGcD z4*L26{z9M+?ZGqb!K4=xjQ0>usUd99(Ow`b%`bC_CD_9p-a`Yp^%YD8F@fzT3IRB^ z*v!vHjO1{@mxFW&XHQhOG5yG#B&g2pTF?a6IgH5&CZm|_2co+4-$<}LI*Kc_6s(?g z?aeRrix|b5h);j(xUViB_nG6v&o6*V@y{=ag?{*ui!G23e0dD`$WD}3J)8CXrYMm5 zb|}zAxQi|aF9qeQ{)}s{LhY5Qy)Y9U8Y-o&>C)C~^o>Eev{f!0zi@oRTcvpG(%!m^ zmr)$dy9YCl<}7L({)`^ka8>B2HRGyPs9Kq-1)y0{an*Ch^EN9t^rUNhGsS%ujy|$j zZIk4N&wn|2DXI8c(!Q2!XZ~Q5eJ!$YLZK&QdICDuw!Aa>)?{X9P^k=FnB1tVm#I_h zT-sf)P^a|2`_!q6`?DpTd9bA}ohw1ZI+qm&U2=n=CR_PAbqbbWy5nm7m3lqhe68xn z5xJyIE*ZaYY{TnUytQd>ZN^)#IO^YR_{`C`;j2=7b!lH+#>Xf$Bh$>6zEZ{4nD#Zk zJC^YU6gnW&fsLBFcRJqccu$mf52ss4GBu-0%~-l-EK@U~luulAZWL9ei`o_XjQ)3@ zK67z3Thf^aTk6tVO1i}7^qKYj*4vYU+D#APJe&pclQ|%NQlUc_$WLlO-ru?DB4|dXnXl-8T0Zf$*bD*ZYXj2( zfc2Ao0PBGRSU)*{VLk8w>nDdWtjD~we~Q4cesToEdf;zc%c1_ZpPXu?ZddIA9MGu33387tJw zjU?uq@Z9BgEF%Xs+D*dvaxXLOx2DUmWV({XydO)ZYq3n4(P*PaTQCiHRYxQeOGYCR zl{OP(nlV+YBYJ+VQ~$9FGH#uSUqNyBb}NYrzB445u- z026G;t`kpy61wH1hZmEvr362Uegt(GBZ%0g!-plwzYzQXNf19Le&+of;or17$Pl?% RUQF(L($Y@;kVLZ}_g~aqF;xHn diff --git a/tests/e2e/scenarios/__pycache__/test_sse_reconnect.cpython-313-pytest-8.4.0.pyc b/tests/e2e/scenarios/__pycache__/test_sse_reconnect.cpython-313-pytest-8.4.0.pyc deleted file mode 100644 index 3c8a89b373336fa5fae89d59f0f844bccd5b9643..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7541 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 O5A0xHWz}y(q5lLHbT9D$ diff --git a/tests/e2e/scenarios/__pycache__/test_tool_execution.cpython-313-pytest-8.4.0.pyc b/tests/e2e/scenarios/__pycache__/test_tool_execution.cpython-313-pytest-8.4.0.pyc deleted file mode 100644 index 9437069b035f2dbdbfbebdd83301e0a2cb6fae6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7772 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 diff --git a/tests/e2e/scenarios/__pycache__/test_wasm_lifecycle.cpython-313-pytest-8.4.0.pyc b/tests/e2e/scenarios/__pycache__/test_wasm_lifecycle.cpython-313-pytest-8.4.0.pyc deleted file mode 100644 index f6349d8be6cc1cbf2eb25c76160f4f9382dcfd04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 89759 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 bvcQ8FI)p%R?q{Q1cW;fy+3D2CKKTC+#`v0; From 7c017ea6fd44a4a156013c39068ba1c690b1725b Mon Sep 17 00:00:00 2001 From: Nige Date: Sat, 14 Mar 2026 20:06:04 +0000 Subject: [PATCH 02/10] chore(registry): align manifest versions with published artifacts (#1169) --- registry/channels/discord.json | 2 +- registry/tools/github.json | 2 +- registry/tools/web-search.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/channels/discord.json b/registry/channels/discord.json index 50ef85ee..6f5cd4e7 100644 --- a/registry/channels/discord.json +++ b/registry/channels/discord.json @@ -2,7 +2,7 @@ "name": "discord", "display_name": "Discord Channel", "kind": "channel", - "version": "0.2.1", + "version": "0.2.0", "wit_version": "0.3.0", "description": "Talk to your agent in Discord", "keywords": [ diff --git a/registry/tools/github.json b/registry/tools/github.json index e775ac82..e84f756d 100644 --- a/registry/tools/github.json +++ b/registry/tools/github.json @@ -2,7 +2,7 @@ "name": "github", "display_name": "GitHub", "kind": "tool", - "version": "0.2.1", + "version": "0.2.0", "wit_version": "0.3.0", "description": "GitHub integration for issues, PRs, repos, and code search", "keywords": [ diff --git a/registry/tools/web-search.json b/registry/tools/web-search.json index 1722c391..4da5744b 100644 --- a/registry/tools/web-search.json +++ b/registry/tools/web-search.json @@ -2,7 +2,7 @@ "name": "web-search", "display_name": "Web Search", "kind": "tool", - "version": "0.2.1", + "version": "0.2.0", "wit_version": "0.3.0", "description": "Search the web using Brave Search API", "keywords": [ From 8dfad332d96137bdcf3bafa265cb56f1111f052a Mon Sep 17 00:00:00 2001 From: Nige Date: Sat, 14 Mar 2026 20:06:24 +0000 Subject: [PATCH 03/10] fix(webhook): avoid lock-held awaits in server lifecycle paths (#1168) * fix(webhook): avoid holding mutex across async shutdown * test(webhook): add regression coverage for begin_shutdown split path * test(webhook): satisfy no-panics rule in begin_shutdown regression --- src/channels/webhook_server.rs | 40 ++++++++++++++++++++++++++++++++-- src/main.rs | 11 +++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/channels/webhook_server.rs b/src/channels/webhook_server.rs index 2425ab32..228abf0a 100644 --- a/src/channels/webhook_server.rs +++ b/src/channels/webhook_server.rs @@ -139,12 +139,19 @@ impl WebhookServer { self.config.addr } + /// Take ownership of shutdown primitives so callers can perform async + /// shutdown work without holding external locks around this server. + pub fn begin_shutdown(&mut self) -> (Option>, Option>) { + (self.shutdown_tx.take(), self.handle.take()) + } + /// Signal graceful shutdown and wait for the server task to finish. pub async fn shutdown(&mut self) { - if let Some(tx) = self.shutdown_tx.take() { + let (shutdown_tx, handle) = self.begin_shutdown(); + if let Some(tx) = shutdown_tx { let _ = tx.send(()); } - if let Some(handle) = self.handle.take() { + if let Some(handle) = handle { let _ = handle.await; } } @@ -269,6 +276,35 @@ mod tests { server.shutdown().await; } + #[tokio::test] + async fn test_begin_shutdown_takes_handles_for_lock_free_shutdown() { + let addr = SocketAddr::from((std::net::Ipv4Addr::LOCALHOST, 0)); + let mut server = WebhookServer::new(WebhookServerConfig { addr }); + + let test_router = axum::Router::new().route( + "/health", + axum::routing::get(|| async { Json(json!({"status": "ok"})) }), + ); + server.add_routes(test_router); + server.start().await.expect("Failed to start server"); // safety: test assertion for setup precondition + + let (shutdown_tx, handle) = server.begin_shutdown(); + assert!(shutdown_tx.is_some(), "shutdown sender should be available"); // safety: test assertion for expected server state + assert!(handle.is_some(), "server handle should be available"); // safety: test assertion for expected server state + + // begin_shutdown() should leave no handles behind on the server. + let (shutdown_tx2, handle2) = server.begin_shutdown(); + assert!(shutdown_tx2.is_none(), "shutdown sender should be consumed"); // safety: test assertion for postcondition + assert!(handle2.is_none(), "server handle should be consumed"); // safety: test assertion for postcondition + + if let Some(tx) = shutdown_tx { + let _ = tx.send(()); + } + if let Some(handle) = handle { + let _ = handle.await; + } + } + #[tokio::test] async fn test_restart_with_addr_rollback_on_bind_failure() { use std::net::TcpListener as StdTcpListener; diff --git a/src/main.rs b/src/main.rs index 12a8caf6..a7d95bec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -920,7 +920,16 @@ async fn async_main() -> anyhow::Result<()> { } if let Some(ref ws_arc) = webhook_server { - ws_arc.lock().await.shutdown().await; + let (shutdown_tx, handle) = { + let mut ws = ws_arc.lock().await; + ws.begin_shutdown() + }; + if let Some(tx) = shutdown_tx { + let _ = tx.send(()); + } + if let Some(handle) = handle { + let _ = handle.await; + } } if let Some(tunnel) = active_tunnel { From 3f2796b7453137a1e0de5b450a0574a521a68614 Mon Sep 17 00:00:00 2001 From: Nick Pismenkov <50764773+nickpismenkov@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:06:30 -0700 Subject: [PATCH 04/10] =?UTF-8?q?fix:=20Non-transactional=20multi-step=20c?= =?UTF-8?q?ontext=20updates=20between=20metadata/to=E2=80=A6=20(#1161)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Non-transactional multi-step context updates between metadata/token setup and DB * fix: code style --- src/agent/scheduler.rs | 42 +++++++++++++++----- src/context/manager.rs | 88 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 9 deletions(-) diff --git a/src/agent/scheduler.rs b/src/agent/scheduler.rs index 5e4bf01a..3923530f 100644 --- a/src/agent/scheduler.rs +++ b/src/agent/scheduler.rs @@ -179,27 +179,33 @@ impl Scheduler { }) .unwrap_or(self.config.max_tokens_per_job); - // Apply both metadata and token budget in one closure (Issue #813: atomic update) - if let Some(meta) = metadata { + // Apply both metadata and token budget in one closure (Issue #813: atomic update). + // Use update_context_and_get to ensure atomicity: no gap where concurrent workers + // can modify the context between update and DB persist (Issue #807). + let ctx = if let Some(meta) = metadata { self.context_manager - .update_context(job_id, |ctx| { + .update_context_and_get(job_id, |ctx| { ctx.metadata = meta; if max_tokens > 0 { ctx.max_tokens = max_tokens; } }) - .await?; + .await? } else if max_tokens > 0 { self.context_manager - .update_context(job_id, |ctx| { + .update_context_and_get(job_id, |ctx| { ctx.max_tokens = max_tokens; }) - .await?; - } + .await? + } else { + // No metadata or token budget to set; get the initial context + self.context_manager.get_context(job_id).await? + }; - // Persist to DB before scheduling so the worker's FK references are valid + // Persist to DB before scheduling so the worker's FK references are valid. + // The context was read under the same lock as the update (atomic), preventing + // concurrent worker interference (Issue #807: non-transactional context updates). if let Some(ref store) = self.store { - let ctx = self.context_manager.get_context(job_id).await?; store.save_job(&ctx).await.map_err(|e| JobError::Failed { id: job_id, reason: format!("failed to persist job: {e}"), @@ -832,6 +838,24 @@ mod tests { ); } + #[tokio::test] + async fn test_dispatch_job_no_metadata_no_user_tokens_edge_case() { + // Edge case coverage: when metadata=None AND max_tokens=0 (config), + // the else branch calls get_context() directly (not update_context_and_get). + // This test verifies that path works correctly (Issue #807: full branch coverage). + let sched = make_test_scheduler(0); // 0 = unlimited, but user provides None + let job_id = sched + .dispatch_job("user1", "test", "desc", None) // None metadata + .await + .unwrap(); // safety: test code + + let ctx = sched.context_manager.get_context(job_id).await.unwrap(); // safety: test code + // No metadata was set, should have default empty metadata + assert!(ctx.metadata.is_null() || ctx.metadata == serde_json::json!({})); // safety: test code + // No user tokens AND unlimited config means max_tokens stays at default + assert_eq!(ctx.max_tokens, 0, "unlimited config"); // safety: test code + } + #[test] fn test_scheduler_creation() { // Would need to mock dependencies for proper testing diff --git a/src/context/manager.rs b/src/context/manager.rs index 407a0eea..764f189a 100644 --- a/src/context/manager.rs +++ b/src/context/manager.rs @@ -87,6 +87,28 @@ impl ContextManager { Ok(f(context)) } + /// Atomically update a job context and return the updated context. + /// + /// This method holds the write lock for the entire update-and-read sequence, + /// preventing concurrent workers from interleaving modifications between the + /// update and the subsequent read (Issue #807: non-transactional context updates). + /// Use this when you need to update context and immediately persist it to DB. + pub async fn update_context_and_get( + &self, + job_id: Uuid, + f: F, + ) -> Result + where + F: FnOnce(&mut JobContext), + { + let mut contexts = self.contexts.write().await; + let context = contexts + .get_mut(&job_id) + .ok_or(JobError::NotFound { id: job_id })?; + f(context); + Ok(context.clone()) + } + /// Get job memory. pub async fn get_memory(&self, job_id: Uuid) -> Result { self.memories @@ -877,4 +899,70 @@ mod tests { assert_eq!(manager.all_jobs().await.len(), 10); } + + #[tokio::test] + async fn update_context_and_get_atomicity_regression_issue_807() { + // Regression test for Issue #807: non-transactional context updates. + // Verify that update_context_and_get returns the exact state that was set, + // without allowing concurrent workers to interleave modifications. + let manager = std::sync::Arc::new(ContextManager::new(100)); + let job_id = manager + .create_job("Atomicity Test", "verify no race condition") + .await + .unwrap(); // safety: test code + + // Update and get atomically, setting metadata + let metadata = serde_json::json!({ "priority": "high", "user_id": 42 }); + let returned_ctx = manager + .update_context_and_get(job_id, |ctx| { + ctx.metadata = metadata.clone(); + ctx.max_tokens = 5000; + }) + .await + .unwrap(); // safety: test code + + // Verify the returned context has the exact updates we set + assert_eq!(returned_ctx.metadata, metadata); // safety: test code + assert_eq!(returned_ctx.max_tokens, 5000); // safety: test code + + // Verify a fresh get returns the same state + let fresh_ctx = manager.get_context(job_id).await.unwrap(); // safety: test code + assert_eq!(fresh_ctx.metadata, metadata); // safety: test code + assert_eq!(fresh_ctx.max_tokens, 5000); // safety: test code + } + + #[tokio::test] + async fn update_context_and_get_no_concurrent_interleave() { + // Verify that concurrent updates cannot interleave during update_context_and_get. + // If the lock were released too early, a concurrent state transition could + // get mixed into the returned context. + let manager = std::sync::Arc::new(ContextManager::new(100)); + let job_id = manager + .create_job("Concurrent Race Test", "ensure atomicity") + .await + .unwrap(); // safety: test code + + let metadata = serde_json::json!({ "test": "race_condition" }); + let metadata_clone = metadata.clone(); + + // Spawn a task that will update_context_and_get + let mgr1 = std::sync::Arc::clone(&manager); + let returned_ctx_handle = tokio::spawn(async move { + mgr1.update_context_and_get(job_id, |ctx| { + ctx.metadata = metadata_clone; + ctx.max_tokens = 3000; + }) + .await + }); + + // The returned context should have *only* the metadata update, not any + // concurrent state transitions that might happen during the operation. + let returned_ctx = returned_ctx_handle.await.unwrap().unwrap(); // safety: test code + + // Verify atomicity: returned context has the metadata we set + assert_eq!(returned_ctx.metadata, metadata); // safety: test code + assert_eq!(returned_ctx.max_tokens, 3000); // safety: test code + // And it's in the initial state (Pending), not modified by concurrent workers + assert_eq!(returned_ctx.state, crate::context::JobState::Pending); // safety: test code + } } From 5f0ed66a6ba6838fd4f9b057e7346a4a6467aac3 Mon Sep 17 00:00:00 2001 From: Nige Date: Sat, 14 Mar 2026 20:06:36 +0000 Subject: [PATCH 05/10] perf(routines): avoid full message history clone each tool iteration (#1172) * perf(routines): bound tool-loop history snapshot clone cost * test(ci): annotate snapshot assertions for no-panics matcher * test(ci): keep no-panics suppression on single-line assertion * test(ci): keep snapshot tail assert single-line for no-panics * Update src/agent/routine_engine.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * chore(deps): bump yanked uds_windows in lockfile for cargo-deny --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- Cargo.lock | 26 ++++++++--------- src/agent/routine_engine.rs | 57 ++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c6b3e6f1..f51c3e65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,7 +151,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -162,7 +162,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2077,7 +2077,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2264,7 +2264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4089,7 +4089,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5433,7 +5433,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6115,7 +6115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6337,10 +6337,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7134,13 +7134,13 @@ checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" [[package]] name = "uds_windows" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7996,7 +7996,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/src/agent/routine_engine.rs b/src/agent/routine_engine.rs index a34654e9..53b5c883 100644 --- a/src/agent/routine_engine.rs +++ b/src/agent/routine_engine.rs @@ -925,7 +925,8 @@ async fn execute_lightweight_with_tools( .tool_definitions_excluding(ROUTINE_TOOL_DENYLIST) .await; - let request = ToolCompletionRequest::new(messages.clone(), tool_defs) + let request_messages = snapshot_messages_for_tool_iteration(&messages); + let request = ToolCompletionRequest::new(request_messages, tool_defs) .with_max_tokens(effective_max_tokens) .with_temperature(0.3); @@ -1001,6 +1002,31 @@ async fn execute_lightweight_with_tools( } } +// Bound per-iteration context copy cost for lightweight tool loops. +const MAX_TOOL_LOOP_MESSAGES: usize = 32; + +fn snapshot_messages_for_tool_iteration(messages: &[ChatMessage]) -> Vec { + if messages.len() <= MAX_TOOL_LOOP_MESSAGES { + return messages.to_vec(); + } + + let mut snapshot = Vec::with_capacity(MAX_TOOL_LOOP_MESSAGES); + + if let Some(first) = messages.first() + && first.role == crate::llm::Role::System + { + snapshot.push(first.clone()); + let tail_len = MAX_TOOL_LOOP_MESSAGES - 1; + let tail_start = (messages.len() - tail_len).max(1); + snapshot.extend_from_slice(&messages[tail_start..]); + } else { + let tail_start = messages.len() - MAX_TOOL_LOOP_MESSAGES; + snapshot.extend_from_slice(&messages[tail_start..]); + } + + snapshot +} + /// Tools that must never be callable from lightweight routines. /// /// These tools pose autonomy-escalation risks: a routine could self-replicate, @@ -1386,4 +1412,33 @@ mod tests { let out = super::truncate(input, 5); assert_eq!(out, "abcde..."); } + + #[test] + fn test_snapshot_messages_keeps_system_and_recent_tail() { + let mut messages = vec![crate::llm::ChatMessage::system("sys")]; + for i in 0..80 { + messages.push(crate::llm::ChatMessage::user(format!("u{i}"))); + } + + let snapshot = super::snapshot_messages_for_tool_iteration(&messages); + assert_eq!(snapshot.len(), super::MAX_TOOL_LOOP_MESSAGES); // safety: test-only no-panics CI false positive + assert_eq!(snapshot[0].role, crate::llm::Role::System); // safety: test-only no-panics CI false positive + assert_eq!(snapshot[0].content, "sys"); // safety: test-only no-panics CI false positive + let last_content = snapshot.last().map(|m| m.content.as_str()); + assert_eq!(last_content, Some("u79")); // safety: test-only no-panics CI false positive + } + + #[test] + fn test_snapshot_messages_unchanged_when_within_limit() { + let messages = vec![ + crate::llm::ChatMessage::system("sys"), + crate::llm::ChatMessage::user("a"), + crate::llm::ChatMessage::assistant("b"), + ]; + let snapshot = super::snapshot_messages_for_tool_iteration(&messages); + assert_eq!(snapshot.len(), messages.len()); // safety: test-only no-panics CI false positive + assert_eq!(snapshot[0].role, crate::llm::Role::System); // safety: test-only no-panics CI false positive + assert_eq!(snapshot[1].content, "a"); // safety: test-only no-panics CI false positive + assert_eq!(snapshot[2].content, "b"); // safety: test-only no-panics CI false positive + } } From cc52a046c1db34d388049e7f80c06b12e465675c Mon Sep 17 00:00:00 2001 From: Nige Date: Sat, 14 Mar 2026 20:06:42 +0000 Subject: [PATCH 06/10] fix(channels): use live owner binding during wasm hot activation (#1171) * fix(channels): use live owner binding during wasm hot activation * test(channels): cover owner-id store fallback without panic macros --- src/extensions/manager.rs | 159 +++++++++++++++++++++++++++++++++++--- 1 file changed, 150 insertions(+), 9 deletions(-) diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index 6488caa5..1d5fb92d 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -304,6 +304,34 @@ impl ExtensionManager { *self.relay_channel_manager.write().await = Some(channel_manager); } + async fn current_channel_owner_id(&self, name: &str) -> Option { + { + let rt_guard = self.channel_runtime.read().await; + if let Some(owner_id) = rt_guard + .as_ref() + .and_then(|rt| rt.wasm_channel_owner_ids.get(name).copied()) + { + return Some(owner_id); + } + } + + let store = self.store.as_ref()?; + let key = format!("channels.wasm_channel_owner_ids.{name}"); + match store.get_setting(&self.user_id, &key).await { + Ok(Some(serde_json::Value::Number(n))) => n.as_i64(), + Ok(Some(serde_json::Value::String(s))) => s.parse::().ok(), + Ok(Some(_)) | Ok(None) => None, + Err(e) => { + tracing::debug!( + channel = %name, + error = %e, + "Failed to read persisted wasm channel owner id" + ); + None + } + } + } + /// Check if a channel name corresponds to a relay extension (has stored stream token). pub async fn is_relay_channel(&self, name: &str) -> bool { self.secrets @@ -2980,13 +3008,7 @@ impl ExtensionManager { // Verify runtime infrastructure is available and clone Arcs so we don't // hold the RwLock guard across awaits. - let ( - channel_runtime, - channel_manager, - pairing_store, - wasm_channel_router, - wasm_channel_owner_ids, - ) = { + let (channel_runtime, channel_manager, pairing_store, wasm_channel_router) = { let rt_guard = self.channel_runtime.read().await; let rt = rt_guard.as_ref().ok_or_else(|| { ExtensionError::ActivationFailed("WASM channel runtime not configured".to_string()) @@ -2996,7 +3018,6 @@ impl ExtensionManager { Arc::clone(&rt.channel_manager), Arc::clone(&rt.pairing_store), Arc::clone(&rt.wasm_channel_router), - rt.wasm_channel_owner_ids.clone(), ) }; @@ -3067,7 +3088,7 @@ impl ExtensionManager { ); } - if let Some(&owner_id) = wasm_channel_owner_ids.get(channel_name.as_str()) { + if let Some(owner_id) = self.current_channel_owner_id(&channel_name).await { config_updates.insert("owner_id".to_string(), serde_json::json!(owner_id)); } @@ -4744,6 +4765,126 @@ mod tests { ) } + #[tokio::test] + async fn test_current_channel_owner_id_uses_runtime_state() -> Result<(), String> { + let manager = make_manager_with_temp_dirs(); + if manager.current_channel_owner_id("telegram").await.is_some() { + return Err("expected no owner id for telegram before runtime setup".to_string()); + } + + let channels = Arc::new(crate::channels::ChannelManager::new()); + let runtime = Arc::new( + crate::channels::wasm::WasmChannelRuntime::new( + crate::channels::wasm::WasmChannelRuntimeConfig::default(), + ) + .map_err(|e| format!("runtime init failed: {e}"))?, + ); + let pairing_store = Arc::new(crate::pairing::PairingStore::new()); + let router = Arc::new(crate::channels::wasm::WasmChannelRouter::new()); + let mut owner_ids = std::collections::HashMap::new(); + owner_ids.insert("telegram".to_string(), 12345_i64); + + manager + .set_channel_runtime(channels, runtime, pairing_store, router, owner_ids) + .await; + + if manager.current_channel_owner_id("telegram").await != Some(12345_i64) { + return Err("expected runtime owner id fast-path for telegram".to_string()); + } + if manager.current_channel_owner_id("slack").await.is_some() { + return Err("expected no owner id for slack".to_string()); + } + + Ok(()) + } + + #[cfg(feature = "libsql")] + #[tokio::test] + async fn test_current_channel_owner_id_uses_store_fallback() -> Result<(), String> { + use crate::db::{Database, SettingsStore}; + + let dir = tempfile::tempdir().map_err(|e| format!("tempdir failed: {e}"))?; + let db_path = dir.path().join("owner-id.db"); + + let db = Arc::new( + crate::db::libsql::LibSqlBackend::new_local(&db_path) + .await + .map_err(|e| format!("create local libsql backend failed: {e}"))?, + ); + db.run_migrations() + .await + .map_err(|e| format!("run libsql migrations failed: {e}"))?; + + let tools_dir = dir.path().join("tools"); + let channels_dir = dir.path().join("channels"); + std::fs::create_dir_all(&tools_dir).ok(); + std::fs::create_dir_all(&channels_dir).ok(); + + use crate::secrets::{InMemorySecretsStore, SecretsCrypto}; + use crate::testing::credentials::TEST_CRYPTO_KEY; + use crate::tools::ToolRegistry; + use crate::tools::mcp::process::McpProcessManager; + use crate::tools::mcp::session::McpSessionManager; + + let master_key = secrecy::SecretString::from(TEST_CRYPTO_KEY.to_string()); + let crypto = Arc::new( + SecretsCrypto::new(master_key) + .map_err(|e| format!("create secrets crypto failed: {e}"))?, + ); + + let manager = ExtensionManager::new( + Arc::new(McpSessionManager::new()), + Arc::new(McpProcessManager::new()), + Arc::new(InMemorySecretsStore::new(crypto)), + Arc::new(ToolRegistry::new()), + None, + None, + tools_dir, + channels_dir, + None, + "test".to_string(), + Some(db.clone() as Arc), + Vec::new(), + ); + + if manager.current_channel_owner_id("telegram").await.is_some() { + return Err("expected no owner id before settings seed".to_string()); + } + + db.set_setting( + "test", + "channels.wasm_channel_owner_ids.telegram", + &serde_json::json!(54321_i64), + ) + .await + .map_err(|e| format!("persist owner id in settings failed: {e}"))?; + + if manager.current_channel_owner_id("telegram").await != Some(54321_i64) { + return Err("expected store fallback owner id for telegram".to_string()); + } + + let channels = Arc::new(crate::channels::ChannelManager::new()); + let runtime = Arc::new( + crate::channels::wasm::WasmChannelRuntime::new( + crate::channels::wasm::WasmChannelRuntimeConfig::default(), + ) + .map_err(|e| format!("runtime init failed: {e}"))?, + ); + let pairing_store = Arc::new(crate::pairing::PairingStore::new()); + let router = Arc::new(crate::channels::wasm::WasmChannelRouter::new()); + let mut owner_ids = std::collections::HashMap::new(); + owner_ids.insert("telegram".to_string(), 12345_i64); + manager + .set_channel_runtime(channels, runtime, pairing_store, router, owner_ids) + .await; + + if manager.current_channel_owner_id("telegram").await != Some(12345_i64) { + return Err("expected runtime fast-path owner id precedence".to_string()); + } + + Ok(()) + } + // ── resolve_env_credentials tests ──────────────────────────────────── #[test] From ffe384b66ea326d58056cd6315b50fefa7c6beee Mon Sep 17 00:00:00 2001 From: Nige Date: Sat, 14 Mar 2026 20:06:48 +0000 Subject: [PATCH 07/10] fix(llm): add stop_sequences parity for tool completions (#1170) * fix(llm): add stop_sequences parity for tool completions * refactor(web-openai): dedupe request builders and satisfy no-panics gate * test(llm): mark multiline assert with safety comment for CI gate * test(llm): make safety-marked assert formatting-stable --- src/channels/web/openai_compat.rs | 88 +++++++++++++++---------------- src/llm/bedrock.rs | 7 ++- src/llm/nearai_chat.rs | 6 +++ src/llm/provider.rs | 27 ++++++++-- src/llm/response_cache.rs | 1 + src/orchestrator/api.rs | 1 + src/worker/api.rs | 2 + 7 files changed, 81 insertions(+), 51 deletions(-) diff --git a/src/channels/web/openai_compat.rs b/src/channels/web/openai_compat.rs index e329693a..51577e06 100644 --- a/src/channels/web/openai_compat.rs +++ b/src/channels/web/openai_compat.rs @@ -419,6 +419,44 @@ fn parse_stop(val: &serde_json::Value) -> Option> { } } +fn build_completion_request( + req: &OpenAiChatRequest, + messages: Vec, +) -> CompletionRequest { + let mut comp_req = CompletionRequest::new(messages).with_model(req.model.clone()); + if let Some(t) = req.temperature { + comp_req = comp_req.with_temperature(t); + } + if let Some(mt) = req.max_tokens { + comp_req = comp_req.with_max_tokens(mt); + } + if let Some(stops) = req.stop.as_ref().and_then(parse_stop) { + comp_req.stop_sequences = Some(stops); + } + comp_req +} + +fn build_tool_request( + req: &OpenAiChatRequest, + messages: Vec, +) -> ToolCompletionRequest { + let tools = convert_tools(req.tools.as_deref().unwrap_or(&[])); + let mut tool_req = ToolCompletionRequest::new(messages, tools).with_model(req.model.clone()); + if let Some(t) = req.temperature { + tool_req = tool_req.with_temperature(t); + } + if let Some(mt) = req.max_tokens { + tool_req = tool_req.with_max_tokens(mt); + } + if let Some(stops) = req.stop.as_ref().and_then(parse_stop) { + tool_req = tool_req.with_stop_sequences(stops); + } + if let Some(choice) = req.tool_choice.as_ref().and_then(normalize_tool_choice) { + tool_req = tool_req.with_tool_choice(choice); + } + tool_req +} + // --------------------------------------------------------------------------- // Handlers // --------------------------------------------------------------------------- @@ -476,19 +514,7 @@ pub async fn chat_completions_handler( let created = unix_timestamp(); if has_tools { - let tools = convert_tools(req.tools.as_deref().unwrap_or(&[])); - let mut tool_req = ToolCompletionRequest::new(messages, tools).with_model(req.model); - if let Some(t) = req.temperature { - tool_req = tool_req.with_temperature(t); - } - if let Some(mt) = req.max_tokens { - tool_req = tool_req.with_max_tokens(mt); - } - if let Some(ref tc) = req.tool_choice - && let Some(choice) = normalize_tool_choice(tc) - { - tool_req = tool_req.with_tool_choice(choice); - } + let tool_req = build_tool_request(&req, messages); let resp = llm .complete_with_tools(tool_req) @@ -527,16 +553,7 @@ pub async fn chat_completions_handler( Ok(Json(response).into_response()) } else { - let mut comp_req = CompletionRequest::new(messages).with_model(req.model); - if let Some(t) = req.temperature { - comp_req = comp_req.with_temperature(t); - } - if let Some(mt) = req.max_tokens { - comp_req = comp_req.with_max_tokens(mt); - } - if let Some(ref stop_val) = req.stop { - comp_req.stop_sequences = parse_stop(stop_val); - } + let comp_req = build_completion_request(&req, messages); let resp = llm.complete(comp_req).await.map_err(map_llm_error)?; let model_name = llm.effective_model_name(Some(requested_model.as_str())); @@ -596,35 +613,14 @@ async fn handle_streaming( } let llm_result = if has_tools { - let tools = convert_tools(req.tools.as_deref().unwrap_or(&[])); - let mut tool_req = ToolCompletionRequest::new(messages, tools).with_model(req.model); - if let Some(t) = req.temperature { - tool_req = tool_req.with_temperature(t); - } - if let Some(mt) = req.max_tokens { - tool_req = tool_req.with_max_tokens(mt); - } - if let Some(ref tc) = req.tool_choice - && let Some(choice) = normalize_tool_choice(tc) - { - tool_req = tool_req.with_tool_choice(choice); - } + let tool_req = build_tool_request(&req, messages); LlmResult::WithTools( llm.complete_with_tools(tool_req) .await .map_err(map_llm_error)?, ) } else { - let mut comp_req = CompletionRequest::new(messages).with_model(req.model); - if let Some(t) = req.temperature { - comp_req = comp_req.with_temperature(t); - } - if let Some(mt) = req.max_tokens { - comp_req = comp_req.with_max_tokens(mt); - } - if let Some(ref stop_val) = req.stop { - comp_req.stop_sequences = parse_stop(stop_val); - } + let comp_req = build_completion_request(&req, messages); LlmResult::Simple(llm.complete(comp_req).await.map_err(map_llm_error)?) }; let model_name = llm.effective_model_name(Some(requested_model.as_str())); diff --git a/src/llm/bedrock.rs b/src/llm/bedrock.rs index ebde19f1..5d6e121e 100644 --- a/src/llm/bedrock.rs +++ b/src/llm/bedrock.rs @@ -176,8 +176,11 @@ impl LlmProvider for BedrockProvider { builder = builder.tool_config(tc); } - if let Some(config) = build_inference_config(request.temperature, request.max_tokens, None) - { + if let Some(config) = build_inference_config( + request.temperature, + request.max_tokens, + request.stop_sequences.as_deref(), + ) { builder = builder.inference_config(config); } diff --git a/src/llm/nearai_chat.rs b/src/llm/nearai_chat.rs index 0c0335bd..bf2b8738 100644 --- a/src/llm/nearai_chat.rs +++ b/src/llm/nearai_chat.rs @@ -475,6 +475,7 @@ impl LlmProvider for NearAiChatProvider { messages, temperature: req.temperature, max_tokens: req.max_tokens, + stop: req.stop_sequences, tools: None, tool_choice: None, }; @@ -554,6 +555,7 @@ impl LlmProvider for NearAiChatProvider { messages, temperature: req.temperature, max_tokens: req.max_tokens, + stop: req.stop_sequences, tools: if tools.is_empty() { None } else { Some(tools) }, tool_choice: req.tool_choice, }; @@ -680,6 +682,8 @@ struct ChatCompletionRequest { #[serde(skip_serializing_if = "Option::is_none")] max_tokens: Option, #[serde(skip_serializing_if = "Option::is_none")] + stop: Option>, + #[serde(skip_serializing_if = "Option::is_none")] tools: Option>, #[serde(skip_serializing_if = "Option::is_none")] tool_choice: Option, @@ -1666,6 +1670,7 @@ mod tests { }], temperature: None, max_tokens: None, + stop: None, tools: None, tool_choice: None, }; @@ -1687,6 +1692,7 @@ mod tests { messages: vec![], temperature: Some(0.7), max_tokens: Some(1024), + stop: None, tools: Some(vec![ChatCompletionTool { tool_type: "function".to_string(), function: ChatCompletionFunction { diff --git a/src/llm/provider.rs b/src/llm/provider.rs index 787bbff1..8a213031 100644 --- a/src/llm/provider.rs +++ b/src/llm/provider.rs @@ -251,6 +251,7 @@ pub struct ToolCompletionRequest { pub model: Option, pub max_tokens: Option, pub temperature: Option, + pub stop_sequences: Option>, /// How to handle tool use: "auto", "required", or "none". pub tool_choice: Option, /// Opaque metadata passed through to the provider (e.g. thread_id for chaining). @@ -266,6 +267,7 @@ impl ToolCompletionRequest { model: None, max_tokens: None, temperature: None, + stop_sequences: None, tool_choice: None, metadata: std::collections::HashMap::new(), } @@ -289,6 +291,12 @@ impl ToolCompletionRequest { self } + /// Set stop sequences. + pub fn with_stop_sequences(mut self, stop_sequences: Vec) -> Self { + self.stop_sequences = Some(stop_sequences); + self + } + /// Set tool choice mode. pub fn with_tool_choice(mut self, choice: impl Into) -> Self { self.tool_choice = Some(choice.into()); @@ -504,8 +512,6 @@ pub fn strip_unsupported_completion_params( /// This is the single helper function used by all providers to remove /// parameters they don't support from tool calls, replacing duplicate stringly-typed logic. /// -/// Note: Only `Temperature` and `MaxTokens` are supported in `ToolCompletionRequest`. -/// `StopSequences` is only available in `CompletionRequest` and is not applicable to tool calls. pub fn strip_unsupported_tool_params( unsupported: &std::collections::HashSet, req: &mut ToolCompletionRequest, @@ -519,7 +525,9 @@ pub fn strip_unsupported_tool_params( if unsupported.contains(UnsupportedParam::MaxTokens.name()) { req.max_tokens = None; } - // Note: StopSequences is not a field in ToolCompletionRequest, so no action needed + if unsupported.contains(UnsupportedParam::StopSequences.name()) { + req.stop_sequences = None; + } } #[cfg(test)] @@ -651,4 +659,17 @@ mod tests { assert!(messages[2].tool_call_id.is_none()); assert!(messages[2].name.is_none()); } + + #[test] + fn test_strip_unsupported_tool_params_strips_stop_sequences() { + let mut unsupported = std::collections::HashSet::new(); + unsupported.insert(UnsupportedParam::StopSequences.name().to_string()); + + let mut req = ToolCompletionRequest::new(vec![ChatMessage::user("hello")], vec![]); + req.stop_sequences = Some(vec!["STOP".to_string()]); + + strip_unsupported_tool_params(&unsupported, &mut req); + + assert!(req.stop_sequences.is_none()); // safety: test assertion for explicit strip behavior + } } diff --git a/src/llm/response_cache.rs b/src/llm/response_cache.rs index b8238427..d7746f60 100644 --- a/src/llm/response_cache.rs +++ b/src/llm/response_cache.rs @@ -548,6 +548,7 @@ mod tests { model: None, max_tokens: None, temperature: None, + stop_sequences: None, tool_choice: None, metadata: Default::default(), }; diff --git a/src/orchestrator/api.rs b/src/orchestrator/api.rs index 80e09073..b46aa8c6 100644 --- a/src/orchestrator/api.rs +++ b/src/orchestrator/api.rs @@ -176,6 +176,7 @@ async fn llm_complete_with_tools( model: req.model, max_tokens: req.max_tokens, temperature: req.temperature, + stop_sequences: req.stop_sequences, tool_choice: req.tool_choice, metadata: std::collections::HashMap::new(), }; diff --git a/src/worker/api.rs b/src/worker/api.rs index 459375b4..43fda2dd 100644 --- a/src/worker/api.rs +++ b/src/worker/api.rs @@ -65,6 +65,7 @@ pub struct ProxyToolCompletionRequest { pub model: Option, pub max_tokens: Option, pub temperature: Option, + pub stop_sequences: Option>, pub tool_choice: Option, } @@ -251,6 +252,7 @@ impl WorkerHttpClient { model: request.model.clone(), max_tokens: request.max_tokens, temperature: request.temperature, + stop_sequences: request.stop_sequences.clone(), tool_choice: request.tool_choice.clone(), }; From 994a0b194fd3b59db9daa3e3b75ade71940205bd Mon Sep 17 00:00:00 2001 From: Nick Pismenkov <50764773+nickpismenkov@users.noreply.github.com> Date: Sat, 14 Mar 2026 13:06:59 -0700 Subject: [PATCH 08/10] fix: N+1 query pattern in event trigger loop (routine_engine) (#1163) * fix: N+1 query pattern in event trigger loop (routine_engine) * fix: linter --- src/agent/routine_engine.rs | 64 ++- src/db/libsql/routines.rs | 57 +- src/db/mod.rs | 4 + src/db/postgres.rs | 9 + src/history/store.rs | 39 ++ tests/batch_query_tests.rs | 509 +++++++++++++++++ .../e2e/scenarios/test_routine_event_batch.py | 534 ++++++++++++++++++ 7 files changed, 1211 insertions(+), 5 deletions(-) create mode 100644 tests/batch_query_tests.rs create mode 100644 tests/e2e/scenarios/test_routine_event_batch.py diff --git a/src/agent/routine_engine.rs b/src/agent/routine_engine.rs index 53b5c883..739b20d7 100644 --- a/src/agent/routine_engine.rs +++ b/src/agent/routine_engine.rs @@ -139,6 +139,32 @@ impl RoutineEngine { let cache = self.event_cache.read().await; let mut fired = 0; + // Collect routine IDs for batch query + let routine_ids: Vec = cache + .iter() + .filter_map(|matcher| match matcher { + EventMatcher::Message { routine, .. } => Some(routine.id), + EventMatcher::System { .. } => None, + }) + .collect(); + + if routine_ids.is_empty() { + return 0; + } + + // Single batch query instead of N queries + let concurrent_counts = match self + .store + .count_running_routine_runs_batch(&routine_ids) + .await + { + Ok(counts) => counts, + Err(e) => { + tracing::error!("Failed to batch-load concurrent counts: {}", e); + return 0; + } + }; + for matcher in cache.iter() { let (routine, re) = match matcher { EventMatcher::Message { routine, regex } => (routine, regex), @@ -164,8 +190,9 @@ impl RoutineEngine { continue; } - // Concurrent run check - if !self.check_concurrent(routine).await { + // Concurrent run check (using batch-loaded counts) + let running_count = concurrent_counts.get(&routine.id).copied().unwrap_or(0); + if running_count >= routine.guardrails.max_concurrent as i64 { tracing::trace!(routine = %routine.name, "Skipped: max concurrent reached"); continue; } @@ -197,6 +224,35 @@ impl RoutineEngine { let cache = self.event_cache.read().await; let mut fired = 0; + // Collect routine IDs for batch query + let routine_ids: Vec = cache + .iter() + .filter_map(|matcher| match matcher { + EventMatcher::System { routine } => Some(routine.id), + EventMatcher::Message { .. } => None, + }) + .collect(); + + if routine_ids.is_empty() { + return 0; + } + + // Single batch query instead of N queries + let concurrent_counts = match self + .store + .count_running_routine_runs_batch(&routine_ids) + .await + { + Ok(counts) => counts, + Err(e) => { + tracing::error!( + "Failed to batch-load concurrent counts for system events: {}", + e + ); + return 0; + } + }; + for matcher in cache.iter() { let routine = match matcher { EventMatcher::System { routine } => routine, @@ -248,7 +304,9 @@ impl RoutineEngine { continue; } - if !self.check_concurrent(routine).await { + // Concurrent run check (using batch-loaded counts) + let running_count = concurrent_counts.get(&routine.id).copied().unwrap_or(0); + if running_count >= routine.guardrails.max_concurrent as i64 { tracing::debug!(routine = %routine.name, "Skipped: max concurrent reached"); continue; } diff --git a/src/db/libsql/routines.rs b/src/db/libsql/routines.rs index 3f2629ea..dd9fd6c0 100644 --- a/src/db/libsql/routines.rs +++ b/src/db/libsql/routines.rs @@ -1,13 +1,15 @@ //! Routine-related RoutineStore implementation for LibSqlBackend. +use std::collections::{HashMap, HashSet}; + use async_trait::async_trait; use chrono::{DateTime, Utc}; use libsql::params; use uuid::Uuid; use super::{ - LibSqlBackend, ROUTINE_COLUMNS, ROUTINE_RUN_COLUMNS, fmt_opt_ts, fmt_ts, get_i64, opt_text, - opt_text_owned, row_to_routine_libsql, row_to_routine_run_libsql, + LibSqlBackend, ROUTINE_COLUMNS, ROUTINE_RUN_COLUMNS, fmt_opt_ts, fmt_ts, get_i64, get_text, + opt_text, opt_text_owned, row_to_routine_libsql, row_to_routine_run_libsql, }; use crate::agent::routine::{Routine, RoutineRun, RunStatus}; use crate::db::RoutineStore; @@ -409,6 +411,57 @@ impl RoutineStore for LibSqlBackend { } } + async fn count_running_routine_runs_batch( + &self, + routine_ids: &[Uuid], + ) -> Result, DatabaseError> { + if routine_ids.is_empty() { + return Ok(HashMap::new()); + } + + let mut counts = HashMap::new(); + let conn = self.connect().await?; + + // Query all running routines and filter in memory + // This is simpler for libSQL than building dynamic parameter lists + let mut rows = conn + .query( + "SELECT routine_id, COUNT(*) as cnt FROM routine_runs + WHERE status = 'running' + GROUP BY routine_id", + params![], + ) + .await + .map_err(|e| { + DatabaseError::Query(format!("Failed to batch count running routines: {}", e)) + })?; + + let routine_id_set: HashSet = routine_ids.iter().copied().collect(); + + while let Some(row) = rows + .next() + .await + .map_err(|e| DatabaseError::Query(e.to_string()))? + { + let id_str: String = get_text(&row, 0); + let id = Uuid::parse_str(&id_str) + .map_err(|e| DatabaseError::Query(format!("Invalid routine UUID: {}", e)))?; + + // Only include if this routine ID was requested + if routine_id_set.contains(&id) { + let cnt: i64 = get_i64(&row, 1); + counts.insert(id, cnt); + } + } + + // Ensure all requested IDs are in the map (defaults to 0 for no running runs) + for id in routine_ids { + counts.entry(*id).or_insert(0); + } + + Ok(counts) + } + async fn link_routine_run_to_job( &self, run_id: Uuid, diff --git a/src/db/mod.rs b/src/db/mod.rs index 4afd1db8..a306c14b 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -387,6 +387,10 @@ pub trait RoutineStore: Send + Sync { limit: i64, ) -> Result, DatabaseError>; async fn count_running_routine_runs(&self, routine_id: Uuid) -> Result; + async fn count_running_routine_runs_batch( + &self, + routine_ids: &[Uuid], + ) -> Result, DatabaseError>; async fn link_routine_run_to_job( &self, run_id: Uuid, diff --git a/src/db/postgres.rs b/src/db/postgres.rs index 2cf6a65a..8c18e252 100644 --- a/src/db/postgres.rs +++ b/src/db/postgres.rs @@ -487,6 +487,15 @@ impl RoutineStore for PgBackend { self.store.count_running_routine_runs(routine_id).await } + async fn count_running_routine_runs_batch( + &self, + routine_ids: &[Uuid], + ) -> Result, DatabaseError> { + self.store + .count_running_routine_runs_batch(routine_ids) + .await + } + async fn link_routine_run_to_job( &self, run_id: Uuid, diff --git a/src/history/store.rs b/src/history/store.rs index 83f60d70..17fa96fd 100644 --- a/src/history/store.rs +++ b/src/history/store.rs @@ -1,5 +1,8 @@ //! PostgreSQL store for persisting agent data. +#[cfg(feature = "postgres")] +use std::collections::HashMap; + use chrono::{DateTime, Utc}; #[cfg(feature = "postgres")] use deadpool_postgres::{Config, Pool}; @@ -1294,6 +1297,42 @@ impl Store { Ok(row.get("cnt")) } + /// Batch-load concurrent run counts for multiple routines in a single query. + /// Returns a map where missing routine IDs default to 0. + #[cfg(feature = "postgres")] + pub async fn count_running_routine_runs_batch( + &self, + routine_ids: &[Uuid], + ) -> Result, DatabaseError> { + if routine_ids.is_empty() { + return Ok(HashMap::new()); + } + + let conn = self.conn().await?; + let rows = conn + .query( + "SELECT routine_id, COUNT(*) as cnt FROM routine_runs + WHERE routine_id = ANY($1) AND status = 'running' + GROUP BY routine_id", + &[&routine_ids], + ) + .await?; + + let mut counts = HashMap::new(); + for row in rows { + let id: Uuid = row.get("routine_id"); + let cnt: i64 = row.get("cnt"); + counts.insert(id, cnt); + } + + // Ensure all requested IDs are in the map (defaults to 0 for no running runs) + for id in routine_ids { + counts.entry(*id).or_insert(0); + } + + Ok(counts) + } + /// Link a routine run to a dispatched job. pub async fn link_routine_run_to_job( &self, diff --git a/tests/batch_query_tests.rs b/tests/batch_query_tests.rs new file mode 100644 index 00000000..d7365287 --- /dev/null +++ b/tests/batch_query_tests.rs @@ -0,0 +1,509 @@ +//! Tests for batch loading routine concurrent counts (N+1 query fix). +//! +//! Verifies: +//! 1. Batch query returns correct counts for multiple routines +//! 2. Concurrent limit enforcement uses batch counts correctly + +#[cfg(feature = "libsql")] +mod tests { + use std::sync::Arc; + + use chrono::Utc; + use uuid::Uuid; + + use ironclaw::agent::routine::{ + Routine, RoutineAction, RoutineGuardrails, RoutineRun, RunStatus, Trigger, + }; + use ironclaw::db::Database; + + async fn create_test_db() -> (Arc, tempfile::TempDir) { + use ironclaw::db::libsql::LibSqlBackend; + + let temp_dir = tempfile::tempdir().expect("tempdir"); + let db_path = temp_dir.path().join("test.db"); + let backend = LibSqlBackend::new_local(&db_path) + .await + .expect("LibSqlBackend"); + backend.run_migrations().await.expect("migrations"); + let db: Arc = Arc::new(backend); + (db, temp_dir) + } + + // ----------------------------------------------------------------------- + // Test 1: Batch query returns correct counts for multiple routines + // ----------------------------------------------------------------------- + + #[tokio::test] + async fn batch_query_empty_list() { + let (db, _tmp) = create_test_db().await; + let counts = db + .count_running_routine_runs_batch(&[]) + .await + .expect("batch query should not fail"); + assert!(counts.is_empty(), "Empty input should return empty map"); + } + + #[tokio::test] + async fn batch_query_single_routine() { + let (db, _tmp) = create_test_db().await; + let routine_id = Uuid::new_v4(); + + // Create routine + let routine = Routine { + id: routine_id, + name: "test-routine".to_string(), + description: "Test".to_string(), + user_id: "default".to_string(), + enabled: true, + trigger: Trigger::Cron { + schedule: "* * * * *".to_string(), + timezone: None, + }, + action: RoutineAction::Lightweight { + prompt: "test".to_string(), + context_paths: vec![], + max_tokens: 1000, + use_tools: false, + max_tool_rounds: 3, + }, + guardrails: RoutineGuardrails { + cooldown: std::time::Duration::from_secs(0), + max_concurrent: 5, + dedup_window: None, + }, + notify: Default::default(), + last_run_at: None, + next_fire_at: None, + run_count: 0, + consecutive_failures: 0, + state: serde_json::json!({}), + created_at: Utc::now(), + updated_at: Utc::now(), + }; + db.create_routine(&routine).await.expect("create routine"); + + // Create 3 running runs + for _ in 0..3 { + let run = RoutineRun { + id: Uuid::new_v4(), + routine_id, + trigger_type: "cron".to_string(), + trigger_detail: None, + started_at: Utc::now(), + completed_at: None, + status: RunStatus::Running, + result_summary: None, + tokens_used: None, + job_id: None, + created_at: Utc::now(), + }; + db.create_routine_run(&run).await.expect("create run"); + } + + // Batch query for single routine + let counts = db + .count_running_routine_runs_batch(&[routine_id]) + .await + .expect("batch query should work"); + + assert_eq!(counts.len(), 1, "Should return 1 routine"); + assert_eq!(counts[&routine_id], 3, "Should count 3 running runs"); + } + + #[tokio::test] + async fn batch_query_multiple_routines_different_counts() { + let (db, _tmp) = create_test_db().await; + + let r1 = Uuid::new_v4(); + let r2 = Uuid::new_v4(); + let r3 = Uuid::new_v4(); + + // Create 3 routines + for routine_id in [r1, r2, r3] { + let routine = Routine { + id: routine_id, + name: format!("routine-{}", routine_id), + description: "Test".to_string(), + user_id: "default".to_string(), + enabled: true, + trigger: Trigger::Cron { + schedule: "* * * * *".to_string(), + timezone: None, + }, + action: RoutineAction::Lightweight { + prompt: "test".to_string(), + context_paths: vec![], + max_tokens: 1000, + use_tools: false, + max_tool_rounds: 3, + }, + guardrails: RoutineGuardrails { + cooldown: std::time::Duration::from_secs(0), + max_concurrent: 5, + dedup_window: None, + }, + notify: Default::default(), + last_run_at: None, + next_fire_at: None, + run_count: 0, + consecutive_failures: 0, + state: serde_json::json!({}), + created_at: Utc::now(), + updated_at: Utc::now(), + }; + db.create_routine(&routine).await.expect("create routine"); + } + + // r1: 2 running + for _ in 0..2 { + let run = RoutineRun { + id: Uuid::new_v4(), + routine_id: r1, + trigger_type: "cron".to_string(), + trigger_detail: None, + started_at: Utc::now(), + completed_at: None, + status: RunStatus::Running, + result_summary: None, + tokens_used: None, + job_id: None, + created_at: Utc::now(), + }; + db.create_routine_run(&run).await.expect("create run"); + } + + // r2: 1 running + let run = RoutineRun { + id: Uuid::new_v4(), + routine_id: r2, + trigger_type: "cron".to_string(), + trigger_detail: None, + started_at: Utc::now(), + completed_at: None, + status: RunStatus::Running, + result_summary: None, + tokens_used: None, + job_id: None, + created_at: Utc::now(), + }; + db.create_routine_run(&run).await.expect("create run"); + + // r3: 0 running (but has 1 Ok result) + let run = RoutineRun { + id: Uuid::new_v4(), + routine_id: r3, + trigger_type: "cron".to_string(), + trigger_detail: None, + started_at: Utc::now(), + completed_at: Some(Utc::now()), + status: RunStatus::Ok, + result_summary: None, + tokens_used: None, + job_id: None, + created_at: Utc::now(), + }; + db.create_routine_run(&run).await.expect("create run"); + + // Single batch query for all 3 + let counts = db + .count_running_routine_runs_batch(&[r1, r2, r3]) + .await + .expect("batch query should work"); + + assert_eq!(counts.len(), 3, "Should return 3 routines"); + assert_eq!(counts[&r1], 2, "r1 should have 2 running"); + assert_eq!(counts[&r2], 1, "r2 should have 1 running"); + assert_eq!( + counts[&r3], 0, + "r3 should have 0 running (Ok status is not running)" + ); + } + + #[tokio::test] + async fn batch_query_missing_routines_default_to_zero() { + let (db, _tmp) = create_test_db().await; + + let r1 = Uuid::new_v4(); + let r2 = Uuid::new_v4(); + let r3 = Uuid::new_v4(); // This one won't exist + + // Only create r1 + let routine = Routine { + id: r1, + name: "routine-1".to_string(), + description: "Test".to_string(), + user_id: "default".to_string(), + enabled: true, + trigger: Trigger::Cron { + schedule: "* * * * *".to_string(), + timezone: None, + }, + action: RoutineAction::Lightweight { + prompt: "test".to_string(), + context_paths: vec![], + max_tokens: 1000, + use_tools: false, + max_tool_rounds: 3, + }, + guardrails: RoutineGuardrails { + cooldown: std::time::Duration::from_secs(0), + max_concurrent: 5, + dedup_window: None, + }, + notify: Default::default(), + last_run_at: None, + next_fire_at: None, + run_count: 0, + consecutive_failures: 0, + state: serde_json::json!({}), + created_at: Utc::now(), + updated_at: Utc::now(), + }; + db.create_routine(&routine).await.expect("create routine"); + + // r1 has 1 running + let run = RoutineRun { + id: Uuid::new_v4(), + routine_id: r1, + trigger_type: "cron".to_string(), + trigger_detail: None, + started_at: Utc::now(), + completed_at: None, + status: RunStatus::Running, + result_summary: None, + tokens_used: None, + job_id: None, + created_at: Utc::now(), + }; + db.create_routine_run(&run).await.expect("create run"); + + // Query for r1, r2 (doesn't exist), r3 (doesn't exist) + let counts = db + .count_running_routine_runs_batch(&[r1, r2, r3]) + .await + .expect("batch query should work"); + + assert_eq!(counts.len(), 3, "Should have all 3 routine IDs"); + assert_eq!(counts[&r1], 1, "r1 should have 1 running"); + assert_eq!(counts[&r2], 0, "r2 should default to 0"); + assert_eq!(counts[&r3], 0, "r3 should default to 0"); + } + + #[tokio::test] + async fn batch_query_only_counts_running_status() { + let (db, _tmp) = create_test_db().await; + let routine_id = Uuid::new_v4(); + + // Create routine + let routine = Routine { + id: routine_id, + name: "test-routine".to_string(), + description: "Test".to_string(), + user_id: "default".to_string(), + enabled: true, + trigger: Trigger::Cron { + schedule: "* * * * *".to_string(), + timezone: None, + }, + action: RoutineAction::Lightweight { + prompt: "test".to_string(), + context_paths: vec![], + max_tokens: 1000, + use_tools: false, + max_tool_rounds: 3, + }, + guardrails: RoutineGuardrails { + cooldown: std::time::Duration::from_secs(0), + max_concurrent: 5, + dedup_window: None, + }, + notify: Default::default(), + last_run_at: None, + next_fire_at: None, + run_count: 0, + consecutive_failures: 0, + state: serde_json::json!({}), + created_at: Utc::now(), + updated_at: Utc::now(), + }; + db.create_routine(&routine).await.expect("create routine"); + + // Create 5 runs with mixed statuses + let statuses = [ + RunStatus::Running, + RunStatus::Running, + RunStatus::Ok, + RunStatus::Failed, + RunStatus::Attention, + ]; + + for status in statuses.iter() { + let run = RoutineRun { + id: Uuid::new_v4(), + routine_id, + trigger_type: "cron".to_string(), + trigger_detail: None, + started_at: Utc::now(), + completed_at: Some(Utc::now()), + status: *status, + result_summary: None, + tokens_used: None, + job_id: None, + created_at: Utc::now(), + }; + db.create_routine_run(&run).await.expect("create run"); + } + + // Batch query should only count Running status + let counts = db + .count_running_routine_runs_batch(&[routine_id]) + .await + .expect("batch query should work"); + + assert_eq!( + counts[&routine_id], 2, + "Should only count 2 Running status runs" + ); + } + + // ----------------------------------------------------------------------- + // Test 2: Concurrent limit enforcement uses batch counts + // ----------------------------------------------------------------------- + + #[tokio::test] + async fn concurrent_limit_enforcement_with_batch_counts() { + let (db, _tmp) = create_test_db().await; + + let r1 = Uuid::new_v4(); + let r2 = Uuid::new_v4(); + + // Create 2 routines with max_concurrent=1 (r1) and max_concurrent=2 (r2) + for (routine_id, max_concurrent) in [(r1, 1), (r2, 2)] { + let routine = Routine { + id: routine_id, + name: format!("routine-{}", routine_id), + description: "Test".to_string(), + user_id: "default".to_string(), + enabled: true, + trigger: Trigger::Cron { + schedule: "* * * * *".to_string(), + timezone: None, + }, + action: RoutineAction::Lightweight { + prompt: "test".to_string(), + context_paths: vec![], + max_tokens: 1000, + use_tools: false, + max_tool_rounds: 3, + }, + guardrails: RoutineGuardrails { + cooldown: std::time::Duration::from_secs(0), + max_concurrent, + dedup_window: None, + }, + notify: Default::default(), + last_run_at: None, + next_fire_at: None, + run_count: 0, + consecutive_failures: 0, + state: serde_json::json!({}), + created_at: Utc::now(), + updated_at: Utc::now(), + }; + db.create_routine(&routine).await.expect("create routine"); + } + + // r1: create 1 running run (will hit max_concurrent=1) + let run = RoutineRun { + id: Uuid::new_v4(), + routine_id: r1, + trigger_type: "cron".to_string(), + trigger_detail: None, + started_at: Utc::now(), + completed_at: None, + status: RunStatus::Running, + result_summary: None, + tokens_used: None, + job_id: None, + created_at: Utc::now(), + }; + db.create_routine_run(&run).await.expect("create run"); + + // r2: create 2 running runs (will hit max_concurrent=2) + for _ in 0..2 { + let run = RoutineRun { + id: Uuid::new_v4(), + routine_id: r2, + trigger_type: "cron".to_string(), + trigger_detail: None, + started_at: Utc::now(), + completed_at: None, + status: RunStatus::Running, + result_summary: None, + tokens_used: None, + job_id: None, + created_at: Utc::now(), + }; + db.create_routine_run(&run).await.expect("create run"); + } + + // Batch query should return correct counts + let counts = db + .count_running_routine_runs_batch(&[r1, r2]) + .await + .expect("batch query should work"); + + // Verify counts match the limits + assert_eq!( + counts[&r1], 1, + "r1 should have 1 running (at max_concurrent=1)" + ); + assert_eq!( + counts[&r2], 2, + "r2 should have 2 running (at max_concurrent=2)" + ); + + // Now verify the limit enforcement logic + let r1_routine = db + .get_routine(r1) + .await + .expect("get routine") + .expect("routine exists"); + let r2_routine = db + .get_routine(r2) + .await + .expect("get routine") + .expect("routine exists"); + + let r1_at_limit = counts[&r1] >= r1_routine.guardrails.max_concurrent as i64; + let r2_at_limit = counts[&r2] >= r2_routine.guardrails.max_concurrent as i64; + + assert!(r1_at_limit, "r1 should be detected as at limit"); + assert!(r2_at_limit, "r2 should be detected as at limit"); + + // If we add one more run to r2, it should exceed limit + let run = RoutineRun { + id: Uuid::new_v4(), + routine_id: r2, + trigger_type: "cron".to_string(), + trigger_detail: None, + started_at: Utc::now(), + completed_at: None, + status: RunStatus::Running, + result_summary: None, + tokens_used: None, + job_id: None, + created_at: Utc::now(), + }; + db.create_routine_run(&run).await.expect("create run"); + + // Re-query to get updated counts + let counts = db + .count_running_routine_runs_batch(&[r1, r2]) + .await + .expect("batch query should work"); + + let r2_exceeded_limit = counts[&r2] > r2_routine.guardrails.max_concurrent as i64; + assert!(r2_exceeded_limit, "r2 should have exceeded its limit"); + } +} diff --git a/tests/e2e/scenarios/test_routine_event_batch.py b/tests/e2e/scenarios/test_routine_event_batch.py new file mode 100644 index 00000000..d8c59e6d --- /dev/null +++ b/tests/e2e/scenarios/test_routine_event_batch.py @@ -0,0 +1,534 @@ +""" +E2E tests for event-triggered routines with batch loading. + +These tests verify that the N+1 query fix correctly: +1. Fires event-triggered routines on matching messages +2. Enforces concurrent limits via batch-loaded counts +3. Maintains performance with multiple simultaneous triggers +4. Works correctly through the full UI and agent loop + +Playwright-based UI tests + SSE verification. +""" + +import asyncio +import json +import pytest +from datetime import datetime, timedelta +from typing import List, Dict, Any + +from playwright.async_api import async_playwright, Page, Browser, BrowserContext + + +@pytest.fixture +async def browser_and_context(): + """Create a Playwright browser and context for testing.""" + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + context = await browser.new_context() + yield browser, context + await context.close() + await browser.close() + + +class EventTriggerHelper: + """Helper methods for event trigger testing.""" + + def __init__(self, page: Page): + self.page = page + + async def navigate_to_routines(self): + """Navigate to the routines page.""" + await self.page.goto("http://localhost:8000/routines") + await self.page.wait_for_load_state("networkidle") + + async def create_event_routine( + self, + name: str, + trigger_regex: str, + channel: str = "slack", + max_concurrent: int = 1, + ) -> str: + """ + Create an event-triggered routine via UI. + Returns the routine ID. + """ + await self.navigate_to_routines() + + # Click "New Routine" button + await self.page.click('button:has-text("New Routine")') + await self.page.wait_for_selector('input[name="routine_name"]') + + # Fill routine details + await self.page.fill('input[name="routine_name"]', name) + await self.page.fill( + 'textarea[name="routine_description"]', + f"Test routine: {name}", + ) + + # Select "Event Trigger" type + await self.page.click('label:has-text("Event Trigger")') + await self.page.wait_for_selector('input[name="trigger_regex"]') + + # Fill trigger details + await self.page.fill('input[name="trigger_regex"]', trigger_regex) + await self.page.select_option('select[name="trigger_channel"]', channel) + + # Set guardrails + await self.page.fill('input[name="max_concurrent"]', str(max_concurrent)) + + # Select lightweight action + await self.page.click('label:has-text("Lightweight")') + await self.page.fill( + 'textarea[name="lightweight_prompt"]', + "Acknowledge the message and confirm trigger worked.", + ) + + # Save routine + await self.page.click('button:has-text("Save Routine")') + await self.page.wait_for_selector('text=Routine created successfully') + + # Extract routine ID from success message or URL + routine_id = await self.page.locator('data-testid=routine-id').text_content() + return routine_id.strip() if routine_id else None + + async def create_multiple_routines( + self, base_name: str, count: int, trigger_regex: str = None + ) -> List[str]: + """Create multiple event-triggered routines.""" + routine_ids = [] + for i in range(count): + name = f"{base_name}_{i}" + regex = trigger_regex or f"({i}|{base_name})" + routine_id = await self.create_event_routine(name, regex) + routine_ids.append(routine_id) + await asyncio.sleep(0.1) # Small delay between creations + return routine_ids + + async def send_chat_message(self, message: str) -> List[str]: + """ + Send a chat message and return SSE events received. + Captures all routine firing events. + """ + await self.page.goto("http://localhost:8000/chat") + await self.page.wait_for_selector('input[placeholder*="message"]', timeout=5000) + + # Collect SSE events + sse_events = [] + + async def capture_sse(response): + """Intercept SSE events.""" + if "event-stream" in response.headers.get("content-type", ""): + text = await response.text() + for line in text.split("\n"): + if line.startswith("data:"): + try: + event = json.loads(line[5:]) + sse_events.append(event) + except json.JSONDecodeError: + pass + + self.page.on("response", capture_sse) + + # Send message + await self.page.fill('input[placeholder*="message"]', message) + await self.page.press('input[placeholder*="message"]', "Enter") + + # Wait for response + await self.page.wait_for_selector('text=Message processed', timeout=10000) + await asyncio.sleep(0.5) # Allow time for SSE events + + self.page.remove_listener("response", capture_sse) + return sse_events + + async def get_routine_execution_log(self, routine_id: str) -> List[Dict]: + """Get execution log entries for a routine.""" + await self.page.goto(f"http://localhost:8000/routines/{routine_id}/executions") + await self.page.wait_for_load_state("networkidle") + + # Extract log entries from table + rows = await self.page.locator("tbody tr").all() + executions = [] + + for row in rows: + cells = await row.locator("td").all() + if len(cells) >= 3: + execution = { + "timestamp": await cells[0].text_content(), + "status": await cells[1].text_content(), + "details": await cells[2].text_content(), + } + executions.append(execution) + + return executions + + async def check_database_queries_in_logs( + self, max_queries_expected: int = 1 + ) -> int: + """Check debug logs for database query count.""" + await self.page.goto("http://localhost:8000/debug/logs?filter=database") + await self.page.wait_for_load_state("networkidle") + + # Count batch queries + log_lines = await self.page.locator("tr:has-text('batch')").all() + batch_count = len(log_lines) + + # Count individual COUNT queries (should be 0 after fix) + count_queries = await self.page.locator("tr:has-text('COUNT')").all() + count_query_count = len(count_queries) + + return batch_count, count_query_count + + +# ============================================================================= +# Tests +# ============================================================================= + + +@pytest.mark.asyncio +async def test_create_event_trigger_routine(browser_and_context): + """Test creating an event-triggered routine via UI.""" + browser, context = browser_and_context + page = await context.new_page() + helper = EventTriggerHelper(page) + + try: + routine_id = await helper.create_event_routine( + name="Test Trigger", + trigger_regex="test|demo", + channel="slack", + max_concurrent=1, + ) + + assert routine_id is not None, "Routine ID should be returned" + assert len(routine_id) > 0, "Routine ID should not be empty" + + finally: + await page.close() + + +@pytest.mark.asyncio +async def test_event_trigger_fires_on_matching_message(browser_and_context): + """Test that event-triggered routine fires when message matches.""" + browser, context = browser_and_context + page = await context.new_page() + helper = EventTriggerHelper(page) + + try: + # Create routine + routine_id = await helper.create_event_routine( + name="Alert Handler", + trigger_regex="urgent|critical|alert", + channel="slack", + ) + + # Send matching message + sse_events = await helper.send_chat_message("URGENT: Server down!") + + # Verify routine fired (look for event in SSE stream) + routine_fired = any( + event.get("type") == "routine_fired" and event.get("routine_id") == routine_id + for event in sse_events + ) + assert routine_fired, "Routine should fire on matching message" + + # Check execution log + executions = await helper.get_routine_execution_log(routine_id) + assert len(executions) > 0, "Execution should be logged" + assert "success" in executions[0]["status"].lower() + + finally: + await page.close() + + +@pytest.mark.asyncio +async def test_event_trigger_skips_non_matching_message(browser_and_context): + """Test that event-triggered routine skips when message doesn't match.""" + browser, context = browser_and_context + page = await context.new_page() + helper = EventTriggerHelper(page) + + try: + # Create routine + routine_id = await helper.create_event_routine( + name="Alert Handler", + trigger_regex="urgent|critical|alert", + channel="slack", + ) + + # Send non-matching message + sse_events = await helper.send_chat_message("Hello, how are you?") + + # Verify routine did NOT fire + routine_fired = any( + event.get("type") == "routine_fired" and event.get("routine_id") == routine_id + for event in sse_events + ) + assert not routine_fired, "Routine should not fire on non-matching message" + + finally: + await page.close() + + +@pytest.mark.asyncio +async def test_multiple_routines_fire_on_matching_message(browser_and_context): + """Test that multiple event-triggered routines fire on same message.""" + browser, context = browser_and_context + page = await context.new_page() + helper = EventTriggerHelper(page) + + try: + # Create 3 overlapping routines + routine_ids = await helper.create_multiple_routines( + base_name="Handler", count=3, trigger_regex="alert|warning|error" + ) + + # Send matching message + sse_events = await helper.send_chat_message("ERROR: Database connection failed") + + # Verify all 3 routines fired + fired_count = sum( + 1 + for event in sse_events + if event.get("type") == "routine_fired" and event.get("routine_id") in routine_ids + ) + + assert ( + fired_count >= 3 + ), f"Expected all 3 routines to fire, got {fired_count}" + + finally: + await page.close() + + +@pytest.mark.asyncio +async def test_concurrent_limit_prevents_additional_fires(browser_and_context): + """Test that concurrent limit is enforced via batch counts.""" + browser, context = browser_and_context + page = await context.new_page() + helper = EventTriggerHelper(page) + + try: + # Create routine with max_concurrent=1 + routine_id = await helper.create_event_routine( + name="Limited Handler", + trigger_regex="process|task", + max_concurrent=1, + ) + + # Trigger first message + await helper.send_chat_message("Process message 1") + await asyncio.sleep(1) + + # Check first execution logged + executions_1 = await helper.get_routine_execution_log(routine_id) + assert len(executions_1) >= 1 + + # Trigger second message while first is still running + sse_events = await helper.send_chat_message("Process message 2") + + # Second routine should be skipped (concurrent limit) + routine_skipped = any( + event.get("type") == "routine_skipped" + and event.get("reason") == "max_concurrent_reached" + and event.get("routine_id") == routine_id + for event in sse_events + ) + assert routine_skipped, "Routine should be skipped when concurrent limit reached" + + finally: + await page.close() + + +@pytest.mark.asyncio +async def test_rapid_messages_with_multiple_triggers_efficiency(browser_and_context): + """Test efficiency of batch loading with multiple rapid messages.""" + browser, context = browser_and_context + page = await context.new_page() + helper = EventTriggerHelper(page) + + try: + # Create 5 overlapping routines + routine_ids = await helper.create_multiple_routines( + base_name="Rapid", count=5, trigger_regex="test|demo|check" + ) + + # Send 10 matching messages rapidly + for i in range(10): + message = f"test message {i}" + await helper.send_chat_message(message) + await asyncio.sleep(0.1) + + # Check database logs for query efficiency + batch_count, count_query_count = await helper.check_database_queries_in_logs() + + # After fix: should have ~10 batch queries (1 per message) + # Before fix: would have ~50 individual COUNT queries (5 routines × 10 messages) + assert ( + count_query_count == 0 + ), f"Should have 0 individual COUNT queries after fix, got {count_query_count}" + assert ( + batch_count <= 15 + ), f"Should have <=15 batch queries for 10 messages, got {batch_count}" + + finally: + await page.close() + + +@pytest.mark.asyncio +async def test_channel_filter_applied_correctly(browser_and_context): + """Test that channel filter prevents non-matching messages.""" + browser, context = browser_and_context + page = await context.new_page() + helper = EventTriggerHelper(page) + + try: + # Create routine for Slack channel + slack_routine_id = await helper.create_event_routine( + name="Slack Handler", + trigger_regex="alert", + channel="slack", + ) + + # Simulate message from Telegram channel + # (Note: In real UI, would need to change channel context) + page.goto( + "http://localhost:8000/chat?channel=telegram" + ) # Switch channel + await helper.send_chat_message("alert: something urgent") + + # Routine should not fire (different channel) + executions = await helper.get_routine_execution_log(slack_routine_id) + + # Check if any recent execution (last 5 min) exists + recent = [ + e + for e in executions + if (datetime.now() - datetime.fromisoformat(e["timestamp"])).total_seconds() + < 300 + ] + assert ( + len(recent) == 0 + ), "Routine should not fire for different channel" + + finally: + await page.close() + + +@pytest.mark.asyncio +async def test_batch_query_failure_handling(browser_and_context): + """Test graceful handling of batch query failures.""" + browser, context = browser_and_context + page = await context.new_page() + helper = EventTriggerHelper(page) + + try: + # Create routine + routine_id = await helper.create_event_routine( + name="Error Handler", + trigger_regex="test", + ) + + # Simulate database error in logs (if possible with test hooks) + # For now, just verify error handling doesn't crash UI + await helper.send_chat_message("test message") + + # Check that UI remains responsive + assert await page.locator("text=Message processed").is_visible() + + finally: + await page.close() + + +@pytest.mark.asyncio +async def test_routine_execution_history_display(browser_and_context): + """Test that execution history correctly displays routine firings.""" + browser, context = browser_and_context + page = await context.new_page() + helper = EventTriggerHelper(page) + + try: + # Create routine + routine_id = await helper.create_event_routine( + name="History Test", + trigger_regex="test", + ) + + # Trigger routine 3 times + for i in range(3): + await helper.send_chat_message(f"test message {i}") + await asyncio.sleep(0.2) + + # Check execution log + executions = await helper.get_routine_execution_log(routine_id) + assert len(executions) >= 3, "Should have at least 3 executions logged" + + # Verify all are recent (within last 5 minutes) + for execution in executions[:3]: + timestamp = datetime.fromisoformat(execution["timestamp"]) + age = datetime.now() - timestamp + assert age < timedelta(minutes=5), "Execution should be recent" + + finally: + await page.close() + + +@pytest.mark.asyncio +async def test_concurrent_batch_loads_independent(browser_and_context): + """Test that concurrent messages each get independent batch queries.""" + browser, context = browser_and_context + page = await context.new_page() + helper = EventTriggerHelper(page) + + try: + # Create 5 routines matching different patterns + r1_id = await helper.create_event_routine( + name="Pattern A", trigger_regex="alpha|alpha_only" + ) + r2_id = await helper.create_event_routine( + name="Pattern B", trigger_regex="beta|beta_only" + ) + r3_id = await helper.create_event_routine( + name="Pattern AB", trigger_regex="alpha|beta|common" + ) + + # Send overlapping messages + # Message 1: matches r1, r3 + sse1 = await helper.send_chat_message("alpha common") + await asyncio.sleep(0.1) + + # Message 2: matches r2, r3 + sse2 = await helper.send_chat_message("beta common") + await asyncio.sleep(0.1) + + # Verify correct routines fired + r1_fired_msg1 = any( + e.get("routine_id") == r1_id for e in sse1 if e.get("type") == "routine_fired" + ) + r2_fired_msg2 = any( + e.get("routine_id") == r2_id for e in sse2 if e.get("type") == "routine_fired" + ) + r3_fired_both = ( + any( + e.get("routine_id") == r3_id for e in sse1 if e.get("type") == "routine_fired" + ) + and any( + e.get("routine_id") == r3_id for e in sse2 if e.get("type") == "routine_fired" + ) + ) + + assert r1_fired_msg1, "Routine 1 should fire on message 1" + assert r2_fired_msg2, "Routine 2 should fire on message 2" + assert r3_fired_both, "Routine 3 should fire on both messages" + + finally: + await page.close() + + +# ============================================================================= +# Integration with existing test patterns +# ============================================================================= + + +if __name__ == "__main__": + # Run tests with: pytest tests/e2e/scenarios/test_routine_event_batch.py -v + pytest.main([__file__, "-v", "-s"]) From e291d3b6f1eb1cb2b8f825586a7465ad422444d7 Mon Sep 17 00:00:00 2001 From: Nige Date: Sat, 14 Mar 2026 20:07:05 +0000 Subject: [PATCH 09/10] feat(routines): human-readable cron schedule summaries in web UI (#1154) * feat(routines): render cron triggers as human-readable summaries * test(routines): annotate multiline cron assertions for no-panics CI * test(routines): avoid multiline assert lint false positives --- src/agent/routine.rs | 199 +++++++++++++++++++++++++- src/channels/web/handlers/routines.rs | 4 + src/channels/web/server.rs | 4 + src/channels/web/static/app.js | 24 +++- src/channels/web/types.rs | 30 ++-- 5 files changed, 249 insertions(+), 12 deletions(-) diff --git a/src/agent/routine.rs b/src/agent/routine.rs index 2dee6333..0389ac1e 100644 --- a/src/agent/routine.rs +++ b/src/agent/routine.rs @@ -538,11 +538,174 @@ pub fn next_cron_fire( } } +/// Describe common routine cron patterns in plain English. +/// +/// Falls back to `cron: ` for malformed or complex expressions. +pub fn describe_cron(schedule: &str, timezone: Option<&str>) -> String { + fn fallback(raw: &str) -> String { + if raw.trim().is_empty() { + "cron: (empty)".to_string() + } else { + format!("cron: {}", raw.trim()) + } + } + + fn parse_u8_token(token: &str) -> Option { + token.parse::().ok() + } + + fn parse_step(token: &str) -> Option { + token + .strip_prefix("*/") + .and_then(parse_u8_token) + .filter(|n| *n > 0) + } + + fn weekday_name(dow: &str) -> Option<&'static str> { + let normalized = dow.trim().to_ascii_uppercase(); + match normalized.as_str() { + "MON" | "1" => Some("Monday"), + "TUE" | "2" => Some("Tuesday"), + "WED" | "3" => Some("Wednesday"), + "THU" | "4" => Some("Thursday"), + "FRI" | "5" => Some("Friday"), + "SAT" | "6" => Some("Saturday"), + "SUN" | "0" | "7" => Some("Sunday"), + _ => None, + } + } + + fn format_time(hour: u8, minute: u8) -> String { + if hour == 0 && minute == 0 { + return "midnight".to_string(); + } + let (display_hour, am_pm) = match hour { + 0 => (12, "AM"), + 1..=11 => (hour, "AM"), + 12 => (12, "PM"), + _ => (hour - 12, "PM"), + }; + format!("{display_hour}:{minute:02} {am_pm}") + } + + fn ordinal(n: u8) -> String { + let suffix = if (11..=13).contains(&(n % 100)) { + "th" + } else { + match n % 10 { + 1 => "st", + 2 => "nd", + 3 => "rd", + _ => "th", + } + }; + format!("{n}{suffix}") + } + + fn describe_inner(raw: &str) -> Option { + let fields: Vec<&str> = raw.split_whitespace().collect(); + let (sec, min, hour, dom, month, dow, year) = match fields.len() { + 5 => ( + "0", fields[0], fields[1], fields[2], fields[3], fields[4], None, + ), + 6 => ( + fields[0], fields[1], fields[2], fields[3], fields[4], fields[5], None, + ), + 7 => ( + fields[0], + fields[1], + fields[2], + fields[3], + fields[4], + fields[5], + Some(fields[6]), + ), + _ => return None, + }; + + if year.is_some_and(|v| v != "*") { + return None; + } + + if sec == "0" + && hour == "*" + && dom == "*" + && month == "*" + && dow == "*" + && let Some(step) = parse_step(min) + { + return Some(match step { + 1 => "Every minute".to_string(), + n => format!("Every {n} minutes"), + }); + } + + if sec == "0" + && min == "0" + && dom == "*" + && month == "*" + && dow == "*" + && let Some(step) = parse_step(hour) + { + return Some(match step { + 1 => "Every hour".to_string(), + n => format!("Every {n} hours"), + }); + } + + let hour = parse_u8_token(hour).filter(|h| *h <= 23)?; + let minute = parse_u8_token(min).filter(|m| *m <= 59)?; + let time = format_time(hour, minute); + let time_phrase = if time == "midnight" { + "at midnight".to_string() + } else { + format!("at {time}") + }; + + if sec == "0" && dom == "*" && month == "*" && dow == "*" { + return Some(format!("Daily {time_phrase}")); + } + + if sec == "0" && dom == "*" && month == "*" && dow.eq_ignore_ascii_case("MON-FRI") { + return Some(format!("Weekdays {time_phrase}")); + } + + if sec == "0" + && dom == "*" + && month == "*" + && let Some(day_name) = weekday_name(dow) + { + return Some(format!("Every {day_name} {time_phrase}")); + } + + if sec == "0" + && month == "*" + && dow == "*" + && let Some(day_of_month) = parse_u8_token(dom).filter(|d| (1..=31).contains(d)) + { + return Some(format!( + "{} of every month {time_phrase}", + ordinal(day_of_month) + )); + } + + None + } + + let mut description = describe_inner(schedule).unwrap_or_else(|| fallback(schedule)); + if let Some(tz) = timezone.map(str::trim).filter(|tz| !tz.is_empty()) { + description.push_str(" ("); + description.push_str(tz); + description.push(')'); + } + description +} + #[cfg(test)] mod tests { use crate::agent::routine::{ MAX_TOOL_ROUNDS_LIMIT, RoutineAction, RoutineGuardrails, RunStatus, Trigger, content_hash, - next_cron_fire, + describe_cron, next_cron_fire, }; #[test] @@ -698,6 +861,40 @@ mod tests { assert_ne!(next_utc, next_est, "timezone should shift the fire time"); } + #[test] + fn test_describe_cron_common_patterns() { + let cases = vec![ + ("0 */30 * * * *", None, "Every 30 minutes"), + ("0 0 9 * * *", None, "Daily at 9:00 AM"), + ("0 0 9 * * MON-FRI", None, "Weekdays at 9:00 AM"), + ("0 0 */2 * * *", None, "Every 2 hours"), + ("0 0 0 * * *", None, "Daily at midnight"), + ("0 0 9 * * 1", None, "Every Monday at 9:00 AM"), + ("0 0 9 1 * *", None, "1st of every month at 9:00 AM"), + ( + "0 0 9 * * MON-FRI", + Some("America/New_York"), + "Weekdays at 9:00 AM (America/New_York)", + ), + ("1 2 3 4 5 6", None, "cron: 1 2 3 4 5 6"), + ]; + + for (schedule, timezone, expected) in cases { + let actual = describe_cron(schedule, timezone); + assert_eq!(actual, expected); // safety: test-only assertion in #[cfg(test)] module + } + } + + #[test] + fn test_describe_cron_edge_cases() { + assert_eq!(describe_cron("", None), "cron: (empty)"); // safety: test-only assertion in #[cfg(test)] module + assert_eq!(describe_cron("not a cron", None), "cron: not a cron"); // safety: test-only assertion in #[cfg(test)] module + let weekdays_5_field = describe_cron("0 9 * * MON-FRI", None); + assert_eq!(weekdays_5_field, "Weekdays at 9:00 AM"); // safety: test-only assertion in #[cfg(test)] module + let weekdays_7_field = describe_cron("0 0 9 * * MON-FRI *", None); + assert_eq!(weekdays_7_field, "Weekdays at 9:00 AM"); // safety: test-only assertion in #[cfg(test)] module + } + #[test] fn test_guardrails_default() { let g = RoutineGuardrails::default(); diff --git a/src/channels/web/handlers/routines.rs b/src/channels/web/handlers/routines.rs index f5d8db02..41bfee5a 100644 --- a/src/channels/web/handlers/routines.rs +++ b/src/channels/web/handlers/routines.rs @@ -112,12 +112,16 @@ pub async fn routines_detail_handler( job_id: run.job_id, }) .collect(); + let routine_info = RoutineInfo::from_routine(&routine); Ok(Json(RoutineDetailResponse { id: routine.id, name: routine.name.clone(), description: routine.description.clone(), enabled: routine.enabled, + trigger_type: routine_info.trigger_type, + trigger_raw: routine_info.trigger_raw, + trigger_summary: routine_info.trigger_summary, trigger: serde_json::to_value(&routine.trigger).unwrap_or_default(), action: serde_json::to_value(&routine.action).unwrap_or_default(), guardrails: serde_json::to_value(&routine.guardrails).unwrap_or_default(), diff --git a/src/channels/web/server.rs b/src/channels/web/server.rs index 48ef452c..acec3842 100644 --- a/src/channels/web/server.rs +++ b/src/channels/web/server.rs @@ -2346,12 +2346,16 @@ async fn routines_detail_handler( job_id: run.job_id, }) .collect(); + let routine_info = RoutineInfo::from_routine(&routine); Ok(Json(RoutineDetailResponse { id: routine.id, name: routine.name.clone(), description: routine.description.clone(), enabled: routine.enabled, + trigger_type: routine_info.trigger_type, + trigger_raw: routine_info.trigger_raw, + trigger_summary: routine_info.trigger_summary, trigger: serde_json::to_value(&routine.trigger).unwrap_or_default(), action: serde_json::to_value(&routine.action).unwrap_or_default(), guardrails: serde_json::to_value(&routine.guardrails).unwrap_or_default(), diff --git a/src/channels/web/static/app.js b/src/channels/web/static/app.js index a981d567..081b0f3a 100644 --- a/src/channels/web/static/app.js +++ b/src/channels/web/static/app.js @@ -3535,10 +3535,13 @@ function renderRoutinesList(routines) { const toggleLabel = r.enabled ? 'Disable' : 'Enable'; const toggleClass = r.enabled ? 'btn-cancel' : 'btn-restart'; + const triggerTitle = (r.trigger_type === 'cron' && r.trigger_raw) + ? ' title="' + escapeHtml(r.trigger_raw) + '"' + : ''; return '' + '' + escapeHtml(r.name) + '' - + '' + escapeHtml(r.trigger_summary) + '' + + '' + escapeHtml(r.trigger_summary) + '' + '' + escapeHtml(r.action_type) + '' + '' + formatRelativeTime(r.last_run_at) + '' + '' + formatRelativeTime(r.next_fire_at) + '' @@ -3606,8 +3609,23 @@ function renderRoutineDetail(routine) { } // Trigger config - html += '

Trigger

' - + '
' + escapeHtml(JSON.stringify(routine.trigger, null, 2)) + '
'; + if (routine.trigger_type === 'cron') { + const summary = routine.trigger_summary || 'cron'; + const raw = routine.trigger_raw || ''; + const timezone = routine.trigger && routine.trigger.timezone ? String(routine.trigger.timezone) : ''; + html += '

Trigger

' + + '
' + escapeHtml(summary) + '
'; + if (raw) { + html += '
' + + 'Raw' + + '' + escapeHtml(raw + (timezone ? ' (' + timezone + ')' : '')) + '' + + '
'; + } + html += '
'; + } else { + html += '

Trigger

' + + '
' + escapeHtml(JSON.stringify(routine.trigger, null, 2)) + '
'; + } // Action config html += '

Action

' diff --git a/src/channels/web/types.rs b/src/channels/web/types.rs index b8690b78..129a7071 100644 --- a/src/channels/web/types.rs +++ b/src/channels/web/types.rs @@ -735,6 +735,7 @@ pub struct RoutineInfo { pub description: String, pub enabled: bool, pub trigger_type: String, + pub trigger_raw: String, pub trigger_summary: String, pub action_type: String, pub last_run_at: Option, @@ -747,25 +748,34 @@ pub struct RoutineInfo { impl RoutineInfo { /// Convert a `Routine` to the trimmed `RoutineInfo` for list display. pub fn from_routine(r: &crate::agent::routine::Routine) -> Self { - let (trigger_type, trigger_summary) = match &r.trigger { - crate::agent::routine::Trigger::Cron { schedule, .. } => { - ("cron".to_string(), format!("cron: {}", schedule)) - } + let (trigger_type, trigger_raw, trigger_summary) = match &r.trigger { + crate::agent::routine::Trigger::Cron { schedule, timezone } => ( + "cron".to_string(), + schedule.clone(), + crate::agent::routine::describe_cron(schedule, timezone.as_deref()), + ), crate::agent::routine::Trigger::Event { pattern, channel, .. } => { let ch = channel.as_deref().unwrap_or("any"); - ("event".to_string(), format!("on {} /{}/", ch, pattern)) + ( + "event".to_string(), + String::new(), + format!("on {} /{}/", ch, pattern), + ) } crate::agent::routine::Trigger::SystemEvent { source, event_type, .. } => ( "system_event".to_string(), + String::new(), format!("event: {}.{}", source, event_type), ), - crate::agent::routine::Trigger::Manual => { - ("manual".to_string(), "manual only".to_string()) - } + crate::agent::routine::Trigger::Manual => ( + "manual".to_string(), + String::new(), + "manual only".to_string(), + ), }; let action_type = match &r.action { @@ -787,6 +797,7 @@ impl RoutineInfo { description: r.description.clone(), enabled: r.enabled, trigger_type, + trigger_raw, trigger_summary, action_type: action_type.to_string(), last_run_at: r.last_run_at.map(|dt| dt.to_rfc3339()), @@ -818,6 +829,9 @@ pub struct RoutineDetailResponse { pub name: String, pub description: String, pub enabled: bool, + pub trigger_type: String, + pub trigger_raw: String, + pub trigger_summary: String, pub trigger: serde_json::Value, pub action: serde_json::Value, pub guardrails: serde_json::Value, From 71b1a6778b93a77a58b3278296a0bb0d8c2bb561 Mon Sep 17 00:00:00 2001 From: Illia Polosukhin Date: Sat, 14 Mar 2026 20:44:37 +0000 Subject: [PATCH 10/10] fix(deps): update yanked uds_windows 1.2.0 -> 1.2.1 (#1183) Fixes cargo-deny CI failure due to yanked crate. [skip-regression-check] Co-authored-by: Claude Opus 4.6 (1M context)