@@ -657,8 +657,11 @@ pub enum MemoryMessage {
657657 record : BackgroundJobRecord ,
658658 reply : SharedReply < Result < ( ) , String > > ,
659659 } ,
660+ /// List background jobs, optionally scoped to one host channel before the
661+ /// result limit is applied.
660662 ListBackgroundJobs {
661663 chat_id : Option < String > ,
664+ channel : Option < String > ,
662665 limit : usize ,
663666 reply : SharedReply < Result < Vec < BackgroundJobRecord > , String > > ,
664667 } ,
@@ -672,8 +675,11 @@ pub enum MemoryMessage {
672675 record : NotificationRecord ,
673676 reply : SharedReply < Result < ( ) , String > > ,
674677 } ,
678+ /// List notifications, optionally scoped to one host channel before the
679+ /// result limit is applied.
675680 ListNotifications {
676681 chat_id : Option < String > ,
682+ channel : Option < String > ,
677683 limit : usize ,
678684 unseen_only : bool ,
679685 reply : SharedReply < Result < Vec < NotificationRecord > , String > > ,
@@ -705,10 +711,12 @@ pub enum MemoryMessage {
705711 response : String ,
706712 reply : SharedReply < Result < ( ) , String > > ,
707713 } ,
708- /// List clarification tickets with optional filters.
714+ /// List clarification tickets with optional filters. `channel` is applied
715+ /// in SQLite before the limit so one host cannot starve another's inbox.
709716 ListClarificationTickets {
710717 job_id : Option < String > ,
711718 chat_id : Option < String > ,
719+ channel : Option < String > ,
712720 status : Option < String > ,
713721 limit : usize ,
714722 reply : SharedReply < Result < Vec < ClarificationTicketRecord > , String > > ,
@@ -1869,6 +1877,7 @@ impl ActorLogic<MemoryMessage> for SqliteMemoryActor {
18691877 }
18701878 MemoryMessage :: ListBackgroundJobs {
18711879 chat_id,
1880+ channel,
18721881 limit,
18731882 reply,
18741883 } => {
@@ -1886,6 +1895,10 @@ impl ActorLogic<MemoryMessage> for SqliteMemoryActor {
18861895 filters. push ( "chat_id = ?" ) ;
18871896 params_vec. push ( Box :: new ( cid) ) ;
18881897 }
1898+ if let Some ( ch) = channel {
1899+ filters. push ( "channel = ?" ) ;
1900+ params_vec. push ( Box :: new ( ch) ) ;
1901+ }
18891902
18901903 if !filters. is_empty ( ) {
18911904 sql. push_str ( " WHERE " ) ;
@@ -1955,6 +1968,7 @@ impl ActorLogic<MemoryMessage> for SqliteMemoryActor {
19551968 }
19561969 MemoryMessage :: ListNotifications {
19571970 chat_id,
1971+ channel,
19581972 limit,
19591973 unseen_only,
19601974 reply,
@@ -1969,6 +1983,10 @@ impl ActorLogic<MemoryMessage> for SqliteMemoryActor {
19691983 filters. push ( "chat_id = ?" ) ;
19701984 params_vec. push ( Box :: new ( cid) ) ;
19711985 }
1986+ if let Some ( ch) = channel {
1987+ filters. push ( "channel = ?" ) ;
1988+ params_vec. push ( Box :: new ( ch) ) ;
1989+ }
19721990 if unseen_only {
19731991 filters. push ( "seen_at_ms IS NULL" ) ;
19741992 }
@@ -2183,6 +2201,7 @@ impl ActorLogic<MemoryMessage> for SqliteMemoryActor {
21832201 MemoryMessage :: ListClarificationTickets {
21842202 job_id,
21852203 chat_id,
2204+ channel,
21862205 status,
21872206 limit,
21882207 reply,
@@ -2201,6 +2220,10 @@ impl ActorLogic<MemoryMessage> for SqliteMemoryActor {
22012220 filters. push ( "chat_id = ?" ) ;
22022221 params_vec. push ( Box :: new ( cid) ) ;
22032222 }
2223+ if let Some ( ch) = channel {
2224+ filters. push ( "channel = ?" ) ;
2225+ params_vec. push ( Box :: new ( ch) ) ;
2226+ }
22042227 if let Some ( s) = status {
22052228 filters. push ( "status = ?" ) ;
22062229 params_vec. push ( Box :: new ( s) ) ;
@@ -2298,7 +2321,8 @@ fn dropped_tool_call_ids(
22982321#[ cfg( test) ]
22992322mod root_thread_id_tests {
23002323 use super :: {
2301- chat_id_from_root_thread_id, is_root_session_thread_id, MemoryMessage , SharedReply ,
2324+ chat_id_from_root_thread_id, is_root_session_thread_id, BackgroundJobRecord ,
2325+ ClarificationTicketRecord , MemoryMessage , NotificationRecord , SharedReply ,
23022326 SqliteMemoryActor ,
23032327 } ;
23042328 use crate :: session:: SessionManager ;
@@ -2339,6 +2363,151 @@ mod root_thread_id_tests {
23392363 assert ! ( !is_root_session_thread_id( "tauri" , "tauri:s-abc:extra:" ) ) ;
23402364 }
23412365
2366+ #[ tokio:: test]
2367+ async fn channel_scoped_inbox_query_filters_before_limit ( ) {
2368+ let actor = SqliteMemoryActor :: new ( ":memory:" ) . expect ( "memory actor" ) ;
2369+ let node = NodeHandle :: new ( actor, 16 , 1 , Duration :: from_millis ( 1 ) ) ;
2370+
2371+ for ( id, channel, created_at_ms) in [
2372+ ( "tauri-notification" , "tauri" , 1_i64 ) ,
2373+ ( "terminal-notification" , "terminal" , 2_i64 ) ,
2374+ ] {
2375+ let ( tx, rx) = tokio:: sync:: oneshot:: channel ( ) ;
2376+ node. send_packet ( MemoryMessage :: InsertNotification {
2377+ record : NotificationRecord {
2378+ notification_id : id. to_string ( ) ,
2379+ chat_id : "chat-1" . to_string ( ) ,
2380+ channel : channel. to_string ( ) ,
2381+ thread_id : None ,
2382+ kind : "test" . to_string ( ) ,
2383+ title : "Test" . to_string ( ) ,
2384+ body : "Test" . to_string ( ) ,
2385+ action_kind : None ,
2386+ action_payload : None ,
2387+ seen_at_ms : None ,
2388+ resolved_at_ms : None ,
2389+ created_at_ms,
2390+ } ,
2391+ reply : SharedReply :: new ( tx) ,
2392+ } )
2393+ . await
2394+ . expect ( "enqueue notification" ) ;
2395+ rx. await
2396+ . expect ( "notification actor reply" )
2397+ . expect ( "insert notification" ) ;
2398+ }
2399+
2400+ let ( tx, rx) = tokio:: sync:: oneshot:: channel ( ) ;
2401+ node. send_packet ( MemoryMessage :: ListNotifications {
2402+ chat_id : None ,
2403+ channel : Some ( "tauri" . to_string ( ) ) ,
2404+ limit : 1 ,
2405+ unseen_only : false ,
2406+ reply : SharedReply :: new ( tx) ,
2407+ } )
2408+ . await
2409+ . expect ( "enqueue list" ) ;
2410+ let rows = rx
2411+ . await
2412+ . expect ( "notification actor reply" )
2413+ . expect ( "list notifications" ) ;
2414+
2415+ assert_eq ! ( rows. len( ) , 1 ) ;
2416+ assert_eq ! ( rows[ 0 ] . notification_id, "tauri-notification" ) ;
2417+
2418+ for ( id, channel, updated_at_ms) in [
2419+ ( "tauri-job" , "tauri" , 1_i64 ) ,
2420+ ( "terminal-job" , "terminal" , 2_i64 ) ,
2421+ ] {
2422+ let ( tx, rx) = tokio:: sync:: oneshot:: channel ( ) ;
2423+ node. send_packet ( MemoryMessage :: UpsertBackgroundJob {
2424+ record : BackgroundJobRecord {
2425+ job_id : id. to_string ( ) ,
2426+ kind : "test" . to_string ( ) ,
2427+ chat_id : "chat-1" . to_string ( ) ,
2428+ channel : channel. to_string ( ) ,
2429+ thread_id : None ,
2430+ state : "waiting" . to_string ( ) ,
2431+ payload_json : "{}" . to_string ( ) ,
2432+ resume_after_restart : false ,
2433+ detached : false ,
2434+ last_error : None ,
2435+ created_at_ms : updated_at_ms,
2436+ updated_at_ms,
2437+ } ,
2438+ reply : SharedReply :: new ( tx) ,
2439+ } )
2440+ . await
2441+ . expect ( "enqueue background job" ) ;
2442+ rx. await
2443+ . expect ( "background job actor reply" )
2444+ . expect ( "insert background job" ) ;
2445+ }
2446+
2447+ let ( tx, rx) = tokio:: sync:: oneshot:: channel ( ) ;
2448+ node. send_packet ( MemoryMessage :: ListBackgroundJobs {
2449+ chat_id : None ,
2450+ channel : Some ( "tauri" . to_string ( ) ) ,
2451+ limit : 1 ,
2452+ reply : SharedReply :: new ( tx) ,
2453+ } )
2454+ . await
2455+ . expect ( "enqueue background job list" ) ;
2456+ let jobs = rx
2457+ . await
2458+ . expect ( "background job actor reply" )
2459+ . expect ( "list background jobs" ) ;
2460+ assert_eq ! ( jobs. len( ) , 1 ) ;
2461+ assert_eq ! ( jobs[ 0 ] . job_id, "tauri-job" ) ;
2462+
2463+ for ( id, channel, updated_at_ms) in [
2464+ ( "tauri-ticket" , "tauri" , 1_i64 ) ,
2465+ ( "terminal-ticket" , "terminal" , 2_i64 ) ,
2466+ ] {
2467+ let ( tx, rx) = tokio:: sync:: oneshot:: channel ( ) ;
2468+ node. send_packet ( MemoryMessage :: UpsertClarificationTicket {
2469+ record : ClarificationTicketRecord {
2470+ ticket_id : id. to_string ( ) ,
2471+ job_id : id. to_string ( ) ,
2472+ chat_id : "chat-1" . to_string ( ) ,
2473+ channel : channel. to_string ( ) ,
2474+ thread_id : None ,
2475+ tool_call_id : None ,
2476+ prompt : "Test" . to_string ( ) ,
2477+ choices_json : None ,
2478+ response : None ,
2479+ status : "waiting" . to_string ( ) ,
2480+ created_at_ms : updated_at_ms,
2481+ updated_at_ms,
2482+ } ,
2483+ reply : SharedReply :: new ( tx) ,
2484+ } )
2485+ . await
2486+ . expect ( "enqueue clarification ticket" ) ;
2487+ rx. await
2488+ . expect ( "clarification ticket actor reply" )
2489+ . expect ( "insert clarification ticket" ) ;
2490+ }
2491+
2492+ let ( tx, rx) = tokio:: sync:: oneshot:: channel ( ) ;
2493+ node. send_packet ( MemoryMessage :: ListClarificationTickets {
2494+ job_id : None ,
2495+ chat_id : None ,
2496+ channel : Some ( "tauri" . to_string ( ) ) ,
2497+ status : None ,
2498+ limit : 1 ,
2499+ reply : SharedReply :: new ( tx) ,
2500+ } )
2501+ . await
2502+ . expect ( "enqueue clarification ticket list" ) ;
2503+ let tickets = rx
2504+ . await
2505+ . expect ( "clarification ticket actor reply" )
2506+ . expect ( "list clarification tickets" ) ;
2507+ assert_eq ! ( tickets. len( ) , 1 ) ;
2508+ assert_eq ! ( tickets[ 0 ] . ticket_id, "tauri-ticket" ) ;
2509+ }
2510+
23422511 #[ tokio:: test]
23432512 async fn get_context_returns_messages_in_insert_order ( ) {
23442513 let actor = SqliteMemoryActor :: new ( ":memory:" ) . expect ( "memory actor" ) ;
0 commit comments