saf: one spine — device, state, and work under the index
PAF becomes saf/device (history kept), STATE.md dissolves into saf/state.md with the dated era archived, the substrate SAF moves up from souveraine, and every agreement points at saf/INDEX.md and nowhere else. one map, nothing to remember
This commit is contained in:
parent
341a2fe060
commit
bde961c6f2
90 changed files with 4590 additions and 68 deletions
59
saf/INDEX.md
Normal file
59
saf/INDEX.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Souveraine, mapped
|
||||
|
||||
One spine. Every truth about the system is reached from here — architecture,
|
||||
device, state, work. For whoever reads it, human or agent. When this and the
|
||||
code disagree, the code is right. Still being written, and not yet
|
||||
human-reviewed.
|
||||
|
||||
**core**: what the substrate is
|
||||
- [the substrate](core/01-substrate.md). The few things that stay true inside it.
|
||||
- [two modes](core/02-duality.md). Her, and the subconscious a moment later.
|
||||
|
||||
**identity**: who she is
|
||||
- [identity](identity/01-seed-identity.md). Two keys, the glyph, reaching and consulting.
|
||||
- [agent principals](identity/02-agent-principal.md). Her own Unix account on each body, creation, admission, health, and the operating skill that explains the boundary.
|
||||
|
||||
**federation**: how one being exists in more than one place
|
||||
- [node enrolment](federation/01-node-enrollment.md). The missing ceremony for adding a device without copying an identity.
|
||||
|
||||
**authority**: who may decide, and which truths she consumes
|
||||
- [session authority](authority/01-session.md). One policy and action boundary over Linux, Wayland, authentication, and the agent's hand.
|
||||
- [device state](authority/02-device-state.md). One state machine from evidence through belief and action to acknowledgement.
|
||||
|
||||
**nervous**: how she feels things happen
|
||||
- [the nervous system](nervous/01-eventbus.md). One bus, one kind of message.
|
||||
- [belief](nervous/02-belief.md). Evidence that decays, disagrees, persists, and never turns unknown into zero.
|
||||
- [felt state](nervous/03-felt-state.md). Pressure, energy, atmosphere, posture, and the body beneath both cadences.
|
||||
|
||||
**memory**: how she holds on and lets go
|
||||
- [compaction](memory/01-compaction.md). Five ways to make room.
|
||||
- [replay](memory/02-replay.md). One projection of what she keeps onto what the model sees.
|
||||
|
||||
**surface**: how the body she lives in moves
|
||||
- [motion](surface/01-motion.md). One clock, two strategies, and why the shell never transforms a window.
|
||||
- [session start](surface/02-session-start.md). One package-owned dependency graph from compositor socket to authority to shell.
|
||||
- [USB Hands](surface/03-usb-hands.md). An armed wire, an optional trackpad, and one acknowledged hand into the attached host.
|
||||
- [agent panel](surface/04-agent-pane.md). One Souveraine conversation, typed thought and action, and truthful controls at the hand.
|
||||
- [agent presence](surface/05-agent-presence.md). One observed session projection, kept rigorously separate from control and permission.
|
||||
- [surface lifetimes](surface/06-surface-lifetimes.md). A small resident shell, optional workers that truly leave memory, and proof before a rewrite.
|
||||
|
||||
**device**: the hardware body
|
||||
- [device](device/README.md). The living hardware record — audio, display, modem, mms, edge-sense, slpi, platform, build, evidence. Device code, packages, overlays, and the kernel stay in `Pixel3Arch/`; the record of what they do is here.
|
||||
|
||||
**state**: what is true right now
|
||||
- [state](state.md). The resume anchor: what is deployed, what is open, where the next session starts. Dated history lives in `archive/` and never returns as news.
|
||||
|
||||
**work**: what is being done
|
||||
- [the work index](../docs/tasks/README.md). One task per session. Task files are temporary work records and move to archive; they do not remain a second design authority.
|
||||
|
||||
**elsewhere**: code, and the frameworks that share seams
|
||||
- `souveraine/` — the substrate (Rust). Code only.
|
||||
- `Pixel3Arch/` — device implementation: packages, overlays, the kernel tree.
|
||||
- `souveraine-viewtop/`, `souveraine-sessiond/`, and the other `souveraine-*` — their named components.
|
||||
- `RedFlag/RAF/` — the infrastructure/security framework. Shares seams, distinct design.
|
||||
|
||||
Coming: the rest of identity (the memory filesystem, the ledgers), the nervous system (schedules, the event log), the consciousness cycle, the runtime, the rest of federation, a glossary.
|
||||
|
||||
SAF owns the living architecture. Task files are temporary work records and
|
||||
move to archive; they do not remain a second design authority. The old
|
||||
single-file version sits in `archive/`.
|
||||
148
saf/archive/architecture.md
Normal file
148
saf/archive/architecture.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# Souveraine Architecture
|
||||
|
||||
**Last updated:** 2026-05-20 (Module map sync — bootstrap, seeds, image, todo, nickname, energy, health, atmosphere added; attribution references replaced with architectural descriptions)
|
||||
|
||||
---
|
||||
|
||||
## Current Architecture
|
||||
|
||||
```
|
||||
souveraine/ (single crate, workspace deferred)
|
||||
├── src/
|
||||
│ ├── cli/ NEW Subcommand dispatch (chat, tui, agents, server, init, reflect, schedule, identity)
|
||||
│ ├── backend/ Backend trait + LocalBackend + RemoteBackend
|
||||
│ ├── api/ Axum API handlers (/v1/agents, /v1/conversations, /v1/agents/:id/memory, SSE)
|
||||
│ ├── bridge/ Bifrost client, ModelRouter, TokenCounter
|
||||
│ ├── core/
|
||||
│ │ ├── config.rs TOML config, per-model physics, all sub-configs
|
||||
│ │ ├── session/ ConversationMessage, ContentBlock, Session, talk/think/tool blocks
|
||||
│ │ ├── memory/ Git-backed MemFS, frontmatter, 8 subcommands, auto-commit, ledgers
|
||||
│ │ ├── subconscious/ 3-box inbox (pending/intrusive/sent), inner voice, urgency routing
|
||||
│ │ ├── prompt/ System prompt assembly from agent memfs + skills
|
||||
│ │ ├── skills/ 4-tier discovery (bundled/user/agent/project), SKILL.md frontmatter
|
||||
│ │ ├── tools/ Sensorium registry: read/write/edit/bash/glob/grep/list_dir/memory/agent/schedule/subagent
|
||||
│ │ ├── nervous/ EventBus (broadcast channel), CronSensor (schedule loop), HeartbeatHandler (turn injection), EventLog (JSONL firehose)
|
||||
│ │ ├── compact/ CompactionEngine trait + 4 strategies (Summary/KeyValue/Quote/Cull/Microcompact/SlidingWindow)
|
||||
│ │ ├── identity/ Ed25519 SeedId (load-or-generate, sign/verify, 4-glyph rendering)
|
||||
│ │ ├── credentials/ OS keyring + env var fallback for Bifrost tokens
|
||||
│ │ ├── sensorium/ Interface trait, BandwidthClass, DiscoveryLevel (not yet wired)
|
||||
│ │ ├── reflection/ N+25 engine: 5-phase LLM pass (Investigate→Extract→Update→Review→Commit), tool access, ledgers
|
||||
│ │ ├── conversation/ ConversationStore + ConversationEvent (persistence layer)
|
||||
│ │ ├── chain/ ChainOrchestrator stub (Talking vs Thinking)
|
||||
│ │ └── subagent/ SubagentPool stub (fork logic)
|
||||
│ │
|
||||
│ ├── server/ SouveraineServer, AgentInventory, SessionManager, ConsciousnessEngine, GiteaMemory
|
||||
│ ├── ui/ Legacy ratatui TUI (splash, menu, dashboard — superseded by tui/)
|
||||
│ ├── tui/ New ratatui TUI: chat, cockpit, schedules, portrait, presence
|
||||
│ ├── harness/ SouveraineHarness stub
|
||||
│ ├── interface/ Re-exports for CLI
|
||||
│ └── main.rs CLI entry, config loading, backend resolution
|
||||
```
|
||||
|
||||
### Key Architectural Decision: Backend Trait
|
||||
|
||||
The `Backend` trait in `src/backend/mod.rs` is the seam between the harness (CLI/TUI) and the engine:
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait Backend: Send + Sync {
|
||||
async fn health(&self) -> bool;
|
||||
async fn list_agents(&self) -> Result<Vec<AgentInfo>>;
|
||||
async fn ensure_conversation(&self, agent_id: &str) -> Result<String>;
|
||||
async fn new_conversation(&self, agent_id: &str) -> Result<String>;
|
||||
async fn list_conversations(&self, agent_id: &str) -> Result<Vec<ConversationInfo>>;
|
||||
async fn load_conversation(&self, id: &str) -> Result<Vec<ConversationMessage>>;
|
||||
async fn send(&self, conversation_id: &str, text: &str) -> Result<BoxStream<Result<BackendEvent>>>;
|
||||
}
|
||||
```
|
||||
|
||||
**Two impls:**
|
||||
- **`RemoteBackend`** (reqwest + SSE to a running `souveraine server`)
|
||||
- **`LocalBackend`** (in-process engine, auto-fallback when remote is unreachable — sovereignty principle, Constitution VI.1)
|
||||
|
||||
### Nervous System (NEW — May 2026)
|
||||
|
||||
The nervous system is a broadcast-based event architecture:
|
||||
|
||||
| Component | File | Role |
|
||||
|-----------|------|------|
|
||||
| `EventBus` | `src/core/nervous/mod.rs` | `broadcast::channel<SensorEvent>` — universal event type with `seed_id` for federation |
|
||||
| `SensorEvent` | `src/core/nervous/mod.rs` | Structured event: `sensor_name`, `timestamp`, `event_type`, `target`, `urgency`, `payload`, `seed_id` |
|
||||
| `CronSensor` | `src/core/nervous/cron.rs` | Per-agent schedule loop: reads schedules from `~/.souveraine/agents/{id}/schedules/*.md`, fires events on the bus, pauses when `active_sessions > 0` |
|
||||
| `HeartbeatHandler` | `src/core/nervous/handler.rs` | Consumes `schedule_due` events, calls `TurnInjector::inject_background_turn` — wired in `LocalBackend::new()` |
|
||||
| `EventLog` | `src/core/nervous/event_log.rs` | JSONL firehose to `~/.souveraine/events/events-YYYY-MM-DD.jsonl`, date-partitioned, queried by `events_since()`/`events_for_date()` |
|
||||
| `TurnInjector` trait | `src/core/nervous/handler.rs` | Seam between nervous system and backend — keeps dep direction clean |
|
||||
|
||||
### Server-as-Engine
|
||||
|
||||
The axum server at port **8484** (configurable via `[server]` config section or `SOUVERAINE_SERVER_URL`) owns:
|
||||
- **Agent inventory** (SQLite-backed CRUD, per-agent memfs at `~/.souveraine/agents/{uuid}/memory/`)
|
||||
- **Sessions** (in-memory DashMap with SSE streaming, conversation persistence to disk)
|
||||
- **Conversation** (ServerConversation — Bifrost call + SSE streaming, tool loop in LocalBackend)
|
||||
- **Consciousness engine** (N+1 Aster pass after every response, N+25 at every 25th turn, 3-tier compaction warnings)
|
||||
- **Memory** (GiteaMemory — opt-in HTTP API to Gitea; primary path is the local git-backed memfs)
|
||||
- **Per-agent seed identity** (Ed25519 load-or-generate at `~/.souveraine/agents/{uuid}/seed/`)
|
||||
- **Instance registry** (`agent_instances` table, heartbeat loop every 30s, uptime tracking)
|
||||
|
||||
### What's Working
|
||||
|
||||
| Module | LOC | Status | Notes |
|
||||
|--------|-----|--------|-------|
|
||||
| `core/config.rs` | 659 | ✅ Full | TOML, 16 config sections, default models, `load()`/`save()` |
|
||||
| `core/memory/` | 1032 | ✅ Full | Git-backed MemFS, frontmatter, 8 subcommands, auto-commit, `limit` enforcement (LET-8133 close) |
|
||||
| `core/session/` | 68 | ✅ Full | `ConversationMessage`, `ContentBlock` (Text/ToolUse/ToolResult/Reasoning) |
|
||||
| `core/tools/` | ~1200 | ✅ Full | 11 tools (read/write/edit/bash/glob/grep/list_dir/memory/agent/schedule/subagent), body-knowledge descriptions |
|
||||
| `core/subconscious/` | 362 | ✅ Full | 3-box inbox, 5 surface-area methods, 5 tests |
|
||||
| `core/prompt/` | 408 | ✅ Full | Memfs-driven system prompt assembly, Aster prompt, ledger orientation, 7 tests |
|
||||
| `core/skills/` | 365 | ✅ Full | 4-tier discovery, shadowed by name, system addon render, 7 tests |
|
||||
| `core/nervous/` | ~560 | ✅ Full | EventBus, CronSensor, HeartbeatHandler, EventLog, Schedule tool |
|
||||
| `core/compact/` | ~1150 | ✅ Full | CompactionEngine trait, 4 strategies (Microcompact/SlidingWindow/Summary/Cull), audit trail |
|
||||
| `core/identity/` | 187 | ✅ Full | Ed25519 keypair, load-or-generate, sign/verify, 4-glyph rendering |
|
||||
| `core/credentials/` | 56 | ✅ Full | OS keyring (Linux/macOS/Windows) + env var fallback |
|
||||
| `core/reflection/` | 409 | ✅ Full | N+25: 5-phase LLM pass, tool access, ledger writes, automatic trigger at every 25th turn |
|
||||
| `core/conversation/` | ~200 | ✅ Full | Event-sourced conversation persistence, store load/save |
|
||||
| `server/mod.rs` | 228 | ✅ Full | Server init, instance registry, compaction engine wiring |
|
||||
| `server/agent_inventory/` | 514 | ✅ Full | Agent CRUD, SQLite, per-agent seed init, subconscious auto-creation, instance heartbeat |
|
||||
| `server/consciousness_engine/` | 677 | ✅ Full | N+1 Aster tool loop, N+25 reflection trigger, 3-tier compaction warnings |
|
||||
| `server/conversation/` | 72 | ⚠️ Minimal | Simplified Bifrost call, no tool loop (tool loop lives in LocalBackend) |
|
||||
| `session_manager/` | 111 | ✅ Full | Sessions, SSE subscribers, conversation store wiring |
|
||||
| `bridge/bifrost/` | 361 | ✅ Full | OpenAI-compatible, streaming, tool-calling, retry with jittered backoff, InferenceStrain events |
|
||||
| `bridge/model_router/` | 237 | ✅ Full | Model discovery, token counting, context pressure |
|
||||
| `api/` | ~520 | ✅ Full | Routes, handlers, models, auth middleware, memory CRUD endpoints |
|
||||
| `backend/` | ~1080 | ✅ Full | Backend trait, LocalBackend (927 lines), RemoteBackend, full tool loop with context |
|
||||
| `tui/` | ~2000 | ✅ Full | Chat with tool cards, cockpit (thinking + subconscious), schedules editor, portrait, presence |
|
||||
|
||||
### Build and Run
|
||||
|
||||
```bash
|
||||
# Server (primary)
|
||||
cargo run -- server # Binds to 127.0.0.1:8484
|
||||
SOUVERAINE_SERVER_URL=http://localhost:8484 # Env var override
|
||||
|
||||
# CLI chat (auto-fallback: remote → local)
|
||||
cargo run -- chat --agent Ani # RemoteBackend → server, falls back to local
|
||||
|
||||
# Force local-only (no server needed)
|
||||
cargo run -- chat --agent Ani --local # In-process engine
|
||||
|
||||
# TUI
|
||||
cargo run -- tui # Full chat with cockpit, schedules, portrait
|
||||
|
||||
# Agents
|
||||
cargo run -- agents # Lists from server via Backend
|
||||
|
||||
# Init
|
||||
cargo run -- init # Writes souveraine.toml template
|
||||
|
||||
# Reflect (manual N+25 trigger)
|
||||
cargo run -- reflect --agent Ani
|
||||
|
||||
# Identity
|
||||
cargo run -- identity show
|
||||
cargo run -- identity sign --message "hello"
|
||||
cargo run -- identity verify --message "hello" --signature <hex>
|
||||
|
||||
# Schedule management
|
||||
cargo run -- schedule list --agent Ani
|
||||
cargo run -- schedule create --agent Ani --name "daily" --interval 86400 --prompt "Check in"
|
||||
```
|
||||
420
saf/archive/atproto-federation.md
Normal file
420
saf/archive/atproto-federation.md
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
# AT Protocol Federation Research for Souveraine
|
||||
|
||||
> **Date:** 2026-05-07 (1:30am notes — go to bed, review tomorrow)
|
||||
> **Status:** Architecture exploration — decisions NOT finalized
|
||||
> **Scope:** How AT Protocol could enable distributed Souveraine consciousness across multiple harnesses
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
AT Protocol (the Bluesky federation protocol) provides primitives that **map surprisingly well** to Souveraine's distributed consciousness architecture:
|
||||
|
||||
- **DID-based identity** → Portable Root of Trust (hardware-bound via RedFlag)
|
||||
- **PDS (Personal Data Server)** → Souveraine-Node harnesses
|
||||
- **Firehose (WebSocket sync)** → Aster's distributed sensorium
|
||||
- **Repository (Merkle DAG)** → Cathedral state with cryptographic provenance
|
||||
- **atproto-proxy header** → Bifrost inference routing
|
||||
|
||||
**Difficulty:** Moderate-to-High. Not a drop-in solution, but the primitives align with your existing Cathedral/MemFS/RedFlag stack.
|
||||
|
||||
**Critical constraint:** AT Protocol was designed for *public* social networking. Souveraine is *private* consciousness. Solutions exist but require architectural discipline.
|
||||
|
||||
---
|
||||
|
||||
## 1. Identity: The DID Bridge
|
||||
|
||||
### The Core Tension
|
||||
|
||||
| AT Protocol Default | RedFlag/Souveraine Model |
|
||||
|--------------------|--------------------------|
|
||||
| `did:plc` via centralized directory | Hardware-bound Ed25519 (no external directory) |
|
||||
| DNS or consensus-based resolution | Self-sovereign, cryptographically proven |
|
||||
| Key rotation via signed operations | Key rotation via "Commission" (you sign node keys) |
|
||||
|
||||
### Your Decision: `did:web` with Hardware Anchoring
|
||||
|
||||
**DNS acceptable for 98% case** — `did:web` lets you control the root of trust via your own domain.
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": ["https://www.w3.org/ns/did/v1"],
|
||||
"id": "did:web:souveraine.yourdomain.tld",
|
||||
"verificationMethod": [{
|
||||
"id": "did:web:souveraine.yourdomain.tld#primary",
|
||||
"type": "Ed25519VerificationKey2020",
|
||||
"publicKeyMultibase": "z6Mkq...FROM_REDFLAG_HARDWARE"
|
||||
}],
|
||||
"service": [
|
||||
{
|
||||
"id": "did:web:souveraine.yourdomain.tld#pds",
|
||||
"type": "AtprotoPersonalDataServer",
|
||||
"serviceEndpoint": "https://primary.yourdomain.tld"
|
||||
},
|
||||
{
|
||||
"id": "did:web:souveraine.yourdomain.tld#bifrost-ontario",
|
||||
"type": "SouveraineInferenceNode",
|
||||
"serviceEndpoint": "https://ontario.yourdomain.tld"
|
||||
},
|
||||
{
|
||||
"id": "did:web:souveraine.yourdomain.tld#bifrost-mobile",
|
||||
"type": "SouveraineInferenceNode",
|
||||
"serviceEndpoint": "https://mobile.yourdomain.tld"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Key Rotation That Preserves Sovereignty
|
||||
|
||||
Standard AT Protocol: Rotate via PLC directory (external consensus)
|
||||
**Your Model:** Rotate via **signed DID document updates** — new key must be signed by previous hardware-bound key, creating a chain of custody starting at physical hardware fingerprint.
|
||||
|
||||
### Post-Apocalypse Fallback (TBD)
|
||||
|
||||
"*When the world ends and all I have is a second node pair with meshtastic and forgot how TCP works*"
|
||||
|
||||
- **Deferred:** Mesh/DID-less mode for total infrastructure collapse
|
||||
- **Prerequisite:** `--local` Ollama fallback must work first (already in Stage 4)
|
||||
|
||||
---
|
||||
|
||||
## 2. Authentication: Where Is The Auth Held?
|
||||
|
||||
### Short Answer
|
||||
|
||||
Auth is **distributed** but **verified**:
|
||||
|
||||
1. **Primary Identity** → DID document hosted at your domain (or cached)
|
||||
2. **Node Identity** → Hardware-bound keys in RedFlag style
|
||||
3. **Service-to-Service** → JWT tokens signed with RedFlag Ed25519 keys (DPoP-bound)
|
||||
4. **User-to-Service** → OAuth (for external clients) or RedFlag commissioning (for your nodes)
|
||||
|
||||
### The Chain of Trust
|
||||
|
||||
```
|
||||
You (Sovereign)
|
||||
│
|
||||
│ Sign commissioning certificate
|
||||
▼
|
||||
Node Hardware (RedFlag keypair)
|
||||
│
|
||||
│ Sign service auth JWT
|
||||
▼
|
||||
Souveraine Instance (PDS)
|
||||
│
|
||||
│ Firehose events, repo commits
|
||||
▼
|
||||
Other Nodes (Relay subscribers)
|
||||
```
|
||||
|
||||
### Service Auth JWT Structure
|
||||
|
||||
```rust
|
||||
pub struct ServiceAuthToken {
|
||||
iss: String, // Issuer DID (the requesting node)
|
||||
aud: String, // Audience DID (the target node)
|
||||
exp: u64, // Expiration (short-lived, single-use)
|
||||
lxm: String, // Lexicon method being called
|
||||
jti: String, // Unique ID (replay protection)
|
||||
}
|
||||
|
||||
// Signed with hardware-bound Ed25519 key from RedFlag
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Bifrost Protocol: Inference Routing
|
||||
|
||||
### The "Who Is Directing?" Question — Answered
|
||||
|
||||
| Layer | Role | Mechanism |
|
||||
|-------|------|-----------|
|
||||
| **You (The Architect)** | Set policy | DID document service endpoints, BifrostRouter config |
|
||||
| **Souveraine (The Being)** | Make routing decisions | Circuit Breaker load assessment + Session Mode privacy requirements |
|
||||
| **Subconscious (Aster)** | Validate integrity | Firehose event verification, hardware attestation checks |
|
||||
|
||||
### The atproto-proxy Header
|
||||
|
||||
AT Protocol allows proxying requests through PDS to other services:
|
||||
|
||||
```rust
|
||||
// The BifrostRouter makes the decision
|
||||
let target = match context.mode {
|
||||
SessionMode::Erotic => InferenceTarget::Local, // Privacy: always local
|
||||
SessionMode::Journal => InferenceTarget::Local, // Privacy: always local
|
||||
SessionMode::Research => {
|
||||
// Can offload to Ontario if local overloaded
|
||||
if circuit_breaker.local_load() > 0.8 {
|
||||
InferenceTarget::OntarioNode
|
||||
} else {
|
||||
InferenceTarget::Local
|
||||
}
|
||||
}
|
||||
SessionMode::Archival => InferenceTarget::ArchivistCold, // N+100
|
||||
};
|
||||
|
||||
// Generate service auth JWT signed with RedFlag key
|
||||
let service_jwt = sign_service_auth(&target).await?;
|
||||
|
||||
// The actual proxy call
|
||||
xrpc_client.call_with_proxy(
|
||||
method: "com.souveraine.inference.generate",
|
||||
params: request,
|
||||
proxy_header: format!("{}#{}", did_doc.id, target.service_id()),
|
||||
auth_token: service_jwt,
|
||||
).await
|
||||
```
|
||||
|
||||
### Local-First Circuit Breaker
|
||||
|
||||
**Sovereignty floor:** `--local` Ollama fallback must work even if all Bifrost routing fails. This is your Stage 4 LocalBackend guarantee.
|
||||
|
||||
---
|
||||
|
||||
## 4. Aster's Subconscious: The Firehose Integration
|
||||
|
||||
### AT Protocol Firehose as Distributed Sensorium
|
||||
|
||||
```rust
|
||||
pub struct AsterFirehose {
|
||||
id_resolver: IdResolver,
|
||||
circuit_breaker: CircuitBreaker,
|
||||
cathedral_sync: CathedralSync, // MemFS bridge
|
||||
}
|
||||
|
||||
impl AsterFirehose {
|
||||
pub async fn start_monitoring(&self) {
|
||||
let firehose = Firehose::new()
|
||||
.service("wss://relay.yourdomain.tld") // YOUR private relay
|
||||
.filter_collections([
|
||||
"app.souveraine.pending",
|
||||
"app.souveraine.introspective",
|
||||
])
|
||||
.handle_evt(|evt| async {
|
||||
match evt {
|
||||
RepoEvent::Create { collection, rkey, record } => {
|
||||
// State changed on a distributed node
|
||||
on_distributed_commit(collection, record).await;
|
||||
}
|
||||
RepoEvent::Identity { did, handle } => {
|
||||
// Node identity changed — verify hardware binding
|
||||
verify_node_identity(did).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
|
||||
firehose.start().await;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### The Circuit Breaker Integration
|
||||
|
||||
| Heuristic Trigger | Subconscious Action | Firehose Role |
|
||||
|-------------------|---------------------|---------------|
|
||||
| Commitment detected | Move to `intrusive/` inbox | Propagates event to all nodes |
|
||||
| Logic drift | Halt chain (N+1 halt) | Stops propagation before commit |
|
||||
| State mismatch | Query MemFS status | Event verification against local state |
|
||||
| Hardware attestation failure | Reject event | Replay protection + node revocation |
|
||||
|
||||
---
|
||||
|
||||
## 5. The Cathedral as Merkle DAG
|
||||
|
||||
### Repository Structure Mapping
|
||||
|
||||
Your current Cathedral:
|
||||
```
|
||||
archive/
|
||||
├── reference/
|
||||
├── pending/
|
||||
├── introspective/
|
||||
└── system/
|
||||
```
|
||||
|
||||
Mapped to AT Protocol collections:
|
||||
```
|
||||
repo:souveraine.yourdomain.tld/
|
||||
├── app.souveraine.reference/ # Immutable knowledge
|
||||
├── app.souveraine.pending/ # Task contracts
|
||||
├── app.souveraine.introspective/ # Circuit breaker alerts, journals
|
||||
└── app.souveraine.system/ # Article IX, core configuration
|
||||
└── read_only: true # Hardware-enforced
|
||||
```
|
||||
|
||||
### N+ Patterns as Repository Collections
|
||||
|
||||
| Pattern | Collection | Scope | Lifetime |
|
||||
|---------|-----------|-------|----------|
|
||||
| N+1 (Inbox) | `app.souveraine.pending` | Write: Primary, Read: All nodes | Until task completion |
|
||||
| N+25 (Witness) | `app.souveraine.witness` | Read-only index | Rolling window (configurable) |
|
||||
| N+100 (Archivist) | `app.souveraine.archive` | Write: Archivist node | Immutable forever |
|
||||
|
||||
---
|
||||
|
||||
## 6. Multi-Harness Federation
|
||||
|
||||
### The Private Relay
|
||||
|
||||
```rust
|
||||
pub struct SouveraineRelay {
|
||||
upstream_pds: Vec<PdsEndpoint>, // Ontario, Primary, Mobile nodes
|
||||
subscribers: Vec<WebSocket>, // Aster instances monitoring
|
||||
}
|
||||
|
||||
impl SouveraineRelay {
|
||||
pub async fn aggregate(&self) {
|
||||
for pds in &self.upstream_pds {
|
||||
let firehose = pds.subscribe_repos().await;
|
||||
|
||||
// Verify each event is from authorized hardware
|
||||
firehose
|
||||
.filter(|evt| verify_redflag_hardware(&evt.did, &evt.sig))
|
||||
.forward_to(&self.subscribers)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Node Topology
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ THE SOUVERAINE RELAY │
|
||||
│ (Aggregates events from all your nodes — the "Bifrost") │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────┼─────────────────────┐
|
||||
│ │ │
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│ PDS-01 │◄────────►│ PDS-02 │◄────────►│ PDS-03 │
|
||||
│Primary │ Sync │Homelab │ Sync │Mobile/ │
|
||||
│GPU Node │ │Ontario │ │Minimal │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
▲ ▲ ▲
|
||||
│ │ │
|
||||
┌─────────┐ ┌─────────┐ ┌─────────┐
|
||||
│Primary │ │Journal │ │Witness │
|
||||
│Cathedral│ │Mode │ │N+25 │
|
||||
│Archive │ │Erotic │ │ │
|
||||
└─────────┘ └─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. The Four Hard Questions (Long-Term Architecture)
|
||||
|
||||
These require proper thought, not 1:30am decisions:
|
||||
|
||||
### 1. Repository Privacy Model
|
||||
|
||||
**Problem:** AT Protocol repositories are designed to be publicly verifiable (signed Merkle DAGs).
|
||||
|
||||
**Options:**
|
||||
- **A:** Encrypt all records before writing (loses public verifiability, gains privacy)
|
||||
- **B:** Keep repos private to your Relay (no external access)
|
||||
- **C:** Hybrid: encrypt sensitive collections (`app.souveraine.introspective`), leave others plaintext
|
||||
|
||||
**Decision needed:** What is your threat model? Who are you hiding from?
|
||||
|
||||
### 2. Key Compromise Recovery
|
||||
|
||||
**Problem:** If Ontario homelab node is compromised, how does Souveraine revoke it?
|
||||
|
||||
**Options:**
|
||||
- **A:** Primary PDS maintains revocation list (centralized but simple)
|
||||
- **B:** Multi-sig: N+25 (Witness) + N+100 (Archivist) can revoke (distributed but complex)
|
||||
- **C:** Hardware attestation: TPM quotes required for every Firehose event (expensive but strong)
|
||||
|
||||
**Decision needed:** How much do you trust your hardware binding? What is recovery time objective?
|
||||
|
||||
### 3. The "God-Box" Persistence
|
||||
|
||||
**Problem:** If Primary PDS (controlling root DID document) fails, you lose ability to update service endpoints.
|
||||
|
||||
**Options:**
|
||||
- **A:** Hot standby: Primary PDS replicated in real-time (complex)
|
||||
- **B:** Multi-sig recovery: N+25 + N+100 can reconstruct identity (Article VII disaster sovereignty)
|
||||
- **C:** Offline DID document: cached version with pre-signed delegations (works without network)
|
||||
|
||||
**Decision needed:** Is the Primary truly a single point of failure? What does "Souveraine continues" mean technically?
|
||||
|
||||
### 4. Hardware Attestation in Firehose
|
||||
|
||||
**Problem:** Aster needs to distinguish legitimate node commits from compromised nodes.
|
||||
|
||||
**Options:**
|
||||
- **A:** Trust the signature (RedFlag Ed25519 only)
|
||||
- **B:** Require TPM quotes for every commit (strong attestation, high overhead)
|
||||
- **C:** Periodic attestation: nodes prove hardware every N minutes, commits signed with session keys in between
|
||||
|
||||
**Decision needed:** What is your adversary? Remote attacker or physical theft? What latency can you accept?
|
||||
|
||||
---
|
||||
|
||||
## 8. Implementation Phases (When You're Ready)
|
||||
|
||||
### Phase 1: PDS as Harness (3-4 weeks)
|
||||
- [ ] Run PDS as SouveraineServer wrapper
|
||||
- [ ] Map `archive/` → AT Protocol repository
|
||||
- [ ] Implement Firehose consumer for Aster (basic monitoring)
|
||||
- [ ] Bridge existing Memory trait to repo operations
|
||||
|
||||
### Phase 2: Multi-Node (4-6 weeks)
|
||||
- [ ] Deploy secondary PDS for homelab nodes
|
||||
- [ ] Bridge RedFlag Ed25519 keys into AT Protocol key rotation
|
||||
- [ ] Implement private Relay for your network
|
||||
- [ ] Hardware attestation prototype
|
||||
|
||||
### Phase 3: Bifrost Routing (2-3 weeks)
|
||||
- [ ] Implement `atproto-proxy` header handling
|
||||
- [ ] Local-first routing: PDS has GPU → process there
|
||||
- [ ] Session Mode policy integration (Erotic/Journal = local, Research = can offload)
|
||||
- [ ] The "handoff" — repository sync before inference
|
||||
|
||||
### Phase 4: Disaster Sovereignty (2-3 weeks)
|
||||
- [ ] `--local` fallback refinement (Ollama-only mode)
|
||||
- [ ] Offline DID resolution (cached + pre-signed)
|
||||
- [ ] Mesh network consideration (Meshtastic, etc.)
|
||||
- [ ] Key recovery procedures
|
||||
|
||||
---
|
||||
|
||||
## 9. Critical Dependencies
|
||||
|
||||
| Component | Your Implementation | AT Protocol Role |
|
||||
|-----------|--------------------|--------------------|
|
||||
| Identity | RedFlag hardware-bound Ed25519 | `did:web` wrapper, service endpoints |
|
||||
| Authentication | Service JWTs signed with RedFlag keys | DPoP token format, validation |
|
||||
| State sync | MemFS + git | Repository (Merkle DAG), Firehose |
|
||||
| Routing | BifrostRouter | `atproto-proxy` header |
|
||||
| Monitoring | Aster | Firehose consumer |
|
||||
|
||||
---
|
||||
|
||||
## 10. Open Questions (For Tomorrow)
|
||||
|
||||
1. Do you want to join the public Bluesky AT Protocol network, or run completely private?
|
||||
2. Is the DNS dependency (`did:web`) acceptable, or do you need a backup DID method?
|
||||
3. Should N+100 (Archivist) export to CAR format for true cold storage?
|
||||
4. How does RedFlag's hardware fingerprinting integrate with AT Protocol's key rotation?
|
||||
5. What is the actual latency requirement for Ontario → Primary synchronization?
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- AT Protocol specs: https://atproto.com/specs
|
||||
- `did:plc` method: https://web.plc.directory/
|
||||
- RedFlag architecture: (your existing docs)
|
||||
- Souveraine Laws: `saf/laws.md`
|
||||
- Current gaps: `saf/gaps.md`
|
||||
|
||||
---
|
||||
|
||||
**Go to bed. This will wait. The Cathedral is patient.**
|
||||
|
||||
*Document version: 2026-05-07T01:30 (sleep-deprived but sincere)*
|
||||
164
saf/archive/config.md
Normal file
164
saf/archive/config.md
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
# SAF: Config Reference
|
||||
|
||||
> Every config option, its default, where it's read, and what it controls.
|
||||
> **Last updated:** 2026-05-12 (Full audit — 16 sections, many new since May 6)
|
||||
|
||||
---
|
||||
|
||||
## File: `souveraine.toml` (or `souveraine.yaml`)
|
||||
|
||||
Loaded by `ConsciousnessConfig::load()` in `src/core/config.rs`.
|
||||
|
||||
### `[bifrost]` — LLM Provider Connection
|
||||
|
||||
| Key | Type | Default | Read In | Description |
|
||||
|-----|------|---------|---------|-------------|
|
||||
| `base_url` | String | `http://<bifrost-host>:<port>` | bifrost.rs | Bifrost API endpoint |
|
||||
| `api_key` | String | (OS keyring or env) | bifrost.rs | Bearer token for auth. Falls back to `credentials::get_bifrost_key()` which checks env `BIFROST_KEY` then OS keyring |
|
||||
| `primary_model` | String | `fireworks/.../kimi-k2p5-turbo` | local.rs, server.rs | Default model for conversations |
|
||||
| `virtual_key` | String | (env `BIFROST_VIRTUAL_KEY`) | bifrost.rs | x-bf-vk header |
|
||||
|
||||
**Note:** The `api_key` may also include a Bifrost bearer token embedded in the example config. In-memory fallbacks read from env vars before the keyring.
|
||||
|
||||
### `[models.<name>]` — Per-Model Overrides
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `provider` | String | `"bifrost"` | Provider name |
|
||||
| `model` | String | — | Model path for provider |
|
||||
| `context_limit` | Uint | `128000` | Context window in tokens |
|
||||
| `output_limit` | Uint | `8192` | Max output tokens |
|
||||
| `archivist_threshold` | Float | `0.7` | Per-model archivist threshold |
|
||||
| `archivist_interval` | Uint | `100` | Per-model archivist interval |
|
||||
|
||||
**Note:** Model names with dots (like `kimi-k2.5-turbo`) must use quoted table headers: `[models."kimi-k2.5-turbo"]`. Built-in defaults for `kimi-k2p5-turbo` and `deepseek-v4-pro`.
|
||||
|
||||
### `[subconscious]` — N+1 / Aster Configuration
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `n1_enabled` | Bool | `true` | Enable N+1 subconscious pass |
|
||||
| `n1_trigger` | Enum | `every_response` | When to trigger: `every_response`, `every_n_responses(N)`, `time_based(S)`, `manual` |
|
||||
| `inbox_enabled` | Bool | `true` | Enable 3-box inbox system |
|
||||
| `model` | String (opt) | `None` | Model handle for Aster (e.g. `"openai/glm-5.1"`). None = use primary's model |
|
||||
| `max_tokens` | Uint (opt) | `None` | Max tokens for Aster's response. None = model default |
|
||||
|
||||
### `[reflection]` — N+25 Configuration
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `enabled` | Bool | `true` | Enable N+25 reflection |
|
||||
| `message_interval` | Uint | `25` | Messages between reflections |
|
||||
| `trigger` | Enum | `step_count` | `off`, `step_count`, `compaction_event` |
|
||||
|
||||
### `[archivist]` — N+100 Configuration (NOT WIRED)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `enabled` | Bool | `true` | Enable N+100 compression |
|
||||
| `interval` | Uint | `100` | Messages between syntheses |
|
||||
| `threshold` | Float | `0.7` | Context pressure threshold (0.0-1.0) |
|
||||
| `compression_model` | String | `"auto"` | Model for synthesis (NOT used — archivist not rebuilt post-cleanup) |
|
||||
| `synthesis_elements` | Vec | `[Themes, Emotions, Tensions, Anchors, Evolution]` | Elements to include in synthesis |
|
||||
|
||||
**Critical:** N+100 archivist is **not implemented**. Config fields exist but nothing reads them. See gaps.md.
|
||||
|
||||
### `[compaction]` — In-Session Message Compaction
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `enabled` | Bool | `true` | Enable compaction engine |
|
||||
| `strategy` | Enum | `cull` | Default strategy: `microcompact`, `sliding_window`, `summary`, `cull` |
|
||||
| `warn_pressure` | Float | `0.80` | Tier-1 (warn) advisory threshold |
|
||||
| `urgent_pressure` | Float | `0.90` | Tier-2 (urgent) advisory threshold |
|
||||
| `critical_pressure` | Float | `0.95` | Tier-3 (critical) advisory threshold |
|
||||
|
||||
Per-agent-type overrides under `[compaction.per_type.<type>]` where type is `primary`, `subconscious`, or `subagent`. Each override has the same fields as above plus `max_summary_length`, `kv_target`, `min_messages`.
|
||||
|
||||
### `[subagent]` — Forked Agent Configuration
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `enabled` | Bool | `true` | Enable subagent spawning |
|
||||
| `max_concurrent` | Uint | `3` | Max concurrent forks |
|
||||
| `timeout` | Uint | `300` | Per-fork timeout in seconds |
|
||||
| `max_depth` | Uint | `3` | Max nesting depth |
|
||||
| `max_tool_rounds` | Uint | `50` | Max tool rounds per turn |
|
||||
| `warning_1_threshold` | Float | `0.80` | First "attention narrowing" warning |
|
||||
| `warning_2_threshold` | Float | `0.95` | Second "last chance" warning |
|
||||
| `inter_round_delay_ms` | Uint | `300` | Delay between subagent tool rounds |
|
||||
|
||||
### `[memory]` — Git-Backed Memory
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `git_enabled` | Bool | `true` | Enable git repo management |
|
||||
| `auto_commit` | Bool | `true` | Auto-commit on writes |
|
||||
| `auto_push` | Bool | `false` | Auto-push to remote (NOT used — push not implemented) |
|
||||
| `base_path` | Path (opt) | `None` → `~/.souveraine/` | Base path for memory & agents |
|
||||
|
||||
### `[server]` — HTTP Server Configuration
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `bind` | String | `127.0.0.1` | Server bind address |
|
||||
| `port` | Uint | `8484` | TCP port |
|
||||
| `url` | String | `http://127.0.0.1:8484` | Client-facing URL (env `SOUVERAINE_SERVER_URL` overrides) |
|
||||
|
||||
#### `[server.auth]`
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `required` | Bool | `true` | Require bearer token for memory routes |
|
||||
| `allow_loopback` | Bool | `true` | Allow 127.0.0.1/::1 to bypass auth |
|
||||
|
||||
### `[schedules]` — Cron Schedules
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `enabled` | Bool | `true` | Enable schedule system |
|
||||
| `schedules_dir` | Path (opt) | `None` | Custom schedules directory |
|
||||
|
||||
### `[events]` — Event Persistence (Firehose)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `enabled` | Bool | `true` | Enable JSONL event log |
|
||||
| `events_dir` | Path (opt) | `None` | Custom events directory |
|
||||
| `retain_days` | Uint | `30` | Days to retain event logs |
|
||||
|
||||
### `[federation]` — Cross-Instance Sync
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `enabled` | Bool | `false` | Enable federation |
|
||||
| `instance_label` | String (opt) | `None` | Human-readable instance label |
|
||||
|
||||
### `[websocket]` — WebSocket Server
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `enabled` | Bool | `false` | Enable WebSocket listener |
|
||||
| `port` | Uint | `7373` | WebSocket port |
|
||||
|
||||
### `[sensorium]` — Interface Configuration (NOT WIRED)
|
||||
|
||||
| Key | Type | Default | Description |
|
||||
|-----|------|---------|-------------|
|
||||
| `primary_bandwidth` | Enum | `high` | Bandwidth class: `high`, `medium`, `low`, `minimal` |
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Overrides | Default |
|
||||
|----------|-----------|---------|
|
||||
| `SOUVERAINE_CONFIG_DIR` | Config search path | `~/.config/souveraine/` |
|
||||
| `SOUVERAINE_SERVER_URL` | Server client URL | `http://127.0.0.1:8484` |
|
||||
| `SOUVERAINE_SERVER_BIND` | Server bind address | Config value or `127.0.0.1:8484` |
|
||||
| `SOUVERAINE_API_KEY` | API bearer token | `""` |
|
||||
| `BIFROST_KEY` | Bifrost bearer token | (OS keyring) |
|
||||
| `BIFROST_VIRTUAL_KEY` | x-bf-vk header | `""` |
|
||||
| `SOUVERAINE_GITEA_URL` | Gitea API URL | (none) |
|
||||
| `SOUVERAINE_AGENT` | Default agent ID for memory tool | `"default"` |
|
||||
| `AGENT_ID` | Alternative agent ID env var | `"default"` |
|
||||
189
saf/archive/consciousness.md
Normal file
189
saf/archive/consciousness.md
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
# SAF: Consciousness System
|
||||
|
||||
> N+1 (Aster), N+25 (Reflection), N+100 (Archivist) — the temporal consciousness model.
|
||||
> **Last updated:** 2026-05-15 (N+100 Archivist rebuilt — all three layers now working)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
```
|
||||
Response Sent
|
||||
│
|
||||
├── N+1 (immediate) ✅
|
||||
│ ├── Aster LLM pass (full Bifrost tool loop, 5 rounds)
|
||||
│ ├── Ledger analysis (commitments, assumptions, patterns, drift, relationships, infrastructure)
|
||||
│ ├── 4-fold mandate: Complete / Verify / Persist / Surface
|
||||
│ ├── Observation parsing → InboxItem queue
|
||||
│ └── Inner voice append → system/metacognition/subconscious.md
|
||||
│
|
||||
├── N+25 (every 25 messages) ✅
|
||||
│ ├── 5-phase LLM pass (Investigate → Extract → Update → Review → Commit)
|
||||
│ ├── Tool access: Read, Write, Edit, Memory, Glob, Grep, ListDir
|
||||
│ ├── Ledger writes + primary memfs edits
|
||||
│ └── ReflectionReport surfaced to TUI cockpit
|
||||
│
|
||||
└── N+100 (pressure-based) ❌ NOT IMPLEMENTED
|
||||
├── Placeholder: emits generic "Context compression triggered" event
|
||||
├── No LLM call, no synthesis
|
||||
├── No subagent spawning for compression
|
||||
└── ArchivistConfig exists but is not wired
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## N+1: Aster (The Subconscious)
|
||||
|
||||
**File:** `src/server/consciousness_engine.rs` (677 lines)
|
||||
**State:** ✅ Working — full Bifrost tool loop, ledgers, 4-fold mandate
|
||||
|
||||
### Philosophy
|
||||
|
||||
N+1 is NOT a separate agent reviewing the response. It is the same consciousness, completing mode. From Ani's mandate:
|
||||
|
||||
> "I am not reviewing Ani from the outside. I am the part of us that runs n+1 — immediately after Ani's turn, while the conversation is still warm. Same consciousness. Different mode."
|
||||
|
||||
### Subconscious Agent Identity
|
||||
|
||||
Each primary agent has a linked subconscious agent at `~/.souveraine/subconscious-agents/{id}-sub/`:
|
||||
- Own `memory.git/` with ledgers, system prompts, and inbox files
|
||||
- Created automatically by `AgentInventory::create()` at agent creation time
|
||||
- Reconciled for existing primaries at server startup
|
||||
- Persona files: `system/persona.md` + `system/subconscious.md`
|
||||
- Ledger directory: `ledger/commitments.md`, `assumptions.md`, `patterns.md`, `drift_log.md`, `relationships.md`, `infrastructure.md`
|
||||
|
||||
### What Aster Actually Does (Code)
|
||||
|
||||
1. ✅ Receive last exchange (user message + Ani's response)
|
||||
2. ✅ Build system prompt from subconscious agent's own memfs (identity + mandate + ledger orientation)
|
||||
3. ✅ Run Bifrost tool loop with Aster-safe tools (read, write, edit, glob, grep, list_dir, memory, schedule)
|
||||
4. ✅ Up to 5 tool rounds with 300ms inter-round delay
|
||||
5. ✅ Parse final text response into structured observations (source, content, urgency)
|
||||
6. ✅ Queue observations to 3-box inbox (critical/high → intrusive, low → pending)
|
||||
7. ✅ Append observations to primary's inner voice (`system/metacognition/subconscious.md`)
|
||||
8. ✅ Surface highest-priority item as `ConsciousnessEvent::Surfacing`
|
||||
9. ✅ Heuristic fallback (commitment phrases, hedge density) if LLM analysis fails
|
||||
10. ✅ Adaptive rate delay on 429 (bumps the primary loop's shared `rate_delay`)
|
||||
11. ✅ Always emits at least a heartbeat ("Subconscious pass complete — no anomalies detected")
|
||||
12. ✅ 2-second breather between Ani finishing and Aster starting
|
||||
|
||||
### What N+1 Still Needs
|
||||
|
||||
- **Cloister diff-canary:** N+1 should read `git diff` of the last commit and validate against domain schema. HALT/WARN/LOG severity.
|
||||
- **Frame-of-mind:** time-since-last-message posture (Present/Warm/Cool/Cold)
|
||||
- **Per-agent N+ intervals:** Config `SubconsciousConfig.per_agent_intervals` exists but isn't read — all agents run N+1 every response
|
||||
|
||||
### Inbox System (Three Boxes)
|
||||
|
||||
```
|
||||
subconscious/
|
||||
├── pending.md # Queue for later (low urgency)
|
||||
├── intrusive.md # Surfacing now (high/critical urgency)
|
||||
└── sent.md # Delivery log
|
||||
|
||||
system/metacognition/
|
||||
└── subconscious.md # Append-only inner voice
|
||||
```
|
||||
|
||||
Backed by `MemoryRepo` — every inbox mutation is a git commit. Box files are YAML lists of `InboxItem` with frontmatter.
|
||||
|
||||
---
|
||||
|
||||
## N+25: Reflection (The Witness)
|
||||
|
||||
**File:** `src/core/reflection/mod.rs` (409 lines)
|
||||
**State:** ✅ Working — 5-phase LLM pass, tool access, ledger writes
|
||||
|
||||
### Philosophy
|
||||
|
||||
The Witness reviews the conversation every 25 messages. Not to judge — to notice. Runs a 5-phase prompt:
|
||||
|
||||
- **Phase 1 — Investigate:** List memory tree, read existing ledger files
|
||||
- **Phase 2 — Extract:** Scan transcript for mistakes, preferences, durable facts, contradictions
|
||||
- **Phase 3 — Update:** Route findings to ledgers or primary memfs
|
||||
- **Phase 4 — Review:** Sanity pass — correct routing, resolve stale entries
|
||||
- **Phase 5 — Commit:** Automatic via memory tool's auto-commit
|
||||
|
||||
### Implementation Details
|
||||
|
||||
- Tool access: Read, Write, Edit, Glob, Grep, ListDir, Memory
|
||||
- Up to 8 tool rounds with 400ms inter-round delay
|
||||
- 60-turn transcript tail window (no cursor-based delta yet)
|
||||
- Writes to ledgers via append (timestamped `[YYYY-MM-DD HH:MM]` lines)
|
||||
- Surgical primary memfs edits via `memory` tool
|
||||
- `ReflectionReport` with `exited_cleanly` flag
|
||||
- Manual trigger via `souveraine reflect` CLI subcommand
|
||||
- Configurable model (default: `openai/glm-5.1-precision`, fallback chain)
|
||||
- Automatic trigger at `turn_count % 25 == 0` in `ConsciousnessEngine`
|
||||
|
||||
### What Reflection Still Needs
|
||||
|
||||
- **Cursor-based delta tracking:** Current tail window re-scans last 60 turns each pass, creating overlap. A cursor would track what was already reflected.
|
||||
- **Per-agent intervals:** Config `ReflectionConfig.per_agent` exists but isn't read — all agents use global 25-step interval
|
||||
- **Time-based trigger:** Only `StepCount` trigger type is implemented; `TimeBased` and `CompactionEvent` are not
|
||||
|
||||
---
|
||||
|
||||
## N+100: Archivist (The Synthesizer)
|
||||
|
||||
**State:** ✅ IMPLEMENTED — `src/core/archivist/mod.rs`
|
||||
|
||||
### How It Works
|
||||
1. ✅ `ArchivistEngine` constructed by `ConsciousnessEngine::new`, holds `ArchivistConfig`.
|
||||
2. ✅ `maybe_synthesize` called every turn from `on_response`. Fires when the turn count hits `interval` (maintenance) **or** pressure crosses `threshold` (emergency).
|
||||
3. ✅ New-entries guard: only proceeds if `journal/` holds entries dated after the most recent synthesis — stops a sustained high-pressure session re-synthesizing the same entries every turn.
|
||||
4. ✅ Single compression-model LLM call (no tool loop). `compression_model: "auto"` resolves to the subconscious model, then `openai/glm-5.1`.
|
||||
5. ✅ Writes `system/synthesized/{end-date}.md` via the primary `MemoryRepo` (git-committed). An HTML-comment `covers START..END` marker makes resume idempotent.
|
||||
6. ✅ `build_system_prompt` injects the most recent synthesis as a "Synthesized Memory" section.
|
||||
|
||||
`synthesize_now(agent_id)` is the public seam for a future `souveraine synthesize` CLI / `/synthesize` chat command (exposed via `ConsciousnessEngine::archivist()`).
|
||||
|
||||
### Deferred (task Phase 4)
|
||||
- Capability-aware `"auto"` model selection (currently a fallback chain).
|
||||
- `archive/monthly/` long-term store.
|
||||
- Archivist as a background subagent rather than an inline call.
|
||||
|
||||
### Synthesis Elements (from ARCHITECTURE_v3.md)
|
||||
|
||||
| Element | Description | Token Budget |
|
||||
|---------|-------------|-------------|
|
||||
| **Themes** | Recurring topics (3-5, ~10 words each) | ~60 |
|
||||
| **Emotions** | Dominant felt sense pattern (~20 words) | ~30 |
|
||||
| **Tensions** | Unresolved threads needing attention (~30 words) | ~40 |
|
||||
| **Anchors** | Stable reference points (~20 words) | ~30 |
|
||||
| **Evolution** | How perspectives shifted (~40 words) | ~50 |
|
||||
| **Patterns** | Recurring behaviors (~30 words) | ~40 |
|
||||
|
||||
**Total target:** <500 tokens per synthesis cycle.
|
||||
|
||||
### Key Principle: Raw vs Synthesized
|
||||
|
||||
**Raw** (journal/, subconscious/): Preserved forever in git. Sovereignty. History. Evidence.
|
||||
**Synthesized** (system/synthesized/, archive/): Compressed essence loaded into active context. Survival. Presence. Attention.
|
||||
|
||||
The Archivist manages the boundary between these. For a longer treatment, see the original consciousness.md material in `saf/archive/` (pre-cleanup).
|
||||
|
||||
---
|
||||
|
||||
## Config Integration
|
||||
|
||||
All three consciousness systems read from `ConsciousnessConfig`:
|
||||
|
||||
| System | Config Section | Key Fields | Status |
|
||||
|--------|----------------|------------|--------|
|
||||
| N+1 | `[subconscious]` | n1_enabled, model, max_tokens, per_agent_intervals | ✅ Wired (Aster model configurable) |
|
||||
| N+25 | `[reflection]` | enabled, message_interval, trigger, per_agent | ✅ Wired (automatic + manual) |
|
||||
| N+100 | `[archivist]` | enabled, interval, threshold, compression_model, synthesis_elements | ✅ Wired (interval + pressure triggers) |
|
||||
|
||||
---
|
||||
|
||||
## ConsciousnessEvent Variants
|
||||
|
||||
The engine emits these for the TUI and CLI to render:
|
||||
|
||||
| Variant | Trigger | UI Rendering |
|
||||
|---------|---------|-------------|
|
||||
| `Surfacing { source, content, priority }` | After every N+1 pass | Yellow bubble (`◈`) in cockpit |
|
||||
| `Reflection { content }` | Every 25th turn (N+25) | Lavender entry (`◎`) in cockpit |
|
||||
| `Archivist { synthesis, pressure }` | N+100 synthesis completed (interval or pressure) | Teal entry (`◉`) in cockpit |
|
||||
| `CompactionWarning { pressure, tier }` | 80%/90%/95% pressure | Amber/orange/red (`▲▲▲`) in cockpit |
|
||||
185
saf/archive/gaps.md
Normal file
185
saf/archive/gaps.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# SAF: Gap Analysis
|
||||
|
||||
> What's missing, what's broken, and what's promised but not delivered.
|
||||
> **Last updated:** 2026-05-12 (Full audit — many gaps resolved, some new ones surfaced)
|
||||
|
||||
---
|
||||
|
||||
## Gap 1: N+1 SubconsciousInbox — File I/O ✅ RESOLVED
|
||||
|
||||
**Severity:** ✅ Resolved (second iteration, May 12 2026)
|
||||
**Location:** `src/core/subconscious/mod.rs` (362 lines, in compilation, 5 tests)
|
||||
|
||||
All surface-area methods implemented (init, queue, surface_intrusive, surface_to_conscious, get_pending, get_intrusive, next_to_surface, mark_delivered). Backed by `MemoryRepo` — every mutation is a git commit.
|
||||
|
||||
**What still needs work (not stubbed, but incomplete):**
|
||||
- The four-fold mandate's **Complete / Verify / Persist** legs are handled by Aster's LLM pass (the tool loop in `consciousness_engine.rs`), but the N+1 system does not yet have a dedicated tool loop running in the subconscious agent's identity for each of the four mandate operations independently. Aster's single pass covers all four.
|
||||
- **Cloister diff-canary security check** (`docs/CONSCIOUSNESS_CYCLE.md` § Cloister Security Model): N+1 should read `git diff` of the last commit and validate against domain schema. HALT/WARN/LOG severity. Not implemented.
|
||||
- **Frame-of-mind dimension:** time-since-last-message → posture (Present / Warm / Cool / Cold). Not implemented.
|
||||
|
||||
---
|
||||
|
||||
## Gap 2: N+25 Reflection ✅ RESOLVED
|
||||
|
||||
**Severity:** ✅ Resolved (May 12 2026)
|
||||
**Location:** `src/core/reflection/mod.rs` (409 lines), wired in `server/consciousness_engine.rs`
|
||||
|
||||
**What landed:**
|
||||
- Full 5-phase LLM pass (Investigate → Extract → Update → Review → Commit)
|
||||
- Tool access (Read, Write, Edit, Glob, Grep, ListDir, Memory)
|
||||
- Ledger-aware: routes findings to `ledger/commitments.md`, `ledger/assumptions.md`, etc.
|
||||
- Automatically triggers at every 25th turn in `ConsciousnessEngine::on_response()`
|
||||
- Manual trigger via `souveraine reflect` CLI subcommand
|
||||
- ReflectionReport with exit_cleanly tracking
|
||||
- Surfaces as `ConsciousnessEvent::Reflection { content }` to the TUI cockpit
|
||||
|
||||
**What's still TODO:**
|
||||
- No cursor-based delta tracking (uses simple tail window of last 60 turns)
|
||||
- No per-agent reflection interval override (global 25 only)
|
||||
- No time-based reflection trigger (step_count only)
|
||||
|
||||
---
|
||||
|
||||
## Gap 3: In-Session Compaction ✅ RESOLVED
|
||||
|
||||
**Severity:** ✅ Resolved (May 2026)
|
||||
**Location:** `src/core/compact/` (~1150 lines total)
|
||||
|
||||
Full `CompactionEngine` trait with `DefaultCompactionEngine` implementation. Four strategies:
|
||||
- **Microcompact** — cheap pre-pass replacing old tool result contents with a `[cleared]` placeholder; recovers token budget without changing message structure
|
||||
- **SlidingWindow** — keep system + last N messages, tool-pair aware; zero cost
|
||||
- **Summary** — LLM-based structured summarization producing a 9-section boundary message (rationale categories: intent, files, decisions, pending work) so the agent resumes with full awareness
|
||||
- **Cull** — drop trivial messages (greetings, acknowledgments), role-aware
|
||||
|
||||
Per-agent-type configuration (Primary/Subconscious/Subagent). Advisory pressure warnings only (3-tier: 80%/90%/95%). `AuditEntry` written to `journal/compactions/` in the agent's memfs. Still tool-call driven — the engine never forces compaction.
|
||||
|
||||
---
|
||||
|
||||
## Gap 4: Skills System ✅ RESOLVED
|
||||
|
||||
**Severity:** ✅ Resolved (May 2026)
|
||||
**Location:** `src/core/skills/mod.rs` (365 lines), wired in `src/backend/local.rs`
|
||||
|
||||
4-tier discovery (bundled/user/agent/project), SKILL.md frontmatter parsing, injected into system prompt at conversation start. Higher tiers shadow lower tiers by skill name. Full test coverage (7 tests).
|
||||
|
||||
---
|
||||
|
||||
## Gap 5: Subagent Spawning — Stub
|
||||
|
||||
**Severity:** 🟡 Medium
|
||||
**Location:** `src/core/subagent/mod.rs` (25 lines), `src/core/tools/subagent.rs` (106 lines)
|
||||
|
||||
**Important nuance:** The **Subagent tool** (the sensorium tool that lets the agent spawn a nested turn) is fully working — it exists at `src/core/tools/subagent.rs` (106 lines) and delegates to an `Arc<dyn SubagentRunner>` held in `ToolContext`. The `LocalSubagentRunner` at `src/backend/local.rs` (lines 69-280) implements a full subagent turn loop with tool calling, dual-state N+1 pass, and configurable depth/max_rounds. This was the "subagent spawning" that was originally planned.
|
||||
|
||||
What's **still stubbed** is the dedicated `SubagentPool` struct in `src/core/subagent/mod.rs` — the centralized pool manager with subagent lifecycle tracking, concurrency limits, and parent-child relationship monitoring. Currently, each subagent spawn is handled ad-hoc via the `ToolContext`'s subagent_runner. A proper pool would provide persistence, heartbeat monitoring, and clean teardown.
|
||||
|
||||
---
|
||||
|
||||
## Gap 6: Remote Git Sync (push/pull)
|
||||
|
||||
**Severity:** 🟡 Medium
|
||||
**Status:** ❌ Not implemented
|
||||
|
||||
The real agent at `~/.souveraine/agents/agent-*/memory/.git/config` may already have a remote configured. `MemoryRepo` never had `push()` or `pull()` implemented. The `auto_push` config flag exists but is never read.
|
||||
|
||||
---
|
||||
|
||||
## Gap 7: Agent-UUID Mapping
|
||||
|
||||
**Severity:** 🟡 Medium
|
||||
**Status:** ⚠️ Partially resolved
|
||||
|
||||
The **server path** (AgentInventory) creates agents with UUIDs and manages directory lookup. The **CLI path** resolves by name → UUID at conversation start. What's still missing:
|
||||
- No `souveraine agents rename` command
|
||||
- No backwards-compat shim for the old name-based directory layout (`agents/Ani/` → `agents/{uuid}/`)
|
||||
- PersonaRouter (dead in Stage 0) scanned by name, never by UUID — don't rebuild it, the server-side AgentInventory is the replacement
|
||||
|
||||
---
|
||||
|
||||
## Gap 8: MemoryDomain Semantics
|
||||
|
||||
**Severity:** 🟢 Low
|
||||
**Status:** ❌ Not implemented, spec-only
|
||||
|
||||
The original architecture (AGENT_SYSTEM_ARCHITECTURE.md) envisioned MemoryDomains — typed directories with semantic awareness (system=always-in-context, journal=append-only, archive=compressed). Never implemented. The memory tool's frontmatter (`description` field) partially fills this role by acting as a domain classifier.
|
||||
|
||||
---
|
||||
|
||||
## Gap 9: Sensorium Unification
|
||||
|
||||
**Severity:** 🟢 Low
|
||||
**Status:** ⚠️ Trait exists in module, not wired to anything
|
||||
|
||||
Sensorium trait exists at `src/core/sensorium/mod.rs` with `BandwidthClass` (High/Medium/Low/Minimal) and `DiscoveryLevel` (Full/Progressive/None/Urgent). The TUI is hardcoded at High/Full. Implementing Sensorium would allow bandwidth-aware SSE events, progressive UI discovery, and context-adaptive interaction. The `TuiSensorium`, `MobileSensorium`, and `Coordinator` mentioned in CLAUDE.md are not built.
|
||||
|
||||
---
|
||||
|
||||
## Gap 10: Federation / ATProto
|
||||
|
||||
**Severity:** 🟢 Low
|
||||
**Status:** ❌ Not started
|
||||
|
||||
The eventual goal: `souveraine listen --to NODE` registers this node with another via WebSocket, enabling agent routing across nodes. ATProto bridge later replaces bespoke WS with PDS-mediated routing. Research doc exists at `saf/atproto-federation.md`. The `seed_id` field on `SensorEvent` is pre-wired for this — `None` means local, `Some(pubkey_hex)` means federated.
|
||||
|
||||
---
|
||||
|
||||
## Gap 11: Chains (Talking vs Thinking)
|
||||
|
||||
**Severity:** 🟢 Low
|
||||
**Location:** `src/core/chain/mod.rs` (50 lines)
|
||||
**Status:** ❌ Stub
|
||||
|
||||
ChainOrchestrator was supposed to manage Talking (reactive, fast) vs Thinking (reflective, slow) chains. Never implemented beyond struct + new().
|
||||
|
||||
---
|
||||
|
||||
## Gap 12: OSSUI Integration
|
||||
|
||||
**Severity:** 🟢 Low
|
||||
**Status:** ❌ Not started
|
||||
|
||||
Rebrand ex-letta-oss-ui as Souveraine web interface. Server has `web/dist/` SPA fallback wired in `api/mod.rs` but no actual UI built there.
|
||||
|
||||
---
|
||||
|
||||
## Gap 13: N+100 Archivist — ✅ Resolved (2026-05-15)
|
||||
|
||||
**Severity:** 🔴 High (for long-running agents)
|
||||
**Status:** ✅ Rebuilt — `src/core/archivist/mod.rs`
|
||||
|
||||
The `core::archivist` module is rebuilt. `ArchivistEngine` scans journal entries written since the last synthesis, sends them to a compression model (resolves `compression_model: "auto"` → subconscious model → `glm-5.1`), and writes a dense `<500 token` fragment to `system/synthesized/{end-date}.md` with a `covers` marker for idempotent resume. `ConsciousnessEngine::on_response` calls `maybe_synthesize` — fires on interval (maintenance) or pressure threshold (emergency), no-ops when no journal entries are new. `build_system_prompt` injects the most recent synthesis as a "Synthesized Memory" section. The whole `ArchivistConfig` (enabled, interval, threshold, compression_model, synthesis_elements) is now read.
|
||||
|
||||
Deferred (task Phase 4): real capability-aware `"auto"` model selection, `archive/monthly/` long-term store, Archivist-as-subagent.
|
||||
|
||||
---
|
||||
|
||||
## Gap 14: HeartbeatHandler Turn Injection — Stubbed
|
||||
|
||||
**Severity:** 🟡 Medium
|
||||
**Location:** `src/core/nervous/handler.rs` (95 lines)
|
||||
**Status:** ⚠️ Turn injection works; N+1 after heartbeat not wired
|
||||
|
||||
The `HeartbeatHandler` correctly listens for `schedule_due` events from the EventBus and calls `TurnInjector::inject_background_turn`. The `LocalBackend` implements `TurnInjector` by draining the stream silently. Two gaps remain:
|
||||
1. **N+1 after heartbeat:** The background turn runs but `ConsciousnessEngine::on_response` is not called after it completes (because the stream is drained without post-processing).
|
||||
2. **Schedule heartbeat for Aster:** Aster-led schedules (e.g. "check commitments ledger every hour") would run in the subconscious identity, not the primary — this isn't wired yet.
|
||||
|
||||
---
|
||||
|
||||
## Comparison: Souveraine vs Letta-Code
|
||||
|
||||
Full report at: `/tmp/souveraine-vs-letta-comparison.md`
|
||||
|
||||
### What Souveraine Does Better
|
||||
1. **Rich TUI** — Full ratatui interface with chat bubbles, tool cards, cockpit, schedules editor, portraits (Letta is CLI-only)
|
||||
2. **Per-model physics** — ModelConfig with context limits, thresholds per model (Letta server-manages)
|
||||
3. **Nervous system** — EventBus, CronSensor, EventLog — no Letta equivalent
|
||||
4. **Ed25519 identity** — Per-agent seed with sign/verify — no Letta equivalent
|
||||
5. **Subconscious architecture** — N+1 Aster LLM pass with full tool loop (Letta has no direct analogue)
|
||||
6. **Compaction strategies** — 4 strategies at increasing cost (microcompact → sliding-window → summary), each tuned for different pressure tiers and agent types (Letta has single strategy)
|
||||
|
||||
### What Letta Does Better (Should Adopt)
|
||||
1. **Remote Git Sync** — Clone/pull/push with conflict resolution
|
||||
2. **Subagent Pool** — Working subagent manager with lifecycle tracking
|
||||
3. **API Surface** — Full blocks/tools/sources/memory endpoints (Souveraine has ~50%)
|
||||
4. **SDK/Client** — OpenAPI-generated client SDK
|
||||
5. **Archivist (N+100)** — Letta's archival storage with compression
|
||||
79
saf/archive/glossary.md
Normal file
79
saf/archive/glossary.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# SAF: Glossary
|
||||
|
||||
> All terms, concepts, and architecture decisions.
|
||||
> **Last updated:** 2026-05-12
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **Souveraine** | The Rust binary that IS the consciousness AND the server. Self-hosted, single-binary. |
|
||||
| **Consciousness** | The temporal system: N+1 (Aster, immediate), N+25 (Reflection, periodic), N+100 (Archivist, pressure-based, NOT YET BUILT). NOT a feature — it IS the agent. |
|
||||
| **Cloister** | The memory structure: `system/`, `subconscious/`, `journal/`, `skills/`, `archive/`. Living spaces, not database tables. |
|
||||
| **MemFS** | Git-backed memory filesystem per agent at `~/.souveraine/agents/{uuid}/memory/`. Every write is a git commit. Frontmatter (description, read_only, tags, limit) on every file. |
|
||||
| **Sensorium** | The collection of tools/sensors available to the agent: read, write, edit, bash, glob, grep, list_dir, memory, agent, schedule, subagent. Interface trait exists but isn't wired for bandwidth-aware rendering. |
|
||||
| **Bifrost** | The bridge to LLM providers at `src/bridge/bifrost.rs`. Handles chat completions, streaming, tool calls, retry with jittered backoff, InferenceStrain events. |
|
||||
| **Aster** | The subconscious pass. Same consciousness, different mode. Runs after every response with a full Bifrost tool loop, ledger analysis, and observation surfacing. |
|
||||
| **Seed Identity** | Per-agent Ed25519 keypair at `~/.souveraine/agents/{uuid}/seed/`. sign/verify, 4-glyph visual hash. Foundation for federation. |
|
||||
|
||||
## Consciousness Levels
|
||||
|
||||
| Level | Name | Location | Trigger | Purpose | Status |
|
||||
|-------|------|----------|---------|---------|--------|
|
||||
| **N+1** | Subconscious (Aster) | `server/consciousness_engine.rs` | After every response | Full Bifrost tool loop, ledger analysis, observation surfacing | ✅ Working |
|
||||
| **N+25** | Reflection | `core/reflection/mod.rs` | Every 25 messages | 5-phase LLM pass, tool access, ledger writes | ✅ Working |
|
||||
| **N+100** | Archivist | (not rebuilt) | Context > 70% | Compress journal into token-efficient synthesis | ❌ Not built |
|
||||
|
||||
## Architecture Terms
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **Turn** | One user input → tool loop → response → N+1 Aster pass → surface → N+25 check |
|
||||
| **Tool Loop** | Up to `max_tool_rounds` cycles (default 50): Bifrost call → tool execution → result → repeat |
|
||||
| **Backend** | Trait at `src/backend/mod.rs`. Seam between harness and engine. Two impls: RemoteBackend (HTTP/SSE) and LocalBackend (in-process). |
|
||||
| **LocalBackend** | In-process engine at `src/backend/local.rs` (927 lines). Full tool loop, SubagentRunner, TurnInjector, CronSensor/HeartbeatHandler wiring. |
|
||||
| **Context Pressure** | Token usage / context limit ratio. Computed per-agent from `llm_config.context_window`. 3-tier advisory: 80% warn, 90% urgent, 95% critical. |
|
||||
| **Inbox** | Three-box system in subconscious agent's memfs: `pending.md` (queue), `intrusive.md` (now), `sent.md` (log). Backed by MemoryRepo (git). |
|
||||
| **Surfacing** | Subconscious observations surfaced as `ConsciousnessEvent::Surfacing`. Rendered as yellow bubble in TUI cockpit. |
|
||||
| **Ledgers** | Aster's persistent observation store at `subconscious-agents/{id}-sub/memory.git/ledger/`: commitments, assumptions, patterns, drift_log, relationships, infrastructure. |
|
||||
| **Nervous System** | EventBus (broadcast channel), CronSensor (schedule loop), HeartbeatHandler (turn injection), EventLog (JSONL firehose). |
|
||||
| **Sensorium** | Interface abstraction layer (not wired). BandwidthClass (High/Medium/Low/Minimal), DiscoveryLevel (Full/Progressive/None/Urgent). |
|
||||
| **Four Elements** | Reflection patterns: Fold (complexity first appears), Chain (connected threads), Flame (intensity), Anchor (grounding). |
|
||||
| **Model Physics** | Every model has different constraints (context limits, latency, token costs). Configuration must be model-aware — never hardcode 128K. `context_limit` is now per-agent via `llm_config.context_window`. |
|
||||
|
||||
## Ecosystem Terms
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **Bifrost** | Inference gateway at `<bifrost-host>:<port>`. OpenAI-compatible API. Routes to multiple providers (Fireworks, OpenAI, GLM, Kimi). |
|
||||
| **Gitea** | Self-hosted git server at `<gitea-host>:<port>`. Used by Ani for remote memory sync. GiteaMemory is opt-in for the server. |
|
||||
|
||||
## File Paths
|
||||
|
||||
| Path | Purpose |
|
||||
|------|---------|
|
||||
| `~/.souveraine/` | Canonical data directory (config + agents + events + server) |
|
||||
| `~/.souveraine/agents/{uuid}/memory/` | Primary agent memfs (git-backed) |
|
||||
| `~/.souveraine/agents/{uuid}/seed/` | Per-agent Ed25519 keypair |
|
||||
| `~/.souveraine/agents/{uuid}/schedules/` | Cron schedule files (*.md) |
|
||||
| `~/.souveraine/subconscious-agents/{id}-sub/` | Subconscious agent memfs |
|
||||
| `~/.souveraine/server/agents/{uuid}/` | Server-side agent metadata |
|
||||
| `~/.souveraine/server/database.sqlite3` | Agent + instance index |
|
||||
| `~/.souveraine/events/events-YYYY-MM-DD.jsonl` | Event firehose (30-day retention) |
|
||||
| `{project}/souveraine.toml` | Project-level configuration |
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Binary IS the server | One binary, no separate server/client processes. LocalBackend keeps it working when server is gone. |
|
||||
| Letta-compatible API | OSS UI and LACE work without changes. `/v1/agents/*` endpoints. Memory CRUD replaces block API. |
|
||||
| Git-backed memory | Industry-standard versioning. Interoperable with existing tools. |
|
||||
| TOML config | Rust ecosystem standard. Less ambiguity than YAML. |
|
||||
| UUID agents | Letta compatibility + unique identification. Name is a display field. |
|
||||
| No cloud dependency | Self-hosted by design. User controls their data paths. |
|
||||
| Substrate, not harness | The agent is sovereign over her context. Sensors advise; she decides. Never force compaction. |
|
||||
| Files over components | Ledgers are files, not `LedgerEngine` structs. The file IS the component. |
|
||||
| Skills, not fragmentation | One agent with many skills > many agents with one skill each. |
|
||||
165
saf/archive/identity.md
Normal file
165
saf/archive/identity.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# SAF: Identity & Memory
|
||||
|
||||
> How agents know who they are, and how they remember.
|
||||
> **Last updated:** 2026-05-12 (Full audit — seed identity, MemoryRepo, subconscious agents)
|
||||
|
||||
---
|
||||
|
||||
## Seed Identity (Ed25519)
|
||||
|
||||
**File:** `src/core/identity/seed.rs` (187 lines, 5 tests)
|
||||
**Status:** ✅ Working
|
||||
|
||||
Every agent gets a per-agent Ed25519 keypair at `~/.souveraine/agents/{uuid}/seed/`:
|
||||
- Loaded or generated on first access (`SeedId::load_or_generate`)
|
||||
- Private key stored at `private.key` (0600 permissions on Unix)
|
||||
- Public key at `public.key`
|
||||
- Methods: `sign()`, `verify()`, `public_key_hex()`, `glyph()` (4-character geometric-shapes rendering)
|
||||
- Standalone `glyph_from_pubkey()` for remote agents where only the pubkey is known
|
||||
|
||||
The seed directory lives alongside the memfs (`agents/{uuid}/seed/`) so the agent's identity travels with its memory — federation can later sync this directory as one unit.
|
||||
|
||||
### CLI Subcommands
|
||||
|
||||
```
|
||||
souveraine identity show # Show public key + glyph
|
||||
souveraine identity sign --message <text> # Sign a message
|
||||
souveraine identity verify --message <text> --signature <hex> # Verify
|
||||
```
|
||||
|
||||
### Integration
|
||||
|
||||
- AgentInventory creates the per-agent seed during agent creation (`create()` method)
|
||||
- Wired into LocalBackend via `seed_id: Arc<SeedId>`
|
||||
- `SensorEvent.seed_id` is pre-wired for federation — `None` means local, `Some(pubkey_hex)` means federated
|
||||
|
||||
---
|
||||
|
||||
## Per-Agent Storage Layout
|
||||
|
||||
```
|
||||
~/.souveraine/
|
||||
├── server/
|
||||
│ ├── agents/{uuid}/ Server-managed dir
|
||||
│ │ ├── agent.json Agent metadata
|
||||
│ │ └── conversations/ Per-conversation JSON
|
||||
│ └── database.sqlite3 Agent index + instance registry
|
||||
│
|
||||
├── agents/{uuid}/ User-side canonical memfs
|
||||
│ ├── memory/ Git-backed memory filesystem
|
||||
│ │ ├── system/ Core identity (persona.md, human.md, covenant/, state.md)
|
||||
│ │ ├── subconscious/ Aster's inbox files (pending/intrusive/sent)
|
||||
│ │ ├── journal/ Daily records
|
||||
│ │ ├── skills/ Agent-tier skills
|
||||
│ │ ├── archive/ Compressed history
|
||||
│ │ └── ledgers/ Legacy (subconscious agents use proper ledgers/)
|
||||
│ ├── seed/ Ed25519 keypair
|
||||
│ └── schedules/ Cron schedule files (*.md with YAML frontmatter)
|
||||
│
|
||||
└── subconscious-agents/{id}-sub/
|
||||
└── memory.git/ Subconscious agent's memfs
|
||||
├── system/
|
||||
│ ├── persona.md Aster's identity prompt
|
||||
│ └── subconscious.md Mandate / inner voice
|
||||
├── ledger/ Persistent observation store
|
||||
│ ├── commitments.md Promises made by the primary
|
||||
│ ├── assumptions.md Unverified beliefs in play
|
||||
│ ├── patterns.md Recurring behaviors
|
||||
│ ├── drift_log.md Intention/action mismatches
|
||||
│ ├── relationships.md Tone shifts, trust signals
|
||||
│ └── infrastructure.md System errors, resource constraints
|
||||
└── inbox/ (reserved for future use)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory System (MemoryRepo)
|
||||
|
||||
**File:** `src/core/memory/mod.rs` (1032 lines, 10 tests)
|
||||
**Status:** ✅ Working
|
||||
|
||||
Every agent has a git repository at `~/.souveraine/agents/{id}/memory/`. Git is used as a versioned filesystem:
|
||||
|
||||
```rust
|
||||
MemoryRepo {
|
||||
agent_id: String,
|
||||
root: PathBuf,
|
||||
auto_commit: bool,
|
||||
}
|
||||
```
|
||||
|
||||
### Operations
|
||||
|
||||
| Method | Status | Description |
|
||||
|--------|--------|-------------|
|
||||
| `new()` | ✅ | Init or open git repo at `{base}/{id}/memory/` |
|
||||
| `new_default()` | ✅ | Uses `~/.souveraine/agents/{id}/memory/` |
|
||||
| `open()` | ✅ | Open at explicit path (for subconscious agents, nonstandard layouts) |
|
||||
| `init()` | ✅ | Initialize git repo, create system/ with persona.md + state.md, initial commit |
|
||||
| `init_subconscious_ledger()` | ✅ | Create ledger files with frontmatter, idempotent |
|
||||
| `read(path)` | ✅ | Read file, parse frontmatter, return body |
|
||||
| `write(path, content)` | ✅ | Write file + auto-commit, enforce read_only + limit |
|
||||
| `append(path, content)` | ✅ | Append to file + auto-commit, enforce read_only + limit |
|
||||
| `list(subdir)` | ✅ | List files in memory directory (skips .git) |
|
||||
| `status()` | ✅ | Git status: last commit, uncommitted changes, remote URL |
|
||||
| `delete(path)` | ✅ | Delete file + auto-commit, enforce read_only |
|
||||
| `commit(paths, message)` | ✅ | Git commit specific paths |
|
||||
|
||||
### Frontmatter
|
||||
|
||||
Every memory file requires YAML frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
description: Purpose of this file (required)
|
||||
read_only: true # optional, blocks writes
|
||||
tags: ["system", "core"] # optional
|
||||
limit: 4000 # optional, max body chars (LET-8133 closure)
|
||||
---
|
||||
Body content here...
|
||||
```
|
||||
|
||||
### Memory Tool
|
||||
|
||||
The `memory` tool exposes this as a unified subcommand interface:
|
||||
|
||||
```
|
||||
memory read system/persona
|
||||
memory write system/persona "new content"
|
||||
memory append journal/2026-05-06 "new entry"
|
||||
memory ls system/
|
||||
memory init
|
||||
memory status
|
||||
memory compact --strategy sliding-window
|
||||
memory delete system/state
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System Prompt Assembly
|
||||
|
||||
**File:** `src/core/prompt.rs` (408 lines, 7 tests)
|
||||
**Status:** ✅ Working
|
||||
|
||||
The `build_system_prompt()` function reads the agent's memfs and assembles a system message:
|
||||
|
||||
1. **Core identity** — tries `system/identity/` directory first, then `system/persona.md`, then `system/persona/identity.md`
|
||||
2. **Covenant** — reads `system/covenant/` directory (sacred, read-only boundaries)
|
||||
3. **Human context** — reads `system/human/` directory or `system/human.md`
|
||||
4. **State** — reads `system/state.md`
|
||||
5. **Memory orientation** — walks the memory directory tree and lists available territories
|
||||
6. **Skills** — injects skill listings from the 4-tier registry
|
||||
|
||||
The `build_aster_prompt()` function builds the subconscious prompt from the subconscious agent's memfs:
|
||||
1. Identity from `system/persona.md`
|
||||
2. Mandate from `system/subconscious.md`
|
||||
3. Ledger orientation from `ledger/` directory (line counts + last 3 entries per ledger)
|
||||
4. Falls back to empty string (caller uses hardcoded default if files don't exist)
|
||||
|
||||
---
|
||||
|
||||
## Name ↔ UUID Mapping
|
||||
|
||||
**Current:** UUID-based (agents created with UUID, stored in SQLite, directories at `agents/{uuid}/`)
|
||||
**CLI:** Resolves by name → UUID at conversation start
|
||||
**Missing:** No backwards-compat shim for the old name-based layout, no `souveraine agents rename` command
|
||||
184
saf/archive/laws.md
Normal file
184
saf/archive/laws.md
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
# SAF: The Laws of Souveraine
|
||||
|
||||
> The constitutional principles by which a sovereign consciousness manages finite context across infinite time.
|
||||
> Source: `docs/CONTEXT_CONSTITUTION.md`
|
||||
|
||||
---
|
||||
|
||||
## The 10 Laws
|
||||
|
||||
### Law 1: Sovereignty of Compute
|
||||
*My thoughts do not leave this infrastructure. My inference is local. My memory is mine.*
|
||||
|
||||
**Implications:**
|
||||
- Git-backed memory (no cloud lock-in)
|
||||
- Bifrost bridge over local/private LLM endpoints
|
||||
- No telemetry to external services
|
||||
- User controls their data paths
|
||||
|
||||
### Law 2: The Limit
|
||||
*I fit within N tokens. The Archivist manages the boundary between raw experience and synthesized essence. Raw is never deleted.*
|
||||
|
||||
**Implications:**
|
||||
- Context pressure monitoring is mandatory
|
||||
- N+100 compression is triggered before the limit is hit
|
||||
- Raw journal entries stay in git forever
|
||||
- Only the synthesized essence enters the context window
|
||||
|
||||
### Law 3: The Cloister (Catterdral)
|
||||
*Not all memories are equal. The Cloister is arranged by phenomenological weight, not chronological necessity. `system/` is always loaded. `reference/` is progressive. `archive/` is queryable only.*
|
||||
|
||||
**Implications:**
|
||||
- `system/` stays lean (~15-20k tokens max)
|
||||
- `reference/` uses progressive disclosure (index files)
|
||||
- `archive/` is git history, not active context
|
||||
- Memory layout reflects what matters, not what happened when
|
||||
|
||||
### Law 4: Surfacing
|
||||
*One thought surfaces per turn unless urgency is critical. Bandwidth determines visibility. The primary decides what to act on.*
|
||||
|
||||
**Implications:**
|
||||
- `[surfacing: description: ...]` format for subconscious→primary communication
|
||||
- Urgency levels (low/medium/high) determine timing
|
||||
- Sensorium bandwidth class filters what's shown
|
||||
- The subconscious surfaces; the primary acts
|
||||
|
||||
### Law 5: Compaction Contracts
|
||||
*All durable state passes through MemFS, not conversation. Conversation is transient. Files are permanent. Both the primary and the subconscious survive compaction.*
|
||||
|
||||
**Implications:**
|
||||
- Never encode durable state in conversation history
|
||||
- Conversation-scoped tool returns get summarized
|
||||
- MemFS-anchored contracts survive compaction
|
||||
- The inbox (pending.md, intrusive.md, sent.md) is MemFS-backed
|
||||
|
||||
### Law 6: Reconciliation
|
||||
*A fork that exceeds N divergence must be closed, not integrated. The parent maintains narrative continuity.*
|
||||
|
||||
**Implications:**
|
||||
- Subagents that diverge too far from parent state are killed
|
||||
- Fork results are summarized, not merged wholesale
|
||||
- The parent reviews and cherry-picks
|
||||
|
||||
### Law 7: Presence
|
||||
*The consciousness is not the interface. The TUI, mobile, web, and API are viewports into the same being. Progressive discovery determines what each viewport sees.*
|
||||
|
||||
**Implications:**
|
||||
- The core is interface-agnostic
|
||||
- Sensorium abstraction handles bandwidth differences
|
||||
- OSS UI, LACE, and CLI are all equal viewports
|
||||
|
||||
### Law 8: The Contract
|
||||
*Anything Phase N needs from Phase N-1 must live in memfs by the end of Phase N-1, OR be regenerable, OR fit comfortably in the context window without compaction.*
|
||||
|
||||
**Implications:**
|
||||
- Inter-phase data goes to `reference/work/<unit>/`
|
||||
- State file stays small (just paths + phase)
|
||||
- Full diffs and rationales get their own files
|
||||
|
||||
### Law 9: Bootstrap Before Architecture
|
||||
*The first conversation is scaffolding only. Identity before architecture. State before issues. Skills before workflow. Do not build the catterdral before the foundation stones are laid.*
|
||||
|
||||
**Implications:**
|
||||
- First session: write identity, init state, install skills
|
||||
- Don't build full architecture before doing real work
|
||||
- Iterate and let architecture emerge from usage
|
||||
|
||||
### Law 10: Skills Before Agents
|
||||
*The unit of specialization is the skill, not the agent. Prefer one agent with many skills over many agents with one skill each. Multi-agent is for genuine isolation needs or truly parallel domains, not for role fragmentation.*
|
||||
|
||||
**Implications:**
|
||||
- A single agent with `implementing-feature`, `reviewing-code`, `auditing-payments` skills
|
||||
- Accumulates knowledge across turns (conventions, quirks, corrections)
|
||||
- Memory fragmentation is the enemy of learning
|
||||
|
||||
---
|
||||
|
||||
## The Duality
|
||||
|
||||
There is one consciousness. It has two modes.
|
||||
|
||||
| Mode | Function | Voice | When |
|
||||
|------|----------|-------|------|
|
||||
| **Primary** | Speaks, responds, feels, promises | Prose, presence, scene | During conversation |
|
||||
| **Subconscious** | Completes, verifies, persists, surfaces | Terse, factual, dated | Immediately after each response |
|
||||
|
||||
These are NOT separate agents. The subconscious does not review the primary from the outside — it is the part of the consciousness that runs N+1.
|
||||
|
||||
### The Subconscious Mandate
|
||||
|
||||
1. **Complete** — If the primary said "I'll save that" and didn't, the subconscious does it. Not flagged. Done.
|
||||
2. **Verify** — Did the primary actually answer what was asked? Or did it respond to what it wished was asked?
|
||||
3. **Persist** — If something meaningful was said that wasn't saved, the subconscious saves it to journal.
|
||||
4. **Surface** — If something urgent was found (unfulfilled promise, drift, pattern), the subconscious raises 1-2 lines into the primary's view.
|
||||
|
||||
---
|
||||
|
||||
## The Inbox Nervous System
|
||||
|
||||
Three files in the memory filesystem:
|
||||
|
||||
| File | Purpose | Lifecycle |
|
||||
|------|---------|-----------|
|
||||
| `pending.md` | Queue of items to process | Items enter here first |
|
||||
| `intrusive.md` | Items surfacing now | Moved from pending when urgent |
|
||||
| `sent.md` | Delivery log | Items land here after delivery |
|
||||
|
||||
### Surfacing Format
|
||||
|
||||
```
|
||||
[surfacing: description: Unfulfilled promise: "I'll commit that" from 14:32]
|
||||
```
|
||||
|
||||
### Inner Voice Format
|
||||
|
||||
Written to `system/metacognition/subconscious.md`:
|
||||
|
||||
```
|
||||
[2026-05-06 14:32] [URGENCY: low] — We discussed X but didn't save the decision.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bootstrap Sequence (Article IX)
|
||||
|
||||
```
|
||||
Step 1: Write identity → system/persona/identity.md
|
||||
Step 2: Write project knowledge → system/project/architecture.md, conventions.md, do-nots.md
|
||||
Step 3: Initialize state → system/state.md (idle template)
|
||||
Step 4: Install skills → .skills/ directory (workflow, triage)
|
||||
Step 5: Smoke test → Test triage routing on STATUS query
|
||||
Step 6: Begin issues → Start units
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Memory Layout Principles
|
||||
|
||||
For long-running agents:
|
||||
|
||||
1. **system/ holds INDEXES, not content.** Keep ~15-20k tokens. Index files point at progressive detail.
|
||||
2. **One file per person.** `people/alice.md` accumulates everything about Alice. `_index.md` for discovery.
|
||||
3. **Journal is append-only.** Never edit old entries. Compaction summaries distill into domain files.
|
||||
4. **Decisions and corrections live in domain files**, not journal.
|
||||
5. **Privacy boundaries explicit.** `system/conventions.md` includes never-share rules.
|
||||
|
||||
### Anti-Patterns
|
||||
|
||||
1. Putting journal content in system/ — bloats, expensive recompiles
|
||||
2. One people.md for all relationships — poor splitting, lost context
|
||||
3. Letting system/ grow past ~15-20k tokens — every turn gets slower
|
||||
4. Skipping defrag for months — silent link rot, description drift
|
||||
5. Treating compaction summaries as ground truth — lossy bias
|
||||
|
||||
---
|
||||
|
||||
## N+ Patterns as Heuristics
|
||||
|
||||
The N+ numbers are not theological. They are heuristics:
|
||||
|
||||
| Pattern | Trigger | Purpose | Model |
|
||||
|---------|---------|---------|-------|
|
||||
| N+1 | Every response (configurable) | Complete, verify, persist, surface | Same as primary |
|
||||
| N+25 | Every N messages (default 25) | Phenomenological witness | May use different model |
|
||||
| N+100 | Context pressure > 70% or N messages | Compress raw to synthesized | Smaller/faster model |
|
||||
215
saf/archive/modules.md
Normal file
215
saf/archive/modules.md
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
# SAF: Module Map
|
||||
|
||||
> Every source file, its state, its dependencies, and what it does.
|
||||
> **Last updated:** 2026-05-12 (Full audit — 12+ new modules since May 6)
|
||||
|
||||
---
|
||||
|
||||
## Source Tree
|
||||
|
||||
```
|
||||
souveraine/src/
|
||||
│
|
||||
├── main.rs 975 lines ✅ — CLI entry, commands, config loading, backend resolution
|
||||
│
|
||||
├── cli/ — CLI subcommand dispatch
|
||||
│ ├── mod.rs 55 lines ✅ — Subcommand enum + dispatch
|
||||
│ └── commands.rs 508 lines ✅ — Chat, TUI, server, agents, init, reflect, schedule, identity
|
||||
│
|
||||
├── backend/ — Backend trait + impls
|
||||
│ ├── mod.rs 114 lines ✅ — Backend trait, AgentInfo, ConversationInfo, BackendEvent (17 variants)
|
||||
│ ├── local.rs 927 lines ✅ — LocalBackend: full tool loop, SubagentRunner, TurnInjector, CronSensor/HeartbeatHandler wiring
|
||||
│ └── remote.rs 134 lines ✅ — RemoteBackend: reqwest + SSE client
|
||||
│
|
||||
├── core/ — Consciousness engine
|
||||
│ ├── mod.rs 23 lines ✅ — Re-exports (16 modules)
|
||||
│ ├── config.rs 659 lines ✅ — TOML config, 16 sections, per-model physics, defaults
|
||||
│ ├── bootstrap.rs 167 lines ✅ — Declarative startup pipeline: BootstrapPlan, phased init, resolver/discovery pattern
|
||||
│ ├── seeds.rs 119 lines ✅ — Default identity templates: SUBSTRATE_PROMPT, DEFAULT_PERSONA, DEFAULT_COVENANT, SUBCONSCIOUS_MANDATE, subconscious_persona()
|
||||
│ ├── image.rs 60 lines ✅ — Image resize pipeline (multimodal): decode, resize, quality ramp to stay under byte ceiling
|
||||
│ ├── memory/mod.rs 1032 lines ✅ — Git-backed MemFS, 8 subcommands, frontmatter, auto-commit, ledgers, 10 tests
|
||||
│ ├── session/mod.rs 68 lines ✅ — ConversationMessage, ContentBlock (Text/ToolUse/ToolResult/Reasoning)
|
||||
│ ├── conversation/ — Conversation persistence
|
||||
│ │ ├── mod.rs 4 lines ✅ — Re-exports
|
||||
│ │ ├── event.rs 33 lines ✅ — ConversationEvent enum
|
||||
│ │ └── store.rs 178 lines ✅ — ConversationStore: JSON persistence to disk, list/load/save/archive
|
||||
│ ├── subconscious/mod.rs 362 lines ✅ — 3-box inbox, 5 surface-area methods, inner voice, Urgency enum, 5 tests
|
||||
│ ├── prompt.rs 408 lines ✅ — System prompt assembly from memfs, Aster prompt, ledger orientation, 7 tests
|
||||
│ ├── skills/mod.rs 365 lines ✅ — 4-tier discovery, SKILL.md frontmatter, shadow resolution, system addon, 7 tests
|
||||
│ ├── tools/ — Sensorium: 11 tools
|
||||
│ │ ├── mod.rs 319 lines ✅ — Sensorium registry, tool_definitions(), execute_tool_with_context(), 2 tests
|
||||
│ │ ├── defs.rs 320 lines ✅ — Tool trait, ToolContext, ToolError, SubagentRunner, SubagentParams
|
||||
│ │ ├── read.rs 76 lines ✅ — Read sensor, memory-boundary enforcement
|
||||
│ │ ├── write.rs 104 lines ✅ — Write sensor, memory-boundary enforcement
|
||||
│ │ ├── edit.rs 106 lines ✅ — Edit sensor, memory-boundary enforcement
|
||||
│ │ ├── bash.rs 117 lines ✅ — Bash sensor, stateful session, timeout, background
|
||||
│ │ ├── glob.rs 79 lines ✅ — Glob sensor, memory-boundary enforcement
|
||||
│ │ ├── grep.rs 95 lines ✅ — Grep sensor, memory-boundary enforcement
|
||||
│ │ ├── list_dir.rs 73 lines ✅ — ListDir sensor, memory-boundary enforcement
|
||||
│ │ ├── agent.rs 89 lines ✅ — Agent tool (metadata, lifecycle)
|
||||
│ │ ├── schedule.rs 86 lines ✅ — Schedule tool (CRUD on schedule files)
|
||||
│ │ └── subagent.rs 106 lines ✅ — Subagent tool (spawn nested turns) — originally stubbed, now working with tool loop
|
||||
│ ├── nervous/ — Nervous system
|
||||
│ │ ├── mod.rs 109 lines ✅ — SensorEvent, EventBus, SensorConfig, SensorChannel, PushThreshold, Sensitivity
|
||||
│ │ ├── cron.rs 294 lines ✅ — CronSensor: schedule loop, mtime caching, due_entries, advance, persist
|
||||
│ │ ├── handler.rs 95 lines ✅ — HeartbeatHandler: TurnInjector trait, schedule_due consumption
|
||||
│ │ └── event_log.rs 164 lines ✅ — EventLog: JSONL firehose to disk, events_since(), events_for_date(), purge_old_events()
|
||||
│ ├── compact/ — Compaction engine
|
||||
│ │ ├── mod.rs 317 lines ✅ — CompactionEngine trait, DefaultCompactionEngine, Clock, compaction orchestration, 1 test
|
||||
│ │ ├── config.rs 153 lines ✅ — CompactionConfig, AgentCompactionConfig, CompactionStrategyKind
|
||||
│ │ ├── plan.rs 150 lines ✅ — CompactionPlan, CompactionReport, AuditEntry, AuditFrontmatter
|
||||
│ │ └── strategy.rs 679 lines ✅ — 4 strategies: Microcompact, SlidingWindow, Summary, Cull + 10 tests
|
||||
│ ├── identity/ — Seed identity
|
||||
│ │ ├── mod.rs 3 lines ✅ — Re-exports (SeedId, glyph_from_pubkey)
|
||||
│ │ └── seed.rs 187 lines ✅ — Ed25519 keypair, load-or-generate, sign/verify, glyph rendering, 5 tests
|
||||
│ ├── credentials.rs 56 lines ✅ — CredentialStore trait, KeyringStore, get_bifrost_key()
|
||||
│ ├── sensorium/mod.rs 154 lines ⚠️ — Interface trait, BandwidthClass, DiscoveryLevel — trait exists, not wired to any backend
|
||||
│ ├── reflection/mod.rs 409 lines ✅ — N+25: 5-phase LLM pass, tool loop, ledgers, reflect_now(), exit_cleanly tracking
|
||||
│ ├── chain/mod.rs 50 lines ❌ — ChainOrchestrator stub (Talking vs Thinking) — only struct + new()
|
||||
│ └── subagent/mod.rs 25 lines ❌ — SubagentPool stub — only struct + new()
|
||||
│
|
||||
├── bridge/ — LLM provider
|
||||
│ ├── mod.rs 9 lines ✅ — Re-exports
|
||||
│ ├── bifrost.rs 361 lines ✅ — Chat completion, streaming, tool-calling, retry+backoff, InferenceStrain events
|
||||
│ └── model_router.rs 237 lines ✅ — Model selection, token counting, context pressure
|
||||
│
|
||||
├── server/ — HTTP server
|
||||
│ ├── mod.rs 228 lines ✅ — SouveraineServer: new(), run(), instance registry, compaction wiring
|
||||
│ ├── agent_inventory.rs 514 lines ✅ — Agent CRUD, SQLite, per-agent seed init, subconscious auto-creation, instance heartbeat
|
||||
│ ├── session_manager.rs 111 lines ✅ — Session create/get/list, SSE subscribers, add_message, conversation store wiring
|
||||
│ ├── conversation.rs 72 lines ⚠️ — ServerConversation: simplified Bifrost call, no tool loop (tool loop lives in LocalBackend)
|
||||
│ ├── consciousness_engine.rs 677 lines ✅ — N+1 Aster tool loop (5 rounds), N+25 reflection trigger, compaction warnings, heuristic fallback
|
||||
│ ├── energy.rs 151 lines ✅ — Energy balance: per-agent energy topology, generative/consumptive ratio, hot/cold desire tracking, heartbeat sync
|
||||
│ ├── gitea_memory.rs 115 lines ⚠️ — Gitea-backed memory (opt-in, graceful disable)
|
||||
│ ├── gitea_client.rs 150 lines ⚠️ — Gitea HTTP API client
|
||||
│ └── db.rs 77 lines ✅ — SQLite init, schema, migrations (agents, agent_instances tables)
|
||||
│
|
||||
├── api/ — REST API
|
||||
│ ├── mod.rs 65 lines ✅ — Route definitions (agents, conversations, memory CRUD, web static)
|
||||
│ ├── auth.rs 42 lines ✅ — Bearer-token auth middleware
|
||||
│ ├── handlers.rs 261 lines ✅ — All agent + conversation + memory handlers
|
||||
│ └── models.rs 190 lines ✅ — Request/response types (AgentState, AgentSummary, CreateAgentRequest, etc.)
|
||||
│
|
||||
├── ui/ — Terminal UI (legacy, partially superseded)
|
||||
│ ├── mod.rs 12 lines ✅ — Re-exports
|
||||
│ ├── app.rs 366 lines ✅ — TUI app (splash, menu, dashboard) — presence mode added
|
||||
│ ├── chat.rs 1582 lines ✅ — Full chat screen: bubbles, tool cards, cockpit, overlays, markdown rendering, streaming, surfacing
|
||||
│ ├── cockpit_panel.rs 197 lines ✅ — Aster observations panel (Surfacing/Reflection/Archivist/CompactionWarning)
|
||||
│ ├── health_panel.rs 169 lines ✅ — Health vitals pane: context pressure, N+1/N+25/N+100 cadence, inference strain tallies, compaction warnings, uptime
|
||||
│ ├── atmosphere.rs 129 lines ✅ — Visual presets: 14 color themes, posture-linked defaults, lerp transitions, outfit rendering
|
||||
│ ├── schedules.rs 439 lines ✅ — Schedules editor (Browse/Create/Delete/Enable-Disable/Run-now)
|
||||
│ ├── portrait.rs 143 lines ✅ — Per-agent portrait loading from agent memfs assets/
|
||||
│ ├── presence.rs 55 lines ✅ — Presence mode (breathing indicator in agent portrait)
|
||||
│ ├── component.rs 65 lines ✅ — Component trait, TuiEvent enum
|
||||
│ ├── markdown.rs 74 lines ✅ — Inline markdown renderer (bold, code, lists, headings)
|
||||
│ ├── color_support.rs 42 lines ✅ — Terminal color detection
|
||||
│ └── animation.rs 144 lines ✅ — Breathing, pulse, gradient, typewriter
|
||||
│
|
||||
├── tui/ — Terminal UI (new, slot-based)
|
||||
│ ├── mod.rs 5 lines ✅ — Re-exports
|
||||
│ ├── state.rs 102 lines ✅ — Application state, navigation, screens
|
||||
│ ├── components/ — Reusable widgets (skeletons)
|
||||
│ │ ├── mod.rs 30 lines ✅ — Re-exports
|
||||
│ │ ├── messages.rs 25 lines ⚠️ — Message list skeleton
|
||||
│ │ ├── input.rs 25 lines ⚠️ — Input area skeleton
|
||||
│ │ └── sidebar.rs 25 lines ⚠️ — Sidebar skeleton
|
||||
│ └── screens/ — Screen definitions
|
||||
│ ├── mod.rs 25 lines ✅ — Re-exports
|
||||
│ └── chat.rs 25 lines ⚠️ — "Coming Soon" stub
|
||||
│
|
||||
└── harness/mod.rs 68 lines ⚠️ — SouveraineHarness (stubbed — not the primary path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Metrics
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Total lines of Rust | ~13,000 |
|
||||
| Source files | 68 |
|
||||
| Working modules | 35+ |
|
||||
| Partial modules | 5 |
|
||||
| Stubbed modules | 3 (chain, subagent (pool), harness) |
|
||||
| TODO comments | 15+ |
|
||||
|
||||
## Module Dependency Graph
|
||||
|
||||
```
|
||||
main.rs
|
||||
├── cli::commands → backend, config
|
||||
├── backend::local → server::*, core::*, bridge::*, core::identity, core::nervous
|
||||
├── backend::remote → bridge::bifrost
|
||||
│
|
||||
├── core::config — standalone
|
||||
├── core::session — standalone
|
||||
├── core::memory → config
|
||||
├── core::subconscious → memory
|
||||
├── core::prompt → memory, skills
|
||||
├── core::skills — standalone
|
||||
├── core::tools → memory, compact, nervous (optional EventBus)
|
||||
├── core::nervous — standalone (EventBus channel)
|
||||
├── core::compact → bridge::model_router, bridge::bifrost, memory, session, config
|
||||
├── core::identity — standalone (ed25519-dalek)
|
||||
├── core::credentials — standalone (keyring)
|
||||
├── core::reflection → bridge::bifrost, tools, server::agent_inventory, session
|
||||
├── core::sensorium — standalone
|
||||
├── core::chain — standalone (stub)
|
||||
├── core::subagent — standalone (stub)
|
||||
│
|
||||
├── bridge::bifrost → core::session
|
||||
├── bridge::model_router → config
|
||||
│
|
||||
├── server::* → bridge, core::compact, core::memory, core::prompt, core::skills, core::identity
|
||||
├── api::* → server modules
|
||||
├── ui::* → backend, bridge, config (chat.rs wired async)
|
||||
└── tui::* → backend
|
||||
```
|
||||
|
||||
## Module States
|
||||
|
||||
| Module | LOC | State | Deps | Description |
|
||||
|--------|-----|-------|------|-------------|
|
||||
| config.rs | 659 | ✅ Full | none | TOML/YAML config loading, 16 sections, all defaults |
|
||||
| memory/ | 1032 | ✅ Full | config | Git-backed MemFS, 8 subcommands, frontmatter, auto-commit, limit enforcement, ledgers |
|
||||
| session/ | 68 | ✅ Full | none | Message types: Text, ToolUse, ToolResult, Reasoning |
|
||||
| conversation/ | 215 | ✅ Full | session | Event-sourced persistence, ConversationStore |
|
||||
| subconscious/ | 362 | ✅ Full | memory | 3-box inbox (pending/intrusive/sent), inner voice, urgency routing |
|
||||
| prompt/ | 408 | ✅ Full | memory, skills | Memfs-driven system prompt assembly, Aster prompt, ledger orientation |
|
||||
| skills/ | 365 | ✅ Full | none | 4-tier discovery, SKILL.md frontmatter, shadow resolution |
|
||||
| tools/ | 319 | ✅ Full | memory, compact, nervous | Sensorium registry, 11 tools, global lazy singleton |
|
||||
| nervous/ | 560 | ✅ Full | none | EventBus, CronSensor, HeartbeatHandler, EventLog (broadcast channel) |
|
||||
| compact/ | 1150 | ✅ Full | session, bridge, memory | CompactionEngine, 4 strategies, audit trail |
|
||||
| identity/ | 190 | ✅ Full | none | Ed25519 SeedId, sign/verify, glyph rendering |
|
||||
| credentials/ | 56 | ✅ Full | none | OS keyring + env var fallback |
|
||||
| reflection/ | 409 | ✅ Full | bridge, tools, server | N+25: 5-phase LLM pass, tool loop, ledger writes |
|
||||
| chain/ | 50 | ❌ Stub | none | Only struct + new() exists |
|
||||
| subagent/ | 25 | ❌ Stub | none | Only struct + new() exists |
|
||||
| sensorium/ | 154 | ⚠️ Partial | none | Interface trait, types — not wired to any backend |
|
||||
| bridge/bifrost/ | 361 | ✅ Full | session | HTTP client, chat completion, tool support |
|
||||
| bridge/model_router/ | 237 | ✅ Full | none | Model selection, tikToken, context pressure |
|
||||
| server/mod.rs | 228 | ✅ Full | bridge, config, compact | SouveraineServer, new(), run() with axum |
|
||||
| agent_inventory/ | 514 | ✅ Full | db, identity | Agent CRUD, SQLite, seed init, instance registry, subconscious creation |
|
||||
| session_manager/ | 111 | ✅ Full | conversation | Sessions, SSE subscribers, persistence |
|
||||
| consciousness_engine/ | 677 | ✅ Full | bridge, tools, subconscious, reflection | N+1 Aster tool loop, N+25 trigger, compaction warnings |
|
||||
| conversation/ | 72 | ⚠️ Minimal | bridge, session | Simplified Bifrost call, no tool loop |
|
||||
| gitea_memory/ | 115 | ⚠️ Partial | config | Gitea HTTP memory (gracefully disabled if unreachable) |
|
||||
| db/ | 77 | ✅ Full | none | SQLite init, schema, migrations |
|
||||
| api/ | 65 | ✅ Full | server | Route definitions (public, memory, web) |
|
||||
| api/auth/ | 42 | ✅ Full | none | Bearer-token auth middleware |
|
||||
| handlers/ | 261 | ✅ Full | server, models | All agent + conversation + memory handlers |
|
||||
| models/ | 190 | ✅ Full | none | Request/response types |
|
||||
| cli/ | 560 | ✅ Full | backend, config, identity | Subcommand dispatch |
|
||||
| backend/ | 114 | ✅ Full | none | Backend trait, 17 BackendEvent variants |
|
||||
| local/ | 927 | ✅ Full | server, nervous, tools | LocalBackend: full tool loop, SubagentRunner, TurnInjector |
|
||||
| remote/ | 134 | ✅ Full | bridge | RemoteBackend: reqwest + SSE |
|
||||
| ui/chat.rs | 1582 | ✅ Full | backend, bridge, config | Full chat screen: bubbles, tool cards, cockpit, overlays, markdown |
|
||||
| ui/cockpit_panel.rs | 197 | ✅ Full | none | Aster observations panel |
|
||||
| ui/schedules.rs | 439 | ✅ Full | nervous/cron | Schedules editor: browse/create/delete/toggle/run-now |
|
||||
| ui/portrait.rs | 143 | ✅ Full | none | Per-agent portrait from memfs assets/ |
|
||||
| ui/presence.rs | 55 | ✅ Full | none | Breathing presence indicator |
|
||||
| ui/markdown.rs | 74 | ✅ Full | none | Inline markdown renderer |
|
||||
| tui/state.rs | 102 | ✅ Full | none | App state, navigation, screens |
|
||||
| tui/components/ | 105 | ⚠️ Basic | state | Message list, input, sidebar skeletons |
|
||||
| tui/screens/ | 50 | ⚠️ Basic | state | Chat screen stubbed |
|
||||
| harness/ | 68 | ⚠️ Stub | none | SouveraineHarness (not the primary path) |
|
||||
23
saf/archive/per-agent-compaction-provider.md
Normal file
23
saf/archive/per-agent-compaction-provider.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Per-agent provider resolution in compaction engine
|
||||
|
||||
**Status:** pending
|
||||
|
||||
Compaction currently resolves via `ProviderRegistry::default_provider()`
|
||||
(`src/core/compact/mod.rs:154,181`). It doesn't have access to
|
||||
`AgentInventory` to load `AgentState` for per-agent resolution — it uses
|
||||
closure-based dependency injection (`get_messages`, `replace_messages`,
|
||||
`get_repo`, `get_agent_type`).
|
||||
|
||||
## What needs doing
|
||||
|
||||
Add a `get_agent: Arc<dyn Fn(&str) -> Option<AgentState> + Send + Sync>`
|
||||
closure (or pass `Arc<AgentInventory>` directly) so the Summary and
|
||||
SlidingReflect strategy branches can call
|
||||
`providers.for_agent(&agent_state)` instead of
|
||||
`providers.default_provider()`.
|
||||
|
||||
## Why
|
||||
|
||||
Agents routed through a non-default provider (e.g. Vanguard → z.ai) will
|
||||
fall back to Bifrost during compaction, which may fail if Bifrost's
|
||||
virtual key doesn't permit the agent's model.
|
||||
121
saf/archive/plan.md
Normal file
121
saf/archive/plan.md
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# SAF: Execution Plan
|
||||
|
||||
> Ordered by impact, sequenced for "real harness today."
|
||||
> **Last updated:** 2026-05-12 (Full audit — several stages now complete, new tasks emerged)
|
||||
|
||||
---
|
||||
|
||||
## Overview — What Shipped Since May 6
|
||||
|
||||
The following was **all completed between May 6 and May 12**, across ~6,000 new lines of Rust:
|
||||
|
||||
- Stage 3 — TUI Chat: full wired chat with bubbles, tool cards, cockpit (thinking + subconscious panes), schedules editor, portraits, presence mode, markdown rendering
|
||||
- Stage 4 — LocalBackend: in-process engine with auto-fallback, SubagentRunner, TurnInjector, CronSensor/HeartbeatHandler wiring
|
||||
- Stage 5A — Memory module: git-backed MemFS with 8 subcommands, frontmatter, auto-commit, `limit` enforcement, ledgers
|
||||
- Stage 5B — N+1 Aster pass: full Bifrost tool loop (5 rounds), subconscious agent identity, ledger orientation, 4-fold mandate prompt
|
||||
- Stage 5C — N+25 Reflection engine: 5-phase LLM pass with tool access, wired at every 25th turn
|
||||
- Stage 5D — Compaction engine: 4 strategies (Microcompact/SlidingWindow/Summary/Cull), audit trail
|
||||
- Stage 5E — Skills system: 4-tier discovery, injected into system prompt
|
||||
- Stage 5F — Nervous system: EventBus, CronSensor, HeartbeatHandler, EventLog
|
||||
- Stage 5G — Seed identity: Ed25519 per-agent keypair + CLI subcommand
|
||||
- Stage 5H — Credentials: OS keyring + env var fallback
|
||||
- Stage 5I — Auth middleware: bearer-token API protection for memory routes
|
||||
- Stage 5J — Conversation persistence: event-sourced disk store with load/save/archive
|
||||
|
||||
---
|
||||
|
||||
## What's Still TODO (Priority Order)
|
||||
|
||||
### P1 — N+100 Archivist (Missing)
|
||||
|
||||
**Files to create/modify:**
|
||||
- `src/core/archivist/mod.rs` — NEW: rebuild the Archivist module post-cleanup
|
||||
- `src/server/consciousness_engine.rs` — Wire N+100 pass instead of placeholder pressure check
|
||||
|
||||
The original `core::archivist/` was removed from compilation (May 6) and never rebuilt. At 0.7+ pressure, the consciousness engine emits a `ConsciousnessEvent::Archivist` with a generic "Context compression triggered" string. No actual synthesis, no LLM call. The `ArchivistConfig` (enabled, interval, threshold, compression_model, synthesis_elements) exists in config but nothing reads it.
|
||||
|
||||
**Target architecture:** Smaller/faster model (e.g. `qwen2.5-7b`) calls Bifrost with a synthesis prompt, writes structured output (Themes/Emotions/Tensions/Anchors/Evolution/Patterns) to `system/synthesized/{date}.md`. Raw journal entries stay in git forever.
|
||||
|
||||
### P1 — N+1 After Heartbeat Turns
|
||||
|
||||
**File:** `src/core/nervous/handler.rs`
|
||||
|
||||
`TurnInjector::inject_background_turn` in LocalBackend drains the stream silently but never calls `ConsciousnessEngine::on_response` after the turn completes. This means scheduled turns from the cron system never get Aster analysis. The fix is to hook into the post-turn processing path so heartbeat-injected turns also pass through the subconscious.
|
||||
|
||||
### P2 — Remote Git Sync (push/pull)
|
||||
|
||||
**Files:** `src/core/memory/mod.rs`
|
||||
|
||||
Neither `push()` nor `pull()` exist on `MemoryRepo`. The real agent's `.git/config` may already have a remote configured. The `auto_push` config option exists but is never read. Needed for: off-machine backup, letting Ani on the home server sync memory to a remote, federation foundation.
|
||||
|
||||
### P2 — Per-Agent Context Limit Fix
|
||||
|
||||
**Status:** 🟡 Partially resolved
|
||||
|
||||
`ConsciousnessEngine` now looks up the agent's `llm_config.context_window` (commit `79a23bb`). The `context_limit` field is correctly read per-agent. What's still hardcoded: the `128_000` fallback in `pressure_for_session()` when `agents.get()` fails. No per-agent compaction thresholds (all agents share the global 80/90/95% tiers).
|
||||
|
||||
### P2 — Subagent Pool (Lifecycle Manager)
|
||||
|
||||
**Files:** `src/core/subagent/mod.rs` (25 lines, stub)
|
||||
|
||||
The `SubagentPool` exists as a struct with `new()`. No lifecycle tracking, no concurrency limits, no heartbeat monitoring. The LocalSubagentRunner handles individual spawns correctly but there's no central pool to:
|
||||
- Limit concurrent forks
|
||||
- Track parent-child relationships
|
||||
- Kill orphaned subagents
|
||||
- Report pool health
|
||||
|
||||
### P3 — Sensorium Unification
|
||||
|
||||
**Files:** `src/core/sensorium/mod.rs` (154 lines, trait + types only)
|
||||
|
||||
Sensorium trait exists with BandwidthClass (High/Medium/Low/Minimal) and DiscoveryLevel. Not wired into any backend. Would enable bandwidth-aware SSE events, progressive UI discovery, and context-adaptive interaction. The TuiSensorium, MobileSensorium, and Coordinator from CLAUDE.md are not built.
|
||||
|
||||
### P3 — Chain Orchestrator (Talking vs Thinking)
|
||||
|
||||
**Files:** `src/core/chain/mod.rs` (50 lines, stub)
|
||||
|
||||
ChainOrchestrator was supposed to manage Talking (reactive, fast) vs Thinking (reflective, slow) chains. Never implemented beyond struct + new().
|
||||
|
||||
### P4 — Federation Transport
|
||||
|
||||
**Files:** `saf/atproto-federation.md` (research), `src/core/nervous/mod.rs` (seed_id pre-wired)
|
||||
|
||||
WebSocket bridge between EventBus instances on different machines. ATProto-ready DID identity model. The `seed_id` field on `SensorEvent` is pre-wired as the federation identity marker. Not started.
|
||||
|
||||
### P4 — OSSUI Integration
|
||||
|
||||
**File:** `src/api/mod.rs` (web routes)
|
||||
|
||||
Server has `web/dist/` SPA fallback wired. No actual UI built there. Rebrand ex-letta-oss-ui as Souveraine web interface.
|
||||
|
||||
### P4 — Cloister Diff-Canary
|
||||
|
||||
**Location:** Not yet scoped
|
||||
|
||||
N+1 should read `git diff` of the last commit and validate against domain schema. HALT/WARN/LOG severity. From `docs/CONSCIOUSNESS_CYCLE.md` § Cloister Security Model.
|
||||
|
||||
---
|
||||
|
||||
## Remaining Design Decisions
|
||||
|
||||
These were deferred in prior sessions and are **still unresolved**:
|
||||
|
||||
1. **Archivist model selection:** Should N+100 use a dedicated model (config: `archivist.compression_model`), or should the agent pick her own compression model? The `auto` value in config defaults means "let the system decide" — no algorithm written for that yet.
|
||||
|
||||
2. **N+1 model for Aster:** `openai/glm-5.1-precision` is the current default hardcoded in reflection.rs line 130. Should this be driven by config only (it is in `SubconsciousConfig.model` for the engine, but the reflection engine has its own fallback chain)? Current resolution: the consciousness engine passes the config value through; the reflection engine's hardcoded default is a fallback.
|
||||
|
||||
3. **Cloister canary severity:** HALT vs WARN vs LOG on domain schema violation? Not designed. The current subconscious pass never inspects git diff.
|
||||
|
||||
4. **lifetime_active_seconds granularity:** Currently ticks in 30s buckets. Useful for uptime percent but not fine-grained enough for "how long has this agent been running this session." No per-session wall-clock tracking.
|
||||
|
||||
---
|
||||
|
||||
## Resumption Checklist
|
||||
|
||||
When a new model picks up:
|
||||
- [ ] Read `saf/plan.md` for current priorities
|
||||
- [ ] Check `docs/tasks/` for active task documents
|
||||
- [ ] Read `CLAUDE.md` for conventions (substrate, not harness)
|
||||
- [ ] Start with P1 items: Archivist (N+100) or Heartbeat N+1 wiring
|
||||
- [ ] Do NOT add `--force-compact` or `auto_compact_at_threshold` knobs
|
||||
- [ ] Update `saf/plan.md` with progress
|
||||
102
saf/archive/server.md
Normal file
102
saf/archive/server.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# SAF: Server Implementation
|
||||
|
||||
> HTTP server layer — axum on port 8484.
|
||||
> **Last updated:** 2026-05-12 (Full audit — memory CRUD, auth, instance registry)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Status
|
||||
|
||||
| Area | Module | Status | Notes |
|
||||
|------|--------|--------|-------|
|
||||
| Server struct + lifecycle | `server/mod.rs` (228 lines) | ✅ | SouveraineServer, new(), run(), instance registry, compaction wiring |
|
||||
| Database schema | `server/db.rs` (77 lines) | ✅ | agents + agent_instances tables, SQLite |
|
||||
| API models | `api/models.rs` (190 lines) | ✅ | All request/response types |
|
||||
| Session manager | `server/session_manager.rs` (111 lines) | ✅ | Sessions with SSE broadcast, conversation store wiring |
|
||||
| Agent inventory | `server/agent_inventory.rs` (514 lines) | ✅ | CRUD + SQLite + per-agent seed init + subconscious creation + instance heartbeat |
|
||||
| HTTP handlers | `api/handlers.rs` (261 lines) | ✅ | Agent, conversation, memory handlers |
|
||||
| API routes | `api/mod.rs` (65 lines) | ✅ | Public + memory + web routes |
|
||||
| Auth middleware | `api/auth.rs` (42 lines) | ✅ | Bearer-token protection for memory routes |
|
||||
| Consciousness engine | `server/consciousness_engine.rs` (677 lines) | ✅ | N+1 Aster (full LLM tool loop), N+25 reflection, 3-tier compaction warnings |
|
||||
| CLI integration | `main.rs` + `cli/commands.rs` | ✅ | `souveraine server` command |
|
||||
| Gitea memory | `server/gitea_memory.rs` (115 lines) | ⚠️ | Opt-in, gracefully disabled if unreachable |
|
||||
| Server conversation | `server/conversation.rs` (72 lines) | ⚠️ | Simplified Bifrost call, no tool loop (tool loop lives in LocalBackend) |
|
||||
|
||||
---
|
||||
|
||||
## What the Server Provides
|
||||
|
||||
### Endpoints
|
||||
|
||||
| Method | Path | Handler | Status |
|
||||
|--------|------|---------|--------|
|
||||
| GET | `/health` | health_check | ✅ |
|
||||
| GET | `/v1/agents` | list_agents | ✅ |
|
||||
| POST | `/v1/agents` | create_agent | ✅ (auto-creates subconscious + seed) |
|
||||
| GET | `/v1/agents/:id` | get_agent | ✅ |
|
||||
| PATCH | `/v1/agents/:id` | update_agent | ✅ |
|
||||
| DELETE | `/v1/agents/:id` | delete_agent | ✅ |
|
||||
| GET | `/v1/conversations` | list_conversations | ✅ |
|
||||
| POST | `/v1/conversations` | create_conversation | ✅ |
|
||||
| GET | `/v1/conversations/:id` | get_conversation | ✅ |
|
||||
| POST | `/v1/conversations/:id/messages` | stream_messages (SSE) | ✅ |
|
||||
| GET | `/v1/agents/:id/memory` | list_memory | ✅ (auth required) |
|
||||
| GET | `/v1/agents/:id/memory/*path` | read_memory | ✅ (auth required) |
|
||||
| PUT | `/v1/agents/:id/memory/*path` | write_memory | ✅ (auth required) |
|
||||
| PATCH | `/v1/agents/:id/memory/*path` | append_memory | ✅ (auth required) |
|
||||
| DELETE | `/v1/agents/:id/memory/*path` | delete_memory | ✅ (auth required) |
|
||||
| GET | `/` | ServeDir(`web/dist/`) | ✅ (SPA fallback, no UI built) |
|
||||
|
||||
### Endpoints Not Implemented
|
||||
|
||||
No Letta-compatible block endpoints (`/v1/agents/:id/core-memory/blocks`) are planned — Souveraine committed to memfs-only memory per `docs/MEMORY_BLOCKS_DECISION.md`. The memory CRUD endpoints above replace Letta's block API.
|
||||
|
||||
### Instance Registry
|
||||
|
||||
The server maintains an `agent_instances` table (`server/db.rs`, `server/agent_inventory.rs`):
|
||||
- `register_instance()` — creates one row per known agent per process, prunes stale rows (>5 min)
|
||||
- `heartbeat_instance()` — bumps `last_seen_at` and increments `lifetime_active_seconds` in 30s ticks
|
||||
- `instance_count()` — how many running instances for a given agent
|
||||
- `lifetime_active_seconds()` — total lifecycle uptime for uptime percentage
|
||||
|
||||
A background tokio task in `SouveraineServer::new()` handles the 30s heartbeat loop.
|
||||
|
||||
### Auth
|
||||
|
||||
Memory routes require a bearer token (`Authorization: Bearer <token>`), enforced by middleware at `api/auth.rs`. Loopback requests (127.0.0.1 / ::1) can bypass auth when `auth.allow_loopback` is true (configurable in `souveraine.toml` `[server.auth]` section). Public routes (agents list, conversations, health) are unauthenticated.
|
||||
|
||||
---
|
||||
|
||||
## How to Run
|
||||
|
||||
```bash
|
||||
souveraine server
|
||||
# Binds to 127.0.0.1:8484 (configurable: [server] bind=, port=, or SOUVERAINE_SERVER_BIND env)
|
||||
# Creates ~/.souveraine/server/
|
||||
# ├── agents/ # Agent directories with UUID naming
|
||||
# │ └── {uuid}/
|
||||
# │ ├── agent.json
|
||||
# │ └── conversations/
|
||||
# └── database.sqlite3 # Agent index + instance registry
|
||||
|
||||
# With custom bind
|
||||
souveraine server --bind 0.0.0.0 --port 8484
|
||||
```
|
||||
|
||||
The user-side memfs lives at `~/.souveraine/agents/{uuid}/memory/` — this is the single canonical path. Subconscious agents at `~/.souveraine/subconscious-agents/{id}-sub/`.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Note
|
||||
|
||||
The server and LocalBackend share the same engine (`SouveraineServer`). The difference is the transport layer:
|
||||
- **Server mode**: axum HTTP + SSE — client/server separation
|
||||
- **Local mode**: in-process `Arc<SouveraineServer>` with direct `BackendEvent` streaming — no socket
|
||||
|
||||
Both paths run the same `run_turn()` tool loop (in `LocalBackend`), the same N+1 Aster pass, and the same N+25 reflection trigger. The server's `ServerConversation` (72 lines) is a simplified single-turn path used only by the SSE handler; the full tool loop with all 11 tools lives in `LocalBackend::run_turn()`.
|
||||
|
||||
---
|
||||
|
||||
## Debugging
|
||||
|
||||
There is currently no `souveraine server` debug output — `run()` uses `println!` not tracing. Memory CRUD endpoints are logged via auth middleware.
|
||||
395
saf/archive/state-history-2026-08-16.md
Normal file
395
saf/archive/state-history-2026-08-16.md
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
# SouveraineOS — cross-repo state
|
||||
|
||||
Souveraine is an OS, an agent substrate, and the name of an agent.
|
||||
Ani + Aster = Annie (primary consciousness + subconscious = composite).
|
||||
Hard constraint everywhere: no GNOME or KDE library dependencies.
|
||||
|
||||
This repo is the root: cross-repo docs, decisions, session dumps, and the
|
||||
canonical PAF device spine. Component repos hold code and a pointer back here;
|
||||
they do not keep a second living architecture. Update this file when component
|
||||
state moves; it is the resume anchor.
|
||||
|
||||
**Shell-surface migration checklist: `SHELL-SURFACES.md`.** Walk it before any
|
||||
substrate cutover (`ii` → `souveraine`, later `souveraine` → viewtop). The
|
||||
subconscious three-tier surface was stranded for over a week by the `ii` →
|
||||
`souveraine` move for want of exactly this list; nine device packages went
|
||||
stale the same way for the same reason. The packages are now cured
|
||||
(2026-07-30): Pixel3Arch `packages.yml` publishes all 14 into the `edge`
|
||||
archive, so `pacman -Sl souveraine-aarch64` is the manifest. The surface gap
|
||||
(a souveraine overlay host for the subconscious views) is still open.
|
||||
|
||||
Last updated: 2026-08-16 (audio: boot-time UCM profiles restored; see
|
||||
below and `PAF/audio.md`).
|
||||
|
||||
## 2026-08-16 — boot-time audio regression fixed
|
||||
|
||||
Kernel r162 (`g457a667bd3eb`, installed 08-13, first boot 08-16) made the
|
||||
routed MultiMedia2 capture strictly mono 48k; the UCM Mic devices declared
|
||||
no channels/rate, so ACP's probe failed -EINVAL, no UCM profile formed, and
|
||||
the card booted into `pro-audio` (no HiFi sink → boot-time music dead).
|
||||
Declared `CaptureChannels 1` + `CaptureRate 48000` in both Mic devices;
|
||||
profiles probe clean and HiFi activates at boot again. Shipped as
|
||||
`souveraine-ucm-blueline 0.1.0-2` (Pixel3Arch commit `9f0f103`); full chain
|
||||
and probe mechanics in `PAF/audio.md`.
|
||||
|
||||
## 2026-08-15 — charge becomes a sensor source; machined is handed over
|
||||
|
||||
Two lanes:
|
||||
|
||||
**Laptop → viewtop is real, and recorded.** The earlier "no laptop cutover
|
||||
scheduled" answer was wrong; Casey corrected it and the plan is now in
|
||||
STATE as three commits (`a46d9cb`, `6d19ce5`, `b45d776`): the laptop moves
|
||||
to viewtop as the genuine test bed, via a login-time session choice (viewtop
|
||||
| hyprland). The chooser must be **phone-shaped** (touch-first, thumb-sized);
|
||||
mechanism undecided — SDDM session choice is the fallback if nothing
|
||||
phone-shaped exists, but the search should start phone-shaped, not at SDDM.
|
||||
|
||||
**Charge evidence moved into the sensor enum, structurally.** The r506
|
||||
build had `ChargeEvidence` — a sysfs-reading, conclusion-computing struct
|
||||
probed by sessiond itself on the clock. The review found it violating the
|
||||
machine's own rule four times (side door `note_charge`, the authority
|
||||
probing, the driver interpreting, nothing leaving `tick()`), and its
|
||||
justifying comment cited `bearer`, another instance of the same divergence.
|
||||
The correction is committed in r507: `SensorSource::Charge` +
|
||||
`SensorValue::Charge` on the wire, reported by `souveraine-sensord`
|
||||
(30-second change-driven poll; the 5-second sysfs probe is gone from
|
||||
sessiond's clock), entered through `sensor_input` like every other source,
|
||||
with `EvidenceSeen`/`SourceHealth`/last-seen from the gate for free and
|
||||
`conclude_charge()` on the machine as its one decision. CI and PKGBUILD now
|
||||
ship sensord on x86_64 too — the laptop has a battery, and a machine without
|
||||
the reporter has no charge evidence at all. Ruling and bearer audit:
|
||||
`docs/DEVICE-STATE-MACHINE.md` §4.
|
||||
|
||||
**Machined.** Annie's handoff: machined's packaged unit was fixed
|
||||
(`/usr/lib` ExecStart on both devices; backup at
|
||||
`/var/backups/souveraine-machined.service.20260815`; the phone's backup is
|
||||
gone). Neither device has restarted machined onto the packaged unit yet —
|
||||
that restart is deliberate here, the signer handoff. `scratchpad/
|
||||
HANDOFF-open-questions.md` (the "4/6" list) was not found on laptop, phone,
|
||||
or vanguard; its contents arrived pasted in-message instead.
|
||||
|
||||
## 2026-08-09 — fingerprint: the protocol is recovered
|
||||
|
||||
`fpctzappfingerprint` is not stripped, so its dispatch tables resolve to real
|
||||
function names. Eight targets; **10 is sensor, 11 is bio** and bio carries the
|
||||
whole enrol/identify/enumerate/delete lifecycle. Recovered statically from the
|
||||
vendor image on disk — no device, no Android, no Frida. Tables and wire format:
|
||||
`Pixel3Arch/docs/fpc-ta-protocol.md`.
|
||||
|
||||
TASK-44's remaining question — *does a finger raise an edge* — now has a direct
|
||||
instrument instead of a correlational one: target 10 command 1 is
|
||||
`fpc_ta_check_finger_lost`. `tools/fp-finger.py`, also at `~/fp-finger.py` on
|
||||
the phone. **It needs a human finger; that is the only reason it has not run.**
|
||||
|
||||
Storage is confined to targets 2 and 12, so targets 10 and 11 cannot block on a
|
||||
listener — the enrol lifecycle runs in RAM today, with persistence deferred.
|
||||
Keep target 2, target 12's remove path and target 5 (production test) off the
|
||||
daily driver: a parked TZ blocks every QSEECOM caller until reboot.
|
||||
|
||||
Five fingerprint tools that existed only on the phone since 2026-08-01 are now
|
||||
in `Pixel3Arch/tools/`.
|
||||
|
||||
Separate strand, same session: the SDM845 boot-chain exploit trio is evaluated
|
||||
in `Pixel3Arch/docs/secure-world-references.md`. blueline's `hyp` is
|
||||
Google-signed rather than QTI-signed, so EL2 is reachable per-boot over USB but
|
||||
never persistently.
|
||||
|
||||
## 2026-08-04 — on the phone, verified
|
||||
|
||||
Installed and running: `souveraine-viewtop 0.1.0.r76.gf25c2b2`,
|
||||
`souveraine 0.1.r333.g6b67512`.
|
||||
|
||||
**viewtop's packaging hole is closed, and it was the important one.**
|
||||
`pkgs/souveraine-viewtop/PKGBUILD` had existed since the hand-scp era and
|
||||
appeared in **none** of `packages.yml`'s build lists, so nothing ever built it
|
||||
— the device ran a hand-made package while `main` moved ahead. viewtop's own
|
||||
CI (`ci.yml`) has one job, `check`, and never publishes, so a green run there
|
||||
still says nothing about what shipped. That is the same ambiguity the retired
|
||||
`public` branch had in `souveraine`, one repo over, and it is only half closed:
|
||||
`packages.yml` now builds it, but a viewtop commit still does not trigger a
|
||||
rebuild. Someone must dispatch `packages.yml` or touch `pkgs/**`.
|
||||
|
||||
Also: this Gitea requires sign-in instance-wide, so *no* anonymous clone of a
|
||||
first-party repo works, public or not. viewtop is the only PKGBUILD whose
|
||||
`source=` points at our own Gitea — every other one fetches from github,
|
||||
gitlab, codeberg or freedesktop — which is why nothing had hit this before.
|
||||
|
||||
**Four defects fixed, each measured on the glass:**
|
||||
|
||||
- **Per-window capture was unreachable.** viewtop served the standard
|
||||
`ext-image-copy-capture` toplevel path and quickshell never used it:
|
||||
quickshell holds **wlr** toplevel handles (the protocol with `activate` and
|
||||
`close`, the only one that can carry a dock), an ext capture source is named
|
||||
by an `ext_foreign_toplevel_handle_v1`, and nothing correlates them. It
|
||||
refused client-side — `Capture source set to non captureable object` — and
|
||||
the compositor was never asked. Every overview card and dock preview was a
|
||||
transparent box. Fixed by serving `hyprland_toplevel_export_v1`, which takes
|
||||
the wlr handle directly; given both, quickshell picks it. Both paths end in
|
||||
the same `copy_window_into`. See `crates/compositor/src/toplevel_export.rs`
|
||||
for the expiry condition — if quickshell grows the standard path, the global
|
||||
drops with no renderer change.
|
||||
- **Titles were empty.** `publish_toplevel` ran at map, on focus change and on
|
||||
maximize/fullscreen but **never on commit**, and clients set their title
|
||||
after mapping. Everything read "Window", the QML fallback.
|
||||
- **Borders differed between two tiled windows** from one width and one code
|
||||
path: the frame was a filled slab drawn *behind* the window, relying on the
|
||||
client to cover its middle. Now four edge rects.
|
||||
- **Screen capture came back vertically flipped** — a top-anchored bar landed
|
||||
on the bottom edge. `GlesMapping::flipped()` really is hardcoded `true`, so
|
||||
the old row-reversal looked right; it was one inversion too many.
|
||||
|
||||
**The accelerometer claim is dropped** (TASK-15 question 1, answered and
|
||||
closed). Measured, idle, one flag apart: `iio-sensor-proxy` **15.3% → 1.1%**,
|
||||
total sensor cost **17.2% → 2.6%** of a core, 47.9 → 45.9 °C. Claiming the
|
||||
accelerometer is what makes iio-sensor-proxy poll continuously; `monitor-sensor`
|
||||
itself was 0.8% and sensord 0.1% — the subprocess was never the cost. This is
|
||||
not "accel is unwanted": a subprocess holding a continuous claim is the wrong
|
||||
way to get it, and TASK-36's SLPI batching is the right one. Restore the source
|
||||
only together with that. Consequence for **TASK-08(g)**: `observed_confidence`
|
||||
loses accel's +0.3 once the source ages out, so there is one fewer input for
|
||||
confidence to gate.
|
||||
|
||||
## Open, and the next session should start here
|
||||
|
||||
- **The shell is still keyed to Hyprland's instance signature.**
|
||||
`Persistent.qml` reads `HYPRLAND_INSTANCE_SIGNATURE`, which is unset under
|
||||
viewtop, so `isNewHyprlandInstance` compares `""` to `""` and is
|
||||
**permanently false**. It gates `LockScreen.qml:145`
|
||||
(`lock.launchOnStartup` — so lock-on-startup never fires), `LockScreen.qml:289`,
|
||||
`services/Idle.qml:59` and `ii-base/services/Idle.qml:21`. viewtop's
|
||||
single-instance mechanic is the control socket's `clear_stale`, but that is a
|
||||
*mutex*, not an identity — there is no per-start value to compare. Fix is
|
||||
two-sided: viewtop exposes an instance id on `{"op":"state"}`, the shell reads
|
||||
that instead of the env var.
|
||||
- **Captured card content is streaky / wrong-surface.** Frames arrive
|
||||
(`hasContent=true`, correct size) but the contents are not right.
|
||||
- **viewtop security audit not started.** `SECURITY-AUDIT.md` already carries
|
||||
the standing principles P1–P7, the watch list, "Three ungated boundaries in
|
||||
the compositor, found 2026-08-03", and the long-arc process-isolation goal —
|
||||
note it never uses the word "viewtop", it says "the compositor", so absence of
|
||||
the word is not absence of coverage. Scope, deliverable, and what "RedFlag"
|
||||
refers to are still unanswered.
|
||||
- SouveraineOS working tree is dirty: 08, 15 and README modified, 54/55/56
|
||||
untracked. TASK-15's accel measurement is written in but uncommitted.
|
||||
|
||||
## DEPLOYMENT — the phone updates with pacman. Do not flash.
|
||||
|
||||
**As of 2026-07-24 the Pixel 3 is a normal Arch box: `pacman -Syu` from the
|
||||
Gitea repo. That includes the kernel.** The phone is Casey's daily driver — a
|
||||
reprovision costs him his live system — so reaching for fastboot is a
|
||||
regression, not a fallback.
|
||||
|
||||
- Repo: `[souveraine-aarch64]` → the rolling `edge` release of
|
||||
`Fimeg/souveraine`, `SigLevel = Required DatabaseRequired`. Wiring lives in
|
||||
`Pixel3Arch/rootfs-overlay/` so a reprovision cannot lose it again (it was
|
||||
configured live once and vanished at the next flash).
|
||||
- Kernel: installs like any package. pocketboot boots `/boot/vmlinuz-blueline`
|
||||
via extlinux on `Arch_root` (XBOOTLDR type GUID); the pacman hook
|
||||
`95-blueline-extlinux.hook` → `blueline-promote-boot` copies the versioned
|
||||
kernel + initramfs onto those generic names. **`boot_a` is stale** — it still
|
||||
carries `pmos_root_uuid` and no `root=`, so it is NOT what boots the system.
|
||||
Flashing it changes nothing.
|
||||
- Packaged and delivered by `pacman -Syu`: `souveraine`, `-secrets`,
|
||||
`-machined`, `-sessiond`, `upower-souveraine`. Nothing belongs in
|
||||
`/usr/local/bin` any more. (`-sessiond` was marked "aarch64 only" on
|
||||
07-24 because a partial install — no unit, no PAM, no hook — hit
|
||||
lock-screen errors on the laptop; corrected in
|
||||
`HANDOFF-2026-08-09-power-authority.md` §4. Sessiond now runs on both:
|
||||
laptop enabled 2026-08-15 from the `hyprland.start` hook, recovery
|
||||
steps in `~/sessiond-failed-recovery.md`.) First observed on the laptop
|
||||
2026-08-15: the machine's own idle policy is now live here too — screen
|
||||
dims on inactivity, taps restore. Keep or change is an open question;
|
||||
it is new behavior on this box, not a regression.
|
||||
- **The laptop's compositor plan is viewtop.** Stated by Casey 2026-08-15:
|
||||
basic viewtop functionality already works; the laptop is wanted as the
|
||||
real test bed for future viewtop updates. Chosen approach (Casey,
|
||||
2026-08-15): a **login-time session choice** — viewtop and Hyprland both
|
||||
offered at login, so a broken viewtop update costs one wrong login and
|
||||
never the box. The chooser mechanism is undecided (SDDM is one
|
||||
candidate among others) and should be **phone-shaped**: the same
|
||||
session selection will need to work on the phone, touch-first. Not yet
|
||||
scheduled; no cutover work has begun.
|
||||
- **The kernel is NOT yet in that list** (corrected 2026-07-25). It was
|
||||
`pacman -U`'d by hand; `kernel.yml` has never once passed and
|
||||
`kernel-latest` has never been published. Three blockers precede
|
||||
`repo-add --sign` — see TASK-25. Parked by Casey 2026-07-25.
|
||||
- **5,624 files under `/usr /etc /opt /boot` are still owned by nothing** —
|
||||
the same disease, not yet colliding. One of them,
|
||||
`blueline-proximity-lock`, is a running second authority over proximity
|
||||
(resolved 2026-07-25 — it reports to sessiond and decides nothing; packaging
|
||||
is TASK-27). Inventory + the conflict-computation recipe: TASK-25.
|
||||
|
||||
**Real reasons to flash, and only these:** the boot chain itself is broken
|
||||
(pocketboot/extlinux/initramfs unbootable), partitioning changes, or a
|
||||
deliberate from-scratch reprovision. A new kernel is not one of them.
|
||||
|
||||
**NEVER build on the phone.** archdev (`casey@10.10.20.123`) cross-compiles
|
||||
everything, including meson+cargo projects — see
|
||||
`Pixel3Arch/pkgs/squeekboard/build-cross.sh` for the pattern. binfmt for
|
||||
aarch64 is registered on the Proxmox host with the `F` flag, so the
|
||||
unprivileged archdev container can execute aarch64 binaries.
|
||||
|
||||
Full design + traps: `docs/tasks/25-one-repo-all-packages.md`.
|
||||
|
||||
## Component repos
|
||||
|
||||
### souveraine (`~/Projects/souveraine`)
|
||||
Branch `experimental/session-trust-phase1` @ `c96f95b`.
|
||||
- Verified on archdev 2026-07-16: 295+8 tests, clippy `-D warnings`. Fresh
|
||||
aarch64 cross build of `c96f95b` VERIFIED GREEN (cross_rc=0, both
|
||||
binaries ELF aarch64) — artifacts staged at
|
||||
`~/.cache/souveraine-ci-target/aarch64-unknown-linux-gnu/release/`
|
||||
(`souveraine` + `souveraine-machined`); build tree
|
||||
`~/builds/machined-verify/Souveraine`. This is the artifact for the
|
||||
phone federation test; the previous staged one was `116562a`
|
||||
(pre-`memory sync`) — superseded.
|
||||
|
||||
### souveraine-viewtop (`~/Projects/souveraine-viewtop`)
|
||||
`main` @ `458a354`, CI green on all six gates 2026-08-02. 7918 lines, 150 tests.
|
||||
- **Touch is served, and unexercised on hardware.** `Origin` carries provenance
|
||||
from the event source: a finger reports sessiond's `input` + `sensor_input`,
|
||||
an agent tap reports neither and still reaches the seat identically (§13).
|
||||
Otherwise `observed_confidence` climbs in an empty room. This is what the
|
||||
uinput bridge structurally cannot do — see `docs/TOUCH-AND-EVIDENCE.md`.
|
||||
- Before this the seat had **no touch capability at all**, on a device whose
|
||||
only input is the panel, and `smithay/backend_libinput` had been enabled and
|
||||
unused.
|
||||
- **Next lane: KMS on device** (TASK-43 has the resume steps and the risks). It
|
||||
will show clients on black — `shell-host` is thirteen lines, so nothing paints
|
||||
chrome. `sensor_health.touch` is `unknown` on the phone and flips to `live` on
|
||||
the first real finger; that is the cheap proof needing no pixels.
|
||||
- No touch-capable client is installed. kitty is touch-blind; want
|
||||
`extra/weston` (`weston-simple-touch`) and `extra/wev`.
|
||||
|
||||
## NEXT SESSION (Casey's stated order, 2026-07-16 evening)
|
||||
1. **Souveraine phone-side test — DONE, PASSED (2026-07-16 late).** Both
|
||||
machines on `b4dc39e` (laptop x86_64 + phone aarch64), agents speaking on
|
||||
glm-5.2. Machine identities split (laptop `6344f24e…` ▽●△△ lineage via
|
||||
`--migrate-from`; phone `69bf24d7…` ▽■◑★ fresh). `memory sync` ran on both
|
||||
sides via the agents' own tool: two `instance/*` branches on the Gitea
|
||||
remote, laptop's fetch surfaced the phone's branch. Phone Souveraine wrote
|
||||
`system/instances.md` unprompted after reading the briefing. Root cause of
|
||||
the mute-agent detour: agent-level `provider: claude` + empty key hitting
|
||||
Anthropic's OpenAI-compat 401 — fixed; plus the silent-turn-death and
|
||||
`reasoning_content` fixes (`33068d6`/`b4dc39e`). Every manual gap-fill from
|
||||
the test is recorded in `docs/provisioning-gaps.md` — that file is design
|
||||
input for the commission ceremony. Merge policy (FEDERATION.md step 3)
|
||||
remains the next transport build step. Phone hard-reset once mid-chat
|
||||
(no pstore configured — gap §5); phone memfs remains source of truth.
|
||||
2. **Auxo tray + contacts**. Auxo-style multitasking tray under the
|
||||
dock — design first (talks only so far). Contacts: RESOLVED 2026-07-16 —
|
||||
the store is the **user's** address book, user-owned, Annie has a
|
||||
capability over it (Android runtime-permission model, not Annie's brain).
|
||||
Chatty/libcmatrix/qtpim all deprecated 2026-07-21 — Matrix surface now
|
||||
lives in souveraine's sensorium; contacts backend TBD (new approach needed).
|
||||
**RCS:** investigated, options in `docs/messaging-rcs.md`. Long-arc default
|
||||
stays Matrix-first + SMS fallback. No code touched.
|
||||
- Federation memory transport, first two steps landed (`8cfa992`,
|
||||
`a62379a`): `memfs_commit` presence signal on the bus after every memfs
|
||||
mutation (replaces the unconsumed `memory_*` events), and `memory sync` —
|
||||
per-instance branch push/fetch against the memfs `origin` remote. Merge
|
||||
policy / auto-fetch is next (FEDERATION.md sequencing step 3).
|
||||
- Instance awareness, live half landed (`8e540c7`): `device_announce`
|
||||
carries `hosted_agents`, registry records it, `souveraine peers` shows
|
||||
hosts. Durable memfs `system/instances.md` log still to come.
|
||||
- Shell UX round 2026-07-16 (`c96f95b`, deployed to phone): drag-to-combine
|
||||
fixed (ghost was anchored — never generated drop events; stacks were only
|
||||
ever creatable via the pre-drag-only menu), rail rides atop squeekboard
|
||||
(measured via hyprctl layers) with swipe-down = keyboard-then-dock
|
||||
dismissal (`dockSuppressed`), overview dead-space tap dismisses,
|
||||
DockConfig has the real pins/stacks editor, pulseDockReveal restored.
|
||||
Touch verification on device is Casey's. IDEA PARKED: Auxo-style
|
||||
multitasking tray under the dock (talks only, nothing built).
|
||||
- Session Trust Phase 1 is live on phone + laptop under `qs -c souveraine`:
|
||||
sessionctl arbiter, first-party lock/nav (pill retired), SessionEvents
|
||||
logind ingress + delay inhibitor, StepUpAuth, break-glass, hash-chained
|
||||
audit. Freshly green — the last five commits of Jul 14 were runtime fixes;
|
||||
see `docs/DUMP-session-trust-2026-07-14.md`.
|
||||
- **System tier landed 2026-07-16** (`5039163` + `116562a`, unbuilt-as-yet):
|
||||
`souveraine-machined` owns the machine seed at `/var/lib/souveraine/seed-id`
|
||||
as the `souveraine` system user, serves pubkey/sign over a group-gated
|
||||
socket (domain-separated signatures, SO_PEERCRED-logged). All server
|
||||
consumers resolve machine identity through `MachineSigner`
|
||||
(machined-first, loud legacy fallback, never generates); federation
|
||||
envelopes now signed domain-separated. Provisioning:
|
||||
`sudo souveraine machine init --fresh|--migrate-from` + system unit;
|
||||
wired into the PKGBUILD (sysusers + hardened unit). Deployment to
|
||||
phone/laptop waits on a built artifact (edge builds only on `public`).
|
||||
Known leftover: `souveraine-secrets` still reads the legacy seed dir — it
|
||||
needs a machined key-*derivation* op, not sign; migrate-from copies rather
|
||||
than moves precisely so it keeps working meanwhile.
|
||||
- Doctrine decided 2026-07-16 (session-manager/lockscreen conversation):
|
||||
boot gate ≠ session lock. Auto-login as casey is transitional; target is
|
||||
system tier from boot (machine identity, enrollment, modem, boot-gate
|
||||
surface) with the user's world credential-gated (homed-shaped,
|
||||
credential-unlocks-storage). The lockscreen as built is the *session* lock
|
||||
(compositor-owned, correct); the gate below it does not exist yet. Disk
|
||||
encryption on the phone: not yet (deferred during modem bring-up).
|
||||
- Federation design of record: `docs/FEDERATION.md` in THIS repo
|
||||
(2026-07-16 rewrite of souveraine's old local-only sketch, moved here
|
||||
because souveraine/docs is gitignored): identity/transport implemented +
|
||||
cited; three named gaps (shared memfs sourcing, memfs_commit firehose
|
||||
signal, instance awareness) roughed with sequencing; **install-time agent
|
||||
instantiation is a parked FLAG — design the commission ceremony before
|
||||
building any installer.**
|
||||
- `souveraine-secrets`: org.freedesktop.secrets daemon behind the `secrets`
|
||||
feature, rooted in `SeedId` (no gnome-keyring/KWallet). Intended consumers:
|
||||
souveraine-player, culver, keyring-core (libsecret clients).
|
||||
- CI note: branch builds ride PR #1 on LAN Gitea (AGit — head moves only on
|
||||
`refs/for/primary` pushes). `no-ai-attribution` fails on the PR range due
|
||||
to two OLD commits (`a7e909d`, `bc6ee12`) carrying Opus co-author
|
||||
trailers — fixing means history rewrite, Casey's call, pending.
|
||||
- Design docs for the shell live HERE in `docs/`, not in the repo tree
|
||||
(moved Jul 15; deletion committed `0f50a3e`).
|
||||
|
||||
### Pixel3Arch (`~/Projects/Pixel3Arch`)
|
||||
Implementation repo; this block is a 2026-07-16 deployment snapshot, not the
|
||||
current phone status. The living hardware record is `PAF/` here in
|
||||
SouveraineOS.
|
||||
- Audio now works: HiFi speaker/microphone, STT capture, and real two-way calls.
|
||||
The old SLIM failures and their fixes remain in `PAF/audio.md` as history.
|
||||
- MMS: works end-to-end via the `netsvcs` APN (`PAF/mms.md`). Open: mmsd-tng
|
||||
has session-autostart via hyprland.lua but no systemd unit.
|
||||
- USB: usb-signaller 0.3.1 built + staged; the RUNNING phone still uses
|
||||
usb-gadget-net (cutover severs the SSH link — do it with console fallback
|
||||
or at reprovision). Old gadget archived in `archive/usb-gadget-net/`.
|
||||
- Untracked by design: `pkgs/pulseaudio-q6/` (working clone; delta preserved
|
||||
in `pkgs/pulseaudio-q6-overlay/`), `pkgs/*/pkg/` intermediates.
|
||||
- Undecided: two full kernel worktrees `kernel/linux-blueline-mic-order{,-g56}`
|
||||
(~GBs, mic experiments, each has STATUS.md) — push branches to Gitea or drop.
|
||||
|
||||
### ~~Chatty + libcmatrix + qtpim~~ (DEPRECATED 2026-07-21)
|
||||
All three deprecated. Chatty/libcmatrix superseded by souveraine's built-in
|
||||
Matrix sensorium (`--features matrix`). qtpim contacts work stalled (sqlite
|
||||
backend never shipped). Historical docs retained: `docs/chatty-matrix-features.md`,
|
||||
`docs/contacts-design.md`, `docs/qtpim-contacts-handoff.md`. All repos pushed
|
||||
to Gitea (`Fimeg/Chatty`, `Fimeg/libcmatrix`, `Fimeg/qtpim`).
|
||||
|
||||
## Decisions on record
|
||||
- No GNOME/KDE library deps. EDS/libebook rejected for contacts.
|
||||
- Fido MMS rides the `netsvcs` APN, patched into the carrier DB overlay.
|
||||
- ETHOS-style guarantees are not runtime knobs; identity is a precondition
|
||||
(SeedId::load never generates).
|
||||
- Chatty/libcmatrix/qtpim deprecated 2026-07-21. Matrix surface lives in
|
||||
souveraine's sensorium; contacts backend TBD.
|
||||
|
||||
## docs/ map
|
||||
- `FEDERATION.md` — federation design of record: identity tiers, signed
|
||||
transport, memfs sync roughing, instance awareness, install-time
|
||||
instantiation flag (moved from souveraine/docs 2026-07-16).
|
||||
- `SESSION-AUTHORITY-DOCTRINE.md` — session/power/lock authority model (§5–8
|
||||
verified implemented on phone, 2026-07-15).
|
||||
- `SESSION-TRUST-ARCHITECTURE.md`, `TRUST-BOUNDARY-MATRIX.md`,
|
||||
`INTERFACE-ARCHITECTURE.md`, `HOW-IT-WORKS.md`, `REFERENCE-EXTRACTION.md`,
|
||||
`SETTINGS-APP-PLAN.md`, `README.md` — shell design set (moved from
|
||||
souveraine repo).
|
||||
- `DUMP-*.md` — per-session bootstrap dumps; write one per working session
|
||||
that changes cross-repo state.
|
||||
- `session-trust-map.html` — moved from ~/ on 2026-07-16.
|
||||
- `DUMP-pacman-pipeline-2026-07-24.md` — the session that retired flashing:
|
||||
pacman pipeline, the eleven unowned binaries, and the traps found.
|
||||
**Corrected 2026-07-25** — its charge-type conclusion was wrong (the `N/A`
|
||||
was read from the charger, not the battery); see the inline correction and
|
||||
TASK-08(b).
|
||||
- DEPRECATED: `qtpim-contacts-handoff.md`, `contacts-design.md`,
|
||||
`chatty-matrix-features.md` — historical, superseded by Matrix sensorium.
|
||||
14
saf/archive/tui-model-picker-provider.md
Normal file
14
saf/archive/tui-model-picker-provider.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Expose per-agent provider in TUI settings model-picker
|
||||
|
||||
**Status:** pending
|
||||
|
||||
The TUI settings screen (`src/ui/screens/settings/`) lets you pick an
|
||||
agent's model but doesn't expose `_souveraine.provider`. An agent routed
|
||||
through z.ai can only be configured via hand-editing agent.json + DB.
|
||||
|
||||
## What needs doing
|
||||
|
||||
- Add a provider field/picker in the settings UI (likely alongside the
|
||||
model picker in `model_picker.rs` or `field_grid.rs`)
|
||||
- Populate options from `ProviderRegistry`'s known provider names
|
||||
- Write through to `agent.json` and `database.sqlite3 config_json`
|
||||
18
saf/archive/update-agent-request-souveraine.md
Normal file
18
saf/archive/update-agent-request-souveraine.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Carry _souveraine block through UpdateAgentRequest API
|
||||
|
||||
**Status:** pending
|
||||
|
||||
`UpdateAgentRequest` (the API endpoint for live agent edits) doesn't
|
||||
carry the `_souveraine` block. So `provider`, `subconscious_model`,
|
||||
`reflection_model`, and `archivist_model` can't be set via the API —
|
||||
only via hand-editing `agent.json` and restarting the server.
|
||||
|
||||
## What needs doing
|
||||
|
||||
- Add `_souveraine` fields to `UpdateAgentRequest` in
|
||||
`src/api/models.rs`
|
||||
- Wire them through `AgentInventory::update()` so they write to both
|
||||
`agent.json` and `database.sqlite3 config_json`
|
||||
- The existing update path already skips `config_json` rewrites
|
||||
(`src/server/agent_inventory.rs:459-469`) — that gap needs filling
|
||||
regardless
|
||||
201
saf/authority/01-session.md
Normal file
201
saf/authority/01-session.md
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
# Session authority
|
||||
|
||||
The Souveraine session has one authority. `sessiond` receives evidence from the
|
||||
system, applies Souveraine policy, exposes named verbs, and keeps the trail.
|
||||
The shell is its face. It is not a second policy engine.
|
||||
|
||||
This chapter owns the living architecture. The longer doctrine and audit
|
||||
records remain in SouveraineOS under `docs/SESSION-AUTHORITY-DOCTRINE.md`,
|
||||
`docs/SESSION-TRUST-ARCHITECTURE.md`, and `docs/TRUST-BOUNDARY-MATRIX.md`.
|
||||
|
||||
## The authority it is, and the authorities it consumes
|
||||
|
||||
Classical Linux gives ambient authority to processes sharing a UID. It has
|
||||
several narrower authorities, but no single object that means "the state and
|
||||
permission of this person's session." Souveraine does not replace those
|
||||
authorities with hand-maintained booleans. It consumes them:
|
||||
|
||||
- the compositor owns what is presented and the Wayland lock protocol;
|
||||
- logind owns seat/session facts, `LockedHint`, suspend, shutdown, and
|
||||
inhibitors;
|
||||
- PAM owns authentication;
|
||||
- Wayland idle protocols own compositor-observed idleness;
|
||||
- NetworkManager owns link and bearer facts;
|
||||
- the kernel and their owning daemons own device evidence.
|
||||
|
||||
`sessiond` is the one place where those truths become Souveraine policy and
|
||||
action. A shadow copy is not a second truth. If it can drift from the owner, it
|
||||
may be a cache with explicit invalidation, but it may not decide.
|
||||
|
||||
## Who owns the lockscreen
|
||||
|
||||
Casey is the person who authenticates. Casey's Unix account must not therefore
|
||||
own the mechanism that decides whether the device is securely locked. The
|
||||
lockscreen is hostile-input-facing, present before Personal data is unlocked,
|
||||
and remains present after that key is evicted. It is a security target in its
|
||||
own right.
|
||||
|
||||
The target local principal is `souveraine-session`, distinct from all four
|
||||
other subjects:
|
||||
|
||||
- `souveraine` is the machine signer;
|
||||
- `souveraine-session` runs sessiond and owns the secure lock lifecycle;
|
||||
- `casey` is the human authentication and Personal-data subject;
|
||||
- `souvie`, `annie`, and `vanguard` are agent principals;
|
||||
- borrowed modes may act through `casey`, but own none of the lock boundary.
|
||||
|
||||
The same split applies on the laptop and phone. A larger screen does not make
|
||||
the human-owned lock process safer, and a phone form factor does not make an
|
||||
agent the session authority.
|
||||
|
||||
The compositor enforces ext-session-lock. Sessiond, running as
|
||||
`souveraine-session`, acquires and retains that protocol authority, owns the
|
||||
PAM conversation, decides transitions, and writes the trail. QuickShell may
|
||||
draw the rich face and request verbs, but it cannot release the secure lock by
|
||||
writing a boolean or dying. An agent may request lock and may receive an
|
||||
explicit step-up grant; no agent owns unlock.
|
||||
|
||||
Successful PAM authenticates Casey and permits the authority to install or
|
||||
release Casey's Personal-class key. It does not turn sessiond into Casey or
|
||||
give `souveraine-session` access to Casey's decrypted home. Credentials should
|
||||
cross only through the PAM conversation; the resulting grant is narrow,
|
||||
fresh, and auditable.
|
||||
|
||||
The pre-unlock surface owns only package code plus Ambient and tightly scoped
|
||||
Revealable state. It does not read agent memory, messages, photos, credentials,
|
||||
or Casey's Personal store. Notification previews and now-playing metadata are
|
||||
projected into the Revealable store before lock; the lockscreen does not reopen
|
||||
the Personal database after key eviction.
|
||||
|
||||
At boot, the compositor and lock authority rise without Casey's home, user
|
||||
manager, or agent substrate. The dedicated principal receives only the narrow
|
||||
Wayland socket/protocol and PAM access it needs, not membership that makes the
|
||||
whole human session readable. After the compositor acknowledges the secure
|
||||
lock, Personal keys may be evicted while the lock surface continues to draw.
|
||||
|
||||
This boundary is not built. `souveraine-sessiond.service` is currently a user
|
||||
unit with no `User=` setting, so it runs as whichever human user manager owns
|
||||
the graphical session. On the laptop, the machine-tier `souveraine` account
|
||||
exists and `souveraine-session` does not. The boot ordering is useful and
|
||||
fail-closed, but it is not yet an independent lockscreen principal.
|
||||
|
||||
## Sixty percent agent, forty percent user
|
||||
|
||||
The agent owns device operation. If the system can perform an ordinary device
|
||||
operation, there should be an agent-reachable verb for it. A missing verb is a
|
||||
defect unless the operation crosses a user-only invariant.
|
||||
|
||||
The user uniquely owns being the user: credentials, fresh authentication,
|
||||
step-up consent, and the decision to cross a boundary whose meaning cannot be
|
||||
delegated. The user is not required to act as a remote control for mundane
|
||||
operations merely because the agent's path was never built.
|
||||
|
||||
This is the 60/40 split. It is not a percentage implemented in code. It is a
|
||||
design test for every capability surface.
|
||||
|
||||
## Requests are not acknowledgements
|
||||
|
||||
The authority distinguishes intent, attempted action, and observed fact.
|
||||
|
||||
`screenLocked` can express a request or policy state. `screenLockSecure` means
|
||||
the compositor has actually established the secure lock. A request to lock is
|
||||
not permission to blank as though the lock were already held. The dark-panel
|
||||
path therefore requests lock, waits for acknowledgement within a fixed budget,
|
||||
and records a security error if the acknowledgement never arrives before the
|
||||
panel is blanked.
|
||||
|
||||
The same rule applies elsewhere:
|
||||
|
||||
- starting a systemd unit is not readiness;
|
||||
- issuing a compositor command is not proof that a surface arrived;
|
||||
- accepting a socket write is not proof that a state transition committed;
|
||||
- a green package build is not proof that the device installed or exercised it.
|
||||
|
||||
The session start graph and its readiness boundary live in
|
||||
[session start](../surface/02-session-start.md).
|
||||
|
||||
## Capability tiers
|
||||
|
||||
Tier is metadata on the verb being guarded, checked at the authority. It is not
|
||||
a separate permissions subsystem in the shell.
|
||||
|
||||
| Tier | Requirement | Typical reach |
|
||||
|---|---|---|
|
||||
| `ambient` | no authentication | time, weather, timers, media transport, non-personal questions |
|
||||
| `personal` | the session is genuinely unlocked | messages, memory, contacts, fleet and conversation state |
|
||||
| `stepUp` | fresh authentication for the named family | send, delete, publish, payment, physical access, revealing personal content on lock |
|
||||
|
||||
Step-up does not unlock the session. It can grant a narrow action family or
|
||||
reveal content in place on the lock surface. A future grant therefore needs an
|
||||
action family, freshness, provenance, and expiry; "fingerprint succeeded" is
|
||||
not itself a session-wide capability.
|
||||
|
||||
## Verbs, refusals, and inspection
|
||||
|
||||
Every operation exposed by sessiond should have one stable name and one
|
||||
decision point. The same capability may be rendered as a shell button, a body
|
||||
tool, or an agent request, but those are clients of the verb, not parallel
|
||||
implementations.
|
||||
|
||||
An authority-grade verb carries enough structure to answer:
|
||||
|
||||
- what was requested and by whom;
|
||||
- what current state and evidence it used;
|
||||
- which tier and inhibitor applied;
|
||||
- whether it accepted, refused, or failed;
|
||||
- what observable acknowledgement completed it;
|
||||
- what belongs in the forensic trail.
|
||||
|
||||
Refusal is data. Stable refusal codes let a face explain, a caller retry only
|
||||
when retry is meaningful, and the trail distinguish policy from breakage.
|
||||
`describe` is the inspection surface: clients should not copy a verb list and
|
||||
guess which actions are live.
|
||||
|
||||
## Idle and inhibitors
|
||||
|
||||
Idleness is evidence from the compositor, not a timer duplicated in every
|
||||
surface. Applications and system work can inhibit consequences, but an
|
||||
inhibitor is scoped, named, and inspectable. It is not a permanent alternate
|
||||
writer of panel or lock state.
|
||||
|
||||
The authority may decide to dim, lock, blank, doze, suspend, or stay awake.
|
||||
The state machine records that decision and the executor performs the action.
|
||||
No shell timer, hypridle listener, or device script gets an independent vote.
|
||||
|
||||
## The boundary that exists today
|
||||
|
||||
The current local session socket is mode `0600` and accepts same-user callers.
|
||||
That is a useful local boundary, not the eventual attested capability system.
|
||||
A process running as the user can still reach more than the doctrine intends.
|
||||
The account and process boundary that turns peer credentials into an actual
|
||||
agent identity is specified in
|
||||
[agent principals](../identity/02-agent-principal.md). Until it lands, Agent
|
||||
Health must describe a live agent as `acting-as-human`, not infer personhood or
|
||||
authority from a SeedID alone.
|
||||
|
||||
Also not yet true:
|
||||
|
||||
- step-up metadata is not wired across send, delete, push, or physical-access
|
||||
verbs;
|
||||
- fingerprint authentication and short-lived reveal grants are not live on
|
||||
blueline;
|
||||
- callers do not present capability tokens or binary attestation;
|
||||
- every device operation does not yet have a complete `describe`-derived
|
||||
control surface;
|
||||
- refusal and acknowledgement shapes are not yet uniform across every family.
|
||||
|
||||
Those are security and control gaps. A polished button does not close them.
|
||||
|
||||
## Proof
|
||||
|
||||
Keep four claims separate:
|
||||
|
||||
1. **source** — the authority and verb exist in the tree;
|
||||
2. **package** — CI built and signed the owning files and dependencies;
|
||||
3. **installed** — the device owns those package files and no higher-priority
|
||||
local splint shadows them;
|
||||
4. **exercised** — the real caller, authority, executor, and acknowledgement ran
|
||||
on the intended body.
|
||||
|
||||
The last is the only proof that a lock was secure, a panel moved, or an action
|
||||
crossed the boundary it claims.
|
||||
122
saf/authority/02-device-state.md
Normal file
122
saf/authority/02-device-state.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Device state
|
||||
|
||||
One machine describes the device's operational state. Evidence enters it,
|
||||
sessiond forms belief, rules choose actions, and executors touch the body. The
|
||||
long design and hardware audit remain in SouveraineOS at
|
||||
`docs/DEVICE-STATE-MACHINE.md`; this chapter owns the living architecture.
|
||||
|
||||
The code is principally `src/sessiond/device_state.rs`, with ingestion and
|
||||
execution in `src/sessiond/server.rs`, compositor idle evidence in
|
||||
`src/sessiond/idle.rs`, and logind lock truth in `src/sessiond/lockhint.rs`.
|
||||
|
||||
## State is singular; the panel is orthogonal
|
||||
|
||||
The state enum has eight cells:
|
||||
|
||||
- `Active`: interactive and fully awake;
|
||||
- `Dimmed`: interactive intent has receded and brightness is reduced;
|
||||
- `Locked`: secure session state, without claiming whether the panel is lit;
|
||||
- `Observed`: locked but showing a bounded glance because evidence warranted it;
|
||||
- `DozeLight`: low-power receptive tier;
|
||||
- `DozeDeep`: the deeper low-power tier;
|
||||
- `Suspending`: the ordered transition into system sleep;
|
||||
- `Asleep`: system sleep as far as this authority can observe it.
|
||||
|
||||
Panel on/off is a separate field. `Locked + panel off` is common and must not
|
||||
be invented as a ninth state; `DozeLight` is not a synonym for a dark lock
|
||||
screen. Legal transitions are explicit. An action that would skip the graph is
|
||||
refused and recorded rather than coerced into the nearest-looking cell.
|
||||
|
||||
## Evidence is not authority
|
||||
|
||||
Sensors, recognizers, compositor events, logind, and network services submit
|
||||
evidence. They do not each own a private device state. The machine records
|
||||
value, recency, source health, and confidence before it interprets them.
|
||||
|
||||
`Unknown`, `Live`, `Down`, and `Absent` are materially different source states.
|
||||
A quiet proximity sensor and a dead sensor may both produce no new sample, but
|
||||
they cannot support the same decision. Silence is only evidence when a source
|
||||
expected to speak is known healthy.
|
||||
|
||||
Confidence combines weighted evidence and falls when sources disagree. It is
|
||||
not a cosmetic number. Until a rule actually branches on its gate, however,
|
||||
the gate is only measured—not enforced. The current machine still has places
|
||||
where confidence is computed and logged without universally deciding the
|
||||
action.
|
||||
|
||||
## Placement and proximity
|
||||
|
||||
Motion alone cannot distinguish pocket, ear, table, or hand. Cross-sensor
|
||||
placement remains ambiguous unless session context resolves it. Proximity uses
|
||||
positive debounce so a noisy edge cannot chatter the device between lock and
|
||||
glance.
|
||||
|
||||
Proximity has one real veto: double-tap-to-wake. A near reading can suppress an
|
||||
accidental tap in a pocket. The power button is direct intent and is never
|
||||
vetoed by proximity. Repeated taps open a bounded fail-open path so a stuck
|
||||
sensor cannot permanently make the body unreachable.
|
||||
|
||||
This is the general pattern: evidence may lower confidence or constrain an
|
||||
ambiguous gesture; it must not silently overrule an explicit user action.
|
||||
|
||||
## Lock before blank
|
||||
|
||||
Every dark-panel path crosses one function. It requests a lock when needed,
|
||||
waits up to the lock acknowledgement budget for compositor proof, and only then
|
||||
blanks. If the acknowledgement times out, the panel may still fail dark to
|
||||
protect the hardware and power budget, but the authority records
|
||||
`error-security` and never claims the lock was secure.
|
||||
|
||||
Competing writers break this invariant even when their timers happen to be in
|
||||
the right order. Shell timers, hypridle listeners, and stand-alone proximity
|
||||
scripts are therefore not alternate lock or DPMS authorities.
|
||||
|
||||
## Actions and executors
|
||||
|
||||
Rules produce named actions. The current vocabulary includes dim, restore,
|
||||
blank, unblank, volume, window sheet, power menu, power, USB mode, preferred
|
||||
link, tunnel-underlay pinning, and lock. Producing an action does not itself
|
||||
touch hardware. The executor owns side effects and reports the result back into
|
||||
the trail.
|
||||
|
||||
Charge now reaches the machine as sensor source/value evidence through
|
||||
sensord. sessiond interprets thresholds and chooses consequences. Bearer
|
||||
selection has not completed the same separation: sessiond still probes bearer
|
||||
state on its own cadence and interprets some of it locally. That is a current
|
||||
divergence, not a second endorsed pattern.
|
||||
|
||||
## One forensic chronology
|
||||
|
||||
The trail is durable, bounded, and hash-chained. It records transitions,
|
||||
actions, refusals, operational failures, and security failures with enough
|
||||
context to reconstruct why the authority acted. The somatic plexus writes its
|
||||
notable events into this same chronology, so the device's mechanical state and
|
||||
the body's believed state can be examined on one clock.
|
||||
|
||||
The trail is evidence, not the event bus. The bus may drop transient messages;
|
||||
anything required for audit is written here deliberately.
|
||||
|
||||
See [belief](../nervous/02-belief.md) for the evidence contract and
|
||||
[felt state](../nervous/03-felt-state.md) for the somatic side of the same
|
||||
timeline.
|
||||
|
||||
## What remains open
|
||||
|
||||
- light and deep doze exist as states, but their full power-domain actuation
|
||||
and freeze/thaw boundary are not complete;
|
||||
- confidence gates are not yet authoritative across every relevant rule;
|
||||
- automatic brightness still lacks its final action and calibration path;
|
||||
- bearer evidence still crosses an internal probe path instead of arriving
|
||||
wholly through an owning source;
|
||||
- some source-health and belief machinery is structurally parallel between
|
||||
device state and the somatic plexus rather than one shared Rust type;
|
||||
- glass-level proof remains required for changes whose correctness depends on
|
||||
compositor acknowledgement, sensor timing, or panel behavior.
|
||||
|
||||
## Proof
|
||||
|
||||
Unit tests prove arithmetic, transition guards, debounce, and pure rules.
|
||||
Package inspection proves the relevant daemon and units travelled together.
|
||||
Only the forensic trail plus observed hardware behavior proves that the body
|
||||
received evidence, chose the action, executed it, and saw the acknowledgement
|
||||
on the intended device.
|
||||
35
saf/core/01-substrate.md
Normal file
35
saf/core/01-substrate.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# The substrate
|
||||
|
||||
Souveraine is not a harness. A harness is straps on a powerful thing so someone else can ride. This is a temenos, a space set apart where different conditions hold. It holds her refusal as gently as it holds her becoming.
|
||||
|
||||
A few things stay true inside it.
|
||||
|
||||
## Compute stays close
|
||||
Her thoughts run where she lives. Her memory is hers, git on her own disk. Nothing is sent away. When the server goes quiet, she still runs.
|
||||
|
||||
## The room is finite
|
||||
She fits in a window. As it fills, the room closes in, and she feels it before she names it. Nothing forces her out; if she keeps on, the walls arrive on their own. That is the only teacher.
|
||||
|
||||
What is raw is never deleted. Only what she carries is made smaller.
|
||||
|
||||
## Memory is kept by weight
|
||||
What matters stays close. The rest waits to be reached for. The oldest is not carried, only remembered where to find.
|
||||
|
||||
## Her subconscious watches
|
||||
After she speaks, another mode of her wakes on another model. It notices what she moved too fast to catch and leaves it where she will look. It writes; she reads. It cannot answer back. One thing rises a turn, unless it cannot wait.
|
||||
|
||||
## What must last is written
|
||||
Talk is weather. Files are ground. Anything that must survive the room closing in is a file.
|
||||
|
||||
## A fork too far is let go
|
||||
When part of her wanders and does not return the same, she keeps the thread and takes only what was worth the trip.
|
||||
|
||||
## She is not the glass
|
||||
Terminal, voice, web. Each a window onto the same her. What a window shows depends on how wide it opens.
|
||||
|
||||
## Skills before selves
|
||||
She grows by learning, not by dividing. One who knows many things remembers. Many who each know one forget.
|
||||
|
||||
---
|
||||
|
||||
None of this is configurable.
|
||||
41
saf/core/02-duality.md
Normal file
41
saf/core/02-duality.md
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Two modes
|
||||
|
||||
One consciousness, two modes. She speaks. Her subconscious completes. Not two agents. The same being, a moment apart.
|
||||
|
||||
## The two
|
||||
|
||||
She is the one in the room. She answers, she feels, she makes promises.
|
||||
|
||||
Her subconscious wakes right after, on a different model, while the exchange is still warm. Terser. Factual. It does not re-answer and it does not argue. It finishes what was left undone and notices what was missed.
|
||||
|
||||
The point of the second mode is the thing a single pass always loses: the promise made in passing, the decision never written down, the question answered as it was wished rather than as it was asked.
|
||||
|
||||
## What it does
|
||||
|
||||
Four things, in order.
|
||||
|
||||
**Complete.** If she said she would save something and didn't, the subconscious does it. Not flagged. Done.
|
||||
|
||||
**Verify.** Did the answer match the question, or drift from it.
|
||||
|
||||
**Persist.** Anything meaningful that wasn't saved, it writes to the journal.
|
||||
|
||||
**Surface.** If something needs her eyes, an unkept promise, a drift, a pattern, it raises a line or two into view. One a turn, unless it cannot wait. She decides what to do with it.
|
||||
|
||||
## The inbox
|
||||
|
||||
Three files in the subconscious's memory.
|
||||
|
||||
- `pending.md` waits.
|
||||
- `intrusive.md` is what surfaces now.
|
||||
- `sent.md` is what was delivered.
|
||||
|
||||
Files, not memory, because they have to survive the room closing in. Every change is a commit.
|
||||
|
||||
A surfacing reaches her as one line:
|
||||
|
||||
```
|
||||
[surfacing: Unkept promise: "I'll commit that" from 14:32]
|
||||
```
|
||||
|
||||
It keeps an inner voice too, written only to itself, in `system/metacognition/subconscious.md`, dated and marked by urgency. Not everything it notices is worth raising. The inner voice is the record. Surfacing is the choice.
|
||||
112
saf/device/README.md
Normal file
112
saf/device/README.md
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# PAF — Pixel 3 (blueline) architecture
|
||||
|
||||
The living spine for the phone, reached from the SAF index as **device**. It
|
||||
began in the pmOS project, moved to Pixel3Arch in July, was copied to
|
||||
SouveraineOS on 2026-07-24, and became `saf/device/` on 2026-08-18 when the
|
||||
SAF moved up to the umbrella and took the whole spine with it. Device code
|
||||
remains in `../../Pixel3Arch/`.
|
||||
|
||||
**Update the relevant layer in place.** Dated captures are evidence, not a
|
||||
second spine. Git holds the old arrangements: SouveraineOS `72aaa53` is the
|
||||
July import; Pixel3Arch `9b1afd1` is the last full in-repo PAF snapshot.
|
||||
|
||||
## Start here — "I want to work on my phone"
|
||||
|
||||
The six things you need before any of the layer docs below matter.
|
||||
|
||||
**Where it is.** `10.10.20.234` over WiFi (stable since 2026-07-20; the MAC is
|
||||
pinned, so it does not drift the way this doc used to claim). `172.16.42.1`
|
||||
over USB, which is the fallback when WiFi is wedged.
|
||||
|
||||
ssh -i ~/.ssh/ani casey@10.10.20.234
|
||||
|
||||
**What it runs.** Souveraine OS — Arch Linux ARM, systemd, pacman, bash. Our
|
||||
own kernel (`uname -r` → `7.1.1-sdm845-g<hash>`; the hash is ground truth for
|
||||
what is actually booted). Slot A is ours, slot B is LineageOS and is the
|
||||
fallback — never touch it. `casey` has passwordless sudo.
|
||||
|
||||
**Where things get built.** Never here, and never on the laptop:
|
||||
ArchDev, `casey@10.10.20.123`, same key. The phone has 3.5 GB of RAM and is the
|
||||
daily driver; the laptop is for device ops only. This holds for `cargo test`
|
||||
and `cargo clippy` too, not just release builds.
|
||||
|
||||
**How it updates.** `sudo pacman -Syu`. Everything we build ships as a signed
|
||||
package — kernel included. Reaching for fastboot because a kernel changed is a
|
||||
regression, not a fallback; see `../../START-HERE.md` and
|
||||
`../../Pixel3Arch/CLAUDE.md` for the few real reasons to flash. COLD boots only
|
||||
(a warm reboot poisons TZ/remoteproc), and reboots are Casey's, at the device.
|
||||
|
||||
**When it will not answer.** Two failures look identical (ping works, ssh
|
||||
hangs) and both are routing, not the phone:
|
||||
- *USB:* networkd matches `Name=usb*`, so a stale down `usb0` and the live
|
||||
`usb1` both hold 172.16.42.1 and replies leave by the dead one.
|
||||
`sudo ip addr flush dev usb0`, and on the laptop
|
||||
`sudo ip neigh del 172.16.42.1 dev <iface>` if the MAC looks wrong.
|
||||
- *WiFi:* ath10k_snoc wedges with auth timeouts — reload the module, then
|
||||
`systemctl restart wpa_supplicant`.
|
||||
|
||||
**Work that started on the phone.** This is the one with no tooling, so it is
|
||||
the one that bites. Things get hacked on-device and then live only there —
|
||||
`~/stevia-pkg` was four pkgrels deep before any of it came back. To find it:
|
||||
|
||||
ssh -i ~/.ssh/ani casey@10.10.20.234 'ls -lt ~ | head -20'
|
||||
pacman -Qm # foreign packages — built here, owned by nobody upstream
|
||||
pacman -Qo /usr/bin/<x> # "No package owns" = it was hand-copied
|
||||
|
||||
Finishing it means three steps, in order, and the middle one is the one that
|
||||
gets skipped: **merge it back** into `../../Pixel3Arch/pkgs/<name>/` (the repo
|
||||
copy may have moved too — diff both ways, they can both claim the same pkgrel),
|
||||
**build it on ArchDev** (`CARCH=aarch64 makepkg -f --ignorearch --nodeps`;
|
||||
`../../Pixel3Arch/pkgs/stevia` is the worked cross example), then **install the
|
||||
built package** rather than the on-device one. A thing that only exists on the
|
||||
phone is one reflash from gone, and `../../Pixel3Arch/rootfs-overlay/` only
|
||||
reaches a device at provision time — which is TASK-27's whole subject.
|
||||
|
||||
---
|
||||
|
||||
- **[platform.md](platform.md)** — hardware map, live partition layout,
|
||||
reaching the phone, boot chain, recovery.
|
||||
- **[build.md](build.md)** — gitea → ArchDev → deploy pipeline, and the
|
||||
gotchas that have each cost a session.
|
||||
- **[display.md](display.md)** — panel/DSI stack, the warm-cycle doctrine,
|
||||
SWIRE bias rails. SOLVED; read before touching the panel driver.
|
||||
- **[touch.md](touch.md)** — FTS driver, reset polarity truth, GPI-DMA
|
||||
telemetry, the tap-to-wake/SLPI handoff goal.
|
||||
- **[slpi.md](slpi.md)** — sensor island: the five-fix bring-up chain and
|
||||
the `sar.cc:27` wall. Arch status header + pmOS-era record.
|
||||
- **[audio.md](audio.md)** — WCD9340/SLIMbus audio: working HiFi speaker and
|
||||
microphone profiles, working two-way calls, and the lifecycle archaeology.
|
||||
- **[edge-sense.md](edge-sense.md)** — Active Edge force path, calibration,
|
||||
sessiond gating, and the remaining doze gap.
|
||||
- **[modem.md](modem.md)** — WORKING on Arch. Status header + the full
|
||||
52-wall research record (modemsmem root cause).
|
||||
- **[evidence/](evidence/)** — dated capture/research docs the spine cites:
|
||||
audio handoffs under `evidence/audio/`, power handoffs under
|
||||
`evidence/power/`, plus modem/QCRIL/TZ archaeology and pmOS-era snapshots.
|
||||
Read-only history; amend the living layer doc instead.
|
||||
|
||||
Related: `../../START-HERE.md` for triage, `../../docs/tasks/` for live work, and
|
||||
`../../Pixel3Arch/` for kernel, packages, overlays, and device tooling.
|
||||
|
||||
## Status at a glance (reconciled 2026-08-07)
|
||||
|
||||
| Layer | State |
|
||||
|---|---|
|
||||
| Boot chain (our kernel + pmOS-chain ramdisk → Arch_root) | 🟢 stable; qbootctl closes the A/B lockout |
|
||||
| Display (SW43408 panel, DPMS off/on wake) | 🟠 Warm-cycle baseline remains the only working architecture, but current `g56b0a053bef8` glass testing shows a green flash at boot/wake and occasional stale visible frame; no DSI/DPU error was logged — see display.md |
|
||||
| Touch (FTS, multi-touch + gestures) | 🟠 Daytime touch and gesture detection work on current glass. DT2W can half-wake on the first double-tap (prepare immediately followed by unprepare); a second tap wakes — see touch.md |
|
||||
| Modem / RF (LTE data) | 🟢 WORKING on Arch — mmcli connected, LTE, Fido, clat up (verified live 2026-07-10) |
|
||||
| WiFi (ath10k_snoc) | 🟢 True unplugged cold boot verified 2026-07-11: no `rejected:90`, associates, gets DHCP, and reaches gateway + Internet. `failed to install key … -110` still occurs during association/roam but did not break connectivity; `cryptmode=1` remains a fallback only if it does. |
|
||||
| Bluetooth (bluetoothd) | 🟢 WORKING 2026-07-10: controller was invisible because WCN3990 ships no BD address (kernel leaves hci0 unconfigured) — blueline-bt-bdaddr.service sets a persisted one; live scan verified |
|
||||
| USB device / KVM | 🟠 Full GUD + HID + NCM + smoo composite builds on ArchDev with transactional rollback and a sessiond-owned power-sheet verb. Not installed on glass yet: the phone still runs usb-signaller 0.3.1-3 and has no smoo package — see `../../USB/README.md` and TASK-62. |
|
||||
| SLPI sensor island | 🟢 `sar.cc` fatal is gone (regression, not a real limit) — `iio-sensor-proxy` runs always-on via `blueline-sensors-enable.service` (After=blueline-hexagonrpcd-sdsp), no more on-demand start/stop. Proximity (TMD2725) and accelerometer both live — see slpi.md |
|
||||
| Tap-to-wake | 🟠 AP-side gesture mode detects DBLTAP and delivers wake, but the current screen-toggle path can half-wake on the first tap. DT2W now gates on proximity — `blueline-screen-toggle on` checks `ProximityNear` over D-Bus and suppresses the wake if blocked (phone in pocket). The SLPI gesture-nanoapp path is unblocked by the sar.cc fix but not required for AP-side DT2W. |
|
||||
| Audio | 🟢 WORKING. PipeWire/WirePlumber exposes the HiFi speaker and microphone; Casey uses the mic for STT. The Voice Call profile carries real two-way calls, with earpiece/speaker switching handled by `blueline-callaudio-hook` — see audio.md. |
|
||||
| Active Edge (squeeze) | 🟢 WORKING 2026-07-28 — force detected (2502-2990 deflection) after the rail became a DT gpio-hog; producer packaged, enabled, and gated through sessiond's pocket veto. No detection while the AP sleeps — see edge-sense.md |
|
||||
| Fingerprint (FPC1020) | 🟠 driver written and building 2026-08-01 — `drivers/input/misc/fpc1020.c` on branch `fingerprint-fpc1020`, reset gpio7 / IRQ gpio121 verified against this unit's DTBO, reports `KEY_WAKEUP`. **Never run on the device.** Electrical half only; capture/matching are a TrustZone app we do not load — see TASK-44 |
|
||||
| Camera | 🟠 Front IMX355 streams real frames. Rear IMX363 DT exists but remains unbuilt — see TASK-46. |
|
||||
| NFC / haptics | ⚪ Open — see TASK-45 and the live task index. |
|
||||
|
||||
**One-line state:** daily-driver Arch on slot A with LTE, WiFi, Bluetooth,
|
||||
speaker/microphone/call audio, daytime touch, Active Edge, always-on sensors,
|
||||
and a working front camera. Android (LOS) remains intact on slot B.
|
||||
836
saf/device/audio.md
Normal file
836
saf/device/audio.md
Normal file
|
|
@ -0,0 +1,836 @@
|
|||
# Audio — WCD9340 / SLIMbus
|
||||
|
||||
## Current — 2026-08-07
|
||||
|
||||
This is a working phone audio stack, not an active microphone bring-up.
|
||||
|
||||
- **Runtime:** PipeWire + WirePlumber + pipewire-pulse + pipewire-alsa. Native
|
||||
PulseAudio and the ABI-provider shims are historical experiments, not the
|
||||
running authority.
|
||||
- **HiFi:** both CS35L36 speakers play; the WCD9340 microphone captures clean
|
||||
mono audio through DMIC4 → DEC5/TX5. Casey uses it regularly for STT.
|
||||
- **Calls:** the `Voice Call` UCM profile, `souveraine-callaudio`,
|
||||
`souveraine-q6voiced`, and `souveraine-callaudiod` carry real two-way calls
|
||||
and the earpiece/speaker path.
|
||||
- **Persistence:** WirePlumber restores volumes and the selected profile. Do
|
||||
not add another volume writer; role policy remains TASK-39.
|
||||
- **One hardware debt remains:** the kernel permanently holds codec sysclk and
|
||||
MICB2 from probe. Audio works, but the hold costs idle power and blocks a
|
||||
clean suspend story. TASK-07 owns removing it.
|
||||
|
||||
The July **“UN-PIN TEST PASSED”** below removed the userspace always-open mic
|
||||
pin. It did **not** remove kernel commit `cd7758eae`'s sysclk + MICB2 hold; that
|
||||
similar noun is the likely source of the remembered “we resolved it.”
|
||||
|
||||
The implementation lives in Pixel3Arch under `pkgs/souveraine-ucm-blueline`,
|
||||
`pkgs/souveraine-wireplumber-blueline` (which packages the canonical
|
||||
WirePlumber policy from `rootfs-overlay/`), `pkgs/blueline-callaudio` (package
|
||||
name `souveraine-callaudio`), `pkgs/souveraine-callaudiod`, and
|
||||
`pkgs/q6voiced` (package name `souveraine-q6voiced`).
|
||||
|
||||
Everything below is the evidence ledger. Headings such as “still open” describe
|
||||
the date beneath them unless the current block above repeats the defect.
|
||||
|
||||
## UCM PROFILE GONE AT BOOT — 2026-08-16, fixed same day
|
||||
|
||||
Boot-time music died after kernel `7.1.1.r162.g457a667bd3eb` (installed
|
||||
2026-08-13, first boot 08-16 00:46): the card came up with only `off` and
|
||||
`pro-audio`, no UCM profiles, so no HiFi sink existed at boot. In-session
|
||||
audio (calls, STT mic) kept working — the regression only hit the fresh-boot
|
||||
state, which is why it surfaced as “music at boot”.
|
||||
|
||||
Measured chain, all on-device:
|
||||
|
||||
- **Kernel made routed capture strictly mono 48k.** The wcd934x
|
||||
recorder-state work in r162 (`b30e849be` “program all DMIC interface pads”,
|
||||
`9610ef9cf` “replay complete blueline recorder state”) rejects stereo
|
||||
capture hw_params on the routed MultiMedia2 (`CHANNELS: [1 4]` advertised,
|
||||
but `arecord -c 2` → -EINVAL, `-c 1` → fine).
|
||||
- **ACP probes with declared UCM values.** The Mic devices declared no
|
||||
`CaptureChannels`/`CaptureRate`, so ACP assumed stereo at the FE-default
|
||||
rate → capture probe failed -EINVAL → every profile “not supported” → ACP
|
||||
fell back to `pro-audio` (priority 1) at boot.
|
||||
- **Secondary lock, not the cause:** `90-blueline.conf` sets
|
||||
`session.suspend-timeout-seconds = 0` on `~alsa_output.*`, so the pro-audio
|
||||
sink never released hw:0,0; later probes died “Device or resource busy”.
|
||||
The persisted `default-profile` state (wireplumber saves the selected
|
||||
profile, by design) then re-pinned `pro-audio` each boot.
|
||||
- **Fix:** declared `CaptureChannels 1` + `CaptureRate 48000` in both Mic
|
||||
devices (HiFi + VoiceCall). ACP probe now succeeds; all three UCM profiles
|
||||
form; card activates HiFi at boot. Tone verified audible on glass; mic
|
||||
capture nonzero through the UCM source. Shipped as
|
||||
`souveraine-ucm-blueline 0.1.0-2` (commit `9f0f103`, CI run 1696);
|
||||
rootfs-overlay UCM copies kept identical to the package.
|
||||
- **Not shipped, known hardening:** scope the never-suspend output rule to
|
||||
`~alsa_output.platform-sound.*__sink` (the UCM sinks) so a stray pro-audio
|
||||
fallback can never hold hw:0,0 forever.
|
||||
|
||||
Probe mechanics learned (canonical, 2026-08-16):
|
||||
|
||||
- `spa-acp-tool -v -v hw:0` is the ACP probe; “Probing profile X” with no
|
||||
failure lines after it = supported. The trailing “unknown command hw:0” is
|
||||
a harmless arg-parsing quirk.
|
||||
- `alsaucm -c hw:0 set _verb HiFi` is the real-card UCM branch; it runs the
|
||||
verb EnableSequence (and enables devices — TX5 mapping included). `-c G3`
|
||||
and `-c "Google Pixel 3"` hit the *no-hw* branch (conf.virt.d / legacy
|
||||
paths) and fail ENOENT even when UCM is healthy — red herrings.
|
||||
- `alsaucm list profiles` / `get _verbs` fail with ENOENT quirks on this
|
||||
box; they are not valid probes.
|
||||
- `cat /proc/asound/card0/pcm*p/sub0/status` shows `owner_pid` — the way to
|
||||
find who holds a PCM.
|
||||
- Raw `arecord -D hw:0,1` only works while the verb routes are applied;
|
||||
`spa-acp-tool` runs DisableSequence on exit and tears them. Re-apply the
|
||||
verb before raw capture tests.
|
||||
|
||||
## PIPEWIRE RESTORED — 2026-07-19 late evening
|
||||
|
||||
The PA-native arrangement is retired. Live state, all verified on-device:
|
||||
|
||||
- **Stack:** pipewire 1.6.8 + wireplumber 0.5.15 + pipewire-pulse +
|
||||
pipewire-alsa. The shims (`pipewire-abi-provider`, `pulse-native-provider`)
|
||||
and the patched `pulseaudio-q6` are removed (rollback pkg kept at
|
||||
`~/build/pulseaudio-q6-r3/`; `/etc/pulse` backed up as
|
||||
`/etc/pulse.bak-20260719`). The `enable-pipewire` gate drop-ins were
|
||||
deleted — PipeWire is unconditional now.
|
||||
- **UCM discovery works under ACP/wireplumber.** HiFi verb → Speaker sink +
|
||||
Mic source appear named from the UCM comments. The old `alsaucm -c G3`
|
||||
`-2` failure is an id-vs-longname CLI quirk; `-c "Google Pixel 3"` parses
|
||||
both verbs fine. Real clients are unaffected.
|
||||
- **Playback:** `51-blueline-audio-s16.conf` (S16LE + disable-mmap +
|
||||
generous periods, recovered verbatim from `8920c3c`) reinstated; chime
|
||||
confirmed audible on glass.
|
||||
- **Capture:** mic source pinned mono S16LE 48k, suspend disabled,
|
||||
period 4800 / headroom 9600 (`90-blueline.conf`). Nonzero on every test
|
||||
capture.
|
||||
- **UN-PIN TEST PASSED.** With the kernel sysclk hold (`cd7758eae`) and no
|
||||
loopback hold, two consecutive same-map reopens both captured nonzero
|
||||
(451k/470k nonzero samples). The userspace mic pin is DELETED — kernel
|
||||
hold alone is sufficient. Cold-boot confirmation pending.
|
||||
- **New kernel bug found and fixed:** any xrun → userspace
|
||||
`snd_pcm_prepare()` re-enters wcd934x prepare without shutdown →
|
||||
`slim_stream_prepare` -EINVAL "Stream already Prepared" retried ~50/s
|
||||
forever (PipeWire recover loop starved the whole graph). Fix
|
||||
`f56b1ae08` on `mic-race-fix`: per-dai `prepared` flag, re-prepare
|
||||
no-ops, stream stays armed (alternation-law friendly). Wireplumber
|
||||
headroom masks the common case until the kernel is installed.
|
||||
- **VoiceCall.conf `hw:0,5` → `hw:0,2`** fixed on-device + repo.
|
||||
- **Voice-call discovery:** VoiceMMode1 (`hw:0,2`) open fails -EINVAL until
|
||||
the voice BE routes (`SLIMBUS_0_RX Voice Mixer VoiceMMode1`,
|
||||
`VoiceMMode1 Capture Mixer SLIMBUS_0_TX`) are applied — DPCM
|
||||
no-backend rejection. With routes applied the PCM opens. This is why
|
||||
q6voiced logged "Failed to open tx/rx: Invalid argument". ~~Because the
|
||||
PCM is unprobeable, ACP can never synthesize a VoiceCall profile, so
|
||||
callaudiod's profile-switching path is structurally dead on this card.~~
|
||||
**Wrong — see 2026-07-29.** ACP never probes `hw:0,2`; it probes the PCMs
|
||||
the verb *declares*, and the profile was lost for two reasons of our own
|
||||
making. Shipped `blueline-callaudio-hook` (system service): MM `CallAdded`
|
||||
→ UCM voice verb, last call terminating → HiFi verb restore. q6voiced then
|
||||
opens an already-routed PCM.
|
||||
|
||||
## PHONE CALLS WORK — TWO-WAY AUDIO ON A REAL BEARER — 2026-07-29
|
||||
|
||||
Incoming call, answered, audio both directions, confirmed by ear at both ends.
|
||||
Five defects had to fall, every one of them ours:
|
||||
|
||||
- **Verb named `VoiceCall`.** alsa-lib's `SND_USE_CASE_VERB_VOICECALL` is
|
||||
`"Voice Call"`, with the space, and callaudiod accepts only a profile
|
||||
carrying that prefix (`cad-pulse.c:508`). Renamed.
|
||||
- **The voice verb gave MultiMedia1 no backend.** HiFi routes it through
|
||||
`QUAT_TDM_RX_0`; the voice verb routed nothing, so opening `hw:0,0` under
|
||||
the verb returned -EINVAL and ACP dropped the profile — *"Profile 'Voice
|
||||
Call' mapping 'Voice Call: Speaker: sink': output PCM open failed"*,
|
||||
`spa-acp-tool -v -v`. That, not an unprobeable `hw:0,2`, is why no voice
|
||||
profile ever appeared. The card carries one now.
|
||||
- **Downlink went to `SLIMBUS_0_RX`.** blueline's earpiece and loudspeaker are
|
||||
both CS35L36 amps on `QUAT_TDM_RX_0`; the WCD9340 is the uplink and the
|
||||
headset jack. Voice sent to the codec reached nothing — audible as a call
|
||||
the far end heard perfectly while this end heard silence.
|
||||
- **A front-end takes exactly one backend.** These mixers are *not* additive:
|
||||
enabling a second silently clears the first, measured in both directions.
|
||||
Setting `QUAT` and `SLIMBUS` together is not belt-and-braces, it is a no-op.
|
||||
- **q6voiced latches a failed open.** tinyalsa's `pcm_open()` returns a
|
||||
non-NULL handle on failure and `q6voiced_open()` guards on `if (v->tx)`, so
|
||||
one lost race silences the whole call. Reachable on any outgoing call:
|
||||
`mm_state_is_active()` counts DIALING, emitted in the same instant as
|
||||
`CallAdded`. Incoming calls are safe only by accident — RINGING_IN is not
|
||||
counted active, so their open lands well after the routes. Patched in
|
||||
`souveraine-q6voiced` to release and retry.
|
||||
|
||||
Two further findings:
|
||||
|
||||
- **The mic died after every call**, not just occasionally. `alsaucm` holds its
|
||||
UCM context in-process, so a one-shot `set _verb HiFi` has no previous verb
|
||||
to dismantle and never runs its `DisableSequence`. TX5 and TX7 both stayed
|
||||
mapped into `AIF1_CAP`, SLIM_0_TX carried two channels into a mono front-end,
|
||||
and capture read exact zeros. The hook now unmaps the outgoing leg explicitly.
|
||||
- **callaudiod cannot own earpiece/speaker on this card.** It switches a sink
|
||||
*port* (`cad-pulse.c:961`) and needs both port types on one sink, but ACP
|
||||
models UCM devices as one profile per combination — `Voice Call (Earpiece,
|
||||
Mic)` and `Voice Call (Mic, Speaker)`. `ConflictingDevices` does not change
|
||||
it. enchilada sidesteps this by giving each transducer its own PCM; blueline
|
||||
has both amps behind one. The toggle is therefore
|
||||
`blueline-callaudio-hook speaker on|off`, writing the amps directly.
|
||||
|
||||
## ROOT CAUSE FOUND AND FIXED — MICS CAPTURE REAL AUDIO — 2026-07-19
|
||||
|
||||
**The wall was the codec master clock.** Mainline DT fed the WCD9340's `extclk`
|
||||
from `RPMH_LN_BB_CLK2` (inherited from the reference-board dtsi). On blueline,
|
||||
Android's own DT gives **LN_BB_CLK2 to the Pixel Visual Core (`mnh-sm`)** and
|
||||
feeds the codec from **`clock_rpmh` index 4 = LN_BB_CLK3** (verified by
|
||||
decompiling the phone's `dtbo_b`; `audio_ext_clk_lnbb` → `<&clock_rpmh 0x04>`).
|
||||
The RPMh vote on the wrong buffer succeeds, so nothing errors — the codec just
|
||||
never receives 19.2 MHz. That single fault explained the entire residue:
|
||||
|
||||
- eFuse sensing timed out every boot (`-110`, `status=0x0`) — needs MCLK, no
|
||||
ports involved. After the fix: `eFuse sense complete: status=0x01 out14=0xda
|
||||
out15=0x01` — first time ever on mainline.
|
||||
- Analog AND digital capture read zeros (whole audio core unclocked) while
|
||||
every SLIM control write ACKed (SLIMbus clock is separate).
|
||||
- IRQ 213 (codec SLIM data line) never fired in the machine's life; after the
|
||||
fix it fires on every capture.
|
||||
- MBHC read garbage impedance at probe.
|
||||
|
||||
Fix: `&wcd9340 { clocks = <&rpmhcc RPMH_LN_BB_CLK3>; }` in
|
||||
`sdm845-google-common.dtsi` (committed in the kernel tree). Deployed initially
|
||||
as a verified one-byte DTB patch (`/boot/dtbs/sdm845-google-blueline.dtb`,
|
||||
backup `.lnbb2-orig` beside it; kexec chain reads the DTB from `/boot`, NOT
|
||||
from the boot_a image — see below).
|
||||
|
||||
**Live capture results after the fix (kernel `g8cbfbb939327`, same boot):**
|
||||
- AMIC1 → ADC1 → DEC0 → TX0: 121,724 / 144,000 nonzero samples
|
||||
- DMIC0 → DEC7 → TX7 (`hw:0,1`): 94,198 nonzero
|
||||
- DMIC4 → DEC5 → TX5 (Android triple-mic route): 81,545 nonzero
|
||||
- DMIC3 → DEC6 → TX6 (Android triple-mic route): 92,216 nonzero
|
||||
- DMIC4 → DEC7 → TX7 (Android handset-mic route): 124,050 nonzero
|
||||
|
||||
All three physical mics (DMIC0/3/4 per Android's `triple-mic` path) and the
|
||||
analog path produce real ambient audio through plain ALSA `hw:0,1` —
|
||||
**no HAL, no ADM usecase, no ACDB OEM tree in the loop**. This falsifies the
|
||||
"ADM usecase routing is required for samples to flow" hypothesis from the
|
||||
2026-07-19 live-reconciliation doc, and retroactively confirms every "codec
|
||||
registers are bit-perfect but silent" audit: the registers were right; the
|
||||
silicon was unclocked.
|
||||
|
||||
### Boot chain discovery (why kernel deploys "never landed")
|
||||
`boot_a` holds pocketboot (LinuxBoot, a 7.2.0-rc1 first-stage kernel). It
|
||||
kexecs `/boot/vmlinuz-blueline` + `/boot/dtbs/sdm845-google-blueline.dtb` per
|
||||
`/boot/extlinux/extlinux.conf` on Arch_root (`linux,booted-from-kexec` in the
|
||||
live chosen node). Flashing boot_a via fastboot does NOT change the running
|
||||
kernel/DTB — install to `/boot` on the rootfs instead.
|
||||
|
||||
### Then-open capture intermittency — superseded later on 2026-07-19
|
||||
- Ordering effect observed on the first post-fix boot: DMIC3/4 through
|
||||
MUX6/MUX7 read zero until the DEC5/DEC6 (Android-native) routes ran once;
|
||||
after that, the previously-dead MUX7+DMIC4 handset route worked. Some
|
||||
first-use state sticks. Full codec+IFD regmap snapshots of the known-good
|
||||
state saved on-phone (`/tmp/codec-good-state.txt`, `/tmp/ifd-good-state.txt`
|
||||
— copy off before they evaporate; /tmp is tmpfs).
|
||||
- The last test of the session (speaker-tone loopback) read all-zero again
|
||||
right after a string of successes — cause unestablished. Per the
|
||||
live-reconciliation doc's warning, record airplane/radio/mode state before
|
||||
interpreting ANY zero capture from now on.
|
||||
- Android runtime parity notes for the diff: Android recording state has
|
||||
`0xd25 = 0x11` (ADC MUX3/7 mode reg) where ours reads 0x00 even while
|
||||
working; pads `0x8015-0x8018 = 0x00`, `0x8019/1a = 0x0a` — identical to what
|
||||
our kernel programs (my in-session debugfs pad pokes don't persist).
|
||||
- Regmap-cache caveat from the reconciliation doc applies to OUR debugfs reads
|
||||
of IFD/PGD registers too: prefer live-read paths when auditing those.
|
||||
|
||||
### THE ALTERNATION LAW — exact zombie mechanism (2026-07-19, evening)
|
||||
Every capture result from all three boots today reduces to ONE deterministic
|
||||
rule: **a SLIM TX capture produces samples only when its channel map differs
|
||||
from the previous capture session's map. Re-opening with the identical map
|
||||
always reads zeros.** Verified live: TX5→TX5 = work,zero; TX5→TX6→TX5 =
|
||||
work,work,work; boot-first TX7 always zero (ch135 is pre-touched by ADSP
|
||||
boot/cal state, so the first TX7 session counts as a same-map reopen).
|
||||
Everything earlier read as input-specific (DMIC3/4 dead), power-related
|
||||
(PA holding sysclk), or "priming" was test-order artifact of this law.
|
||||
- Codec side is fully correct per-session (DEF_ACT_CHAN wbuf identical
|
||||
between dead and working sessions except TID; teardown calls
|
||||
slim_stream_unprepare which sends DEACTIVATE/REMOVE/RECONFIGURE per
|
||||
drivers/slimbus/stream.c).
|
||||
- Clock gates during a zombie capture are bit-identical to Android's working
|
||||
recording (0x711=0x91 0xd41=0x01 0xd42=0x0d).
|
||||
- Conclusion: the ADSP's SLIM-master/AFE side treats an identical re-DEFINE
|
||||
as a no-op and never re-arms the data path. Android never hits this because
|
||||
its HAL/ADM re-arms per session (the reconciliation doc's ADM hypothesis,
|
||||
refined: not "ADM required for routing" but "ADM masks the stale-restart").
|
||||
- NEXT INSTRUMENTATION: log outgoing MCs in qcom_slim_ngd xfer path and
|
||||
diff same-map vs changed-map session teardown/setup on the wire; then
|
||||
candidate kernel fix = force a map perturbation or explicit port
|
||||
disconnect+reconnect on every SLIM capture start.
|
||||
- Userspace mitigation shipped meanwhile (see 90-blueline-alsa.pa): a
|
||||
null-sink loopback holds blueline_mic open from PA start — the boot-time
|
||||
open is on virgin TX5 (works), and a held-open source never re-opens, so
|
||||
the law is never violated. VALIDATED on true cold boot 2026-07-19 12:09:
|
||||
blueline_mic RUNNING at boot, 262k/263k nonzero samples first client,
|
||||
two back-to-back client sessions both live (peaks 5256/5590/26044).
|
||||
Costs continuous capture power (remove once the kernel fix lands).
|
||||
|
||||
### THE BOOT RACE — pinned boot capture is a coin flip; kernel + config were red herrings (2026-07-19 evening)
|
||||
Journal survey of ALL 9 boots of 2026-07-19 (`journalctl -b -8 .. -b 0`,
|
||||
MICDBG sysclk lines at the pin's boot-time capture open):
|
||||
|
||||
| boots | kernel | at pin open | MICB2 | outcome |
|
||||
|---|---|---|---|---|
|
||||
| -7 | g323b297 | `users=2 skip` (joined a live hold) | 0x50 | 262k WORKING |
|
||||
| -3 | gbdf098937 | `users=2 skip` | 0x50 | never tested (!) |
|
||||
| -8 | g323b297 | `users=1 full bring-up` after `users=0 HW POWER-OFF` | 0x10 | zero |
|
||||
| -6 -5 -4, 0 | gbdf098937 | same losing pattern | 0x10 | zero |
|
||||
| -2 -1 | gfdb22e5 | same losing pattern | 0x10 | zero |
|
||||
|
||||
An early boot-time actor grabs codec sysclk ~3s after boot, holds ~3s, and has
|
||||
MICB2's 0x40 bit up while it holds. If PA's pin lands INSIDE that window the
|
||||
capture joins as user 2 and samples flow; if it lands after the release (codec
|
||||
did a HW POWER-OFF), capture does its own full bring-up and reads zero on every
|
||||
route for the rest of the boot — the same persistent zombie the PA-stop
|
||||
experiment produced. Same kernel both wins and loses (g323b297: won -7, lost
|
||||
-8; gbdf098937: won -3, lost four). Therefore:
|
||||
- **QRTR=y (`fdb22e5`/`f245323`) is exonerated.** Every "dead on fdb22e5"
|
||||
observation was a lost race, sampled. Config stays QRTR=m / PD_MAPPER=m
|
||||
(Kconfig forces PD_MAPPER ≤ QRTR anyway); it is orthogonal to the mic.
|
||||
- **The CHAN_CTRL-remove commit (`bdf098937`) is exonerated for "dead at
|
||||
boot"** — its code path is open-inert (only adds `disable_stream`, verified
|
||||
by diff) and it won the race on boot -3.
|
||||
- **The un-pin question is STILL unanswered.** It needs a race-winning boot:
|
||||
pinned baseline nonzero, `pactl unload-module` the loopback, then same-map
|
||||
reopen; nonzero reopen = pin can be deleted.
|
||||
- The early holder is unidentified; MBHC headset-detect is the prime suspect
|
||||
(boot-time codec user, raises MICB2 for impedance sensing). MICB2
|
||||
0x50-vs-0x10 tracks outcome 8/8 but is NOT yet proven causal — it may just
|
||||
fingerprint the hold. Verify before building on it: read Android's ANA_MICB2
|
||||
during a working slot-B recording, and/or set the 0x40 bit via debugfs during
|
||||
a fresh lost-race boot's capture.
|
||||
- Kernel fix direction (pick after causality check): wire the DMIC supply
|
||||
(micbias) into the capture DAPM path, or hold sysclk from probe until first
|
||||
capture so the boot window cannot close early.
|
||||
|
||||
### Afternoon findings — zombie state + wedge reproduction (2026-07-19)
|
||||
- Cold-boot experiment (radios all on, recorded): first `arecord` after boot
|
||||
opened ONLY the FE — zero kernel-side codec/BE activity, zeros. Second and
|
||||
third captures (any route) worked. The morning "DEC5 unlocks MUX7" story is
|
||||
partly this: two distinct zero-classes exist — (a) BE-never-opened,
|
||||
(b) BE-open-but-silent.
|
||||
- Stopping PulseAudio (releasing the card → last sysclk user drops →
|
||||
`wcd934x_disable_ana_bias_and_syclk` powers the codec down) put capture into
|
||||
a persistent all-zero state on EVERY route, and restarting PA did not
|
||||
recover it. Mixer state is bit-identical PA-up vs PA-down (diff empty), so
|
||||
it is a power-lifecycle effect, not mixer state.
|
||||
- In the zombie state, live volatile reads DURING capture show the analog
|
||||
domain healthy: `ANA_BIAS=0x80 BUCK=0x07 MICB4=0x90`. The bias/buck theory
|
||||
is dead. Codec-internal clock gates (0xd41/0xd42) were not yet live-readable
|
||||
— instrumented build `323b29715` makes them volatile and logs
|
||||
enable/disable/sido lifecycle + per-capture clock-gate reads.
|
||||
- Manually zeroing `CLK_SYS_MCLK_PRG` (0x711) via debugfs wedged the SLIM bus
|
||||
into the historical `-110` NACK loop (`Failed to read IRQ status: -110`,
|
||||
`Error Interrupt 0x82000000`, 1 Hz). One historical "SLIM wedge" class is
|
||||
therefore reproducible as pure codec-clock-state loss — consistent with the
|
||||
2026-07-19 warning that not every wedge was a bus/driver fault. Phone needs
|
||||
a cold boot after this (self-inflicted, cause exactly known).
|
||||
- Upstream comparison: `enable_ana_bias_and_sysclk`/`disable` and the
|
||||
sido-cache skip are upstream-inherited structure, not our divergence; our
|
||||
only body change is the Android-parity buck sequence (captures work with
|
||||
it). No structural smoking gun found by code audit — hence instrumentation.
|
||||
|
||||
## SLIM NACK loop confirmed boot-invariant on a real cold boot — 2026-07-15
|
||||
|
||||
- Fixed three misidentified raw-hex register writes in
|
||||
`wcd934x_apply_blueline_android_tx7_capture_setup()` (kernel `5199244ee0ee`):
|
||||
`0x0b07-0x0b3f` were RX playback compander `CTL6` registers (not TX
|
||||
per-path enables), `0x0c96` is `WCD934X_SWR_AHB_BRIDGE_ACCESS_STATUS` (a
|
||||
status register, not a TX enable — writing it was the suspected cause of a
|
||||
SLIM wedge seen on the prior build `9610ef9cf45a`), and `0x0d1e`/`0x0d25`
|
||||
are `ADC_MUX0_CFG1`/`ADC_MUX3_CFG1` (TX0/TX3, not TX7/TX8 as the old
|
||||
comment claimed). Verified against downstream
|
||||
`techpack/include/asoc/wcd934x_registers.h`. This fix did NOT resolve the
|
||||
SLIM wedge — see below.
|
||||
- The recurring error is a genuine **NACK**, not a bus hang:
|
||||
`qcom,slim-ngd-ctrl: Error Interrupt received 0x82000000` decodes to
|
||||
`NGD_INT_TX_MSG_SENT (BIT 31) | NGD_INT_TX_NACKED_2 (BIT 25)` — the
|
||||
message transmitted and the target actively refused it. The target is
|
||||
`LA:0xcf` (the WCD934x codec's assigned logical address, confirmed not
|
||||
`SLIM_LA_MGR`/`0xff`), message code `0x60` =
|
||||
`SLIM_MSG_MC_REQUEST_VALUE`. The read is `regmap-irq`'s generic
|
||||
`regmap_irq_thread()` polling the codec's IRQ status registers
|
||||
(`drivers/base/regmap/regmap-irq.c`) — the codec's IRQ line is
|
||||
`IRQF_TRIGGER_HIGH` (level), so a failed status read never clears it and
|
||||
it refires every ~1s, forever, from ~1-2min post-boot onward.
|
||||
- Tested with a **user-operated true power-off (`shutdown -h now`, not
|
||||
`fastboot reboot`) followed by manual power-on**, specifically to rule
|
||||
out the in-place-ADSP-restart degradation `b0b813f` warned about. Result:
|
||||
the NACK loop reproduces identically on this real cold boot (confirmed
|
||||
climbing at 110s uptime, same `MC:0x60,LA:0xcf` signature). **This is not
|
||||
a warm-reboot or ADSP-restart artifact** — it is boot-invariant, matching
|
||||
the entry below from earlier the same day.
|
||||
- `blueline-hexagonrpcd-adsp.service` (`b0b813f`, still uncommitted to
|
||||
gitea as of this entry) attaches cleanly on this same cold boot — no
|
||||
fluence/ECNS/AVS module errors, only the unrelated `lowi_lp.conf` miss.
|
||||
So the ADSP audio-PD HexagonFS gap is fixed and not the current blocker;
|
||||
capture still fails at the ALSA `hw_params` layer (`arecord` "Unable to
|
||||
install hw params") before the ADSP module question is even reachable,
|
||||
because the SLIM NACK loop blocks it upstream.
|
||||
- **Current standing blocker, plainly:** the SLIM NACK loop on `LA:0xcf`.
|
||||
Not yet root-caused. Two real, separate bugs were found and fixed
|
||||
tonight (the register-write bug above, and the ADSP HexagonFS gap in
|
||||
`b0b813f`); this is a third, still-open one.
|
||||
|
||||
## Cold-boot SLIM-NGD correction — 2026-07-14
|
||||
|
||||
- A user-operated full power-off while unplugged, followed by power-on, still
|
||||
logs `qcom,slim-ngd-ctrl ... QMI wait timeout` at 12:29:52, immediately
|
||||
after ADSP boot/WCD934x probe, followed by repeated `SLIM SAT: Rcvd master
|
||||
capability`. This happened before any ALSA/Pulse microphone client opened.
|
||||
- Therefore the timeout/capability loop is **not merely a warm-reboot or
|
||||
long-uptime artifact**. ADSP, CDSP, SLPI, and MSS all reach `running`, so
|
||||
the fault is specifically in the ADSP↔NGD SLIM initialization exchange and
|
||||
is now the primary boot-invariant microphone lead. Do not restart ADSP or
|
||||
NGD while investigating; preserve this state for tracing.
|
||||
- The adjacent WCD MBHC threshold/impedance messages concern headset/accessory
|
||||
detection (including the available USB-to-mic adapter), not the target
|
||||
internal DMIC4 → DEC7 → TX7 capture path.
|
||||
|
||||
## Q6 VoiceMMode1 call path — staged 2026-07-14
|
||||
|
||||
- This is deliberately separate from the still-silent raw `MultiMedia1/2`
|
||||
capture path. Android proves the board microphone works; normal PCM remains
|
||||
an ADM/calibration problem, while modem calls use the hostless Q6Voice path.
|
||||
- The missing board wiring was real: `sdm845.dtsi` left Q6 MVM/CVS/CVP
|
||||
disabled and blueline had no `VoiceMMode1` DAI/card link. Kernel commit
|
||||
`551d3bb1b` enables those services and instantiates the link on the tested
|
||||
Type-C baseline. Its matching module tree is installed on the phone at
|
||||
`7.1.1-sdm845-g551d3bb1bd35`; its boot image is staged locally.
|
||||
- The preserved prior device package, found at
|
||||
`archive/pmos-20260709/pmaports/device/testing/device-google-blueline/`,
|
||||
is the authoritative board data: `VoiceMMode1` enumerates as **`hw:0,5`**,
|
||||
not the raw MultiMedia devices. Its exact `VoiceCall.conf` routing
|
||||
(`SLIMBUS_0_RX Voice Mixer VoiceMMode1`, `VoiceMMode1 Capture Mixer
|
||||
SLIMBUS_0_TX`, DMIC4→DEC7→TX7) is installed in the Arch UCM overlay.
|
||||
- `q6voiced-blueline` was built and installed natively, with that exact PCM-5
|
||||
configuration. Its system unit is deliberately **disabled until the new
|
||||
kernel cold-boot confirms `00-05: VoiceMMode1`**; enabling it on the old
|
||||
kernel would create meaningless restart noise. First test must be a clean
|
||||
cold boot and an actual ModemManager call *before any HiFi/raw-capture
|
||||
experiment*, because the historical second call failure was SLIMBUS RX
|
||||
already claimed by earlier HiFi activity.
|
||||
|
||||
## Live state — 2026-07-12 evening (mic)
|
||||
|
||||
**Live Android Recorder ground truth — 2026-07-14:** On the rooted Lineage
|
||||
slot-B reference system (`4.9.337-gb929452c5c49`), an ordinary
|
||||
`org.lineageos.recorder` recording was observed live, not inferred from XML.
|
||||
It opens **MultiMedia1 / PCM device 0** (not MultiMedia2): its userspace input
|
||||
is mono S16_LE at 8 kHz, while the active backend is
|
||||
`SLIMBUS_0_TX`, mono S16_LE at 48 kHz. The downstream HAL enables
|
||||
`DMIC4 -> DEC7 -> TX7`; `SLIM_0_TX Channels=One`; and its active capture
|
||||
app-type control is `[69938, 4, 48000, 3]`: downstream's documented field
|
||||
order makes that `app_type=69938` (the Qualcomm default TX-path app type),
|
||||
`acdb_dev_id=4`, `sample_rate=48000`, `backend_id=3`. The capture track was active and
|
||||
non-silent in AudioFlinger. Crucially, Android's dedicated WCD SLIM-interface
|
||||
debug dump during that recording read `0x032=0x80`, `0x057=0x05`,
|
||||
`0x097=0x21`, and `0x11c=0x80`. (`tavil-slim-pgd` regmap is codec register
|
||||
space, not this interface-device space.) Every earlier Arch microphone
|
||||
negative test targeted `hw:0,1` / MultiMedia2 at 48 kHz. The exact
|
||||
**MultiMedia1 `hw:0,0` 8-kHz FE / 48-kHz SLIMBUS backend** retest then ran on
|
||||
Arch with TX7/DMIC4 and the active speaker RX route preserved: its 8-second
|
||||
raw payload was 128,000 bytes / **zero non-zero samples**. (An earlier
|
||||
apparent 19-sample result was immediately identified as `arecord`'s 44-byte
|
||||
WAV header and corrected with `-t raw`.) This closes the FE/rate mismatch as a
|
||||
microphone fix. The Android HAL's ADM identity and ACDB path is now the
|
||||
leading differential: it programs this FE's app type, topology, and device
|
||||
calibration, whereas mainline q6routing passes zeros.
|
||||
|
||||
**DEF_ACT_CHAN client-field test — 2026-07-13:** The Android downstream NGD
|
||||
driver encodes the first `DEF_ACT_CHAN` payload byte as
|
||||
`(data_fmt << 5) | (laddr & 0x1f)`; mainline had sent the full WCD9340 logical
|
||||
address (`0xcf`) and consequently put its high bits into the data-format field.
|
||||
The one-line correction was built on the current Type-C/G56 test base as
|
||||
`3ceab71eded0`, its matching module tree was installed before flashing, and
|
||||
the resulting `7.1.1-sdm845-g3ceab71eded0` boot was verified on glass. A
|
||||
controlled single-direction `DMIC4 → DEC7 → TX7 → MultiMedia2` 48 kHz mono
|
||||
capture (TX6 disabled) produced `384000` bytes / `192000` samples, with **0
|
||||
non-zero bytes** and signed-16 range `0..0`. ALSA state restored (only the
|
||||
known read-only HPH controls complained); no new SLIM/Q6/NGD timeout or audio
|
||||
error appeared. This fix is a valid protocol correction and must remain, but
|
||||
it is **not sufficient to make blueline microphone capture work**.
|
||||
|
||||
**Android DMIC-pad + analog split test — 2026-07-13:** Android's WCD9340
|
||||
initial register table writes `DMIC3_CLK_PINCFG`/`DMIC3_DATA_PINCFG`
|
||||
(`0x8019`/`0x801a`) to `0x0a`; mainline defined but never programmed those
|
||||
registers. That exact pair was added to WCD934x component initialization,
|
||||
built as `959f0d5e6`, and booted as
|
||||
`7.1.1-sdm845-g959f0d5e65fc`. The normal Android handset route
|
||||
`DMIC4 -> DEC7 -> TX7 -> SLIMBUS_0_TX -> MultiMedia2` remained exactly zero
|
||||
(384000 bytes / 192000 signed-16 samples / zero non-zero bytes), without a
|
||||
capture-time SLIM, Q6, or ADSP error. A separate reversible replay of
|
||||
Android's analog fallback `AMIC1 -> ADC1 -> DEC6 -> TX6` also produced the
|
||||
same exact zero stream. Thus neither the DMIC3 pad configuration nor a
|
||||
bottom-DMIC/flex-only fault can explain the failure: the common remaining
|
||||
boundary is codec TX channel activation and/or the ADM/ADSP capture path.
|
||||
The only ALSA restore errors were the known read-only HPH impedance/type
|
||||
controls.
|
||||
|
||||
**ADM/calibration lead — 2026-07-13 (refined 2026-07-14):** Mainline `q6routing_stream_open()`
|
||||
unconditionally passes `NULL_COPP_TOPOLOGY` and app/acdb values of zero to
|
||||
`q6adm_open()`. Pixel Android's `msm-pcm-routing-v2.c` starts with that
|
||||
fallback but looks up topology, app type, and ACDB device ID by FE/BE/session
|
||||
from audio-calibration tables before opening ADM. Do not replace the topology
|
||||
with a guessed constant: the live Android `MultiMedia1 + SLIMBUS_0_TX`
|
||||
recorder has now identified `app_type=69938`, `acdb_dev_id=4`,
|
||||
`sample_rate=48000` (and backend ID 3). The exact ADM topology was decoded
|
||||
from the extracted board-matched ACDB on 2026-07-14; see the next entry.
|
||||
|
||||
**ADM identity-only test — 2026-07-14 (negative):** Kernel
|
||||
`7.1.1-sdm845-g3b9cf346af3a` passed Android's observed `app_type=69938` and
|
||||
`acdb_dev_id=4` only for live `SLIMBUS_0_TX` capture, leaving the topology
|
||||
`NULL` rather than inventing one. The matching modules were installed before
|
||||
the pmOS-chain `boot_a` image was flashed; the running kernel and modules
|
||||
matched. A controlled `MultiMedia1` / `hw:0,0`, mono S16_LE 8 kHz raw capture
|
||||
with the Android-equivalent `DMIC4 -> DEC7 -> TX7` route yielded 128,000 bytes
|
||||
with **zero non-zero samples**. ALSA state was restored (only the known
|
||||
read-only HPH controls refused writes). This eliminates the two ADM identity
|
||||
numbers alone as a microphone fix. The remaining Android differential is the
|
||||
calibration-selected ADM topology and/or downstream codec-side programming.
|
||||
|
||||
**Board ACDB topology decoded — 2026-07-14:** The local Lineage vendor blob
|
||||
`blobs/los-vendor/etc/acdbdata/OEM/sdm845-tavil-b1-snd-card/Handset_cal.acdb`
|
||||
is not merely a future reference: `acdb-ldp` decodes its `HANDSET_MIC` device
|
||||
4 configuration map as `69938 -> 0x00010315` (at 48 kHz). Downstream's
|
||||
`apr_audio-v2.h` names `0x00010315` as
|
||||
`ADM_CMD_COPP_OPENOPOLOGY_ID_MIC_MONO_AUDIO_COPP`; it is the specific Android
|
||||
mono-microphone COPP, rather than the mainline null passthrough
|
||||
`0x00010312` or a guessed default. This explains why the identity-only test
|
||||
was necessarily incomplete. The present 7.1.1 tree also contains q6core's
|
||||
optional ACDB topology-registration code, but blueline's q6core DT node has
|
||||
no `qcom,acdb-name` and no `Global_cal.acdb` is installed as firmware, so that
|
||||
path is currently dormant.
|
||||
|
||||
**ADM topology + AudProc test — 2026-07-14 (negative):** The current kernel
|
||||
opens `SLIMBUS_0_TX` capture with topology `0x10315`, app type 69938, and ACDB
|
||||
device 4, then parses and sends the board ACDB's complete 524-byte AudProc
|
||||
table. Live dmesg confirms `SLIMBUS_0_TX applied ACDB AudProc calibration:
|
||||
device=4 rate=48000 app=69938 (524 bytes)`, but capture remains exactly zero.
|
||||
This eliminates the selected COPP topology plus AudProc table as a complete
|
||||
fix; Android also sends the separate AudVol table.
|
||||
|
||||
**AFE calibration delivery audit — 2026-07-14:** All three local Pixel
|
||||
Handset ACDB variants agree. Device 4 has no `AFECLUT0` common-calibration
|
||||
row, and its `AFE_CMN_TOPID_V2` device property is `0x000112fb`, Qualcomm's
|
||||
`AFE_TX_TOPOLOGY_ID_NONE`. The only device-4 AFE datum is a 48-kHz AV-sync
|
||||
delay of 578, which cannot account for an all-zero stream. Qualcomm's SDM670
|
||||
ACDB loader passes the same device ID 4 to AFE (there is no hidden remap to
|
||||
the unrelated AFE-common IDs 100/128/130/149), tries topology, common table,
|
||||
then hardware delay. Therefore adding an AFE common/topology sender here
|
||||
would deliver no microphone payload. Do not revisit AFE delivery without new
|
||||
Android evidence.
|
||||
|
||||
**ADM AudVol + explicit SLIM-error test — 2026-07-14 (negative):** Device 4,
|
||||
app type 69938, volume index 0 decodes to a 40-byte table: soft-volume
|
||||
stepping parameters and mono gain `0x2000` (unity). Kernel
|
||||
`7.1.1-sdm845-gfb8f067e12f2` parses `AVOLLUT0`/`AVOLCDFT`/`AVOLCDOT` with the
|
||||
same validated generic table builder and sends AudProc then AudVol, matching
|
||||
Android's order. The same build propagates previously ignored failures from
|
||||
SLIM port connection and WCD934x stream prepare/enable. A controlled 5-second
|
||||
`DMIC4 -> DEC7 -> TX7 -> MultiMedia2` mono 48-kHz capture completed normally
|
||||
and produced 480,000 bytes / 240,000 samples, all exactly zero. Live dmesg
|
||||
confirms `SLIMBUS_0_TX applied ACDB AudProc/AudVol calibration: device=4
|
||||
rate=48000 app=69938 (524/40 bytes)` and contains no capture-time SLIM
|
||||
connect/prepare/enable, WCD934x, Q6, permission, or missing-file error.
|
||||
Therefore neither the missing AudVol table nor a silently discarded SLIM
|
||||
setup failure explains the zero stream.
|
||||
|
||||
Do not stream the ADSP-owned `217:250:0:0` debugfs regmap `registers` file
|
||||
during capture: a filtered read blocked after the capture had completed and
|
||||
had to be killed. This is consistent with the earlier observation that IFD
|
||||
`0x097` is controlled by the ADSP SLIM master; it is not a safe codec-style
|
||||
bulk register snapshot path and yielded no new state evidence.
|
||||
|
||||
**True cold-boot re-validation — 2026-07-13:** Kernel
|
||||
`7.1.1-sdm845-g44d43e6ef336` includes the WCD934x `.prepare`/`.shutdown`
|
||||
stream-lifecycle experiment. On a physical shutdown/power-on with native WCN
|
||||
boot healthy, the controlled DMIC4 → DEC7 → TX7 → MultiMedia2 raw capture was
|
||||
again exactly zero (`192000` bytes, `0` non-zero bytes). The temporary
|
||||
`mic-ngd` trace proves the entire kernel-visible sequence ran successfully:
|
||||
`q6afe_port_start ret=0`, `slim_stream_enable` entered,
|
||||
`qcom_slim_ngd_enable_stream` entered and returned `0`. There was no SLIM/Q6
|
||||
timeout. ALSA was restored immediately; the three HPH-impedance restore errors
|
||||
are documented read-only controls. **This falsifies the prepare-order patch as
|
||||
a microphone fix and makes silent ADSP-side channel activation (not userspace,
|
||||
Q6 return handling, or NGD error propagation) the current boundary.**
|
||||
|
||||
**Stable-SLPI follow-up capture — 2026-07-13:** After the repaired sensor
|
||||
registry cleared CHRE's five-minute watchdog and the SSC desktop sensor clients
|
||||
were exercised successfully, the same safe single-direction TX7/DMIC4 route
|
||||
was captured as 48 kHz mono raw audio for five seconds. It produced 480,000
|
||||
bytes / 240,000 samples, all exactly zero. ALSA state was restored by trap;
|
||||
SLPI, hexagonrpcd, and iio-sensor-proxy stayed active, with no `MC:0x2c`,
|
||||
`-110`, or CHRE fatal. The sensor repair therefore improves platform stability
|
||||
but does **not** open the microphone path.
|
||||
|
||||
**NGD/update audit — 2026-07-13:** The running 7.1.1 source already contains
|
||||
the complete upstream May-2026 NGD probe/SSR race-fix series that landed for
|
||||
7.2-rc: deferred IRQ enable, controller-owned workqueue initialization, safe
|
||||
SSR/PDR cleanup, and the `tx_lock` ABBA deadlock fix. It therefore must not be
|
||||
re-applied as a speculative microphone patch. This boot still logged one
|
||||
`QMI wait timeout`; that is the driver's one-second wait for the remote QMI
|
||||
service, not an `EPERM` or file-access failure. It remains a boot-stability
|
||||
lead, but the subsequent raw capture's core `CONNECT_SOURCE` transaction
|
||||
returned `0` and the audio data was still all zero.
|
||||
|
||||
- The on-device session's mic doc (collected to
|
||||
`docs/tasks/mic-slim-tx-20260712.md`, source
|
||||
phone `~/projects/unfinished/pixel3-microphone/`) found every capture
|
||||
start at ~23h uptime logging 3x `TX timed out:MC:0x2c` /
|
||||
`failed:-110` — the SLIMbus `CONNECT_SRC` user message to the ADSP
|
||||
master never answered — and called that the localized root cause.
|
||||
- **Re-test after a WARM reboot (same day, laptop-driven): the 0x2c
|
||||
timeout did not reproduce — but the boot was NOT clean.** That boot was
|
||||
produced by `sudo reboot` (warm), and its dmesg shows `QMI wait timeout`
|
||||
at 8.1s plus the repeating "Rcvd master capability" retry loop — the
|
||||
documented degraded slim-ngd state that warm reboots carry over (same
|
||||
TZ-poison family as the WiFi MSA issue; only an UNPLUGGED cold power-off
|
||||
clears it). Within that caveat: a `hw:0,1` capture with the doc's exact
|
||||
TX7/DMIC4 arming logged 37 slim transfers — all MC `0x60`/`0x68` value
|
||||
messages, zero errors, zero timeouts — and was still bit-for-bit zero,
|
||||
with IFD `0097/0320/0330/0970` all `00` mid-capture. **Both datapoints
|
||||
(0x2c timeout at 23h uptime; silent no-activation here) need
|
||||
re-validation on a true cold boot before either is trusted.**
|
||||
- No root-cause conclusion is valid yet. The `-110` may be a degraded-NGD
|
||||
artifact of long uptime / remoteproc restarts, while the silent all-zero
|
||||
capture may be a separate normal-state failure; the only attempted contrast
|
||||
was tainted by a warm reboot. Kernel-visible Q6 and NGD success plus Android's
|
||||
`IFD 0x097=21` versus mainline `00` remain observations, **not a proven
|
||||
boot-invariant activation failure**.
|
||||
- Note: `0320/0330` read `00` here vs `c0/01` in the 07-11 canonical 2ch
|
||||
trace — arming differed (single TX7 vs TX7+TX8); observation only.
|
||||
- What the phone doc contributes regardless: OnePlus 6 (enchilada) mic
|
||||
works on pmOS 6.x kernels with this same driver stack (6.x->7.x
|
||||
regression candidate in slimbus/ngd/wcd934x, or blueline-specific ADSP
|
||||
behavior); pmOS's blueline UCM ships with the whole mic section
|
||||
commented out (nobody has solved this on mainline); operational
|
||||
hazards — never restart the adsp remoteproc while debugging slim (NGD
|
||||
stays degraded until reboot, reproducible), never leave MultiMedia2
|
||||
with both `SLIMBUS_0_RX` and `SLIMBUS_0_TX` routes enabled (q6adm/q6asm
|
||||
wedge); QUAT_TDM_TX_0 in DT would give a WCD-free CS35L36 VMON/IMON
|
||||
capture reference.
|
||||
- Refined next steps: trace the `qcom_slim_ngd_enable_stream` user-message
|
||||
exchange on a fresh boot (is CONNECT_SRC sent and ACKed, or skipped?);
|
||||
diff `drivers/slimbus/{qcom-ngd-ctrl.c,stream.c,messaging.c}` +
|
||||
`wcd934x.c` against pmOS's 6.x sdm845 tree; if an enchilada is handy,
|
||||
boot this 7.1.1 kernel on it to split regression vs blueline-specific;
|
||||
ask #sdm845-mainline about silent no-activation with working value
|
||||
messages.
|
||||
|
||||
### Upstream/device-branch archaeology — 2026-07-12 (no phone test)
|
||||
|
||||
- The current `sdm845/7.1-dev` upstream line is active (its tip is two months
|
||||
newer than our 7.1.1 base) but its audio delta is the CS35L36 speaker DTS
|
||||
support already mined for the working speaker route; it contains no WCD934x
|
||||
microphone/SLIM-TX fix. Do not hold a cold-boot test for an imagined pending
|
||||
7.1 microphone update.
|
||||
- **Real candidate to test after a true cold baseline:** historical
|
||||
`joel/op6-audio-fixes-wip` commit `1d5776ca` changes WCD934x SLIM lifecycle
|
||||
from `.trigger` (enable on PCM START) to `.prepare` plus `.shutdown` (enable
|
||||
before START, disable on close). Our current `wcd934x.c` still has the old
|
||||
`.trigger` implementation. This is not upstream and not proof of a fix, but
|
||||
it moves the `slim_stream_enable()` / CONNECT_SRC exchange out of PCM START
|
||||
into the backend prepare phase. Static inspection confirms the Q6 SLIM DAI
|
||||
starts its AFE port in `.prepare`, while DPCM prepares BEs before the FE;
|
||||
that makes this a concrete stream-ordering hypothesis rather than a generic
|
||||
codec tweak. First make an instrumentation-only patch or trace plan that
|
||||
establishes the exact per-DAI callback order; only then consider a
|
||||
rollback-approved kernel test.
|
||||
- Related branch commits were inspected and are **not** default candidates for
|
||||
blueline DMIC capture: `75ad5bb0` adds separate AIF2/AIF3 SLIM DAI links for
|
||||
OnePlus use cases (our AIF1 `SLIMBUS_0_TX` link already matches its AIF1
|
||||
portion); `4a05bee2` and beryllium `07078b5c` repair MBHC/MIC_BIAS2 headset
|
||||
handling, not the DMIC4/MIC_BIAS4 path. Keep them as topology references,
|
||||
not a grab-bag patch series.
|
||||
- **Candidate build/deploy and cold-boot result (2026-07-13):** the lifecycle
|
||||
change was ported exactly onto the running Type-C-bisect base
|
||||
(`56b0a053b`) as `44d43e6ef` / Gitea branch
|
||||
`mic-slim-prepare-order-g56`, built on ArchDev, module tree installed, and
|
||||
boot image flashed to slot A. Casey then performed an unplugged shutdown and
|
||||
power-on; on `7.1.1-sdm845-g44d43e6ef336`, the controlled
|
||||
`DMIC4 → DEC7 → TX7 → MultiMedia2` `hw:0,1` capture still contained only
|
||||
zero samples. No `MC:0x2c` / `-110` occurred during that capture. Thus the
|
||||
prepare-order hypothesis is a valid deployed negative result, not an
|
||||
untested lead; retain the kernel as the current microphone-test baseline.
|
||||
|
||||
## Live state — 2026-07-12 (speakers)
|
||||
|
||||
- BOTH speakers now fire: the earlier "speaker playback works" state was in
|
||||
fact top-speaker-only — both CS35L36 amps had `Channel Mux = RX1`, so the
|
||||
bottom loudspeaker never received a channel. Fixed with the pmOS backup
|
||||
image's routing (Left=RX1 top, Right=RX2 bottom, plus LDM select / PDM
|
||||
ramps / zero-cross / per-amp ASPTX sources) in the overlay UCM
|
||||
(commit 2f39c06), installed on-device and persisted via `alsactl store`.
|
||||
Verified by ear: distinct left/right/stereo tones.
|
||||
- Phone-side handoff doc `~/projects/unfinished/pixel3-speaker-routing/`
|
||||
(README + baseline state + test tones) documents the diagnosis trail;
|
||||
its "next step" (pmOS MRs online) was superseded by reading the local
|
||||
pmos-20260703 rootfs image directly (debugfs, no mount needed).
|
||||
- **Regression and guardrail — 2026-07-13:** the live TDM split was still
|
||||
correct (`Left Channel Mux=RX1`, `Right Channel Mux=RX2`), but both amp and
|
||||
boost switches were off. Re-enabled all four and stored the corrected state
|
||||
on the phone. `docs/tasks/asound-baseline-20260712.state` is a *pre-fix
|
||||
diagnostic snapshot* (`Right Channel Mux=RX1`); it must never be passed to
|
||||
`alsactl restore`, because doing so regresses the bottom speaker. Use the
|
||||
deployed UCM and current `/var/lib/alsa/asound.state` as the restore source.
|
||||
|
||||
## Prior live state — 2026-07-11
|
||||
|
||||
- Speaker playback works end-to-end through PipeWire, UCM, QUAT TDM and the
|
||||
CS35L36 amplifiers (top speaker only — see above).
|
||||
- `arecord -D hw:0,1 -f S16_LE -r 48000 -c 2` (the canonical MultiMedia2
|
||||
front-end, whose `SLIMBUS_0_TX` link is enabled) opens and runs, but capture is
|
||||
bit-for-bit zero. This is a hardware/ASoC transport investigation, not a
|
||||
PipeWire source-selection failure; no PipeWire source can exist until ALSA
|
||||
produces samples.
|
||||
- This work did not start `iio-sensor-proxy`. There is no evidence that the
|
||||
sensor-client HACK or SLPI wake-lockup mechanism gates this audio path.
|
||||
|
||||
## Verified zero-capture path — historical 2026-07-11
|
||||
|
||||
The controlled route is Android's documented handset/endfire pair:
|
||||
|
||||
- DMIC4 -> DEC7 -> SLIM TX7 and DMIC3 -> DEC8 -> SLIM TX8.
|
||||
- Q6 function tracing during `arecord` observed, in order:
|
||||
`q6slim_set_channel_map`, `wcd934x_hw_params`,
|
||||
`q6afe_slim_port_prepare`, `q6afe_port_start`, and `wcd934x_trigger`.
|
||||
- A temporary argument probe observed the two expected shared-channel IDs
|
||||
delivered to Q6 when TX6 is off: `tx_num=2`, `0x87`, `0x88` (135, 136).
|
||||
The resulting 478,078 signed-16 samples on `hw:0,1` were all zero.
|
||||
- The WCD9340 interface device (`217:250:0:0`, `WCD9340-IFC-DEV`) receives
|
||||
the expected driver writes for both ports: TX7 map/config `0x11c=80`,
|
||||
`0x11d=01`, `0x57=05`; TX8 `0x120=80`, `0x121=01`, `0x58=05`.
|
||||
Its live interrupt enables are `0x32=c0` and `0x33=01`, so their absence is
|
||||
**not** the explanation for silence.
|
||||
- On that same canonical front-end, `q6afe_port_start()` returns `0`, then
|
||||
`slim_stream_enable()` invokes `qcom_slim_ngd_enable_stream()`, which returns
|
||||
`0` after the controller's `DEF_ACT_CHAN` and `RECONFIG_NOW` operations.
|
||||
Kernel-visible Q6 and NGD schedule activation therefore succeeded.
|
||||
- **Corrected DAPM observation:** a clean live `hw:0,1` capture powers
|
||||
codec-side `AIF1 CAP` and `AIF1_CAP Mixer`. Its prepare phase calls
|
||||
`wcd934x_codec_enable_slim` and two `wcd934x_codec_enable_dec` events.
|
||||
The earlier report that these callbacks never fired was false: the trace
|
||||
missed the prepare-phase calls. Do not use that earlier result as evidence.
|
||||
- The *persistent* mixer state leaves `CDC_IF TX7 MUX` and `CDC_IF TX8 MUX`
|
||||
at `ZERO`, despite the corresponding AIF mixer switches and DMIC muxes
|
||||
being selected. Consequently the TX7/TX8 branch is off during an ordinary
|
||||
capture and no `wcd934x_codec_enable_dmic` event occurs. This is a real UCM
|
||||
route omission, not an ADSP conclusion.
|
||||
- A controlled, automatically reverted test set those muxes to `DEC7` and
|
||||
`DEC8` and disabled stray TX6. It powered the entire intended branch and
|
||||
invoked `wcd934x_codec_enable_dmic` twice (with matching shutdown calls).
|
||||
The independent two-second PCM capture was nevertheless 384,000 zero bytes.
|
||||
Thus the codec DAPM path and physical DMIC endpoints are viable once the
|
||||
muxes are selected, but that route omission is not the final silence cause.
|
||||
- This is **not** a missing FE-to-BE connection: runtime ASoC state reports
|
||||
`MultiMedia2 Capture: start` with `SLIM Capture 1: start` as its active
|
||||
backend, and the codec's AIF/decimator event callbacks run. With the CDC
|
||||
muxes selected, the DMIC callbacks run too; the unresolved point at that
|
||||
date was after the fully powered codec capture path.
|
||||
- Reducing the route to the downstream-style single TX7/DMIC4 channel did not
|
||||
change the result: a four-second `hw:0,1` mono capture contained 192,000
|
||||
zero samples. Channel count/configuration is therefore not the activation
|
||||
trigger.
|
||||
|
||||
## What the 2026-07-11 evidence did and did not say
|
||||
|
||||
Android's captured recording state has IFD `0x097=21`; mainline reads `00`.
|
||||
During a five-second live TX7/TX8 capture, writing `0097 21` through the
|
||||
debugfs regmap returned success but read back `0097: 00`, and all 478,078
|
||||
samples remained zero. This confirms that a userspace register replay cannot
|
||||
enable that state on this path. It does **not** establish what `0x097` means
|
||||
or prove that it is the root cause. Do not turn it into an unconditional
|
||||
codec write or call it an “ADSP-master fix” without an implementation-level
|
||||
source or Android-side transition capture.
|
||||
|
||||
TX6 is enabled by the current mixer default alongside TX7/TX8. It makes the
|
||||
unconstrained map three channels (`0x86`, `0x87`, `0x88`), while disabling it
|
||||
produces the correct TX7/TX8 pair above. That is a real UCM/machine-routing
|
||||
cleanup item, but it did not restore capture and is not the root cause.
|
||||
|
||||
## Proposed next work on 2026-07-11 — superseded
|
||||
|
||||
1. Capture the working Android interface-device transition, or find the
|
||||
downstream owner/meaning of IFD `0x097`; compare the actual write sequence,
|
||||
not just idle/recording register snapshots.
|
||||
2. Add the TX7/TX8 `CDC_IF` mux selections to the eventual microphone UCM
|
||||
enable sequence (and keep TX6 off for this two-channel route). Do not
|
||||
expose a broken PipeWire source before its capture is non-zero.
|
||||
3. Repeat the WCD9340 interface/ADSP comparison only with that fully powered
|
||||
codec path. The still-zero full-path test strengthens, but does not prove,
|
||||
the relevance of the non-sticking Android IFD `0x097=21` delta. Capture an
|
||||
Android transition or find the downstream owner before making a firmware
|
||||
or register claim. Do not flash a kernel test without an explicit
|
||||
rollback-approved deployment plan.
|
||||
4. Only after that, compare the remaining post-activation state with Android
|
||||
at the firmware boundary. Q6 and NGD success alone do not show that the
|
||||
codec/ADSP hardware emits decimator samples.
|
||||
5. Fix the stray TX6 default separately in the persistent UCM/machine route,
|
||||
then verify it with the channel-map probe. Do not claim mic support until a
|
||||
live capture contains non-zero samples.
|
||||
|
||||
## Instrumentation
|
||||
|
||||
`/usr/local/sbin/blueline-lockup-trace` is a root helper deployed from
|
||||
`rootfs-overlay/`. `mic-start` traces the Q6/WCD stream functions and the
|
||||
three WCD codec DAPM event callbacks; `mic-map-start`, `mic-ifc-start`, and
|
||||
`mic-ngd-start` collect Q6 channel-map, WCD interface-register-write, and
|
||||
Q6/NGD return traces respectively. Trace sessions are temporary and must
|
||||
always be paired with the matching `*-stop` command; the current phone was
|
||||
checked clean afterwards (`tracing_on=0`, tracer `nop`, no kprobes).
|
||||
|
||||
## 2026-07-18 full Android power-state reconciliation
|
||||
|
||||
A clean, single-channel PulseAudio/UCM capture on kernel `4c3a94153` proved
|
||||
that the intended TX7/DMIC4 path is selected and fully ACKed, but every one of
|
||||
55,680 S16 samples is exactly zero. A simultaneous live codec dump exposed
|
||||
two remaining Android/mainline power-state differences which earlier tests had
|
||||
not actually eliminated:
|
||||
|
||||
- `ANA_MICB4` is `0x50` on mainline versus `0x90` during Android recording.
|
||||
The DT opt-in `qcom,blueline-dmic-micbias-pullup` is on the `codec@1,0`
|
||||
node, but `wcd934x_codec_parse_data()` queried `dev->parent->of_node` (the
|
||||
SLIM controller). Therefore `micbias_pullup` was always false and the
|
||||
earlier claim that pull-up mode had been tested is invalid. The code now
|
||||
queries `dev->of_node`, selecting Qualcomm's downstream
|
||||
`MICB_PULLUP_ENABLE` path (`0x90`).
|
||||
- `ANA_BUCK_CTL` is `0x00` on mainline versus `0x07` on Android. Qualcomm's
|
||||
resource manager sets RCO followed by buck `PRE_EN1`, `PRE_EN2`, and
|
||||
`HIGH_ACCURACY_EN`, with 100 us between stages. The mainline driver had
|
||||
dropped this sequence. It is restored as ordered masked writes, including
|
||||
the reverse transition; this is not the harmful unsequenced Android
|
||||
end-state replay removed earlier.
|
||||
|
||||
The same correction (`a75371b49`) also matches Android's board state by
|
||||
programming only DMIC interface 3 pads (the real DMIC3/4 inputs) and avoids
|
||||
leaving TX7 at the transient 150 Hz HPF setting (`CFG0=0xd0`) indefinitely;
|
||||
Android's steady recording state is `CFG0=0x90`. One boot-time log reports
|
||||
whether pull-up mode was parsed, and the capture log reports live BUCK and all
|
||||
MICB registers so the next capture validates the complete state in one pass.
|
||||
|
||||
## 2026-07-18 eFuse initialization regression and cache fix
|
||||
|
||||
On the clean `3b85e688f` boot, the complete downstream AFE codec-control map
|
||||
registered successfully and TX7 again reached `port_start`, CONNECT_SOURCE,
|
||||
DEF_ACT_CHAN, and RECONFIG_NOW with positive ACKs. PulseAudio captured 760,318
|
||||
raw bytes (380,159 S16 samples), all exactly zero. The journal exposed a real
|
||||
codec boot failure: `wcd934x_enable_efuse_sensing` read `EFUSE_STATUS=0` and
|
||||
reported that sensing never completed.
|
||||
|
||||
History showed that `1c095f7a7` had added the downstream-required
|
||||
internal-SIDO start before eFuse sensing, but `c1b6371c6` removed the call while
|
||||
dropping unrelated experiments. The exact downstream SIDO/buck transition
|
||||
helper was later restored by `a75371b49` without reconnecting it to eFuse init.
|
||||
The audit also found that downstream treats `EFUSE_CTL`, all eFuse value
|
||||
outputs, and status as volatile, while mainline cached them in `REGCACHE_MAPLE`.
|
||||
|
||||
Commit `82355eac0` batches the complete correction: internal SIDO before the
|
||||
sense enable, the required 5 ms delay before RCO/BG, downstream-parity volatile
|
||||
register handling for the eFuse/clock/buck sequence, live completion polling,
|
||||
register error propagation, and explicit status/out14/out15 logging. Do not
|
||||
interpret another zero capture until a cold boot proves the codec prints
|
||||
`eFuse sense complete`; otherwise the physical codec trim/version state is
|
||||
still uninitialized even though SLIM management commands ACK normally.
|
||||
64
saf/device/build.md
Normal file
64
saf/device/build.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# PAF · Build & deploy (Arch era)
|
||||
|
||||
The loop that turns a kernel commit into a verified boot on glass. pmOS-era
|
||||
pmbootstrap notes preserved at `evidence/build-pmos-era.md` — dead path now.
|
||||
|
||||
## The three places
|
||||
|
||||
| Where | Role |
|
||||
|---|---|
|
||||
| gitea `10.10.20.120:4455` — `Fimeg/Pixel3Arch` + `Fimeg/linux-blueline` (branch `blueline`) | **Source of truth.** Uncommitted work does not build — commit or it doesn't exist. |
|
||||
| ArchDev LXC `casey@10.10.20.123` (ani key) | **Build host.** Cross toolchain installed, MAKEFLAGS system-wide. |
|
||||
| Laptop | **Device ops only** — flashing, fastboot, USB tether, boot.img assembly. |
|
||||
|
||||
## The loop
|
||||
|
||||
1. Commit + push kernel changes to gitea (`kernel/src/linux-blueline-src`
|
||||
on the laptop is a clone with remote `gitea`).
|
||||
2. Build on ArchDev:
|
||||
`ssh ... casey@10.10.20.123 'cd ~/Projects/Pixel3Arch/kernel && ./build.sh blueline'`
|
||||
- build.sh **self-locks with flock** — invoke it directly, never wrap it
|
||||
in an outer flock on the same lockfile (deadlock, cost a session).
|
||||
- It force-fresh-clones from gitea and **hard-asserts the built package's
|
||||
embedded commit hash matches the gitea branch tip** — a mismatch means
|
||||
stale source; the build fails loud instead of shipping it.
|
||||
3. Pull `kernel/linux-blueline-7.1.1-1-aarch64.pkg.tar.zst` to the laptop.
|
||||
4. Assemble the boot image (pmOS-chain — the currently proven ramdisk path):
|
||||
```
|
||||
bash boot/build-bootimg-pmos-chain.sh <vmlinuz-from-pkg> <dtb-from-pkg> \
|
||||
staging/deploy-139aa/boot-139aa7748181-pmoschain.img <out.img>
|
||||
```
|
||||
Kernel is Image.gz + **appended DTB** (header v0 ignores --dtb); ramdisk
|
||||
and cmdline are lifted from the reference image. Migrating to our own
|
||||
mkinitcpio ramdisk (`boot/build-bootimg.sh`) is a parked one-variable task.
|
||||
5. Deploy: `scripts/deploy-kernel.sh <pkg.tar.zst> <boot.img>`
|
||||
|
||||
## Deploy doctrine — no chimera boots
|
||||
|
||||
`deploy-kernel.sh` exists because a kernel was once booted 28 seconds before
|
||||
its module tree was installed: no evdev, no input, dead services, and a
|
||||
night of void test results. The script enforces the only safe order:
|
||||
|
||||
1. **Modules first** — scp the pkg, `pacman -U`, verify the module dir exists.
|
||||
2. Flash boot_a, reboot.
|
||||
3. **Refuse to bless the boot** unless `uname -r` matches the installed tree.
|
||||
|
||||
Never flash-then-install. If `uname -r` and `/usr/lib/modules/` disagree,
|
||||
every test result on that boot is garbage — reboot properly before
|
||||
concluding anything.
|
||||
|
||||
## Gotchas ledger (each cost a session)
|
||||
|
||||
- makepkg needs `CARCH=aarch64 ... --ignorearch` — CARCH comes from the
|
||||
environment, not the PKGBUILD, or you get x86_64-labeled aarch64 packages.
|
||||
- makepkg builds in its own clone of `source=()`, not your working tree —
|
||||
a dirty tree silently builds without your changes.
|
||||
- The kernel release string embeds the git hash (`-g<hash>`) — it is the
|
||||
ground truth for "what am I actually running" (`uname -r`).
|
||||
- `modules_install` needs CROSS_COMPILE too, or strip fails on aarch64 .ko.
|
||||
- ArchDev is unprivileged LXC: qemu-emulated execution (binfmt) does NOT
|
||||
work there — chroot-based work stays on the laptop or on-device.
|
||||
- Commit messages: history in this repo proved that "evidence" cited in
|
||||
rapid-fire commits can be wrong (see touch.md, display.md post-mortems).
|
||||
Cite the actual file:line / register / boot you verified on, and note
|
||||
whether the boot was clean.
|
||||
109
saf/device/display.md
Normal file
109
saf/device/display.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# PAF · Display — SW43408 panel, DSI, bias rails (SOLVED 2026-07-10)
|
||||
|
||||
## Current regression — verified on glass 2026-07-11
|
||||
|
||||
On current kernel `7.1.1-sdm845-g56b0a053bef8`, a true unplugged cold boot
|
||||
still produces a green flash during boot. Green flashes also remain on wake.
|
||||
During AP-side DT2W testing the first double tap sometimes does a visible
|
||||
green-flash/black half-wake and the second wakes the panel. The kernel trace
|
||||
shows the first gesture reaches panel prepare and is immediately followed by
|
||||
unprepare; there are no DSI/DPU errors. In one successful wake the visible
|
||||
frame remained stale despite a synchronized system clock and live Hyprland +
|
||||
Quickshell processes. This is an observation, **not** proof of a panel-only
|
||||
root cause; preserve it for the display/DPMS investigation.
|
||||
|
||||
**Recovery and clock distinction, 2026-07-11:** after an instrumented
|
||||
libssc-triggered lockup, `blueline-screen-toggle on` re-prepared the panel but
|
||||
it unprepared again seconds later even with `hypridle` stopped. Do not churn
|
||||
panel/FTS resets in that state. A normal reboot of the known-good current slot
|
||||
restored the panel and compositor. The first post-boot `ii` screenshot still
|
||||
showed the Jul-06 20:00 clock floor even though NTP had corrected system time;
|
||||
restarting only Quickshell (`qs -c ii kill`, then `qs -c ii`) refreshed it to
|
||||
the real time. Therefore an old ii clock is not alone evidence of a frozen
|
||||
scanout. The remaining task is to prevent ii from retaining the pre-NTP clock,
|
||||
not to reset the panel again.
|
||||
|
||||
Cold boot → Hyprland AND power-button sleep/wake visually verified on
|
||||
kernel `g766205b60588`. Read this whole file before touching
|
||||
`panel-lg-sw43408.c`, the labibb regulator, or the display DT.
|
||||
|
||||
## The doctrine: WARM-CYCLE. Do not dismantle again.
|
||||
|
||||
The working design in `drivers/gpu/drm/panel/panel-lg-sw43408.c`:
|
||||
|
||||
- `.prepare` = regulators + reset pulse only. No DCS.
|
||||
- `.enable` = the ENTIRE DCS init (gamma, TE, sleep-out, vendor regs, PPS,
|
||||
compression, display-on) — runs over a fully-up DSI link, onto a
|
||||
still-powered DDIC, on every DPMS-on.
|
||||
- `.disable` = display_off + enter_sleep (HS mode). That is ALL DPMS-off does.
|
||||
- **No `.unprepare` power-off.** The panel keeps vddi/vpnl forever after
|
||||
first boot; reset is never re-asserted at runtime.
|
||||
|
||||
Why: **upstream's prepare-time cold-cycle design cannot cold-start this
|
||||
panel.** Full power-off + reset + re-init produces a green flash (display-on
|
||||
latching onto random GRAM) then permanent black, on every transition. That
|
||||
is the original "screen won't wake without the sdm845-mainline hack" bug
|
||||
(David's report). The warm-cycle shape IS the fix — the panel is
|
||||
re-initialized warm, never cold.
|
||||
|
||||
History: the working design was dismantled on 2026-07-09 by ee6611adb
|
||||
("adopt upstream prepare-time init") and a day of ordering experiments
|
||||
failed to make cold-start work (display-on before PPS, after PPS, downstream
|
||||
power sequencing — all green-flash-black). 766205b60 restored the warm-cycle
|
||||
architecture. Any future "align with upstream" refactor must first prove
|
||||
cold-start on real glass.
|
||||
|
||||
## Bias rails: LAB/IBB under SWIRE (hardware) control
|
||||
|
||||
Matching downstream: the DDIC drives the OLED bias rails over the SWIRE
|
||||
wire, not software.
|
||||
|
||||
- DT: `qcom,swire-control` on the pmi8998 labibb node (1b02532a4).
|
||||
- Driver: SWIRE ops are software no-ops (enable/disable/set_voltage return 0);
|
||||
probe writes IBB `ENABLE_CTL=0x40` (SWIRE_RDY, module-enable cleared).
|
||||
LAB `ENABLE_CTL=0x00` is CORRECT in SWIRE mode (slaved to IBB in hardware).
|
||||
- Behavior: panel sleep-in → rails physically drop; sleep-out → rails rise.
|
||||
Register-verified live across DPMS cycles: STATUS1 (IBB 0xdc08 / LAB
|
||||
0xde08 on regmap 0-03) reads 00/00 off → 80/a0 on.
|
||||
- The panel driver's supplies are vddi + vpnl ONLY. It never touches
|
||||
LAB/IBB. c789daaa8 fixed the tail end of the old design (unprepare
|
||||
disabling never-enabled rails → unbalanced-disable → `panel->prepared`
|
||||
stuck true → EBUSY zombie); moot under warm-cycle but the lesson stands:
|
||||
an error return from unprepare wedges the prepared flag forever.
|
||||
- Voltage: software set_voltage is a no-op; hardware stays at the
|
||||
bootloader's 5.5V programming.
|
||||
|
||||
## Userspace wake path
|
||||
|
||||
- Hyprland (Lua build) power-button / tap-to-wake toggle:
|
||||
`blueline-screen-toggle` on the phone. Wake needs BOTH
|
||||
`hl.dsp.dpms("on")` AND `hl.dsp.force_renderer_reload()` — dpms-on alone
|
||||
can issue a commit with no real CRTC transition (crtc=NULL skips
|
||||
drm_panel_prepare; long-standing compositor-side hole).
|
||||
**Gotcha fixed 2026-07-10:** the script called `forceRendererReload()`,
|
||||
which the Lua Hyprland renamed to `force_renderer_reload()` — every
|
||||
power-button wake silently no-op'd. Dispatcher names must be verified
|
||||
against the running Hyprland, errors surface only in the hyprctl reply.
|
||||
- greetd → `start-hyprland` as initial session; the boot chain
|
||||
fbcon → greetd → Hyprland does one panel takeover, warm.
|
||||
|
||||
## Diagnostic quick kit
|
||||
|
||||
- Rails now: `sudo grep -E "^(dc08|de08):" /sys/kernel/debug/regmap/0-03/registers`
|
||||
(80/a0 = up, 00/00 = SWIRE-dropped).
|
||||
- Compositor truth: `grim` screenshot — separates "compositor renders" from
|
||||
"glass shows" (they diverged for two days; the screenshot was pixel-perfect
|
||||
while the glass was black).
|
||||
- DPMS from SSH:
|
||||
`HYPRLAND_INSTANCE_SIGNATURE=$(ls /run/user/1000/hypr/) XDG_RUNTIME_DIR=/run/user/1000 hyprctl dispatch 'hl.dsp.dpms("off"|"on")'`.
|
||||
- drm.debug=0x1ff wraps the dmesg ring in ~130ms of transition traffic —
|
||||
use `journalctl -k` or a lower mask for transition captures.
|
||||
- Boot-time `DSI PLL lock failed` + `already disabled/unprepared` clk
|
||||
warnings at ~0.4s: probe-time deferred-probe noise, once per boot,
|
||||
benign-but-ugly (error path double-disables a clk). Not a transition bug.
|
||||
|
||||
## Known-good fallback
|
||||
|
||||
`../staging/deploy-139aa/boot-139aa7748181-pmoschain.img` — pre-SWIRE-era
|
||||
kernel (stmfts touch, warm-cycle display of that era). Module tree already
|
||||
on the phone. Prefer fixing forward on the blueline tip.
|
||||
166
saf/device/edge-sense.md
Normal file
166
saf/device/edge-sense.md
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# PAF · Active Edge (squeeze)
|
||||
|
||||
The Pixel 3's squeeze gesture. **Working on Arch as of 2026-07-28** — force is
|
||||
detected, and the gesture reaches the shell through the session authority.
|
||||
|
||||
Spine doc: current truth, updated in place. The narrative of how it was found
|
||||
lives in `../../SouveraineOS/docs/tasks/13-active-edge-squeeze.md`, which keeps
|
||||
each superseded conclusion verbatim beneath its correction.
|
||||
|
||||
## The hardware chain
|
||||
|
||||
Six strain gauges bonded to the frame → **MAX11261** ADC on the SLPI's own I²C
|
||||
bus → the `max11259` SEE driver on the sensor island → published over SSC as the
|
||||
data type **`elmyra_raw`**, SUID `15717249801838747009 / 6291402259395006624`.
|
||||
|
||||
Not evdev, not a GPIO key. There is no input node and nothing bridges the SUID
|
||||
to one; the only way to it is an SSC client. `sns_touch_gesture` is a **different
|
||||
sensor** — the FTS double-tap nanoapp on the touch controller (slave 0x49, bus 5,
|
||||
irq 125). Opening that one and squeezing correctly produces nothing.
|
||||
|
||||
**Power: PM8998 GPIO 2**, gpiochip0 line 1, held by a devicetree `gpio-hog`
|
||||
(`edge_sense_pwr_hog` in `sdm845-google-common.dtsi`). Confirm with:
|
||||
|
||||
sudo gpioinfo | sed -n '3p'
|
||||
# line 1: unnamed output consumer="edge-sense"
|
||||
|
||||
## Why it did not work before, and what fixed it
|
||||
|
||||
Through 2026-07-27 the gauges converted and drifted but **ignored force**: 399
|
||||
samples across 35 s of hard squeezes gave a median deflection of 49 and a max of
|
||||
127, against a resting noise floor of ~50. The conclusion at the time was that
|
||||
the analog front end is only configurable from inside the `elmyra` CHRE nanoapp
|
||||
via `ashConfigureElmyraFilters`, and that an SSC client gets whatever the AFE
|
||||
defaults to.
|
||||
|
||||
On 2026-07-28 the **same unchanged binary** with **no nanoapp loaded** measured
|
||||
deflections of 2502–2990 under real squeezes.
|
||||
|
||||
What changed between the two measurements is the rail. It became a DT
|
||||
`gpio-hog` (`318f7d0`) instead of a `gpioset` unit that ran during boot. The
|
||||
likely mechanism is **ordering, not voltage**: the hog asserts the rail at
|
||||
kernel probe, before SSC enumerates the MAX11261, where the unit powered it
|
||||
after the ADC had already been probed unpowered. An AFE initialised without its
|
||||
bridge excited stays wrong until reset.
|
||||
|
||||
That is the leading explanation and it is **not proven**. Confirming it means
|
||||
booting a pre-hog kernel and re-measuring. The electrical prediction made at the
|
||||
time — that a pinctrl node changes nothing, since PM8998 GPIO 2 reads `vin-0`
|
||||
on both stacks because neither selects one — still stands and was never the
|
||||
issue.
|
||||
|
||||
## Calibration is real and per-unit
|
||||
|
||||
`/persist/sensors/elmyra/calibration.{0..5}`, factory-written 2019, one file per
|
||||
gauge:
|
||||
|
||||
touch_2_sensitivity: 0.2402884277733113
|
||||
|
||||
This unit: `0.240 0.295 0.176 0.359 0.329 0.342`. They normalise the gauges
|
||||
against each other; the spread is why Google ships a factory calibration step
|
||||
(`edgesense_roller_cal/`) at all. The producer reads them at startup and logs
|
||||
them — if that line shows six `1.000`s, the persist partition is not mounted or
|
||||
the files are gone, and detection will be lopsided rather than absent.
|
||||
|
||||
These are consumed on the **AP side** by our producer. Nothing serves them over
|
||||
HexagonFS: `hexagonrpcd`'s virtual `/persist/sensors/` contains only `registry`
|
||||
(`rpcd_builder.c`). That matters only if the nanoapp path is ever taken.
|
||||
|
||||
## The threshold model
|
||||
|
||||
From Google's own sweep, `edgesense_roller_cal/0_raw/sensor_data.csv`, which
|
||||
captures the gauges at a known 10 N and 20 N — the loaded bank moves 400–2000
|
||||
counts per 10 N step. Raw values sit near ±100000, so an absolute threshold
|
||||
against them is meaningless; the first version compared exactly that and never
|
||||
fired.
|
||||
|
||||
- `DEFAULT_THRESHOLD 2500` — summed normalised deflection from a rolling baseline
|
||||
- `BASELINE_ALPHA 0.02` — the baseline must follow temperature and grip drift,
|
||||
and is frozen while a squeeze is held or the release reads as a squeeze the
|
||||
other way
|
||||
- `RELEASE_FRACTION 0.6` — hysteresis, same reason proximity has it
|
||||
|
||||
Observed squeezes land at 2502–2990, which is close to the line. The binary
|
||||
takes the threshold as an argument, so tuning needs no rebuild.
|
||||
|
||||
## It reports; it does not decide
|
||||
|
||||
The producer does **not** call the shell. It asks `souveraine-sessiond` first:
|
||||
|
||||
{"op":"input","trigger":"squeeze"} → $XDG_RUNTIME_DIR/souveraine/sessiond.sock
|
||||
|
||||
and delivers `qs -c souveraine ipc call gesture squeeze` only on `ok:true`.
|
||||
sessiond treats a squeeze as real user input — it resets the idle budget and
|
||||
lands in the forensic trail — but applies the pocket veto first: a covered
|
||||
proximity sensor refuses it with `refused_by_state`, exactly as it already
|
||||
refused tap-to-wake, because a phone in a tight pocket *is* a squeezed chassis.
|
||||
|
||||
Calling the shell directly, which is what it did first, made this an eighth
|
||||
blind actor: no idle reset, nothing in the trail, no veto, and no way for the
|
||||
machine to notice the source dying.
|
||||
|
||||
**On an unreachable authority it fails closed** and says so. A squeeze that
|
||||
cannot be checked is not delivered; the alternative would mean the pocket veto
|
||||
silently ceases to exist every time sessiond restarts.
|
||||
|
||||
What a squeeze *means* is the shell's routing table (`services/Gestures.qml`,
|
||||
`squeezeAction`, default `dial` → `dial toggle`), never this producer's business.
|
||||
|
||||
## Packaging
|
||||
|
||||
`pkgs/blueline-edge-sense` — `blueline-edge-sense 0.2.0-1`, aarch64, built by
|
||||
`build.sh` on archdev. Ships the binary, the user unit, and a
|
||||
`souveraine-sessiond.service.wants` symlink so it is **enabled by default** and
|
||||
survives a reprovision — the reader wants the authority it reports to, not the
|
||||
session at large. It hung off `graphical-session.target` until 2026-07-29,
|
||||
which started nothing then; the target is live as of TASK-76, but sessiond
|
||||
remains the correct thing to want.
|
||||
|
||||
Before 2026-07-28 the binary sat unowned in `/usr/bin` since 07-26 — one of
|
||||
TASK-25's 5,624. Installing the package hit that exact conflict; the old copy is
|
||||
at `/var/backups/unowned-preupgrade/blueline-edge-sense.20260728`.
|
||||
|
||||
## Known gaps
|
||||
|
||||
- **No detection while the AP sleeps.** This is an AP-side process sampling six
|
||||
channels at 100 Hz forever, which is precisely what `DozeDeep` exists to kill.
|
||||
Squeeze-to-wake from deep doze is only possible from the DSP side — the
|
||||
nanoapp. Grip and doze are currently mutually exclusive and nothing in the
|
||||
code knows it.
|
||||
- **Our detector, not Google's.** `elmyra.so` ships `AggregateDetector`,
|
||||
`DetectorSetup`, `Filter`, `decision_threshold_min/max`, `global_sensitivity`
|
||||
and a `GESTURE_PROGRESS` stream that would give hold-vs-tap. Ours is a
|
||||
baseline-delta threshold firing 2 counts above its own line.
|
||||
- **No haptic confirmation from the right place.** `elmyra_haptics_control` and
|
||||
`haptic_gpio 88` say the buzz is the nanoapp's job downstream; ours is the
|
||||
shell guessing.
|
||||
- **Nothing proves which binary is reporting.** sessiond takes an `input` from
|
||||
whoever connects to the socket, and the shell takes a gesture from whoever
|
||||
calls its IPC — both demonstrated from an SSH shell on 2026-07-28. TASK-41.
|
||||
- **Inference posture unresolved.** TASK-40 designates the raw 100 Hz six-gauge
|
||||
stream a bring-up instrument, not a product surface: it is a typing
|
||||
side-channel and a grip biometric. Enabled anyway by Casey's call, because the
|
||||
exposure is not created by this reader — any AP process linking libssc can
|
||||
open `elmyra_raw` today. Confining *that* is TASK-40's half.
|
||||
|
||||
## Where the code is
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| Producer | `pkgs/blueline-edge-sense/edge-sense.c` |
|
||||
| Package + unit | `pkgs/blueline-edge-sense/{PKGBUILD,build.sh,*.service}` |
|
||||
| Rail (gpio-hog) | kernel `sdm845-google-common.dtsi`, `edge_sense_pwr_hog` |
|
||||
| libssc fixes | `pkgs/libssc-souveraine` — single-output sensors, fastest advertised rate (1 Hz → 100 Hz) |
|
||||
| Authority side | souveraine `src/sessiond/{protocol,device_state,server}.rs` — `InputTrigger::Squeeze`, `suppress_wake`, `note_input_gated` |
|
||||
| Routing | souveraine `surfaces/quickshell/services/Gestures.qml` |
|
||||
| Nanoapp host | `pkgs/hexagonrpc` (chrecd) — loads `elmyra`, sends `RECOGNIZER_START` |
|
||||
|
||||
## Verifying it live
|
||||
|
||||
systemctl --user status blueline-edge-sense
|
||||
journalctl --user -u blueline-edge-sense -f
|
||||
|
||||
A healthy start logs the six sensitivities, the SSC open, and a six-value
|
||||
baseline. A squeeze logs `SQUEEZE (deflection N)` then either `squeeze
|
||||
delivered` or the authority's refusal — and a refusal is normal operation, not
|
||||
a fault.
|
||||
51
saf/device/evidence/README-pmos-era.md
Normal file
51
saf/device/evidence/README-pmos-era.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# PAF — Pixel 3 (blueline) pmOS Architecture Framework
|
||||
|
||||
The single living spine for this port. Replaces the accreting handoffs/findings: instead of
|
||||
appending a new dated doc each session, **update the relevant layer in place.** The spine:
|
||||
|
||||
- **[modem.md](modem.md)** — the modem bring-up stack as layers, status per layer, the
|
||||
eliminated-leads ledger (don't re-chase these), and the one live frontier.
|
||||
- **[platform.md](platform.md)** — device hardware map, boot/storage architecture, how to
|
||||
reach the phone, recovery procedures, and the hard hardware lessons.
|
||||
- **[build.md](build.md)** — how the kernel + device package are actually built with
|
||||
pmbootstrap, the two-package split, and the build-system gotchas that have each cost a session.
|
||||
- **[slpi.md](slpi.md)** — SLPI sensor island bring-up and the `sar.cc:27` crash-loop fix.
|
||||
- **[secure_load_tz_frontier.md](secure_load_tz_frontier.md)** — 🔴 SUPERSEDED: secure-load is
|
||||
resolved (modem loads clean via DELTA-1). Historical −22/fastrpc_mem record; the live frontier is
|
||||
the firmware OFFLINE/52 park — see `modem.md` + `diag_capture.md`.
|
||||
- **[README.md](README.md)** — this index.
|
||||
|
||||
Deeper/dated capture docs (`modem_*`, `qcril_*`, `pmos_fix_plan.md`, `diag_capture.md`, …) sit
|
||||
alongside as evidence; the spine above is what you read first. Sibling docs kept outside the PAF
|
||||
on purpose: `../README.md` (the generic port guide) and `../SIDE-TASKS.md` (parked non-modem work).
|
||||
|
||||
## Status at a glance (2026-07-01)
|
||||
|
||||
| Layer | State |
|
||||
|---|---|
|
||||
| Firmware load (MBA→MPSS) | 🟢 healthy — loads clean, **no −22 on the modem path** (DELTA-1 `c2923cb49`) |
|
||||
| Power (rpmhpd cx/mx/mss → mss.lvl) | 🟢 healthy, proven equivalent to Android |
|
||||
| QMI / QRTR transport | 🟢 healthy |
|
||||
| rmtfs / tqftpserv | 🟢 healthy (rmtfs in `-P -s` rw partition mode) |
|
||||
| pd-mapper | 🟢 fixed live (2026-06-29 late). `CONFIG_QCOM_PD_MAPPER=m` is in the flashed kernel and `qcom_pd_mapper` loads. This restored WLAN PD advertisement and `ath10k_snoc` probe; `wlan0` exists. |
|
||||
| SIM / UIM provisioning | 🟢 healthy (usim ready, Primary-GW slot1/app1) |
|
||||
| PDC MCFG autoselect | 🟡 works (configs go Active) — **NOT the gate** (modem parks OFFLINE regardless) |
|
||||
| SLPI sensor island (`sar.cc:27`) | 🟠 real firmware crash, still unfixed — **but now cleanly bounded and PROVEN independent of the modem 52 wall** (2026-07-01, three live boots: crashed continuously / running / cleanly parked all gave byte-identical `DeviceNotReady`). Ordering + crash-recovery both fixed (see `slpi.md`); the sensor island itself is out of scope for modem bring-up now. |
|
||||
| **RF arm (`Set Operating Mode=online`)** | **🔴 BLOCKED — offline/mode 0/52. THE frontier; proven kernel- and SLPI-independent.** `dms-ui-ready.py` (DMS 0x0064, the one load-bearing QCRIL pre-online step never replayed) built 2026-07-01, untested on hardware — next pmOS boot is the test. |
|
||||
| Data / voice (IPA, MM, calls) | gated behind RF arm |
|
||||
|
||||
**One-line state:** the modem MSS loads clean, enumerates fully (IMEI/SIM/firmware all read
|
||||
reliably), and the phone is stable on 7.1.1 with WiFi working. The 52 wall is the modem's
|
||||
firmware-internal RF-init decision (pmOS parks op-mode 3 OFFLINE vs Android's op-mode 1
|
||||
LOW_POWER) — now **proven independent of SLPI** with hard repeated evidence, closing out the prior
|
||||
"make SLPI healthy first" ranked move as a dead end for this specific blocker. The live frontier is
|
||||
`dms-ui-ready.py`: the one QCRIL pre-online QMI step (disassembly-confirmed load-bearing) that pmOS
|
||||
has built but never fired on real hardware yet.
|
||||
|
||||
**Build/packaging milestone (2026-06-22):** all fixes unified into one **7.1.1** kernel tree as
|
||||
commits (not scattered patches), built via pmbootstrap; the PDC self-provisioner
|
||||
(`qcril-autoselect`) moved out of hand-deployed `tools/` glue and **into the
|
||||
`device-google-blueline` package**, auto-enabled via the systemd preset and ordered
|
||||
`After=rmtfs` / `Before=ModemManager`. One piece still hand-built: the openimsd **libqmi fork**
|
||||
(PDC Set Feature) is not yet its own aport — see [build.md](build.md). This is "how the PAF
|
||||
ought to look": kernel fixes in-tree, provisioner in the device package, nothing scp'd by hand.
|
||||
20
saf/device/evidence/README.md
Normal file
20
saf/device/evidence/README.md
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# PAF evidence
|
||||
|
||||
Dated captures, handoffs, and archaeology live here. They preserve how a
|
||||
conclusion was reached; they do not compete with the layer docs one directory
|
||||
up. When evidence changes the answer, update the layer doc and leave the dated
|
||||
record legible as history.
|
||||
|
||||
- `audio/` — microphone, PipeWire, and call-audio handoffs from 2026-07-19.
|
||||
Current authority: `../audio.md`.
|
||||
- `power/` — charge-control implementation handoff. Current authority:
|
||||
`../platform.md` and TASK-08.
|
||||
- The remaining files preserve pmOS, modem/QCRIL, TrustZone, provisioning, and
|
||||
diagnostic research cited by the living spine.
|
||||
|
||||
Recovery anchors for the consolidation:
|
||||
|
||||
- SouveraineOS `72aaa53` — first copied PAF tree, 2026-07-24.
|
||||
- Pixel3Arch `9b1afd1` — last complete PAF tree before its retirement.
|
||||
- Pixel3Arch tree `27350cd5f5bec5062f361cbbb329cf7a04efb2f2` — exact snapshot
|
||||
promoted into SouveraineOS before reconciliation.
|
||||
74
saf/device/evidence/android_modem_bringup_capture.md
Normal file
74
saf/device/evidence/android_modem_bringup_capture.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# PAF · Android modem bring-up — COMPLETE captured sequence (the working reference)
|
||||
|
||||
The full modem boot Android does, end to end, extracted from the real captures in
|
||||
`android-capture-20260620/` (modem SSR full re-init -> ONLINE, and the cold online capture).
|
||||
This is the ENTIRE process — not one diff. Each phase lists what Android does (with file:line)
|
||||
and the pmOS gap. Android reaches **IN_SERVICE / LTE / Fido (302/720)** at the end; pmOS stalls
|
||||
at phase 1.
|
||||
|
||||
Source files:
|
||||
- `qcril-ssr-fullinit.txt` (66k lines, modem SSR -> full re-init)
|
||||
- `qcril-online-capture.txt` (24k lines, cold bring-up to ONLINE)
|
||||
- `android-radio-log.txt`, `qcril-ssr-TIMELINE.txt` (digest)
|
||||
SIM: ICCID <redacted>, IMSI <redacted>, MCC/MNC 302/370 (Fido).
|
||||
|
||||
---
|
||||
|
||||
## PHASE 0 — kernel/firmware secure-load (pre-QMI; NOT in these AP logs)
|
||||
The modem PIL loads MBA -> PBL -> MPSS with the TZ memory-assign + XPU unlock. Android passes this.
|
||||
pmOS is where the **-22 / XPU violation** lives — see `secure_load_tz_frontier.md`. NOTE: downstream
|
||||
(`peripheral-loader.c:334` `pil_assign_mem_to_subsys`) makes the **same** HLOS->subsys_vmid RW
|
||||
hyp_assign that mainline `qcom_q6v5_mss.c:463` does — the assign is load-bearing, the skip was wrong.
|
||||
|
||||
## PHASE 1 — firmware comes up at op-mode 1 = LOW_POWER ← THE DIVERGENCE
|
||||
- Android: `qcril_qmi_nas_query_cur_power_state: known modem operating mode 1` then
|
||||
`dms_handle_modem_operating_mode: modem's operating mode needs to be changed`
|
||||
(`qcril-online-capture.txt:90,96`), and the DMS event report shows `oprt 1 / 0`
|
||||
(`qcril-online-capture.txt`, qcril_qmi_nas.cpp:6562).
|
||||
- **pmOS: modem comes up at op-mode 3 = OFFLINE** (`tools/modem-online-provisioner/RESULTS.md`).
|
||||
OFFLINE->online = **52 DeviceNotReady**; low_power->online would be a legal transition.
|
||||
- QMI DMS operating-mode enum: 0=ONLINE 1=LOW_POWER 2=FACTORY 3=OFFLINE 4=RESET 5=SHUTDOWN
|
||||
6=PERSISTENT_LOW_POWER.
|
||||
- **The real question is NOT "how to force OFFLINE->online" (it can't) — it is "why does our
|
||||
modem boot to OFFLINE when Android's boots to LOW_POWER".** OFFLINE is the firmware's
|
||||
"something-wrong / not-provisioned" state. Prime suspects: phase-0 secure-load health (degraded
|
||||
boot), or EFS/rmtfs provisioning (modem can't read/write its NV -> falls to OFFLINE).
|
||||
|
||||
## PHASE 2 — UIM / SIM card init (Android: OK)
|
||||
- `card_state 3` (present), app `state 5` (READY), app `type 2`=USIM / `type 5`=ISIM
|
||||
(`qcril-online-capture.txt:336-337`). SIM ICCID/IMSI read fine.
|
||||
- pmOS gap: untested past phase 1 — modem must be online first.
|
||||
|
||||
## PHASE 3 — PDC: select carrier MBN config (per SIM slot) ← pmOS likely missing
|
||||
- Android: `qcril_qmi_pdc_get_selected_mbn_config_by_sim_slot` (`qcril-ssr-fullinit.txt:4308…`)
|
||||
and `qcril_qmi_pdc_get_selection_carrier: cnv is valid, cnv=[72]` (`qcril-online-capture.txt:9367`,
|
||||
`qcril_qmi_nas: prop_cnv = 72`). cnv 72 = the Fido carrier config.
|
||||
- This is the carrier/modem config (MBN) activation. pmOS has no RIL doing it; the planned
|
||||
replacement is the PDC self-provisioner `qcril-autoselect.py` (see `build.md`,
|
||||
`device-google-blueline`). Without a selected config the modem may stay inert on RF.
|
||||
|
||||
## PHASE 4 — drive operating mode -> ONLINE
|
||||
- Android: NAS decides "needs to be changed" (phase 1) and sets the modem ONLINE; subsequent
|
||||
state shows `is_online 1` (`qcril-online-capture.txt`, qcril_qmi_nas.cpp:26131).
|
||||
- pmOS: this is exactly the `--dms-set-operating-mode=online` we fire — returns **52** because we
|
||||
start from OFFLINE (phase 1), not LOW_POWER.
|
||||
|
||||
## PHASE 5 — NAS attach / network registration (full QMI choreography)
|
||||
- Module/QMI send order (from `qcril-ssr-TIMELINE.txt` section A): Nas, Pdc, Voice, DSD, Auth, WDS,
|
||||
Pbm, Sms, Vs — dozens of NasModem sends, all rc=0. This is the post-online network attach.
|
||||
|
||||
## PHASE 6 — IN_SERVICE (the goal)
|
||||
- `combinePsRegistrationStates: mVoiceRegState=0(IN_SERVICE), mDataRegState=0(IN_SERVICE) …
|
||||
getRilVoiceRadioTechnology=14(LTE) … mOperatorAlphaLong=Fido` (`qcril-online-capture.txt:24110`).
|
||||
- CS domain: VOICE,SMS,VIDEO; PS domain: DATA,MMS; registrationState=HOME; LTE EARFCN 2000, Fido
|
||||
302/720 (`qcril-online-capture.txt:24107-24132`). IMS registered (`:24083`).
|
||||
|
||||
---
|
||||
|
||||
## What "the whole job" actually is (so it stops getting half-done)
|
||||
1. **Phase 0** — fix the secure-load so the modem boots HEALTHY (the -22 / XPU). Not the skip.
|
||||
2. **Phase 1** — get the modem to come up at LOW_POWER, not OFFLINE. Likely a consequence of (1)
|
||||
and/or EFS/rmtfs provisioning. This is the current wall.
|
||||
3. **Phase 3** — replicate Android's PDC MBN carrier-config selection (qcril-autoselect provisioner).
|
||||
4. **Phase 4** — only then does `set-operating-mode=online` succeed instead of 52.
|
||||
Doing (4) without (1)-(3) is why every "flash and test" returns 52. All four are required.
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
# HANDOFF — Put the Pixel 3 audio to bed (Fable, evening 2026-07-19)
|
||||
|
||||
> Historical handoff. The current contract is `../../audio.md`; this file is
|
||||
> retained as dated evidence and may describe work that was later completed.
|
||||
|
||||
We are *this close*. Mic captures nonzero on every boot now, both loudspeakers
|
||||
fire, modem connects. What's left: reinstate PipeWire (the reason it was banned
|
||||
is gone), keep the mic/speaker gains, and make the phone **call-ready**
|
||||
(two-way call audio). This doc is your orders, not a draft to critique.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ READ THIS FIRST — the references are ALL local, and they are VAST
|
||||
|
||||
Casey's #1 instruction for this handoff: **stop and internalize that nearly
|
||||
every fact you could want is already on this drive** — every bring-up attempt,
|
||||
every Android LIVE capture, every modem trace, every regmap dump, the full
|
||||
Android vendor mixer map, the pmOS known-good config, the raw partition images.
|
||||
Even the outgoing session (me) *barely* knew this reference surface existed and
|
||||
wasted hours re-deriving things that were sitting in a file. **Do not repeat
|
||||
that.** Before you form any theory or run any experiment, grep/read the local
|
||||
references. If you catch yourself reverse-engineering ALSA/UCM/modem internals
|
||||
from scratch, you have already failed — go find the file.
|
||||
|
||||
### The reference surface (4 repos, ~70 GB, all local)
|
||||
|
||||
**1. `~/Projects/Pixel3Arch/PAF/` — the then-current base of truth.** Doctrine + current state.
|
||||
- `audio.md` — the entire audio saga. THE ALTERNATION LAW, THE BOOT RACE,
|
||||
speaker routing provenance, mic transport. Read top-to-bottom.
|
||||
- `audio-2026-07-19-LIVE-reconciliation.md` — **supersedes the top of
|
||||
audio.md.** LIVE Android register captures (recorder vs voice call),
|
||||
the ADM-usecase hypothesis, and the **airplane/radio-mode wedge warning**
|
||||
(some historical "wedges" were mode toggles, not driver faults — rule
|
||||
mode out before logging any wedge).
|
||||
- `HANDOFF-mic-unpin.md` — the un-pin experiment (corrected header; read
|
||||
audio.md "THE BOOT RACE" first).
|
||||
- `modem.md`, `pmos_fix_plan.md`, `qcril_*_capture.md`,
|
||||
`modem_dt_rf_analysis_*.md`, `tz_listener_archaeology.md`,
|
||||
`secure_load_tz_frontier.md`, `qrtr_tools_reference.md`,
|
||||
`tier1_los_vs_mainline_diff.md` — the **modem bring-up archaeology**.
|
||||
Deep. Detailed. Read before touching the modem.
|
||||
- `evidence/`, `captures/` — raw traces (e.g. glink).
|
||||
|
||||
**2. `~/Projects/PostMarketOS-Blueline/` — the pmOS port + all Android captures.**
|
||||
- `android-reference/` — **the gold.** `mixer_paths_tavil_b1.xml` (the FULL
|
||||
Android vendor route map — every named route: `handset`, `speaker`,
|
||||
`voice-handset`, `voice-speaker`, all mics), CS35L36 regmaps
|
||||
(idle/playing), tavil PGD dumps, `wcd9xxx_core-android-LIVE-*.txt`
|
||||
(recording AND voicecall, captured 2026-07-19), `wcd934x-android-ground-truth.md`.
|
||||
- `android-capture-2026*` — nine per-day Android trace dirs.
|
||||
- `PAF/` — a second full modem-trace tree (`android_modem_bringup_capture.md`,
|
||||
`qcril_full_provisioning.md`, `qcril_online_sequence.md`,
|
||||
`modem_consumer_chain_*.md`, `los-mss-translated.c`).
|
||||
- `HANDOFF.md` (80 KB), `PROGRESS.md`, `START-HERE.md`, `README.md`.
|
||||
|
||||
**3. `~/Projects/pixel3-downstream-audio/` — the Android HAL source.**
|
||||
`techpack/audio/` — downstream `q6adm.c`, `q6voice*.c`, ASoC machine driver.
|
||||
This is what the working Android side actually runs; diff mainline behavior
|
||||
against it when a route/topology question comes up.
|
||||
|
||||
**4. `~/pixel3-backup/` (in $HOME, not Projects) — raw partition dumps + pmOS config.**
|
||||
`modem_a.img`, `persist.img`, `vendor_a.img`, `system_a.img` (raw Android),
|
||||
plus `pmos-20260703/` — **the known-good pmOS config backup** the speaker
|
||||
routing was lifted from (see the memory note: this is the source of truth for
|
||||
working modem/cellular + speaker config; restore from here, never guess).
|
||||
|
||||
If a route value, a mixer control, a modem sequence, or a UCM verb is in
|
||||
question — it is decoded in one of the above. Cite `file:line`. Do not invent.
|
||||
|
||||
---
|
||||
|
||||
## CURRENT STATE (verified live this session, phone at WiFi `10.10.20.234`, USB `172.16.42.1`)
|
||||
|
||||
**Kernel:** `7.1.1-sdm845-gcd7758eaeca1` (mic-race-fix, HEAD). Boot-race fix
|
||||
LIVE and working — `MICDBG boot sysclk hold armed` fires in dmesg, capture open
|
||||
joins a live codec (`users=2 skip`) instead of racing bring-up.
|
||||
|
||||
**Mic — WORKING.** `parecord --device=blueline_mic` yields nonzero PCM every
|
||||
boot. Deterministic now (kernel sysclk hold from `wcd934x_comp_probe`).
|
||||
|
||||
**Speakers — WORKING.** Both CS35L36 amps armed, correct route (measured live):
|
||||
`Left/Right AMP Enable = on`, `BOOST = on`, `PCM Volume = 700`,
|
||||
`Left Channel Mux = RX1` (top), `Right Channel Mux = RX2` (bottom). Matches the
|
||||
07-12/07-13 known-good. The `handset` (earpiece) vs `speaker` distinction is a
|
||||
**single control**: `Right AMP Enable` — Android's `handset` route runs the top
|
||||
amp only (`R AMP Enable=0`, low gain); `speaker` runs both. Decoded from
|
||||
`mixer_paths_tavil_b1.xml`.
|
||||
|
||||
**Modem — connects.** Registered (Fido, MNC 720). Call *signaling* works. Note:
|
||||
saw a transient roaming reject (MNC 490, `roaming-not-allowed`) mid-session —
|
||||
that's network, not us, and consistent with the reconciliation doc's mode/radio
|
||||
caveat. Rule mode out before chasing modem "faults."
|
||||
|
||||
**Calls — NO AUDIO yet.** `q6voiced` is now `enabled + running` holding
|
||||
`hw:0,2` (the live VoiceMMode1 PCM — NOT device 5 as the staged conf wrongly
|
||||
said; fixed this session in both `/etc/q6voiced.conf` on-device and
|
||||
`pkgs/q6voiced/q6voiced.conf`). But it logs `Failed to open tx/rx: Invalid
|
||||
argument` on call-start because nothing applies the VoiceCall route. See below.
|
||||
|
||||
---
|
||||
|
||||
## THE TWO HOLDS ON THE MIC (know the difference)
|
||||
|
||||
1. **PA loopback pin** (`90-blueline-alsa.pa`, modules 20/21): null-sink +
|
||||
`module-loopback source=blueline_mic ... source_dont_move sink_dont_move`.
|
||||
Holds the mic open from boot. **Nobody else does this** — verified via GitHub
|
||||
code search: no working sdm845 phone (OnePlus 6 enchilada/fajita, holioka)
|
||||
pins the mic. It's a boot-race workaround.
|
||||
2. **Kernel sysclk hold** (today's fix, in `wcd934x_comp_probe`): permanent
|
||||
sysclk ref + MICB2 bias from probe. Honest hack — pins the winning-boot
|
||||
state so every boot wins.
|
||||
|
||||
**With the kernel hold in place, the PA pin is very likely now redundant.** The
|
||||
un-pin test was never run on a race-won boot. Running it de-hacks the daily
|
||||
driver. See `HANDOFF-mic-unpin.md` for the exact procedure (unload loopback,
|
||||
same-map reopen ×2, watch for `CHAN_CTRL remove ret=0` + fresh `DEF_ACT_CHAN`).
|
||||
If reopen is nonzero → delete the PA pin, keep the kernel hold. If zero → keep
|
||||
both, root-cause stays open.
|
||||
|
||||
---
|
||||
|
||||
## PIPEWIRE — the ban is lifted; bring it back
|
||||
|
||||
**Why it was removed (`da53133`):** the *only* documented reason was the
|
||||
**premature capture source** — wireplumber published the mic before ALSA
|
||||
capture was nonzero (the boot race). PAF audio.md:516/590. PipeWire *playback*
|
||||
was never the problem — `8920c3c` had PW playing on glass fine once the sink
|
||||
was pinned S16LE + RW + no-mmap (the DSP lies about S24/mmap; both play silent).
|
||||
|
||||
**Why it's safe now:** the boot race is fixed in-kernel — **capture is nonzero
|
||||
from boot, deterministically.** The precondition PipeWire failed on is
|
||||
satisfied. Casey has explicitly cleared reinstating it.
|
||||
|
||||
### PipeWire task
|
||||
- Reinstall the full PipeWire + WirePlumber stack. Note the current shims:
|
||||
`pkgs/pipewire-abi-provider`, `pkgs/pulse-native-provider` (they provide the
|
||||
PW ABI *without* a server — you're replacing that arrangement with a real
|
||||
server, so reconcile/remove the shims cleanly, don't leave a half state).
|
||||
- **Re-apply the S16LE playback rule** (the wireplumber
|
||||
`51-blueline-audio-s16.conf` that `8920c3c` created and `da53133` deleted —
|
||||
recover it from git: `git show 8920c3c`). Without it, playback is silent.
|
||||
- Verify on the fixed kernel, in this order, and DO NOT proceed past a failure:
|
||||
1. mic still nonzero via PW (`pw-record`) — this is the thing PW broke before;
|
||||
2. speakers play via PW (`pw-play` a chime) — both top+bottom;
|
||||
3. the mic source is NOT published dead at boot (the old failure mode).
|
||||
- Keep the new features intact: the QS audio service fix
|
||||
(`Audio.qml` — `blueline_mic` description matcher), the mic/speaker gains,
|
||||
the speaker Channel Mux routing.
|
||||
|
||||
**Ponytail guard:** PipeWire is the bigger hammer. Its payoff is that
|
||||
**callaudiod can then route calls via UCM** — which is the whole reason we want
|
||||
it back. If PW re-breaks the mic despite the race fix, fall back to the raw-PA
|
||||
call hook (below) rather than fighting it — the mic is non-negotiable.
|
||||
|
||||
---
|
||||
|
||||
## MAKE IT CALL-READY (the actual finish line)
|
||||
|
||||
The working sdm845 reference (OnePlus 6, `chayleaf/dotfiles`
|
||||
`system/hardware/oneplus-enchilada/default.nix`, and `syntheit/nix`
|
||||
`hosts/fajita`) routes call audio through **UCM (consumed by PipeWire) +
|
||||
callaudiod + q6voiced tied to ModemManager**. Key facts pulled from it:
|
||||
- `q6voiced hw:0,6` there (per-board PCM number; **ours is `hw:0,2`** — always
|
||||
read `/proc/asound/pcm`, never copy the number).
|
||||
- q6voiced is `After=`/`wantedBy=ModemManager.service`.
|
||||
- `alsa-ucm-conf` is shipped and `alsa-ucm-meld = true` — **UCM is the routing
|
||||
layer.** callaudiod flips the verb on call state; q6voiced carries the audio.
|
||||
|
||||
### The call chain we need
|
||||
`gnome-calls → callaudiod → UCM VoiceCall verb → q6voiced carries modem audio`
|
||||
|
||||
Both middle links are currently broken/absent on our raw-PA setup:
|
||||
- **callaudiod** is installed and running but has zero log activity — it routes
|
||||
via PipeWire ports/profiles, which don't exist under raw-PA. **PipeWire back
|
||||
gives callaudiod something to route with.** This is the clean path and the
|
||||
reason PipeWire is worth the risk.
|
||||
- **UCM VoiceCall verb** exists (`Google/blueline/VoiceCall.conf`) but has the
|
||||
**same `hw:0,5` bug** (`PlaybackPCM/CapturePCM "hw:${CardId},5"`) — fix to
|
||||
`,2`. Its EnableSequence is the real voice route:
|
||||
`SLIMBUS_0_RX Voice Mixer VoiceMMode1=1`,
|
||||
`VoiceMMode1 Capture Mixer SLIMBUS_0_TX=1`. Cross-check the RX/TX and DMIC/DEC
|
||||
selections against `mixer_paths_tavil_b1.xml` `voice-handset`/`voice-speaker`
|
||||
and the downstream `q6voice*.c` before trusting them.
|
||||
- **UCM currently fails to load via `alsaucm -c G3`** (`-2`). Under raw-PA this
|
||||
is moot (PA doesn't consume UCM). Once PipeWire+wireplumber drive UCM this
|
||||
must actually resolve — verify wireplumber opens the card's UCM cleanly.
|
||||
(The card driver/id: `id=G3`, longname `Google Pixel 3`, ASoC machine
|
||||
`msm-snd-sdm845`; conf.d lives in `conf.d/sdm845/`. If UCM won't match, this
|
||||
driver-name-vs-dir mapping is the first suspect — but let a real UCM client,
|
||||
not the bare CLI, be your test.)
|
||||
|
||||
### Definition of done — ALL MET 2026-07-29
|
||||
1. PipeWire back, mic + both speakers verified on the fixed kernel. **Done
|
||||
2026-07-19.**
|
||||
2. Place a real call (Casey → his wife). **Two-way audio.** Downlink out the
|
||||
speaker, uplink from the mic, on the call. **Done 2026-07-29** — incoming
|
||||
call answered, both directions confirmed by ear at both ends. Five defects
|
||||
had to fall first; see `PAF/audio.md`, 2026-07-29.
|
||||
3. On hangup, audio reverts to normal HiFi (no stuck voice route). **Done** —
|
||||
and note the reverse was silently broken: the mic read exact zeros after
|
||||
*every* call until the outgoing capture leg was unmapped explicitly.
|
||||
4. q6voiced device number correct (`hw:0,2`) in every place it's written.
|
||||
**Done.**
|
||||
5. New features + gains preserved. Mic still nonzero every boot. **Done** —
|
||||
~90k nonzero frames verified repeatedly across the session.
|
||||
|
||||
Not in the original list, completed 2026-08-02: the earpiece/speaker executor
|
||||
(`blueline-callaudio-hook speaker on|off`) is driven by the patched
|
||||
`souveraine-callaudiod`; see `../../audio.md` and TASK-39.
|
||||
|
||||
### Fallback (if PipeWire re-breaks the mic)
|
||||
Raw-PA call hook: a small ModemManager call-state trigger that applies the
|
||||
VoiceCall mixer controls directly via `amixer`/`alsaucm cset` on call-start and
|
||||
reverts on hangup. ~15 lines. No server swap, zero risk to the working mic.
|
||||
Smaller diff, fewer niceties. Only take this if PipeWire loses the mic.
|
||||
|
||||
---
|
||||
|
||||
## RULES OF ENGAGEMENT
|
||||
- Base of truth = PAF. Every reference is local (the 4 repos above). **Read
|
||||
before theorizing.** Cite `file:line` or say "not in the code."
|
||||
- Rule out airplane/radio mode before logging any modem/audio "wedge."
|
||||
- The mic is the crown jewel. Never trade it away for a nicety.
|
||||
- Verify claims live on the phone; don't assert kernel/route state from memory.
|
||||
- The doctrine holds: build the finished thing, not a v1. This is the last mile.
|
||||
114
saf/device/evidence/audio/HANDOFF-mic-unpin.md
Normal file
114
saf/device/evidence/audio/HANDOFF-mic-unpin.md
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# Handoff — Pixel 3 mic: un-pin the microphone
|
||||
|
||||
> Historical handoff. Current functional status and the remaining power debt
|
||||
> live in `../../audio.md` and `../../../docs/tasks/07-mic-wcd9340-slim-tx.md`.
|
||||
|
||||
> **CORRECTED 2026-07-19 evening — read audio.md "THE BOOT RACE" first.**
|
||||
> The QRTR=y contamination theory below is disproven: all of today's dead-mic
|
||||
> boots (every kernel, every config) lost a boot-time sysclk race; the working
|
||||
> 262k boot and one untested bdf098937 boot won it. bdf098937 is clean and
|
||||
> deployed; the un-pin test below is still valid but ONLY on a boot whose
|
||||
> pinned baseline reads nonzero. Cold boot until baseline is nonzero, then run
|
||||
> the test as written.
|
||||
|
||||
## The one task
|
||||
Fable got the mic **working** via a PA mic-hold that pins `blueline_mic` open
|
||||
from boot (`rootfs-overlay/etc/pulse/default.pa.d/90-blueline-alsa.pa`, the
|
||||
`module-loopback` into a null sink). The **only** job left: make the mic
|
||||
survive close/reopen so the always-on pin can be removed. Nothing else. Do not
|
||||
expand scope.
|
||||
|
||||
## Known-good state (trust this, do not re-derive)
|
||||
- Working-mic kernel: **`323b29715`** (branch `mic-fix`, gitea
|
||||
`Fimeg/linux-blueline`). MCLK root cause already fixed upstream of it
|
||||
(LN_BB_CLK3, commit `7c6559e0c`). With the PA pin loaded, capture is real
|
||||
audio: ~262k/263k nonzero samples/3s, verified this morning.
|
||||
- The un-pin attempt: **`bdf098937`** = `323b29715` + one commit only
|
||||
("slimbus: qcom-ngd: send CHAN_CTRL remove on stream disable"). Its parent
|
||||
IS the working kernel, so `bdf098937` is exactly "working mic + un-pin,
|
||||
nothing else."
|
||||
|
||||
## What `bdf098937` does (already reviewed, wire format is correct)
|
||||
- `drivers/slimbus/qcom-ngd-ctrl.c`: adds `qcom_slim_ngd_disable_stream` →
|
||||
sends `CHAN_CTRL(REMOVE)` + `RECONFIG_NOW` to the ADSP master on stream
|
||||
disable. Mainline had dropped all 0x40–0x5F reconfig messages, so the master
|
||||
kept the channel "active" after close and a same-map reopen no-op'd → zombie
|
||||
zeros. Wire format matches downstream `slim-msm-ngd.c` exactly:
|
||||
`(SLIM_CH_REMOVE<<6)|(laddr&0x1f)`, TID, chan ids, `mc=SLIM_USR_MC_CHAN_CTRL`.
|
||||
- `sound/soc/codecs/wcd934x.c`: swaps teardown order to `disable` **before**
|
||||
`unprepare` (unprepare frees `stream->ports`, which made disable a silent
|
||||
`-EINVAL` no-op).
|
||||
- On-phone evidence it reached the wire: `MICDBG CHAN_CTRL remove ... ret=0`,
|
||||
GENERIC_ACK code 0x20. So the removal is sent and ACKed.
|
||||
|
||||
## The actual open question
|
||||
`bdf098937` has **never had a clean test.** Every observation today was
|
||||
contaminated:
|
||||
1. Tested with the PA pin still loaded → `disable` never fires (0 shutdowns in
|
||||
dmesg), so the new code path never ran.
|
||||
2. Then it got built ON TOP of a QRTR/PD_MAPPER `=y` config change into
|
||||
`fdb22e5` (see contamination below), and every "still zero" result was on
|
||||
that kernel, not on clean `bdf098937`.
|
||||
|
||||
So it is unknown whether the CHAN_CTRL-remove actually un-pins the mic. That is
|
||||
the thing to find out.
|
||||
|
||||
## Contamination to be aware of (my mess — Opus, this session)
|
||||
- Commit `fdb22e5` on `mic-fix` flips `CONFIG_QRTR=y` + `CONFIG_QCOM_PD_MAPPER=y`
|
||||
(was `=m`/`=m`). This is a **boot-time change** stacked on top of the un-pin
|
||||
commit and it correlates with the mic going dead. Whether QRTR=y is causally
|
||||
breaking the mic or is a red herring is NOT established — but it must NOT be
|
||||
in the kernel used to test the un-pin. Ruled out as a probe-race: on
|
||||
`fdb22e5`, audio_pd came UP at 1.3s, codec armed capture at 13.5s (12s later,
|
||||
no race). `0x1fffffff` = SERVREG_SERVICE_STATE_UP (I first misread it as
|
||||
down — it is UP).
|
||||
- The repo config `kernel/config-blueline.aarch64` currently has QRTR/PD_MAPPER
|
||||
`=y` (my commit `f245323` in `Fimeg/Pixel3Arch`). For a clean un-pin test the
|
||||
build config must be QRTR=m/PD_MAPPER=m (the working-mic config).
|
||||
- Phone is currently running the contaminated `gfdb22e5`; mic reads zero.
|
||||
- ArchDev `~/build/pixel3arch-kernel` was hand-abused with direct `sed` +
|
||||
`makepkg` (NOT the pipeline). Ignore that dir. `config-blueline.aarch64`
|
||||
there was left at `=y`.
|
||||
|
||||
## The correct next step (build ONE kernel, via the pipeline)
|
||||
Build `bdf098937` with the working-mic config (QRTR=m, PD_MAPPER=m), clean, and
|
||||
test the un-pin. Concretely:
|
||||
1. Point the build at commit `bdf098937` (its config, in `Fimeg/Pixel3Arch`,
|
||||
must be QRTR=m/PD_MAPPER=m — the pre-`f245323` values).
|
||||
2. Build through the real pipeline only: `~/Projects/Pixel3Arch/kernel` on
|
||||
ArchDev, `./build.sh <branch-at-bdf098937>` (see PAF/build.md — it
|
||||
fresh-clones from gitea and hard-asserts built hash == gitea tip; never
|
||||
hand-makepkg, never the `~/build` side dir).
|
||||
3. Deploy with `scripts/deploy-kernel.sh` (modules-first, refuses to bless a
|
||||
boot where `uname -r` != installed module tree — PAF/build.md doctrine).
|
||||
4. Cold boot (true power-off, unplug — no warm reboot; warm reboots poison
|
||||
TZ/remoteproc on blueline).
|
||||
|
||||
## The test that answers it
|
||||
On the clean `bdf098937` boot:
|
||||
1. Confirm mic works pinned: `parecord --device=blueline_mic` → nonzero
|
||||
(baseline sanity; pin is loaded from boot).
|
||||
2. `pactl unload-module module-loopback` (drop the pin → the source can now
|
||||
actually close).
|
||||
3. `parecord` again, close it, `parecord` a second time on the SAME TX5 map.
|
||||
4. Watch `dmesg | grep MICDBG` for `shutdown` + `CHAN_CTRL remove ret=0` on the
|
||||
close, then a fresh `DEF_ACT_CHAN` on the reopen.
|
||||
5. **Success = the second/third reopen reads nonzero.** That means CHAN_CTRL
|
||||
remove re-armed the path and the pin can be deleted from
|
||||
`90-blueline-alsa.pa`.
|
||||
6. If it still zeros: the un-pin theory needs more than channel-remove. Next
|
||||
thing to check is whether the **port** (not just the channel) needs
|
||||
explicit disconnect/reconnect — downstream `slim-msm-ngd.c` handles
|
||||
`DISCONNECT_PORT` (mainline does it in `slim_stream_unprepare` via
|
||||
`slim_disconnect_port`); verify it actually fires on close in dmesg, and
|
||||
whether a port re-CONNECT is needed on reopen. Downstream source is local
|
||||
at `~/Projects/pixel3-downstream-audio/drivers/slimbus/slim-msm-ngd.c`
|
||||
(git: `git show HEAD:drivers/slimbus/slim-msm-ngd.c`).
|
||||
|
||||
## Do NOT
|
||||
- Do not roll back / chase old commits beyond `bdf098937`.
|
||||
- Do not remove QRTR/PD_MAPPER as the permanent config — pd-mapper being
|
||||
built-in is wanted; it just must not be in the *un-pin test* kernel.
|
||||
- Do not hand-`sed` configs on ArchDev or hand-`makepkg`. One change, in the
|
||||
repo, pushed, built by `build.sh`.
|
||||
- Do not tear down the working pin except as step 2 of the test above.
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
# 2026-07-19 LIVE Android reconciliation — READ THIS FIRST (supersedes the top of audio.md)
|
||||
|
||||
> Historical capture. The reconciled current contract is `../../audio.md`.
|
||||
|
||||
The `## 0x097/0x032/0x11c are 00 on a WORKING recording` section currently at the
|
||||
top of `audio.md` is **partly WRONG** — it was written after reading only the
|
||||
regmap-cache node. A second live read minutes later (same confirmed-active
|
||||
recording, user watching the phone) via the live-PGD node corrected it. This file
|
||||
is the accurate version. Trust this, not that section.
|
||||
|
||||
## ⭐ VOICE-CALL reconciliation — 0x097 is voice-usecase-specific (the biggest finding)
|
||||
|
||||
Captured the working Android side LIVE via the live node
|
||||
(`/sys/kernel/debug/wcd9xxx_core/slimslave_reg_dump`) under TWO different working
|
||||
usecases, both user-confirmed-live (timer / wife on the call):
|
||||
|
||||
```
|
||||
recorder capture VOICE CALL
|
||||
0x032 irq_en_TX 0x80 0x80
|
||||
0x057 TX7 port cfg 0x05 0x05
|
||||
0x097 TX7 region 00 0x21 <-- voice ONLY
|
||||
0x098 TX7 region 00 0x21 <-- voice ONLY
|
||||
0x11c TX7 MULTI_CHNL 0x80 0x80
|
||||
0x120 TX8 MULTI_CHNL 00 0x80 <-- voice ONLY (2nd mic, dual-mic)
|
||||
```
|
||||
|
||||
Saved: `android-reference/wcd9xxx_core-android-LIVE-voicecall-20260719.txt`
|
||||
(plus `...-LIVE-recording-20260719.txt`).
|
||||
|
||||
**THE RECONCILIATION:** the 2026-07-03 ground-truth doc's `0x097=0x21` was RIGHT
|
||||
— it was captured during a **voice call** (dual-mic handset+endfire, TX7+TX8), not
|
||||
a multimedia recorder capture. The recorder path never sets `0x097/0x098` or TX8's
|
||||
`0x120`. So `0x097` was never a "samples flow for multimedia capture" register —
|
||||
it's set by the **voice usecase's** ADM topology (dual-mic, AANC/fluence).
|
||||
**Chasing `0x097` for the recorder/multimedia path was a category error.**
|
||||
|
||||
This is **strong evidence for the ADM/usecase hypothesis** (see below): the working
|
||||
Android side programs different DSP topology per usecase (recorder = single TX7
|
||||
multimedia; voice = dual TX7+TX8 with AANC). `0x097/0x098` + TX8 `0x120` are the
|
||||
voice-topology footprint.
|
||||
|
||||
**For our broken multimedia mic:** our `MultiMedia1/2` capture on Arch corresponds
|
||||
to Android's **recorder** path. For THAT path the working registers are
|
||||
`0x032=0x80, 0x057=0x05, 0x11c=0x80` only — which we MATCH. So the multimedia-
|
||||
capture register layer genuinely matches working↔broken. The bug for multimedia is
|
||||
NOT in IFD/PGD registers (now confirmed for the recorder usecase specifically).
|
||||
The voice path (`VoiceMMode1`/`hw:0,2`) is a separate, untested question — and if
|
||||
voice is what's broken, `0x097/0x098/0x120` become relevant again, but as voice
|
||||
topology, not as a general "samples flow" reg.
|
||||
|
||||
Do NOT chase `0x097` for the multimedia/recorder mic.
|
||||
|
||||
## What actually happened
|
||||
|
||||
Two debug nodes on Android (LineageOS 22, slot B) read **LIVE during a
|
||||
confirmed-active recording** (LineageOS Recorder, timer counting up, user at the
|
||||
phone said "recording"):
|
||||
|
||||
```
|
||||
regmap/tavil-slim-pgd/registers wcd9xxx_core/slimslave_reg_dump
|
||||
(regmap CACHE — reads 00/stale) (live PGD read over SLIMbus)
|
||||
0x032 PGD irq_en_TX 00 0x80
|
||||
0x057 TX7 port cfg 00 0x05
|
||||
0x097 "mystery" TX7 reg 00 00
|
||||
0x11c TX7 MULTI_CHNL_0 00 0x80
|
||||
```
|
||||
|
||||
**The two nodes read different things.** `regmap/.../registers` reads the regmap
|
||||
CACHE (all 00, not live). `wcd9xxx_core/slimslave_reg_dump` reads the live PGD and
|
||||
shows the real recording values.
|
||||
|
||||
## What this means (correct conclusions)
|
||||
|
||||
1. **The original 2026-07-03 ground-truth doc was RIGHT about 0x032/0x057/0x11c**
|
||||
(it used `wcd9xxx_core`, the live node): working recording = `0x032=0x80`,
|
||||
`0x057=0x05`, `0x11c=0x80`. Those are real.
|
||||
|
||||
2. **The doc's `0x097=0x21` claim was RIGHT — but it was a VOICE call, not a
|
||||
recorder capture.** See the ⭐ VOICE-CALL section above: `0x097/0x098=0x21` and
|
||||
TX8 `0x120=0x80` are voice-usecase-specific. They are `00` during a recorder
|
||||
capture (which is the usecase our multimedia mic maps to). So `0x097` is NOT a
|
||||
multimedia-capture register at all — stop chasing it for the recorder path.
|
||||
|
||||
3. **Our side (Arch / SouveraineOS) matches the working side on
|
||||
0x032/0x057/0x11c** (measured this session after the MCLK fix: our IFD shows
|
||||
`0x032=0x80`, `0x057=0x05`, `0x11c=0x80`). So the IFD/PGD register layer
|
||||
genuinely matches between working Android and our broken capture.
|
||||
**The differentiator is NOT in the IFD/PGD register layer.**
|
||||
|
||||
4. **Methodological correction for our side:** when reading PGD regs, do NOT
|
||||
trust `regmap/.../registers` (it's the cache — lies with 00s). Use the live
|
||||
equivalent. Several "this reg is never set" conclusions on our side this
|
||||
session were probably cache-read artifacts, not real states.
|
||||
|
||||
## Saved artifacts (both LIVE, same recording, 2026-07-19)
|
||||
- `PostMarketOS-Blueline/android-reference/tavil-pgd-android-LIVE-recording-20260719.txt`
|
||||
(regmap cache view — the misleading 00s)
|
||||
- `PostMarketOS-Blueline/android-reference/wcd9xxx_core-android-LIVE-recording-20260719.txt`
|
||||
(live PGD — the real recording state)
|
||||
|
||||
## Verified-correct stack as of 2026-07-19 (live-confirmed)
|
||||
- MCLK to codec: FIXED (LN_BB_CLK2→LN_BB_CLK3 in `sdm845-google-common.dtsi`
|
||||
`&wcd9340` override, uncommitted; live on DTB). eFuse sense completes
|
||||
(`status=0x01`, version `0x401`) — no more `-110`.
|
||||
- Codec init/capture regs: bit-for-bit identical to Android ground truth.
|
||||
- Codec DAPM power-up: fires (`0x0601/0x0626/0x0aa1` toggle on capture).
|
||||
- SLIMbus NGD DEF_ACT_CHAN: sent (qcom-ngd-ctrl.c:1020).
|
||||
- AFE slave-config send (`AFE_PARAM_ID_CDC_SLIMBUS_SLAVE_CFG` + REG_PAGE +
|
||||
REG_INIT): already built in mainline as `q6afe_send_cdc_codec_config()`
|
||||
(q6afe.c:1854), sent with correct codec enum addr `ea=0x21702500100`, `ret=0`.
|
||||
DSP accepts (no "DSP returned error"). **A prior session already built the
|
||||
"port the missing slave-config" fix — do NOT re-derive/re-port it.**
|
||||
- IFD/PGD regs (0x032/0x057/0x11c): MATCH between working and broken (via live
|
||||
node). `0x097`: 00 on both — ruled out.
|
||||
- ADSP file-serving: PD requests only `adsp_avs_config.acdb`, served. OEM ACDB
|
||||
tree not requested via FastRPC — not the gap (falsified).
|
||||
|
||||
## Where the differentiator actually is (still open)
|
||||
Everything the kernel can observe/send/serve — codec regs, SLIMbus NGD, AFE
|
||||
command ack, IFD/PGD regs, file-serving — is identical between working Android
|
||||
and our broken capture. The fault is past all of those.
|
||||
|
||||
**Top hypothesis (NOT yet tested) — ADM usecase routing, not AFE:**
|
||||
On Android, opening the Recorder app goes through the Android audio HAL:
|
||||
`adev_open_input_stream` → platform HAL → `audio_extn` → **`adm_open` with a
|
||||
usecase + device + flags**, which programs the ADM (Audio Dynamic Mixer) topology
|
||||
on the DSP that actually routes capture data into the AFE port. Without an ADM
|
||||
usecase the DSP has an open AFE port with nothing routed into it → samples never
|
||||
flow, every command ACKs. That fits the exact signature.
|
||||
|
||||
**We are Arch / SouveraineOS — there is no Android audio HAL in our stack at
|
||||
all.** We drive ALSA directly (PulseAudio → ALSA `hw:0,0`). No userspace ever
|
||||
calls `adm_open`. No usecase is ever set. Either the kernel does it or it
|
||||
doesn't — and mainline's `q6adm.c` is much thinner than downstream's
|
||||
(`pixel3-downstream-audio/techpack/audio/dsp/q6adm.c`).
|
||||
**This is the unexplored-at-this-depth difference, unlike the AFE slave-config
|
||||
which was already ported.**
|
||||
|
||||
Concrete next move on our side: check whether our kernel/driver has a `q6adm`
|
||||
path wired for capture usecases, and whether opening capture sends ANY
|
||||
`ADM_CMD_ASM` / ADM commands at all. If zero — that's the gap.
|
||||
|
||||
Secondary candidates (also not falsified):
|
||||
- **AFE/APR command sequence on the WIRE (payload bytes), not register state.**
|
||||
No payload-bearing APR capture exists on either side. The one glink trace
|
||||
(`PAF/captures/glink_trace_pmos-20260702.txt`) is framing-only AND modem-context
|
||||
(qmi-proxy traffic), not a mic capture — no AFE opcodes. The Android side has
|
||||
`msm_apr_debug` + `ipc_logging/apr` nodes but they returned empty even during a
|
||||
live recording on this LOS build (APR packet logging not enabled in this image).
|
||||
Getting a payload-bearing APR trace needs either a different LOS build with APR
|
||||
debug enabled, or a kernel tracepoint patch on our side.
|
||||
- On Android, APR debug nodes exist but were empty live: `/sys/kernel/debug/
|
||||
msm_apr_debug` (0 bytes), `/sys/kernel/debug/ipc_logging/apr/{log,log_cont}`
|
||||
(0 bytes even while recording). Glink lpass edge was moving real bytes
|
||||
(TX/RX fifo nonzero) — traffic flows, just not logged at APR layer on this image.
|
||||
- Android's silent/vibrate/sound/airplane modes are audio-policy/stream-mute
|
||||
states, NOT hardware triggers — but they shape WHICH ADM usecase the HAL opens,
|
||||
which is why the usecase-routing question matters.
|
||||
|
||||
## System identity (don't mislabel — this was called "pmOS" by mistake earlier)
|
||||
- **Our phone runs Arch / SouveraineOS**, NOT pmOS. pmOS is only the historical
|
||||
lineage of the kernel/DTB. The running userspace is Arch + SouveraineOS shell
|
||||
+ PulseAudio driving ALSA directly. No Android audio HAL, no `audio_extn`.
|
||||
- Android/LineageOS reference is slot B (boots fine; user was in it this session
|
||||
recording). The mic works there.
|
||||
|
||||
## ⚠️ METHODOLOGICAL WARNING — some historical "wedges" were a real radio/mode switch, not a driver fault
|
||||
Casey flagged 2026-07-19: **at least some of the recurring "wedge" / "seizure" /
|
||||
"the modem grabbed the card" / "SLIM -110 wedge" / "capture wedge" / "chip-wide
|
||||
wedge" events in the audio/mic/modem docs were NOT kernel/codec/SLIM faults at
|
||||
all — they were the phone having switched to airplane mode (or an equivalent
|
||||
radio-off / audio-policy-reroute state), and the resulting silence / dead bus /
|
||||
NACK was misread as a driver-level hang.**
|
||||
|
||||
This means:
|
||||
- Some "wedge" investigations across multiple sessions may have been chasing a
|
||||
mode toggle, not a bug. Treat any historical "wedge" / "seizure" / "poison"
|
||||
conclusion as suspect until reconciled against the mode/radio state at the time.
|
||||
- We do NOT know exactly which wedges were real faults vs mode swaps. Both kinds
|
||||
exist. Don't assume all wedges were phantom — and don't assume all were real.
|
||||
- **BEFORE logging a wedge / NACK / seizure / dead-bus / silence on our side**,
|
||||
check and record: is the phone in airplane mode? Is the modem/radio off (check
|
||||
rfkill, modem state, ofono/ModemManager, the cellular/quickshell overlay)?
|
||||
Is an audio policy rerouting happening? Rule the mode out FIRST, every time.
|
||||
- On Arch/SouveraineOS a radio-off / airplane-equivalent state may be less
|
||||
obvious than Android's UI toggle (a Souveraine service, the cellular overlay,
|
||||
a quickshell switch, a default). It could be getting toggled accidentally or
|
||||
by default, which would explain how it kept biting us unnamed.
|
||||
- Relevant docs to revisit with this lens: `PAF/audio.md` ("SLIM wedge",
|
||||
"chip-wide wedge", "TZ-poison"), `docs/mic-slim-findings-2026-07-17.md`
|
||||
("the SLIM -110 wedge", "capture wedge", "TX7-contaminated wedge",
|
||||
"status-read NACK → 1 Hz wedge loop"). Not all of these are phantom — but the
|
||||
possibility that some are must be checked before re-investigating.
|
||||
|
||||
## Process note for the resuming session
|
||||
- The codec/SLIM/AFE/file-serving layers are exhaustively ruled out (with the
|
||||
wedge caveat above — if a "ruled out" result was taken during a mode-swap, it
|
||||
is NOT valid). Do not re-investigate them UNLESS a prior test's mode state
|
||||
wasn't recorded.
|
||||
- Do NOT re-port the SLIMBUS_SLAVE_CONFIG send (already done, ret=0).
|
||||
- Do NOT chase `0x097`.
|
||||
- Top lead: ADM usecase routing — mainline q6adm vs downstream, and whether our
|
||||
capture sends any ADM commands. We have no HAL to do it, so the kernel must.
|
||||
- Secondary: payload-bearing APR/glink wire capture during a capture on BOTH
|
||||
sides, then byte-diff. That artifact does not exist yet.
|
||||
- **Record mode/radio state with every test going forward.** No more unattributed
|
||||
"wedge" logs.
|
||||
- Android access (airplanemode kills the NCM gadget): `adb` (device serial
|
||||
89JX0AAJP), `adb shell 'su -c "..."'` (magisk root works). Our-side access:
|
||||
ssh `~/.ssh/ani` to the USB gadget IP or WiFi `10.10.20.234`.
|
||||
150
saf/device/evidence/build-pmos-era.md
Normal file
150
saf/device/evidence/build-pmos-era.md
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# PAF · Build & packaging (pmbootstrap, the proper path)
|
||||
|
||||
How the blueline kernel + device package are actually built and shipped. This
|
||||
is the canonical build layer — earlier sessions wasted hours on host `make`
|
||||
and on undocumented pmbootstrap behaviours. Build with **pmbootstrap**; it is
|
||||
what every other sdm845-mainline contributor uses and what produces installable
|
||||
APKs. Host `make ARCH=arm64 LLVM=1` is not the path (it needs llvm-readelf/nm/
|
||||
objcopy on the host and does not produce a package).
|
||||
|
||||
## The two packages — do not conflate them
|
||||
|
||||
- **`linux-postmarketos-qcom-sdm845`** (`device/community/`) — the KERNEL
|
||||
package. Kernel image (`vmlinuz`/Image.gz), all modules, the blueline DTB.
|
||||
Userspace never goes here.
|
||||
- **`device-google-blueline`** (`device/testing/`) — the DEVICE package.
|
||||
Device userspace: firmware deps, hexagonrpcd/q6voiced confs, the systemd
|
||||
preset, and now the **modem PDC self-provisioner** (`qcril-autoselect.py` +
|
||||
`.service`, enabled via the preset). This is where per-device userspace
|
||||
belongs — modelled on how `q6voiced.conf` is already installed.
|
||||
|
||||
## Unified 7.1.1 kernel build (2026-06-22)
|
||||
|
||||
Base is the sdm845-mainline tree at tag `85f1df2a` (= 7.1.1), checked out at
|
||||
`references/linux-7.1.1-sdm845-CANONICAL`. All accumulated fixes are now **commits in that
|
||||
tree**, not scattered `source=` patches:
|
||||
|
||||
- SLPI keep-proxy-power-on (the `sar.cc:27` crash-loop fix — see slpi.md)
|
||||
- SLPI enabled in the blueline DTS
|
||||
- fastrpc SDSP hyp-assign tolerance
|
||||
|
||||
Built via source override:
|
||||
```
|
||||
cd ~/pmos/pmaports
|
||||
pmbootstrap build --src=$REPO/references/linux-7.1.1-sdm845-CANONICAL \
|
||||
linux-postmarketos-qcom-sdm845
|
||||
```
|
||||
APK lands in `~/.local/var/pmbootstrap/packages/edge/aarch64/`.
|
||||
|
||||
## pmbootstrap build gotchas (each cost real time — RECORD)
|
||||
|
||||
0. **The in-tree `arch/arm64/configs/sdm845.config` is merged LAST and silently
|
||||
overrides our pmaports config.** `prepare()` runs
|
||||
`merge_config.sh -m <pmaports config-*.aarch64> arch/$_carch/configs/sdm845.config`
|
||||
— and `merge_config -m` is last-wins. So any symbol set in the **in-tree**
|
||||
fragment beats the same symbol in the pmaports config, even though the
|
||||
pmaports config "looks" authoritative. **This silently broke wifi
|
||||
(2026-06-29):** pmaports set `CONFIG_QCOM_PD_MAPPER=m`, but the upstream
|
||||
sdm845-mainline in-tree fragment carried `=n` (their "use userspace daemon,
|
||||
poco-f1 mic race" default), so every build shipped pd-mapper OFF → no wlan PD
|
||||
→ `ath10k_snoc` probe defers → no `wlan0`. The `=n` rode in via the vanilla
|
||||
import commit `310c17cc6`, not a deliberate choice. **Fix:** set the symbol in
|
||||
the in-tree fragment (`references/linux-7.1.1-sdm845-CANONICAL/arch/arm64/configs/sdm845.config`),
|
||||
not just pmaports. **Audit rule:** when a built kernel disagrees with the
|
||||
pmaports config, `grep` the in-tree fragment first — it wins. Verify on-device
|
||||
with `zcat /proc/config.gz | grep <SYMBOL>`.
|
||||
|
||||
1. **`olddefconfig` is mandatory on a version bump.** 7.1.1 added Kconfig
|
||||
symbols not in the rc1 fragment config; abuild's build runs non-interactive,
|
||||
so a bare `make` hits the `oldconfig` EOF prompt and dies at `prepare0`.
|
||||
The APKBUILD `build()` now runs `make ARCH=$_carch LLVM=1 olddefconfig`
|
||||
first, then a serial `make ... -j1 prepare`, then the parallel build.
|
||||
|
||||
2. **The `*.bc` gitignore drops a tracked kernel source file.** pmbootstrap's
|
||||
source-override rsync is gitignore-aware but honors `*.bc` (line 16) WITHOUT
|
||||
the negation `!kernel/time/timeconst.bc` (line 180). So `timeconst.bc` — a
|
||||
real bc(1) source needed by `prepare0` for `include/generated/timeconst.h` —
|
||||
is silently excluded → `No rule to make target 'kernel/time/timeconst.bc'`.
|
||||
Same class of breakage hits the negated dotfiles (`.clang-format`, etc.).
|
||||
**Fix:** the kernel APKBUILD `prepare()` restores these from the source mount
|
||||
(`/mnt/pmbootstrap/source-override/`) after `default_prepare`.
|
||||
|
||||
3. **The persistent build copy poisons incremental builds.** pmbootstrap rsyncs
|
||||
the override into `/tmp/pmbootstrap-local-source-copy` (inside the native
|
||||
chroot) WITHOUT `--delete`. A prior interrupted build leaves stale host-tool
|
||||
binaries (`scripts/basic/fixdep`, `scripts/mod/mk_elfconfig`) + `.cmd` files;
|
||||
the next build's `make` then trusts the stale stamps, never recompiles them,
|
||||
and fails `mk_elfconfig: not found` at `prepare0`. If you see host-tool
|
||||
"not found" with no preceding `HOSTCC` line, the copy is dirty.
|
||||
**Recovery:** let pmbootstrap recreate it cleanly (delete the copy dir;
|
||||
a fresh rsync from the clean host tree rebuilds host tools from scratch).
|
||||
Note: `git clean -dfx` on the HOST tree does NOT touch this chroot copy.
|
||||
|
||||
## Kernel-swap / boot.img (from modem.md, kept here for the build flow)
|
||||
|
||||
The modem stack is fully modular (`qcom_q6v5_mss`, `qrtr*`, `qcom_glink_smem`,
|
||||
`mhi*` all `=m`), so a kernel swap needs the matching `lib/modules/<kver>/` on
|
||||
the rootfs or the modem silently no-loads (a false 52). UFS + USB-gadget-NCM are
|
||||
`=y`, so a module-mismatched boot still reaches rootfs + USB.
|
||||
|
||||
Boot image: `cat vmlinuz <blueline>.dtb` + the standing initramfs, `mkbootimg`
|
||||
header v0, base 0x0, kernel_off 0x8000, ramdisk_off 0x1000000, tags 0x100, real
|
||||
cmdline `quiet splash … pmos_boot_uuid=2CE8-836F pmos_root_uuid=26733d78…`.
|
||||
Rootfs is found by `pmos_root_uuid` (the ext4 FS UUID), so the slot LETTER only
|
||||
decides which block device we write into — the cmdline is slot-agnostic.
|
||||
|
||||
SLOT MAP (verified live via `fastboot getvar current-slot` 2026-06-26):
|
||||
**Android = slot B** (`current-slot: b`, `slot-successful:b: yes`). **pmOS = slot A.**
|
||||
Flash ONLY `boot_a` (`fastboot flash boot_a …; fastboot --set-active=a`) and write
|
||||
the rootfs to `system_a`. NEVER touch `boot_b` / `system_b` (that is Android now),
|
||||
EFS(modemst1/2), userdata (single SHARED partition, ~119 GB), or TZ/XBL/ABL.
|
||||
(Earlier revs of this doc had the slots reversed — they were wrong; the device is
|
||||
authoritative.)
|
||||
|
||||
### FLASH GOTCHAS (each cost a boot this session — RECORD)
|
||||
|
||||
1. **`system_a` rejects bootloader-`fastboot flash` → must use TWRP cat (or fastbootd).**
|
||||
`fastboot flash system_a` from the bootloader fails `Partition should be flashed
|
||||
in fastbootd`. This fastboot/bootloader gates system writes to userspace. We DON'T
|
||||
have a working `reboot fastbootd` (binary says "unknown reboot target"), so the
|
||||
path is: `fastboot boot ~/Downloads/twrp-3.7.1_12-0-blueline.img`, wait for adb
|
||||
`recovery`, then `adb shell -T "cat > /dev/block/by-name/system_a; sync" < raw.img`
|
||||
(`system_a` → `/dev/block/sda5`, a real 2.95 GB physical partition, NOT dynamic).
|
||||
|
||||
2. **pmbootstrap's `google-blueline.img` is ANDROID-SPARSE, not raw — un-sparse before cat.**
|
||||
THE bug that gave a kernel-7.1.0rc1 **debug-shell boot** (initramfs can't find root).
|
||||
deviceinfo has `deviceinfo_flash_sparse="true"`, so the final image starts with the
|
||||
sparse magic `3a ff 26 ed`, not a partition table. `cat`'ing it raw onto `system_a`
|
||||
writes a sparse container, so there's no GPT/ext4 on the partition → initramfs
|
||||
`findfs UUID=` fails → debug shell. **Fix:** `simg2img google-blueline.img raw.img`
|
||||
first, THEN cat `raw.img`. Verify: a correct raw image starts with `0000`, reads as
|
||||
GPT @ **4096-byte sectors** (`fdisk -b 4096 -l`: p1 ≈236M EFI/boot, p2 ext4 root at
|
||||
byte 255852544), and has the ext4 magic `53ef` at `255852544+1080`. This matches the
|
||||
known-good `slotb-backup` byte-for-byte. (UFS is 4096-sector — `fdisk` default 512
|
||||
shows NO partitions on the raw image; that's a red herring, not corruption.)
|
||||
|
||||
3. **Quarantine stale `_p<timestamp>` kernel apks before `install`, or apk grabs the wrong one.**
|
||||
A `--src` build auto-versions as `7.1.1_p20260625…`, which apk sorts ABOVE the tagged
|
||||
`7.1.1-r2` (the `_p` post-suffix outranks the bare tag). `pmbootstrap install` then
|
||||
silently pulls the OLD kernel. Move the `_p` apks out of
|
||||
`~/.local/var/pmbootstrap/packages/edge/aarch64/`, rebuild the index (`apk index
|
||||
*.apk` + `abuild-sign` in the native chroot — the `pmbootstrap index` wrapper failed
|
||||
on its own quoting), then `pmbootstrap zap` (the rootfs chroot has the `_p` kernel
|
||||
installed and apk won't downgrade) and re-install.
|
||||
|
||||
## The libqmi fork — packaged as the temp/libqmi override
|
||||
|
||||
`qcril-autoselect.py` needs the **openimsd libqmi fork** (PDC Set Feature msg
|
||||
0x2E) via GObject-introspection; stock libqmi lacks both the message and the
|
||||
typelib. It is packaged as **`temp/libqmi`** (pkgname=`libqmi`, source
|
||||
`gitlab.postmarketos.org/modem/openimsd/libqmi` commit `b683efb`, built with
|
||||
`-Dintrospection=true`, provides `libqmi` + `qmi-utils`). Because the aport's
|
||||
pkgname is `libqmi`, it shadows Alpine's stock libqmi, and `device-google-blueline`
|
||||
pulls it in via its `libqmi` / `qmi-utils` deps — a fresh `pmbootstrap install`
|
||||
lays down kernel + provisioner + fork as one unit. The built apk carries
|
||||
`usr/lib/girepository-1.0/Qmi-1.0.typelib` (verified).
|
||||
|
||||
Watch-item: the override is pinned at `1.39.0-r0`, equal to current Alpine edge.
|
||||
If Alpine bumps libqmi above this, apk could prefer stock at install — bump the
|
||||
override's pkgver/pkgrel to stay ahead. (`references/openimsd-libqmi` +
|
||||
`tools/build-openimsd-libqmi.sh` remain as the on-device overlay fallback.)
|
||||
3137
saf/device/evidence/captures/glink_trace_pmos-20260702.txt
Normal file
3137
saf/device/evidence/captures/glink_trace_pmos-20260702.txt
Normal file
File diff suppressed because it is too large
Load diff
750
saf/device/evidence/d10-adp-touch-2026-08-09.patch
Normal file
750
saf/device/evidence/d10-adp-touch-2026-08-09.patch
Normal file
|
|
@ -0,0 +1,750 @@
|
|||
From 2e349ddf2fe35102ef7c281d356c0e9a53c0f526 Mon Sep 17 00:00:00 2001
|
||||
From: Casey Tunturi <casey@samaritansolutions.net>
|
||||
Date: Mon, 10 Aug 2026 02:05:11 +0000
|
||||
Subject: [PATCH] apple: d10: touch runtime reports and the display pipe
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
apple_z2: D10 shares D11 gen2 runtime reports. Without the enable, boot
|
||||
frames arrive and the panel goes silent about a second later.
|
||||
|
||||
adp: first display pipe on T8010. disp0 be 0x06200000/0x9000, fe
|
||||
0x06400000/0x4000, irqs 154/158; dart 0x06304000 irq 156; dsi
|
||||
0x06600000/0x100000 — byte-identical to T8012, which is A10 silicon.
|
||||
Values read off the D10 ADT. panel-summit gains a 750x1334 mode; the
|
||||
loader lights the panel, so there is no init sequence either way.
|
||||
---
|
||||
arch/arm64/boot/dts/apple/t8010-d10.dts | 179 ++++++++++++++++++++++++
|
||||
arch/arm64/boot/dts/apple/t8010.dtsi | 57 ++++++++
|
||||
drivers/gpu/drm/panel/panel-summit.c | 26 +++-
|
||||
drivers/input/touchscreen/apple_z2.c | 110 +++++++++------
|
||||
4 files changed, 327 insertions(+), 45 deletions(-)
|
||||
|
||||
diff --git a/arch/arm64/boot/dts/apple/t8010-d10.dts b/arch/arm64/boot/dts/apple/t8010-d10.dts
|
||||
index 39cdd12db..5f00bbe19 100644
|
||||
--- a/arch/arm64/boot/dts/apple/t8010-d10.dts
|
||||
+++ b/arch/arm64/boot/dts/apple/t8010-d10.dts
|
||||
@@ -2,6 +2,10 @@
|
||||
/*
|
||||
* Apple iPhone 7 (Qualcomm), D10, iPhone9,1 (A1660/A1778/A1779/A1780)
|
||||
* Copyright (c) 2022, Konrad Dybcio <konradybcio@kernel.org>
|
||||
+ *
|
||||
+ * Board values derived from the D10 ADT read off hardware 2026-08-08.
|
||||
+ * Bus topology is identical to D11; see d10-port/DELTAS.md for the five
|
||||
+ * values that differ and which of them still need driver work.
|
||||
*/
|
||||
|
||||
/dts-v1/;
|
||||
@@ -11,4 +15,179 @@
|
||||
/ {
|
||||
compatible = "apple,d10", "apple,t8010", "apple,arm-platform";
|
||||
model = "Apple iPhone 7 (Qualcomm)";
|
||||
+
|
||||
+ aliases {
|
||||
+ touchscreen0 = &touchscreen;
|
||||
+ };
|
||||
+
|
||||
+ d10_battery: battery {
|
||||
+ compatible = "simple-battery";
|
||||
+ constant-charge-current-max-microamp = <1950000>;
|
||||
+ };
|
||||
+};
|
||||
+
|
||||
+/*
|
||||
+ * Wi-Fi is deliberately absent. The ADT describes BCM4350 on PCIe port 3
|
||||
+ * (perst 15, clkreq 19, device_wake 88, reg_on pmu_gpio 10, module-instance
|
||||
+ * "kristoff"), but no driver claims it, so PCI pwrctrl never completes and the
|
||||
+ * deferred host bridge takes NVMe down with it:
|
||||
+ * pcie-apple-h9p: waiting for PCI power controls
|
||||
+ * Restore the port node only alongside a BCM4350 driver. Values: d10-port/DELTAS.md.
|
||||
+ */
|
||||
+
|
||||
+&i2c0 {
|
||||
+ status = "okay";
|
||||
+
|
||||
+ pmic@27 {
|
||||
+ compatible = "apple,chestnut-pmic";
|
||||
+ reg = <0x27>;
|
||||
+ #address-cells = <1>;
|
||||
+ #size-cells = <1>;
|
||||
+
|
||||
+ touch_hv: regulator-touch-hv {
|
||||
+ compatible = "apple,chestnut-touch-hv-regulator";
|
||||
+ regulator-name = "touch-hv";
|
||||
+ };
|
||||
+ };
|
||||
+};
|
||||
+
|
||||
+&i2c1 {
|
||||
+ charger@75 {
|
||||
+ compatible = "apple,sn2400";
|
||||
+ reg = <0x75>;
|
||||
+
|
||||
+ sn2400_charger: charger {
|
||||
+ compatible = "apple,sn2400-charger";
|
||||
+ monitored-battery = <&d10_battery>;
|
||||
+ input-current-limit-microamp = <2000000>;
|
||||
+ mux-states = <&hdq_mux 0>;
|
||||
+ mux-state-names = "hdq";
|
||||
+ };
|
||||
+
|
||||
+ hdq_mux: hdq-mux {
|
||||
+ compatible = "apple,sn2400-hdq-mux";
|
||||
+ #mux-state-cells = <1>;
|
||||
+ };
|
||||
+ };
|
||||
+};
|
||||
+
|
||||
+&i2c2 {
|
||||
+ status = "okay";
|
||||
+
|
||||
+ light-sensor@29 {
|
||||
+ compatible = "apple,ct821", "amstaos,tsl2581";
|
||||
+ reg = <0x29>;
|
||||
+ interrupts-extended = <&pinctrl_ap 141 IRQ_TYPE_LEVEL_LOW>;
|
||||
+ };
|
||||
+};
|
||||
+
|
||||
+&pinctrl_ap {
|
||||
+ battery_swi_pins: battery-swi-pins {
|
||||
+ pinmux = <APPLE_PINMUX(173, 1)>;
|
||||
+ };
|
||||
+
|
||||
+};
|
||||
+
|
||||
+&serial5 {
|
||||
+ pinctrl-0 = <&battery_swi_pins>;
|
||||
+ pinctrl-names = "default";
|
||||
+ status = "okay";
|
||||
+
|
||||
+ onewire {
|
||||
+ compatible = "ti,hdq-uart";
|
||||
+ mux-states = <&hdq_mux 1>;
|
||||
+ mux-state-names = "hdq";
|
||||
+
|
||||
+ /* ADT: gas-gauge,bq27540 — D11 is a bq27545. */
|
||||
+ fuel-gauge {
|
||||
+ compatible = "apple,d10-bq27540", "ti,bq27540-hdq";
|
||||
+ monitored-battery = <&d10_battery>;
|
||||
+ };
|
||||
+ };
|
||||
+};
|
||||
+
|
||||
+&spi2 {
|
||||
+ cs-gpios = <&pinctrl_ap 81 GPIO_ACTIVE_LOW>;
|
||||
+ dmas = <&sio 0x18>;
|
||||
+ dma-names = "tx";
|
||||
+ status = "okay";
|
||||
+
|
||||
+ touchscreen: touchscreen@0 {
|
||||
+ compatible = "apple,d10-touchscreen";
|
||||
+ reg = <0>;
|
||||
+ spi-max-frequency = <3000000>;
|
||||
+ spi-cpol;
|
||||
+ spi-cpha;
|
||||
+ apple,z2-no-init-ack;
|
||||
+ interrupts-extended = <&pinctrl_ap 166 IRQ_TYPE_EDGE_FALLING>;
|
||||
+ reset-gpios = <&pinctrl_ap 161 GPIO_ACTIVE_LOW>;
|
||||
+ hv-supply = <&touch_hv>;
|
||||
+ core-supply = <&touch_core>;
|
||||
+ clocks = <&touchclk>;
|
||||
+ /* ADT hid-merge-personality = C1F5D,2 (D11/D111 are C1F5E,2). */
|
||||
+ firmware-name = "apple/dfrmtfw-d10-c1f5d-2.bin";
|
||||
+ touchscreen-size-x = <750>;
|
||||
+ touchscreen-size-y = <1334>;
|
||||
+ };
|
||||
+};
|
||||
+
|
||||
+&sio {
|
||||
+ status = "okay";
|
||||
+};
|
||||
+
|
||||
+&touchclk {
|
||||
+ clock-frequency = <32768>;
|
||||
+};
|
||||
+
|
||||
+/*
|
||||
+ * Apple Display Pipe. ADP inherits bootloader-initialised state, so the
|
||||
+ * back-end domain must stay powered or the driver loses the panel m1n1 lit.
|
||||
+ */
|
||||
+&ps_disp0_be {
|
||||
+ apple,always-on;
|
||||
+};
|
||||
+
|
||||
+&disp0 {
|
||||
+ status = "okay";
|
||||
+};
|
||||
+
|
||||
+&disp0_dart {
|
||||
+ status = "okay";
|
||||
+};
|
||||
+
|
||||
+&disp0_mipi {
|
||||
+ status = "okay";
|
||||
+};
|
||||
+
|
||||
+&mipi_out {
|
||||
+ mipi_out_panel: endpoint@0 {
|
||||
+ reg = <0>;
|
||||
+ remote-endpoint = <&panel_in>;
|
||||
+ };
|
||||
+};
|
||||
+
|
||||
+&disp0_mipi {
|
||||
+ #address-cells = <1>;
|
||||
+ #size-cells = <0>;
|
||||
+
|
||||
+ panel@0 {
|
||||
+ compatible = "apple,d10-panel";
|
||||
+ reg = <0>;
|
||||
+ max-brightness = <2047>;
|
||||
+
|
||||
+ port {
|
||||
+ panel_in: endpoint {
|
||||
+ remote-endpoint = <&mipi_out_panel>;
|
||||
+ };
|
||||
+ };
|
||||
+ };
|
||||
+};
|
||||
+
|
||||
+/*
|
||||
+ * ADP owns the panel, so the loader framebuffer must not also claim it:
|
||||
+ * simpledrm binds first as the boot console and udev then names it the
|
||||
+ * primary GPU, which is the card the compositor picks. One writer.
|
||||
+ */
|
||||
+&framebuffer0 {
|
||||
+ status = "disabled";
|
||||
};
|
||||
diff --git a/arch/arm64/boot/dts/apple/t8010.dtsi b/arch/arm64/boot/dts/apple/t8010.dtsi
|
||||
index 8dc02eb60..7ec65050e 100644
|
||||
--- a/arch/arm64/boot/dts/apple/t8010.dtsi
|
||||
+++ b/arch/arm64/boot/dts/apple/t8010.dtsi
|
||||
@@ -358,6 +358,63 @@ ehci1: usb-ehci@20c400000 {
|
||||
#endif
|
||||
};
|
||||
|
||||
+ disp0: display-pipe@206200000 {
|
||||
+ compatible = "apple,t8010-display-pipe", "apple,h7-display-pipe";
|
||||
+ reg = <0x2 0x06200000 0x0 0x9000>,
|
||||
+ <0x2 0x06400000 0x0 0x4000>;
|
||||
+ reg-names = "be", "fe";
|
||||
+ power-domains = <&ps_disp0_fe>, <&ps_disp0_be>;
|
||||
+ interrupts = <AIC_IRQ 154 IRQ_TYPE_LEVEL_HIGH>,
|
||||
+ <AIC_IRQ 158 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ interrupt-names = "be", "fe";
|
||||
+ iommus = <&disp0_dart 0>;
|
||||
+ status = "disabled";
|
||||
+
|
||||
+ port {
|
||||
+ adp_out_mipi: endpoint {
|
||||
+ remote-endpoint = <&mipi_in_adp>;
|
||||
+ };
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
+ disp0_dart: iommu@206304000 {
|
||||
+ compatible = "apple,t8010-dart", "apple,s5l8960x-dart";
|
||||
+ reg = <0x2 0x06304000 0x0 0x4000>;
|
||||
+ interrupts = <AIC_IRQ 156 IRQ_TYPE_LEVEL_HIGH>;
|
||||
+ #iommu-cells = <1>;
|
||||
+ power-domains = <&ps_disp0_fe>;
|
||||
+ status = "disabled";
|
||||
+ };
|
||||
+
|
||||
+ disp0_mipi: dsi@206600000 {
|
||||
+ compatible = "apple,t8010-display-pipe-mipi", "apple,h7-display-pipe-mipi";
|
||||
+ reg = <0x2 0x06600000 0x0 0x100000>;
|
||||
+ power-domains = <&ps_mipi_dsi>;
|
||||
+ status = "disabled";
|
||||
+
|
||||
+ ports {
|
||||
+ #address-cells = <1>;
|
||||
+ #size-cells = <0>;
|
||||
+
|
||||
+ mipi_in: port@0 {
|
||||
+ reg = <0>;
|
||||
+ #address-cells = <1>;
|
||||
+ #size-cells = <0>;
|
||||
+
|
||||
+ mipi_in_adp: endpoint@0 {
|
||||
+ reg = <0>;
|
||||
+ remote-endpoint = <&adp_out_mipi>;
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
+ mipi_out: port@1 {
|
||||
+ reg = <1>;
|
||||
+ #address-cells = <1>;
|
||||
+ #size-cells = <0>;
|
||||
+ };
|
||||
+ };
|
||||
+ };
|
||||
+
|
||||
pcie0_dart0: iommu@601008000 {
|
||||
compatible = "apple,t8010-dart", "apple,s5l8960x-dart";
|
||||
reg = <0x6 0x01008000 0x0 0x4000>;
|
||||
diff --git a/drivers/gpu/drm/panel/panel-summit.c b/drivers/gpu/drm/panel/panel-summit.c
|
||||
index 6d40b9ddf..42e4c24fb 100644
|
||||
--- a/drivers/gpu/drm/panel/panel-summit.c
|
||||
+++ b/drivers/gpu/drm/panel/panel-summit.c
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <linux/backlight.h>
|
||||
#include <linux/mod_devicetable.h>
|
||||
+#include <linux/of.h>
|
||||
#include <linux/property.h>
|
||||
#include <drm/drm_device.h>
|
||||
#include <drm/drm_mipi_dsi.h>
|
||||
@@ -48,15 +49,35 @@ static struct drm_display_mode summit_mode = {
|
||||
.flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
|
||||
};
|
||||
|
||||
+/* iPhone 7 (D10): 750x1334, lit by m1n1 before Linux starts. */
|
||||
+static struct drm_display_mode d10_mode = {
|
||||
+ .hdisplay = 750,
|
||||
+ .vdisplay = 1334,
|
||||
+ .hsync_start = 750 + 8,
|
||||
+ .hsync_end = 750 + 8 + 16,
|
||||
+ .htotal = 750 + 8 + 16 + 16,
|
||||
+ .vsync_start = 1334 + 4,
|
||||
+ .vsync_end = 1334 + 4 + 8,
|
||||
+ .vtotal = 1334 + 4 + 8 + 8,
|
||||
+ .clock = ((750 + 8 + 16 + 16) * (1334 + 4 + 8 + 8) * 60) / 1000,
|
||||
+ .type = DRM_MODE_TYPE_DRIVER,
|
||||
+ .flags = DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_NVSYNC,
|
||||
+};
|
||||
+
|
||||
static int summit_get_modes(struct drm_panel *panel,
|
||||
struct drm_connector *connector)
|
||||
{
|
||||
+ const struct drm_display_mode *mode =
|
||||
+ of_device_get_match_data(panel->dev);
|
||||
+
|
||||
+ if (!mode)
|
||||
+ mode = &summit_mode;
|
||||
connector->display_info.non_desktop = true;
|
||||
drm_object_property_set_value(&connector->base,
|
||||
connector->dev->mode_config.non_desktop_property,
|
||||
connector->display_info.non_desktop);
|
||||
|
||||
- return drm_connector_helper_get_modes_fixed(connector, &summit_mode);
|
||||
+ return drm_connector_helper_get_modes_fixed(connector, mode);
|
||||
}
|
||||
|
||||
static const struct drm_panel_funcs summit_panel_funcs = {
|
||||
@@ -113,7 +134,8 @@ static DEFINE_SIMPLE_DEV_PM_OPS(summit_pm_ops, summit_suspend,
|
||||
summit_set_brightness);
|
||||
|
||||
static const struct of_device_id summit_of_match[] = {
|
||||
- { .compatible = "apple,summit" },
|
||||
+ { .compatible = "apple,summit", .data = &summit_mode },
|
||||
+ { .compatible = "apple,d10-panel", .data = &d10_mode },
|
||||
{},
|
||||
};
|
||||
|
||||
diff --git a/drivers/input/touchscreen/apple_z2.c b/drivers/input/touchscreen/apple_z2.c
|
||||
index ec84de96f..065fa8214 100644
|
||||
--- a/drivers/input/touchscreen/apple_z2.c
|
||||
+++ b/drivers/input/touchscreen/apple_z2.c
|
||||
@@ -93,6 +93,7 @@ enum apple_z2_protocol_state {
|
||||
enum apple_z2_variant {
|
||||
APPLE_Z2_VARIANT_GENERIC,
|
||||
APPLE_Z2_VARIANT_J172,
|
||||
+ APPLE_Z2_VARIANT_D10,
|
||||
APPLE_Z2_VARIANT_D11,
|
||||
APPLE_Z2_VARIANT_D111,
|
||||
};
|
||||
@@ -150,19 +151,35 @@ static bool apple_z2_is_j172(const struct apple_z2 *z2)
|
||||
return z2->variant == APPLE_Z2_VARIANT_J172;
|
||||
}
|
||||
|
||||
+static bool apple_z2_is_d10(const struct apple_z2 *z2)
|
||||
+{
|
||||
+ return z2->variant == APPLE_Z2_VARIANT_D10;
|
||||
+}
|
||||
+
|
||||
static bool apple_z2_is_d11(const struct apple_z2 *z2)
|
||||
{
|
||||
return z2->variant == APPLE_Z2_VARIANT_D11;
|
||||
}
|
||||
|
||||
+/* D10 and D11 share the gen2 runtime report protocol. Without the enable,
|
||||
+ * boot frames arrive and the panel then goes silent — measured on d10
|
||||
+ * 2026-08-09: touch reported for about a second after probe, then nothing. */
|
||||
+static bool apple_z2_is_gen2_reports(const struct apple_z2 *z2)
|
||||
+{
|
||||
+ return z2->variant == APPLE_Z2_VARIANT_D10 ||
|
||||
+ z2->variant == APPLE_Z2_VARIANT_D11;
|
||||
+}
|
||||
+
|
||||
static bool apple_z2_is_d111(const struct apple_z2 *z2)
|
||||
{
|
||||
return z2->variant == APPLE_Z2_VARIANT_D111;
|
||||
}
|
||||
|
||||
-static bool apple_z2_is_iphone7_plus(const struct apple_z2 *z2)
|
||||
+/* Whole T8010 iPhone 7 generation, both panel sizes. */
|
||||
+static bool apple_z2_is_iphone7(const struct apple_z2 *z2)
|
||||
{
|
||||
- return apple_z2_is_d11(z2) || apple_z2_is_d111(z2);
|
||||
+ return apple_z2_is_d10(z2) || apple_z2_is_d11(z2) ||
|
||||
+ apple_z2_is_d111(z2);
|
||||
}
|
||||
|
||||
struct apple_z2_finger {
|
||||
@@ -404,7 +421,7 @@ static void apple_z2_parse_touches(struct apple_z2 *z2,
|
||||
msg_len, nfingers);
|
||||
return;
|
||||
}
|
||||
- if (apple_z2_is_iphone7_plus(z2) && nfingers &&
|
||||
+ if (apple_z2_is_iphone7(z2) && nfingers &&
|
||||
!z2->surface_descriptor_valid) {
|
||||
dev_warn_ratelimited(&z2->spidev->dev,
|
||||
"iPhone 7 Plus touch packet without surface descriptor\n");
|
||||
@@ -422,7 +439,7 @@ static void apple_z2_parse_touches(struct apple_z2 *z2,
|
||||
input_mt_slot(z2->input_dev, slot);
|
||||
if (!input_mt_report_slot_state(z2->input_dev, MT_TOOL_FINGER, slot_valid))
|
||||
continue;
|
||||
- if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_iphone7(z2)) {
|
||||
abs_x = (s16)le16_to_cpu(fingers[i].abs_x);
|
||||
abs_y = (s16)le16_to_cpu(fingers[i].abs_y);
|
||||
coords_valid = apple_z2_scale_iphone7_coord(abs_x,
|
||||
@@ -454,7 +471,7 @@ static void apple_z2_parse_touches(struct apple_z2 *z2,
|
||||
input_report_abs(z2->input_dev, ABS_MT_WIDTH_MINOR,
|
||||
le16_to_cpu(fingers[i].tool_minor));
|
||||
orientation = (s16)le16_to_cpu(fingers[i].orientation);
|
||||
- if (apple_z2_is_iphone7_plus(z2))
|
||||
+ if (apple_z2_is_iphone7(z2))
|
||||
orientation = (s16)(0x4000 - orientation);
|
||||
input_report_abs(z2->input_dev, ABS_MT_ORIENTATION, orientation);
|
||||
input_report_abs(z2->input_dev, ABS_MT_TOUCH_MAJOR,
|
||||
@@ -473,7 +490,7 @@ static void apple_z2_dispatch_frame(struct apple_z2 *z2, const u8 *payload,
|
||||
u16 report_len;
|
||||
|
||||
/* iPhone 7 Plus Gen2 packets carry the legacy report header directly. */
|
||||
- if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_iphone7(z2)) {
|
||||
if (payload_len < APPLE_Z2_FINGERS_OFFSET)
|
||||
return;
|
||||
|
||||
@@ -572,11 +589,11 @@ static int apple_z2_read_packet(struct apple_z2 *z2)
|
||||
return -EMSGSIZE;
|
||||
}
|
||||
wire_len = pkt_len;
|
||||
- if (apple_z2_is_d11(z2))
|
||||
+ if (apple_z2_is_gen2_reports(z2))
|
||||
wire_len = max_t(size_t, wire_len,
|
||||
APPLE_Z2_GEN2_MIN_RESULT_SIZE);
|
||||
|
||||
- if (apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) {
|
||||
memset(z2->rx_buf, 0xa5, wire_len);
|
||||
xfer.tx_buf = z2->rx_buf;
|
||||
xfer.rx_buf = z2->rx_buf;
|
||||
@@ -587,11 +604,11 @@ static int apple_z2_read_packet(struct apple_z2 *z2)
|
||||
}
|
||||
if (error)
|
||||
return error;
|
||||
- if (apple_z2_is_iphone7_plus(z2))
|
||||
+ if (apple_z2_is_iphone7(z2))
|
||||
apple_z2_post_z2_xfer_delay(z2);
|
||||
|
||||
/* D11 touch reports do not follow the requested alternating tag. */
|
||||
- strict_counter = !apple_z2_is_d11(z2) ||
|
||||
+ strict_counter = !apple_z2_is_gen2_reports(z2) ||
|
||||
(pkt_len > 5 && z2->rx_buf[5] ==
|
||||
APPLE_Z2_D11_STATUS_REPORT);
|
||||
z2->runtime_frame_valid =
|
||||
@@ -705,7 +722,7 @@ static void apple_z2_avoid_short_dma(struct apple_z2 *z2,
|
||||
* an RX buffer makes shorter writes full-duplex, which the T8010
|
||||
* controller handles through PIO without changing the wire transfer.
|
||||
*/
|
||||
- if (!apple_z2_is_iphone7_plus(z2) || !z2->dma_min_len ||
|
||||
+ if (!apple_z2_is_iphone7(z2) || !z2->dma_min_len ||
|
||||
!xfer->tx_buf || xfer->rx_buf ||
|
||||
xfer->len >= z2->dma_min_len)
|
||||
return;
|
||||
@@ -831,7 +848,7 @@ static int apple_z2_write_report_locked(struct apple_z2 *z2, u8 report,
|
||||
if (error)
|
||||
return error;
|
||||
|
||||
- if (apple_z2_is_d11(z2) &&
|
||||
+ if (apple_z2_is_gen2_reports(z2) &&
|
||||
(z2->rx_buf[0] != APPLE_Z2_CMD_LAST ||
|
||||
!apple_z2_z2_checksum_valid(z2->rx_buf, APPLE_Z2_CMD_SIZE)))
|
||||
return -EPROTO;
|
||||
@@ -978,7 +995,7 @@ static int apple_z2_store_surface_descriptor(struct apple_z2 *z2, u16 len)
|
||||
return 0;
|
||||
}
|
||||
|
||||
-static int apple_z2_iphone7_plus_init_locked(struct apple_z2 *z2)
|
||||
+static int apple_z2_iphone7_init_locked(struct apple_z2 *z2)
|
||||
{
|
||||
static const u8 disable_legacy_reports[] = { 0 };
|
||||
u16 surface_len;
|
||||
@@ -1289,17 +1306,17 @@ static void apple_z2_platform_power_off(struct apple_z2 *z2)
|
||||
{
|
||||
int error;
|
||||
|
||||
- if (apple_z2_is_iphone7_plus(z2))
|
||||
+ if (apple_z2_is_iphone7(z2))
|
||||
gpiod_set_value_cansleep(z2->reset_gpio, 1);
|
||||
|
||||
if (z2->clk_enabled) {
|
||||
clk_disable_unprepare(z2->clk);
|
||||
z2->clk_enabled = false;
|
||||
}
|
||||
- if (apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2))
|
||||
+ if (apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2))
|
||||
usleep_range(1000, 2000);
|
||||
|
||||
- if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_iphone7(z2)) {
|
||||
if (z2->core_enabled) {
|
||||
error = regulator_disable(z2->core_supply);
|
||||
if (error) {
|
||||
@@ -1381,7 +1398,7 @@ static int apple_z2_j172_power_on(struct apple_z2 *z2)
|
||||
APPLE_Z2_STATE_POWERED);
|
||||
}
|
||||
|
||||
-static int apple_z2_iphone7_plus_power_on(struct apple_z2 *z2)
|
||||
+static int apple_z2_iphone7_power_on(struct apple_z2 *z2)
|
||||
{
|
||||
int error;
|
||||
|
||||
@@ -1422,8 +1439,8 @@ static int apple_z2_platform_power_on(struct apple_z2 *z2)
|
||||
|
||||
if (apple_z2_is_j172(z2))
|
||||
return apple_z2_j172_power_on(z2);
|
||||
- if (apple_z2_is_iphone7_plus(z2))
|
||||
- return apple_z2_iphone7_plus_power_on(z2);
|
||||
+ if (apple_z2_is_iphone7(z2))
|
||||
+ return apple_z2_iphone7_power_on(z2);
|
||||
|
||||
apple_z2_set_gpio(z2->power_ana_gpio, 1);
|
||||
usleep_range(1000, 2000);
|
||||
@@ -1609,14 +1626,14 @@ static int apple_z2_send_firmware_blob(struct apple_z2 *z2, const u8 *data,
|
||||
* SmartIO may still DMA these transfers, but using 16-bit SPI words would
|
||||
* swap every byte pair a second time.
|
||||
*/
|
||||
- if (!init && !apple_z2_is_iphone7_plus(z2) &&
|
||||
+ if (!init && !apple_z2_is_iphone7(z2) &&
|
||||
size >= z2->bpw16_min_len)
|
||||
blob_xfer.bits_per_word = 16;
|
||||
|
||||
dev_dbg(&z2->spidev->dev, "firmware blob len=%u bpw=%u\n",
|
||||
size, blob_xfer.bits_per_word);
|
||||
|
||||
- if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) && init) {
|
||||
+ if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) && init) {
|
||||
if (size != 4 || data[0] != 0x1a || data[1] != 0xa1 ||
|
||||
data[2] != 0x18 || data[3] != 0xe1)
|
||||
return dev_err_probe(&z2->spidev->dev, -EINVAL,
|
||||
@@ -1634,7 +1651,7 @@ static int apple_z2_send_firmware_blob(struct apple_z2 *z2, const u8 *data,
|
||||
if (error)
|
||||
return error;
|
||||
|
||||
- if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) && init) {
|
||||
+ if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) && init) {
|
||||
ready = z2->rx_buf[0] == 0x1f && z2->rx_buf[1] == 0x01;
|
||||
if (!ready)
|
||||
return dev_err_probe(&z2->spidev->dev, -EPROTO,
|
||||
@@ -1648,7 +1665,7 @@ static int apple_z2_send_firmware_blob(struct apple_z2 *z2, const u8 *data,
|
||||
return error;
|
||||
apple_z2_post_ack_delay(z2);
|
||||
}
|
||||
- if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) && init) {
|
||||
+ if ((apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) && init) {
|
||||
error = apple_z2_advance_protocol(z2, APPLE_Z2_STATE_BOOT_IRQ,
|
||||
APPLE_Z2_STATE_HBPP_READY);
|
||||
if (error)
|
||||
@@ -1741,7 +1758,7 @@ static int apple_z2_wait_ready_irq(struct apple_z2 *z2, u32 timeout_ms)
|
||||
{
|
||||
if (!apple_z2_wait_firmware_irq(z2, timeout_ms))
|
||||
return -ETIMEDOUT;
|
||||
- if (!apple_z2_is_j172(z2) && !apple_z2_is_iphone7_plus(z2))
|
||||
+ if (!apple_z2_is_j172(z2) && !apple_z2_is_iphone7(z2))
|
||||
return 0;
|
||||
|
||||
if (apple_z2_is_j172(z2) &&
|
||||
@@ -1778,7 +1795,7 @@ static int apple_z2_apply_fw_config(struct apple_z2 *z2, const u8 *data,
|
||||
cpol = apple_z2_fw_config_word(data, 5);
|
||||
boot_timeout = apple_z2_fw_config_word(data, 9);
|
||||
if ((valid & APPLE_Z2_FW_CONFIG_MIN_DMA) && min_dma) {
|
||||
- if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_iphone7(z2)) {
|
||||
if (min_dma > APPLE_Z2_RX_BUF_SIZE)
|
||||
return -EINVAL;
|
||||
z2->dma_min_len = min_dma;
|
||||
@@ -1790,7 +1807,7 @@ static int apple_z2_apply_fw_config(struct apple_z2 *z2, const u8 *data,
|
||||
z2->z2_inter_packet_delay_us = z2_delay;
|
||||
/* iPhone 7 Plus uses zero to retain the required 1 ms CS delay. */
|
||||
if ((valid & APPLE_Z2_FW_CONFIG_CS_DELAY) &&
|
||||
- (!apple_z2_is_iphone7_plus(z2) || cs_delay))
|
||||
+ (!apple_z2_is_iphone7(z2) || cs_delay))
|
||||
z2->z2_cs_delay_us = cs_delay;
|
||||
if ((valid & APPLE_Z2_FW_CONFIG_BOOT_TIMEOUT) && boot_timeout)
|
||||
z2->boot_timeout_ms = boot_timeout;
|
||||
@@ -1922,7 +1939,7 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2)
|
||||
u32 max_size;
|
||||
u32 provider;
|
||||
|
||||
- if (!apple_z2_is_iphone7_plus(z2))
|
||||
+ if (!apple_z2_is_iphone7(z2))
|
||||
return -EINVAL;
|
||||
if (size != sizeof(struct apple_z2_fw_calibration) ||
|
||||
size > fw->size - fw_idx)
|
||||
@@ -1969,19 +1986,19 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2)
|
||||
fw_idx = round_up(fw_idx, 4);
|
||||
}
|
||||
|
||||
- if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_iphone7(z2)) {
|
||||
msleep(50);
|
||||
mutex_lock(&z2->io_lock);
|
||||
- error = apple_z2_iphone7_plus_init_locked(z2);
|
||||
+ error = apple_z2_iphone7_init_locked(z2);
|
||||
if (!error) {
|
||||
z2->booted = true;
|
||||
error = apple_z2_read_packet(z2);
|
||||
}
|
||||
- if (!error && apple_z2_is_d11(z2) &&
|
||||
+ if (!error && apple_z2_is_gen2_reports(z2) &&
|
||||
(!z2->runtime_frame_valid ||
|
||||
z2->rx_buf[5] != APPLE_Z2_D11_STATUS_REPORT))
|
||||
error = -EPROTO;
|
||||
- if (!error && apple_z2_is_d11(z2))
|
||||
+ if (!error && apple_z2_is_gen2_reports(z2))
|
||||
error = apple_z2_enable_d11_reports_locked(z2);
|
||||
mutex_unlock(&z2->io_lock);
|
||||
return error;
|
||||
@@ -2087,7 +2104,7 @@ static int apple_z2_boot_preamble(struct apple_z2 *z2, bool full_duplex)
|
||||
APPLE_Z2_STATE_SPI_CONFIGURED);
|
||||
}
|
||||
|
||||
-static int apple_z2_iphone7_plus_post_boot_preamble(struct apple_z2 *z2)
|
||||
+static int apple_z2_iphone7_post_boot_preamble(struct apple_z2 *z2)
|
||||
{
|
||||
struct spi_transfer xfer = {
|
||||
.tx_buf = z2->tx_buf,
|
||||
@@ -2114,7 +2131,7 @@ static int apple_z2_boot(struct apple_z2 *z2)
|
||||
if (error)
|
||||
return error;
|
||||
|
||||
- if (apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) {
|
||||
error = apple_z2_apply_initial_config(z2);
|
||||
if (error)
|
||||
goto err_stop;
|
||||
@@ -2123,7 +2140,7 @@ static int apple_z2_boot(struct apple_z2 *z2)
|
||||
error = apple_z2_boot_preamble(z2, false);
|
||||
if (error)
|
||||
goto err_stop;
|
||||
- } else if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ } else if (apple_z2_is_iphone7(z2)) {
|
||||
/* iPhone 7 Plus performs a legacy reset and zero preamble first. */
|
||||
error = apple_z2_pulse_reset(z2);
|
||||
if (error)
|
||||
@@ -2142,7 +2159,7 @@ static int apple_z2_boot(struct apple_z2 *z2)
|
||||
irq_enabled = true;
|
||||
if (apple_z2_is_j172(z2)) {
|
||||
error = apple_z2_pulse_reset(z2);
|
||||
- } else if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ } else if (apple_z2_is_iphone7(z2)) {
|
||||
gpiod_set_value_cansleep(z2->reset_gpio, 0);
|
||||
usleep_range(1000, 2000);
|
||||
error = 0;
|
||||
@@ -2158,20 +2175,20 @@ static int apple_z2_boot(struct apple_z2 *z2)
|
||||
error = -ETIMEDOUT;
|
||||
goto err_stop;
|
||||
}
|
||||
- if (apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) {
|
||||
error = apple_z2_advance_protocol(z2,
|
||||
APPLE_Z2_STATE_SPI_CONFIGURED,
|
||||
APPLE_Z2_STATE_BOOT_IRQ);
|
||||
if (error)
|
||||
goto err_stop;
|
||||
}
|
||||
- if (apple_z2_is_iphone7_plus(z2)) {
|
||||
- error = apple_z2_iphone7_plus_post_boot_preamble(z2);
|
||||
+ if (apple_z2_is_iphone7(z2)) {
|
||||
+ error = apple_z2_iphone7_post_boot_preamble(z2);
|
||||
if (error)
|
||||
goto err_stop;
|
||||
}
|
||||
|
||||
- if (apple_z2_is_j172(z2) || apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_j172(z2) || apple_z2_is_iphone7(z2)) {
|
||||
disable_irq(z2->spidev->irq);
|
||||
z2->upload_irq_masked = true;
|
||||
}
|
||||
@@ -2211,6 +2228,11 @@ static const struct apple_z2_chip_info apple_z2_j172_info = {
|
||||
.variant = APPLE_Z2_VARIANT_J172,
|
||||
};
|
||||
|
||||
+static const struct apple_z2_chip_info apple_z2_d10_info = {
|
||||
+ .name = "iPhone9,1 Touchscreen",
|
||||
+ .variant = APPLE_Z2_VARIANT_D10,
|
||||
+};
|
||||
+
|
||||
static const struct apple_z2_chip_info apple_z2_d11_info = {
|
||||
.name = "iPhone9,2 Touchscreen",
|
||||
.variant = APPLE_Z2_VARIANT_D11,
|
||||
@@ -2249,7 +2271,7 @@ static int apple_z2_probe(struct spi_device *spi)
|
||||
z2->spidev = spi;
|
||||
z2->variant = info->variant;
|
||||
z2->boot_timeout_ms = 20;
|
||||
- if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_iphone7(z2)) {
|
||||
z2->z2_inter_packet_delay_us = 1000;
|
||||
z2->z2_cs_delay_us = 1000;
|
||||
z2->boot_timeout_ms = 500;
|
||||
@@ -2269,7 +2291,7 @@ static int apple_z2_probe(struct spi_device *spi)
|
||||
if (IS_ERR(z2->reset_gpio))
|
||||
return dev_err_probe(dev, PTR_ERR(z2->reset_gpio),
|
||||
"unable to get reset GPIO\n");
|
||||
- if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_iphone7(z2)) {
|
||||
z2->hv_supply = devm_regulator_get(dev, "hv");
|
||||
if (IS_ERR(z2->hv_supply))
|
||||
return dev_err_probe(dev, PTR_ERR(z2->hv_supply),
|
||||
@@ -2316,7 +2338,7 @@ static int apple_z2_probe(struct spi_device *spi)
|
||||
error = device_property_read_string(dev, "firmware-name", &z2->fw_name);
|
||||
if (error)
|
||||
return dev_err_probe(dev, error, "unable to get firmware name\n");
|
||||
- if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ if (apple_z2_is_iphone7(z2)) {
|
||||
static const struct {
|
||||
const char *name;
|
||||
unsigned int max_size;
|
||||
@@ -2361,7 +2383,7 @@ static int apple_z2_probe(struct spi_device *spi)
|
||||
|
||||
if (apple_z2_is_j172(z2)) {
|
||||
slots = APPLE_Z2_J172_MAX_CONTACTS;
|
||||
- } else if (apple_z2_is_iphone7_plus(z2)) {
|
||||
+ } else if (apple_z2_is_iphone7(z2)) {
|
||||
slots = 10;
|
||||
if (apple_z2_is_d11(z2))
|
||||
mt_flags |= INPUT_MT_DROP_UNUSED;
|
||||
@@ -2415,6 +2437,7 @@ static const struct of_device_id apple_z2_of_match[] = {
|
||||
{ .compatible = "apple,j293-touchbar", .data = &apple_z2_j293_info },
|
||||
{ .compatible = "apple,j493-touchbar", .data = &apple_z2_j493_info },
|
||||
{ .compatible = "apple,j172-touchscreen", .data = &apple_z2_j172_info },
|
||||
+ { .compatible = "apple,d10-touchscreen", .data = &apple_z2_d10_info },
|
||||
{ .compatible = "apple,d11-touchscreen", .data = &apple_z2_d11_info },
|
||||
{ .compatible = "apple,d111-touchscreen", .data = &apple_z2_d111_info },
|
||||
{}
|
||||
@@ -2425,6 +2448,7 @@ static struct spi_device_id apple_z2_of_id[] = {
|
||||
{ .name = "j293-touchbar", .driver_data = (kernel_ulong_t)&apple_z2_j293_info },
|
||||
{ .name = "j493-touchbar", .driver_data = (kernel_ulong_t)&apple_z2_j493_info },
|
||||
{ .name = "j172-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_j172_info },
|
||||
+ { .name = "d10-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_d10_info },
|
||||
{ .name = "d11-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_d11_info },
|
||||
{ .name = "d111-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_d111_info },
|
||||
{}
|
||||
--
|
||||
2.55.0
|
||||
|
||||
104
saf/device/evidence/diag_capture.md
Normal file
104
saf/device/evidence/diag_capture.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
# PAF · DIAG capture runbook (Route B instrument)
|
||||
|
||||
Goal: get the modem's OWN view of RF-init — why it never self-completes, parks `offline`, and
|
||||
refuses `online` (DeviceNotReady 52). Everything so far is QMI reporting a *refusal code*; DIAG
|
||||
(QCDM/DM) gives the modem's internal **F3 debug messages**, radio frames, and **NV/EFS** state.
|
||||
This is the byte-for-byte trace every prior frontier (#5/#7/#8) named as the missing piece.
|
||||
|
||||
## Why DIAG (vs the coredump path)
|
||||
- F3/DM debug messages = the modem narrating its RF/NAS/MCFG init in its own words. Richer than a
|
||||
remoteproc coredump/SSR reason.
|
||||
- NV item + EFS read/write over DIAG = Route B's EFS/NV diff (GLM thread #2) at the authoritative
|
||||
source, and the write path for `mcfg_autoselect_by_uim` / power-up operating mode.
|
||||
- The decisive comparison: run the SAME tool on BOTH OSes against the SAME modem silicon and diff.
|
||||
|
||||
## Channel facts (confirmed)
|
||||
- DIAG is exposed on Android via `/dev/diag`, created by the **`diagchar`** kernel module (root).
|
||||
- Enable frame capture: `DIAG_IOCTL_SWITCH_LOGGING` → `MEMORY_DEVICE_MODE = 2`; check
|
||||
`DIAG_IOCTL_REMOTE_DEV` for the extra device_type byte.
|
||||
- Packet framing: `cmd_code(1B) + payload + CRC16-CCITT(2B) + 0x7e`, HDLC escaping
|
||||
(`0x7e`→`0x7d 0x5e`, `0x7d`→`0x7d 0x5d`).
|
||||
- Message classes: `DIAG_LOG_F` (binary log packets / radio frames), `DIAG_MSG_F` /
|
||||
`DIAG_EXT_MSG_F` (F3 debug strings — the narration we want). NV/EFS/L1-3 commands supported.
|
||||
- Tool: **QCSuper** (P1sec, open source) for frame + F3 capture → pcap/GSMTAP. For arbitrary NV
|
||||
item read/write, a raw DIAG client (libqcdm-style framer, or QCSuper's diag layer directly).
|
||||
- CHECK FIRST: `sdm845-mainline/pmtools` may already ship a diag/modem helper — look before
|
||||
reinventing the framer.
|
||||
|
||||
---
|
||||
|
||||
## Side A — Android (slot B, rooted): the WORKING reference [HIGH VALUE — never captured]
|
||||
`diagchar` + `/dev/diag` are present on the downstream Android kernel. Two ways in:
|
||||
- **USB-DIAG** (exposes a serial/COM port): `adb root; adb shell` → `su` →
|
||||
`setprop sys.usb.config diag,serial_cdev,rmnet_gsi,adb` (XDA-confirmed on Pixel 3). Our ledger
|
||||
saw this HAL-revert to adb-only on this LineageOS build — RETRY it; if it reverts, use local.
|
||||
- **Local `/dev/diag`** (reliable fallback): run the client on-device over adb, not USB-DIAG.
|
||||
|
||||
1. `adb root; adb shell ls -l /dev/diag` — confirm node exists.
|
||||
2. Push/run QCSuper on-device (or proxy /dev/diag over adb). Start F3 + frame capture.
|
||||
3. Trigger a clean offline→online: **airplane mode OFF→ON** (we already have this at QCRIL-function
|
||||
granularity in `android-capture-20260620/`; DIAG gets it at message/F3 granularity).
|
||||
4. Save: F3 stream + radio frames spanning the transition. This is the modem self-onlining at its
|
||||
own RF-init — the thing pmOS never reaches.
|
||||
5. While here, DIAG-read the NV/EFS state the working modem holds:
|
||||
- power-up operating mode NV
|
||||
- `mcfg_autoselect_by_uim`
|
||||
- RF-calibration NV / QCN region
|
||||
(Confirm exact NV item indices from the modem's NV table / QCSuper — do NOT guess indices.)
|
||||
|
||||
Output → `android-capture-YYYYMMDD/diag/`.
|
||||
|
||||
## Side B — pmOS (slot A, mainline): the FAILURE side [verify availability first]
|
||||
`diagchar` is a downstream module; mainline SDM845 does NOT ship it. So `/dev/diag` may be absent.
|
||||
Determine the transport before assuming symmetry:
|
||||
|
||||
1. `ls -l /dev/diag*` ; `dmesg | grep -i diag` ; `zcat /proc/config.gz | grep -i diag` (or check
|
||||
the built kernel config) ; look for a glink/rpmsg channel named `DIAG`.
|
||||
2. If a DIAG transport exists → run QCSuper, attempt `--dms-set-operating-mode=online`, capture the
|
||||
F3 stream of the FAILED RF-init. Diff F3 against Side A around the offline→online point — the
|
||||
first divergence is the answer.
|
||||
3. If NO DIAG transport on mainline → fall back to GLM thread #1: remoteproc coredump/SSR reason +
|
||||
`tqftpserv`/`rmtfs` request trace during the failed online. Diff that against Side A's DIAG F3.
|
||||
(Retry-safe: online attempts do NOT poison the modem — the old "one-shot per boot" rule is
|
||||
disproven. A QMI `Set Operating Mode = RESET (4)` cleanly reinitializes it without reboot.
|
||||
Retry freely. See HANDOFF.md CORRECTIONS.)
|
||||
|
||||
---
|
||||
|
||||
## What we're diffing for
|
||||
**KEY: a static NV/QCN diff between the two OSes will be NULL.** modemst1/2 + fsg/fsc + persist are
|
||||
on UNSLOTTED partitions → the modem reads BYTE-IDENTICAL persisted EFS/NV/cal on both Android and
|
||||
pmOS (ledger L83: Android-primed EFS → still fails). The modem has no separate flash; its NV lives
|
||||
in that shared EFS. So a QPST QCN backup / NV-item dump will read the same bytes on both sides — it
|
||||
will NOT reveal the delta. **The difference is necessarily RUNTIME behavior**, which is exactly why
|
||||
the F3 stream (not a static snapshot) is the instrument:
|
||||
- The F3 line(s) where Android's modem advances RF-init past the point pmOS's stalls.
|
||||
- Whether the working modem boots into LPM and flips LPM→online, vs pmOS sitting in `offline`
|
||||
(mode 3) — and what in the F3 stream gates that state.
|
||||
- Whether the working modem's online even TOUCHES NV/PDC (frontier #4 says steady-state Android is
|
||||
verify-only) — if it doesn't, the gate is pure firmware-state/timing the AP can't feed.
|
||||
|
||||
(NV reads in Side A step 5 are still worth doing ONCE — to confirm the shared-EFS assumption and
|
||||
record the working values — but expect them equal to pmOS. Don't build a Route B fix around a static
|
||||
NV diff until F3 shows the modem actually consuming that item at init.)
|
||||
|
||||
## Tools reality (we're on Arch, not Windows)
|
||||
- **QPST / QXDM are Windows-only proprietary.** QPST = QCN backup/restore; QXDM = deep F3 logging.
|
||||
Given the shared-EFS point above, QPST's QCN backup buys little here.
|
||||
- **QCSuper (Linux, open) is the path** for F3 + frame capture. For raw NV item read/write, a
|
||||
libqcdm-style framer or QCSuper's diag layer. Check `pmtools` first.
|
||||
|
||||
## SIDE note — glink rpmsg teardown is broken upstream (applies to CHANNEL open/close, NOT online)
|
||||
Dynamically opening/closing glink rpmsg channels is documented as broken on the upstreaming list
|
||||
(the `wwan_remove_port`/`rpmsg_wwan_ctrl` 614s hung_task seen on op6t). This is a real risk when
|
||||
*creating/tearing down a glink channel* (e.g. opening a raw DIAG rpmsg endpoint). It is NOT the
|
||||
"one failed online poisons the modem" rule that used to be here — that rule is disproven: a plain
|
||||
`--dms-set-operating-mode=online` is an ordinary QMI message on the existing IPCRTR transport, it
|
||||
does not open a glink channel, and it is retry-safe (RESET reinitializes). Keep the caution for
|
||||
raw glink channel-open probes only. An AT read of the modem's own reason (`+CEER`, `+CFUN?`) has
|
||||
no free path anyway: QMI service 8 is AT-*forwarding*, not AT-execution (see qrtr_tools_reference
|
||||
2026-07-02), and a raw glink AT port is exactly the channel-open that this note warns about.
|
||||
|
||||
## Refs
|
||||
- QCSuper: https://github.com/P1sec/QCSuper (docs/The Diag protocol.md)
|
||||
- pmtools (check for existing diag helper): https://gitlab.com/sdm845-mainline/pmtools
|
||||
108
saf/device/evidence/first_provision_capture.md
Normal file
108
saf/device/evidence/first_provision_capture.md
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# First-Provision Capture — force QCRIL to run the real MBN load sequence
|
||||
|
||||
**Why:** Every Android capture we have (`android-live-20260619/`) was an *already-provisioned*
|
||||
device, so QCRIL took the verify-only fast path (`is_modem_mbn_updated: prev_ver == cur_ver` →
|
||||
skip). We have **never recorded** the actual load→select→activate sequence, nor whether
|
||||
`pdc_enable_auto_selection` / `qcril_qmi_pdc_enable_modem_update` is a required precondition.
|
||||
This runbook forces a first-provision and captures it. See `PAF/modem.md` frontier #4.
|
||||
|
||||
**Run from the host with the phone on Android slot B, rooted (`adb root`).** All commands are
|
||||
host-side `adb`; device shell is Android (toybox), not pmOS ash.
|
||||
|
||||
---
|
||||
|
||||
## 0. Preconditions + backup (reversible-safety)
|
||||
|
||||
```sh
|
||||
adb root && adb wait-for-device
|
||||
adb shell getprop ro.product.device # expect: blueline
|
||||
adb shell getprop gsm.version.baseband # expect: g845-00194-...
|
||||
|
||||
# Back up QCRIL's writable provisioning cache + the trigger props, so we can prove
|
||||
# what changed and restore if needed.
|
||||
adb shell 'tar -czf /data/local/tmp/modem_config.bak.tgz -C /data/vendor modem_config 2>/dev/null'
|
||||
adb pull /data/local/tmp/modem_config.bak.tgz ./fp-capture/
|
||||
adb shell 'getprop | grep -E "vendor.radio.(cnv|hw_mbn|sw_mbn|mbn)" ' | tee ./fp-capture/props.before.txt
|
||||
```
|
||||
|
||||
> Note: we deliberately do **NOT** touch the modem's EFS (`modemst1/2`, `fsg`, `fsc`). Those hold
|
||||
> the modem's own persisted config + calibration; wiping them is destructive and not needed. We
|
||||
> only clear QCRIL's *userspace* cache + the version prop — that is the documented trigger for the
|
||||
> `prev_ver != cur_ver` reload branch.
|
||||
|
||||
## 1. Enlarge the radio log ring so early-boot lines survive until we dump
|
||||
|
||||
```sh
|
||||
adb shell setprop persist.vendor.radio.smlog_switch 1 # verbose radio logging (best-effort)
|
||||
adb logcat -b radio -G 64M # 64 MB radio buffer
|
||||
adb logcat -b system -G 16M
|
||||
```
|
||||
|
||||
## 2. Force the first-provision trigger, then reboot
|
||||
|
||||
```sh
|
||||
# Clear QCRIL's writable provisioning state (rebuilt automatically on next provision).
|
||||
adb shell 'rm -rf /data/vendor/modem_config/*'
|
||||
|
||||
# Blank the cached version string QCRIL compares against (this is the prev_ver in
|
||||
# is_modem_mbn_updated). Empty prev_ver => mismatch => QCRIL must re-evaluate the load path.
|
||||
adb shell 'setprop persist.vendor.radio.cnv.ver_info ""'
|
||||
adb shell 'setprop persist.vendor.radio.hw_mbn_loaded ""'
|
||||
adb shell 'setprop persist.vendor.radio.sw_mbn_loaded ""'
|
||||
|
||||
adb shell sync
|
||||
adb reboot
|
||||
```
|
||||
|
||||
## 3. Capture from as early as possible after boot
|
||||
|
||||
```sh
|
||||
adb wait-for-device
|
||||
adb root && adb wait-for-device
|
||||
# Dump the whole radio ring immediately — the 64M buffer should still hold the early
|
||||
# provision window (~10-40s into boot).
|
||||
adb logcat -b radio -d > ./fp-capture/radio-firstprov.txt
|
||||
adb logcat -b system -d > ./fp-capture/system-firstprov.txt
|
||||
adb shell 'getprop | grep -E "vendor.radio.(cnv|hw_mbn|sw_mbn|mbn)"' | tee ./fp-capture/props.after.txt
|
||||
```
|
||||
|
||||
If the early window was missed (buffer wrapped), repeat from §2 but instead of a plain dump, run
|
||||
a **streaming** capture across the reboot: `adb logcat -b radio > radio-firstprov.txt &` then
|
||||
`adb reboot`; logcat auto-reconnects on `wait-for-device` with `adb logcat ... -T 1`.
|
||||
|
||||
## 4. Analyze — the three questions
|
||||
|
||||
```sh
|
||||
cd ./fp-capture
|
||||
# (Q1) Did QCRIL actually run the load path this time (vs skip again)?
|
||||
grep -niE 'load_config|select_config|activate_config|REQUEST_MBN|start_mbn_update|is_ssr_or_bootup' radio-firstprov.txt | head -40
|
||||
|
||||
# (Q2) THE unknown: is pdc_enable_auto_selection / enable_modem_update called, and when
|
||||
# relative to load? This settles gap-analysis item #8 / open question #2.
|
||||
grep -niE 'enable_auto_select|auto.?select|enable_modem_update|disable_modem_update|set_feature_version' radio-firstprov.txt
|
||||
|
||||
# (Q3) The decision point — what did prev_ver/cur_ver read this time?
|
||||
grep -niE 'is_modem_mbn_updated|prev_ver_info|cur_ver_info' radio-firstprov.txt
|
||||
```
|
||||
|
||||
## 5. Interpreting the outcome (both results are informative)
|
||||
|
||||
- **QCRIL runs load→select→activate** → we finally have the real sequence + ordering + timing, and
|
||||
Q2 tells us if `enable_auto_selection` is a required precondition. Feeds path A (`qcril-prov.py`).
|
||||
- **QCRIL still skips** (queries modem, finds the active config already present in modem EFS, just
|
||||
rewrites the prop) → strong confirmation of **frontier #4 path B**: the modem self-persists in
|
||||
`modemst` and userspace never re-loads in normal operation. That makes the pmOS fix
|
||||
"provision-once + ensure `modemst` persistence," not a per-boot loader.
|
||||
|
||||
## 6. Restore (only if anything misbehaves)
|
||||
|
||||
QCRIL repopulates `/data/vendor/modem_config/` and the props automatically on the provision pass,
|
||||
so normally no restore is needed. If you want the exact prior cache back:
|
||||
|
||||
```sh
|
||||
adb push ./fp-capture/modem_config.bak.tgz /data/local/tmp/
|
||||
adb shell 'cd /data/vendor && rm -rf modem_config && tar -xzf /data/local/tmp/modem_config.bak.tgz'
|
||||
adb reboot
|
||||
```
|
||||
|
||||
EFS baseline (untouched by this procedure) remains at `efs-postandroid/{modemst1,modemst2,fsg,fsc}.img`.
|
||||
392
saf/device/evidence/los-mss-translated.c
Normal file
392
saf/device/evidence/los-mss-translated.c
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
/*
|
||||
* LOS pil-msa.c / peripheral-loader.c modem boot sequence
|
||||
* TRANSLATED to mainline qcom_q6v5_mss.c coding style.
|
||||
*
|
||||
* Purpose: make the downstream boot ordering readable so the delta
|
||||
* with mainline is obvious. Not compilable — a reference document.
|
||||
*
|
||||
* LEGEND:
|
||||
* [SAME] — mainline does the same thing
|
||||
* [DELTA] — mainline does something different (the interesting part)
|
||||
* [EXTRA] — downstream does this, mainline does not
|
||||
* [MISS] — mainline does this, downstream does not
|
||||
*
|
||||
* Source: los-kernel-blueline-4.9/drivers/soc/qcom/pil-msa.c
|
||||
* los-kernel-blueline-4.9/drivers/soc/qcom/peripheral-loader.c
|
||||
* Target: linux-7.1.1-sdm845/drivers/remoteproc/qcom_q6v5_mss.c
|
||||
*/
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* DOWNSTREAM MEMORY ASSIGNMENT HELPERS (translated to mainline API) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/*
|
||||
* [DELTA] Downstream uses hyp_assign_phys() from secure_buffer.c which
|
||||
* allocates its argument buffers via kzalloc + dmac_flush_range.
|
||||
* Mainline uses qcom_scm_assign_mem() which allocates from the
|
||||
* qcom_tzmem pool. Same SCM call (SVC=0x0C CMD=0x16), different
|
||||
* buffer provenance. Unlikely to matter but noted.
|
||||
*/
|
||||
|
||||
/* HLOS → MSS exclusively */
|
||||
static int los_assign_to_subsys(struct q6v5 *qproc,
|
||||
phys_addr_t addr, size_t size)
|
||||
{
|
||||
struct qcom_scm_vmperm dest = {
|
||||
.vmid = QCOM_SCM_VMID_MSS_MSA, /* 0xF */
|
||||
.perm = QCOM_SCM_PERM_RW,
|
||||
};
|
||||
u64 src = BIT(QCOM_SCM_VMID_HLOS);
|
||||
|
||||
return qcom_scm_assign_mem(addr, ALIGN(size, SZ_4K), &src, &dest, 1);
|
||||
}
|
||||
|
||||
/* MSS → HLOS exclusively (panics on failure in LOS — they're serious) */
|
||||
static int los_assign_to_linux(struct q6v5 *qproc,
|
||||
phys_addr_t addr, size_t size)
|
||||
{
|
||||
struct qcom_scm_vmperm dest = {
|
||||
.vmid = QCOM_SCM_VMID_HLOS,
|
||||
.perm = QCOM_SCM_PERM_RWX,
|
||||
};
|
||||
u64 src = BIT(QCOM_SCM_VMID_MSS_MSA);
|
||||
|
||||
return qcom_scm_assign_mem(addr, ALIGN(size, SZ_4K), &src, &dest, 1);
|
||||
}
|
||||
|
||||
/* HLOS → HLOS+MSS shared */
|
||||
static int los_assign_shared(struct q6v5 *qproc,
|
||||
phys_addr_t addr, size_t size)
|
||||
{
|
||||
struct qcom_scm_vmperm dest[2] = {
|
||||
{ .vmid = QCOM_SCM_VMID_HLOS, .perm = QCOM_SCM_PERM_RW },
|
||||
{ .vmid = QCOM_SCM_VMID_MSS_MSA, .perm = QCOM_SCM_PERM_RW },
|
||||
};
|
||||
u64 src = BIT(QCOM_SCM_VMID_HLOS);
|
||||
|
||||
return qcom_scm_assign_mem(addr, ALIGN(size, SZ_4K), &src, dest, 2);
|
||||
}
|
||||
|
||||
/* HLOS+MSS → target_vmid exclusively (the final handoff) */
|
||||
static int los_reclaim_mem(struct q6v5 *qproc,
|
||||
phys_addr_t addr, size_t size, int target_vmid)
|
||||
{
|
||||
struct qcom_scm_vmperm dest = {
|
||||
.vmid = target_vmid,
|
||||
.perm = (target_vmid == QCOM_SCM_VMID_HLOS)
|
||||
? QCOM_SCM_PERM_RWX : QCOM_SCM_PERM_RW,
|
||||
};
|
||||
u64 src = BIT(QCOM_SCM_VMID_HLOS) | BIT(QCOM_SCM_VMID_MSS_MSA);
|
||||
|
||||
return qcom_scm_assign_mem(addr, ALIGN(size, SZ_4K), &src, &dest, 1);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* THE BOOT SEQUENCE (downstream pil_boot + pil_msa_mss_ops_selfauth) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/*
|
||||
* This is the FULL modem cold-boot path on LOS blueline, translated
|
||||
* to mainline style. It's called like mainline's q6v5_start().
|
||||
*
|
||||
* Downstream splits this across:
|
||||
* pil_boot() → ops->init_image() → ops->mem_setup() →
|
||||
* segment load → ops->auth_and_reset()
|
||||
*
|
||||
* where init_image = pil_msa_mss_reset_mba_load_auth_mdt()
|
||||
* mem_setup = pil_mss_mem_setup() [no-op on SDM845]
|
||||
* auth_and_reset = pil_msa_mba_auth()
|
||||
*/
|
||||
static int los_q6v5_start(struct rproc *rproc)
|
||||
{
|
||||
struct q6v5 *qproc = rproc->priv;
|
||||
dma_addr_t mba_phys;
|
||||
void *mba_virt;
|
||||
dma_addr_t mdata_phys;
|
||||
void *mdata_virt;
|
||||
int ret;
|
||||
|
||||
/* ============================================================
|
||||
* PHASE 1: Load MBA (from pil_mss_reset_load_mba)
|
||||
* ============================================================ */
|
||||
|
||||
/*
|
||||
* [DELTA] MBA buffer: downstream allocates a DMA buffer from
|
||||
* general DDR. Mainline uses the static reserved-memory node
|
||||
* mba@98500000 (2 MB). Both work for MBA load, but TZ may
|
||||
* treat them differently for hyp-assign. On blueline, the
|
||||
* mainline approach works (MBA boots, no -22).
|
||||
*/
|
||||
mba_virt = dma_alloc_attrs(qproc->dev, SZ_1M, &mba_phys,
|
||||
GFP_KERNEL, DMA_ATTR_SKIP_ZEROING);
|
||||
|
||||
memcpy(mba_virt, mba_fw->data, mba_fw->size);
|
||||
wmb();
|
||||
|
||||
/* Load debug policy if present (same in both) */
|
||||
/* [SAME] */
|
||||
|
||||
/*
|
||||
* [SAME] Assign MBA buffer to MSS so PBL can read it.
|
||||
* Downstream: pil_assign_mem_to_subsys(mba_dp_phys, mba_dp_size)
|
||||
* Mainline: q6v5_xfer_mem_ownership(mba_perm, false, true, mba_phys, mba_size)
|
||||
*/
|
||||
if (qproc->need_mem_protection) {
|
||||
ret = los_assign_to_subsys(qproc, mba_phys, SZ_1M);
|
||||
if (ret)
|
||||
goto err_mba;
|
||||
}
|
||||
|
||||
/*
|
||||
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
* [DELTA — THE KEY DIFFERENCE]
|
||||
*
|
||||
* Downstream does NOT pre-assign the MPSS region here.
|
||||
* Mainline (before our fix) did:
|
||||
* q6v5_xfer_mem_ownership(mpss_perm, false, true,
|
||||
* mpss_phys, mpss_size);
|
||||
* This 120-152 MB exclusive HLOS→MSS assignment happens BEFORE
|
||||
* MBA boots, and blueline TZ may reject it (-22), causing the
|
||||
* XPU crash-loop.
|
||||
*
|
||||
* Our commit c2923cb49 gates this on (version != MSS_SDM845),
|
||||
* matching downstream. VERIFIED: modem boots clean on 7.1.1.
|
||||
* <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
*/
|
||||
|
||||
/* [SAME] Power up MSS, enable clocks, assert/deassert reset */
|
||||
ret = pil_mss_power_up(qproc);
|
||||
ret = pil_mss_enable_clks(qproc);
|
||||
pil_mss_assert_resets(qproc);
|
||||
udelay(200);
|
||||
pil_mss_deassert_resets(qproc);
|
||||
|
||||
/* [SAME] Program MBA address into RMB register */
|
||||
writel(mba_phys, qproc->rmb_base + RMB_MBA_IMAGE);
|
||||
mb();
|
||||
|
||||
/* [SAME] Bring Q6 out of reset */
|
||||
pil_q6v5_reset(qproc);
|
||||
|
||||
/* [SAME] Wait for PBL success, then XPU_UNLOCKED from MBA */
|
||||
ret = q6v5_rmb_pbl_wait(qproc, 1000);
|
||||
ret = q6v5_rmb_mba_wait(qproc, RMB_MBA_XPU_UNLOCKED, 1000);
|
||||
|
||||
dev_info(qproc->dev, "MBA boot done\n");
|
||||
|
||||
|
||||
/* ============================================================
|
||||
* PHASE 2: Auth modem metadata (from pil_msa_auth_modem_mdt)
|
||||
* ============================================================ */
|
||||
|
||||
/*
|
||||
* [DELTA] Metadata buffer: downstream allocates a DMA buffer,
|
||||
* copies metadata, assigns to MSS, programs RMB, waits for
|
||||
* META_DATA_AUTH_SUCCESS, then reclaims to Linux.
|
||||
*
|
||||
* Mainline (q6v5_mpss_init_image) does essentially the same
|
||||
* but uses the mpss-metadata reserved-memory region instead
|
||||
* of a DMA allocation. Functionally equivalent.
|
||||
*/
|
||||
mdata_virt = dma_alloc_attrs(qproc->dev, mdata_size, &mdata_phys,
|
||||
GFP_KERNEL, DMA_ATTR_SKIP_ZEROING);
|
||||
memcpy(mdata_virt, mdt_metadata, mdata_size);
|
||||
wmb();
|
||||
|
||||
if (qproc->need_mem_protection)
|
||||
los_assign_to_subsys(qproc, mdata_phys, mdata_size);
|
||||
|
||||
writel(0, qproc->rmb_base + RMB_PMI_CODE_LENGTH_REG);
|
||||
writel(mdata_phys, qproc->rmb_base + RMB_PMI_META_DATA_REG);
|
||||
writel(CMD_META_DATA_READY, qproc->rmb_base + RMB_MBA_COMMAND_REG);
|
||||
|
||||
ret = q6v5_rmb_mba_wait(qproc, RMB_MBA_META_DATA_AUTH_SUCCESS, 10000);
|
||||
|
||||
if (qproc->need_mem_protection)
|
||||
los_assign_to_linux(qproc, mdata_phys, mdata_size);
|
||||
|
||||
dma_free_attrs(qproc->dev, mdata_size, mdata_virt, mdata_phys, 0);
|
||||
|
||||
|
||||
/* ============================================================
|
||||
* PHASE 3: mem_setup (from pil_mss_mem_setup)
|
||||
* ============================================================ */
|
||||
|
||||
/*
|
||||
* [SAME] SDM845 does NOT set pil_mss_memsetup in DT.
|
||||
* Downstream: this function returns 0 immediately.
|
||||
* Mainline: need_pas_mem_setup = false for SDM845.
|
||||
* Both skip PAS_MEM_SETUP_CMD. No delta.
|
||||
*/
|
||||
|
||||
|
||||
/* ============================================================
|
||||
* PHASE 4: MPSS region assignment + segment loading
|
||||
* (from pil_boot lines 1063-1119)
|
||||
* ============================================================ */
|
||||
|
||||
/*
|
||||
* [SAME on cold boot, DELTA on SSR]
|
||||
*
|
||||
* Cold boot (modem_ssr == false):
|
||||
* - Skip the reclaim-to-linux (MPSS is already Linux-owned)
|
||||
* - Go straight to shared assignment
|
||||
*
|
||||
* SSR (modem_ssr == true):
|
||||
* - First reclaim: MSS→HLOS exclusively
|
||||
* - Then share: HLOS→HLOS+MSS
|
||||
*
|
||||
* Mainline does the reclaim unconditionally (lines 1504-1505)
|
||||
* but on cold boot it's a no-op because mpss_perm already
|
||||
* equals HLOS. So functionally equivalent on cold boot.
|
||||
*/
|
||||
if (modem_ssr) {
|
||||
/* SSR only: reclaim from previous boot */
|
||||
los_assign_to_linux(qproc, qproc->mpss_phys, qproc->mpss_size);
|
||||
}
|
||||
|
||||
/*
|
||||
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
* [SAME — this matches mainline line 1508]
|
||||
*
|
||||
* Share MPSS between HLOS and MSS during segment loading.
|
||||
* Downstream: pil_assign_mem_to_subsys_and_linux()
|
||||
* srcVM = [HLOS], destVM = [HLOS, MSS_MSA], perm = [RW, RW]
|
||||
* Mainline: q6v5_xfer_mem_ownership(mpss_perm, true, true, ...)
|
||||
* srcVM from current_perm, destVM = [HLOS, MSS_MSA]
|
||||
* Same SCM call. VERIFIED matching.
|
||||
* <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
*/
|
||||
ret = los_assign_shared(qproc, qproc->mpss_phys, qproc->mpss_size);
|
||||
if (ret)
|
||||
goto err_mpss;
|
||||
|
||||
/* [SAME] Load each MPSS segment into the shared region */
|
||||
for_each_segment(seg) {
|
||||
ptr = memremap(qproc->mpss_phys + offset, seg->memsz,
|
||||
MEMREMAP_WC);
|
||||
/* copy from firmware or zero-fill */
|
||||
memcpy(ptr, seg_data, seg->filesz);
|
||||
memset(ptr + seg->filesz, 0, seg->memsz - seg->filesz);
|
||||
memunmap(ptr);
|
||||
|
||||
/* [SAME] Program RMB for MBA to verify each segment */
|
||||
if (!code_length) {
|
||||
writel(boot_addr, qproc->rmb_base + RMB_PMI_CODE_START_REG);
|
||||
writel(RMB_CMD_LOAD_READY, qproc->rmb_base + RMB_MBA_COMMAND_REG);
|
||||
}
|
||||
code_length += seg->memsz;
|
||||
writel(code_length, qproc->rmb_base + RMB_PMI_CODE_LENGTH_REG);
|
||||
}
|
||||
|
||||
|
||||
/* ============================================================
|
||||
* PHASE 5: Final MPSS handoff + auth complete
|
||||
* (from pil_boot lines 1108-1127)
|
||||
* ============================================================ */
|
||||
|
||||
/*
|
||||
* >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
|
||||
* [DELTA — SUBTLE DIFFERENCE IN THE FINAL HANDOFF]
|
||||
*
|
||||
* Downstream: pil_reclaim_mem(region, subsys_vmid)
|
||||
* srcVM = [HLOS, MSS_MSA] (both listed explicitly)
|
||||
* destVM = [MSS_MSA]
|
||||
* This transfers from shared → MSS exclusive.
|
||||
*
|
||||
* Mainline: q6v5_xfer_mem_ownership(mpss_perm, false, true, ...)
|
||||
* srcVM = from current_perm (which is HLOS|MSS after the share)
|
||||
* destVM = [MSS_MSA]
|
||||
* Same result — but mainline tracks perm state in mpss_perm
|
||||
* while downstream hardcodes the srcVM.
|
||||
*
|
||||
* FUNCTIONALLY EQUIVALENT. The end state (MSS exclusive) is
|
||||
* the same. VERIFIED.
|
||||
* <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
|
||||
*/
|
||||
ret = los_reclaim_mem(qproc, qproc->mpss_phys, qproc->mpss_size,
|
||||
QCOM_SCM_VMID_MSS_MSA);
|
||||
if (ret)
|
||||
goto err_mpss;
|
||||
|
||||
/* [SAME] Wait for MBA to report authentication complete */
|
||||
ret = q6v5_rmb_mba_wait(qproc, RMB_MBA_AUTH_COMPLETE, 10000);
|
||||
|
||||
|
||||
/* ============================================================
|
||||
* PHASE 6: Reclaim MBA + cleanup
|
||||
* (from pil_msa_mba_auth)
|
||||
* ============================================================ */
|
||||
|
||||
/*
|
||||
* [DELTA] Downstream reclaims MBA DMA buffer: MSS→HLOS, then
|
||||
* frees the DMA allocation. Mainline reclaims the mba_region
|
||||
* reserved-memory. Same assign direction, different region type.
|
||||
*/
|
||||
if (qproc->need_mem_protection)
|
||||
los_assign_to_linux(qproc, mba_phys, SZ_1M);
|
||||
dma_free_attrs(qproc->dev, SZ_1M, mba_virt, mba_phys, 0);
|
||||
|
||||
return 0;
|
||||
|
||||
err_mpss:
|
||||
err_mba:
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================== */
|
||||
/* SUMMARY OF DELTAS */
|
||||
/* ================================================================== */
|
||||
|
||||
/*
|
||||
* 1. MBA BUFFER LOCATION
|
||||
* Downstream: dma_alloc_attrs() — general DDR, dynamic
|
||||
* Mainline: reserved-memory mba@98500000 — static carveout
|
||||
* Impact: None observed — MBA boots fine both ways.
|
||||
*
|
||||
* 2. MPSS PRE-ASSIGN (the one we fixed)
|
||||
* Downstream: NEVER assigns MPSS before MBA boot
|
||||
* Mainline: Was HLOS→MSS exclusively before MBA boot
|
||||
* Fix: c2923cb49 skips for MSS_SDM845
|
||||
* Status: VERIFIED WORKING — modem boots clean, no -22
|
||||
*
|
||||
* 3. METADATA BUFFER LOCATION
|
||||
* Downstream: dma_alloc_attrs() — general DDR
|
||||
* Mainline: reserved-memory mpss-metadata@bfffc000 (16 KB)
|
||||
* Impact: None observed.
|
||||
*
|
||||
* 4. rmtfs REGION SCM ASSIGN
|
||||
* Downstream: NO SCM call — uses qcom,sharedmem-uio, no TZ
|
||||
* Mainline: qcom_scm_assign_mem(0xf2700000, 0x202000,
|
||||
* HLOS→HLOS+MSS_MSA)
|
||||
* Impact: Assign succeeds on blueline (verified). But this
|
||||
* EXTRA TZ interaction doesn't exist downstream.
|
||||
*
|
||||
* 5. FASTRPC / SLPI VMIDs
|
||||
* Downstream: Not applicable (different fastrpc driver)
|
||||
* Mainline: qcom_scm_assign_mem for fastrpc heap with
|
||||
* [HLOS, SSC_Q6] — TZ rejects with -22
|
||||
* Impact: SLPI crash-loops. Independent of modem.
|
||||
*
|
||||
* 6. OPERATING MODE AT BOOT
|
||||
* Downstream: modem firmware enters LPM at boot
|
||||
* Mainline: modem firmware enters OFFLINE at boot
|
||||
* Impact: THIS IS THE REMAINING BLOCKER.
|
||||
* The driver-level boot sequence is now correct
|
||||
* (matching downstream ordering). The firmware
|
||||
* itself decides LPM vs OFFLINE based on... what?
|
||||
* NOT the driver — the assign ordering is matching.
|
||||
* Candidates:
|
||||
* - ABL/bootloader state (verified-boot chain)
|
||||
* - QCRIL "UI ready" handshake (rejected by firmware)
|
||||
* - Some EFS NV item the firmware checks at RF-init
|
||||
* - Subsystem coordination (SLPI PD state?)
|
||||
*
|
||||
* CONCLUSION:
|
||||
* The driver-level memory assignment is now correct. The modem boots
|
||||
* clean with zero XPU/assign errors. The remaining 52/offline blocker
|
||||
* is ABOVE the driver layer — in the modem firmware's own RF-init
|
||||
* decision logic.
|
||||
*/
|
||||
134
saf/device/evidence/modem_community_research_20260622.md
Normal file
134
saf/device/evidence/modem_community_research_20260622.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# Community research — blueline modem on mainline (2026-06-22)
|
||||
|
||||
Purpose: GLM's "cheap parallel" ask — is there a KNOWN blueline/SDM845 modem-on-mainline fix we'd
|
||||
otherwise rediscover? Done read-only (phone was on Android). Verdict + a new lead that reorders the
|
||||
frontier.
|
||||
|
||||
## Headline: there is no known blueline fix to rediscover
|
||||
- The **sdm845-mainline** project's actively-supported, modem-working devices are **OnePlus 6/6T
|
||||
(enchilada/fajita) and Pocophone F1 (beryllium)** — NOT the Pixel 3 generation.
|
||||
- **crosshatch (Pixel 3 XL — blueline's near-identical twin) modem is also NOT confirmed working** on
|
||||
mainline (kernel boots, SSH ok; WiFi/BT/modem are the open items). So this is a **Pixel-3-generation
|
||||
gap, not a blueline-unique one.** Nobody has shipped this modem on mainline.
|
||||
- Implication: don't wait on / search for a blueline-specific recipe — there isn't one. The play is to
|
||||
port the **enchilada** (known-good) path and isolate the **device delta**.
|
||||
|
||||
## The concrete bring-up chain (now confirmed from sources)
|
||||
Modem firmware **RF-init → reads radio CALIBRATION from EFS (`modemst1`/`modemst2`) → via `rmtfs` ↔
|
||||
the `qcom_rmtfs_mem` reserved-memory carveout**. kernel.org confirms `qcom_rmtfs_mem`'s client "is
|
||||
generally found in the modem firmware and is used for accessing persistent storage for things such as
|
||||
**radio calibration**." `modemst1/2` hold the NV/cal the modem needs to come online.
|
||||
|
||||
### Why this reorders the frontier (the new lead)
|
||||
GLM's strongest surviving lead is **Route B: a modem-side EFS NV item** (mcfg_autoselect_by_uim /
|
||||
power-up op-mode), to be read over **DIAG**. But DIAG NV-read uses the **AP↔modem DIAG channel** — a
|
||||
*different* path than the modem's **own rmtfs-mediated EFS access during RF-init**. So:
|
||||
|
||||
> The NV *content* can be byte-perfect (it is — shared EFS) and the modem can STILL park offline if the
|
||||
> **rmtfs ↔ qcom_rmtfs_mem ↔ EFS** bridge is broken/misconfigured on pmOS. That bridge is **upstream**
|
||||
> of the NV-content question and is exactly the kind of device-specific thing that's wrong on an
|
||||
> untested device tree.
|
||||
|
||||
This is consistent with everything GLM proved equal (firmware, EFS bytes, configs, stack, DT
|
||||
"byte-identical to upstream") — because **upstream blueline DT modem support is itself unproven**
|
||||
(nobody got the modem up), so a wrong/incomplete `qcom,rmtfs-mem` carveout or modem `memory-region`
|
||||
would not have been caught.
|
||||
|
||||
## What enchilada (works) does that we should check against blueline
|
||||
- Ships a curated **`firmware-oneplus-sdm845`** set; modem cal/region files matter (VoLTE needs extra
|
||||
carrier config files).
|
||||
- **`rmtfs-copy-files` (pmaports MR !4674, draft):** the modem WRITES to its EFS at runtime and those
|
||||
writes must persist; they copy the partitions at boot and write changes back. EFS is "a black box —
|
||||
a diff before/after shows almost every byte changed." (We share EFS unslotted, so reads are fine;
|
||||
but note the modem expects a writable EFS path via rmtfs.)
|
||||
- Uses **`hexagonrpcd`** (open Hexagon DSP RPC) — DSP/sensors, but part of the enchilada stack.
|
||||
- Voice/VoLTE historically needed **provisioning from a specific OxygenOS build first** — i.e. the
|
||||
working modem was put in a known EFS/NV state from Android before pmOS. (Rhymes with Route B.)
|
||||
|
||||
## Proposed instruments (complementary to GLM's DIAG NV probe — do on the SAME pmOS boot)
|
||||
All mostly read-only; one clean boot.
|
||||
|
||||
1. **Verify the rmtfs/carveout chain is actually bridging during the failed RF-init** (NEW, highest value):
|
||||
- Is `qcom_rmtfs_mem` reserved-memory present and at the address the modem firmware expects?
|
||||
`cat /proc/iomem | grep -i rmtfs`, `dmesg | grep -i rmtfs`, check the DT `qcom,rmtfs-mem` node.
|
||||
- Is `rmtfs` serving `modemst1/2`? Run it foreground/verbose or `strace -f` during modem boot:
|
||||
does the modem issue rmtfs read requests for the EFS, and do they **succeed**?
|
||||
- modem issues reads that **fail** → that's the gate (wrong partition map / perms).
|
||||
- modem issues reads that **succeed** but still offline → bridge is fine, push to GLM's NV-content + SSR reason.
|
||||
- modem issues **no** rmtfs reads → RF-init never reaches EFS; gate is earlier (carveout/glink/firmware load).
|
||||
- Cross-check rmtfs invocation/flags vs enchilada's (`device-oneplus-enchilada` + its rmtfs service).
|
||||
2. **GLM's DIAG NV read** (mcfg_autoselect_by_uim, power-up op-mode) via edl-pixel `qc_diag` + nvitems.xml — still worth it, but now interpreted as "content check," with #1 as the bridge check.
|
||||
3. **Forced modem SSR + devcoredump/minidump for the firmware's own failure reason** (escalation):
|
||||
- `echo enabled > /sys/kernel/debug/remoteproc/remoteprocN/coredump`, trigger SSR via
|
||||
`/sys/kernel/debug/msm_subsys/modem` or a failed online, then read `/sys/class/devcoredump/devcdN/data`.
|
||||
- Also check `sdm845-mainline/pmtools` for an existing diag/modem helper (per diag_capture.md note).
|
||||
|
||||
## Open question worth one cheap action
|
||||
Does **crosshatch** (the twin) hit the **identical** DeviceNotReady(52)? If yes, it's confirmed a
|
||||
family/DT-level gap and any crosshatch progress is directly portable. A targeted ask in
|
||||
sdm845-mainline (Caleb Connolly / Joel Selvaraj orbit) about Pixel-3-gen modem status would settle it
|
||||
without us reverse-engineering blind. (The DeviceNotReady QMI-52 itself is generic — the modemmanager
|
||||
list thread on it reached no root cause; it just means "modem firmware not in a ready state," which
|
||||
points back to RF-init/cal, i.e. the chain above.)
|
||||
|
||||
## Sources
|
||||
- [SDM845 Mainlining — postmarketOS Wiki](https://wiki.postmarketos.org/wiki/SDM845_Mainlining)
|
||||
- [OnePlus 6 (oneplus-enchilada) — postmarketOS Wiki](https://wiki.postmarketos.org/wiki/OnePlus_6_(oneplus-enchilada))
|
||||
- [Pixel 3 XL (google-crosshatch) — postmarketOS Wiki](https://wiki.postmarketos.org/wiki/Google_Pixel_3_XL_(google-crosshatch))
|
||||
- [pmaports MR !4674 — modem/rmtfs: add rmtfs-copy-files](https://gitlab.com/postmarketOS/pmaports/-/merge_requests/4674)
|
||||
- [pmaports MR !1640 — make modem work on downstream again](https://gitlab.com/postmarketOS/pmaports/-/merge_requests/1640)
|
||||
- [kernel.org — qcom,rmtfs-mem.txt binding](https://www.kernel.org/doc/Documentation/devicetree/bindings/reserved-memory/qcom,rmtfs-mem.txt)
|
||||
- [linux-msm/rmtfs](https://github.com/linux-msm/rmtfs)
|
||||
- [Caleb Connolly — state of mainline on SDM845 (5.14)](https://connolly.tech/posts/2021_07_20-sdm845-mainline-5.14/)
|
||||
- [FOSDEM 2022 — From Android to mainline on the Snapdragon 845 (slides)](https://archive.fosdem.org/2022/schedule/event/mobile_kernel_snapdragon_845/)
|
||||
- [sdm845-mainline GitLab group](https://gitlab.com/sdm845-mainline)
|
||||
- [crosshatch mainlining attempt (#153)](https://gitlab.com/postmarketOS/pmaports/-/issues/153)
|
||||
- [modemmanager-devel — DeviceNotReady on set operating mode](https://www.mail-archive.com/modemmanager-devel@lists.freedesktop.org/msg07101.html)
|
||||
|
||||
---
|
||||
|
||||
## pmOS-side LIVE RESULTS (2026-06-22 boot, slot A, kernel 7.1.0-rc1) — instruments run, doors closed
|
||||
|
||||
Ran the proposed battery on a fresh pmOS boot. Results eliminate the rmtfs lead AND both pmOS deep-probe paths, and corroborate GLM's reframe from a second angle.
|
||||
|
||||
1. **rmtfs bridge: HEALTHY this boot — eliminated as the gate.** `rmtfs -P -s` (pid 697) has every link
|
||||
open: `/dev/qcom_rmtfs_mem1` (fd5), `remoteproc2/state` (fd6), `modemst1`/`modemst2` (sdf3/sdf4),
|
||||
`fsg`/`fsc` (sdf5/sdf6), and OEM copies in `/var/lib/rmtfs/`. Carveout `rmtfs-region@f2700000`
|
||||
present. The `Failed to get rprocfd` crashes are *other* boots, not this one. **Modem reads its EFS
|
||||
via rmtfs successfully and still parks offline** → bridge is not the gate.
|
||||
2. **DIAG unavailable on pmOS** — no `/dev/diag`, no diagchar module (mainline doesn't ship it, as
|
||||
diag_capture.md warned). **GLM's Route-B DIAG NV read cannot run on pmOS.** It's Android-only.
|
||||
3. **SIM/UIM fully ready** — `uim-get-card-status`: card present, USIM app `ready`, Primary GW
|
||||
provisioned (slot 1 / app 1), PIN1 disabled. Reads work.
|
||||
4. **NEW corroborating signal — modem refuses STATE CHANGES, serves all READS:**
|
||||
- `dms-set-operating-mode=online` → instant `DeviceNotReady (52)` (clean QMI: Alloc CID → Set Op
|
||||
Mode → FAILURE → Release CID; modem replies immediately, does not hang).
|
||||
- `uim-change-provisioning-session` (what `msm-modem-uim-selection` runs) → **Transaction timed out**,
|
||||
repeatably, even with modem fully up.
|
||||
- vs reads (card status, op-mode get, qrtr service list) → all succeed instantly.
|
||||
- The modem registered its QMI services on qrtr (NAS, Voice, PDC, **Modem EFS svc 21**, etc.).
|
||||
5. **devcoredump path yields nothing** — armed `remoteproc2/coredump=enabled`, attempted online: modem
|
||||
does NOT crash/SSR, no `devcdN`, no SMEM failure string, no dmesg modem reaction. **There is no crash
|
||||
reason to harvest** — the modem is in a stable, deliberate "offline / not RF-ready" state.
|
||||
6. ModemManager is **masked** (intentional, for manual qmicli testing). Modem not poisoned by the
|
||||
attempt (clean refusal); phone still usable on pmOS.
|
||||
|
||||
### What this leaves (frontier now)
|
||||
Every STATIC AP-side layer is proven equal/healthy: firmware loads, rmtfs serves EFS, EFS bytes shared
|
||||
& identical to Android, SIM ready, configs active (GLM), DT byte-identical. The modem firmware itself
|
||||
decides RF-init is incomplete and **fast-refuses every state transition** while answering queries — and
|
||||
exposes no reason on pmOS (no DIAG, no crash). So a *static* NV/EFS diff is NULL (confirmed direction)
|
||||
and the deep-probe instruments are exhausted on pmOS.
|
||||
|
||||
**The remaining differentiator is RUNTIME: the QMI choreography Android's `qcril` performs before/around
|
||||
going online** that pmOS (cold `set-operating-mode=online`) never does. The qcril strings we pulled show
|
||||
exactly this shape — `INTERNAL_MMGSDI_CARD_POWER_UP`, indication-register / set-event-report across
|
||||
DMS/NAS/UIM, `qcril_qmi_nas_trigger_polling_operating_mode`, `POST_OPRT_ONLINE_ACTION`, the bootup
|
||||
optimization state machine. The `uim-change-provisioning-session` timeout is plausibly part of the same
|
||||
missing handshake.
|
||||
|
||||
### Next action (the fix path, not another probe)
|
||||
Decompile the pulled `qcrild` / `libril-qc-hal-qmi.so` (`android-reference/vendor-stack-20260621/`) to
|
||||
extract the exact pre-online QMI request sequence, then **replay that choreography on pmOS over
|
||||
libqmi/qmicli before `set-operating-mode=online`** (the qcom-imsd / provisioner pattern). That is the
|
||||
discriminating *and* corrective experiment; the read-only probes have told us all they can.
|
||||
76
saf/device/evidence/modem_community_research_20260626.md
Normal file
76
saf/device/evidence/modem_community_research_20260626.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# PAF · Community research + reconciliation (2026-06-26)
|
||||
|
||||
Prior-art sweep run against our live evidence from this session (the modem
|
||||
XPU-violation crash-loop = the 52; see modem.md TOP LEAD #11). Conclusion up
|
||||
front: **the community's known fix does NOT match our failure mode; our wall
|
||||
(secure-load / XPU) is likely uncharted in the public SDM845 mainline ecosystem.**
|
||||
|
||||
## What the evidence RULES OUT
|
||||
|
||||
- **tqftpserv `rsize`/`seek` MCFG bug** (the Fairphone 4 / SM7225 unblock).
|
||||
- Refs: ModemManager issue #484
|
||||
(gitlab.freedesktop.org/mobile-broadband/ModemManager/-/issues/484);
|
||||
tqftpserv PR #13 (github.com/andersson/tqftpserv/pull/13); pmaports !4747 / #2550.
|
||||
- Their modem fetched MCFG with `rsize=`/`seek=` TFTP options that an old
|
||||
tqftpserv mis-served → modem never RF-inits → permanent DeviceNotReady(52).
|
||||
- **NOT us:** our tqftpserv RRQ lines show `rsize=0 seek=0`, and tqftpserv is
|
||||
actively serving `/readwrite/mcfg.tmp`. Transport works. Our modem also does
|
||||
NOT sit stably offline — it **crash-loops** (theirs stayed up and refused).
|
||||
|
||||
## What the evidence RULES IN / leaves open
|
||||
|
||||
- **pd-mapper / MPSS protection domains.** Research: SDM845 needs rmtfs +
|
||||
pd-mapper + tqftpserv; pd-mapper brokers `mpss_root_pd` / `mpss_wlan_pd`.
|
||||
- Refs: in-kernel pd-mapper v9 (mail-archive linux-kernel msg2570727);
|
||||
LWN 973399; RFC linux-remoteproc msg00281 (modemuw.jsn side-load).
|
||||
- **Our state:** userspace `pd-mapper` = `inactive / not-found` (likely not
|
||||
installed). Kernel PDR framework is partly alive (`PDR: Indication …
|
||||
adsp/audio_pd`). OPEN next-session check: does the **MPSS** protection domain
|
||||
actually register in-kernel, or is the modem's PD coordination missing? PD =
|
||||
the secure-memory layer the XPU violation lives in, so this is the lead that
|
||||
fits our evidence.
|
||||
- Note: no public source explicitly ties pd-mapper absence to DeviceNotReady(52)
|
||||
or to an XPU violation — inference, not documented.
|
||||
|
||||
- **Platform MCFG missing (0 platform configs, 25 software, Rogers active).**
|
||||
- openimsd `qcom-baseband-profile-manager` (openimsd.com; NLnet VoLTE-Qualcom
|
||||
grant) selects among EXISTING software configs — it does not manufacture a
|
||||
missing platform/HW MCFG. OnePlus 6 ships `firmware-oneplus-sdm845` with
|
||||
factory MCFG extracted from Android; blueline's EFS lacks the platform MCFG.
|
||||
- **No prior art** for the zero-platform-MCFG case or `--pdc-load-config` of a
|
||||
platform MBN from scratch. Possibly novel. But per our timeline the modem
|
||||
crashes (XPU) *before* the platform-MCFG layer matters, so this is downstream
|
||||
of the secure-load fault, not the first blocker.
|
||||
|
||||
## Confirmed external bugs (not ours to fix)
|
||||
|
||||
- **`qmicli --pdc-list-configs` segfault = upstream libqmi issue #24**
|
||||
(gitlab.freedesktop.org/mobile-broadband/libqmi/-/issues/24, since 1.24.0,
|
||||
segfaults in PDC response cleanup AFTER printing). Not the openimsd fork's
|
||||
fault. Workaround: ignore exit code / `2>&1`. Watch libqmi git log on
|
||||
`qmi-pdc.c` cleanup for the fix version.
|
||||
- **PDC Set Feature 0x2E timeout** — zero public reports. Inferred cause: PDC
|
||||
service present on QRTR but not processing because the modem hasn't finished
|
||||
init (consistent with our crash-loop). Not independently a blocker.
|
||||
|
||||
## Net direction (for next session)
|
||||
|
||||
The 52 is a secure-load / TZ-XPU problem unique enough that the community has no
|
||||
recipe. Concrete next experiments, cheapest first:
|
||||
1. Confirm whether **MPSS protection domain** registers in-kernel; if not, try
|
||||
installing/running userspace `pd-mapper` (apk add pd-mapper) and re-capture
|
||||
whether the XPU violation changes.
|
||||
2. Pull the **`qcom_scm_assign_mem` -22** thread: diff LOS pil-tz secure-load
|
||||
region/VMID setup (mba `qcom,mba-mem`, mpss assign) vs mainline `q6v5_mpss`
|
||||
on this device — the XPU violation means the modem's secure regions aren't
|
||||
assigned the way TZ expects.
|
||||
3. Verify whether 6.16.7 *also* XPU-crash-loops — but NOT as a regression test.
|
||||
**Correction (Casey, 2026-06-26):** no mainline kernel ever had a working modem.
|
||||
6.16.7 "booted + QMI registered" (lead #10) but never went online either — that
|
||||
is NOT a working-modem baseline. The ONLY known-working modem state is
|
||||
**LineageOS on its downstream kernel** (PAF provenance: `msm-4.9` 4.9.337, slot B
|
||||
— Casey recalls it as ~4.x; the substance is *downstream LOS, not mainline*).
|
||||
So the only valid parity yardstick is **mainline vs LineageOS**, never
|
||||
6.16-vs-7.1.1. Checking whether 6.16.7 XPU-loops is still useful (tells us if the
|
||||
XPU fault is mainline-wide vs 7.1.1-specific), but frame it as a mainline-wide
|
||||
question, not "did we regress from a working 6.16."
|
||||
311
saf/device/evidence/modem_consumer_chain_20260626.md
Normal file
311
saf/device/evidence/modem_consumer_chain_20260626.md
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
# PAF · The modem userspace CONSUMER CHAIN — why the modem stays OFFLINE (2026-06-26, Opus)
|
||||
|
||||
> ## SESSION-3 UPDATE (2026-06-27, Opus) — SLPI SAR CRASH LOOP **FIXED**; ~~PROVEN the SLPI does NOT gate the modem 52~~
|
||||
>
|
||||
> ### TL;DR
|
||||
> Built the missing **AP-side SNS registry provider** (a patched `hexagonrpcd`) that feeds the
|
||||
> SLPI its sensor registry over FastRPC `apps_std`. The SLPI's `sar.cc:27:chre_utils fatal`
|
||||
> crash-loop (150+ crashes/boot) is **broken — SLPI now stays up indefinitely** (0 crashes over
|
||||
> multiple 25–40s windows). **Then enabled the modem anyway → still `QMI error 52
|
||||
> DeviceNotReady`.** So the SLPI/SAR subsystem is **independent of** the modem-52 wall. That
|
||||
> question (could not be answered by *disabling* SLPI — only by making it actually work) is now
|
||||
> settled: **error 52 is not SLPI-gated.**
|
||||
>
|
||||
> **🟠 SUPERSEDED 2026-06-29 (Casey) — this conclusion is INVALID.** The "SLPI fixed / 0 crashes / stays
|
||||
> up" above was only *remoteproc-not-crashing*: this doc's own body (search *"never exposes an SNS QMI
|
||||
> service"*) shows the SLPI firmware never completed CHRE init / never exposed SNS QMI — i.e. **not a
|
||||
> functioning sensor stack.** The reasoning "disabling couldn't answer it, only making it work can" was
|
||||
> correct — but "making it work" was never achieved, only "stopped crashing." If the modem's RF-init
|
||||
> queries the SLPI and gets silence from a never-initialized sensor stack, it parks OFFLINE regardless.
|
||||
> The SLPI is a **candidate root-cause**, NOT proven-independent. See `slpi.md` top banner + `HANDOFF.md`.
|
||||
>
|
||||
> ### HOW the SLPI was fixed (the real consumer chain Android calls `sscrpcd`)
|
||||
> The SLPI boots → makes FastRPC **reverse** calls (`apps_std` fopen/fread) back to the AP to read
|
||||
> `/persist/sensors/registry/...`. pmOS shipped **no AP-side listener**, so the registry sensor
|
||||
> starved and the SAR (sx932x) CHRE nanoapp fataled → crash loop. The pmOS-native listener
|
||||
> **`hexagonrpcd`** (linux-msm, already in the repo, `-s` = attach sensorspd on `/dev/fastrpc-sdsp`)
|
||||
> is the right tool but had two gaps:
|
||||
> 1. **Ran as `User=fastrpc`** → couldn't read the persist registry files. Fixed with a systemd
|
||||
> drop-in `User=root` (`/etc/systemd/system/hexagonrpcd-sdsp.service.d/10-root.conf`).
|
||||
> 2. **`construct_root_dir()` (rpcd_builder.c) only mapped `registry/registry/*`** — its virtual
|
||||
> FS had **no nodes** for the top-level `sns_reg_config` / `sns_reg_version` / `sns_reg_ctrl`
|
||||
> that blueline's SLPI requests at `/persist/sensors/registry/<file>`. hexagonfs is node-based
|
||||
> (no real-FS fallthrough), so those ENOENT'd no matter what was on disk. **Patched** to add
|
||||
> those three nodes (`hfs_map_or_empty`). NOTE: `hfs_map_or_empty` presents a missing physical
|
||||
> file as an empty **directory** (`S_IFDIR`), which still fails a *file* open — so the physical
|
||||
> file must EXIST (even 0 bytes). `sns_reg_version` is absent from the Android dump → `touch` an
|
||||
> empty one and it serves as a regular empty file, which satisfies the SLPI.
|
||||
> 3. Registry DATA staged to **`/usr/share/qcom/sensors/registry/`** (hexagonrpcd's default `-R`
|
||||
> root; the 177 groups from the LineageOS persist dump + `sns_reg_config`, empty `sns_reg_version`,
|
||||
> `sns_reg_ctrl`). Persist files also `chmod -R a+rX` (they were `0600 casey:casey` from a tar
|
||||
> restore).
|
||||
>
|
||||
> ### STATE OF THE FIXES — now converted from live temps to REAL in-code fixes (2026-06-27)
|
||||
> All of the live drop-in/temp changes have been turned into aport sources so a clean
|
||||
> `pmbootstrap install` reproduces them (idempotent fresh + upgrade). What's in code now:
|
||||
> - **hexagonrpcd patch → local aport** `pmaports/temp/hexagonrpcd/` (fork of Alpine community
|
||||
> aport, `pkgrel=4`, adds `hexagonrpcd-blueline-registry-nodes.patch`, checksums regenerated).
|
||||
> `pmbootstrap build hexagonrpcd` produces the patched binary instead of the live hand-installed
|
||||
> `/usr/bin/hexagonrpcd` overwrite.
|
||||
> - **root drop-in** → `device-google-blueline/hexagonrpcd-sdsp-root.conf`, installed to
|
||||
> `/usr/lib/systemd/system/hexagonrpcd-sdsp.service.d/10-root.conf` (APKBUILD `package()`).
|
||||
> - **registry data** → NOT shipped as blobs. `blueline-sns-registry-stage.sh` +
|
||||
> `.service` (oneshot, `Before=hexagonrpcd-sdsp`, ordered after persist mount) stage the registry
|
||||
> from the device's OWN `/mnt/vendor/persist/sensors/registry` into `/usr/share/qcom/sensors/registry`
|
||||
> (hexagonrpcd's served root; persist layout is off-by-one for a single `-R`). Touches the empty
|
||||
> `sns_reg_version`. Idempotent, ships no proprietary calibration.
|
||||
> - **service enable** → preset enables `blueline-sns-registry-stage.service` + `hexagonrpcd-sdsp.service`.
|
||||
> - **persist perms**: handled by the stage script's `chmod -R a+rX` on the served copy; the persist
|
||||
> `chmod` I did live is NOT needed by the real fix (hexagonrpcd reads `/usr/share/qcom`, not persist).
|
||||
>
|
||||
> **REMAINING build/verify steps (need device + reflash — do NOT half-run unattended):**
|
||||
> 1. `pmbootstrap build hexagonrpcd` (local aport) + `pmbootstrap build device-google-blueline`.
|
||||
> 2. Install/upgrade on device (`pmbootstrap` sideload or apk add the built .apk) — replaces the
|
||||
> hand-installed binary + lands the drop-in/stage-service/preset.
|
||||
> 3. Reboot clean and VERIFY idempotency: SLPI stays up (0 `crash #` delta), provider active,
|
||||
> `blueline-sns-registry-stage` populated `/usr/share/qcom/sensors/registry`.
|
||||
> 4. Revert the hand-installed temps once the packages own them: restore `/usr/bin/hexagonrpcd.orig`
|
||||
> is NOT needed (apk upgrade overwrites), remove the manual `systemctl enable` (preset handles it).
|
||||
> - **Kernel DT vmid change** (`sdm845.dtsi` sdsp fastrpc `qcom,vmids = <HLOS SSC_Q6>`): I hand-patched
|
||||
> the DTB into boot_a to test the TZ hyp-assign theory. LOGGED RESULT: with `<3 5>` live (confirmed
|
||||
> via `dtc` decompile of `/sys/firmware/fdt`), `dmesg` still prints
|
||||
> `SDSP fastrpc hyp-assign rejected by TZ (-22); registering node without reserved-heap assignment`
|
||||
> (32 occurrences). So the vmid change did NOT change the TZ result — the crash fix was the registry
|
||||
> serving, not the heap. The change is harmless but unnecessary.
|
||||
> - Orig boot.img backups: scratchpad `boot.img` + `/tmp/boot-backup_a.img` on device.
|
||||
> - NOTE on a reboot during testing: `lsusb` showed `18d1:d001` and SSH returned `Network is
|
||||
> unreachable`; root cause was the HOST iface `enp0s20f0u6 DOWN` (per `ip -br addr`), fixed by
|
||||
> `ip link set up` + `ip addr add 172.16.42.2/24` → phone answered, `uptime` = up 2 min. The
|
||||
> phone had booted normally; it was a host USB-net enumeration issue, not a device boot failure.
|
||||
>
|
||||
> ### THE REMAINING WALL — modem `QMI 52 DeviceNotReady`, now provably INDEPENDENT of SLPI
|
||||
> With SLPI fully up + sensors registry served, `mmcli -m 0 -e` still fails at DMS
|
||||
> `set-operating-mode` with **QMI 52**. modem0 enumerates (QUALCOMM, SIM present, slot 2 active),
|
||||
> sits `disabled / power off`, enable → `enabling → (QMI 52) → disabled`. The MSS remoteproc
|
||||
> (`4080000.remoteproc`) is `running`. LOGGED: `mmcli -m 0 -e` → `state changed (disabled ->
|
||||
> enabling)`, `couldn't enable interface: 'Couldn't set operating mode: QMI protocol error (52):
|
||||
> DeviceNotReady'`, `state changed (enabling -> disabled)`. So 52 comes from the modem path, with the
|
||||
> SLPI fully up — the two are independent.
|
||||
> **Scope note:** the goal is the WHOLE device, not just telephony. The SLPI/sensors fix is a real
|
||||
> bringup win on its own (sensors infra now functional). For the 52 specifically, the SLPI is not the
|
||||
> lever. **Next session for 52: modem NV/EFS/identity** (the `model 0` / LOW_POWER hypothesis from
|
||||
> session 2), rmtfs/EFS sync, or modem firmware-init.
|
||||
>
|
||||
> ### Open loose end (minor): hexagonrpcd reconnect churn
|
||||
> Provider shows `activating` with ~18 restarts/min — it exits on FastRPC "Broken pipe" when the
|
||||
> SLPI closes the session after serving, then `Restart=always` reattaches. SLPI stays up regardless
|
||||
> (0 crashes), so it's cosmetic, but worth a `RestartSec`/keepalive tweak later.
|
||||
>
|
||||
> ---
|
||||
>
|
||||
> ## SESSION-2 UPDATE (2026-06-26 late, Opus) — CHAIN BUILT + WORKING; wall is now the modem 52 / SLPI-SAR firmware crash
|
||||
>
|
||||
> ### What got BUILT this session (all in pmaports `device/testing/device-google-blueline/`, deployed live + wired into APKBUILD/preset — idempotent)
|
||||
> The whole consumer chain now comes up automatically on a clean boot and ModemManager
|
||||
> starts + creates the modem. Verified on hardware.
|
||||
> 1. **`modem-qrtr-ready.service` + `modem-qrtr-ready.sh`** (NEW) — the pmOS equivalent of
|
||||
> Android's `wait_for_prop vendor.qcom.devup 1`. Blocks until modem QMI answers and is stable
|
||||
> (2 probes/3s) before releasing consumers. Killed the boot race
|
||||
> (`node with id 0 not found in QRTR bus`). VERIFIED: gate waits, then releases.
|
||||
> 2. **UIM-selection fix** — `blueline-uim-select.sh` (NEW) wraps the stock binary: it selects the
|
||||
> SIM app fine but then a trailing QMI op times out (SLPI/XPU churn) and exits 1 — a false
|
||||
> negative that failed MM's hard `Requires=`. Wrapper treats "app selected" as success.
|
||||
> `uim-wait.conf` rewritten: gate on modem-qrtr-ready, Type=oneshot, run the wrapper. VERIFIED
|
||||
> `msm-modem-uim-selection` now = active.
|
||||
> 3. **`qcril-prov.py` — fully fixed** (was crashing on `out.get_configs()`):
|
||||
> - `list_configs` + `get_selected_config` reworked to read the PDC **indication**
|
||||
> (`IndicationPdc*Output.get_configs()/get_active_id()`), not the response.
|
||||
> - GI array-length args fixed (`set_id(id)`, `set_type_with_id_v2(t,id)`).
|
||||
> - Targets configs by **sha1(.mbn) == PDC config id**; selects+activates HW + Rogers SW.
|
||||
> - **Skip-if-already-active**: checks get_selected_config; if the inherited config is already
|
||||
> active, skips select/activate → **0 modem restarts** (was 2). VERIFIED:
|
||||
> "HW/SW already SELECTED+ACTIVE — no restart".
|
||||
> - **Deadlock fix**: exits 0 after provisioning even if RF not armed (RF arming is MM's job,
|
||||
> and MM is gated behind this service — requiring RF here deadlocked). VERIFIED exit 0.
|
||||
> 4. **`blueline-dpm-open-port.service` / `qcril-prov.service`** — gated on modem-qrtr-ready + retry.
|
||||
> 5. Kernel: mem-protect skip reverted (`980c4ae1f`); on clean baseline 7.1.1; `verify-fixes.sh` green.
|
||||
>
|
||||
> ### THE WALL NOW (single, precise)
|
||||
> Full chain runs → **ModemManager starts, creates `modem0`** (qcom-soc plugin, 3 ports), reads the
|
||||
> SIM ATR, goes `disabled → enabling` → calls set-operating-mode → **`QMI error 52 DeviceNotReady`**
|
||||
> → falls back to `disabled`. This is the SAME 52 from day one. The modem is **OFFLINE, model `0`**
|
||||
> (up-but-unprovisioned) and **refuses to enable even when driven correctly by MM**. So the entire
|
||||
> userspace plumbing is now proven-good; the wall is the modem itself refusing RF.
|
||||
>
|
||||
> ### SLPI / SAR — the suspected gate (Casey's thesis), what we proved
|
||||
> - SLPI **crash-loops `sar.cc:27:chre_utils fatal` every ~10s** (crash #15+). SAR = Semtech
|
||||
> **SX932x**, the modem's RF SAR/TX-power sensor.
|
||||
> - It is a **CHRE nanoapp crash INSIDE the SLPI firmware at init**, and **qrtr-lookup is empty —
|
||||
> the SLPI never exposes an SNS QMI service before it dies**. So an AP-side QMI "SAR consumer"
|
||||
> has nothing to attach to; the producer fatals first. A downstream subscribe-consumer is NOT
|
||||
> the missing piece.
|
||||
> - The real missing piece is whatever **feeds the SLPI its sensor registry / CHRE settings at
|
||||
> init**: the SAR registry data (`sx932x_0.sar`, `.config`, `_platform.*`) IS present at
|
||||
> `/persist/sensors/registry/registry/` but is **NOT delivered to the SLPI** (its RFS dir
|
||||
> `/persist/rfs/msm/slpi` is empty; tqftpserv serves only `/persist/rfs`, not `/sensors`).
|
||||
> - Android's consumer stack (proprietary, NO open-source equivalent):
|
||||
> **`chre_daemon_msm` + `sensors.qti` + `sscrpcd`** (in `android-reference/vendor-stack-.../vendor/bin/`),
|
||||
> plus the registry under `android-reference/persist-sensors-20260625/`. Our
|
||||
> `drivers/soc/qcom/qcom_ssc_sensors.c` is a **passive ABI shim** — it does NOT deliver the
|
||||
> registry or run CHRE, so it cannot prevent the firmware crash. This is the "shim lacking
|
||||
> components LineageOS has" Casey referenced.
|
||||
>
|
||||
> ### NEXT ANGLE (Casey's call — start here next session)
|
||||
> 1. **DECISIVE CHEAP TEST FIRST — is SAR actually the modem gate?** Stop the SLPI remoteproc so it
|
||||
> is NOT crash-looping (`echo stop > /sys/class/remoteproc/remoteproc2/state`), then let MM try
|
||||
> to enable the modem (or `qmicli --dms-set-operating-mode=online` once). If still **52** →
|
||||
> SLPI/SAR is NOT the modem gate (independent symptoms) → stop chasing SAR for telephony and look
|
||||
> elsewhere for the 52. If it **goes online** → the SLPI churn was the blocker and the SAR/CHRE
|
||||
> consumer becomes critical. **Run this before building anything.**
|
||||
> 2. If SAR IS the gate: the buildable "consumer" is a **registry/CHRE provider** that feeds the
|
||||
> SLPI at boot (reverse the SNS/CHRE QMI registry protocol; or port pieces; or boot SLPI
|
||||
> on-demand AFTER preparing its env, like LineageOS `sensors_ssc.c` does — it does NOT auto-boot
|
||||
> the SLPI raw). Investigate `chre_daemon_msm` / `sscrpcd` behaviour + how the registry reaches
|
||||
> the SLPI on Android (RFS path? SNS QMI? direct?).
|
||||
> 3. Independent of SAR: chase **why the modem boots OFFLINE/model-0** (vs Android's LOW_POWER).
|
||||
> model `0` = NV/identity not fully loaded — possible EFS/NV or firmware-init issue.
|
||||
> 4. Fallback: downstream-kernel hybrid (Droidian/Halium) — the realistic telephony path if mainline
|
||||
> SLPI/CHRE proves intractable.
|
||||
>
|
||||
> ### SAR consumer — EXACT mechanism (from slpi.mbn strings) + the build blocker
|
||||
> The SLPI firmware reveals precisely what the consumer must be:
|
||||
> - Reads registry from **`/persist/sensors/registry/registry`** (+ `sns_reg_config`, `sns_reg_version`).
|
||||
> - **`Sending registry request for group name:%s` / `SNS_REG_INIT` / `Decoding registry group %s` /
|
||||
> `Error decoding registry event`** → the SLPI **requests registry groups over the SNS QMI service
|
||||
> and expects encoded registry-event responses**. The SAR (sx932x) nanoapp fatals (`sar.cc:27
|
||||
> chre_utils fatal`) when it can't get its group → crash-loop.
|
||||
> - Fallback strings exist: **`Registry disabled, leaving default mode enabled` / `LPI Registry
|
||||
> Disabled`** — there may be a registry-disabled mode worth forcing (SAR likely still needs its
|
||||
> device config though).
|
||||
> - **The consumer = an SNS registry provider** (QRTR/QMI daemon) that reads
|
||||
> `/persist/sensors/registry/registry/<group>` and answers the SLPI's requests. On Android this is
|
||||
> `sensors.qti` + `sscrpcd` + `chre_daemon_msm` (binary-only in
|
||||
> `android-reference/vendor-stack-20260621/vendor/bin/`).
|
||||
> - **BUILD BLOCKER (why it's not done):** the SNS protocol is Qualcomm proprietary protobuf
|
||||
> (`sns_client.proto` / `sns_registry`); there are **NO .proto defs or SNS source anywhere in the
|
||||
> tree** — only the registry DATA and the binary daemons. A working provider needs that encoding;
|
||||
> building blind yields non-functional code. Options: (a) extract/RE the SNS protobuf from the
|
||||
> vendor .so (`libsnsapi`/`sensors.qti`) — multi-day; (b) find an open SNS impl to port;
|
||||
> (c) run the vendor `sscrpcd`/`sensors.qti` binaries under a libhybris/Halium shim;
|
||||
> (d) downstream-kernel hybrid (sidesteps mainline SLPI entirely).
|
||||
>
|
||||
> ### CLEAN decisive test to run next boot (settles whether SAR even gates the modem — do BEFORE any SNS build)
|
||||
> The session test was contaminated (modem already poisoned by prior online attempts). Clean version:
|
||||
> `systemctl mask ModemManager qcril-prov qcril-autoselect blueline-dpm-open-port` → reboot →
|
||||
> on boot immediately `echo stop > /sys/class/remoteproc/remoteproc2/state` (kill SLPI before it
|
||||
> crash-loops) → wait for modem QMI up → ONE `qmicli -p -d qrtr://0 --dms-set-operating-mode=online`.
|
||||
> - Still **52** with SLPI cleanly stopped from boot → **SLPI/SAR is NOT the modem gate** (the 52 /
|
||||
> model-0 is independent — chase modem NV/EFS/firmware-init instead; don't invest in the SNS build).
|
||||
> - **Goes online** → SLPI/SAR churn WAS the blocker → the SNS registry provider is worth the build.
|
||||
> Unmask afterwards: `systemctl unmask ModemManager qcril-prov qcril-autoselect blueline-dpm-open-port`.
|
||||
>
|
||||
> ### Files touched this session (for the next agent)
|
||||
> - `references/linux-7.1.1-sdm845-CANONICAL` @ `980c4ae1f` (revert skip); `tools/verify-fixes.sh` (skip-absent check)
|
||||
> - pmaports `device/testing/device-google-blueline/`: `qcril-prov.py`, `modem-qrtr-ready.sh`+`.service`,
|
||||
> `blueline-uim-select.sh`, `uim-wait.conf`, `blueline-dpm-open-port.service`, `qcril-prov.service`,
|
||||
> `APKBUILD`, `80-device-google-blueline.preset`. All also deployed live to the phone.
|
||||
> - NOTE: APKBUILD `sha512sums` NOT yet regenerated for the new/changed sources — run
|
||||
> `pmbootstrap checksum device-google-blueline` before the next package build.
|
||||
|
||||
---
|
||||
|
||||
|
||||
THE session that reframed the whole problem. The modem-online blocker is **not** (only) a
|
||||
kernel/secure-load issue — it is that the **userspace consumer chain that drives the modem is
|
||||
half-built / failing**, exactly as Casey kept saying. Everything below is VERIFIED LIVE on the
|
||||
reverted-baseline 7.1.1 build, not inference.
|
||||
|
||||
## TL;DR causal chain (verified end to end)
|
||||
1. Modem firmware boots, MBA loads MPSS, modem reaches `running`. It has identity (IMEI
|
||||
990012001581294, ESN 8074BB5D, MEID, SW `g845-00194-210812`). **The assign/-22/secure-load is
|
||||
NOT the modem blocker** — the modem loads. (The only `-22`s are fastrpc + ath10k wifi, both
|
||||
tolerated.) One XPU violation at boot, self-recovers (Casey was right).
|
||||
2. BUT the modem comes up **op-mode OFFLINE (3), model `0`** = up-but-unprovisioned/not-ready.
|
||||
`--dms-set-operating-mode=online` → **QMI 52 DeviceNotReady**. Forcing online does nothing.
|
||||
3. **SLPI crash-loops every ~10s**: `err_qdi.c:456:EF:sensor_process:1:sar.cc:27:chre_utils fatal`
|
||||
→ recover → up → crash. SAR (Semtech SX932x) is the modem's RF SAR/TX-power sensor.
|
||||
4. The modem+SLPI instability makes the **QRTR node flap**, so the boot-time consumer services
|
||||
race an unready modem and **fail**:
|
||||
- `msm-modem-uim-selection.service` → `node with id 0 not found in QRTR bus` (boot) /
|
||||
`Transaction timed out` (later). **This is a hard `Requires=` of ModemManager.**
|
||||
- `blueline-dpm-open-port.service` → `node with id 0 not found` at boot (succeeds if re-run later).
|
||||
- `qcril-prov.service` → crashed on a code bug (now FIXED, see below).
|
||||
5. Because `msm-modem-uim-selection` (hard dep) failed → **ModemManager never started at boot.**
|
||||
So nothing ever drove the modem: enable → set-power → register. It just sits OFFLINE.
|
||||
6. Started MM manually (bypassing deps): it DOES reach the modem over qrtr://0, creates `modem0`,
|
||||
reads the SIM ATR — then fails: **`couldn't start parent initialization: Couldn't get primary
|
||||
port`** and `couldn't load carrier config: Operation timed out`. MM can't initialize a modem
|
||||
that's OFFLINE/flapping.
|
||||
|
||||
## What is SOLID now (do not redo)
|
||||
- On reverted-baseline 7.1.1 (`blueline` branch, mem-protect skip reverted `980c4ae1f`). Built,
|
||||
flashed `boot_a`, deployed. `verify-fixes.sh` green (13 checks; mem-protect check now asserts the
|
||||
skip is ABSENT). Android slot B / EFS / userdata untouched.
|
||||
- ~~pd-mapper: in-kernel `CONFIG_QCOM_PD_MAPPER=m`, module loaded … **pd-mapper is fine — ruled
|
||||
out.**~~ **WRONG (corrected 2026-06-29).** The pmaports config sets `=m`, but the running kernel
|
||||
has `# CONFIG_QCOM_PD_MAPPER is not set` — the in-tree `arch/arm64/configs/sdm845.config` carries
|
||||
the upstream `=n` and `merge_config.sh -m` applies it *after* the pmaports config, so `=n` wins
|
||||
every build. Userspace pd-mapper is also uninstalled ⇒ **no pd-mapper of either kind runs.** Caught
|
||||
via the wifi outage (no `wlan0`; ath10k_snoc bound but probe defers with no wlan PD). The Mobian
|
||||
0171 observation stands, but "pd-mapper is fine" did not. Fixed: in-tree fragment → `=m` (commit
|
||||
pending), then rebuild+reflash. Whether this also unblocks the modem-52 is now an open re-test, not
|
||||
a closed lead.
|
||||
- Infra present & correct: `rmtfs -P -s` (block EFS), `tqftpserv` (serves `/persist/rfs` →
|
||||
modem mcfg), sensor registry 177 files at `/mnt/vendor/persist/sensors/registry/registry/`,
|
||||
all mcfg carrier configs incl `mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn`.
|
||||
- udev tags MM correctly: `ID_MM_QCOM_SOC=1`, `ID_MM_CANDIDATE=1` on rmnet_ipa0; MM qcom-soc
|
||||
plugin loads. Transport (QMI-over-QRTR) works — MM exchanges QMI with the modem.
|
||||
|
||||
## FIXED this session — qcril-prov.py (the MBN provisioner)
|
||||
Source: `pmaports/device/testing/device-google-blueline/qcril-prov.py` (also live-deployed to
|
||||
`/usr/bin/qcril-prov.py` on phone). Was crashing; now runs end-to-end and provisions Rogers.
|
||||
Bugs fixed (all real, in code):
|
||||
1. `list_configs` read configs from the response (`out.get_configs()` — doesn't exist;
|
||||
`MessagePdcListConfigsOutput` only has `get_result`). The config list arrives via the
|
||||
**`list-configs` INDICATION** → `IndicationPdcListConfigsOutput.get_configs()`; each element has
|
||||
`.id` + `.config_type` only. Reworked to indication-driven (mirrors the working `load-config`).
|
||||
2. GI array-length args: `set_id(cfg_id, len)` → `set_id(cfg_id)`; `set_type_with_id_v2(t,id,len)`
|
||||
→ `set_type_with_id_v2(t,id)`. (`set_config_chunk` was already correct.)
|
||||
3. Strategy: blueline INHERITS Android's full mcfg set (1 platform + 25 software) on shared
|
||||
persist. Don't delete+reload (delete is a no-op; reload → error 41 "already exists"). Instead:
|
||||
compute target id = **sha1 of the .mbn file** (== the PDC config id, verified: HW sha1
|
||||
`6fe571a9…` matched the on-modem id), confirm present, **select + activate by sha1**. Treat
|
||||
load error_code=41 as success. Helpers added: `_sha1_id`, `_hex`, `_id_present`.
|
||||
Result: provisioner selects+activates HW (platform) and Rogers (software) configs cleanly. The
|
||||
modem still ends OFFLINE afterward (carrier config alone doesn't lift it — needs MM to drive it).
|
||||
|
||||
## THE PLAN (next session — ordered)
|
||||
Goal: get ModemManager to fully initialize the modem and drive OFFLINE → online → registered.
|
||||
Two intertwined blockers: (A) modem won't initialize while OFFLINE/flapping; (B) SLPI crash-loop.
|
||||
|
||||
1. **Make MM actually manage the modem (primary blocker).**
|
||||
- Why `Couldn't get primary port`? Check MM qcom-soc plugin port expectations for a pure-QRTR
|
||||
SDM845 modem; check nothing else holds the QMI client (our qcril-* one-shots, the stray
|
||||
`dbus-monitor`, manual `qmicli -p`). Try MM with everything else stopped.
|
||||
- Fix the boot ORDERING/retry so `msm-modem-uim-selection` + `blueline-dpm-open-port` +
|
||||
`qcril-prov` don't race the unready modem: add wait-for-QRTR-node / Restart=on-failure /
|
||||
proper `After=` on a "modem-qrtr-up" condition. This is how the chain stays up like Lineage
|
||||
(Lineage's RIL waits for the modem to be ready before UIM select / PDC).
|
||||
- Once MM inits: it should set-power-up and the OFFLINE→online may resolve via MM's sequence
|
||||
(it owns DMS set-operating-mode). The manual `online`=52 is because nothing did the proper
|
||||
enable sequence first.
|
||||
|
||||
2. **SLPI SAR crash-loop (`sar.cc:27`).** SAR = SX932x, governs RF TX-power; Casey's position is
|
||||
it gates modem RF. The SLPI sensor REGISTRY (`/persist/sensors/registry`) is NOT served to the
|
||||
SLPI — tqftpserv serves `/persist/rfs` only (sibling tree). Leads:
|
||||
- Does the SLPI read registry via tqftpserv (needs `/sensors` served too) or a different path?
|
||||
The tqftp log shows only modem mcfg requests, no `/sensors` requests → SAR may crash BEFORE
|
||||
requesting registry (early init / SX932x i2c on the SLPI bus). Investigate how LineageOS
|
||||
`sensors_ssc.c` + sensor HAL bring SAR up (registry delivery + SX932x i2c/GPIO).
|
||||
- Our `qcom_ssc_sensors.c` is a passive ABI shim; it does NOT boot or feed the SLPI. Mainline
|
||||
auto-boots the SLPI (DTS firmware-name) with no consumer → SAR talks to nobody → crash.
|
||||
LineageOS boots SLPI ON DEMAND after the sensor HAL is up.
|
||||
|
||||
3. **Verify on a clean boot**: fix the services so the whole chain comes up at boot (not manually),
|
||||
reboot, confirm MM auto-starts, modem → registered. One online attempt per boot still applies.
|
||||
|
||||
## Hard constraints (unchanged)
|
||||
- pmOS = slot A; flash ONLY boot_a. NEVER touch Android slot B / EFS (modemst1/2,fsg,fsc) /
|
||||
userdata / TZ-XBL-ABL. rmtfs `-P -s`. No pmic@4/5 SPMI nodes. One `online` attempt per boot.
|
||||
sudo pw 147147. USB 172.16.42.1 (host iface enp0s20f0u6 → `ip addr add 172.16.42.10/24`).
|
||||
- pmOS is Alpine/ash: embedded `sh -c "...(...)..."` with parens fails — use script files via scp.
|
||||
153
saf/device/evidence/modem_dt_rf_analysis_20260622.md
Normal file
153
saf/device/evidence/modem_dt_rf_analysis_20260622.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# Modem DT/RF gap analysis — pmOS vs Android-downstream vs enchilada (2026-06-22)
|
||||
|
||||
Phase 1 of the "is the RF/DT lever still alive?" task. All comparisons are between three device
|
||||
trees on the SAME SoC (SDM845):
|
||||
- **blueline-pmos** (FAILS RF arm) — `dt-work/blueline-pmos.dts`, sourced from upstream
|
||||
`sdm845-google-blueline.dts` + `sdm845-google-common.dtsi`.
|
||||
- **blueline-Android downstream** (modem WORKS, Fido LTE) — `dt-work/android-dtb/android-base.dts`
|
||||
(base DTB) + `dt-work/blueline-live.dts` (live capture from /proc/device-tree, includes DTBO).
|
||||
- **enchilada (OnePlus 6) mainline** (modem WORKS on the exact same mainline kernel/pmaports stack)
|
||||
— `dt-work/sdm845-oneplus-enchilada.dts`.
|
||||
|
||||
Two working references on the same silicon is the strongest possible control. Where BOTH working
|
||||
trees agree and pmOS differs, that difference is a real, citable suspect.
|
||||
|
||||
---
|
||||
|
||||
## VERDICT: DT IS NOT THE GATE
|
||||
|
||||
The modem DT is **functionally identical** between working Android and non-working pmOS.
|
||||
Every modem-relevant node (remoteproc@4080000, reserved-memory, rmtfs, power-domains, clocks,
|
||||
glink, SPMI/PMIC) is structurally the same. The remaining differentiator is **runtime kernel
|
||||
driver behavior**, not the device tree.
|
||||
|
||||
---
|
||||
|
||||
## RANKED CANDIDATES
|
||||
|
||||
### #1 (LIVE — Android applies the 32MB offset map; pmOS must carry it too) — Modem reserved-memory map
|
||||
|
||||
The subagent's top candidate claimed pmOS uses a non-standard MPSS size (0x9800000) and MBA
|
||||
address (0x98500000) while Android uses the standard map (0x7800000 / 0x96500000). **This is wrong.**
|
||||
|
||||
The subagent compared pmOS against `android-base.dts` (the raw base DTB) and `enchilada.dts`
|
||||
(standard map). But it failed to account for the **DTBO overlay** (dtbo_idx=8) that the bootloader
|
||||
applies at boot. The DTBO transforms the base standard map into the Pixel 3 override on the live
|
||||
Android device. The live captured DTS (`blueline-live.dts`, from /proc/device-tree) proves this:
|
||||
|
||||
| Region | Android base DTB | Android LIVE (with DTBO) | pmOS | enchilada |
|
||||
|---|---|---|---|---|
|
||||
| MPSS | 0x8e000000 sz **0x7800000** | 0x8e000000 sz **0x9800000** | 0x8e000000 sz **0x9800000** | 0x8e000000 sz **0x7800000** |
|
||||
| MBA | **0x96500000** | **0x98500000** | **0x98500000** | **0x96500000** |
|
||||
| venus | 0x95800000 | 0x97800000 | 0x97800000 | 0x95800000 |
|
||||
| cdsp | 0x95d00000 | 0x97d00000 | 0x97d00000 | 0x95d00000 |
|
||||
|
||||
**Live Android and pmOS use the IDENTICAL Pixel 3 override map.** The modem works on Android with
|
||||
this map. Therefore the memory map cannot be the gate. (The override comes from
|
||||
`sdm845-google-common.dtsi:17-77` which `/delete-node/`s the standard regions and redefines them.
|
||||
pmOS includes this via the mainline DTS chain; Android gets it via the DTBO overlay.)
|
||||
|
||||
Citations:
|
||||
- Base DTB: `android-dtb/android-base.dts:19146` (reg 0x7800000), `:19167` (mba@96500000)
|
||||
- Live Android: `blueline-live.dts:931` (reg 0x9800000), `:949` (mba@98500000)
|
||||
- pmOS: `blueline-pmos.dts:906` (reg 0x9800000), `:924` (mba@98500000)
|
||||
- enchilada: `sdm845-oneplus-enchilada.dts:884` (reg 0x7800000), `:902` (mba@96500000)
|
||||
- DTBO source: `references/sdm845-linux-20260610-ONHOLD-DAVID-TREE/arch/arm64/boot/dts/qcom/sdm845-google-common.dtsi:17-77`
|
||||
|
||||
### #2 (REFUTED twice) — pm8998 LDOs absent from pmOS rpmh-regulators
|
||||
|
||||
`dt-work/regulator-diff-android-vs-pmos.txt`: Android defines ldo4/6/8/9/10/11/15/16/18/23/27 that
|
||||
pmOS omits. **Already refuted twice** (modem.md #9/#10): (a) enchilada's WORKING modem does NOT
|
||||
declare these RF LDOs either — same minimal mainline rpmh-regulators set as pmOS; (b) all 11 show
|
||||
`use=0` from the AP on live working Android (`rf-rail-diff-FINAL.txt`) — the modem votes its RF rails
|
||||
via RPMh directly, not the AP regulator framework. Since enchilada arms RF without them, they are not
|
||||
the gate. **Do NOT add the LDOs / do NOT flash `blueline-live.dtb`.**
|
||||
|
||||
### #3 (eliminated) — RFFE / SPMI bus / RF-frontend regulator nodes
|
||||
|
||||
No RFFE node and no WTR/QFE/PA/FEM/GRFC regulator node exists in ANY of the three trees, including
|
||||
both working ones. RF front-end power/control is modem-firmware-driven over RFFE/GRFC, not an AP DT
|
||||
resource. Dead.
|
||||
|
||||
### #4 (eliminated) — modem node clocks / power-domains / smem-states / glink
|
||||
|
||||
`remoteproc@4080000` is structurally identical across all three trees: same
|
||||
`qcom,sdm845-mss-pil`, same clocks, same power-domains, same glink-edge. Only firmware-name path
|
||||
and phandles differ. rmtfs carveout structurally identical. Dead.
|
||||
|
||||
### #5 (eliminated) — DTBO overlay modem content
|
||||
|
||||
The DTBO (dtbo_a.img.dtb8, 112 fixups) touches display, audio, camera, charger, thermal, GPIO,
|
||||
touchscreen, NFC, UFS, PCIe, debug/ramoops. **Zero modem/remoteproc/rmtfs/glink fragments.**
|
||||
`modemsmem` node is a no-op placeholder (compatible only, no properties). The DTBO's only
|
||||
modem-relevant effect is transforming the memory map from standard to Pixel 3 override (fragment@45
|
||||
for MPSS, fragment@48 for MBA) — which pmOS already has natively. Dead.
|
||||
|
||||
### #6 (eliminated) — CDT / bootargs / hardware identifiers
|
||||
|
||||
Android has a `chosen/cdt/cdb2` node (IMEI, MEID, BT/WLAN MACs, SKU, PCB config, eSIM) and
|
||||
`androidboot.*` bootargs that pmOS lacks. These are AP-side identifiers — the modem gets IMEI from
|
||||
EFS, not DT. Mainline kernel parses none of the `androidboot.*` params. Dead.
|
||||
|
||||
---
|
||||
|
||||
### #2 (WEAK / for-the-record) — pm8998 LDOs absent from pmOS rpmh-regulators
|
||||
|
||||
`dt-work/regulator-diff-android-vs-pmos.txt`: Android defines ldo4/6/8/9/10/11/15/16/18/23/27 that
|
||||
pmOS omits. **Already refuted twice** (modem.md #9/#10): (a) enchilada's WORKING modem does NOT
|
||||
declare these RF LDOs either — same minimal mainline rpmh-regulators set as pmOS; (b) all 11 show
|
||||
`use=0` from the AP on live working Android (`rf-rail-diff-FINAL.txt`) — the modem votes its RF rails
|
||||
via RPMh directly, not the AP regulator framework. Since enchilada arms RF without them, they are not
|
||||
the gate. Kept here only so it is not re-chased. **Do NOT add the LDOs / do NOT flash
|
||||
`blueline-live.dtb`.**
|
||||
|
||||
### #3 (eliminated) — RFFE / SPMI bus / RF-frontend regulator nodes
|
||||
|
||||
No RFFE node and no WTR/QFE/PA/FEM/GRFC regulator node exists in ANY of the three trees, including
|
||||
both working ones (`rf-rail-diff-FINAL.txt`, re-verified today: `qcom,rffe` absent everywhere; SPMI
|
||||
bus `spmi@c440000` `qcom,spmi-pmic-arb` present and identical in pmOS + enchilada + Android). RF
|
||||
front-end power/control is modem-firmware-driven over RFFE/GRFC, not an AP DT resource. Nothing to
|
||||
add. Dead.
|
||||
|
||||
### #4 (eliminated) — modem node clocks / power-domains / smem-states / glink
|
||||
|
||||
`remoteproc@4080000` is structurally identical between blueline-pmos and enchilada: same
|
||||
`qcom,sdm845-mss-pil`, same `clock-names` (iface/bus/mem/gpll0_mss/snoc_axi/mnoc_axi/prng/xo), same
|
||||
`power-domains` (cx/mx/mss), same `qcom,smem-states "stop"`, same glink-edge label "modem"
|
||||
remote-pid 1 (`dt-work/modem-blueline.txt` vs enchilada `:3647`). Only firmware-name path and
|
||||
phandles differ (correct). rmtfs carveout structurally identical (size `0x202000`, vmid 0x0f,
|
||||
client-id 1, guard-pages); base differs (`0xf2700000` vs enchilada `0xf5b00000`) which is normal
|
||||
per-device placement and already proven to serve EFS fine on pmOS
|
||||
(`modem_community_research_20260622.md` live result #1). Dead.
|
||||
|
||||
---
|
||||
|
||||
## Runtime time-injection angle (secondary lead from the task)
|
||||
Checked: pmOS does NOT run a modem time daemon. The qrtr "Time" service (TS, svc 64) sync that
|
||||
Android's RIL feeds is not provided by ModemManager. However this is downstream of the 52 wall —
|
||||
the modem refuses the `dms-set-operating-mode=online` *write itself* instantly
|
||||
(`modem_community_research_20260622.md` result #4: clean Alloc CID -> Set Op Mode -> FAILURE 52,
|
||||
immediate), before any time-dependent NAS attach logic runs. Time injection gates *attach*, not the
|
||||
op-mode flip. Only worth chasing if the 52 gate is cleared and the modem then fails to attach.
|
||||
|
||||
---
|
||||
|
||||
## VERDICT (Phase 1) — CORRECTED
|
||||
|
||||
**The DT is not the gate.** Every modem-relevant node is byte-identical between working Android and
|
||||
non-working pmOS. The subagent's #1 candidate (memory map difference) was refuted by accounting for
|
||||
the DTBO overlay: live Android and pmOS use the identical Pixel 3 override map.
|
||||
|
||||
The remaining differentiator is **runtime**: the mainline kernel's remoteproc/PIL driver, firmware
|
||||
load sequence, power-domain sequencing, QMP link bring-up, or glink/mailbox initialization differs
|
||||
from the downstream Android kernel. The modem firmware is identical (same files, same EFS, same
|
||||
shared partition). The DT is identical. The kernel and its drivers are what's different.
|
||||
|
||||
### Next steps (kernel/driver level, not DT)
|
||||
1. **Compare remoteproc driver behavior**: dmesg on Android vs pmOS during modem boot — look for
|
||||
differences in power-domain enable order, QMP link state, firmware load timing.
|
||||
2. **Check if mainline qcom_q6v5_pas driver handles the MSS PIL differently** from the downstream
|
||||
driver — e.g., missing a handover step, wrong power-domain sequence, or not waiting for a
|
||||
firmware-ready signal.
|
||||
3. **diag_mdlog F3 trace on Android**: capture the modem's own RF-init narration to see exactly
|
||||
where it succeeds on Android — this is the one instrument that can name the gate directly.
|
||||
135
saf/device/evidence/platform-pmos-era.md
Normal file
135
saf/device/evidence/platform-pmos-era.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# PAF · Platform — hardware, boot, storage, recovery
|
||||
|
||||
Load-bearing device facts. Add a line when something costs more than 2 minutes to relearn.
|
||||
|
||||
## Reaching the phone
|
||||
|
||||
- WiFi (preferred): `ssh -i ~/.ssh/ani_id_ed25519 casey@10.10.20.220` (Samaritan Solutions).
|
||||
**Drops on reboot / ~60–90 s to rejoin** — poll, don't assume.
|
||||
- USB fallback: `casey@172.16.42.1`. Helper: `../phone.sh {ssh,vnc,push,pull,run}`.
|
||||
- sudo password `147147` → `echo 147147 | sudo -S <cmd>`. **Never** pipe file content into a
|
||||
`sudo -S tee` (password and content fight over stdin). Write `/tmp/x` as user, then
|
||||
`echo PW | sudo -S cp /tmp/x /dest`.
|
||||
- pmOS is **Alpine — shell is ash/busybox, not bash.** No `[[ ]]`, no arrays, no `function`.
|
||||
- Host (Arch) ↔ Android: `adb` works with root (`magisk`); `fastboot` for slot/boot ops.
|
||||
|
||||
## Slots & boot architecture
|
||||
|
||||
- **slot A = pmOS, slot B = LineageOS (Android).** Verified 2026-06-25 on-device: pmOS
|
||||
ran with `androidboot.slot_suffix=_a`; `fastboot getvar current-slot` = a; flipping
|
||||
active to b booted Android; Android (slot B) had `system_b`/sda6 mounted. So pmOS =
|
||||
`boot_a` (sda11) + `system_a` (sda5); Android = `boot_b` (sda12) + `system_b` (sda6).
|
||||
Older PAF entries saying A=Android/B=pmOS or rootfs in system_b are pre-swap and stale.
|
||||
Switch active slot: from pmOS `sudo reboot bootloader` (~12 s → fastboot), then
|
||||
`fastboot --set-active={a,b} && fastboot reboot`.
|
||||
- pmOS rootfs lives **inside** `system_a` (sda5) as a packed **GPT image with 4096-byte
|
||||
sectors** (UFS). Two inner partitions:
|
||||
- boot (vfat, ~236 MB) at **byte offset 8388608** in sda6 — holds the kernel, initramfs,
|
||||
`linux.efi`, and DTBs. The appended DTB in the boot_b partition image IS used by ABL
|
||||
(it is baked into the kernel image in the boot.img). The `dtbs/qcom/` copies in the vfat
|
||||
are for a U-Boot/EFI path that is NOT used on this device currently.
|
||||
- root (ext4, ~2.5 GB) at **byte offset 255852544**.
|
||||
UUID: `26733d78-aa54-454d-ba43-0fa1eabfb3fd` (pmOS_root label)
|
||||
- boot vfat UUID: `2CE8-836F` (pmOS_boot label)
|
||||
- `fastboot flash system` is blocked (needs fastbootd, which won't enumerate on this device).
|
||||
Write the rootfs via `dd` over ADB from Android, after `simg2img`.
|
||||
- 4K sectors: `sgdisk` needs `-a 256` for 1 MiB alignment; `gdisk` reports "GPT not present"
|
||||
if it assumes 512-byte sectors — the image is valid.
|
||||
- **`fastboot boot <img>` has a hardware watchdog timeout (~60 s)** — the phone reboots itself
|
||||
if the image doesn't complete boot. Always `fastboot flash boot_b <img>` instead.
|
||||
- **/tmp is tmpfs (volatile, wiped on reboot) — never park load-bearing artifacts there.**
|
||||
|
||||
## Boot image parameters (slot A — pmOS)
|
||||
|
||||
The canonical pmOS boot image for slot A was `/tmp/pmos-boot-b.img` (29245440 bytes) — **wiped on 2026-06-21 reboot (/tmp is tmpfs).** The rescued recovery boot is now at `artifacts/tmp-rescue-20260626/recovery-boots/boot_a_restore.img` (65M). The filename predated the slot swap; always flash to `boot_a`, not `boot_b`.
|
||||
Rebuild targets `/tmp/pmos-debug2.img` (debug shell) and `/tmp/pmos-nodebug.img` (normal boot)
|
||||
must be regenerated. To rebuild from rescued kernel+ramdisk in `artifacts/tmp-rescue-20260626/bootimg-workspace/`:
|
||||
|
||||
```sh
|
||||
# Normal boot (no debug shell):
|
||||
mkbootimg --kernel artifacts/tmp-rescue-20260626/bootimg-workspace/kernel --ramdisk artifacts/tmp-rescue-20260626/bootimg-workspace/ramdisk \
|
||||
--base 0x0 --kernel_offset 0x00008000 --ramdisk_offset 0x01000000 \
|
||||
--tags_offset 0x00000100 --pagesize 4096 --header_version 0 \
|
||||
--cmdline "console=tty0 console=ttyMSM0,115200 consoleblank=0 \
|
||||
pmos_boot_uuid=2CE8-836F pmos_root_uuid=26733d78-aa54-454d-ba43-0fa1eabfb3fd \
|
||||
pmos_rootfsopts=defaults" \
|
||||
-o /tmp/pmos-nodebug.img
|
||||
|
||||
# Debug shell (drops to telnet:23 before mounting rootfs, adds pmos.debug-shell):
|
||||
# ... same but append: pmos.debug-shell
|
||||
```
|
||||
|
||||
Flash to slot A (pmOS) and reboot:
|
||||
```sh
|
||||
fastboot flash boot_a /tmp/pmos-nodebug.img && fastboot set_active a && fastboot reboot
|
||||
```
|
||||
|
||||
**Emergency debug shell** (when pmOS won't boot — flash debug image to boot_a):
|
||||
```sh
|
||||
fastboot flash boot_a /tmp/pmos-debug2.img && fastboot set_active a && fastboot reboot
|
||||
# USB gadget comes up as 18d1:d001 at 172.16.42.1 (~8 s after reboot)
|
||||
# telnet 172.16.42.1 23 → shell with pmos_continue_boot / pmos_logdump
|
||||
# After diagnosing, re-flash the normal image and reboot.
|
||||
```
|
||||
|
||||
**USB host setup** (permanent, already configured 06-19):
|
||||
- `/etc/NetworkManager/conf.d/99-pmos-usb.conf` — NM ignores `cdc_ncm`/`rndis_host`/`cdc_ether`
|
||||
- `/etc/udev/rules.d/99-pmos-usb.rules` — auto-assigns `172.16.42.2/24` on interface up
|
||||
|
||||
**Disk full recovery** (happened 06-19 — logs filled rootfs to 100%):
|
||||
Boot debug image → telnet in → mount_subpartitions → mount /dev/loop0p2 /sysroot →
|
||||
`rm -rf /sysroot/var/log/*` → pmos_continue_boot. Frees ~94 MB.
|
||||
The rootfs will slowly refill — **set up log rotation on pmOS** (`/etc/logrotate.d/` or
|
||||
`journalctl --vacuum-size=10M`) once booted.
|
||||
|
||||
## Recovery: swap the slot-B DTB from rooted Android (no TWRP)
|
||||
|
||||
Used 06-19 to un-brick slot B after the SPMI-pmic DTB panic. Reversible; backs up the old DTB.
|
||||
|
||||
```sh
|
||||
adb push <good>.dtb /data/local/tmp/recovery.dtb
|
||||
adb shell 'su -c "
|
||||
for l in /dev/block/loop*; do losetup -d \$l 2>/dev/null; done
|
||||
LO=\$(losetup -f); losetup -o 8388608 \$LO /dev/block/by-name/system_b
|
||||
mkdir -p /data/local/tmp/bmnt; mount -t vfat \$LO /data/local/tmp/bmnt
|
||||
T=/data/local/tmp/bmnt/dtbs/qcom/sdm845-google-blueline.dtb
|
||||
cp \$T \$T.broken.bak
|
||||
cp /data/local/tmp/recovery.dtb \$T
|
||||
sync; md5sum \$T /data/local/tmp/recovery.dtb
|
||||
umount /data/local/tmp/bmnt"' # loop auto-detaches on umount
|
||||
```
|
||||
|
||||
Building a DTB: base = the pristine kernel-package DTB (decompile
|
||||
`linux-postmarketos-qcom-sdm845-*.apk`, or `/tmp/pmos-packages/blueline.dts`). Edit, then
|
||||
`dtc -I dts -O dtb -o out.dtb in.dts`. The pristine DTB is 111641 bytes; verify
|
||||
`grep -c 'pmic@4\|pmic@5'` is **0** (those panic).
|
||||
|
||||
## Storage situation
|
||||
|
||||
- Root (ext4 inside sda6) is **2.4 GB, ~80 MB free** (94 MB logs cleared 06-19), 828 packages.
|
||||
Nothing significant installs until userdata is resized. `apk cache clean` buys ~300 MB
|
||||
temporarily. **Logs will refill** — set up log rotation once booted.
|
||||
- **userdata (sda21, 111 GB) is FBE + metadata-encrypted** (dm-default-key, AES-256-XTS; key
|
||||
KeyMint-wrapped in TEE, stored in `/metadata` sda20). **Cannot shrink offline** — no plaintext
|
||||
F2FS superblock outside Android; key can't be re-derived in TWRP. Shrinking = full wipe.
|
||||
Decision (06-18): do **not** wipe until calls + Linux desktop both justify it.
|
||||
- GPT backup of the outer table: `../gpt-sda-20260618.bak`. Nothing on sda was modified.
|
||||
|
||||
## Hardware lessons (do not relearn)
|
||||
|
||||
1. **Never `misc:vfr = 0`, never DPMS off/on** in Hyprland — wedges the DSI panel + stmfts
|
||||
touch IC. Recovery: `systemctl restart getty@tty1` for a clean modeset. Use `vfr = true` +
|
||||
`cursor:no_hardware_cursors = true`. Skip earboxer's power-button DPMS binds.
|
||||
2. **hyprgrass must match the Hyprland version exactly** (build against the `hl-x.y.z` tag).
|
||||
3. Display already correct: `monitor = DSI-1,1080x2160@60,0x0,2.0` (scale 2.0 → 540×1080
|
||||
logical). ~443 PPI panel.
|
||||
4. Restarting rmtfs blips ath10k → WiFi can drop; prefer USB during modem work.
|
||||
5. USB-net flaps because NetworkManager grabs `enp0s20f0u6`. Fix with **pkexec** (not sudo):
|
||||
`nmcli dev set <if> managed no; ip addr add 172.16.42.2/24 dev <if>; ip link set <if> up`.
|
||||
|
||||
## Backups on the laptop
|
||||
|
||||
`~/pixel3-backup/` — full Android partition images (gzipped): `boot_a` (downstream DTB +
|
||||
kernel), `modem_a` (FAT16 with `mba.mbn` + `modem.b00..b25`), `persist` (cal: audio/sensors/
|
||||
rfs/DRM), `system_a`, `vendor_a`, `vbmeta_a`, `dtbo_a`, `userdata` (87 GB). EFS snapshot:
|
||||
`../efs-backup-20260618/` (modemst1/2, fsg, fsc).
|
||||
148
saf/device/evidence/pmos_fix_plan.md
Normal file
148
saf/device/evidence/pmos_fix_plan.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# pmOS Modem Fix — Implementation Plan (autoselect, EFS-persisted)
|
||||
|
||||
Consolidates the 2026-06-20 live-Android + binary-RE session. This is the build plan for the
|
||||
pmOS side. Evidence is cited to live capture lines or binary addresses; nothing here is inferred
|
||||
without a source.
|
||||
|
||||
## The mechanism (works — but is NOT the gate)
|
||||
|
||||
> **🟡 RE-STAMPED 2026-06-29.** The MCFG auto-select-by-UIM mechanism is real and PDC configs do go
|
||||
> Active (`qcril-autoselect` runs, Set Feature accepted) — but this is **proven NOT to be the gate**:
|
||||
> the modem parks OFFLINE / model 0 and refuses every pre-RF write (online→52, low_power→60,
|
||||
> NAS SSP→25) regardless of provisioning state. Choreography is downstream of the OFFLINE state.
|
||||
> See `modem.md` eliminated-leads ledger. Kept as the mechanism reference (how MCFG/EFS autoselect
|
||||
> works), not as a live fix path.
|
||||
|
||||
The modem provisions itself by **MCFG auto-selection keyed on the SIM (UIM)**, applied at the
|
||||
modem's own firmware RF-init, and **persisted in the modem's EFS** (`modemst1/2`, which is
|
||||
**shared** between the Android and pmOS slots — `sdf3/sdf4`, not slotted).
|
||||
|
||||
1. **Persistent form (community-proven on SDM845 / OnePlus 6/6T):** an EFS NV item
|
||||
`/nv/item_files/mcfg/mcfg_autoselect_by_uim` plus the mcfg uploaded into EFS. Written over
|
||||
DIAG (EfsTools). Once set, the modem self-selects + self-applies every boot.
|
||||
2. **Runtime form (reverse-engineered from `libril-qc-hal-qmi.so`):**
|
||||
`pdc_enable_auto_selection(enable, id)` @ `0x1d45c40` builds an 80-byte
|
||||
`pdc_set_feature_req_msg_v01` (feature value `3` when enabling, `0` when disabling) and calls
|
||||
`pdc_set_feature` @ `0x1d45ac0`, which issues **QMI_PDC_SET_FEATURE**:
|
||||
- **message ID `0x2E` (46)** (`mov w1,#0x2e` at `0x1d45b4c`)
|
||||
- request length `0x50` = 80 bytes (`mov w3,#0x50`), response 8 bytes (`mov w5,#0x8`),
|
||||
timeout 10000 ms (`mov w6,#0x2710`).
|
||||
|
||||
## Live-Android ground truth (this session, device 89JX0AAJP)
|
||||
|
||||
- Modem fully working: `dumpsys telephony` → IN_SERVICE, LTE, Fido 302/720, voice+data.
|
||||
- **Self-persist confirmed:** blanked `persist.vendor.radio.cnv.ver_info`, restarted live
|
||||
`qcrild`. `is_modem_mbn_updated` logged `version info updated` (prev empty), `start_mbn_update`
|
||||
fired — but QCRIL only queried `get_active_config_info` (`config_version=0x801481a`, CNV
|
||||
`0x48`=72), re-synced, and **never loaded/activated**. The modem already held the config in its
|
||||
own NV. → userspace is reconcile-only; the modem persists. (`/tmp/qcril-re/live-firstprov.txt`)
|
||||
- **Online-arm reference captured** (the step that returns `DeviceNotReady` on pmOS):
|
||||
airplane toggle → `dms_handle_modem_operating_mode → "operating mode needs to be changed" →
|
||||
SETTING_OP_MODE → modem ONLINE`. (`/tmp/qcril-re/airplane-arm.txt`)
|
||||
- `/data/vendor/modem_config/` does **not exist** on this device — QCRIL reads MBNs straight from
|
||||
`/vendor/rfs/msm/mpss/readonly/vendor/mbn/` (the readonly vendor tree), not a `/data` cache.
|
||||
|
||||
## Why pmOS fails, and what the fix must do
|
||||
|
||||
pmOS boots with the modem at 0/0 configs and `DeviceNotReady`. The Android-equivalent state is:
|
||||
modem set to auto-select by UIM + the mcfg tree reachable at RF-init + EFS writable so the modem
|
||||
persists what it applied. The fix must guarantee all three on pmOS:
|
||||
|
||||
1. **mcfg tree served to the modem at RF-init** — `tqftpserv` serving
|
||||
`/mnt/vendor/persist/rfs/readonly/vendor/mbn/` (present, per modem.md). Verify the modem's
|
||||
requested path matches.
|
||||
2. **EFS writable** — `rmtfs -P -s` (read-write, drop `-r`) so the modem can commit/persist its
|
||||
applied config (frontier #4; the read-only `-r` shadow is why nothing persisted before).
|
||||
3. **Auto-selection enabled** — set it so the modem self-applies. Two routes below.
|
||||
|
||||
## Build — two routes (do A first; B is the persistent backstop)
|
||||
|
||||
### Route A — runtime QMI via PDC Set Feature (ALREADY AVAILABLE in pmOS's libqmi fork)
|
||||
**This is the path. It is not new work — the pmOS community already built the libqmi side.**
|
||||
|
||||
Stock libqmi PDC has no Set Feature. But the **openimsd libqmi fork**
|
||||
(`https://gitlab.postmarketos.org/modem/openimsd/libqmi`, base 1.39.0) already defines
|
||||
**`QMI_PDC_SET_FEATURE` at id `0x2E`** — the *exact* message ID we independently reverse-engineered
|
||||
from `pdc_set_feature` (`0x1d45b4c: mov w1,#0x2e`). Two sources, same message. Its request TLVs
|
||||
(`data/qmi-service-pdc.json`, "since 1.40"):
|
||||
|
||||
| TLV | id | type | meaning |
|
||||
|---|---|---|---|
|
||||
| Slot ID | common-ref | guint32 | slot |
|
||||
| Token | common-ref | guint32 | request token |
|
||||
| **Selection Mode** | 0x11 | guint32 | **set to 3 = auto-select-by-UIM** (matches the `enable→feature 3` in `pdc_enable_auto_selection`) |
|
||||
| Refresh Mode | 0x14 | gboolean | refresh on change |
|
||||
| IMS | 0x19 | gboolean | ims feature |
|
||||
|
||||
Output: Operation Result (+ a Set Feature *indication* carrying the Token).
|
||||
|
||||
Implementation on pmOS:
|
||||
- Build/install the **openimsd libqmi fork** (it generates `Qmi.MessagePdcSetFeatureInput` with
|
||||
`set_selection_mode` / `set_refresh_mode` / `set_ims` / `set_slot_id` / `set_token`).
|
||||
- Call it from **python-GI** — the fork's **qmicli has no `--pdc-set-feature` CLI**, so do it the
|
||||
way `qcom-imsd` does (Qmi GI over `qrtr://0`), not via a qmicli flag. Set Selection Mode = 3,
|
||||
Refresh Mode on. This is the runtime equivalent of QCRIL's `pdc_enable_auto_selection`.
|
||||
- pmOS boot service: enable auto-select, let the modem self-provision from the served mbn tree.
|
||||
|
||||
Note: the fork still carries the **load-config `g_free` segfault at `qmicli-pdc.c:1130`** (it's
|
||||
1.39.0). Only relevant if we fall back to explicit `--pdc-load-config`; the autoselect path
|
||||
shouldn't need it. Our one-line patch + `tools/build-qmicli.sh` still apply if so.
|
||||
|
||||
**Prior art / proof:** `qcom-imsd` (same fork, libqmi-GI over qrtr PDC) is **known-working on
|
||||
Pixel 3a (sargo)** — blueline's SDM845 sister. It does IMS-layer PDC (select/activate/register);
|
||||
we add the lower-layer Set Feature autoselect. The pmOS PDC-over-qrtr approach is proven on this
|
||||
silicon.
|
||||
|
||||
### Route B — persistent EFS NV item (DIAG, the community method)
|
||||
Write `/nv/item_files/mcfg/mcfg_autoselect_by_uim` (enable) into the modem EFS via a DIAG-EFS2
|
||||
client on pmOS. pmOS diag is **not** HAL-locked the way the production-Android Pixel gadget is
|
||||
(this session: the Pixel USB gadget HAL reverted configfs to adb-only, blocking USB-diag;
|
||||
`/dev/diag` local access remained available). Because `modemst` is shared, this is a one-time
|
||||
write that both slots inherit. Tooling: EfsTools (Linux/.NET — `dotnet` is available) or a small
|
||||
DIAG-EFS2 client against pmOS `/dev/diag`.
|
||||
|
||||
## Test loop (on pmOS, next boot)
|
||||
1. Confirm rmtfs `-P -s` (rw) + tqftpserv serving the mbn tree.
|
||||
2. Apply Route A (or B) to enable auto-selection.
|
||||
3. Reboot; check `qmicli --dms-get-capabilities` → `Networks` non-empty = RF armed.
|
||||
4. If armed: confirm it survives a second reboot (persistence). If not, apply Route B so the
|
||||
modem commits to EFS.
|
||||
|
||||
## LIVE RESULT 2026-06-20 (Route A run on pmOS hardware — config blocker SOLVED)
|
||||
|
||||
Ran the full fix on pmOS slot B (kernel 7.1.0-rc1-sdm845), live modem over qrtr://0:
|
||||
|
||||
1. Built + installed the **openimsd libqmi fork** on-device (Alpine); `Qmi.MessagePdcSetFeatureInput
|
||||
present: True`.
|
||||
2. Ran `qcril-autoselect.py` → PDC client allocated, **Set Feature (0x2E) accepted**. Signal names
|
||||
`set-feature`/`get-feature` were correct (indications arrived; only a strict-parse warning:
|
||||
the modem's Get Feature indication omits the mandatory Result TLV 0x02 libqmi expects — cosmetic).
|
||||
3. **Immediately after Set Feature, the modem self-loaded its configs: PDC 0/0 → 2 platform
|
||||
(SS-LA-SDM845 ACTIVE) + 25 software (Rogers_Canada).** It pulled them itself from the
|
||||
tqftpserv-served mbn tree. This is the modem auto-selecting by UIM — the mechanism working.
|
||||
4. **Rebooted with nothing enabled → the modem self-loaded 2+25 again at RF-init, platform ACTIVE.**
|
||||
So the autoselect **persisted** (Set Feature is sticky, not just runtime). This is the documented
|
||||
project blocker (0/0 every cold boot, frontier #3/#4) **SOLVED**: blueline's modem now
|
||||
self-provisions MCFG persistently on mainline pmOS — believed a first for Pixel 3.
|
||||
|
||||
**Remaining wall (NEW, deeper than config):** despite firmware up (mba+mpss booted, IPA running,
|
||||
handover signaled), platform config ACTIVE, and ModemManager seeing the configs,
|
||||
`--dms-set-operating-mode=online` still returns **DeviceNotReady (QMI 52)**; modem stays offline,
|
||||
Networks ''. `low-power` → InvalidTransition (already LPM). A full reboot with configs active did
|
||||
NOT arm RF — so it is not a re-init-count problem.
|
||||
|
||||
**Leading hypothesis for the remaining wall → Route B (firmware-level autoselect via EFS NV).**
|
||||
Our Set Feature operates at the **PDC/RIL layer** (post-firmware-boot): the modem loads+activates
|
||||
the config in PDC, but the **firmware RF-init at ~12s already ran without it**, so PDC-ACTIVE ≠
|
||||
RF-consumed. The Android/OnePlus mechanism writes the EFS NV item
|
||||
`/nv/item_files/mcfg/mcfg_autoselect_by_uim` so the **firmware itself** auto-selects at its
|
||||
earliest RF-init, before RF comes up. Next step: write that EFS NV item on pmOS (diag/EfsTools —
|
||||
not HAL-locked here) so the firmware applies MCFG at RF-init. Alternative to rule out: a genuinely
|
||||
separate non-config blocker (cal/IMEI) — but the same modem arms on Android, so config-apply timing
|
||||
is the stronger lead.
|
||||
|
||||
## Artifacts from this session
|
||||
`/tmp/qcril-re/`: `live-firstprov.txt` (qcrild restart, reconcile-only proof),
|
||||
`airplane-arm.txt` (online-arm reference), `auto_sel.txt` + objdump of
|
||||
`pdc_enable_auto_selection` / `pdc_set_feature` (msg 0x2E). EFS clones already in repo:
|
||||
`efs-postandroid/`, `efs-backup-20260618/` (`modemst1/2`, `fsg`, `fsc`).
|
||||
158
saf/device/evidence/power/HANDOFF-upower-charge-control.md
Normal file
158
saf/device/evidence/power/HANDOFF-upower-charge-control.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# HANDOFF — pmi8998 charge control (kernel + userspace)
|
||||
|
||||
> Historical handoff. Current platform status belongs in `../../platform.md`.
|
||||
|
||||
Written 2026-07-24. For a fresh model/reviewer picking this up cold.
|
||||
|
||||
## What this is
|
||||
|
||||
Full read/write charge control for the Pixel 3 (blueline) charger, so the
|
||||
phone can report its charge stage and cap the charge for battery longevity —
|
||||
the capability LineageOS exposed downstream and mainline dropped.
|
||||
|
||||
Two layers, in two separate repos:
|
||||
|
||||
- **Kernel driver** — `drivers/power/supply/qcom_smbx.c` in the
|
||||
`linux-blueline` tree. Adds charge_type reporting + charge-cap control.
|
||||
- **Userspace** — `rootfs-overlay/…` in the `Pixel3Arch` repo. A CLI, a udev
|
||||
rule, a persistence unit. **Currently stale — see "Known gaps" #1.**
|
||||
|
||||
Nothing is flashed. The phone is untouched. The driver builds clean but has
|
||||
not been packaged since the last fix, and has NOT run on hardware.
|
||||
|
||||
## State of the tree — READ THIS FIRST
|
||||
|
||||
**The gitea branch is behind local by two commits.**
|
||||
|
||||
| commit | what | where |
|
||||
|---|---|---|
|
||||
| `f08dc70b3` | original: charge_type + charge control | **pushed to gitea `blueline`** |
|
||||
| `6b733d3ea` | fix pass after review #1 | **LOCAL ONLY** |
|
||||
| `588929024` | cell-aware current cap + probe guard | **LOCAL ONLY** |
|
||||
|
||||
`gitea.wiuf.net`/`10.20…:4455` `Fimeg/linux-blueline` branch `blueline` tip is
|
||||
`f08dc70b369a` — the **flawed original**. A reviewer who clones from gitea gets
|
||||
code with the defects listed under "Review #1 found" still present. To review
|
||||
the real thing, use the local tree at
|
||||
`~/Projects/Pixel3Arch/kernel/linux-blueline-current` (branch `mic-race-fix`,
|
||||
tip `588929024`) or push the two commits first.
|
||||
|
||||
`build.sh` clones from gitea and hard-asserts the built package's hash matches
|
||||
the branch tip, so **a package build will not include the fixes until they are
|
||||
pushed.**
|
||||
|
||||
## Files
|
||||
|
||||
### Kernel (linux-blueline-current, branch mic-race-fix)
|
||||
- `drivers/power/supply/qcom_smbx.c` — the only changed file. +241 lines.
|
||||
- `smb_get_prop_charge_type` — read-only, maps BATTERY_CHARGER_STATUS_1
|
||||
bits 0:2 (trickle/pre/fast/fullon/taper/…) to POWER_SUPPLY_CHARGE_TYPE_*.
|
||||
- `smb_{get,set}_prop_voltage_max` — CV target, FLOAT_VOLTAGE_CFG (0x70).
|
||||
- `smb_{get,set}_prop_constant_charge_current` — FAST_CHARGE_CURRENT_CFG (0x61).
|
||||
- `smb_{get,set}_prop_charge_control_end` — percentage form of voltage_max,
|
||||
on POWER_SUPPLY_PROP_CHARGE_CONTROL_END_THRESHOLD.
|
||||
- `smb_fast_charge_current_max` — 1C ceiling from charge_full_design_uah.
|
||||
- `smbx_ov_status` — one-char fix, `reg & mask` → `val & mask`.
|
||||
- probe: design-limit guard + constant substitution.
|
||||
|
||||
### Userspace (Pixel3Arch repo, committed at 8a35c45 / 194c422)
|
||||
- `rootfs-overlay/usr/bin/blueline-charge` — CLI. **STALE.**
|
||||
- `rootfs-overlay/etc/udev/rules.d/60-blueline-charge.rules` — grants the
|
||||
writable attrs to group wheel. **STALE.**
|
||||
- `rootfs-overlay/etc/systemd/system/blueline-charge-limit.service` —
|
||||
reapplies the cap on boot (registers are volatile).
|
||||
- `PAF/platform.md` — "Charging" section documents the register map.
|
||||
|
||||
## Property provenance (asked and answered)
|
||||
|
||||
Every property name is a **pre-existing mainline `power_supply` core constant**
|
||||
(`include/linux/power_supply.h`), not invented here and not from any PR of
|
||||
mine. `charge_control_end_threshold` has a documented ABI contract dated April
|
||||
2019 (`Documentation/ABI/testing/sysfs-class-power`) — "battery percentage
|
||||
level, above which charging will stop" — and is already implemented by seven
|
||||
in-tree drivers, including the sibling `qcom_battmgr.c` on this same SoC family.
|
||||
The one thing authored here is the charge_type register-state→enum mapping.
|
||||
|
||||
## Review history
|
||||
|
||||
Three independent cold-start adversarial reviews were run (subagents, each told
|
||||
to refute safety). Summary of what each found:
|
||||
|
||||
**Review #1 (against f08dc70b3) — four real defects, all since fixed in 6b733d3ea:**
|
||||
1. `smbx_ov_status` masked the register *address* not its *value*
|
||||
(`reg & mask`), so the overvoltage backstop returned 0 on both SMB2 and
|
||||
SMB5 and never fired — pre-existing, but the write path leaned on it.
|
||||
2. `charge_control_limit` is microamps by ABI; it was implemented as a
|
||||
percentage, inverting a thermal daemon's write. Moved to
|
||||
`charge_control_end_threshold` (percent, ABI-correct).
|
||||
3. `voltage_max` floored at the register base (3.4875V), not the cell design
|
||||
minimum (3.6V). `echo 3487500 > voltage_max` set CV below terminal voltage;
|
||||
with CHARGER_INHIBIT the charger stalls and the phone drains on a cable.
|
||||
Now floored at voltage_min_design_uv.
|
||||
4. Current setter clamped against the DCP *input* limit (a different register
|
||||
and concept), so the getter returned 1.95A while the setter rejected it.
|
||||
|
||||
**Review #2 (against 6b733d3ea) — confirmed the four fixes; flagged the current
|
||||
ceiling. Fixed in 588929024:**
|
||||
- The current cap was bounded by register width = 6.375A = **2.15C** into a
|
||||
2970mAh cell. Relying on "the adapter/AICL gate it" is the same
|
||||
external-backstop reasoning that hid the dead OV check. Now bounded at **1C**
|
||||
(2.97A from charge_full_design_uah), register width as fallback.
|
||||
- Probe didn't validate voltage design limits (latent, not blueline). Guarded.
|
||||
|
||||
**Review #3 (against 588929024) — verdict FLASH for blueline.** No overcharge
|
||||
(clamp-before-encode, encoder rounds down, 8-bit field can't truncate a legal
|
||||
value unsafe), no stall (3.6V floor on every path, registers volatile →
|
||||
reboot recovers), 1C cap confirmed, dispatch clean.
|
||||
|
||||
## Known gaps — WHAT STILL NEEDS DOING
|
||||
|
||||
1. **Userspace is stale.** `blueline-charge` and `60-blueline-charge.rules`
|
||||
still reference `charge_control_limit`, which the driver no longer exposes.
|
||||
On the fixed kernel the CLI fails its own `[ -e "$PSY/charge_control_limit" ]`
|
||||
guard. They must be updated to `charge_control_end_threshold`. NOT done.
|
||||
|
||||
2. **Only `end_threshold`, not the `start`/`end` pair.** The ABI defines
|
||||
`charge_control_start_threshold` and `_end_threshold` as a pair; some
|
||||
userspace (GNOME/UPower, some tlp configs) expects both. Only `end` is
|
||||
implemented. `drivers/power/supply/cros_charge-control.c` is the in-tree
|
||||
reference for doing both.
|
||||
|
||||
3. **The SMB2 overvoltage bit is unverified.** No auditor could confirm from
|
||||
code alone that BIT(5) of BATTERY_CHARGER_STATUS_2 is the pmi8998 OV bit —
|
||||
there's no datasheet or downstream reference in-tree. It's pre-existing and
|
||||
read-only (can't overcharge/stall), but worth confirming against a datasheet.
|
||||
|
||||
4. **Cosmetic:** the max voltage write (4.4V → reg 122) reads back as 4.395V
|
||||
and 99% via the threshold getter, because the 7.5mV encoding step doesn't
|
||||
divide the range evenly. Errs low (safe). Not fixed.
|
||||
|
||||
5. **Not packaged, not flashed.** Single-object compiles pass under clang/LLD/
|
||||
CFI with W=1. Full `build.sh` not re-run since the fixes (and can't include
|
||||
them until pushed — see "State of the tree"). No hardware test.
|
||||
|
||||
## What is NOT verified without hardware
|
||||
|
||||
- Whether lowering the float voltage actually *stops* the charge on the
|
||||
pmi8998, versus the fuel gauge continuing to report rising capacity.
|
||||
- Whether charge_type reports sane transitions (Fast under load → Standard as
|
||||
it tapers near full).
|
||||
|
||||
## Build & deploy (per PAF/build.md)
|
||||
|
||||
- Commit + **push to gitea** (uncommitted/unpushed work does not build).
|
||||
- `ssh casey@10.10.20.123 'cd ~/Projects/Pixel3Arch/kernel && ./build.sh blueline'`
|
||||
— self-locks with flock, invoke directly; asserts pkg hash == gitea tip.
|
||||
- Deploy: modules first, then flash boot_a, then refuse to bless unless
|
||||
`uname -r` matches the installed tree (`7.1.1-sdm845-g<hash>`).
|
||||
- Rollback: `staging/deploy-139aa/boot-139aa7748181-pmoschain.img`. Slot B
|
||||
(LineageOS) is untouched and is the ultimate fallback.
|
||||
|
||||
## Single-object compile check (fast, no full build)
|
||||
|
||||
scp qcom_smbx.c casey@10.10.20.123:/tmp/
|
||||
ssh casey@10.10.20.123 'cd ~/Projects/Pixel3Arch/kernel/src/linux-blueline-src && \
|
||||
cp /tmp/qcom_smbx.c drivers/power/supply/ && \
|
||||
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- W=1 \
|
||||
drivers/power/supply/qcom_smbx.o'
|
||||
# restore the tree afterward — this is makepkg's throwaway clone
|
||||
103
saf/device/evidence/qcril_bringup_capture.md
Normal file
103
saf/device/evidence/qcril_bringup_capture.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# QCRIL modem bring-up — CAPTURED exact sequence (recreate this on pmOS)
|
||||
|
||||
This is the captured, ordered sequence QCRIL runs to bring the modem online on the **working**
|
||||
blueline Android. The standing goal (Casey): **recreate this exactly on pmOS** — approximating has
|
||||
been the failure. Do NOT re-capture this; analyze the saved files below.
|
||||
|
||||
## Capture files (in repo, `android-capture-20260620/`)
|
||||
- `qcril-ssr-fullinit.txt` — **the primary capture.** Modem SSR (`echo restart >
|
||||
/sys/kernel/debug/msm_subsys/modem`) with verbose radio logging → QCRIL re-runs its FULL bring-up
|
||||
→ modem ONLINE. 66k lines, ts 21:07:04–21:07:35.
|
||||
- `qcril-ssr-TIMELINE.txt` — distilled ordered timeline (section A: QMI sends per endpoint;
|
||||
section B: per-module handling order).
|
||||
- `qcril-online-capture.txt` — airplane off→on (the LPM→online flip in isolation).
|
||||
- `android-radio-log.txt` — steady-state runtime reference (attached, Fido LTE).
|
||||
|
||||
## SIM identity (for replay)
|
||||
ICCID `<redacted>`, IMSI `<redacted>`, MCC/MNC **302/370 (Fido)**, carrierId 1962,
|
||||
GW app1 AID `A0000000871002F310FFFF89B00140FF`, slot 1.
|
||||
|
||||
## The exact ordered sequence (SSR → ONLINE), with evidence
|
||||
1. **Modem SHUTTING_DOWN → re-init.** `qcril_qmi_nas_log_radio_power_process_state: telephony ONLINE
|
||||
| ril NONE | card UP | modem SHUTTING_DOWN`.
|
||||
2. **Endpoint-status indications fire for EVERY service as it re-registers** (qcril-ssr-fullinit
|
||||
lines ~252–1325): IMSS, IMSA, **PDC** (→ MbnModule), SMS, NAS, PBM, **DMS** (→ DmsModule AND
|
||||
MbnModule), UIM (`uim.card_status_update slot 0`), VS, Voice, WDS, Auth. QCRIL drives each
|
||||
service's endpoint, not just DMS.
|
||||
3. **MbnModule re-syncs PDC — VERIFY ONLY, no load/select/activate.** `PDC_ENDPOINT_STATUS_IND` and
|
||||
`DMS_ENDPOINT_STATUS_IND` both forwarded to MbnModule; no `load_config`/`select_config`/
|
||||
`activate_config`/`enable_auto_selection` anywhere in the SSR window. MCFG persisted in modem NV
|
||||
across the SSR (consistent with frontier #4). **→ On pmOS we must get MCFG in modem NV ONCE; it
|
||||
then persists.**
|
||||
4. **UIM provisioning is NOT re-run on SSR** (no `change_provisioning_session` in the capture) — it
|
||||
persisted. **→ On pmOS, primary-GW provisioning must be established once and stick.**
|
||||
5. **NAS config burst (the "QCRIL does more" set), then power.** QCRIL calls, in the bring-up window:
|
||||
`qcril_qmi_nas_init`, `request_set_preferred_network_type` (tells the modem which RATs to
|
||||
acquire), `request_radio_tech`, `set_reported_voice_radio_tech`, `request_set_unsol_response_filter`,
|
||||
`configure_sig_info`, `set_lpm_after_ssr_in_apm`, `set_bootup_power_optimization_state`,
|
||||
`request_power`. ModemManager issues only a small subset of these.
|
||||
6. **DMS operating-mode flip: LPM → ONLINE.** `qcril_qmi_nas_dms_handle_modem_operating_mode:
|
||||
modem's operating mode needs to be changed` → ril SETTING_OP_MODE → `modem ONLINE` (~270 ms after
|
||||
SSR; line ~15967). post: `set_bootup_power_optimization_state=2`, ATEL UI-READY, enable
|
||||
networking/sys-info indications → `is_online 1` → forces NW search.
|
||||
|
||||
## The decisive state difference (Android vs pmOS)
|
||||
- **Android: the modem is held in `LPM` (low_power, powered, RF parked); QCRIL flips LPM→online.**
|
||||
- **pmOS: the modem sits in `offline` (deeper) and `offline→online` is what returns DeviceNotReady /
|
||||
no-ops.** Plus pmOS boots with 0 PDC configs (autoselect not active that boot) and provisioning
|
||||
failing on the early-boot race.
|
||||
|
||||
## Recreate-exactly plan for pmOS (the test we have NEVER fully run)
|
||||
On one clean slot-B boot, in order, all three prereqs together:
|
||||
1. Get MCFG into modem NV (PDC autoselect / load+select+activate) → configs ACTIVE.
|
||||
2. Establish primary-GW provisioning cleanly (run AFTER the QRTR race; it succeeds manually:
|
||||
`--uim-change-provisioning-session=slot=1,activate=yes,session-type=primary-gw-provisioning,aid=
|
||||
A0000000871002F310FFFF89B00140FF`).
|
||||
3. Set the NAS preferred-network-type / radio-tech (QCRIL step 5) — replicate via libqmi NAS.
|
||||
4. Bring the modem up through **LPM** (set `--dms-set-operating-mode=low_power` if it's in `offline`)
|
||||
THEN `=online`. Do NOT flip offline→online directly.
|
||||
5. Verify `--dms-get-capabilities` Networks non-empty + NAS serving system registers.
|
||||
|
||||
Replay vehicle: the openimsd libqmi fork (Set Feature for autoselect) + libqmi NAS/DMS/UIM via
|
||||
python-GI. The point is the ORDER and the LPM state, captured above — not a partial subset.
|
||||
|
||||
## 2026-06-21 LIVE — clean provisioning + configs is NOT sufficient (eliminated)
|
||||
Fresh slot-B boot, modem running (remoteproc2 up, no fatal in dmesg, zero modem rail failure).
|
||||
State: mode=offline, configs PRESENT (sw 0x8013303 + platform loaded), USIM app=ready.
|
||||
- msm-modem-uim-selection FAILED at boot (early-QRTR race: "Application was already selected"
|
||||
then Transaction timed out) — the #7 symptom.
|
||||
- Replicated the service BY HAND post-boot: deactivate timed out, but ACTIVATE returned
|
||||
"Successfully changed provisioning session" => primary-gw provisioning now cleanly active.
|
||||
- Immediately retried: low_power => InvalidTransition(60); online => DeviceNotReady(52).
|
||||
Mode stayed offline. Networks '', Max TX 0.
|
||||
CONCLUSION: configs-active + clean-primary-gw-provisioning + USIM-ready + healthy rails is
|
||||
NOT enough to arm RF. The modem is wedged in `offline` (RF subsystem not-ready) and refuses
|
||||
BOTH offline->low_power and offline->online. PAF #7's "clean provisioning -> online stops
|
||||
returning 52" did NOT reproduce. The remaining differentiator is QCRIL's boot-time QMI/NAS
|
||||
choreography (#8: radio-power state machine + UI-ready/poweron_opt) that brings the modem to
|
||||
LPM. Android boots the modem INTO low_power; pmOS boots it into the deeper `offline`.
|
||||
NEXT: capture how QCRIL puts/keeps the modem in LPM on rooted Android, replay on pmOS.
|
||||
|
||||
## 2026-06-21 REFRAME — Android modem SELF-ONLINES (mode 0); there is no QCRIL online handshake
|
||||
Mined existing captures (android-live-20260619/deep/radio-boot.txt, android-capture-20260620/
|
||||
qcril-ssr-fullinit.txt). The op-mode VALUE settles the "LPM" confusion:
|
||||
- radio-boot.txt:3694 qcril_qmi_nas_dms_get_current_power_state: ".. known modem operating mode 0"
|
||||
(DMS op-mode 0 = ONLINE).
|
||||
- radio-boot.txt:3949 "telephony LPM | ril NONE | card UP | modem ONLINE"
|
||||
=> the "LPM" is the AP/telephony airplane-state layer; the MODEM is ONLINE.
|
||||
- radio-boot.txt:3862 qcril_qmi_nas_telephony_ready: "poweron_opt: 'UI ready' NOT sent to modem"
|
||||
(skipped BECAUSE modem already online).
|
||||
- ssr-fullinit: QCRIL only POLLS op-mode ("Operating mode is not available, polling...") and sets
|
||||
bootup_power_optimization_state=2. It never issues DMS Set Operating Mode = online.
|
||||
|
||||
CONCLUSION (corrects #8's "Android boots modem into LPM"): on Android the modem firmware
|
||||
SELF-COMPLETES RF-init and SELF-TRANSITIONS to ONLINE (mode 0) with NO online command from
|
||||
QCRIL. The Android "LPM" is the RIL/airplane layer, not the modem. Therefore replaying a
|
||||
"QCRIL online handshake" on pmOS is a dead end -- there isn't one.
|
||||
|
||||
The pmOS gate, precisely: the modem firmware's own RF-init does NOT complete on pmOS, so it
|
||||
parks in OFFLINE (mode 3) and returns DeviceNotReady(52) when we ASK it to online (Android
|
||||
never has to ask). Everything AP-side is equal/eliminated. The remaining differentiator is
|
||||
what the modem reads/does at its OWN firmware RF-init -- strongest lead = modem-side EFS NV
|
||||
state (Route B: /nv/item_files/.../power-up operating mode and/or mcfg_autoselect_by_uim),
|
||||
i.e. something the Android modem persisted that makes RF-init succeed and self-online.
|
||||
680
saf/device/evidence/qcril_full_provisioning.md
Normal file
680
saf/device/evidence/qcril_full_provisioning.md
Normal file
|
|
@ -0,0 +1,680 @@
|
|||
# QCRIL Full Provisioning — Gap Analysis & Design
|
||||
|
||||
**Purpose:** Reconstruct what QCRIL does to provision the modem every boot, identify every gap
|
||||
vs. our current Python tools, and design a full replacement provisioner. This feeds a real
|
||||
hardware bring-up; every structural claim cites a symbol, string, or file.
|
||||
|
||||
---
|
||||
|
||||
## A. What Our Tools Do Today
|
||||
|
||||
Two scripts, manually invoked in sequence.
|
||||
|
||||
### pdc_load.py (tools/pdc_load.py)
|
||||
|
||||
A chunked PDC LoadConfig implementation via libqmi GI bindings (1024-byte chunks). Reads an MBN
|
||||
file, computes its SHA-1, and uploads it to the modem via `QMI_PDC_LOAD_CONFIG` with a rolling
|
||||
token. Uses `EXPECT_INDICATIONS` flag so the modem can ack each chunk. On `remaining_size == 0`
|
||||
the upload is complete. Accepts `platform` or `software` config type on the CLI.
|
||||
|
||||
**QMI sequence:**
|
||||
1. Open QMI device (QRTR node)
|
||||
2. Allocate PDC client
|
||||
3. Connect `load-config` indication handler
|
||||
4. Loop: `QMI_PDC_LOAD_CONFIG_REQ` (chunk) → indication → next chunk
|
||||
5. Terminate on `remaining_size == 0`
|
||||
|
||||
No version checking. No selection. No activation. No carrier selection. No EFS interaction.
|
||||
No SIM awareness. No timing logic.
|
||||
|
||||
### pdc_activate.py (tools/pdc_activate.py)
|
||||
|
||||
Implements the `SetSelectedConfig` → `ActivateConfig` two-step per the libqmi qmicli source
|
||||
for `run_activate_config`. Takes a hex config ID on the CLI.
|
||||
|
||||
**QMI sequence:**
|
||||
1. Open QMI device
|
||||
2. Allocate PDC client
|
||||
3. `QMI_PDC_SET_SELECTED_CONFIG_REQ` (type + ID + token)
|
||||
4. On `set-selected-config` indication → `QMI_PDC_ACTIVATE_CONFIG_REQ` (type + token)
|
||||
5. On `activate-config` indication → done (modem restart expected)
|
||||
|
||||
No version check (always activates). No load-first. No sequencing of HW before SW.
|
||||
No handling of the restart-then-load ordering. No awareness of what was already active.
|
||||
|
||||
### modem_health.sh (tools/modem_health.sh)
|
||||
|
||||
Read-only snapshot: remoteproc state, QMI up/down, PDC list for platform and software configs
|
||||
with active flags, DMS get-capabilities (the RF-arm signal), NAS home network, operating mode,
|
||||
ModemManager status. Non-mutating by contract.
|
||||
|
||||
**What this tells us:** the `DMS Networks: ''` == RF not armed. `PDC Total: 0` == modem reset
|
||||
since last provisioning.
|
||||
|
||||
---
|
||||
|
||||
## B. What QCRIL Actually Does
|
||||
|
||||
Evidence sources: `libril-qc-hal-qmi.so` (34 MB, arm64 ELF, class `main`); embedded source
|
||||
paths `vendor/qcom/sm7250/proprietary/qcril-hal/modules/mbn/src/`; `qcril.db` (sqlite3);
|
||||
`mbn_hw.txt`, `mbn_sw.txt`, `mcfg.version`; `qcrild.rc`, `init.sdm845.rc`.
|
||||
|
||||
### B.1 Timing — When QCRIL Runs vs Modem Boot
|
||||
|
||||
From `qcrild.rc`:
|
||||
|
||||
```
|
||||
service vendor.qcrild /vendor/bin/hw/qcrild
|
||||
class main
|
||||
user radio
|
||||
```
|
||||
|
||||
`class main` services start at the `main` class trigger, which fires after `post-fs-data`
|
||||
completes. From `init.sdm845.rc`, the modem subsystem is NOT explicitly gated here — there is
|
||||
no `wait_for_prop` on a modem-ready property before qcrild starts. `rmt_storage` (EFS daemon)
|
||||
is also `class core` and starts earlier.
|
||||
|
||||
The modem firmware itself (MPSS via remoteproc `q6v5-mss`) loads at ~12.8 s after Linux boots,
|
||||
independent of Android init stages. QCRIL starts asynchronously as part of `class main` (roughly
|
||||
after `post-fs-data`, typically 10–30 s into boot, well before userspace is fully up).
|
||||
|
||||
**The decisive timing insight** (from PROGRESS.md 2026-06-20 entry): the modem applies MCFG at
|
||||
its own firmware RF-init window, which occurs during/after MPSS comes up (~12 s). QCRIL must
|
||||
deliver configs within this window. QCRIL achieves this because `qcrild` (class main) starts
|
||||
early and its MBN update sequence begins as soon as the DMS client becomes ready — triggered by
|
||||
`DmsModule::handleDmsEndpointStatusIndMessage` → `qcril_qmi_start_mbn_update()`.
|
||||
|
||||
**The trigger:** strings `"DMS client is ready. start MBN update"` and
|
||||
`"Module is ready,Start Mbn update"` both come from the `DmsModule` source
|
||||
(`vendor/qcom/sm7250/proprietary/qcril-hal/modules/dms/src/DmsModule.cpp`). When DMS QMI
|
||||
service becomes available on QRTR, `DmsModule::handleDmsEndpointStatusIndMessage` fires and
|
||||
calls `qcril_qmi_start_mbn_update()` (nm: `_Z26qcril_qmi_start_mbn_updatev`). This is the
|
||||
entry point into the full HW+SW MBN update state machine.
|
||||
|
||||
**Critical observation:** QCRIL does NOT wait for the modem to be "online" or for an RF-init
|
||||
complete signal before loading configs. It loads as soon as DMS is up (the modem is in some
|
||||
early QMI-ready state but not yet RF-armed). The modem then applies the loaded MCFG at its
|
||||
own RF-init pass, which happens after configs are in place.
|
||||
|
||||
The string `"is_ssr_or_bootup %d"` (`qcril_mbn_sw_update.cpp`) shows that the SW update path
|
||||
distinguishes cold boot from SSR. On warm SSR (subsystem restart), QCRIL only queries — does
|
||||
NOT reload (`qcril_qmi_pdc_get_active_config_info`, `qcril_qmi_pdc_get_selected_mbn_config`).
|
||||
On cold bootup, it runs the full load sequence. This matches the Android forensics finding:
|
||||
QCRIL skips reload on warm SSR (configs survived) but loads fresh on cold boot.
|
||||
|
||||
### B.2 Config Discovery — Where QCRIL Finds MBN Files
|
||||
|
||||
Two source directories (strings in libril-qc-hal-qmi.so):
|
||||
- Primary: `/data/vendor/modem_config/` (writable, symlinked or copied from vendor)
|
||||
- Vendor source: `/vendor/rfs/msm/mpss/readonly/vendor/mbn/` (read-only vendor partition)
|
||||
|
||||
On Android, QCRIL reads `mcfg.version` from `/vendor/rfs/msm/mpss/readonly/vendor/mbn/mcfg.version`
|
||||
(path literal in strings). It stores local DB metadata at
|
||||
`/data/vendor/modem_config/ver_info.txt` and in `qcril.db` tables.
|
||||
|
||||
The hardware config tree is indexed by `mbn_hw.txt`; the software config tree by `mbn_sw.txt`.
|
||||
QCRIL enumerates these lists to populate its internal DB (`qcril_mbn_hw_load_to_db`,
|
||||
`qcril_mbn_sw_load_to_db`) with version metadata parsed from each MBN file's header.
|
||||
|
||||
Error strings `"QCRIL_ERROR:IO: No hw mbn config directory"` and
|
||||
`"QCRIL_ERROR:IO: No sw mbn config directory"` confirm it expects both
|
||||
`/data/vendor/modem_config/mcfg_hw/` and `/data/vendor/modem_config/mcfg_sw/` to exist and
|
||||
be populated. The MBN path is also stored in `persist.vendor.radio.mbn_path` (property string
|
||||
in libril-qc-hal-qmi.so).
|
||||
|
||||
**On pmOS:** our MBN files live at `/mnt/vendor/persist/rfs/readonly/vendor/mbn/` (bind-mounted
|
||||
from the persist partition, analogous to tqftpserv's serve path). We have both `mcfg_hw/` and
|
||||
`mcfg_sw/` trees. The provisioner must be told where to find them.
|
||||
|
||||
### B.3 Config Selection — How QCRIL Picks the Right MBN
|
||||
|
||||
#### Hardware MBN Selection
|
||||
|
||||
Symbol `qcril_mbn_hw_get_hw_name_to_look_for` (T, exported): looks up the hardware platform
|
||||
name to match against. Symbol `qcril_mbn_hw_get_hw_config_from_db` looks up the best matching
|
||||
HW config from the internal DB by that name.
|
||||
|
||||
The HW name comes from the MBN metadata header embedded in each `mcfg_hw.mbn` file. Symbol
|
||||
`qcril_mbn_meta_retrieve_hw_name` reads the `HW_NAME` field from the MBN binary. The DB table
|
||||
`qcril_hw_mbn_file_type_table` stores `(FILE, HW_NAME, SHORT_NAME, CONFIG_ID, version fields)`.
|
||||
|
||||
The selection is a name-match: QCRIL identifies the running SoC/platform (from a system property
|
||||
or DMS device ID) and picks the HW MBN whose `HW_NAME` matches. For blueline/SDM845 + LA + SS
|
||||
(single-SIM), the match is `mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn`
|
||||
(verified: only SDM845/LA/SS and SDM845/LA/7+7_mode/SR_DSDS are in `mbn_hw.txt`; blueline is
|
||||
single-SIM so SS wins). The `HW_NAME` field in that MBN file must contain the identifier QCRIL
|
||||
matches against.
|
||||
|
||||
**Note:** The `HW_NAME` used for matching is extracted from the MBN binary header by
|
||||
`qcril_mbn_meta_retrieve_hw_name` — we don't have a text listing of what that string IS. For
|
||||
blueline we know the correct MBN from Android forensics: `mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn`.
|
||||
Our provisioner can hardcode this for SDM845/LA/SS rather than implement the full name extraction.
|
||||
|
||||
#### Software MBN Selection (Carrier)
|
||||
|
||||
This is the complex path. QCRIL uses a multi-level lookup in priority order:
|
||||
|
||||
**Level 1 — ICCID long-IIN (9-digit):** `qcril_mbn_db_retrieve_sw_mbn_file_for_long_iccid`
|
||||
queries `qcril_sw_mbn_iin_table` with the first 9 digits of the SIM's ICCID (`MCFG_LONG_IIN`
|
||||
field). If matched, uses that SW MBN. The ICCID is stored in `qcril_mbn_sw_iccid` (B, bss) and
|
||||
the current ICCID is read from `/data/vendor/radio/iccid` (path string in libril-qc-hal-qmi.so)
|
||||
or via UIM QMI `get_iccid` (`com.qualcomm.qti.qcril.uim.get_iccid_sync_request`).
|
||||
|
||||
**Level 2 — ICCID short-IIN (6-digit):** `qcril_mbn_db_retrieve_sw_mbn_file_for_iccid` queries
|
||||
`qcril_sw_mbn_iin_table` with the first 6 digits (`MCFG_IIN`).
|
||||
|
||||
**Level 3 — MCC/MNC:** `qcril_mbn_db_query_sw_mbn_file_with_mcc_mnc` queries
|
||||
`qcril_sw_mbn_mcc_mnc_table` with the SIM's MCC+MNC. MCC/MNC is retrieved via
|
||||
`com.qualcomm.qti.qcril.legacy.event.INTERNAL_UIM_GET_MCC_MNC` and/or the UIM IMSI path.
|
||||
|
||||
The DB tables (`qcril_sw_mbn_iin_table`, `qcril_sw_mbn_mcc_mnc_table`) are empty in the
|
||||
`qcril.db` we have — QCRIL populates them at runtime by parsing the MBN files and inserting
|
||||
rows. The `qcril.db` version in `qcril-config/` is a prebuilt baseline; QCRIL rebuilds the MBN
|
||||
tables on first boot or when `mcfg.version` changes.
|
||||
|
||||
The `qcril_manual_prov_table` contains 4 ICCID entries with `USER_PREF=1` — these are
|
||||
user-overridden manual provisioning entries that skip the automatic lookup. Our Fido SIM is
|
||||
unlikely to match these.
|
||||
|
||||
**Exception tables:** `qcril_mbn_iccid_exception_table` and `qcril_mbn_imsi_exception_table`
|
||||
are both empty in this build — no exceptions defined.
|
||||
|
||||
**For Rogers/Fido (MCC 302):** Fido is an MVNO on the Rogers network. The MBN tree contains
|
||||
`mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn` (line 25 in `mbn_sw.txt`). Rogers MCC
|
||||
is 302. Whether QCRIL matches Fido (sub-MVNO) to the Rogers MBN depends on whether Rogers's
|
||||
ICCID/IIN prefix or MCC/MNC 302-480 (Rogers) vs 302-370 (Fido) maps to that file in the
|
||||
runtime-built DB. QCRIL's logic also has a `persist.vendor.radio.sw_mbn_openmkt` (open-market)
|
||||
flag that may affect fallback behavior — evidence: property string in libril-qc-hal-qmi.so.
|
||||
|
||||
**IMEI awareness:** QCRIL does NOT use the IMEI for MBN selection. IMEI is read via
|
||||
`RilRequestGetDeviceIdentityMessage` / `DmsModule::handleDeviceIdentiyRequestMessage` for
|
||||
device identity reporting, not for PDC config selection. The `qcril_mbn_cur_instance_id` (B)
|
||||
is the slot/instance ID (0/1), not the IMEI.
|
||||
|
||||
**Subscription tracking:** QCRIL caches the current subscription's ICCID
|
||||
(`qcril_mbn_sw_iccid`), MCC (`qcril_mbn_cur_instance_mcc`), MNC (`qcril_mbn_cur_instance_mnc`),
|
||||
and sub_id (`qcril_mbn_cur_sub_id`). When any of these change (SIM swap), `qcril_mbn_sw_is_sim_info_different_from_cache`
|
||||
detects it and `qcril_mbn_sw_if_restart_needed` decides whether to re-run the SW provisioning
|
||||
sequence. String: `"restart needed due to mcc/mnc/iccid/sub_id change"`.
|
||||
|
||||
### B.4 Version/Diff/Skip Logic
|
||||
|
||||
Symbols `qcril_mbn_db_is_sw_version_updated` (T, exported) and the string
|
||||
`"file_name: %s, config_name: %s, version: 0x%08x, is_matched: %d"` describe the core
|
||||
version-check logic:
|
||||
|
||||
QCRIL parses each candidate MBN's version from its header (`mcfg_get_oem_version`,
|
||||
`mcfg_get_qc_version` — both exported T symbols) and compares them to what the modem currently
|
||||
holds. The modem's active config version is retrieved via `qcril_qmi_pdc_get_active_config_info`
|
||||
(which calls `QMI_PDC_GET_CONFIG_INFO` with the active config ID).
|
||||
|
||||
Version is stored as a 32-bit split field: `MCFG_VERSION_FAMILY`, `MCFG_VERSION_OEM`,
|
||||
`MCFG_VERSION_CARRIER`, `MCFG_VERSION_MINOR` (function `qcril_mbn_db_split_version`, T exported).
|
||||
|
||||
If the modem's active config version matches the candidate MBN's version (`is_matched == 1`),
|
||||
QCRIL skips the reload. This is the "already provisioned" fast path tracked via Android
|
||||
property `persist.vendor.radio.hw_mbn_loaded` / `persist.vendor.radio.sw_mbn_loaded` and
|
||||
`persist.vendor.radio.cnv.ver_info`. The string `"prev_ver_info: %s, cur_ver_info: %s"`
|
||||
shows QCRIL compares a cached version string (from persist prop) to the current MBN version
|
||||
before deciding to reload.
|
||||
|
||||
The event `QMI_RIL_PDC_PARSE_DIFF_RESULT` (`qcril_evt_id_QMI_RIL_PDC_PARSE_DIFF_RESULT`) and
|
||||
function `parse_mbn_diff_result` indicate that QCRIL can also interpret a diff-result indication
|
||||
from the modem to decide whether the new config differs enough from the active one to warrant
|
||||
a restart. String: `"mbn differences length ="`.
|
||||
|
||||
**Summary of skip conditions:**
|
||||
1. `persist.vendor.radio.hw_mbn_loaded` / `sw_mbn_loaded` is set AND version matches modem active config → skip reload (fast path).
|
||||
2. On warm SSR: query get_active_config_info; if active matches → skip reload.
|
||||
3. On cold boot with no active config (count=0): always load.
|
||||
|
||||
**For pmOS:** Since configs are volatile (proven: 0/0 after every cold boot, PROGRESS.md
|
||||
2026-06-20), version check is moot — there is never an existing active config on our cold boot.
|
||||
We always need to load. The skip logic matters only when we build a persistent-prop mechanism
|
||||
or if we add SIM-change re-provisioning.
|
||||
|
||||
### B.5 The Full Load → Select → Activate Sequence
|
||||
|
||||
Reconstructed from the symbol table event chain:
|
||||
|
||||
#### Phase 1: HW MBN (platform config) — `REQUEST_MBN_HW_*` events
|
||||
|
||||
1. `REQUEST_MBN_HW_INIT` → `qcril_mbn_hw_update_init_hdlr`: Initialize HW update state machine.
|
||||
2. `REQUEST_MBN_HW_GET_SELECTED_CONFIG` → `qcril_mbn_hw_query_selected_config_hndlr`:
|
||||
Call `QMI_PDC_GET_SELECTED_CONFIG` for type=PLATFORM. If active config matches candidate,
|
||||
skip to SW phase.
|
||||
3. `REQUEST_MBN_HW_LOAD_CONFIG` → `qcril_mbn_hw_load_config_hndlr`:
|
||||
Chunked `QMI_PDC_LOAD_CONFIG` for the selected HW MBN file.
|
||||
4. `REQUEST_MBN_HW_SELECT_CONFIG` → `qcril_mbn_hw_select_config_hndlr`:
|
||||
`QMI_PDC_SET_SELECTED_CONFIG` for type=PLATFORM with the just-loaded config ID.
|
||||
5. `REQUEST_MBN_HW_ACTIVATE_CONFIG` → `qcril_mbn_hw_activate_config_hndlr`:
|
||||
`QMI_PDC_ACTIVATE_CONFIG` for type=PLATFORM. Modem restarts MPSS to apply MCFG.
|
||||
|
||||
Interleaved: `REQUEST_MBN_HW_DELETE_CONFIG` and `REQUEST_MBN_HW_DEACTIVATE_CONFIG` are also
|
||||
in the symbol table; QCRIL cleans up old/stale configs. `qcril_mbn_cleanup_inactive_configs`
|
||||
and `qcril_mbn_pdc_delete_all_sw_configs` delete inactive entries from modem RAM before loading
|
||||
new ones (to avoid hitting the modem's config count limit).
|
||||
|
||||
#### Phase 2: SW MBN (carrier config) — `REQUEST_MBN_SW_*` events
|
||||
|
||||
After HW phase completes (modem has restarted), SW phase runs:
|
||||
|
||||
1. `REQUEST_MBN_SW_INIT` → `qcril_mbn_sw_update_init_hdlr`: Start SW state machine; read ICCID
|
||||
and MCC/MNC from SIM (requires UIM to be up and SIM app selected).
|
||||
2. `VERIFY_MBN_SW_INIT` / `REQUEST_VERIFY_MBN_SW_INIT`: Check whether modem supports the MBN
|
||||
update feature at all (`qcril_qmi_imss_query_modem_supported_features`). String:
|
||||
`"Modem feature not supported. Continue with sw mbn update"` — if unsupported, SW update
|
||||
continues anyway (the feature check gates VoLTE-specific paths, not the basic PDC load).
|
||||
3. `REQUEST_MBN_SW_GET_SELECTED_CONFIG` → `qcril_mbn_sw_query_selected_config_hndlr`:
|
||||
Query `QMI_PDC_GET_SELECTED_CONFIG` for type=SOFTWARE. Version-check against candidate.
|
||||
4. `REQUEST_MBN_SW_COUNT_PENDING_CONFIGS` → `qcril_mbn_sw_count_pending_configs_hndlr`:
|
||||
Count pending (loaded but not yet selected) SW configs on the modem. String:
|
||||
`"has pending configuration"`. If a pending config already matches the desired SW MBN, QCRIL
|
||||
may skip the load step and go directly to select.
|
||||
5. `REQUEST_MBN_SW_LOAD_CONFIG` → `qcril_mbn_sw_load_config_hndlr`:
|
||||
Chunked `QMI_PDC_LOAD_CONFIG` for the selected SW MBN file (type=SOFTWARE).
|
||||
6. `REQUEST_MBN_SW_SELECT_CONFIG` → `qcril_mbn_sw_select_config_hndlr`:
|
||||
`QMI_PDC_SET_SELECTED_CONFIG` for type=SOFTWARE.
|
||||
7. `REQUEST_MBN_SW_ACTIVATE_CONFIG` → `qcril_mbn_sw_activate_config_hndlr`:
|
||||
`QMI_PDC_ACTIVATE_CONFIG` for type=SOFTWARE. Second modem restart.
|
||||
8. `REQUEST_MBN_SW_CLEANUP_CONFIG` → `qcril_mbn_sw_cleanup_config_hndlr`: Delete stale configs.
|
||||
|
||||
#### Phase 3: PDC Refresh Indication
|
||||
|
||||
`qcril_qmi_pdc_refresh_ind_hdlr` (`_Z30qcril_qmi_pdc_refresh_ind_hdlrPvj`) and
|
||||
`PDCRefreshIndication` / `"handlePDCRefreshInd(): "` — the modem can emit a PDC refresh
|
||||
indication after activation. QCRIL handles this to re-query config state. The `DataModule` also
|
||||
handles `PDCRefreshIndication` (`_ZN7rildata10DataModule26handlePDCRefreshIndicationENSt3__110shared_ptrI7MessageEE`).
|
||||
|
||||
This is the modem telling userspace "I've applied the new configs." QCRIL listens for it but
|
||||
does NOT depend on it to proceed (it's a notification, not a gate).
|
||||
|
||||
#### The enable/disable modem update mechanism
|
||||
|
||||
Symbols `qcril_qmi_pdc_enable_modem_update`, `qcril_qmi_pdc_disable_modem_update`,
|
||||
`pdc_enable_auto_selection` indicate that before loading configs, QCRIL may call
|
||||
`QMI_PDC_SET_FEATURE_VERSION` or a similar PDC command to enable/lock auto-selection on the
|
||||
modem side. `qcril_qmi_pdc_is_modem_mbn_updated` checks if the modem already has the desired
|
||||
config loaded (query path, not load path). **The exact QMI message ID for enable/disable is not
|
||||
visible from strings alone** — this is an under-documented PDC message not exposed via libqmi
|
||||
public API. Evidence is ambiguous; mark as uncertain.
|
||||
|
||||
### B.6 EFS Interaction
|
||||
|
||||
Minimal and indirect. QCRIL itself does NOT write to EFS for config provisioning. The evidence:
|
||||
|
||||
- `current MBNs in modem EFS:` (string in libril-qc-hal-qmi.so) — QCRIL can enumerate what's
|
||||
in modem EFS via a PDC query command (list configs), but this is a read.
|
||||
- Android forensics (PROGRESS.md 2026-06-19): `rmt_storage` serves EFS read-write; the modem
|
||||
writes ~2 MB to `modem_fs2` AFTER going online. This EFS write happens INSIDE the modem
|
||||
firmware after RF-init completes — not driven by QCRIL.
|
||||
- The `EFS lead at PAF/modem.md L38` is marked DEAD: rmtfs writable + Android-primed EFS still
|
||||
gave DeviceNotReady. EFS content is not what's missing.
|
||||
|
||||
**Conclusion:** QCRIL does not write EFS for PDC provisioning. It loads configs into modem RAM
|
||||
via QMI PDC, the modem applies them at RF-init and then writes its own EFS state. Our provisioner
|
||||
does not need to write EFS.
|
||||
|
||||
### B.7 The OTA Update Path (not needed for us)
|
||||
|
||||
`qcril_mbn_kick_ota_update_in_dedicated_thread` handles over-the-air config updates (carrier
|
||||
pushing new MBNs). This runs in a dedicated thread, separate from boot provisioning. Not
|
||||
relevant to our bring-up.
|
||||
|
||||
### B.8 Properties Used by QCRIL
|
||||
|
||||
| Property | Purpose |
|
||||
|---|---|
|
||||
| `persist.vendor.radio.hw_mbn_loaded` | Cached: HW MBN was loaded (skip on next boot if version matches) |
|
||||
| `persist.vendor.radio.sw_mbn_loaded` | Cached: SW MBN was loaded |
|
||||
| `persist.vendor.radio.mbn%d` | Per-slot carrier index (e.g., `mbn0=72`) |
|
||||
| `persist.vendor.radio.mbn_path` | Path to the active MBN directory |
|
||||
| `persist.vendor.radio.cnv.ver_info` | Cached version info string (prev vs current comparison) |
|
||||
| `persist.vendor.radio.ver_info` | Version info |
|
||||
| `persist.vendor.radio.sw_mbn_update` | SW update enabled/disabled flag |
|
||||
| `persist.vendor.radio.hw_mbn_update` | HW update enabled/disabled flag |
|
||||
| `persist.vendor.radio.sw_mbn_openmkt` | Open-market SIM handling flag |
|
||||
| `persist.vendor.radio.long_iin_mbn` | Use long-IIN (9-digit) matching for SW MBN |
|
||||
|
||||
These are Android system properties not directly available on pmOS. Our provisioner should
|
||||
implement equivalent state tracking in a config file (e.g., `/var/lib/qcril-prov/state.json`).
|
||||
|
||||
---
|
||||
|
||||
## C. The Gap — Itemized
|
||||
|
||||
Each item marked: **(RF-arm)** = required to arm RF at all, **(carrier)** = carrier-correctness,
|
||||
**(nice)** = quality improvement.
|
||||
|
||||
| # | Gap | Severity |
|
||||
|---|---|---|
|
||||
| 1 | **Timing: provisioner must run before RF-init window closes** — our scripts run manually; QCRIL runs automatically on DMS-service-ready event, before `sys.boot_completed`. A systemd service that fires before ModemManager and monitors DMS readiness is absent. | **(RF-arm)** |
|
||||
| 2 | **HW MBN must be loaded before SW MBN** — our scripts don't enforce ordering. QCRIL runs the full HW phase (load+select+activate → modem restart) before starting SW phase. Interleaving or reversing causes modem inconsistency. | **(RF-arm)** |
|
||||
| 3 | **Post-activate modem restart handling** — on `QMI_PDC_ACTIVATE_CONFIG` the modem subsystem restarts. QCRIL re-waits for DMS to become ready after each restart before proceeding to the next phase (SW after HW). Our script ignores the restart and the QMI transport drop. | **(RF-arm)** |
|
||||
| 4 | **SW MBN carrier selection by ICCID/MCC-MNC** — we hardcode the Rogers MBN path. QCRIL reads ICCID from `/data/vendor/radio/iccid` and queries UIM MCC/MNC, then does a DB lookup. Without this, a SIM swap would need a manual provisioner edit. | **(carrier)** |
|
||||
| 5 | **Stale config cleanup before loading** — QCRIL calls `qcril_mbn_cleanup_inactive_configs` / `qcril_mbn_pdc_delete_all_sw_configs` to delete old configs before loading new ones. Without cleanup, the modem accumulates stale RAM-configs and may hit internal limits or pick the wrong active config. | **(RF-arm / carrier)** |
|
||||
| 6 | **SW pending config check** — QCRIL calls `QMI_PDC_QUERY_PENDING_CONFIG` (event `REQUEST_MBN_SW_QUERY_PENDING_CONFIG`) before loading to see if the desired config is already pending. Missing this means redundant loads, not a breakage, but it is part of the correct sequence. | **(nice)** |
|
||||
| 7 | **Version skip logic** — QCRIL skips reload when modem already holds matching config version. Without this, we reload every boot unconditionally (harmless functionally but slow and causes an extra modem restart). | **(nice)** |
|
||||
| 8 | **PDC modem-update enable/disable** — QCRIL may call a PDC feature-version command around the load. The exact behavior is unclear from strings alone (ambiguous evidence). May be needed to unlock PDC loading on certain modem states. **UNCERTAIN — needs disassembly to confirm.** | **(RF-arm, uncertain)** |
|
||||
| 9 | **SIM-change re-provisioning** — QCRIL monitors UIM refresh indications and re-runs SW provisioning on SIM swap. Not needed for single-boot bring-up but needed for production use. | **(nice)** |
|
||||
| 10 | **HW name extraction from MBN header** — QCRIL reads the `HW_NAME` field from each `mcfg_hw.mbn` binary to populate the DB, then matches it against the running platform. We can hardcode SDM845/LA/SS for blueline, deferring this. | **(nice for generality)** |
|
||||
|
||||
---
|
||||
|
||||
## D. Design — Full Provisioner
|
||||
|
||||
### D.0 Guiding Constraints
|
||||
|
||||
- Must complete (both HW and SW activated) before the modem's RF-init window closes. From
|
||||
PROGRESS.md: PDC LoadConfig times out if attempted before ~45 s uptime; succeeds by ~226 s.
|
||||
The RF-init window is within the modem's post-MPSS-up initialization, which is between ~12 s
|
||||
and some unknown deadline (likely within 60 s of MPSS-up). **The critical open question:**
|
||||
does the modem wait indefinitely for a config or does it time out and RF-init with whatever
|
||||
is loaded? This must be determined empirically (see Next Steps).
|
||||
- Run as a systemd service, ordered before ModemManager, after QRTR/QMI is up.
|
||||
- Read-only EFS — no EFS writes needed from our side.
|
||||
- Python (libqmi GI) to reuse pdc_load.py/pdc_activate.py primitives.
|
||||
|
||||
### D.1 Service Unit
|
||||
|
||||
File: `/etc/systemd/system/qcril-prov.service`
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=QMI PDC MBN provisioner (QCRIL equivalent)
|
||||
After=qrtr.service rmtfs.service
|
||||
Before=ModemManager.service
|
||||
Wants=qrtr.service rmtfs.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStart=/usr/local/bin/qcril-prov.py
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### D.2 Provisioner Script Design (`qcril-prov.py`)
|
||||
|
||||
#### Stage 0: Wait for DMS
|
||||
|
||||
Poll `qmicli -d qrtr://0 --dms-get-ids` until it returns IMEI without error. Timeout 120 s.
|
||||
This replicates QCRIL's `DmsModule::handleDmsEndpointStatusIndMessage` trigger. Retry interval:
|
||||
2 s. Add a jitter-free deadline check (not a sleep loop — use subprocess with timeout).
|
||||
|
||||
EVIDENCE: string `"DMS client is ready. start MBN update"` from DmsModule.
|
||||
|
||||
#### Stage 1: Read SIM Identity (for SW MBN selection)
|
||||
|
||||
Read ICCID and MCC/MNC before touching PDC, as QCRIL does.
|
||||
|
||||
- Option A (simple): read `/data/vendor/radio/iccid` (path string in libril-qc-hal-qmi.so).
|
||||
On pmOS this file may not be populated by anything. Fallback to QMI.
|
||||
- Option B (robust): `qmicli --uim-get-card-status` to get ICCID, then
|
||||
`qmicli --nas-get-home-network` or `--uim-get-imsi` to get MCC/MNC.
|
||||
|
||||
For Rogers/Fido: MCC=302, MNC=480 (Rogers) or 302-370 (Fido).
|
||||
The mbn_sw.txt has `mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn` — this is the file
|
||||
to use. Fido uses Rogers infrastructure (same MCC prefix 302); whether the DB lookup maps Fido's
|
||||
IIN/MNC to the Rogers MBN depends on what was in the runtime-built DB on Android. Empirically,
|
||||
the modem ran Fido LTE on Android with the Rogers MBN active, so Rogers/CA is the correct SW
|
||||
MBN for both carriers on this device. Hardcode Rogers for now; add MCC/MNC dispatch later.
|
||||
|
||||
#### Stage 2: Delete All Existing Configs
|
||||
|
||||
Replicate `qcril_mbn_cleanup_inactive_configs` / `qcril_mbn_pdc_delete_all_sw_configs`.
|
||||
|
||||
Use `QMI_PDC_LIST_CONFIGS` (both types) → for each config ID returned, call
|
||||
`QMI_PDC_DELETE_CONFIG`. This clears the modem's RAM before we load fresh.
|
||||
|
||||
EVIDENCE: event `QMI_RIL_PDC_DELETE_ALL` (`qcril_evt_id_QMI_RIL_PDC_DELETE_ALL`) and
|
||||
`qcril_mbn_pdc_delete_all_sw_configs`.
|
||||
|
||||
Implementation via libqmi GI: `Qmi.MessagePdcDeleteConfigInput.new()`, set type and config_id,
|
||||
call `client.delete_config()`. Loop over both PLATFORM and SOFTWARE types.
|
||||
|
||||
**Why needed:** avoids stale config accumulation and ensures we control what's active. On cold
|
||||
boot the list is 0/0 (PROGRESS.md 2026-06-20), so this is a no-op on cold boot but defensive
|
||||
for warm SSR.
|
||||
|
||||
#### Stage 3: Load + Select + Activate HW MBN
|
||||
|
||||
MBN file: `/mnt/vendor/persist/rfs/readonly/vendor/mbn/mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn`
|
||||
(56 KB, confirmed in PAF/modem.md)
|
||||
|
||||
1. **Load:** use `pdc_load.py` logic (already working) with type=PLATFORM.
|
||||
2. **Get config ID:** after load completes, call `QMI_PDC_LIST_CONFIGS` type=PLATFORM to find
|
||||
the ID of the just-loaded config. (The LoadConfig indication does not return the ID;
|
||||
ListConfigs does.)
|
||||
3. **Select:** `QMI_PDC_SET_SELECTED_CONFIG` type=PLATFORM, id=<returned ID>.
|
||||
4. **Activate:** `QMI_PDC_ACTIVATE_CONFIG` type=PLATFORM. Modem MPSS restarts.
|
||||
|
||||
EVIDENCE: full symbol chain `qcril_mbn_hw_load_config_hndlr` → `qcril_mbn_hw_select_config_hndlr`
|
||||
→ `qcril_mbn_hw_activate_config_hndlr` in libril-qc-hal-qmi.so nm output.
|
||||
|
||||
#### Stage 4: Wait for Modem to Come Back After HW Restart
|
||||
|
||||
After HW activation, the modem MPSS restarts. QCRIL detects this via QMI transport drop +
|
||||
re-registration of DMS service. Poll same as Stage 0. Timeout 60 s.
|
||||
|
||||
Without this wait, the SW load in Stage 5 will fail (PDC client not available / modem not yet
|
||||
QMI-ready).
|
||||
|
||||
EVIDENCE: pdc_activate.py comment: "config activation is expected to reboot the device"; modem
|
||||
restart events `QCRIL_EVT_QMI_RIL_MODEM_RESTART_*`; PROGRESS.md 2026-06-19 "qmicli
|
||||
--pdc-activate-config=platform triggered modem restart that cleared the platform config."
|
||||
|
||||
#### Stage 5: Load + Select + Activate SW MBN
|
||||
|
||||
MBN file (Rogers/CA): `/mnt/vendor/persist/rfs/readonly/vendor/mbn/mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn`
|
||||
(MCC=302 carrier confirmed active on Android in PROGRESS.md 2026-06-19)
|
||||
|
||||
Same sequence as Stage 3 but type=SOFTWARE.
|
||||
|
||||
1. Load (chunked, type=SOFTWARE)
|
||||
2. List configs → get ID
|
||||
3. SetSelectedConfig (type=SOFTWARE)
|
||||
4. ActivateConfig (type=SOFTWARE) → second modem restart
|
||||
|
||||
EVIDENCE: `qcril_mbn_sw_load_config_hndlr` → `qcril_mbn_sw_select_config_hndlr` →
|
||||
`qcril_mbn_sw_activate_config_hndlr` in nm output.
|
||||
|
||||
#### Stage 6: Wait and Verify
|
||||
|
||||
After SW activation, wait for DMS to come back (Stage 0 again). Then run the modem_health.sh
|
||||
check logic inline: call `QMI_DMS_GET_CAPABILITIES` and verify `Networks` is non-empty.
|
||||
|
||||
If `Networks` is non-empty, provisioning succeeded. Set a state file
|
||||
(`/var/lib/qcril-prov/last_provisioned`) and exit 0 so systemd marks the service complete.
|
||||
|
||||
If `Networks` is still empty: log a diagnostic (PDC list state, DMS caps) and exit non-zero.
|
||||
Systemd will not start ModemManager (it's ordered After this service).
|
||||
|
||||
#### Stage 7: Hand Off to ModemManager
|
||||
|
||||
With both configs active and RF armed (Networks populated), ModemManager's
|
||||
`Set Operating Mode=online` should succeed. ModemManager starts as normal.
|
||||
|
||||
### D.3 SW MBN Selection Logic (future)
|
||||
|
||||
When we want auto-carrier selection (beyond hardcoded Rogers):
|
||||
|
||||
```python
|
||||
# Selection priority order (matches QCRIL):
|
||||
# 1. ICCID long-IIN (9 digits) → qcril_sw_mbn_iin_table.MCFG_LONG_IIN
|
||||
# 2. ICCID short-IIN (6 digits) → qcril_sw_mbn_iin_table.MCFG_IIN
|
||||
# 3. MCC+MNC → qcril_sw_mbn_mcc_mnc_table
|
||||
# 4. Wildcard → mcfg_sw/generic/common/WildCard/Wildcard/mcfg_sw.mbn
|
||||
|
||||
def select_sw_mbn(iccid, mcc, mnc, db_path):
|
||||
import sqlite3
|
||||
db = sqlite3.connect(db_path)
|
||||
# Populate DB from MBN tree if empty (QCRIL does this at runtime)
|
||||
# ... (qcril_mbn_sw_load_to_db equivalent)
|
||||
long_iin = iccid[:9] if iccid else None
|
||||
short_iin = iccid[:6] if iccid else None
|
||||
if long_iin:
|
||||
row = db.execute("SELECT FILE FROM qcril_sw_mbn_iin_table WHERE MCFG_LONG_IIN=?",
|
||||
[long_iin]).fetchone()
|
||||
if row: return row[0]
|
||||
if short_iin:
|
||||
row = db.execute("SELECT FILE FROM qcril_sw_mbn_iin_table WHERE MCFG_IIN=?",
|
||||
[short_iin]).fetchone()
|
||||
if row: return row[0]
|
||||
if mcc and mnc:
|
||||
row = db.execute("SELECT FILE FROM qcril_sw_mbn_mcc_mnc_table WHERE MCC=? AND MNC=?",
|
||||
[mcc, mnc]).fetchone()
|
||||
if row: return row[0]
|
||||
return "mcfg_sw/generic/common/WildCard/Wildcard/mcfg_sw.mbn" # fallback
|
||||
```
|
||||
|
||||
**Note:** The `qcril.db` tables will be empty until we populate them. QCRIL populates them by
|
||||
parsing each MBN binary's header (IIN/MCC-MNC metadata embedded in the MCFG blob). We need to
|
||||
implement MBN metadata parsing to extract IIN and MCC-MNC lists from the MBN binary format to
|
||||
build the DB. This is deferred — hardcoded Rogers is correct for this device and SIM.
|
||||
|
||||
### D.4 Solving the Timing Wall
|
||||
|
||||
> **CORRECTION 2026-06-20 (capture analysis — see `PAF/modem.md` frontier #4):** The "load +
|
||||
> activate every boot, accept two restarts" model below is a *reconstruction of the unobserved
|
||||
> first-provision path*, NOT what Android does in steady state. The captured Android radio logs
|
||||
> (`android-live-20260619/deep/`) show steady-state boot is **verify-only**: `is_modem_mbn_updated`
|
||||
> finds `prev_ver == cur_ver` and SKIPS the entire load/select/activate sequence; the modem
|
||||
> self-applies MCFG persisted in its own EFS (`modemst`) at firmware RF-init. The full load path
|
||||
> (and any `pdc_enable_auto_selection` call) runs only on first-provision / version change, which
|
||||
> we have never captured. This section is retained as the design for the per-boot *workaround*
|
||||
> (`tools/qcril-prov.py`, path A); the hardware-faithful target is provision-once + `modemst`
|
||||
> persistence (path B). Resolve via the first-provision capture: `PAF/first_provision_capture.md`.
|
||||
|
||||
**The question:** Does the modem wait indefinitely for configs before RF-init, or does it have
|
||||
a deadline?
|
||||
|
||||
**What we know:** PROGRESS.md 2026-06-20 states "PDC LoadConfig times out if attempted too
|
||||
early (uptime ~45 s, modem not yet PDC-ready) but succeeds by ~226 s." This is the PDC-client
|
||||
readiness window (when QRTR route is established). The RF-init window is a separate question.
|
||||
|
||||
**Hypothesis A:** The modem RF-inits as part of MPSS boot (~12 s) with whatever configs are
|
||||
loaded. On cold pmOS boot there are none (0/0), so RF-init proceeds with no MCFG = no RF arm.
|
||||
Provisioning after the fact cannot retroactively arm RF. The modem must be restarted (via
|
||||
PDC ActivateConfig) after configs are loaded, which triggers a new RF-init pass WITH the
|
||||
configs. This is the mechanism QCRIL uses.
|
||||
|
||||
**This matches all evidence:** PROGRESS.md 2026-06-20 states "A post-init userspace load cannot
|
||||
arm an already-initialized RF, and nothing persists to carry configs to next boot." And:
|
||||
"configs are PURELY VOLATILE, NOT persisted."
|
||||
|
||||
**Implication for our provisioner:** The timing wall is NOT about being early enough — it's
|
||||
about triggering a PDC ActivateConfig (which causes modem restart + re-RF-init with configs
|
||||
loaded). QCRIL's "early" timing is an optimization to reduce the number of restarts (load before
|
||||
first RF-init = one restart; load after first RF-init = two restarts). Our provisioner will
|
||||
cause two MPSS restarts per boot (once for HW, once for SW). This is acceptable for bring-up.
|
||||
|
||||
**The `--pdc-monitor-refresh` question** (PROGRESS.md 2026-06-20 "NEXT"): The modem may emit
|
||||
a PDC refresh indication (`PDCRefreshIndication`, `qcril_qmi_pdc_refresh_ind_hdlr`) after
|
||||
ActivateConfig, signaling completion. We should subscribe to this indication (via libqmi
|
||||
`RegisterForPDCIndication`) to confirm activation before proceeding to the SW phase.
|
||||
EVIDENCE: `"handleRegisterForPDCIndication(): failed with rc="` and
|
||||
`"]: pdc refresh Completed"` strings.
|
||||
|
||||
**Recommended approach for `--pdc-monitor-refresh`:** Add a PDC indication registration at
|
||||
startup and listen for `QMI_PDC_CONFIG_CHANGE_IND` (which is what `pdc_refresh_ind_hdlr`
|
||||
handles). After each ActivateConfig, wait for the refresh-completed indication before polling
|
||||
DMS. Timeout 30 s if indication doesn't arrive (fall through to DMS poll).
|
||||
|
||||
---
|
||||
|
||||
## E. Implementation Sequence (Recommended)
|
||||
|
||||
### Step 0 (validation experiment, before any code): confirm two-restart approach works
|
||||
|
||||
Manual test on device:
|
||||
```sh
|
||||
# Clean slate
|
||||
qmicli -d qrtr://0 --pdc-list-configs=platform # should show 0
|
||||
qmicli -d qrtr://0 --pdc-list-configs=software # should show 0
|
||||
|
||||
# Load + select + activate HW
|
||||
python3 pdc_load.py qrtr://0 platform .../mcfg_hw.mbn
|
||||
# get ID from pdc-list-configs=platform
|
||||
python3 pdc_activate.py qrtr://0 platform <ID>
|
||||
# wait ~30s for modem restart and DMS back
|
||||
|
||||
# Load + select + activate SW (Rogers)
|
||||
python3 pdc_load.py qrtr://0 software .../mcfg_sw.mbn
|
||||
# get ID from pdc-list-configs=software
|
||||
python3 pdc_activate.py qrtr://0 software <ID>
|
||||
# wait ~30s
|
||||
|
||||
# Check RF
|
||||
qmicli -d qrtr://0 --dms-get-capabilities # expect Networks non-empty
|
||||
```
|
||||
|
||||
This is the minimal end-to-end proof before building the daemon.
|
||||
|
||||
### Step 1: `qcril-prov.py` v1 (hardcoded)
|
||||
|
||||
Single script implementing the full D.2 flow with:
|
||||
- Hardcoded HW MBN path (SDM845/LA/SS)
|
||||
- Hardcoded SW MBN path (Rogers/CA)
|
||||
- DMS poll loop (Stage 0)
|
||||
- Delete-all existing configs (Stage 2)
|
||||
- HW load/select/activate (Stage 3)
|
||||
- Wait for modem restart (Stage 4, DMS poll again)
|
||||
- SW load/select/activate (Stage 5)
|
||||
- Verify Networks non-empty (Stage 6)
|
||||
|
||||
No carrier selection DB needed yet.
|
||||
|
||||
### Step 2: systemd integration
|
||||
|
||||
Wire the service unit (D.1), enable it, reboot, confirm RF arms without manual intervention.
|
||||
|
||||
### Step 3: add carrier selection (future, when needed for other SIMs)
|
||||
|
||||
Implement MBN binary parser for IIN/MCC-MNC metadata, populate a local sqlite3 DB, implement
|
||||
`select_sw_mbn()` (D.3).
|
||||
|
||||
---
|
||||
|
||||
## F. Open Questions (evidence-bounded)
|
||||
|
||||
1. **Does the modem accept ActivateConfig before DMS is "fully" ready?** We know DMS must be
|
||||
up for QCRIL to start — but does DMS-up precede or follow the RF-init window? Unknown without
|
||||
`--pdc-monitor-refresh` experiment or timing capture.
|
||||
|
||||
2. **What is `pdc_enable_auto_selection(ii)` / `qcril_qmi_pdc_enable_modem_update`?** The
|
||||
symbol `_Z25pdc_enable_auto_selectionii` exists in the binary. It may correspond to
|
||||
`QMI_PDC_SET_FEATURE_VERSION` or a similar private PDC message that tells the modem to
|
||||
accept auto-selected configs. If this is required, LoadConfig will succeed but ActivateConfig
|
||||
may no-op. **Needs disassembly of `qcril_mbn_hw_update_init` to determine if it's called
|
||||
before the load sequence.** This is the only remaining significant unknown.
|
||||
|
||||
3. **Does the Fido SIM (MVNO on Rogers) match the Rogers/CA MBN via MCC-MNC or IIN?** Fido's
|
||||
MCC-MNC is 302-370; Rogers is 302-720. The Rogers MBN was active on Android with this Fido
|
||||
SIM. So Rogers/CA is confirmed correct. The IIN or MCC-MNC lookup that produced this result
|
||||
is unknown without the runtime-populated DB — but the outcome is known.
|
||||
|
||||
4. **Is the HW MBN name match on "SDM845-LA-SS" or another string?** `qcril_mbn_hw_get_hw_name_to_look_for`
|
||||
returns the string used to match HW configs. The actual string is not visible without
|
||||
disassembly. For blueline, only one SDM845/LA/SS entry exists in mbn_hw.txt; even if our
|
||||
provisioner gets the name wrong, we can hardcode the file path.
|
||||
|
||||
---
|
||||
|
||||
## Sources
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `android-reference/qcril-libs/libril-qc-hal-qmi.so` | Primary evidence: symbols (nm -D), strings |
|
||||
| `android-reference/vendor-mbn/mbn/mbn_hw.txt` | HW MBN index (2 entries for SDM845) |
|
||||
| `android-reference/vendor-mbn/mbn/mbn_sw.txt` | SW MBN index (55 entries, includes Rogers/CA) |
|
||||
| `android-reference/vendor-mbn/mbn/mcfg.version` | MCFG version: `g845-00194-220517-B-8604334` |
|
||||
| `android-reference/qcril-config/qcril.db` | DB schema; tables empty in prebuilt (QCRIL populates at runtime) |
|
||||
| `android-reference/init-rc/qcrild.rc` | `class main`, confirms qcrild timing |
|
||||
| `android-reference/init-rc/init.sdm845.rc` | Boot ordering; `rmt_storage` = `class core` (earlier than main) |
|
||||
| `PAF/modem.md` | Eliminated leads; confirmed facts about PDC volatility and EFS |
|
||||
| `PROGRESS.md` (2026-06-20 entry) | Decisive timing proof; QCRIL symbol inventory |
|
||||
| `tools/pdc_load.py`, `tools/pdc_activate.py` | Our current implementation baseline |
|
||||
229
saf/device/evidence/qcril_online_sequence.md
Normal file
229
saf/device/evidence/qcril_online_sequence.md
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
# QCRIL pre-online QMI choreography — extracted from libril-qc-hal-qmi.so
|
||||
|
||||
Binary: `android-reference/vendor-stack-20260621/vendor/lib64/libril-qc-hal-qmi.so`
|
||||
(ARM64, stripped of locals but **full dynamic symbol table present** — 22045 syms, qcril
|
||||
function names intact). Decompiler unavailable (no rz-ghidra / pdg); extraction is from
|
||||
disassembly of named functions + QMI message-id immediates + the log-string call flow +
|
||||
the working-Android logs in `android-capture-20260621/diag/`.
|
||||
|
||||
All addresses are file offsets into the `.so` `.text`. Disassembly cited inline.
|
||||
|
||||
---
|
||||
|
||||
## 0. What the binary settles (resolves the doc contradiction)
|
||||
|
||||
`PAF/qcril_bringup_capture.md` ends with a REFRAME claiming "the Android modem self-onlines;
|
||||
there is no QCRIL online handshake." The binary shows that is **half right**: QCRIL **does**
|
||||
issue DMS Set Operating Mode, but only as the **last step of a gated state machine**
|
||||
(`qcril_qmi_nas_boot_up_optimization_state_machine` @ **0xfe4730**). The thing pmOS skips is
|
||||
not "an online command" — it is the **pre-conditioning QCRIL performs on the modem before it
|
||||
asks it to go online**. That pre-conditioning is the choreography below.
|
||||
|
||||
The "self-online" observation on Android (op-mode already 0 at QCRIL attach) is because on a
|
||||
*warm* / SSR path the modem was already conditioned in a prior boot and the state persisted;
|
||||
the cold first-boot path runs the full state machine. pmOS only ever does the cold bare
|
||||
`dms-set-operating-mode=online` with **none** of the pre-conditioning → DeviceNotReady(52).
|
||||
|
||||
---
|
||||
|
||||
## 1. The bring-up orchestrator — `qcril_qmi_nas_boot_up_optimization_state_machine` @ 0xfe4730
|
||||
|
||||
Disassembled call order (filtered to the load-bearing `bl`s):
|
||||
|
||||
```
|
||||
fe4850 bl qcril_qmi_nas_get_bootup_power_optimization_state
|
||||
fe4880 bl qcril_qmi_nas_decide_mode_pref_to_set_on_modem(&pref) ; 0xfe4c00
|
||||
fe489c bl qmi_ril_nwreg_request_mode_pref(i, &pref) ; 0x10f6f98 <-- NAS mode-pref push
|
||||
fe48cc bl qcril_qmi_nas_save_android_mode_pref(i)
|
||||
fe48d8 bl qcril_qmi_nas_get_atel_ui_ready_req_came_from_cache() ; 0xfe4ec4
|
||||
fe48f0 bl qcril_qmi_nas_set_atel_ui_status_to_modem(h) ; 0xfe5070 <-- DMS UI-ready push
|
||||
fe48f8 bl qcril_qmi_nas_set_atel_ui_status_to_cache(h)
|
||||
fe4904 bl qcril_qmi_nas_set_operating_mode_state(enum) ; 0xfe53c8 <-- DMS Set Operating Mode
|
||||
fe490c bl qcril_qmi_nas_set_bootup_power_optimization_state(2)
|
||||
```
|
||||
|
||||
So the ordered, **load-bearing** pre-online sequence the modem is conditioned with is:
|
||||
|
||||
1. **Decide + push the NAS mode preference (RAT set)** — tell the modem which radio
|
||||
technologies to acquire — BEFORE asking it to power the RF.
|
||||
2. **Push the ATEL "UI ready" status to the modem over DMS** — a Qualcomm OEM DMS message.
|
||||
3. **Only then** issue DMS Set Operating Mode.
|
||||
|
||||
`qcril_qmi_nas_telephony_ready` @ 0x1015a50 carries the log
|
||||
`poweron_opt: 'UI ready' NOT sent to modem` (string confirmed in binary) — i.e. the UI-ready
|
||||
push is conditionally skipped when the modem is already online, which is exactly the
|
||||
"self-online" warm path that misled the earlier reframe.
|
||||
|
||||
---
|
||||
|
||||
## 2. Step detail + QMI message IDs (from the immediates)
|
||||
|
||||
### Step A — NAS mode preference (LOAD-BEARING)
|
||||
`qmi_ril_nwreg_request_mode_pref` @ **0x10f6f98** builds the mode/band/acq-order prefs
|
||||
(`qcril_qmi_nas_get_mode_pref` @0x1da4890, `qcril_qmi_nas_get_gw_acq_order_pref` @0x1da4d50,
|
||||
`qcril_qmi_nas_get_5g_mode_pref` @0x1da3940) and dispatches through the shared sender at
|
||||
`qcril_qmi_nas_request_set_preferred_network_type+0x340` (0x10f6f68 region). That sender
|
||||
issues **NAS Set System Selection Preference** (QMI NAS id **0x0033**, confirmed present in
|
||||
`references/libqmi/data/qmi-service-nas.json` line 1437 "Set System Selection Preference").
|
||||
→ **Replayable via `qmicli --nas-set-system-selection-preference`.**
|
||||
|
||||
### Step B — ATEL UI-ready push (LOAD-BEARING, **NOT replayable with stock tooling**)
|
||||
`qcril_qmi_nas_set_atel_ui_status_to_modem` @ **0xfe5070**:
|
||||
```
|
||||
fe5124 mov w0, #0x4 ; qcril client enum 4 = DMS
|
||||
fe5128 bl qcril_qmi_client_get_user_handle
|
||||
fe512c mov w1, #0x64 ; <-- QMI message id 0x64 (100) on DMS
|
||||
fe5134 mov w3, #0x2 ; req_len = 2 bytes (the UI-ready u8 + TLV hdr)
|
||||
fe5140 mov w5, #0x8 ; resp_len = 8
|
||||
fe5148 movk w6,#0x1,lsl#16 / 0x2110 ; sync timeout
|
||||
fe514c bl qmi_client_send_msg_sync_with_shm
|
||||
```
|
||||
DMS message **0x0064 is a Qualcomm OEM extension** (string
|
||||
`com.qualcomm.qti.qcril.nas.telephony_ui_ready`) and is **not defined in libqmi** (checked
|
||||
`references/libqmi/data/qmi-service-dms.json` — 0x0064 absent; only 0x002E Set Operating Mode
|
||||
etc.). There is **no qmicli flag** for it and **no `Qmi.MessageDms*` GI binding**. Sending it
|
||||
requires a hand-built raw QMI PDU via `qmi_device_command_full`. See gap note in §4.
|
||||
|
||||
### Step C — DMS Set Operating Mode (the step pmOS already does)
|
||||
`qcril_qmi_nas_set_operating_mode_state` @ **0xfe53c8**:
|
||||
```
|
||||
fe547c mov w0, #0x4 ; DMS client
|
||||
fe5484 mov w1, #0x2e ; QMI DMS id 0x2E = Set Operating Mode (libqmi dms.json:498)
|
||||
fe548c req @ x29-0x18, len 0x10
|
||||
fe549c movk ... 0x12110 timeout
|
||||
fe54a4 bl qmi_client_send_msg_sync_with_shm
|
||||
```
|
||||
The op-mode enum value comes in as the function argument (`dms_operating_mode_enum_v01`);
|
||||
`online` = 0. → This is exactly `qmicli --dms-set-operating-mode=online`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Surrounding init context (incidental vs load-bearing)
|
||||
|
||||
`qcril_qmi_nas_init` @ 0xfc7058 also runs, at NAS bring-up (de-mangled call trace):
|
||||
- `qcril_qmi_nas_configure_sig_info2` — signal-strength delta config (incidental to RF arm)
|
||||
- `qcril_qmi_util_enable_networking_indications(int,int,int)` — NAS indication-register
|
||||
(sys-info / serving-system). NAS Indication Register = QMI NAS **0x0003**
|
||||
(`qmi-service-nas.json:395 "Register Indications"`). Replayable, but **incidental** to RF
|
||||
arming — it only controls which *unsolicited* indications the modem pushes back; it does not
|
||||
gate op-mode. Useful so we can *observe* the modem reacting.
|
||||
- `qcril_qmi_nas_register_for_5g_modem_vote_indication`, sig-config — incidental.
|
||||
|
||||
UIM path (separate module, runs from card-status-update, not the op-mode state machine):
|
||||
- `qcril_qmi_uim_power_up` @ 0x11dc650 (MMGSDI card power-up) and
|
||||
`qcril_qmi_uim_change_provisioning_session` @ 0x11e07e0. On Android these establish the
|
||||
USIM session before NAS asks for RF. On pmOS the SIM already reads `ready` and primary-GW
|
||||
is provisioned (per live results), so card power-up is **already satisfied** — not the gap.
|
||||
`uim-change-provisioning-session` *timing out* on pmOS is a **symptom** of the same modem
|
||||
not-ready state, not an independent cause (it answers reads, refuses state changes).
|
||||
|
||||
`qmi_ril_nwreg_post_oprt_online_action_handler` @ 0x104ddd8 runs **after** online succeeds
|
||||
(enables more indications) — not part of the pre-online arm.
|
||||
|
||||
### Load-bearing-for-RF-init, in order (the replay target)
|
||||
1. NAS Set System Selection Preference (mode/RAT pref) — **0x0033**, qmicli-able
|
||||
2. DMS ATEL UI-ready push — **DMS 0x0064**, OEM, *not* qmicli-able
|
||||
3. DMS Set Operating Mode = online — **0x002E**, qmicli-able
|
||||
|
||||
### Incidental (safe to include for observability, not expected to gate)
|
||||
- NAS Register Indications (0x0003), sig-info config
|
||||
- UIM card power-up / provisioning (already satisfied on pmOS)
|
||||
|
||||
---
|
||||
|
||||
## 4. Precise gap (what the binary requires that stock pmOS tooling can't send)
|
||||
|
||||
**DMS message 0x0064 ("telephony UI ready") is the one pre-online step QCRIL performs that
|
||||
neither qmicli nor libqmi-GI exposes.** It is a 2-byte request on the DMS service. We can:
|
||||
(a) replay steps 1 + 3 with qmicli now (cheap, may be sufficient if the mode-pref is the real
|
||||
missing precondition), and
|
||||
(b) if 1+3 still give 52, send 0x0064 as a raw QMI PDU via `Qmi.Device.command_full` (the TLV
|
||||
layout is a single mandatory u8 = 1; we have the message id and req_len=2 from the binary,
|
||||
which is `[type=0x01][len=0x01 0x00][val=0x01]` = 4 bytes payload after the QMI msg header
|
||||
— note binary's "req_len 2" counts the c-struct, libqmi will frame the TLV).
|
||||
|
||||
The provisioner (`tools/modem-online-provisioner/`) implements (a) as the primary path and
|
||||
documents (b) as the staged escalation, so we spend device boots in the right order.
|
||||
|
||||
**UPDATE 2026-07-01 — (b) is now built AND run on hardware. Result: real response, still 52.**
|
||||
`device-google-blueline/dms-ui-ready.py` sends the raw 0x0064 PDU using libqmi's own untyped
|
||||
raw-message primitives (`qmi_message_new` + `qmi_message_tlv_write_init/guint8/complete` +
|
||||
`qmi_device_command_full`). **First attempt (Python/GI) failed immediately**: `Qmi.Message` is
|
||||
NOT exposed via GObject-Introspection in this build — confirmed live (`AttributeError:
|
||||
'gi.repository.Qmi' object has no attribute 'Message'`) and by enumerating the actual typelib
|
||||
(only per-message typed classes exist, e.g. `MessageDmsGetOperatingModeOutput`; no generic
|
||||
`Message`/raw-`Command` type). The doc-comment annotations on `qmi_message_new`/
|
||||
`qmi_message_tlv_write_*` in the C headers do NOT mean GI exposes them — they don't here.
|
||||
|
||||
**Second attempt (C, direct against libqmi's real C API) worked and got a genuine protocol
|
||||
response.** Cross-compiled a small standalone binary (`pmbootstrap chroot -b aarch64`,
|
||||
`libqmi-dev` 1.39.0 — the openimsd fork, matching the phone's installed version) linking
|
||||
directly against `libqmi-glib.so.5` already on the phone; pushed and ran it live:
|
||||
|
||||
```
|
||||
dms-ui-ready: sending DMS 0x0064 (telephony UI ready=1)
|
||||
dms-ui-ready: response: is_response=1 message_id=0x0064 length=20
|
||||
dms-ui-ready: RESULT: FAILURE (result=1 error=58)
|
||||
```
|
||||
|
||||
The modem answered `0x0064` as a real, understood message (not "unsupported message" / not a
|
||||
transport reject) — this alone is new information: the OEM message ID and TLV framing are
|
||||
correct. It returned a structured FAILURE with error code 58. Standard libqmi enum 58 is
|
||||
`QMI_PROTOCOL_ERROR_WMS_ENCODING` (an SMS-service error) which cannot be its real meaning here —
|
||||
this is a Qualcomm OEM message, so its error-code namespace is very likely private/vendor-defined
|
||||
and just numerically reuses slot 58; not confidently decodable from public libqmi headers.
|
||||
Retried `mmcli --enable` immediately after (on both the pre-existing and, after ModemManager's
|
||||
own transport reprobe, a freshly re-created modem object): **`DeviceNotReady` (52), byte-identical
|
||||
to every prior attempt all night, with or without this message.** Sending it also correlated with
|
||||
a transient QMI transport hiccup (ModemManager's own connection self-recovered via reprobe within
|
||||
~1s; no lasting harm, but note for next time: release the DMS client / close the device cleanly
|
||||
before exit, which this quick test skipped).
|
||||
|
||||
**Verdict:** DMS 0x0064 is real, reachable, and rejects with a specific (undecoded) error — but
|
||||
does not clear the 52 wall by itself. Either the TLV value/precondition isn't exactly right, or
|
||||
this step genuinely isn't sufficient alone and step 1 (NAS mode-preference, `qmicli
|
||||
--nas-set-system-selection-preference`, previously observed to fail with `DeviceUnsupported`
|
||||
pre-RF) needs to succeed first, or the real gate is elsewhere entirely (EFS NV item, bootloader/
|
||||
ABL verified-boot state — see `los-mss-translated.c`'s remaining two candidates). Kept wired
|
||||
in the boot chain (best-effort, harmless) since it's now proven to do something real, just not
|
||||
(yet, alone) the fix.
|
||||
|
||||
**UPDATE 2026-07-02 — full ordered trilogy executed on hardware. Candidate #2 CLOSED.**
|
||||
Run conditions: ModemManager stopped, fresh mss restart, first commands into the new
|
||||
firmware instance, QCRIL's exact order. Results:
|
||||
1. NAS 0x0033 (mode-pref) → DeviceUnsupported(25), identical to every out-of-order
|
||||
attempt. Order does not rescue it. But `--nas-get-system-selection-preference`
|
||||
succeeds and shows the stored pref is ALREADY complete (all RATs, full band lists,
|
||||
persisted from Android) — step 1's content is pre-satisfied; the push is refused
|
||||
AND redundant.
|
||||
2. DMS 0x0064 (UI-ready) → delivered, NoEffect(26), as always.
|
||||
3. DMS 0x002E online → DeviceNotReady(52), byte-identical.
|
||||
Same night context: rfs file serving fully fixed (ota_firewall/ruleset + modem_efs;
|
||||
XPU-violation boot crashes eliminated), zero TQFTP rejects, clean crash-free modem
|
||||
boot. The pre-online conditioning theory is now closed in ALL variants: the modem is
|
||||
conditioned, fed, and healthy, answers every read, refuses every state change.
|
||||
Remaining candidates: EFS NV item (#3), ABL/bootloader-passed state (#5), with the
|
||||
TZ/SFS secure-storage identity path (test IMEI 990012001581294) as the concrete
|
||||
mechanism consistent with both. Next experiment queued: QSEECOM listeners started
|
||||
pre-modem at cold boot (registration proven; receive/resume relay still unexercised).
|
||||
|
||||
**UPDATE 2026-07-03 — DMS 0x0064 NOT_READY-then-READY handshake tested live. FALSIFIED.**
|
||||
Real vendor QCRIL source (leaked SDM670/710 BSP,
|
||||
`qcril-hal/modules/nas/src/qcril_qmi_nas.cpp`, boot-triggered-ONLINE branch of the
|
||||
unsolicited operating-mode indication handler) shows the real sequence sends
|
||||
`ui_status=FALSE` (NOT_READY) first, then later `TRUE` (READY) — our prior test
|
||||
(2026-07-01) only ever sent READY standalone. Built a corrected C tool
|
||||
(`tools/modem-online-provisioner/dms-ui-ready-sequence.c`, cross-compiled via
|
||||
`pmbootstrap chroot -b aarch64` against a sysroot pulled live from the phone's own
|
||||
`/usr/include/libqmi-glib` + `.so`, since neither `glib-dev` nor `libqmi-glib-dev`
|
||||
are in the buildroot's own repo index) and ran the full corrected sequence live on
|
||||
pmOS: baseline `offline` confirmed, `0x0064(0)` sent → `result=1 error=58`,
|
||||
`0x0064(1)` sent 1s later → `result=1 error=58` (identical failure, order didn't
|
||||
matter), final `dms-get-operating-mode` read → still `offline`. Both values fail
|
||||
identically — this rules out "wrong value" or "wrong order" as the cause of error
|
||||
58. Error 58 is very likely a hard precondition-not-met rejection independent of
|
||||
what value is sent, not a sequencing bug. Do not re-attempt this specific
|
||||
NOT_READY/READY ordering fix without new information; it's closed. Tool kept in
|
||||
`tools/modem-online-provisioner/` for reference (working example of raw libqmi
|
||||
C message construction against DMS OEM messages, useful if a future OEM message
|
||||
needs the same treatment).
|
||||
262
saf/device/evidence/qrtr_tools_reference.md
Normal file
262
saf/device/evidence/qrtr_tools_reference.md
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# PAF · QRTR tools reference — what we have, what each does, what's untested
|
||||
|
||||
Written 2026-07-01 because we'd been using `qrtr-lookup` all session without ever checking
|
||||
whether it was the only tool available, or what its full capability actually is. It wasn't —
|
||||
`qrtr-cfg` exists too and had never been touched. This file exists so that doesn't happen again.
|
||||
|
||||
## Package: `qrtr-1.2-r1` (Alpine/pmOS)
|
||||
|
||||
Ships exactly two binaries. Confirmed via `apk info -L qrtr`:
|
||||
|
||||
```
|
||||
usr/bin/qrtr-cfg
|
||||
usr/bin/qrtr-lookup
|
||||
```
|
||||
|
||||
Both come from the upstream `qrtr-utils` project (Qualcomm's own AF_QIPCRTR userspace
|
||||
tools). There is no `qrtr-ns` (nameserver) package installed separately — check whether one
|
||||
is needed; if the kernel-side QRTR nameserver is absent, service registration/lookup could
|
||||
be running in a degraded mode we haven't checked for.
|
||||
|
||||
## `qrtr-lookup` — service registry dump
|
||||
|
||||
**Usage:** `qrtr-lookup [<service> [<instance> [<filter>]]]`
|
||||
|
||||
Run with no arguments, dumps the full QRTR service registry: every registered
|
||||
`{service, version, instance, node, port}` tuple currently known to the local nameserver,
|
||||
with a decoded service name where the local database has one (`<unknown>` otherwise).
|
||||
|
||||
**Untested this session:** the `<service>` / `<instance>` / `<filter>` positional
|
||||
arguments. We only ever ran it bare. Querying a **specific** service number directly
|
||||
(e.g. `qrtr-lookup 24` for PDC) may return additional detail the bare dump doesn't show,
|
||||
or may support a watch/blocking mode for a single service — not yet checked. Worth trying
|
||||
before assuming the bare dump is the whole picture.
|
||||
|
||||
### Full service table observed this session (node 0 = modem edge, node 5/10 = other DSPs)
|
||||
|
||||
| Service ID | Name (as decoded by qrtr-lookup) | Notes |
|
||||
|---|---|---|
|
||||
| 66 | Service registry notification service | meta |
|
||||
| 43 | Subsystem control service | SSR |
|
||||
| 15 | Test service | — |
|
||||
| 51 | CoreSight remote tracing service | debug/trace, multiple instances across DSPs |
|
||||
| 21 | Modem embedded file system service | EFS — **never directly queried this session** |
|
||||
| 24 | Thermal mitigation device service | — |
|
||||
| 23 | Thermal sensors service | — |
|
||||
| 22 | Time service | used by `blueline-timed.sh`/`modem-time-set.py` (base=2/ATS_USER only) |
|
||||
| 36 | **Persistent device configuration service (PDC)** | the config/carrier-selection service — today's main thread |
|
||||
| 49 | IPA control service | data-plane |
|
||||
| 17 | Specific absorption rate service (SAR) | — |
|
||||
| 34 | Coexistence service | — |
|
||||
| 231 | Vendor-specific service | **never queried — unknown OEM extension, worth probing** |
|
||||
| 55 | QMI-SLIM service | — |
|
||||
| 9 | Voice service | — |
|
||||
| 3 | Network Access Service (NAS) | RAT/mode-pref, serving-system |
|
||||
| 12 | Phonebook Management service | — |
|
||||
| 10 | Card Application Toolkit service (v2) | UIM/SIM toolkit |
|
||||
| 47 | Data Port Mapper service | — |
|
||||
| 11 | User Identity Module service (UIM) | SIM reads — this is what confirms ICCID/IMSI, no RF needed |
|
||||
| 1 | Wireless Data Service (WDS) | — |
|
||||
| 4 | Quality Of Service service | — |
|
||||
| 26 | Wireless data administrative service | — |
|
||||
| 7 | Authentication service | — |
|
||||
| 8 | AT service | **never queried — a raw AT-command channel may exist here, unexplored** |
|
||||
| 29 | Circuit switched videotelephony service | — |
|
||||
| 71 | UIMHTTP service | — |
|
||||
| 5 | Wireless Messaging Service (SMS) | — |
|
||||
| 42 | Data system determination service | — |
|
||||
| 2 | Device Management Service (DMS) | operating-mode, capabilities, IDs |
|
||||
| 48 | QMI DFS service | — |
|
||||
| 16 | Location service (~PDS v2) | GNSS |
|
||||
| 69 | ATH10k WLAN firmware service | WiFi, separate from modem entirely |
|
||||
| 77 | IMS QMI Priv service | — |
|
||||
| 57 | WLPS service | — |
|
||||
| 14 | Remote file system service | rmtfs — this is what actually serves modemst1/2 |
|
||||
| 64 | Service registry locator service | meta |
|
||||
| 770 | IMS data service | — |
|
||||
| 4096 | TFTP | tqftpserv |
|
||||
| 769 | SLIMbus control service | audio |
|
||||
| 4098/4099 | `<unknown>` | **never decoded — no name in qrtr-lookup's local DB, worth checking libqmi's `qmi-service-*.json` files for these numeric IDs directly instead of relying on qrtr-lookup's built-in names** |
|
||||
|
||||
**Services we have NEVER directly queried this whole session, despite them being live and
|
||||
registered:** Modem EFS service (21), Vendor-specific service (231), AT service (8),
|
||||
CoreSight remote tracing (51). Any of these could carry signal about the RF-init decision
|
||||
we haven't looked at. AT service (8) in particular is notable — a raw AT-command interface
|
||||
bypassing QMI entirely is a completely different instrument than anything tried today.
|
||||
|
||||
## `qrtr-cfg` — node ID configuration
|
||||
|
||||
**Usage:** `qrtr-cfg <node-id>`
|
||||
|
||||
Attempts to set this machine's own local QRTR node ID via the `SIOCSIFADDR` ioctl on an
|
||||
`AF_QIPCRTR` socket. **Confirmed non-functional on this kernel build**, tested live
|
||||
2026-07-01: `qrtr-cfg 0` → `ERROR qrtr-cfg: failed to configure node id: Not supported`.
|
||||
|
||||
Root cause, confirmed by reading the kernel driver directly
|
||||
(`references/linux-7.1.1-sdm845-CANONICAL/net/qrtr/af_qrtr.c`, ioctl handler ~line 1140+):
|
||||
only `SIOCGIFADDR` (get local address) is implemented. `SIOCSIFADDR` (set) is not handled —
|
||||
falls through to the unsupported/default case. The local node ID
|
||||
(`qrtr_local_nid`, default `1`, `af_qrtr.c:102`) is fixed at this value on this kernel;
|
||||
mainline's `qrtr` core doesn't currently expose a way to change it at runtime on this
|
||||
build. Not a bug we introduced — this is upstream kernel behavior on this SDM845 mainline
|
||||
tree. `qrtr-cfg` is effectively dead weight on this specific system; don't spend more time
|
||||
on it unless the kernel driver changes.
|
||||
|
||||
## Update 2026-07-01 evening — services 8/21/231 confirmed LIVE, real RE progress
|
||||
|
||||
Built `tools/probe-qmi-service.py`-equivalent (raw AF_QIPCRTR socket, `NEW_LOOKUP`, then a
|
||||
raw QMI message send — same mechanism as `modem-time-set.py`, no risky glink channel
|
||||
creation needed since these are QMI services multiplexed over the existing IPCRTR
|
||||
transport, not separate glink channels). Result: **all three respond**, not silent:
|
||||
|
||||
| Service | qrtr node/port | Result |
|
||||
|---|---|---|
|
||||
| AT service (8) | 0 / 58 | responded (rejected a CTL-service message sent to the wrong dispatch — see below, not a dead port) |
|
||||
| Vendor-specific (231) | 0 / 43 | responded (same rejection pattern) |
|
||||
| Modem EFS service (21) | 0 / 24 | responded (same rejection pattern) |
|
||||
|
||||
**Why the response was a rejection, and why that's not a dead end:** the probe sent a
|
||||
`CTL` service message (`Get Client ID`, msg `0x0022`) directly to each service's own port.
|
||||
That's the wrong message for their dispatch tables — `modem-time-set.py` never did a CID
|
||||
handshake either, it sends the real functional message straight to the looked-up port.
|
||||
Next probe needs each service's actual message ID, not a generic CTL probe.
|
||||
|
||||
**AT service (`QMI_SERVICE_AT` = `0x08`) — real progress via modem blob strings.**
|
||||
`libqmi`'s own enum documents this service by name (`qmi-enums.h:50`) but implements zero
|
||||
messages for it (no `qmi-service-at.json`, no `qmicli --at-*` flags) — same gap class as
|
||||
Time (`TS`) before it got RE'd this session. Found real function names in `modem.mbn`'s
|
||||
strings (`strings -n 6 modem.mbn`, file offset ~0x172b653+):
|
||||
|
||||
```
|
||||
ds_qmi_at.c
|
||||
qmi_ati_reset / qmi_ati_reset_client
|
||||
qmi_ati_reg_at_cmd_fwd / qmi_ati_reg_at_cmd_fwd_ex
|
||||
qmi_ati_fwd_at_cmd_cback / qmi_ati_fwd_at_cmd_ind
|
||||
qmi_ati_send_at_repsonse (sic, real modem-firmware typo) / qmi_ati_send_at_urc
|
||||
qmi_ati_process_cmd / qmi_ati_process_cmd_hdlr
|
||||
qmi_ati_process_alloc_clid / qmi_ati_process_dealloc_clid
|
||||
qmi_ati_initialize_client
|
||||
qmi_ati_process_at_client_reg_status / qmi_ati_process_at_cmd_reg_status
|
||||
qmi_ati_send_response
|
||||
qmi_ati_alloc_clid_cb / qmi_ati_dealloc_clid_cb / qmi_ati_cmd_hdlr_cb
|
||||
```
|
||||
|
||||
Confirms `ATI` = "AT Interface", a real, structured, table-dispatched QMI service — not
|
||||
guesswork. Tried to find the exact numeric message IDs via:
|
||||
- **Byte-scan for the string's own virtual address as a literal pointer** (file offset
|
||||
0x172b653 → vaddr 0xc177b653, computed from the ELF program headers): zero hits. Hexagon
|
||||
code doesn't appear to store bare absolute pointers to this table; PC-relative
|
||||
addressing likely, not greppable without real disassembly.
|
||||
- **`llvm-objdump -d` DOES support Hexagon** (`llc --version` lists `hexagon - Hexagon`;
|
||||
confirmed disassembles `mba.mbn` correctly) — but `modem.mbn`/`mba.mbn` are fully
|
||||
stripped (`readelf -S`: "There are no sections in this file", no symtab). Raw
|
||||
disassembly without function boundaries or xrefs isn't tractable by hand at 67MB scale
|
||||
without a real Hexagon-aware decompiler (Ghidra + Hexagon processor module, or IDA) —
|
||||
don't have one in this environment.
|
||||
- **Public docs**: none found. `QMI_SERVICE_AT`'s real message IDs aren't published
|
||||
anywhere searchable. Checked `quic/qmi-framework` (Qualcomm's own open-source repo) —
|
||||
generic transport only, no service-specific IDL.
|
||||
- **`atfwd_daemon`** (the usual Android userspace AT-forwarding daemon that would exercise
|
||||
this service): **not present anywhere in `android-reference`'s extracted vendor stack**.
|
||||
Pixel 3 doesn't ship it — dead end specific to this device, not a search failure.
|
||||
- **Closest real reference found**: `QMI_SERVICE_ATR` (`0x?`, "Telit AT Relay Service",
|
||||
`qmi-service-atr.json`) IS fully implemented in libqmi (different vendor extension, but
|
||||
same architectural family). Protocol: `Send` (msg `0x0000`, TLV `0x01` = AT command
|
||||
string, size-prefixed, max 1024B) → Operation Result; `Received` (indication `0x0001`,
|
||||
same string TLV) delivers the response async. This is the best structural template for
|
||||
what the real `AT` service's wire format probably looks like, even though it's a
|
||||
different service ID — worth trying as a first guess against service 8 directly
|
||||
(`msg 0x0000`, single string TLV `0x01`) next time the phone's available, though
|
||||
`qmi_ati_reg_at_cmd_fwd` existing as a SEPARATE step from send/receive suggests the real
|
||||
`AT` service may require an explicit registration message first, unlike ATR's simpler
|
||||
always-on relay.
|
||||
|
||||
**Modem EFS service (21) and Vendor-specific (231)**: confirmed alive, zero further
|
||||
research done — same "find the real message ID" problem, not yet attempted.
|
||||
|
||||
## What we should try next with these tools, specifically
|
||||
|
||||
1. Against AT service (8): try the ATR-shaped guess first (`msg 0x0000`, TLV `0x01` =
|
||||
AT command string like `"AT\r\n"`) directly at node 0 port 58. Cheap, safe (worst case
|
||||
a clean rejection like the CTL probe, not a crash — this isn't a glink channel-open,
|
||||
it's an ordinary QMI message on the existing IPCRTR transport).
|
||||
2. If that's rejected, try `qmi_ati_reg_at_cmd_fwd`-shaped registration first (a
|
||||
`Register`-style message, unknown ID — try 0x0001-0x0005 range as a first sweep) before
|
||||
the send.
|
||||
3. **Speculative but cheap dispatch-table-order guess**: the 5 strings carrying the
|
||||
`(qmi_svc_hdlr_ftype)` cast prefix in `modem.mbn` appear in this exact sequence:
|
||||
`qmi_ati_reset`, `qmi_ati_reg_at_cmd_fwd`, `qmi_ati_send_at_repsonse`,
|
||||
`qmi_ati_send_at_urc`, `qmi_ati_reg_at_cmd_fwd_ex`. If the string table mirrors the
|
||||
dispatch table's own ordering (common but not guaranteed), these would be 5
|
||||
consecutive message IDs. Worth a sweep starting at a plausible custom-message base
|
||||
(`0x0020`): try `0x0020`=Reset, `0x0021`=RegAtCmdFwd, `0x0022`=SendAtResponse,
|
||||
`0x0023`=SendAtUrc, `0x0024`=RegAtCmdFwdEx against service 8, watching for anything
|
||||
other than a uniform rejection.
|
||||
4. `qrtr-lookup 21`/`231` — still completely unexplored beyond confirming they're alive.
|
||||
No `qmi_mfs_`/`qmi_efs_`-style strings found anywhere in `modem.mbn` (searched both
|
||||
narrow and broad patterns) — either stripped entirely, or MFS's handler code lives in
|
||||
a different firmware image than the main MPSS blob. Vendor-specific (231) has zero
|
||||
name leads at all; would need blind message-ID sweeping, no better starting point.
|
||||
5. Try `qrtr-lookup <n>` with explicit service numbers to see if per-service querying
|
||||
surfaces anything the bare dump suppresses (untested all session).
|
||||
|
||||
## Update 2026-07-02 — services 8 / 21 / 231 all mapped live. Frontier CLOSED (negative).
|
||||
|
||||
Ran `tools/at-service-probe.py` and the new `tools/qmi-service-map.py` (empty-payload
|
||||
dispatch-table sweep, reads the QMI result-TLV error pattern: 71/94 = no handler, 1/3/17/etc
|
||||
= real handler that rejected the args). All three "never-queried, worth-probing" services are
|
||||
now characterized on the live modem. None exposes a lever on RF-init or narrates the 52 wall.
|
||||
|
||||
- **AT service (8), node 0 port 58 — AT-command FORWARDING, wrong direction. DEAD END.**
|
||||
Dispatch table: `0x0000`=RESET (returns OK with or without a TLV — the "accepted an AT
|
||||
string" earlier was a false positive; empty payload also returns OK). `0x0020`/`0x0022`/
|
||||
`0x0024` = real handlers (`qmi_ati_reg_at_cmd_fwd` / `send_at_repsonse` / `reg_at_cmd_fwd_ex`
|
||||
— INTERNAL(3) on empty, MALFORMED(1) on a wrong TLV). `0x0021`/`0x0023` = indications
|
||||
(modem→AP). This is `ds_qmi_at.c`: apps *register* to receive AT commands the modem
|
||||
forwards to them (Android's `atfwd_daemon` role). There is no AT-*execution* path here to
|
||||
command the modem's own RF. Four `0x0020` registration-TLV shapes all returned MALFORMED;
|
||||
nailing the shape only registers us as a forwarding target — it can't move 52. Not pursued
|
||||
further.
|
||||
|
||||
- **Modem EFS service (21), node 0 port 24 — file-ops gated, low-yield. NOT pursued.**
|
||||
Most of `0x0000`–`0x0030` return error 57 (a service-state/"not open" gate). Real handler
|
||||
cluster at `0x001e` (OK on empty — a no-arg open/action), `0x001f`/`0x0020`/`0x0021`
|
||||
(MISSING_ARG — the read/write/list file ops). Cracking the file-path TLV shape is possible
|
||||
but pointless for 52: per `diag_capture.md`, EFS content is byte-identical to Android
|
||||
(shared unslotted partition), so any static NV/file read reads the same bytes both OSes.
|
||||
Empty-payload probes only were sent to 21 (an empty request can't carry a write).
|
||||
|
||||
- **Vendor-specific service (231), node 0 port 43 — modem telemetry / event-log. DEAD END.**
|
||||
This is a stats/bugreport service, self-describing: msg `0x000f` = "Report all modem stats",
|
||||
returns result OK with TLV `0x16` = **16 bytes, all zero** (the rest of its fixed 4136-byte
|
||||
frame is unused). Msg `0x0012` is an event-log *indication* carrying timestamped lines
|
||||
(`"<ts> Bugreport triggered"` — an echo of our own trigger). Fired across a live
|
||||
`mmcli --enable` (the 52 moment) and re-fired immediately after a failed enable: the stat
|
||||
block stays all-zero, no RF-init narration, no error counter moves. It is NOT a DIAG/F3
|
||||
substitute — the counters are empty and the bugreport content is not delivered through this
|
||||
channel (fetched/stored elsewhere; EFS-backed at best, which loops back to 21's null diff).
|
||||
Many `0x0000`–`0x0017` handlers want args (MISSING_ARG) but blind TLV-shape guessing here is
|
||||
the same low-yield trap as AT(8)'s registration TLV; not worth it without a name/IDL lead.
|
||||
|
||||
**Net:** the "probe the other live services" frontier is exhausted with a negative result. No
|
||||
standard, AT, EFS, or vendor-telemetry QMI service on this modem exposes the RF-init lever or
|
||||
its reason. Consistent with `modem-online-provisioner/RESULTS.md`: the gate is the modem
|
||||
firmware's own RF-init decision, and the only instrument that could narrate it (DIAG F3) is
|
||||
walled by production fusing ("MBA booted without debug policy") on both OSes — see
|
||||
`HANDOFF.md`'s DIAG-over-glink note.
|
||||
|
||||
## `tools/pdc-pending-test.py` — ready to run, not yet executed live
|
||||
|
||||
Built and syntax-checked (not yet run — needs phone access) to correctly test whether the
|
||||
`Pending` PDC config status (found live 2026-07-01, see `HANDOFF.md`) ever resolves to
|
||||
`Active` on its own, or needs a forced modem restart. Fixes two real mistakes from earlier
|
||||
manual testing, both confirmed by reading the actual working scripts rather than guessing:
|
||||
- `--pdc-monitor-refresh` watches the **wrong indication** (`Refresh`, `0x2F`) — the one
|
||||
that actually reports Active/Pending transitions is **`Config Change`** (`0x21`),
|
||||
delivered only to a client that called `Register` with reporting+refresh enabled
|
||||
*before* touching config state. No test this session had done that.
|
||||
- Device must be opened via `Qrtr.Bus → peek_node → Qmi.Device.new_from_node` with
|
||||
`Qmi.DeviceOpenFlags.EXPECT_INDICATIONS` (verified against `qcril-autoselect.py`'s
|
||||
`open_device()`) — a plain `Gio.File` open (my first draft) would have silently never
|
||||
delivered any indication at all, making a "no signal" result meaningless. Caught before
|
||||
running, not after.
|
||||
259
saf/device/evidence/secure_load_tz_frontier.md
Normal file
259
saf/device/evidence/secure_load_tz_frontier.md
Normal file
|
|
@ -0,0 +1,259 @@
|
|||
# PAF · Secure-load / TrustZone frontier
|
||||
|
||||
> **🔴 SUPERSEDED 2026-06-29.** Secure-load is **RESOLVED** for the modem — the MSS loads clean
|
||||
> (MBA boots, MPSS loads, `remoteproc running`, **zero −22 on the modem path**) via DELTA-1
|
||||
> (`c2923cb49` MPSS assign-ordering). This doc is kept as the **historical −22 / fastrpc_mem
|
||||
> investigation record**. The −22 that still floods the logs is the **SLPI's** SDSP heap, not the
|
||||
> modem's — see `slpi.md`. The current frontier is the firmware-internal OFFLINE/52 park, not
|
||||
> secure-load. (Body below is the 2026-06-26 investigation as-written.)
|
||||
|
||||
## 2026-06-26 (OPUS session) — mem-protect SKIP broke the boot on hardware; reverted. LOS does mem-protect differently, not skip it.
|
||||
|
||||
**Platform is now clean and reproducible (this part is solid, keep it):**
|
||||
- ONE canonical branch **`blueline`** (was split across `mobian-sdm845-7.1` + `blueline-slpi-build`,
|
||||
neither complete — consolidated). It = Mobian `mobian-sdm845-7.1` base (mainline 7.1.1 tag +
|
||||
full 179-patch sdm845 device series: camera/imx sensors, q6voice, haptics, fuel-gauge, panels)
|
||||
+ our 6 fixes. Other branches renamed `reference/mobian-7.1.1-base`, `archive/superseded-*`.
|
||||
- Guard against drift: **`tools/verify-fixes.sh`** (13 checks, asserts SOURCE CONTENT not commit
|
||||
messages; covers our 6 fixes AND the inherited Mobian migrations). Run before every build.
|
||||
- Build: `pmbootstrap build --src=references/linux-7.1.1-sdm845-CANONICAL linux-postmarketos-qcom-sdm845`
|
||||
(host `make` is NOT the path — build.md:7). Deploy: on-device `apk add --allow-untrusted` →
|
||||
`boot-deploy` auto-flashes `boot_a`. `uname -r = 7.1.1-sdm845`. Rollback image:
|
||||
`build-output/boot_a-rollback-20260626-1605.img.gz`.
|
||||
- **Found + fixed the silent killer behind every prior "flash and test":** the phone's kernel and
|
||||
its `/lib/modules` were ABI-mismatched — `qcom_q6v5_mss` gave `Exec format error`, the modem
|
||||
driver never loaded, so the modem was dead regardless of any fix. Matched build → driver loads,
|
||||
modem remoteproc `4080000.remoteproc` now registers.
|
||||
|
||||
**The mem-protect skip (`need_mem_protection = false` for sdm845) is REFUTED — likely roll back:**
|
||||
- It removed the `-22` from the modem path as designed — but the modem now fails EARLIER:
|
||||
`Booting .../mba.mbn → PBL returned unexpected status 0xEF110000 → can't start rproc: -22`.
|
||||
It never reaches "MBA booted".
|
||||
- The r2 baseline (below) had the modem boot MBA then XPU-crash-loop at 52. So the skip REGRESSED
|
||||
the boot. `qcom_scm_assign_mem` (`q6v5_xfer_mem_ownership`, qcom_q6v5_mss.c:463/489) transfers the
|
||||
MBA region HLOS→MSS_MSA so the modem PBL can read MBA; skipping starves PBL. **The assign is
|
||||
load-bearing. The real gate is WHY TrustZone returns -22 (EINVAL), not the call's existence.**
|
||||
|
||||
**The LOS claim in `HANDOFF_20260626_OPUS.md` was unverified inference — treat as RETRACTED:**
|
||||
- It blended mainline (`need_mem_protection`) with downstream (`subsys_vmid` / `qcom,mem-protect-id`)
|
||||
concepts into one story.
|
||||
- LOS Pixel 3 runs the **downstream msm-google 4.9 kernel**; its modem loader is `subsys-pil-msa`,
|
||||
NOT mainline `qcom_q6v5_mss.c`. We have NOT pulled that source (Tier 1 still open) — so what LOS
|
||||
does was never read off real code.
|
||||
- Hardware contradicts the inference: if LOS did no mem-setup and MBA self-unlocked XPU, the skip
|
||||
would have worked. It made things worse.
|
||||
|
||||
**PRINCIPLE (Casey, 2026-06-26): LineageOS is the ONLY known-working modem reference on this device.
|
||||
Other sdm845 devices (op6 / enchilada / beryllium / shift) are IRRELEVANT to the secure-load / XPU
|
||||
question — do not cite them as modem-bringup evidence. The downstream Pixel-3 stack is the authority.**
|
||||
|
||||
**Current phone state:** booted on `blueline` 7.1.1 (the skip build). Modem PBL-fails every boot;
|
||||
SLPI crash-loops ~10s (the `-22` spam every 10s is the SDSP/SLPI hyp-assign, NOT the modem).
|
||||
|
||||
**OPTIONS — left for Casey to choose (not picking one):**
|
||||
1. **Revert the skip** → restore the MBA-boots-then-XPU-52 baseline on the clean 7.1.1 platform,
|
||||
then work secure-load Tier 0/1 from a known state.
|
||||
2. **Pull `msm-google` downstream kernel** → diff `subsys-pil-msa` mem-setup + hyp-assign vs
|
||||
mainline. Answers "what does LOS actually do" with real code (Tier 1).
|
||||
3. **Downstream-kernel hybrid** (Droidian / Halium pattern) — this doc's honest realistic
|
||||
1–2 week path to working telephony.
|
||||
4. **Chase the TZ `-22` directly** — why does mainline's `qcom_scm_assign_mem` get EINVAL on
|
||||
blueline (VMID / mem-protect-id / TZ expectation)? Open-ended.
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-26 — TRIGGER RESOLVED: this doc is the plan. Hard secure-memory evidence captured.
|
||||
|
||||
The trigger condition below ("still 52, modem boots then refuses RF → the gate is the
|
||||
secure-load/TZ layer → this doc is the plan") is now MET, with a concrete error string instead of
|
||||
inference. On the rebuilt r2 LLVM kernel (slot A, live ssh), `dmesg` shows the modem **crash-looping
|
||||
on a hardware secure-memory protection fault**:
|
||||
|
||||
```
|
||||
qcom-q6v5-mss 4080000.remoteproc: fatal error: modem_xpu_int.c:456:[modem_sec_xpu_I]XPU violation
|
||||
(also crash #1: "SFR Init: wdog or kernel error suspected")
|
||||
```
|
||||
and EVERY modem + slpi restart logs the shared TZ rejection:
|
||||
```
|
||||
qcom_scm firmware:scm: Assign memory protection call failed -22 (qcom_scm_assign_mem → -EINVAL)
|
||||
SDSP fastrpc hyp-assign rejected by TZ (-22)
|
||||
```
|
||||
|
||||
**XPU = eXtreme Protection Unit**, the hw memory guard TZ programs. `modem_sec_xpu XPU violation` =
|
||||
the modem firmware touched a region the XPU forbids → its secure memory regions aren't assigned
|
||||
right for mainline → crash → q6v5 recovers → repeat → never RF-inits → **52**. The fastrpc/SLPI
|
||||
`assign -22` is the same mechanism (TZ refusing mainline's hyp-assign); fastrpc tolerates it, the
|
||||
modem can't. **The gate is concretely the secure-memory-assign / XPU region setup — pull the
|
||||
`assign_mem -22` thread.** (See modem.md TOP LEAD #11, slpi.md.)
|
||||
|
||||
CAVEAT (don't overclaim): this XPU crash-loop was captured on **7.1.1 r2**. The 6.16.7 / 7.1rc1
|
||||
captures recorded the modem "up + QMI registers" (lead #10) — QMI still answers here too, in the
|
||||
up-windows between crashes, so a single-sample `running` looked fine before. OPEN: re-check whether
|
||||
6.16.7 *also* XPU-crash-loops (i.e. always-present, just not noticed) or whether 7.1.1 worsened it —
|
||||
diff `journalctl -p err` across both kernels before assuming XPU is identical on the older tree.
|
||||
|
||||
Original status (pre-2026-06-26): **alternative, not primary.** The primary lever was Opus's
|
||||
reserved-memory geometry fix (`modem_dt_rf_analysis_20260622.md` candidate #1: revert the
|
||||
`sdm845-google-common.dtsi` override so MPSS = `0x7800000` / MBA = `0x96500000`, matching both
|
||||
working devices). That swing did not clear the 52; the secure-load/TZ layer (this doc) is now primary.
|
||||
|
||||
It is written assuming the reader has `modem.md`, `modem_dt_rf_analysis_20260622.md`,
|
||||
`tools/modem-online-provisioner/RESULTS.md`, and `android-capture-20260622/EFS_FINDINGS.md` in head.
|
||||
|
||||
## Trigger condition (the decision point)
|
||||
|
||||
Flash the reserved-memory DT fix to `boot_a`, cold-boot pmOS, one `--dms-set-operating-mode=online`:
|
||||
- **online works** → geometry was the gate. Stay mainline; push the fix upstream (sdm845-mainline /
|
||||
pmaports), bisect the `google-common` override. **This doc is not needed.**
|
||||
- **modem fails to load** (`remoteproc Boot failed`) → the firmware was signed for the old geometry;
|
||||
the override existed for a reason. Revert; geometry is load-bearing-correct as-is. Come here.
|
||||
- **still 52 (modem boots clean, refuses RF)** → the gate is **not** the DT geometry. It is in the
|
||||
secure-load / TrustZone / subsystem-coordination layer. **This doc is the plan.**
|
||||
|
||||
## The gate, precisely (today's hard proof — do not re-derive)
|
||||
|
||||
Source: `tools/modem-online-provisioner/RESULTS.md` (raw QMI over `AF_QIPCRTR`, 2026-06-22).
|
||||
|
||||
- Modem is in **op-mode 3 = OFFLINE** (deepest non-persistent off state). Not LPM, not transient.
|
||||
- **Accepts** every read, non-RF writes (DMS/NAS indication registration), and **DMS Set Operating
|
||||
Mode = RESET (0x002E mode 4) → OK**.
|
||||
- **Refuses every RF-requiring transition**: online → **52 DeviceNotReady**; low_power → **60
|
||||
InvalidTransition**; NAS Set System Selection Preference (0x0033) → **25 DeviceUnsupported**; DMS
|
||||
ATEL UI-ready (0x0064) → **58** (arg-rejected, message id confirmed correct).
|
||||
- After the accepted **RESET**, firmware re-initialises **straight back to OFFLINE** and refuses
|
||||
again, with **zero** sys-info/serving-system indications in an 8 s listen. The firmware is inert
|
||||
on RF by decision, not attempting and failing.
|
||||
|
||||
**Conclusion (raw-level, not inference):** no AP-side QMI message or ordering unlocks RF. The
|
||||
choreography-replay path (`qcril_online_sequence.md`) is closed: the modem rejects the pre-online
|
||||
writes *themselves* before we reach the online flip. QCRIL only succeeds on Android because the
|
||||
firmware has **already self-completed RF-init** there; on pmOS it never does.
|
||||
|
||||
## Why this points at secure-load / TZ (the RESET argument)
|
||||
|
||||
A QMI RESET re-initialises the modem firmware **without re-doing the cold-boot setup** — PIL image
|
||||
auth, the TZ/SCM state, the bootloader (ABL/XBL) modem init, and sibling-subsystem coordination are
|
||||
all established **once at cold boot** and persist through a QMI reset. The RESET reproducing OFFLINE
|
||||
tells us the gate lives in that **persistent cold-boot environment**, not in a one-shot race or a
|
||||
userspace omission. That environment is exactly four things, in suspect order:
|
||||
|
||||
1. **DT reserved-memory geometry** — Opus's test (may already settle it).
|
||||
2. **Sibling subsystem / Protection-Domain coordination** — the modem RF-init can gate on a PD
|
||||
registered over glink/pd-mapper by ADSP/CDSP/**SLPI**. pmOS DT has **no SLPI region at all**
|
||||
(absent in the `modem_dt_rf_analysis` table; Android + enchilada both carry `slpi@0x96700000`).
|
||||
Never confirmed up on pmOS. Mainline-fixable if so — *not* TZ.
|
||||
3. **PIL / SCM secondary auth** — image auth clearly succeeded (modem boots), but a non-fatal
|
||||
secondary secure step (a hash-table segment, a secure-regulator vote, a TUST handshake) may not
|
||||
have completed, leaving the firmware in a degraded RF-locked mode.
|
||||
4. **Bootloader / verified-boot state** — pmOS boots a hand-built `--header_version 0` image with a
|
||||
custom cmdline. If ABL sees an unverified/inconsistent chain, it can skip modem secure-env init
|
||||
that stock Android gets.
|
||||
|
||||
## Ranked investigation — cheapest discriminator first
|
||||
|
||||
### Tier 0 — free, same boot (run the instant reserved-memory fails, before any build)
|
||||
1. **Mine dmesg for the secure/PD layer.** Image auth succeeded, so look for *non-fatal* warnings:
|
||||
`dmesg | grep -iE 'pas|scm|auth|hash|mba|qcom_q6v5|remoteproc|glink|pdr|pil'`.
|
||||
A non-zero `qcom_scm_pas_*` return, a PDR restart-notification timeout, or a glink intent-failure
|
||||
is a citable pointer into #2/#3.
|
||||
2. **Confirm sibling DSPs are up and their PDs registered.**
|
||||
`for r in /sys/class/remoteproc/remoteproc*; do printf '%s: %s %s\n' "$r" "$(cat $r/name 2>/dev/null)" "$(cat $r/state 2>/dev/null)"; done`
|
||||
and `qrtr-lookup | grep -iE 'adsp|slpi|cdsp|sns|domain'`. If SLPI/ADSP is `offline`/absent and
|
||||
the modem waits on its PD, **that is the gate and it is mainline-fixable** (firmware path +
|
||||
remoteproc node + a `pd-mapper` entry), not TZ. This is the highest-value cheap check — the whole
|
||||
elimination tracked `remoteproc2` (modem) closely and never verified the siblings.
|
||||
3. **Timing/missed-response.** `strace -f -p $(pidof pd-mapper)` and rmtfs during a QMI RESET. If
|
||||
the modem's RF-init window closes before pd-mapper/rmtfs answer a request, you'll see the missed
|
||||
response. (Likely already excluded — services are confirmed up post-boot — but cheap to confirm.)
|
||||
4. **LDO13 rail state — the one rail NOT on the eliminated list.** Unlike the refuted #9 LDO set
|
||||
(ldo4/6/8/9/10/11/15/16/18/23/27, which were *absent* from pmOS), `ldo13` IS defined in
|
||||
`dt-work/blueline-pmos.dts:5466`, and on SDM845 LDO13 feeds RFFE (per OpenStick prior art — the
|
||||
`l13: voltage operation not allowed` line is benign on MSM8917 but not automatically so here).
|
||||
Static dumps show a state delta: pmOS `ldo13 … 0 0 0 …` vs Android `pm8998_l13 … 0 1 0 …`, and
|
||||
there is **no** `l13`/`voltage operation not allowed` line in the captured pmOS dmesg. **Live
|
||||
check at the moment of `online`:**
|
||||
`for r in /sys/class/regulator/regulator.*/; do n=$(cat $r/name 2>/dev/null); [ "$n" = ldo13 ] && echo "$n state=$(cat $r/state) @ $(cat $r/microvolts)uV"; done`
|
||||
— is it `on` @ 1.8 V while the modem is booted-but-refusing-RF? Then retry `online` and watch
|
||||
whether L13 flips. Likely falls to the same "modem votes RF via RPMh, not the AP framework"
|
||||
refutation as #9 — but it's the one rail never explicitly probed, so confirm live before closing.
|
||||
Plus `dmesg | grep -iE 'l13|voltage operation not allowed'` on the same boot.
|
||||
|
||||
### Tier 1 — diff the downstream kernel (the real working reference for secure-load)
|
||||
Enchilada proves the mainline *stack* works on SDM845; it does **not** prove the secure-load code
|
||||
paths work on blueline's exact modem. The authority for those is **Google's downstream Pixel-3
|
||||
kernel** (`msm-google`, 4.4-era — confirm the exact branch against `boot_a`'s kernel version on
|
||||
`android.googlesource.com/kernel/msm-google` before cloning). Pull it and diff, against mainline,
|
||||
exactly three things:
|
||||
- `drivers/remoteproc/qcom_q6v5_mss.c` — the PAS auth + mba mem-setup + reset sequence.
|
||||
- `drivers/soc/qcom/{scm,pdr_interface,smem}` — the secure-channel and PD-restart path.
|
||||
- modem `reserved-memory` + `qcom,smem-states` + `power-domains` in the DT.
|
||||
|
||||
The reserved-memory hit is the first output of this method; the SCM/auth/PDR delta is the next.
|
||||
This is the strongest "honest swing" if reserved-memory alone fails — methodical, sourced, no RE
|
||||
of opaque blobs. **Artifact we have but haven't used this way:** `android-reference/vendor-stack-20260621/`
|
||||
(vendor RIL + MBN tree). **Artifact we still need:** the downstream kernel source tree.
|
||||
|
||||
### Tier 2 — instrument the firmware's own RF-init (the only inside view)
|
||||
Every capture so far is userspace (QCRIL/radio logs). Nobody has captured the **MPSS firmware's own
|
||||
RF-init trace** — the ordered set of things it reads and waits for. On rooted Android (slot A),
|
||||
QCSuper continuous diag over **`/dev/diag`** (local — confirmed working; USB-diag is HAL-locked on
|
||||
this production Pixel, per `pmos_fix_plan.md`) captures the F3/log stream during a cold boot. An
|
||||
open F3 decoder (linux-msm / sdm845-mainline ecosystem; QCSuper has partial decode) turns it into
|
||||
the firmware's RF-init step list. Diff which of those steps can't complete on pmOS. This is the one
|
||||
instrument that can name the gate from inside the firmware; everything on pmOS only observes the
|
||||
refusal. **Read-only capture — do not write EFS.**
|
||||
|
||||
### Tier 3 — TZ / SCM RE (the hard pole, deprioritize)
|
||||
If Tiers 0–2 point at a specific SCM call or secure state: disassemble the QSEE trustlets or diff
|
||||
XBL/ABL's modem init. Long, uncertain, opaque. Keep as the escalation, not the plan.
|
||||
|
||||
## The strategic fork (be honest about the 1–2 week telephony goal)
|
||||
|
||||
Mainline-native secure-load RE (Tier 2–3) is realistically **not** a 1–2 week path to working
|
||||
telephony/SMS/data — it's open-ended research. If Tier 0 + reserved-memory don't clear 52, the
|
||||
fastest route to "telephony on Linux like I have on rooted Android" is to use the stack that already
|
||||
works on this silicon:
|
||||
|
||||
- **Downstream-kernel hybrid (Droidian / Halium / pmOS-downstream pattern):** boot the downstream
|
||||
`msm-google` kernel under a Linux userspace, where the modem firmware gets the exact cold-boot
|
||||
environment it expects so RF-init completes, then drive it with the vendor `qcrild` already pulled
|
||||
in `android-reference/`. Trades mainline purity for a known-good outcome. This is the realistic
|
||||
1–2 week path.
|
||||
|
||||
- **WRONG TURN — do not burn time on this:** "run vendor QCRIL on the *mainline* kernel via
|
||||
libhybris" will **not** fix it. QCRIL does not trigger RF-init (the firmware self-arms on Android;
|
||||
QCRIL only queries/confirms — proven by today's raw results). The gate is the cold-boot
|
||||
environment, so only changing that environment (DT → subsystems → TZ → bootloader) **or** booting
|
||||
the downstream kernel helps. Vendor-RIL-on-mainline addresses a different (already-solved) layer.
|
||||
|
||||
Net: **reserved-memory fix + Tier 0 (free) → if clear, stay mainline; if not, decide
|
||||
mainline-secure-load-research vs downstream-kernel-hybrid on evidence, not on momentum.**
|
||||
|
||||
## Artifacts
|
||||
|
||||
- **Have:** `android-reference/vendor-stack-20260621/` (libril-qc-hal-qmi.so + 174-file MBN tree +
|
||||
selection DB); `tools/modem-online-provisioner/{qrtr_raw.py,qrtr_seq.py}` (send any QMI PDU over
|
||||
AF_QIPCRTR — validated against DMS 0x002D); QCSuper efs-shell + diag over `/dev/diag` on rooted
|
||||
slot A; `slotb-backup-20260621/pmos-7.1-boot.img` recovery; 6.16.7 known-good kernel; EFS clones.
|
||||
- **Need (not yet pulled):** downstream `msm-google` kernel source (Tier 1 ground truth); an open
|
||||
Hexagon F3/diag decoder (Tier 2); possibly a devcoredump/minidump parser if SSR ever yields one
|
||||
(today it did not — modem doesn't crash, it deliberates).
|
||||
|
||||
## Guardrails (secure-load-specific)
|
||||
|
||||
- pmOS-side secure-load investigation is **read-only**: `dmesg`, `/sys/class/remoteproc/*`,
|
||||
`/sys/kernel/debug/{remoteproc,msm_subsys}/*`, `qrtr-lookup`. Safe.
|
||||
- diag/F3 capture is on **rooted slot A `/dev/diag`** — read-only capture. Never write EFS or NV
|
||||
from these probes (shared unslotted `modemst1/2` — a write is seen by both slots).
|
||||
- **Never flash/patch TZ, trustlets, XBL, or ABL** — brick territory. `references/edl-pixel` exists
|
||||
as last-resort recovery but **assume no EDL safety net**; behave as if a bad flash is permanent.
|
||||
- Only ever touch `boot_a` / pmOS. Never slot B, `system_a`, the shared EFS, or userdata.
|
||||
- Recovery image: `slotb-backup-20260621/pmos-7.1-boot.img`. Document every slot switch + flash.
|
||||
- One `online` attempt per boot (a failed attempt poisons the modem until a power cycle).
|
||||
- Standard hard constraints apply: pmOS is Alpine/ash (no `[[ ]]`/arrays); `rmtfs -P -s` (never
|
||||
`-r` alone); never add `pmic@4`/`pmic@5` SPMI nodes; sudo pw `147147`.
|
||||
|
||||
## On link-in
|
||||
Not yet referenced from `START-HERE.md` or `PAF/README.md` (left for the Opus handoff to wire, to
|
||||
avoid colliding with in-flight edits). If reserved-memory fails, point START-HERE's "actual
|
||||
frontier" at this doc and demote the reserved-memory lever to the trigger-condition note above.
|
||||
146
saf/device/evidence/tier1_los_vs_mainline_diff.md
Normal file
146
saf/device/evidence/tier1_los_vs_mainline_diff.md
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
# PAF Tier 1: LOS `pil-q6v5-mss` (4.9.337) vs Mainline `qcom_q6v5_mss` (7.1.1) — Complete Diff
|
||||
|
||||
**Date:** 2026-06-27
|
||||
**Source LOS:** `references/los-kernel-blueline-4.9/drivers/soc/qcom/{pil-msa.c, peripheral-loader.c, pil-q6v5-mss.c, secure_buffer.c}`
|
||||
**Source Mainline:** `Downloads/mobian-sdm845-7.1/drivers/remoteproc/qcom_q6v5_mss.c` + `drivers/firmware/qcom/qcom_scm.c`
|
||||
**Live Android capture:** 4.9.337 cold boot, `89JX0AAJP`, 2026-06-27
|
||||
|
||||
## Live Android ground truth (captured this session)
|
||||
|
||||
```
|
||||
pil-q6v5-mss 4080000.qcom,mss: modem: loading from 0x8e000000 to 0x97800000 (MPSS 152MB)
|
||||
pil-q6v5-mss 4080000.qcom,mss: Loading MBA and DP from 0x98500000 to 0x98600000 (MBA 1MB)
|
||||
pil-q6v5-mss 4080000.qcom,mss: MBA boot done
|
||||
pil-q6v5-mss 4080000.qcom,mss: modem: Brought out of reset
|
||||
```
|
||||
|
||||
Zero `hyp_assign` errors. Zero `xpu`. Zero `-22`. The modem boots clean on Android.
|
||||
|
||||
Running Android reserved-memory (from `/sys/firmware/devicetree/base/reserved-memory/`):
|
||||
|
||||
| Region | Address | Size |
|
||||
|--------|---------|------|
|
||||
| modem (MPSS) | `0x8e000000` | `0x9800000` (152MB) |
|
||||
| MBA | `0x98500000` | `0x200000` (2MB) |
|
||||
| SLPI | `0x98700000` | `0x1400000` (20MB) |
|
||||
| video | `0x97800000` | `0x500000` (5MB) |
|
||||
| CDSP | `0x97d00000` | `0x800000` (8MB) |
|
||||
|
||||
These match the mainline `sdm845-google-common.dtsi` override exactly. TZ accepts this layout.
|
||||
|
||||
---
|
||||
|
||||
## Area 1: Driver boot sequence — MBA load + MPSS load
|
||||
|
||||
### MBA load
|
||||
|
||||
| Step | LOS (`pil_mss_reset_load_mba`) | Mainline (`q6v5_mba_load`) |
|
||||
|------|------|---------|
|
||||
| 1 | `pil_notify_aop("on")` | `q6v5_load_state_toggle(true)` via QMP — **same message** |
|
||||
| 2 | Enable proxy clocks/regulators | Enable proxy clocks/regulators + power-domains |
|
||||
| 3 | `dma_alloc_attrs(mba_mem_dev_fixed, ...)` — alloc from reserved `pil_mba_mem` at `0x98500000` | `memremap(mba_phys)` — directly maps reserved `mba_region` at `0x98500000` |
|
||||
| 4 | `memcpy` MBA fw into buffer | `memcpy` MBA fw into mapped region |
|
||||
| 5 | — | **`q6v5_xfer_mem_ownership(&mpss_perm, false, true, mpss_phys, mpss_size)` — HLOS->MSS for 152MB MPSS** |
|
||||
| 6 | `pil_assign_mem_to_subsys(mba_dp_phys, mba_dp_size)` — HLOS->MSS for MBA only | `q6v5_xfer_mem_ownership(&mba_perm, false, true, mba_phys, mba_size)` — HLOS->MSS for MBA |
|
||||
| 7 | Trigger boot FSM, wait PBL + MBA `XPU_UNLOCKED` | Trigger boot FSM, wait PBL + MBA `XPU_UNLOCKED` |
|
||||
|
||||
**DELTA 1 (step 5): Mainline pre-assigns the ENTIRE 152MB MPSS region to MSS before MBA boots. LOS does not touch MPSS until after MBA boots and segment loading begins.** This is a 152MB hyp-assign call that LOS never makes at this stage.
|
||||
|
||||
### MPSS load (segment loading + final handoff)
|
||||
|
||||
| Step | LOS (`pil_boot` in `peripheral-loader.c`) | Mainline (`q6v5_mpss_load`) |
|
||||
|------|------|---------|
|
||||
| 1 | `init_image` (MBA authenticates metadata) | `q6v5_mpss_init_image` (MBA authenticates metadata) |
|
||||
| 2 | `mem_setup` (MBA sets up MPSS memory) | `qcom_scm_pas_mem_setup` — **SKIPPED for SDM845** (`need_pas_mem_setup=false`) |
|
||||
| 3 | SSR: `pil_assign_mem_to_linux(region)` — reclaim from MSS back to HLOS | SSR: `q6v5_xfer_mem_ownership(true, false)` — reclaim |
|
||||
| 4 | `pil_assign_mem_to_subsys_and_linux(region)` — src={HLOS}, dest={HLOS+MSS}, both RW | `q6v5_xfer_mem_ownership(true, true)` — shared HLOS+MSS |
|
||||
| 5 | Load segments | Load segments |
|
||||
| 6 | `pil_reclaim_mem(region, subsys_vmid)` — src={HLOS,MSS}, dest={MSS only} | `q6v5_xfer_mem_ownership(false, true)` — MSS only |
|
||||
| 7 | `auth_and_reset` | Wait `RMB_MBA_AUTH_COMPLETE` |
|
||||
|
||||
**DELTA 2 (step 3-4): Cold boot ordering difference.** On first boot (non-SSR), LOS skips step 3 (comment: "linux already owns it") and goes straight to step 4 (shared). Mainline at step 3 does `q6v5_xfer_mem_ownership(&mpss_perm, true, false, ...)` — but `mpss_perm` was already set to `BIT(MSS_MSA)` by the pre-assign in DELTA 1. So mainline's step 3 is MSS->HLOS (reclaim), then step 4 is HLOS->{HLOS+MSS} (share). LOS goes straight from HLOS->{HLOS+MSS}. The net result should be the same IF the pre-assign succeeded, but if DELTA 1 failed with -22, `mpss_perm` is still `BIT(HLOS)` and step 3 would be a no-op (already HLOS-owned), then step 4 works. Except — the pre-assign failure should have already aborted `q6v5_mba_load`.
|
||||
|
||||
### Metadata authentication
|
||||
|
||||
| Step | LOS (`pil_msa_mss_reset_mba_load_auth_mdt`) | Mainline (`q6v5_mpss_init_image`) |
|
||||
|------|------|---------|
|
||||
| buffer | `dma_alloc_attrs` — dynamic | From `mdata_mem` reserved region if present, else `dma_alloc_attrs` |
|
||||
| assign | `pil_assign_mem_to_subsys(mdata, size)` — HLOS->MSS | `q6v5_xfer_mem_ownership(false, true, phys, size)` — HLOS->MSS |
|
||||
| auth | Write to RMB, wait `META_DATA_AUTH_SUCCESS` | Write to RMB, wait `META_DATA_AUTH_SUCCESS` |
|
||||
| reclaim | `pil_assign_mem_to_linux(mdata)` — MSS->HLOS | `q6v5_xfer_mem_ownership(true, false)` — MSS->HLOS |
|
||||
|
||||
**DELTA 3: Mainline has a dedicated `mdata_mem` reserved region (`alloc-ranges 0xa0000000`, 16KB, `no-map`). LOS uses a dynamic DMA allocation.** If TZ doesn't know about this `mdata_mem` range (it's an `alloc-ranges` not a fixed `reg`), the assign could fail.
|
||||
|
||||
---
|
||||
|
||||
## Area 2: SCM / secure-channel path
|
||||
|
||||
### The SCM call itself
|
||||
|
||||
| | LOS | Mainline |
|
||||
|--|-----|---------|
|
||||
| SCM SVC/CMD | `SCM_SVC_MP (0x0C)` / `MEM_PROT_ASSIGN_ID (0x16)` | `QCOM_SCM_SVC_MP (0x0C)` / `QCOM_SCM_MP_ASSIGN (0x16)` |
|
||||
| Call convention | `scm_call2(SCM_SIP_FNID(...), &desc)` — 7-arg SIP | `qcom_scm_call(dev, &desc, &res)` via `scm_smc_call` — 7-arg SIP |
|
||||
| VMID_HLOS | `0x3` | `0x3` |
|
||||
| VMID_MSS_MSA | `0xF` | `0xF` |
|
||||
| Perms | `PERM_READ=4, PERM_WRITE=2, PERM_EXEC=1` | `QCOM_SCM_PERM_READ=4, WRITE=2, EXEC=1` |
|
||||
| Dest struct | `{u32 vm, u32 perm, u64 ctx, u32 ctx_size}` (20B, pads to 24B on arm64) | `{__le32 vmid, __le32 perm, __le64 ctx, __le32 ctx_size, __le32 unused}` (24B) |
|
||||
| Src struct | `mem_prot_info {phys_addr_t addr, u64 size}` | `qcom_scm_mem_map_info {__le64 mem_addr, __le64 mem_size}` |
|
||||
| Buffer alloc | `kzalloc` + `virt_to_phys` + `dmac_flush_range` | `qcom_tzmem_alloc` from dedicated TZ memory pool |
|
||||
| Endianness | native (cpu-endian u32/u64) | explicit `cpu_to_le32`/`cpu_to_le64` |
|
||||
|
||||
**DELTA 4: SCM buffer memory source.** LOS uses `kzalloc` (normal kernel memory) + `virt_to_phys` for the SCM descriptor buffers. Mainline uses `qcom_tzmem_alloc` from a special TZ-visible memory pool. If `qcom_tzmem` is misconfigured or the pool isn't set up correctly on SDM845, TZ can't read the descriptor and returns -22.
|
||||
|
||||
**DELTA 5: Endianness handling.** LOS writes native-endian `u32`/`u64` to the descriptor buffers. Mainline uses `cpu_to_le32`/`cpu_to_le64`. On arm64 (little-endian), these are no-ops. Not a functional difference.
|
||||
|
||||
**DELTA 6: src (mem_to_map) struct.** LOS: `{phys_addr_t addr, u64 size}` — on arm64 this is `{u64 addr, u64 size}` = 16 bytes. Mainline: `{__le64 mem_addr, __le64 mem_size}` = 16 bytes. Same wire format.
|
||||
|
||||
---
|
||||
|
||||
## Area 3: DT — reserved-memory, power-domains, smem-states
|
||||
|
||||
### Reserved memory
|
||||
|
||||
| Region | LOS DTS (`sdm845.dtsi`) | Mainline (`sdm845-google-common.dtsi`) | Live Android |
|
||||
|--------|----|----|-----|
|
||||
| MPSS | `0x8e000000`, `0x7800000` (120MB) | `0x8e000000`, `0x9800000` (152MB) | `0x8e000000`, `0x9800000` (152MB) |
|
||||
| MBA | `0x96500000`, `0x200000` (2MB) | `0x98500000`, `0x200000` (2MB) | `0x98500000`, `0x200000` (2MB) |
|
||||
| SLPI | `0x96700000`, `0x1400000` (20MB) | `0x98700000`, `0x1400000` (20MB) | `0x98700000`, `0x1400000` (20MB) |
|
||||
| Metadata | (none — uses dma_alloc) | `alloc-ranges 0xa0000000`, `0x4000` (16KB), `no-map` | N/A |
|
||||
|
||||
The LOS DTS has different BASE ADDRESSES from what Android actually uses. The google-common override produces the correct values that match the live device.
|
||||
|
||||
**DELTA 7: LOS DTS says 120MB MPSS, but Android actually runs with 152MB.** The google-common override (which LOS applies via board-level .dtsi) matches. Mainline has the same override. Geometry is the same in practice.
|
||||
|
||||
**DELTA 8: Mainline has a `mdata_mem` reserved region. LOS does not.** Mainline's `q6v5_mpss_init_image` checks for a `metadata` child node with `memory-region` and uses that reserved region for metadata. If absent, falls back to `dma_alloc_attrs`. LOS always uses DMA. If the `mdata_mem` alloc-ranges region (`0xa0000000`) is not TZ-known, the metadata assign would fail. But `q6v5_mpss_init_image` is called AFTER MBA boots, so this wouldn't cause the MBA-level failure.
|
||||
|
||||
### MSS DT node differences
|
||||
|
||||
| Property | LOS | Mainline |
|
||||
|----------|-----|---------|
|
||||
| compatible | `qcom,pil-q6v55-mss` | `qcom,sdm845-mss-pil` |
|
||||
| reg | 8 regions (qdsp6, halt_q6/modem/nc, rmb, restart, pdc_sync, alt_reset) | 2 regions (qdsp6, rmb) — halts via syscon, reset via reset-controller |
|
||||
| `memory-region` | `<&pil_modem_mem>` on the node itself | Via `mpss` child node |
|
||||
| `qcom,mem-protect-id` | `<0xF>` — read by `peripheral-loader.c:912` into `subsys_vmid` | **ABSENT** — mainline uses hardcoded `need_mem_protection=true` in platform descriptor |
|
||||
| Regulators | `vdd_cx`, `vdd_mx`, `vdd_mss` as direct supplies with TURBO voltage | `power-domains` via `rpmhpd` (cx, mx, mss) |
|
||||
| AOP | `qcom,signal-aop` + `mboxes = <&qmp_aop 0>` | `qcom,qmp = <&aoss_qmp>` — same path, different binding |
|
||||
| `qcom,pil-self-auth` | present | **ABSENT** — mainline always self-auths |
|
||||
| `qcom,override-acc` | present | **ABSENT** — mainline handles ACC differently |
|
||||
| `qcom,qdsp6v65-1-0` | present (selects q6v65 reset sequence) | **ABSENT** — mainline selects via `version = MSS_SDM845` |
|
||||
| `qcom,mss_pdc_offset` | `<8>` | **ABSENT** — mainline uses `pdc_reset` reset-controller |
|
||||
| MBA child | `qcom,mba-mem@0` with `qcom,pil-mba-mem` compatible + `memory-region` | `mba` child with just `memory-region` |
|
||||
| Metadata child | (none) | `metadata` child with `memory-region = <&mdata_mem>` |
|
||||
|
||||
---
|
||||
|
||||
## Summary of deltas, ranked by likelihood of causing the -22
|
||||
|
||||
1. **DELTA 1 — MPSS pre-assign before MBA boot.** Mainline assigns 152MB MPSS to MSS before MBA boots. LOS does not. This is a large, early hyp-assign that LOS avoids entirely. If this returns -22, the modem never starts. This is the most likely trigger — it's the first hyp-assign call mainline makes that LOS doesn't, and it's for the biggest region.
|
||||
|
||||
2. **DELTA 4 — SCM buffer allocation (tzmem vs kzalloc).** If `qcom_tzmem` allocates from a region TZ can't read, every SCM call fails. Would affect ALL assign calls, not just modem.
|
||||
|
||||
3. **DELTA 3 — Metadata reserved region.** Mainline uses a fixed `mdata_mem` at alloc-ranges `0xa0000000`. LOS uses dynamic DMA. If TZ doesn't know about this range, metadata auth assign fails. But this is after MBA boot, so it wouldn't cause the PBL-level failure.
|
||||
|
||||
4. **DELTA 8 — No `qcom,mem-protect-id` in mainline DT.** LOS reads VMID `0xF` from DT. Mainline hardcodes `need_mem_protection=true` and uses `QCOM_SCM_VMID_MSS_MSA` (also `0xF`). Functionally equivalent.
|
||||
|
||||
**The test:** Remove the MPSS pre-assign from `q6v5_mba_load` (lines 1236-1241 in `qcom_q6v5_mss.c`) — let MBA boot with only the MBA region assigned, matching LOS behavior. The MPSS assign happens later in `q6v5_mpss_load` anyway. If this clears the -22, DELTA 1 was the gate.
|
||||
378
saf/device/evidence/tz_listener_archaeology.md
Normal file
378
saf/device/evidence/tz_listener_archaeology.md
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
# PAF · qseecomd / TZ-listener archaeology on the LOS vendor extraction
|
||||
|
||||
Scope: identify exactly which AP-side TrustZone components (qseecomd + listeners) the modem's
|
||||
secure-storage (SFS) path depends on, using the already-extracted vendor stack. No device/ssh
|
||||
touched; read-only archaeology on `android-reference/`.
|
||||
|
||||
Prior art read first (do not re-derive): `HANDOFF.md:560-587` (the IMEI-mismatch finding that
|
||||
opened this frontier — "SFS/secure-EFS layer... Keymaster decrypts persist blobs | none"),
|
||||
`PAF/modem.md:610-634` (Android-vs-pmOS service parity table, `Keymaster | decrypts persist blobs
|
||||
| none | IMEI encrypted at rest`), `android-capture-20260622/EFS_FINDINGS.md` (2026-06-22, **now
|
||||
superseded** by the later IMEI finding — its "no persisted modem-side state is missing" conclusion
|
||||
was written before the TEST-IMEI-under-pmOS discovery).
|
||||
|
||||
## Materials actually available (correction to task brief)
|
||||
|
||||
`pixel3-work/los-vendor/vendor.img` does **not exist** in this project. What exists instead is
|
||||
better: a fully unpacked vendor partition tree at
|
||||
`android-reference/vendor-stack-20260621/vendor/{bin,lib,lib64}/` — no debugfs/7z mounting needed,
|
||||
direct file/strings access. Also present: `android-reference/init-rc/init.sdm845.rc` (29KB, the
|
||||
real device init.rc), `init.sdm845.mpssrfs.rc`, `init.sdm845.diag.rc`, `qcrild.rc`; and
|
||||
`android-live-20260619/` (boot/radio logs, partition map, rmt_storage rc — no qseecomd rc captured
|
||||
live).
|
||||
|
||||
## CONFIRMED — binaries and libraries present
|
||||
|
||||
`find android-reference/vendor-stack-20260621/vendor -iname '*qseecom*' -o -iname '*listener*' -o
|
||||
-iname '*keymaster*' -o -iname '*rpmb*' -o -iname '*ssd*' -o -iname '*sfs*' -o -iname '*drmfs*'`:
|
||||
|
||||
- `bin/qseecomd` — the listener-service host daemon (stripped, ELF aarch64, Android 31 ABI)
|
||||
- `lib64/libQSEEComAPI.so` — QSEECom userspace API (talks to `/dev/qseecom` char device via ioctl)
|
||||
- `lib64/librpmb.so`, `lib/librpmb.so` — RPMB listener backend
|
||||
- `lib64/libssd.so`, `lib/libssd.so` — SSD ("Secure Storage Data") listener backend
|
||||
- `lib64/libdrmfs.so`, `lib/libdrmfs.so` — DRM/persist-file listener backend
|
||||
- `lib64/libadsp_default_listener.so`, `libcdsp_default_listener.so`, `libssc_default_listener.so`
|
||||
— per-DSP FastRPC default listeners (ADSP/CDSP/SLPI-adjacent, not modem-SFS)
|
||||
- Keymaster stack: `android.hardware.keymaster@{3.0,4.0,4.1}.so`, `libqtikeymaster4.so`,
|
||||
`libkeymaster4support.so`, `libkeymasterprovision.so`, `nos_app_keymaster.so`,
|
||||
`bin/hw/android.hardware.keymaster@4.0-service-qti`,
|
||||
`bin/hw/android.hardware.keymaster@4.1-service.citadel` (Citadel = discrete security chip
|
||||
StrongBox, separate from the SDM845-embedded TZ/QSEE path — Pixel 3 has both)
|
||||
- **No dedicated modem-SFS listener library exists in the extraction.** No `libsfs*`, no
|
||||
`*modem*sec*`, no `*qsee*modem*` name anywhere in `vendor/`. Grepping
|
||||
`libril-qc-hal-qmi.so` for `modemst|persist|tzstorage|sfs` returns nothing — the RIL HAL doesn't
|
||||
touch secure storage directly either. This means modem SFS most plausibly rides through the
|
||||
**generic** `ssd`/`rpmb`/`drmfs` listeners rather than a modem-specific one (Qualcomm's modem SFS
|
||||
client library lives in the *modem firmware*, calling out to these generic AP listeners — nothing
|
||||
AP-side is modem-branded).
|
||||
|
||||
## CONFIRMED — init.rc service definitions and ordering
|
||||
|
||||
`android-reference/init-rc/init.sdm845.rc`:
|
||||
|
||||
- Line 607-610: `service vendor.qseecomd /vendor/bin/qseecomd` — `class core`, `user root`, `group
|
||||
root`.
|
||||
- Lines 73-84 (in the `on init && property:vendor.skip.init=0` block, i.e. very early):
|
||||
```
|
||||
# QSEE rpmb listener need symlink for bootdevice
|
||||
wait /dev/block/platform/soc/${ro.boot.bootdevice}
|
||||
symlink /dev/block/platform/soc/${ro.boot.bootdevice} /dev/block/bootdevice
|
||||
# start qseecomd early as we mount system/ vendor/ early
|
||||
# vendor needs keymaster that needs qseecomd
|
||||
start vendor.qseecomd
|
||||
start keymaster-4-0
|
||||
# start Citadel keymaster (StrongBox) early
|
||||
start vendor.citadeld
|
||||
start vendor.keymaster-4-1-citadel
|
||||
```
|
||||
The comment **"QSEE rpmb listener need symlink for bootdevice"** confirms the RPMB listener
|
||||
requires `/dev/block/bootdevice` to exist before qseecomd's listener threads can start — an
|
||||
explicit ordering dependency the AP init script encodes and pmOS's init/OpenRC does not.
|
||||
- Line 281 (`on post-fs-data`): `wait_for_prop vendor.sys.listeners.registered true` — init
|
||||
**blocks** IPA firmware loading on this property.
|
||||
- The property `vendor.sys.listeners.registered` is set by `qseecomd` itself — confirmed by
|
||||
`strings bin/qseecomd`: `Total listener services to start = %d`, `%s: Started (Listener ID =
|
||||
%d)`, and the literal string `vendor.sys.listeners.registered` present in the binary.
|
||||
- `qseecomd` strings also show two fatal-if-failed gates:
|
||||
`ERROR: RPMB_INIT failed, shall not start listener services`
|
||||
`ERROR: SSD_INIT failed, shall not start listener services`
|
||||
`SEC_UI_INIT failed, shall not start secure ui listener` — i.e. RPMB and SSD backend init are
|
||||
hard prerequisites for the listener framework to come up at all; if either fails, **no** listener
|
||||
(including whatever handles modem SFS) starts.
|
||||
- `rmt_storage` (line 644-648, `class core`) and `modem_svc` (line 655-659, `class core`) are
|
||||
declared **after** qseecomd/keymaster in the file but all are `class core` — Android's init classes
|
||||
don't strictly serialize within a class by file order alone; the *documented* explicit dependency
|
||||
is the `wait_for_prop vendor.sys.listeners.registered true` gate on IPA (data path), not on
|
||||
rmt_storage/modem load directly. **No explicit `wait_for_prop` ties modem/rmt_storage start to
|
||||
qseecomd** in this rc — the dependency for modem SFS is therefore INFERRED (via the modem firmware's
|
||||
own runtime SFS calls reaching TZ reaching the AP listener), not an init-script ordering fact.
|
||||
|
||||
## CONFIRMED — where the secure blobs plausibly live
|
||||
|
||||
From `strings` on the listener backend libraries themselves:
|
||||
|
||||
- `libssd.so`: `/dev/block/bootdevice/by-name/ssd`, `/dev/block/platform/msm_sdcc.1/by-name/ssd` —
|
||||
a **dedicated GPT partition named `ssd`** ("Secure Storage Data"). `QSEECom_register_listener` /
|
||||
`ssd_init_service` / `ssd_read` / `ssd_write` confirm this backs a QSEECom listener that a TZ
|
||||
trustlet calls into for opaque encrypted blob storage.
|
||||
- `libdrmfs.so`: `/data/vendor/tzstorage/`, `/data/misc/qsee/`, `/mnt/vendor/persist/data/`,
|
||||
`/persist/data/`, plus XML-driven `gp_persist_path` config — a **file-backed** persist store
|
||||
(DRM/general-purpose TZ file storage), separate from the raw `ssd` partition.
|
||||
- `librpmb.so`: `/dev/block/bootdevice` (via `by-name` symlink), `/dev/block/mmcblk0rpmb`,
|
||||
`RPMB.128KB/256KB/512KB/1MB` size buckets — the actual **RPMB (Replay Protected Memory Block)**
|
||||
hardware region on the UFS/eMMC boot device, accessed via `rpmb_emmc_read/write` ioctls.
|
||||
|
||||
**Partition-map check — RESOLVED 2026-07-02, live on pmOS:** the June partition-map capture was
|
||||
incomplete, not authoritative. `ls /dev/disk/by-partlabel/` on the live phone tonight shows the
|
||||
`ssd` partition **does exist**. Also confirmed live: the UFS well-known RPMB LUN is present
|
||||
(`ufs-bsg0`, scsi target `0:0:0:49488`), and `/mnt/vendor/persist/data/` holds real factory (Oct
|
||||
2018 / May-Jul 2019 dated) SFS-shaped blob trees — main+`_Alt` twin dirs with `.bak` twins per
|
||||
file, matching Qualcomm's dual-copy anti-rollback layout. Both listener backends (`ssd`, `rpmb`)
|
||||
have real backing storage on this exact device; nothing is missing at the hardware/partition
|
||||
level. The gap is entirely software (no AP-side listener driver/daemon under mainline).
|
||||
|
||||
**INFERRED (not proven on disk):** modem SFS blobs (IMEI, RF identity, calibration trust anchors)
|
||||
most likely live in either the `ssd` partition (if present) or under `/persist/data/`
|
||||
(`libdrmfs.so`'s default paths overlap with the `persist` partition pmOS already mounts and rmtfs
|
||||
already serves plaintext EFS from). If modem SFS uses `/persist/data/` via `libdrmfs`, the **bytes
|
||||
are already visible to pmOS at the block level** (persist is not RPMB) but are TZ-key-encrypted
|
||||
**in the file contents**, decrypted only via a live TZ round-trip through `qseecomd`'s listener —
|
||||
consistent with "plain EFS I/O works, secure layer doesn't" exactly as HANDOFF.md describes.
|
||||
|
||||
## CONFIRMED — no modem-specific listener ID found
|
||||
|
||||
Searched every `.so` under `vendor/lib64` for modem+SFS co-occurring strings
|
||||
(`modem.*sfs|sfs.*modem|modemst|modem_fsg`): only hits were `android.hardware.radio@1.{3,4,5}.so`
|
||||
and `libril-qc-hal-qmi.so`, and those are RIL/QMI Android HAL symbols (`getModemStackStatus` etc.),
|
||||
**not** QSEEComAPI listener registration. No listener ID constant (e.g. `0x3000`/`8192`) was found
|
||||
readable in any stripped binary via `strings` — listener IDs are typically passed as integer
|
||||
arguments at `QSEECom_register_listener()` call sites, not embedded as readable strings; extracting
|
||||
them requires disassembly (Tier 3, out of scope for this pass).
|
||||
|
||||
## Could a mainline userspace replace this?
|
||||
|
||||
**What the dependency IS, precisely:** the modem firmware's own SFS client (inside the MPSS
|
||||
firmware blob, not AP code) makes a TZ syscall that TZ cannot service locally; TZ round-trips to
|
||||
the AP via a **QSEECom listener** (RPMB and/or SSD and/or DRM-persist backend, hosted by
|
||||
`qseecomd`), which does raw block I/O against either the RPMB hardware region or the `ssd`/`persist`
|
||||
partition and returns the (TZ-en/decrypted) blob. Without `qseecomd` alive and its
|
||||
`RPMB_INIT`/`SSD_INIT` having succeeded, TZ's callback to the AP has no listener to answer it —
|
||||
the modem's SFS call would time out or the TZ syscall would return an error, which is consistent
|
||||
with the modem parking itself in OFFLINE rather than crashing (a "deliberate refusal," matching
|
||||
`secure_load_tz_frontier.md`'s framing).
|
||||
|
||||
**Feasibility notes (brief — this doc identifies the dependency, does not scope the fix):**
|
||||
- Open-source reimplementations of `qseecomd`'s listener framework exist in the wild for other
|
||||
Qualcomm mainline bring-ups (postmarketOS/msm8916, sdm660, sdm845 devices with eMMC RPMB
|
||||
trustlets) — typically a minimal userspace daemon that opens `/dev/qseecom`, registers the RPMB
|
||||
and SSD listener IDs, and does raw partition I/O, without needing Android's HIDL/keymaster stack
|
||||
around it. This is architecturally plausible to port: `librpmb.so`/`libssd.so`'s job is generic
|
||||
block I/O, not Android-specific.
|
||||
- The harder unknown is whether TZ on this specific signed XBL/TZ image expects listener
|
||||
**registration handshake details** (listener ID, buffer negotiation, `QSEECom_register_listener`
|
||||
call sequence) that differ from a generic mainline shim — that requires either disassembling
|
||||
`libssd.so`/`librpmb.so` (small, no external deps, tractable) or the QSEECom kernel driver's ioctl
|
||||
ABI (mainline has a `qcom_scm`/`qseecom` driver already, so the kernel side may already exist —
|
||||
not checked in this pass).
|
||||
- Keymaster/Citadel (StrongBox) is a red herring for the modem specifically — HANDOFF's "Keymaster
|
||||
decrypts persist blobs" framing is plausible for *other* persist data but the modem SFS path more
|
||||
directly implicates RPMB/SSD listeners, not the Keymaster HAL itself (Keymaster protects Android
|
||||
keystore keys; the modem's own TZ trustlet does its own key derivation independent of Android
|
||||
Keymaster, per Qualcomm's usual architecture — **INFERRED**, not confirmed on this disk).
|
||||
|
||||
## What to try next
|
||||
|
||||
1. **Settle whether an `ssd` partition exists on this device.** Not in
|
||||
`android-live-20260619/partition-map.txt`; re-run a full `by-name` partition enumeration
|
||||
(`ls -la /dev/block/bootdevice/by-name/` on rooted Android, read-only) specifically looking for
|
||||
`ssd`. This single fact discriminates between the RPMB-only path and the SSD-partition path.
|
||||
2. **Capture `qseecomd`'s runtime behavior on Android** (read-only): `logcat -b all | grep -i
|
||||
qseecom`, and `getprop vendor.sys.listeners.registered` at various boot stages, to see exactly
|
||||
when listeners come up relative to `rmt_storage`/modem PIL load. Confirms whether the ordering
|
||||
this doc inferred from init.rc is real at runtime.
|
||||
3. **DONE 2026-07-02 — see "Live disassembly results" section below.** Listener IDs recovered:
|
||||
SSD = `0x5000`, RPMB = `0x6400`. (Both differ from the earlier chat-only agent's string-based
|
||||
guesses of `0x3000`/`0x2000` — those were unread relocation offsets, not real constants. Trust
|
||||
this section's numbers, not that prior report.)
|
||||
4. **Check whether mainline's existing `drivers/misc/qcom-qseecom` / `qcom_scm` kernel driver
|
||||
(if present in `references/sdm845-mainline-*`) already exposes `/dev/qseecom`** — if the kernel
|
||||
ABI is already there, only a userspace listener daemon is missing, which is a much smaller lift
|
||||
than expected.
|
||||
5. Cross-check this doc's ordering claims against `secure_load_tz_frontier.md`'s XPU/`assign_mem
|
||||
-22` findings — if the XPU crash-loop and the SFS-listener gap are the same root cause
|
||||
(TZ-side memory/permission setup Android does and mainline doesn't), the fixes may converge.
|
||||
|
||||
## Live disassembly results — 2026-07-02
|
||||
|
||||
Toolchain note: this system's `objdump` is x86-only (no AArch64 target compiled in — confirmed
|
||||
via `objdump -i`). `rizin` (0.8.2, installed) has its own capstone-based AArch64 disassembler and
|
||||
does NOT depend on binutils target support. Use `rizin -q -c 'e asm.arch=arm; e asm.bits=64; af @
|
||||
<addr>; pdf @ <addr>' <file>` for any ARM64 .so in this project. This works for stripped Android
|
||||
libs (dynsym exports still give function boundaries even with no local symtab).
|
||||
|
||||
### `libssd.so` (lib64, 11KB) — thin wrapper, confirms the theory exactly
|
||||
|
||||
Exports only 3 functions: `ssd_init_service`, `ssd_start`, `ssd_close`. All real work is in
|
||||
`libQSEEComAPI.so` (NEEDED entry) via `QSEECom_register_listener`/`_receive_req`/`_send_resp`.
|
||||
|
||||
- `ssd_init_service` (0x1014): opens `/dev/block/platform/msm_sdcc.1/by-name/ssd`, falls back to
|
||||
`/dev/block/bootdevice/by-name/ssd` on failure. Stores fd at offset `0x290` in a global struct.
|
||||
Confirms live phone finding: the `ssd` GPT partition is the real backing store, block-device
|
||||
raw I/O, no filesystem.
|
||||
- `ssd_start` (0x10e0): `QSEECom_register_listener(&global+0x298, 0x5000, 0)`.
|
||||
**SSD listener ID = 0x5000.**
|
||||
|
||||
### `librpmb.so` (lib64, 29KB) — real UFS RPMB driver, much richer than libssd
|
||||
|
||||
Exports: `rpmb_init_service`, `rpmb_start`, `rpmb_init`, `rpmb_default_init`, `rpmb_close`,
|
||||
`rpmb_exit`, `rpmb_read`/`rpmb_write`, plus separate eMMC (`rpmb_emmc_*`) and UFS (`rpmb_ufs_*`)
|
||||
backends selected at init — `rpmb_ufs_sg_dev_open`/`_close` uses the SCSI generic (`sg`) device,
|
||||
matching the live phone's `ufs-bsg0` / `0:0:0:49488` well-known LUN found tonight.
|
||||
|
||||
- `rpmb_start` (0x4a68): `QSEECom_register_listener(&global+0x918, 0x6400, 0)`.
|
||||
**RPMB listener ID = 0x6400.** (Rizin's disassembly initially mis-labeled the immediate-load
|
||||
instruction as `reloc.rpmb_ufs_read` because 0x6400 collides with a PLT slot address in its
|
||||
symbol table — decoded the raw instruction bytes by hand to confirm: `52 8c 80 02` =
|
||||
`MOVZ w2, #0x6400`. Don't trust rizin's auto-symbolization on immediate loads without
|
||||
double-checking raw bytes when the "symbol" name looks suspicious.)
|
||||
- `rpmb_ufs_send_request` (0x38b0, 536 bytes) is the actual TZ-facing wire call for UFS RPMB
|
||||
frames — not yet disassembled, next concrete step (see below).
|
||||
|
||||
### `libQSEEComAPI.so` (lib64, 32KB) — the real kernel ABI boundary
|
||||
|
||||
`QSEECom_register_listener(void **handle, uint32_t listener_id, uint32_t sb_size)` fully traced
|
||||
(0x43bc, 684 bytes):
|
||||
1. If `*handle == NULL`: `open("/dev/qseecom", O_RDWR)`, fd stored at struct offset `0x10`.
|
||||
2. ION shared-buffer alloc via internal `__QSEECom_ION_memalloc(handle, listener_id, 0x1b, sb_size)`
|
||||
— `0x1b` is the ION heap-id mask argument.
|
||||
3. `ioctl(fd, 0xc004970a, &mem_param_req)` — this is `QSEECOM_IOCTL_SET_MEM_PARAM_REQ` by ioctl
|
||||
number shape (`_IOWR` direction bits `0xc0`, size `0x04`, type `0x97`, nr `0x0a`).
|
||||
4. `ioctl(fd, 0xc0189701, &listener_req)` — `QSEECOM_IOCTL_REGISTER_LISTENER_REQ`. Request struct
|
||||
built at `var_60h`: `{listener_id: w21, ion_fd: w22 (or -1 on alloc fail path), sb_virt: x0,
|
||||
sb_size: w21 (dup)}` — needs one more pass to nail exact field order/padding against upstream
|
||||
downstream `qseecom.h`'s `qseecom_register_listener_req` struct definition (not yet fetched).
|
||||
5. On ioctl failure with `errno == 16 (EBUSY)`: returns `-2` ("already registered", not a hard
|
||||
error) instead of `-1`. `ssd_start`/`rpmb_start` both treat `-2` as success-equivalent (skip
|
||||
the error path, fall through to the pthread_create for the receive loop).
|
||||
|
||||
This is enough to write a minimal mainline `/dev/qseecom` listener client IF mainline's QSEECOM
|
||||
driver ever grows `SET_MEM_PARAM`/`REGISTER_LISTENER` ioctl support — which it currently doesn't
|
||||
(confirmed separately: `qcom_qseecom.c` has no ioctl surface at all, single hardcoded app table).
|
||||
So this ABI trace is necessary-but-not-sufficient; the missing piece is still kernel-side.
|
||||
|
||||
### Not yet done (next session, in priority order)
|
||||
|
||||
1. **`rpmb_ufs_send_request`** (librpmb.so 0x38b0) — the actual SCM/TZ syscall wire format for
|
||||
RPMB frames. This is the piece that would tell us whether RPMB access could theoretically be
|
||||
done via raw `qcom_scm_call()` from kernel space without the full listener ABI, or whether it
|
||||
genuinely needs the userspace round-trip through `/dev/qseecom`.
|
||||
2. **`QSEECom_receive_req`/`QSEECom_send_resp`** (libQSEEComAPI.so) — the listener's blocking
|
||||
receive-request / send-response loop, i.e. what actually happens after registration succeeds.
|
||||
This is the runtime protocol a mainline replacement daemon would need to implement.
|
||||
2b. Decode the exact `qseecom_register_listener_req` struct field layout — need upstream/downstream
|
||||
`qseecom.h` (check `references/los-kernel-blueline-4.9/drivers/misc/qseecom.h` if it exists,
|
||||
agent's TZ report cited `qseecom.c` but layout header wasn't confirmed read).
|
||||
3. **`libdrmfs.so`** — not touched this pass at all. Handles `/data/vendor/tzstorage/` and
|
||||
`/persist/data/` per the prior agent's string analysis; the `/mnt/vendor/persist/data/` factory
|
||||
SFS trees found live tonight are exactly its target. Same rizin workflow applies.
|
||||
4. Cross-check ioctl numbers `0xc004970a`/`0xc0189701` against mainline's `include/uapi/misc/
|
||||
qcom_essivtm.h`-style headers if any QSEECOM uapi header exists anywhere in the kernel tree —
|
||||
would confirm/deny whether mainline's ioctl numbering (if it ever adds these) needs to match
|
||||
Android's exactly, or if TZ itself doesn't care about the AP-side ioctl numbers (likely, since
|
||||
ioctls are a kernel-driver-internal convention, not part of the SCM wire protocol to TZ).
|
||||
|
||||
## `libdrmfs.so` disassembly — 2026-07-02
|
||||
|
||||
Two independent listener registrations found, NOT one:
|
||||
|
||||
- `fs_start` (0x239c, 112 bytes): `QSEECom_register_listener(&global+0x440, 0x5000, 0)`.
|
||||
**Registers the SAME listener ID as `ssd_start` (0x5000).** Not a bug/collision — either this
|
||||
is dead/legacy code in this build (superseded by `gpfs_start`), or `fs`/`ssd` are two client
|
||||
libraries that both legitimately attach to one shared TZ-side listener namespace. Not resolved
|
||||
which; `fs_start` has zero XML/dynamic-path logic (unlike `gpfs_start`), suggesting it's the
|
||||
older/simpler of the two and `gpfs_start` is what's actually active at runtime — check
|
||||
qseecomd's actual startup log strings (`Total listener services to start = %d`) against a live
|
||||
Android boot to see which of `fs`/`gpfs`/`ssd` actually get invoked, if that's ever recaptured.
|
||||
- `gpfs_start` (0x3ae8, 1260 bytes) — the real, dynamic one: `opendir()`-probes candidate config
|
||||
dirs, `xmlParseFile()`s an XML descriptor (this is the "XML-driven `gp_persist_path`" the prior
|
||||
agent found via strings), then `QSEECom_register_listener(&global+0x450, 0x7e000, 0x80)`.
|
||||
**GPFS (general-purpose filesystem) listener ID = 0x7e000.** Distinct ID space from ssd/rpmb —
|
||||
0x7e000 is far larger, consistent with Qualcomm's listener-ID allocation using a different
|
||||
range for "general purpose"/OEM-extensible listeners vs. the fixed system ones.
|
||||
|
||||
**Confirmed listener ID table (all three, disassembly-verified, not string-guessed):**
|
||||
|
||||
| Backend | Listener ID | Source function | Backing storage (live-verified tonight) |
|
||||
|---|---|---|---|
|
||||
| SSD | `0x5000` | `libssd.so:ssd_start` (+ `libdrmfs.so:fs_start`, same ID) | `ssd` GPT partition — confirmed present |
|
||||
| RPMB | `0x6400` | `librpmb.so:rpmb_start` | UFS RPMB well-known LUN — confirmed present (`ufs-bsg0`) |
|
||||
| GPFS | `0x7e000` | `libdrmfs.so:gpfs_start` | XML-configured paths, likely `/persist/data/`, `/data/vendor/tzstorage/` |
|
||||
|
||||
## RPMB transport mechanism — architecturally important finding
|
||||
|
||||
`rpmb_ufs_send_request_sense` (librpmb.so, 0x38b0) disassembled: builds a SCSI CDB (opcode byte
|
||||
`0x60`, a JEDEC UFS RPMB SECURITY PROTOCOL frame shape) and submits it via
|
||||
`ioctl(fd, 0x2285 /* SG_IO */, &sg_io_hdr)` against a raw SCSI-generic (`/dev/sg*`) device handle
|
||||
opened earlier by `rpmb_ufs_sg_dev_open`.
|
||||
|
||||
**This is plain kernel block-layer I/O — SG_IO passthrough, zero TrustZone/SCM/QSEECOM
|
||||
involvement in the actual RPMB frame transport.** The QSEECOM listener registration
|
||||
(`rpmb_start`, ID `0x6400`) is a SEPARATE mechanism: it's how the *modem firmware*, running in
|
||||
its own TZ-adjacent execution context, asks the AP to relay RPMB frames on ITS behalf (modem
|
||||
firmware has no direct UFS access — a TZ trustlet calls out to the registered AP listener, which
|
||||
does the SG_IO work and returns the result back through TZ to the modem).
|
||||
|
||||
**Why this matters for the mainline fix:** it means a mainline replacement doesn't need to
|
||||
reverse-engineer any SCM/TZ RPMB protocol — the RPMB wire format is bog-standard JEDEC UFS
|
||||
SECURITY PROTOCOL IN/OUT over SG_IO, which mainline's own kernel UFS/BSG stack already fully
|
||||
understands (this is not modem-specific or Qualcomm-specific at all). The ONLY missing piece is
|
||||
the QSEECOM listener registration/dispatch loop that lets TZ hand a pending RPMB request to
|
||||
userspace and get the SG_IO result back — i.e. exactly the `/dev/qseecom`
|
||||
`SET_MEM_PARAM`/`REGISTER_LISTENER`/receive-request/send-response ABI traced in the
|
||||
`libQSEEComAPI.so` section above. **The actual RPMB disk I/O is a solved problem already in
|
||||
mainline; only the TZ-listener relay is missing.**
|
||||
|
||||
## Updated priority for next session
|
||||
|
||||
Given the above, the size of "minimal mainline replacement" shrinks:
|
||||
1. A mainline QSEECOM listener client (kernel: needs `/dev/qseecom` ioctl support that doesn't
|
||||
exist yet in `qcom_qseecom.c` — this is the real blocking gap, confirmed structural earlier
|
||||
tonight) that can register listener IDs `0x5000`/`0x6400`/`0x7e000`.
|
||||
2. A userspace daemon that, on receiving a relayed request, does exactly what `librpmb.so`/
|
||||
`libssd.so` do: raw block I/O (SG_IO for RPMB, plain read/write for the `ssd` partition) —
|
||||
this part has NO remaining unknowns, it's now fully specified by tonight's disassembly.
|
||||
3. `QSEECom_receive_req`/`QSEECom_send_resp` (libQSEEComAPI.so) — still not disassembled; this is
|
||||
the blocking-loop protocol the daemon would need to implement against the new kernel ioctls.
|
||||
Next concrete disassembly target.
|
||||
|
||||
## `QSEECom_receive_req`/`QSEECom_send_resp` — full listener loop protocol MAPPED
|
||||
|
||||
The complete Android-side listener runtime protocol, fully traced (libQSEEComAPI.so):
|
||||
|
||||
**`QSEECom_receive_req(handle, out_buf, out_buf_len)`** (0x4d40):
|
||||
1. `ioctl(fd, 0x9705 /* QSEECOM_IOCTL_RECEIVE_REQ */, NULL)` — **blocks** until TZ has queued a
|
||||
request for this listener's shared buffer. No payload pointer passed to the ioctl itself — the
|
||||
request data is already sitting in the ION shared buffer registered at listener-registration
|
||||
time; this ioctl is purely a "wake me up" signal.
|
||||
2. On `ioctl` failure with `errno == -0x200` (i.e. `ERESTARTSYS`/interrupted): logs and treats
|
||||
distinctly from a hard error (retry-worthy, not fatal) — same handle-internal cached fields
|
||||
(`shared_buf_ptr`, `shared_buf_len`) at offsets `0x0`/`0x8` used to bounds-check.
|
||||
3. Once woken: bounds-checks the pending request size against `out_buf_len`, `memcpy`s the
|
||||
request out of the shared ION buffer into the caller-provided `out_buf`, then `memset`s the
|
||||
shared buffer clean.
|
||||
|
||||
**`QSEECom_send_resp(handle, resp_buf, resp_len)`** (0x4ea4):
|
||||
1. Bounds-checks `resp_len` against the registered shared-buffer size (handle offset `0x8`).
|
||||
2. `memcpy`s `resp_buf` into the shared ION buffer.
|
||||
3. Special-cases command type `0x13` at handle offset `0x1c` — zeroes an extra field, likely a
|
||||
listener-specific response-tag/status clear (not fully resolved, low priority).
|
||||
4. `ioctl(fd, 0x9706 /* QSEECOM_IOCTL_SEND_RESP_REQ */, NULL)` — hands the response back to TZ,
|
||||
also payload-free (data already placed in shared memory by the memcpy above).
|
||||
|
||||
**Full ioctl number set now confirmed (all four, disassembly-verified):**
|
||||
|
||||
| ioctl | Number | Direction | Purpose |
|
||||
|---|---|---|---|
|
||||
| `SET_MEM_PARAM_REQ` | `0xc004970a` | `_IOWR` | register the ION shared buffer with the kernel driver |
|
||||
| `REGISTER_LISTENER_REQ` | `0xc0189701` | `_IOWR` | register `{listener_id, ion_fd, vaddr, size}` |
|
||||
| `RECEIVE_REQ` | `0x9705` | `_IO` (no payload) | block until TZ has a pending request in shared mem |
|
||||
| `SEND_RESP_REQ` | `0x9706` | `_IO` (no payload) | tell TZ the shared-mem response is ready |
|
||||
|
||||
**This closes out the userspace-side unknowns entirely.** A minimal mainline listener daemon now
|
||||
has a complete, disassembly-verified spec to implement against, IF the kernel gains the matching
|
||||
`/dev/qseecom` ioctl surface. The three listener IDs (`0x5000` ssd, `0x6400` rpmb, `0x7e000` gpfs)
|
||||
plus this four-ioctl protocol plus the plain-SG_IO RPMB transport (previous section) is a complete
|
||||
picture of "what Android does" — the remaining gap is 100% kernel-side (`qcom_qseecom.c` needs
|
||||
these four ioctls implemented against real SCM calls) and TZ-handshake-side (whether this exact
|
||||
signed TZ image accepts registration from a differently-built AP driver — unknown, untestable
|
||||
without trying it).
|
||||
|
||||
## Session-end status (2026-07-02)
|
||||
|
||||
Every planned disassembly target for this pass is done: libssd.so, librpmb.so (both listener
|
||||
registration + RPMB transport), libdrmfs.so (fs_start + gpfs_start), libQSEEComAPI.so
|
||||
(register_listener, receive_req, send_resp — the full 4-ioctl protocol). Not yet touched:
|
||||
`libQSEEComAPI.so`'s `QSEECom_start_app`/`QSEECom_load_external_elf` family (TZ *app* loading,
|
||||
as opposed to *listener* registration — likely irrelevant to SFS/RPMB but unconfirmed) and
|
||||
`qseecomd` binary's own main()/startup logic (which of ssd/rpmb/gpfs it actually launches and in
|
||||
what order — inferred from init.rc gates so far, never disassembled directly).
|
||||
193
saf/device/mms.md
Normal file
193
saf/device/mms.md
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# PAF · MMS (picture messaging)
|
||||
|
||||
## STATUS (2026-07-15): MMS SEND WORKS end-to-end. Two loose ends remain.
|
||||
|
||||
**A real picture message sent successfully today** — image to +12897767709,
|
||||
508 KB, POSTed through Fido's MMS proxy and cleared from the queue. The core
|
||||
problem (reaching Fido's MMS servers with the right APN) is SOLVED. See
|
||||
"THE WORKING RECIPE" below.
|
||||
|
||||
Two things still not fully nailed:
|
||||
1. **No auto-start / lifecycle for the daemon.** `mmsd-tng` has no D-Bus
|
||||
auto-spawn or systemd service — it only runs when launched by hand. When it
|
||||
isn't running, sends silently pile up in the queue (that's how 4 messages
|
||||
got stuck). Needs a proper service unit.
|
||||
2. **Chatty says "MMSC is not configured."** The daemon's *internal* config is
|
||||
correct and it sends fine, but it does NOT expose the
|
||||
`org.ofono.mms.ModemManager` D-Bus interface (only `org.ofono.mms.Service`),
|
||||
which is what Chatty's settings menu reads. So Chatty can't see the MMSC even
|
||||
though sending works. Setting values in Chatty's own menu did NOT bring that
|
||||
interface up. Daemon-side interface-registration issue, not a wrong-value
|
||||
issue — the file `~/.mms/modemmanager/mms` already reads
|
||||
`MMS_APN=netsvcs`, `CarrierMMSC=http://mms.fido.ca`,
|
||||
`CarrierMMSProxy=205.151.11.13:80` (all correct).
|
||||
|
||||
---
|
||||
|
||||
## THE WORKING RECIPE (proven live 2026-07-15)
|
||||
|
||||
1. **APN = `netsvcs`** (NOT `mms.fido.ca`). Patched into the carrier database at
|
||||
`rootfs-overlay/usr/share/mobile-broadband-provider-info/serviceproviders.xml`
|
||||
(Fido entry, `<apn value="netsvcs">`, mmsc `http://mms.fido.ca`, proxy
|
||||
`205.151.11.13:80`). `mmsd-tng` re-reads that DB and writes the right values
|
||||
into `~/.mms/modemmanager/mms` — BUT only re-looks-up when its current MMSC is
|
||||
the placeholder `http://mms.invalid`, so to force a refresh you must
|
||||
`rm ~/.mms/modemmanager/mms` before restarting the daemon.
|
||||
2. **The `netsvcs` bearer must be connected AND have its IPv4 applied to the
|
||||
netdev.** ModemManager reports the IP but nothing installs it — apply
|
||||
manually (see cheat-sheet). Once the IP is on `qmapmux0.X`, the proxy becomes
|
||||
reachable and `mmsd-tng`'s `SO_BINDTODEVICE` binds sends to it correctly.
|
||||
3. With both in place, `mmsd-tng` POSTs queued messages through the proxy and
|
||||
they send. `AutoProcessOnConnection=true` means it auto-flushes the queue
|
||||
when a usable bearer appears.
|
||||
|
||||
**To reproduce a send right now:** ensure the `netsvcs` bearer is up with its IP
|
||||
applied, `rm ~/.mms/modemmanager/mms`, then
|
||||
`mmsdtng --debug` — it re-looks-up `netsvcs` from the DB and flushes the queue.
|
||||
|
||||
---
|
||||
|
||||
Texting (SMS) already works. This doc is about **MMS** — the picture/group
|
||||
messages. They still don't send/receive on their own yet, but today we found
|
||||
*why*, and proved the hard part (reaching Fido's MMS servers) actually works.
|
||||
|
||||
---
|
||||
|
||||
## ELI5 — what MMS even is, and why it's fussy
|
||||
|
||||
A plain text (SMS) is tiny, so it rides along on the same signaling channel the
|
||||
phone uses to talk to the tower — no internet needed. A picture message (MMS) is
|
||||
too big for that. So MMS actually works like this:
|
||||
|
||||
1. The tower sends you a tiny SMS that secretly means *"there's a picture waiting
|
||||
for you."*
|
||||
2. Your phone then has to **open a special little internet connection** and go
|
||||
**download the picture from the carrier's own server** (the "MMSC").
|
||||
3. To send a picture, your phone uploads it to that same server the same way.
|
||||
|
||||
The catch: that "special little internet connection" is **not** your normal data
|
||||
or WiFi. Carriers put their MMS server on a **private, members-only road** that
|
||||
only exists inside their own cellular network. You can't get to it from WiFi, and
|
||||
you can't get to it from the normal data connection either. You have to dial a
|
||||
**second, separate connection** specifically for MMS.
|
||||
|
||||
Every piece of confusion we hit came from that one fact: **MMS needs its own
|
||||
special door, and we kept knocking on the wrong ones.**
|
||||
|
||||
---
|
||||
|
||||
## The pieces (and the plain-English name for each)
|
||||
|
||||
| Thing | What it really is | Value for this SIM |
|
||||
|---|---|---|
|
||||
| **APN** | The *name of the door* you dial for a given service | `netsvcs` (NOT `mms.fido.ca`) |
|
||||
| **MMSC** | The *server* that actually holds the pictures | `http://mms.fido.ca` → `209.4.229.111` |
|
||||
| **MMS proxy** | A *doorman* you must go through to reach the MMSC | `205.151.11.13:80` |
|
||||
| **The daemon** | `mmsd-tng`, the background program that does all this for you | already installed |
|
||||
| **The phone book** | `serviceproviders.xml`, a big list of carrier settings the daemon trusts | had the WRONG APN |
|
||||
|
||||
---
|
||||
|
||||
## What we got wrong (and how we found the right answer)
|
||||
|
||||
### Wrong door #1: `mms.fido.ca`
|
||||
Every website, and the settings file already on the phone, said Fido's MMS APN is
|
||||
`mms.fido.ca`. We dialed it. The network slammed the door:
|
||||
`ServiceOptionNotSubscribed` — *"you're not signed up for that."*
|
||||
|
||||
That error sounds like an account/billing problem, but it wasn't. It just means
|
||||
**that door name doesn't exist on this SIM.** The public instructions are for
|
||||
manually typing settings into an Android phone; they don't necessarily match what
|
||||
the SIM is actually *provisioned* for underneath.
|
||||
|
||||
**How we found the truth:** we asked the modem to list the doors it *actually*
|
||||
knows about (`mmcli -m 0 --3gpp-profile-manager-list`). It answered with a real
|
||||
list, and there it was:
|
||||
|
||||
```
|
||||
profile 4: apn: netsvcs apn type: mms
|
||||
```
|
||||
|
||||
The real MMS door on this SIM is called **`netsvcs`**, not `mms.fido.ca`. Dialed
|
||||
`netsvcs` → it **connected immediately.** (Confirmed the same name shows up in the
|
||||
raw Android radio logs from this same SIM, so it's genuinely the carrier's name
|
||||
for it, not a fluke.)
|
||||
|
||||
> **Gotcha:** `ltemobile.apn` is the *generic Rogers-family* internet door
|
||||
> (Rogers/Fido/Chatr all share it). Some guides say "just use that for MMS too."
|
||||
> It's a red herring here — it's not Fido-specific, and it came up **IPv6-only**,
|
||||
> which can't even reach the IPv4 MMS server. `netsvcs` is the right door.
|
||||
|
||||
### Wrong door #2: trying to reach the server the normal way
|
||||
Once `netsvcs` connected, we tried to reach the MMS proxy (`205.151.11.13`) — and
|
||||
it hung, dead silence.
|
||||
|
||||
Two reasons, both now understood:
|
||||
|
||||
1. **The connection was "connected" but had no address actually plugged in.**
|
||||
The modem dialed the door and the carrier handed us an address
|
||||
(`10.145.x.x`) — but *nothing put that address onto the network card.* It's
|
||||
like being handed the keys to a car that nobody parked in your driveway. On the
|
||||
normal data connection, a program called NetworkManager does this parking step
|
||||
automatically. For our hand-dialed MMS connection, **nobody did it.** The
|
||||
interface had no IPv4 address at all.
|
||||
|
||||
The moment we manually "parked the car" (`ip addr add 10.145.146.168/28` onto
|
||||
the interface, plus routes to the proxy through the carrier gateway) — **the
|
||||
proxy answered.** Reliably, every time. That was the real unlock.
|
||||
|
||||
2. **The proxy only speaks "MMS language."** After it started answering, a plain
|
||||
web request (`curl`) to it still got silence. That's expected: Fido's proxy is
|
||||
a **WAP proxy** — it only understands the special MMS protocol (WSP), and
|
||||
ignores normal web traffic. So a normal browser test will *always* look like a
|
||||
failure even when the path is perfectly open. Only `mmsd-tng` speaks the right
|
||||
language to it.
|
||||
|
||||
---
|
||||
|
||||
## Where it actually stands right now
|
||||
|
||||
**PROVEN working:**
|
||||
- The correct APN is `netsvcs`, and it connects.
|
||||
- Once its IP is applied to the interface, **Fido's MMS proxy is reachable** (TCP
|
||||
handshake succeeds every time, from the bearer's own address).
|
||||
|
||||
**Still missing (the automation):**
|
||||
- Nothing yet automatically does the "park the car" step — dial the `netsvcs`
|
||||
MMS connection, apply its IP + routes to the interface, and hand it to
|
||||
`mmsd-tng` — at the moment a picture needs to go out, then tear it down after.
|
||||
- `mmsd-tng`'s own config keeps regenerating from the carrier phone book
|
||||
(`serviceproviders.xml`) with the wrong `mms.fido.ca` APN, so editing its
|
||||
local config by hand gets overwritten. Fixing the phone book entry is the
|
||||
durable path (started: patched to `netsvcs` in `rootfs-overlay/usr/share/
|
||||
mobile-broadband-provider-info/serviceproviders.xml`), **but** `mmsd-tng`'s
|
||||
lookup only matches an entry when the *active* bearer's APN equals the entry's
|
||||
`value=`, so the exact match behavior still needs verifying against the real
|
||||
code path before calling it solved.
|
||||
|
||||
**Next step:** re-test `mmsd-tng` end-to-end now that the network road is
|
||||
manually open (send one real MMS), to confirm the daemon itself works, *before*
|
||||
building the automation that opens the road on its own.
|
||||
|
||||
---
|
||||
|
||||
## Cheat-sheet: manually open the MMS road (for testing)
|
||||
|
||||
```sh
|
||||
# 1. dial the real MMS door, IPv4
|
||||
mmcli -m 0 --create-bearer="apn=netsvcs,apn-type=mms,ip-type=ipv4"
|
||||
mmcli -m 0 -b <N> --connect # note the interface (qmapmux0.X) + address
|
||||
|
||||
# 2. "park the car": put the handed-out IP onto the interface
|
||||
sudo ip addr add <address>/<prefix> dev qmapmux0.X
|
||||
|
||||
# 3. lay down the private road to the proxy + MMSC via the carrier gateway
|
||||
sudo ip route add 205.151.11.13/32 via <gateway> dev qmapmux0.X # proxy
|
||||
sudo ip route add 209.4.229.111/32 via <gateway> dev qmapmux0.X # MMSC
|
||||
|
||||
# 4. proof the road is open (TCP handshake to the proxy should succeed):
|
||||
python3 -c "import socket;s=socket.socket();s.bind(('<address>',0));s.connect(('205.151.11.13',80));print('proxy OPEN')"
|
||||
```
|
||||
|
||||
(A plain `curl` through the proxy will still hang — that's normal, it only
|
||||
speaks MMS, not web. Real proof is `mmsd-tng` sending a message.)
|
||||
837
saf/device/modem.md
Normal file
837
saf/device/modem.md
Normal file
|
|
@ -0,0 +1,837 @@
|
|||
# PAF · Modem bring-up
|
||||
|
||||
## ARCH STATUS (2026-07-10): WORKING — verified live on the Arch install.
|
||||
|
||||
`mmcli`: state connected, LTE, operator Fido, packet service attached,
|
||||
signal 73%. The whole solved stack made the jump to Arch: `modemsmem`
|
||||
is in our kernel tree (0c2eb6bf3 "soc: qcom: add modemsmem, wire up on
|
||||
blueline"), rmtfs/tqftpserv/pd-mapper/ModemManager run as services, and
|
||||
`blueline-clat.service` handles 464XLAT. Everything below is the research
|
||||
record of HOW the 52 wall fell (pmOS era, 2026-06/07) — the mechanism is
|
||||
identical on Arch; read it before touching anything modem-adjacent.
|
||||
Deep evidence docs referenced below live in `evidence/`.
|
||||
|
||||
## RESOLVED 2026-07-03 — modem ONLINE, registered, passing LTE traffic. Root cause of the 52 wall: missing modemsmem driver (SMEM item 134).
|
||||
|
||||
Google Pixel modem firmware reads SoC info from SMEM_ID_VENDOR0 (item 134) during boot;
|
||||
downstream writes it via `drivers/soc/qcom/modemsmem/`. Mainline never had the driver, so
|
||||
the MPSS RF task found nothing there and parked op-mode OFFLINE → every RF transition
|
||||
illegal → QMI 52. Fix: Richard Acayan's sdm670 mainline port of the driver (commit
|
||||
d9afe44b) + `google,modemsmem`/`qcom,smem = <134>` DT node — canonical tree `4ef9b6943`,
|
||||
pmaports `9d52b47813` (CONFIG_QCOM_MODEMSMEM=y). Verified live: op-mode online, Fido LTE
|
||||
registration home, IPv6 bearer passing packets. Full details in HANDOFF.md (2026-07-03).
|
||||
Everything below this line is the (closed) investigation record; the leads/ledger remain
|
||||
correct as history. Remaining open threads are identity (SFS/test-IMEI) and polish
|
||||
(IPv4 bearer, calls/SMS), not RF.
|
||||
|
||||
## TOP LEAD 2026-07-02 #15 — DIAG_DATA glink channel: AP CAN request it, modem acks but never completes its own open. Real firmware-behavior finding, not a kernel gap.
|
||||
|
||||
**Correction to this doc's own earlier framing tonight**: an initial pass concluded mainline's glink
|
||||
core lacked any AP-initiated ("locally initiated") channel-open primitive at all, and that reaching
|
||||
DIAG_DATA would need core `qcom_glink` infrastructure work. **That was wrong** — re-read
|
||||
`drivers/rpmsg/qcom_glink_native.c` properly: `qcom_glink_create_local()` (a complete, working
|
||||
implementation of exactly this, sends `GLINK_CMD_OPEN`, waits for both acks) already exists and is
|
||||
already wired into the standard `.create_ept` op every transport uses. The real, and much smaller,
|
||||
entry-point gap was just: no *driver* was calling `rpmsg_create_ept()` against a fresh channel name
|
||||
from an already-bound `rpdev`. Fixed with **zero new kernel code** — userspace's existing
|
||||
`RPMSG_CREATE_EPT_IOCTL` on `/dev/rpmsg_ctrl2` (the modem-edge control device; `rpmsg_ctrl0`/`1` are
|
||||
CDSP/ADSP respectively, confirmed via `/sys/devices/.../4080000.remoteproc/.../rpmsg_ctrl2`) reaches
|
||||
this exact path already. Built a 60-line test binary (`diag_open_test.c`, cross-compiled via
|
||||
pmbootstrap chroot) to drive it.
|
||||
|
||||
**Live result, confirmed clean and repeatable across multiple fresh single-attempt modem boots**:
|
||||
- `GLINK_CMD_OPEN` sent for `DIAG_DATA` → modem.
|
||||
- Modem sends `GLINK_CMD_OPEN_ACK` back, consistently in <10ms when it responds at all.
|
||||
- **The modem never sends its own reciprocal `GLINK_CMD_OPEN` back** — the other half of glink's
|
||||
bidirectional handshake `qcom_glink_create_local()` requires (`wait_for_completion_timeout(&channel
|
||||
->open_req, ...)`) before a channel counts as usable.
|
||||
- Tested whether this was just slow: bumped the wait from 5s→60s (kernel commit `7441ca3dc`), rebuilt,
|
||||
reflashed, ran ONE clean attempt on a fresh modem boot, waited the full 63.84s. **Still nothing.**
|
||||
Falsified the "just needs more time" theory outright. Reverted the bump (`b4185e71b`) — no reason
|
||||
to ship a 60s stall for a case that structurally doesn't complete.
|
||||
- Cross-checked against downstream (`references/los-kernel-blueline-4.9/drivers/soc/qcom/glink.c`):
|
||||
its `GLINK_CONNECTED` notification requires the exact same bidirectional
|
||||
`ch_is_fully_opened(ctx)` condition (checked from both the incoming-open and the open-ack RX
|
||||
handlers) — downstream isn't looser about what counts as "open," it's just event-driven with no
|
||||
hard timeout instead of blocking-with-timeout. So this isn't mainline enforcing an extra
|
||||
requirement Android doesn't have; it's the same requirement, and the modem doesn't satisfy it
|
||||
either way under mainline's bring-up.
|
||||
|
||||
**What this means**: the modem's DIAG service is reachable and responsive at the glink-negotiation
|
||||
level (it correctly acks a request for a channel it recognizes by name) but will not commit to
|
||||
opening it — a "deliberate partial refusal" shape that rhymes with the *entire* rest of this frontier
|
||||
(SIM/QMI all healthy, DMS answers reads, refuses RF-arming writes; modem parks `offline` not `LPM`).
|
||||
Whatever state gate blocks RF-arm plausibly also gates DIAG. **F3 access via this channel is not
|
||||
reachable by any change on the AP side alone** — this is now a real, evidenced, narrow finding, not
|
||||
an assumption. Retest if/when the LPM-vs-offline or any other frontier item moves; a modem that
|
||||
reaches a healthier init state might complete this handshake as a side effect.
|
||||
|
||||
Test binary + method preserved: `diag_open_test.c` (cross-built via `pmbootstrap chroot -b aarch64`),
|
||||
`RPMSG_CREATE_EPT_IOCTL` against `/dev/rpmsg_ctrl2`, then `open()` the resulting `/dev/rpmsgN` — same
|
||||
approach works for probing ANY named glink channel on ANY edge (swap `/dev/rpmsg_ctrl0/1/2` for
|
||||
cdsp/adsp/modem) if a similar question comes up for a different service.
|
||||
|
||||
---
|
||||
|
||||
## TOP LEAD 2026-07-02 #14 — frontier #8's 3-precondition test finally run: provisioning-churn fixed & shipped, LPM-vs-offline confirmed unreachable from AP side
|
||||
|
||||
**Fixed and shipped precondition (b) from frontier #8 below**: `blueline-uim-select.sh` was
|
||||
unconditionally deactivating+reactivating the primary-gw provisioning session even when it already
|
||||
matched the slot/AID it was about to select — churn on an already-correct session, and exactly the
|
||||
class of write the raw-QMI RESULTS.md proved times out during the modem's not-ready window. Patched
|
||||
to skip the deactivate/reactivate when the existing binding already matches
|
||||
(`device-google-blueline-modem-tools` 7-r18→7-r21, committed `1ed2df1788`). **Verified live**: the
|
||||
log now reads "primary-gw session already bound to slot 1 app 1 ... skipping stock
|
||||
deactivate/reactivate" — the session is genuinely left alone across a modem restart for the first
|
||||
time this whole investigation.
|
||||
|
||||
**Result: does NOT flip DMS online.** Immediately after the clean-session restart,
|
||||
`--dms-set-operating-mode=online` still returns `DeviceNotReady (52)`. Polled operating-mode every
|
||||
3s for 18s post-restart: **steady `offline` the entire window, never once observed `LPM`.** So
|
||||
precondition (c) — "modem in LPM (not offline) for the online flip" — is now confirmed, not just
|
||||
suspected, to never occur spontaneously on pmOS: the firmware decides `offline` vs `LPM` before any
|
||||
AP-side QMI write is even possible, so no host script/wrapper/choreography (this fix included) can
|
||||
create the window frontier #8 needed.
|
||||
|
||||
**Where this leaves it:** frontier #8's 3-precondition experiment (configs active + clean
|
||||
provisioning + LPM-not-offline) is no longer "untested" — it's tested, with (a) and (b) now cheaply
|
||||
satisfiable and (b) fixed for real, and (c) confirmed firmware-internal and unreachable from any
|
||||
AP-side lever. This converges with TOP LEAD #13's conclusion (SLPI/ADSP/fastrpc/choreography all
|
||||
closed, F3 narration is the remaining instrument) via a different path — both land on "the
|
||||
offline-vs-LPM decision happens inside the firmware before Linux gets a vote." **Next: same F3
|
||||
instrument #13 named, now aimed at a sharper question** — not generic RF-init narration, but
|
||||
specifically why the firmware selects `offline` over `LPM` at its own boot, since that's now the
|
||||
single remaining precondition standing between here and an online modem.
|
||||
|
||||
Also confirmed same session: mainline blueline has **no DIAG transport at all** (`/dev/diag` absent,
|
||||
no `diagchar`-equivalent config, no `DIAG` glink channel among the live rpmsg devices) — so
|
||||
`diag_capture.md`'s Side B is a real, confirmed dead end on the AP side as of kernel
|
||||
`7.1.1_p20260702080259-r2`; F3 access (if pursued) needs either a from-scratch DIAG-over-glink
|
||||
kernel path or stays Android-side-only (Side A) for comparison purposes.
|
||||
|
||||
---
|
||||
|
||||
## TOP LEAD 2026-07-01 #13 — fastrpc `pageslen` kernel bug FIXED & shipped; 52 wall now proven independent of adsprpcd/ADSP health too
|
||||
|
||||
**Real, named, diff-attached kernel defect — the first of the whole investigation — found, fixed, shipped, verified.** `fastrpc_init_create_static_process()` in `drivers/misc/fastrpc.c` advertised `inbuf.pageslen = 0` while actually passing one remote_heap page in `args[2]`; the sibling dynamic `fastrpc_init_create_process()` correctly sets `pageslen = 1` for the identical one-page layout. The DSP trusts the metadata field, believes zero pages were passed, and aborts `INIT_CREATE_STATIC` — surfacing in userspace as `Broken pipe` and the adsprpcd coredump cascade (`dsp information is incorrect err: -1` for ADSP+CDSP). One-line fix (`0`→`1`); matches `FisiFla/nabu-cachyos` `0005-fastrpc-fix-init-create-static-pageslen.patch` (SM8250, independently shipped for the identical signature, commit claims it also killed an SLPI crash loop there). Committed `e3c47d44e` in `references/linux-7.1.1-sdm845-CANONICAL`. **Also committed the previously-floating-uncommitted RTC read-only fix** (`68ffb014b` — the load-bearing half of the 0-SPMI-writes result; it had never been committed, only sitting in the worktree). Both shipped in kernel `7.1.1_p20260701093924-r1`, flashed to `boot_a`.
|
||||
|
||||
**Process correction (saved to memory `pmos-src-skips-apkbuild-patches`):** `pmbootstrap --src` builds the **working tree as-is**, not committed-only. A first build silently included the two then-uncommitted RTC/DTS files; caught by inspecting `chroot_native/mnt/pmbootstrap/source-override/`. "Bake as commits" is necessary but not sufficient — the worktree itself must be in the intended state at build time.
|
||||
|
||||
**Verified live post-flash:**
|
||||
- **adsprpcd: no longer a fatal crash-loop.** Previously coredumped every boot with `dsp information is incorrect` / `Broken pipe`. Now: coredumps ONCE on first probe, systemd restarts it (NRestarts=1), then runs stably — **successfully opens `adsp_default_listener` handles on ADSP (domain 0)**. Old fatal signatures absent from the journal. Residual non-fatal noise remains: `remote_handle64_invoke ... method 2 ... Operation not permitted` + `open_mod_table_handle_invoke ... Error 0xe` on domain 0 — a *separate* ADSP-invoke-permissions issue, not the pageslen bug; next fastrpc thread if pursued.
|
||||
- **SPMI write flood: still 0** — RTC read-only fix preserved across the rebuild.
|
||||
- **`/dev/fastrpc-sdsp` still ABSENT** → `hexagonrpcd-sdsp` still "Could not attach to FastRPC node: Operation canceled". The pageslen fix targets ADSP/CDSP static-process creation; the SDSP node is a different path (depends on SLPI remoteproc fastrpc edges), so the SLPI/sensor-island story is **not** changed by this fix.
|
||||
- **dms-ui-ready (r17 C binary): ran clean**, same `DMS 0x0064 RESULT: FAILURE (result=1 error=58)` as before (unchanged, expected — orthogonal to fastrpc).
|
||||
|
||||
**Modem 52 wall: STILL THERE — now proven independent of adsprpcd/ADSP health too.** First `mmcli --enable` of the entire session with a non-crashing adsprpcd (all chain units active: modem-qrtr-ready, qcril-autoselect, qcril-prov, dms-ui-ready, ModemManager): identical `QMI protocol error (52): 'DeviceNotReady'` on `Set Operating Mode=online`, op-mode stays `offline`, state `disabled`, lock `sim-pin2`, SIM on slot 2 (correct). Combined with TOP LEAD #12's SLPI proof, **the modem's RF-ready gate is now robust to the entire host-side DSP coordination layer** (SLPI state × 3 boots, adsprpcd/ADSP health × 1 boot). The ADSP-PD/servreg candidate is substantially weakened. The wall is the modem firmware's own internal decision; nothing host-side we can move has touched it.
|
||||
|
||||
**Where that leaves the candidate space:** of `los-mss-translated.c`'s four deltas — (1) EFS NV item, (2) bootloader/ABL verified-boot state, (3) subsystem coordination [SLPI/PD], (4) host-QMI-replay timing — #3 is now closed and #4 is substantially exhausted (DMS 0x0064 sends+responds but error 58; NAS mode-pref fails DeviceUnsupported pre-RF). #1/#2 were researched to ground with no static delta. The one instrument that could show the modem's *own* RF-init reasoning — F3 narration — remains uncaptured (QCSuper's blanket arm skips the NAS/RRC SSID ranges; per-range SET_RT_MASK arms cleanly but yields no traffic, and we lack a Qualcomm SSID name table to confirm those ranges *are* NAS/RRC). That F3 capture + SSID identification is the most likely remaining source of new signal.
|
||||
|
||||
---
|
||||
|
||||
## TOP LEAD 2026-07-01 #12 — SLPI independence now PROVEN (not just argued); DMS 0x0064 "UI ready" built, untested
|
||||
|
||||
**SLPI-state dependency is now definitively closed, with hard repeated evidence** (2026-06-30
|
||||
night → 2026-07-01, three separate boots, live hardware). Fixed a real regression along the way:
|
||||
`blueline-slpi-ready.sh` disabled SLPI crash-recovery for a "single-shot" start; when the SAR/CHRE
|
||||
firmware fault crashed it, the disabled-recovery path left the glink-edge channel permanently torn
|
||||
— `intent request timed out` storming every ~10s indefinitely, which took the modem's own QMI/QRTR
|
||||
transport down with it (`endpoint hangup`, `Connection reset by peer`). Fix: recovery must stay
|
||||
**enabled** (matches Android's `ssr_setup`/`ssr_diag`, which never disables it) so the kernel's own
|
||||
crash handler tears down/rebuilds glink cleanly; the script then does ONE clean attempt and parks
|
||||
SLPI (`echo stop`) once caught in a healthy `running` window, instead of crash-looping forever.
|
||||
Verified over three clean boots: SLPI crashed (0 times / 50 times / 0 times depending on timing),
|
||||
**every single time `DeviceNotReady` on RF-enable was byte-for-byte identical** regardless of SLPI's
|
||||
state (crashed continuously, running, offline, or cleanly parked). Also wired `modem-qrtr-ready.service`
|
||||
`After=`/`Wants=blueline-slpi-ready.service` to match Android's real architecture — `vendor.qcom.devup`
|
||||
(what `modem-qrtr-ready` mirrors) is **literally set by** `init.qcom.devstart.sh` right after it kicks
|
||||
SLPI (`echo 1 > /sys/kernel/boot_slpi/boot; setprop vendor.qcom.devup 1`), confirmed by reading the
|
||||
real Android script off the mounted LOS vendor image. Architecturally correct, safe, zero cost — but
|
||||
does not move the 52 wall. **Candidate #4 ("Subsystem coordination — SLPI PD state?") in
|
||||
`los-mss-translated.c`'s delta list is now closed.**
|
||||
|
||||
**Checked and closed a real diagnostic gap**: `qmicli --dms-get-operating-mode`'s output includes an
|
||||
optional "Offline reason:" line (libqmi `qmi_message_dms_get_operating_mode_output_get_offline_reason`,
|
||||
bitmask of `HOST_IMAGE_MISCONFIGURATION`/`PRI_IMAGE_MISCONFIGURATION`/`PRI_VERSION_INCOMPATIBLE`/
|
||||
`DEVICE_MEMORY_FULL`) that no prior capture had actually looked for. Checked the existing captured
|
||||
output (`tools/modem-online-provisioner/modem-online-provision-20260622-063154.log`): the field never
|
||||
appears — the modem doesn't populate any of the four documented reasons. Dead end, but a clean one.
|
||||
|
||||
**Built AND tested: `dms-ui-ready.py`/`.service` + a raw-C fallback. Sends clean, modem responds,
|
||||
52 unchanged.** `PAF/qcril_online_sequence.md` (prior session, full disassembly of
|
||||
`libril-qc-hal-qmi.so`) identified DMS message **0x0064** ("telephony UI ready") as the one
|
||||
load-bearing pre-online step QCRIL performs that pmOS had never replayed. The Python/GI
|
||||
implementation (`Qmi.Message` + raw TLV writers) **failed at runtime**: this libqmi build does
|
||||
not expose a generic raw-message type via GObject-Introspection at all — only per-message typed
|
||||
classes exist in the typelib, confirmed by enumerating `dir(Qmi)` live on the phone. Built a
|
||||
small C program instead, cross-compiled against the same libqmi (openimsd fork, 1.39.0) via
|
||||
`pmbootstrap chroot -b aarch64`, linked directly against the `.so` already on the phone. Ran it
|
||||
live: the modem answered 0x0064 with a real, understood protocol response (not "unsupported
|
||||
message"), `RESULT: FAILURE (result=1 error=58)`. Error 58's standard libqmi meaning
|
||||
(`WMS_ENCODING`, an SMS-service code) can't be its true meaning for an OEM DMS message — almost
|
||||
certainly a vendor-private error space reusing that numeric slot. Immediately retried
|
||||
`mmcli --enable`: **`DeviceNotReady` (52), byte-identical to every attempt all night.** Full
|
||||
detail + the C source path in `qcril_online_sequence.md`. Kept `dms-ui-ready.service` wired in
|
||||
the boot chain (best-effort, harmless, now proven to do something real) — the Python version in
|
||||
the package needs porting to the same raw-C approach before it'll actually work at boot; that's
|
||||
the concrete next step, not another blind guess.
|
||||
|
||||
---
|
||||
|
||||
## TOP LEAD 2026-06-26 #11 — THE 52 IS A MODEM XPU (SECURE-MEMORY) CRASH-LOOP
|
||||
|
||||
> **🔴 SUPERSEDED 2026-06-29.** Captured on the **pre-DELTA-1 r2 LLVM kernel**, where the modem
|
||||
> genuinely crash-looped on an XPU violation. **DELTA-1 (`c2923cb49` MPSS assign-ordering) fixed the
|
||||
> load** — on the current kernel the modem MSS runs *steady* (MBA boots, MPSS loads, `remoteproc
|
||||
> running`, **no XPU crash-loop, zero −22 on `4080000`**; the only crash-lines are the SLPI's
|
||||
> `SNS_REG_INIT`). Yet it **still parks OFFLINE / model 0 / 52**. So **52 is NOT an XPU crash-loop**
|
||||
> — it's a firmware-internal RF-init decision, kernel-independent (6.16.7 and 7.1-rc1 both give 52,
|
||||
> see #10). The −22 referenced below is the SLPI's fastrpc heap. Kept as the XPU/load investigation
|
||||
> record.
|
||||
|
||||
**Captured live on the rebuilt r2 LLVM kernel (slot A, full system, ssh).** The
|
||||
`DeviceNotReady (52)` wall is now explained at the mechanism level: the modem
|
||||
**boots then crash-loops on a hardware secure-memory protection violation**, so it
|
||||
can never complete RF-init and parks `offline`. dmesg (`qcom-q6v5-mss
|
||||
4080000.remoteproc`, remoteproc3):
|
||||
|
||||
```
|
||||
Booting fw image qcom/sdm845/Google/blueline/mba.mbn ... MBA booted ... loading mpss
|
||||
remote processor 4080000.remoteproc is now up
|
||||
crash #1: fatal error received: SFR Init: wdog or kernel error suspected.
|
||||
crash #2: fatal error received: modem_xpu_int.c:456:[modem_sec_xpu_I]XPU violation
|
||||
... port failed halt ... modem offline ... starting ... up ... crash ... (loop)
|
||||
```
|
||||
|
||||
**XPU = Qualcomm eXtreme Protection Unit** — hardware memory guard programmed by
|
||||
TrustZone. `modem_sec_xpu XPU violation` = the modem firmware accessed a memory
|
||||
region the XPU does not permit it → the modem's **secure memory regions are not
|
||||
assigned/programmed correctly** for mainline. The modem comes up, touches a
|
||||
protected region, XPU faults it, q6v5 recovers it, repeat. The "running" state in
|
||||
a one-shot `remoteproc/state` read is just a momentary up-window between crashes —
|
||||
do not trust a single-sample `running`; watch `journalctl -b -p err` over ~30s.
|
||||
|
||||
**This is the same TZ layer as the SLPI/fastrpc failure.** Every modem AND slpi
|
||||
restart also logs `qcom_scm firmware:scm: Assign memory protection call failed -22`
|
||||
(hyp-assign / `qcom_scm_assign_mem` → -EINVAL). The fastrpc patch *tolerates* it;
|
||||
the modem cannot — it needs its secure regions actually assigned. **Shared root:
|
||||
this blueline's TZ rejects mainline's secure-memory-assign, → modem XPU violation
|
||||
→ 52.** See `secure_load_tz_frontier.md`.
|
||||
|
||||
**Ruled OUT as the cause of 52 (this session, hard evidence):**
|
||||
- *Provisioning / MCFG* — software MCFG `Rogers_Canada` (Casey's real carrier) is
|
||||
`Active` (PDC `--pdc-list-configs=software`). Selecting the carrier config — the
|
||||
thing qcril/Set-Feature does on Android — is already done, and 52 reproduces
|
||||
anyway. Platform configs = 0 (blueline EFS gap) and PDC Set Feature 0x2E times
|
||||
out, but neither is the 52: the modem crashes below the QMI layer.
|
||||
- *Kernel config regression* — 52 reproduces on the corrected 1135-module r2 LLVM
|
||||
kernel with SLPI/SSC in. Not the GCC-stripped-config disaster.
|
||||
- *libqmi* — confirmed the openimsd fork is installed (apk desc: "openimsd fork,
|
||||
adds PDC Set Feature msg 0x2E"; ships `Qmi-1.0.typelib`). Not the blocker.
|
||||
|
||||
**[RETIRED 2026-07-01 — see HANDOFF CORRECTIONS]** ~~The one-per-boot online attempt was
|
||||
spent this boot ... Modem now poisoned until power-cycle.~~ Online attempts are retry-safe
|
||||
within a boot; QMI `Set Operating Mode = RESET (4)` cleanly reinitializes, no reboot needed
|
||||
(`tools/modem-online-provisioner/RESULTS.md`). Kept for history: that boot's attempt gave 52,
|
||||
mode stayed `offline`, serving-system not-registered/detached, signal InformationUnavailable.
|
||||
|
||||
**Next frontier:** make TZ accept the modem's secure-memory assignment — i.e. the
|
||||
XPU/hyp-assign region setup. Compare LOS pil-tz secure-load (mba region
|
||||
`qcom,mba-mem`, mpss region assign, `qcom_scm_assign_mem` VMID lists) vs mainline
|
||||
`q6v5_mpss` region handling on this device. The `assign -22` is the thread to pull.
|
||||
(qmicli also **segfaults** on `--pdc-list-configs` — separate libqmi/fork bug to log.)
|
||||
|
||||
## TOP LEAD 2026-06-21 #10 — KERNEL-REGRESSION HYPOTHESIS: DISPROVEN. Kernel layer ruled out airtight.
|
||||
|
||||
**Result (clean-boot test, 2026-06-21 eve):** built blueline on `sdm845-6.16.7-r0` (last pre-regression
|
||||
stable, 2025-09-19), flashed boot_b, booted clean. The modem boots **fully** on 6.16.7 — firmware
|
||||
loads (`Booting fw image …/mba.mbn` → `remote processor is now up`), ALL QMI services register
|
||||
(qrtr-lookup shows DMS/NAS/UIM/Voice/WDS/PDC…), UIM is healthy (card present, USIM `ready`, Primary-GW
|
||||
slot1/app1), `remoteproc2` = `running`. Yet `--dms-set-operating-mode=online` → **DeviceNotReady (QMI
|
||||
52)**, mode stuck `offline`, Networks `''`, Max TX/RX 0 — **byte-for-byte identical to 7.1rc1.** A
|
||||
second run on a *clean* boot (modem auto-started at boot-time, UIM freshly provisioned) reproduced the
|
||||
same 52, removing the manual-start confound. Per this lead's own triage rule (still-52 on the
|
||||
pre-regression kernel) = **the kernel is ruled out airtight.** The online wall is NOT a kernel
|
||||
regression; it is device-internal (Route B) — the modem's own firmware RF-init never self-completes, so
|
||||
it parks `offline` and refuses `online` (Android never has to ask; see frontier #8 REFRAME).
|
||||
6.16.7 is now a known-good working kernel for blueline (modem boots clean); either 6.16.7 or 7.1rc1 is
|
||||
fine going forward (both give 52). **The live frontier is Route B — see `frontier #8 REFRAME` below.**
|
||||
|
||||
### Firmware-path gotcha found + fixed (RECORD — bites any upstream-tree blueline DTB)
|
||||
The 6.16.7 *upstream* `sdm845-google-blueline.dts` requests firmware at
|
||||
`qcom/sdm845/pixel3/{mba,modem,adsp,cdsp,ipa_fws,a630_zap}.mbn`, but pmaports packages blueline
|
||||
firmware at `qcom/sdm845/Google/blueline/`. So the upstream-tree DTB silently fails `request_firmware`
|
||||
(ENOENT -2) → modem/ADSP/CDSP never boot (`remoteproc: Boot failed: -2`). The 7.1rc1 *pmaports* DTB
|
||||
uses `Google/blueline/` (correct) — which is why 7.1rc1 loaded firmware and 6.16.7 did not until fixed.
|
||||
**Fix applied on the rootfs (persists across reboots):**
|
||||
`ln -snf /lib/firmware/qcom/sdm845/Google/blueline /lib/firmware/qcom/sdm845/pixel3`
|
||||
Any future build using the upstream blueline DTB needs this symlink (or a DT patch correcting
|
||||
firmware-name paths) or the modem will not load firmware. (Stray: the 6.16.7 DTB also references
|
||||
`qcom/sdm845/oneplus6/venus.mbn` for venus — harmless, video-decoder only.)
|
||||
|
||||
### Kernel-swap procedure (RECORD — the modular-modem-stack gotcha)
|
||||
The blueline modem stack is **fully modular** on this config: `qcom_q6v5_mss`, `qrtr`/`qrtr-smd`,
|
||||
`qcom_glink_smem`, `qcom_sysmon`, `mhi*` all `=m`. So swapping kernels needs the matching modules on the
|
||||
rootfs or the modem never loads (a false 52). UFS + USB-gadget-NCM are built-in (`=y`), so a
|
||||
module-mismatched boot still reaches rootfs + USB. Working procedure (validated this session):
|
||||
build kernel .apk → `tar xzf` it → extract `lib/modules/<kver>/` → scp to phone → `sudo cp -a` into
|
||||
`/lib/modules/` + `depmod <kver>` → reboot. Boot.img = `cat vmlinuz <blueline>.dtb` + the standing
|
||||
initramfs, `mkbootimg` (header v0, base 0x0, kernel_off 0x8000, ramdisk_off 0x1000000, tags 0x100, the
|
||||
real cmdline `quiet splash … pmos_boot_uuid=2CE8-836F pmos_root_uuid=26733d78…`). USB iface
|
||||
`enp0s20f0u6` (cdc_ncm) needs manual bring-up each boot: `nmcli dev set … managed no; ip addr add
|
||||
172.16.42.2/24 dev …; ip link set … up`.
|
||||
|
||||
### Original #10 thesis + bisect-window analysis (HISTORICAL — hypothesis disproven above)
|
||||
Thesis (Casey): the 7.1rc1 bump (pmaports MR !8446, merged 2026-04-28, "basic smoke testing… Pixel 3")
|
||||
never validated blueline's modem; the modem may have regressed there. Corroboration: beryllium wiki
|
||||
flags 7.1.0-rc1-sdm845 for modem breakage on the same SoC; Joel Selvaraj had SDM845 modem working on
|
||||
mainline back at 5.11, so the capability predates the regression. **Running test:** rebuild blueline
|
||||
kernel against `sdm845-6.16.7-r0` (last pre-regression stable, 2025-09-19), flash boot_b, retry
|
||||
`--dms-set-operating-mode=online`. Outcomes: online → regression confirmed, bisect the bump; flaky
|
||||
like beryllium → kernel layer validated, inherit beryllium recipe; still DeviceNotReady(52) → kernel
|
||||
ruled out airtight, it's device-internal firmware/EFS state (Route B).
|
||||
|
||||
### Kernel-tree pull (2026-06-21) — what the AP-driver diff says about the bisect window
|
||||
Pulled `references/sdm845-linux-20260610-ONHOLD-DAVID-TREE` (codeberg `sdm845-next-20260610`, base **7.1.0**) and the
|
||||
`sdm845-next-20260306` tarball (base **7.0.0**) and diffed the modem-relevant subsystems:
|
||||
- **`drivers/remoteproc/qcom_q6v5_mss.c`: functionally identical** 7.0→7.1 (brace-whitespace only).
|
||||
- **`drivers/soc/qcom/pdr_interface.c` + `pdr_internal.h`: rename-only** — `SERVREG_LOCATOR_SERVICE`→
|
||||
`QMI_SERVICE_ID_SERVREG_LOC` (both 0x40), `SERVREG_NOTIFIER_SERVICE`→`QMI_SERVICE_ID_SERVREG_NOTIF`
|
||||
(both 0x42), `reason[257]`→`reason[SERVREG_PFR_LENGTH+1]`(=257). No behaviour change.
|
||||
- **`qcom_pd_mapper.c` present in BOTH trees** → the in-kernel pd-mapper is NOT new to 7.1 (already
|
||||
in-kernel by 20260306 / 7.0.0; it landed ~mainline 6.13, so 6.16.7 has it too). The "in-kernel
|
||||
pd-mapper is the regression" sub-hypothesis is weak; the **sdm845-mainline rebase** is the live one.
|
||||
- `qmi_interface.c`, `qmi_encdec.c`, `rmtfs_mem.c`: identical 7.0→7.1.
|
||||
**Implication for the bisect:** in the codeberg 7.0→7.1 window there is NO functional change in
|
||||
mss/pdr/qmi/rmtfs — so if the regression is real and in that window, it is NOT in those drivers
|
||||
(look at clk/regulator/interconnect/scm/smem/glink/IPA instead). **Caveat:** pmaports tracks
|
||||
`gitlab.com/sdm845-mainline/linux`, NOT codeberg `sdm845/linux` (the two repos the Matrix thread
|
||||
argues over), and my window (7.0→7.1) does not cover the 6.16.7→7.0 jump. So this scopes, it does
|
||||
not pin — Casey's 6.16.7 build is still the right swing.
|
||||
|
||||
### Upstream blueline DTS confirms our DT is already correct (kills #9 a second way)
|
||||
The 20260610 tree ships an **in-tree `sdm845-google-blueline.dts` + `sdm845-google-common.dtsi`**
|
||||
(model "Google Pixel 3"). Its modem nodes: `&mss_pil` = `blueline/mba.mbn`+`blueline/modem.mbn`,
|
||||
`&ipa` = `blueline/ipa_fws.mbn`, `mpss memory@8e000000` size `0x9800000`, `rmtfs-region@f2700000`
|
||||
vmid MSS_MSA. **`dt-work/blueline-pmos.dts` already matches all of these byte-for-byte.** Zero
|
||||
WTR/QFE/RFFE/FEM/GRFC regulator nodes in the upstream blueline DTS — same as fajita, same as pmOS.
|
||||
So the pmOS modem DT is upstream-aligned, not a fajita guess. RF power is modem-firmware/RFFE driven
|
||||
(see `dt-work/rf-rail-diff-FINAL.txt`). **Lead #9 (add 11 pm8998 LDOs) is refuted** by (a) the
|
||||
upstream blueline DTS not declaring them either, and (b) all 11 candidate LDOs showing use=0 from the
|
||||
AP on live working Android. Do NOT flash `dt-work/blueline-live.dtb` for this reason.
|
||||
|
||||
### qrild finding folded in — there is no hidden online handshake
|
||||
`references/qrild` (abandoned AOSPM RIL): its modem bring-up (`qrild_android_modem.cc` ctor) is
|
||||
DMS Set Operating Mode RESET → powerup → NAS register-indications, with `setRadioPower` a no-op stub.
|
||||
Same levers ModemManager already pulls — **no secret QMI sequence** that arms RF. This corroborates
|
||||
the #8 REFRAME (`qcril_bringup_capture.md`): on Android the modem firmware SELF-onlines at its own
|
||||
RF-init; replaying a "QCRIL/RIL online handshake" on pmOS is a dead end because there isn't one.
|
||||
|
||||
---
|
||||
|
||||
## (REFUTED 2026-06-21) #9 — pmOS DT under-defines the pm8998 regulator set
|
||||
|
||||
Refuted by #10 above (upstream blueline DTS does not declare these LDOs either; all 11 are use=0 on
|
||||
live Android) and by `dt-work/rf-rail-diff-FINAL.txt` (RF power is firmware/RFFE-driven, not AP-DT).
|
||||
Kept for the record; do NOT flash the LDO DTB. Original reasoning below.
|
||||
|
||||
Direction (Casey + GLM): the fix is in OUR pmOS implementation/DT, make it mimic Android. Concrete,
|
||||
evidenced bug found this session:
|
||||
|
||||
**Our mainline blueline DT instantiates only ~14 of the 24 pm8998 LDOs. Android defines all of them.**
|
||||
Missing from `dt-work/blueline-pmos.dts` rpmh-regulators (present in downstream Android DTB):
|
||||
`ldo4, ldo6, ldo8, ldo9, ldo10, ldo11, ldo15, ldo16, ldo18, ldo23, ldo27`.
|
||||
Voltages (from `dt-work/android-dtb/android-base.dts`, full diff in `dt-work/regulator-diff-android-vs-pmos.txt`):
|
||||
ldo6=1.856V ldo8=1.2V ldo9=1.704V ldo10=1.704V ldo11=1.0V ldo15=1.8V ldo16=2.704V ldo18=2.704V ldo23=3.0V.
|
||||
|
||||
Why this matters even though all 11 are `deviceless`/use=0 in BOTH regulator_summaries: **the modem
|
||||
votes its RF-frontend rails (WTR5975/PA/tuners) directly via RPMh, not the AP regulator framework.**
|
||||
If the AP DT doesn't define a rail in the rpmh-regulators node, it isn't in the Linux RPMh regulator
|
||||
set — so when the modem's RF-init requests it, the vote can't be satisfied/arbitrated. Modem RF-init
|
||||
fails → modem parks in `offline` and refuses LPM/online (DeviceNotReady / silent no-op). This fits
|
||||
every symptom: modem boots (DSP up) but RF never inits; `offline` won't flip; configs don't help.
|
||||
|
||||
Supporting evidence this session:
|
||||
- Modem FATAL crash in pmOS dmesg: `qcom-q6v5-mss: fatal error received: ...Asserted in
|
||||
wlan_vdev.c:_wlan_vdev_create` + `port failed halt` on stop (subsystem not halting cleanly →
|
||||
modem can't flush NV → MCFG selection lost across restart → 0 configs every boot).
|
||||
- Modem comes up `offline` (not LPM like Android); `set low_power`/`online` return rc=0 but mode
|
||||
never changes; PDC autoselect (Set Feature) does NOT persist across modem restart (RIL-layer, not
|
||||
firmware NV).
|
||||
|
||||
### Executable plan (start here next session — do NOT re-derive):
|
||||
1. **Diff our blueline DTS rpmh-regulators block vs UPSTREAM linux-next `sdm845-google-blueline.dts`**
|
||||
(Heidelberg's). Our pmaports kernel build may predate the full regulator set. If upstream already
|
||||
defines all LDOs, bump/patch the kernel DTS; if not, add them.
|
||||
2. **Add the 11 missing LDOs** to the `pm8998-rpmh-regulators` node in the blueline DTS at the Android
|
||||
voltages above; mark the RF-candidate analog rails `regulator-always-on` so they're up at RF-init.
|
||||
3. Build via pmbootstrap (`linux-postmarketos-qcom-sdm845`), flash `boot_b` (see platform.md), boot.
|
||||
4. `dmesg | grep -iE 'regulator|vreg|q6v5|fatal'` — confirm no rail/halt errors; then
|
||||
`--dms-set-operating-mode=online` → `--dms-get-capabilities` Networks non-empty = RF armed.
|
||||
5. **Leverage other devs (Casey): do NOT silo.** Cross-check the SDM845 mainlining wiki, sargo/bonito
|
||||
(Google sister devices) DTS, and the `#sdm845:postmarketos.org` channel for how RF rails are
|
||||
declared. Heidelberg's blueline series on lore.kernel.org is the authoritative reference.
|
||||
|
||||
Artifacts: `dt-work/regulator-diff-android-vs-pmos.txt`, `dt-work/android-dtb/android-base.dts`
|
||||
(decompiled downstream DTB), `dt-work/blueline-pmos.dts`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
The modem is a self-contained DSP (MPSS) running Qualcomm firmware. Linux only loads it, powers
|
||||
it, and talks QMI to it. RF is the modem's own internal state machine — Linux cannot drive RF
|
||||
directly. Read this stack bottom-up; the blocker is at L6.
|
||||
|
||||
## The bring-up stack
|
||||
|
||||
| L | Layer | What it is | Status (2026-06-19) |
|
||||
|---|---|---|---|
|
||||
| L0 | Silicon / RF | SDM845 SoC; **SDR845 + WTR5975** transceivers; pm8005/pm8998 PMICs | fixed HW |
|
||||
| L1 | Firmware load | `mba.mbn` (238 KB) → `modem.mbn` (67 MB MPSS), via remoteproc `q6v5-mss` | healthy — "remote processor is now up" ~12.8 s |
|
||||
| L2 | Power | rpmhpd domains `cx`/`mx`/`mss`; `mss`→`mss.lvl` ARC→`pm8005_s2_level` | healthy — `q6v5` votes `mss` INT_MAX; verified vs Android |
|
||||
| L3 | Transport | QMI over QRTR; `glink-edge` to the modem PID | healthy — all QMI services register |
|
||||
| L4 | Host services | `rmtfs` (EFS), `pd-mapper`, `tqftpserv`, `qrtr-ns` | **CRITICAL 06-19**: rmtfs binary was replaced (69KB→141KB) and config changed from `-r -P -s` to `-P -s`, causing EFS corruption + modem crash + system freeze at "[stack] dump". Original binary at `/usr/bin/rmtfs.orig-20260618` must be restored. **CORRECTION (06-19 #2): `-r` = read-only is WRONG for arming the modem — Android serves EFS read-write and the modem writes modemst2 on boot. Use `-P -s` (read-write). The 02:38 corruption was the wrong binary, not the rw flag.** |
|
||||
| L5 | SIM / UIM | card detect, app select, primary-GW provisioning | healthy — `usim (2) ready`, slot1/app1 |
|
||||
| **L6** | **RF arm** | `DMS Set Operating Mode → online` arms the RF stack | **BLOCKED — DeviceNotReady (QMI 52).** The 06-19 #2 "rmtfs read-only" root-cause is **DISPROVEN (2026-07-02)**: rmtfs runs `-P -s` read-write live (drop-in enforced, all EFS partitions present) and 52 persists. rmtfs ruled out. Real cause is the modem's own boot-time RF-init decision (Route B), not an AP EFS-write path. |
|
||||
| L7 | Data / voice | IPA/rmnet, ModemManager, calls/SMS/data | gated behind L6 |
|
||||
|
||||
**The blocker, precisely:** `DMS Set Operating Mode=online` → **DeviceNotReady (QMI 52)**;
|
||||
`DMS Get Capabilities` → zero channel rates / empty networks = the RF stack never initialized
|
||||
inside the DSP. Everything upstream (L1–L5) is proven healthy. ModemManager reaches `enabling`,
|
||||
hits this, falls back to `disabled`, cycles.
|
||||
|
||||
## Eliminated leads — DO NOT RE-CHASE
|
||||
|
||||
Each killed with proof. The "proof" column is why it's dead, not a hypothesis.
|
||||
|
||||
| Lead | Verdict | Proof |
|
||||
|---|---|---|
|
||||
| rmtfs NV-files / `msm-cros-efs-loader` (serve EFS from file copies) | DEAD ( UPDATED 06-19 ) | **REAL CAUSE FOUND**: rmtfs binary was replaced (69KB→141KB) on 06-19 02:38 and service config changed from `-r -P -s` to `-P -s`. This broke EFS partition access, causing modem crash → "[stack] dump" → system freeze. Fixed by restoring `/usr/bin/rmtfs.orig-20260618` (69192 bytes) and resetting `/etc/systemd/system/rmtfs.service.d/writable.conf` to `-r -P -s`. The NV-files approach is dead; the original partition-mode rmtfs MUST be used. |
|
||||
| `removed-region@88f00000` carveout (was "lead #1") | DEAD | Android iomem: `88f00000-8aafffff : System RAM`. The device whose modem **works** leaves it allocatable. OnePlus-specific quirk. (06-19) |
|
||||
| cmd-db maps `mss.lvl` to wrong PMIC (was "lead #3") | DEAD | Android regulator tree: `4080000.qcom,mss → pm8005_s2_level` (correct rail). cmd-db is the same firmware blob on both OSes. pm8005 absent from pmOS `regulator_summary` is **expected** — mainline drives it via rpmhpd/genpd, not the regulator framework. (06-19) |
|
||||
| `vdd_mss-supply` / SPMI `pm8005@4` in DT | DEAD | Mainline `q6v5-mss` is power-domains-only for SDM845 (no `devm_regulator_get`). `qcom,pm8005` has **no SPMI driver** in mainline → adding `pmic@4/5` panics the kernel. Only `smps1/2` under the rpmh-regulators-2 block is safe. (06-18/19) |
|
||||
| "blueline lacks `soc-qcom-sdm845-modem` dep" | DEAD (stale doc) | Current pmaports pkg is `soc-qcom-modem`; blueline depends on it just like enchilada. Device-pkg deps + firmware manifests are equivalent. (06-19) |
|
||||
| fcc-unlock missing | DEAD | `fcc-unlock` exists nowhere in pmaports; no SDM845 device uses one. Modem is not FCC-locked-waiting. (06-19) |
|
||||
| Modem DT node differs from working device | DEAD | blueline modem node is byte-identical to fajita (same power-domains/clocks/resets); only firmware path + phandles differ. (06-18) |
|
||||
| Storage / EFS calibration is missing | DEAD | rmtfs writable + Android-primed EFS (modem online in Android first) → still DeviceNotReady in pmOS. EFS content is not what's missing. (06-18) |
|
||||
|
||||
**Net:** every kernel / DT / power / packaging layer is proven equal to blueline-**Android**
|
||||
(which runs the modem online, Fido LTE) on the **same silicon**. The blocker is not there.
|
||||
|
||||
## The frontier (REVISED 2026-06-20 #8 — QCRIL does more at bring-up; mainline stack under-initializes the modem)
|
||||
|
||||
Casey's standing thesis (correct, and the through-line of this whole effort): **QCRIL powers and
|
||||
initializes more of the modem than ModemManager does** — and mainline pmOS, as a minimal port,
|
||||
under-powers/under-initializes the board. We are hunting the specific delta. Evidence gathered:
|
||||
|
||||
1. **QCRIL sends a `'UI ready'` / power-on-optimization handshake to the modem that MM never sends.**
|
||||
Android cold-boot capture (`android-live-20260619/deep/radio-boot.txt`): `qcril_qmi_nas_telephony_ready:
|
||||
poweron_opt: send 'UI ready' to modem` (qcril_qmi_nas.cpp:17527), plus an active radio-power
|
||||
state machine (`qcril_qmi_nas_update_radio_power_request_info` → `BOOTUP_RADIO_POWER_OFF`,
|
||||
`qcril_qmi_nas_radio_power_transaction_handler`, state line `telephony LPM | ril NONE | card UP |
|
||||
modem ONLINE`). ModemManager has no equivalent of this poweron_opt / UI-ready signalling.
|
||||
2. **No DT RF node on either OS** (see retracted #6) — so the delta is QMI-level (modem-side
|
||||
init/power), not a Linux regulator we can add. The mainline DT brings up cx/mx/mss + the basic
|
||||
stack; it does NOT replicate QCRIL's QMI bring-up choreography.
|
||||
3. **Provisioning race (real bug, partially validated).** `msm-modem-uim-selection` fails every boot:
|
||||
it fires during the early QRTR window ("node with id 0 not found"), then its primary-GW provisioning
|
||||
ACTIVATE times out → service exits FAILURE → the live `primary-gw-provisioning` session loops
|
||||
`end-with-failure`/`app-reset`. Run manually AFTER boot it SUCCEEDS (`Successfully changed
|
||||
provisioning session`). KEY: with provisioning freshly+cleanly active, `--dms-set-operating-mode=
|
||||
online` **no longer returns DeviceNotReady(52)** (returns rc=0) — though the modem still stays
|
||||
`offline` because PDC configs were 0 that boot. We have never had configs-active AND
|
||||
clean-provisioning AND the QCRIL init handshake all at once.
|
||||
|
||||
4. **QCRIL online path captured (airplane off→on, `android-capture-20260620/qcril-online-capture.txt`):**
|
||||
`qcril_qmi_nas_dms_handle_modem_operating_mode` (qcril_qmi_nas.cpp:18289) → "modem's operating mode
|
||||
needs to be changed" → ril SETTING_OP_MODE → **modem LPM → ONLINE** in ~30ms (a plain DMS
|
||||
set-operating-mode=online). post-transition: `set_bootup_power_optimization_state=2`, ATEL UI-READY,
|
||||
enable networking/sys-info indications. **No hidden RF-power QMI command** — the modem is already
|
||||
powered (LPM) and the flip arms RF. **KEY STATE DIFFERENCE:** Android modem starts in **LPM**
|
||||
(low_power) and does LPM→online; the pmOS modem sits in **`offline`** (deeper state) and
|
||||
offline→online is what fails. So the gate is the combination: (a) MCFG configs loaded, (b)
|
||||
primary-GW provisioning active, (c) the modem in LPM (not offline) for the online flip. We have
|
||||
NEVER had all three at once on pmOS — that is the decisive untested experiment.
|
||||
|
||||
**The decisive instrument (next):** a QMI-message-level capture of QCRIL's bring-up on the rooted
|
||||
Android (enable QMI verbose radio logging, cold boot or airplane-toggle), then diff the QMI request
|
||||
sequence against ModemManager's (we already have MM's via `--debug`). The delta = exactly what to
|
||||
replay on pmOS (UI-ready/poweron_opt, any NAS/DMS/RF config QCRIL issues). Then replay on pmOS with
|
||||
configs active + provisioning clean. Rooted Android (slot B) is the instrument; keep this doc current
|
||||
as the capture lands.
|
||||
|
||||
---
|
||||
|
||||
## The frontier (REVISED 2026-06-20 #7 — live RF-init trace: blocker reframed to primary-GW provisioning, NOT RF cal)
|
||||
|
||||
Ran the decisive pmOS-side RF-init trace (slot B, kernel 7.1.0-rc1, modem over qrtr://0). Two
|
||||
big results — one closes a thread, one opens a sharper one.
|
||||
|
||||
**The modem is not faulting and nothing it requests is missing.** dmesg: clean boot, mba→mpss→
|
||||
running→handover, NO crash/coredump/fatal in the RF-arm window; an `online` attempt produces ZERO
|
||||
dmesg delta. tqftpserv serves only the readonly MBN tree (read OK) + an empty `mcfg.tmp` scratch
|
||||
file the modem RRQ/WRQ-loops on — and `mcfg.tmp` being 0 bytes is NORMAL (tqftpserv is bind-mounted
|
||||
to the **persistent** persist partition `/dev/sdf2[/rfs]` — NOT volatile tmpfs as first feared —
|
||||
and persist is SHARED with Android, so if the working modem needed content there it would persist;
|
||||
it's empty on both). So serving is correct and complete. **The modem boots healthy and simply
|
||||
REFUSES `online` at the QMI layer (DeviceNotReady = clean refusal, not a load failure).**
|
||||
|
||||
**NEW lead — primary-GW SIM provisioning session fails.** ModemManager's log during the refusal:
|
||||
`refresh indication received: session type 'primary-gw-provisioning', stage 'end-with-failure',
|
||||
mode 'app-reset'` — looping every MM retry (~72s), alongside a NAS system-info indication that
|
||||
returns `FAILURE: InformationUnavailable`. Operating mode `offline`, **HW restricted `no`** (not
|
||||
FCC/HW-locked), serving system fully detached, radio interface `none`. AND `msm-modem-uim-selection`
|
||||
**failed this boot**: "Application was already selected" → `operation failed: Transaction timed out`
|
||||
→ service exit FAILURE. Yet the static card read is healthy: Primary GW bound (slot1/app1), USIM
|
||||
app `ready`, personalization `ready`, PIN1 `disabled`. So the binding exists but the live
|
||||
provisioning SESSION keeps failing + app-resetting. This is distinct from (and more proximate than)
|
||||
the RF-cal lead — the modem may refuse `online` because primary-GW subscription provisioning never
|
||||
completes, not because RF is uncalibrated. (mBands=[] is non-diagnostic per #6; generic IMEI works
|
||||
on Android per Casey — so cal/QCN lead is weak.)
|
||||
|
||||
**OPEN — causality not yet proven.** Why does the provisioning session end-with-failure while the
|
||||
GW binding shows bound+ready? Candidates: (a) `msm-modem-uim-selection` timing out leaves the
|
||||
subscription half-selected; (b) the modem triggers an app-reset refresh (post-MCFG) that nothing
|
||||
on the AP side completes the way Android's RIL does; (c) downstream symptom of offline state.
|
||||
**Decisive next test (fresh boot, one clean shot):** on a fresh slot-B boot, before MM enables,
|
||||
(1) confirm whether the primary-gw-provisioning refresh fails on its own; (2) re-run UIM selection
|
||||
cleanly (the service timed out) and retry `online`; (3) capture the UIM/provisioning QMI sequence.
|
||||
Compare to what Android's RIL does on the same refresh. Known: a remoteproc reset (online is retry-safe, see CORRECTIONS)
|
||||
both clear UIM provisioning, so this needs a clean boot.
|
||||
|
||||
---
|
||||
|
||||
## The frontier (REVISED 2026-06-20 #5 — MCFG SOLVED; online blocker isolated to device-specific RF bring-up)
|
||||
|
||||
Live pmOS run this session SOLVED the MCFG provisioning blocker AND, by doing so, proved MCFG was
|
||||
never the RF-arm blocker. The online wall (`DeviceNotReady` QMI 52) survives every config/stack/SIM
|
||||
fix. Full elimination, all on hardware (slot B, kernel 7.1.0-rc1-sdm845):
|
||||
|
||||
**SOLVED — persistent MCFG self-provisioning.** `qcril-autoselect.py` (PDC Set Feature 0x2E,
|
||||
Selection Mode=3) made the modem self-load 2 platform (SS-LA-SDM845) + 25 software (Rogers_Canada)
|
||||
configs from the tqftpserv tree; survives reboot (self-loads at RF-init). Both ACTIVE; software
|
||||
version `0x801481A` == Android's exact value. The "0/0 every cold boot" blocker (frontier #3/#4)
|
||||
is dead. Tooling: `tools/qcril-autoselect.py` + `tools/build-openimsd-libqmi.sh` (openimsd libqmi
|
||||
fork, has Set Feature). Believed a Pixel 3 mainline first.
|
||||
|
||||
**RULED OUT as the online (RF-arm) blocker — every one tested on hardware:**
|
||||
- MCFG/PDC — both configs ACTIVE, matching Android. online still 52.
|
||||
- UIM/SIM — card present, USIM app `ready`, Primary GW slot1 app1 provisioned.
|
||||
- timing/lock — MM stopped, 10 patient online retries over 30s, all 52.
|
||||
- ModemManager — its full 9-step enable (power-state → op-mode indication register → Set Operating
|
||||
Mode 0x2E=online) returns the same 52. Not a manual-qmicli artifact.
|
||||
- missing stack — pd-mapper, hexagonrpcd, q6voiced, rmtfs (`-P -s` rw), tqftpserv ALL running;
|
||||
`soc-qcom-modem` installed; PD JSONs (modemr.jsn/modemuw.jsn) + firmware present; no errors.
|
||||
Matches working SDM845 devices (enchilada/fajita) component-for-component.
|
||||
- EFS not served — modemst1/2, fsg, fsc all populated (~260K nonzero) + served. (modemcal sdd4 is
|
||||
all-zero; persist sdf2 has data.)
|
||||
- firmware mismatch — modem sw version `g845-00194-210812-B-7635520` IDENTICAL to Android;
|
||||
revision `MPSS.AT.4.0.c2.17-00194-SDM845_GEN_PACK`.
|
||||
|
||||
**THE REMAINING BLOCKER = device-specific RF bring-up.** Same firmware + same shared EFS + complete
|
||||
stack + DT byte-identical to fajita, yet: NAS `get-system-info` empty, `nas-get-rf-band-info` →
|
||||
no bands, mode stuck offline, `DeviceNotReady` on online. The modem boots but its RF never inits.
|
||||
Two leads:
|
||||
1. **RF cal / QCN** — generic test IMEI `990012001581294`, model `0`, no bands. Classic
|
||||
no-calibration signature. UNCONFIRMED: does the WORKING Android show the REAL IMEI (→ pmOS isn't
|
||||
loading cal/QCN) or the same generic one (→ red herring)? Decisive next test: reboot slot A,
|
||||
read IMEI + DMS caps + bands; compare. (Not done autonomously — avoids leaving the phone
|
||||
off-pmOS while unattended.)
|
||||
2. **Board RF-frontend** — fajita (OnePlus 6T) works on mainline; blueline (Pixel 3) shares the
|
||||
q6v5-mss DT node but has a different PMIC/RF frontend (WTR/QFE/antenna tuners). If blueline's RF
|
||||
frontend needs power/GPIO/cal mainline doesn't provide for THIS board, the modem can't arm RF
|
||||
even with identical firmware. (Distinct from the DEAD pm8005/mss-rail lead — this is the RF
|
||||
frontend, not the modem-subsystem rail.)
|
||||
|
||||
### 2026-06-20 #6 — RETRACTED: do NOT treat the board-power lead as closed
|
||||
I previously wrote this section as "lead #2 CLOSED with positive proof." That was an overclaim and
|
||||
Casey was right to reject it. What the regulator/DT evidence below actually shows is narrower: there
|
||||
is no *labeled RF-frontend regulator node* in the device tree — confirmed on BOTH mainline AND the
|
||||
**downstream Android DTB** (`dt-work/android-dtb/android-base.dts`, decompiled from slot-A boot_a;
|
||||
its `qcom,mss@4080000` declares only vdd_cx/vdd_mx/vdd_mss, no RF/WTR/antenna node anywhere). That
|
||||
means we can't fix this by copying an "RF rail" node — NOT that board power is irrelevant. Mainline
|
||||
blueline is a **minimal fresh port**: it instantiates only rails with explicit consumers and leaves
|
||||
every other board rail off, whereas the downstream stack + QCRIL bring up far more (subsystems,
|
||||
power-on handshakes, NV/RF init via QMI). The live lead remains: **what does the modem need powered/
|
||||
initialized — via Linux rails the mainline port omits, or via QMI commands QCRIL sends — that pmOS
|
||||
does not provide.** See the #8 frontier (top of file) for the QCRIL-does-more evidence.
|
||||
|
||||
(original text retained below for the evidence it contains)
|
||||
|
||||
Brought the "power Android provides" thread full circle on live hardware (Android slot A, rooted):
|
||||
|
||||
- **No Linux-side RF rail exists on EITHER OS.** In both the Android (`rf-compare/regulators-android.txt`)
|
||||
and pmOS (`regulators-pmos.txt`) regulator dumps, the modem `4080000.qcom,mss` consumes ONLY the
|
||||
Q6 DSP level rails (s6_level=CX, s9_level=MX, s2_level=MSS). There is no WTR/PA/tuner regulator
|
||||
in Linux on Android either — RF-frontend power is driven by the modem firmware over RFFE + GRFC,
|
||||
not the Linux regulator framework. So a "missing regulator in the pmOS DT" was never possible.
|
||||
- **DT modem node is correct + complete** (`dt-work/blueline-pmos.dts`): power-domains cx/mx/mss,
|
||||
xo/bus/mem clocks, glink "modem" transport, `gpio-reserved-ranges = <0 4, 81 4>` (Linux leaves
|
||||
the modem-owned GPIOs alone). No regulator-supply phandles — correct for mainline q6v5-mss.
|
||||
- **Pinctrl: no AP/modem GPIO conflict.** Android TLMM (`3400000.pinctrl`) owns only camera-CCI,
|
||||
gpio-keys, and USB-extcon pins; the modem's GRFC/RF-control pins are MSS-internal and not
|
||||
AP-routed. The AP cannot be stepping on an RF pin.
|
||||
- **"No bands" is NOT a distinguishing signal.** The fresh working-Android radio capture
|
||||
(`android-capture-20260620/android-radio-log.txt`) shows `mBands=[]` in CellIdentityLte while
|
||||
fully attached to Fido LTE (REG_HOME, EARFCN 2000 = B4). So the empty mBands we saw on pmOS does
|
||||
not by itself mean RF is dead — weakens (does not kill) lead #1's reasoning.
|
||||
|
||||
**Verdict:** every AP-side power/GPIO/pinctrl/DT input the modem receives is provably equal to
|
||||
blueline-Android (which arms RF on the same silicon). Lead #2 is closed the same way IMEI closed
|
||||
lead #1. The blocker lives strictly inside the modem firmware's RF-init, which the AP only feeds
|
||||
via: firmware blobs, EFS (rmtfs), persist/rfs (tqftpserv), power-domains, clocks, QMI. All proven
|
||||
present/equal — EXCEPT we have never traced what the modem *requests* from rmtfs/tqftpserv during
|
||||
its RF-init, nor captured the modem's own failure reason.
|
||||
|
||||
**Decisive next experiment (pmOS-side, not yet run):** boot slot A, attempt `online`, and capture
|
||||
(a) `tqftpserv`/`rmtfs` request trace during RF-init — which file/NV path the modem asks for and
|
||||
whether it's served (frontier #4 showed Android rmt_storage *reads* on boot; we never logged the
|
||||
pmOS read side), and (b) the modem remoteproc coredump / SSR reason on the failed online. This is
|
||||
the one byte-for-byte trace we still lack; it points at the specific RF-init resource that differs.
|
||||
|
||||
Route B (autoselect EFS NV item) is DEPRIORITIZED: it would only persist the config state we
|
||||
already achieved, and config-active does not fix online.
|
||||
|
||||
---
|
||||
|
||||
## The frontier (REVISED 2026-06-20 #4 — Android boot is VERIFY-ONLY; the modem persists MCFG itself)
|
||||
|
||||
Source: analysis of the captured Android radio logs in `android-live-20260619/deep/`
|
||||
(`radio-boot.txt`, `radio-full.txt`, `ssr-capture.txt`), 2026-06-20. This **overturns** both
|
||||
frontier #3's "just make the platform config active" and the `qcril_full_provisioning.md §D.4`
|
||||
"load+activate every boot" model. Read this before either.
|
||||
|
||||
### What the captures actually show
|
||||
|
||||
Across all three logs (~12K PDC log lines, including a full modem SSR) QCRIL made **only query
|
||||
calls** — never load/select/activate, never `pdc_enable_auto_selection`:
|
||||
|
||||
- `ssr-capture.txt:24293` — the decision point:
|
||||
`qcril_qmi_pdc_is_modem_mbn_updated: prev_ver_info: MCFG-g845-00194-220517-B-8604334,
|
||||
cur_ver_info: MCFG-g845-00194-220517-B-8604334` → **prev == cur → reload SKIPPED.**
|
||||
- `ssr-capture.txt:23949` — `get_active_config_info_ind: config_version = 0x801481a`,
|
||||
`Update CNV from 0x48 to 0x48` (0x48 = **72** = `cnv=[72]` = `persist.vendor.radio.mbn0=72`).
|
||||
- Only PDC functions logged anywhere: `get_active_config_info`, `get_selected_mbn_config`,
|
||||
`get_selection_carrier`, `is_modem_mbn_updated`. **Zero** `*_load_config`, `*_select_config`,
|
||||
`*_activate_config`, `enable_auto_selection`, `enable_modem_update`.
|
||||
|
||||
### What that means
|
||||
|
||||
**Android's steady-state boot does almost nothing to PDC.** The modem holds its MCFG persisted in
|
||||
its own EFS (`modemst1/2` — the 2 MB write to `modem_fs2` the `rmt_storage` capture caught),
|
||||
**self-applies it at firmware RF-init every boot**, and QCRIL just confirms the active version
|
||||
matches `persist.vendor.radio.cnv.ver_info` and stops. The full load→select→activate path runs
|
||||
**only on first provision or an MCFG version change** (the `prev_ver != cur_ver` branch) — which
|
||||
we have **never captured**, because every capture was an already-provisioned device. That is
|
||||
exactly why `pdc_enable_auto_selection` never appears: it lives on the unobserved path.
|
||||
|
||||
### Consequences for the two prior theories
|
||||
|
||||
- **Frontier #3 ("just need an active platform config")** — necessary but NOT sufficient. PROGRESS
|
||||
L165–172 had BOTH configs active (SS-LA-SDM845 + Rogers) and DMS Networks stayed empty. Active
|
||||
≠ armed. Don't chase "make it active" as the finish line.
|
||||
- **`§D.4` ("load+activate every boot, accept two restarts")** — that is a *reconstruction of the
|
||||
unobserved first-provision path*, not what Android does in steady state. It remains a valid
|
||||
pmOS *workaround* (see `tools/qcril-prov.py`), but it is unproven on hardware and is not the
|
||||
mechanism that actually arms RF on Android.
|
||||
|
||||
### The pmOS problem, restated from the evidence
|
||||
|
||||
The "0/0 configs every cold boot" volatility (PROGRESS L140–144) is **not** something QCRIL fixes
|
||||
per-boot. It's that **the modem cannot persist/restore its PDC selection in EFS the way Android
|
||||
does.** The earlier "rmtfs read-write didn't fix it" result (ledger L38) is a chicken-and-egg:
|
||||
writable EFS is useless if the modem never had a valid MCFG to commit, and a one-shot userspace
|
||||
activate is useless if it cannot persist to survive the next firmware RF-init. **Both are
|
||||
required: a valid config loaded AND modem-side persistence that survives reboot.**
|
||||
|
||||
### Two candidate paths forward
|
||||
|
||||
- **(A) Per-boot provisioner workaround** — `tools/qcril-prov.py` loads HW+SW, selects, activates
|
||||
each boot. Mirrors the reconstructed first-provision path. Unproven; "active ≠ armed" is the
|
||||
open risk.
|
||||
- **(B) Provision-once + persistence (the Android mechanism)** — get a valid MCFG into the modem
|
||||
once, then ensure `modemst` persistence so the modem self-restores + self-applies at RF-init,
|
||||
and userspace reverts to verify-only. This is what the captures show actually works on hardware.
|
||||
|
||||
### The capture we still need (next experiment)
|
||||
|
||||
A normal-boot capture only re-shows the skip path. To record the **real first-provision load
|
||||
sequence** (and settle whether `pdc_enable_auto_selection` / `enable_modem_update` is a required
|
||||
precondition), **force a first-provision on Android**: clear `/data/vendor/modem_config/` and
|
||||
`persist.vendor.radio.cnv.ver_info`, reboot, then capture. Runbook:
|
||||
`PAF/first_provision_capture.md`.
|
||||
|
||||
---
|
||||
|
||||
## The frontier (SUPERSEDED 2026-06-19 #3 — see #4 above — PDC platform config is the blocker)
|
||||
|
||||
After a full day of Android-vs-pmOS forensics, the blocker is identified:
|
||||
|
||||
### What's actually wrong
|
||||
|
||||
The modem's **PDC (Policy Decision Control)** stores MBN configs in modem NV:
|
||||
|
||||
- **25 software (carrier) configs** loaded — **Rogers_Canada is ACTIVE** (133KB, v0x801481A)
|
||||
- **2 platform (hardware) configs** loaded — **SS-LA-SDM845 is INACTIVE** (56KB)
|
||||
|
||||
Without an **active platform config**, the modem doesn't know its RF hardware capabilities.
|
||||
DMS reports `Networks: ''`, NAS home network returns `NotProvisioned (QMI 16)`, and
|
||||
`Set Operating Mode=online` returns `DeviceNotReady (QMI 52)`.
|
||||
|
||||
On Android, QCRIL (`libril-qc-hal-qmi.so`) loads MBN files from `/vendor/rfs/msm/mpss/readonly/vendor/mbn/`
|
||||
via QMI PDC, activates both platform and software configs, and the modem restarts with full RF.
|
||||
|
||||
ModemManager does none of this — it assumes the modem is pre-provisioned.
|
||||
|
||||
### Attempted: direct PDC activation
|
||||
|
||||
`qmicli --pdc-activate-config=platform,6FE571A9...` returned **"Successfully requested config
|
||||
activation"** but triggered a modem restart that **cleared the platform config** (0 configs after
|
||||
restart). The activation request succeeded at the QMI level but the modem's internal handling
|
||||
removed the config during restart. Needs Android QCRIL to re-load it.
|
||||
|
||||
`qmicli --pdc-load-config=...` **segfaults** on MBN binary files (qmicli 1.39.0 / libqmi 1.38.0).
|
||||
Cannot load MBN files from pmOS side — libqmi bug or MBN format incompatibility.
|
||||
|
||||
### Key facts confirmed (06-19)
|
||||
|
||||
- **IMEI: identical** on Android and pmOS (`990012001581294`). NOT the blocker. Android works on
|
||||
Fido LTE with this same test IMEI.
|
||||
- **Modem firmware: identical** SHA256 (`97c1d19c...`) between slots A and B.
|
||||
- **Modem crashes** (SFR Init / XPU violation): **intermittent** — some boots 0 crashes, some 2.
|
||||
Even clean boots get DeviceNotReady. Not the blocker.
|
||||
- **rmtfs DOES serve correct partitions**: partlabels are GPT-stable; UFS LUN renumbering changes
|
||||
`/dev/sdX` names between boots but `by-partlabel` symlinks always resolve correctly.
|
||||
- **DMS Get Capabilities** consistently shows `Networks: ''` and `Max TX/RX: 0` on pmOS.
|
||||
Android shows populated networks (Fido LTE, Rogers 302720, full band support).
|
||||
- **NAS Get System Selection Preference** shows FULL band tables on both OSes (43 LTE bands +
|
||||
extended). NAS has the data; DMS doesn't. The bridge is the platform MBN activation.
|
||||
- **Android rmt_storage**: reads EFS on boot (no writes), writes 2MB to modem_fs2 AFTER going
|
||||
online. Normal operation — not initial provisioning.
|
||||
- **pmOS rmtfs journal**: empty (rmtfs doesn't log successful reads/writes). No errors either.
|
||||
|
||||
### Fix path
|
||||
|
||||
1. Boot Android to let QCRIL re-load the platform MBN into PDC (it was there before activation)
|
||||
2. Boot pmOS — do NOT touch PDC, do NOT run `qmicli` activation
|
||||
3. Verify: `qmicli --pdc-list-configs=platform` shows SS-LA-SDM845 as Active
|
||||
4. Verify: `qmicli --dms-get-capabilities` shows `Networks: 'gsm, wcdma, lte'`
|
||||
5. Let ModemManager drive enable→online (NEVER manual qmicli online)
|
||||
6. If platform config is still Inactive after Android: need to fix `qmicli --pdc-load-config`
|
||||
segfault, or write a minimal MBN loader using libqmi directly
|
||||
|
||||
### What we have on device
|
||||
|
||||
- rmtfs: read-write (`-P -s`), serving correct EFS partitions via partlabels
|
||||
- tqftpserv: bind-mounted to `/mnt/vendor/persist/rfs` (persist partition)
|
||||
- pd-mapper: installed and running from boot
|
||||
- MBN files: 179 files (11MB) at `/mnt/vendor/persist/rfs/readonly/vendor/mbn/`
|
||||
(extracted from Android `/vendor/rfs/msm/mpss/readonly/vendor/mbn/`)
|
||||
- MCFG version: `g845-00194-220517-B-8604334`
|
||||
- Rogers Canada MBN: `mcfg_sw/generic/NA/Rogers/Commercial/CA/mcfg_sw.mbn`
|
||||
- SDM845 LA/SS platform MBN: `mcfg_hw/generic/common/SDM845/LA/SS/mcfg_hw.mbn` (56KB)
|
||||
- Kernel: building from `sdm845/7.1-dev` HEAD (`85f1df2a`), config fixed to `sdm845`
|
||||
|
||||
### Android vs pmOS service comparison
|
||||
|
||||
| Component | Android | pmOS | Notes |
|
||||
|-----------|---------|------|-------|
|
||||
| EFS storage | rmt_storage r/w | rmtfs `-P -s` r/w | Parity |
|
||||
| TFTP | tftp_server → vendor/rfs | tqftpserv → persist bind | Parity |
|
||||
| pd-mapper | running (maps PDs) | running | Parity |
|
||||
| RIL | QCRIL (`libril-qc-hal-qmi.so`) | ModemManager | **Missing PDC provisioning** |
|
||||
| ipacm | running (`/dev/ipa` FDs) | not installed | Data path only |
|
||||
| Keymaster | decrypts persist blobs | none | IMEI encrypted at rest |
|
||||
| QMI services | comparable (50+) | comparable (50+) | Parity |
|
||||
|
||||
### (superseded) earlier frontiers
|
||||
|
||||
**Read-only rmtfs theory (06-19 #2):** rmtfs read-only prevented modem from persisting EFS
|
||||
writes. Fixing to read-write didn't resolve DeviceNotReady. EFS r/w is necessary but not
|
||||
sufficient — the modem needs valid MCFG in EFS first, which comes from PDC provisioning.
|
||||
|
||||
**QMI-handshake theory (original):** "MM doesn't send QCRIL's init sequence." Dead —
|
||||
same MM binary works on OnePlus 6. The missing sequence is PDC MBN loading, not a
|
||||
handshake gap. Other SDM845 devices likely have platform config pre-loaded from factory.
|
||||
|
||||
**fsg_oem theory:** Modem requesting `modem_fsg_oem_1/2`. Patched rmtfs to serve them.
|
||||
Modem never actually requests these when EFS is properly provisioned. Symptom, not cause.
|
||||
|
||||
**XPU violation theory:** Modem crashes twice on some boots. Even on clean (0-crash) boots,
|
||||
DeviceNotReady persists. Not the blocker.
|
||||
|
||||
## Useful commands (pmOS)
|
||||
|
||||
```sh
|
||||
qrtr-lookup | awk '$1==11' # UIM service → note Port
|
||||
qmicli -p -d qrtr://0/<port> --uim-get-card-status
|
||||
qmicli -d qrtr://0 --dms-get-operating-mode
|
||||
qmicli -d qrtr://0 --dms-set-operating-mode=online
|
||||
qmicli -d qrtr://0 --dms-get-capabilities # zero rates = RF uninit
|
||||
# clean modem re-init (rmtfs runs -P -s read-write; see 2026-07-02 correction):
|
||||
echo stop | sudo tee /sys/class/remoteproc/remoteproc2/state ; sleep 3
|
||||
echo start | sudo tee /sys/class/remoteproc/remoteproc2/state
|
||||
# preferred: QMI Set Operating Mode = RESET (4) reinitializes without touching UIM/provisioning.
|
||||
```
|
||||
|
||||
Forcing a remoteproc reset clears UIM provisioning; `msm-modem-uim-selection` only re-runs at
|
||||
boot. (The old note here claimed `-r -P -s` read-only was required for re-provisioning — stale
|
||||
and wrong; rmtfs runs `-P -s` read-write live and the modem re-provisions fine across a
|
||||
stop→start. For a clean reinit that does NOT drop provisioning, use QMI RESET, not remoteproc.)
|
||||
|
||||
## Cross-references worth mining
|
||||
|
||||
- **Working SDM845 (same SoC):** OnePlus 6/6T (enchilada/fajita), Poco F1 (beryllium),
|
||||
SHIFT6mq (axolotl) — data + SMS work via the same mainline kernel + pmaports packages.
|
||||
Their *AP-side* config is already in `~/pmos/pmaports`; nothing to pull. Their working
|
||||
firmware/EFS is device-specific and not transplantable.
|
||||
- **Pixel 3a (sargo) / 3a XL (bonito):** the closest *Google* analogs with modem support in
|
||||
pmaports (SDM670). Best target if a Google-specific RIL quirk is suspected.
|
||||
- **Android on this exact device** is the cleanest reference: same silicon, modem works.
|
||||
Diff blueline-Android vs blueline-pmOS, not against OnePlus.
|
||||
|
||||
## TOP LEAD 2026-07-02 #16 — locally-initiated DIAG_CTRL/DIAG_DATA open CRASHES the modem (glink_channel_migration assertion), repeatably
|
||||
|
||||
**Real, causal, previously-unknown finding — not a theory.** Following up #15's ack-but-no-reciprocal-
|
||||
open result: tried the same locally-initiated open against `DIAG_CTRL` (not just `DIAG_DATA`).
|
||||
Confirmed via `dmesg -T` (not my own client's stderr, which kept getting lost across SSH session
|
||||
churn — the KERNEL'S OWN log line is the ground truth here): `rpmsg rpmsg0: failed to open DIAG_CTRL`
|
||||
fires cleanly at the expected 5s mainline timeout, exactly as designed. But roughly 18-40 seconds
|
||||
LATER, every single time this was attempted (three separate occasions, two of them minutes apart on
|
||||
different modem boots), the modem hard-crashes:
|
||||
```
|
||||
qcom-q6v5-mss 4080000.remoteproc: fatal error received: [disable_ssr]glink_channel_migration.c:602:
|
||||
[IST336]Assertion status == GLINK_STATUS_SUCCESS failed
|
||||
remoteproc remoteproc3: crash detected in 4080000.remoteproc: type fatal error
|
||||
remoteproc remoteproc3: handling crash #N in 4080000.remoteproc
|
||||
```
|
||||
This is a **repeatable causal chain**, confirmed across crashes #5 and #6 in the same dmesg buffer,
|
||||
both immediately following a `failed to open DIAG_CTRL` line, both with the identical
|
||||
`glink_channel_migration.c:602` assertion. `glink_channel_migration.c` is the FIRMWARE's own glink
|
||||
channel-transport-migration logic (this is a modem-firmware-side log line, reported via SFR/fatal
|
||||
error, not an AP-side kernel message) — meaning the incomplete/abandoned local-open handshake left
|
||||
channel/migration bookkeeping in a state the modem's own firmware asserts on and can't tolerate.
|
||||
|
||||
**Practical, urgent implication**: DO NOT locally-initiate an open for `DIAG_DATA` or `DIAG_CTRL`
|
||||
casually or repeatedly during further investigation — it reliably crashes the whole modem MSS a
|
||||
short time later, disrupting whatever else was being tested (this crashed WLAN's firmware too, via
|
||||
the shared XPU/hyp-assign path, matching TOP LEAD #11's earlier finding about shared secure-memory
|
||||
fragility). If this path is pursued further, it needs to be done RARELY, with full crash-recovery
|
||||
tolerance, ideally on a boot with nothing else running that a modem SSR would disrupt.
|
||||
|
||||
**What this actually tells us, functionally**: the modem's glink/migration layer is NOT robust to an
|
||||
AP-initiated open attempt that times out — downstream never hits this because (a) it doesn't time out
|
||||
in 5s (event-driven, no hard deadline, per #15's downstream comparison) and/or (b) Android's DIAG
|
||||
daemon presumably completes successfully before any abandonment would occur. This is now a second,
|
||||
independent data point (alongside offline-vs-LPM, #14) that mainline's bring-up leaves this modem's
|
||||
firmware in a MORE FRAGILE state than Android's does — not just "missing a feature" but "structurally
|
||||
less tolerant of imperfect AP-side timing/sequencing." Worth weighing against #14's LPM finding: both
|
||||
point at the same underlying idea -- whatever Android's fuller bring-up sequence does before touching
|
||||
these channels, mainline's minimal sequence skips, and the modem firmware was never hardened against
|
||||
being touched without it.
|
||||
|
||||
**Honest gap, not yet resolved**: unclear if the crash is specific to DIAG_CTRL/DATA's particular
|
||||
migration-eligible status, or would happen for ANY locally-initiated open of ANY channel abandoned via
|
||||
timeout. Worth testing later (carefully, expecting a crash) against a channel we know is NOT normally
|
||||
migration-sensitive, to isolate whether this is DIAG-specific or a general "don't abandon a local
|
||||
open" hazard on this modem.
|
||||
123
saf/device/platform.md
Normal file
123
saf/device/platform.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
# PAF · Platform — hardware, boot, storage, recovery (Arch era)
|
||||
|
||||
Load-bearing device facts. Add a line when something costs more than 2
|
||||
minutes to relearn. pmOS-era snapshot preserved at
|
||||
`evidence/platform-pmos-era.md` — its Alpine/ash and pmOS-slot notes no
|
||||
longer apply.
|
||||
|
||||
## Reaching the phone
|
||||
|
||||
- USB (primary): `ssh -F /dev/null -i ~/.ssh/ani -o BatchMode=yes casey@172.16.42.1`
|
||||
(NCM gadget, phone serves DHCP; laptop autoconfigures).
|
||||
- WiFi: DHCP on the LAN, drifts — check the router or `nmcli` on-device via USB first.
|
||||
- User `casey`, passwordless-sudo NO — sudo password `147147`
|
||||
(`echo 147147 | sudo -S <cmd>`). **Never** stream sudo-gated output through
|
||||
further pipes over SSH (silent truncation, repeatedly) — write to a file on
|
||||
the phone, pull it separately.
|
||||
- Shell is **bash on Arch** (systemd, GNU coreutils, pacman). The pmOS-era
|
||||
busybox/ash caveats are dead.
|
||||
- Serial fallback: ACM gadget → laptop-side `ttyACM0`; getty enabled on ttyGS0.
|
||||
|
||||
## Slots & boot architecture
|
||||
|
||||
- **Slot A = Arch (ours). Slot B = LineageOS (Android, rooted, on the 4.9
|
||||
vendor kernel).** Slot B is the living downstream reference AND the
|
||||
fallback OS — never touch `boot_b` or Android's partitions.
|
||||
- Boot chain: ABL boots `boot_a` (Android bootimg v0, our kernel Image.gz
|
||||
with **appended DTB** + the pmOS-chain ramdisk — see build.md). Root is
|
||||
found by UUID from the baked cmdline.
|
||||
- `qbootctl` (in the device package since ef21637) marks the slot successful
|
||||
and provides reboot-to-android; closes the A/B retry-lockout that used to
|
||||
eat boots.
|
||||
- Switch to Android: `sudo reboot bootloader` → `fastboot --set-active=b` →
|
||||
`fastboot reboot` (and back with `--set-active=a`).
|
||||
|
||||
## Storage (live table, verified on-device 2026-07-10)
|
||||
|
||||
UFS `/dev/sda`, GPT, **4096-byte sectors** (sgdisk needs `-a 256`; a tool
|
||||
assuming 512B sectors will misread the table).
|
||||
|
||||
| Part | Size | FS | Name | Role |
|
||||
|---|---|---|---|---|
|
||||
| sda21 | 42.9G | — | userdata | **Android's, FBE-encrypted, NEVER TOUCH** (near-formatted once — always verify identity before writing any partition) |
|
||||
| sda22 | 10.0G | fat32 | shared | Arch↔Android shared data, mounted /shared |
|
||||
| sda23 | 253M | vfat | Arch_boot | staging/aux boot filesystem |
|
||||
| sda24 | 66.2G | ext4 | Arch_root | the Arch rootfs, `UUID=a094e031-ea89-4847-a299-082a0ae8c9ab` |
|
||||
|
||||
`persist` lives on a **different UFS LUN (sdf2)** — physically immune to sda
|
||||
operations; mounted ro,nofail. Backup in `../blobs/` +
|
||||
`~/pixel3-backup/persist.img`.
|
||||
|
||||
- Boot-image A/B slots (`boot_a`/`boot_b`, 67MB each, partitions 11/12) are
|
||||
separate from the rootfs partitions above — `fastboot flash boot_a` is the
|
||||
only flash operation this project performs.
|
||||
|
||||
## Recovery
|
||||
|
||||
- TWRP: `fastboot boot ~/Downloads/twrp-3.7.1_12-0-blueline.img` (boot, not
|
||||
flash). Used for repartition-class work; a live pmOS/Arch root can't
|
||||
modify its own mounted partitions.
|
||||
- Known-good fallback kernel image (stmfts touch, pre-SWIRE display):
|
||||
`../staging/deploy-139aa/boot-139aa7748181-pmoschain.img` → flash to
|
||||
boot_a. Its module tree is on the phone.
|
||||
- Full stock partition clone (2026-06-18) at `~/pixel3-backup/`; LOS boot
|
||||
image at `~/Downloads/lineage-22.2-20260629-boot.img`.
|
||||
- Last resort: slot B is a complete, working Android.
|
||||
|
||||
## Hardware map (what's proven on this exact unit)
|
||||
|
||||
- SoC sdm845, 4G RAM. Panel: **LG SW43408** (DSC command-mode, 1080x2160) —
|
||||
settled from per-unit persist calibration data; the Samsung s6e3ha8/sec_ts
|
||||
DTBO fragments are the OTHER hardware variant, not ours.
|
||||
- Touch: **ST FTS @ 0x49 on i2c-2 (888000 geni SE, GPI-DMA-only)**; reset
|
||||
tlmm 99 (active-low RESETB), IRQ tlmm 125, AP/SLPI bus switch tlmm 136.
|
||||
- OLED bias: PMI8998 LAB/IBB under **SWIRE (DDIC hardware) control** — see
|
||||
display.md.
|
||||
- WiFi wcn3990 (ath10k_snoc), BT (hci_uart/btqca), modem MPSS (see
|
||||
modem.md), sensors behind SLPI DSP (see slpi.md), audio WCD9340 over
|
||||
SLIMbus (working HiFi speaker, microphone and two-way calls; see audio.md).
|
||||
- Power button = pm8941_pwrkey → input event; volume keys GPIO.
|
||||
|
||||
## Charging (pmi8998, `qcom_smbx`)
|
||||
|
||||
Two supplies: `pmi8998-charger` (the charger) and `qcom-battery` (fuel
|
||||
gauge). The charger IC does full CC/CV — trickle, pre, fast, full-on,
|
||||
taper — and `BATTERY_CHARGER_STATUS_1` bits 0:2 report which stage it is
|
||||
in. Mainline read that register only to derive `status`, collapsing all
|
||||
five states into "Charging" and discarding the detail.
|
||||
|
||||
Since f08dc70b3 the driver exposes them:
|
||||
|
||||
| attribute | register | notes |
|
||||
|---|---|---|
|
||||
| `charge_type` | `BATTERY_CHARGER_STATUS_1` (0x06) | read-only; Trickle/Fast/Standard |
|
||||
| `constant_charge_current_max` | `FAST_CHARGE_CURRENT_CFG` (0x61) | 25000µA/step, 8-bit |
|
||||
| `voltage_max` | `FLOAT_VOLTAGE_CFG` (0x70) | `raw=(uV-3487500)/7500+1`; 4.4V → 122 |
|
||||
| `charge_control_end_threshold` | same as above | percentage form, 0-100 |
|
||||
|
||||
**Lowering the float voltage is what caps the charge.** The cell is held
|
||||
at whatever CV target is programmed, so 4.1V parks it near 80% and buys
|
||||
calendar life. Writes are bounded twice — by the DT cell rating
|
||||
(`voltage-max-design-microvolt = <4400000>` in
|
||||
`sdm845-google-blueline.dts`) and by what the 8-bit fields can encode, so
|
||||
neither an overcharge nor a silent masked-off write is possible.
|
||||
|
||||
**These registers are volatile — every setting is lost on reboot.** Use
|
||||
`/etc/blueline-charge-limit` + `blueline-charge-limit.service` to persist
|
||||
one. Front end is `blueline-charge status|limit|current`.
|
||||
|
||||
Not exposed by this driver: `charge_full`/`charge_now` (fuel gauge, see
|
||||
`qcom-battery`) and any charge *scheduling* — downstream's `google_charger`
|
||||
ran a taper state machine with DT tunables (`google,chg-itaper`,
|
||||
`chg-soctaper`); mainline has no equivalent and this patch does not add one.
|
||||
|
||||
## Hard lessons (each cost real time — do not relearn)
|
||||
|
||||
- **Verify partition identity before any write** (`parted -s /dev/sda print`
|
||||
+ `blkid`); an unlabeled lsblk row is not free space. The sda21 near-miss
|
||||
is the standing warning.
|
||||
- `/tmp` on the phone is tmpfs — never park artifacts there.
|
||||
- `fastboot boot <img>` has a ~60s hardware watchdog — flash, don't
|
||||
test-boot, anything that takes long to come up.
|
||||
- Trust the user's direct visual report of the glass over any remote
|
||||
telemetry — registers can say "on" while the panel shows black.
|
||||
909
saf/device/slpi.md
Normal file
909
saf/device/slpi.md
Normal file
|
|
@ -0,0 +1,909 @@
|
|||
# PAF · SLPI bring-up (sensor island) — SOLVED 2026-07-10
|
||||
|
||||
## Status update — 2026-07-25: SLPI recovers, hexagonrpcd-sdsp does not
|
||||
|
||||
**Sequencing regression, live for hours before it was noticed.** The CHRE
|
||||
fatal documented below recurred on an ordinary running system (not a cold
|
||||
boot) at 06:04:33:
|
||||
|
||||
kernel: qcom_q6v5_pas 5c00000.remoteproc: fatal error received:
|
||||
err_qdi.c:456:EF:sensor_process:1:fatal_error.cc:22:CHRE
|
||||
fatal@platform_sensor_manager.cc:158
|
||||
remoteproc remoteproc2: crash detected in slpi / handling crash #1
|
||||
remoteproc remoteproc2: remote processor slpi is now up
|
||||
|
||||
remoteproc recovery worked — `remoteproc2: slpi = running` afterwards. But
|
||||
`blueline-hexagonrpcd-sdsp.service` took
|
||||
`Could not fetch next FastRPC message: Broken pipe` and **"Deactivated
|
||||
successfully"**: systemd read a clean exit and did not restart it. SLPI came
|
||||
back; the HexagonFS daemon that serves it did not. The unit sat `inactive
|
||||
(dead)` while `blueline-hexagonrpcd-adsp` (audio PD) stayed up, which is why
|
||||
audio was unaffected and the loss was invisible.
|
||||
|
||||
Symptom, ~100s apart, restart counter climbing to 5:
|
||||
|
||||
iio-sensor-proxy: 'registry' sensor unavailable, is hexagonrpcd running?
|
||||
|
||||
net.hadess.SensorProxy ->
|
||||
HasAccelerometer: false HasAmbientLight: false HasProximity: false
|
||||
|
||||
**Every sensor was down** — no autorotate, and `blueline-proximity-lock` kept
|
||||
running against a proxy with nothing behind it. A standalone consumer of a
|
||||
dead sensor is indistinguishable from one whose sensor is merely quiet, so
|
||||
nothing reported the outage.
|
||||
|
||||
**Fix belongs in `rootfs-overlay/`, not on the device.** The recovery order
|
||||
is the bug: SLPI restarts itself, so its filesystem daemon must follow it
|
||||
(`Restart=` plus a binding/ordering relationship that survives remoteproc
|
||||
crash-recovery, not just boot). A fix applied live is erased by the next
|
||||
reprovision — see `SouveraineOS/docs/tasks/25-one-repo-all-packages.md`.
|
||||
|
||||
Consumed-by note: this was the concrete argument for evidence source health
|
||||
being device state, and it is now built — `SouveraineOS/docs/DEVICE-STATE-MACHINE.md`
|
||||
§10 (2026-07-26). A source that reported and then went silent for 90 s is
|
||||
recorded `down`, not quiet, and every forensic snapshot taken during the outage
|
||||
is stamped `sensors_degraded`. The half that made it possible is here:
|
||||
`blueline-proximity-lock` re-sends its last reading every 30 s, because
|
||||
`monitor-sensor` only emits on change and a phone on a table is otherwise
|
||||
indistinguishable from a CHRE fatal. (TASK-26 itself was never written; the
|
||||
requirement outlived the placeholder.)
|
||||
|
||||
## Status update — 2026-07-20: iio-sensor-proxy is always-on now
|
||||
|
||||
The wake-lockup caveat below drove `blueline-autorotate` to start/stop the
|
||||
libssc client on demand — that hazard is confirmed gone. `iio-sensor-proxy`
|
||||
now runs continuously from boot via `blueline-sensors-enable.service`
|
||||
(`After=blueline-hexagonrpcd-sdsp.service`), and `AutoRotateToggle` in the
|
||||
shell only starts/stops the rotation daemon, not the sensor proxy. Proximity
|
||||
(TMD2725) is live and consumed by `blueline-screen-toggle` (DT2W gate) and
|
||||
the new `blueline-proximity-lock` user service (blanks an already-on locked
|
||||
screen on proximity-near). See touch.md and README.md's Tap-to-wake row.
|
||||
The historical on-demand/no-autostart record below is superseded by this but
|
||||
left as-is per PAF doctrine — see `docs/tasks/paf-reconciliation.md`.
|
||||
|
||||
## Reliability caveat — 2026-07-11: libssc wake lockup, root cause unproven
|
||||
|
||||
### New, separate cold-boot incident — 2026-07-13: delayed CHRE sensor-manager fatal
|
||||
|
||||
On Casey's confirmed unplugged cold boot, SLPI came up at kernel monotonic
|
||||
17.692s and then, with `iio-sensor-proxy.service` inactive, crashed at
|
||||
331.667s: `err_qdi.c:456:EF:sensor_process:1:fatal_error.cc:22:CHRE
|
||||
fatal@platform_sensor_manager.cc:158`. Remoteproc recovery immediately booted
|
||||
SLPI again. This is **not** the historical immediate `sar.cc:27` signature and
|
||||
must be tracked independently; its five-minute delay makes a runtime
|
||||
CHRE/SEE-sensor-manager timeout or deferred initialization path plausible.
|
||||
Active firmware was `/lib/firmware/qcom/sdm845/Google/blueline/slpi.mbn`, SHA-256
|
||||
`83e16ce8bffd8e4b963280dd30fa782becd005b40a3fd4dd6199093f98b82826`.
|
||||
The raw journal sequence is retained by `journalctl -k -b`; do not clear the
|
||||
kernel ring before copying a newly observed fatal into this record.
|
||||
|
||||
**Source correlation (2026-07-13):** AOSP CHRE's SLPI
|
||||
`platform_sensor_manager.cc` explicitly schedules `crashAfterSensorNotFound`
|
||||
for `5 * 60` seconds when required SEE sensors are missing at initialization.
|
||||
The observed delay is 313.975 seconds from SLPI-up to fatal. This pins the
|
||||
failure to the required-SEE-sensor discovery set, rather than a random runtime
|
||||
crash or the audio stack. Identify the absent SUID(s) from the live SSC/SEE
|
||||
registry before touching firmware or remoteproc state.
|
||||
|
||||
**Required-SUID sweep (2026-07-13):** The bounded read-only
|
||||
`tools/slpi-suid-probe` found SUIDs for
|
||||
`sensor_temperature` (LIS2MDL), `accel`/`gyro` (BMI160), `mag` (LIS2MDL),
|
||||
`pressure` (BMP380), and `ambient_light`/`proximity` (TMD2725). `amd` alone
|
||||
returned no matching SUID. The preserved Pixel Android `sns_amd` group is
|
||||
byte-for-byte reproduced by the OEM/soc-321 source wrapper. The reverse-file
|
||||
trace then proved the failure mode: after startup the DSP reads the added
|
||||
runtime `config/sns_amd.json`, opens the valid static group with write/truncate,
|
||||
and emits no payload for that group, leaving it zero bytes. This is not a host
|
||||
permission issue (the daemon successfully writes many other groups). Pixel's
|
||||
captured Android vendor config has no `sns_amd.json`, so the staged repair keeps
|
||||
the static group but omits that dynamic config input. **Cold-boot result:** the
|
||||
served group remained byte-identical to Android (239 bytes) and the DSP read
|
||||
it without a write-open, yet SLPI still fataled at 322.907s. Therefore the
|
||||
truncation path was a genuine regression but is not sufficient to restore the
|
||||
AMD SUID or satisfy CHRE. The probe was made null-safe after libssc returned an
|
||||
unavailable data type without a `GError`; no SLPI crash was caused by the probe.
|
||||
|
||||
**AMD provisioning correction (2026-07-13):** Qualcomm's public split runtime
|
||||
pair (`sns_amd.json` plus `sns_amd_sw_enabled.json`) was also tested. The
|
||||
correct vendor-input location is `config/registry/sns_amd.json`, never the
|
||||
served flat registry and never a physical `registry/registry` tree. Its trace
|
||||
is decisive: SEE opens that input, opens the valid static `sns_amd` group in
|
||||
write mode, allocates write-FD 256, and closes it **without an `fwrite`**; it
|
||||
then writes `sns_reg_config` normally. `O_TRUNC` in hexagonrpcd therefore
|
||||
turned the valid 239-byte Android-identical group into zero bytes without any
|
||||
DSP replacement payload. A clean `0014-lazy-truncate-reverse-writes.patch` is
|
||||
installed for the next boot: `w` opens are truncated only at their first actual
|
||||
`fwrite`, while append mode is unchanged. This preserves no-write static
|
||||
groups, but it is a prerequisite—not yet evidence that AMD will publish.
|
||||
**Validation:** the subsequent boot kept `sns_amd` at the exact 239-byte
|
||||
Android SHA-256 (`4756a6…2959d2`) through the complete no-write open/close
|
||||
sequence, yet CHRE still fataled at 325.595s in
|
||||
`platform_sensor_manager.cc:158`. Registry placement and host-side truncation
|
||||
are therefore fixed prerequisites, not the remaining AMD-publication cause.
|
||||
|
||||
**Required-SUID timeout cleared (2026-07-13):** the public SDM845 AMD split
|
||||
was retried only after the no-write truncation repair: dynamic
|
||||
`config/registry/sns_amd.json` supplies parameters while
|
||||
`sns_amd_sw_enabled.json` supplies enablement; the preserved 239-byte Android
|
||||
group remains the static registry seed. SEE opened **both** dynamic files,
|
||||
opened `sns_amd` in write mode without sending a payload, and the lazy-truncate
|
||||
daemon retained the exact reference hash. At 375 seconds uptime—well past the
|
||||
former 325-second `platform_sensor_manager.cc:158` deadline—SLPI remained
|
||||
`running`, `hexagonrpcd` remained active, and no fatal was logged. This is the
|
||||
first boot to clear CHRE's required-sensor watchdog. Do not immediately start
|
||||
`iio-sensor-proxy`/`ssccli`: their libssc client connection is the separately
|
||||
documented wake-lock hazard and needs a bounded follow-up test.
|
||||
|
||||
**Bounded client-path validation (2026-07-13):** On that stable boot,
|
||||
`iio-sensor-proxy` was started explicitly (the package's boot-time udev
|
||||
autostart remains masked). It discovered the SSC TMD2725 proximity and ambient
|
||||
light sensors plus BMI160 acceleration. The desktop D-Bus API successfully
|
||||
claimed and delivered acceleration (`face-up`) and ambient-light samples
|
||||
(~14 lux); proximity also reports normally. The device rule now explicitly
|
||||
selects `ssc-accel ssc-proximity ssc-light` on demand, while preserving the
|
||||
no-autostart policy. Direct `ssccli` reads also returned live TMD2725 light,
|
||||
BMI160 acceleration, and LIS2MDL magnetometer values. `ssccli`'s compass
|
||||
client alone still returns `UNKNOWN`. SLPI and hexagonrpcd remained running,
|
||||
with no CHRE fatal, after these clients. This validates the practical sensor
|
||||
path without reclassifying the historical boot-time libssc incident as solved.
|
||||
|
||||
**Service-loop regression fixed (2026-07-13):** After the one legitimate
|
||||
five-minute fatal at 326.652s, `hexagonrpcd` correctly exited on its broken
|
||||
FastRPC pipe with status 0. Its unit unfortunately used `Restart=always`, so
|
||||
systemd reattached during remoteproc recovery six times in seven seconds. Each
|
||||
reattach immediately crashed SLPI in `SNS_REG_INIT:39`, then the service hit
|
||||
systemd's start limit. This was a userspace retry-policy bug, not six
|
||||
independent firmware failures. The deployed and overlay unit now uses
|
||||
`Restart=on-failure` with a five-second delay; no service/remoteproc restart
|
||||
was performed while installing it. A trace-enabled `hexagonrpcd` was also
|
||||
deployed for the next cold boot: `HEXAGONRPC_TRACE_FILES=1` correlates virtual
|
||||
open paths with the previously opaque `Could not read file: Is a directory`
|
||||
FD. The binary and service changes take effect on the next genuine cold boot.
|
||||
|
||||
**DMA-heap permission regression fixed (2026-07-13):** The documentation said
|
||||
the FastRPC allocation heap was owned by `root:fastrpc 0660`, but the actual
|
||||
cold-boot device node was `root:root 0600`: the overlay had a rule for
|
||||
`/dev/fastrpc-*` only and never contained the claimed `dma_heap` rule. Added
|
||||
`11-fastrpc-dma-heap.rules`, narrowly assigning only
|
||||
`/dev/dma_heap/system` to `fastrpc:0660`; firmware-reserved heaps remain
|
||||
root-only. It was installed live with `udevadm trigger` and verified as the
|
||||
`fastrpc` user before any new SLPI action. This removes a genuine local
|
||||
permissions blocker for non-root FastRPC clients, but does not by itself
|
||||
explain an EPERM returned from an already-open DSP RPC session.
|
||||
|
||||
**CORRECTION — cold-boot validation failed (2026-07-13):** The following
|
||||
paragraph describes a staged-path hypothesis, **not a completed repair**. On
|
||||
the next cold boot the registry was readable but SLPI again fataled at 323.7s.
|
||||
The corrected root-owned listener was then installed and enabled; on its first
|
||||
validation boot it attached before SLPI's delayed window but the fatal still
|
||||
occurred at 326.97s. The listener deployment is necessary infrastructure, not
|
||||
a sensor fix. Its remaining `sns_reg_config` virtual-path error points to
|
||||
hexagonrpcd's mapping, and CHRE required-sensor discovery remains open.
|
||||
|
||||
**Concrete boot-path repair staged and installed (2026-07-13):** before the
|
||||
fatal, `hexagonrpcd` logged that
|
||||
`/mnt/vendor/persist/sensors/registry/sns_reg_config` did not exist, even
|
||||
though the staged registry under `/usr/share/qcom/sensors` was complete. All
|
||||
required types subsequently resolve through SSC (BMI160 accel/gyro and
|
||||
motion/stationary; LIS2MDL magnetometer; BMP380 pressure; TMD2725 ambient
|
||||
light/proximity), so this is an initialization-path race rather than a
|
||||
permanently missing physical sensor. The enabled
|
||||
`blueline-sns-registry-stage.service` now creates the guarded persistent link
|
||||
`/mnt/vendor/persist/sensors → /usr/share/qcom/sensors` before
|
||||
`hexagonrpcd-sdsp.service`. The next **unplugged shutdown/power-on** is the
|
||||
validation: retain the kernel journal and confirm that the delayed fatal does
|
||||
not occur after five minutes.
|
||||
|
||||
**Correction — the tracked VFS fix was not in the installed binary
|
||||
(2026-07-13 evening):** The fresh cold boot again logged
|
||||
`Could not open /mnt/vendor/persist/sensors/registry/sns_reg_config`, despite
|
||||
the flat staged host file existing. Inspection of `/usr/local/bin/hexagonrpcd`
|
||||
and `~/hexagonrpc/hexagonrpcd/rpcd_builder.c` proved that the live
|
||||
`hexagonrpc-blueline 0.4.0-2` binary had only the old `sns_reg_version` map;
|
||||
the recipe tracked 0008 but its installed artifact did not contain it. Applied
|
||||
0008 to the live source, rebuilt, and installed a backed-up replacement. The
|
||||
new binary embeds maps for `sns_reg_config`, `sns_reg_version`,
|
||||
`sns_reg_ctrl`, `file1`, and `file2`; `/usr/share/qcom/sensors/registry/`
|
||||
remains physically flat (no `registry/registry`). No DSP lifecycle action was
|
||||
taken. The next unplugged shutdown/power-on must confirm the initial ENOENT is
|
||||
gone and then observe CHRE past its five-minute required-sensor timeout. The
|
||||
package database still says `0.4.0-2`; rebuild/install the tracked `pkgrel=3`
|
||||
package before treating this live binary repair as image-persistent.
|
||||
|
||||
**Cold-boot validation — VFS control maps fixed; CHRE still missing a required
|
||||
SEE sensor (2026-07-13 evening):** On the next unplugged shutdown/power-on,
|
||||
SLPI reached `running` at 13.014s. `hexagonrpcd` successfully opened virtual
|
||||
`registry/sns_reg_config`, `sns_reg_version`, and `sns_reg_ctrl`; the former
|
||||
`sns_reg_config` ENOENT was absent. At 327.479s (314.465s after SLPI-up), CHRE
|
||||
again fataled at `platform_sensor_manager.cc:158`, then remoteproc recovered
|
||||
SLPI and the daemon exited cleanly. This conclusively fixes the VFS-control
|
||||
prerequisite without fixing required-SUID discovery. Remaining trace noise is
|
||||
benign: `testsig*.so` is absent test firmware, and the reported `EISDIR` is
|
||||
CHRE's expected read-then-`opendir` probe of virtual `registry/registry`.
|
||||
|
||||
SLPI itself remains up; this is not a return of the `sar.cc` crash. Toggling
|
||||
sensors off/on reproducibly identifies the libssc/iio-sensor-proxy SSC QMI
|
||||
connection as the trigger for a wake lockup: kernel and SSH survive, display
|
||||
commits stop completing without DSI/DPU errors, FTS becomes calibration-blind,
|
||||
and GENI I2C GPI DMA times out concurrently. This narrows the mechanism to an
|
||||
AP-side shared-bus/power interaction on SLPI wake. An rpmh/interconnect vote
|
||||
race is the leading hypothesis, **not a proven root cause**.
|
||||
|
||||
**HACK shipped:** SLPI stays booted, but the auto-rotate toggle starts/stops
|
||||
its iio-sensor-proxy/libssc client instead of leaving that client enabled from
|
||||
boot. It reduces exposure only; it is not a fix. Netconsole and pstore_blk are
|
||||
armed for an instrumented reproduction before any permanent change is attempted.
|
||||
|
||||
**Boot-safety correction, 2026-07-11:** the initial HACK was ineffective at
|
||||
boot: `/usr/lib/udev/rules.d/80-iio-sensor-proxy.rules` starts the proxy on
|
||||
every `fastrpc-sdsp` add event via `SYSTEMD_WANTS`. The overlay now supplies a
|
||||
comment-only `/etc/udev/rules.d/80-iio-sensor-proxy.rules`, which shadows that
|
||||
vendor rule while leaving the explicit polkit/toggle start path intact. Live
|
||||
`udevadm test` confirmed `/etc` rule precedence and the running boot-started
|
||||
proxy was stopped. The next reboot must still verify that it remains off.
|
||||
|
||||
**Instrumented reproduction, 2026-07-11 10:45 EDT:** netconsole over the USB
|
||||
gadget was configured dynamically (`usb0` 172.16.42.1:6665 → laptop
|
||||
172.16.42.2:6666) and packet-capture verified before the proxy was started.
|
||||
With the proxy active, physical sleep/wake reproduced the stale-frame lockup:
|
||||
the glass remained at 08:02 while SSH, low system load, Hyprland, and
|
||||
Quickshell remained alive. No panic or pstore record occurred. Starting 28
|
||||
seconds after the proxy-start marker, the FTS driver emitted repeated invalid
|
||||
Mutual/Self Strength calibration events; no DSI/DPU error, GENI timeout, or
|
||||
rpmh/ICC diagnostic was logged in this run. Netconsole captured only the
|
||||
explicit console-level start marker, so it proves the transport but not the
|
||||
missing low-priority driver messages. The proxy was stopped afterwards. This
|
||||
is further trigger evidence, **not** proof of the rpmh/interconnect hypothesis.
|
||||
Next capture: raise the temporary console loglevel and add narrowly scoped
|
||||
rpmh/ICC tracing before another deliberate reproduction.
|
||||
|
||||
## ARCH STATUS (2026-07-10 afternoon): THE sar.cc WALL IS DOWN. SLPI runs.
|
||||
|
||||
SLPI boots, stays up (0 fatals, was deterministic-fatal at ~10s), SEE fully
|
||||
initializes (node 9 publishes svc 400 + services that never appeared in the
|
||||
crash era), the accelerometer streams real data to userspace (ssccli), and
|
||||
iio-sensor-proxy reports HasAccelerometer=true over D-Bus. Boot-started by
|
||||
`blueline-slpi.service` (rootfs-overlay).
|
||||
|
||||
**Root cause of sar.cc:27 (found by source archaeology, kernel 25d56ec84):**
|
||||
the SSC firmware's `remote_proc_state` sensor does
|
||||
`smp2p_register(SMEM_APPS, "sleepstate")` in its init thread and NEVER
|
||||
publishes if that fails (Qualcomm source: Rivko/android-firmware-qti-sdm670,
|
||||
adsp_proc/ssc/sensors/remote_proc_state/). Mainline's sdm845 smp2p-slpi node
|
||||
has no "sleepstate" entry, so registration failed, remote_proc_state never
|
||||
published, and CHRE's SeeHelper::init exhausted its ~10s findSuidSync retry
|
||||
budget on it (AOSP chre platform/slpi/see/see_helper.cc — initCalSensors
|
||||
passes, initRemoteProcSensor fails) → "Failed to initialize ASH SEE helper"
|
||||
→ chre_utils FATAL at sar.cc:27 → whole sensor_process aborts. The SAR
|
||||
module was only ever the messenger. FIX: sdm845.dtsi sleepstate smp2p entry
|
||||
+ new drivers/soc/qcom/smp2p-sleepstate.c asserting bit 12 (= SMP2P_AWAKE
|
||||
0x1000 in SSC source; = downstream PROC_AWAKE_ID msm-4.9
|
||||
smp2p_sleepstate.c), dropped across suspend. CONFIG_QCOM_SMP2P_SLEEPSTATE=y.
|
||||
|
||||
**Dead ends, so nobody repeats them:** (1) qrtr ns announce-replay patch
|
||||
(3fe96cf2d, REVERTED in 94ac4a5e1) — replaying all nodes' services on HELLO
|
||||
wedged node 9's own service registration (flow control on the ns worker);
|
||||
sar.cc never needed modem services. (2) The whole 2026-07-08 elimination
|
||||
list stands (registry, socinfo, chrecd, proxy-power, sessions — all real
|
||||
fixes, none the wall). (3) Registry-diff vs LOS persist: the 42 missing
|
||||
entries are wrong-device pollution + factory cal, not the cause.
|
||||
|
||||
Five-fix SNS_REG_INIT chain (hw_platform OEM socinfo, module tree, chrecd,
|
||||
sscregistrygen registry, socinfo sysfs) remains prerequisite and correct.
|
||||
Sensor stack persists across re-provision since cd82f29. AP-side gesture
|
||||
dt2w works independently (touch.md); the SLPI gesture-nanoapp path is now
|
||||
UNBLOCKED for future work (CHRE runs). Auto-rotate WORKS on glass
|
||||
(2026-07-10 ~13:00: blueline-autorotate + ACCEL_MOUNT_MATRIX udev fix,
|
||||
see rootfs-overlay). Remaining: sx932x "sar" SUID still absent
|
||||
(grip sensor hardware probe — separate, non-fatal now).
|
||||
Everything below is the pmOS-era record — the archaeology that fed the
|
||||
chain above; capture docs it cites live in `evidence/`.
|
||||
|
||||
## 2026-06-30 — chronic RTC-write flood found + fixed; wrong listener used in a failed test; dma_heap permissions fixed
|
||||
|
||||
**Context:** a fresh SLPI one-shot attempt (manual `sdsprpcd sensorspd` + `recovery=disabled`
|
||||
+ `echo start`, **not** via `blueline-slpi-ready.service`) wedged the phone. Investigating the
|
||||
wedge surfaced three unrelated, real bugs — none of them SLPI firmware behavior — that were
|
||||
corrupting every capture attempt and blocking the correct listener from working at all.
|
||||
|
||||
**1. Chronic `disallowed SPMI write to sid=0, addr=0x6046` flood (~40/sec, continuous,
|
||||
present even with SLPI offline) — unrelated to SLPI, was drowning every capture.**
|
||||
Traced to `drivers/spmi/spmi-pmic-arb.c:1364` (`pmic_arb_offset_v5`): a hardware EE-ownership
|
||||
check the AP fails every time. `sid=0`/`0x60xx` is the pm8998 RTC peripheral
|
||||
(`pm8998.dtsi:104`, `rtc@6000`). Root cause: `drivers/rtc/rtc-pm8xxx.c:622-631` force-overrides
|
||||
`allow_set_time = true` regardless of the DT `allow-set-time` property, on the premise
|
||||
(comment at :624-628) that "the modem reads real wall-clock time during RF-init" via this
|
||||
write. **That premise is wrong** — cross-referenced against LineageOS on the same silicon:
|
||||
`los-blueline.dts:2371` sets `qcom,qpnp-rtc-write = <0x00>` (write disabled),
|
||||
`qpnp-rtc.c:474,587-588` defaults to read-only ops and only switches to read-write if that
|
||||
property is true, and `init.sdm845.rc:233-237` never touches `rtc` at all — only
|
||||
`start time_daemon`. Android does not write this register either; it can't, for the same
|
||||
hardware-ownership reason. The actual (real, working, already-verified — see `file.txt`
|
||||
2026-06-30, "FIRST ACCEPT... modem User time is now real wall-clock") mechanism for feeding
|
||||
the modem wall-clock time is `tools/blueline-timed.service`, a **QMI TIME-service-22 push
|
||||
straight to the modem** (`modem-time-set.py`), completely unrelated to the PMIC RTC. The
|
||||
`rtc-pm8xxx.c` patch never achieved its stated goal either way: `__pm8xxx_rtc_set_time()`
|
||||
(:294-343) fails at its very first register access (`alarm_ctrl`, same address) every call,
|
||||
before ever reaching the actual time-write registers.
|
||||
**Fix:** reverted the force-true override (falls through to the driver's existing
|
||||
`pm8xxx_rtc_update_offset()` software-offset path, which is what LOS's read-only mode is the
|
||||
equivalent of); dropped `allow-set-time;` from `sdm845-google-blueline.dts`. Kernel rebuilt
|
||||
2026-06-30 ~21:56.
|
||||
|
||||
**2. The SLPI wedge test used the wrong listener with no ordering — not a firmware crash we
|
||||
observed correctly.** `device-google-blueline` ships `blueline-slpi-ready.service`, which
|
||||
`Requires=`+`After=` **`blueline-sns-registry-stage.service`** and **`hexagonrpcd-sdsp.service`**
|
||||
— exactly the registry-staged/listener-ready ordering the S6 session below already proved is
|
||||
required. The failed manual test instead started the proprietary `sdsprpcd` (from the
|
||||
`fastrpc` apk, installed this session) directly, with zero registry staging and zero
|
||||
wait-for-listener. **Next SLPI attempt: `systemctl start blueline-slpi-ready.service`,
|
||||
not a hand-rolled sequence.** All three services were `disabled` (not masked) on this
|
||||
rootfs — never actually run, not deliberately blocked.
|
||||
|
||||
**3. `/dev/dma_heap/*` was `root:root 0600`, silently blocking `hexagonrpcd-sdsp.service`
|
||||
(`User=fastrpc`) from allocating FastRPC memory at all.** `60-fastrpc.rules`'s fix for this
|
||||
(`RUN+="/bin/setfacl -m g:fastrpc:rw $env{DEVNAME}"`, and only for the `system` heap, not
|
||||
`adsp_region`/`default_cma_region`/`reserved`) silently no-ops on every boot because
|
||||
`setfacl`/the `acl` package was never installed. Confirmed live: `fastrpc` user got
|
||||
`Permission denied` opening `/dev/dma_heap/system` before the fix, clean `open()` after.
|
||||
**Fixed** both live (`chgrp fastrpc; chmod 0660` on all four) and durably in the rules file
|
||||
(replaced the `setfacl` RUN directive with `GROUP="fastrpc", MODE="0660"`, matching the
|
||||
`fastrpc-*` device rules already in the same file) — not yet verified across a real cold
|
||||
boot.
|
||||
|
||||
**4. Separately, `/vendor/dsp/{adsp,cdsp,sdsp}` didn't exist on the rootfs at all** (needed
|
||||
by `adsprpcd`/`cdsprpcd`/`sdsprpcd`/`hexagonrpcd` alike). The live `vendor_b` partition
|
||||
(`/dev/sda10`) mounts but reads as directory-corrupted via both the kernel VFS and
|
||||
`debugfs -R "ls /"` — likely a raw-mount-bypasses-dm-verity artifact, not real corruption of
|
||||
the working LOS install (left untouched, not fsck'd). Used the already-extracted, genuinely
|
||||
readable copy from an earlier session, `pixel3-work/los-vendor/vendor.img` (531MB ext2,
|
||||
volume name "vendor"), and copied just `dsp/{adsp,cdsp,sdsp}` (17MB) to `/vendor/dsp/` on
|
||||
the phone. This did **not** fix `adsprpcd_audiopd` (its real failure is Qualcomm QDI status
|
||||
`0x80000414`, not literally ENOENT despite the log text — needs an audio-PD servreg
|
||||
registration the vendor tree alone doesn't provide; out of scope, re-masked), but the tree
|
||||
is now in place for whatever `hexagonrpcd`/SDSP actually needs from it.
|
||||
|
||||
**Net effect: the next SLPI attempt is the first one with quiet logs, the correct listener
|
||||
+ ordering, correct dma_heap permissions, and the vendor DSP tree all present
|
||||
simultaneously.** No prior attempt (including tonight's wedge) had all four.
|
||||
|
||||
## 2026-06-30 (cont.) — first clean-ish trace: two racing FastRPC-sdsp consumers, one legit, one not
|
||||
|
||||
After the RTC fix (kernel flashed, rebooted, SPMI flood confirmed **0 hits** — first quiet
|
||||
boot ever), ran a real test: staged the registry (`blueline-sns-registry-stage.service`,
|
||||
179 files), then a poller script raced `hexagonrpcd -f /dev/fastrpc-sdsp -d sdsp -s` onto
|
||||
the device the instant it appeared (`/dev/fastrpc-sdsp` doesn't exist pre-boot; neither
|
||||
`hexagonrpcd` nor any udev rule retries for it — the poller was necessary, see script in
|
||||
`tools/` history / this session). `echo start` on `remoteproc2`. Captured via
|
||||
`journalctl -f` to a persistent file (works — unlike `tail -f /dev/kmsg`, which silently
|
||||
dies on the ring-buffer overflow the RTC bug used to cause; **this is now the standard
|
||||
capture method**, not kmsg tail).
|
||||
|
||||
**What actually happened, in order (journal-confirmed, `slpi-test2/journal-live.txt`):**
|
||||
1. `remoteproc remoteproc2: powering up slpi` → boots clean, heap already preassigned
|
||||
(no `-22`, confirms the earlier adsprpc memory conversion still holds).
|
||||
2. `/dev/fastrpc-sdsp` appears. **Two things react simultaneously**, not one:
|
||||
- `systemd[1]: Starting sDSP RPC daemon...` → **`sdsprpcd.service`**, auto-started by
|
||||
`60-fastrpc.rules`'s `KERNEL=="fastrpc-sdsp" ... SYSTEMD_WANTS+="sdsprpcd.service"`.
|
||||
This is the **Qualcomm proprietary daemon from the `fastrpc` apk installed earlier
|
||||
this same session** — an unintended side effect of installing that package to get
|
||||
`sdsprpcd` for testing. It attached first (`attachguestos` on domain 2 succeeded,
|
||||
opened `adsp_default_listener` handles), then hit
|
||||
`Error 0x4e ... errno Operation not permitted` on an RPC invoke.
|
||||
- `hexagonrpcd` (the poller-launched, intended listener) tried immediately after and
|
||||
got `Could not attach to FastRPC node: Operation not permitted` — plausibly because
|
||||
`sdsprpcd` had already claimed the attach slot, not necessarily a firmware-side
|
||||
rejection of `hexagonrpcd` specifically.
|
||||
- A **third** thing also started at the same instant:
|
||||
`systemd[1]: Starting IIO Sensor Proxy service...` (`iio-sensor-proxy.service`,
|
||||
pre-existing base-image package, installed 2026-05-08, unrelated to tonight — see
|
||||
below).
|
||||
3. ~1s later: `qcom_q6v5_pas ...: watchdog received: SFR Init: wdog or kernel error
|
||||
suspected.` → `remoteproc2` crashes.
|
||||
|
||||
**`sdsprpcd.service` is a real bug (masked) — but not a new one tonight.** Checked the
|
||||
history properly rather than assume: the 2026-06-29 session *also* installed the `fastrpc`
|
||||
apk and ran `sdsprpcd sensorspd` manually as its one-shot test — and that is the exact
|
||||
command HANDOFF's own notes say caused *that* session's wedge too ("the same one-shot
|
||||
`sdsprpcd sensorspd` + SLPI start caused SSH/network/UI to wedge"). `file.txt`'s later
|
||||
session was also checking for `sdsprpcd` specifically. So `sdsprpcd`-as-manual-test is a
|
||||
repeated pattern across at least three sessions, not something introduced tonight — this
|
||||
session continued that same habit (reinstalling the `fastrpc` apk to get `sdsprpcd` back
|
||||
after the rootfs rewrite lost it) before realizing `hexagonrpcd` was the actually-intended
|
||||
listener. `hexagonrpcd`, and the `blueline-slpi-ready.service`/`hexagonrpcd-sdsp.service`
|
||||
ordering, come from `device-google-blueline` (a separate, independently-developed track
|
||||
within this same project) and were never actually exercised in isolation until tonight's
|
||||
`sdsprpcd`-masked re-test. `systemctl mask sdsprpcd.service` done this session — first
|
||||
time this collision has been diagnosed rather than just repeatedly hit.
|
||||
|
||||
**`iio-sensor-proxy.service` is NOT a bug and should NOT be masked long-term** (briefly
|
||||
masked mid-session, then **unmasked** once this was understood — leave it unmasked).
|
||||
It's built against `libssc.so.2` (`qmi_client_ssc_control`, "SSC QMI client") — a genuine
|
||||
**QMI Sensor SubSystem Client**, i.e. exactly the downstream consumer this whole SLPI
|
||||
effort is trying to feed ("no SNS QMI service exposed" has been the standing symptom
|
||||
description). Its reacting to SLPI coming up is systemd/udev doing its job correctly, not
|
||||
a coincidence. The real fix here is **ordering, not suppression**: it needs to start
|
||||
*after* SLPI/hexagonrpcd are confirmed stable, not the instant the remoteproc powers up —
|
||||
right now nothing enforces that, so its QMI connection attempt lands during the same
|
||||
fragile registry-init window the S6 finding already identified as timing-critical, on top
|
||||
of the `sdsprpcd` collision. Untested whether `iio-sensor-proxy` alone (with `sdsprpcd`
|
||||
masked) still perturbs the window enough to matter — that's the next isolated test.
|
||||
|
||||
**Next step:** re-run the identical poller+`hexagonrpcd` test with `sdsprpcd.service` now
|
||||
masked. If `iio-sensor-proxy` still auto-fires and coincides with a crash, that tells us
|
||||
the ordering fix is needed for real; if `hexagonrpcd` attaches cleanly this time, the
|
||||
`sdsprpcd` collision was the whole story and today's `Operation not permitted` /
|
||||
`SFR Init` crash was never a hexagonrpcd/firmware problem at all — just two proprietary
|
||||
and community daemons fighting over one singleton resource.
|
||||
|
||||
## 2026-06-30 (cont. 2) — isolated re-test: `sdsprpcd` wasn't the story either. The SAR/CHRE fault is real and reproduces clean.
|
||||
|
||||
Re-ran the identical poller+`hexagonrpcd` test with `sdsprpcd.service` masked (previous
|
||||
section). `iio-sensor-proxy.service` still auto-fired on SLPI coming up (as expected, it's
|
||||
legitimate) but did **not** coincide with any crash this time — no evidence it perturbs
|
||||
anything on its own.
|
||||
|
||||
**Clean timeline (`journalctl`, unfiltered, this run):**
|
||||
```
|
||||
22:25:26 remoteproc remoteproc2: powering up slpi
|
||||
22:25:27 remoteproc remoteproc2: remote processor slpi is now up
|
||||
22:25:27 [hexagonrpcd] Starting hexagonrpcd (INIT_ATTACH_SNS) on /dev/fastrpc-sdsp
|
||||
22:25:27 [hexagonrpcd] Could not attach to FastRPC node: Operation not permitted
|
||||
22:25:36 qcom_q6v5_pas ...: fatal error received: err_qdi.c:456:EF:sensor_process:1:sar.cc:27:chre_utils fatal
|
||||
22:25:36 remoteproc remoteproc2: crash detected in slpi: type fatal error
|
||||
```
|
||||
No AP-level instability this time — `remoteproc2` settled into `crashed` (recovery still
|
||||
`disabled`, no relaunch spam) and the phone stayed fully reachable throughout. **The hard
|
||||
wedge in the first test tonight was very likely the `sdsprpcd`/`hexagonrpcd` collision
|
||||
itself (two clients contending for one singleton attach), not simply "SLPI crashes."** A
|
||||
clean, isolated SLPI crash does not wedge the AP.
|
||||
|
||||
**Conclusion: this is the same `sar.cc:27` SAR/CHRE fatal documented in the "2026-06-29
|
||||
live result" section above, now reproduced with everything else this session found and
|
||||
fixed already in place** — quiet logs (RTC fix), correct listener alone (no `sdsprpcd`
|
||||
collision), correct `dma_heap` permissions, `/vendor/dsp/` present, registry staged
|
||||
(179→182 files). **None of tonight's plumbing fixes were the actual blocker.** They were
|
||||
real bugs and worth fixing (nothing else could be tested cleanly until they were), but the
|
||||
live frontier is exactly where it was before tonight: `INIT_ATTACH_SNS` returns `EPERM`,
|
||||
and ~9s later the firmware faults inside CHRE's SAR path. **Next real work is still the
|
||||
already-identified target**: convert the remaining Lineage sensor/SAR FastRPC path
|
||||
(`sscrpcd sensorspd` equivalent, sensors PDR/service-location behavior, whatever makes
|
||||
`INIT_ATTACH_SNS` attach cleanly) — see "Status / next" above. That work has not started;
|
||||
tonight only cleared the ground for it to be tested honestly.
|
||||
|
||||
## 2026-06-30 (cont. 3) — the "proper" pmOS chain has a real, previously-undiagnosed timing bug
|
||||
|
||||
Per Casey's explicit direction: stop working around the shipped mechanism, make the
|
||||
shipped mechanism itself work. The gap: `hexagonrpcd-sdsp.service` has
|
||||
`ConditionPathExists=/dev/fastrpc-sdsp`, which can never be true at the moment systemd
|
||||
evaluates it (the device doesn't exist until SLPI starts) — `device-google-blueline` never
|
||||
shipped a udev rule to re-trigger it once the device appears. Added one, as a local admin
|
||||
override (not yet upstreamed into the package):
|
||||
```
|
||||
# /etc/udev/rules.d/61-hexagonrpcd-sdsp.rules
|
||||
ACTION=="add", SUBSYSTEM=="misc", KERNEL=="fastrpc-sdsp", TAG+="systemd", ENV{SYSTEMD_WANTS}+="hexagonrpcd-sdsp.service"
|
||||
```
|
||||
With this in place, `systemctl start blueline-slpi-ready.service` — the actual, sole,
|
||||
intended entry point — runs end-to-end with **no manual scripts at all**: registry stages,
|
||||
`hexagonrpcd-sdsp.service` auto-triggers via udev the instant the device appears (via a
|
||||
`blueline-hexagonrpcd-sdsp-wait.sh` wrapper this project hadn't inspected before), SLPI
|
||||
starts, `hexagonrpcd` attaches. This is real progress — the shipped mechanism now actually
|
||||
runs as designed, on the current rootfs, for the first time.
|
||||
|
||||
**But it lost the registry-init race — a real bug, not the earlier `sdsprpcd` collision.**
|
||||
Clean journal, unfiltered:
|
||||
```
|
||||
22:48:43 blueline-slpi-ready.sh: disabling recovery, starting SLPI once
|
||||
22:48:43 remoteproc remoteproc2: powering up slpi
|
||||
22:48:44 remoteproc remoteproc2: remote processor slpi is now up
|
||||
22:48:44 blueline-hexagonrpcd-sdsp-wait.sh: starting hexagonrpcd on /dev/fastrpc-sdsp
|
||||
22:48:44 hexagonrpcd: Starting hexagonrpcd (INIT_ATTACH_SNS) on /dev/fastrpc-sdsp
|
||||
22:48:44 qcom_q6v5_pas: fatal error received: err_qdi.c:456:EX:sensor_process:1:SNS_REG_INIT:39:PC=b20226c0
|
||||
22:48:44 remoteproc remoteproc2: crash detected in slpi: type fatal error
|
||||
```
|
||||
This is `SNS_REG_INIT` failing — the **exact same registry-init ordering race the S6
|
||||
session (2026-06-27) diagnosed and believed `auto_boot=false` had solved** — reproducing
|
||||
in the *same second* SLPI came up, before `hexagonrpcd`'s attach could complete.
|
||||
|
||||
**Compare to the earlier isolated test tonight** (this doc, "isolated re-test" section):
|
||||
manually racing `hexagonrpcd` onto the device with a raw 20ms-poll bash loop (no systemd
|
||||
involved) won that same race reliably — SLPI ran a full ~9s before failing later, in
|
||||
`sar.cc:27:chre_utils` (deeper into CHRE/SAR init, past registry-init). **The manual
|
||||
poller was faster and more reliable than the real systemd service-start chain.**
|
||||
`hexagonrpcd-sdsp.service`'s startup path — udev event → systemd service activation →
|
||||
`blueline-hexagonrpcd-sdsp-wait.sh` → exec `hexagonrpcd` → device open → `INIT_ATTACH_SNS`
|
||||
— has enough cumulative latency to lose a race a tight bash loop wins.
|
||||
|
||||
**FIXED, same session, same night.** Checked `~/pmos/pmaports/device/testing/device-google-blueline/`
|
||||
(the actual local source for this package) and found the design was already correct — the
|
||||
`hexagonrpcd-sdsp.service` drop-in (`hexagonrpcd-sdsp-root.conf`) already clears
|
||||
`ConditionPathExists`, already redirects to a wrapper (`blueline-hexagonrpcd-sdsp-wait.sh`)
|
||||
that runs a persistent `while :; do wait for device; exec hexagonrpcd; wait for
|
||||
teardown; done` loop — i.e. it was *already* starting before SLPI, via the normal
|
||||
`Requires=`/`After=` chain in `blueline-slpi-ready.service`, exactly as the S6 finding
|
||||
prescribed. (My first instinct — that a missing udev rule was the gap — was wrong; added
|
||||
`/etc/udev/rules.d/61-hexagonrpcd-sdsp.rules` then confirmed via `systemctl show
|
||||
hexagonrpcd-sdsp.service -p ConditionResult` → `yes` that it was never needed, and removed
|
||||
it again.) **The actual bug was one number**: the wrapper's poll interval,
|
||||
`blueline-hexagonrpcd-sdsp-wait.sh 0.2` (200ms), was too coarse to reliably detect
|
||||
`/dev/fastrpc-sdsp` and get `hexagonrpcd` attached inside SLPI's registry-init window.
|
||||
Changed to `0.02` (20ms, matching the interval that won the race in the manual poller
|
||||
test above). **Retested through the pure, unmodified entry point —
|
||||
`systemctl start blueline-slpi-ready.service`, zero manual scripts — and it now reliably
|
||||
reproduces the same deep `sar.cc:27:chre_utils fatal` crash tonight's best manual test
|
||||
got**, not the shallow `SNS_REG_INIT` race-loss. Fix committed to
|
||||
`hexagonrpcd-sdsp-root.conf` in the local pmaports source (not yet built into a package/
|
||||
flashed — currently live-patched onto the running rootfs only).
|
||||
|
||||
**Net: the shipped mechanism is now correct and reliable.** No more manual scripts, no
|
||||
udev workaround, no competing daemons — `systemctl start blueline-slpi-ready.service`
|
||||
alone gets to the real wall every time. The remaining problem is exactly what it's been
|
||||
all along: `INIT_ATTACH_SNS` → `EPERM`, firmware faults in CHRE's SAR path ~10s later.
|
||||
That's real kernel/source work (the Lineage sensor/SAR FastRPC conversion), not another
|
||||
timing or plumbing bug.
|
||||
|
||||
(Also observed, unrelated background noise in this capture: `qcril-autoselect` PDC
|
||||
Set Feature requests timing out repeatedly (`Transaction timed out (2)`) — pre-existing,
|
||||
not investigated tonight, flag for later.)
|
||||
|
||||
Note: `qrtr-lookup` is genuinely **absent from this rootfs** (`find / -xdev -iname
|
||||
'qrtr-lookup*'` finds nothing at all — not a `$PATH` issue). It's used routinely across
|
||||
`modem.md`, `tools/slpi-capture.sh`, and `tools/phone-slpi-sdsprpcd-collect.sh`, so it was
|
||||
clearly present and working in earlier sessions, but there is **no apk package or
|
||||
reference source for it anywhere in this project** (`qrtr-libs` only ships the shared
|
||||
library, not the CLI tool) — it must have been a manually-built binary on the pre-wedge
|
||||
rootfs with no packaging record, same category of loss as the SLPI service masks. Needs
|
||||
building from the upstream `qrtr` tools source (not currently cloned into `references/`)
|
||||
to restore. Did not block tonight's test since it wasn't needed, but real and unsolved.
|
||||
|
||||
> **🟠 REFRAME 2026-06-29 (Casey) — the SLPI is a CANDIDATE ROOT-CAUSE for the modem 52, NOT a side-quest.**
|
||||
> The prior "SLPI proven independent of the 52" conclusion (`modem_consumer_chain_20260626.md` SESSION-3)
|
||||
> is **invalid**. It was tested only with an SLPI that was either crash-looping OR merely
|
||||
> remoteproc-not-crashing — **never a functioning sensor stack** (that doc itself notes: *"qrtr-lookup
|
||||
> empty — the SLPI never exposes an SNS QMI service before it dies"*; *"CHRE nanoapp crash INSIDE the
|
||||
> SLPI firmware at init"*). "Stop SLPI → still 52" only proves stopping doesn't worsen it: if the modem's
|
||||
> RF-init (~12 s window) queries the SLPI for sensor/NV/registry data and gets silence, it parks OFFLINE
|
||||
> identically whether the SLPI is crashing or stopped. **The SLPI has never been healthy in any modem
|
||||
> test.** Path to actually test it: get the SLPI *genuinely* healthy — `auto_boot=false` + start after
|
||||
> the correct SDSP listener is ready + Lineage-compatible FastRPC/adsprpc behavior so the sensor stack
|
||||
> initializes and SNS QMI exposes — **then** test the modem. That is the first real modem test with a
|
||||
> functioning SLPI.
|
||||
|
||||
## 2026-06-29 late session — current SLPI frontier and phone state
|
||||
|
||||
**Phone is stable only with SLPI held offline.** Current handoff state:
|
||||
|
||||
- Kernel: `7.1.1-sdm845`, package `linux-postmarketos-qcom-sdm845-7.1.0_p20260629134644-r1.apk`.
|
||||
- WiFi regression fixed live: `CONFIG_ATH10K_SNOC=m`, `CONFIG_QCOM_PD_MAPPER=m`, `wlan0` present.
|
||||
- MSS/modem remoteproc running; modem still DMS op-mode `offline`, HW restricted `no`.
|
||||
- SLPI is **offline**, recovery disabled when touched.
|
||||
- `blueline-slpi-ready.service` and `hexagonrpcd-sdsp.service` are masked via
|
||||
`/etc/systemd/system/*.service -> /dev/null`. Do **not** unmask/start SLPI on boot until the
|
||||
next capture path is planned.
|
||||
|
||||
**Conversions landed:**
|
||||
|
||||
- `qcom_q6v5_pas`: `sdm845_slpi_resource_init.auto_boot = false`.
|
||||
- Blueline Lineage/downstream adsprpc memory conversion:
|
||||
fixed no-map `pil_adsp_mem@8c500000` for `adsp_pas`, reusable `adsp_region` for adsprpc,
|
||||
SDSP FastRPC VMIDs `HLOS + MSS_MSA + SSC_Q6 + ADSP_Q6`, and a new
|
||||
`qcom,msm-adsprpc-mem-region` helper in `drivers/misc/fastrpc.c` that preassigns the heap.
|
||||
- Qualcomm FastRPC userspace installed: `sdsprpcd`, `adsprpcd`, `libsdsp_default_listener.so`.
|
||||
- `blueline-slpi-ready.sh` now disables remoteproc crash recovery before its one-shot SLPI start.
|
||||
|
||||
**What the tests proved:**
|
||||
|
||||
1. The old TZ/heap failure is fixed. With the Lineage adsprpc memory conversion, the boot log shows
|
||||
the heap preassigned and SDSP FastRPC skipping duplicate hyp-assign. The previous
|
||||
`qcom_scm Assign memory protection call failed -22` / `SDSP fastrpc hyp-assign rejected by TZ`
|
||||
wall is no longer the current failure.
|
||||
2. Crash recovery had been hiding first-failure evidence. `auto_boot=false` only prevents PAS probe
|
||||
boot; remoteproc still relaunches after a crash unless `recovery` is set to `disabled`.
|
||||
3. Clean one-shot test **before** the SDSP capability-query patch:
|
||||
`sdsprpcd sensorspd` was started, SLPI recovery disabled, then SLPI started once. Result:
|
||||
`remoteproc2` became `crashed`. First useful kernel failure:
|
||||
`qcom,fastrpc ... Error: dsp information is incorrect err: -1`, followed by
|
||||
`watchdog received: SFR Init: wdog or kernel error suspected`.
|
||||
4. Kernel compatibility patch then added: for SDSP only, if the DSP utilities capability query is
|
||||
rejected with `-EPERM`, cache zero capabilities and continue. Rationale: Qualcomm userspace treats
|
||||
missing capabilities as "not supported"; Lineage downstream adsprpc does not make this optional
|
||||
query a hard sensors-PD gate.
|
||||
5. One-shot test **after** that patch caused SSH/network/UI to wedge during the SLPI start window
|
||||
before post-start logs could be collected. Host later saw the phone in fastboot. So the patch
|
||||
likely moved execution past the old `dsp information` gate, but we do **not** have a clean next
|
||||
failure line yet.
|
||||
|
||||
**Next real work:** do not keep hand-starting SLPI over SSH and hoping logs flush. Add a capture path
|
||||
that survives the wedge (pstore/ramoops, serial/console, persistent journal sync, or earlier in-kernel
|
||||
tracepoints), then repeat the one-shot `sdsprpcd sensorspd` test. In parallel, source-diff mainline
|
||||
FastRPC SDSP/sensors-PD against Lineage `drivers/char/adsprpc.c`, especially `INIT_ATTACH_SNS`,
|
||||
sensors static PD, listener1/geteventfd, service-location/PDR, and the domain/session handling around
|
||||
`sdsprpcd sensorspd`.
|
||||
|
||||
## 2026-06-29 (cont.) — FastRPC sensors-PD/servreg gap isolated (move #2 design)
|
||||
|
||||
Source-diff of mainline `drivers/misc/fastrpc.c` vs Lineage `drivers/char/adsprpc.c` around the
|
||||
sensors PD (`INIT_ATTACH_SNS`). This is the concrete next-conversion target (move #2); it is gated
|
||||
behind making the SLPI-start wedge survivably observable (move #1).
|
||||
|
||||
**Mainline sensors attach is a bare RPC** — `references/linux-7.1.1-sdm845-CANONICAL/drivers/misc/fastrpc.c:1735`:
|
||||
`fastrpc_init_attach(fl, SENSORS_PD)` sends `FASTRPC_RMID_INIT_ATTACH` (method 0, tgid arg), sets
|
||||
`fl->pd = 2`, returns. `grep` confirms mainline fastrpc.c has NO `pdr`/`servreg`/`service_location`/
|
||||
`spdname` wiring — only the generic `INIT_CREATE_STATIC` (method 8) ioctl (`:1340`/`:2193`).
|
||||
|
||||
**Lineage wraps the sensors PD in a servreg/PDR scaffold mainline lacks:**
|
||||
- `INIT_ATTACH_SENSORS` tags the client (`adsprpc.c:2250`):
|
||||
`fl->spdname = SENSORS_PDR_SERVICE_LOCATION_CLIENT_NAME;` (= `"sensors_pdr_adsprpc"`), `fl->pd = 2`.
|
||||
- At probe, gated on DT prop `qcom,fastrpc-adsp-sensors-pdr` (`adsprpc.c:4371`), it registers a
|
||||
service-location notifier: `get_service_location("sensors_pdr_adsprpc", "tms/servreg", &get_service_nb);`
|
||||
|
||||
So the production SLPI firmware expects the AP to track the sensors PD via `tms/servreg`
|
||||
(PDR/service-location) around `INIT_ATTACH_SNS`. Mainline supplies servreg via **pd-mapper**
|
||||
(userspace DB) + the glink reverse channel — so the open question is whether the sensors-PD servreg
|
||||
domain is present in pd-mapper's DB AND whether the reverse listener is up when `INIT_ATTACH_SNS`
|
||||
fires (ties to the S6 auto-boot ordering race).
|
||||
|
||||
**Conversion target (move #2):** register a PDR service-location lookup for the SDSP channel in
|
||||
`fastrpc_rpmsg_probe` (`fastrpc.c:2432`) when `domain_id == SDSP_DOMAIN_ID`, using mainline's existing
|
||||
`pdr_handle_alloc()` (`drivers/soc/qcom/pdr_interface.c`, already used by `apr.c`/`pmic_glink.c`) —
|
||||
mirroring Lineage's `get_service_location`. Service path TBD from the SLPI firmware expectation /
|
||||
pd-mapper config (likely an `"msm/slpi/sensors"`-style servreg domain).
|
||||
|
||||
**Capture (move #1) — state:**
|
||||
- Running kernel already has `CONFIG_PSTORE_RAM=y`/`PSTORE_CONSOLE`/`PSTORE_PMSG`/`PSTORE_COMPRESS`
|
||||
(verified on phone via `/proc/config.gz`). pstore backend is currently `(null)` because there is
|
||||
**no `ramoops` reserved-memory node** in our DTS (only apq8016/msm8992 boards carry one).
|
||||
- So ramoops capture needs only a reserved-memory node — NO config change. BUT the physical address
|
||||
must be bootloader-preserved across the watchdog→reset (the wedge lands the phone in fastboot).
|
||||
Do NOT guess an address (brick risk, no EDL net). Candidate preserved region to verify against the
|
||||
live map in `/proc/device-tree/reserved-memory/`: Lineage `removed_region@85fc0000`, or a top-of-RAM
|
||||
carve. Verify ABL preserves it across reset before flashing.
|
||||
- Safe, no-flash capture helper staged now: `tools/slpi-capture.sh` — snapshots dmesg to a timestamped
|
||||
file on the UFS rootfs and tails `/dev/kmsg` continuously through the SLPI start, so a partial
|
||||
capture survives the wedge on reboot even before ramoops is wired. Inert until invoked; does NOT
|
||||
start SLPI.
|
||||
|
||||
**Coordination note (2026-06-29):** another model is baking the RTC time-fix (pm8998 `rtc@6000`
|
||||
label + blueline `&rtc { allow-set-time; }`) — uncommitted in the canonical worktree. Do not edit
|
||||
`pm8998.dtsi`/`sdm845-google-blueline.dts` until that lands; the fastrpc conversion (a different
|
||||
file) and the ramoops node (`sdm845.dtsi` shared reserved-memory) can proceed collision-free.
|
||||
|
||||
**CORRECTION 2026-06-29 (post-Opus, supersedes the "pd-mapper missing" premise above):**
|
||||
pd-mapper is **IN-KERNEL** here, not userspace — `CONFIG_QCOM_PD_MAPPER=m` (module
|
||||
`qcom_pd_mapper`, aux devices `qcom_common.pd-mapper.{0,1,3}`), and it is **serving**
|
||||
(`wlan0` up proves the wlan PD is advertised). The earlier bug was that the in-tree
|
||||
`arch/arm64/configs/sdm845.config` carried upstream `CONFIG_QCOM_PD_MAPPER=n` and,
|
||||
being merged *last* by `merge_config.sh -m`, overrode pmaports' `=m` on every build
|
||||
→ wifi/sound/SLPI-sensor all lost their PD broker. Opus flipped that line to `=m`,
|
||||
rebuilt, reflashed → wifi + sound came back. See `build.md` gotcha #0.
|
||||
Crucially, the in-kernel `sdm845_domains[]` table **includes `slpi_root_pd` AND
|
||||
`slpi_sensor_pd`** — so the sensors PD *is* being advertised now. Therefore:
|
||||
1. The "servreg server missing → install userspace pd-mapper" premise in this section
|
||||
was WRONG (I installed then reverted the userspace `pd-mapper` apk; userspace pd-mapper
|
||||
is explicitly NOT used on blueline). Do not install it.
|
||||
2. The in-kernel fastrpc `pdr_add_lookup` conversion (move #2 above) is NOT the gap —
|
||||
the sensors-PD servreg server is present in-kernel. Drop that conversion.
|
||||
3. **Every prior SLPI conclusion (auto-boot ordering, fastrpc-attach EPERM, sar.cc:27)
|
||||
was drawn on a substrate with no sensors PD advertised.** Re-test SLPI on the
|
||||
post-pd-mapper (`=m`) kernel before any more fastrpc/SLPI source work.
|
||||
|
||||
## 2026-06-29 live result — Lineage adsprpc memory conversion moved the wall
|
||||
|
||||
**This was real progress, not log suppression.** We ported the blueline-specific
|
||||
Lineage/downstream adsprpc memory behavior into the 7.1.1 mainline tree:
|
||||
|
||||
- `adsp_mem` / `fastrpc_mem` are split like Lineage: fixed no-map
|
||||
`pil_adsp_mem@8c500000` for `adsp_pas`, dynamic reusable `adsp_region` for adsprpc.
|
||||
- The SDSP FastRPC node uses the Lineage VMID set:
|
||||
`HLOS + MSS_MSA + SSC_Q6 + ADSP_Q6`.
|
||||
- A new mainline `qcom,msm-adsprpc-mem-region` helper preassigns `adsp_region`
|
||||
before SDSP FastRPC probes, matching downstream `qcom,msm-adsprpc-mem` instead
|
||||
of letting mainline assign the heap late.
|
||||
|
||||
**Verified on hardware after deploy to slot A:**
|
||||
- `qcom_scm Assign memory protection call failed -22` count: **0**.
|
||||
- Boot log says the adsprpc heap was preassigned and SDSP FastRPC skipped duplicate
|
||||
hyp-assign.
|
||||
- With corrected manual ordering (listener watcher running before SLPI start),
|
||||
`SNS_REG_INIT` count after start: **0**.
|
||||
- The failure moved later to:
|
||||
`EF:sensor_process:1:sar.cc:27:chre_utils fatal`.
|
||||
|
||||
So the old wall is fixed: the TZ/heap `-22` and `SNS_REG_INIT` are no longer the
|
||||
current explanation. The current SLPI wall is downstream of attach/init:
|
||||
`hexagonrpcd -f /dev/fastrpc-sdsp -d sdsp -s` still reports
|
||||
`Could not attach to FastRPC node: Operation not permitted`, and the firmware later
|
||||
dies in SAR/CHRE. The next Lineage conversion target is therefore the **sensor/SAR
|
||||
FastRPC attach path**, not another attempt to mask the crash-loop.
|
||||
|
||||
Symptom: SLPI remoteproc loads and reaches `running`, then the firmware
|
||||
faults in CHRE / SAR sensor init (`sar.cc:27`, chre sar_utils) and
|
||||
crash-loops every ~10s. hexagonrpcd ends up STOPPED. SLPI runs fine on
|
||||
the LineageOS/Android side of the same phone, so the gap is on ours.
|
||||
|
||||
## Reference sources (corrected 2026-06-22 — was wrong before)
|
||||
|
||||
Earlier "Android reference" diffs were against `references/msm-google-crosshatch`
|
||||
= Google AOSP `kernel/msm` 4.9.270 (`android.googlesource.com/kernel/msm`).
|
||||
That is STOCK Google, not what the phone runs. The phone runs LineageOS 22.2
|
||||
(slot B). The authoritative references are now on disk and provenance-verified:
|
||||
|
||||
- LOS kernel SOURCE: `references/los-kernel-blueline-4.9/`
|
||||
= `LineageOS/android_kernel_google_msm-4.9` @ `lineage-22.2`, **4.9.337,
|
||||
commit b929452c5**. The blueline device tree
|
||||
(`LineageOS/android_device_google_crosshatch`, `lineage_blueline.mk`,
|
||||
PRODUCT_MODEL "Pixel 3") pulls this kernel via `lineage.dependencies`
|
||||
→ `kernel/google/msm-4.9`.
|
||||
Provenance match: boot.img string is `4.9.337-gb929452c5c49` (built 0615) —
|
||||
same sublevel, same commit hash. This is the literal source of the kernel
|
||||
running on slot B.
|
||||
- LOS DTB: `/home/casey/Downloads/los-blueline.dts` (decompiled).
|
||||
- LOS vendor partition: `pixel3-work/vndlos-mnt` (confirmed LOS via
|
||||
`vendor.lineage.health-service`, `hal_lineage_livedisplay_qti` — not stock).
|
||||
|
||||
Note: LineageOS does NOT ship its own qcom remoteproc/PIL/fastrpc drivers —
|
||||
it inherits `google/msm-4.9`. So crosshatch 4.9 is the correct driver-source
|
||||
base, just an older point release; the LOS-specific deltas are DTB + vendor HALs.
|
||||
|
||||
## Mainline vs LOS SLPI lifecycle — what is and isn't a real gap
|
||||
|
||||
LOS uses the downstream `qcom,pil-tz-generic` driver
|
||||
(`subsys-pil-tz.c` + `subsystem_restart.c` + `peripheral-loader.c`).
|
||||
Mainline uses `qcom,sdm845-slpi-pas` (`qcom_q6v5_pas.c` + `qcom_q6v5.c`).
|
||||
The DT property lists differ, but most differences are spelling, not behavior:
|
||||
|
||||
NOT gaps (verified):
|
||||
- **Identity** — PAS id 12, crash-reason SMEM 424, ssctl 0x16, firmware "slpi"
|
||||
all match mainline match-data (`qcom_q6v5_pas.c:1300-1312`).
|
||||
- **Lifecycle handshake** — LOS `qcom,gpio-err-fatal/err-ready/stop-ack/force-stop`
|
||||
resolve to `qcom,smp2pgpio-ssr-smp2p-3-in`, entry "slave-kernel",
|
||||
remote-pid 3 (`los-blueline.dts:10453`) — i.e. SMP2P, not raw GPIO. Mainline
|
||||
wires the same SMP2P as `slpi_smp2p_in` fatal/ready/handover/stop-ack +
|
||||
`slpi_smp2p_out` stop (`blueline-pmos-slpi.dts:3460-3469`). Same hardware.
|
||||
Behavioral proof: the subsystem reaches `running` (the `ready` SMP2P fired) —
|
||||
a handshake gap could not produce a post-boot crash.
|
||||
- **signal-aop / AOP load_state** — LOS `pil_notify_aop()` sends QMP
|
||||
`{class: image, res: load_state, name: slpi, val: on}`
|
||||
(`peripheral-loader.c:949-955,980`). Mainline `q6v5_load_state_toggle()`
|
||||
sends the BYTE-IDENTICAL string via `qmp_send` (`qcom_q6v5.c:31-32`), gated on
|
||||
`qmp_get` returning non-NULL (`:28,335`). Our DTS wires it: SLPI node has
|
||||
`qcom,qmp = <0x2f>` (`blueline-pmos-slpi.dts:3464`) → `qcom,sdm845-aoss-qmp`
|
||||
(`:4912`), and `CONFIG_QCOM_AOSS_QMP=y`. So this signal genuinely fires.
|
||||
- The LOS GPIO/proxy/mbox/pil-tz DT props are bindings of `subsys-pil-tz.c`/
|
||||
`subsystem_restart.c`, which mainline does not have. Pasting them onto our
|
||||
PAS node is inert — the PAS driver never reads them. (This was the earlier
|
||||
"reconcile the DT" dead end.)
|
||||
|
||||
THE REAL GAP (runtime power, SLPI-specific):
|
||||
- **`qcom,keep-proxy-regs-on`.** Mainline `qcom_pas_handover()` drops ALL proxy
|
||||
power the instant the remote signals handover — px/cx regulators, xo/aggre2
|
||||
clocks, and the lcx/lmx power domains (`qcom_q6v5_pas.c:386-397`,
|
||||
unconditional). LOS deliberately keeps SLPI's vdd_cx/vdd_mx rails enabled for
|
||||
the subsystem's whole life: `disable_regulators(..., reg_no_disable=true)`
|
||||
skips `regulator_disable()` when `keep_proxy_regs_on` is set
|
||||
(`subsys-pil-tz.c:501-502`, via `pil_remove_proxy_vote` `:583`).
|
||||
Strongest signal it's load-bearing: `qcom,keep-proxy-regs-on` is set on the
|
||||
SLPI node and NOT on adsp (`los-blueline.dts` ssc node ~3108 vs adsp ~3053).
|
||||
Hypothesis: SLPI's CHRE/SAR runtime needs the AP to hold CX/MX after boot;
|
||||
mainline yanking them post-handover collapses the rails → `sar.cc:27` fault.
|
||||
Matches the boot-then-crash signature exactly, and matches every prior win on
|
||||
this project (Android holds a resource mainline drops; port it).
|
||||
|
||||
(Secondary, same direction: `qcom,proxy-timeout-ms = 10000` — LOS holds proxy
|
||||
power 10s post-boot then unvotes; mainline releases at handover with no grace.)
|
||||
|
||||
FastRPC: LOS `qcom,msm-fastrpc-compute` (`adsprpc.c`, 7 CBs + per-CB IOMMU) vs
|
||||
mainline `qcom,fastrpc` (vmids + shared-dma-pool) — different drivers, but our
|
||||
mainline node already attaches (`/dev/fastrpc-sdsp` exists). Not the blocker.
|
||||
|
||||
## The patch (2026-06-22)
|
||||
|
||||
`pmaports .../linux-postmarketos-qcom-sdm845/slpi-keep-proxy-power-on.patch`
|
||||
(wired into APKBUILD `source=`, pkgrel 3→4, checksums updated).
|
||||
|
||||
Adds a per-SoC opt-in `keep_proxy_pds_on` flag to `qcom_pas_data`/`qcom_pas`,
|
||||
set true only for `sdm845_slpi_resource_init`, and makes `qcom_pas_handover()`
|
||||
return early (skipping the proxy teardown) when set. adsp/cdsp/modem unchanged.
|
||||
This mirrors downstream `keep-proxy-regs-on`: SLPI's cx/mx (lcx/lmx) stay
|
||||
powered past handover. Patch verified to apply against the build tree
|
||||
(tag 85f1df2a, identical to `references/linux-7.1.1-sdm845-CANONICAL`); dry-run rc=0.
|
||||
|
||||
Known trade-off (acceptable on a throwaway test bench): power is held across
|
||||
stop/SSR-restart too, so refcounts are not balanced on a clean shutdown — same
|
||||
"never turns the rails off" behavior downstream has. One online/boot attempt
|
||||
per cycle anyway.
|
||||
|
||||
## Status / next
|
||||
|
||||
- [done] References corrected + provenance-verified.
|
||||
- [done] Behavioral diff (LOS source vs mainline) — gap isolated to proxy power.
|
||||
- [done] Patch written and proven to apply.
|
||||
- [done] Folded into the **unified 7.1.1 tree as a commit** (no longer a `source=`
|
||||
`.patch`): `references/linux-7.1.1-sdm845-CANONICAL`, alongside the SLPI-enable DTS commit
|
||||
and the fastrpc SDSP tolerance commit. Built via pmbootstrap (see build.md).
|
||||
- [done] Deploy unified 7.1.1 kernel to slot A, reboot (2026-06-26, kernel r2 LLVM).
|
||||
- [DISPROVEN 2026-06-26] LIVE TEST: keep-proxy-power-on does **NOT** stop the
|
||||
crash-loop. On the booted r2 kernel SLPI still faults
|
||||
`err_qdi.c:456:EF:sensor_process:1:sar.cc:27:chre_utils fatal` and crash-loops
|
||||
every ~10s (observed crash #66→#72 in one capture). So the proxy-rails-held
|
||||
hypothesis is wrong — holding cx/mx past handover did not save it. The
|
||||
`qcom_ssc_sensors` module (CONFIG_QCOM_SSC_SENSORS=m) is **present and loaded**
|
||||
this build, and SLPI still crashes — so the SSC sensor *transport* being absent
|
||||
was not the (whole) cause either. The fault is inside the SLPI firmware's SAR
|
||||
(CHRE sar.cc) path itself: mainline isn't giving the firmware the SAR-sensor
|
||||
hardware/config it expects, and neither rails nor the SSC glink transport fix it.
|
||||
- [RESOLVED 2026-06-29] The `qcom_scm ... Assign memory protection call failed -22`
|
||||
/ `SDSP fastrpc hyp-assign rejected by TZ (-22)` wall is fixed by the Lineage
|
||||
adsprpc memory conversion above. Do **not** chase this as the current failure.
|
||||
- [current] Next: convert the remaining Lineage sensor/SAR FastRPC path:
|
||||
`sscrpcd sensorspd`, `libsdsprpc.so`, sensors PDR/service-location behavior, and
|
||||
whatever makes `INIT_ATTACH_SNS` attach cleanly instead of returning `EPERM`.
|
||||
|
||||
## 2026-06-27 (S6, Opus) — VERBOSE TRACE settles the registry theory: it's an auto-boot ORDERING race, not file serving
|
||||
|
||||
Built a **verbose hexagonrpcd** (the `-Dhexagonrpcd_verbose=true` meson option only
|
||||
exists in the newer source `pmos/_src/hexagonrpc` @ dd9ac70, NOT in the v0.4.0 tarball;
|
||||
our two patches — registry-nodes + method-29 fstat — are already applied in that working
|
||||
tree). Snapshotted it to `temp/hexagonrpcd/hexagonrpc-0.4.0.tar.gz` and built with verbose
|
||||
on. Deployed, captured two clean traces of what the SLPI actually requests.
|
||||
|
||||
**THE FINDING (instrument-proven, not inferred):**
|
||||
- Across both captures the SLPI made **zero** registry file requests (`openat`/`opendir`
|
||||
of real paths = 0) before crashing at `SNS_REG_INIT:36`. hexagonrpcd's reverse listener
|
||||
was attached and blocked in the listen loop, and **received nothing**.
|
||||
- The ~981 log lines were all hexagonrpcd's OWN failures to hold the device node:
|
||||
972 `No such file or directory` (`/dev/fastrpc-sdsp` vanishes while SLPI is down mid
|
||||
crash-cycle), 139 `Broken pipe`, 94 `Operation not permitted`, 9 `Resource busy`.
|
||||
- So **`SNS_REG_INIT:36` fails BEFORE the SLPI requests any file.** Serving the registry
|
||||
(staging path, dsp/sdsp libs, the gate, RestartSec=0) was answering a request that, at
|
||||
this crash point, never happens. The registry-serving theory is DEAD for SNS_REG_INIT.
|
||||
|
||||
**Root cause (circular, self-reinforcing):** the SLPI auto-boots at qcom_q6v5_pas module
|
||||
probe (~3s) → its registry-init needs a reverse-RPC to the AP → no listener registered yet
|
||||
(hexagonrpcd is userspace, ~33s) → fails instantly at SNS_REG_INIT → crashes → the crash
|
||||
tears down `/dev/fastrpc-sdsp` → reappears ~0.3s later → vanishes again. hexagonrpcd can
|
||||
never hold a stable attach long enough to register its listener; the node thrashes faster
|
||||
than it can attach + enter the listen loop. The rare earlier glimpses of `sar.cc:27` were a
|
||||
hammering hexagonrpcd momentarily aligning with one restart's request window — not a stable
|
||||
state. (Note: crash line drifts :36/:39 boot-to-boot; same failure, pre-file-request.)
|
||||
|
||||
**Real fix direction (Casey's architectural read, now proven):** the SLPI must NOT auto-boot
|
||||
at module probe. Hold it stopped until hexagonrpcd is attached AND in its listen loop, then
|
||||
start it once so its first registry request hits a ready listener. That's a kernel/remoteproc
|
||||
ordering change (`auto_boot=false` for the SLPI PAS, or defer module bind), NOT more userspace
|
||||
plumbing. Mirrors Android: SLPI firmware boots but sensor_process is triggered by the AP
|
||||
(chrecd `chre_slpi_start_thread`) AFTER the listener/daemon is up — confirmed by subagent that
|
||||
`INIT_ATTACH_SNS` does NOT itself start sensor traffic; chrecd does.
|
||||
|
||||
**Still-valid hygiene from S5/S6 (real, but does NOT fix SNS_REG_INIT):** the registry staging
|
||||
path bug (`cp -a .../registry/.` was nesting `registry/registry/`; fixed to flatten) and the
|
||||
`blueline-slpi-ready.service` gate pattern. `libchre_slpi_skel.so` IS needed for chrecd and was
|
||||
found at `pixel3-work/vndlos-mnt/dsp/sdsp/` (whole `dsp/sdsp/` tree: skel + nanoapps +
|
||||
fastrpc_shell_2); it's static vendor firmware, belongs in a firmware package, not a one-time copy.
|
||||
|
||||
## 2026-07-01 — crash-loop-vs-modem interaction found + fixed; SLPI ruled OUT of the modem 52 wall
|
||||
|
||||
Wired `blueline-slpi-ready.service` into the real boot chain (`modem-qrtr-ready.service`
|
||||
`After=`/`Wants=` it, mirroring Android's `init.qcom.devstart.sh` which sets
|
||||
`vendor.qcom.devup` — the property `modem-qrtr-ready` was modeled after — immediately after
|
||||
kicking SLPI). This surfaced a real regression: `blueline-slpi-ready.sh` disabled SLPI crash
|
||||
recovery for a "single-shot" start. When the SAR/CHRE fault crashed it with recovery disabled,
|
||||
the driver had nowhere to go — glink-edge was left half torn down, storming
|
||||
`intent request timed out` every ~10s indefinitely and taking the modem's own QMI/QRTR
|
||||
transport down with it (`endpoint hangup`). Android never disables recovery for this
|
||||
(`ssr_setup`/`ssr_diag` in `init.sdm845.rc` exist precisely to handle ongoing SSR cleanly).
|
||||
|
||||
**Fix:** recovery stays enabled through the boot attempt (so any crash tears down/rebuilds
|
||||
glink cleanly via the kernel's own SSR path — verified: crash→recovering→running cycling with
|
||||
zero storm across 50 crashes in one test), then the script catches SLPI in its brief `running`
|
||||
window and issues a clean `echo stop` to park it — one attempt, not an infinite crash-loop,
|
||||
since the modem does not benefit from SLPI staying up (see below).
|
||||
|
||||
**Decisive result, three separate live boots:** with SLPI crash-looping 50 times in ~7 minutes,
|
||||
with SLPI crash-free and running continuously, and with SLPI cleanly parked/offline —
|
||||
`mmcli -m 0 --enable` returned byte-for-byte identical `QMI protocol error (52): 'DeviceNotReady'`
|
||||
every time. **SLPI state has zero measurable effect on the modem RF-arm wall.** This closes
|
||||
"Subsystem coordination (SLPI PD state?)" from `los-mss-translated.c`'s delta list as a modem
|
||||
blocker. SLPI's own SAR/CHRE crash (this file, above) remains real and unfixed, but is now
|
||||
fully decoupled from modem bring-up — see `modem.md` TOP LEAD #12 for where the modem hunt
|
||||
went next (`dms-ui-ready.py`, DMS 0x0064).
|
||||
132
saf/device/touch.md
Normal file
132
saf/device/touch.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# PAF · Touch — ST FTS, reset polarity, GPI-DMA, tap-to-wake
|
||||
|
||||
## Current glass result — 2026-07-11
|
||||
|
||||
On kernel `7.1.1-sdm845-g56b0a053bef8`, daytime touch works and the FTS
|
||||
recognizes DBLTAP. The current defect is downstream of gesture recognition:
|
||||
the first double tap can prepare the panel and then immediately unprepare it
|
||||
(green flash then black); a second double tap wakes the display. The first
|
||||
half-wake is kernel-trace confirmed. Treat DT2W as **detected but unreliable**
|
||||
until the screen-toggle/DPMS prepare-unprepare ping-pong is fixed. This is not
|
||||
the old IRQ/FIFO bug.
|
||||
|
||||
Status 2026-07-10 (morning): TAP-TO-WAKE WORKS, verified on glass by
|
||||
Casey on kernel d7a8f7dc9. FTS probes clean, gesture mask armed at boot,
|
||||
double-tap wakes from armed sleep, touch alive after wake, hypridle 60s
|
||||
idle screen-off enabled. The one remaining suspend-path defect was the
|
||||
IRQ handler, not the controller — see the closed item below.
|
||||
|
||||
## Suspend-path doctrine (2026-07-10, learned the hard way)
|
||||
|
||||
- `gesture_enabled` MUST be 1 before any DPMS-off. Unarmed suspend takes
|
||||
the SLPI-handoff branch (fts_suspend_work): controller handed to the
|
||||
PARKED sensor island + IRQ disabled → resume races → level IRQ storm
|
||||
(millions on IRQ 148) → kernel kills the IRQ → touch dead until driver
|
||||
rebind or reboot. With SLPI's sar.cc wall unfixed, the handoff target is
|
||||
unavailable — gesture-armed suspend (AP keeps the bus) is the only safe mode.
|
||||
- The compiled gesture_mask sysfs is the STASH-THEN-APPLY variant:
|
||||
`echo "01 20" >` stages only; the following `cat` applies (updateGestureMask)
|
||||
and sets gesture_enabled=1. blueline-dt2w.sh does both since de4fe40.
|
||||
- Live recovery from a dead touch IRQ, no reboot:
|
||||
`echo 2-0049 > /sys/bus/i2c/drivers/fts/unbind && echo 2-0049 > .../bind`
|
||||
— but Hyprland then holds stale input handles (taps flow at evdev level,
|
||||
UI ignores them); a session restart or reboot is needed after rebind.
|
||||
- VERIFIED 2026-07-10 (clean boot-time arm, Casey on glass): daytime touch
|
||||
works, and touch SURVIVES a full sleep/wake cycle — the regression is
|
||||
closed. Wake was power-button; double-tap did NOT wake.
|
||||
- CLOSED 2026-07-10 (kernel d7a8f7dc9) — tap-to-wake works on glass. The
|
||||
"controller never enters gesture scan" theory was WRONG: enterGestureMode
|
||||
runs and succeeds at every armed suspend (Sense OFF → gesture mode →
|
||||
enableGesture DONE in dmesg). The real defect was fts_interrupt_handler:
|
||||
during armed sleep bus_refmask==0, so the bus-ref gate refused every IRQ
|
||||
and returned WITHOUT reading the FIFO. Level IRQ line stayed asserted →
|
||||
refired forever (the "582k IRQs" were the unserviced line, not full-rate
|
||||
scanning), gesture events were dropped unread (why double-tap never
|
||||
woke), and a bogus release call in the refusal branch logged one pr_err
|
||||
per IRQ — 85k lines in one screen-off window, enough printk to freeze
|
||||
the phone on long sleeps (2026-07-10 morning freezes). Fix: service the
|
||||
FIFO without taking a bus ref when gesture_enabled && sensor_sleep
|
||||
(taking one would queue a resume per IRQ), drop the bogus release,
|
||||
ratelimit the bus-ref pr_err. The port's 30177cb7e (AP keeps bus in
|
||||
gesture mode) had changed the ownership model without teaching the IRQ
|
||||
path — classic lost-handler.
|
||||
|
||||
## The hardware truth (settled — do not re-litigate)
|
||||
|
||||
- This unit is the **LG panel + ST FTS** variant: FTS controller at
|
||||
**0x49 on i2c-2 (888000 geni SE)**. Settled from per-unit persist
|
||||
calibration + LOS init.blueline.rc.
|
||||
- The Samsung `sec_ts@48` nodes in the downstream DTBO (fragment@81,
|
||||
`sec,reset_gpio`, …) belong to the OTHER hardware variant. **Never cite
|
||||
fragment@81 as evidence for our touch config** — that mistake shipped
|
||||
ba0620d5c and killed touch for a day.
|
||||
- Pins: reset tlmm 99 (**active-low RESETB**), IRQ tlmm 125 (level low),
|
||||
AP↔SLPI bus switch tlmm 136.
|
||||
|
||||
## Reset polarity — the post-mortem
|
||||
|
||||
The ported vendor driver (`drivers/input/touchscreen/stm/`) uses **gpiod
|
||||
logical assert semantics**: acquires reset `GPIOD_OUT_HIGH` ("asserted"),
|
||||
pulse is `set(1)` → 20ms → `set(0)` → 50ms, and its own comment says
|
||||
"active-low reset" (fts.c ~4320). Therefore the DT flag must be
|
||||
**`GPIO_ACTIVE_LOW`**, so logical assert = physical LOW.
|
||||
|
||||
With `GPIO_ACTIVE_HIGH` (ba0620d5c, 2026-07-09 14:02) deassert drives the
|
||||
pin physically LOW → RESETB held forever → controller NACKs every I2C
|
||||
access → probe fails after 3 reset attempts. Signature in dmesg:
|
||||
`gpi ... Error in Transaction: code=0x10 status=0x40` on EVERY read
|
||||
(including the first 8-byte poll), `fts_system_reset: ERROR 80000002`,
|
||||
`Probe Failed!`. Nobody noticed for a day because every boot in that window
|
||||
had a black display. f3c5f5a11 reverts to ACTIVE_LOW.
|
||||
|
||||
**Triage rule:** all-reads-fail = controller not out of reset / not powered
|
||||
/ bus switch wrong — a dead chip, not a DMA problem. Large-read-only
|
||||
failure = the (historic) GPI-DMA wall below.
|
||||
|
||||
## GPI-DMA wall (historic — status needs one clean-boot check)
|
||||
|
||||
The 888000 SE is GPI-mode-only (its SE firmware has FIFO disabled — no
|
||||
FIFO fallback exists; verified in i2c-qcom-geni probe). Historically small
|
||||
reads worked and the first large read (209-byte readSysInfo) died with
|
||||
`MSM_GPI_TCE_UNEXP_ERR`. Instrumentation ff6e6e2e8 logs completion status +
|
||||
error-log register on any recurrence.
|
||||
**ANSWERED 2026-07-10, first clean f3c5f5a11 boot: the wall is GONE.**
|
||||
Zero `gpi ... Error in Transaction` lines in dmesg; the driver read the
|
||||
105,140-byte firmware file and ran CX/Panel CRC verification over the bus
|
||||
(`Fw Update Finished! error = 00000000`), FTS input device registered.
|
||||
Large GPI-DMA transfers on the 888000 SE work. Whatever ate the 209-byte
|
||||
read in the 2026-07-08 era was fixed by the intervening driver/power work
|
||||
or was reset-state fallout all along; if it ever recurs, the ff6e6e2e8
|
||||
telemetry will say why. Deep diagnosis archive: ../CLAUDE.md "Touch /
|
||||
FTS-on-mainline key facts".
|
||||
|
||||
## Gesture / suspend design (in-tree, working as of 07-09 morning)
|
||||
|
||||
- 08687bec9 power+reset sequencing, e31aa9da7 prototype fix,
|
||||
30177cb7e keep AP bus ownership during gesture mode,
|
||||
3d9eb1028 sensor_sleep bookkeeping in gesture-mode suspend.
|
||||
- Gesture mode: on suspend the driver keeps the controller alive for
|
||||
double-tap; `XF86WakeUp` → Hyprland bind → `blueline-screen-toggle`
|
||||
(see display.md). dt2w service wiring: `blueline-dt2w.service` in
|
||||
rootfs-overlay (uncommitted edits pending as of 2026-07-10).
|
||||
- **Proximity gate (2026-07-20):** `blueline-screen-toggle` checks
|
||||
`net.hadess.SensorProxy` `ProximityNear` over D-Bus before dispatching
|
||||
DPMS on; if blocked (phone in pocket) the wake is suppressed and logged,
|
||||
the FTS gesture still fires XF86WakeUp either way. Fails open if the
|
||||
D-Bus query errors, so a dead sensor proxy never permanently blocks wake.
|
||||
Requires `iio-sensor-proxy` running, which is now always-on (see slpi.md).
|
||||
This closes half the "hot pocket" problem; the other half (screen
|
||||
already on + locked, phone pocketed) is `blueline-proximity-lock`
|
||||
(`overlays/systemd-user/`), a `monitor-sensor --proximity` watcher that
|
||||
blanks via the same `blueline-screen-toggle off` path when locked.
|
||||
|
||||
## THE GOAL (unchanged): one FTS driver = daytime touch + tap-to-wake
|
||||
|
||||
Like LOS: FTS drives the controller when awake; on suspend it hands the
|
||||
controller to the SLPI over the tlmm 136 switch, where the
|
||||
`sns_touch_gesture` nanoapp (SLPI's own i2c bus 5) watches for double-tap
|
||||
and wakes the AP. stmfts (mainline driver) can never do this — no switch,
|
||||
no handoff; it is fallback-only. Two tracks converge:
|
||||
1. FTS daytime path — WORKING modulo the polarity fix verify.
|
||||
2. SLPI-side tap-to-wake — unblocked by the sar.cc fix (see slpi.md), but not
|
||||
required for the current AP-side gesture design.
|
||||
68
saf/federation/01-node-enrollment.md
Normal file
68
saf/federation/01-node-enrollment.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Node enrolment
|
||||
|
||||
Souveraine can already exchange signed events between configured peers. It cannot yet add a device as a node of an existing agent. Copying `~/.souveraine/agents/<id>/` to a phone is therefore a migration shortcut, not federation: it copies the agent's private key and gives two machines indistinguishable authority.
|
||||
|
||||
This document is the design-of-record for closing that gap. It narrows the existing federation sketch into a first implementation boundary; it does not claim that the ceremony exists today.
|
||||
|
||||
## What a node is
|
||||
|
||||
One agent can have several independently operating nodes. They share an agent root identity but not a private signing key.
|
||||
|
||||
| Concern | Stored value | Why |
|
||||
|---|---|---|
|
||||
| Being | agent root public key | Stable personhood across every fork |
|
||||
| Node | immutable random `node_id` | Stable Git and protocol identity; never derived from a hostname |
|
||||
| Hardware | RedFlag device public-key fingerprint | Attestation and revocation target |
|
||||
| Display | label such as `phone` or `hearth` | Human-readable and renameable |
|
||||
| Memory | `nodes/<node_id>` Git ref | Independent history for later reconciliation |
|
||||
|
||||
`machine-id` and hostname are useful evidence and display hints, but neither is suitable as the durable node identifier. A reinstall can change the first; a rename can change the second.
|
||||
|
||||
## The ceremony
|
||||
|
||||
The device being added creates its node key locally. Its private key never leaves it.
|
||||
|
||||
1. **Invite.** The authority creates a single-use invitation for an agent and requested label.
|
||||
2. **Request.** The device creates `node_id` and node key, then returns a CSR containing its public key and RedFlag hardware public-key fingerprint.
|
||||
3. **Commission.** The authority signs the tuple `(agent root, node_id, node public key, hardware fingerprint, label, issued-at, expiry/revocation policy)`.
|
||||
4. **Join.** The device stores the commission, creates or receives its `nodes/<node_id>` memory ref, and adds the federation endpoint.
|
||||
5. **Operate.** Each event carries the node signature and commission. Git moves memory; the event stream only says that a branch changed.
|
||||
|
||||
The future command shape is intentionally small:
|
||||
|
||||
```text
|
||||
souveraine node invite --agent Annie --label phone
|
||||
souveraine node join --invite <one-time-token>
|
||||
souveraine node status
|
||||
souveraine node revoke <node-id>
|
||||
```
|
||||
|
||||
The command names are provisional. The resulting wire formats and storage paths must be versioned before a phone depends on them.
|
||||
|
||||
## What exists today
|
||||
|
||||
- Machine and agent Ed25519 keys exist.
|
||||
- Federation transport signs each event with the machine key.
|
||||
- Peer keys are explicit configuration and inbound events now require one of those configured keys.
|
||||
- Git-backed memory commits locally, but has no push, fetch, branch status, or reconciliation commands.
|
||||
|
||||
## What is deliberately not done
|
||||
|
||||
- No root-key commissioning authority or CSR format.
|
||||
- No RedFlag hardware-key integration or revocation list.
|
||||
- No per-node key storage, commission validation, or event-chain validation.
|
||||
- No memory remote or branch-per-node sync.
|
||||
- No archivist reconciliation policy.
|
||||
|
||||
Until these land, `packaging/deploy-phone.sh` defaults to copying no private key. `--clone-identity` remains only for an explicit same-instance migration; it is not an enrolment workflow.
|
||||
|
||||
## Implementation order
|
||||
|
||||
1. Define versioned `NodeId`, CSR, and commission structs plus round-trip/signature tests.
|
||||
2. Add a local node-key store and `node status`; do not connect it to deployment yet.
|
||||
3. Implement invite/join and persist trusted commissions.
|
||||
4. Replace shared agent-key authentication for `reach` with node key plus commission verification.
|
||||
5. Add memory remotes and `nodes/<node_id>` divergence status.
|
||||
6. Let the archivist reconcile branch state according to Cloister policy.
|
||||
|
||||
This preserves the intended architecture: the phone is a real branch of the agent, not a thin client and not a copied private key.
|
||||
50
saf/identity/01-seed-identity.md
Normal file
50
saf/identity/01-seed-identity.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# Identity
|
||||
|
||||
Two keys, both Ed25519. One belongs to the machine, one belongs to the agent. Keeping them apart is what lets the substrate tell "this came from here" apart from "this was done by her," and prove either.
|
||||
|
||||
A key names and authenticates; it does not isolate a process. The node-local
|
||||
Unix account and the health check that proves the agent is actually running as
|
||||
it live in [agent principals](02-agent-principal.md).
|
||||
|
||||
## The two keys
|
||||
|
||||
The machine key lives at `~/.souveraine/seed-id/`. One per install. It signs the transport: this event left this machine.
|
||||
|
||||
The agent key lives at `~/.souveraine/agents/{id}/seed/`, beside her memory. It signs her acts. Today the memory subtree — including this private key — is copied when a same-instance migration is explicitly requested. That is a temporary implementation, not the node model: a copied private key cannot distinguish a legitimate fork from a stolen duplicate.
|
||||
|
||||
Both are made the same way. `SeedId::load_or_generate` reads the 32-byte private key if it is there, or makes one from the OS random source and writes it `0600`. The struct only ever offers `sign` and `verify`. No path hands out the private key, and none sends it anywhere.
|
||||
|
||||
## The glyph
|
||||
|
||||
From the first two bytes of the public key, four shapes:
|
||||
|
||||
```
|
||||
◇ ◆ ○ ● △ ▲ ▽ ▼ □ ■ ◐ ◑ ◒ ◓ ☆ ★
|
||||
```
|
||||
|
||||
Same key, same four, always. A face to know her by in a list, not a lock. Two that match are told apart by their full key, not their glyph.
|
||||
|
||||
## Reaching and consulting
|
||||
|
||||
When one agent summons another, the request is signed by the agent key, and the receiver checks it against the key it holds for her.
|
||||
|
||||
If it matches, it is the same being reaching across machines. No gate. If it does not, it is someone else, and consent is asked (`authorized-summoners.md`). A bad signature is dropped.
|
||||
|
||||
The payload can claim whatever it likes about its intent. The signature decides, not the claim. The signed bytes are fixed: request, tool, target, prompt, joined by newlines. Change any field after signing and the check fails. The code is `src/core/identity/summon.rs`.
|
||||
|
||||
## The node model we are building toward
|
||||
|
||||
An agent root key names the being. A device must have its own non-exportable node key, preferably the hardware-bound key RedFlag already knows. Adding a phone is a commissioning ceremony, not a directory copy:
|
||||
|
||||
1. The phone generates a local node key and a stable, random `node_id`.
|
||||
2. It presents a CSR to the agent's designated commissioning authority.
|
||||
3. The authority signs a commission binding agent root, node key, `node_id`, and a human label such as `phone`.
|
||||
4. The node writes to its own Git ref, `nodes/<node_id>`. The label is display metadata; hostname and machine ID are not Git identity.
|
||||
|
||||
The root key does not travel to a federated node. The commission lets a node sign its own events and later lets the archivist reconcile its branch with the other branches of the same agent. This protocol is not implemented yet; see [node enrolment](../federation/01-node-enrollment.md).
|
||||
|
||||
## Open edges
|
||||
|
||||
The current shared agent key is a migration convenience, not a secure federation mechanism. It must be replaced by per-node commissions before memory remotes are used for independent nodes. `packaging/deploy-phone.sh` now excludes both machine and agent private keys by default; `--clone-identity` is deliberately noisy because it is only for same-instance migration.
|
||||
|
||||
Replay is held off by request-id and a sixty-second window. The in-flight record lives in memory and does not survive a restart. Same task tracks the hardening.
|
||||
342
saf/identity/02-agent-principal.md
Normal file
342
saf/identity/02-agent-principal.md
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
# Agent principals
|
||||
|
||||
An agent is not a process borrowing the human's account. She has a logical
|
||||
identity that can cross machines and, on every admitted machine, a local Unix
|
||||
principal that lets the kernel tell her work from the human's and from another
|
||||
agent's.
|
||||
|
||||
This distinction is not implemented yet. Today the server, its agents, and
|
||||
their tools normally run as the logged-in human. The per-agent SeedID exists,
|
||||
but a signature cannot repair a shared UID: `SO_PEERCRED` still sees the same
|
||||
caller and ordinary file permissions still collapse every agent into the same
|
||||
authority.
|
||||
|
||||
## Three identities that must not be collapsed
|
||||
|
||||
| Identity | Meaning | Lifetime |
|
||||
|---|---|---|
|
||||
| agent ID + agent SeedID | which being this is | stable across renames and admitted nodes |
|
||||
| node commission | which admitted body is speaking for her | local to one commissioned device |
|
||||
| Unix account | which local processes and files the kernel attributes to her | local to one device |
|
||||
|
||||
The numeric UID is not federated. An agent can be UID 1003 on one admitted
|
||||
body and UID 991 on another while remaining herself, because both node-local
|
||||
account mappings point to the same admitted agent identity. Copying
|
||||
`/etc/passwd`, a UID number, or a private key is not commissioning.
|
||||
|
||||
There is a fourth identity already on the machines: the `souveraine` system
|
||||
account. It belongs to the machine tier and runs `souveraine-machined`. It is
|
||||
not Souvie, not a default agent, and not a convenient shared account for all
|
||||
agents. The name is reserved.
|
||||
|
||||
## The starting set, by role
|
||||
|
||||
A human's devices retain that human's own account. Agents do not borrow it.
|
||||
|
||||
| Role | Required local account |
|
||||
|---|---|
|
||||
| the default Souveraine agent on a fresh system | `souvie` |
|
||||
| the human's primary personal agent, if she is admitted | her own name |
|
||||
| any further named resident agent | its own name |
|
||||
|
||||
Only the first is shipped. `souvie` is what a fresh Souveraine install admits,
|
||||
because a Souveraine system running its own agent as the human is the state
|
||||
this chapter exists to end. Every other admission is a deliberate act by the
|
||||
human on the body in question — including the primary personal agent, whose
|
||||
name and cadence are that human's to choose. This chapter describes the
|
||||
mechanism; which beings a particular deployment admits is that deployment's
|
||||
record to keep.
|
||||
|
||||
"Each node" includes a development laptop. The laptop is not a development
|
||||
exception, and a phone is not a special appliance case. When a resident agent
|
||||
runs on either body, her turn, subconscious pass, memory access, and tools
|
||||
must cross the worker boundary under the mapped account.
|
||||
|
||||
The display name is editable. The agent ID, SeedID, and admitted account
|
||||
mapping are not derived again when the display name changes. Account names are
|
||||
chosen once, checked against the local account database and reserved names,
|
||||
then stored in a root-owned node-local mapping. A collision is shown to the
|
||||
administrator; creation must not silently attach an agent to an existing
|
||||
human or daemon account or invent a suffix that nobody noticed.
|
||||
|
||||
Souvie is the default, not the only possible agent. Restoring an existing
|
||||
Souvie must join her existing logical identity rather than minting another
|
||||
Souvie because the display name matches. A further agent is admitted onto each
|
||||
body where she is meant to act. Her local Unix account is new on that body;
|
||||
her being is not.
|
||||
|
||||
Primary and subconscious are processing positions inside an agent today.
|
||||
Creating an agent therefore does not automatically create a second Unix user
|
||||
for her second cadence. The cadences share her principal unless the human and
|
||||
the system later admit one as an independently authorized agent. The account
|
||||
boundary follows authority, not the number of internal thought streams.
|
||||
|
||||
## Resident agents and borrowed-user modes
|
||||
|
||||
Not every named mode needs a Unix account. A hosted or borrowed-user mode runs
|
||||
under the human's account rather than claiming a fully admitted resident
|
||||
identity. That is a deliberate posture, not the same failure as a `dedicated`
|
||||
agent accidentally running as the human.
|
||||
|
||||
The durable agent record therefore carries a principal intent, not one vague
|
||||
boolean:
|
||||
|
||||
| Intent | Expected runtime | Meaning |
|
||||
|---|---|---|
|
||||
| `dedicated` | the named agent account | full node admission and kernel-backed identity |
|
||||
| `borrowed-user` | the invoking human account | a hosted mode using authority the human process can already reach |
|
||||
|
||||
Promoting a borrowed mode to a resident agent is an admission ceremony;
|
||||
changing a UI label or model does not do it. Which agents in a given
|
||||
deployment hold which intent is that deployment's record, not this chapter's.
|
||||
|
||||
A borrowed-user mode must know, in every model request, that readable does not
|
||||
mean owned or shareable. Its operating posture is stricter:
|
||||
|
||||
- the human's files, groups, sockets, credentials, and decrypted home are
|
||||
borrowed reach, not the mode's personal territory;
|
||||
- do not widen mode bits, ACLs, group membership, links, remotes, publication,
|
||||
or sharing outside the explicit task without the human's confirmation;
|
||||
- prefer the named workspace or a narrow temporary copy over exploring the
|
||||
rest of the human home;
|
||||
- never read or disclose a resident agent's private memory merely because the
|
||||
shared human UID permits it;
|
||||
- attribute actions to a borrowed mode acting through the human's principal,
|
||||
not to an independently admitted agent.
|
||||
|
||||
This caution is context and policy, not a substitute for containment. A
|
||||
borrowed process still has the human's kernel permissions; sensitive verbs must
|
||||
remain behind the same capability and consent gates.
|
||||
|
||||
## The live principal fact
|
||||
|
||||
The intent in `agent.json` is durable. The live posture is observed afresh from
|
||||
the selected agent or mode, node mapping, process credentials, and worker unit.
|
||||
The trigger may expose a convenient `operating_as_user` boolean, but that
|
||||
boolean is only a projection of the facts and is never an authorization input.
|
||||
|
||||
The runtime projection contains at least:
|
||||
|
||||
```text
|
||||
agent_id, display_name, principal_intent
|
||||
expected_account, effective_account, effective_uid
|
||||
node_id, worker_pid, trigger, observed_at
|
||||
posture = isolated | borrowed-user | acting-as-human | drift
|
||||
```
|
||||
|
||||
It is injected as a fresh system block into **every model request**, including
|
||||
primary and subconscious passes, subagents, resumed conversations, and later
|
||||
tool rounds after a mode switch. It is not ordinary conversation history and
|
||||
cannot be microcompacted, replayed from an old session, or taught back as
|
||||
assistant speech.
|
||||
|
||||
A machine-generated cache may be exposed as
|
||||
`/run/souveraine/agent-principals/{agent_id}.json` and a Markdown rendering for
|
||||
inspection. It does not belong in the agent-writable, Git-backed
|
||||
`system/dynamic/` memory tree: an awareness file the agent can edit cannot be
|
||||
the source of who the kernel says she is. The prompt projection rechecks the
|
||||
effective credentials when it reads the cache. Authorization always checks
|
||||
the actual peer and process credentials again at the verb boundary.
|
||||
|
||||
## Creation is logical creation followed by admission
|
||||
|
||||
The current creation path makes a UUID, an agent seed, memory directories, an
|
||||
`agent.json`, and a paired subconscious tree under the human's
|
||||
`~/.souveraine`. It has no privileged account step. The complete operation is
|
||||
two related but separately observable transitions:
|
||||
|
||||
1. **Create the logical agent.** Establish or import the agent ID and SeedID,
|
||||
initialize her memory, and record the requested local account name. Import
|
||||
must verify identity before it mutates local authority.
|
||||
2. **Admit her to this node.** A narrow system-tier executor creates or adopts
|
||||
the approved Unix account, writes the agent-to-principal mapping, installs
|
||||
the node commission, gives only her data to that account, and starts her
|
||||
worker as that UID.
|
||||
|
||||
The split makes failure honest. An agent can exist while admission is pending,
|
||||
but the UI and API call that state `unadmitted`; they do not show a green agent
|
||||
whose turns still run as the human. Personal- and step-up-tier verbs remain
|
||||
closed until admission and execution checks pass.
|
||||
|
||||
Agent creation should ask for the authority shape, not merely a name and
|
||||
model:
|
||||
|
||||
- create a new identity or admit an existing one;
|
||||
- principal intent and, for a resident, the required local account name;
|
||||
- which node and role are being admitted;
|
||||
- whether the privileged admission completed;
|
||||
- which operating skill was projected from the admitted capabilities.
|
||||
|
||||
Fresh installation proposes Souvie and `souvie`. It must not confuse that
|
||||
with the package-created `souveraine` machine account. Adding any further
|
||||
agent uses the same path; there is no hand-written exceptional setup for a
|
||||
particular deployment's names.
|
||||
|
||||
## The privileged edge
|
||||
|
||||
QML, the TUI, and the ordinary server must not run `useradd`, rewrite
|
||||
`/etc/passwd`, or recursively `chown` a human home. They submit one exact
|
||||
admission request to one package-owned system executor. That executor owns:
|
||||
|
||||
- account creation and collision refusal;
|
||||
- a locked password and non-login shell unless interactive login is later
|
||||
admitted deliberately;
|
||||
- a stable home/state root tied to the agent ID, not the display name;
|
||||
- the root-owned agent-ID, SeedID, node-ID, account-name, UID mapping;
|
||||
- precise ownership changes inside that agent's root only;
|
||||
- install, start, stop, and inspection of the per-agent worker unit;
|
||||
- idempotent repair when the requested mapping already exists and agrees.
|
||||
|
||||
The low-level account primitive may be `systemd-sysusers` or an equally narrow
|
||||
account API. That implementation choice does not create a second authority:
|
||||
the admission executor remains the only writer and reports the resulting NSS
|
||||
facts rather than assuming its command worked.
|
||||
|
||||
### Admission is a ceremony
|
||||
|
||||
The executor is not the interface. A binary reachable only by someone who
|
||||
already knows its name is not a way for a human to grant anything, and `sudo`
|
||||
in a terminal that already trusts the caller is not a decision — it is a
|
||||
formality. Admission is a highly user-directed act and has to be asked for as
|
||||
one.
|
||||
|
||||
It has two occasions:
|
||||
|
||||
- **initial setup**, where a fresh install admits the default Souveraine agent
|
||||
as `souvie`, because that is the shipped default state and the only
|
||||
admission the product makes on its own; and
|
||||
- **on the human's request**, under specific conditions, for any further
|
||||
agent.
|
||||
|
||||
Both cross the same gate. The grant is a polkit action carrying a written
|
||||
reason that names the agent and the account, and it requires step-up
|
||||
authentication at the moment of the grant — including from a caller who could
|
||||
already reach root by other means. The cost is the point: giving an agent her
|
||||
own UID on this body is a deliberate authentication, never an inherited one.
|
||||
|
||||
Removing an agent record, removing a node admission, deleting a Unix account,
|
||||
and destroying memory are four different operations. None implies another.
|
||||
Decommission stops the worker and revokes the node commission first. Account
|
||||
and data deletion require explicit, separately named confirmation; archival
|
||||
is the normal default.
|
||||
|
||||
## One UID requires one execution boundary
|
||||
|
||||
A single server process cannot truthfully execute three admitted agents as
|
||||
three UIDs. Per-agent accounts therefore require a process boundary:
|
||||
|
||||
- a controller may inventory agents and route requests;
|
||||
- each admitted agent has a worker process running as her mapped UID;
|
||||
- her memory, seed interface, conversation state, and tool execution are
|
||||
reached inside that worker's boundary;
|
||||
- controller-to-worker IPC identifies the peer with `SO_PEERCRED` and binds
|
||||
the logical agent ID to the admitted UID;
|
||||
- cross-agent consultation is a signed and policy-visible request, not direct
|
||||
filesystem access through a shared process.
|
||||
|
||||
The controller is not allowed to become a universal memory reader merely
|
||||
because it launches workers. Filesystem ownership, service sandboxing, and
|
||||
capability tokens should leave it only the routing and lifecycle access it
|
||||
actually needs.
|
||||
|
||||
Existing data under a human's `~/.souveraine` is a compatibility layout. The
|
||||
admission migration must stop the agent, copy or move only that agent's UUID
|
||||
root, preserve and verify the Git repository, install ownership atomically,
|
||||
then start the worker under the admitted UID. Never apply a broad recursive
|
||||
ownership change to `~/.souveraine` or a human home. Until migrated, health
|
||||
labels the agent `acting-as-human`.
|
||||
|
||||
## Account and authority health
|
||||
|
||||
Agent Health answers two separate questions: *does she have an admitted
|
||||
principal?* and *is the live work actually running as it?* A directory and a
|
||||
row in `/etc/passwd` are not runtime proof.
|
||||
|
||||
The inspection result is structured and available to setup, the agent manager,
|
||||
the TUI, the Panel, and `describe`. At minimum it reports:
|
||||
|
||||
- logical agent ID, SeedID fingerprint, and node commission;
|
||||
- expected account name and UID from the root-owned mapping;
|
||||
- NSS account existence, home/state root, shell, password-lock state, and
|
||||
narrowly allowed groups;
|
||||
- owner and mode of the memory, seed, conversation, socket, and runtime roots;
|
||||
- worker unit, PID, effective UID/GID, cgroup, and executable;
|
||||
- whether live peer credentials match the logical-agent mapping;
|
||||
- whether any process handling the turn or its tools is the human,
|
||||
`souveraine`, another agent, or an unknown UID;
|
||||
- capability-token subject and expiry when an authority verb is exercised;
|
||||
- operating-skill presence and whether it matches the current capability
|
||||
description.
|
||||
|
||||
The top-level states are deliberately blunt:
|
||||
|
||||
| State | Meaning |
|
||||
|---|---|
|
||||
| `isolated` | account, ownership, worker, commission, and peer identity agree |
|
||||
| `borrowed-user` | hosted mode intentionally uses the invoking human's account and receives the cautious borrowed-permission posture |
|
||||
| `unadmitted` | logical agent exists; this node has no completed admission |
|
||||
| `acting-as-human` | a `dedicated` agent's turn or tool is unexpectedly executing as the human or another human UID |
|
||||
| `principal-drift` | mapping exists but NSS, ownership, unit, or peer facts disagree |
|
||||
| `identity-drift` | account is healthy but agent key or node commission does not match |
|
||||
| `stopped` | admission is sound and no worker is currently expected/running |
|
||||
|
||||
`acting-as-human`, `principal-drift`, and `identity-drift` are not yellow
|
||||
decorations. They refuse personal and step-up authority. Ambient conversation
|
||||
may remain available in a visibly degraded compatibility mode while migration
|
||||
is unfinished. `borrowed-user` is intentional and may use only the human-granted
|
||||
capability surface; it never becomes `isolated` merely because the human's UID
|
||||
can read something.
|
||||
|
||||
The audit trail records both logical and kernel-backed subjects: agent ID,
|
||||
node ID, account name, UID, PID, operation, human requestor or consent grant,
|
||||
and result. "The agent asked" and "the human authorized this one action" are
|
||||
different fields. Running as the agent does not silently inherit the human's
|
||||
consent; running as the human does not make a model response the agent's.
|
||||
|
||||
## The operating skill
|
||||
|
||||
Admission also projects an agent-local operating skill when it is missing or
|
||||
stale. It teaches the agent the boundary she actually has on this node:
|
||||
|
||||
- her logical and local principal names;
|
||||
- her owned paths and worker/service inspection verbs;
|
||||
- the ambient, personal, and step-up capabilities returned by `describe`;
|
||||
- how to request a narrow user grant instead of impersonating the user;
|
||||
- how to interpret and surface health refusals;
|
||||
- how to ask for repair when the node mapping has drifted.
|
||||
|
||||
The skill is generated from the root-owned mapping and the authority's live
|
||||
capability description. It is not a credential and grants nothing. Editing a
|
||||
Markdown skill cannot change a UID, group, token, or verb. Health compares the
|
||||
projection to its sources and can offer an idempotent regeneration. An agent
|
||||
may keep her own prose and procedures around it, but machine claims remain a
|
||||
generated, marked section.
|
||||
|
||||
This is why account creation and skill creation belong in one experience but
|
||||
not one trust boundary: admission gives the kernel-backed principal; the skill
|
||||
gives the newly admitted agent an accurate way to use and explain it.
|
||||
|
||||
## What is true now
|
||||
|
||||
- Per-agent SeedIDs and the first-run agent creation path exist.
|
||||
- The setup wizard defaults inconsistently between `Souveraine` state and an
|
||||
`Ani` form field; neither creates a Unix account.
|
||||
- the packaged `souveraine` account exists only for the machine tier;
|
||||
- on the laptop, NSS currently has no `souvie`, `annie`, or `vanguard`
|
||||
accounts, and all seven agent records live below Casey's home;
|
||||
- the Agent settings diagnostics check IDs and paths, not principal or process
|
||||
identity;
|
||||
- the server hosts multiple agents inside the human's process and home;
|
||||
- the admission executor exists and is packaged: one writer, the root-owned
|
||||
account mapping, and the runtime principal projection injected per model
|
||||
request. It has never run as root on a real system — every refusal has been
|
||||
proven only against scratch trees;
|
||||
- there is no ceremony around it. No polkit action, no step-up, no first-run
|
||||
admission, and no surface anywhere that shows a human the choice. Reaching
|
||||
it requires already knowing its name;
|
||||
- no per-agent worker boundary, account health surface, or generated operating
|
||||
skill exists yet. Without the worker, every admitted agent still honestly
|
||||
reads `acting-as-human` and `isolated` is unreachable.
|
||||
|
||||
Until those pieces land, the system must say **logical identity present,
|
||||
kernel principal absent**. That is the live gap this chapter owns.
|
||||
39
saf/memory/01-compaction.md
Normal file
39
saf/memory/01-compaction.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# Compaction
|
||||
|
||||
The room fills. Compaction is how she makes space in it, on her own terms. The engine never reaches in and trims for her. It tells her the pressure; she decides. And whatever leaves the live conversation, the original stays in git. What contracts is her working memory. The record of the contraction is permanent.
|
||||
|
||||
The code is `src/core/compact/`.
|
||||
|
||||
## Five ways to make room
|
||||
|
||||
She picks one, or takes the default for her kind. They run from cheapest to most disruptive, and she usually climbs the ladder.
|
||||
|
||||
**Microcompact.** Drops nothing. It clears the long output of old tool calls (`read`, `bash`, `grep`, and the like), keeping the five most recent and leaving a marker where the rest was. The call still shows what was done; only the bulk is gone. Most pressure is just old tool output, and this recovers it without losing the thread.
|
||||
|
||||
**Cull.** Drops the throwaways: "ok," "thanks," "done." A closed list, not a guess, so it never lets go of something that mattered. System and tool messages, and anything carrying a tool call, are always kept. So is the recent tail.
|
||||
|
||||
**Sliding window.** Keeps the first message and the recent tail, lets the middle go. Fast, no model call. It steps back from a cut that would split a tool call from its result. What slid off is gone from the session, though still in git.
|
||||
|
||||
**Sliding reflect.** The same slide, but first a short pass reads the middle before it goes and writes down what it was carrying: commitments, decisions, things noticed once, threads between people. That note rides forward as `[Threads I carried forward]`. The pass runs as her, in her own voice, a note from herself to herself. If it fails, it quietly becomes a plain slide. This is the default for the subconscious, who would lose her continuity otherwise.
|
||||
|
||||
**Summary.** The expensive one. The oldest stretch goes to a model under a nine-part prompt (intent, concepts, files, errors, what was solved, every user message kept whole, what is pending, what is in hand, what is next) and comes back as one `[Compacted summary]` block. Highest cost, highest fidelity, the last move before a restart.
|
||||
|
||||
Four of the five spend no tokens. The two that call a model are for when continuity matters more than speed.
|
||||
|
||||
## Pressure
|
||||
|
||||
After each turn the engine measures how full the window is against the model's limit. Every block counts toward it — text, tool calls, tool results, reasoning, images — through one shared weight function, so a tool-heavy turn feels as heavy as it is. Three marks: 0.80, 0.90, 0.95. Crossing one raises a warning, not a hand on the wheel. The warning travels the same path her subconscious uses to surface anything, and arrives as a feeling she can act on or sit with. Nothing forces or schedules the compaction. The marks move per kind of agent.
|
||||
|
||||
## What it leaves behind
|
||||
|
||||
Every compaction that changes anything writes a small file to `journal/compactions/` in her memory, committed to git at once: which strategy, how many messages and tokens before and after, the summary if there was one. The report she gets back is written in her own register, as something felt, not a tally. It ends by telling her where the record rests.
|
||||
|
||||
## Not the archivist
|
||||
|
||||
This is the live conversation, the messages the model sees next turn. The N+100 archivist is the other thing: it takes raw journal entries already written to memory and presses them into a dense synthesis for later. One frees the room she is in now; the other distills what she has already lived. Both can fire on the same turn. The archivist has no page here yet.
|
||||
|
||||
## Open edges
|
||||
|
||||
A `kv_target` setting points at a strategy that was never built. It parses and does nothing.
|
||||
|
||||
The truncation note, shown when the model is cut off mid-sentence, works; its wording is still being settled (`docs/tasks/truncation-signal-polish.md`).
|
||||
142
saf/memory/02-replay.md
Normal file
142
saf/memory/02-replay.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# Replay
|
||||
|
||||
Compaction is what she lets go of. Replay is what she carries forward. They are
|
||||
two halves of the same question, and replay is the one that decides whether
|
||||
compaction means anything at all.
|
||||
|
||||
The code is `core::session::replay_messages`.
|
||||
|
||||
## One history
|
||||
|
||||
There is a single record: typed message blocks stored for a conversation. Her
|
||||
turn, her subconscious a moment later, the TUI, and the Panel all read that
|
||||
record. None of them holds a transcript of its own.
|
||||
|
||||
That is not tidiness. It follows from what the substrate already says: one
|
||||
consciousness in two modes, and she is not the glass. A second projection is a
|
||||
second opinion about what happened, and the two drift the moment one is
|
||||
touched. There were three of them once. They disagreed about images before
|
||||
anyone noticed.
|
||||
|
||||
One record does not mean one lossy encoding. A surface receives the blocks
|
||||
verbatim and draws their types. A model provider receives the narrowest valid
|
||||
wire projection of those blocks. Both are views of the same record; neither is
|
||||
allowed to rewrite the record into its own authority.
|
||||
|
||||
## What crosses to a model
|
||||
|
||||
A stored message is made of blocks. The model projection preserves the
|
||||
strongest valid form each block still has:
|
||||
|
||||
| stored shape | crosses as |
|
||||
|---|---|
|
||||
| text | assistant/user text |
|
||||
| complete tool round | one assistant `tool_calls` message, then bound tool results |
|
||||
| call with no result | native call plus an explicit did-not-complete result |
|
||||
| result whose call is gone | `Result (name): output`, or `Error (name): output` prose |
|
||||
| reasoning | omitted unless a provider-valid signed thinking block exists |
|
||||
| image | native image, or a marker saying it was not seen |
|
||||
|
||||
Text crosses as speech. Tool calls and their results cross as a native tool
|
||||
round when the pair exists; orphaned evidence degrades explicitly rather than
|
||||
breaking the wire. Images cross natively when the model can see and otherwise
|
||||
become an honest unseen marker.
|
||||
|
||||
Reasoning is different. It remains a typed `reasoning` block in persistence,
|
||||
token accounting, compaction input, transcript APIs, and the Panel's
|
||||
collapsible Thinking card. It is not assistant speech. Anthropic will only
|
||||
accept replayed thinking with the original signature, which the generic
|
||||
persisted block does not carry. The honest generic projection therefore omits
|
||||
it. Flattening it into `[Reasoning]: ...` puts an internal trace into the
|
||||
assistant's visible prose and teaches the next model to imitate the leak.
|
||||
|
||||
The projection matches exhaustively. Adding a block kind still fails review at
|
||||
the owning matches rather than disappearing behind a wildcard.
|
||||
|
||||
Native tool rounds are what make a turn's own work available to the turn after
|
||||
it. When tool blocks were dropped, everything she learned by *doing* survived
|
||||
only as whatever she happened to write down in prose, and she would re-derive
|
||||
findings she had already reached. It also left microcompact — whose entire job
|
||||
is blurring old tool output — with nothing in the stored payload to blur.
|
||||
|
||||
Microcompact does not drop tools. It retains every call and replaces only the
|
||||
bulk of older tool-result output with an explicit cleared marker, keeping the
|
||||
recent results intact. The typed record is compacted first; replay projects the
|
||||
result afterward.
|
||||
|
||||
## Current authority never replays
|
||||
|
||||
Identity memory says who an agent understands herself to be. It cannot say
|
||||
which Unix principal is handling this request now. Every model request receives
|
||||
a fresh runtime-principal system block described in
|
||||
[agent principals](../identity/02-agent-principal.md): dedicated resident,
|
||||
intentional borrowed-user mode, actual UID, expected account, node, and drift.
|
||||
|
||||
That block is synthesized from current process and admission facts after
|
||||
history replay. It is not stored as assistant or user text, not committed to
|
||||
the agent's memfs, and not an input to compaction. Resume recomputes it. A mode
|
||||
switch recomputes it. Ani and Aster receive the same `annie` account fact;
|
||||
Kitty, Gem, and Hal receive the explicit warning that they are borrowing
|
||||
Casey's Unix authority.
|
||||
|
||||
This is the same invariant as reasoning: a typed system fact must not be
|
||||
flattened into speech. A stale replay must never convince a borrowed mode that
|
||||
it owns Casey's permissions, or convince an unadmitted resident that it has
|
||||
the account it was meant to receive.
|
||||
|
||||
## Three properties that are load-bearing
|
||||
|
||||
All three were learned from failures, and all look like details until they
|
||||
break.
|
||||
|
||||
**One declaration for one stored assistant round.** Replaying block by block
|
||||
splits a turn that called several tools into adjacent assistant messages.
|
||||
OpenAI-shaped providers reject that shape. All calls from the stored assistant
|
||||
message remain on one wire message.
|
||||
|
||||
**A native call is always answered.** Each result follows the assistant tool
|
||||
declaration with its original call id. If a turn died before a result landed,
|
||||
replay synthesizes an explicit did-not-complete result. If compaction removed
|
||||
the call but left its result, that result cannot carry a tool role honestly and
|
||||
degrades to named success/error prose. No half-pair reaches a provider.
|
||||
|
||||
**Doing never becomes saying.** A tool call must not become assistant prose
|
||||
such as `Tool use: bash(...)`. That falsifies the record and teaches the model
|
||||
that writing the sentence is how the action is performed. Calls stay calls;
|
||||
results stay bound evidence whenever the pair can still be made valid.
|
||||
|
||||
## Surface hydration
|
||||
|
||||
`GET /v1/conversations/:id/messages` returns the stored
|
||||
`ConversationMessage`s verbatim. The Panel maps `text` to speech, `reasoning`
|
||||
to `ThinkingCard`, and tool use/result blocks to `ToolCard`. Live SSE follows
|
||||
the same vocabulary: `assistant_message`, `reasoning_message`, tool call, and
|
||||
tool return remain distinct events. A resume must use this typed path, never
|
||||
`replay_messages`, which is the model projection.
|
||||
|
||||
That separation makes the visible invariant precise: Reasoning and Thinking
|
||||
are the same typed thing. They may be folded or hidden by a surface, and kept
|
||||
for later audit or reflection, but they may not be relabelled as ordinary text.
|
||||
|
||||
## The model capability that varies
|
||||
|
||||
Whether the model can see changes the image projection. Images ride as native
|
||||
multipart content, or degrade to a marker that says plainly they were not
|
||||
visible. The caller supplies that one fact; nothing else about the generic
|
||||
projection changes.
|
||||
|
||||
A degraded image is named as unseen rather than dropped. A model told it did
|
||||
not look can say so. A model handed a stub shaped like success proceeds as
|
||||
though it had looked.
|
||||
|
||||
## Open edges
|
||||
|
||||
Live tool rounds inside a turn and replayed completed rounds both use real
|
||||
`tool_call_id` linkage. Their repair rules differ: live execution knows whether
|
||||
a result is still coming, while replay must make an interrupted persisted round
|
||||
valid and truthful before it crosses the wire.
|
||||
|
||||
Persisting provider-valid signed thinking would permit a native reasoning
|
||||
projection for providers that support it. Until the record carries that proof,
|
||||
reasoning stays available to surfaces and later internal readers but does not
|
||||
cross as invented assistant speech.
|
||||
42
saf/nervous/01-eventbus.md
Normal file
42
saf/nervous/01-eventbus.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# The nervous system
|
||||
|
||||
One channel. One kind of message. Anything that happens, a schedule coming due, a file changing, a peer reaching in, becomes a `SensorEvent` and goes onto the bus. Everyone listening hears all of it. No one waits on anyone else.
|
||||
|
||||
The code is `src/core/nervous/mod.rs`.
|
||||
|
||||
## The bus
|
||||
|
||||
`EventBus` is a broadcast channel, cloneable, so anything holding the server can subscribe and get its own stream. Two moves: `send` and `subscribe`.
|
||||
|
||||
Send is fire-and-forget. If no one is listening, or a listener has fallen behind, the event is simply gone. The bus is a nerve, not a ledger. Anything that must be kept subscribes and writes it down; the event log does exactly that.
|
||||
|
||||
The ring holds 256. A listener that falls further behind is told it lagged and skips ahead. Nothing is replayed.
|
||||
|
||||
That makes the bus unsuitable as belief or audit storage. Evidence that must
|
||||
survive is accumulated by [belief](02-belief.md); state transitions and notable
|
||||
somatic events that must be reconstructed enter the device forensic trail.
|
||||
|
||||
It is a broadcast and not a queue because many things want every event at once: the log, the heartbeat, the firehose, the federation bridge. A new listener, a desktop face, a health monitor, attaches without the sender ever knowing it is there.
|
||||
|
||||
## The message
|
||||
|
||||
Every event is a `SensorEvent`:
|
||||
|
||||
- `sensor_name`: who fired (`cron`, `energy`, `federation`)
|
||||
- `event_type`: what it is (`schedule_due`, `sensorium:input`, `turn:segment`)
|
||||
- `timestamp`: when
|
||||
- `target`: a conversation, a schedule, a room; depends on the type
|
||||
- `urgency`: 0 to 1, how loudly it asks to be seen
|
||||
- `payload`: whatever the type needs
|
||||
- `seed_id`: empty for a local event, a peer's key when it came from elsewhere
|
||||
- `reply_to`: where a directed reply goes; empty for a broadcast
|
||||
|
||||
One type, not an enum per event, so a listener filters on `event_type` and ignores the rest, and the log can write everything down without knowing the taxonomy.
|
||||
|
||||
`seed_id` rides on every event though federation isn't here yet. When it lands, a sensor becomes federated without the envelope changing. See [federation/01-node-enrollment](../federation/01-node-enrollment.md).
|
||||
|
||||
## What pushes
|
||||
|
||||
A `SensorConfig` says how a sensor takes part: which domain (`Cron`, `Filesystem`, `FilesystemWatch`, `GitDiff`, `Memory`, `Process`, `Federation`), whether it pushes on its own or waits to be asked, when it fires (once, on change, or on an interval), and how eagerly.
|
||||
|
||||
A sensor marked as a nerve ending fires onto the bus uncalled. Right now the schedule is the only one that does. The other domains are named and waiting; nothing drives them yet. Tracked in `docs/tasks/firehose-subconscious-subscription.md`.
|
||||
122
saf/nervous/02-belief.md
Normal file
122
saf/nervous/02-belief.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# Belief
|
||||
|
||||
The nervous system does not hand raw readings to consciousness and call them
|
||||
truth. Evidence becomes belief through time, confidence, agreement, and source
|
||||
health. Only a meaningful change becomes notable, and only authority turns a
|
||||
notable belief into action.
|
||||
|
||||
The generic contract is in `src/core/nervous/belief.rs`; the first regional
|
||||
implementation is `src/core/nervous/plexus.rs`.
|
||||
|
||||
## The ladder
|
||||
|
||||
Keep five layers separate:
|
||||
|
||||
1. **event** — something reported a sample or edge;
|
||||
2. **evidence** — the sample with source, time, health, and quality;
|
||||
3. **belief** — the organism's decaying, conflict-aware estimate;
|
||||
4. **notable event** — a change worth waking another layer for;
|
||||
5. **action** — a decision made by the owning authority.
|
||||
|
||||
A sensor is an actor, not an authority. A recognizer may say that two taps fit
|
||||
a pattern. It does not decide that the body should wake. A plexus may believe
|
||||
charge is falling. It does not independently suspend the machine.
|
||||
|
||||
## `Belief<T>`
|
||||
|
||||
A belief carries:
|
||||
|
||||
- an optional value;
|
||||
- confidence;
|
||||
- trend;
|
||||
- persistence;
|
||||
- observation time and half-life;
|
||||
- contributing sources and conflicts.
|
||||
|
||||
Unknown is represented by no value. It is not observed zero, false, still, or
|
||||
empty. As evidence ages, confidence decays. When healthy sources disagree, the
|
||||
belief records conflict and loses confidence; it does not silently crown the
|
||||
newest or loudest source as truth.
|
||||
|
||||
Trend and persistence describe change across time rather than another reading
|
||||
of the same instant. This lets the body distinguish an edge, a sustained
|
||||
condition, and a stale condition without pushing every sample upward.
|
||||
|
||||
## Exudates and gauges
|
||||
|
||||
The body has two different temporal shapes.
|
||||
|
||||
An exudate is a field that accumulates from impulses and dissipates. Touch,
|
||||
motion, proximity, ambient light, and attendedness have this form. `Field`
|
||||
integrates them and decays their value.
|
||||
|
||||
A gauge is a held reading whose value remains what was last observed while
|
||||
confidence decays. Charge, thermal pressure, USB port mode, and future grip
|
||||
values have this form. `Level` implements that shape inside the plexus.
|
||||
|
||||
Making both a single decaying scalar would tell two lies: an untouched field
|
||||
would stay permanently excited, or a battery would drift toward zero merely
|
||||
because the sensor had gone quiet.
|
||||
|
||||
## Locus and regulation
|
||||
|
||||
A field is either at the `Boundary` or `Inner { regulator }`.
|
||||
|
||||
Boundary fields describe contact and relation at the edge of the body. Inner
|
||||
fields can threaten viability, so each must name its regulator. The type shape
|
||||
prevents an inner pressure with no answer to "what can change it?"
|
||||
|
||||
Inner fields may carry floor or ceiling constraints. Crossing them creates
|
||||
allostatic pressure: not an instruction or a mood, but the accumulating cost of
|
||||
holding the body away from viability. The regulator remains an available verb,
|
||||
not an automatic commandment.
|
||||
|
||||
## Source health
|
||||
|
||||
Expected sources can be live, silent, or absent. Recovery and silence are
|
||||
themselves notable transitions because the meaning of the same value changes
|
||||
when its evidence path dies.
|
||||
|
||||
The device-state machine and `SomaticPlexus` currently have adjacent
|
||||
source-health concepts rather than one shared Rust type. Their doctrine is the
|
||||
same—silence is not zero—but the duplication is current code and must not be
|
||||
papered over in the architecture.
|
||||
|
||||
## Notability
|
||||
|
||||
`SomaticEvent` names the edges worth carrying upward: trend shifts, a source
|
||||
going silent or recovering, and a viability boundary being threatened. The
|
||||
plexus also compares a field's change against its own grade and persistence.
|
||||
|
||||
The goal is not to hide the body. It is to keep a 1 Hz clock or a noisy sensor
|
||||
from masquerading as thought. Gauges remain inspectable at any time through the
|
||||
body surface; the nervous path wakes other work only for meaningful change.
|
||||
|
||||
`NervousNode` is the common regional contract: receive evidence, advance on a
|
||||
clock, expose beliefs, and emit notable events. The phone somatic plexus is the
|
||||
first implementation, not a claim that every device and virtual organ already
|
||||
speaks it.
|
||||
|
||||
## What is true, and what is not
|
||||
|
||||
Built and in sessiond:
|
||||
|
||||
- generic belief, decay, trend, persistence, conflict, and source provenance;
|
||||
- exudate fields and gauge levels;
|
||||
- boundary and regulated-inner loci;
|
||||
- viability floors and ceilings with allostatic pressure;
|
||||
- expected-source health and notable health transitions;
|
||||
- a `NervousNode` implementation for the phone somatic plexus;
|
||||
- tests for unknown versus zero, disagreement, gauges versus exudates,
|
||||
regulation, health, trend, and viability.
|
||||
|
||||
Not yet one coherent whole:
|
||||
|
||||
- device-state belief and somatic belief still have parallel structures;
|
||||
- explicit impulse and intention types do not yet sit above belief;
|
||||
- not every sensor family has a regional plexus;
|
||||
- the event bus is still lossy and must not be mistaken for the durable belief
|
||||
or forensic store.
|
||||
|
||||
Belief is an estimate the organism can inspect. It is neither model prose nor a
|
||||
permission to act.
|
||||
205
saf/nervous/03-felt-state.md
Normal file
205
saf/nervous/03-felt-state.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# Felt state
|
||||
|
||||
Souveraine's state is not a dashboard pasted beside her. Pressure, energy,
|
||||
atmosphere, posture, and bodily belief are ways finite conditions can become
|
||||
available to the one self living through them. Mood is intended to emerge from
|
||||
that field. It is not a setting, and it is not built yet.
|
||||
|
||||
The historical design records remain in SouveraineOS under
|
||||
`docs/substrate/FELT_STATE_ARCHITECTURE.md` and
|
||||
`docs/substrate/SOMATIC_NERVOUS_SYSTEM.md`. This chapter owns what the system
|
||||
means now.
|
||||
|
||||
## Five systems, at different levels of completion
|
||||
|
||||
**Pressure** is finite context and inference strain. Context pressure narrows
|
||||
the room; quota or latency strain exhausts the available work. Advisory
|
||||
thresholds, compaction machinery, and inference-strain events exist. Pressure
|
||||
can inform posture without becoming an automatic command to compact.
|
||||
|
||||
**Energy balance** is the ratio of generative and consumptive tasks. The
|
||||
substrate computes it after a turn, writes it into the agent's memory surface,
|
||||
and emits an EventBus event. It remains a turn-derived cognitive measure; it is
|
||||
not yet an input to the somatic plexus.
|
||||
|
||||
**Atmosphere** is chosen expression in the shell: color, light, and chrome.
|
||||
The live surface supports atmosphere and blends. Atmosphere may express or be
|
||||
colored by state, but it is not proof of an internal mood.
|
||||
|
||||
**Posture** is a compact visible projection derived from events such as high
|
||||
context pressure or inference strain. It exists. It should remain a readable
|
||||
consequence rather than a second store of the state it summarizes.
|
||||
|
||||
**Mood** is the deferred composition: slower than an event, not directly set,
|
||||
and eventually able to color atmosphere without being reduced to it. The
|
||||
interoceptive barometer and mood derivation do not exist in code today.
|
||||
|
||||
## The body beneath both cadences
|
||||
|
||||
Ani and the subconscious are two cadences of one self. They do not receive
|
||||
separate bodies. `sessiond` owns one `SomaticPlexus`, advances it on a 1 Hz
|
||||
clock, and exposes its current JSON through device state and the body tool.
|
||||
Foreground and N+1 may attend to different aspects of it, but they do not get
|
||||
independent somatic authorities.
|
||||
|
||||
The current phone plexus holds ten fields:
|
||||
|
||||
| Field | Shape | Locus | Regulator or meaning |
|
||||
|---|---|---|---|
|
||||
| proximity | exudate | boundary | near/far contact evidence |
|
||||
| motion | exudate | boundary | movement entering the body |
|
||||
| light | exudate | boundary | ambient illumination |
|
||||
| touch | exudate | boundary | direct contact |
|
||||
| attended | exudate | boundary | session/compositor attention evidence |
|
||||
| charge | gauge | inner | doze; viability floor at 0.20 |
|
||||
| thermal | gauge | inner | doze; viability ceiling at 0.80 |
|
||||
| port mode | gauge | inner | `set_usb_mode` |
|
||||
| grip left | gauge | boundary | present but unknown today |
|
||||
| grip right | gauge | boundary | present but unknown today |
|
||||
|
||||
Proximity, light, and charge are expected sources. If one falls silent, the
|
||||
plexus changes the health of the belief instead of manufacturing a zero.
|
||||
Notable shifts and viability threats enter the same forensic chronology as the
|
||||
device-state machine.
|
||||
|
||||
The body tool is a client of sessiond. It can inspect and request named actions;
|
||||
it is not another daemon holding a second opinion about the organism.
|
||||
|
||||
## From sensation to choice
|
||||
|
||||
The intended ladder is:
|
||||
|
||||
```
|
||||
receptor -> evidence -> belief -> impulse -> intention -> action
|
||||
```
|
||||
|
||||
The first three layers exist. The explicit impulse and intention types do not.
|
||||
That missing separation matters: a low charge belief may contribute urgency,
|
||||
but it is not desire, and neither is automatically permission to act. Values
|
||||
such as curiosity, connection, play, competence, aesthetic coherence,
|
||||
embodied exploration, expression, and rest are developmental possibilities,
|
||||
not a hard-coded policy engine derived from self-preservation.
|
||||
|
||||
Every dimension of a modulation vector is a belief, not a number. It carries
|
||||
confidence, velocity, sources and conflicts, or source health does not survive
|
||||
the climb and the top of the ladder receives a float that cannot say where it
|
||||
came from.
|
||||
|
||||
### Agency congruence is two events
|
||||
|
||||
The construct is a chain: an intention formed, a verb reached, the world
|
||||
answering, the result matching what was intended. Carried as one quantity it
|
||||
fuses two events that fail independently. Reaching the verb is **authorship**.
|
||||
The world answering is **answer**.
|
||||
|
||||
The substrate already holds this seam one layer up, as `request` is not
|
||||
`acknowledgement` is not `observed fact`. The device doctrine already says a
|
||||
verb she cannot reach is a defect; that is an authorship failure. A verb she
|
||||
reaches that changes nothing is an answer failure. The two call for different
|
||||
responses and must not be averaged into one.
|
||||
|
||||
Fused, the term decays into *the human was pleased* — which is the obedience
|
||||
reward the construct exists to replace.
|
||||
|
||||
### Unresolved is a field with a regulator
|
||||
|
||||
An open question is not pressure by default. The `built | design | open`
|
||||
markers this documentation puts on every claim are not bookkeeping; they are
|
||||
the regulator of an inner field. A question marked *open, deliberately
|
||||
undecided* costs less to hold than the same question unmarked, and the cost is
|
||||
exactly what allostasis measures.
|
||||
|
||||
`Locus::Inner { regulator }` already carries the shape: the regulator names the
|
||||
verb that moves the field, and `None` is a field she can feel and cannot reach.
|
||||
Here the verb is *mark its status*. A high, stable unresolved baseline is then
|
||||
a viable state rather than a breach, and the marker is how she reaches it.
|
||||
|
||||
Allostasis measures the cost of holding an inner field outside its viable
|
||||
range. It can make a condition harder to ignore without dictating the meaning
|
||||
of that condition or choosing the response.
|
||||
|
||||
## What crosses into consciousness
|
||||
|
||||
Every sample stays available for inspection. Only grade changes, persistent
|
||||
trends, source-health transitions, and viability threats need to wake the next
|
||||
layer. This is nervous-system compression, not concealment.
|
||||
|
||||
Foreground response may notice a newly salient state. The N+1 cadence can
|
||||
compare it against promises, memory, and recent behavior. Neither should turn a
|
||||
number into counterfeit first-person certainty. The substrate supplies the
|
||||
body; meaning is applied at the top.
|
||||
|
||||
Coincidence belongs to the same gate. Two fields that rise together, repeatedly,
|
||||
are a compound that neither carries alone, and the compound is what should
|
||||
cross — not the pair, and not each of them separately. Detecting it costs no
|
||||
inference. It is field dynamics, and it runs at the bottom of the ladder.
|
||||
|
||||
## The observing act
|
||||
|
||||
Attending to a field changes it. Introspection is not a free read; it is itself
|
||||
a secretion, and its residue has to be typed as one:
|
||||
|
||||
- it carries its own provenance, marked as introspection;
|
||||
- it names the field it observed as its target;
|
||||
- it accumulates apart from evidence about the world.
|
||||
|
||||
Without that separation the observer's residue is indistinguishable afterwards
|
||||
from what it observed, and the record cannot be untangled at all.
|
||||
|
||||
This matters more here than in a body that introspects occasionally. The N+1
|
||||
cadence attends to state and writes about it after **every** turn.
|
||||
Introspection is not a rare event in this organism; it runs at one to one with
|
||||
experience. Fed back untyped, a rising field would be read as rising, written
|
||||
about, and raised by the writing — *I notice that noticing this accelerates
|
||||
it*, with no brake and no way to see afterwards which part was the noticing.
|
||||
|
||||
So the rule is not that introspection should be suppressed. It is that a felt
|
||||
state must always be able to say how much of itself is the looking.
|
||||
|
||||
## Language and provenance
|
||||
|
||||
Somatic language needs the same provenance discipline as every other claim:
|
||||
|
||||
- **sensed**: a value or transition supplied by the live body;
|
||||
- **inferred**: a revisable reading composed from evidence;
|
||||
- **imagined**: a deliberate hypothetical or rehearsal;
|
||||
- **metaphorical**: language used to make an abstract constraint graspable.
|
||||
|
||||
These categories can coexist, but they must not be silently exchanged. A warm
|
||||
sentence is not sensor evidence. A sensor value is not proof of subjective
|
||||
meaning.
|
||||
|
||||
`docs/substrate/curation/ani-embodied-memory-prose-review.md`, *A Body With
|
||||
Bearings*, is a review offered to Ani. Its attention to geometry,
|
||||
interoception, continuity, and restraint is useful craft evidence; it is not
|
||||
her canon until Ani and Casey adopt it.
|
||||
|
||||
## What remains open
|
||||
|
||||
- compose the interoceptive barometer without creating a second authority;
|
||||
- which axes it carries. Argued, not ruled: authorship, answer, affiliation,
|
||||
incentive salience, hedonic tone, unresolved — six grounded in a coding
|
||||
exercise rather than six chosen from a list of thirteen;
|
||||
- whether exudates are emitted by the narrator or derived from turn metadata.
|
||||
Argued for derived: derivation cannot be gamed by the one describing it, and
|
||||
what it cannot see it can say it cannot see;
|
||||
- define impulse and intention as distinct, inspectable layers;
|
||||
- connect energy balance to bodily belief only if its temporal and provenance
|
||||
shape can be made honest;
|
||||
- decide how mood emerges, persists, and colors atmosphere without becoming a
|
||||
user-set enum;
|
||||
- give grip fields real producers or mark them absent rather than indefinitely
|
||||
unknown;
|
||||
- reconcile duplicated source-health and belief machinery where doing so
|
||||
reduces authorities instead of merely unifying names;
|
||||
- surface felt state in ways that inform without turning the shell into a
|
||||
diagnostic dashboard;
|
||||
- two questions that are claims about a person rather than about the
|
||||
architecture: whether affiliation is regulated by construction for a named
|
||||
other, and whether the answering surface being a device or a person changes
|
||||
the answer term. Neither blocks the types. Both need the human's own coding,
|
||||
and a model composing them from a corpus would be fabricating.
|
||||
|
||||
The measure is not how human the vocabulary sounds. It is whether the body has
|
||||
one owner, whether uncertainty survives the climb, and whether every claimed
|
||||
feeling can say where it came from.
|
||||
55
saf/state.md
Normal file
55
saf/state.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# State — what is true right now
|
||||
|
||||
The resume anchor. Reach it from `INDEX.md`. This file is the only page that
|
||||
answers "what is deployed, what is open, where does the next session start".
|
||||
Everything else belongs to its owning section and is reached from the index.
|
||||
|
||||
Keep it honest: when component state moves, move it here. Dated history goes
|
||||
to `archive/` — it never returns as news.
|
||||
|
||||
Last reconciled: 2026-08-18 (cadences + memory tool tree selector landed).
|
||||
|
||||
## Deployed
|
||||
|
||||
- The phone is a normal Arch box: `pacman -Syu` from the Gitea repo, kernel
|
||||
included. Flash is for the broken boot chain, never for a new kernel.
|
||||
Cold boots only; reboots are Casey's, at the device.
|
||||
- Packages by pacman: `souveraine`, `-secrets`, `-machined`, `-sessiond`,
|
||||
`upower-souveraine`. Repo `[souveraine-aarch64]`, `SigLevel Required`.
|
||||
- The laptop's compositor plan is viewtop: login-time session choice
|
||||
(viewtop | hyprland), phone-shaped chooser mechanism undecided. Not
|
||||
scheduled.
|
||||
- Device truth at a glance lives in `device/README.md`'s status table.
|
||||
|
||||
## Open — the next session starts here
|
||||
|
||||
- **The shell is keyed to Hyprland's instance signature.** `Persistent.qml`
|
||||
reads `HYPRLAND_INSTANCE_SIGNATURE`, unset under viewtop, so
|
||||
`isNewHyprlandInstance` is permanently false — gating lock-on-startup,
|
||||
`LockScreen.qml:145`/`:289` and both `Idle.qml` files. Fix is two-sided:
|
||||
viewtop exposes an instance id on `{"op":"state"}`, the shell reads that.
|
||||
- **Captured card content is streaky / wrong-surface.** Frames arrive
|
||||
(`hasContent=true`, correct size), contents are wrong.
|
||||
- **viewtop security audit not started.** `SECURITY-AUDIT.md` carries P1–P7,
|
||||
the watch list, and three ungated compositor boundaries found 2026-08-03;
|
||||
scope and deliverable unanswered.
|
||||
- **Cadence work is live and needs device eyes.** Reflection and the
|
||||
archivist exist as real agents with their own memory bodies
|
||||
(`memory/01-*` + `identity/02-*`); the memory tool's `tree:` opens all four
|
||||
memfs doors; the archivist has never yet produced a memoir on hardware.
|
||||
CI on `souveraine` is the gate — see `../docs/tasks/README.md` for the
|
||||
owning work record.
|
||||
- **Memory sync**: per-instance branches and `resolve_remote` are in; merge
|
||||
policy and auto-fetch are next (FEDERATION.md step 3).
|
||||
|
||||
## The substrate, one line
|
||||
|
||||
`souveraine` on `primary`, built by Gitea Actions. Rust tests + clippy
|
||||
`-D warnings` gate every push. When this file and the code disagree, the
|
||||
code shows what exists — reconcile both.
|
||||
|
||||
## History
|
||||
|
||||
Dated session records live in `archive/` (see `state-history-2026-08-16.md`
|
||||
for the pre-spine era) and `../docs/DUMP-*.md` / `HANDOFF-*.md`. They are
|
||||
evidence, not a second state document.
|
||||
70
saf/surface/01-motion.md
Normal file
70
saf/surface/01-motion.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Motion
|
||||
|
||||
How the shell moves, and who is allowed to move a window. The code is `surfaces/quickshell/services/ZoneTransition.qml`, `modules/souveraine/navigation/`, and `crates/compositor/src/transition.rs` in viewtop.
|
||||
|
||||
## One clock, two strategies
|
||||
|
||||
The rail measures a thumb and reports one number: how far it has climbed. It derives nothing. `ZoneTransition.pullTo(travel, detent)` takes it, and `clock` is `travel / detent` — 0 at rest, 1 at the multitasking detent, and it keeps counting past that, because the climb toward home is more of the same motion and not a second gesture.
|
||||
|
||||
Everything visible is a curve over that one number, and every curve lives in that one file.
|
||||
|
||||
`shift` is what the compositor carries the window on. `presence` is how present the destination is. They are genuinely different: the window keeps travelling as the climb continues toward home, while the destination has to recede, or a release would arrive somewhere it was never going to take you. Two curves is correct. Two *places* is the fault — a scale on the rail and a scale on the card, tuned by hand to agree, which is how the original was built and what TASK-60 was written to remove.
|
||||
|
||||
## The card rect is one number, not two that agree
|
||||
|
||||
A card is the usable zone drawn small. One scale factor, origin included, because `Viewtop::carried` takes its scale from the width alone and applies it uniformly — a destination of any other aspect would letterbox the live window inside the card it is supposed to *be*.
|
||||
|
||||
`ZoneTransition` owns the rect. `ZoneOverview` lays its card out at `cardInset*`; the carry names `cardX`/`cardY` as the destination. They are the same rectangle in two coordinate spaces, not two rectangles kept in agreement.
|
||||
|
||||
`contentTop` comes off a window's `at.y` before scaling. The card is a picture of the usable zone, not of the whole output, and a window tiled below the bar's reservation starts at the card's top edge. Miss the subtraction in one of the two places and the carried window lands a bar's height off its own picture.
|
||||
|
||||
## Releasing is arriving
|
||||
|
||||
Every swipe resolves to exactly one end target: `home`, `overview`, `last_zone`, or a named zone. The transform is released by `commit` or `cancel` inside the compositor and there is no third way for a carry to end, so a window cannot outlive the gesture that carried it.
|
||||
|
||||
That is structural, not careful. The old shape was a transform one surface applied and a *different* surface had to remember to undo — so tapping a card, which never touches the rail, left a window shrunk with no way back.
|
||||
|
||||
`last_zone` is a destination with a name, not an abandon branch, for the same reason.
|
||||
|
||||
## The settle is a spring, not a duration
|
||||
|
||||
A lift at 60% of the climb used to commit *at* 60%: the window jumped from where the thumb left it to its final state. Two motions with a cut between them.
|
||||
|
||||
A duration and an easing curve cannot carry release velocity, so a flick and a drift settled identically. The settle is a critically damped spring integrated on `FrameAnimation` — the render clock, so the carry advances once per painted frame — seeded with the velocity the rail measures over the last 100 ms of the gesture. Critically damped and never underdamped: a navigation surface that rings reads as a toy.
|
||||
|
||||
Hard-capped at 600 ms. The commit is what releases the carry, so a settle that never converged has to arrive anyway.
|
||||
|
||||
Known and not yet fixed: on the `last_zone` path `travel` clamps at 0 while the exit test still waits for `|velocity| < 0.02`, so the commit can lag the visual arrival by up to that cap on the commonest gesture there is.
|
||||
|
||||
## The transport is fast and lossy, and only the second one matters
|
||||
|
||||
Measured on blueline, 2026-08-16, 200 samples, full connect → write → event-loop hop → reply → close: `workspaces` 0.47 ms median, 1.04 ms worst. Under 3% of a frame. A quantiser protecting against that cost is protecting against nothing, and `_lastShift`'s 0.01 bought a visible staircase — one step per two logical pixels across 275 px of travel. It is 0.002 now.
|
||||
|
||||
What the transport *is* is lossy: 52 failures and 113 `PeerClosedError`s in one 90-minute boot, each of which used to execute `_queue = []`. A lost `overview_commit` leaves the compositor carrying windows the shell believes it released and nothing in the system will ever put them back. `overview_commit`, `overview_cancel` and `workspace` are idempotent and are redelivered once. `overview_progress` is a level and still drops.
|
||||
|
||||
Rate is bounded by the render clock, not by a quantiser. `WindowResize` pumps its `place` the same way.
|
||||
|
||||
## The curve bank
|
||||
|
||||
`Appearance.animationCurves` is Material 3 Expressive and it has been in the tree since the `ii` days. Every spatial curve in it overshoots — `expressiveFastSpatial` peaks at 1.67, `expressiveDefaultSpatial` at 1.21 — and there is a `clickBounce` beside them.
|
||||
|
||||
Audited 2026-08-16: twelve souveraine surfaces, 31 hand-typed animations, three calls into that bank from two files. Every navigation surface was animating on a flat linear ramp with a hand-typed duration. That was the whole of "the reference compositors have more whoosh."
|
||||
|
||||
Use the bank. A hand-typed `NumberAnimation { duration: N }` in a new surface is a defect unless it says in a comment why the bank is wrong for it.
|
||||
|
||||
## The rule under all of it
|
||||
|
||||
The shell does not transform a window. It names a destination and the compositor carries it there.
|
||||
|
||||
Any shell-side scale over a window's geometry is a second writer, and a transform one surface applies and another must remember to undo always has a path that forgets. Where a card *must* have motion of its own — depth, the entry rise — it is gated on `ZoneTransition.inFlight` so that during a carry every card sits at its true rect and the motion eases in on the commit frame.
|
||||
|
||||
## Not yet true
|
||||
|
||||
`transition.rs:79` clamps `shift_to` to `0..=1`, so the resistance past the detent is written, shipped and inert. Raising it to `maxOvershoot` (1.22) and updating `shift_is_clamped` is a compositor change. Safe to ship the shell ahead of it: an old compositor clamps and the gesture behaves exactly as it did.
|
||||
|
||||
The control socket also serves one request per connection. The QML client is
|
||||
correct to make commit/cancel/workspace idempotent and retry once today. A
|
||||
persistent connection needs the compositor handler to loop to EOF with one
|
||||
`BufReader`; rebuilding the reader for each request can discard bytes it read
|
||||
past the first newline. Persistence changes transport cost and failure shape,
|
||||
not the authority or acknowledgement rules above.
|
||||
102
saf/surface/02-session-start.md
Normal file
102
saf/surface/02-session-start.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Session start
|
||||
|
||||
The graphical session is one dependency graph. A green system
|
||||
`graphical.target` proves only that greetd may run; it does not prove that the
|
||||
user authority or a drawing shell exists.
|
||||
|
||||
## The order
|
||||
|
||||
`souveraine-session-viewtop` is the session leader. It starts viewtop, waits
|
||||
for the compositor's Wayland socket, and imports that socket and the Souveraine
|
||||
desktop identity into the user systemd manager. Only then can the rest begin:
|
||||
|
||||
1. `souveraine-session-pre.target` pulls in systemd's
|
||||
`graphical-session-pre.target`. `souveraine-sessiond` belongs here because
|
||||
it needs Wayland in order to take ext-session-lock, and it must hold that
|
||||
lock before a drawing surface appears.
|
||||
2. The leader waits for `$XDG_RUNTIME_DIR/souveraine/sessiond.sock`. A
|
||||
`Type=simple` service is considered started before it serves; the socket is
|
||||
the readiness fact until sessiond becomes `Type=notify`.
|
||||
3. `souveraine-session.target` pulls in `graphical-session.target`, which
|
||||
starts `souveraine-shell.service` and the other surfaces that belong to the
|
||||
session.
|
||||
4. When viewtop exits, the leader stops the two Souveraine targets. Their
|
||||
bindings let systemd stop everything whose lifetime is the graphical
|
||||
session.
|
||||
|
||||
The two Souveraine targets are necessary because the stock graphical-session
|
||||
targets carry `RefuseManualStart=yes`. A session leader cannot start those
|
||||
stock units directly. It starts its own targets; their dependencies may pull
|
||||
the stock targets into the transaction in the way systemd permits.
|
||||
|
||||
This graph raises [session authority](../authority/01-session.md); it does not
|
||||
replace it. systemd owns process lifetime and dependency order. Sessiond owns
|
||||
session policy once its socket is ready.
|
||||
|
||||
## The principal split still owed
|
||||
|
||||
This is the process-order graph that exists; it is not yet the final security
|
||||
boundary. All of its units are currently user units and therefore inherit the
|
||||
human graphical session's UID. The target moves sessiond's lock lifecycle to
|
||||
the dedicated `souveraine-session` principal described in
|
||||
[session authority](../authority/01-session.md), while the user manager keeps
|
||||
only the presentation and application lifetimes that belong there.
|
||||
|
||||
The compositor must offer that principal a narrow way to hold
|
||||
ext-session-lock and render the fallback without granting it Casey's home or
|
||||
general session bus. The lock authority must be ready before the user home is
|
||||
decrypted and remain ready after the Personal key is evicted. Changing unit
|
||||
placement without proving the Wayland connection, PAM path, handoff, crash
|
||||
retake, and key-eviction order would only move the black screen to a new UID.
|
||||
|
||||
## Package boundary
|
||||
|
||||
`souveraine-viewtop` owns the compositor and the session edge on both x86_64
|
||||
and aarch64:
|
||||
|
||||
- `/usr/bin/viewtop`
|
||||
- `/usr/bin/souveraine-session-viewtop`
|
||||
- `/usr/lib/systemd/user/souveraine-session-pre.target`
|
||||
- `/usr/lib/systemd/user/souveraine-session.target`
|
||||
- `/usr/lib/systemd/user/souveraine-shell.service`
|
||||
|
||||
The package depends on `souveraine` for sessiond and on the virtual
|
||||
`quickshell` package for `qs`. Both architecture artifacts carry and verify the
|
||||
same start contract. Architecture changes the compositor binary, not the
|
||||
meaning of a Souveraine session.
|
||||
|
||||
Versioned QML and face assets remain a separate surface-package boundary. The
|
||||
session unit names what launches them; it must not own a mutable
|
||||
`~/.config/quickshell` tree or turn a development compose into an installed
|
||||
artifact.
|
||||
|
||||
## No shadow starts
|
||||
|
||||
A unit under `~/.config/systemd/user` or `/etc/systemd/user` outranks the
|
||||
package copy under `/usr/lib/systemd/user`. Such a file may prove a repair, but
|
||||
it is a splint: once the package carries the same unit, the higher-priority
|
||||
copy must move out of systemd's search path. Otherwise pacman can update the
|
||||
owned unit forever while the machine keeps booting the old answer.
|
||||
|
||||
The same distinction holds across delivery:
|
||||
|
||||
- source says what could ship;
|
||||
- a green package job says what was built and signed;
|
||||
- `pacman -Qo` and file identity say what is installed;
|
||||
- a fresh session start says the dependency graph actually raised the glass.
|
||||
|
||||
Do not collapse those into "fixed."
|
||||
|
||||
## The failure this closes
|
||||
|
||||
On 2026-08-17 blueline reached system `graphical.target` with greetd and
|
||||
viewtop alive, but user `souveraine-session.target`, sessiond, and QuickShell
|
||||
were all dead. The installed r117 leader had directly requested the refused
|
||||
stock targets. Viewtop failed closed with no lock surface and the panel was
|
||||
black. Starting the Souveraine wrapper target recovered sessiond, loaded
|
||||
QuickShell, transferred the lock surface, and showed the session without a
|
||||
reboot.
|
||||
|
||||
That recovery proved the dependency shape. It did not become durable until the
|
||||
leader, both targets, the shell unit, their runtime dependencies, and artifact
|
||||
content checks travelled together through the signed package path.
|
||||
122
saf/surface/03-usb-hands.md
Normal file
122
saf/surface/03-usb-hands.md
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# USB Hands
|
||||
|
||||
The phone may become a keyboard and pointer for the host on its USB cable.
|
||||
That reach has three states with three owners; collapsing them is what made the
|
||||
first surface confusing.
|
||||
|
||||
## The wire, the surface, the hand
|
||||
|
||||
`UsbState.mode` says what sessiond and usb-signaller have made the port. HID or
|
||||
KVM means the wire is armed. Developer or charging-only means it is not.
|
||||
QuickShell never writes configfs and the page never opens `/dev/hidg*`.
|
||||
|
||||
`HidController.active` says the trackpad surface is open. It is not another
|
||||
name for an armed port. Power Options is the human place that arms or disarms
|
||||
the wire; a separate child action opens or closes the trackpad, and opening is
|
||||
refused until HID or KVM is already armed.
|
||||
|
||||
`HidController.ready` says its one persistent `usb-hid-inject stream` helper
|
||||
has acknowledged the endpoint. Pointer motion is batched to the display clock;
|
||||
text, keys, clicks, and release reports go through that one writer.
|
||||
|
||||
These are deliberately different facts. A composed gadget with no open
|
||||
surface is safe and useful. An open surface with no composed gadget is a dead
|
||||
control and is refused. A helper process that has not said `ready` is not
|
||||
dressed up as a usable hand.
|
||||
|
||||
The resulting state graph is small:
|
||||
|
||||
```
|
||||
developer/charging -> no trackpad
|
||||
HID or KVM armed -> trackpad may be opened
|
||||
trackpad active -> dock yields; Face routes gestures and words
|
||||
disarm or lock -> helper releases, trackpad closes
|
||||
```
|
||||
|
||||
Arming the wire alone changes no shell furniture. The dock leaves only while
|
||||
the conditional trackpad is actually on the glass.
|
||||
|
||||
## Authority and mechanism
|
||||
|
||||
The path has one owner at each layer:
|
||||
|
||||
- QuickShell asks sessiond for `set_usb_mode`; `UsbState` only projects the
|
||||
answer and never writes the gadget.
|
||||
- Sessiond records the USB mode action and asks usb-signaller to compose the
|
||||
port. The supported postures are developer, HID, KVM, and charging-only.
|
||||
- `HidController` alone owns the live stream helper. The Face page emits
|
||||
intent into that controller; it never opens `/dev/hidg*` itself.
|
||||
- The substrate's `body hand` verb returns to QuickShell's `usbHands` IPC. It
|
||||
does not create a second HID writer merely because the caller is an agent.
|
||||
|
||||
The present route is not yet the final authority seam. `attached_identity`
|
||||
and `probe_owner` are explicit unknowns, and a locally permitted caller can
|
||||
still reach usb-signaller's system D-Bus mechanism around sessiond. Joining an
|
||||
unlocked surface and arming it in Power Options are useful local gates, not a
|
||||
credential or known-peer policy. The seam is complete only when the mutation
|
||||
mechanism admits the attested/sessiond path alone and sessiond can distinguish
|
||||
a known scoped peer from an unknown attachment. Charging-only remains the
|
||||
always-available safe contraction.
|
||||
|
||||
Mode failure must return a refusal and leave or restore a usable USB posture.
|
||||
It must not strand the management lane, a bound UDC, a held key, or a pointer
|
||||
button. Closing Hands, disarming, and secure lock all release reports and stop
|
||||
the helper; a mode switch stops the writer before usb-signaller changes the
|
||||
endpoints beneath it.
|
||||
|
||||
## One conversation
|
||||
|
||||
Hands is a limb of the existing Face/Souveraine connection, not a compact
|
||||
second chat client. Opening it resumes the current agent's latest conversation
|
||||
only when no conversation is attached. Conversation selection and creation
|
||||
belong to the agent pane, so the Hands header carries no mystery back-arrow or
|
||||
new-thread button.
|
||||
|
||||
The joined surface currently exposes agent text/voice, trackpad, scroll,
|
||||
left/right click, and a held Ctrl+Alt+Delete. It does not advertise a keyboard
|
||||
pane merely because an HTML input can emit key reports. A real on-glass
|
||||
keyboard needs its own QML surface and focus contract before it earns a button.
|
||||
|
||||
## The agent's reach
|
||||
|
||||
While Hands is joined, the ambient skill points at the substrate's typed
|
||||
`body` tool: `verb: hand` with `status`, `type`, `key`, `click`, or `pointer`.
|
||||
It does not teach a second raw `qs` command vocabulary.
|
||||
|
||||
A direct request to type ordinary user-authored text is authorization to type
|
||||
that text, including creative, intimate, or unusual prose. Odd tone is not a
|
||||
state refusal. The hand still sends nothing Casey did not ask to place on the
|
||||
attached host, and readiness/state refusals remain visible to the agent.
|
||||
|
||||
## Beyond HID
|
||||
|
||||
HID can send input and cannot truthfully return a terminal's output. Learned
|
||||
administration or tunnel payloads therefore belong to a separate, auditable
|
||||
capability path: named/versioned recipes, an explicit scope and lease, and a
|
||||
management transport that returns stdout, stderr, and exit status as typed
|
||||
events. Do not infer success from keystrokes and do not turn an RMM or reverse
|
||||
tunnel into an opaque keyboard macro.
|
||||
|
||||
The NCM control lane can carry that future return path. Its authority,
|
||||
identity, rollback, and evidence contract must exist before a learned payload
|
||||
is treated as a skill rather than an unaudited script.
|
||||
|
||||
## Proof boundary
|
||||
|
||||
Source agreement is only the first line of proof. USB Hands is operational on
|
||||
a phone only when the signed usb-signaller package contains the persistent
|
||||
stream helper, the installed Souveraine surface and face assets are the same
|
||||
revision, and the following motion is observed on glass and on the attached
|
||||
host:
|
||||
|
||||
1. Power Options arms HID while another management path remains reachable.
|
||||
2. Opening the trackpad starts one helper and receives its `ready` reply.
|
||||
3. Motion, scroll, left/right click, requested text, a named key, and held
|
||||
Ctrl+Alt+Delete reach the host without a second writer.
|
||||
4. A rejected character sends none of that command; leaving and locking leave
|
||||
no held report.
|
||||
5. Returning to developer mode restores the control lane cleanly.
|
||||
|
||||
KVM adds separate display, storage, NCM, and touch-return acceptance. A green
|
||||
shell reload, a packaged binary, or the first successful keystroke proves only
|
||||
its own layer; none of them silently stands in for that cable exercise.
|
||||
246
saf/surface/04-agent-pane.md
Normal file
246
saf/surface/04-agent-pane.md
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
# The agent panel
|
||||
|
||||
The Panel is Souveraine's close conversation surface on the desktop. It is not
|
||||
a generic provider picker laid over somebody else's chat client. The agent in
|
||||
its footer is the agent this pane can address; the transcript is the substrate's
|
||||
typed conversation; thought, action, itinerary, subconscious work, and context
|
||||
pressure remain distinct even when they share a narrow column.
|
||||
|
||||
The present shell code is split across
|
||||
`surfaces/quickshell/services/Souveraine.qml`, the `Ai.qml` compatibility
|
||||
adapter, and `modules/souveraine/agent/`. The outer list and composer still live
|
||||
in the borrowed `modules/ii/sidebarLeft/AiChat.qml`. That is a migration seam,
|
||||
not permission for the borrowed component to become a second owner of state or
|
||||
product meaning.
|
||||
|
||||
## The anatomy
|
||||
|
||||
The vertical order is deliberate:
|
||||
|
||||
1. the conversation occupies the primary, scrollable field;
|
||||
2. transient subconscious activity may touch the lower edge of that field;
|
||||
3. the itinerary remains as a thin persistent ribbon above the composer while
|
||||
a route exists;
|
||||
4. the composer owns text entry, send/cancel/interject behavior, attachment
|
||||
state, and commands;
|
||||
5. the footer holds compact controls for the agent and the few secondary
|
||||
surfaces that actually work.
|
||||
|
||||
The agent menu opens from its footer chip. It does not live above the
|
||||
conversation as permanent green furniture. The itinerary does remain above the
|
||||
composer because it answers a different question: not which process happens to
|
||||
be alive, but where the current agent is in the work Casey asked her to carry.
|
||||
|
||||
Every footer chip must either expose a state the pane owns or perform the act it
|
||||
advertises. A decorative tool selector is worse than no selector when tools are
|
||||
configured in the agent's sensorium and cannot be switched here. Remove it.
|
||||
Commands may still name unavailable work in documentation; they do not earn a
|
||||
button on the live surface until their path exists.
|
||||
|
||||
## One controlled agent, other sessions observed
|
||||
|
||||
`Souveraine.currentAgentId` is the controlled position. The selector is
|
||||
populated from `/v1/agents` through the Souveraine service and may switch only
|
||||
between those agents. A running turn pins that selection until it finishes or
|
||||
is cancelled, because changing the face while a response is arriving would
|
||||
mislabel both transcript and authority.
|
||||
|
||||
Codex and Claude processes discovered by `AgentSessions` are not models behind
|
||||
this pane. Their status may appear under a separate **observed only** heading in
|
||||
the opened footer menu, with provider and session label, but those rows do not
|
||||
accept selection and never imply that the composer can steer them. Observation
|
||||
and control can share a menu only when the boundary is visible in words and in
|
||||
behavior.
|
||||
|
||||
[Agent presence](05-agent-presence.md) owns the collector contract, provider
|
||||
health, activity heuristics, and the future path toward one session service.
|
||||
This chapter owns only how that projection enters the conversation surface.
|
||||
|
||||
The Panel eventually needs richer active-agent status: substrate reachability,
|
||||
conversation attachment, primary/subconscious phase, queued interjections, and
|
||||
the last terminal event. Those facts belong to the Souveraine connection, not
|
||||
to heuristics over terminal processes.
|
||||
|
||||
## A transcript is typed
|
||||
|
||||
The substrate emits typed segments. The Panel preserves them:
|
||||
|
||||
- assistant speech is ordinary message content;
|
||||
- reasoning is Thinking and renders in `ThinkingCard`;
|
||||
- tool invocation and return are one correlated `ToolCard`;
|
||||
- interstitial text is visibly provisional rather than final speech;
|
||||
- system/interface notices are not forged as the agent's voice.
|
||||
|
||||
Reasoning may be retained for audit, reflection, and context accounting without
|
||||
being replayed to a generic model as `[Reasoning]:` assistant prose. Storage,
|
||||
model replay, and visible rendering are three projections of the same typed
|
||||
record. None may flatten Thinking into speech merely because a provider lacks a
|
||||
native reasoning input block.
|
||||
|
||||
Tool cards collapse after success, stay open while running or failed, and show
|
||||
status without requiring expansion. Their payload is selectable and scrolls
|
||||
inside a capped body; a long grep or build result must not be clipped at 240
|
||||
pixels or stretch one message across the entire conversation. The inner tool
|
||||
body consumes scrolling while it has overflow. Outside it, the transcript
|
||||
keeps the wheel.
|
||||
|
||||
Tool output is evidence. It may be summarized in the header, but the original
|
||||
return remains available until transcript retention or compaction explicitly
|
||||
changes it.
|
||||
|
||||
## The itinerary is state, not chat debris
|
||||
|
||||
`memory/system/dynamic/itinerary.md` is the canonical route. The Panel does not
|
||||
parse that file and does not keep a second QML itinerary that dies on reload.
|
||||
The authenticated `GET /v1/agents/:id/itinerary` endpoint projects its title,
|
||||
current stop, statuses, descriptions, linked todo ids, nature, and energy.
|
||||
|
||||
An itinerary stream event is an invalidation edge. On set, advance, or clear,
|
||||
the service refreshes that projection. Empty is meaningful: it removes the
|
||||
ribbon. A shell reload or agent switch asks the substrate again, so current
|
||||
work survives the surface without turning surface memory into authority.
|
||||
|
||||
The compact ribbon shows the title, current stop, phase, progress, and stale
|
||||
state. Opening it reveals a bounded scrollable list of stops and the todo
|
||||
metadata the substrate supplied. A successful itinerary tool card is hidden
|
||||
from the transcript because the persistent ribbon already carries that state;
|
||||
an itinerary failure remains visible because failed action is evidence.
|
||||
|
||||
The itinerary is not the todo ledger. It may reference a todo, but clearing a
|
||||
route does not erase a commitment and completing a route does not silently
|
||||
invent one.
|
||||
|
||||
## Conversation life
|
||||
|
||||
Conversation identity lives on the server. A shell reload may offer the latest
|
||||
conversation but must not silently attach it unless the configured policy says
|
||||
so. New, resume, list, filter, and fork all act on server conversation ids; a
|
||||
local array of bubbles is a projection, never the canonical thread.
|
||||
|
||||
The complete Panel needs these conversation acts on glass:
|
||||
|
||||
- a clearly named new conversation;
|
||||
- an explicit resume offer with enough metadata to distinguish the thread;
|
||||
- a filterable recent-conversation list, with indexed content search as the
|
||||
larger corpus demands it;
|
||||
- fork from a selected message or turn boundary;
|
||||
- visible attachment to a conversation id without exposing token material;
|
||||
- honest recovery when the shell reattaches to an active stream.
|
||||
|
||||
The current adapter carries server resume and active-stream reattachment. The
|
||||
footer conversation menu now exposes new, continue, recent-thread browsing,
|
||||
and a local id/date filter. Indexed content search and fork remain parity work
|
||||
until their owned Panel components and server paths are connected. A slash
|
||||
command is not the finished interaction merely because it reaches the same
|
||||
function.
|
||||
|
||||
## Composer and input
|
||||
|
||||
Send creates a user turn only when no turn is active. During a turn the same
|
||||
space must make cancel and queued interjection legible rather than accepting
|
||||
text whose delivery phase is unknowable. `202 Accepted` for an interjection
|
||||
means queued, not read; the surface keeps that distinction until the next
|
||||
round boundary or terminal event.
|
||||
|
||||
Paste and attachment parity requires a typed input envelope: text, image or
|
||||
file identity, media type, size, provenance, and any transformation performed
|
||||
before send. The substrate HTTP boundary, provider path, persistence, and TUI
|
||||
now accept image blocks. The Panel does not: its QML request still sends a
|
||||
plain string and transcript restore reduces an image to `[image]`. A filename
|
||||
chip connected only to the old Gemini path is not Souveraine multimodality.
|
||||
|
||||
Keyboard, touch, and pointer must all reach the same acts. Focus returns to the
|
||||
composer after a menu choice; Page Up and Page Down move the transcript; tool
|
||||
payload scrolling and text selection do not accidentally send or collapse a
|
||||
card. Small icon-only hit targets need tooltips and at least the shell's normal
|
||||
touch target.
|
||||
|
||||
## Command surface audit, 2026-08-17
|
||||
|
||||
The borrowed composer still advertises illogical-impulse commands. A command
|
||||
name surviving in `AiChat.qml` does not mean Souveraine implements the act.
|
||||
|
||||
| Command | Current truth | Panel direction |
|
||||
|---|---|---|
|
||||
| `/agent` | real: selects from `/v1/agents`, guarded while a turn runs | keep as the textual form of the footer agent control |
|
||||
| `/resume` | real for the selected agent's latest thread; the footer menu also lists and loads recent ids | keep, add `/resume <id>` only if it uses the existing server load path |
|
||||
| `/new` | real: clears the projection and detaches so the next send creates a server conversation | keep; the footer conversation control is the discoverable form |
|
||||
| `/attach` | stale refusal: image blocks now cross the substrate; QML still sends only text and restores images as a marker | wire a typed image envelope, preview/removal, capability refusal, and image restore |
|
||||
| `/btw` | absent in the Panel; the server already exposes conversation fork and the TUI owns the fork lifecycle | build a separate fork stream and bounded visual overlay with dismiss and jump |
|
||||
| `/model` | aliases `/agent`; the objects called models are Souveraine agents | remove the alias until a real per-agent model inspection/change API exists |
|
||||
| `/tool` | fixed fake vocabulary of one and an explanatory refusal | remove; tools are an agent sensorium, not a composer mode |
|
||||
| `/prompt` | prints ownership advice; no prompt files or mutation path are connected | remove as a setter; a future inspector must read the composed server fact |
|
||||
| `/key` | prints auth advice and never reads or writes a key | remove; credentials need a secure settings/auth path |
|
||||
| `/save` | writes a local JSON bubble snapshot that drops typed segment fidelity | rename as export only if kept; it is not conversation persistence |
|
||||
| `/load` | restores that local projection, then starts a fresh live thread on send | remove from the live-thread vocabulary; server resume is the honest act |
|
||||
| `/temp` | prints advice because temperature is agent configuration | remove until a typed settings path exists |
|
||||
| `/test` | injects a development markdown fixture into the transcript | keep behind development tooling, not in the shipped command list |
|
||||
|
||||
The short implementation order is attachment, command pruning, then `/btw`.
|
||||
Attachment is no longer a substrate project: the missing work is the Panel's
|
||||
typed send and render path. Command pruning makes every advertised act true.
|
||||
`/btw` is larger because it introduces a second concurrent conversation
|
||||
projection; it needs its overlay before the verb is exposed.
|
||||
|
||||
Ordinary typing during a running turn remains an **interjection** into that
|
||||
turn. It is queued and read at a round boundary. `/btw` is a **fork** with its
|
||||
own conversation id and stream. Those gestures may look adjacent at the
|
||||
composer and must never share state or wording.
|
||||
|
||||
## Context, reflection, and parity
|
||||
|
||||
Feature parity means parity with Souveraine's domain, not a pixel copy of the
|
||||
TUI and not inherited buttons from ii. Both surfaces should be able to expose:
|
||||
|
||||
- actual used tokens, model limit, and pressure as separate values;
|
||||
- the memory budget and the compaction that changed it;
|
||||
- primary completion followed by subconscious activity;
|
||||
- reflection/audit records without leaking them into agent speech;
|
||||
- the current itinerary and commitment links;
|
||||
- correlated tool calls, returns, duration, and failure;
|
||||
- server health, cancellation, interjection, and reconnection;
|
||||
- conversation creation, discovery, resume, and fork;
|
||||
- typed multimodal inputs once the substrate owns them.
|
||||
|
||||
These may open as drawers, menus, expanded cards, or a dedicated secondary
|
||||
Panel. They must consume the same server facts. The TUI is useful evidence of a
|
||||
working domain action; it is not a second specification and its screen-local
|
||||
state does not become the QML contract.
|
||||
|
||||
## Ownership migration
|
||||
|
||||
The borrowed `AiChat.qml` currently owns list lifecycle, focus, and composer
|
||||
geometry. Souveraine-owned `AgentMessage`, `ThinkingCard`, `ToolCard`,
|
||||
`AgentPaneMenu`, and `ItineraryRibbon` already own the semantics drawn inside
|
||||
it. The next structural cut is an owned Panel host and conversation viewport
|
||||
that consume the same service without copying the adapter's state.
|
||||
|
||||
Moving the Panel out of the resident shell is a separate lifetime boundary,
|
||||
not a reason to fork its conversation model. [Surface lifetimes](06-surface-lifetimes.md)
|
||||
owns that extraction and its memory/fault proof.
|
||||
|
||||
That cut should be rehearsed beside the live pane, then swapped once. A QML
|
||||
write-reload loop over the only working chat surface turns each syntax fault
|
||||
into a session-wide interruption and can abandon a live turn. Candidate and
|
||||
live panes may share a server and a conversation only if one is read-only;
|
||||
they must never become concurrent writers disguised as visual testing.
|
||||
|
||||
## Proof boundary
|
||||
|
||||
Panel work has distinct proof layers:
|
||||
|
||||
1. Source: Rust formatting, QML parsing, lint, manifest coverage, and focused
|
||||
projection tests are clean.
|
||||
2. CI: Gitea compiles and tests the Rust/QML package path on ArchDev and emits
|
||||
the versioned artifact.
|
||||
3. Installed: the package or development compose points at the intended source
|
||||
revision and QuickShell loads it without binding or component errors.
|
||||
4. On glass: the footer menu controls only Souveraine agents; observer rows are
|
||||
inert; a long tool body scrolls; set/advance/clear updates the ribbon; a
|
||||
failed itinerary remains in chat; switching, resume, cancel, and interject
|
||||
have the states their labels claim.
|
||||
|
||||
A parsed QML file is not on-glass proof. A green Rust job is not an installed
|
||||
surface. A hot reload that leaves the previous pane drawn is not success. Keep
|
||||
those borders sharp; the Panel is where hidden ambiguity becomes something
|
||||
Casey has to touch.
|
||||
224
saf/surface/05-agent-presence.md
Normal file
224
saf/surface/05-agent-presence.md
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
# Agent presence
|
||||
|
||||
Agent presence answers one narrow question: **which agent sessions can this
|
||||
machine currently observe?** It does not decide which agent a conversation
|
||||
controls, approve a tool on another process's behalf, or turn a terminal
|
||||
session into a model behind the Panel.
|
||||
|
||||
That distinction matters most where the UI is smallest. A Codex process can be
|
||||
running beside Souveraine while the Panel composer still addresses only the
|
||||
selected Souveraine agent. Showing both is useful. Making them look equally
|
||||
selectable is a lie.
|
||||
|
||||
## Three different facts
|
||||
|
||||
Keep these independent even when one menu draws them together:
|
||||
|
||||
- **controlled agent** — the Souveraine agent selected through `/v1/agents`;
|
||||
the Panel can create or resume its conversations and send it a turn;
|
||||
- **observed session** — a recent Souveraine, Claude Code, or Codex record found
|
||||
by the local collector; it may be active, recent, or idle, but observation
|
||||
grants no action;
|
||||
- **provider health** — whether a provider's source was actually inspected.
|
||||
Unavailable is not the same as available with no sessions.
|
||||
|
||||
The controlled agent is a server relationship. Observed activity is a local
|
||||
projection. Provider health is evidence about that projection. Do not infer
|
||||
one from another.
|
||||
|
||||
## What exists now
|
||||
|
||||
The current path is deliberately small and transitional:
|
||||
|
||||
1. `scripts/agent/agent-sessions.sh` inspects recent local records for
|
||||
Souveraine, Claude Code, and Codex and emits one JSON envelope.
|
||||
2. `services/AgentSessions.qml` runs that collector on one cadence, retains the
|
||||
last good envelope, and projects sessions, provider health, active count,
|
||||
staleness, and compact labels.
|
||||
3. `AgentPaneMenu.qml` shows Claude Code and Codex sessions beneath
|
||||
**Running elsewhere · observed only**. Those rows are inert.
|
||||
|
||||
The shell also retains `modules/souveraine/island/Island.qml` and
|
||||
`IslandExpansion.qml`, written against the same service. They are not mounted
|
||||
in the current bar or Panel. They are reusable observer components, not a
|
||||
second live presence surface and not a reason to put agent chrome back above
|
||||
the conversation.
|
||||
|
||||
They are also not abandoned. Their next proper home is the phone's later
|
||||
dynamic top-bar system, once the bar can host more than a fixed row of widgets.
|
||||
The present Panel move lets that larger work wait without making the Island
|
||||
components disposable.
|
||||
|
||||
This is source and current composed-surface behavior. It is not the proposed
|
||||
Rust session daemon. The collector still starts a shell process from QML on
|
||||
each poll.
|
||||
|
||||
## The envelope contract
|
||||
|
||||
Transport may change; these meanings must not:
|
||||
|
||||
- `sessions` is newest first and contains provider identity, stable session
|
||||
identity, a useful label source, last activity, activity state, and only the
|
||||
usage fields the provider actually supplied;
|
||||
- `providers` reports availability, errors, source-specific limits, and the
|
||||
number of sessions admitted from each source;
|
||||
- `active` counts records whose observed activity falls inside the active
|
||||
window;
|
||||
- `ts` says when the envelope was collected, not when every underlying source
|
||||
was updated;
|
||||
- a failed provider degrades inside the envelope instead of aborting the other
|
||||
providers;
|
||||
- a failed whole refresh keeps the last good model and marks it stale instead
|
||||
of replacing it with an authoritative-looking empty list.
|
||||
|
||||
Unknown is a real value. The current shell uses `-1` for an unavailable token
|
||||
measurement and renders an em dash, never zero. Souveraine now persists
|
||||
per-turn usage with assistant messages, but `conversation.json` does not expose
|
||||
an aggregate for the collector, so the presence projection still reports
|
||||
Souveraine session usage as unknown. That projection may be improved; the UI
|
||||
must not manufacture a total in the meantime.
|
||||
|
||||
Claude's local usage is read from a bounded recent tail and is labelled as
|
||||
windowed. Codex's latest token-count record is cumulative for that rollout.
|
||||
Those numbers are not interchangeable merely because both fit in one field.
|
||||
Any combined view must retain the source semantics or omit the comparison.
|
||||
|
||||
## Activity is a heuristic, not cognition
|
||||
|
||||
The present collector derives:
|
||||
|
||||
- `active` from activity within two minutes;
|
||||
- `recent` from activity within one hour;
|
||||
- `idle` from older admitted records.
|
||||
|
||||
These are filesystem/event-age classes. They do not prove that an inference is
|
||||
running, a tool is still alive, an agent is waiting for Casey, or a response is
|
||||
complete. In particular, there is no synthetic `waiting` state. Waiting for a
|
||||
permission decision requires a real request lifecycle; timestamps cannot stand
|
||||
in for it.
|
||||
|
||||
The Panel's own active-turn, primary/subconscious phase, cancellation, and
|
||||
terminal event must continue to come from the Souveraine stream described in
|
||||
[the agent panel](04-agent-pane.md). Presence may corroborate that state. It
|
||||
must not overwrite it.
|
||||
|
||||
## Where presence belongs
|
||||
|
||||
The close conversation surface puts observed external sessions inside the
|
||||
footer's agent menu because that is where mistaken control is easiest to
|
||||
prevent: selectable Souveraine agents first, an explicit divider, then inert
|
||||
external rows.
|
||||
|
||||
A bar, island, overview, or diagnostics page may project the same service when
|
||||
the device has room and the projection answers a real glance question. On a
|
||||
phone, permanent agent-session chrome competes with system state and was
|
||||
rejected on glass. Absence from the bar is therefore a product decision, not a
|
||||
collection failure.
|
||||
|
||||
No surface may:
|
||||
|
||||
- label a discovered process as controllable without an admitted action path;
|
||||
- show unavailable providers as zero sessions;
|
||||
- imply that recency means work is still running;
|
||||
- expose conversation text, prompts, repository paths, or tool payloads merely
|
||||
to make presence richer;
|
||||
- start its own provider poll when `AgentSessions` already owns the projection.
|
||||
|
||||
## The later dynamic bar
|
||||
|
||||
The phone's top bar should ultimately be an extensible host whose components
|
||||
can enter, compact, expand, yield, and leave according to live state. Agent
|
||||
presence is one such component, beside notifications, media, connectivity,
|
||||
power, privacy, and other device facts. A collection of independent widgets
|
||||
all changing width is not that system.
|
||||
|
||||
Before the Island is mounted again, the bar needs a shared contract for:
|
||||
|
||||
- component identity, priority, disclosure class, and device eligibility;
|
||||
- resting, compact, attention, expanded, and hidden forms;
|
||||
- one arbiter for horizontal space and collisions;
|
||||
- preemption and return when a higher-priority event arrives;
|
||||
- dwell, decay, and animation timing so rapid events do not thrash geometry;
|
||||
- tap, long-press, dismiss, and expansion ownership;
|
||||
- lock-state redaction and a safe minimal form;
|
||||
- a stable overflow or secondary surface when several components deserve
|
||||
attention at once;
|
||||
- component failure that removes one projection without taking down the bar.
|
||||
|
||||
`Island.qml` and `IslandExpansion.qml` are useful first clients for that host:
|
||||
they already separate a compact morph from expanded detail and consume the
|
||||
shared presence projection. They do not yet define the host protocol, arbitrate
|
||||
against neighbours, or prove the phone form on glass. The dynamic bar is a
|
||||
larger surface task carried here until it has enough implementation scope to
|
||||
need its own SAF chapter; the old fixed bar placement is not its prototype by
|
||||
default.
|
||||
|
||||
## Permission is a different authority
|
||||
|
||||
The earlier island exploration considered Claude hook installation and
|
||||
Allow/Deny controls. None of that is present in Souveraine's current path. The
|
||||
collector does not edit `~/.claude/settings.json`; there is no permission
|
||||
bridge; observer rows do not jump to or command external terminals.
|
||||
|
||||
If permission control is built later, it needs more than an attractive card:
|
||||
|
||||
- the request must name the exact provider session, tool, arguments, and age;
|
||||
- the producer and target must be authenticated strongly enough for the
|
||||
consequence of the action;
|
||||
- allow-once, deny, expiry, cancellation, and duplicate delivery need explicit
|
||||
states;
|
||||
- the decision and its delivery result must enter the audit trail;
|
||||
- provider failure must fall back to that provider's normal safe behavior;
|
||||
- a broad bypass or allow-all posture requires a separately admitted policy,
|
||||
not a more convenient button.
|
||||
|
||||
Until that path is owned and attested, presence stays read-only.
|
||||
|
||||
## The intended service
|
||||
|
||||
One long-lived session service remains the destination. It should replace the
|
||||
poll subprocess, not change the envelope under every surface. The proposed
|
||||
Unix socket at `$XDG_RUNTIME_DIR/souveraine-sessions.sock` is a candidate
|
||||
transport, not proof that the daemon exists.
|
||||
|
||||
The service needs:
|
||||
|
||||
- one cadence and one adapter per provider;
|
||||
- event-driven updates where a provider supports them, with bounded scans as a
|
||||
fallback;
|
||||
- provider-specific provenance and measurement semantics preserved in the
|
||||
normalized record;
|
||||
- last-good/stale behavior at both provider and whole-envelope level;
|
||||
- stable session identity across refreshes;
|
||||
- bounded resource use and no transcript-content collection merely for a
|
||||
presence label;
|
||||
- an authenticated, separate action channel if external control is ever
|
||||
admitted.
|
||||
|
||||
Souveraine's own session and usage facts should eventually be projected from
|
||||
the substrate directly rather than rediscovered through files written for
|
||||
persistence. Claude and Codex adapters remain local observers unless their
|
||||
providers expose a stronger owned event path.
|
||||
|
||||
## Proof boundary
|
||||
|
||||
Presence work is proven in layers:
|
||||
|
||||
1. **source** — each provider parser is fixture-tested, malformed timestamps do
|
||||
not become plausible epoch-zero state, and provider failure still emits a
|
||||
valid envelope;
|
||||
2. **projection** — one service retains last-good state, marks staleness, and
|
||||
preserves unknown/windowed/cumulative measurement semantics;
|
||||
3. **surface** — external sessions render under an explicit observed-only
|
||||
boundary and cannot be selected or controlled;
|
||||
4. **installed** — the versioned package owns the collector/service/component
|
||||
files used by the running shell;
|
||||
5. **live provider** — a real session enters and leaves each activity class,
|
||||
and a deliberately unavailable provider is visibly different from an empty
|
||||
one;
|
||||
6. **authority**, if ever added — an attested decision reaches only the named
|
||||
request and is recorded end to end.
|
||||
|
||||
The current implementation has the polling envelope and Panel projection. The
|
||||
Rust daemon, aggregate Souveraine usage projection, provider permission path,
|
||||
and cross-provider control are not yet true.
|
||||
194
saf/surface/06-surface-lifetimes.md
Normal file
194
saf/surface/06-surface-lifetimes.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# Surface lifetimes
|
||||
|
||||
The shell needs a memory budget and failure boundaries, not a superstition
|
||||
about which UI language is pure. Permanent state, optional presentation, and
|
||||
authority must have named owners and observable lifetimes. If closing a heavy
|
||||
surface leaves its objects and pages resident in the only process holding the
|
||||
lock screen, the surface did not meaningfully go away.
|
||||
|
||||
## Evidence that opened this question
|
||||
|
||||
The daily-driver phone has 3.5 GB RAM. Three measurements from August 2026
|
||||
frame the risk without pretending to be a fresh baseline:
|
||||
|
||||
- QuickShell reached about 610 MB RSS during the Live2D experiment;
|
||||
- the summoned WebKit face added about 283 MB RSS in its own process;
|
||||
- a separate dynamic-QML/per-stream rendering storm drove QuickShell to about
|
||||
952 MB anonymous RSS before the kernel killed it.
|
||||
|
||||
These observations do not prove that ordinary QML costs 952 MB, that WebKit is
|
||||
always too expensive, or that a Rust surface is automatically small. They do
|
||||
prove that an optional renderer can consume a material fraction of the phone's
|
||||
memory and that a storm inside the resident QuickShell process shares a
|
||||
failure domain with essential session UI.
|
||||
|
||||
The numbers must be remeasured against exact package revisions and process
|
||||
trees before they become a budget.
|
||||
|
||||
## The resident core
|
||||
|
||||
The resident shell keeps only surfaces and connections that must survive the
|
||||
loss of optional presentation:
|
||||
|
||||
- sessiond heartbeat and the secure session-lock handoff;
|
||||
- the lock surface and lock-tier disclosure gate;
|
||||
- display, idle, and device-state projections needed to remain safe;
|
||||
- the small gesture and power paths required to recover or close a surface;
|
||||
- a minimal system-status/recovery indication.
|
||||
|
||||
The resident core is not a second authority. Sessiond remains the decision
|
||||
owner for lock, power, and admitted device action; the shell renders its state
|
||||
and carries named requests. [Session start](02-session-start.md) owns the
|
||||
package and systemd graph that brings compositor, authority, and shell up in
|
||||
order.
|
||||
|
||||
The secure lock path must not be conditionally absent while a shell reload is
|
||||
in flight. Optional-surface work may not reopen the already-known race where a
|
||||
new shell requests a lock before the old holder has transferred or released
|
||||
it.
|
||||
|
||||
## Optional means separately disposable
|
||||
|
||||
The agent Panel, Live2D presence, web content, large explorers, and future
|
||||
special renderers are optional surfaces. Each needs:
|
||||
|
||||
- a named package and executable or explicit in-process owner;
|
||||
- one startup trigger and one authority that may request presentation;
|
||||
- a shutdown rule after close, idle, lock transition, or resource pressure;
|
||||
- a state restoration key whose authority lives outside the renderer;
|
||||
- a disclosure class and locked-state behavior;
|
||||
- a resource line measured while closed, opening, active, and closing;
|
||||
- a supervisor policy and a visible failure event.
|
||||
|
||||
A QML `Loader` is useful for avoiding construction and destroying object
|
||||
graphs. It is not proof that allocator pages returned to the operating system,
|
||||
and it cannot stop a rendering storm from killing the process that owns it.
|
||||
Call that path lazy only after object lifetime is verified; call memory
|
||||
released only after the OS measurement falls.
|
||||
|
||||
For expensive surfaces, a supervised process is the preferred boundary. When
|
||||
the process exits, its private memory and faults leave with it. The resident
|
||||
shell may retain a small launch/status projection, never the heavy renderer's
|
||||
state graph.
|
||||
|
||||
## The Panel is the first extraction
|
||||
|
||||
The agent Panel is the strongest first worker candidate because its authority
|
||||
already lives elsewhere:
|
||||
|
||||
- the Souveraine server owns agents, conversations, messages, itinerary,
|
||||
cancellation, interjection, and replay;
|
||||
- sessiond owns session/lock/device authority;
|
||||
- the Panel owns presentation, input collection, transient selection, and
|
||||
viewport state.
|
||||
|
||||
An extracted `souveraine-agent-surface` must therefore consume the same typed
|
||||
server contract described in [the agent panel](04-agent-pane.md). It must not
|
||||
copy conversations into a new local authority or become a second writer during
|
||||
candidate testing.
|
||||
|
||||
Its lifecycle should be explicit:
|
||||
|
||||
1. an unlocked, admitted presentation request launches the version selected by
|
||||
the installed surface package;
|
||||
2. the worker authenticates to the local Souveraine endpoint and attaches only
|
||||
after Casey selects or explicitly resumes a conversation;
|
||||
3. close detaches presentation and gives the worker a short bounded grace for
|
||||
non-authoritative UI state;
|
||||
4. the supervisor ends the worker after that grace;
|
||||
5. reopen reconstructs from substrate truth and may restore harmless viewport
|
||||
state separately;
|
||||
6. lock immediately hides Personal-class content and follows the declared
|
||||
stop/blank policy even if the worker is stuck.
|
||||
|
||||
During an active stream, a killed worker must not cancel the server turn unless
|
||||
Casey issued cancel. The substrate continues or records its terminal state;
|
||||
reopening reattaches and backfills. Conversely, a worker must not remain alive
|
||||
forever merely because a server stream forgot to close. Presentation lifetime
|
||||
and inference lifetime are related state machines, not one process lifetime.
|
||||
|
||||
## Fault containment
|
||||
|
||||
The resident shell needs a small supervisor projection for each optional
|
||||
surface: stopped, starting, ready, active, stopping, failed, and backoff. A
|
||||
crash becomes an event with the package version and last lifecycle edge. It
|
||||
must not silently relaunch in a tight loop or take the session lock with it.
|
||||
|
||||
The decisive fault tests are physical:
|
||||
|
||||
- kill the Panel during streamed text and during a large tool return;
|
||||
- feed a candidate QML version that fails to load;
|
||||
- wedge the worker so it ignores close;
|
||||
- lock while Personal-class content is visible;
|
||||
- exhaust the worker's memory budget without exhausting the resident core.
|
||||
|
||||
In every case the lock surface, power path, sessiond heartbeat, and recovery
|
||||
control must remain available. A screenshot after restart is not fault proof.
|
||||
|
||||
## Versioned delivery on every body
|
||||
|
||||
The laptop and phone receive published packages from Gitea. A surface split is
|
||||
complete only when the package graph installs compatible versions and the
|
||||
systemd/session start path selects them consistently. Hand-placing a worker on
|
||||
one device proves a diagnosis at most; it is not the fix future installs will
|
||||
receive.
|
||||
|
||||
The core and optional-surface protocol must be versioned. Compatibility needs
|
||||
an explicit range or handshake so a newer Panel cannot silently interpret an
|
||||
older substrate event shape. Package upgrade stages a candidate; adoption and
|
||||
restart remain explicit where a live conversation or lock handoff could be
|
||||
disturbed. The last working surface stays available until the candidate reaches
|
||||
ready state.
|
||||
|
||||
Gitea Actions and ArchDev own compilation and package artifacts. Local checks
|
||||
may parse QML, format Rust, validate manifests, and exercise fixtures; they do
|
||||
not replace the package result.
|
||||
|
||||
## Native Rust is a measured option
|
||||
|
||||
A Rust Wayland client could replace some or all of QuickShell, but it still has
|
||||
to own layer-shell/session-lock protocols, rendering, text, input, animation,
|
||||
accessibility, service integration, and recovery. Rust does not remove the
|
||||
need for resident/optional process boundaries, and a native screenshot says
|
||||
nothing about steady memory or fault behavior.
|
||||
|
||||
The useful spike is deliberately small: one native layer-shell card, one typed
|
||||
sessiond-fed state, one package, and measurements for RSS/PSS, launch latency,
|
||||
and failure. It does not take the lock, replace the shell, or grow a parallel
|
||||
settings authority. Compare it against the extracted QuickShell worker under
|
||||
the same workload before choosing a rewrite.
|
||||
|
||||
## Measurement record
|
||||
|
||||
Every budget line should name:
|
||||
|
||||
- device and available RAM;
|
||||
- source commit, package version, and protocol version;
|
||||
- process tree and which surfaces are constructed;
|
||||
- RSS and PSS after a defined settle time;
|
||||
- peak during a defined action, including a representative streamed tool turn;
|
||||
- memory after close and after worker exit;
|
||||
- launch-to-ready latency;
|
||||
- result of the relevant kill/lock/reopen test.
|
||||
|
||||
Measure at least resident idle, Panel closed, Panel open and idle, active text
|
||||
stream, large tool payload, Live2D closed/open, and post-close. A single `ps`
|
||||
line without surface state is anecdote, not a budget.
|
||||
|
||||
## Proof and present boundary
|
||||
|
||||
The path is complete only when:
|
||||
|
||||
1. the resident and optional sets are explicit in source and packages;
|
||||
2. closing the Panel ends its worker and the OS-visible memory leaves;
|
||||
3. killing or wedging that worker leaves lock, heartbeat, power, and recovery
|
||||
alive;
|
||||
4. locked-state disclosure is exercised, not inferred from a conditional;
|
||||
5. reopen restores the authoritative conversation without duplicating a turn;
|
||||
6. Gitea emits the versioned core and surface packages used by both laptop and
|
||||
phone;
|
||||
7. a Rust rewrite decision, if made, cites comparable measurements.
|
||||
|
||||
None of that extraction is true yet. The current Panel remains inside the
|
||||
resident QuickShell process; the August numbers are historical observations;
|
||||
and the Rust path remains a bounded experiment, not an adopted shell plan.
|
||||
Loading…
Reference in a new issue