Skip to content
Portal Control Protocol

Learning Runtime

The learning runtime records how PCP capabilities are used and scores how well adapters handle specific apps. This data stays local to the device and is never transmitted. It serves two purposes: improving adapter selection over time and generating onboarding suggestions for new users.

Invocation history is stored as append-only JSONL at ~/.local/share/portal/learning/history.jsonl. Each line is a single JSON record:

{
"timestamp": "2026-05-10T14:22:01.456Z",
"app_id": "org.gnome.Evolution",
"capability_id": "email.move",
"params_hash": "a3f2b1c4",
"success": true,
"latency_ms": 142
}

Fields:

  • timestamp — when the invocation occurred
  • app_id — the desktop file ID of the target app
  • capability_id — which PCP capability was invoked
  • params_hash — truncated hash of the invocation parameters (for grouping similar calls without storing user data)
  • success — whether the adapter completed without error
  • latency_ms — wall clock time from dispatch to completion

The JSONL format is intentionally simple. It supports tail -f for live debugging, standard text processing tools for analysis, and atomic appends that never corrupt the file on crash.

All learning data is local-only. No invocation records, effectiveness scores, or usage patterns are transmitted off the device. The params_hash field lets the system group similar invocations without storing the actual parameters.

Every capability invocation writes a record to the history store. The write is asynchronous and non-blocking; a slow disk does not delay the capability execution path.

Records are queryable through the learning.history.query capability:

  • Filter by app_id to see all interactions with a specific app
  • Filter by capability_id to see how often a capability is used
  • Filter by since timestamp for time-bounded analysis
  • limit to cap result count

This query capability is useful for diagnostics and for the suggestion engine.

The learning runtime tracks how well each adapter performs for each app. An adapter that reliably handles a specific app gets a higher score and is preferred during adapter selection.

/// Tracks adapter effectiveness over time.
/// Persisted by the Context Manager crate across reboots.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdapterEffectivenessScore {
pub adapter_id: String,
pub app_desktop_id: String,
pub total_invocations: u64,
pub successful_invocations: u64,
pub failed_invocations: u64,
pub avg_execution_time_ms: f64,
pub last_success: Option<DateTime<Utc>>,
pub last_failure: Option<DateTime<Utc>>,
}

Scores range from 0.0 to 1.0:

Range Meaning
0.0 - 0.3 Poor match. Adapter rarely works for this app.
0.3 - 0.7 Adequate. Adapter works but may miss edge cases.
0.7 - 1.0 Strong match. Adapter reliably handles this app.
pub fn effectiveness_score(s: &AdapterEffectivenessScore) -> f64 {
if s.total_invocations == 0 {
return 0.5; // Neutral for untested adapters
}
let success_rate = s.successful_invocations as f64 / s.total_invocations as f64;
// Weight recent performance more heavily
let recency_factor = match s.last_success {
Some(ts) if ts > Utc::now() - Duration::from_days(7) => 1.2,
Some(_) => 0.8,
None => 0.5,
};
(success_rate * recency_factor).clamp(0.0, 1.0)
}

The recency factor means a success in the last week counts 1.2x, while older successes count 0.8x. This ensures that regressions (an adapter breaking after an app update) are detected faster than they would be under a simple cumulative average.

During adapter selection for a given app, PCP ranks adapters by their effectiveness score and picks the highest. If an adapter’s score drops below 0.3, it is deprioritized but not retired. The GenericAdapter takes over as the fallback. Retiring adapters entirely would be premature because app updates or PCP fixes might restore functionality.

Scores persist across reboots via the Context Manager crate. PCP provides the data; Context Manager serializes and loads it on boot.

The learning runtime also records which capabilities are actually used, independent of adapter performance:

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityUsageRecord {
pub capability_id: String,
pub app_id: AppId,
pub invocation_count: u64,
pub last_invoked: DateTime<Utc>,
pub avg_execution_time_ms: f64,
pub domain: Domain,
}

This data serves two purposes. First, it informs adapter development priorities: if a capability is invoked frequently but its adapter has a low effectiveness score, that is a signal to improve the adapter. Second, capabilities invoked fewer than five times in 30 days are candidates for adapter simplification or removal.

The learning runtime uses invocation history to generate suggestions for users who are new to PCP:

  • suggestion.capability.next — frequency-based prediction of which capability the user is likely to invoke next, based on observed sequences
  • suggestion.capability.related — co-occurrence analysis: capabilities that are often used together in the same session

The suggestion engine also includes stuck detection. If the same capability is invoked five or more times within 60 seconds, the system emits a StuckAlert. This typically indicates the user is repeating a command because it is not working as expected, and the alert prompts System Intelligence to offer an alternative approach.

All learning state persists across reboots through a single serialized structure:

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PcpLearningState {
pub adapter_scores: HashMap<(String, String), AdapterEffectivenessScore>,
pub confirmation_behaviors: HashMap<ActionClass, ConfirmationBehavior>,
pub usage_records: Vec<CapabilityUsageRecord>,
pub last_updated: DateTime<Utc>,
}

The Context Manager crate handles disk I/O. On boot, PCP requests the learning state from Context Manager, injects adapter scores into the selection logic, and makes usage records available for reporting. The boot path adds roughly 100ms to startup time.

Last updated: