Skip to main content

meerkat_mobkit/
rpc.rs

1//! JSON-RPC request handling for both module-only and unified runtime modes.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::future::Future;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::time::Duration;
8
9use serde::{Deserialize, Serialize};
10use serde_json::Value;
11
12use crate::console_aggregator::is_implicit_delegate_member;
13use crate::mob_handle_runtime::{topology_restore_failed_peer_ids, topology_restore_warning_json};
14use crate::runtime::{
15    BigQuerySessionStoreAdapter, BigQuerySessionStoreError, ConsoleRestJsonRequest,
16    ConsoleRestJsonResponse, DeliveryHistoryRequest, DeliverySendError, DeliverySendRequest,
17    GatingDecideError, GatingDecideRequest, GatingDecision, GatingEvaluateRequest, GatingRiskTier,
18    LocalJsonMemoryStoreError, MemoryIndexError, MemoryIndexRequest, MemoryQueryRequest,
19    MobkitRuntimeHandle, ModuleRouteError, ModuleRouteRequest, ROUTING_RETRY_MAX_CAP,
20    RoutingResolveError, RoutingResolveRequest, RuntimeDecisionState, RuntimeRoute,
21    RuntimeRouteMutationError, ScheduleDefinition, ScheduleValidationError, SessionPersistenceRow,
22    SubscribeError, SubscribeRequest, SubscribeScope, handle_console_rest_json_route,
23    route_module_call, validate_schedules,
24};
25use crate::unified_runtime::{EventQuery, UnifiedRuntime};
26
27mod console_ingress;
28mod gating_methods;
29pub(crate) mod memory_methods;
30pub(crate) mod mob_methods;
31pub(crate) mod params;
32mod routing_delivery_methods;
33mod scheduling_methods;
34mod session_store_methods;
35pub(crate) mod storage_methods;
36mod subscribe_methods;
37pub(crate) mod topology_methods;
38pub(crate) mod workgraph_methods;
39
40pub use console_ingress::handle_console_ingress_json;
41
42use gating_methods::{
43    GatingParamsError, parse_gating_audit_params, parse_gating_decide_params,
44    parse_gating_evaluate_params, parse_gating_pending_params,
45};
46use memory_methods::{
47    MemoryParamsError, parse_agent_memory_forget_params, parse_agent_memory_manifest_params,
48    parse_agent_memory_recall_params, parse_agent_memory_remember_params,
49    parse_agent_memory_update_params, parse_memory_index_params, parse_memory_query_params,
50    parse_memory_stores_params,
51};
52use routing_delivery_methods::{
53    RoutingDeliveryParamsError, parse_delivery_history_params, parse_delivery_send_params,
54    parse_routing_resolve_params, parse_routing_route_add_params,
55    parse_routing_route_delete_params, parse_routing_routes_list_params,
56};
57use scheduling_methods::{format_schedule_validation_error, parse_scheduling_params};
58use session_store_methods::{
59    BigQuerySessionStoreRpcError, format_bigquery_store_error, parse_bigquery_session_store_params,
60    run_bigquery_session_store_request,
61};
62use subscribe_methods::{SubscribeParamsError, parse_subscribe_request};
63
64pub const JSONRPC_VERSION: &str = "2.0";
65pub const MOBKIT_CONTRACT_VERSION: &str = "0.4.0";
66pub const MAX_SCHEDULES_PER_REQUEST: usize = 256;
67pub(crate) const MOBPACK_AUTHORING_METHODS: &[&str] = &[
68    "mobkit/mobpacks/schema",
69    "mobkit/mobpacks/catalogs",
70    "mobkit/tools/catalog",
71    "mobkit/skills/catalog",
72    "mobkit/agent_definitions/list",
73    "mobkit/mobpacks/templates",
74    "mobkit/mobpacks/validate",
75    "mobkit/mobpacks/source",
76    "mobkit/mobpacks/export",
77    "mobkit/mobpacks/import",
78    "mobkit/mobpacks/list",
79    "mobkit/mobpacks/get",
80    "mobkit/mobpacks/create",
81    "mobkit/mobpacks/save",
82    "mobkit/mobpacks/delete",
83    "mobkit/mobpacks/undo",
84    "mobkit/mobpacks/redo",
85    "mobkit/mobpacks/apply_operation",
86    "mobkit/mobpacks/graph_projection",
87    "mobkit/mobpacks/graph_to_flow",
88    "mobkit/mobpacks/deploy_command",
89    "mobkit/mobpacks/deploy",
90];
91
92pub(crate) fn mobpack_authoring_capabilities() -> Value {
93    serde_json::json!({
94        "domain": "mobpack_authoring",
95        "runtime_mutation": false,
96        "host_mutation_methods": {
97            "mobkit/mobpacks/deploy": "when execute=true, writes a mobpack archive and runs rkat mob run on the host",
98            "mobkit/mobpacks/validate": "when rkat_validate=true, writes a mobpack archive and runs rkat mob validate on the host"
99        },
100        "deploy_command": "rkat mob run",
101        "methods": MOBPACK_AUTHORING_METHODS,
102        "operations": crate::mobpack::mobpack_authoring_operations(),
103    })
104}
105
106async fn mobpack_runtime_catalog_state(
107    runtime: &UnifiedRuntime,
108) -> crate::mobpack::MobpackRuntimeCatalogState {
109    let loaded_modules = runtime.loaded_modules().await;
110    let runtime_flow_rows = crate::mobpack::runtime_flow_registry_rows_from_definition(
111        runtime.mob_handle().definition(),
112    );
113    let runtime_agent_definition_sources =
114        crate::mobpack::runtime_agent_definition_sources_from_definition(
115            runtime.mob_handle().definition(),
116        );
117    let runtime_skill_realms =
118        crate::mobpack::runtime_skill_realms_from_definition(runtime.mob_handle().definition());
119    let mut runtime_methods = vec![
120        "mobkit/capabilities".to_string(),
121        "mobkit/models/catalog".to_string(),
122        "mobkit/spawn_member".to_string(),
123        "mobkit/list_members".to_string(),
124        "mobkit/get_member".to_string(),
125        "mobkit/run_flow".to_string(),
126        "mobkit/list_flows".to_string(),
127        "mobkit/list_runs".to_string(),
128    ];
129    runtime_methods.extend(
130        MOBPACK_AUTHORING_METHODS
131            .iter()
132            .map(std::string::ToString::to_string),
133    );
134    if runtime.has_contact_directory() {
135        runtime_methods.push("mobkit/cross_mob/directory".to_string());
136    }
137    if runtime.has_peer_mob_handles().await && runtime.has_inproc_contacts() {
138        runtime_methods.extend([
139            "mobkit/cross_mob/wire".to_string(),
140            "mobkit/cross_mob/unwire".to_string(),
141            "mobkit/cross_mob/send".to_string(),
142        ]);
143    }
144    crate::mobpack::MobpackRuntimeCatalogState {
145        loaded_modules,
146        runtime_methods,
147        has_contact_directory: runtime.has_contact_directory(),
148        has_peer_mob_handles: runtime.has_peer_mob_handles().await,
149        has_inproc_contacts: runtime.has_inproc_contacts(),
150        runtime_flow_rows,
151        runtime_agent_definition_sources,
152        runtime_skill_realms,
153    }
154}
155
156async fn handle_unified_mobpack_authoring_rpc(
157    runtime: &UnifiedRuntime,
158    method: &str,
159    params: &Value,
160    response_id: Value,
161) -> JsonRpcResponse {
162    let runtime_catalog_state = match method {
163        "mobkit/mobpacks/schema"
164        | "mobkit/mobpacks/catalogs"
165        | "mobkit/tools/catalog"
166        | "mobkit/skills/catalog"
167        | "mobkit/agent_definitions/list"
168        | "mobkit/mobpacks/templates"
169        | "mobkit/mobpacks/list"
170        | "mobkit/mobpacks/get"
171        | "mobkit/mobpacks/apply_operation" => Some(mobpack_runtime_catalog_state(runtime).await),
172        _ => None,
173    };
174    let result = match method {
175        "mobkit/mobpacks/catalogs" => Ok(crate::mobpack::mobpack_catalogs_response_with_runtime(
176            runtime_catalog_state.as_ref(),
177        )),
178        "mobkit/tools/catalog" => Ok(crate::mobpack::mobpack_tools_catalog_response_with_runtime(
179            runtime_catalog_state.as_ref(),
180        )),
181        "mobkit/skills/catalog" => Ok(
182            crate::mobpack::mobpack_skills_catalog_response_with_runtime(
183                runtime_catalog_state.as_ref(),
184            ),
185        ),
186        "mobkit/agent_definitions/list" => Ok(
187            crate::mobpack::mobpack_agent_definitions_response_with_runtime(
188                runtime_catalog_state.as_ref(),
189            ),
190        ),
191        "mobkit/mobpacks/templates" => Ok(crate::mobpack::mobpack_templates_response_with_runtime(
192            runtime_catalog_state.as_ref(),
193        )),
194        _ => {
195            return handle_mobpack_authoring_rpc_with_runtime(
196                method,
197                params,
198                response_id.clone(),
199                runtime_catalog_state.as_ref(),
200            )
201            .unwrap_or_else(|| JsonRpcResponse {
202                jsonrpc: JSONRPC_VERSION.to_string(),
203                id: response_id,
204                result: None,
205                error: Some(JsonRpcError {
206                    code: -32601,
207                    message: "Method not found".to_string(),
208                    data: None,
209                }),
210            });
211        }
212    };
213    match result {
214        Ok(result) => JsonRpcResponse {
215            jsonrpc: JSONRPC_VERSION.to_string(),
216            id: response_id,
217            result: Some(result),
218            error: None,
219        },
220        Err(message) => JsonRpcResponse {
221            jsonrpc: JSONRPC_VERSION.to_string(),
222            id: response_id,
223            result: None,
224            error: Some(JsonRpcError {
225                code: -32602,
226                message,
227                data: None,
228            }),
229        },
230    }
231}
232
233pub(crate) fn handle_mobpack_authoring_rpc(
234    method: &str,
235    params: &Value,
236    response_id: Value,
237) -> Option<JsonRpcResponse> {
238    handle_mobpack_authoring_rpc_with_runtime(method, params, response_id, None)
239}
240
241pub(crate) fn handle_mobpack_authoring_rpc_with_runtime(
242    method: &str,
243    params: &Value,
244    response_id: Value,
245    runtime: Option<&crate::mobpack::MobpackRuntimeCatalogState>,
246) -> Option<JsonRpcResponse> {
247    let result = match method {
248        "mobkit/mobpacks/schema" => Ok(crate::mobpack::mobpack_schema_response_with_runtime(
249            runtime,
250        )),
251        "mobkit/mobpacks/catalogs" => Ok(crate::mobpack::mobpack_catalogs_response_with_runtime(
252            runtime,
253        )),
254        "mobkit/tools/catalog" => Ok(crate::mobpack::mobpack_tools_catalog_response_with_runtime(
255            runtime,
256        )),
257        "mobkit/skills/catalog" => {
258            Ok(crate::mobpack::mobpack_skills_catalog_response_with_runtime(runtime))
259        }
260        "mobkit/agent_definitions/list" => {
261            Ok(crate::mobpack::mobpack_agent_definitions_response_with_runtime(runtime))
262        }
263        "mobkit/mobpacks/templates" => Ok(crate::mobpack::mobpack_templates_response_with_runtime(
264            runtime,
265        )),
266        "mobkit/mobpacks/validate" => crate::mobpack::validate_mobpack(params)
267            .and_then(|result| serde_json::to_value(result).map_err(|err| err.to_string())),
268        "mobkit/mobpacks/source" => crate::mobpack::source_mobpack(params)
269            .and_then(|result| serde_json::to_value(result).map_err(|err| err.to_string())),
270        "mobkit/mobpacks/export" => crate::mobpack::export_mobpack(params)
271            .and_then(|result| serde_json::to_value(result).map_err(|err| err.to_string())),
272        "mobkit/mobpacks/import" => crate::mobpack::import_mobpack(params),
273        "mobkit/mobpacks/list" => crate::mobpack::list_mobpack_drafts_with_runtime(params, runtime),
274        "mobkit/mobpacks/get" => crate::mobpack::get_mobpack_draft_with_runtime(params, runtime),
275        "mobkit/mobpacks/create" => crate::mobpack::create_mobpack_draft(params),
276        "mobkit/mobpacks/save" => crate::mobpack::save_mobpack_draft(params),
277        "mobkit/mobpacks/delete" => crate::mobpack::delete_mobpack_draft(params),
278        "mobkit/mobpacks/undo" => crate::mobpack::undo_mobpack_draft(params),
279        "mobkit/mobpacks/redo" => crate::mobpack::redo_mobpack_draft(params),
280        "mobkit/mobpacks/apply_operation" => {
281            crate::mobpack::apply_mobpack_authoring_operation_with_runtime(params, runtime)
282        }
283        "mobkit/mobpacks/graph_projection" => crate::mobpack::graph_projection_mobpack(params),
284        "mobkit/mobpacks/graph_to_flow" => crate::mobpack::graph_to_flow_mobpack(params),
285        "mobkit/mobpacks/deploy_command" => crate::mobpack::deploy_command_preview(params)
286            .and_then(|result| serde_json::to_value(result).map_err(|err| err.to_string())),
287        "mobkit/mobpacks/deploy" => crate::mobpack::deploy_mobpack(params)
288            .and_then(|result| serde_json::to_value(result).map_err(|err| err.to_string())),
289        _ => return None,
290    };
291    Some(match result {
292        Ok(result) => JsonRpcResponse {
293            jsonrpc: JSONRPC_VERSION.to_string(),
294            id: response_id,
295            result: Some(result),
296            error: None,
297        },
298        Err(message) => JsonRpcResponse {
299            jsonrpc: JSONRPC_VERSION.to_string(),
300            id: response_id,
301            result: None,
302            error: Some(JsonRpcError {
303                code: -32602,
304                message,
305                data: None,
306            }),
307        },
308    })
309}
310
311#[derive(Debug, Clone, PartialEq, Eq)]
312pub enum RpcCapabilitiesError {
313    InvalidJson,
314    InvalidSchema,
315    MissingContractVersion,
316    InvalidContractVersion,
317}
318
319impl std::fmt::Display for RpcCapabilitiesError {
320    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321        match self {
322            Self::InvalidJson => write!(f, "invalid JSON"),
323            Self::InvalidSchema => write!(f, "invalid schema"),
324            Self::MissingContractVersion => write!(f, "missing contract version"),
325            Self::InvalidContractVersion => write!(f, "invalid contract version"),
326        }
327    }
328}
329
330impl std::error::Error for RpcCapabilitiesError {}
331
332#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
333pub struct RpcCapabilities {
334    pub contract_version: String,
335    #[serde(flatten)]
336    pub extra: BTreeMap<String, Value>,
337}
338
339pub fn parse_rpc_capabilities(line: &str) -> Result<RpcCapabilities, RpcCapabilitiesError> {
340    let raw: Value = serde_json::from_str(line).map_err(|_| RpcCapabilitiesError::InvalidJson)?;
341    let object = raw.as_object().ok_or(RpcCapabilitiesError::InvalidSchema)?;
342    let contract = object
343        .get("contract_version")
344        .ok_or(RpcCapabilitiesError::MissingContractVersion)?;
345    let contract_str = contract
346        .as_str()
347        .ok_or(RpcCapabilitiesError::InvalidContractVersion)?;
348    if contract_str.trim().is_empty() {
349        return Err(RpcCapabilitiesError::InvalidContractVersion);
350    }
351    serde_json::from_value(raw).map_err(|_| RpcCapabilitiesError::InvalidSchema)
352}
353
354/// JSON-RPC error code returned by `mobkit/mob_events/{query,subscribe}`
355/// when the caller's `after_seq` is past the current ledger frontier.
356/// The error `data` field carries `{ after_cursor, latest_cursor }` so
357/// SDKs can surface a typed exception. Single source of truth — keep
358/// this in sync with `MobEventsStaleError` in the Python and TypeScript
359/// SDKs.
360pub const MOB_EVENTS_STALE_CURSOR_CODE: i64 = -32010;
361
362/// JSON-RPC error code returned by `mobkit/memory/index` and
363/// `mobkit/memory/query` when the configured memory backend cannot
364/// persist or retrieve the row. Distinct from
365/// [`MOB_EVENTS_STALE_CURSOR_CODE`] so SDKs can branch on `-32010`
366/// without misclassifying a memory backend failure as a stale-cursor
367/// event.
368pub const MEMORY_BACKEND_UNAVAILABLE_CODE: i64 = -32012;
369
370/// JSON-RPC error code returned by `mobkit/console/query_timeline` when
371/// the requested console cursor cannot be replayed from the durable console
372/// timeline. Distinct from [`MOB_EVENTS_STALE_CURSOR_CODE`] because SDKs
373/// reify `-32010` specifically as a mob-events ledger error.
374pub const CONSOLE_TIMELINE_REPLAY_UNAVAILABLE_CODE: i64 = -32013;
375
376/// JSON-RPC error code for the fail-closed storage refusals `rpc_gateway`
377/// emits at `mobkit/init` (M5): file-name twins the layout refuses to pick
378/// between, a store that failed to open where the silent fallback used to
379/// be (session/runtime/blob/metadata/console/continuity), and state-root
380/// creation failures. The message carries the remediation (the storage
381/// doctor, or the explicit ephemeral declaration). Distinct from `-32603`
382/// so SDKs can reify a deliberate durability refusal instead of reporting
383/// a generic internal error. Single source of truth — keep in sync with
384/// `StorageResolutionError` in the Python and TypeScript SDKs.
385pub const STORAGE_RESOLUTION_CODE: i64 = -32014;
386
387/// JSON-RPC error code returned by every `mobkit/workgraph/*` method when
388/// the runtime has no WorkGraph service configured
389/// (`data.kind = "workgraph_unavailable"`). Single source of truth — keep
390/// in sync with the Python and TypeScript SDKs.
391pub const WORKGRAPH_UNAVAILABLE_CODE: i64 = -32041;
392
393/// JSON-RPC error code for WorkGraph CAS/revision conflicts (upstream
394/// `StaleRevision`/`Conflict`), `data.kind = "workgraph_conflict"` with the
395/// upstream message in `data.detail`. SDKs and the console retry by
396/// refetching the current revision.
397pub const WORKGRAPH_CONFLICT_CODE: i64 = -32042;
398
399/// JSON-RPC error code for all other WorkGraph domain failures
400/// (`data.kind = "workgraph_error"`, full detail — K2 disclosure posture).
401pub const WORKGRAPH_ERROR_CODE: i64 = -32000;
402
403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
404pub struct JsonRpcRequest {
405    pub jsonrpc: String,
406    #[serde(default)]
407    pub id: Option<Value>,
408    pub method: String,
409    #[serde(default)]
410    pub params: Value,
411}
412
413#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
414pub struct JsonRpcError {
415    pub code: i64,
416    pub message: String,
417    /// Optional structured payload as defined by JSON-RPC 2.0. Used by
418    /// typed errors (e.g. `event_query_stale` with `after_cursor` /
419    /// `latest_cursor`) so SDKs can surface a typed exception. Existing
420    /// construction sites can omit it via `..Default::default()`.
421    #[serde(default, skip_serializing_if = "Option::is_none")]
422    pub data: Option<Value>,
423}
424
425impl JsonRpcError {
426    pub fn new(code: i64, message: impl Into<String>) -> Self {
427        Self {
428            code,
429            message: message.into(),
430            data: None,
431        }
432    }
433
434    #[must_use]
435    pub fn with_data(mut self, data: Value) -> Self {
436        self.data = Some(data);
437        self
438    }
439}
440
441#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
442pub struct JsonRpcResponse {
443    pub jsonrpc: String,
444    pub id: Value,
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub result: Option<Value>,
447    #[serde(skip_serializing_if = "Option::is_none")]
448    pub error: Option<JsonRpcError>,
449}
450
451pub fn handle_mobkit_rpc_json(
452    runtime: &mut MobkitRuntimeHandle,
453    request_json: &str,
454    timeout: Duration,
455) -> String {
456    let raw_request: Value = match serde_json::from_str(request_json) {
457        Ok(raw_request) => raw_request,
458        Err(_) => {
459            return serialize_response(&JsonRpcResponse {
460                jsonrpc: JSONRPC_VERSION.to_string(),
461                id: Value::Null,
462                result: None,
463                error: Some(JsonRpcError {
464                    code: -32700,
465                    message: "Parse error".to_string(),
466                    data: None,
467                }),
468            });
469        }
470    };
471    let response_id = raw_request
472        .as_object()
473        .and_then(|object| object.get("id"))
474        .cloned()
475        .unwrap_or(Value::Null);
476    let request: JsonRpcRequest = match serde_json::from_value(raw_request) {
477        Ok(request) => request,
478        Err(_) => {
479            return serialize_response(&JsonRpcResponse {
480                jsonrpc: JSONRPC_VERSION.to_string(),
481                id: response_id,
482                result: None,
483                error: Some(JsonRpcError {
484                    code: -32600,
485                    message: "Invalid Request".to_string(),
486                    data: None,
487                }),
488            });
489        }
490    };
491    let is_notification = request.id.is_none();
492    let response_id = request.id.clone().unwrap_or(Value::Null);
493
494    if request.jsonrpc != "2.0" {
495        let response = JsonRpcResponse {
496            jsonrpc: JSONRPC_VERSION.to_string(),
497            id: response_id,
498            result: None,
499            error: Some(JsonRpcError {
500                code: -32600,
501                message: "Invalid Request".to_string(),
502                data: None,
503            }),
504        };
505        return if is_notification {
506            String::new()
507        } else {
508            serialize_response(&response)
509        };
510    }
511
512    if let Err(message) =
513        crate::member_comms_id::validate_public_rpc_member_aliases(&request.params)
514    {
515        let response = JsonRpcResponse {
516            jsonrpc: JSONRPC_VERSION.to_string(),
517            id: response_id,
518            result: None,
519            error: Some(JsonRpcError {
520                code: -32602,
521                message: format!("Invalid params: {message}"),
522                data: None,
523            }),
524        };
525        return if is_notification {
526            String::new()
527        } else {
528            serialize_response(&response)
529        };
530    }
531
532    let response = match request.method.as_str() {
533        // No `storage` object here: the module-only server holds a bare
534        // `MobkitRuntimeHandle` with no mob/unified runtime, so the
535        // composition-time `ResolvedStorageSummary` (H1/H2) is not reachable
536        // on this surface.
537        "mobkit/status" => JsonRpcResponse {
538            jsonrpc: JSONRPC_VERSION.to_string(),
539            id: response_id,
540            result: Some(serde_json::json!({
541                "contract_version": MOBKIT_CONTRACT_VERSION,
542                "running": runtime.is_running(),
543                "loaded_modules": runtime.loaded_modules(),
544            })),
545            error: None,
546        },
547        "mobkit/capabilities" => {
548            let mut methods = vec![
549                "mobkit/status",
550                "mobkit/capabilities",
551                "mobkit/reconcile",
552                "mobkit/spawn_member",
553                "mobkit/scheduling/evaluate",
554                "mobkit/scheduling/dispatch",
555                "mobkit/routing/resolve",
556                "mobkit/routing/routes/list",
557                "mobkit/routing/routes/add",
558                "mobkit/routing/routes/delete",
559                "mobkit/delivery/send",
560                "mobkit/delivery/history",
561                "mobkit/events/subscribe",
562                "mobkit/memory/stores",
563                "mobkit/memory/index",
564                "mobkit/memory/query",
565                "mobkit/session_store/bigquery",
566                "mobkit/gating/evaluate",
567                "mobkit/gating/pending",
568                "mobkit/gating/decide",
569                "mobkit/gating/audit",
570                "mobkit/call_tool",
571                "mobkit/models/catalog",
572                storage_methods::STORAGE_DOCTOR_METHOD,
573            ];
574            methods.extend_from_slice(MOBPACK_AUTHORING_METHODS);
575            JsonRpcResponse {
576                jsonrpc: JSONRPC_VERSION.to_string(),
577                id: response_id,
578                result: Some(serde_json::json!({
579                    "contract_version": MOBKIT_CONTRACT_VERSION,
580                    "methods": methods,
581                    "loaded_modules": runtime.loaded_modules(),
582                    "runtime_capabilities": {
583                        "can_spawn_members": false,
584                        "can_send_messages": false,
585                        "can_wire_members": false,
586                        "can_retire_members": false,
587                        "available_spawn_modes": ["module"],
588                    },
589                    "authoring_capabilities": mobpack_authoring_capabilities(),
590                })),
591                error: None,
592            }
593        }
594        "mobkit/models/catalog" => JsonRpcResponse {
595            jsonrpc: JSONRPC_VERSION.to_string(),
596            id: response_id,
597            result: Some(build_models_catalog_result()),
598            error: None,
599        },
600        // Read-only state-directory diagnosis. The module-only server holds
601        // no runtime state directory, so `state_dir` is required here and
602        // the durability census always reports unavailable.
603        storage_methods::STORAGE_DOCTOR_METHOD => {
604            match storage_methods::parse_storage_doctor_params(&request.params) {
605                Ok(Some(params)) => {
606                    let diagnosis = crate::storage_doctor::diagnose_state_dir_blocking_with_options(
607                        &params.scope(),
608                        None,
609                        params.doctor_options(),
610                    );
611                    JsonRpcResponse {
612                        jsonrpc: JSONRPC_VERSION.to_string(),
613                        id: response_id,
614                        result: Some(storage_methods::storage_doctor_result_json(
615                            &params, &diagnosis, None,
616                        )),
617                        error: None,
618                    }
619                }
620                Ok(None) => JsonRpcResponse {
621                    jsonrpc: JSONRPC_VERSION.to_string(),
622                    id: response_id,
623                    result: None,
624                    error: Some(JsonRpcError {
625                        code: -32602,
626                        message: "Invalid params: state_dir required (the module-only runtime \
627                                  has no state directory)"
628                            .to_string(),
629                        data: None,
630                    }),
631                },
632                Err(reason) => JsonRpcResponse {
633                    jsonrpc: JSONRPC_VERSION.to_string(),
634                    id: response_id,
635                    result: None,
636                    error: Some(JsonRpcError {
637                        code: -32602,
638                        message: format!("Invalid params: {reason}"),
639                        data: None,
640                    }),
641                },
642            }
643        }
644        method if MOBPACK_AUTHORING_METHODS.contains(&method) => {
645            handle_mobpack_authoring_rpc(method, &request.params, response_id.clone())
646                .unwrap_or_else(|| JsonRpcResponse {
647                    jsonrpc: JSONRPC_VERSION.to_string(),
648                    id: response_id,
649                    result: None,
650                    error: Some(JsonRpcError {
651                        code: -32601,
652                        message: "Method not found".to_string(),
653                        data: None,
654                    }),
655                })
656        }
657        "mobkit/reconcile" => {
658            let modules = match params::required_string_array(&request.params, "modules") {
659                Ok(m) => m,
660                Err(reason) => {
661                    return serialize_response(&JsonRpcResponse {
662                        jsonrpc: JSONRPC_VERSION.to_string(),
663                        id: response_id,
664                        result: None,
665                        error: Some(JsonRpcError {
666                            code: -32602,
667                            message: format!("Invalid params: {reason}"),
668                            data: None,
669                        }),
670                    });
671                }
672            };
673
674            match runtime.reconcile_modules(modules.clone(), timeout) {
675                Ok(added) => JsonRpcResponse {
676                    jsonrpc: JSONRPC_VERSION.to_string(),
677                    id: response_id,
678                    result: Some(serde_json::json!({
679                        "accepted": true,
680                        "reconciled_modules": modules,
681                        "added": added
682                    })),
683                    error: None,
684                },
685                Err(err) => JsonRpcResponse {
686                    jsonrpc: JSONRPC_VERSION.to_string(),
687                    id: response_id,
688                    result: None,
689                    error: Some(JsonRpcError {
690                        code: -32602,
691                        message: format!("Invalid params: {err:?}"),
692                        data: None,
693                    }),
694                },
695            }
696        }
697        "mobkit/spawn_member" => {
698            let module_id = request
699                .params
700                .get("module_id")
701                .and_then(Value::as_str)
702                .unwrap_or_default()
703                .to_string();
704            if module_id.is_empty() {
705                JsonRpcResponse {
706                    jsonrpc: JSONRPC_VERSION.to_string(),
707                    id: response_id,
708                    result: None,
709                    error: Some(JsonRpcError {
710                        code: -32602,
711                        message: "Invalid params: module_id required".to_string(),
712                        data: None,
713                    }),
714                }
715            } else {
716                match runtime.spawn_member(&module_id, timeout) {
717                    Ok(()) => JsonRpcResponse {
718                        jsonrpc: JSONRPC_VERSION.to_string(),
719                        id: response_id,
720                        result: Some(serde_json::json!({
721                            "accepted": true,
722                            "module_id": module_id
723                        })),
724                        error: None,
725                    },
726                    Err(err) => JsonRpcResponse {
727                        jsonrpc: JSONRPC_VERSION.to_string(),
728                        id: response_id,
729                        result: None,
730                        error: Some(JsonRpcError {
731                            code: -32602,
732                            message: format!("Invalid params: {err:?}"),
733                            data: None,
734                        }),
735                    },
736                }
737            }
738        }
739        "mobkit/scheduling/evaluate" => match parse_scheduling_params(&request.params) {
740            Ok((schedules, tick_ms)) => match runtime.evaluate_schedule_tick(&schedules, tick_ms) {
741                Ok(evaluation) => JsonRpcResponse {
742                    jsonrpc: JSONRPC_VERSION.to_string(),
743                    id: response_id,
744                    result: Some(serde_json::to_value(evaluation).unwrap_or(Value::Null)),
745                    error: None,
746                },
747                Err(err) => JsonRpcResponse {
748                    jsonrpc: JSONRPC_VERSION.to_string(),
749                    id: response_id,
750                    result: None,
751                    error: Some(JsonRpcError {
752                        code: -32602,
753                        message: format!(
754                            "Invalid params: {}",
755                            format_schedule_validation_error(err)
756                        ),
757                        data: None,
758                    }),
759                },
760            },
761            Err(message) => JsonRpcResponse {
762                jsonrpc: JSONRPC_VERSION.to_string(),
763                id: response_id,
764                result: None,
765                error: Some(JsonRpcError {
766                    code: -32602,
767                    message: format!("Invalid params: {message}"),
768                    data: None,
769                }),
770            },
771        },
772        "mobkit/scheduling/dispatch" => match parse_scheduling_params(&request.params) {
773            Ok((schedules, tick_ms)) => match runtime.dispatch_schedule_tick(&schedules, tick_ms) {
774                Ok(dispatch) => JsonRpcResponse {
775                    jsonrpc: JSONRPC_VERSION.to_string(),
776                    id: response_id,
777                    result: Some(serde_json::to_value(dispatch).unwrap_or(Value::Null)),
778                    error: None,
779                },
780                Err(err) => JsonRpcResponse {
781                    jsonrpc: JSONRPC_VERSION.to_string(),
782                    id: response_id,
783                    result: None,
784                    error: Some(JsonRpcError {
785                        code: -32602,
786                        message: format!(
787                            "Invalid params: {}",
788                            format_schedule_validation_error(err)
789                        ),
790                        data: None,
791                    }),
792                },
793            },
794            Err(message) => JsonRpcResponse {
795                jsonrpc: JSONRPC_VERSION.to_string(),
796                id: response_id,
797                result: None,
798                error: Some(JsonRpcError {
799                    code: -32602,
800                    message: format!("Invalid params: {message}"),
801                    data: None,
802                }),
803            },
804        },
805        "mobkit/routing/resolve" => {
806            match parse_routing_resolve_params(&request.params).and_then(|resolve_request| {
807                runtime
808                    .resolve_routing(resolve_request)
809                    .map_err(RoutingDeliveryParamsError::Routing)
810            }) {
811                Ok(resolution) => JsonRpcResponse {
812                    jsonrpc: JSONRPC_VERSION.to_string(),
813                    id: response_id,
814                    result: Some(serde_json::to_value(resolution).unwrap_or(Value::Null)),
815                    error: None,
816                },
817                Err(err) => JsonRpcResponse {
818                    jsonrpc: JSONRPC_VERSION.to_string(),
819                    id: response_id,
820                    result: None,
821                    error: Some(JsonRpcError {
822                        code: -32602,
823                        message: format!("Invalid params: {}", err.message()),
824                        data: None,
825                    }),
826                },
827            }
828        }
829        "mobkit/routing/routes/list" => match parse_routing_routes_list_params(&request.params) {
830            Ok(()) => JsonRpcResponse {
831                jsonrpc: JSONRPC_VERSION.to_string(),
832                id: response_id,
833                result: Some(serde_json::json!({
834                    "routes": runtime.list_runtime_routes()
835                })),
836                error: None,
837            },
838            Err(err) => JsonRpcResponse {
839                jsonrpc: JSONRPC_VERSION.to_string(),
840                id: response_id,
841                result: None,
842                error: Some(JsonRpcError {
843                    code: -32602,
844                    message: format!("Invalid params: {}", err.message()),
845                    data: None,
846                }),
847            },
848        },
849        "mobkit/routing/routes/add" => match parse_routing_route_add_params(&request.params)
850            .and_then(|route| {
851                runtime
852                    .add_runtime_route(route)
853                    .map_err(RoutingDeliveryParamsError::RouteMutation)
854            }) {
855            Ok(route) => JsonRpcResponse {
856                jsonrpc: JSONRPC_VERSION.to_string(),
857                id: response_id,
858                result: Some(serde_json::json!({ "route": route })),
859                error: None,
860            },
861            Err(err) => JsonRpcResponse {
862                jsonrpc: JSONRPC_VERSION.to_string(),
863                id: response_id,
864                result: None,
865                error: Some(JsonRpcError {
866                    code: -32602,
867                    message: format!("Invalid params: {}", err.message()),
868                    data: None,
869                }),
870            },
871        },
872        "mobkit/routing/routes/delete" => match parse_routing_route_delete_params(&request.params)
873            .and_then(|route_key| {
874                runtime
875                    .delete_runtime_route(&route_key)
876                    .map_err(RoutingDeliveryParamsError::RouteMutation)
877            }) {
878            Ok(route) => JsonRpcResponse {
879                jsonrpc: JSONRPC_VERSION.to_string(),
880                id: response_id,
881                result: Some(serde_json::json!({ "deleted": route })),
882                error: None,
883            },
884            Err(err) => JsonRpcResponse {
885                jsonrpc: JSONRPC_VERSION.to_string(),
886                id: response_id,
887                result: None,
888                error: Some(JsonRpcError {
889                    code: -32602,
890                    message: format!("Invalid params: {}", err.message()),
891                    data: None,
892                }),
893            },
894        },
895        "mobkit/delivery/send" => {
896            match parse_delivery_send_params(&request.params).and_then(|send_request| {
897                runtime
898                    .send_delivery(send_request)
899                    .map_err(RoutingDeliveryParamsError::Delivery)
900            }) {
901                Ok(record) => JsonRpcResponse {
902                    jsonrpc: JSONRPC_VERSION.to_string(),
903                    id: response_id,
904                    result: Some(serde_json::to_value(record).unwrap_or(Value::Null)),
905                    error: None,
906                },
907                Err(err) => JsonRpcResponse {
908                    jsonrpc: JSONRPC_VERSION.to_string(),
909                    id: response_id,
910                    result: None,
911                    error: Some(JsonRpcError {
912                        code: -32602,
913                        message: format!("Invalid params: {}", err.message()),
914                        data: None,
915                    }),
916                },
917            }
918        }
919        "mobkit/delivery/history" => match parse_delivery_history_params(&request.params) {
920            Ok(history_request) => JsonRpcResponse {
921                jsonrpc: JSONRPC_VERSION.to_string(),
922                id: response_id,
923                result: Some(
924                    serde_json::to_value(runtime.delivery_history(history_request))
925                        .unwrap_or(Value::Null),
926                ),
927                error: None,
928            },
929            Err(err) => JsonRpcResponse {
930                jsonrpc: JSONRPC_VERSION.to_string(),
931                id: response_id,
932                result: None,
933                error: Some(JsonRpcError {
934                    code: -32602,
935                    message: format!("Invalid params: {}", err.message()),
936                    data: None,
937                }),
938            },
939        },
940        "mobkit/events/subscribe" => {
941            match parse_subscribe_request(&request.params).and_then(|subscribe_request| {
942                runtime
943                    .subscribe_events(subscribe_request)
944                    .map_err(SubscribeParamsError::Runtime)
945            }) {
946                Ok(subscribe_result) => JsonRpcResponse {
947                    jsonrpc: JSONRPC_VERSION.to_string(),
948                    id: response_id,
949                    result: Some(serde_json::to_value(subscribe_result).unwrap_or(Value::Null)),
950                    error: None,
951                },
952                Err(err) => JsonRpcResponse {
953                    jsonrpc: JSONRPC_VERSION.to_string(),
954                    id: response_id,
955                    result: None,
956                    error: Some(JsonRpcError {
957                        code: -32602,
958                        message: format!("Invalid params: {}", err.message()),
959                        data: None,
960                    }),
961                },
962            }
963        }
964        "mobkit/memory/stores" => match parse_memory_stores_params(&request.params) {
965            Ok(()) => JsonRpcResponse {
966                jsonrpc: JSONRPC_VERSION.to_string(),
967                id: response_id,
968                result: Some(serde_json::json!({
969                    "stores": runtime.memory_stores(),
970                })),
971                error: None,
972            },
973            Err(err) => JsonRpcResponse {
974                jsonrpc: JSONRPC_VERSION.to_string(),
975                id: response_id,
976                result: None,
977                error: Some(JsonRpcError {
978                    code: -32602,
979                    message: format!("Invalid params: {}", err.message()),
980                    data: None,
981                }),
982            },
983        },
984        "mobkit/memory/index" => match parse_memory_index_params(&request.params) {
985            Ok(index_request) => match runtime.memory_index(index_request) {
986                Ok(indexed) => JsonRpcResponse {
987                    jsonrpc: JSONRPC_VERSION.to_string(),
988                    id: response_id,
989                    result: Some(serde_json::to_value(indexed).unwrap_or(Value::Null)),
990                    error: None,
991                },
992                Err(MemoryIndexError::BackendPersistFailed(error)) => JsonRpcResponse {
993                    jsonrpc: JSONRPC_VERSION.to_string(),
994                    id: response_id,
995                    result: None,
996                    error: Some(JsonRpcError {
997                        code: MEMORY_BACKEND_UNAVAILABLE_CODE,
998                        message: format!(
999                            "Memory backend unavailable: {}",
1000                            MemoryParamsError::backend_message(&error)
1001                        ),
1002                        data: None,
1003                    }),
1004                },
1005                Err(err) => JsonRpcResponse {
1006                    jsonrpc: JSONRPC_VERSION.to_string(),
1007                    id: response_id,
1008                    result: None,
1009                    error: Some(JsonRpcError {
1010                        code: -32602,
1011                        message: format!(
1012                            "Invalid params: {}",
1013                            MemoryParamsError::Index(err).message()
1014                        ),
1015                        data: None,
1016                    }),
1017                },
1018            },
1019            Err(err) => JsonRpcResponse {
1020                jsonrpc: JSONRPC_VERSION.to_string(),
1021                id: response_id,
1022                result: None,
1023                error: Some(JsonRpcError {
1024                    code: -32602,
1025                    message: format!("Invalid params: {}", err.message()),
1026                    data: None,
1027                }),
1028            },
1029        },
1030        "mobkit/memory/query" => match parse_memory_query_params(&request.params) {
1031            Ok(query_request) => JsonRpcResponse {
1032                jsonrpc: JSONRPC_VERSION.to_string(),
1033                id: response_id,
1034                result: Some(
1035                    serde_json::to_value(runtime.memory_query(query_request))
1036                        .unwrap_or(Value::Null),
1037                ),
1038                error: None,
1039            },
1040            Err(err) => JsonRpcResponse {
1041                jsonrpc: JSONRPC_VERSION.to_string(),
1042                id: response_id,
1043                result: None,
1044                error: Some(JsonRpcError {
1045                    code: -32602,
1046                    message: format!("Invalid params: {}", err.message()),
1047                    data: None,
1048                }),
1049            },
1050        },
1051        "mobkit/session_store/bigquery" => {
1052            match parse_bigquery_session_store_params(&request.params)
1053                .and_then(run_bigquery_session_store_request)
1054            {
1055                Ok(result) => JsonRpcResponse {
1056                    jsonrpc: JSONRPC_VERSION.to_string(),
1057                    id: response_id,
1058                    result: Some(result),
1059                    error: None,
1060                },
1061                Err(BigQuerySessionStoreRpcError::Params(message)) => JsonRpcResponse {
1062                    jsonrpc: JSONRPC_VERSION.to_string(),
1063                    id: response_id,
1064                    result: None,
1065                    error: Some(JsonRpcError {
1066                        code: -32602,
1067                        message: format!("Invalid params: {message}"),
1068                        data: None,
1069                    }),
1070                },
1071                Err(BigQuerySessionStoreRpcError::Store(error)) => JsonRpcResponse {
1072                    jsonrpc: JSONRPC_VERSION.to_string(),
1073                    id: response_id,
1074                    result: None,
1075                    error: Some(JsonRpcError {
1076                        code: -32011,
1077                        message: format!(
1078                            "BigQuery session store request failed: {}",
1079                            format_bigquery_store_error(&error)
1080                        ),
1081                        data: None,
1082                    }),
1083                },
1084            }
1085        }
1086        "mobkit/gating/evaluate" => match parse_gating_evaluate_params(&request.params) {
1087            Ok(gating_request) => JsonRpcResponse {
1088                jsonrpc: JSONRPC_VERSION.to_string(),
1089                id: response_id,
1090                result: Some(
1091                    serde_json::to_value(runtime.evaluate_gating_action(gating_request))
1092                        .unwrap_or(Value::Null),
1093                ),
1094                error: None,
1095            },
1096            Err(err) => JsonRpcResponse {
1097                jsonrpc: JSONRPC_VERSION.to_string(),
1098                id: response_id,
1099                result: None,
1100                error: Some(JsonRpcError {
1101                    code: -32602,
1102                    message: format!("Invalid params: {}", err.message()),
1103                    data: None,
1104                }),
1105            },
1106        },
1107        "mobkit/gating/pending" => match parse_gating_pending_params(&request.params) {
1108            Ok(()) => JsonRpcResponse {
1109                jsonrpc: JSONRPC_VERSION.to_string(),
1110                id: response_id,
1111                result: Some(serde_json::json!({
1112                    "pending": runtime.list_gating_pending(),
1113                })),
1114                error: None,
1115            },
1116            Err(err) => JsonRpcResponse {
1117                jsonrpc: JSONRPC_VERSION.to_string(),
1118                id: response_id,
1119                result: None,
1120                error: Some(JsonRpcError {
1121                    code: -32602,
1122                    message: format!("Invalid params: {}", err.message()),
1123                    data: None,
1124                }),
1125            },
1126        },
1127        "mobkit/gating/decide" => {
1128            match parse_gating_decide_params(&request.params).and_then(|decide_request| {
1129                runtime
1130                    .decide_gating_action(decide_request)
1131                    .map_err(GatingParamsError::Decision)
1132            }) {
1133                Ok(result) => JsonRpcResponse {
1134                    jsonrpc: JSONRPC_VERSION.to_string(),
1135                    id: response_id,
1136                    result: Some(serde_json::to_value(result).unwrap_or(Value::Null)),
1137                    error: None,
1138                },
1139                Err(err) => JsonRpcResponse {
1140                    jsonrpc: JSONRPC_VERSION.to_string(),
1141                    id: response_id,
1142                    result: None,
1143                    error: Some(JsonRpcError {
1144                        code: -32602,
1145                        message: format!("Invalid params: {}", err.message()),
1146                        data: None,
1147                    }),
1148                },
1149            }
1150        }
1151        "mobkit/gating/audit" => match parse_gating_audit_params(&request.params) {
1152            Ok(limit) => JsonRpcResponse {
1153                jsonrpc: JSONRPC_VERSION.to_string(),
1154                id: response_id,
1155                result: Some(serde_json::json!({
1156                    "entries": runtime.gating_audit_entries(limit),
1157                })),
1158                error: None,
1159            },
1160            Err(err) => JsonRpcResponse {
1161                jsonrpc: JSONRPC_VERSION.to_string(),
1162                id: response_id,
1163                result: None,
1164                error: Some(JsonRpcError {
1165                    code: -32602,
1166                    message: format!("Invalid params: {}", err.message()),
1167                    data: None,
1168                }),
1169            },
1170        },
1171        "mobkit/call_tool" => {
1172            let module_id = request.params.get("module_id").and_then(Value::as_str);
1173            let tool = request.params.get("tool").and_then(Value::as_str);
1174            let arguments = request
1175                .params
1176                .get("arguments")
1177                .cloned()
1178                .unwrap_or(serde_json::json!({}));
1179
1180            match (module_id, tool) {
1181                (Some(module_id), Some(tool)) if !module_id.is_empty() && !tool.is_empty() => {
1182                    let route = route_module_call(
1183                        runtime,
1184                        &ModuleRouteRequest {
1185                            module_id: module_id.to_string(),
1186                            method: tool.to_string(),
1187                            params: arguments,
1188                        },
1189                        timeout,
1190                    );
1191                    match route {
1192                        Ok(response) => JsonRpcResponse {
1193                            jsonrpc: JSONRPC_VERSION.to_string(),
1194                            id: response_id,
1195                            result: Some(serde_json::json!({
1196                                "module_id": response.module_id,
1197                                "tool": response.method,
1198                                "result": response.payload
1199                            })),
1200                            error: None,
1201                        },
1202                        Err(ModuleRouteError::UnloadedModule(mid)) => JsonRpcResponse {
1203                            jsonrpc: JSONRPC_VERSION.to_string(),
1204                            id: response_id,
1205                            result: None,
1206                            error: Some(JsonRpcError {
1207                                code: -32601,
1208                                message: format!("Module '{mid}' not loaded"),
1209                                data: None,
1210                            }),
1211                        },
1212                        Err(err) => JsonRpcResponse {
1213                            jsonrpc: JSONRPC_VERSION.to_string(),
1214                            id: response_id,
1215                            result: None,
1216                            error: Some(JsonRpcError {
1217                                code: -32000,
1218                                message: format!("Tool call failed: {err:?}"),
1219                                data: None,
1220                            }),
1221                        },
1222                    }
1223                }
1224                _ => JsonRpcResponse {
1225                    jsonrpc: JSONRPC_VERSION.to_string(),
1226                    id: response_id,
1227                    result: None,
1228                    error: Some(JsonRpcError {
1229                        code: -32602,
1230                        message: "Invalid params: module_id and tool required".to_string(),
1231                        data: None,
1232                    }),
1233                },
1234            }
1235        }
1236        method if method.contains('/') && !method.starts_with("mobkit/") => {
1237            let module_id = method
1238                .split('/')
1239                .next()
1240                .map(ToString::to_string)
1241                .unwrap_or_default();
1242            let route = route_module_call(
1243                runtime,
1244                &ModuleRouteRequest {
1245                    module_id,
1246                    method: method.to_string(),
1247                    params: request.params,
1248                },
1249                timeout,
1250            );
1251            match route {
1252                Ok(response) => JsonRpcResponse {
1253                    jsonrpc: JSONRPC_VERSION.to_string(),
1254                    id: response_id,
1255                    result: Some(serde_json::json!({
1256                        "module_id": response.module_id,
1257                        "method": response.method,
1258                        "payload": response.payload
1259                    })),
1260                    error: None,
1261                },
1262                Err(ModuleRouteError::UnloadedModule(module_id)) => JsonRpcResponse {
1263                    jsonrpc: JSONRPC_VERSION.to_string(),
1264                    id: response_id,
1265                    result: None,
1266                    error: Some(JsonRpcError {
1267                        code: -32601,
1268                        message: format!("Module '{module_id}' not loaded"),
1269                        data: None,
1270                    }),
1271                },
1272                Err(err) => JsonRpcResponse {
1273                    jsonrpc: JSONRPC_VERSION.to_string(),
1274                    id: response_id,
1275                    result: None,
1276                    error: Some(JsonRpcError {
1277                        code: -32000,
1278                        message: format!("Module route failed: {err:?}"),
1279                        data: None,
1280                    }),
1281                },
1282            }
1283        }
1284        _ => JsonRpcResponse {
1285            jsonrpc: JSONRPC_VERSION.to_string(),
1286            id: response_id,
1287            result: None,
1288            error: Some(JsonRpcError {
1289                code: -32601,
1290                message: "Method not found".to_string(),
1291                data: None,
1292            }),
1293        },
1294    };
1295    if is_notification {
1296        String::new()
1297    } else {
1298        serialize_response(&response)
1299    }
1300}
1301
1302/// Identity-first runtime context passed to the RPC handler.
1303pub struct IdentityFirstContext {
1304    pub runtime: std::sync::Arc<crate::identity_first::IdentityRuntime>,
1305    pub roster_provider: std::sync::Arc<dyn crate::identity_first::contracts::RosterProvider>,
1306    pub topology_provider:
1307        Option<std::sync::Arc<dyn crate::identity_first::contracts::TopologyProvider>>,
1308    pub customizer: Option<std::sync::Arc<dyn crate::identity_first::contracts::AgentCustomizer>>,
1309    pub agent_memory_provider:
1310        Option<std::sync::Arc<dyn crate::identity_first::AgentMemoryProvider>>,
1311    pub mob_definition: Option<meerkat_mob::MobDefinition>,
1312}
1313
1314#[derive(Debug, Clone, PartialEq, Eq)]
1315enum IdentityMemberReadiness {
1316    Ready,
1317    TimedOut,
1318    Failed(String),
1319}
1320
1321struct IdentityStartupReadyWait {
1322    status: crate::identity_first::IdentityBootstrapStatus,
1323    timed_out: bool,
1324    startup_ready: bool,
1325}
1326
1327/// Join identity bootstrap terminality and mob-member readiness at one
1328/// bootstrap generation. The readiness callback takes owned ids so production
1329/// can move them into the mob future while deterministic tests inject a gated
1330/// completion/error without mocking the full mob actor.
1331async fn wait_identity_startup_ready<F, Fut>(
1332    identity_rt: &crate::identity_first::IdentityRuntime,
1333    wait_timeout: Duration,
1334    mut wait_members: F,
1335) -> Result<IdentityStartupReadyWait, String>
1336where
1337    F: FnMut(Vec<meerkat_mob::ids::AgentIdentity>, Duration) -> Fut + Send,
1338    Fut: Future<Output = IdentityMemberReadiness> + Send,
1339{
1340    let started = std::time::Instant::now();
1341    let (mut status, mut timed_out, mut generation) = identity_rt
1342        .wait_identity_bootstrap_terminal_with_generation(wait_timeout)
1343        .await;
1344    loop {
1345        if timed_out || !status.ready {
1346            return Ok(IdentityStartupReadyWait {
1347                status,
1348                timed_out,
1349                startup_ready: false,
1350            });
1351        }
1352
1353        // Subscribe before validating the terminal snapshot so a reconcile
1354        // that starts during member readiness wakes this loop instead of
1355        // combining observations from two different passes.
1356        let mut bootstrap_changes = identity_rt.subscribe_identity_bootstrap_status();
1357        let (current_generation, current_status) =
1358            identity_rt.identity_bootstrap_status_with_generation();
1359        if current_generation != generation || current_status != status {
1360            let remaining = wait_timeout.saturating_sub(started.elapsed());
1361            if remaining.is_zero() {
1362                return Ok(IdentityStartupReadyWait {
1363                    status: current_status,
1364                    timed_out: true,
1365                    startup_ready: false,
1366                });
1367            }
1368            (status, timed_out, generation) = identity_rt
1369                .wait_identity_bootstrap_terminal_with_generation(remaining)
1370                .await;
1371            continue;
1372        }
1373
1374        let member_ids = identity_rt
1375            .identity_bootstrap_member_ids_for_status(&status)
1376            .await;
1377        let (mapped_generation, mapped_status) =
1378            identity_rt.identity_bootstrap_status_with_generation();
1379        if mapped_generation != generation || mapped_status != status {
1380            let remaining = wait_timeout.saturating_sub(started.elapsed());
1381            if remaining.is_zero() {
1382                return Ok(IdentityStartupReadyWait {
1383                    status: mapped_status,
1384                    timed_out: true,
1385                    startup_ready: false,
1386                });
1387            }
1388            (status, timed_out, generation) = identity_rt
1389                .wait_identity_bootstrap_terminal_with_generation(remaining)
1390                .await;
1391            continue;
1392        }
1393        if member_ids.len() != status.identities.len() {
1394            return Err("identity bootstrap readiness mapping is incomplete".to_string());
1395        }
1396
1397        let remaining = wait_timeout.saturating_sub(started.elapsed());
1398        let readiness = wait_members(member_ids, remaining);
1399        tokio::pin!(readiness);
1400        let readiness_result = tokio::select! {
1401            result = &mut readiness => Some(result),
1402            changed = bootstrap_changes.changed() => {
1403                if changed.is_err() {
1404                    return Err(
1405                        "identity bootstrap readiness status channel closed".to_string(),
1406                    );
1407                }
1408                None
1409            }
1410        };
1411        let Some(readiness_result) = readiness_result else {
1412            let remaining = wait_timeout.saturating_sub(started.elapsed());
1413            if remaining.is_zero() {
1414                let (_, latest) = identity_rt.identity_bootstrap_status_with_generation();
1415                return Ok(IdentityStartupReadyWait {
1416                    status: latest,
1417                    timed_out: true,
1418                    startup_ready: false,
1419                });
1420            }
1421            (status, timed_out, generation) = identity_rt
1422                .wait_identity_bootstrap_terminal_with_generation(remaining)
1423                .await;
1424            continue;
1425        };
1426
1427        // `select!` may choose a simultaneously-ready stale member result
1428        // even though the bootstrap-change branch is ready too. Validate the
1429        // generation before handling success, timeout, or error alike.
1430        let (result_generation, result_status) =
1431            identity_rt.identity_bootstrap_status_with_generation();
1432        if result_generation != generation || result_status != status {
1433            let remaining = wait_timeout.saturating_sub(started.elapsed());
1434            if remaining.is_zero() {
1435                return Ok(IdentityStartupReadyWait {
1436                    status: result_status,
1437                    timed_out: true,
1438                    startup_ready: false,
1439                });
1440            }
1441            (status, timed_out, generation) = identity_rt
1442                .wait_identity_bootstrap_terminal_with_generation(remaining)
1443                .await;
1444            continue;
1445        }
1446        match readiness_result {
1447            IdentityMemberReadiness::Ready => {
1448                let (ready_generation, ready_status) =
1449                    identity_rt.identity_bootstrap_status_with_generation();
1450                if ready_generation == generation && ready_status == status {
1451                    return Ok(IdentityStartupReadyWait {
1452                        status,
1453                        timed_out: false,
1454                        startup_ready: true,
1455                    });
1456                }
1457                let remaining = wait_timeout.saturating_sub(started.elapsed());
1458                if remaining.is_zero() {
1459                    return Ok(IdentityStartupReadyWait {
1460                        status: ready_status,
1461                        timed_out: true,
1462                        startup_ready: false,
1463                    });
1464                }
1465                (status, timed_out, generation) = identity_rt
1466                    .wait_identity_bootstrap_terminal_with_generation(remaining)
1467                    .await;
1468            }
1469            IdentityMemberReadiness::TimedOut => {
1470                return Ok(IdentityStartupReadyWait {
1471                    status,
1472                    timed_out: true,
1473                    startup_ready: false,
1474                });
1475            }
1476            IdentityMemberReadiness::Failed(error) => {
1477                return Err(format!("identity bootstrap readiness failed: {error}"));
1478            }
1479        }
1480    }
1481}
1482
1483pub fn handle_unified_rpc_json<'a>(
1484    runtime: &'a UnifiedRuntime,
1485    request_json: &'a str,
1486    timeout: Duration,
1487    http_base_url: Option<&'a str>,
1488    identity_ctx: Option<&'a IdentityFirstContext>,
1489) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
1490    handle_unified_rpc_json_with_live(
1491        runtime,
1492        request_json,
1493        timeout,
1494        http_base_url,
1495        identity_ctx,
1496        None,
1497    )
1498}
1499
1500/// Runtime-owned JSON-RPC dispatch. Hosts that keep the unified runtime in an
1501/// [`Arc`] should use this entrypoint so cancellation-safe member mutations
1502/// can move a runtime owner into the identity foreground supervisor.
1503pub fn handle_unified_rpc_json_arc<'a>(
1504    runtime: &'a Arc<UnifiedRuntime>,
1505    request_json: &'a str,
1506    timeout: Duration,
1507    http_base_url: Option<&'a str>,
1508    identity_ctx: Option<&'a IdentityFirstContext>,
1509) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
1510    handle_unified_rpc_json_with_live_arc(
1511        runtime,
1512        request_json,
1513        timeout,
1514        http_base_url,
1515        identity_ctx,
1516        None,
1517    )
1518}
1519
1520/// [`handle_unified_rpc_json`] plus the gateway's type-erased live handler
1521/// (`mobkit/live/*`). `None` keeps every live method answering the typed
1522/// `live_unavailable` error — the posture of an ephemeral gateway or a
1523/// deployment that did not opt into `runtime_options.live`.
1524pub fn handle_unified_rpc_json_with_live<'a>(
1525    runtime: &'a UnifiedRuntime,
1526    request_json: &'a str,
1527    timeout: Duration,
1528    http_base_url: Option<&'a str>,
1529    identity_ctx: Option<&'a IdentityFirstContext>,
1530    live: Option<&'a crate::live_wiring::LiveRpcHandler>,
1531) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
1532    Box::pin(handle_unified_rpc_json_inner(
1533        runtime,
1534        None,
1535        request_json,
1536        timeout,
1537        http_base_url,
1538        identity_ctx,
1539        live,
1540    ))
1541}
1542
1543/// [`handle_unified_rpc_json_with_live`] with an owned runtime available to
1544/// cancellation-safe foreground operations.
1545pub fn handle_unified_rpc_json_with_live_arc<'a>(
1546    runtime: &'a Arc<UnifiedRuntime>,
1547    request_json: &'a str,
1548    timeout: Duration,
1549    http_base_url: Option<&'a str>,
1550    identity_ctx: Option<&'a IdentityFirstContext>,
1551    live: Option<&'a crate::live_wiring::LiveRpcHandler>,
1552) -> Pin<Box<dyn Future<Output = String> + Send + 'a>> {
1553    Box::pin(handle_unified_rpc_json_inner(
1554        runtime.as_ref(),
1555        Some(runtime),
1556        request_json,
1557        timeout,
1558        http_base_url,
1559        identity_ctx,
1560        live,
1561    ))
1562}
1563
1564async fn handle_unified_rpc_json_inner(
1565    runtime: &UnifiedRuntime,
1566    runtime_owner: Option<&Arc<UnifiedRuntime>>,
1567    request_json: &str,
1568    timeout: Duration,
1569    http_base_url: Option<&str>,
1570    identity_ctx: Option<&IdentityFirstContext>,
1571    live: Option<&crate::live_wiring::LiveRpcHandler>,
1572) -> String {
1573    let raw_request: Value = match serde_json::from_str(request_json) {
1574        Ok(raw_request) => raw_request,
1575        Err(_) => {
1576            return serialize_response(&JsonRpcResponse {
1577                jsonrpc: JSONRPC_VERSION.to_string(),
1578                id: Value::Null,
1579                result: None,
1580                error: Some(JsonRpcError {
1581                    code: -32700,
1582                    message: "Parse error".to_string(),
1583                    data: None,
1584                }),
1585            });
1586        }
1587    };
1588    let response_id = raw_request
1589        .as_object()
1590        .and_then(|object| object.get("id"))
1591        .cloned()
1592        .unwrap_or(Value::Null);
1593    let request: JsonRpcRequest = match serde_json::from_value(raw_request) {
1594        Ok(request) => request,
1595        Err(_) => {
1596            return serialize_response(&JsonRpcResponse {
1597                jsonrpc: JSONRPC_VERSION.to_string(),
1598                id: response_id,
1599                result: None,
1600                error: Some(JsonRpcError {
1601                    code: -32600,
1602                    message: "Invalid Request".to_string(),
1603                    data: None,
1604                }),
1605            });
1606        }
1607    };
1608    let is_notification = request.id.is_none();
1609    let response_id = request.id.clone().unwrap_or(Value::Null);
1610
1611    if request.jsonrpc != "2.0" {
1612        let response = JsonRpcResponse {
1613            jsonrpc: JSONRPC_VERSION.to_string(),
1614            id: response_id,
1615            result: None,
1616            error: Some(JsonRpcError {
1617                code: -32600,
1618                message: "Invalid Request".to_string(),
1619                data: None,
1620            }),
1621        };
1622        return if is_notification {
1623            String::new()
1624        } else {
1625            serialize_response(&response)
1626        };
1627    }
1628
1629    if let Err(message) =
1630        crate::member_comms_id::validate_public_rpc_member_aliases(&request.params)
1631    {
1632        let response = JsonRpcResponse {
1633            jsonrpc: JSONRPC_VERSION.to_string(),
1634            id: response_id,
1635            result: None,
1636            error: Some(JsonRpcError {
1637                code: -32602,
1638                message: format!("Invalid params: {message}"),
1639                data: None,
1640            }),
1641        };
1642        return if is_notification {
1643            String::new()
1644        } else {
1645            serialize_response(&response)
1646        };
1647    }
1648
1649    let response = match request.method.as_str() {
1650        "mobkit/status" => {
1651            let mob_state = Some(runtime.mob_handle().status_observation_snapshot());
1652            let is_running = runtime.module_is_running().await;
1653            let loaded = runtime.loaded_modules().await;
1654            let mut result = serde_json::json!({
1655                "contract_version": MOBKIT_CONTRACT_VERSION,
1656                "running": is_running,
1657                "loaded_modules": loaded,
1658                "mob_state": format!("{mob_state:?}"),
1659            });
1660            if let Some(url) = http_base_url {
1661                result["http_base_url"] = Value::String(url.to_string());
1662            }
1663            if let Some(ctx) = identity_ctx {
1664                result["identity_bootstrap"] =
1665                    serde_json::to_value(ctx.runtime.identity_bootstrap_status())
1666                        .unwrap_or(Value::Null);
1667            }
1668            if let Some(storage) = runtime.resolved_storage() {
1669                result["storage"] = storage.status_json();
1670            }
1671            if let Some(job_health) = runtime.job_health_projection() {
1672                result["detached_jobs"] = job_health
1673                    .get("detached_jobs")
1674                    .cloned()
1675                    .unwrap_or(Value::Null);
1676            }
1677            JsonRpcResponse {
1678                jsonrpc: JSONRPC_VERSION.to_string(),
1679                id: response_id,
1680                result: Some(result),
1681                error: None,
1682            }
1683        }
1684        "mobkit/capabilities" => {
1685            let loaded = runtime.loaded_modules().await;
1686            let mut methods = vec![
1687                "mobkit/init",
1688                "mobkit/status",
1689                "mobkit/capabilities",
1690                "mobkit/reconcile",
1691                "mobkit/spawn_member",
1692                "mobkit/scheduling/evaluate",
1693                "mobkit/scheduling/dispatch",
1694                "mobkit/routing/resolve",
1695                "mobkit/routing/routes/list",
1696                "mobkit/routing/routes/add",
1697                "mobkit/routing/routes/delete",
1698                "mobkit/delivery/send",
1699                "mobkit/delivery/history",
1700                "mobkit/events/subscribe",
1701                "mobkit/query_events",
1702                "mobkit/memory/stores",
1703                "mobkit/memory/index",
1704                "mobkit/memory/query",
1705                "mobkit/session_store/bigquery",
1706                "mobkit/gating/evaluate",
1707                "mobkit/gating/pending",
1708                "mobkit/gating/decide",
1709                "mobkit/gating/audit",
1710                "mobkit/call_tool",
1711                "mobkit/models/catalog",
1712                "mobkit/blob/get",
1713                "mobkit/send_message",
1714                "mobkit/find_members",
1715                "mobkit/ensure_member",
1716                "mobkit/list_members",
1717                "mobkit/get_member",
1718                "mobkit/retire_member",
1719                "mobkit/respawn_member",
1720                "mobkit/reconcile_edges",
1721                "mobkit/rediscover",
1722                "mobkit/mob_events/query",
1723                "mobkit/mob_events/subscribe",
1724                // Always available: local-only member introspection
1725                "mobkit/cross_mob/peer_info",
1726                "mobkit/cross_mob/wire_local",
1727                "mobkit/cross_mob/unwire_local",
1728                "mobkit/peer_pubkey",
1729                "mobkit/member_status",
1730                "mobkit/identity/resolved_tools",
1731                "mobkit/force_cancel_member",
1732                "mobkit/spawn_helper",
1733                "mobkit/fork_helper",
1734                "mobkit/attach_existing_session",
1735                "mobkit/cancel_flow",
1736                "mobkit/flow_status",
1737                "mobkit/list_flows",
1738                "mobkit/list_runs",
1739                "mobkit/run_flow",
1740                "mobkit/collect_completed",
1741                "mobkit/wait_ready",
1742                "mobkit/mob_labels/set",
1743                "mobkit/mob_labels/get",
1744                "mobkit/mob_labels/delete",
1745                "mobkit/run_labels/set",
1746                "mobkit/run_labels/get",
1747                "mobkit/run_labels/delete",
1748                storage_methods::STORAGE_DOCTOR_METHOD,
1749            ];
1750            methods.extend_from_slice(MOBPACK_AUTHORING_METHODS);
1751            // Workgraph methods are advertised only when the service is
1752            // configured (`runtime_options.workgraph = false` or a failed
1753            // store open leave them off).
1754            let workgraph_configured = runtime.workgraph_service().is_some();
1755            if workgraph_configured {
1756                methods.extend_from_slice(workgraph_methods::WORKGRAPH_READ_METHODS);
1757                methods.extend_from_slice(workgraph_methods::WORKGRAPH_MUTATE_METHODS);
1758            }
1759            // Live methods are advertised only when the gateway attached a
1760            // live transport (`runtime_options.live`, persistent mode).
1761            if live.is_some() {
1762                methods.extend_from_slice(&[
1763                    "mobkit/live/open",
1764                    "mobkit/live/status",
1765                    "mobkit/live/close",
1766                    "mobkit/live/refresh",
1767                    "mobkit/live/send_input",
1768                    "mobkit/live/commit_input",
1769                    "mobkit/live/interrupt",
1770                    "mobkit/live/truncate",
1771                ]);
1772            }
1773            if identity_ctx.is_some() {
1774                methods.extend_from_slice(&[
1775                    "mobkit/send",
1776                    "mobkit/interact",
1777                    "mobkit/dispatch",
1778                    "mobkit/subscribe",
1779                    "mobkit/status_identity",
1780                    "mobkit/respawn",
1781                    "mobkit/retire",
1782                    "mobkit/reset",
1783                    "mobkit/delete_identity",
1784                    "mobkit/inspect_identity",
1785                    "mobkit/reconcile_identity",
1786                    "mobkit/status_identity_bootstrap",
1787                    "mobkit/wait_identity_bootstrap",
1788                ]);
1789            }
1790            if identity_ctx
1791                .and_then(|ctx| ctx.agent_memory_provider.as_ref())
1792                .is_some()
1793            {
1794                methods.push("mobkit/agent_memory/recall");
1795                if identity_ctx
1796                    .and_then(|ctx| ctx.agent_memory_provider.as_ref())
1797                    .is_some_and(|provider| provider.supports_remember())
1798                {
1799                    methods.push("mobkit/agent_memory/remember");
1800                }
1801                if identity_ctx
1802                    .and_then(|ctx| ctx.agent_memory_provider.as_ref())
1803                    .is_some_and(|provider| provider.supports_forget())
1804                {
1805                    methods.push("mobkit/agent_memory/forget");
1806                }
1807                if identity_ctx
1808                    .and_then(|ctx| ctx.agent_memory_provider.as_ref())
1809                    .is_some_and(|provider| provider.supports_supersede())
1810                {
1811                    methods.push("mobkit/agent_memory/update");
1812                }
1813                if identity_ctx
1814                    .and_then(|ctx| ctx.agent_memory_provider.as_ref())
1815                    .is_some_and(|provider| provider.supports_manifest())
1816                {
1817                    methods.push("mobkit/agent_memory/manifest");
1818                }
1819            }
1820            // Cross-mob directory always advertised when configured
1821            if runtime.has_contact_directory() {
1822                methods.push("mobkit/cross_mob/directory");
1823            }
1824            // High-level wire/unwire/send require peer mob handles AND inproc contacts.
1825            // resolve_contact() rejects non-Inproc transports at execution time, so
1826            // advertising these methods for TCP/UDS-only deployments guarantees failures.
1827            if runtime.has_peer_mob_handles().await && runtime.has_inproc_contacts() {
1828                methods.extend_from_slice(&[
1829                    "mobkit/cross_mob/wire",
1830                    "mobkit/cross_mob/unwire",
1831                    "mobkit/cross_mob/send",
1832                ]);
1833            }
1834            let job_health = runtime.job_health_projection();
1835            if job_health.is_some() {
1836                methods.extend_from_slice(&[
1837                    "jobs/get",
1838                    "jobs/list",
1839                    "jobs/cancel",
1840                    "jobs/progress",
1841                    "jobs/result",
1842                    "jobs/artifacts",
1843                    "jobs/retry",
1844                    "jobs/health",
1845                    "jobs/subscribe",
1846                    "jobs/unsubscribe",
1847                ]);
1848                if job_health
1849                    .as_ref()
1850                    .and_then(|projection| projection.get("monitors_available"))
1851                    .and_then(Value::as_bool)
1852                    .unwrap_or(false)
1853                {
1854                    methods.push("monitors/start");
1855                }
1856            }
1857            let topology = runtime.topology_runtime_handle();
1858            let (topology_methods, topology_capabilities) =
1859                topology_methods::capability_projection(&topology, None, false);
1860            methods.extend(topology_methods);
1861            // H1/H2 storage durability resolution — same object as
1862            // `mobkit/status`; `null` when the spec was composed externally
1863            // without a declaration.
1864            let storage = runtime
1865                .resolved_storage()
1866                .map(|summary| summary.status_json())
1867                .unwrap_or(Value::Null);
1868            JsonRpcResponse {
1869                jsonrpc: JSONRPC_VERSION.to_string(),
1870                id: response_id,
1871                result: Some(serde_json::json!({
1872                    "contract_version": MOBKIT_CONTRACT_VERSION,
1873                    "runtime_type": "unified",
1874                    "methods": methods,
1875                    "storage": storage,
1876                    // Doctrine flag: when true the identity RPC set is live
1877                    // and member RPCs route durable targets through the
1878                    // identity authority.
1879                    "identity_first": identity_ctx.is_some(),
1880                    // True when a WorkGraph service is configured and the
1881                    // mobkit/workgraph/* group is live.
1882                    "workgraph": workgraph_configured,
1883                    "detached_jobs": job_health
1884                        .as_ref()
1885                        .and_then(|projection| projection.get("detached_jobs"))
1886                        .cloned()
1887                        .unwrap_or(Value::Null),
1888                    "loaded_modules": loaded,
1889                    "runtime_capabilities": {
1890                        "can_spawn_members": true,
1891                        "can_send_messages": true,
1892                        "can_wire_members": true,
1893                        "can_retire_members": true,
1894                        "available_spawn_modes": ["module", "profile"],
1895                    },
1896                    "authoring_capabilities": mobpack_authoring_capabilities(),
1897                    "topology_control": topology_capabilities,
1898                })),
1899                error: None,
1900            }
1901        }
1902        topology_methods::TOPOLOGY_QUERY_METHOD => {
1903            let topology = runtime.topology_runtime_handle();
1904            topology_methods::handle_query(&topology, response_id, None, false).await
1905        }
1906        topology_methods::TOPOLOGY_PLAN_METHOD => {
1907            let topology = runtime.topology_runtime_handle();
1908            topology_methods::handle_plan(&topology, response_id, &request.params, None).await
1909        }
1910        topology_methods::TOPOLOGY_APPLY_METHOD => {
1911            let topology = runtime.topology_runtime_handle();
1912            topology_methods::handle_apply(
1913                &topology,
1914                response_id,
1915                &request.params,
1916                None,
1917                Some("local-host"),
1918            )
1919            .await
1920        }
1921        topology_methods::TOPOLOGY_OPERATION_METHOD => {
1922            let topology = runtime.topology_runtime_handle();
1923            topology_methods::handle_operation(&topology, response_id, &request.params, None).await
1924        }
1925        topology_methods::TOPOLOGY_AUDIT_METHOD => {
1926            let topology = runtime.topology_runtime_handle();
1927            topology_methods::handle_audit(&topology, response_id, &request.params, None).await
1928        }
1929        "mobkit/reconcile" => {
1930            let modules = match params::required_string_array(&request.params, "modules") {
1931                Ok(m) => m,
1932                Err(reason) => {
1933                    return serialize_response(&JsonRpcResponse {
1934                        jsonrpc: JSONRPC_VERSION.to_string(),
1935                        id: response_id,
1936                        result: None,
1937                        error: Some(JsonRpcError {
1938                            code: -32602,
1939                            message: format!("Invalid params: {reason}"),
1940                            data: None,
1941                        }),
1942                    });
1943                }
1944            };
1945
1946            match runtime.reconcile_modules(modules.clone(), timeout).await {
1947                Ok(added) => JsonRpcResponse {
1948                    jsonrpc: JSONRPC_VERSION.to_string(),
1949                    id: response_id,
1950                    result: Some(serde_json::json!({
1951                        "accepted": true,
1952                        "reconciled_modules": modules,
1953                        "added": added
1954                    })),
1955                    error: None,
1956                },
1957                Err(err) => JsonRpcResponse {
1958                    jsonrpc: JSONRPC_VERSION.to_string(),
1959                    id: response_id,
1960                    result: None,
1961                    error: Some(JsonRpcError {
1962                        code: -32602,
1963                        message: format!("Invalid params: {err:?}"),
1964                        data: None,
1965                    }),
1966                },
1967            }
1968        }
1969        "mobkit/spawn_member" => {
1970            // Support both legacy module_id pattern and mob profile+meerkat_id pattern
1971            let module_id = request.params.get("module_id").and_then(Value::as_str);
1972            let profile = request.params.get("profile").and_then(Value::as_str);
1973            let meerkat_id = request.params.get("meerkat_id").and_then(Value::as_str);
1974
1975            if let Some(module_id) = module_id {
1976                // Legacy module spawn: {"module_id": "routing"}
1977                if module_id.is_empty() {
1978                    JsonRpcResponse {
1979                        jsonrpc: JSONRPC_VERSION.to_string(),
1980                        id: response_id,
1981                        result: None,
1982                        error: Some(JsonRpcError {
1983                            code: -32602,
1984                            message: "Invalid params: module_id required".to_string(),
1985                            data: None,
1986                        }),
1987                    }
1988                } else {
1989                    match runtime.spawn_member(module_id, timeout).await {
1990                        Ok(()) => JsonRpcResponse {
1991                            jsonrpc: JSONRPC_VERSION.to_string(),
1992                            id: response_id,
1993                            result: Some(serde_json::json!({
1994                                "accepted": true,
1995                                "module_id": module_id
1996                            })),
1997                            error: None,
1998                        },
1999                        Err(err) => JsonRpcResponse {
2000                            jsonrpc: JSONRPC_VERSION.to_string(),
2001                            id: response_id,
2002                            result: None,
2003                            error: Some(JsonRpcError {
2004                                code: -32602,
2005                                message: format!("Invalid params: {err:?}"),
2006                                data: None,
2007                            }),
2008                        },
2009                    }
2010                }
2011            } else if let (Some(profile), Some(meerkat_id)) = (profile, meerkat_id) {
2012                // The system identity is reserved for runtime-plane console
2013                // events (memory.* sinks, bootstrap): the aggregator exempts
2014                // it from the roster-visibility gate and namespacing, so a
2015                // member bearing the name would bypass both. Reject loudly.
2016                let target_identity_runtime = identity_ctx
2017                    .map(|ctx| &ctx.runtime)
2018                    .or_else(|| runtime.identity_runtime());
2019                let raw_target_validation = crate::member_comms_id::validate_raw_member_target(
2020                    target_identity_runtime,
2021                    meerkat_id,
2022                )
2023                .await;
2024                if meerkat_id == crate::console_contracts::SYSTEM_EVENT_IDENTITY {
2025                    JsonRpcResponse {
2026                        jsonrpc: JSONRPC_VERSION.to_string(),
2027                        id: response_id,
2028                        result: None,
2029                        error: Some(JsonRpcError {
2030                            code: -32602,
2031                            message: format!("Invalid params: '{meerkat_id}' is reserved"),
2032                            data: None,
2033                        }),
2034                    }
2035                } else if let Err(message) = raw_target_validation.as_ref() {
2036                    JsonRpcResponse {
2037                        jsonrpc: JSONRPC_VERSION.to_string(),
2038                        id: response_id,
2039                        result: None,
2040                        error: Some(JsonRpcError {
2041                            code: -32602,
2042                            message: format!("Invalid params: {message}"),
2043                            data: None,
2044                        }),
2045                    }
2046                } else {
2047                    // `runtime.spawn` takes the attached identity runtime's
2048                    // alias lock. Compatibility callers may instead supply an
2049                    // explicit identity context to a runtime without one; keep
2050                    // that authority's reservation through the lower-plane
2051                    // spawn without double-locking attached runtimes.
2052                    let compatibility_reservation = if runtime.identity_runtime().is_none() {
2053                        crate::member_comms_id::reserve_raw_member_target(
2054                            target_identity_runtime,
2055                            meerkat_id,
2056                        )
2057                        .await
2058                        .map(Some)
2059                    } else {
2060                        Ok(None)
2061                    };
2062                    match compatibility_reservation {
2063                        Err(message) => JsonRpcResponse {
2064                            jsonrpc: JSONRPC_VERSION.to_string(),
2065                            id: response_id,
2066                            result: None,
2067                            error: Some(JsonRpcError {
2068                                code: -32602,
2069                                message: format!("Invalid params: {message}"),
2070                                data: None,
2071                            }),
2072                        },
2073                        Ok(compatibility_reservation) => {
2074                            let member_id = compatibility_reservation
2075                                .as_ref()
2076                                .map(|reservation| reservation.alias().to_string())
2077                                .or_else(|| raw_target_validation.as_ref().ok().cloned())
2078                                .unwrap_or_else(|| meerkat_id.trim().to_string());
2079                            // Mob agent spawn: {"profile": "default", "meerkat_id": "agent-1"}
2080                            let spec = meerkat_mob::SpawnMemberSpec::from_wire(
2081                                profile.to_string(),
2082                                member_id.clone(),
2083                                request
2084                                    .params
2085                                    .get("initial_message")
2086                                    .and_then(Value::as_str)
2087                                    .map(|s| meerkat_core::ContentInput::from(s.to_string())),
2088                                None,
2089                                None,
2090                            );
2091                            let spawn_result = Box::pin(runtime.spawn(spec)).await;
2092                            drop(compatibility_reservation);
2093                            match spawn_result {
2094                                Ok(_member_ref) => JsonRpcResponse {
2095                                    jsonrpc: JSONRPC_VERSION.to_string(),
2096                                    id: response_id,
2097                                    result: Some(serde_json::json!({
2098                                        "accepted": true,
2099                                        "meerkat_id": member_id
2100                                    })),
2101                                    error: None,
2102                                },
2103                                Err(err) => JsonRpcResponse {
2104                                    jsonrpc: JSONRPC_VERSION.to_string(),
2105                                    id: response_id,
2106                                    result: None,
2107                                    error: Some(JsonRpcError {
2108                                        code: -32602,
2109                                        message: format!("Invalid params: {err}"),
2110                                        data: None,
2111                                    }),
2112                                },
2113                            }
2114                        }
2115                    }
2116                }
2117            } else {
2118                JsonRpcResponse {
2119                    jsonrpc: JSONRPC_VERSION.to_string(),
2120                    id: response_id,
2121                    result: None,
2122                    error: Some(JsonRpcError {
2123                        code: -32602,
2124                        message: "Invalid params: module_id or (profile + meerkat_id) required"
2125                            .to_string(),
2126                        data: None,
2127                    }),
2128                }
2129            }
2130        }
2131        "mobkit/scheduling/evaluate" => match parse_scheduling_params(&request.params) {
2132            Ok((schedules, tick_ms)) => {
2133                match runtime.evaluate_schedule_tick(&schedules, tick_ms).await {
2134                    Ok(evaluation) => JsonRpcResponse {
2135                        jsonrpc: JSONRPC_VERSION.to_string(),
2136                        id: response_id,
2137                        result: Some(serde_json::to_value(evaluation).unwrap_or(Value::Null)),
2138                        error: None,
2139                    },
2140                    Err(err) => JsonRpcResponse {
2141                        jsonrpc: JSONRPC_VERSION.to_string(),
2142                        id: response_id,
2143                        result: None,
2144                        error: Some(JsonRpcError {
2145                            code: -32602,
2146                            message: format!(
2147                                "Invalid params: {}",
2148                                format_schedule_validation_error(err)
2149                            ),
2150                            data: None,
2151                        }),
2152                    },
2153                }
2154            }
2155            Err(message) => JsonRpcResponse {
2156                jsonrpc: JSONRPC_VERSION.to_string(),
2157                id: response_id,
2158                result: None,
2159                error: Some(JsonRpcError {
2160                    code: -32602,
2161                    message: format!("Invalid params: {message}"),
2162                    data: None,
2163                }),
2164            },
2165        },
2166        "mobkit/scheduling/dispatch" => match parse_scheduling_params(&request.params) {
2167            Ok((schedules, tick_ms)) => {
2168                match runtime.dispatch_schedule_tick(&schedules, tick_ms).await {
2169                    Ok(dispatch) => JsonRpcResponse {
2170                        jsonrpc: JSONRPC_VERSION.to_string(),
2171                        id: response_id,
2172                        result: Some(serde_json::to_value(dispatch).unwrap_or(Value::Null)),
2173                        error: None,
2174                    },
2175                    Err(err) => JsonRpcResponse {
2176                        jsonrpc: JSONRPC_VERSION.to_string(),
2177                        id: response_id,
2178                        result: None,
2179                        error: Some(JsonRpcError {
2180                            code: -32602,
2181                            message: format!("Invalid params: {err}"),
2182                            data: None,
2183                        }),
2184                    },
2185                }
2186            }
2187            Err(message) => JsonRpcResponse {
2188                jsonrpc: JSONRPC_VERSION.to_string(),
2189                id: response_id,
2190                result: None,
2191                error: Some(JsonRpcError {
2192                    code: -32602,
2193                    message: format!("Invalid params: {message}"),
2194                    data: None,
2195                }),
2196            },
2197        },
2198        "mobkit/routing/resolve" => {
2199            let resolve_result = match parse_routing_resolve_params(&request.params) {
2200                Ok(resolve_request) => runtime
2201                    .resolve_routing(resolve_request)
2202                    .await
2203                    .map_err(RoutingDeliveryParamsError::Routing),
2204                Err(e) => Err(e),
2205            };
2206            match resolve_result {
2207                Ok(resolution) => JsonRpcResponse {
2208                    jsonrpc: JSONRPC_VERSION.to_string(),
2209                    id: response_id,
2210                    result: Some(serde_json::to_value(resolution).unwrap_or(Value::Null)),
2211                    error: None,
2212                },
2213                Err(err) => JsonRpcResponse {
2214                    jsonrpc: JSONRPC_VERSION.to_string(),
2215                    id: response_id,
2216                    result: None,
2217                    error: Some(JsonRpcError {
2218                        code: -32602,
2219                        message: format!("Invalid params: {}", err.message()),
2220                        data: None,
2221                    }),
2222                },
2223            }
2224        }
2225        "mobkit/routing/routes/list" => match parse_routing_routes_list_params(&request.params) {
2226            Ok(()) => {
2227                let routes = runtime.list_runtime_routes().await;
2228                JsonRpcResponse {
2229                    jsonrpc: JSONRPC_VERSION.to_string(),
2230                    id: response_id,
2231                    result: Some(serde_json::json!({
2232                        "routes": routes
2233                    })),
2234                    error: None,
2235                }
2236            }
2237            Err(err) => JsonRpcResponse {
2238                jsonrpc: JSONRPC_VERSION.to_string(),
2239                id: response_id,
2240                result: None,
2241                error: Some(JsonRpcError {
2242                    code: -32602,
2243                    message: format!("Invalid params: {}", err.message()),
2244                    data: None,
2245                }),
2246            },
2247        },
2248        "mobkit/routing/routes/add" => {
2249            let add_result = match parse_routing_route_add_params(&request.params) {
2250                Ok(route) => runtime
2251                    .add_runtime_route(route)
2252                    .await
2253                    .map_err(RoutingDeliveryParamsError::RouteMutation),
2254                Err(e) => Err(e),
2255            };
2256            match add_result {
2257                Ok(route) => JsonRpcResponse {
2258                    jsonrpc: JSONRPC_VERSION.to_string(),
2259                    id: response_id,
2260                    result: Some(serde_json::json!({ "route": route })),
2261                    error: None,
2262                },
2263                Err(err) => JsonRpcResponse {
2264                    jsonrpc: JSONRPC_VERSION.to_string(),
2265                    id: response_id,
2266                    result: None,
2267                    error: Some(JsonRpcError {
2268                        code: -32602,
2269                        message: format!("Invalid params: {}", err.message()),
2270                        data: None,
2271                    }),
2272                },
2273            }
2274        }
2275        "mobkit/routing/routes/delete" => {
2276            let delete_result = match parse_routing_route_delete_params(&request.params) {
2277                Ok(route_key) => runtime
2278                    .delete_runtime_route(&route_key)
2279                    .await
2280                    .map_err(RoutingDeliveryParamsError::RouteMutation),
2281                Err(e) => Err(e),
2282            };
2283            match delete_result {
2284                Ok(route) => JsonRpcResponse {
2285                    jsonrpc: JSONRPC_VERSION.to_string(),
2286                    id: response_id,
2287                    result: Some(serde_json::json!({ "deleted": route })),
2288                    error: None,
2289                },
2290                Err(err) => JsonRpcResponse {
2291                    jsonrpc: JSONRPC_VERSION.to_string(),
2292                    id: response_id,
2293                    result: None,
2294                    error: Some(JsonRpcError {
2295                        code: -32602,
2296                        message: format!("Invalid params: {}", err.message()),
2297                        data: None,
2298                    }),
2299                },
2300            }
2301        }
2302        "mobkit/delivery/send" => {
2303            let send_result = match parse_delivery_send_params(&request.params) {
2304                Ok(send_request) => runtime
2305                    .send_delivery(send_request)
2306                    .await
2307                    .map_err(RoutingDeliveryParamsError::Delivery),
2308                Err(e) => Err(e),
2309            };
2310            match send_result {
2311                Ok(record) => JsonRpcResponse {
2312                    jsonrpc: JSONRPC_VERSION.to_string(),
2313                    id: response_id,
2314                    result: Some(serde_json::to_value(record).unwrap_or(Value::Null)),
2315                    error: None,
2316                },
2317                Err(err) => JsonRpcResponse {
2318                    jsonrpc: JSONRPC_VERSION.to_string(),
2319                    id: response_id,
2320                    result: None,
2321                    error: Some(JsonRpcError {
2322                        code: -32602,
2323                        message: format!("Invalid params: {}", err.message()),
2324                        data: None,
2325                    }),
2326                },
2327            }
2328        }
2329        "mobkit/delivery/history" => match parse_delivery_history_params(&request.params) {
2330            Ok(history_request) => {
2331                let history = runtime.delivery_history(history_request).await;
2332                JsonRpcResponse {
2333                    jsonrpc: JSONRPC_VERSION.to_string(),
2334                    id: response_id,
2335                    result: Some(serde_json::to_value(history).unwrap_or(Value::Null)),
2336                    error: None,
2337                }
2338            }
2339            Err(err) => JsonRpcResponse {
2340                jsonrpc: JSONRPC_VERSION.to_string(),
2341                id: response_id,
2342                result: None,
2343                error: Some(JsonRpcError {
2344                    code: -32602,
2345                    message: format!("Invalid params: {}", err.message()),
2346                    data: None,
2347                }),
2348            },
2349        },
2350        "mobkit/events/subscribe" => match parse_subscribe_request(&request.params) {
2351            Ok(subscribe_request) => match runtime.subscribe_events(subscribe_request).await {
2352                Ok(subscribe_result) => JsonRpcResponse {
2353                    jsonrpc: JSONRPC_VERSION.to_string(),
2354                    id: response_id,
2355                    result: Some(serde_json::to_value(subscribe_result).unwrap_or(Value::Null)),
2356                    error: None,
2357                },
2358                Err(err) => JsonRpcResponse {
2359                    jsonrpc: JSONRPC_VERSION.to_string(),
2360                    id: response_id,
2361                    result: None,
2362                    error: Some(JsonRpcError {
2363                        code: -32602,
2364                        message: format!("Invalid params: {err}"),
2365                        data: None,
2366                    }),
2367                },
2368            },
2369            Err(err) => JsonRpcResponse {
2370                jsonrpc: JSONRPC_VERSION.to_string(),
2371                id: response_id,
2372                result: None,
2373                error: Some(JsonRpcError {
2374                    code: -32602,
2375                    message: format!("Invalid params: {}", err.message()),
2376                    data: None,
2377                }),
2378            },
2379        },
2380        "mobkit/query_events" => {
2381            let query: EventQuery = if request.params.is_null() {
2382                EventQuery::default()
2383            } else {
2384                match serde_json::from_value(request.params.clone()) {
2385                    Ok(query) => query,
2386                    Err(err) => {
2387                        return serde_json::to_string(&JsonRpcResponse {
2388                            jsonrpc: JSONRPC_VERSION.to_string(),
2389                            id: response_id,
2390                            result: None,
2391                            error: Some(JsonRpcError {
2392                                code: -32602,
2393                                message: format!("Invalid params: invalid query params: {err}"),
2394                                data: None,
2395                            }),
2396                        })
2397                        .unwrap_or_else(|_| r#"{"error":"serialization failed"}"#.to_string());
2398                    }
2399                }
2400            };
2401            match runtime.event_log_store() {
2402                Some(store) => match store.query(query).await {
2403                    Ok(events) => JsonRpcResponse {
2404                        jsonrpc: JSONRPC_VERSION.to_string(),
2405                        id: response_id,
2406                        result: Some(serde_json::to_value(events).unwrap_or(Value::Null)),
2407                        error: None,
2408                    },
2409                    Err(err) => JsonRpcResponse {
2410                        jsonrpc: JSONRPC_VERSION.to_string(),
2411                        id: response_id,
2412                        result: None,
2413                        error: Some(JsonRpcError {
2414                            code: -32603,
2415                            message: format!("query_events failed: {err}"),
2416                            data: None,
2417                        }),
2418                    },
2419                },
2420                None => JsonRpcResponse {
2421                    jsonrpc: JSONRPC_VERSION.to_string(),
2422                    id: response_id,
2423                    result: Some(serde_json::json!({
2424                        "status": "no_event_log_configured",
2425                        "events": [],
2426                    })),
2427                    error: None,
2428                },
2429            }
2430        }
2431        "mobkit/memory/stores" => match parse_memory_stores_params(&request.params) {
2432            Ok(()) => {
2433                let stores = runtime.memory_stores().await;
2434                JsonRpcResponse {
2435                    jsonrpc: JSONRPC_VERSION.to_string(),
2436                    id: response_id,
2437                    result: Some(serde_json::json!({
2438                        "stores": stores,
2439                    })),
2440                    error: None,
2441                }
2442            }
2443            Err(err) => JsonRpcResponse {
2444                jsonrpc: JSONRPC_VERSION.to_string(),
2445                id: response_id,
2446                result: None,
2447                error: Some(JsonRpcError {
2448                    code: -32602,
2449                    message: format!("Invalid params: {}", err.message()),
2450                    data: None,
2451                }),
2452            },
2453        },
2454        "mobkit/memory/index" => match parse_memory_index_params(&request.params) {
2455            Ok(index_request) => match runtime.memory_index(index_request).await {
2456                Ok(indexed) => JsonRpcResponse {
2457                    jsonrpc: JSONRPC_VERSION.to_string(),
2458                    id: response_id,
2459                    result: Some(serde_json::to_value(indexed).unwrap_or(Value::Null)),
2460                    error: None,
2461                },
2462                Err(MemoryIndexError::BackendPersistFailed(error)) => JsonRpcResponse {
2463                    jsonrpc: JSONRPC_VERSION.to_string(),
2464                    id: response_id,
2465                    result: None,
2466                    error: Some(JsonRpcError {
2467                        code: MEMORY_BACKEND_UNAVAILABLE_CODE,
2468                        message: format!(
2469                            "Memory backend unavailable: {}",
2470                            MemoryParamsError::backend_message(&error)
2471                        ),
2472                        data: None,
2473                    }),
2474                },
2475                Err(err) => JsonRpcResponse {
2476                    jsonrpc: JSONRPC_VERSION.to_string(),
2477                    id: response_id,
2478                    result: None,
2479                    error: Some(JsonRpcError {
2480                        code: -32602,
2481                        message: format!(
2482                            "Invalid params: {}",
2483                            MemoryParamsError::Index(err).message()
2484                        ),
2485                        data: None,
2486                    }),
2487                },
2488            },
2489            Err(err) => JsonRpcResponse {
2490                jsonrpc: JSONRPC_VERSION.to_string(),
2491                id: response_id,
2492                result: None,
2493                error: Some(JsonRpcError {
2494                    code: -32602,
2495                    message: format!("Invalid params: {}", err.message()),
2496                    data: None,
2497                }),
2498            },
2499        },
2500        "mobkit/memory/query" => match parse_memory_query_params(&request.params) {
2501            Ok(query_request) => {
2502                let query_result = runtime.memory_query(query_request).await;
2503                JsonRpcResponse {
2504                    jsonrpc: JSONRPC_VERSION.to_string(),
2505                    id: response_id,
2506                    result: Some(serde_json::to_value(query_result).unwrap_or(Value::Null)),
2507                    error: None,
2508                }
2509            }
2510            Err(err) => JsonRpcResponse {
2511                jsonrpc: JSONRPC_VERSION.to_string(),
2512                id: response_id,
2513                result: None,
2514                error: Some(JsonRpcError {
2515                    code: -32602,
2516                    message: format!("Invalid params: {}", err.message()),
2517                    data: None,
2518                }),
2519            },
2520        },
2521        "mobkit/agent_memory/remember" => {
2522            let runtime = match identity_ctx.map(|ctx| ctx.runtime.as_ref()) {
2523                Some(runtime) => runtime,
2524                None => {
2525                    return maybe_error_response(
2526                        is_notification,
2527                        response_id,
2528                        -32601,
2529                        "agent memory is not configured".to_string(),
2530                    );
2531                }
2532            };
2533            match parse_agent_memory_remember_params(&request.params) {
2534                Ok(remember_request) => match runtime
2535                    .remember_agent_memory(
2536                        &remember_request.realm,
2537                        &remember_request.identity,
2538                        remember_request.memory,
2539                    )
2540                    .await
2541                {
2542                    Ok(record) => JsonRpcResponse {
2543                        jsonrpc: JSONRPC_VERSION.to_string(),
2544                        id: response_id,
2545                        result: Some(serde_json::to_value(record).unwrap_or(Value::Null)),
2546                        error: None,
2547                    },
2548                    Err(err) => JsonRpcResponse {
2549                        jsonrpc: JSONRPC_VERSION.to_string(),
2550                        id: response_id,
2551                        result: None,
2552                        error: Some(agent_memory_rpc_error("write", err)),
2553                    },
2554                },
2555                Err(err) => JsonRpcResponse {
2556                    jsonrpc: JSONRPC_VERSION.to_string(),
2557                    id: response_id,
2558                    result: None,
2559                    error: Some(JsonRpcError {
2560                        code: -32602,
2561                        message: format!("Invalid params: {}", err.message()),
2562                        data: None,
2563                    }),
2564                },
2565            }
2566        }
2567        "mobkit/agent_memory/forget" => {
2568            let runtime = match identity_ctx.map(|ctx| ctx.runtime.as_ref()) {
2569                Some(runtime) => runtime,
2570                None => {
2571                    return maybe_error_response(
2572                        is_notification,
2573                        response_id,
2574                        -32601,
2575                        "agent memory is not configured".to_string(),
2576                    );
2577                }
2578            };
2579            match parse_agent_memory_forget_params(&request.params) {
2580                Ok(forget_request) => match runtime
2581                    .forget_agent_memory(
2582                        &forget_request.realm,
2583                        &forget_request.identity,
2584                        &forget_request.memory_id,
2585                    )
2586                    .await
2587                {
2588                    Ok(result) => JsonRpcResponse {
2589                        jsonrpc: JSONRPC_VERSION.to_string(),
2590                        id: response_id,
2591                        result: Some(serde_json::to_value(result).unwrap_or(Value::Null)),
2592                        error: None,
2593                    },
2594                    Err(err) => JsonRpcResponse {
2595                        jsonrpc: JSONRPC_VERSION.to_string(),
2596                        id: response_id,
2597                        result: None,
2598                        error: Some(agent_memory_rpc_error("forget", err)),
2599                    },
2600                },
2601                Err(err) => JsonRpcResponse {
2602                    jsonrpc: JSONRPC_VERSION.to_string(),
2603                    id: response_id,
2604                    result: None,
2605                    error: Some(JsonRpcError {
2606                        code: -32602,
2607                        message: format!("Invalid params: {}", err.message()),
2608                        data: None,
2609                    }),
2610                },
2611            }
2612        }
2613        "mobkit/agent_memory/recall" => {
2614            let runtime = match identity_ctx.map(|ctx| ctx.runtime.as_ref()) {
2615                Some(runtime) => runtime,
2616                None => {
2617                    return maybe_error_response(
2618                        is_notification,
2619                        response_id,
2620                        -32601,
2621                        "agent memory is not configured".to_string(),
2622                    );
2623                }
2624            };
2625            match parse_agent_memory_recall_params(&request.params) {
2626                Ok(recall_request) => {
2627                    match runtime.recall_agent_memory(recall_request.request).await {
2628                        Ok(records) => JsonRpcResponse {
2629                            jsonrpc: JSONRPC_VERSION.to_string(),
2630                            id: response_id,
2631                            result: Some(serde_json::json!({ "records": records })),
2632                            error: None,
2633                        },
2634                        Err(err) => JsonRpcResponse {
2635                            jsonrpc: JSONRPC_VERSION.to_string(),
2636                            id: response_id,
2637                            result: None,
2638                            error: Some(agent_memory_rpc_error("recall", err)),
2639                        },
2640                    }
2641                }
2642                Err(err) => JsonRpcResponse {
2643                    jsonrpc: JSONRPC_VERSION.to_string(),
2644                    id: response_id,
2645                    result: None,
2646                    error: Some(JsonRpcError {
2647                        code: -32602,
2648                        message: format!("Invalid params: {}", err.message()),
2649                        data: None,
2650                    }),
2651                },
2652            }
2653        }
2654        "mobkit/agent_memory/update" => {
2655            let runtime = match identity_ctx.map(|ctx| ctx.runtime.as_ref()) {
2656                Some(runtime) => runtime,
2657                None => {
2658                    return maybe_error_response(
2659                        is_notification,
2660                        response_id,
2661                        -32601,
2662                        "agent memory is not configured".to_string(),
2663                    );
2664                }
2665            };
2666            match parse_agent_memory_update_params(&request.params) {
2667                Ok(update_request) => match runtime
2668                    .update_agent_memory(
2669                        &update_request.realm,
2670                        &update_request.identity,
2671                        &update_request.memory_id,
2672                        update_request.memory,
2673                    )
2674                    .await
2675                {
2676                    Ok(new_id) => JsonRpcResponse {
2677                        jsonrpc: JSONRPC_VERSION.to_string(),
2678                        id: response_id,
2679                        result: Some(serde_json::json!({
2680                            "memory_id": new_id,
2681                            "supersedes": update_request.memory_id,
2682                        })),
2683                        error: None,
2684                    },
2685                    Err(err) => JsonRpcResponse {
2686                        jsonrpc: JSONRPC_VERSION.to_string(),
2687                        id: response_id,
2688                        result: None,
2689                        error: Some(agent_memory_rpc_error("update", err)),
2690                    },
2691                },
2692                Err(err) => JsonRpcResponse {
2693                    jsonrpc: JSONRPC_VERSION.to_string(),
2694                    id: response_id,
2695                    result: None,
2696                    error: Some(JsonRpcError {
2697                        code: -32602,
2698                        message: format!("Invalid params: {}", err.message()),
2699                        data: None,
2700                    }),
2701                },
2702            }
2703        }
2704        "mobkit/agent_memory/manifest" => {
2705            let runtime = match identity_ctx.map(|ctx| ctx.runtime.as_ref()) {
2706                Some(runtime) => runtime,
2707                None => {
2708                    return maybe_error_response(
2709                        is_notification,
2710                        response_id,
2711                        -32601,
2712                        "agent memory is not configured".to_string(),
2713                    );
2714                }
2715            };
2716            match parse_agent_memory_manifest_params(&request.params) {
2717                Ok(manifest_request) => match runtime
2718                    .manifest_agent_memory(
2719                        &manifest_request.realm,
2720                        &manifest_request.identity,
2721                        manifest_request.tier,
2722                    )
2723                    .await
2724                {
2725                    Ok(records) => JsonRpcResponse {
2726                        jsonrpc: JSONRPC_VERSION.to_string(),
2727                        id: response_id,
2728                        result: Some(serde_json::json!({ "records": records })),
2729                        error: None,
2730                    },
2731                    Err(err) => JsonRpcResponse {
2732                        jsonrpc: JSONRPC_VERSION.to_string(),
2733                        id: response_id,
2734                        result: None,
2735                        error: Some(agent_memory_rpc_error("manifest", err)),
2736                    },
2737                },
2738                Err(err) => JsonRpcResponse {
2739                    jsonrpc: JSONRPC_VERSION.to_string(),
2740                    id: response_id,
2741                    result: None,
2742                    error: Some(JsonRpcError {
2743                        code: -32602,
2744                        message: format!("Invalid params: {}", err.message()),
2745                        data: None,
2746                    }),
2747                },
2748            }
2749        }
2750        "mobkit/session_store/bigquery" => {
2751            match parse_bigquery_session_store_params(&request.params)
2752                .and_then(run_bigquery_session_store_request)
2753            {
2754                Ok(result) => JsonRpcResponse {
2755                    jsonrpc: JSONRPC_VERSION.to_string(),
2756                    id: response_id,
2757                    result: Some(result),
2758                    error: None,
2759                },
2760                Err(BigQuerySessionStoreRpcError::Params(message)) => JsonRpcResponse {
2761                    jsonrpc: JSONRPC_VERSION.to_string(),
2762                    id: response_id,
2763                    result: None,
2764                    error: Some(JsonRpcError {
2765                        code: -32602,
2766                        message: format!("Invalid params: {message}"),
2767                        data: None,
2768                    }),
2769                },
2770                Err(BigQuerySessionStoreRpcError::Store(error)) => JsonRpcResponse {
2771                    jsonrpc: JSONRPC_VERSION.to_string(),
2772                    id: response_id,
2773                    result: None,
2774                    error: Some(JsonRpcError {
2775                        code: -32011,
2776                        message: format!(
2777                            "BigQuery session store request failed: {}",
2778                            format_bigquery_store_error(&error)
2779                        ),
2780                        data: None,
2781                    }),
2782                },
2783            }
2784        }
2785        "mobkit/gating/evaluate" => match parse_gating_evaluate_params(&request.params) {
2786            Ok(gating_request) => {
2787                let gating_result = runtime.evaluate_gating_action(gating_request).await;
2788                JsonRpcResponse {
2789                    jsonrpc: JSONRPC_VERSION.to_string(),
2790                    id: response_id,
2791                    result: Some(serde_json::to_value(gating_result).unwrap_or(Value::Null)),
2792                    error: None,
2793                }
2794            }
2795            Err(err) => JsonRpcResponse {
2796                jsonrpc: JSONRPC_VERSION.to_string(),
2797                id: response_id,
2798                result: None,
2799                error: Some(JsonRpcError {
2800                    code: -32602,
2801                    message: format!("Invalid params: {}", err.message()),
2802                    data: None,
2803                }),
2804            },
2805        },
2806        "mobkit/gating/pending" => match parse_gating_pending_params(&request.params) {
2807            Ok(()) => {
2808                let pending = runtime.list_gating_pending().await;
2809                JsonRpcResponse {
2810                    jsonrpc: JSONRPC_VERSION.to_string(),
2811                    id: response_id,
2812                    result: Some(serde_json::json!({
2813                        "pending": pending,
2814                    })),
2815                    error: None,
2816                }
2817            }
2818            Err(err) => JsonRpcResponse {
2819                jsonrpc: JSONRPC_VERSION.to_string(),
2820                id: response_id,
2821                result: None,
2822                error: Some(JsonRpcError {
2823                    code: -32602,
2824                    message: format!("Invalid params: {}", err.message()),
2825                    data: None,
2826                }),
2827            },
2828        },
2829        "mobkit/gating/decide" => {
2830            let decide_result = match parse_gating_decide_params(&request.params) {
2831                Ok(decide_request) => runtime
2832                    .decide_gating_action(decide_request)
2833                    .await
2834                    .map_err(GatingParamsError::Decision),
2835                Err(e) => Err(e),
2836            };
2837            match decide_result {
2838                Ok(result) => JsonRpcResponse {
2839                    jsonrpc: JSONRPC_VERSION.to_string(),
2840                    id: response_id,
2841                    result: Some(serde_json::to_value(result).unwrap_or(Value::Null)),
2842                    error: None,
2843                },
2844                Err(err) => JsonRpcResponse {
2845                    jsonrpc: JSONRPC_VERSION.to_string(),
2846                    id: response_id,
2847                    result: None,
2848                    error: Some(JsonRpcError {
2849                        code: -32602,
2850                        message: format!("Invalid params: {}", err.message()),
2851                        data: None,
2852                    }),
2853                },
2854            }
2855        }
2856        "mobkit/gating/audit" => match parse_gating_audit_params(&request.params) {
2857            Ok(limit) => {
2858                let entries = runtime.gating_audit_entries(limit).await;
2859                JsonRpcResponse {
2860                    jsonrpc: JSONRPC_VERSION.to_string(),
2861                    id: response_id,
2862                    result: Some(serde_json::json!({
2863                        "entries": entries,
2864                    })),
2865                    error: None,
2866                }
2867            }
2868            Err(err) => JsonRpcResponse {
2869                jsonrpc: JSONRPC_VERSION.to_string(),
2870                id: response_id,
2871                result: None,
2872                error: Some(JsonRpcError {
2873                    code: -32602,
2874                    message: format!("Invalid params: {}", err.message()),
2875                    data: None,
2876                }),
2877            },
2878        },
2879        "mobkit/call_tool" => {
2880            let module_id = request.params.get("module_id").and_then(Value::as_str);
2881            let tool = request.params.get("tool").and_then(Value::as_str);
2882            let arguments = request
2883                .params
2884                .get("arguments")
2885                .cloned()
2886                .unwrap_or(serde_json::json!({}));
2887
2888            match (module_id, tool) {
2889                (Some(module_id), Some(tool)) if !module_id.is_empty() && !tool.is_empty() => {
2890                    let route = runtime
2891                        .route_module_call(
2892                            &ModuleRouteRequest {
2893                                module_id: module_id.to_string(),
2894                                method: tool.to_string(),
2895                                params: arguments,
2896                            },
2897                            timeout,
2898                        )
2899                        .await;
2900                    match route {
2901                        Ok(response) => JsonRpcResponse {
2902                            jsonrpc: JSONRPC_VERSION.to_string(),
2903                            id: response_id,
2904                            result: Some(serde_json::json!({
2905                                "module_id": response.module_id,
2906                                "tool": response.method,
2907                                "result": response.payload
2908                            })),
2909                            error: None,
2910                        },
2911                        Err(ModuleRouteError::UnloadedModule(mid)) => JsonRpcResponse {
2912                            jsonrpc: JSONRPC_VERSION.to_string(),
2913                            id: response_id,
2914                            result: None,
2915                            error: Some(JsonRpcError {
2916                                code: -32601,
2917                                message: format!("Module '{mid}' not loaded"),
2918                                data: None,
2919                            }),
2920                        },
2921                        Err(err) => JsonRpcResponse {
2922                            jsonrpc: JSONRPC_VERSION.to_string(),
2923                            id: response_id,
2924                            result: None,
2925                            error: Some(JsonRpcError {
2926                                code: -32000,
2927                                message: format!("Tool call failed: {err:?}"),
2928                                data: None,
2929                            }),
2930                        },
2931                    }
2932                }
2933                _ => JsonRpcResponse {
2934                    jsonrpc: JSONRPC_VERSION.to_string(),
2935                    id: response_id,
2936                    result: None,
2937                    error: Some(JsonRpcError {
2938                        code: -32602,
2939                        message: "Invalid params: module_id and tool required".to_string(),
2940                        data: None,
2941                    }),
2942                },
2943            }
2944        }
2945        "mobkit/models/catalog" => JsonRpcResponse {
2946            jsonrpc: JSONRPC_VERSION.to_string(),
2947            id: response_id,
2948            result: Some(build_models_catalog_result()),
2949            error: None,
2950        },
2951        // Read-only state-directory diagnosis with the live H1/H2 durability
2952        // census attached. `state_dir` is explicit until the M2 layout
2953        // authority gives the runtime a reportable state directory.
2954        storage_methods::STORAGE_DOCTOR_METHOD => {
2955            match storage_methods::parse_storage_doctor_params(&request.params) {
2956                Ok(Some(params)) => {
2957                    let result =
2958                        storage_methods::run_storage_doctor(&params, runtime.resolved_storage())
2959                            .await;
2960                    JsonRpcResponse {
2961                        jsonrpc: JSONRPC_VERSION.to_string(),
2962                        id: response_id,
2963                        result: Some(result),
2964                        error: None,
2965                    }
2966                }
2967                Ok(None) => JsonRpcResponse {
2968                    jsonrpc: JSONRPC_VERSION.to_string(),
2969                    id: response_id,
2970                    result: None,
2971                    error: Some(storage_methods::storage_doctor_state_dir_unavailable_error()),
2972                },
2973                Err(reason) => JsonRpcResponse {
2974                    jsonrpc: JSONRPC_VERSION.to_string(),
2975                    id: response_id,
2976                    result: None,
2977                    error: Some(JsonRpcError {
2978                        code: -32602,
2979                        message: format!("Invalid params: {reason}"),
2980                        data: None,
2981                    }),
2982                },
2983            }
2984        }
2985        method if MOBPACK_AUTHORING_METHODS.contains(&method) => {
2986            handle_unified_mobpack_authoring_rpc(runtime, method, &request.params, response_id)
2987                .await
2988        }
2989        "mobkit/blob/get" => {
2990            mob_methods::handle_blob_get(runtime, response_id, &request.params).await
2991        }
2992        "mobkit/send_message" => {
2993            // Pass the identity runtime so bare durable identities resolve
2994            // through the identity bridge when no roster member matches
2995            // (exact member-id match wins; see `SendMessageTarget`).
2996            Box::pin(mob_methods::handle_send_message(
2997                runtime,
2998                identity_ctx.map(|ctx| &ctx.runtime),
2999                response_id,
3000                &request.params,
3001            ))
3002            .await
3003        }
3004        "mobkit/find_members" => {
3005            mob_methods::handle_find_members(
3006                runtime,
3007                identity_ctx.map(|ctx| &ctx.runtime),
3008                response_id,
3009                &request.params,
3010            )
3011            .await
3012        }
3013        "mobkit/ensure_member" => {
3014            Box::pin(mob_methods::handle_ensure_member(
3015                runtime,
3016                identity_ctx.map(|ctx| &ctx.runtime),
3017                response_id,
3018                &request.params,
3019            ))
3020            .await
3021        }
3022        "mobkit/list_members" => {
3023            mob_methods::handle_list_members(
3024                runtime,
3025                identity_ctx.map(|ctx| &ctx.runtime),
3026                response_id,
3027            )
3028            .await
3029        }
3030        "mobkit/get_member" => {
3031            mob_methods::handle_get_member(
3032                runtime,
3033                identity_ctx.map(|ctx| &ctx.runtime),
3034                response_id,
3035                &request.params,
3036            )
3037            .await
3038        }
3039        "mobkit/retire_member" => {
3040            mob_methods::handle_retire_member(
3041                runtime,
3042                identity_ctx.map(|ctx| &ctx.runtime),
3043                response_id,
3044                &request.params,
3045            )
3046            .await
3047        }
3048        "mobkit/respawn_member" => {
3049            Box::pin(mob_methods::handle_respawn_member(
3050                runtime,
3051                identity_ctx.map(|ctx| &ctx.runtime),
3052                response_id,
3053                &request.params,
3054            ))
3055            .await
3056        }
3057        "mobkit/reconcile_edges" => mob_methods::handle_reconcile_edges(runtime, response_id).await,
3058        "mobkit/rediscover" => mob_methods::handle_rediscover(runtime, response_id).await,
3059        "mobkit/mob_events/query" => {
3060            mob_methods::handle_mob_events_query(runtime, response_id, request.params).await
3061        }
3062        "mobkit/mob_events/subscribe" => {
3063            mob_methods::handle_mob_events_subscribe(runtime, response_id, request.params).await
3064        }
3065        "mobkit/cross_mob/wire" => {
3066            Box::pin(mob_methods::handle_cross_mob_wire(
3067                runtime,
3068                runtime_owner.cloned(),
3069                identity_ctx.map(|ctx| &ctx.runtime),
3070                response_id,
3071                &request.params,
3072            ))
3073            .await
3074        }
3075        "mobkit/cross_mob/unwire" => {
3076            Box::pin(mob_methods::handle_cross_mob_unwire(
3077                runtime,
3078                runtime_owner.cloned(),
3079                identity_ctx.map(|ctx| &ctx.runtime),
3080                response_id,
3081                &request.params,
3082            ))
3083            .await
3084        }
3085        "mobkit/cross_mob/send" => {
3086            mob_methods::handle_cross_mob_send(
3087                runtime,
3088                runtime_owner.cloned(),
3089                identity_ctx.map(|ctx| &ctx.runtime),
3090                response_id,
3091                &request.params,
3092            )
3093            .await
3094        }
3095        "mobkit/cross_mob/directory" => {
3096            mob_methods::handle_cross_mob_directory(runtime, response_id).await
3097        }
3098        "mobkit/cross_mob/peer_info" => {
3099            mob_methods::handle_cross_mob_peer_info(runtime, response_id, &request.params).await
3100        }
3101        "mobkit/cross_mob/wire_local" => {
3102            mob_methods::handle_cross_mob_wire_local(
3103                runtime,
3104                runtime_owner.cloned(),
3105                identity_ctx.map(|ctx| &ctx.runtime),
3106                response_id,
3107                &request.params,
3108            )
3109            .await
3110        }
3111        "mobkit/cross_mob/unwire_local" => {
3112            mob_methods::handle_cross_mob_unwire_local(
3113                runtime,
3114                runtime_owner.cloned(),
3115                identity_ctx.map(|ctx| &ctx.runtime),
3116                response_id,
3117                &request.params,
3118            )
3119            .await
3120        }
3121        "mobkit/peer_pubkey" => mob_methods::handle_peer_pubkey(runtime, response_id).await,
3122        "mobkit/member_status" => {
3123            mob_methods::handle_member_status(
3124                runtime,
3125                identity_ctx.map(|ctx| &ctx.runtime),
3126                response_id,
3127                &request.params,
3128            )
3129            .await
3130        }
3131        "mobkit/identity/resolved_tools" => {
3132            mob_methods::handle_identity_resolved_tools(
3133                runtime,
3134                identity_ctx.map(|ctx| &ctx.runtime),
3135                response_id,
3136                &request.params,
3137            )
3138            .await
3139        }
3140        "mobkit/force_cancel_member" => {
3141            mob_methods::handle_force_cancel_member(
3142                runtime,
3143                identity_ctx.map(|ctx| &ctx.runtime),
3144                response_id,
3145                &request.params,
3146            )
3147            .await
3148        }
3149        "mobkit/spawn_helper" => {
3150            Box::pin(mob_methods::handle_spawn_helper(
3151                runtime,
3152                identity_ctx.map(|ctx| &ctx.runtime),
3153                response_id,
3154                &request.params,
3155            ))
3156            .await
3157        }
3158        "mobkit/fork_helper" => {
3159            Box::pin(mob_methods::handle_fork_helper(
3160                runtime,
3161                identity_ctx.map(|ctx| &ctx.runtime),
3162                response_id,
3163                &request.params,
3164            ))
3165            .await
3166        }
3167        "mobkit/attach_existing_session" => {
3168            Box::pin(mob_methods::handle_attach_existing_session(
3169                runtime,
3170                identity_ctx.map(|ctx| &ctx.runtime),
3171                response_id,
3172                &request.params,
3173            ))
3174            .await
3175        }
3176        "mobkit/cancel_flow" => {
3177            mob_methods::handle_cancel_flow(runtime, response_id, &request.params).await
3178        }
3179        "mobkit/flow_status" => {
3180            mob_methods::handle_flow_status(runtime, response_id, &request.params).await
3181        }
3182        "mobkit/list_flows" => mob_methods::handle_list_flows(runtime, response_id).await,
3183        "mobkit/list_runs" => {
3184            mob_methods::handle_list_runs(runtime, response_id, &request.params).await
3185        }
3186        "mobkit/run_flow" => {
3187            Box::pin(mob_methods::handle_run_flow(
3188                runtime,
3189                response_id,
3190                &request.params,
3191            ))
3192            .await
3193        }
3194        "mobkit/collect_completed" => {
3195            mob_methods::handle_collect_completed(runtime, response_id).await
3196        }
3197        "mobkit/wait_ready" => {
3198            mob_methods::handle_wait_ready(runtime, response_id, &request.params).await
3199        }
3200        "mobkit/mob_labels/set" => {
3201            mob_methods::handle_mob_labels_set(runtime, response_id, &request.params).await
3202        }
3203        "mobkit/mob_labels/get" => mob_methods::handle_mob_labels_get(runtime, response_id).await,
3204        "mobkit/mob_labels/delete" => {
3205            mob_methods::handle_mob_labels_delete(runtime, response_id).await
3206        }
3207        "mobkit/run_labels/set" => {
3208            mob_methods::handle_run_labels_set(runtime, response_id, &request.params).await
3209        }
3210        "mobkit/run_labels/get" => {
3211            mob_methods::handle_run_labels_get(runtime, response_id, &request.params).await
3212        }
3213        "mobkit/run_labels/delete" => {
3214            mob_methods::handle_run_labels_delete(runtime, response_id, &request.params).await
3215        }
3216        // ----- identity-first methods -----
3217        "mobkit/send" => {
3218            let identity_rt = match identity_ctx {
3219                Some(ctx) => &ctx.runtime,
3220                None => return maybe_identity_not_configured(is_notification, response_id),
3221            };
3222            let identity_str = request
3223                .params
3224                .get("identity")
3225                .and_then(|v| v.as_str())
3226                .unwrap_or("");
3227            let target =
3228                match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3229                {
3230                    Ok(target) => target,
3231                    Err(e) => {
3232                        return maybe_error_response(
3233                            is_notification,
3234                            response_id,
3235                            -32602,
3236                            format!("invalid identity: {e}"),
3237                        );
3238                    }
3239                };
3240            let identity = target.identity.clone();
3241            if let Some(response) =
3242                rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3243            {
3244                return if is_notification {
3245                    String::new()
3246                } else {
3247                    serialize_response(&response)
3248                };
3249            }
3250            let content_val = request
3251                .params
3252                .get("content")
3253                .cloned()
3254                .unwrap_or(Value::Null);
3255            let content = match serde_json::from_value::<meerkat_core::ContentInput>(content_val) {
3256                Ok(content) => content,
3257                Err(err) => {
3258                    return maybe_error_response(
3259                        is_notification,
3260                        response_id,
3261                        -32602,
3262                        format!("invalid content: {err}"),
3263                    );
3264                }
3265            };
3266            let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
3267                .then_some(identity_str);
3268            let send_result = identity_rt
3269                .send_admission_tracked(
3270                    &identity,
3271                    expected_alias,
3272                    &content,
3273                    meerkat_core::types::HandlingMode::Queue,
3274                    None,
3275                )
3276                .await;
3277            match send_result {
3278                Ok(admission) => JsonRpcResponse {
3279                    jsonrpc: JSONRPC_VERSION.to_string(),
3280                    id: response_id,
3281                    result: Some(serde_json::json!({
3282                        "fencing_token": admission.fencing_token.get(),
3283                        // Cursor read before delivery. Wait for an
3284                        // inspect_identity completion_cursor that is ahead of
3285                        // this within the same epoch; never compare output
3286                        // text, which repeats.
3287                        "completion_baseline": completion_cursor_json(admission.completion_baseline),
3288                    })),
3289                    error: None,
3290                },
3291                Err(e) => identity_error_response(response_id, &e),
3292            }
3293        }
3294        "mobkit/interact" => {
3295            let identity_rt = match identity_ctx {
3296                Some(ctx) => &ctx.runtime,
3297                None => return maybe_identity_not_configured(is_notification, response_id),
3298            };
3299            let identity_str = request
3300                .params
3301                .get("identity")
3302                .and_then(|v| v.as_str())
3303                .unwrap_or("");
3304            let target =
3305                match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3306                {
3307                    Ok(target) => target,
3308                    Err(e) => {
3309                        return maybe_error_response(
3310                            is_notification,
3311                            response_id,
3312                            -32602,
3313                            format!("invalid identity: {e}"),
3314                        );
3315                    }
3316                };
3317            let identity = target.identity.clone();
3318            if let Some(response) =
3319                rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3320            {
3321                return if is_notification {
3322                    String::new()
3323                } else {
3324                    serialize_response(&response)
3325                };
3326            }
3327            let content_val = request
3328                .params
3329                .get("content")
3330                .cloned()
3331                .unwrap_or(Value::Null);
3332            let content =
3333                match serde_json::from_value::<meerkat_core::ContentInput>(content_val.clone()) {
3334                    Ok(content) => content,
3335                    Err(err) => {
3336                        return maybe_error_response(
3337                            is_notification,
3338                            response_id,
3339                            -32602,
3340                            format!("invalid content: {err}"),
3341                        );
3342                    }
3343                };
3344            let origin = request
3345                .params
3346                .get("origin")
3347                .and_then(|v| v.as_str())
3348                .unwrap_or("console");
3349            let interaction_id = request
3350                .params
3351                .get("interaction_id")
3352                .and_then(|v| v.as_str())
3353                .map(ToString::to_string)
3354                .unwrap_or_else(|| meerkat_core::types::SessionId::new().to_string());
3355            let runtime_member_id = identity_rt
3356                .status(&identity)
3357                .await
3358                .ok()
3359                .and_then(|status| status.agent_runtime_id.map(|id| id.as_str().to_string()));
3360
3361            if let Err(err) = runtime
3362                .reserve_identity_interaction(
3363                    identity.as_str(),
3364                    runtime_member_id.as_deref(),
3365                    &interaction_id,
3366                    origin,
3367                    content_val,
3368                )
3369                .await
3370            {
3371                return maybe_error_response(
3372                    is_notification,
3373                    response_id,
3374                    -32003,
3375                    format!("failed to reserve interaction: {err}"),
3376                );
3377            }
3378
3379            let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
3380                .then_some(identity_str);
3381            let send_result = identity_rt
3382                .send_admission_tracked(
3383                    &identity,
3384                    expected_alias,
3385                    &content,
3386                    meerkat_core::types::HandlingMode::Queue,
3387                    None,
3388                )
3389                .await;
3390            match send_result {
3391                Ok(admission) => JsonRpcResponse {
3392                    jsonrpc: JSONRPC_VERSION.to_string(),
3393                    id: response_id,
3394                    result: Some(serde_json::json!({
3395                        "interaction_id": interaction_id,
3396                        "fencing_token": admission.fencing_token.get(),
3397                        "completion_baseline": completion_cursor_json(admission.completion_baseline),
3398                        "stream": {
3399                            "route": format!("/console/identity/{}/stream", identity.as_str()),
3400                            "identity": identity.as_str(),
3401                        }
3402                    })),
3403                    error: None,
3404                },
3405                Err(e) => {
3406                    runtime
3407                        .record_console_lifecycle(
3408                            identity.as_str(),
3409                            "interaction_failed",
3410                            serde_json::json!({
3411                                "interaction_id": interaction_id,
3412                                "origin": origin,
3413                                "error": e.to_string(),
3414                            }),
3415                        )
3416                        .await;
3417                    identity_error_response(response_id, &e)
3418                }
3419            }
3420        }
3421        "mobkit/dispatch" => {
3422            let identity_rt = match identity_ctx {
3423                Some(ctx) => &ctx.runtime,
3424                None => return maybe_identity_not_configured(is_notification, response_id),
3425            };
3426            let identity_str = request
3427                .params
3428                .get("identity")
3429                .and_then(|v| v.as_str())
3430                .unwrap_or("");
3431            let target =
3432                match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3433                {
3434                    Ok(target) => target,
3435                    Err(e) => {
3436                        return maybe_error_response(
3437                            is_notification,
3438                            response_id,
3439                            -32602,
3440                            format!("invalid identity: {e}"),
3441                        );
3442                    }
3443                };
3444            let identity = target.identity.clone();
3445            if let Some(response) =
3446                rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3447            {
3448                return if is_notification {
3449                    String::new()
3450                } else {
3451                    serialize_response(&response)
3452                };
3453            }
3454            let di_val = request
3455                .params
3456                .get("dispatch_input")
3457                .cloned()
3458                .unwrap_or(Value::Null);
3459            let content_val = di_val
3460                .get("content")
3461                .cloned()
3462                .unwrap_or_else(|| Value::String(String::new()));
3463            let content = match serde_json::from_value::<meerkat_core::ContentInput>(content_val) {
3464                Ok(content) => content,
3465                Err(err) => {
3466                    return maybe_error_response(
3467                        is_notification,
3468                        response_id,
3469                        -32602,
3470                        format!("invalid dispatch_input.content: {err}"),
3471                    );
3472                }
3473            };
3474            let origin_str = di_val
3475                .get("origin")
3476                .and_then(|v| v.as_str())
3477                .unwrap_or("system");
3478            let origin = match origin_str {
3479                "connector" => crate::identity_first::DispatchOrigin::Connector,
3480                "scheduler" => crate::identity_first::DispatchOrigin::Scheduler,
3481                "policy" => crate::identity_first::DispatchOrigin::Policy,
3482                "flow" => crate::identity_first::DispatchOrigin::Flow,
3483                _ => crate::identity_first::DispatchOrigin::System,
3484            };
3485            let correlation_id = di_val
3486                .get("correlation_id")
3487                .and_then(|v| v.as_str())
3488                .map(crate::identity_first::CorrelationId::new);
3489            let idempotency_key = di_val
3490                .get("idempotency_key")
3491                .and_then(|v| v.as_str())
3492                .map(crate::identity_first::DispatchIdempotencyKey::new);
3493            let dispatch_input = crate::identity_first::DispatchInput {
3494                content,
3495                origin,
3496                correlation_id,
3497                idempotency_key,
3498            };
3499            let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
3500                .then_some(identity_str);
3501            let dispatch_result = identity_rt
3502                .dispatch_admission_tracked(&identity, expected_alias, &dispatch_input)
3503                .await;
3504            match dispatch_result {
3505                Ok(admission) => JsonRpcResponse {
3506                    jsonrpc: JSONRPC_VERSION.to_string(),
3507                    id: response_id,
3508                    result: Some(serde_json::json!({
3509                        "fencing_token": admission.fencing_token.get(),
3510                        "durable": admission.durable,
3511                        // See mobkit/send: the correlation atom for "wait for
3512                        // the turn I just submitted".
3513                        "completion_baseline": completion_cursor_json(admission.completion_baseline),
3514                    })),
3515                    error: None,
3516                },
3517                Err(e) => identity_error_response(response_id, &e),
3518            }
3519        }
3520        "mobkit/subscribe" => {
3521            let identity_rt = match identity_ctx {
3522                Some(ctx) => &ctx.runtime,
3523                None => return maybe_identity_not_configured(is_notification, response_id),
3524            };
3525            let identity_str = request
3526                .params
3527                .get("identity")
3528                .and_then(|v| v.as_str())
3529                .unwrap_or("");
3530            let target =
3531                match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3532                {
3533                    Ok(target) => target,
3534                    Err(e) => {
3535                        return maybe_error_response(
3536                            is_notification,
3537                            response_id,
3538                            -32602,
3539                            format!("invalid identity: {e}"),
3540                        );
3541                    }
3542                };
3543            let identity = target.identity.clone();
3544            if let Some(response) =
3545                rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3546            {
3547                return if is_notification {
3548                    String::new()
3549                } else {
3550                    serialize_response(&response)
3551                };
3552            }
3553            match identity_rt.subscribe(&identity).await {
3554                Ok(_receiver) => JsonRpcResponse {
3555                    jsonrpc: JSONRPC_VERSION.to_string(),
3556                    id: response_id,
3557                    result: Some(serde_json::json!({
3558                        "identity": identity.as_str(),
3559                        "stream_id": identity.as_str(),
3560                        "subscribed": true,
3561                    })),
3562                    error: None,
3563                },
3564                Err(e) => identity_error_response(response_id, &e),
3565            }
3566        }
3567        "mobkit/status_identity_bootstrap" => {
3568            let identity_rt = match identity_ctx {
3569                Some(ctx) => &ctx.runtime,
3570                None => return maybe_identity_not_configured(is_notification, response_id),
3571            };
3572            JsonRpcResponse {
3573                jsonrpc: JSONRPC_VERSION.to_string(),
3574                id: response_id,
3575                result: Some(
3576                    serde_json::to_value(identity_rt.identity_bootstrap_status())
3577                        .unwrap_or(Value::Null),
3578                ),
3579                error: None,
3580            }
3581        }
3582        "mobkit/wait_identity_bootstrap" => {
3583            let identity_rt = match identity_ctx {
3584                Some(ctx) => &ctx.runtime,
3585                None => return maybe_identity_not_configured(is_notification, response_id),
3586            };
3587            let params = match request.params.as_object() {
3588                Some(params) => params,
3589                None => {
3590                    return maybe_error_response(
3591                        is_notification,
3592                        response_id,
3593                        -32602,
3594                        "params must be an object".to_string(),
3595                    );
3596                }
3597            };
3598            if let Some(field) = params
3599                .keys()
3600                .find(|field| !matches!(field.as_str(), "target" | "timeout_ms"))
3601            {
3602                return maybe_error_response(
3603                    is_notification,
3604                    response_id,
3605                    -32602,
3606                    format!("unsupported parameter: {field}"),
3607                );
3608            }
3609            let target = match params.get("target") {
3610                None => "materialized".to_string(),
3611                Some(Value::String(target)) => target.clone(),
3612                Some(_) => {
3613                    return maybe_error_response(
3614                        is_notification,
3615                        response_id,
3616                        -32602,
3617                        "target must be a string".to_string(),
3618                    );
3619                }
3620            };
3621            if !matches!(target.as_str(), "materialized" | "startup_ready") {
3622                return maybe_error_response(
3623                    is_notification,
3624                    response_id,
3625                    -32602,
3626                    "target must be 'materialized' or 'startup_ready'".to_string(),
3627                );
3628            }
3629            let wait_timeout = match params.get("timeout_ms") {
3630                None => timeout,
3631                Some(value) => match value.as_u64() {
3632                    Some(value) => Duration::from_millis(value),
3633                    None => {
3634                        return maybe_error_response(
3635                            is_notification,
3636                            response_id,
3637                            -32602,
3638                            "timeout_ms must be a non-negative integer".to_string(),
3639                        );
3640                    }
3641                },
3642            };
3643            let (status, timed_out, startup_ready) = if target == "startup_ready" {
3644                let mob_handle = runtime.mob_handle();
3645                let wait = wait_identity_startup_ready(
3646                    identity_rt,
3647                    wait_timeout,
3648                    move |member_ids, remaining| {
3649                        let mob_handle = mob_handle.clone();
3650                        async move {
3651                            match mob_handle
3652                                .wait_for_members_ready(&member_ids, Some(remaining))
3653                                .await
3654                            {
3655                                Ok(_) => IdentityMemberReadiness::Ready,
3656                                Err(error)
3657                                    if crate::unified_runtime::mob_ops::is_ready_wait_timeout(
3658                                        &error,
3659                                    ) =>
3660                                {
3661                                    IdentityMemberReadiness::TimedOut
3662                                }
3663                                Err(error) => IdentityMemberReadiness::Failed(error.to_string()),
3664                            }
3665                        }
3666                    },
3667                )
3668                .await;
3669                match wait {
3670                    Ok(wait) => (wait.status, wait.timed_out, Some(wait.startup_ready)),
3671                    Err(error) => {
3672                        return maybe_error_response(is_notification, response_id, -32000, error);
3673                    }
3674                }
3675            } else {
3676                let (status, timed_out) = identity_rt
3677                    .wait_identity_bootstrap_terminal(wait_timeout)
3678                    .await;
3679                (status, timed_out, None)
3680            };
3681            let mut result = serde_json::to_value(status).unwrap_or_else(|_| serde_json::json!({}));
3682            if let Some(object) = result.as_object_mut() {
3683                object.insert("timed_out".to_string(), Value::Bool(timed_out));
3684                object.insert("target".to_string(), Value::String(target));
3685                if let Some(startup_ready) = startup_ready {
3686                    object.insert("startup_ready".to_string(), Value::Bool(startup_ready));
3687                }
3688            }
3689            JsonRpcResponse {
3690                jsonrpc: JSONRPC_VERSION.to_string(),
3691                id: response_id,
3692                result: Some(result),
3693                error: None,
3694            }
3695        }
3696        "mobkit/status_identity" => {
3697            let identity_rt = match identity_ctx {
3698                Some(ctx) => &ctx.runtime,
3699                None => return maybe_identity_not_configured(is_notification, response_id),
3700            };
3701            let identity_str = request
3702                .params
3703                .get("identity")
3704                .and_then(|v| v.as_str())
3705                .unwrap_or("");
3706            let target =
3707                match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3708                {
3709                    Ok(target) => target,
3710                    Err(e) => {
3711                        return maybe_error_response(
3712                            is_notification,
3713                            response_id,
3714                            -32602,
3715                            format!("invalid identity: {e}"),
3716                        );
3717                    }
3718                };
3719            let identity = target.identity.clone();
3720            if let Some(response) =
3721                rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3722            {
3723                return if is_notification {
3724                    String::new()
3725                } else {
3726                    serialize_response(&response)
3727                };
3728            }
3729            match identity_rt.status(&identity).await {
3730                Ok(status) => {
3731                    let continuity_health =
3732                        serde_json::to_value(&status.continuity_health).unwrap_or(Value::Null);
3733                    let result = serde_json::json!({
3734                        "state": identity_lifecycle_state_json(status.state),
3735                        "identity": status.identity.as_str(),
3736                        "agent_runtime_id": status.agent_runtime_id.as_ref().map(super::identity_first::AgentRuntimeId::as_str),
3737                        "session_id": status.session_id.as_ref().map(ToString::to_string),
3738                        "profile": status.profile.as_ref().map(meerkat_mob::ProfileName::as_str),
3739                        "addressability": addressability_json(status.addressability),
3740                        "display_name": status.display_name.as_ref().map(super::identity_first::DisplayName::as_str),
3741                        "labels": status.labels,
3742                        "generation": status.generation.map(super::identity_first::ContinuityGeneration::get),
3743                        "checkpoint_version": status.checkpoint_version.map(super::identity_first::CheckpointVersion::get),
3744                        "continuity_health": continuity_health,
3745                        "lease_healthy": status.lease.as_ref().map(|lease| lease.healthy),
3746                        "lease": status.lease.as_ref().map(|lease| serde_json::json!({
3747                            "fencing_token": lease.fencing_token.get(),
3748                            "ttl_remaining_ms": lease.ttl_remaining.as_millis() as u64,
3749                            "healthy": lease.healthy,
3750                        })),
3751                    });
3752                    JsonRpcResponse {
3753                        jsonrpc: JSONRPC_VERSION.to_string(),
3754                        id: response_id,
3755                        result: Some(result),
3756                        error: None,
3757                    }
3758                }
3759                Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
3760                    if rpc_live_only_fallback_allowed(&target, identity_str)
3761                        && let Some(live) = target.live.as_ref()
3762                    {
3763                        JsonRpcResponse {
3764                            jsonrpc: JSONRPC_VERSION.to_string(),
3765                            id: response_id,
3766                            result: Some(rpc_live_identity_status_json(live)),
3767                            error: None,
3768                        }
3769                    } else {
3770                        identity_error_response(response_id, &e)
3771                    }
3772                }
3773                Err(e) => identity_error_response(response_id, &e),
3774            }
3775        }
3776        "mobkit/respawn" => {
3777            let identity_rt = match identity_ctx {
3778                Some(ctx) => &ctx.runtime,
3779                None => return maybe_identity_not_configured(is_notification, response_id),
3780            };
3781            let identity_str = request
3782                .params
3783                .get("identity")
3784                .and_then(|v| v.as_str())
3785                .unwrap_or("");
3786            let target =
3787                match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3788                {
3789                    Ok(target) => target,
3790                    Err(e) => {
3791                        return maybe_error_response(
3792                            is_notification,
3793                            response_id,
3794                            -32602,
3795                            format!("invalid identity: {e}"),
3796                        );
3797                    }
3798                };
3799            let identity = target.identity.clone();
3800            if let Some(response) =
3801                rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3802            {
3803                return if is_notification {
3804                    String::new()
3805                } else {
3806                    serialize_response(&response)
3807                };
3808            }
3809            let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
3810                .then_some(identity_str);
3811            let respawn_result = identity_rt
3812                .respawn_identity_in_place_tracked(&identity, expected_alias)
3813                .await;
3814            match respawn_result {
3815                Ok(record) => {
3816                    // A durable identity recovers its authoritative session
3817                    // in place. Raw-member respawn remains available only to
3818                    // unregistered/classic members because it rotates the
3819                    // session. Keep the legacy warning fields for wire
3820                    // compatibility.
3821                    let live_respawn_warning: Option<Value> = None;
3822                    let cleanup_warning: Option<Value> = None;
3823                    runtime
3824                        .record_console_lifecycle(
3825                            identity.as_str(),
3826                            "identity_respawned",
3827                            serde_json::json!({
3828                                "generation": record.generation.get(),
3829                                "checkpoint_version": record.checkpoint_version.get(),
3830                                "live_respawn_warning": live_respawn_warning.clone(),
3831                                "cleanup_warning": cleanup_warning.clone(),
3832                            }),
3833                        )
3834                        .await;
3835                    JsonRpcResponse {
3836                        jsonrpc: JSONRPC_VERSION.to_string(),
3837                        id: response_id,
3838                        result: Some(serde_json::json!({
3839                            "identity": record.identity.as_str(),
3840                            "agent_runtime_id": record.agent_runtime_id.as_str(),
3841                            "session_id": record.session_id.to_string(),
3842                            "generation": record.generation.get(),
3843                            "checkpoint_version": record.checkpoint_version.get(),
3844                            "live_respawn_warning": live_respawn_warning,
3845                            "cleanup_warning": cleanup_warning,
3846                        })),
3847                        error: None,
3848                    }
3849                }
3850                Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
3851                    if rpc_live_only_fallback_allowed(&target, identity_str)
3852                        && let Some(live) = target.live.as_ref()
3853                    {
3854                        match Box::pin(respawn_rpc_live_identity(runtime, live)).await {
3855                            Ok(result) => {
3856                                runtime
3857                                    .record_console_lifecycle(
3858                                        live.identity.as_str(),
3859                                        "identity_respawned",
3860                                        serde_json::json!({}),
3861                                    )
3862                                    .await;
3863                                JsonRpcResponse {
3864                                    jsonrpc: JSONRPC_VERSION.to_string(),
3865                                    id: response_id,
3866                                    result: Some(result),
3867                                    error: None,
3868                                }
3869                            }
3870                            Err(err) => JsonRpcResponse {
3871                                jsonrpc: JSONRPC_VERSION.to_string(),
3872                                id: response_id,
3873                                result: None,
3874                                error: Some(JsonRpcError {
3875                                    code: -32000,
3876                                    message: format!("respawn failed: {err}"),
3877                                    data: None,
3878                                }),
3879                            },
3880                        }
3881                    } else {
3882                        identity_error_response(response_id, &e)
3883                    }
3884                }
3885                Err(e) => identity_error_response(response_id, &e),
3886            }
3887        }
3888        "mobkit/retire" => {
3889            let identity_rt = match identity_ctx {
3890                Some(ctx) => &ctx.runtime,
3891                None => return maybe_identity_not_configured(is_notification, response_id),
3892            };
3893            let identity_str = request
3894                .params
3895                .get("identity")
3896                .and_then(|v| v.as_str())
3897                .unwrap_or("");
3898            let target =
3899                match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
3900                {
3901                    Ok(target) => target,
3902                    Err(e) => {
3903                        return maybe_error_response(
3904                            is_notification,
3905                            response_id,
3906                            -32602,
3907                            format!("invalid identity: {e}"),
3908                        );
3909                    }
3910                };
3911            let identity = target.identity.clone();
3912            if let Some(response) =
3913                rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
3914            {
3915                return if is_notification {
3916                    String::new()
3917                } else {
3918                    serialize_response(&response)
3919                };
3920            }
3921            let cleanup_handle = runtime.mob_handle();
3922            let cleanup_identity = identity.clone();
3923            let include_current = !identity_rt.has_session_bridge();
3924            let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
3925                .then_some(identity_str);
3926            let retire_result = identity_rt
3927                .retire_and_cleanup_live_members_tracked(
3928                    &identity,
3929                    expected_alias,
3930                    move |retired_alias| async move {
3931                        let stale_member_ids = stale_rpc_member_ids_for_identity_with_handle(
3932                            &cleanup_handle,
3933                            cleanup_identity.as_str(),
3934                            retired_alias
3935                                .as_ref()
3936                                .map(crate::identity_first::AgentRuntimeId::as_str),
3937                            include_current,
3938                        )
3939                        .await;
3940                        retire_rpc_member_ids_with_handle(&cleanup_handle, stale_member_ids)
3941                            .await
3942                            .err()
3943                            .map(|error| {
3944                                serde_json::json!({
3945                                    "kind": "stale_member_cleanup_failed_after_identity_retire",
3946                                    "message": error,
3947                                    "identity": cleanup_identity.as_str(),
3948                                })
3949                            })
3950                    },
3951                )
3952                .await;
3953            match retire_result {
3954                Ok((token, cleanup_warning)) => {
3955                    runtime
3956                        .record_console_lifecycle(
3957                            identity.as_str(),
3958                            "identity_retired",
3959                            serde_json::json!({
3960                                "fencing_token": token.get(),
3961                                "cleanup_warning": cleanup_warning.clone(),
3962                            }),
3963                        )
3964                        .await;
3965                    JsonRpcResponse {
3966                        jsonrpc: JSONRPC_VERSION.to_string(),
3967                        id: response_id,
3968                        result: Some(serde_json::json!({
3969                            "fencing_token": token.get(),
3970                            "cleanup_warning": cleanup_warning,
3971                        })),
3972                        error: None,
3973                    }
3974                }
3975                Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
3976                    if rpc_live_only_fallback_allowed(&target, identity_str)
3977                        && let Some(live) = target.live.as_ref()
3978                    {
3979                        match retire_rpc_live_identity(runtime, live).await {
3980                            Ok(()) => {
3981                                runtime
3982                                    .record_console_lifecycle(
3983                                        live.identity.as_str(),
3984                                        "identity_retired",
3985                                        serde_json::json!({}),
3986                                    )
3987                                    .await;
3988                                JsonRpcResponse {
3989                                    jsonrpc: JSONRPC_VERSION.to_string(),
3990                                    id: response_id,
3991                                    result: Some(
3992                                        serde_json::json!({ "identity": live.identity.as_str() }),
3993                                    ),
3994                                    error: None,
3995                                }
3996                            }
3997                            Err(err) => JsonRpcResponse {
3998                                jsonrpc: JSONRPC_VERSION.to_string(),
3999                                id: response_id,
4000                                result: None,
4001                                error: Some(JsonRpcError {
4002                                    code: -32000,
4003                                    message: format!("retire failed: {err}"),
4004                                    data: None,
4005                                }),
4006                            },
4007                        }
4008                    } else {
4009                        identity_error_response(response_id, &e)
4010                    }
4011                }
4012                Err(e) => identity_error_response(response_id, &e),
4013            }
4014        }
4015        "mobkit/reset" => {
4016            let identity_reset_ctx = match identity_ctx {
4017                Some(ctx) => ctx,
4018                None => return maybe_identity_not_configured(is_notification, response_id),
4019            };
4020            let identity_rt = &identity_reset_ctx.runtime;
4021            let identity_str = request
4022                .params
4023                .get("identity")
4024                .and_then(|v| v.as_str())
4025                .unwrap_or("");
4026            let target =
4027                match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
4028                {
4029                    Ok(target) => target,
4030                    Err(e) => {
4031                        return maybe_error_response(
4032                            is_notification,
4033                            response_id,
4034                            -32602,
4035                            format!("invalid identity: {e}"),
4036                        );
4037                    }
4038                };
4039            let identity = target.identity.clone();
4040            if let Some(response) =
4041                rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
4042            {
4043                return if is_notification {
4044                    String::new()
4045                } else {
4046                    serialize_response(&response)
4047                };
4048            }
4049            let _registered_status = match identity_rt.status(&identity).await {
4050                Ok(status) => {
4051                    if !identity_rt.has_session_bridge() {
4052                        let response = rpc_reset_requires_session_bridge_response(response_id);
4053                        return if is_notification {
4054                            String::new()
4055                        } else {
4056                            serialize_response(&response)
4057                        };
4058                    }
4059                    status
4060                }
4061                Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
4062                    if rpc_live_only_fallback_allowed(&target, identity_str)
4063                        && let Some(live) = target.live.as_ref()
4064                    {
4065                        let response =
4066                            match Box::pin(respawn_rpc_live_identity(runtime, live)).await {
4067                                Ok(result) => {
4068                                    runtime
4069                                        .record_console_lifecycle(
4070                                            live.identity.as_str(),
4071                                            "identity_reset",
4072                                            serde_json::json!({}),
4073                                        )
4074                                        .await;
4075                                    JsonRpcResponse {
4076                                        jsonrpc: JSONRPC_VERSION.to_string(),
4077                                        id: response_id,
4078                                        result: Some(result),
4079                                        error: None,
4080                                    }
4081                                }
4082                                Err(err) => JsonRpcResponse {
4083                                    jsonrpc: JSONRPC_VERSION.to_string(),
4084                                    id: response_id,
4085                                    result: None,
4086                                    error: Some(JsonRpcError {
4087                                        code: -32000,
4088                                        message: format!("reset failed: {err}"),
4089                                        data: None,
4090                                    }),
4091                                },
4092                            };
4093                        return if is_notification {
4094                            String::new()
4095                        } else {
4096                            serialize_response(&response)
4097                        };
4098                    }
4099                    let response = identity_error_response(response_id, &e);
4100                    return if is_notification {
4101                        String::new()
4102                    } else {
4103                        serialize_response(&response)
4104                    };
4105                }
4106                Err(e) => {
4107                    let response = identity_error_response(response_id, &e);
4108                    return if is_notification {
4109                        String::new()
4110                    } else {
4111                        serialize_response(&response)
4112                    };
4113                }
4114            };
4115            identity_rt.set_reset_roster_provider_context(
4116                Some(identity_reset_ctx.roster_provider.clone()),
4117                identity_reset_ctx.mob_definition.clone(),
4118            );
4119            let reset_result = if crate::member_comms_id::is_reserved_generated_alias(identity_str)
4120            {
4121                identity_rt
4122                    .reset_member_alias_tracked(&identity, identity_str)
4123                    .await
4124            } else {
4125                identity_rt.reset_tracked(&identity).await
4126            };
4127            match reset_result {
4128                Ok(record) => {
4129                    let cleanup_warning = Some(serde_json::json!({
4130                        "kind": "stale_member_cleanup_skipped_after_identity_reset",
4131                        "message": "reset published the new generation without retiring stale live mob members; identity control calls reject stale runtime ids",
4132                        "identity": identity.as_str(),
4133                        "agent_runtime_id": record.agent_runtime_id.as_str(),
4134                    }));
4135                    runtime
4136                        .record_console_lifecycle(
4137                            identity.as_str(),
4138                            "identity_reset",
4139                            serde_json::json!({
4140                                "generation": record.generation.get(),
4141                                "checkpoint_version": record.checkpoint_version.get(),
4142                                "cleanup_warning": cleanup_warning.clone(),
4143                            }),
4144                        )
4145                        .await;
4146                    JsonRpcResponse {
4147                        jsonrpc: JSONRPC_VERSION.to_string(),
4148                        id: response_id,
4149                        result: Some(serde_json::json!({
4150                            "identity": record.identity.as_str(),
4151                            "agent_runtime_id": record.agent_runtime_id.as_str(),
4152                            "session_id": record.session_id.to_string(),
4153                            "generation": record.generation.get(),
4154                            "checkpoint_version": record.checkpoint_version.get(),
4155                            "cleanup_warning": cleanup_warning,
4156                        })),
4157                        error: None,
4158                    }
4159                }
4160                Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
4161                    if rpc_live_only_fallback_allowed(&target, identity_str)
4162                        && let Some(live) = target.live.as_ref()
4163                    {
4164                        match Box::pin(respawn_rpc_live_identity(runtime, live)).await {
4165                            Ok(result) => {
4166                                runtime
4167                                    .record_console_lifecycle(
4168                                        live.identity.as_str(),
4169                                        "identity_reset",
4170                                        serde_json::json!({}),
4171                                    )
4172                                    .await;
4173                                JsonRpcResponse {
4174                                    jsonrpc: JSONRPC_VERSION.to_string(),
4175                                    id: response_id,
4176                                    result: Some(result),
4177                                    error: None,
4178                                }
4179                            }
4180                            Err(err) => JsonRpcResponse {
4181                                jsonrpc: JSONRPC_VERSION.to_string(),
4182                                id: response_id,
4183                                result: None,
4184                                error: Some(JsonRpcError {
4185                                    code: -32000,
4186                                    message: format!("reset failed: {err}"),
4187                                    data: None,
4188                                }),
4189                            },
4190                        }
4191                    } else {
4192                        identity_error_response(response_id, &e)
4193                    }
4194                }
4195                Err(e) => identity_error_response(response_id, &e),
4196            }
4197        }
4198        "mobkit/delete_identity" => {
4199            let identity_rt = match identity_ctx {
4200                Some(ctx) => &ctx.runtime,
4201                None => return maybe_identity_not_configured(is_notification, response_id),
4202            };
4203            let identity_str = request
4204                .params
4205                .get("identity")
4206                .and_then(|v| v.as_str())
4207                .unwrap_or("");
4208            let target =
4209                match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
4210                {
4211                    Ok(target) => target,
4212                    Err(e) => {
4213                        return maybe_error_response(
4214                            is_notification,
4215                            response_id,
4216                            -32602,
4217                            format!("invalid identity: {e}"),
4218                        );
4219                    }
4220                };
4221            let identity = target.identity.clone();
4222            if let Some(response) =
4223                rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
4224            {
4225                return if is_notification {
4226                    String::new()
4227                } else {
4228                    serialize_response(&response)
4229                };
4230            }
4231            let _registered_status = match identity_rt.status(&identity).await {
4232                Ok(status) => status,
4233                Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
4234                    if target.live.is_some() {
4235                        let response = JsonRpcResponse {
4236                            jsonrpc: JSONRPC_VERSION.to_string(),
4237                            id: response_id,
4238                            result: None,
4239                            error: Some(JsonRpcError {
4240                                code: -32602,
4241                                message: format!(
4242                                    "delete_identity requires durable identity: {} is live-only",
4243                                    identity.as_str()
4244                                ),
4245                                data: Some(serde_json::json!({
4246                                    "kind": "live_only_identity_delete_unsupported",
4247                                    "identity": identity.as_str(),
4248                                })),
4249                            }),
4250                        };
4251                        return if is_notification {
4252                            String::new()
4253                        } else {
4254                            serialize_response(&response)
4255                        };
4256                    }
4257                    let response = identity_error_response(response_id, &e);
4258                    return if is_notification {
4259                        String::new()
4260                    } else {
4261                        serialize_response(&response)
4262                    };
4263                }
4264                Err(e) => {
4265                    let response = identity_error_response(response_id, &e);
4266                    return if is_notification {
4267                        String::new()
4268                    } else {
4269                        serialize_response(&response)
4270                    };
4271                }
4272            };
4273            let cleanup_handle = runtime.mob_handle();
4274            let cleanup_identity = identity.clone();
4275            let include_current = !identity_rt.has_session_bridge();
4276            let expected_alias = crate::member_comms_id::is_reserved_generated_alias(identity_str)
4277                .then_some(identity_str);
4278            let delete_result = identity_rt
4279                .delete_identity_and_cleanup_live_members_tracked(
4280                    &identity,
4281                    expected_alias,
4282                    move |deleted_alias| async move {
4283                        let stale_member_ids = stale_rpc_member_ids_for_identity_with_handle(
4284                            &cleanup_handle,
4285                            cleanup_identity.as_str(),
4286                            deleted_alias
4287                                .as_ref()
4288                                .map(crate::identity_first::AgentRuntimeId::as_str),
4289                            include_current,
4290                        )
4291                        .await;
4292                        retire_rpc_member_ids_with_handle(&cleanup_handle, stale_member_ids)
4293                            .await
4294                            .err()
4295                            .map(|error| {
4296                                serde_json::json!({
4297                                    "kind": "stale_member_cleanup_failed_after_identity_delete",
4298                                    "identity": cleanup_identity.as_str(),
4299                                    "message": error,
4300                                })
4301                            })
4302                    },
4303                )
4304                .await;
4305            match delete_result {
4306                Ok(cleanup_warning) => {
4307                    runtime
4308                        .record_console_lifecycle(
4309                            identity.as_str(),
4310                            "identity_deleted",
4311                            serde_json::json!({
4312                                "cleanup_warning": cleanup_warning,
4313                            }),
4314                        )
4315                        .await;
4316                    JsonRpcResponse {
4317                        jsonrpc: JSONRPC_VERSION.to_string(),
4318                        id: response_id,
4319                        result: Some(serde_json::json!({
4320                            "identity": identity.as_str(),
4321                            "cleanup_warning": cleanup_warning,
4322                        })),
4323                        error: None,
4324                    }
4325                }
4326                Err(e) => identity_error_response(response_id, &e),
4327            }
4328        }
4329        "mobkit/inspect_identity" => {
4330            let identity_rt = match identity_ctx {
4331                Some(ctx) => &ctx.runtime,
4332                None => return maybe_identity_not_configured(is_notification, response_id),
4333            };
4334            let identity_str = request
4335                .params
4336                .get("identity")
4337                .and_then(|v| v.as_str())
4338                .unwrap_or("");
4339            let target =
4340                match resolve_rpc_identity_control_target(runtime, identity_rt, identity_str).await
4341                {
4342                    Ok(target) => target,
4343                    Err(e) => {
4344                        return maybe_error_response(
4345                            is_notification,
4346                            response_id,
4347                            -32602,
4348                            format!("invalid identity: {e}"),
4349                        );
4350                    }
4351                };
4352            let identity = target.identity.clone();
4353            let status = identity_rt.status(&identity).await;
4354            let completion_cursor = identity_rt.completion_cursor(&identity).await;
4355            if let Some(response) =
4356                rpc_stale_live_alias_error_response(identity_rt, &target, response_id.clone()).await
4357            {
4358                return if is_notification {
4359                    String::new()
4360                } else {
4361                    serialize_response(&response)
4362                };
4363            }
4364            match identity_rt.inspect(&identity).await {
4365                Ok(inspection) => {
4366                    let status = status.ok();
4367                    JsonRpcResponse {
4368                        jsonrpc: JSONRPC_VERSION.to_string(),
4369                        id: response_id,
4370                        result: Some(serde_json::json!({
4371                            "identity": identity.as_str(),
4372                            "state": status.as_ref().map(|status| identity_lifecycle_state_json(status.state)),
4373                            "profile": status.as_ref().and_then(|status| status.profile.as_ref().map(meerkat_mob::ProfileName::as_str)),
4374                            "addressability": status.as_ref().map(|status| addressability_json(status.addressability)),
4375                            "display_name": status.as_ref().and_then(|status| status.display_name.as_ref().map(super::identity_first::DisplayName::as_str)),
4376                            "labels": status.as_ref().map(|status| status.labels.clone()).unwrap_or_default(),
4377                            "generation": status.as_ref().and_then(|status| status.generation.map(super::identity_first::ContinuityGeneration::get)),
4378                            "checkpoint_version": status.as_ref().and_then(|status| status.checkpoint_version.map(super::identity_first::CheckpointVersion::get)),
4379                            "continuity_health": status.as_ref().and_then(|status| serde_json::to_value(&status.continuity_health).ok()).unwrap_or(Value::Null),
4380                            "lease_healthy": status.as_ref().and_then(|status| status.lease.as_ref().map(|lease| lease.healthy)),
4381                            "continuity": status.as_ref().map(|status| serde_json::json!({
4382                                "generation": status.generation.map(super::identity_first::ContinuityGeneration::get),
4383                                "checkpoint_version": status.checkpoint_version.map(super::identity_first::CheckpointVersion::get),
4384                                "session_id": status.session_id.as_ref().map(ToString::to_string),
4385                                "agent_runtime_id": status.agent_runtime_id.as_ref().map(super::identity_first::AgentRuntimeId::as_str),
4386                            })).unwrap_or_else(|| serde_json::json!({})),
4387                            "lease": status.as_ref().and_then(|status| status.lease.as_ref().map(|lease| serde_json::json!({
4388                                "fencing_token": lease.fencing_token.get(),
4389                                "ttl_remaining_ms": lease.ttl_remaining.as_millis() as u64,
4390                                "healthy": lease.healthy,
4391                            }))),
4392                            "output_preview": inspection.output_preview,
4393                            "is_final": inspection.is_final,
4394                            "peer_reachable_count": inspection.peer_reachable_count,
4395                            // Completion identity. `output_preview` cannot
4396                            // answer "did a new turn finish?" — two turns may
4397                            // emit identical text — so pollers compare this
4398                            // against the baseline their send/dispatch
4399                            // returned.
4400                            "completion_cursor": completion_cursor_json(completion_cursor),
4401                        })),
4402                        error: None,
4403                    }
4404                }
4405                Err(e @ crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {
4406                    if let Some(live) = target.live.as_ref() {
4407                        JsonRpcResponse {
4408                            jsonrpc: JSONRPC_VERSION.to_string(),
4409                            id: response_id,
4410                            result: Some(rpc_live_identity_inspect_json(runtime, live).await),
4411                            error: None,
4412                        }
4413                    } else {
4414                        identity_error_response(response_id, &e)
4415                    }
4416                }
4417                Err(e) => identity_error_response(response_id, &e),
4418            }
4419        }
4420        "mobkit/reconcile_identity" => {
4421            let ctx = match identity_ctx {
4422                Some(ctx) => ctx,
4423                None => return maybe_identity_not_configured(is_notification, response_id),
4424            };
4425            // The attached runtime context owns the bootstrap policy. Reuse it
4426            // so lazy deployments never turn an ordinary reconcile into an
4427            // eager fleet hydration. The fallback preserves tests/embedders
4428            // that construct the public RPC context without attaching it.
4429            let reconciled = match runtime.refresh_desired_topology().await {
4430                Ok(Some(result)) => Ok(result),
4431                Ok(None) => {
4432                    let roster_specs = match ctx
4433                        .roster_provider
4434                        .roster(&crate::identity_first::RosterContext {
4435                            mob_definition: ctx.mob_definition.clone(),
4436                            previous_identities: Vec::new(),
4437                        })
4438                        .await
4439                    {
4440                        Ok(specs) => specs,
4441                        Err(e) => {
4442                            return maybe_error_response(
4443                                is_notification,
4444                                response_id,
4445                                -32603,
4446                                format!("roster provider failed: {e}"),
4447                            );
4448                        }
4449                    };
4450                    ctx.runtime
4451                        .restore_flow_tracked(
4452                            roster_specs,
4453                            ctx.topology_provider.clone(),
4454                            ctx.customizer.clone(),
4455                        )
4456                        .await
4457                }
4458                Err(error) => Err(error),
4459            };
4460            match reconciled {
4461                Ok(result) => {
4462                    let outcomes: serde_json::Map<String, Value> = result
4463                        .outcomes
4464                        .iter()
4465                        .map(|(id, outcome)| {
4466                            let val = match outcome {
4467                                crate::identity_first::RestoreOutcome::Created {
4468                                    record, ..
4469                                } => {
4470                                    serde_json::json!({
4471                                        "outcome": "created",
4472                                        "identity": record.identity.as_str(),
4473                                        "agent_runtime_id": record.agent_runtime_id.as_str(),
4474                                        "session_id": record.session_id.to_string(),
4475                                        "generation": record.generation.get(),
4476                                    })
4477                                }
4478                                crate::identity_first::RestoreOutcome::Dormant {
4479                                    record, ..
4480                                } => {
4481                                    serde_json::json!({
4482                                        "outcome": "dormant",
4483                                        "identity": id.as_str(),
4484                                        "agent_runtime_id": record.as_ref().map(|record| record.agent_runtime_id.as_str()),
4485                                        "session_id": record.as_ref().map(|record| record.session_id.to_string()),
4486                                        "generation": record.as_ref().map(|record| record.generation.get()),
4487                                    })
4488                                }
4489                                crate::identity_first::RestoreOutcome::Resumed {
4490                                    record, ..
4491                                } => {
4492                                    serde_json::json!({
4493                                        "outcome": "resumed",
4494                                        "identity": record.identity.as_str(),
4495                                        "agent_runtime_id": record.agent_runtime_id.as_str(),
4496                                        "session_id": record.session_id.to_string(),
4497                                        "generation": record.generation.get(),
4498                                    })
4499                                }
4500                                crate::identity_first::RestoreOutcome::Broken(failure) => {
4501                                    serde_json::json!({
4502                                        "outcome": "broken",
4503                                        "identity": failure.identity.as_str(),
4504                                        "detail": failure.detail,
4505                                    })
4506                                }
4507                            };
4508                            (id.to_string(), val)
4509                        })
4510                        .collect();
4511                    JsonRpcResponse {
4512                        jsonrpc: JSONRPC_VERSION.to_string(),
4513                        id: response_id,
4514                        result: Some(serde_json::json!({
4515                            "outcomes": outcomes,
4516                            "managed_edges": result.managed_edges.len(),
4517                        })),
4518                        error: None,
4519                    }
4520                }
4521                Err(e) => identity_error_response(response_id, &e),
4522            }
4523        }
4524        method if method.contains('/') && !method.starts_with("mobkit/") => {
4525            let module_id = method
4526                .split('/')
4527                .next()
4528                .map(ToString::to_string)
4529                .unwrap_or_default();
4530            let route = runtime
4531                .route_module_call(
4532                    &ModuleRouteRequest {
4533                        module_id: module_id.clone(),
4534                        method: method.to_string(),
4535                        params: request.params,
4536                    },
4537                    timeout,
4538                )
4539                .await;
4540            match route {
4541                Ok(response) => JsonRpcResponse {
4542                    jsonrpc: JSONRPC_VERSION.to_string(),
4543                    id: response_id,
4544                    result: Some(serde_json::json!({
4545                        "module_id": response.module_id,
4546                        "method": response.method,
4547                        "payload": response.payload
4548                    })),
4549                    error: None,
4550                },
4551                Err(ModuleRouteError::UnloadedModule(module_id)) => JsonRpcResponse {
4552                    jsonrpc: JSONRPC_VERSION.to_string(),
4553                    id: response_id,
4554                    result: None,
4555                    error: Some(JsonRpcError {
4556                        code: -32601,
4557                        message: format!("Module '{module_id}' not loaded"),
4558                        data: None,
4559                    }),
4560                },
4561                Err(err) => JsonRpcResponse {
4562                    jsonrpc: JSONRPC_VERSION.to_string(),
4563                    id: response_id,
4564                    result: None,
4565                    error: Some(JsonRpcError {
4566                        code: -32000,
4567                        message: format!("Module route failed: {err:?}"),
4568                        data: None,
4569                    }),
4570                },
4571            }
4572        }
4573        method if method.starts_with("mobkit/live/") => match live {
4574            None => crate::live_wiring::live_unavailable_response(response_id),
4575            Some(live) => {
4576                let params = request.params.clone();
4577                let member_alias = live_member_alias(&params);
4578                let identity_runtime = identity_ctx
4579                    .map(|context| &context.runtime)
4580                    .or_else(|| runtime.identity_runtime());
4581                let authority_target = if let Some(alias) = member_alias.as_deref()
4582                    && let Some(identity_runtime) = identity_runtime
4583                {
4584                    identity_runtime.member_alias_lifecycle_target(alias).await
4585                } else {
4586                    Ok(None)
4587                };
4588                match authority_target {
4589                    Err(error) => identity_error_response(response_id, &error),
4590                    Ok(None)
4591                        if member_alias
4592                            .as_deref()
4593                            .is_some_and(crate::member_comms_id::is_reserved_generated_alias) =>
4594                    {
4595                        JsonRpcResponse {
4596                            jsonrpc: JSONRPC_VERSION.to_string(),
4597                            id: response_id,
4598                            result: None,
4599                            error: Some(JsonRpcError {
4600                                code: -32000,
4601                                message: format!(
4602                                    "generated live target requires current identity authority: {}",
4603                                    member_alias.as_deref().unwrap_or_default()
4604                                ),
4605                                data: None,
4606                            }),
4607                        }
4608                    }
4609                    Ok(Some(target)) => {
4610                        let handle = runtime.mob_handle();
4611                        let identity_runtime = identity_runtime.cloned();
4612                        let live = Arc::clone(live);
4613                        let method = method.to_string();
4614                        let operation_response_id = response_id.clone();
4615                        match crate::identity_first::IdentityRuntime::run_member_alias_targets_operation_tracked(
4616                            vec![target],
4617                            move || async move {
4618                                let session = resolve_live_target(
4619                                    &handle,
4620                                    identity_runtime.as_ref(),
4621                                    true,
4622                                    &params,
4623                                )
4624                                .await?;
4625                                Ok(live(session, method, params, operation_response_id).await)
4626                            },
4627                        )
4628                        .await
4629                        {
4630                            Ok(response) => response,
4631                            Err(error) => identity_error_response(response_id, &error),
4632                        }
4633                    }
4634                    Ok(None) => {
4635                        match resolve_live_target(&runtime.mob_handle(), None, false, &params).await
4636                        {
4637                            Ok(session) => {
4638                                live(session, method.to_string(), params, response_id).await
4639                            }
4640                            Err(error) => JsonRpcResponse {
4641                                jsonrpc: JSONRPC_VERSION.to_string(),
4642                                id: response_id,
4643                                result: None,
4644                                error: Some(JsonRpcError {
4645                                    code: -32000,
4646                                    message: format!("live target resolution failed: {error}"),
4647                                    data: None,
4648                                }),
4649                            },
4650                        }
4651                    }
4652                }
4653            }
4654        },
4655        method if workgraph_methods::is_workgraph_method(method) => {
4656            let service = runtime.workgraph_service();
4657            let admission = runtime.workgraph_admission();
4658            // The stdin surface is host-trusted; no wire principal exists to
4659            // promote into goal/confirm.
4660            match workgraph_methods::handle_workgraph_method(
4661                service.as_ref(),
4662                &admission,
4663                workgraph_methods::WorkgraphSurface::HostStdin,
4664                method,
4665                &request.params,
4666            )
4667            .await
4668            {
4669                Ok(result) => JsonRpcResponse {
4670                    jsonrpc: JSONRPC_VERSION.to_string(),
4671                    id: response_id,
4672                    result: Some(result),
4673                    error: None,
4674                },
4675                Err(error) => JsonRpcResponse {
4676                    jsonrpc: JSONRPC_VERSION.to_string(),
4677                    id: response_id,
4678                    result: None,
4679                    error: Some(error),
4680                },
4681            }
4682        }
4683        _ => JsonRpcResponse {
4684            jsonrpc: JSONRPC_VERSION.to_string(),
4685            id: response_id,
4686            result: None,
4687            error: Some(JsonRpcError {
4688                code: -32601,
4689                message: "Method not found".to_string(),
4690                data: None,
4691            }),
4692        },
4693    };
4694    if is_notification {
4695        String::new()
4696    } else {
4697        serialize_response(&response)
4698    }
4699}
4700
4701fn build_models_catalog_result() -> Value {
4702    let entries: Vec<Value> = meerkat_models::catalog()
4703        .iter()
4704        .filter_map(|e| {
4705            let mut val = serde_json::to_value(e).ok()?;
4706            if let Some(provider) = meerkat_core::Provider::parse_strict(e.provider)
4707                && let Some(profile) = meerkat_models::profile_for(provider, e.id)
4708                && let Ok(p) = serde_json::to_value(&profile)
4709            {
4710                val["profile"] = p;
4711            }
4712            Some(val)
4713        })
4714        .collect();
4715    let defaults: Vec<Value> = meerkat_models::provider_defaults()
4716        .iter()
4717        .filter_map(|d| serde_json::to_value(d).ok())
4718        .collect();
4719    serde_json::json!({
4720        "models": entries,
4721        "provider_defaults": defaults,
4722    })
4723}
4724
4725#[derive(Debug, Clone)]
4726struct RpcLiveIdentityAlias {
4727    identity: crate::identity_first::AgentIdentity,
4728    runtime_member_id: String,
4729    member: meerkat_mob::runtime::MobMemberListEntry,
4730    session_id: Option<String>,
4731}
4732
4733#[derive(Debug, Clone)]
4734struct RpcIdentityControlTarget {
4735    identity: crate::identity_first::AgentIdentity,
4736    live: Option<RpcLiveIdentityAlias>,
4737    /// True when resolution observed this identity in the durable runtime.
4738    /// Live-only fallback must never be used after a later `UnknownIdentity`
4739    /// in this case: that transition means a concurrent delete won the race.
4740    was_registered: bool,
4741}
4742
4743/// Whether an `UnknownIdentity` observed after target resolution may use the
4744/// legacy live-member compatibility path. Once durable ownership was seen,
4745/// a later miss means a concurrent delete won and must remain authoritative.
4746/// Generated `rt:*` names are never raw/live-only identities.
4747fn rpc_live_only_fallback_allowed(
4748    target: &RpcIdentityControlTarget,
4749    requested_identity: &str,
4750) -> bool {
4751    !target.was_registered
4752        && !crate::member_comms_id::is_reserved_generated_alias(requested_identity)
4753        && target.live.as_ref().is_some_and(|live| {
4754            // Resolution can race an identity delete after the durable entry
4755            // is removed but before its generated member is retired. Never
4756            // reinterpret such an identity-owned generation as a legacy raw
4757            // member, even when the request used the durable identity name.
4758            !crate::member_comms_id::is_reserved_generated_alias(&live.runtime_member_id)
4759        })
4760}
4761
4762fn rpc_member_durable_identity(member: &meerkat_mob::runtime::MobMemberListEntry) -> String {
4763    crate::member_comms_id::durable_identity_label(&member.labels)
4764        .map(str::to_owned)
4765        // Fallback surfaces the public alias, not the comms-safe roster id
4766        // (meerkat 0.7 MemberCommsName).
4767        .unwrap_or_else(|| {
4768            crate::member_comms_id::runtime_alias_str(member.agent_identity.as_str()).into_owned()
4769        })
4770}
4771
4772async fn resolve_rpc_live_identity_alias(
4773    runtime: &UnifiedRuntime,
4774    requested_identity: &str,
4775) -> Result<Option<RpcLiveIdentityAlias>, String> {
4776    let matches = resolve_rpc_live_identity_alias_candidates(runtime, requested_identity).await?;
4777    if matches.len() > 1 {
4778        return Err(format!(
4779            "ambiguous live identity alias {requested_identity}: candidates [{}]",
4780            matches
4781                .iter()
4782                .map(|entry| entry.runtime_member_id.clone())
4783                .collect::<Vec<_>>()
4784                .join(", ")
4785        ));
4786    }
4787    Ok(matches.into_iter().next())
4788}
4789
4790async fn resolve_rpc_live_runtime_member_alias(
4791    runtime: &UnifiedRuntime,
4792    runtime_member_id: &str,
4793) -> Result<Option<RpcLiveIdentityAlias>, String> {
4794    let requested_member_id = crate::member_comms_id::mob_member_id(runtime_member_id);
4795    let handle = runtime.mob_handle();
4796    let Some(member) = handle
4797        .list_members_including_retiring()
4798        .await
4799        .into_iter()
4800        .find(|entry| entry.agent_identity == requested_member_id)
4801    else {
4802        return Ok(None);
4803    };
4804    if !rpc_live_identity_alias_member_visible(&member) {
4805        return Ok(None);
4806    }
4807    let durable_identity = rpc_member_durable_identity(&member);
4808    let identity = crate::identity_first::AgentIdentity::parse(&durable_identity)
4809        .map_err(|err| format!("invalid projected identity {durable_identity}: {err}"))?;
4810    let session_id = handle
4811        .resolve_bridge_session_id_observation(&member.agent_identity)
4812        .await
4813        .map(|session_id| session_id.to_string());
4814    Ok(Some(RpcLiveIdentityAlias {
4815        identity,
4816        runtime_member_id: crate::member_comms_id::runtime_alias_str(
4817            member.agent_identity.as_str(),
4818        )
4819        .into_owned(),
4820        member,
4821        session_id,
4822    }))
4823}
4824
4825async fn rpc_runtime_member_alias_exists_hidden(
4826    runtime: &UnifiedRuntime,
4827    runtime_member_id: &str,
4828) -> bool {
4829    let requested_member_id = crate::member_comms_id::mob_member_id(runtime_member_id);
4830    runtime
4831        .mob_handle()
4832        .list_members_including_retiring()
4833        .await
4834        .into_iter()
4835        .find(|entry| entry.agent_identity == requested_member_id)
4836        .is_some_and(|member| !rpc_live_identity_alias_member_visible(&member))
4837}
4838
4839async fn rpc_live_identity_alias_exists_hidden(
4840    runtime: &UnifiedRuntime,
4841    requested_identity: &str,
4842) -> bool {
4843    let requested_member_id = crate::member_comms_id::mob_member_id(requested_identity);
4844    runtime
4845        .mob_handle()
4846        .list_members_including_retiring()
4847        .await
4848        .into_iter()
4849        .any(|member| {
4850            (member.agent_identity == requested_member_id
4851                || crate::member_comms_id::durable_identity_label(&member.labels)
4852                    .is_some_and(|identity| identity == requested_identity))
4853                && !rpc_live_identity_alias_member_visible(&member)
4854        })
4855}
4856
4857async fn resolve_rpc_live_identity_alias_candidates(
4858    runtime: &UnifiedRuntime,
4859    requested_identity: &str,
4860) -> Result<Vec<RpcLiveIdentityAlias>, String> {
4861    let requested_member_id = crate::member_comms_id::mob_member_id(requested_identity);
4862    let handle = runtime.mob_handle();
4863    let members = handle.list_members_including_retiring().await;
4864    let exact_matches = members
4865        .iter()
4866        .filter(|entry| entry.agent_identity == requested_member_id)
4867        .cloned()
4868        .collect::<Vec<_>>();
4869    let label_matches = members
4870        .iter()
4871        .filter(|entry| {
4872            crate::member_comms_id::durable_identity_label(&entry.labels)
4873                .is_some_and(|identity| identity == requested_identity)
4874        })
4875        .cloned()
4876        .collect::<Vec<_>>();
4877    let mut matches = exact_matches;
4878    matches.extend(label_matches);
4879    let mut seen_member_ids = BTreeSet::new();
4880    matches.retain(|entry| seen_member_ids.insert(entry.agent_identity.to_string()));
4881    let mut aliases = Vec::with_capacity(matches.len());
4882    for member in matches {
4883        if !rpc_live_identity_alias_member_visible(&member) {
4884            continue;
4885        }
4886        let durable_identity = rpc_member_durable_identity(&member);
4887        let identity = crate::identity_first::AgentIdentity::parse(&durable_identity)
4888            .map_err(|err| format!("invalid projected identity {durable_identity}: {err}"))?;
4889        let session_id = handle
4890            .resolve_bridge_session_id_observation(&member.agent_identity)
4891            .await
4892            .map(|session_id| session_id.to_string());
4893        aliases.push(RpcLiveIdentityAlias {
4894            identity,
4895            runtime_member_id: crate::member_comms_id::runtime_alias_str(
4896                member.agent_identity.as_str(),
4897            )
4898            .into_owned(),
4899            member,
4900            session_id,
4901        });
4902    }
4903    Ok(aliases)
4904}
4905
4906fn rpc_live_identity_alias_member_visible(
4907    member: &meerkat_mob::runtime::MobMemberListEntry,
4908) -> bool {
4909    rpc_live_identity_alias_visible(member.role.as_str(), &member.labels)
4910}
4911
4912fn rpc_live_identity_alias_visible(
4913    member_role: &str,
4914    labels: &std::collections::BTreeMap<String, String>,
4915) -> bool {
4916    let projected_role = labels
4917        .get("role")
4918        .map(String::as_str)
4919        .unwrap_or(member_role);
4920    !is_implicit_delegate_member(member_role, labels)
4921        && !is_implicit_delegate_member(projected_role, labels)
4922}
4923
4924async fn resolve_rpc_identity_control_target(
4925    runtime: &UnifiedRuntime,
4926    identity_rt: &crate::identity_first::IdentityRuntime,
4927    requested_identity: &str,
4928) -> Result<RpcIdentityControlTarget, String> {
4929    let requested_identity = crate::member_comms_id::runtime_alias_str(requested_identity);
4930    let requested_identity = requested_identity.as_ref();
4931    if crate::member_comms_id::is_reserved_generated_alias(requested_identity) {
4932        for status in identity_rt.statuses().await {
4933            if status
4934                .agent_runtime_id
4935                .as_ref()
4936                .is_some_and(|runtime_id| runtime_id.as_str() == requested_identity)
4937            {
4938                let identity = status.identity;
4939                let registered_live =
4940                    resolve_rpc_live_runtime_member_alias(runtime, requested_identity).await?;
4941                if let Some(registered) = registered_live {
4942                    return Ok(RpcIdentityControlTarget {
4943                        identity,
4944                        live: Some(registered),
4945                        was_registered: true,
4946                    });
4947                }
4948                if rpc_runtime_member_alias_exists_hidden(runtime, requested_identity).await {
4949                    return Err(format!("identity hidden by policy: {requested_identity}"));
4950                }
4951                let durable_live_candidates =
4952                    resolve_rpc_live_identity_alias_candidates(runtime, identity.as_str()).await?;
4953                let durable_live = if durable_live_candidates.len() > 1 {
4954                    return Err(format!(
4955                        "ambiguous live identity alias {}: candidates [{}]",
4956                        identity.as_str(),
4957                        durable_live_candidates
4958                            .iter()
4959                            .map(|alias| alias.runtime_member_id.clone())
4960                            .collect::<Vec<_>>()
4961                            .join(", ")
4962                    ));
4963                } else {
4964                    durable_live_candidates.into_iter().next()
4965                };
4966                return Ok(RpcIdentityControlTarget {
4967                    identity,
4968                    live: durable_live,
4969                    was_registered: true,
4970                });
4971            }
4972        }
4973        let live = resolve_rpc_live_identity_alias(runtime, requested_identity).await?;
4974        if let Some(live_alias) = live {
4975            if let Ok(status) = identity_rt.status(&live_alias.identity).await
4976                && !rpc_live_alias_matches_status_runtime(Some(&live_alias), &status)
4977            {
4978                return Ok(RpcIdentityControlTarget {
4979                    identity: live_alias.identity.clone(),
4980                    live: Some(live_alias),
4981                    was_registered: true,
4982                });
4983            }
4984            let live_identity_candidates =
4985                resolve_rpc_live_identity_alias_candidates(runtime, live_alias.identity.as_str())
4986                    .await?;
4987            if live_identity_candidates.len() > 1 {
4988                return Err(format!(
4989                    "ambiguous live identity alias {}: candidates [{}]",
4990                    live_alias.identity.as_str(),
4991                    live_identity_candidates
4992                        .iter()
4993                        .map(|alias| alias.runtime_member_id.clone())
4994                        .collect::<Vec<_>>()
4995                        .join(", ")
4996                ));
4997            }
4998            return Ok(RpcIdentityControlTarget {
4999                identity: live_alias.identity.clone(),
5000                live: Some(live_alias),
5001                was_registered: false,
5002            });
5003        }
5004        if rpc_runtime_member_alias_exists_hidden(runtime, requested_identity).await {
5005            return Err(format!("identity hidden by policy: {requested_identity}"));
5006        }
5007        return Err(format!("runtime identity not found: {requested_identity}"));
5008    }
5009    if let Ok(identity) = crate::identity_first::AgentIdentity::parse(requested_identity) {
5010        match identity_rt.status(&identity).await {
5011            Ok(status) => {
5012                let registered_live = match status.agent_runtime_id.as_ref() {
5013                    Some(runtime_id) => {
5014                        resolve_rpc_live_runtime_member_alias(runtime, runtime_id.as_str()).await?
5015                    }
5016                    None => None,
5017                };
5018                if let Some(registered) = registered_live {
5019                    return Ok(RpcIdentityControlTarget {
5020                        identity,
5021                        live: Some(registered),
5022                        was_registered: true,
5023                    });
5024                }
5025                if let Some(runtime_id) = status.agent_runtime_id.as_ref()
5026                    && rpc_runtime_member_alias_exists_hidden(runtime, runtime_id.as_str()).await
5027                {
5028                    return Err(format!("identity hidden by policy: {requested_identity}"));
5029                }
5030                let requested_live_candidates =
5031                    resolve_rpc_live_identity_alias_candidates(runtime, requested_identity).await?;
5032                let requested_live = if requested_live_candidates.len() > 1 {
5033                    return Err(format!(
5034                        "ambiguous live identity alias {requested_identity}: candidates [{}]",
5035                        requested_live_candidates
5036                            .iter()
5037                            .map(|alias| alias.runtime_member_id.clone())
5038                            .collect::<Vec<_>>()
5039                            .join(", ")
5040                    ));
5041                } else {
5042                    requested_live_candidates.into_iter().next()
5043                };
5044                return Ok(RpcIdentityControlTarget {
5045                    identity,
5046                    live: requested_live,
5047                    was_registered: true,
5048                });
5049            }
5050            Err(crate::identity_first::IdentityRuntimeError::UnknownIdentity(_)) => {}
5051            Err(err) => return Err(err.to_string()),
5052        }
5053    }
5054    for status in identity_rt.statuses().await {
5055        if status
5056            .agent_runtime_id
5057            .as_ref()
5058            .is_some_and(|runtime_id| runtime_id.as_str() == requested_identity)
5059        {
5060            let identity = status.identity;
5061            let registered_live =
5062                resolve_rpc_live_runtime_member_alias(runtime, requested_identity).await?;
5063            let durable_live_candidates =
5064                resolve_rpc_live_identity_alias_candidates(runtime, identity.as_str()).await?;
5065            let durable_live = if durable_live_candidates.len() > 1 {
5066                return Err(format!(
5067                    "ambiguous live identity alias {}: candidates [{}]",
5068                    identity.as_str(),
5069                    durable_live_candidates
5070                        .iter()
5071                        .map(|alias| alias.runtime_member_id.clone())
5072                        .collect::<Vec<_>>()
5073                        .join(", ")
5074                ));
5075            } else {
5076                durable_live_candidates.into_iter().next()
5077            };
5078            let live = match (registered_live, durable_live) {
5079                (Some(registered), Some(durable))
5080                    if registered.runtime_member_id == durable.runtime_member_id =>
5081                {
5082                    Some(registered)
5083                }
5084                (Some(registered), None) => Some(registered),
5085                (Some(_registered), Some(durable)) => Some(durable),
5086                (None, durable) => durable,
5087            };
5088            return Ok(RpcIdentityControlTarget {
5089                identity,
5090                live,
5091                was_registered: true,
5092            });
5093        }
5094    }
5095    let live = resolve_rpc_live_identity_alias(runtime, requested_identity).await?;
5096    if let Some(live_alias) = live {
5097        if let Some(bound_status) = identity_rt.statuses().await.into_iter().find(|status| {
5098            status
5099                .agent_runtime_id
5100                .as_ref()
5101                .is_some_and(|runtime_id| runtime_id.as_str() == live_alias.runtime_member_id)
5102        }) && bound_status.identity != live_alias.identity
5103        {
5104            return Err(format!(
5105                "stale live identity alias: live console alias {} resolves to {}, but identity runtime binding belongs to {}",
5106                live_alias.identity.as_str(),
5107                live_alias.runtime_member_id,
5108                bound_status.identity.as_str(),
5109            ));
5110        }
5111        let live_identity_candidates =
5112            resolve_rpc_live_identity_alias_candidates(runtime, live_alias.identity.as_str())
5113                .await?;
5114        if live_identity_candidates.len() > 1 {
5115            return Err(format!(
5116                "ambiguous live identity alias {}: candidates [{}]",
5117                live_alias.identity.as_str(),
5118                live_identity_candidates
5119                    .iter()
5120                    .map(|alias| alias.runtime_member_id.clone())
5121                    .collect::<Vec<_>>()
5122                    .join(", ")
5123            ));
5124        }
5125        return Ok(RpcIdentityControlTarget {
5126            identity: live_alias.identity.clone(),
5127            live: Some(live_alias),
5128            was_registered: false,
5129        });
5130    }
5131    if rpc_live_identity_alias_exists_hidden(runtime, requested_identity).await {
5132        return Err(format!("identity hidden by policy: {requested_identity}"));
5133    }
5134    let identity = crate::identity_first::AgentIdentity::parse(requested_identity)
5135        .map_err(|err| err.to_string())?;
5136    Ok(RpcIdentityControlTarget {
5137        identity,
5138        live: None,
5139        was_registered: false,
5140    })
5141}
5142
5143fn rpc_reset_requires_session_bridge_response(response_id: Value) -> JsonRpcResponse {
5144    JsonRpcResponse {
5145        jsonrpc: JSONRPC_VERSION.to_string(),
5146        id: response_id,
5147        result: None,
5148        error: Some(JsonRpcError {
5149            code: -32602,
5150            message: "reset requires an identity runtime with a session bridge".to_string(),
5151            data: Some(serde_json::json!({
5152                "kind": "identity_reset_requires_session_bridge",
5153            })),
5154        }),
5155    }
5156}
5157
5158fn rpc_live_alias_matches_status_runtime(
5159    alias: Option<&RpcLiveIdentityAlias>,
5160    status: &crate::identity_first::IdentityStatus,
5161) -> bool {
5162    let Some(alias) = alias else {
5163        return true;
5164    };
5165    let session_matches = match (
5166        status.session_id.as_ref().map(ToString::to_string),
5167        alias.session_id.as_deref(),
5168    ) {
5169        (Some(status_session), Some(live_session)) => status_session == live_session,
5170        _ => true,
5171    };
5172    status
5173        .agent_runtime_id
5174        .as_ref()
5175        .is_some_and(|runtime_id| runtime_id.as_str() == alias.runtime_member_id)
5176        && alias.identity == status.identity
5177        && session_matches
5178}
5179
5180async fn rpc_stale_live_alias_error_response(
5181    identity_rt: &crate::identity_first::IdentityRuntime,
5182    target: &RpcIdentityControlTarget,
5183    response_id: Value,
5184) -> Option<JsonRpcResponse> {
5185    let live = target.live.as_ref()?;
5186    let Ok(status) = identity_rt.status(&target.identity).await else {
5187        return None;
5188    };
5189    if rpc_live_alias_matches_status_runtime(Some(live), &status) {
5190        return None;
5191    }
5192    Some(JsonRpcResponse {
5193        jsonrpc: JSONRPC_VERSION.to_string(),
5194        id: response_id,
5195        result: None,
5196        error: Some(JsonRpcError {
5197            code: -32000,
5198            message: format!(
5199                "identity runtime binding for {} points at {}, but requested live member is {}",
5200                target.identity.as_str(),
5201                status
5202                    .agent_runtime_id
5203                    .as_ref()
5204                    .map(crate::identity_first::AgentRuntimeId::as_str)
5205                    .unwrap_or("<none>"),
5206                live.runtime_member_id
5207            ),
5208            data: Some(serde_json::json!({
5209                "kind": "stale_identity_runtime_binding",
5210                "identity": target.identity.as_str(),
5211                "registered_runtime_member_id": status.agent_runtime_id.as_ref().map(crate::identity_first::AgentRuntimeId::as_str),
5212                "live_runtime_member_id": live.runtime_member_id,
5213                "registered_session_id": status.session_id.as_ref().map(ToString::to_string),
5214                "live_session_id": live.session_id,
5215            })),
5216        }),
5217    })
5218}
5219
5220fn rpc_member_is_addressable(member: &meerkat_mob::runtime::MobMemberListEntry) -> bool {
5221    member
5222        .labels
5223        .get("addressable")
5224        .map(|value| !value.eq_ignore_ascii_case("false"))
5225        .unwrap_or(true)
5226}
5227
5228fn rpc_live_identity_status_json(alias: &RpcLiveIdentityAlias) -> Value {
5229    serde_json::json!({
5230        "state": crate::mob_handle_runtime::member_status_state_string(alias.member.status),
5231        "identity": alias.identity.as_str(),
5232        "agent_runtime_id": alias.runtime_member_id,
5233        "session_id": alias.session_id,
5234        "profile": alias.member.role.to_string(),
5235        "addressability": if rpc_member_is_addressable(&alias.member) { "addressable" } else { "internal_only" },
5236        "display_name": alias.member.labels.get("display_name"),
5237        "labels": alias.member.labels,
5238        "generation": Value::Null,
5239        "checkpoint_version": Value::Null,
5240        "continuity_health": Value::Null,
5241        "lease_healthy": Value::Null,
5242        "lease": Value::Null,
5243    })
5244}
5245
5246async fn rpc_live_identity_inspect_json(
5247    runtime: &UnifiedRuntime,
5248    alias: &RpcLiveIdentityAlias,
5249) -> Value {
5250    let snapshot = runtime
5251        .mob_handle()
5252        .member_status(&crate::member_comms_id::mob_member_id(
5253            alias.runtime_member_id.as_str(),
5254        ))
5255        .await
5256        .ok();
5257    serde_json::json!({
5258        "identity": alias.identity.as_str(),
5259        "state": crate::mob_handle_runtime::member_status_state_string(alias.member.status),
5260        "profile": alias.member.role.to_string(),
5261        "addressability": if rpc_member_is_addressable(&alias.member) { "addressable" } else { "internal_only" },
5262        "display_name": alias.member.labels.get("display_name"),
5263        "labels": alias.member.labels,
5264        "generation": Value::Null,
5265        "checkpoint_version": Value::Null,
5266        "continuity_health": Value::Null,
5267        "lease_healthy": Value::Null,
5268        "continuity": {
5269            "generation": Value::Null,
5270            "checkpoint_version": Value::Null,
5271            "session_id": alias.session_id,
5272            "agent_runtime_id": alias.runtime_member_id,
5273        },
5274        "lease": Value::Null,
5275        "output_preview": snapshot.as_ref().and_then(|snapshot| snapshot.output_preview.clone()),
5276        "is_final": snapshot.as_ref().map(|snapshot| snapshot.is_final).unwrap_or(false),
5277        "peer_reachable_count": alias.member.wired_to.len(),
5278        // Raw live aliases are not identity-first owned, so no identity
5279        // authority tracks their completions. Null rather than a fabricated
5280        // zero: a client must not mistake "not tracked" for "no turns yet".
5281        "completion_cursor": Value::Null,
5282        // Machine-owned liveness projection (meerkat 0.7.29, ask 14):
5283        // run_state / in_flight_work / health for operator triage.
5284        "progress": snapshot.as_ref().and_then(|snapshot| snapshot.progress.clone()),
5285    })
5286}
5287
5288async fn retire_rpc_live_identity(
5289    runtime: &UnifiedRuntime,
5290    alias: &RpcLiveIdentityAlias,
5291) -> Result<(), String> {
5292    retire_rpc_runtime_member_id(runtime, alias.runtime_member_id.as_str()).await
5293}
5294
5295async fn retire_rpc_runtime_member_id(
5296    runtime: &UnifiedRuntime,
5297    runtime_member_id: &str,
5298) -> Result<(), String> {
5299    retire_rpc_runtime_member_id_with_handle(&runtime.mob_handle(), runtime_member_id).await
5300}
5301
5302async fn retire_rpc_runtime_member_id_with_handle(
5303    handle: &meerkat_mob::MobHandle,
5304    runtime_member_id: &str,
5305) -> Result<(), String> {
5306    match handle
5307        .retire(crate::member_comms_id::mob_member_id(runtime_member_id))
5308        .await
5309    {
5310        Ok(()) => Ok(()),
5311        Err(err) if mob_methods::lifecycle_archive_cleanup_completed(&err.to_string()) => Ok(()),
5312        Err(err) => Err(err.to_string()),
5313    }
5314}
5315
5316fn rpc_member_id_matches_durable_identity(member_id: &str, durable_identity: &str) -> bool {
5317    // Roster ids are comms-safe encodings of public aliases (meerkat 0.7
5318    // MemberCommsName); compare in the public alias space.
5319    crate::member_comms_id::runtime_alias_str(member_id) == durable_identity
5320}
5321
5322fn rpc_runtime_alias_generation(alias: &str, durable_identity: &str) -> Option<u64> {
5323    let alias = crate::member_comms_id::runtime_alias_str(alias);
5324    let rest = alias.strip_prefix("rt:")?;
5325    let (identity, generation) = rest.rsplit_once(':')?;
5326    if identity != durable_identity {
5327        return None;
5328    }
5329    generation.parse().ok()
5330}
5331
5332async fn stale_rpc_member_ids_for_identity_with_handle(
5333    handle: &meerkat_mob::MobHandle,
5334    durable_identity: &str,
5335    current_runtime_member_id: Option<&str>,
5336    include_current: bool,
5337) -> Vec<String> {
5338    let Some(current_generation) = current_runtime_member_id
5339        .and_then(|alias| rpc_runtime_alias_generation(alias, durable_identity))
5340    else {
5341        return Vec::new();
5342    };
5343    handle
5344        .list_members_including_retiring()
5345        .await
5346        .into_iter()
5347        .filter(|member| {
5348            if !rpc_live_identity_alias_member_visible(member) {
5349                return false;
5350            }
5351            let matches_identity =
5352                rpc_member_id_matches_durable_identity(
5353                    member.agent_identity.as_str(),
5354                    durable_identity,
5355                ) || crate::member_comms_id::durable_identity_label(&member.labels)
5356                    .is_some_and(|identity| identity == durable_identity);
5357            let public_alias =
5358                crate::member_comms_id::runtime_alias_str(member.agent_identity.as_str());
5359            matches_identity
5360                && rpc_runtime_alias_generation(public_alias.as_ref(), durable_identity)
5361                    .is_some_and(|generation| {
5362                        generation < current_generation
5363                            || (include_current && generation == current_generation)
5364                    })
5365        })
5366        // `retire_rpc_runtime_member_id` re-encodes; hand it the alias.
5367        .map(|member| {
5368            crate::member_comms_id::runtime_alias_str(member.agent_identity.as_str()).into_owned()
5369        })
5370        .collect()
5371}
5372
5373async fn retire_rpc_member_ids_with_handle(
5374    handle: &meerkat_mob::MobHandle,
5375    member_ids: Vec<String>,
5376) -> Result<(), String> {
5377    for member_id in member_ids {
5378        retire_rpc_runtime_member_id_with_handle(handle, &member_id).await?;
5379    }
5380    Ok(())
5381}
5382
5383async fn respawn_rpc_live_identity(
5384    runtime: &UnifiedRuntime,
5385    alias: &RpcLiveIdentityAlias,
5386) -> Result<Value, String> {
5387    let mut result = Box::pin(respawn_rpc_runtime_member_id(
5388        runtime,
5389        alias.runtime_member_id.as_str(),
5390    ))
5391    .await?;
5392    result["identity"] = serde_json::json!(alias.identity.as_str());
5393    Ok(result)
5394}
5395
5396async fn respawn_rpc_runtime_member_id(
5397    runtime: &UnifiedRuntime,
5398    runtime_member_id: &str,
5399) -> Result<Value, String> {
5400    respawn_rpc_runtime_member_id_with_handle(&runtime.mob_handle(), runtime_member_id).await
5401}
5402
5403async fn respawn_rpc_runtime_member_id_with_handle(
5404    handle: &meerkat_mob::MobHandle,
5405    runtime_member_id: &str,
5406) -> Result<Value, String> {
5407    let member_id = crate::member_comms_id::mob_member_id(runtime_member_id);
5408    // Best-effort repair material: a faulted lookup degrades to None (the
5409    // respawn itself surfaces real faults).
5410    let entry_before_respawn = handle.get_member(&member_id).await.ok().flatten();
5411    let mut topology_restore_warning = None;
5412    match handle.respawn(member_id.clone(), None).await {
5413        Ok(_receipt) => {}
5414        Err(err) => {
5415            if let Some(failed_peer_ids) = topology_restore_failed_peer_ids(&err) {
5416                tracing::warn!(
5417                    member_id = %member_id,
5418                    failed_peer_count = failed_peer_ids.len(),
5419                    failed_peer_ids = ?failed_peer_ids,
5420                    "rpc member respawn restored member with isolated peer edges; continuing degraded respawn"
5421                );
5422                topology_restore_warning = Some(topology_restore_warning_json(&failed_peer_ids));
5423            } else if mob_methods::lifecycle_archive_cleanup_completed(&err.to_string()) {
5424                // A faulted lookup must not read as "absent" (that would mint
5425                // a spurious replacement member); surface it instead.
5426                if handle
5427                    .get_member(&member_id)
5428                    .await
5429                    .map_err(|lookup_err| lookup_err.to_string())?
5430                    .is_none()
5431                    && let Some(entry) = entry_before_respawn
5432                {
5433                    let mut spec =
5434                        meerkat_mob::SpawnMemberSpec::new(entry.role.clone(), member_id.clone());
5435                    if !entry.labels.is_empty() {
5436                        spec = spec.with_labels(entry.labels.clone());
5437                    }
5438                    handle
5439                        .ensure_member(spec)
5440                        .await
5441                        .map_err(|ensure_err| ensure_err.to_string())?;
5442                }
5443            } else {
5444                return Err(err.to_string());
5445            }
5446        }
5447    }
5448    let session_id = handle
5449        .resolve_bridge_session_id_observation(&member_id)
5450        .await
5451        .map(|session_id| session_id.to_string());
5452    Ok(serde_json::json!({
5453        "agent_runtime_id": runtime_member_id,
5454        "session_id": session_id,
5455        "generation": Value::Null,
5456        "checkpoint_version": Value::Null,
5457        "topology_restore_warning": topology_restore_warning,
5458    }))
5459}
5460
5461fn identity_not_configured(response_id: Value) -> String {
5462    error_response(response_id, -32601, "identity-first runtime not configured")
5463}
5464
5465fn maybe_identity_not_configured(is_notification: bool, response_id: Value) -> String {
5466    if is_notification {
5467        String::new()
5468    } else {
5469        identity_not_configured(response_id)
5470    }
5471}
5472
5473/// Wire form of a [`CompletionCursor`]: the comparable atom a client polls to
5474/// learn that a NEW turn finished, without ever comparing output text.
5475///
5476/// [`CompletionCursor`]: crate::identity_first::CompletionCursor
5477fn completion_cursor_json(cursor: crate::identity_first::CompletionCursor) -> Value {
5478    serde_json::json!({
5479        "epoch": cursor.epoch.get(),
5480        "turns": cursor.turns,
5481    })
5482}
5483
5484fn addressability_json(addressability: crate::identity_first::AgentAddressability) -> &'static str {
5485    match addressability {
5486        crate::identity_first::AgentAddressability::Addressable => "addressable",
5487        crate::identity_first::AgentAddressability::InternalOnly => "internal_only",
5488    }
5489}
5490
5491/// Wire vocabulary for identity-first lifecycle states — see
5492/// [`crate::identity_first::IdentityLifecycleState::wire_str`].
5493fn identity_lifecycle_state_json(
5494    state: crate::identity_first::IdentityLifecycleState,
5495) -> &'static str {
5496    state.wire_str()
5497}
5498
5499fn identity_error_response(
5500    response_id: Value,
5501    err: &crate::identity_first::IdentityRuntimeError,
5502) -> JsonRpcResponse {
5503    use crate::identity_first::IdentityRuntimeError;
5504    let (code, message) = match err {
5505        IdentityRuntimeError::UnknownIdentity(id) => (-32001, format!("unknown identity: {id}")),
5506        IdentityRuntimeError::NotAddressable(na) => {
5507            (-32002, format!("not addressable: {}", na.identity))
5508        }
5509        IdentityRuntimeError::NoActiveLease(id) => (-32003, format!("no active lease: {id}")),
5510        // -32005, NOT -32004: -32004 is the SDKs' reserved
5511        // `CAPABILITY_UNAVAILABLE_CODE`, which both SDKs reify into a
5512        // permanent-capability-gap error type. `LeaseLost` is a transient,
5513        // recoverable lease-renewal failure on the identity send/dispatch path,
5514        // so it gets its own identity-plane code (sibling to -32001..-32003).
5515        IdentityRuntimeError::LeaseLost(id) => (-32005, format!("lease lost: {id}")),
5516        _ => (-32603, format!("{err}")),
5517    };
5518    JsonRpcResponse {
5519        jsonrpc: JSONRPC_VERSION.to_string(),
5520        id: response_id,
5521        result: None,
5522        error: Some(JsonRpcError {
5523            code,
5524            message,
5525            data: None,
5526        }),
5527    }
5528}
5529
5530fn error_response(response_id: Value, code: i64, message: impl Into<String>) -> String {
5531    let message = message.into();
5532    let ambiguous_alias_rest = message
5533        .strip_prefix("ambiguous live identity alias ")
5534        .or_else(|| message.strip_prefix("invalid identity: ambiguous live identity alias "));
5535    let stale_live_alias_rest = message
5536        .strip_prefix("stale live identity alias: live console alias ")
5537        .or_else(|| {
5538            message.strip_prefix("invalid identity: stale live identity alias: live console alias ")
5539        });
5540    let hidden_policy_identity = message
5541        .strip_prefix("identity hidden by policy: ")
5542        .or_else(|| message.strip_prefix("invalid identity: identity hidden by policy: "));
5543    let data = if let Some(rest) = ambiguous_alias_rest {
5544        let (identity, candidates) = rest
5545            .split_once(": candidates [")
5546            .map(|(identity, candidates)| {
5547                (
5548                    identity.to_string(),
5549                    candidates
5550                        .trim_end_matches(']')
5551                        .split(',')
5552                        .map(str::trim)
5553                        .filter(|value| !value.is_empty())
5554                        .map(str::to_string)
5555                        .collect::<Vec<_>>(),
5556                )
5557            })
5558            .unwrap_or_else(|| (rest.to_string(), Vec::new()));
5559        Some(serde_json::json!({
5560            "kind": "ambiguous_live_identity_alias",
5561            "identity": identity,
5562            "candidates": candidates,
5563        }))
5564    } else if let Some(rest) = stale_live_alias_rest {
5565        let (identity, rest) = rest.split_once(" resolves to ").unwrap_or((rest, ""));
5566        let (runtime_member_id, bound_identity) = rest
5567            .split_once(", but identity runtime binding belongs to ")
5568            .unwrap_or((rest, ""));
5569        Some(serde_json::json!({
5570            "kind": "stale_live_identity_alias",
5571            "identity": identity,
5572            "live_runtime_member_id": runtime_member_id,
5573            "bound_identity": bound_identity,
5574        }))
5575    } else {
5576        hidden_policy_identity.map(|identity| {
5577            serde_json::json!({
5578                "kind": "identity_hidden_by_policy",
5579                "identity": identity,
5580            })
5581        })
5582    };
5583    serialize_response(&JsonRpcResponse {
5584        jsonrpc: JSONRPC_VERSION.to_string(),
5585        id: response_id,
5586        result: None,
5587        error: Some(JsonRpcError {
5588            code,
5589            message,
5590            data,
5591        }),
5592    })
5593}
5594
5595/// Resolve a `mobkit/live/*` member target to the member's bridge session.
5596/// Accepts `{session_id}` verbatim, `{identity}` / `{member_id}` as a plain
5597/// member name, runtime alias, or durable identity (roster `agent_identity`
5598/// label fallback — the same resolution as `/agents/{id}/events` and
5599/// `cross_mob/peer_info`).
5600fn live_member_alias(params: &Value) -> Option<String> {
5601    if params.get("session_id").and_then(Value::as_str).is_some() {
5602        return None;
5603    }
5604    params
5605        .get("identity")
5606        .and_then(Value::as_str)
5607        .or_else(|| params.get("member_id").and_then(Value::as_str))
5608        .map(|raw| crate::member_comms_id::runtime_alias_str(raw).into_owned())
5609}
5610
5611async fn resolve_live_target(
5612    handle: &meerkat_mob::MobHandle,
5613    identity_runtime: Option<&Arc<crate::identity_first::IdentityRuntime>>,
5614    identity_authoritative: bool,
5615    params: &Value,
5616) -> Result<Option<meerkat_core::types::SessionId>, String> {
5617    if let Some(raw) = params.get("session_id").and_then(Value::as_str) {
5618        return Ok(meerkat_core::types::SessionId::parse(raw).ok());
5619    }
5620    let Some(raw) = live_member_alias(params) else {
5621        return Ok(None);
5622    };
5623    let current_alias = if identity_authoritative {
5624        let identity_runtime = identity_runtime
5625            .ok_or_else(|| "durable live target lost its IdentityRuntime authority".to_string())?;
5626        let identity =
5627            crate::identity_first::IdentityRuntime::identity_for_generated_member_alias(&raw)
5628                .or_else(|| crate::identity_first::AgentIdentity::parse(&raw).ok())
5629                .ok_or_else(|| format!("invalid durable live target {raw:?}"))?;
5630        identity_runtime
5631            .status(&identity)
5632            .await
5633            .map_err(|error| error.to_string())?
5634            .agent_runtime_id
5635            .map(|runtime_id| runtime_id.to_string())
5636            .ok_or_else(|| format!("identity {identity} has no current runtime member"))?
5637    } else {
5638        let direct = crate::member_comms_id::mob_member_id(&raw);
5639        if handle
5640            .get_member(&direct)
5641            .await
5642            .map_err(|error| error.to_string())?
5643            .is_some()
5644        {
5645            raw.clone()
5646        } else {
5647            let candidates = handle
5648                .list_members_including_retiring()
5649                .await
5650                .into_iter()
5651                .filter(|entry| {
5652                    crate::member_comms_id::durable_identity_label(&entry.labels)
5653                        .is_some_and(|identity| identity == raw)
5654                })
5655                .map(|entry| {
5656                    crate::member_comms_id::runtime_alias_str(entry.agent_identity.as_str())
5657                        .into_owned()
5658                })
5659                .collect::<BTreeSet<_>>();
5660            match candidates.len() {
5661                0 => raw.clone(),
5662                1 => candidates
5663                    .into_iter()
5664                    .next()
5665                    .ok_or_else(|| "live member alias candidate disappeared".to_string())?,
5666                _ => {
5667                    return Err(format!(
5668                        "ambiguous live member alias {raw}: candidates [{}]",
5669                        candidates.into_iter().collect::<Vec<_>>().join(", ")
5670                    ));
5671                }
5672            }
5673        }
5674    };
5675    let member_id = crate::member_comms_id::mob_member_id(&current_alias);
5676    Ok(handle.resolve_bridge_session_id(&member_id).await)
5677}
5678
5679fn maybe_error_response(
5680    is_notification: bool,
5681    response_id: Value,
5682    code: i64,
5683    message: impl Into<String>,
5684) -> String {
5685    if is_notification {
5686        String::new()
5687    } else {
5688        error_response(response_id, code, message)
5689    }
5690}
5691
5692pub(crate) fn agent_memory_rpc_error(
5693    operation: &str,
5694    err: crate::identity_first::AgentMemoryError,
5695) -> JsonRpcError {
5696    let code = match &err {
5697        crate::identity_first::AgentMemoryError::InvalidConfig(_)
5698        | crate::identity_first::AgentMemoryError::InvalidRecord(_) => -32602,
5699        crate::identity_first::AgentMemoryError::Unsupported(_) => -32601,
5700        crate::identity_first::AgentMemoryError::Io(_)
5701        | crate::identity_first::AgentMemoryError::Parse(_)
5702        | crate::identity_first::AgentMemoryError::Timeout(_) => -32603,
5703    };
5704    JsonRpcError {
5705        code,
5706        message: format!("agent memory {operation} failed: {err}"),
5707        data: None,
5708    }
5709}
5710
5711fn serialize_response(response: &JsonRpcResponse) -> String {
5712    serde_json::to_string(response).unwrap_or_else(|_| {
5713        r#"{"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"Internal error"}}"#
5714            .to_string()
5715    })
5716}
5717
5718#[cfg(test)]
5719#[allow(clippy::expect_used)]
5720mod tests {
5721    use super::{
5722        IdentityMemberReadiness, error_response, handle_unified_rpc_json, identity_error_response,
5723        resolve_live_target, resolve_rpc_identity_control_target, rpc_live_identity_alias_visible,
5724        rpc_member_id_matches_durable_identity, rpc_runtime_alias_generation,
5725        wait_identity_startup_ready,
5726    };
5727    use crate::identity_first::contracts::{ContinuityStore, LeaseProvider, RosterProvider};
5728    use crate::identity_first::{
5729        AgentAddressability, AgentBuildDraft, AgentIdentity, AgentRuntimeId, BridgeError,
5730        CheckpointVersion, ContinuityGeneration, ContinuityRecord, DurabilityPolicy,
5731        DurableAgentSpec, FencingToken, IdentityLifecycleState, IdentityRuntime,
5732        IdentityRuntimeConfig, LeaseAcquireResult, LeaseGrant, LocalContinuityStore,
5733        LocalLeaseProvider, MarkdownAgentMemoryStore, ResumeSessionOutcome, RosterContext,
5734        RosterError, SessionBridge, SessionSnapshot,
5735    };
5736    use crate::{
5737        DiscoverySpec, IdentityFirstContext, MobBootstrapOptions, MobBootstrapSpec, MobKitConfig,
5738        UnifiedRuntime,
5739    };
5740    use async_trait::async_trait;
5741    use meerkat::{AgentFactory, Config, build_ephemeral_service};
5742    use meerkat_client::TestClient;
5743    use meerkat_mob::{MobDefinition, MobStorage, SpawnMemberSpec};
5744    use serde_json::{Value, json};
5745    use std::collections::BTreeMap;
5746    use std::sync::Arc;
5747    use std::time::Duration;
5748
5749    #[derive(Debug, Default)]
5750    struct EmptyRosterProvider;
5751
5752    #[async_trait]
5753    impl RosterProvider for EmptyRosterProvider {
5754        async fn roster(
5755            &self,
5756            _context: &RosterContext,
5757        ) -> Result<Vec<DurableAgentSpec>, RosterError> {
5758            Ok(Vec::new())
5759        }
5760    }
5761
5762    #[derive(Debug, Default)]
5763    struct ContextRequiredEmptyRosterProvider {
5764        missing_definition_calls: std::sync::atomic::AtomicUsize,
5765    }
5766
5767    impl ContextRequiredEmptyRosterProvider {
5768        fn missing_definition_calls(&self) -> usize {
5769            self.missing_definition_calls
5770                .load(std::sync::atomic::Ordering::SeqCst)
5771        }
5772    }
5773
5774    #[async_trait]
5775    impl RosterProvider for ContextRequiredEmptyRosterProvider {
5776        async fn roster(
5777            &self,
5778            context: &RosterContext,
5779        ) -> Result<Vec<DurableAgentSpec>, RosterError> {
5780            if context.mob_definition.is_none() {
5781                self.missing_definition_calls
5782                    .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5783                return Err(RosterError::ProviderUnavailable(
5784                    "mob definition required".to_string(),
5785                ));
5786            }
5787            Ok(Vec::new())
5788        }
5789    }
5790
5791    #[derive(Debug)]
5792    struct ContextRequiredStaticRosterProvider {
5793        specs: Vec<DurableAgentSpec>,
5794        missing_definition_calls: std::sync::atomic::AtomicUsize,
5795    }
5796
5797    impl ContextRequiredStaticRosterProvider {
5798        fn new(specs: Vec<DurableAgentSpec>) -> Self {
5799            Self {
5800                specs,
5801                missing_definition_calls: std::sync::atomic::AtomicUsize::new(0),
5802            }
5803        }
5804
5805        fn missing_definition_calls(&self) -> usize {
5806            self.missing_definition_calls
5807                .load(std::sync::atomic::Ordering::SeqCst)
5808        }
5809    }
5810
5811    #[async_trait]
5812    impl RosterProvider for ContextRequiredStaticRosterProvider {
5813        async fn roster(
5814            &self,
5815            context: &RosterContext,
5816        ) -> Result<Vec<DurableAgentSpec>, RosterError> {
5817            if context.mob_definition.is_none() {
5818                self.missing_definition_calls
5819                    .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5820                return Err(RosterError::ProviderUnavailable(
5821                    "mob definition required".to_string(),
5822                ));
5823            }
5824            Ok(self.specs.clone())
5825        }
5826    }
5827
5828    #[derive(Debug, Default)]
5829    struct RpcResetTestBridge {
5830        create_calls: std::sync::atomic::AtomicUsize,
5831        last_create_spec: tokio::sync::Mutex<Option<DurableAgentSpec>>,
5832    }
5833
5834    impl RpcResetTestBridge {
5835        async fn last_create_spec(&self) -> Option<DurableAgentSpec> {
5836            self.last_create_spec.lock().await.clone()
5837        }
5838    }
5839
5840    #[async_trait]
5841    impl SessionBridge for RpcResetTestBridge {
5842        async fn create_session(
5843            &self,
5844            _identity: &AgentIdentity,
5845            _runtime_id: &AgentRuntimeId,
5846            spec: &DurableAgentSpec,
5847            _draft: &AgentBuildDraft,
5848            session_id: &meerkat_core::types::SessionId,
5849        ) -> Result<meerkat_core::types::SessionId, BridgeError> {
5850            self.create_calls
5851                .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
5852            *self.last_create_spec.lock().await = Some(spec.clone());
5853            Ok(session_id.clone())
5854        }
5855
5856        async fn resume_session(
5857            &self,
5858            _identity: &AgentIdentity,
5859            _runtime_id: &AgentRuntimeId,
5860            _spec: &DurableAgentSpec,
5861            _draft: &AgentBuildDraft,
5862            session_id: &meerkat_core::types::SessionId,
5863            _snapshot: &SessionSnapshot,
5864        ) -> Result<ResumeSessionOutcome, BridgeError> {
5865            Ok(ResumeSessionOutcome::Resumed {
5866                session_id: session_id.clone(),
5867            })
5868        }
5869
5870        async fn deliver_admitted(
5871            &self,
5872            _runtime_id: &AgentRuntimeId,
5873            _delivery: crate::identity_first::BridgeDelivery,
5874        ) -> Result<meerkat_core::types::SessionId, BridgeError> {
5875            Ok(meerkat_core::types::SessionId::new())
5876        }
5877
5878        async fn checkpoint_session(
5879            &self,
5880            _runtime_id: &AgentRuntimeId,
5881            _session_id: &meerkat_core::types::SessionId,
5882        ) -> Result<SessionSnapshot, BridgeError> {
5883            Ok(SessionSnapshot { data: Vec::new() })
5884        }
5885
5886        async fn retire_member(&self, _runtime_id: &AgentRuntimeId) -> Result<(), BridgeError> {
5887            Ok(())
5888        }
5889    }
5890
5891    struct ReadOnlyAgentMemoryProvider;
5892
5893    #[async_trait]
5894    impl crate::identity_first::AgentMemoryProvider for ReadOnlyAgentMemoryProvider {
5895        async fn recall(
5896            &self,
5897            _request: crate::identity_first::AgentMemoryRecallRequest,
5898        ) -> Result<
5899            Vec<crate::identity_first::AgentMemoryRecord>,
5900            crate::identity_first::AgentMemoryError,
5901        > {
5902            Ok(Vec::new())
5903        }
5904    }
5905
5906    fn rpc_test_mob_spec(
5907        temp_dir: &tempfile::TempDir,
5908    ) -> Result<MobBootstrapSpec, Box<dyn std::error::Error + Send + Sync>> {
5909        let session_path = temp_dir.path().join("sessions");
5910        std::fs::create_dir_all(&session_path)?;
5911        let factory = AgentFactory::new(&session_path).comms(true);
5912        let session_service = Arc::new(build_ephemeral_service(factory, Config::default(), 16));
5913        let definition = MobDefinition::from_toml(
5914            r#"
5915[mob]
5916id = "rpc-identity-alias-test"
5917
5918[profiles.worker]
5919model = "gpt-5.5"
5920external_addressable = true
5921
5922[profiles.worker.tools]
5923comms = true
5924"#,
5925        )?;
5926        Ok(
5927            MobBootstrapSpec::new(definition, MobStorage::in_memory(), session_service)
5928                .with_options(MobBootstrapOptions {
5929                    allow_ephemeral_sessions: true,
5930                    notify_orchestrator_on_resume: true,
5931                    default_llm_client: Some(Arc::new(TestClient::default())),
5932                }),
5933        )
5934    }
5935
5936    /// Test-only identity bridge seam. Public raw creation deliberately
5937    /// rejects generated aliases and runtime-owned `agent_identity` labels;
5938    /// fixtures exercising already-projected identity members must model the
5939    /// trusted lower plane that the identity runtime itself uses.
5940    async fn spawn_identity_projection_fixture(
5941        runtime: &UnifiedRuntime,
5942        mut spec: SpawnMemberSpec,
5943    ) -> Result<(), meerkat_mob::MobError> {
5944        let runtime_alias = spec.identity.to_string();
5945        spec.identity = crate::member_comms_id::mob_member_id(&runtime_alias);
5946        Box::pin(runtime.mob_handle().spawn_spec(spec)).await?;
5947        Ok(())
5948    }
5949
5950    fn rpc_reprofile_mob_spec(
5951        temp_dir: &tempfile::TempDir,
5952    ) -> Result<MobBootstrapSpec, Box<dyn std::error::Error + Send + Sync>> {
5953        let session_path = temp_dir.path().join("sessions");
5954        std::fs::create_dir_all(&session_path)?;
5955        let factory = AgentFactory::new(&session_path).comms(true);
5956        let session_service = Arc::new(build_ephemeral_service(factory, Config::default(), 16));
5957        let definition = MobDefinition::from_toml(
5958            r#"
5959[mob]
5960id = "rpc-reset-reprofile-test"
5961
5962[profiles.domain]
5963model = "gpt-5.5"
5964external_addressable = true
5965
5966[profiles.domain.tools]
5967comms = true
5968
5969[profiles.security]
5970model = "gpt-5.5"
5971external_addressable = true
5972
5973[profiles.security.tools]
5974comms = true
5975shell = true
5976"#,
5977        )?;
5978        Ok(
5979            MobBootstrapSpec::new(definition, MobStorage::in_memory(), session_service)
5980                .with_options(MobBootstrapOptions {
5981                    allow_ephemeral_sessions: true,
5982                    notify_orchestrator_on_resume: true,
5983                    default_llm_client: Some(Arc::new(TestClient::default())),
5984                }),
5985        )
5986    }
5987
5988    fn rpc_durable_spec(identity: &str, profile: &str) -> DurableAgentSpec {
5989        DurableAgentSpec {
5990            identity: AgentIdentity::parse(identity).expect("valid identity"),
5991            profile: meerkat_mob::ProfileName::from(profile),
5992            addressability: AgentAddressability::Addressable,
5993            display_name: None,
5994            labels: BTreeMap::new(),
5995            context: None,
5996            additional_instructions: Vec::new(),
5997            initial_message: None,
5998            runtime_mode_override: None,
5999            backend: None,
6000            binding: None,
6001        }
6002    }
6003
6004    /// `_system` is the reserved runtime-plane console identity: the
6005    /// aggregator exempts it from the roster-visibility gate and identity
6006    /// namespacing (memory.* sink attribution), so a member spawned under
6007    /// that name would emit frames indistinguishable from runtime events
6008    /// and bypass the per-member hidden gate. The spawn surface must
6009    /// reject the name outright.
6010    #[tokio::test]
6011    async fn unified_rpc_spawn_rejects_reserved_system_identity()
6012    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6013        let temp_dir = tempfile::tempdir()?;
6014        let runtime = Box::pin(
6015            UnifiedRuntime::builder()
6016                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6017                .module_config(MobKitConfig {
6018                    modules: Vec::new(),
6019                    discovery: DiscoverySpec {
6020                        namespace: "rpc-reserved-identity-test".to_string(),
6021                        modules: Vec::new(),
6022                    },
6023                    pre_spawn: Vec::new(),
6024                })
6025                .timeout(Duration::from_secs(1))
6026                .build(),
6027        )
6028        .await?;
6029
6030        let response: Value = serde_json::from_str(
6031            &handle_unified_rpc_json(
6032                &runtime,
6033                &json!({
6034                    "jsonrpc": "2.0",
6035                    "id": 1,
6036                    "method": "mobkit/spawn_member",
6037                    "params": {
6038                        "profile": "worker",
6039                        "meerkat_id": crate::console_contracts::SYSTEM_EVENT_IDENTITY,
6040                    },
6041                })
6042                .to_string(),
6043                Duration::from_secs(1),
6044                None,
6045                None,
6046            )
6047            .await,
6048        )?;
6049
6050        assert_eq!(response["error"]["code"], json!(-32602), "{response:#?}");
6051        assert!(
6052            response["error"]["message"]
6053                .as_str()
6054                .is_some_and(|message| message.contains("reserved")),
6055            "rejection must name the reservation: {response:#?}"
6056        );
6057        let _ = runtime.mob_handle().stop().await;
6058        Ok(())
6059    }
6060
6061    #[tokio::test]
6062    async fn unified_rpc_spawn_member_rejects_generated_runtime_alias_namespace()
6063    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6064        let temp_dir = tempfile::tempdir()?;
6065        let runtime = Box::pin(
6066            UnifiedRuntime::builder()
6067                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6068                .module_config(MobKitConfig {
6069                    modules: Vec::new(),
6070                    discovery: DiscoverySpec {
6071                        namespace: "rpc-generated-alias-reservation-test".to_string(),
6072                        modules: Vec::new(),
6073                    },
6074                    pre_spawn: Vec::new(),
6075                })
6076                .timeout(Duration::from_secs(1))
6077                .build(),
6078        )
6079        .await?;
6080
6081        let response: Value = serde_json::from_str(
6082            &handle_unified_rpc_json(
6083                &runtime,
6084                &json!({
6085                    "jsonrpc": "2.0",
6086                    "id": 1,
6087                    "method": "mobkit/spawn_member",
6088                    "params": {
6089                        "profile": "worker",
6090                        "meerkat_id": "rt:user:forged:0",
6091                    },
6092                })
6093                .to_string(),
6094                Duration::from_secs(1),
6095                None,
6096                None,
6097            )
6098            .await,
6099        )?;
6100
6101        assert_eq!(response["error"]["code"], json!(-32602), "{response:#?}");
6102        assert!(
6103            response["error"]["message"]
6104                .as_str()
6105                .is_some_and(|message| message.contains("reserved")),
6106            "generated aliases must be rejected at admission: {response:#?}"
6107        );
6108        assert!(
6109            runtime.mob_handle().list_members().await.is_empty(),
6110            "reserved namespace rejection must happen before raw member spawn"
6111        );
6112        let _ = runtime.mob_handle().stop().await;
6113        Ok(())
6114    }
6115
6116    #[tokio::test]
6117    async fn unified_rpc_spawn_holds_explicit_identity_context_alias_reservation()
6118    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6119        let temp_dir = tempfile::tempdir()?;
6120        let runtime = Box::pin(
6121            UnifiedRuntime::builder()
6122                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6123                .module_config(MobKitConfig {
6124                    modules: Vec::new(),
6125                    discovery: DiscoverySpec {
6126                        namespace: "rpc-explicit-identity-context-reservation-test".to_string(),
6127                        modules: Vec::new(),
6128                    },
6129                    pre_spawn: Vec::new(),
6130                })
6131                .timeout(Duration::from_secs(1))
6132                .build(),
6133        )
6134        .await?;
6135        assert!(runtime.identity_runtime().is_none());
6136
6137        let identity_runtime = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
6138            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6139            lease_provider: Arc::new(LocalLeaseProvider::new()),
6140            runtime_instance_id: "rpc-explicit-identity-context-reservation-test".to_string(),
6141            has_runtime_store: true,
6142            durability_policy: DurabilityPolicy::SyncWriteThrough,
6143            bridge: None,
6144            default_timeout: None,
6145        }));
6146        let identity_ctx = IdentityFirstContext {
6147            runtime: identity_runtime.clone(),
6148            roster_provider: Arc::new(EmptyRosterProvider),
6149            topology_provider: None,
6150            customizer: None,
6151            agent_memory_provider: None,
6152            mob_definition: Some(runtime.mob_handle().definition().clone()),
6153        };
6154        let held_lock = identity_runtime
6155            .raw_member_alias_lock("compat-worker")
6156            .await
6157            .lock_owned()
6158            .await;
6159        let request = json!({
6160            "jsonrpc": "2.0",
6161            "id": 1,
6162            "method": "mobkit/spawn_member",
6163            "params": {
6164                "profile": "worker",
6165                "meerkat_id": " compat-worker ",
6166            },
6167        })
6168        .to_string();
6169
6170        assert!(
6171            tokio::time::timeout(
6172                Duration::from_millis(25),
6173                handle_unified_rpc_json(
6174                    &runtime,
6175                    &request,
6176                    Duration::from_secs(1),
6177                    None,
6178                    Some(&identity_ctx),
6179                ),
6180            )
6181            .await
6182            .is_err(),
6183            "explicit identity authority must block the raw spawn on its canonical alias lock"
6184        );
6185        drop(held_lock);
6186
6187        let response: Value = serde_json::from_str(
6188            &tokio::time::timeout(
6189                Duration::from_secs(2),
6190                handle_unified_rpc_json(
6191                    &runtime,
6192                    &request,
6193                    Duration::from_secs(1),
6194                    None,
6195                    Some(&identity_ctx),
6196                ),
6197            )
6198            .await?,
6199        )?;
6200        assert!(response["error"].is_null(), "{response:#?}");
6201        assert_eq!(response["result"]["meerkat_id"], json!("compat-worker"));
6202
6203        let _ = runtime.mob_handle().stop().await;
6204        Ok(())
6205    }
6206
6207    #[tokio::test]
6208    async fn unified_rpc_rejects_encoded_roster_ingress_and_authoritative_identity_labels()
6209    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6210        let temp_dir = tempfile::tempdir()?;
6211        let runtime = Box::pin(
6212            UnifiedRuntime::builder()
6213                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6214                .module_config(MobKitConfig {
6215                    modules: Vec::new(),
6216                    discovery: DiscoverySpec {
6217                        namespace: "rpc-public-alias-ingress-test".to_string(),
6218                        modules: Vec::new(),
6219                    },
6220                    pre_spawn: Vec::new(),
6221                })
6222                .timeout(Duration::from_secs(1))
6223                .build(),
6224        )
6225        .await?;
6226
6227        let encoded = crate::member_comms_id::mob_member_id_str("rt:secret:0");
6228        let encoded_response: Value = serde_json::from_str(
6229            &handle_unified_rpc_json(
6230                &runtime,
6231                &json!({
6232                    "jsonrpc": "2.0",
6233                    "id": 1,
6234                    "method": "mobkit/get_member",
6235                    "params": { "member_id": encoded.as_ref() },
6236                })
6237                .to_string(),
6238                Duration::from_secs(1),
6239                None,
6240                None,
6241            )
6242            .await,
6243        )?;
6244        assert_eq!(
6245            encoded_response["error"]["code"],
6246            json!(-32602),
6247            "encoded roster spelling must be rejected before resolution: {encoded_response:#?}"
6248        );
6249
6250        let forged_label_response: Value = serde_json::from_str(
6251            &handle_unified_rpc_json(
6252                &runtime,
6253                &json!({
6254                    "jsonrpc": "2.0",
6255                    "id": 2,
6256                    "method": "mobkit/ensure_member",
6257                    "params": {
6258                        "role": "worker",
6259                        "agent_identity": "raw-worker",
6260                        "labels": { "agent_identity": "secret" },
6261                    },
6262                })
6263                .to_string(),
6264                Duration::from_secs(1),
6265                None,
6266                None,
6267            )
6268            .await,
6269        )?;
6270        assert_eq!(
6271            forged_label_response["error"]["code"],
6272            json!(-32602),
6273            "raw creation must not mint runtime-authoritative identity labels: {forged_label_response:#?}"
6274        );
6275        assert!(runtime.mob_handle().list_members().await.is_empty());
6276
6277        let _ = runtime.mob_handle().stop().await;
6278        Ok(())
6279    }
6280
6281    #[tokio::test]
6282    async fn unified_capabilities_separate_mobpack_authoring_from_runtime_controls()
6283    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6284        let temp_dir = tempfile::tempdir()?;
6285        let runtime = Box::pin(
6286            UnifiedRuntime::builder()
6287                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6288                .module_config(MobKitConfig {
6289                    modules: Vec::new(),
6290                    discovery: DiscoverySpec {
6291                        namespace: "rpc-authoring-capabilities-test".to_string(),
6292                        modules: Vec::new(),
6293                    },
6294                    pre_spawn: Vec::new(),
6295                })
6296                .timeout(Duration::from_secs(1))
6297                .build(),
6298        )
6299        .await?;
6300
6301        let response: Value = serde_json::from_str(
6302            &handle_unified_rpc_json(
6303                &runtime,
6304                &json!({
6305                    "jsonrpc": "2.0",
6306                    "id": 1,
6307                    "method": "mobkit/capabilities",
6308                })
6309                .to_string(),
6310                Duration::from_secs(1),
6311                None,
6312                None,
6313            )
6314            .await,
6315        )?;
6316
6317        assert!(response["error"].is_null(), "{response:#?}");
6318        let methods = response["result"]["methods"]
6319            .as_array()
6320            .expect("methods array")
6321            .iter()
6322            .filter_map(Value::as_str)
6323            .collect::<Vec<_>>();
6324        for method in super::MOBPACK_AUTHORING_METHODS {
6325            assert!(
6326                methods.contains(method),
6327                "missing authoring method {method}"
6328            );
6329        }
6330        assert_eq!(
6331            response["result"]["authoring_capabilities"]["domain"],
6332            json!("mobpack_authoring")
6333        );
6334        assert_eq!(
6335            response["result"]["authoring_capabilities"]["runtime_mutation"],
6336            json!(false)
6337        );
6338        assert_eq!(
6339            response["result"]["authoring_capabilities"]["host_mutation_methods"]["mobkit/mobpacks/deploy"],
6340            json!("when execute=true, writes a mobpack archive and runs rkat mob run on the host")
6341        );
6342        assert_eq!(
6343            response["result"]["authoring_capabilities"]["methods"]
6344                .as_array()
6345                .expect("authoring methods")
6346                .iter()
6347                .filter_map(Value::as_str)
6348                .collect::<Vec<_>>(),
6349            super::MOBPACK_AUTHORING_METHODS
6350        );
6351        assert_eq!(
6352            response["result"]["authoring_capabilities"]["deploy_command"],
6353            json!("rkat mob run")
6354        );
6355
6356        Ok(())
6357    }
6358
6359    #[tokio::test]
6360    async fn unified_rpc_dispatches_mobpack_authoring_methods()
6361    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6362        let temp_dir = tempfile::tempdir()?;
6363        let runtime = Box::pin(
6364            UnifiedRuntime::builder()
6365                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6366                .module_config(MobKitConfig {
6367                    modules: Vec::new(),
6368                    discovery: DiscoverySpec {
6369                        namespace: "rpc-authoring-dispatch-test".to_string(),
6370                        modules: Vec::new(),
6371                    },
6372                    pre_spawn: Vec::new(),
6373                })
6374                .timeout(Duration::from_secs(1))
6375                .build(),
6376        )
6377        .await?;
6378
6379        let response: Value = serde_json::from_str(
6380            &handle_unified_rpc_json(
6381                &runtime,
6382                &json!({
6383                    "jsonrpc": "2.0",
6384                    "id": 1,
6385                    "method": "mobkit/mobpacks/schema",
6386                })
6387                .to_string(),
6388                Duration::from_secs(1),
6389                None,
6390                None,
6391            )
6392            .await,
6393        )?;
6394
6395        assert!(response["error"].is_null(), "{response:#?}");
6396        assert_eq!(
6397            response["result"]["media_type"],
6398            json!("application/vnd.meerkat.mobpack")
6399        );
6400        assert_eq!(
6401            response["result"]["commands"]["deploy_rpc"],
6402            json!("mobkit/mobpacks/deploy")
6403        );
6404        assert_eq!(
6405            response["result"]["deploy_settings"]["runtime_backed"],
6406            json!(true)
6407        );
6408        assert_eq!(
6409            response["result"]["deploy_settings"]["authoring_provider"]["runtime_binding"],
6410            json!("bound")
6411        );
6412        assert_eq!(
6413            response["result"]["deploy_settings"]["provenance"]["source"],
6414            json!("UnifiedRuntime.authoring_provider.deploy_target")
6415        );
6416        assert!(response["result"]["sample_mobpacks"].is_null());
6417        assert!(response["result"]["agent_definitions"].is_null());
6418
6419        let catalogs: Value = serde_json::from_str(
6420            &handle_unified_rpc_json(
6421                &runtime,
6422                &json!({
6423                    "jsonrpc": "2.0",
6424                    "id": 2,
6425                    "method": "mobkit/mobpacks/catalogs",
6426                })
6427                .to_string(),
6428                Duration::from_secs(1),
6429                None,
6430                None,
6431            )
6432            .await,
6433        )?;
6434        assert!(catalogs["error"].is_null(), "{catalogs:#?}");
6435        assert_eq!(catalogs["result"]["runtime_backed"], json!(true));
6436        assert_eq!(
6437            catalogs["result"]["authoring_provider"]["id"],
6438            json!("unified_runtime")
6439        );
6440        assert_eq!(
6441            catalogs["result"]["authoring_provider"]["runtime_binding"],
6442            json!("bound")
6443        );
6444        assert_eq!(
6445            catalogs["result"]["sources"]["runtime"],
6446            json!("unified_runtime")
6447        );
6448        assert_eq!(
6449            catalogs["result"]["sources"]["runtime_binding"],
6450            json!("bound")
6451        );
6452        assert!(
6453            catalogs["result"]["runtime_unavailable_reason"].is_null(),
6454            "{catalogs:#?}"
6455        );
6456        assert_eq!(
6457            catalogs["result"]["catalog_snapshot"]["runtime_backed"],
6458            json!(true)
6459        );
6460        assert_eq!(
6461            catalogs["result"]["authoring_provider"]["deploy_target"]["command"],
6462            json!("rkat mob run")
6463        );
6464        assert!(
6465            catalogs["result"]["authoring_provider"]["runtime_methods"]
6466                .as_array()
6467                .is_some_and(|methods| methods.contains(&json!("mobkit/mobpacks/deploy"))),
6468            "{catalogs:#?}"
6469        );
6470
6471        Ok(())
6472    }
6473
6474    #[tokio::test]
6475    async fn reconcile_identity_passes_mob_definition_to_roster_provider()
6476    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6477        let temp_dir = tempfile::tempdir()?;
6478        let runtime = Box::pin(
6479            UnifiedRuntime::builder()
6480                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6481                .module_config(MobKitConfig {
6482                    modules: Vec::new(),
6483                    discovery: DiscoverySpec {
6484                        namespace: "rpc-reconcile-roster-context-test".to_string(),
6485                        modules: Vec::new(),
6486                    },
6487                    pre_spawn: Vec::new(),
6488                })
6489                .timeout(Duration::from_secs(1))
6490                .build(),
6491        )
6492        .await?;
6493        let identity_rt = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
6494            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6495            lease_provider: Arc::new(LocalLeaseProvider::new()),
6496            runtime_instance_id: "rpc-reconcile-roster-context-test".to_string(),
6497            has_runtime_store: true,
6498            durability_policy: DurabilityPolicy::SyncWriteThrough,
6499            bridge: None,
6500            default_timeout: None,
6501        }));
6502        let roster_provider = Arc::new(ContextRequiredEmptyRosterProvider::default());
6503        let identity_ctx = IdentityFirstContext {
6504            runtime: identity_rt,
6505            roster_provider: roster_provider.clone(),
6506            topology_provider: None,
6507            customizer: None,
6508            agent_memory_provider: None,
6509            mob_definition: Some(runtime.mob_handle().definition().clone()),
6510        };
6511
6512        let response: Value = serde_json::from_str(
6513            &handle_unified_rpc_json(
6514                &runtime,
6515                &json!({
6516                    "jsonrpc": "2.0",
6517                    "id": 1,
6518                    "method": "mobkit/reconcile_identity",
6519                    "params": {},
6520                })
6521                .to_string(),
6522                Duration::from_secs(1),
6523                None,
6524                Some(&identity_ctx),
6525            )
6526            .await,
6527        )?;
6528
6529        assert!(response["error"].is_null(), "{response:#?}");
6530        assert_eq!(
6531            roster_provider.missing_definition_calls(),
6532            0,
6533            "reconcile_identity must preserve mob_definition in roster context"
6534        );
6535        Ok(())
6536    }
6537
6538    #[tokio::test]
6539    async fn startup_ready_wait_retries_stale_member_success_and_error_after_generation_change()
6540    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6541        for stale_result in [
6542            IdentityMemberReadiness::Ready,
6543            IdentityMemberReadiness::Failed("stale member failure".to_string()),
6544        ] {
6545            let identity_runtime = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
6546                continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6547                lease_provider: Arc::new(LocalLeaseProvider::new()),
6548                runtime_instance_id: "rpc-startup-ready-generation-test".to_string(),
6549                has_runtime_store: true,
6550                durability_policy: DurabilityPolicy::SyncWriteThrough,
6551                bridge: None,
6552                default_timeout: None,
6553            }));
6554            let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
6555            let callback_runtime = identity_runtime.clone();
6556            let callback_calls = calls.clone();
6557            let wait = wait_identity_startup_ready(
6558                &identity_runtime,
6559                Duration::from_secs(1),
6560                move |_member_ids, _remaining| {
6561                    let call = callback_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
6562                    let callback_runtime = callback_runtime.clone();
6563                    let stale_result = stale_result.clone();
6564                    async move {
6565                        if call == 0 {
6566                            // Supersede the pass in the same poll that returns
6567                            // the old member result. Whichever `select!` branch
6568                            // wins, the generation check must discard it.
6569                            callback_runtime.test_supersede_identity_bootstrap_ready();
6570                            stale_result
6571                        } else {
6572                            IdentityMemberReadiness::Ready
6573                        }
6574                    }
6575                },
6576            )
6577            .await?;
6578
6579            assert!(wait.startup_ready);
6580            assert!(!wait.timed_out);
6581            assert!(wait.status.ready);
6582            assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 2);
6583        }
6584        Ok(())
6585    }
6586
6587    #[tokio::test]
6588    async fn startup_ready_wait_reports_timeout_and_terminal_broken_status()
6589    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6590        let timeout_runtime = IdentityRuntime::new(IdentityRuntimeConfig {
6591            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6592            lease_provider: Arc::new(LocalLeaseProvider::new()),
6593            runtime_instance_id: "rpc-startup-ready-timeout-test".to_string(),
6594            has_runtime_store: true,
6595            durability_policy: DurabilityPolicy::SyncWriteThrough,
6596            bridge: None,
6597            default_timeout: None,
6598        });
6599        let timed_out = wait_identity_startup_ready(
6600            &timeout_runtime,
6601            Duration::from_secs(1),
6602            |_member_ids, _remaining| async { IdentityMemberReadiness::TimedOut },
6603        )
6604        .await?;
6605        assert!(timed_out.timed_out);
6606        assert!(!timed_out.startup_ready);
6607
6608        let broken_runtime = IdentityRuntime::new(IdentityRuntimeConfig {
6609            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6610            lease_provider: Arc::new(LocalLeaseProvider::new()),
6611            runtime_instance_id: "rpc-startup-ready-broken-test".to_string(),
6612            has_runtime_store: true,
6613            durability_policy: DurabilityPolicy::SyncWriteThrough,
6614            bridge: None,
6615            default_timeout: None,
6616        });
6617        broken_runtime.test_fail_identity_bootstrap("injected terminal failure");
6618        let readiness_calls = std::sync::atomic::AtomicUsize::new(0);
6619        let broken = wait_identity_startup_ready(
6620            &broken_runtime,
6621            Duration::from_secs(1),
6622            |_member_ids, _remaining| {
6623                readiness_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
6624                async { IdentityMemberReadiness::Ready }
6625            },
6626        )
6627        .await?;
6628        assert!(!broken.timed_out);
6629        assert!(!broken.startup_ready);
6630        assert!(!broken.status.ready);
6631        assert_eq!(broken.status.counts.broken, 1);
6632        assert!(
6633            broken
6634                .status
6635                .error
6636                .as_deref()
6637                .is_some_and(|error| error.contains("injected terminal failure"))
6638        );
6639        assert_eq!(
6640            readiness_calls.load(std::sync::atomic::Ordering::SeqCst),
6641            0,
6642            "terminal bootstrap failure must not enter member readiness"
6643        );
6644        Ok(())
6645    }
6646
6647    #[tokio::test]
6648    async fn identity_bootstrap_status_and_wait_rpc_are_typed_and_advertised()
6649    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6650        let temp_dir = tempfile::tempdir()?;
6651        let runtime = Box::pin(
6652            UnifiedRuntime::builder()
6653                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6654                .module_config(MobKitConfig {
6655                    modules: Vec::new(),
6656                    discovery: DiscoverySpec {
6657                        namespace: "rpc-identity-bootstrap-status-test".to_string(),
6658                        modules: Vec::new(),
6659                    },
6660                    pre_spawn: Vec::new(),
6661                })
6662                .timeout(Duration::from_secs(1))
6663                .build(),
6664        )
6665        .await?;
6666        let identity_ctx = IdentityFirstContext {
6667            runtime: Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
6668                continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6669                lease_provider: Arc::new(LocalLeaseProvider::new()),
6670                runtime_instance_id: "rpc-identity-bootstrap-status-test".to_string(),
6671                has_runtime_store: true,
6672                durability_policy: DurabilityPolicy::SyncWriteThrough,
6673                bridge: None,
6674                default_timeout: None,
6675            })),
6676            roster_provider: Arc::new(ContextRequiredEmptyRosterProvider::default()),
6677            topology_provider: None,
6678            customizer: None,
6679            agent_memory_provider: None,
6680            mob_definition: Some(runtime.mob_handle().definition().clone()),
6681        };
6682
6683        let runtime_ref = &runtime;
6684        let identity_ctx_ref = &identity_ctx;
6685        let call = move |method: &'static str, params: Value| {
6686            let request = serde_json::json!({
6687                "jsonrpc": "2.0",
6688                "id": method,
6689                "method": method,
6690                "params": params,
6691            })
6692            .to_string();
6693            async move {
6694                handle_unified_rpc_json(
6695                    runtime_ref,
6696                    &request,
6697                    Duration::from_secs(1),
6698                    None,
6699                    Some(identity_ctx_ref),
6700                )
6701                .await
6702            }
6703        };
6704
6705        let status: Value = serde_json::from_str(
6706            &call("mobkit/status_identity_bootstrap", serde_json::json!({})).await,
6707        )?;
6708        assert_eq!(status["result"]["mode"]["mode"], json!("eager_materialize"));
6709        assert_eq!(status["result"]["complete"], json!(true));
6710        assert_eq!(status["result"]["ready"], json!(true));
6711
6712        let waited: Value = serde_json::from_str(
6713            &call(
6714                "mobkit/wait_identity_bootstrap",
6715                serde_json::json!({"target": "materialized", "timeout_ms": 0}),
6716            )
6717            .await,
6718        )?;
6719        assert_eq!(waited["result"]["timed_out"], json!(false));
6720        assert_eq!(waited["result"]["target"], json!("materialized"));
6721
6722        let default_waited: Value = serde_json::from_str(
6723            &call("mobkit/wait_identity_bootstrap", serde_json::json!({})).await,
6724        )?;
6725        assert_eq!(default_waited["result"]["target"], json!("materialized"));
6726
6727        let startup_ready: Value = serde_json::from_str(
6728            &call(
6729                "mobkit/wait_identity_bootstrap",
6730                serde_json::json!({"target": "startup_ready", "timeout_ms": 0}),
6731            )
6732            .await,
6733        )?;
6734        assert_eq!(startup_ready["result"]["timed_out"], json!(false));
6735        assert_eq!(startup_ready["result"]["startup_ready"], json!(true));
6736
6737        for (params, expected_message) in [
6738            (serde_json::json!(null), "params must be an object"),
6739            (serde_json::json!([]), "params must be an object"),
6740            (
6741                serde_json::json!({"target": null}),
6742                "target must be a string",
6743            ),
6744            (serde_json::json!({"target": 42}), "target must be a string"),
6745            (
6746                serde_json::json!({"target": "not_ready"}),
6747                "target must be 'materialized' or 'startup_ready'",
6748            ),
6749            (
6750                serde_json::json!({"timeout_ms": null}),
6751                "timeout_ms must be a non-negative integer",
6752            ),
6753            (
6754                serde_json::json!({"timeout_ms": -1}),
6755                "timeout_ms must be a non-negative integer",
6756            ),
6757            (
6758                serde_json::json!({"timeout_ms": 1.5}),
6759                "timeout_ms must be a non-negative integer",
6760            ),
6761            (
6762                serde_json::json!({"timeout_ms": true}),
6763                "timeout_ms must be a non-negative integer",
6764            ),
6765            (
6766                serde_json::json!({"unexpected": true}),
6767                "unsupported parameter: unexpected",
6768            ),
6769        ] {
6770            let response: Value =
6771                serde_json::from_str(&call("mobkit/wait_identity_bootstrap", params).await)?;
6772            assert_eq!(response["error"]["code"], json!(-32602));
6773            assert_eq!(response["error"]["message"], json!(expected_message));
6774        }
6775
6776        let capabilities: Value =
6777            serde_json::from_str(&call("mobkit/capabilities", serde_json::json!({})).await)?;
6778        let methods = capabilities["result"]["methods"]
6779            .as_array()
6780            .expect("methods array");
6781        assert!(methods.contains(&json!("mobkit/status_identity_bootstrap")));
6782        assert!(methods.contains(&json!("mobkit/wait_identity_bootstrap")));
6783        Ok(())
6784    }
6785
6786    #[tokio::test]
6787    async fn reset_identity_preserves_mob_definition_for_roster_reprofile()
6788    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6789        let temp_dir = tempfile::tempdir()?;
6790        let runtime = Box::pin(
6791            UnifiedRuntime::builder()
6792                .mob_spec(rpc_reprofile_mob_spec(&temp_dir)?)
6793                .module_config(MobKitConfig {
6794                    modules: Vec::new(),
6795                    discovery: DiscoverySpec {
6796                        namespace: "rpc-reset-roster-context-test".to_string(),
6797                        modules: Vec::new(),
6798                    },
6799                    pre_spawn: Vec::new(),
6800                })
6801                .timeout(Duration::from_secs(1))
6802                .build(),
6803        )
6804        .await?;
6805
6806        let continuity_store = Arc::new(LocalContinuityStore::in_memory()?);
6807        let lease_provider = Arc::new(LocalLeaseProvider::new());
6808        let bridge = Arc::new(RpcResetTestBridge::default());
6809        let identity_rt = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
6810            continuity_store: continuity_store.clone(),
6811            lease_provider: lease_provider.clone(),
6812            runtime_instance_id: "rpc-reset-roster-context-test".to_string(),
6813            has_runtime_store: true,
6814            durability_policy: DurabilityPolicy::SyncWriteThrough,
6815            bridge: Some(bridge.clone()),
6816            default_timeout: None,
6817        }));
6818
6819        let identity = AgentIdentity::parse("domain:security")?;
6820        let initial_grants = lease_provider
6821            .acquire_leases(
6822                std::slice::from_ref(&identity),
6823                "rpc-reset-roster-context-test",
6824            )
6825            .await?;
6826        let initial_grant = match initial_grants.get(&identity) {
6827            Some(LeaseAcquireResult::Acquired(grant)) => grant.clone(),
6828            other => return Err(format!("expected acquired lease, got {other:?}").into()),
6829        };
6830        let initial_record = ContinuityRecord {
6831            identity: identity.clone(),
6832            agent_runtime_id: AgentRuntimeId::parse("rt:domain:security:0")?,
6833            session_id: meerkat_core::types::SessionId::new(),
6834            generation: ContinuityGeneration::new(0),
6835            checkpoint_version: CheckpointVersion::new(0),
6836        };
6837        continuity_store
6838            .upsert_continuity_record(&initial_record, initial_grant.fencing_token)
6839            .await?;
6840        identity_rt
6841            .register(
6842                rpc_durable_spec(identity.as_str(), "domain"),
6843                IdentityLifecycleState::Active,
6844                Some(initial_record),
6845                Some(initial_grant),
6846            )
6847            .await;
6848
6849        let roster_provider = Arc::new(ContextRequiredStaticRosterProvider::new(vec![
6850            rpc_durable_spec(identity.as_str(), "security"),
6851        ]));
6852        let identity_ctx = IdentityFirstContext {
6853            runtime: identity_rt.clone(),
6854            roster_provider: roster_provider.clone(),
6855            topology_provider: None,
6856            customizer: None,
6857            agent_memory_provider: None,
6858            mob_definition: Some(runtime.mob_handle().definition().clone()),
6859        };
6860
6861        let response: Value = serde_json::from_str(
6862            &handle_unified_rpc_json(
6863                &runtime,
6864                &json!({
6865                    "jsonrpc": "2.0",
6866                    "id": 1,
6867                    "method": "mobkit/reset",
6868                    "params": { "identity": identity.as_str() },
6869                })
6870                .to_string(),
6871                Duration::from_secs(1),
6872                None,
6873                Some(&identity_ctx),
6874            )
6875            .await,
6876        )?;
6877
6878        assert!(response["error"].is_null(), "{response:#?}");
6879        assert_eq!(
6880            roster_provider.missing_definition_calls(),
6881            0,
6882            "mobkit/reset must preserve mob_definition when installing the reset roster provider"
6883        );
6884        assert_eq!(
6885            bridge
6886                .create_calls
6887                .load(std::sync::atomic::Ordering::SeqCst),
6888            1,
6889            "reset should rebuild through the bridge"
6890        );
6891        let created = bridge
6892            .last_create_spec()
6893            .await
6894            .expect("reset should record created spec");
6895        assert_eq!(created.profile.as_str(), "security");
6896        assert_eq!(
6897            identity_rt
6898                .status(&identity)
6899                .await?
6900                .profile
6901                .expect("identity should keep profile")
6902                .as_str(),
6903            "security"
6904        );
6905        Ok(())
6906    }
6907
6908    #[tokio::test]
6909    async fn unified_rpc_agent_memory_remember_writes_identity_scoped_record()
6910    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
6911        let temp_dir = tempfile::tempdir()?;
6912        let runtime = Box::pin(
6913            UnifiedRuntime::builder()
6914                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
6915                .module_config(MobKitConfig {
6916                    modules: Vec::new(),
6917                    discovery: DiscoverySpec {
6918                        namespace: "rpc-agent-memory-remember-test".to_string(),
6919                        modules: Vec::new(),
6920                    },
6921                    pre_spawn: Vec::new(),
6922                })
6923                .timeout(Duration::from_secs(1))
6924                .build(),
6925        )
6926        .await?;
6927        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
6928            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
6929            lease_provider: Arc::new(LocalLeaseProvider::new()),
6930            runtime_instance_id: "rpc-agent-memory-remember-test".to_string(),
6931            has_runtime_store: true,
6932            durability_policy: DurabilityPolicy::SyncWriteThrough,
6933            bridge: None,
6934            default_timeout: None,
6935        });
6936        let store = Arc::new(MarkdownAgentMemoryStore::open(
6937            temp_dir.path().join("agent-memory"),
6938        )?);
6939        let provider: Arc<dyn crate::identity_first::AgentMemoryProvider> = store.clone();
6940        identity_rt
6941            .set_agent_memory(Some(
6942                crate::identity_first::AgentMemoryRuntimeInjector::new(
6943                    provider.clone(),
6944                    crate::identity_first::AgentMemoryConfig::default(),
6945                ),
6946            ))
6947            .await;
6948        let memory_identity = AgentIdentity::parse("identity:luka")?;
6949        identity_rt
6950            .register(
6951                DurableAgentSpec {
6952                    identity: memory_identity.clone(),
6953                    profile: meerkat_mob::ProfileName::from("worker"),
6954                    addressability: AgentAddressability::Addressable,
6955                    display_name: None,
6956                    labels: Default::default(),
6957                    context: None,
6958                    additional_instructions: Vec::new(),
6959                    initial_message: None,
6960                    runtime_mode_override: None,
6961                    backend: None,
6962                    binding: None,
6963                },
6964                IdentityLifecycleState::Active,
6965                None,
6966                None,
6967            )
6968            .await;
6969        let identity_ctx = IdentityFirstContext {
6970            runtime: Arc::new(identity_rt),
6971            roster_provider: Arc::new(EmptyRosterProvider),
6972            topology_provider: None,
6973            customizer: None,
6974            agent_memory_provider: Some(provider),
6975            mob_definition: None,
6976        };
6977
6978        let capabilities: Value = serde_json::from_str(
6979            &handle_unified_rpc_json(
6980                &runtime,
6981                &json!({
6982                    "jsonrpc": "2.0",
6983                    "id": 1,
6984                    "method": "mobkit/capabilities",
6985                })
6986                .to_string(),
6987                Duration::from_secs(1),
6988                None,
6989                Some(&identity_ctx),
6990            )
6991            .await,
6992        )?;
6993        assert!(
6994            capabilities["result"]["methods"]
6995                .as_array()
6996                .is_some_and(|methods| methods.contains(&json!("mobkit/agent_memory/remember"))),
6997            "{capabilities:#?}"
6998        );
6999        assert!(
7000            capabilities["result"]["methods"]
7001                .as_array()
7002                .is_some_and(|methods| methods.contains(&json!("mobkit/agent_memory/recall"))),
7003            "{capabilities:#?}"
7004        );
7005        assert!(
7006            capabilities["result"]["methods"]
7007                .as_array()
7008                .is_some_and(|methods| methods.contains(&json!("mobkit/agent_memory/forget"))),
7009            "{capabilities:#?}"
7010        );
7011        // The markdown store implements none of the v2 surface; the
7012        // supports_* gates must keep update/manifest off the wire.
7013        assert!(
7014            capabilities["result"]["methods"]
7015                .as_array()
7016                .is_some_and(|methods| !methods.contains(&json!("mobkit/agent_memory/update"))),
7017            "{capabilities:#?}"
7018        );
7019        assert!(
7020            capabilities["result"]["methods"]
7021                .as_array()
7022                .is_some_and(|methods| !methods.contains(&json!("mobkit/agent_memory/manifest"))),
7023            "{capabilities:#?}"
7024        );
7025
7026        let response: Value = serde_json::from_str(
7027            &handle_unified_rpc_json(
7028                &runtime,
7029                &json!({
7030                    "jsonrpc": "2.0",
7031                    "id": 2,
7032                    "method": "mobkit/agent_memory/remember",
7033                    "params": {
7034                        "identity": "identity:luka",
7035                        "realm": "family",
7036                        "title": "School pickup",
7037                        "body": "Pickup is before calendar planning.",
7038                        "tags": ["family", "calendar"]
7039                    },
7040                })
7041                .to_string(),
7042                Duration::from_secs(1),
7043                None,
7044                Some(&identity_ctx),
7045            )
7046            .await,
7047        )?;
7048
7049        assert!(response["error"].is_null(), "{response:#?}");
7050        let memory_id = response["result"]["memory_id"]
7051            .as_str()
7052            .ok_or("memory_id should be present")?
7053            .to_string();
7054        assert_eq!(response["result"]["title"], json!("School pickup"));
7055        assert_eq!(
7056            response["result"]["body"],
7057            json!("Pickup is before calendar planning.")
7058        );
7059        assert_eq!(response["result"]["tags"], json!(["calendar", "family"]));
7060
7061        let records = store.read_records("family", &memory_identity)?;
7062        assert_eq!(records.len(), 1);
7063        assert_eq!(records[0].title, "School pickup");
7064        assert_eq!(records[0].tags, vec!["calendar", "family"]);
7065
7066        let recall_response: Value = serde_json::from_str(
7067            &handle_unified_rpc_json(
7068                &runtime,
7069                &json!({
7070                    "jsonrpc": "2.0",
7071                    "id": 3,
7072                    "method": "mobkit/agent_memory/recall",
7073                    "params": {
7074                        "identity": "identity:luka",
7075                        "realm": "family",
7076                        "selection": "contextual",
7077                        "query_terms": ["pickup"],
7078                        "max_entries": 4
7079                    },
7080                })
7081                .to_string(),
7082                Duration::from_secs(1),
7083                None,
7084                Some(&identity_ctx),
7085            )
7086            .await,
7087        )?;
7088
7089        assert!(recall_response["error"].is_null(), "{recall_response:#?}");
7090        assert_eq!(
7091            recall_response["result"]["records"]
7092                .as_array()
7093                .map(Vec::len),
7094            Some(1)
7095        );
7096        assert_eq!(
7097            recall_response["result"]["records"][0]["body"],
7098            json!("Pickup is before calendar planning.")
7099        );
7100
7101        let forget_response: Value = serde_json::from_str(
7102            &handle_unified_rpc_json(
7103                &runtime,
7104                &json!({
7105                    "jsonrpc": "2.0",
7106                    "id": 4,
7107                    "method": "mobkit/agent_memory/forget",
7108                    "params": {
7109                        "identity": "identity:luka",
7110                        "realm": "family",
7111                        "memory_id": memory_id.clone()
7112                    },
7113                })
7114                .to_string(),
7115                Duration::from_secs(1),
7116                None,
7117                Some(&identity_ctx),
7118            )
7119            .await,
7120        )?;
7121        assert!(forget_response["error"].is_null(), "{forget_response:#?}");
7122        assert_eq!(forget_response["result"]["memory_id"], json!(memory_id));
7123        assert_eq!(forget_response["result"]["deleted"], json!(true));
7124        assert!(store.read_records("family", &memory_identity)?.is_empty());
7125
7126        let recall_after_forget_response: Value = serde_json::from_str(
7127            &handle_unified_rpc_json(
7128                &runtime,
7129                &json!({
7130                    "jsonrpc": "2.0",
7131                    "id": 5,
7132                    "method": "mobkit/agent_memory/recall",
7133                    "params": {
7134                        "identity": "identity:luka",
7135                        "realm": "family",
7136                        "selection": "always"
7137                    },
7138                })
7139                .to_string(),
7140                Duration::from_secs(1),
7141                None,
7142                Some(&identity_ctx),
7143            )
7144            .await,
7145        )?;
7146        assert!(
7147            recall_after_forget_response["error"].is_null(),
7148            "{recall_after_forget_response:#?}"
7149        );
7150        assert_eq!(
7151            recall_after_forget_response["result"]["records"]
7152                .as_array()
7153                .map(Vec::len),
7154            Some(0)
7155        );
7156
7157        let unknown_response: Value = serde_json::from_str(
7158            &handle_unified_rpc_json(
7159                &runtime,
7160                &json!({
7161                    "jsonrpc": "2.0",
7162                    "id": 6,
7163                    "method": "mobkit/agent_memory/remember",
7164                    "params": {
7165                        "identity": "identity:unknown",
7166                        "title": "Orphan",
7167                        "body": "This should not be written."
7168                    },
7169                })
7170                .to_string(),
7171                Duration::from_secs(1),
7172                None,
7173                Some(&identity_ctx),
7174            )
7175            .await,
7176        )?;
7177        assert_eq!(unknown_response["error"]["code"], json!(-32602));
7178        assert!(
7179            unknown_response["error"]["message"]
7180                .as_str()
7181                .is_some_and(|message| message.contains("unknown identity")),
7182            "{unknown_response:#?}"
7183        );
7184
7185        let unknown_forget_response: Value = serde_json::from_str(
7186            &handle_unified_rpc_json(
7187                &runtime,
7188                &json!({
7189                    "jsonrpc": "2.0",
7190                    "id": 7,
7191                    "method": "mobkit/agent_memory/forget",
7192                    "params": {
7193                        "identity": "identity:unknown",
7194                        "memory_id": "mem-missing"
7195                    },
7196                })
7197                .to_string(),
7198                Duration::from_secs(1),
7199                None,
7200                Some(&identity_ctx),
7201            )
7202            .await,
7203        )?;
7204        assert_eq!(unknown_forget_response["error"]["code"], json!(-32602));
7205        assert!(
7206            unknown_forget_response["error"]["message"]
7207                .as_str()
7208                .is_some_and(|message| message.contains("unknown identity")),
7209            "{unknown_forget_response:#?}"
7210        );
7211
7212        Ok(())
7213    }
7214
7215    #[tokio::test]
7216    async fn unified_rpc_agent_memory_update_and_manifest_over_sqlite_store()
7217    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7218        let temp_dir = tempfile::tempdir()?;
7219        let runtime = Box::pin(
7220            UnifiedRuntime::builder()
7221                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
7222                .module_config(MobKitConfig {
7223                    modules: Vec::new(),
7224                    discovery: DiscoverySpec {
7225                        namespace: "rpc-agent-memory-sqlite-test".to_string(),
7226                        modules: Vec::new(),
7227                    },
7228                    pre_spawn: Vec::new(),
7229                })
7230                .timeout(Duration::from_secs(1))
7231                .build(),
7232        )
7233        .await?;
7234        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
7235            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
7236            lease_provider: Arc::new(LocalLeaseProvider::new()),
7237            runtime_instance_id: "rpc-agent-memory-sqlite-test".to_string(),
7238            has_runtime_store: true,
7239            durability_policy: DurabilityPolicy::SyncWriteThrough,
7240            bridge: None,
7241            default_timeout: None,
7242        });
7243        let store = Arc::new(crate::memory::SqliteAgentMemoryStore::open(
7244            temp_dir.path().join("agent-memory"),
7245        )?);
7246        let provider: Arc<dyn crate::identity_first::AgentMemoryProvider> = store.clone();
7247        identity_rt
7248            .set_agent_memory(Some(
7249                crate::identity_first::AgentMemoryRuntimeInjector::new(
7250                    provider.clone(),
7251                    crate::identity_first::AgentMemoryConfig::default(),
7252                ),
7253            ))
7254            .await;
7255        let memory_identity = AgentIdentity::parse("identity:luka")?;
7256        identity_rt
7257            .register(
7258                DurableAgentSpec {
7259                    identity: memory_identity.clone(),
7260                    profile: meerkat_mob::ProfileName::from("worker"),
7261                    addressability: AgentAddressability::Addressable,
7262                    display_name: None,
7263                    labels: Default::default(),
7264                    context: None,
7265                    additional_instructions: Vec::new(),
7266                    initial_message: None,
7267                    runtime_mode_override: None,
7268                    backend: None,
7269                    binding: None,
7270                },
7271                IdentityLifecycleState::Active,
7272                None,
7273                None,
7274            )
7275            .await;
7276        let identity_ctx = IdentityFirstContext {
7277            runtime: Arc::new(identity_rt),
7278            roster_provider: Arc::new(EmptyRosterProvider),
7279            topology_provider: None,
7280            customizer: None,
7281            agent_memory_provider: Some(provider),
7282            mob_definition: None,
7283        };
7284
7285        // The sqlite store supports the v2 surface, so update/manifest are
7286        // advertised alongside the v1 methods.
7287        let capabilities: Value = serde_json::from_str(
7288            &handle_unified_rpc_json(
7289                &runtime,
7290                &json!({
7291                    "jsonrpc": "2.0",
7292                    "id": 1,
7293                    "method": "mobkit/capabilities",
7294                })
7295                .to_string(),
7296                Duration::from_secs(1),
7297                None,
7298                Some(&identity_ctx),
7299            )
7300            .await,
7301        )?;
7302        for method in [
7303            "mobkit/agent_memory/recall",
7304            "mobkit/agent_memory/remember",
7305            "mobkit/agent_memory/forget",
7306            "mobkit/agent_memory/update",
7307            "mobkit/agent_memory/manifest",
7308        ] {
7309            assert!(
7310                capabilities["result"]["methods"]
7311                    .as_array()
7312                    .is_some_and(|methods| methods.contains(&json!(method))),
7313                "missing {method}: {capabilities:#?}"
7314            );
7315        }
7316
7317        let remember_response: Value = serde_json::from_str(
7318            &handle_unified_rpc_json(
7319                &runtime,
7320                &json!({
7321                    "jsonrpc": "2.0",
7322                    "id": 2,
7323                    "method": "mobkit/agent_memory/remember",
7324                    "params": {
7325                        "identity": "identity:luka",
7326                        "realm": "family",
7327                        "title": "School pickup",
7328                        "body": "Pickup is before calendar planning.",
7329                    },
7330                })
7331                .to_string(),
7332                Duration::from_secs(1),
7333                None,
7334                Some(&identity_ctx),
7335            )
7336            .await,
7337        )?;
7338        assert!(
7339            remember_response["error"].is_null(),
7340            "{remember_response:#?}"
7341        );
7342        let memory_id = remember_response["result"]["memory_id"]
7343            .as_str()
7344            .ok_or("memory_id should be present")?
7345            .to_string();
7346
7347        let update_response: Value = serde_json::from_str(
7348            &handle_unified_rpc_json(
7349                &runtime,
7350                &json!({
7351                    "jsonrpc": "2.0",
7352                    "id": 3,
7353                    "method": "mobkit/agent_memory/update",
7354                    "params": {
7355                        "identity": "identity:luka",
7356                        "realm": "family",
7357                        "memory_id": memory_id.clone(),
7358                        "title": "School pickup",
7359                        "body": "Pickup moved to after calendar planning.",
7360                        "tags": ["family"]
7361                    },
7362                })
7363                .to_string(),
7364                Duration::from_secs(1),
7365                None,
7366                Some(&identity_ctx),
7367            )
7368            .await,
7369        )?;
7370        assert!(update_response["error"].is_null(), "{update_response:#?}");
7371        let new_id = update_response["result"]["memory_id"]
7372            .as_str()
7373            .ok_or("updated memory_id should be present")?
7374            .to_string();
7375        assert_ne!(new_id, memory_id);
7376        assert_eq!(update_response["result"]["supersedes"], json!(memory_id));
7377
7378        // Only the successor is recallable after the supersede.
7379        let recall_response: Value = serde_json::from_str(
7380            &handle_unified_rpc_json(
7381                &runtime,
7382                &json!({
7383                    "jsonrpc": "2.0",
7384                    "id": 4,
7385                    "method": "mobkit/agent_memory/recall",
7386                    "params": {
7387                        "identity": "identity:luka",
7388                        "realm": "family",
7389                        "selection": "always"
7390                    },
7391                })
7392                .to_string(),
7393                Duration::from_secs(1),
7394                None,
7395                Some(&identity_ctx),
7396            )
7397            .await,
7398        )?;
7399        assert!(recall_response["error"].is_null(), "{recall_response:#?}");
7400        assert_eq!(
7401            recall_response["result"]["records"]
7402                .as_array()
7403                .map(Vec::len),
7404            Some(1)
7405        );
7406        assert_eq!(
7407            recall_response["result"]["records"][0]["memory_id"],
7408            json!(new_id)
7409        );
7410
7411        let manifest_response: Value = serde_json::from_str(
7412            &handle_unified_rpc_json(
7413                &runtime,
7414                &json!({
7415                    "jsonrpc": "2.0",
7416                    "id": 5,
7417                    "method": "mobkit/agent_memory/manifest",
7418                    "params": {
7419                        "identity": "identity:luka",
7420                        "realm": "family",
7421                        "tier": "working_set",
7422                        "k": 4
7423                    },
7424                })
7425                .to_string(),
7426                Duration::from_secs(1),
7427                None,
7428                Some(&identity_ctx),
7429            )
7430            .await,
7431        )?;
7432        assert!(
7433            manifest_response["error"].is_null(),
7434            "{manifest_response:#?}"
7435        );
7436        let records = manifest_response["result"]["records"]
7437            .as_array()
7438            .ok_or("manifest records array")?;
7439        assert_eq!(records.len(), 1, "{manifest_response:#?}");
7440        assert_eq!(records[0]["id"], json!(new_id));
7441        assert_eq!(records[0]["kind"], json!("fact"));
7442        assert_eq!(records[0]["age_days"], json!(0));
7443        assert!(
7444            records[0].get("body").is_none(),
7445            "manifest is an index, never a dump: {manifest_response:#?}"
7446        );
7447
7448        let bad_tier_response: Value = serde_json::from_str(
7449            &handle_unified_rpc_json(
7450                &runtime,
7451                &json!({
7452                    "jsonrpc": "2.0",
7453                    "id": 6,
7454                    "method": "mobkit/agent_memory/manifest",
7455                    "params": {
7456                        "identity": "identity:luka",
7457                        "tier": "everything"
7458                    },
7459                })
7460                .to_string(),
7461                Duration::from_secs(1),
7462                None,
7463                Some(&identity_ctx),
7464            )
7465            .await,
7466        )?;
7467        assert_eq!(bad_tier_response["error"]["code"], json!(-32602));
7468
7469        Ok(())
7470    }
7471
7472    #[tokio::test]
7473    async fn unified_rpc_agent_memory_capabilities_do_not_advertise_read_only_writes()
7474    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7475        let temp_dir = tempfile::tempdir()?;
7476        let runtime = Box::pin(
7477            UnifiedRuntime::builder()
7478                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
7479                .module_config(MobKitConfig {
7480                    modules: Vec::new(),
7481                    discovery: DiscoverySpec {
7482                        namespace: "rpc-agent-memory-read-only-test".to_string(),
7483                        modules: Vec::new(),
7484                    },
7485                    pre_spawn: Vec::new(),
7486                })
7487                .timeout(Duration::from_secs(1))
7488                .build(),
7489        )
7490        .await?;
7491        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
7492            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
7493            lease_provider: Arc::new(LocalLeaseProvider::new()),
7494            runtime_instance_id: "rpc-agent-memory-read-only-test".to_string(),
7495            has_runtime_store: true,
7496            durability_policy: DurabilityPolicy::SyncWriteThrough,
7497            bridge: None,
7498            default_timeout: None,
7499        });
7500        let provider: Arc<dyn crate::identity_first::AgentMemoryProvider> =
7501            Arc::new(ReadOnlyAgentMemoryProvider);
7502        let identity_ctx = IdentityFirstContext {
7503            runtime: Arc::new(identity_rt),
7504            roster_provider: Arc::new(EmptyRosterProvider),
7505            topology_provider: None,
7506            customizer: None,
7507            agent_memory_provider: Some(provider),
7508            mob_definition: None,
7509        };
7510
7511        let capabilities: Value = serde_json::from_str(
7512            &handle_unified_rpc_json(
7513                &runtime,
7514                &json!({
7515                    "jsonrpc": "2.0",
7516                    "id": 1,
7517                    "method": "mobkit/capabilities",
7518                })
7519                .to_string(),
7520                Duration::from_secs(1),
7521                None,
7522                Some(&identity_ctx),
7523            )
7524            .await,
7525        )?;
7526        let methods = capabilities["result"]["methods"]
7527            .as_array()
7528            .ok_or("methods should be an array")?;
7529
7530        assert!(methods.contains(&json!("mobkit/agent_memory/recall")));
7531        assert!(!methods.contains(&json!("mobkit/agent_memory/remember")));
7532        assert!(!methods.contains(&json!("mobkit/agent_memory/forget")));
7533        Ok(())
7534    }
7535
7536    #[test]
7537    fn identity_lease_lost_maps_off_capability_unavailable_code() {
7538        // -32004 is the SDKs' CAPABILITY_UNAVAILABLE_CODE, which both SDKs
7539        // reify into a permanent-capability-gap error type. LeaseLost is a
7540        // transient/recoverable lease-renewal failure and MUST NOT collide
7541        // with that code, or a recoverable lease loss is mis-typed as a
7542        // permanent capability gap. Regression for the -32004 collision.
7543        let identity = AgentIdentity::parse("review:singleton").expect("valid identity");
7544        let err = crate::identity_first::IdentityRuntimeError::LeaseLost(identity);
7545        let response = identity_error_response(json!("req-1"), &err);
7546        let error = response.error.expect("lease-lost must surface an error");
7547        assert_ne!(
7548            error.code, -32004,
7549            "LeaseLost must not use the capability code"
7550        );
7551        assert_eq!(
7552            error.code, -32005,
7553            "LeaseLost has its own identity-plane code"
7554        );
7555        assert!(error.message.contains("lease lost"));
7556    }
7557
7558    #[test]
7559    fn mobpack_authoring_rpc_helper_preserves_runtime_catalog_binding()
7560    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7561        let runtime = crate::mobpack::MobpackRuntimeCatalogState {
7562            loaded_modules: vec!["editor-host".to_string()],
7563            runtime_methods: vec![
7564                "mobkit/mobpacks/catalogs".to_string(),
7565                "mobkit/mobpacks/apply_operation".to_string(),
7566                "mobkit/mobpacks/deploy".to_string(),
7567            ],
7568            has_contact_directory: true,
7569            has_peer_mob_handles: false,
7570            has_inproc_contacts: false,
7571            runtime_flow_rows: vec![json!({
7572                "id": "runtime_rpc_main",
7573                "source": "mobkit/runtime/flow_projection",
7574                "document": {
7575                    "mob_id": "runtime_rpc",
7576                    "flow": { "name": "main", "steps": [] },
7577                    "members": []
7578                },
7579                "validation": { "ok": true }
7580            })],
7581            runtime_agent_definition_sources: vec![json!({
7582                "id": "runtime_profiles_rpc",
7583                "name": "Runtime RPC profiles",
7584                "source": "mobkit/runtime/agent-definitions",
7585                "document": {
7586                    "mob_id": "runtime_rpc",
7587                    "members": [{
7588                        "id": "m_runtime_reviewer",
7589                        "name": "Runtime reviewer",
7590                        "role": "runtime_reviewer",
7591                        "profileBinding": "inline",
7592                        "model": "gpt-5.5",
7593                        "runtimeMode": "turn_driven",
7594                        "tools": ["builtins"],
7595                        "skills": ["mob.runtime.review"],
7596                        "schema": ""
7597                    }],
7598                    "schemas": []
7599                }
7600            })],
7601            runtime_skill_realms: vec![json!({
7602                "id": "runtime_rpc",
7603                "label": "Runtime RPC",
7604                "source": "mobkit/runtime/skills",
7605                "skills": [{
7606                    "id": "mob.runtime.review",
7607                    "label": "Runtime review",
7608                    "source": "inline",
7609                    "content": "Review runtime work."
7610                }]
7611            })],
7612        };
7613
7614        let catalogs = super::handle_mobpack_authoring_rpc_with_runtime(
7615            "mobkit/mobpacks/catalogs",
7616            &json!({}),
7617            json!(1),
7618            Some(&runtime),
7619        )
7620        .expect("catalogs method");
7621        let catalogs: Value = serde_json::to_value(catalogs)?;
7622        assert_eq!(catalogs["result"]["runtime_backed"], json!(true));
7623        assert_eq!(
7624            catalogs["result"]["authoring_provider"]["runtime_binding"],
7625            json!("bound")
7626        );
7627        assert_eq!(
7628            catalogs["result"]["runtime_flows"][0]["id"],
7629            json!("runtime_rpc_main")
7630        );
7631        let listed = super::handle_mobpack_authoring_rpc_with_runtime(
7632            "mobkit/mobpacks/list",
7633            &json!({}),
7634            json!(2),
7635            Some(&runtime),
7636        )
7637        .expect("list method");
7638        let listed: Value = serde_json::to_value(listed)?;
7639        assert_eq!(listed["result"]["runtime_backed"], json!(true));
7640        assert_eq!(listed["result"]["rows"][0]["id"], json!("runtime_rpc_main"));
7641
7642        let fetched = super::handle_mobpack_authoring_rpc_with_runtime(
7643            "mobkit/mobpacks/get",
7644            &json!({ "id": "runtime_rpc_main" }),
7645            json!(3),
7646            Some(&runtime),
7647        )
7648        .expect("get method");
7649        let fetched: Value = serde_json::to_value(fetched)?;
7650        assert_eq!(fetched["result"]["runtime_backed"], json!(true));
7651        assert_eq!(fetched["result"]["row"]["id"], json!("runtime_rpc_main"));
7652
7653        let definitions = super::handle_mobpack_authoring_rpc_with_runtime(
7654            "mobkit/agent_definitions/list",
7655            &json!({}),
7656            json!(4),
7657            Some(&runtime),
7658        )
7659        .expect("agent definitions method");
7660        let definitions: Value = serde_json::to_value(definitions)?;
7661        let runtime_definition = definitions["result"]["agent_definitions"]
7662            .as_array()
7663            .and_then(|rows| {
7664                rows.iter()
7665                    .find(|row| row["sourceOrigin"] == "mobkit/runtime/agent-definitions")
7666            })
7667            .expect("runtime profile definition");
7668        assert_eq!(runtime_definition["role"], json!("runtime_reviewer"));
7669        assert_eq!(
7670            runtime_definition["toolDefinitions"][0]["id"],
7671            json!("builtins")
7672        );
7673        assert_eq!(
7674            runtime_definition["skillDefinitions"][0]["id"],
7675            json!("mob.runtime.review")
7676        );
7677        assert_eq!(
7678            definitions["result"]["catalog_snapshot"]["runtime_backed"],
7679            json!(true)
7680        );
7681
7682        let tools = super::handle_mobpack_authoring_rpc_with_runtime(
7683            "mobkit/tools/catalog",
7684            &json!({}),
7685            json!(5),
7686            Some(&runtime),
7687        )
7688        .expect("tools catalog method");
7689        let tools: Value = serde_json::to_value(tools)?;
7690        let mob_tool = tools["result"]["tool_catalog"]
7691            .as_array()
7692            .expect("tools")
7693            .iter()
7694            .find(|tool| tool["id"] == "mob")
7695            .expect("mob tool");
7696        assert_eq!(mob_tool["runtime_availability"]["available"], json!(false));
7697
7698        let agents = super::handle_mobpack_authoring_rpc_with_runtime(
7699            "mobkit/agent_definitions/list",
7700            &json!({}),
7701            json!(3),
7702            Some(&runtime),
7703        )
7704        .expect("agent definitions method");
7705        let agents: Value = serde_json::to_value(agents)?;
7706        assert_eq!(agents["result"]["runtime_backed"], json!(true));
7707        let planner = agents["result"]["agent_definitions"]
7708            .as_array()
7709            .expect("agent definitions")
7710            .iter()
7711            .find(|definition| definition["role"] == "planner")
7712            .expect("planner definition");
7713        let planner_mob_tool = planner["toolDefinitions"]
7714            .as_array()
7715            .expect("planner tools")
7716            .iter()
7717            .find(|tool| tool["id"] == "mob")
7718            .expect("planner mob tool");
7719        assert_eq!(
7720            planner_mob_tool["runtimeAvailability"]["state"],
7721            json!("unavailable")
7722        );
7723
7724        Ok(())
7725    }
7726
7727    #[test]
7728    fn module_rpc_dispatches_mobpack_authoring_methods()
7729    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7730        let config = MobKitConfig {
7731            modules: Vec::new(),
7732            discovery: DiscoverySpec {
7733                namespace: "module-rpc-authoring-dispatch-test".to_string(),
7734                modules: Vec::new(),
7735            },
7736            pre_spawn: Vec::new(),
7737        };
7738        let mut runtime = crate::start_mobkit_runtime(config, Vec::new(), Duration::from_secs(1))?;
7739
7740        let capabilities: Value = serde_json::from_str(&super::handle_mobkit_rpc_json(
7741            &mut runtime,
7742            &json!({
7743                "jsonrpc": "2.0",
7744                "id": 1,
7745                "method": "mobkit/capabilities",
7746            })
7747            .to_string(),
7748            Duration::from_secs(1),
7749        ))?;
7750        let methods = capabilities["result"]["methods"]
7751            .as_array()
7752            .expect("methods array")
7753            .iter()
7754            .filter_map(Value::as_str)
7755            .collect::<Vec<_>>();
7756        for method in super::MOBPACK_AUTHORING_METHODS {
7757            assert!(
7758                methods.contains(method),
7759                "missing authoring method {method}"
7760            );
7761        }
7762        assert_eq!(
7763            capabilities["result"]["authoring_capabilities"]["runtime_mutation"],
7764            json!(false)
7765        );
7766        assert_eq!(
7767            capabilities["result"]["authoring_capabilities"]["host_mutation_methods"]["mobkit/mobpacks/deploy"],
7768            json!("when execute=true, writes a mobpack archive and runs rkat mob run on the host")
7769        );
7770
7771        let schema: Value = serde_json::from_str(&super::handle_mobkit_rpc_json(
7772            &mut runtime,
7773            &json!({
7774                "jsonrpc": "2.0",
7775                "id": 2,
7776                "method": "mobkit/mobpacks/schema",
7777            })
7778            .to_string(),
7779            Duration::from_secs(1),
7780        ))?;
7781        assert!(schema["error"].is_null(), "{schema:#?}");
7782        assert_eq!(
7783            schema["result"]["commands"]["deploy_rpc"],
7784            json!("mobkit/mobpacks/deploy")
7785        );
7786        assert!(schema["result"]["agent_definitions"].is_null());
7787
7788        let _ = runtime.shutdown();
7789        Ok(())
7790    }
7791
7792    #[test]
7793    fn generated_runtime_ids_match_their_durable_identity_prefix() {
7794        assert!(!rpc_member_id_matches_durable_identity(
7795            "rt:review:singleton:0",
7796            "review:singleton",
7797        ));
7798        assert!(!rpc_member_id_matches_durable_identity(
7799            "review:singleton:gen1",
7800            "review:singleton",
7801        ));
7802        assert!(!rpc_member_id_matches_durable_identity(
7803            "review:singleton:1",
7804            "review:singleton",
7805        ));
7806        assert!(!rpc_member_id_matches_durable_identity(
7807            "rt:reviewer:singleton:0",
7808            "review:singleton",
7809        ));
7810        assert!(!rpc_member_id_matches_durable_identity(
7811            "rt:review:singleton:qa:0",
7812            "review:singleton",
7813        ));
7814        assert!(!rpc_member_id_matches_durable_identity(
7815            "review:singleton:qa",
7816            "review:singleton",
7817        ));
7818        assert_eq!(
7819            rpc_runtime_alias_generation("rt:review:singleton:7", "review:singleton"),
7820            Some(7)
7821        );
7822        assert_eq!(
7823            rpc_runtime_alias_generation("rt:review:singleton:8", "review:other"),
7824            None
7825        );
7826    }
7827
7828    #[test]
7829    fn rpc_live_identity_visibility_matches_delegate_projection_labels() {
7830        assert!(rpc_live_identity_alias_visible("worker", &BTreeMap::new()));
7831
7832        let mut labels = BTreeMap::new();
7833        labels.insert("role".to_string(), "delegate".to_string());
7834        labels.insert("source_mob_id".to_string(), "mob-a".to_string());
7835        labels.insert("agent_identity".to_string(), "review:singleton".to_string());
7836        assert!(!rpc_live_identity_alias_visible("worker", &labels));
7837        assert!(!rpc_live_identity_alias_visible("delegate", &labels));
7838    }
7839
7840    #[test]
7841    fn ambiguous_live_alias_errors_include_structured_data() -> Result<(), serde_json::Error> {
7842        let response: Value = serde_json::from_str(&error_response(
7843            json!(1),
7844            -32602,
7845            "ambiguous live identity alias review:singleton: candidates [rt:review:singleton:0, rt:review:singleton:1]",
7846        ))?;
7847
7848        assert_eq!(
7849            response["error"]["data"]["kind"],
7850            json!("ambiguous_live_identity_alias")
7851        );
7852        assert_eq!(
7853            response["error"]["data"]["identity"],
7854            json!("review:singleton")
7855        );
7856        assert_eq!(
7857            response["error"]["data"]["candidates"],
7858            json!(["rt:review:singleton:0", "rt:review:singleton:1"])
7859        );
7860        Ok(())
7861    }
7862
7863    #[test]
7864    fn wrapped_ambiguous_live_alias_errors_include_structured_data() -> Result<(), serde_json::Error>
7865    {
7866        let response: Value = serde_json::from_str(&error_response(
7867            json!(1),
7868            -32602,
7869            "invalid identity: ambiguous live identity alias review:singleton: candidates [rt:review:singleton:0, rt:review:singleton:1]",
7870        ))?;
7871
7872        assert_eq!(
7873            response["error"]["data"]["kind"],
7874            json!("ambiguous_live_identity_alias")
7875        );
7876        assert_eq!(
7877            response["error"]["data"]["identity"],
7878            json!("review:singleton")
7879        );
7880        assert_eq!(
7881            response["error"]["data"]["candidates"],
7882            json!(["rt:review:singleton:0", "rt:review:singleton:1"])
7883        );
7884        Ok(())
7885    }
7886
7887    #[tokio::test]
7888    async fn runtime_id_live_only_resolution_rejects_duplicate_projected_identity()
7889    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7890        let temp_dir = tempfile::tempdir()?;
7891        let runtime = Box::pin(
7892            UnifiedRuntime::builder()
7893                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
7894                .module_config(MobKitConfig {
7895                    modules: Vec::new(),
7896                    discovery: DiscoverySpec {
7897                        namespace: "rpc-identity-alias-test".to_string(),
7898                        modules: Vec::new(),
7899                    },
7900                    pre_spawn: Vec::new(),
7901                })
7902                .timeout(Duration::from_secs(1))
7903                .build(),
7904        )
7905        .await?;
7906        for runtime_id in ["rt:review:singleton:0", "rt:review:singleton:1"] {
7907            let mut labels = BTreeMap::new();
7908            labels.insert("agent_identity".to_string(), "review:singleton".to_string());
7909            spawn_identity_projection_fixture(
7910                &runtime,
7911                SpawnMemberSpec::from_wire(
7912                    "worker".to_string(),
7913                    runtime_id.to_string(),
7914                    Some("You are a duplicate Review Agent.".into()),
7915                    None,
7916                    None,
7917                )
7918                .with_labels(labels),
7919            )
7920            .await?;
7921        }
7922        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
7923            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
7924            lease_provider: Arc::new(LocalLeaseProvider::new()),
7925            runtime_instance_id: "rpc-identity-alias-test".to_string(),
7926            has_runtime_store: true,
7927            durability_policy: DurabilityPolicy::SyncWriteThrough,
7928            bridge: None,
7929            default_timeout: None,
7930        });
7931
7932        let err =
7933            resolve_rpc_identity_control_target(&runtime, &identity_rt, "rt:review:singleton:0")
7934                .await
7935                .expect_err("runtime-id live-only fallback should reject duplicate durable alias");
7936        assert!(
7937            err.contains("ambiguous live identity alias review:singleton"),
7938            "unexpected error: {err}"
7939        );
7940
7941        Ok(())
7942    }
7943
7944    #[tokio::test]
7945    async fn durable_resolution_prefers_registered_live_binding_over_stale_duplicates()
7946    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
7947        let temp_dir = tempfile::tempdir()?;
7948        let runtime = Box::pin(
7949            UnifiedRuntime::builder()
7950                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
7951                .module_config(MobKitConfig {
7952                    modules: Vec::new(),
7953                    discovery: DiscoverySpec {
7954                        namespace: "rpc-identity-alias-test".to_string(),
7955                        modules: Vec::new(),
7956                    },
7957                    pre_spawn: Vec::new(),
7958                })
7959                .timeout(Duration::from_secs(1))
7960                .build(),
7961        )
7962        .await?;
7963        for runtime_id in ["rt:review:singleton:0", "rt:review:singleton:1"] {
7964            let mut labels = BTreeMap::new();
7965            labels.insert("agent_identity".to_string(), "review:singleton".to_string());
7966            spawn_identity_projection_fixture(
7967                &runtime,
7968                SpawnMemberSpec::from_wire(
7969                    "worker".to_string(),
7970                    runtime_id.to_string(),
7971                    Some("You are a duplicate Review Agent.".into()),
7972                    None,
7973                    None,
7974                )
7975                .with_labels(labels),
7976            )
7977            .await?;
7978        }
7979        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
7980            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
7981            lease_provider: Arc::new(LocalLeaseProvider::new()),
7982            runtime_instance_id: "rpc-identity-alias-test".to_string(),
7983            has_runtime_store: true,
7984            durability_policy: DurabilityPolicy::SyncWriteThrough,
7985            bridge: None,
7986            default_timeout: None,
7987        });
7988        let identity = AgentIdentity::parse("review:singleton")?;
7989        let record = ContinuityRecord {
7990            identity: identity.clone(),
7991            agent_runtime_id: AgentRuntimeId::parse("rt:review:singleton:1")?,
7992            session_id: meerkat_core::types::SessionId::new(),
7993            generation: ContinuityGeneration::new(1),
7994            checkpoint_version: CheckpointVersion::new(0),
7995        };
7996        identity_rt
7997            .register(
7998                DurableAgentSpec {
7999                    identity,
8000                    profile: meerkat_mob::ProfileName::from("worker"),
8001                    addressability: AgentAddressability::Addressable,
8002                    display_name: None,
8003                    labels: BTreeMap::new(),
8004                    context: None,
8005                    additional_instructions: Vec::new(),
8006                    initial_message: None,
8007                    runtime_mode_override: None,
8008                    backend: None,
8009                    binding: None,
8010                },
8011                IdentityLifecycleState::Active,
8012                Some(record),
8013                None,
8014            )
8015            .await;
8016
8017        let target =
8018            resolve_rpc_identity_control_target(&runtime, &identity_rt, "review:singleton").await?;
8019        assert_eq!(target.identity.as_str(), "review:singleton");
8020        assert_eq!(
8021            target
8022                .live
8023                .as_ref()
8024                .map(|alias| alias.runtime_member_id.as_str()),
8025            Some("rt:review:singleton:1")
8026        );
8027
8028        let target =
8029            resolve_rpc_identity_control_target(&runtime, &identity_rt, "rt:review:singleton:1")
8030                .await?;
8031        assert_eq!(
8032            target
8033                .live
8034                .as_ref()
8035                .map(|alias| alias.runtime_member_id.as_str()),
8036            Some("rt:review:singleton:1")
8037        );
8038
8039        let stale_target =
8040            resolve_rpc_identity_control_target(&runtime, &identity_rt, "rt:review:singleton:0")
8041                .await?;
8042        assert_eq!(
8043            stale_target
8044                .live
8045                .as_ref()
8046                .map(|alias| alias.runtime_member_id.as_str()),
8047            Some("rt:review:singleton:0")
8048        );
8049        let stale_response =
8050            super::rpc_stale_live_alias_error_response(&identity_rt, &stale_target, json!(99))
8051                .await
8052                .expect("old reset generation should be rejected as stale");
8053        assert_eq!(
8054            stale_response
8055                .error
8056                .as_ref()
8057                .and_then(|error| error.data.as_ref())
8058                .and_then(|data| data.get("kind")),
8059            Some(&json!("stale_identity_runtime_binding"))
8060        );
8061        assert_eq!(
8062            stale_response
8063                .error
8064                .as_ref()
8065                .and_then(|error| error.data.as_ref())
8066                .and_then(|data| data.get("registered_runtime_member_id")),
8067            Some(&json!("rt:review:singleton:1"))
8068        );
8069        assert_eq!(
8070            stale_response
8071                .error
8072                .as_ref()
8073                .and_then(|error| error.data.as_ref())
8074                .and_then(|data| data.get("live_runtime_member_id")),
8075            Some(&json!("rt:review:singleton:0"))
8076        );
8077
8078        Ok(())
8079    }
8080
8081    #[tokio::test]
8082    async fn current_generation_resolvers_ignore_stale_reset_member_rows()
8083    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8084        let temp_dir = tempfile::tempdir()?;
8085        let mut runtime = Box::pin(
8086            UnifiedRuntime::builder()
8087                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8088                .module_config(MobKitConfig {
8089                    modules: Vec::new(),
8090                    discovery: DiscoverySpec {
8091                        namespace: "rpc-current-generation-target-test".to_string(),
8092                        modules: Vec::new(),
8093                    },
8094                    pre_spawn: Vec::new(),
8095                })
8096                .timeout(Duration::from_secs(1))
8097                .build(),
8098        )
8099        .await?;
8100        let durable_identity = "review:current-generation";
8101        let stale_alias = "rt:review:current-generation:0";
8102        let current_alias = "rt:review:current-generation:1";
8103        for runtime_id in [stale_alias, current_alias] {
8104            spawn_identity_projection_fixture(
8105                &runtime,
8106                SpawnMemberSpec::from_wire(
8107                    "worker".to_string(),
8108                    runtime_id.to_string(),
8109                    None,
8110                    None,
8111                    None,
8112                )
8113                .with_labels(BTreeMap::from([(
8114                    "agent_identity".to_string(),
8115                    durable_identity.to_string(),
8116                )])),
8117            )
8118            .await?;
8119        }
8120
8121        let identity_runtime = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
8122            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8123            lease_provider: Arc::new(LocalLeaseProvider::new()),
8124            runtime_instance_id: "rpc-current-generation-target-test".to_string(),
8125            has_runtime_store: true,
8126            durability_policy: DurabilityPolicy::SyncWriteThrough,
8127            bridge: None,
8128            default_timeout: None,
8129        }));
8130        let identity = AgentIdentity::parse(durable_identity)?;
8131        identity_runtime
8132            .register(
8133                rpc_durable_spec(durable_identity, "worker"),
8134                IdentityLifecycleState::Active,
8135                Some(ContinuityRecord {
8136                    identity: identity.clone(),
8137                    agent_runtime_id: AgentRuntimeId::parse(current_alias)?,
8138                    session_id: meerkat_core::types::SessionId::new(),
8139                    generation: ContinuityGeneration::new(1),
8140                    checkpoint_version: CheckpointVersion::new(0),
8141                }),
8142                None,
8143            )
8144            .await;
8145
8146        let expected_session = runtime
8147            .mob_handle()
8148            .resolve_bridge_session_id(&crate::member_comms_id::mob_member_id(current_alias))
8149            .await
8150            .ok_or("current member must have a bridge session")?;
8151        let params = json!({"identity": durable_identity});
8152        let target = identity_runtime
8153            .member_alias_lifecycle_target(durable_identity)
8154            .await?
8155            .ok_or("durable identity must resolve to lifecycle authority")?;
8156        let operation_runtime = Arc::clone(&identity_runtime);
8157        let handle = runtime.mob_handle();
8158        let resolved_session = IdentityRuntime::run_member_alias_targets_operation_tracked(
8159            vec![target],
8160            move || async move {
8161                resolve_live_target(&handle, Some(&operation_runtime), true, &params).await
8162            },
8163        )
8164        .await?;
8165        assert_eq!(resolved_session, Some(expected_session));
8166
8167        let fallback_error = resolve_live_target(
8168            &runtime.mob_handle(),
8169            None,
8170            false,
8171            &json!({"identity": durable_identity}),
8172        )
8173        .await
8174        .expect_err("two live generations without identity authority must be ambiguous");
8175        assert!(
8176            fallback_error.contains("ambiguous live member alias"),
8177            "unexpected fallback error: {fallback_error}"
8178        );
8179        let cross_mob_fallback = runtime
8180            .local_member_peer_info(durable_identity)
8181            .await
8182            .expect_err("cross-mob fallback without identity authority must be ambiguous");
8183        assert!(
8184            cross_mob_fallback
8185                .to_string()
8186                .contains("ambiguous durable member alias"),
8187            "unexpected cross-mob fallback error: {cross_mob_fallback}"
8188        );
8189
8190        runtime.attach_identity_first_context(Arc::new(
8191            crate::identity_first::IdentityFirstRuntimeContext::new(
8192                Arc::clone(&identity_runtime),
8193                Arc::new(EmptyRosterProvider),
8194                None,
8195                None,
8196                None,
8197            ),
8198        ));
8199        let (_, comms_name, _) = runtime.local_member_peer_info(durable_identity).await?;
8200        assert!(
8201            comms_name.ends_with(crate::member_comms_id::mob_member_id_str(current_alias).as_ref()),
8202            "peer info must use the current generation: {comms_name}"
8203        );
8204
8205        let remote_comms_name = "remote-mob/worker/peer";
8206        let remote_pubkey = [42_u8; 32];
8207        let remote_peer_id =
8208            meerkat_core::comms::PeerId::from_ed25519_pubkey(&remote_pubkey).to_string();
8209        runtime
8210            .wire_local(
8211                durable_identity,
8212                remote_comms_name,
8213                &remote_peer_id,
8214                &format!("inproc://{remote_comms_name}"),
8215                Some(remote_pubkey),
8216            )
8217            .await?;
8218        let current = runtime
8219            .mob_handle()
8220            .get_member(&crate::member_comms_id::mob_member_id(current_alias))
8221            .await?
8222            .ok_or("current member missing")?;
8223        let stale = runtime
8224            .mob_handle()
8225            .get_member(&crate::member_comms_id::mob_member_id(stale_alias))
8226            .await?
8227            .ok_or("stale member missing")?;
8228        assert!(
8229            current
8230                .wired_to
8231                .iter()
8232                .any(|peer| peer.as_str() == remote_comms_name),
8233            "current generation must receive the wire"
8234        );
8235        assert!(
8236            stale
8237                .wired_to
8238                .iter()
8239                .all(|peer| peer.as_str() != remote_comms_name),
8240            "stale generation must not receive the wire"
8241        );
8242
8243        runtime.shutdown().await;
8244        Ok(())
8245    }
8246
8247    #[tokio::test]
8248    async fn durable_resolution_rejects_hidden_registered_live_binding()
8249    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8250        let temp_dir = tempfile::tempdir()?;
8251        let runtime = Box::pin(
8252            UnifiedRuntime::builder()
8253                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8254                .module_config(MobKitConfig {
8255                    modules: Vec::new(),
8256                    discovery: DiscoverySpec {
8257                        namespace: "rpc-hidden-bound-test".to_string(),
8258                        modules: Vec::new(),
8259                    },
8260                    pre_spawn: Vec::new(),
8261                })
8262                .timeout(Duration::from_secs(1))
8263                .build(),
8264        )
8265        .await?;
8266        spawn_identity_projection_fixture(
8267            &runtime,
8268            SpawnMemberSpec::from_wire(
8269                "worker".to_string(),
8270                "rt:review:singleton:0".to_string(),
8271                Some("You are a hidden Review Agent.".into()),
8272                None,
8273                None,
8274            )
8275            .with_labels(BTreeMap::from([
8276                ("agent_identity".to_string(), "review:singleton".to_string()),
8277                ("role".to_string(), "delegate".to_string()),
8278                ("source_mob_id".to_string(), "upstream".to_string()),
8279            ])),
8280        )
8281        .await?;
8282        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8283            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8284            lease_provider: Arc::new(LocalLeaseProvider::new()),
8285            runtime_instance_id: "rpc-hidden-bound-test".to_string(),
8286            has_runtime_store: true,
8287            durability_policy: DurabilityPolicy::SyncWriteThrough,
8288            bridge: None,
8289            default_timeout: None,
8290        });
8291        let identity = AgentIdentity::parse("review:singleton")?;
8292        identity_rt
8293            .register(
8294                DurableAgentSpec {
8295                    identity: identity.clone(),
8296                    profile: meerkat_mob::ProfileName::from("worker"),
8297                    addressability: AgentAddressability::Addressable,
8298                    display_name: None,
8299                    labels: BTreeMap::new(),
8300                    context: None,
8301                    additional_instructions: Vec::new(),
8302                    initial_message: None,
8303                    runtime_mode_override: None,
8304                    backend: None,
8305                    binding: None,
8306                },
8307                IdentityLifecycleState::Active,
8308                Some(ContinuityRecord {
8309                    identity,
8310                    agent_runtime_id: AgentRuntimeId::parse("rt:review:singleton:0")?,
8311                    session_id: meerkat_core::types::SessionId::new(),
8312                    generation: ContinuityGeneration::new(0),
8313                    checkpoint_version: CheckpointVersion::new(0),
8314                }),
8315                None,
8316            )
8317            .await;
8318
8319        for requested_identity in ["review:singleton", "rt:review:singleton:0"] {
8320            let err =
8321                resolve_rpc_identity_control_target(&runtime, &identity_rt, requested_identity)
8322                    .await
8323                    .expect_err("hidden registered live binding must not resolve");
8324            assert!(
8325                err.contains("identity hidden by policy"),
8326                "unexpected error for {requested_identity}: {err}"
8327            );
8328        }
8329
8330        Ok(())
8331    }
8332
8333    #[tokio::test]
8334    async fn live_only_hidden_alias_reports_policy_error()
8335    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8336        let temp_dir = tempfile::tempdir()?;
8337        let runtime = Box::pin(
8338            UnifiedRuntime::builder()
8339                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8340                .module_config(MobKitConfig {
8341                    modules: Vec::new(),
8342                    discovery: DiscoverySpec {
8343                        namespace: "rpc-hidden-live-only-test".to_string(),
8344                        modules: Vec::new(),
8345                    },
8346                    pre_spawn: Vec::new(),
8347                })
8348                .timeout(Duration::from_secs(1))
8349                .build(),
8350        )
8351        .await?;
8352        spawn_identity_projection_fixture(
8353            &runtime,
8354            SpawnMemberSpec::from_wire(
8355                "worker".to_string(),
8356                "rt:review:singleton:0".to_string(),
8357                Some("You are a hidden Review Agent.".into()),
8358                None,
8359                None,
8360            )
8361            .with_labels(BTreeMap::from([
8362                ("agent_identity".to_string(), "review:singleton".to_string()),
8363                ("role".to_string(), "delegate".to_string()),
8364                ("source_mob_id".to_string(), "upstream".to_string()),
8365            ])),
8366        )
8367        .await?;
8368        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8369            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8370            lease_provider: Arc::new(LocalLeaseProvider::new()),
8371            runtime_instance_id: "rpc-hidden-live-only-test".to_string(),
8372            has_runtime_store: true,
8373            durability_policy: DurabilityPolicy::SyncWriteThrough,
8374            bridge: None,
8375            default_timeout: None,
8376        });
8377
8378        for requested_identity in ["review:singleton", "rt:review:singleton:0"] {
8379            let err =
8380                resolve_rpc_identity_control_target(&runtime, &identity_rt, requested_identity)
8381                    .await
8382                    .expect_err("hidden live-only alias must not collapse into unknown identity");
8383            assert!(
8384                err.contains("identity hidden by policy"),
8385                "unexpected error for {requested_identity}: {err}"
8386            );
8387        }
8388
8389        let identity_ctx = IdentityFirstContext {
8390            runtime: Arc::new(identity_rt),
8391            roster_provider: Arc::new(EmptyRosterProvider),
8392            topology_provider: None,
8393            customizer: None,
8394            agent_memory_provider: None,
8395            mob_definition: None,
8396        };
8397        for requested_identity in ["review:singleton", "rt:review:singleton:0"] {
8398            let response: Value = serde_json::from_str(
8399                &handle_unified_rpc_json(
8400                    &runtime,
8401                    &json!({
8402                        "jsonrpc": "2.0",
8403                        "id": 1,
8404                        "method": "mobkit/status_identity",
8405                        "params": { "identity": requested_identity },
8406                    })
8407                    .to_string(),
8408                    Duration::from_secs(1),
8409                    None,
8410                    Some(&identity_ctx),
8411                )
8412                .await,
8413            )?;
8414            assert_eq!(
8415                response["error"]["data"]["kind"],
8416                json!("identity_hidden_by_policy"),
8417                "unexpected hidden response for {requested_identity}: {response:#?}"
8418            );
8419        }
8420
8421        Ok(())
8422    }
8423
8424    #[tokio::test]
8425    async fn live_only_resolution_rejects_runtime_member_bound_to_other_durable_identity()
8426    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8427        let temp_dir = tempfile::tempdir()?;
8428        let runtime = Box::pin(
8429            UnifiedRuntime::builder()
8430                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8431                .module_config(MobKitConfig {
8432                    modules: Vec::new(),
8433                    discovery: DiscoverySpec {
8434                        namespace: "rpc-identity-alias-test".to_string(),
8435                        modules: Vec::new(),
8436                    },
8437                    pre_spawn: Vec::new(),
8438                })
8439                .timeout(Duration::from_secs(1))
8440                .build(),
8441        )
8442        .await?;
8443        let mut labels = BTreeMap::new();
8444        labels.insert("agent_identity".to_string(), "other:singleton".to_string());
8445        spawn_identity_projection_fixture(
8446            &runtime,
8447            SpawnMemberSpec::from_wire(
8448                "worker".to_string(),
8449                "rt:review:singleton:0".to_string(),
8450                Some("You are a wrong-projected Review Agent.".into()),
8451                None,
8452                None,
8453            )
8454            .with_labels(labels),
8455        )
8456        .await?;
8457
8458        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8459            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8460            lease_provider: Arc::new(LocalLeaseProvider::new()),
8461            runtime_instance_id: "rpc-identity-alias-test".to_string(),
8462            has_runtime_store: true,
8463            durability_policy: DurabilityPolicy::SyncWriteThrough,
8464            bridge: None,
8465            default_timeout: None,
8466        });
8467        let identity = AgentIdentity::parse("review:singleton")?;
8468        let record = ContinuityRecord {
8469            identity: identity.clone(),
8470            agent_runtime_id: AgentRuntimeId::parse("rt:review:singleton:0")?,
8471            session_id: meerkat_core::types::SessionId::new(),
8472            generation: ContinuityGeneration::new(0),
8473            checkpoint_version: CheckpointVersion::new(0),
8474        };
8475        identity_rt
8476            .register(
8477                DurableAgentSpec {
8478                    identity: identity.clone(),
8479                    profile: meerkat_mob::ProfileName::from("worker"),
8480                    addressability: AgentAddressability::Addressable,
8481                    display_name: None,
8482                    labels: BTreeMap::new(),
8483                    context: None,
8484                    additional_instructions: Vec::new(),
8485                    initial_message: None,
8486                    runtime_mode_override: None,
8487                    backend: None,
8488                    binding: None,
8489                },
8490                IdentityLifecycleState::Active,
8491                Some(record),
8492                Some(LeaseGrant {
8493                    identity,
8494                    fencing_token: FencingToken::new(1),
8495                    ttl: Duration::from_mins(1),
8496                }),
8497            )
8498            .await;
8499
8500        let err = resolve_rpc_identity_control_target(&runtime, &identity_rt, "other:singleton")
8501            .await
8502            .expect_err("wrong-projected live alias must not resolve as live-only");
8503        assert!(
8504            err.contains("identity runtime binding belongs to review:singleton"),
8505            "unexpected error: {err}"
8506        );
8507
8508        Ok(())
8509    }
8510
8511    /// Regression: under the meerkat 0.7.1 identity-first roster the runtime
8512    /// members are keyed `rt:{identity}:{generation}`, so a gateway-plane
8513    /// `mobkit/send_message` addressed to the bare durable identity (the
8514    /// only id the SDK hands out pre-burst) used to fail with
8515    /// `mob member not found`. Bare identities must bridge-resolve through
8516    /// the identity runtime — like console send — while an exact roster
8517    /// member id match keeps raw member-id semantics and wins over identity
8518    /// resolution.
8519    #[tokio::test]
8520    async fn send_message_resolves_bare_durable_identity_through_identity_bridge()
8521    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8522        let temp_dir = tempfile::tempdir()?;
8523        let runtime = Box::pin(
8524            UnifiedRuntime::builder()
8525                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8526                .module_config(MobKitConfig {
8527                    modules: Vec::new(),
8528                    discovery: DiscoverySpec {
8529                        namespace: "rpc-send-message-identity-bridge-test".to_string(),
8530                        modules: Vec::new(),
8531                    },
8532                    pre_spawn: Vec::new(),
8533                })
8534                .timeout(Duration::from_secs(5))
8535                .build(),
8536        )
8537        .await?;
8538
8539        // Identity-first roster shape: each durable identity is seated as
8540        // runtime member rt:{identity}:0; the bare identity is NOT a roster id.
8541        for (runtime_id, durable) in [
8542            ("rt:atlas-base-001:0", "atlas-base-001"),
8543            ("rt:draco-base-001:0", "draco-base-001"),
8544        ] {
8545            spawn_identity_projection_fixture(
8546                &runtime,
8547                SpawnMemberSpec::from_wire(
8548                    "worker".to_string(),
8549                    runtime_id.to_string(),
8550                    Some("You are a swarm base agent.".into()),
8551                    None,
8552                    None,
8553                )
8554                .with_labels(BTreeMap::from([(
8555                    "agent_identity".to_string(),
8556                    durable.to_string(),
8557                )])),
8558            )
8559            .await?;
8560        }
8561        // Precedence probe: a bare roster member whose id collides with a
8562        // registered durable identity.
8563        runtime
8564            .spawn(SpawnMemberSpec::from_wire(
8565                "worker".to_string(),
8566                "draco-base-001".to_string(),
8567                Some("You are the raw roster member.".into()),
8568                None,
8569                None,
8570            ))
8571            .await?;
8572
8573        let session_service = runtime
8574            .mob_runtime()
8575            .session_service()
8576            .cloned()
8577            .expect("test mob spec has a session service");
8578        let bridge: Arc<dyn crate::identity_first::SessionBridge> = Arc::new(
8579            crate::identity_first::MobSessionBridge::with_session_service(
8580                runtime.mob_handle(),
8581                session_service,
8582            ),
8583        );
8584        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8585            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8586            lease_provider: Arc::new(LocalLeaseProvider::new()),
8587            runtime_instance_id: "rpc-send-message-identity-bridge-test".to_string(),
8588            has_runtime_store: true,
8589            durability_policy: DurabilityPolicy::SyncWriteThrough,
8590            bridge: Some(bridge),
8591            default_timeout: None,
8592        })
8593        .with_runtime_services(crate::identity_first::AgentRuntimeServices::new(
8594            runtime.mob_handle(),
8595        ));
8596        for (durable, runtime_id) in [
8597            ("atlas-base-001", "rt:atlas-base-001:0"),
8598            ("draco-base-001", "rt:draco-base-001:0"),
8599        ] {
8600            let identity = AgentIdentity::parse(durable)?;
8601            let session_id = runtime
8602                .mob_handle()
8603                .resolve_bridge_session_id(&crate::member_comms_id::mob_member_id(runtime_id))
8604                .await
8605                .unwrap_or_else(meerkat_core::types::SessionId::new);
8606            identity_rt
8607                .register(
8608                    DurableAgentSpec {
8609                        identity: identity.clone(),
8610                        profile: meerkat_mob::ProfileName::from("worker"),
8611                        addressability: AgentAddressability::Addressable,
8612                        display_name: None,
8613                        labels: BTreeMap::new(),
8614                        context: None,
8615                        additional_instructions: Vec::new(),
8616                        initial_message: None,
8617                        runtime_mode_override: None,
8618                        backend: None,
8619                        binding: None,
8620                    },
8621                    IdentityLifecycleState::Active,
8622                    Some(ContinuityRecord {
8623                        identity: identity.clone(),
8624                        agent_runtime_id: AgentRuntimeId::parse(runtime_id)?,
8625                        session_id,
8626                        generation: ContinuityGeneration::new(0),
8627                        checkpoint_version: CheckpointVersion::new(0),
8628                    }),
8629                    Some(LeaseGrant {
8630                        identity,
8631                        fencing_token: FencingToken::new(1),
8632                        ttl: Duration::from_mins(1),
8633                    }),
8634                )
8635                .await;
8636        }
8637        let identity_ctx = IdentityFirstContext {
8638            runtime: Arc::new(identity_rt),
8639            roster_provider: Arc::new(EmptyRosterProvider),
8640            topology_provider: None,
8641            customizer: None,
8642            agent_memory_provider: None,
8643            mob_definition: None,
8644        };
8645
8646        let send = |id: u64, params: Value| {
8647            let runtime = &runtime;
8648            let identity_ctx = &identity_ctx;
8649            async move {
8650                let raw = handle_unified_rpc_json(
8651                    runtime,
8652                    &json!({
8653                        "jsonrpc": "2.0",
8654                        "id": id,
8655                        "method": "mobkit/send_message",
8656                        "params": params,
8657                    })
8658                    .to_string(),
8659                    Duration::from_secs(10),
8660                    None,
8661                    Some(identity_ctx),
8662                )
8663                .await;
8664                serde_json::from_str::<Value>(&raw)
8665            }
8666        };
8667
8668        // 1. Bare durable identity bridges to the rt:{identity}:{generation}
8669        //    member and reports the bridge session that took the delivery.
8670        let response = send(
8671            1,
8672            json!({ "member_id": "atlas-base-001", "message": "status check" }),
8673        )
8674        .await?;
8675        assert!(
8676            response["error"].is_null(),
8677            "bare identity send must bridge-resolve: {response:#?}"
8678        );
8679        assert_eq!(response["result"]["accepted"], json!(true));
8680        assert_eq!(response["result"]["member_id"], json!("atlas-base-001"));
8681        let atlas_session = runtime
8682            .mob_handle()
8683            .resolve_bridge_session_id(&crate::member_comms_id::mob_member_id(
8684                "rt:atlas-base-001:0",
8685            ))
8686            .await
8687            .expect("atlas runtime member has a bridge session after send")
8688            .to_string();
8689        assert_eq!(response["result"]["session_id"], json!(atlas_session));
8690
8691        // 2. Steer rides the same bridge resolution.
8692        let response = send(
8693            2,
8694            json!({
8695                "member_id": "atlas-base-001",
8696                "message": "steer: stand down",
8697                "handling_mode": "steer",
8698            }),
8699        )
8700        .await?;
8701        assert!(
8702            response["error"].is_null(),
8703            "bare identity steer must bridge-resolve: {response:#?}"
8704        );
8705        assert_eq!(response["result"]["accepted"], json!(true));
8706
8707        // 3. Precedence: an exact roster member id wins over identity
8708        //    resolution — the bare member takes the delivery, not the
8709        //    identity's rt:draco-base-001:0 binding.
8710        let response = send(
8711            3,
8712            json!({ "member_id": "draco-base-001", "message": "raw roster delivery" }),
8713        )
8714        .await?;
8715        assert!(
8716            response["error"].is_null(),
8717            "exact roster member send must keep raw semantics: {response:#?}"
8718        );
8719        assert_eq!(response["result"]["accepted"], json!(true));
8720        let draco_raw_session = runtime
8721            .mob_handle()
8722            .resolve_bridge_session_id(&crate::member_comms_id::mob_member_id("draco-base-001"))
8723            .await
8724            .expect("bare draco member has a bridge session after send")
8725            .to_string();
8726        assert_eq!(response["result"]["session_id"], json!(draco_raw_session));
8727        if let Some(draco_rt_session) = runtime
8728            .mob_handle()
8729            .resolve_bridge_session_id(&crate::member_comms_id::mob_member_id(
8730                "rt:draco-base-001:0",
8731            ))
8732            .await
8733        {
8734            assert_ne!(
8735                response["result"]["session_id"],
8736                json!(draco_rt_session.to_string()),
8737                "exact member id match must not be shadowed by identity resolution"
8738            );
8739        }
8740
8741        // 4. Unknown ids keep raw member-not-found semantics.
8742        let response = send(
8743            4,
8744            json!({ "member_id": "phantom-base-999", "message": "nobody home" }),
8745        )
8746        .await?;
8747        assert_eq!(response["error"]["code"], json!(-32000), "{response:#?}");
8748        let message = response["error"]["message"]
8749            .as_str()
8750            .expect("error message");
8751        assert!(
8752            message.starts_with("send_message failed:"),
8753            "unexpected error message: {message}"
8754        );
8755
8756        Ok(())
8757    }
8758
8759    /// Regression: the member-state wire vocabulary is lowercase
8760    /// (`"active"`/`"retiring"`, matching the published SDK constants) on
8761    /// BOTH member-state surfaces — the roster member rows and the
8762    /// identity-first status RPC. The identity surface used to Debug-format
8763    /// the lifecycle state (`"Active"`), so consumers comparing across the
8764    /// two surfaces broke on casing.
8765    #[tokio::test]
8766    async fn member_state_wire_vocabulary_is_lowercase_on_both_surfaces()
8767    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8768        let temp_dir = tempfile::tempdir()?;
8769        let runtime = Box::pin(
8770            UnifiedRuntime::builder()
8771                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8772                .module_config(MobKitConfig {
8773                    modules: Vec::new(),
8774                    discovery: DiscoverySpec {
8775                        namespace: "rpc-state-vocabulary-test".to_string(),
8776                        modules: Vec::new(),
8777                    },
8778                    pre_spawn: Vec::new(),
8779                })
8780                .timeout(Duration::from_secs(5))
8781                .build(),
8782        )
8783        .await?;
8784        runtime
8785            .spawn(SpawnMemberSpec::from_wire(
8786                "worker".to_string(),
8787                "worker-one".to_string(),
8788                None,
8789                None,
8790                None,
8791            ))
8792            .await?;
8793
8794        // Surface 1: roster member rows.
8795        let raw = handle_unified_rpc_json(
8796            &runtime,
8797            &json!({
8798                "jsonrpc": "2.0",
8799                "id": 1,
8800                "method": "mobkit/get_member",
8801                "params": { "member_id": "worker-one" },
8802            })
8803            .to_string(),
8804            Duration::from_secs(5),
8805            None,
8806            None,
8807        )
8808        .await;
8809        let response: Value = serde_json::from_str(&raw)?;
8810        assert_eq!(
8811            response["result"]["state"],
8812            json!("active"),
8813            "member rows must speak the lowercase SDK vocabulary: {response:#?}"
8814        );
8815
8816        // Surface 2: identity-first status RPC.
8817        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8818            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8819            lease_provider: Arc::new(LocalLeaseProvider::new()),
8820            runtime_instance_id: "rpc-state-vocabulary-test".to_string(),
8821            has_runtime_store: true,
8822            durability_policy: DurabilityPolicy::SyncWriteThrough,
8823            bridge: None,
8824            default_timeout: None,
8825        });
8826        let identity = AgentIdentity::parse("review:singleton")?;
8827        identity_rt
8828            .register(
8829                DurableAgentSpec {
8830                    identity: identity.clone(),
8831                    profile: meerkat_mob::ProfileName::from("worker"),
8832                    addressability: AgentAddressability::Addressable,
8833                    display_name: None,
8834                    labels: BTreeMap::new(),
8835                    context: None,
8836                    additional_instructions: Vec::new(),
8837                    initial_message: None,
8838                    runtime_mode_override: None,
8839                    backend: None,
8840                    binding: None,
8841                },
8842                IdentityLifecycleState::Active,
8843                Some(ContinuityRecord {
8844                    identity: identity.clone(),
8845                    agent_runtime_id: AgentRuntimeId::parse("rt:review:singleton:0")?,
8846                    session_id: meerkat_core::types::SessionId::new(),
8847                    generation: ContinuityGeneration::new(0),
8848                    checkpoint_version: CheckpointVersion::new(0),
8849                }),
8850                Some(LeaseGrant {
8851                    identity,
8852                    fencing_token: FencingToken::new(1),
8853                    ttl: Duration::from_mins(1),
8854                }),
8855            )
8856            .await;
8857        let identity_ctx = IdentityFirstContext {
8858            runtime: Arc::new(identity_rt),
8859            roster_provider: Arc::new(EmptyRosterProvider),
8860            topology_provider: None,
8861            customizer: None,
8862            agent_memory_provider: None,
8863            mob_definition: None,
8864        };
8865        let raw = handle_unified_rpc_json(
8866            &runtime,
8867            &json!({
8868                "jsonrpc": "2.0",
8869                "id": 2,
8870                "method": "mobkit/status_identity",
8871                "params": { "identity": "review:singleton" },
8872            })
8873            .to_string(),
8874            Duration::from_secs(5),
8875            None,
8876            Some(&identity_ctx),
8877        )
8878        .await;
8879        let response: Value = serde_json::from_str(&raw)?;
8880        assert_eq!(
8881            response["result"]["state"],
8882            json!("active"),
8883            "identity status must speak the same lowercase vocabulary: {response:#?}"
8884        );
8885
8886        Ok(())
8887    }
8888
8889    /// Regression: `mobkit/send_message` precedence is resolved from a
8890    /// point-in-time roster probe. When a roster member whose id collides
8891    /// with a registered durable identity is transiently absent mid-
8892    /// reconcile (retire completes before the replacement spawn lands), the
8893    /// send must NOT silently fall through to the identity bridge and land
8894    /// in a different agent's conversation — membership declared in the
8895    /// reconcile baseline pins raw member-id semantics, surfacing the mob's
8896    /// own member-not-found error instead.
8897    #[tokio::test]
8898    async fn send_message_pins_baseline_member_over_identity_fallback()
8899    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
8900        let temp_dir = tempfile::tempdir()?;
8901        let runtime = Box::pin(
8902            UnifiedRuntime::builder()
8903                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
8904                .module_config(MobKitConfig {
8905                    modules: Vec::new(),
8906                    discovery: DiscoverySpec {
8907                        namespace: "rpc-send-message-baseline-pin-test".to_string(),
8908                        modules: Vec::new(),
8909                    },
8910                    pre_spawn: Vec::new(),
8911                })
8912                .timeout(Duration::from_secs(5))
8913                .build(),
8914        )
8915        .await?;
8916
8917        // Identity-first member backing the durable identity, plus a raw
8918        // roster member with the colliding bare id.
8919        spawn_identity_projection_fixture(
8920            &runtime,
8921            SpawnMemberSpec::from_wire(
8922                "worker".to_string(),
8923                "rt:draco-base-001:0".to_string(),
8924                None,
8925                None,
8926                None,
8927            ),
8928        )
8929        .await?;
8930        runtime
8931            .spawn(
8932                SpawnMemberSpec::from_wire(
8933                    "worker".to_string(),
8934                    "draco-base-001".to_string(),
8935                    None,
8936                    None,
8937                    None,
8938                )
8939                // This regression needs roster membership, not an autonomous
8940                // kickoff. Keep the fixture quiescent so its deliberate retire
8941                // is the sole teardown owner and the test exercises baseline
8942                // dispatch precedence rather than kickoff/retire interleaving.
8943                .with_runtime_mode(meerkat_mob::MobRuntimeMode::TurnDriven),
8944            )
8945            .await?;
8946
8947        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
8948            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
8949            lease_provider: Arc::new(LocalLeaseProvider::new()),
8950            runtime_instance_id: "rpc-send-message-baseline-pin-test".to_string(),
8951            has_runtime_store: true,
8952            durability_policy: DurabilityPolicy::SyncWriteThrough,
8953            bridge: None,
8954            default_timeout: None,
8955        });
8956        let identity = AgentIdentity::parse("draco-base-001")?;
8957        identity_rt
8958            .register(
8959                DurableAgentSpec {
8960                    identity: identity.clone(),
8961                    profile: meerkat_mob::ProfileName::from("worker"),
8962                    addressability: AgentAddressability::Addressable,
8963                    display_name: None,
8964                    labels: BTreeMap::new(),
8965                    context: None,
8966                    additional_instructions: Vec::new(),
8967                    initial_message: None,
8968                    runtime_mode_override: None,
8969                    backend: None,
8970                    binding: None,
8971                },
8972                IdentityLifecycleState::Active,
8973                Some(ContinuityRecord {
8974                    identity: identity.clone(),
8975                    agent_runtime_id: AgentRuntimeId::parse("rt:draco-base-001:0")?,
8976                    session_id: meerkat_core::types::SessionId::new(),
8977                    generation: ContinuityGeneration::new(0),
8978                    checkpoint_version: CheckpointVersion::new(0),
8979                }),
8980                Some(LeaseGrant {
8981                    identity,
8982                    fencing_token: FencingToken::new(1),
8983                    ttl: Duration::from_mins(1),
8984                }),
8985            )
8986            .await;
8987        let identity_ctx = IdentityFirstContext {
8988            runtime: Arc::new(identity_rt),
8989            roster_provider: Arc::new(EmptyRosterProvider),
8990            topology_provider: None,
8991            customizer: None,
8992            agent_memory_provider: None,
8993            mob_definition: None,
8994        };
8995
8996        // The raw roster member is part of the declared baseline ...
8997        runtime
8998            .mob_runtime()
8999            .set_baseline_member_specs(vec![SpawnMemberSpec::new(
9000                meerkat_mob::ProfileName::from("worker"),
9001                meerkat_mob::AgentIdentity::from("draco-base-001"),
9002            )])
9003            .await;
9004        // ... and is transiently absent (reconcile retired it; the
9005        // replacement spawn has not landed yet).
9006        runtime
9007            .mob_handle()
9008            .retire(meerkat_mob::AgentIdentity::from("draco-base-001"))
9009            .await?;
9010
9011        let raw = handle_unified_rpc_json(
9012            &runtime,
9013            &json!({
9014                "jsonrpc": "2.0",
9015                "id": 1,
9016                "method": "mobkit/send_message",
9017                "params": { "member_id": "draco-base-001", "message": "mid-reconcile send" },
9018            })
9019            .to_string(),
9020            Duration::from_secs(10),
9021            None,
9022            Some(&identity_ctx),
9023        )
9024        .await;
9025        let response: Value = serde_json::from_str(&raw)?;
9026        assert_eq!(
9027            response["error"]["code"],
9028            json!(-32000),
9029            "transiently-absent baseline member must keep raw member-id semantics \
9030             instead of silently delivering through the identity bridge: {response:#?}"
9031        );
9032        let message = response["error"]["message"]
9033            .as_str()
9034            .expect("error message");
9035        assert!(
9036            message.starts_with("send_message failed:"),
9037            "unexpected error message: {message}"
9038        );
9039
9040        Ok(())
9041    }
9042
9043    /// An explicitly supplied identity context is authoritative even when it
9044    /// is not attached to the unified runtime. Generated aliases must still
9045    /// be generation-pinned before cross-mob or helper-fork lower-plane work.
9046    #[tokio::test]
9047    async fn separate_identity_context_pins_generated_alias_for_cross_mob_and_fork()
9048    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
9049        let temp_dir = tempfile::tempdir()?;
9050        let runtime = Arc::new(
9051            Box::pin(
9052                UnifiedRuntime::builder()
9053                    .mob_spec(rpc_test_mob_spec(&temp_dir)?)
9054                    .module_config(MobKitConfig {
9055                        modules: Vec::new(),
9056                        discovery: DiscoverySpec {
9057                            namespace: "rpc-separate-identity-context-test".to_string(),
9058                            modules: Vec::new(),
9059                        },
9060                        pre_spawn: Vec::new(),
9061                    })
9062                    .timeout(Duration::from_secs(5))
9063                    .build(),
9064            )
9065            .await?,
9066        );
9067        let stale_alias = "rt:review:separate:0";
9068        let current_alias = "rt:review:separate:1";
9069        spawn_identity_projection_fixture(
9070            &runtime,
9071            SpawnMemberSpec::from_wire(
9072                "worker".to_string(),
9073                stale_alias.to_string(),
9074                None,
9075                None,
9076                None,
9077            )
9078            .with_labels(BTreeMap::from([(
9079                "agent_identity".to_string(),
9080                "review:separate".to_string(),
9081            )])),
9082        )
9083        .await?;
9084
9085        let identity_rt = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
9086            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
9087            lease_provider: Arc::new(LocalLeaseProvider::new()),
9088            runtime_instance_id: "rpc-separate-identity-context-test".to_string(),
9089            has_runtime_store: true,
9090            durability_policy: DurabilityPolicy::SyncWriteThrough,
9091            bridge: None,
9092            default_timeout: None,
9093        }));
9094        let identity = AgentIdentity::parse("review:separate")?;
9095        identity_rt
9096            .register(
9097                rpc_durable_spec(identity.as_str(), "worker"),
9098                IdentityLifecycleState::Active,
9099                Some(ContinuityRecord {
9100                    identity: identity.clone(),
9101                    agent_runtime_id: AgentRuntimeId::parse(current_alias)?,
9102                    session_id: meerkat_core::types::SessionId::new(),
9103                    generation: ContinuityGeneration::new(1),
9104                    checkpoint_version: CheckpointVersion::new(0),
9105                }),
9106                Some(LeaseGrant {
9107                    identity,
9108                    fencing_token: FencingToken::new(1),
9109                    ttl: Duration::from_mins(1),
9110                }),
9111            )
9112            .await;
9113        let identity_ctx = IdentityFirstContext {
9114            runtime: identity_rt,
9115            roster_provider: Arc::new(EmptyRosterProvider),
9116            topology_provider: None,
9117            customizer: None,
9118            agent_memory_provider: None,
9119            mob_definition: None,
9120        };
9121        assert!(
9122            runtime.identity_runtime().is_none(),
9123            "the regression requires a context supplied only at dispatch"
9124        );
9125
9126        for (method, params) in [
9127            (
9128                "mobkit/cross_mob/wire",
9129                json!({
9130                    "local_member_id": stale_alias,
9131                    "remote_member_id": "remote-worker",
9132                    "remote_mob_id": "remote-mob",
9133                }),
9134            ),
9135            (
9136                "mobkit/fork_helper",
9137                json!({
9138                    "source_member_id": stale_alias,
9139                    "agent_identity": "fork-probe",
9140                    "task": "prove stale source rejection",
9141                }),
9142            ),
9143        ] {
9144            let response: Value = serde_json::from_str(
9145                &super::handle_unified_rpc_json_arc(
9146                    &runtime,
9147                    &json!({
9148                        "jsonrpc": "2.0",
9149                        "id": method,
9150                        "method": method,
9151                        "params": params,
9152                    })
9153                    .to_string(),
9154                    Duration::from_secs(5),
9155                    None,
9156                    Some(&identity_ctx),
9157                )
9158                .await,
9159            )?;
9160            assert_eq!(response["error"]["code"], json!(-32000), "{response:#?}");
9161            assert!(
9162                response["error"]["message"]
9163                    .as_str()
9164                    .is_some_and(|message| message.contains("stale runtime alias")),
9165                "{method} must reject through identity authority before raw fallthrough: {response:#?}"
9166            );
9167        }
9168
9169        assert!(
9170            runtime
9171                .mob_handle()
9172                .list_members_including_retiring()
9173                .await
9174                .iter()
9175                .any(|member| {
9176                    crate::member_comms_id::runtime_alias_str(member.agent_identity.as_str())
9177                        == stale_alias
9178                }),
9179            "the raw stale member remains present, proving rejection was not member-not-found"
9180        );
9181        let _ = runtime.mob_handle().stop().await;
9182        Ok(())
9183    }
9184
9185    #[tokio::test]
9186    async fn embedded_rpc_uses_runtime_owned_identity_context_when_argument_is_absent()
9187    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
9188        let temp_dir = tempfile::tempdir()?;
9189        let mut runtime = Box::pin(
9190            UnifiedRuntime::builder()
9191                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
9192                .module_config(MobKitConfig {
9193                    modules: Vec::new(),
9194                    discovery: DiscoverySpec {
9195                        namespace: "rpc-owned-identity-context-test".to_string(),
9196                        modules: Vec::new(),
9197                    },
9198                    pre_spawn: Vec::new(),
9199                })
9200                .timeout(Duration::from_secs(5))
9201                .build(),
9202        )
9203        .await?;
9204        let stale_alias = "rt:review:owned-context:0";
9205        let current_alias = "rt:review:owned-context:1";
9206        spawn_identity_projection_fixture(
9207            &runtime,
9208            SpawnMemberSpec::from_wire(
9209                "worker".to_string(),
9210                stale_alias.to_string(),
9211                None,
9212                None,
9213                None,
9214            )
9215            .with_labels(BTreeMap::from([(
9216                "agent_identity".to_string(),
9217                "review:owned-context".to_string(),
9218            )])),
9219        )
9220        .await?;
9221
9222        let identity_runtime = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
9223            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
9224            lease_provider: Arc::new(LocalLeaseProvider::new()),
9225            runtime_instance_id: "rpc-owned-identity-context-test".to_string(),
9226            has_runtime_store: true,
9227            durability_policy: DurabilityPolicy::SyncWriteThrough,
9228            bridge: None,
9229            default_timeout: None,
9230        }));
9231        let identity = AgentIdentity::parse("review:owned-context")?;
9232        identity_runtime
9233            .register(
9234                rpc_durable_spec(identity.as_str(), "worker"),
9235                IdentityLifecycleState::Active,
9236                Some(ContinuityRecord {
9237                    identity: identity.clone(),
9238                    agent_runtime_id: AgentRuntimeId::parse(current_alias)?,
9239                    session_id: meerkat_core::types::SessionId::new(),
9240                    generation: ContinuityGeneration::new(1),
9241                    checkpoint_version: CheckpointVersion::new(0),
9242                }),
9243                None,
9244            )
9245            .await;
9246        runtime.attach_identity_first_context(Arc::new(
9247            crate::identity_first::IdentityFirstRuntimeContext::new(
9248                identity_runtime,
9249                Arc::new(EmptyRosterProvider),
9250                None,
9251                None,
9252                None,
9253            ),
9254        ));
9255        let runtime = Arc::new(runtime);
9256
9257        let list: Value = serde_json::from_str(
9258            &super::handle_unified_rpc_json_arc(
9259                &runtime,
9260                &json!({
9261                    "jsonrpc": "2.0",
9262                    "id": 1,
9263                    "method": "mobkit/list_members",
9264                    "params": {},
9265                })
9266                .to_string(),
9267                Duration::from_secs(5),
9268                None,
9269                None,
9270            )
9271            .await,
9272        )?;
9273        assert_eq!(list["result"], json!([]), "{list:#?}");
9274
9275        let get: Value = serde_json::from_str(
9276            &super::handle_unified_rpc_json_arc(
9277                &runtime,
9278                &json!({
9279                    "jsonrpc": "2.0",
9280                    "id": 2,
9281                    "method": "mobkit/get_member",
9282                    "params": {"member_id": stale_alias},
9283                })
9284                .to_string(),
9285                Duration::from_secs(5),
9286                None,
9287                None,
9288            )
9289            .await,
9290        )?;
9291        assert_eq!(
9292            get["error"]["data"]["kind"],
9293            json!("stale_identity_runtime_binding"),
9294            "{get:#?}"
9295        );
9296
9297        runtime.shutdown().await;
9298        Ok(())
9299    }
9300
9301    /// Capturing durable ownership is a one-way authority decision for a
9302    /// request. If the identity disappears before the later status probe,
9303    /// the live projection must not resurrect it through compatibility mode.
9304    #[tokio::test]
9305    async fn durable_ownership_observed_before_unknown_identity_never_live_fallbacks()
9306    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
9307        let temp_dir = tempfile::tempdir()?;
9308        let runtime = Box::pin(
9309            UnifiedRuntime::builder()
9310                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
9311                .module_config(MobKitConfig {
9312                    modules: Vec::new(),
9313                    discovery: DiscoverySpec {
9314                        namespace: "rpc-durable-delete-race-test".to_string(),
9315                        modules: Vec::new(),
9316                    },
9317                    pre_spawn: Vec::new(),
9318                })
9319                .timeout(Duration::from_secs(1))
9320                .build(),
9321        )
9322        .await?;
9323        let durable_identity = "review:delete-race";
9324        let runtime_alias = "rt:review:delete-race:0";
9325        spawn_identity_projection_fixture(
9326            &runtime,
9327            SpawnMemberSpec::from_wire(
9328                "worker".to_string(),
9329                runtime_alias.to_string(),
9330                None,
9331                None,
9332                None,
9333            )
9334            .with_labels(BTreeMap::from([(
9335                "agent_identity".to_string(),
9336                durable_identity.to_string(),
9337            )])),
9338        )
9339        .await?;
9340
9341        let identity_rt = IdentityRuntime::new(IdentityRuntimeConfig {
9342            continuity_store: Arc::new(LocalContinuityStore::in_memory()?),
9343            lease_provider: Arc::new(LocalLeaseProvider::new()),
9344            runtime_instance_id: "rpc-durable-delete-race-test".to_string(),
9345            has_runtime_store: true,
9346            durability_policy: DurabilityPolicy::SyncWriteThrough,
9347            bridge: None,
9348            default_timeout: None,
9349        });
9350        let identity = AgentIdentity::parse(durable_identity)?;
9351        identity_rt
9352            .register(
9353                rpc_durable_spec(durable_identity, "worker"),
9354                IdentityLifecycleState::Active,
9355                Some(ContinuityRecord {
9356                    identity: identity.clone(),
9357                    agent_runtime_id: AgentRuntimeId::parse(runtime_alias)?,
9358                    session_id: meerkat_core::types::SessionId::new(),
9359                    generation: ContinuityGeneration::new(0),
9360                    checkpoint_version: CheckpointVersion::new(0),
9361                }),
9362                Some(LeaseGrant {
9363                    identity: identity.clone(),
9364                    fencing_token: FencingToken::new(1),
9365                    ttl: Duration::from_mins(1),
9366                }),
9367            )
9368            .await;
9369
9370        let target =
9371            resolve_rpc_identity_control_target(&runtime, &identity_rt, durable_identity).await?;
9372        assert!(
9373            target.was_registered,
9374            "resolution must capture durable ownership"
9375        );
9376        assert!(target.live.is_some(), "the race requires a live projection");
9377
9378        identity_rt
9379            .remove(&identity)
9380            .await
9381            .expect("simulate a concurrent durable delete after resolution");
9382        assert!(matches!(
9383            identity_rt.status(&identity).await,
9384            Err(crate::identity_first::IdentityRuntimeError::UnknownIdentity(_))
9385        ));
9386        assert!(
9387            !super::rpc_live_only_fallback_allowed(&target, durable_identity),
9388            "a later UnknownIdentity must preserve the observed durable delete"
9389        );
9390
9391        let mut live_only = target.clone();
9392        live_only.was_registered = false;
9393        assert!(
9394            !super::rpc_live_only_fallback_allowed(&live_only, durable_identity),
9395            "a resolved generated alias must remain identity-owned even when registration vanished"
9396        );
9397        assert!(
9398            !super::rpc_live_only_fallback_allowed(&live_only, runtime_alias),
9399            "generated aliases never enter raw/live-only fallback"
9400        );
9401        live_only
9402            .live
9403            .as_mut()
9404            .expect("live projection")
9405            .runtime_member_id = "legacy-review-member".to_string();
9406        assert!(
9407            super::rpc_live_only_fallback_allowed(&live_only, durable_identity),
9408            "genuine bare live-only identities retain compatibility fallback"
9409        );
9410
9411        let _ = runtime.mob_handle().stop().await;
9412        Ok(())
9413    }
9414
9415    /// A current identity-owned runtime alias must resolve through the durable
9416    /// identity authority. Raw mob retirement would leave continuity, lease,
9417    /// and lifecycle state stale even though the projected member disappeared.
9418    #[tokio::test]
9419    async fn retire_member_current_runtime_alias_uses_identity_authority()
9420    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
9421        let temp_dir = tempfile::tempdir()?;
9422        let runtime = Box::pin(
9423            UnifiedRuntime::builder()
9424                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
9425                .module_config(MobKitConfig {
9426                    modules: Vec::new(),
9427                    discovery: DiscoverySpec {
9428                        namespace: "rpc-owned-retire-test".to_string(),
9429                        modules: Vec::new(),
9430                    },
9431                    pre_spawn: Vec::new(),
9432                })
9433                .timeout(Duration::from_secs(5))
9434                .build(),
9435        )
9436        .await?;
9437        let runtime_alias = "rt:review:owned:0";
9438        spawn_identity_projection_fixture(
9439            &runtime,
9440            SpawnMemberSpec::from_wire(
9441                "worker".to_string(),
9442                runtime_alias.to_string(),
9443                None,
9444                None,
9445                None,
9446            ),
9447        )
9448        .await?;
9449
9450        let continuity_store = Arc::new(LocalContinuityStore::in_memory()?);
9451        let identity_rt = Arc::new(IdentityRuntime::new(IdentityRuntimeConfig {
9452            continuity_store,
9453            lease_provider: Arc::new(LocalLeaseProvider::new()),
9454            runtime_instance_id: "rpc-owned-retire-test".to_string(),
9455            has_runtime_store: true,
9456            durability_policy: DurabilityPolicy::SyncWriteThrough,
9457            bridge: None,
9458            default_timeout: None,
9459        }));
9460        let identity = AgentIdentity::parse("review:owned")?;
9461        identity_rt
9462            .register(
9463                DurableAgentSpec {
9464                    identity: identity.clone(),
9465                    profile: meerkat_mob::ProfileName::from("worker"),
9466                    addressability: AgentAddressability::Addressable,
9467                    display_name: None,
9468                    labels: BTreeMap::new(),
9469                    context: None,
9470                    additional_instructions: Vec::new(),
9471                    initial_message: None,
9472                    runtime_mode_override: None,
9473                    backend: None,
9474                    binding: None,
9475                },
9476                IdentityLifecycleState::Active,
9477                Some(ContinuityRecord {
9478                    identity: identity.clone(),
9479                    agent_runtime_id: AgentRuntimeId::parse(runtime_alias)?,
9480                    session_id: meerkat_core::types::SessionId::new(),
9481                    generation: ContinuityGeneration::new(0),
9482                    checkpoint_version: CheckpointVersion::new(0),
9483                }),
9484                Some(LeaseGrant {
9485                    identity: identity.clone(),
9486                    fencing_token: FencingToken::new(1),
9487                    ttl: Duration::from_mins(1),
9488                }),
9489            )
9490            .await;
9491        let identity_ctx = IdentityFirstContext {
9492            runtime: Arc::clone(&identity_rt),
9493            roster_provider: Arc::new(EmptyRosterProvider),
9494            topology_provider: None,
9495            customizer: None,
9496            agent_memory_provider: None,
9497            mob_definition: None,
9498        };
9499
9500        let raw = handle_unified_rpc_json(
9501            &runtime,
9502            &json!({
9503                "jsonrpc": "2.0",
9504                "id": 1,
9505                "method": "mobkit/retire_member",
9506                "params": { "member_id": runtime_alias },
9507            })
9508            .to_string(),
9509            Duration::from_secs(5),
9510            None,
9511            Some(&identity_ctx),
9512        )
9513        .await;
9514        let response: Value = serde_json::from_str(&raw)?;
9515        assert!(
9516            response["error"].is_null(),
9517            "identity-owned retire_member failed: {response:#?}"
9518        );
9519        assert_eq!(response["result"]["identity_first"], json!(true));
9520        assert_eq!(
9521            identity_rt.status(&identity).await?.state,
9522            IdentityLifecycleState::Retiring,
9523            "the durable authority, not only the Mob projection, must observe retirement"
9524        );
9525        assert!(
9526            runtime
9527                .mob_handle()
9528                .list_members_including_retiring()
9529                .await
9530                .iter()
9531                .any(|entry| {
9532                    crate::member_comms_id::runtime_alias_str(entry.agent_identity.as_str())
9533                        == runtime_alias
9534                }),
9535            "a bridge-less identity test proves the raw Mob fallback was not invoked"
9536        );
9537
9538        Ok(())
9539    }
9540
9541    /// Regression (meerkat 0.7.1 migration): an idle member's session
9542    /// machine sits in `Stopped`, where the archive step's final `Retire`
9543    /// transition is guard-rejected ("disposal completed but ArchiveSession
9544    /// failed: … guard rejected transition from Stopped for input::Retire").
9545    /// `mobkit/retire_member` and `mobkit/respawn_member` must treat that
9546    /// bookkeeping failure as completed cleanup instead of surfacing -32000,
9547    /// and a recovered respawn must leave an active replacement — never a
9548    /// member wedged in `retiring` with its session disposed.
9549    #[tokio::test]
9550    async fn retire_and_respawn_rpcs_succeed_for_idle_member()
9551    -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
9552        let temp_dir = tempfile::tempdir()?;
9553        let runtime = Box::pin(
9554            UnifiedRuntime::builder()
9555                .mob_spec(rpc_test_mob_spec(&temp_dir)?)
9556                .module_config(MobKitConfig {
9557                    modules: Vec::new(),
9558                    discovery: DiscoverySpec {
9559                        namespace: "rpc-idle-lifecycle-test".to_string(),
9560                        modules: Vec::new(),
9561                    },
9562                    pre_spawn: Vec::new(),
9563                })
9564                .timeout(Duration::from_secs(5))
9565                .build(),
9566        )
9567        .await?;
9568
9569        for member in ["worker-one", "worker-two"] {
9570            runtime
9571                .spawn(SpawnMemberSpec::from_wire(
9572                    "worker".to_string(),
9573                    member.to_string(),
9574                    None,
9575                    None,
9576                    None,
9577                ))
9578                .await?;
9579        }
9580
9581        let send = |id: u64, method: &'static str, params: Value| {
9582            let runtime = &runtime;
9583            async move {
9584                let raw = handle_unified_rpc_json(
9585                    runtime,
9586                    &json!({
9587                        "jsonrpc": "2.0",
9588                        "id": id,
9589                        "method": method,
9590                        "params": params,
9591                    })
9592                    .to_string(),
9593                    Duration::from_secs(10),
9594                    None,
9595                    None,
9596                )
9597                .await;
9598                serde_json::from_str::<Value>(&raw)
9599            }
9600        };
9601
9602        // Retire an idle member: must report accepted and remove the member.
9603        let response = send(
9604            1,
9605            "mobkit/retire_member",
9606            json!({"member_id": "worker-one"}),
9607        )
9608        .await?;
9609        assert!(
9610            response["error"].is_null(),
9611            "retire_member must succeed for an idle member: {response:#?}"
9612        );
9613        assert_eq!(response["result"]["accepted"], json!(true));
9614        assert!(
9615            !runtime
9616                .mob_handle()
9617                .list_members_including_retiring()
9618                .await
9619                .iter()
9620                .any(|entry| entry.agent_identity.as_str() == "worker-one"),
9621            "retired member must leave the roster"
9622        );
9623
9624        // Respawn an idle member: must report accepted and leave an active
9625        // (not retiring) replacement in the roster.
9626        let response = send(
9627            2,
9628            "mobkit/respawn_member",
9629            json!({"member_id": "worker-two"}),
9630        )
9631        .await?;
9632        assert!(
9633            response["error"].is_null(),
9634            "respawn_member must succeed for an idle member: {response:#?}"
9635        );
9636        assert_eq!(response["result"]["accepted"], json!(true));
9637        let members = runtime.mob_handle().list_members_including_retiring().await;
9638        let worker_two = members
9639            .iter()
9640            .find(|entry| entry.agent_identity.as_str() == "worker-two")
9641            .expect("respawned member must remain in the roster");
9642        assert_eq!(
9643            worker_two.status,
9644            meerkat_mob::MobMemberStatus::Active,
9645            "respawned member must be active, not wedged in retiring"
9646        );
9647
9648        Ok(())
9649    }
9650}