@@ -434,9 +434,10 @@ async fn actor_request_task(
434434 open_actors : Arc < Mutex < HashSet < String > > > ,
435435 mut rx : mpsc:: Receiver < protocol:: ToServerRequest > ,
436436) {
437- // Cached actor resolution. Populated on first KV request, reused for all
438- // subsequent requests. Actor name is immutable so this never goes stale.
439- let mut cached_actor: Option < ( Id , String ) > = None ;
437+ // Cached actor resolution per actor id. A single KV channel connection can
438+ // multiplex requests for many actors, so the cache must be keyed by actor id.
439+ // Actor name is immutable so each cached entry does not go stale.
440+ let mut cached_actors: HashMap < String , ( Id , String ) > = HashMap :: new ( ) ;
440441
441442 while let Some ( req) = rx. recv ( ) . await {
442443 let is_close = matches ! ( req. data, protocol:: RequestData :: ActorCloseRequest ) ;
@@ -464,11 +465,11 @@ async fn actor_request_task(
464465 )
465466 }
466467 } else {
467- // Lazy-resolve and cache.
468- if cached_actor . is_none ( ) {
468+ // Lazy-resolve and cache per actor id .
469+ if !cached_actors . contains_key ( & req . actor_id ) {
469470 match resolve_actor ( & ctx, & req. actor_id , namespace_id) . await {
470471 Ok ( v) => {
471- cached_actor = Some ( v) ;
472+ cached_actors . insert ( req . actor_id . clone ( ) , v) ;
472473 }
473474 Err ( resp) => {
474475 // Don't cache failures. Next request will retry.
@@ -480,7 +481,8 @@ async fn actor_request_task(
480481 }
481482 }
482483 }
483- let ( parsed_id, actor_name) = cached_actor. as_ref ( ) . unwrap ( ) ;
484+ let ( parsed_id, actor_name) =
485+ cached_actors. get ( & req. actor_id ) . unwrap ( ) ;
484486
485487 let recipient = actor_kv:: Recipient {
486488 actor_id : * parsed_id,
@@ -811,12 +813,21 @@ async fn resolve_actor(
811813 )
812814 } ) ?;
813815
816+ // Resolve the actor from the general actor record rather than the
817+ // runner-specific lookup. SQLite opens happen during actor startup
818+ // before runner-specific fields are guaranteed to exist for v2 actors,
819+ // so requiring RunnerIdKey here can race the database bootstrap path and
820+ // reject perfectly valid opens with SQLITE_CANTOPEN.
814821 let actor = ctx
815- . op ( pegboard:: ops:: actor:: get_for_runner:: Input {
816- actor_id : parsed_id,
822+ . op ( pegboard:: ops:: actor:: get:: Input {
823+ actor_ids : vec ! [ parsed_id] ,
824+ fetch_error : false ,
817825 } )
818826 . await
819- . map_err ( |err| internal_error ( & err) ) ?;
827+ . map_err ( |err| internal_error ( & err) ) ?
828+ . actors
829+ . into_iter ( )
830+ . next ( ) ;
820831
821832 match actor {
822833 Some ( actor) => {
0 commit comments